-
-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Building a Video Streaming App with SvelteKit and Directus #292
Conversation
Co-authored-by: Kevin Lewis <kvn@lws.io>
Co-authored-by: Kevin Lewis <kvn@lws.io>
Co-authored-by: Kevin Lewis <kvn@lws.io>
Co-authored-by: Kevin Lewis <kvn@lws.io>
Co-authored-by: Kevin Lewis <kvn@lws.io>
Co-authored-by: Kevin Lewis <kvn@lws.io>
Co-authored-by: Kevin Lewis <kvn@lws.io>
Co-authored-by: Kevin Lewis <kvn@lws.io>
Co-authored-by: Kevin Lewis <kvn@lws.io>
Co-authored-by: Kevin Lewis <kvn@lws.io>
Co-authored-by: Kevin Lewis <kvn@lws.io>
Co-authored-by: Kevin Lewis <kvn@lws.io>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good post, a few notes/changes needed.
- Note: I am suggesting to move installing
svelte-video-player
to the top of the post alongside other dependencies. - I've removed linebreaks in
<style>
rules. This is to minimize the space they take up. - Please fix the fields syntax throughout. What you've done works, but it's right. Instead of
field.nested_field
, it should be{field: ['nested_field']}
. - I've removed all of the
+
at the top of snippets - I think this might be an error in copy and pasting, but if they're important please add them back.
## Displaying thumbnails and titles | ||
Update the your `routes/+page.svelte` file to use the `getVideos` function to fetch video data and display it using the `VideoGrid` component. This will display the thumbnails, titles, views and dates of the videos. | ||
```svelte | ||
+ | ||
<script lang="ts"> | ||
import { onMount } from "svelte"; | ||
import { getVideos } from "$lib/services/index"; | ||
import VideoGrid from "$lib/components/VideoGrid.svelte"; | ||
import type { Video } from "$lib/types"; | ||
|
||
let videos: Video[] = []; | ||
|
||
onMount(async () => { | ||
try { | ||
videos = await getVideos({ | ||
sort: ["-upload_date"], | ||
limit: 20, | ||
fields: ["*", "thumbnail.*", "video_file.*"], | ||
}); | ||
} catch (error) { | ||
console.error("Error fetching videos:", error); | ||
} | ||
}); | ||
</script> | ||
|
||
<h1>Stream your favourite vidoes</h1> | ||
|
||
|
||
{#if videos.length > 0} | ||
<VideoGrid {videos} /> | ||
{:else} | ||
<p>Loading videos...</p> | ||
{/if} | ||
``` | ||
Directus stores file metadata in the `directus_files` collection. When using file fields in other collections, Directus creates a one-to-many relationship. To include file data when fetching items, you use dot notation in the fields parameter, like `thumbnail.*` and `video_file.*`. This tells Directus to populate the file information from the `directus_files` collection. | ||
|
||
 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
## Displaying thumbnails and titles | |
Update the your `routes/+page.svelte` file to use the `getVideos` function to fetch video data and display it using the `VideoGrid` component. This will display the thumbnails, titles, views and dates of the videos. | |
```svelte | |
+ | |
<script lang="ts"> | |
import { onMount } from "svelte"; | |
import { getVideos } from "$lib/services/index"; | |
import VideoGrid from "$lib/components/VideoGrid.svelte"; | |
import type { Video } from "$lib/types"; | |
let videos: Video[] = []; | |
onMount(async () => { | |
try { | |
videos = await getVideos({ | |
sort: ["-upload_date"], | |
limit: 20, | |
fields: ["*", "thumbnail.*", "video_file.*"], | |
}); | |
} catch (error) { | |
console.error("Error fetching videos:", error); | |
} | |
}); | |
</script> | |
<h1>Stream your favourite vidoes</h1> | |
{#if videos.length > 0} | |
<VideoGrid {videos} /> | |
{:else} | |
<p>Loading videos...</p> | |
{/if} | |
``` | |
Directus stores file metadata in the `directus_files` collection. When using file fields in other collections, Directus creates a one-to-many relationship. To include file data when fetching items, you use dot notation in the fields parameter, like `thumbnail.*` and `video_file.*`. This tells Directus to populate the file information from the `directus_files` collection. | |
 | |
