Skip to content
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

Closed
wants to merge 30 commits into from

Conversation

Claradev32
Copy link

This is the PR for issue: #199

Claradev32 and others added 21 commits April 17, 2024 19:51
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>
@phazonoverload phazonoverload linked an issue Jul 8, 2024 that may be closed by this pull request
6 tasks
@phazonoverload phazonoverload changed the title Build a Video streaming app with Directus and Sveltekit Building a Video Streaming App with SvelteKit and Directus Jul 8, 2024
Copy link
Member

@phazonoverload phazonoverload left a 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.

Comment on lines 160 to 196
## 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.

![Video Listing](<Screenshot 2024-07-04 at 11.53.25.png>)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## 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.
![Video Listing](<Screenshot 2024-07-04 at 11.53.25.png>)
## 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.
![Video Listing](<Screenshot 2024-07-04 at 11.53.25.png>)

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines 198 to 290
## 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.

![Video Player](<Screenshot 2024-07-04 at 12.27.56.png>)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## 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.
![Video Player](<Screenshot 2024-07-04 at 12.27.56.png>)
## 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.
![Video Player](<Screenshot 2024-07-04 at 12.27.56.png>)

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.

Copy link
Author

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 :)

Comment on lines 292 to 383
## 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.
![Search videos](<Screenshot 2024-07-04 at 12.48.17.png>)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## 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.
![Search videos](<Screenshot 2024-07-04 at 12.48.17.png>)
## 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.
![Search videos](<Screenshot 2024-07-04 at 12.48.17.png>)

Please use the // [!code ++] at the end of any line you want to add a + diff to in the rendered post.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@Claradev32
Copy link
Author

Hi @phazonoverload,
I have added the review changes.

@phazonoverload
Copy link
Member

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Build a Video streaming app with Directus and Sveltekit
2 participants