## Displaying Thumbnails and Titles | |
Update the your `routes/+page.svelte` file to use the `getVideos` function to fetch video data and display it using the `VideoGrid` component. This will display the thumbnails, titles, views and dates of the videos. | |
```svelte | |
<script lang="ts"> | |
import { onMount } from "svelte"; | |
import { getVideos } from "$lib/services/index"; | |
import VideoGrid from "$lib/components/VideoGrid.svelte"; | |
import type { Video } from "$lib/types"; | |
let videos: Video[] = []; | |
onMount(async () => { | |
try { | |
videos = await getVideos({ | |
sort: ["-upload_date"], | |
limit: 20, | |
fields: ["*", "thumbnail.*", "video_file.*"], | |
}); | |
} catch (error) { | |
console.error("Error fetching videos:", error); | |
} | |
}); | |
</script> | |
<h1>Stream your favorite videos</h1> | |
{#if videos.length > 0} | |
<VideoGrid {videos} /> | |
{:else} | |
<p>Loading videos...</p> | |
{/if} | |
``` | |
Directus stores file metadata in the `directus_files` collection, and when using file fields in other collections, Directus creates a one-to-many relationship. To include file data when fetching items, you use dot notation in the fields parameter, like `thumbnail.*` and `video_file.*`. This tells Directus to populate the file information from the `directus_files` collection. | |
 |
Please update this to use the correct fields syntax. what you've done works but is not recommended. Nested items should be an object - refer to our docs for more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
## Video Player Page | ||
Install the Svelte video player component to play the videos with the command. | ||
|
||
```shell | ||
npm install svelte-video-player | ||
``` | ||
|
||
Update your `services/index.ts` file to add new functions that will fetch a video by its ID and update the `videos` collection to increment the video's views field. | ||
```ts | ||
+ | ||
// your other imports | ||
import { readItems, readItem, updateItem } from "@directus/sdk"; | ||
|
||
export async function getVideo(id: string): Promise<Video> { | ||
const directus = getDirectusClient(); | ||
const response = await directus.request( | ||
readItem("videos", id, { | ||
fields: ["*", "thumbnail.*", "video_file.*"], | ||
}) | ||
); | ||
return response as Video; | ||
} | ||
|
||
export async function incrementViews(id: string) { | ||
const directus = getDirectusClient(); | ||
const video = await directus.request(readItem("videos", id)); | ||
await directus.request( | ||
updateItem("videos", id, { views: parseInt(video.views || 0) + 1 }) | ||
); | ||
} | ||
``` | ||
|
||
Create a nested route in your **routes** folder in the format `video/[id]/+page.svelte` to create a page to play selected videos. Update this file with the following code: | ||
|
||
```svelte | ||
<script lang="ts"> | ||
import { page } from "$app/stores"; | ||
import { getVideo, incrementViews } from "$lib/services"; | ||
import VideoPlayer from "svelte-video-player"; | ||
import type { Video } from "$lib/types"; | ||
|
||
let video: Video | null = null; | ||
|
||
$: id = $page.params.id; | ||
$: if (id) { | ||
getVideo(id) | ||
.then((v: Video) => { | ||
video = v; | ||
incrementViews(id); | ||
}) | ||
.catch((error) => { | ||
console.error("Error fetching video:", error); | ||
}); | ||
} | ||
</script> | ||
|
||
{#if video} | ||
<h1>{video.title}</h1> | ||
<p> | ||
{video.views} views • {new Date(video.upload_date).toLocaleDateString()} | ||
</p> | ||
<VideoPlayer | ||
poster={`${import.meta.env.VITE_DIRECTUS_URL}/assets/${video.thumbnail.id}`} | ||
source={`${import.meta.env.VITE_DIRECTUS_URL}/assets/${video.video_file.id}`} | ||
/> | ||
<h2>Description</h2> | ||
<p>{video.description}</p> | ||
<h2>Tags</h2> | ||
<div class="tags"> | ||
{#each video.tags as tag} | ||
<span class="tag">{tag}</span> | ||
{/each} | ||
</div> | ||
{:else} | ||
<p>Loading...</p> | ||
{/if} | ||
|
||
<style> | ||
.tags { | ||
display: flex; | ||
flex-wrap: wrap; | ||
gap: 0.5rem; | ||
} | ||
.tag { | ||
background-color: #eee; | ||
padding: 0.25rem 0.5rem; | ||
border-radius: 0.25rem; | ||
} | ||
</style> | ||
``` | ||
Now click on any of the videos to stream it. | ||
|
||
 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
## Video Player Page | |
Install the Svelte video player component to play the videos with the command. | |
```shell | |
npm install svelte-video-player | |
``` | |
Update your `services/index.ts` file to add new functions that will fetch a video by its ID and update the `videos` collection to increment the video's views field. | |
```ts | |
+ | |
// your other imports | |
import { readItems, readItem, updateItem } from "@directus/sdk"; | |
export async function getVideo(id: string): Promise<Video> { | |
const directus = getDirectusClient(); | |
const response = await directus.request( | |
readItem("videos", id, { | |
fields: ["*", "thumbnail.*", "video_file.*"], | |
}) | |
); | |
return response as Video; | |
} | |
export async function incrementViews(id: string) { | |
const directus = getDirectusClient(); | |
const video = await directus.request(readItem("videos", id)); | |
await directus.request( | |
updateItem("videos", id, { views: parseInt(video.views || 0) + 1 }) | |
); | |
} | |
``` | |
Create a nested route in your **routes** folder in the format `video/[id]/+page.svelte` to create a page to play selected videos. Update this file with the following code: | |
```svelte | |
<script lang="ts"> | |
import { page } from "$app/stores"; | |
import { getVideo, incrementViews } from "$lib/services"; | |
import VideoPlayer from "svelte-video-player"; | |
import type { Video } from "$lib/types"; | |
let video: Video | null = null; | |
$: id = $page.params.id; | |
$: if (id) { | |
getVideo(id) | |
.then((v: Video) => { | |
video = v; | |
incrementViews(id); | |
}) | |
.catch((error) => { | |
console.error("Error fetching video:", error); | |
}); | |
} | |
</script> | |
{#if video} | |
<h1>{video.title}</h1> | |
<p> | |
{video.views} views • {new Date(video.upload_date).toLocaleDateString()} | |
</p> | |
<VideoPlayer | |
poster={`${import.meta.env.VITE_DIRECTUS_URL}/assets/${video.thumbnail.id}`} | |
source={`${import.meta.env.VITE_DIRECTUS_URL}/assets/${video.video_file.id}`} | |
/> | |
<h2>Description</h2> | |
<p>{video.description}</p> | |
<h2>Tags</h2> | |
<div class="tags"> | |
{#each video.tags as tag} | |
<span class="tag">{tag}</span> | |
{/each} | |
</div> | |
{:else} | |
<p>Loading...</p> | |
{/if} | |
<style> | |
.tags { | |
display: flex; | |
flex-wrap: wrap; | |
gap: 0.5rem; | |
} | |
.tag { | |
background-color: #eee; | |
padding: 0.25rem 0.5rem; | |
border-radius: 0.25rem; | |
} | |
</style> | |
``` | |
Now click on any of the videos to stream it. | |
 | |
## Creating the Video Player Page | |
Update your `services/index.ts` file to add new functions that will fetch a video by its ID and update the `videos` collection to increment the video's views field. | |
```ts | |
// your other imports | |
import { readItems, readItem, updateItem } from "@directus/sdk"; | |
export async function getVideo(id: string): Promise<Video> { | |
const directus = getDirectusClient(); | |
const response = await directus.request( | |
readItem("videos", id, { | |
fields: ["*", "thumbnail.*", "video_file.*"], | |
}) | |
); | |
return response as Video; | |
} | |
export async function incrementViews(id: string) { | |
const directus = getDirectusClient(); | |
const video = await directus.request(readItem("videos", id)); | |
await directus.request( | |
updateItem("videos", id, { views: parseInt(video.views || 0) + 1 }) | |
); | |
} | |
``` | |
Create a nested route in your **routes** folder in the format `video/[id]/+page.svelte` to create a page to play selected videos. Update this file with the following code: | |
```svelte | |
<script lang="ts"> | |
import { page } from "$app/stores"; | |
import { getVideo, incrementViews } from "$lib/services"; | |
import VideoPlayer from "svelte-video-player"; | |
import type { Video } from "$lib/types"; | |
let video: Video | null = null; | |
$: id = $page.params.id; | |
$: if (id) { | |
getVideo(id) | |
.then((v: Video) => { | |
video = v; | |
incrementViews(id); | |
}) | |
.catch((error) => { | |
console.error("Error fetching video:", error); | |
}); | |
} | |
</script> | |
{#if video} | |
<h1>{video.title}</h1> | |
<p> | |
{video.views} views • {new Date(video.upload_date).toLocaleDateString()} | |
</p> | |
<VideoPlayer | |
poster={`${import.meta.env.VITE_DIRECTUS_URL}/assets/${video.thumbnail.id}`} | |
source={`${import.meta.env.VITE_DIRECTUS_URL}/assets/${video.video_file.id}`} | |
/> | |
<h2>Description</h2> | |
<p>{video.description}</p> | |
<h2>Tags</h2> | |
<div class="tags"> | |
{#each video.tags as tag} | |
<span class="tag">{tag}</span> | |
{/each} | |
</div> | |
{:else} | |
<p>Loading...</p> | |
{/if} | |
<style> | |
.tags { display: flex; flex-wrap: wrap; gap: 0.5rem; } | |
.tag { background-color: #eee; padding: 0.25rem 0.5rem; border-radius: 0.25rem; } | |
</style> | |
``` | |
Now click on any of the videos to stream it. | |
 |
Sorry to be annoying but can you double check the syntax in the video/[id]/+page.svelte
<script>
. I'm not a huge Svelte user, but looks a bit funky so a double check is always best.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The syntax is correct for a Svelte application :)
## Search Functionality | ||
In your `services/index.ts`, add a new funtion that implements search functionality to find videos by title or description. | ||
|
||
```ts | ||
+ | ||
export async function searchVideos(query: string): Promise<Video[]> { | ||
const directus = getDirectusClient(); | ||
const response = await directus.request( | ||
readItems("videos", { | ||
search: query, | ||
fields: ["*", "thumbnail.*", "video_file.*"], | ||
}) | ||
); | ||
return response as Video[]; | ||
} | ||
``` | ||
This function uses the `search` parameter from Directus to perform a search on `videos` collection. | ||
|
||
Update the code in your `routes/+page.svelte` file to use the `searchVideos` function to add search functionality to your page. | ||
|
||
```svelte | ||
+ | ||
<script lang="ts"> | ||
import { onMount } from "svelte"; | ||
import { getVideos, searchVideos } from "$lib/services/index"; | ||
import VideoGrid from "$lib/components/VideoGrid.svelte"; | ||
import type { Video } from "$lib/types"; | ||
|
||
let videos: Video[] = []; | ||
let searchQuery = ""; | ||
let searchResults: Video[] = []; | ||
let isSearching = false; | ||
|
||
onMount(async () => { | ||
await loadLatestVideos(); | ||
}); | ||
|
||
async function loadLatestVideos() { | ||
try { | ||
videos = (await getVideos({ | ||
sort: ["-upload_date"], | ||
limit: 20, | ||
fields: ["*", "thumbnail.*", "video_file.*"], | ||
})) as Video[]; | ||
} catch (error) { | ||
console.error("Error fetching videos:", error); | ||
} | ||
} | ||
|
||
async function handleSearch() { | ||
if (searchQuery.trim()) { | ||
isSearching = true; | ||
try { | ||
const response = await searchVideos(searchQuery); | ||
searchResults = response as Video[]; | ||
} catch (error) { | ||
console.error("Error searching videos:", error); | ||
} finally { | ||
isSearching = false; | ||
} | ||
} else { | ||
searchResults = []; | ||
} | ||
} | ||
</script> | ||
|
||
<h1>Stream your favourite vidoes</h1> | ||
|
||
<form on:submit|preventDefault={handleSearch}> | ||
<input type="text" bind:value={searchQuery} placeholder="Search for videos" /> | ||
<button type="submit">Search</button> | ||
</form> | ||
|
||
{#if isSearching} | ||
<p>Searching...</p> | ||
{:else if searchResults.length > 0} | ||
<h2>Search Results</h2> | ||
<VideoGrid videos={searchResults} /> | ||
{:else if searchQuery} | ||
<p>No results found.</p> | ||
{:else} | ||
<h2>Latest Videos</h2> | ||
{#if videos.length > 0} | ||
<VideoGrid {videos} /> | ||
{:else} | ||
<p>Loading videos...</p> | ||
{/if} | ||
{/if} | ||
``` | ||
|
||
You can now search and stream any video of your choice. | ||
 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
## Search Functionality | |
In your `services/index.ts`, add a new funtion that implements search functionality to find videos by title or description. | |
```ts | |
+ | |
export async function searchVideos(query: string): Promise<Video[]> { | |
const directus = getDirectusClient(); | |
const response = await directus.request( | |
readItems("videos", { | |
search: query, | |
fields: ["*", "thumbnail.*", "video_file.*"], | |
}) | |
); | |
return response as Video[]; | |
} | |
``` | |
This function uses the `search` parameter from Directus to perform a search on `videos` collection. | |
Update the code in your `routes/+page.svelte` file to use the `searchVideos` function to add search functionality to your page. | |
```svelte | |
+ | |
<script lang="ts"> | |
import { onMount } from "svelte"; | |
import { getVideos, searchVideos } from "$lib/services/index"; | |
import VideoGrid from "$lib/components/VideoGrid.svelte"; | |
import type { Video } from "$lib/types"; | |
let videos: Video[] = []; | |
let searchQuery = ""; | |
let searchResults: Video[] = []; | |
let isSearching = false; | |
onMount(async () => { | |
await loadLatestVideos(); | |
}); | |
async function loadLatestVideos() { | |
try { | |
videos = (await getVideos({ | |
sort: ["-upload_date"], | |
limit: 20, | |
fields: ["*", "thumbnail.*", "video_file.*"], | |
})) as Video[]; | |
} catch (error) { | |
console.error("Error fetching videos:", error); | |
} | |
} | |
async function handleSearch() { | |
if (searchQuery.trim()) { | |
isSearching = true; | |
try { | |
const response = await searchVideos(searchQuery); | |
searchResults = response as Video[]; | |
} catch (error) { | |
console.error("Error searching videos:", error); | |
} finally { | |
isSearching = false; | |
} | |
} else { | |
searchResults = []; | |
} | |
} | |
</script> | |
<h1>Stream your favourite vidoes</h1> | |
<form on:submit|preventDefault={handleSearch}> | |
<input type="text" bind:value={searchQuery} placeholder="Search for videos" /> | |
<button type="submit">Search</button> | |
</form> | |
{#if isSearching} | |
<p>Searching...</p> | |
{:else if searchResults.length > 0} | |
<h2>Search Results</h2> | |
<VideoGrid videos={searchResults} /> | |
{:else if searchQuery} | |
<p>No results found.</p> | |
{:else} | |
<h2>Latest Videos</h2> | |
{#if videos.length > 0} | |
<VideoGrid {videos} /> | |
{:else} | |
<p>Loading videos...</p> | |
{/if} | |
{/if} | |
``` | |
You can now search and stream any video of your choice. | |
 | |
## Implementing Search | |
In your `services/index.ts`, add a new funtion that implements search functionality to find videos by title or description. | |
```ts | |
export async function searchVideos(query: string): Promise<Video[]> { | |
const directus = getDirectusClient(); | |
const response = await directus.request( | |
readItems("videos", { | |
search: query, | |
fields: ["*", "thumbnail.*", "video_file.*"], | |
}) | |
); | |
return response as Video[]; | |
} | |
``` | |
This function uses the `search` parameter from Directus to perform a search on `videos` collection. | |
Update the code in your `routes/+page.svelte` file to use the `searchVideos` function to add search functionality to your page. | |
```svelte | |
<script lang="ts"> | |
import { onMount } from "svelte"; | |
import { getVideos, searchVideos } from "$lib/services/index"; | |
import VideoGrid from "$lib/components/VideoGrid.svelte"; | |
import type { Video } from "$lib/types"; | |
let videos: Video[] = []; | |
let searchQuery = ""; | |
let searchResults: Video[] = []; | |
let isSearching = false; | |
onMount(async () => { | |
await loadLatestVideos(); | |
}); | |
async function loadLatestVideos() { | |
try { | |
videos = (await getVideos({ | |
sort: ["-upload_date"], | |
limit: 20, | |
fields: ["*", "thumbnail.*", "video_file.*"], | |
})) as Video[]; | |
} catch (error) { | |
console.error("Error fetching videos:", error); | |
} | |
} | |
async function handleSearch() { | |
if (searchQuery.trim()) { | |
isSearching = true; | |
try { | |
const response = await searchVideos(searchQuery); | |
searchResults = response as Video[]; | |
} catch (error) { | |
console.error("Error searching videos:", error); | |
} finally { | |
isSearching = false; | |
} | |
} else { | |
searchResults = []; | |
} | |
} | |
</script> | |
<h1>Stream your favorite videos</h1> | |
<form on:submit|preventDefault={handleSearch}> | |
<input type="text" bind:value={searchQuery} placeholder="Search for videos" /> | |
<button type="submit">Search</button> | |
</form> | |
{#if isSearching} | |
<p>Searching...</p> | |
{:else if searchResults.length > 0} | |
<h2>Search Results</h2> | |
<VideoGrid videos={searchResults} /> | |
{:else if searchQuery} | |
<p>No results found.</p> | |
{:else} | |
<h2>Latest Videos</h2> | |
{#if videos.length > 0} | |
<VideoGrid {videos} /> | |
{:else} | |
<p>Loading videos...</p> | |
{/if} | |
{/if} | |
``` | |
You can now search and stream any video of your choice. | |
 |
Please use the // [!code ++]
at the end of any line you want to add a + diff to in the rendered post.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
Co-authored-by: Kevin Lewis <kvn@lws.io>
Co-authored-by: Kevin Lewis <kvn@lws.io>
Co-authored-by: Kevin Lewis <kvn@lws.io>
Co-authored-by: Kevin Lewis <kvn@lws.io>
Co-authored-by: Kevin Lewis <kvn@lws.io>
Hi @phazonoverload, |
Thank you for writing this, and we are happy to consider it done in it's current state. My team will schedule it in to be published. This repo is just for authoring/reviewing - the actual piece will be published via a Directus project, so we'll take it from here. I'll close this issue to keep the main branch clean, but this is a acceptance of your work. Could you email us at devrel@directus.io and we'll share what we need to get you paid. Thanks! |
This is the PR for issue: #199