Tworzenie aplikacji do przesyłania strumieniowego wideo za pomocą Nuxt.js, Node i Express

Opublikowany: 2022-03-10
Krótkie podsumowanie ↬ W tym artykule będziemy budować aplikację do strumieniowego przesyłania wideo przy użyciu Nuxt.js i Node.js. W szczególności zbudujemy aplikację Node.js działającą po stronie serwera, która będzie obsługiwać pobieranie i przesyłanie strumieniowe filmów, generowanie miniatur do Twoich filmów oraz udostępnianie podpisów i napisów.

Filmy działają ze strumieniami. Oznacza to, że zamiast wysyłać cały film na raz, film jest wysyłany jako zestaw mniejszych fragmentów, które składają się na cały film. To wyjaśnia, dlaczego filmy są buforowane podczas oglądania wideo na wolnym łączu szerokopasmowym, ponieważ odtwarza tylko otrzymane fragmenty i próbuje załadować więcej.

Ten artykuł jest przeznaczony dla programistów, którzy chcą nauczyć się nowej technologii, tworząc rzeczywisty projekt: aplikację do strumieniowego przesyłania wideo z Node.js jako zapleczem i Nuxt.js jako klientem.

  • Node.js to środowisko uruchomieniowe służące do tworzenia szybkich i skalowalnych aplikacji. Wykorzystamy go do obsługi pobierania i przesyłania strumieniowego filmów, generowania miniatur filmów oraz udostępniania podpisów i napisów do filmów.
  • Nuxt.js to framework Vue.js, który pomaga nam łatwo tworzyć serwerowe aplikacje Vue.js. Wykorzystamy nasze API dla filmów, a ta aplikacja będzie miała dwa widoki: listę dostępnych filmów i widok odtwarzacza dla każdego filmu.

Wymagania wstępne

  • Znajomość HTML, CSS, JavaScript, Node/Express i Vue.
  • Edytor tekstu (np. VS Code).
  • Przeglądarka internetowa (np. Chrome, Firefox).
  • FFmpeg zainstalowany na Twojej stacji roboczej.
  • Node.js. nvm.
  • Możesz pobrać kod źródłowy na GitHub.

Konfigurowanie naszej aplikacji

W tej aplikacji zbudujemy trasy do wykonywania żądań z frontendu:

  • videos trasy, aby uzyskać listę filmów i ich danych.
  • drogę do pobrania tylko jednego filmu z naszej listy filmów.
  • trasa streaming do przesyłania strumieniowego filmów.
  • trasa captions , aby dodać napisy do przesyłanych przez nas filmów.

Po utworzeniu naszych tras utworzymy szkielet naszego interfejsu Nuxt , w którym stworzymy stronę Home i dynamiczną stronę player . Następnie żądamy, aby nasza trasa videos wypełniła stronę główną danymi wideo, kolejną prośbę o przesyłanie strumieniowe filmów na naszej stronie player , a na koniec prośbę o udostępnienie plików napisów, które mają być używane przez filmy.

Aby skonfigurować naszą aplikację, tworzymy katalog naszego projektu,

 mkdir streaming-app
Więcej po skoku! Kontynuuj czytanie poniżej ↓

Konfigurowanie naszego serwera

W naszym katalogu streaming-app tworzymy folder o nazwie backend .

 cd streaming-app mkdir backend

W naszym folderze zaplecza inicjujemy plik package.json , aby przechowywać informacje o naszym projekcie serwera.

 cd backend npm init -y

musimy zainstalować następujące pakiety, aby zbudować naszą aplikację.

  • nodemon automatycznie restartuje nasz serwer, gdy wprowadzamy zmiany.
  • express daje nam ładny interfejs do obsługi tras.
  • cors pozwoli nam na wykonywanie żądań cross-origin, ponieważ nasz klient i serwer będą działały na różnych portach.

W naszym katalogu zaplecza tworzymy assets folderu do przechowywania naszych filmów do przesyłania strumieniowego.

 mkdir assets

Skopiuj plik .mp4 do folderu zasobów i nadaj mu nazwę video1 . Możesz użyć krótkich przykładowych filmów .mp4 , które można znaleźć na Github Repo.

Utwórz plik app.js i dodaj niezbędne pakiety dla naszej aplikacji.

 const express = require('express'); const fs = require('fs'); const cors = require('cors'); const path = require('path'); const app = express(); app.use(cors())

Moduł fs służy do łatwego odczytu i zapisu do plików na naszym serwerze, podczas gdy moduł path zapewnia sposób pracy z katalogami i ścieżkami plików.

Teraz tworzymy trasę ./video . Na żądanie odeśle plik wideo z powrotem do klienta.

 // add after 'const app = express();' app.get('/video', (req, res) => { res.sendFile('assets/video1.mp4', { root: __dirname }); });

Ta trasa obsługuje plik wideo video1.mp4 na żądanie. Następnie nasłuchujemy naszego serwera na porcie 3000 .

 // add to end of app.js file app.listen(5000, () => { console.log('Listening on port 5000!') });

W pliku package.json dodawany jest skrypt uruchamiający nasz serwer za pomocą nodemon.

 "scripts": { "start": "nodemon app.js" },

Następnie na swoim terminalu uruchom:

 npm run start

Jeśli zobaczysz komunikat Listening on port 3000! w terminalu oznacza to, że serwer działa poprawnie. Przejdź do https://localhost:5000/video w przeglądarce i powinieneś zobaczyć odtwarzane wideo.

Żądania do obsługi przez frontend

Poniżej znajdują się żądania, które wyślemy do backendu z naszego frontendu, do obsługi którego potrzebujemy serwer.

  • /videos
    Zwraca tablicę danych makiet wideo, które zostaną użyte do wypełnienia listy filmów na stronie Home w naszym interfejsie.
  • /video/:id/data
    Zwraca metadane dla pojedynczego filmu. Używany przez stronę Player w naszym interfejsie.
  • /video/:id
    Przesyła wideo z podanym identyfikatorem. Używany przez stronę Player .

Stwórzmy trasy.

Zwróć dane makiety dla listy filmów

W przypadku tej aplikacji demonstracyjnej utworzymy tablicę obiektów , które będą przechowywać metadane i na żądanie wyślą je do interfejsu użytkownika. W prawdziwej aplikacji prawdopodobnie odczytałeś dane z bazy danych, które następnie posłużyłyby do wygenerowania takiej tablicy. Dla uproszczenia nie będziemy tego robić w tym samouczku.

W naszym folderze zaplecza utwórz plik mockdata.js i wypełnij go metadanymi dla naszej listy filmów.

 const allVideos = [ { id: "tom and jerry", poster: 'https://image.tmdb.org/t/p/w500/fev8UFNFFYsD5q7AcYS8LyTzqwl.jpg', duration: '3 mins', name: 'Tom & Jerry' }, { id: "soul", poster: 'https://image.tmdb.org/t/p/w500/kf456ZqeC45XTvo6W9pW5clYKfQ.jpg', duration: '4 mins', name: 'Soul' }, { id: "outside the wire", poster: 'https://image.tmdb.org/t/p/w500/lOSdUkGQmbAl5JQ3QoHqBZUbZhC.jpg', duration: '2 mins', name: 'Outside the wire' }, ]; module.exports = allVideos

Widzimy z góry, że każdy obiekt zawiera informacje o filmie. Zwróć uwagę na atrybut poster , który zawiera link do obrazu plakatu wideo.

Stwórzmy trasę videos , ponieważ wszystkie nasze żądania, które mają zostać wysłane przez interfejs, są poprzedzone /videos .

Aby to zrobić, utwórzmy folder Routes i dodajmy plik Video.js dla naszej routes /videos . W tym pliku będziemy potrzebować express i użyć routera express do utworzenia naszej trasy.

 const express = require('express') const router = express.Router()

Kiedy przechodzimy do ścieżki /videos , chcemy uzyskać naszą listę filmów, więc wymagamy pliku mockData.js do naszego pliku Video.js i wysyłamy nasze żądanie.

 const express = require('express') const router = express.Router() const videos = require('../mockData') // get list of videos router.get('/', (req,res)=>{ res.json(videos) }) module.exports = router;

Trasa /videos jest teraz zadeklarowana, zapisz plik i powinien automatycznie zrestartować serwer. Po uruchomieniu przejdź do https://localhost:3000/videos, a nasza tablica zostanie zwrócona w formacie JSON.

Zwróć dane dla pojedynczego wideo

Chcemy mieć możliwość złożenia wniosku o konkretny film z naszej listy filmów. Możemy pobrać określone dane wideo z naszej tablicy, używając podanego przez nas id . Złóżmy żądanie, wciąż w naszym pliku Video.js .

 // make request for a particular video router.get('/:id/data', (req,res)=> { const id = parseInt(req.params.id, 10) res.json(videos[id]) })

Powyższy kod pobiera id z parametrów trasy i konwertuje go na liczbę całkowitą. Następnie wysyłamy do klienta obiekt, który pasuje do id z tablicy videos .

Przesyłanie strumieniowe filmów

W naszym pliku app.js utworzyliśmy trasę /video , która udostępnia wideo klientowi. Chcemy, aby ten punkt końcowy wysyłał mniejsze fragmenty wideo, zamiast udostępniać na żądanie cały plik wideo.

Chcemy być w stanie dynamicznie wyświetlać jeden z trzech filmów znajdujących się w tablicy allVideos i przesyłać je strumieniowo porcjami, więc:

Usuń trasę /video z app.js .

Potrzebujemy trzech filmów, więc skopiuj przykładowe filmy z kodu źródłowego samouczka do katalogu assets/ projektu server . Upewnij się, że nazwy plików wideo odpowiadają id w tablicy videos :

Wróć do naszego pliku Video.js i utwórz trasę do strumieniowego przesyłania filmów.

 router.get('/video/:id', (req, res) => { const videoPath = `assets/${req.params.id}.mp4`; const videoStat = fs.statSync(videoPath); const fileSize = videoStat.size; const videoRange = req.headers.range; if (videoRange) { const parts = videoRange.replace(/bytes=/, "").split("-"); const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize-1; const chunksize = (end-start) + 1; const file = fs.createReadStream(videoPath, {start, end}); const head = { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4', }; res.writeHead(206, head); file.pipe(res); } else { const head = { 'Content-Length': fileSize, 'Content-Type': 'video/mp4', }; res.writeHead(200, head); fs.createReadStream(videoPath).pipe(res); } });

Jeśli przejdziemy do https://localhost:5000/videos/video/outside-the-wire w naszej przeglądarce, możemy zobaczyć strumieniowe przesyłanie wideo.

Jak działa trasa przesyłania strumieniowego wideo

W naszym strumieniowym strumieniu wideo jest napisany sporo kodu, więc spójrzmy na to wiersz po wierszu.

 const videoPath = `assets/${req.params.id}.mp4`; const videoStat = fs.statSync(videoPath); const fileSize = videoStat.size; const videoRange = req.headers.range;

Najpierw z naszego żądania uzyskujemy id z trasy za pomocą req.params.id i używamy go do wygenerowania videoPath wideo do wideo. Następnie odczytujemy plik fileSize przy użyciu zaimportowanego systemu plików fs . W przypadku filmów przeglądarka użytkownika wyśle ​​w żądaniu parametr range . Dzięki temu serwer wie, który fragment wideo należy odesłać do klienta.

Niektóre przeglądarki wysyłają zakres w początkowym żądaniu, ale inne nie. Dla tych, które tego nie robią lub jeśli z jakiegokolwiek innego powodu przeglądarka nie wysyła zakresu, zajmujemy się tym w bloku else . Ten kod pobiera rozmiar pliku i wysyła kilka pierwszych fragmentów wideo:

 else { const head = { 'Content-Length': fileSize, 'Content-Type': 'video/mp4', }; res.writeHead(200, head); fs.createReadStream(path).pipe(res); }

Będziemy obsługiwać kolejne żądania zawierające zakres w bloku if .

 if (videoRange) { const parts = videoRange.replace(/bytes=/, "").split("-"); const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize-1; const chunksize = (end-start) + 1; const file = fs.createReadStream(videoPath, {start, end}); const head = { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4', }; res.writeHead(206, head); file.pipe(res); }

Powyższy kod tworzy strumień odczytu przy użyciu wartości start i end zakresu. Ustaw Content-Length nagłówków odpowiedzi na rozmiar porcji, który jest obliczany na podstawie wartości start i end . Używamy również kodu HTTP 206, co oznacza, że ​​odpowiedź zawiera częściową treść. Oznacza to, że przeglądarka będzie wysyłać żądania, dopóki nie pobierze wszystkich fragmentów wideo.

Co się dzieje na niestabilnych połączeniach

Jeśli użytkownik korzysta z wolnego połączenia, strumień sieciowy zasygnalizuje to, żądając wstrzymania źródła we/wy, aż klient będzie gotowy na więcej danych. Nazywa się to przeciwciśnieniem . Możemy pójść o krok dalej i zobaczyć, jak łatwo jest rozszerzyć strumień. Możemy też łatwo dodać kompresję!

 const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize-1; const chunksize = (end-start) + 1; const file = fs.createReadStream(videoPath, {start, end});

Widzimy powyżej, że tworzony jest ReadStream i obsługuje fragment wideo po kawałku.

 const head = { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4', }; res.writeHead(206, head); file.pipe(res);

Nagłówek żądania zawiera Content-Range , który jest początkiem i końcem zmieniającym się w celu uzyskania następnego fragmentu wideo, który ma być przesyłany strumieniowo do interfejsu użytkownika, content-length to fragment wysłanego wideo. Określamy również rodzaj treści, które przesyłamy strumieniowo, czyli mp4 . Głowica zapisująca 206 ma odpowiadać tylko za pomocą nowo utworzonych strumieni.

Tworzenie pliku z napisami do naszych filmów

Tak wygląda plik .vtt .

 WEBVTT 00:00:00.200 --> 00:00:01.000 Creating a tutorial can be very 00:00:01.500 --> 00:00:04.300 fun to do.

Pliki z napisami zawierają tekst opisujący to, co zostało powiedziane w filmie. Zawiera również kody czasowe określające, kiedy każdy wiersz tekstu powinien być wyświetlany. Chcemy, aby nasze filmy miały podpisy i nie będziemy tworzyć własnego pliku podpisów do tego samouczka, więc możesz przejść do folderu podpisów w katalogu assets w repozytorium i pobrać podpisy.

Utwórzmy nową trasę, która obsłuży żądanie napisów:

 router.get('/video/:id/caption', (req, res) => res.sendFile(`assets/captions/${req.params.id}.vtt`, { root: __dirname }));

Budowanie naszego frontendu

Aby rozpocząć pracę nad wizualną częścią naszego systemu, musielibyśmy zbudować nasze rusztowanie frontendowe.

Uwaga : do stworzenia naszej aplikacji potrzebny jest vue-cli. Jeśli nie masz go zainstalowanego na swoim komputerze, możesz uruchomić npm install -g @vue/cli , aby go zainstalować.

Instalacja

U podstaw naszego projektu stwórzmy nasz folder front-end:

 mkdir frontend cd frontend

i w nim inicjujemy nasz plik package.json , kopiujemy i wklejamy w nim:

 { "name": "my-app", "scripts": { "dev": "nuxt", "build": "nuxt build", "generate": "nuxt generate", "start": "nuxt start" } }

następnie zainstaluj nuxt :

 npm add nuxt

i wykonaj następujące polecenie, aby uruchomić aplikację Nuxt.js:

 npm run dev

Nasza struktura plików Nuxt

Teraz, gdy mamy zainstalowany Nuxt, możemy zacząć układać nasz frontend.

Najpierw musimy utworzyć folder layouts w katalogu głównym naszej aplikacji. Ten folder definiuje układ aplikacji, bez względu na stronę, do której przechodzimy. Rzeczy takie jak nasz pasek nawigacyjny i stopka znajdują się tutaj. W folderze frontend tworzymy default.vue dla naszego domyślnego układu, kiedy uruchamiamy naszą aplikację frontendową.

 mkdir layouts cd layouts touch default.vue

Następnie folder components do tworzenia wszystkich naszych komponentów. Będziemy potrzebować tylko dwóch komponentów, NavBar i komponentu video . Tak więc w naszym głównym folderze frontendu:

 mkdir components cd components touch NavBar.vue touch Video.vue

Wreszcie folder stron, w którym można utworzyć wszystkie nasze strony, takie jak home i about . Dwie strony, których potrzebujemy w tej aplikacji, to home główna wyświetlająca wszystkie nasze filmy i informacje o filmach oraz strona dynamicznego odtwarzacza, która kieruje do filmu, który klikamy.

 mkdir pages cd pages touch index.vue mkdir player cd player touch _name.vue

Nasz katalog frontend wygląda teraz tak:

 |-frontend |-components |-NavBar.vue |-Video.vue |-layouts |-default.vue |-pages |-index.vue |-player |-_name.vue |-package.json |-yarn.lock

Komponent paska nawigacyjnego

Nasz NavBar.vue wygląda tak:

 <template> <div class="navbar"> <h1>Streaming App</h1> </div> </template> <style scoped> .navbar { display: flex; background-color: #161616; justify-content: center; align-items: center; } h1{ color:#a33327; } </style>

Pasek NavBar ma znacznik h1 , który wyświetla aplikację do przesyłania strumieniowego , z niewielkim stylem.

Zaimportujmy NavBar do naszego układu default.vue .

 // default.vue <template> <div> <NavBar /> <nuxt /> </div> </template> <script> import NavBar from "@/components/NavBar.vue" export default { components: { NavBar, } } </script>

Układ default.vue zawiera teraz nasz komponent NavBar i <nuxt /> , który wskazuje, gdzie będzie wyświetlana dowolna utworzona przez nas strona.

Na naszym index.vue (który jest naszą stroną główną) wyślij żądanie do https://localhost:5000/videos , aby pobrać wszystkie filmy z naszego serwera. Przekazanie danych jako rekwizytu do naszego komponentu video.vue stworzymy później. Ale na razie już go zaimportowaliśmy.

 <template> <div> <Video :videoList="videos"/> </div> </template> <script> import Video from "@/components/Video.vue" export default { components: { Video }, head: { title: "Home" }, data() { return { videos: [] } }, async fetch() { this.videos = await fetch( 'https://localhost:5000/videos' ).then(res => res.json()) } } </script>

Komponent wideo

Poniżej najpierw deklarujemy naszą rekwizyt. Ponieważ dane wideo są teraz dostępne w komponencie, używając v-for Vue iterujemy na wszystkich otrzymanych danych i dla każdego z nich wyświetlamy informacje. Możemy użyć dyrektywy v-for aby przejść przez dane i wyświetlić je jako listę. Dodano również podstawową stylizację.

 <template> <div> <div class="container"> <div v-for="(video, id) in videoList" :key="id" class="vid-con" > <NuxtLink :to="`/player/${video.id}`"> <div : class="vid" ></div> <div class="movie-info"> <div class="details"> <h2>{{video.name}}</h2> <p>{{video.duration}}</p> </div> </div> </NuxtLink> </div> </div> </div> </template> <script> export default { props:['videoList'], } </script> <style scoped> .container { display: flex; justify-content: center; align-items: center; margin-top: 2rem; } .vid-con { display: flex; flex-direction: column; flex-shrink: 0; justify-content: center; width: 50%; max-width: 16rem; margin: auto 2em; } .vid { height: 15rem; width: 100%; background-position: center; background-size: cover; } .movie-info { background: black; color: white; width: 100%; } .details { padding: 16px 20px; } </style>

Zauważyliśmy również, że NuxtLink ma dynamiczną trasę, która kieruje do /player/video.id .

Funkcjonalność, której potrzebujemy, polega na tym, że gdy użytkownik kliknie dowolny z filmów, rozpocznie się przesyłanie strumieniowe. Aby to osiągnąć, wykorzystujemy dynamiczną naturę trasy _name.vue .

W nim tworzymy odtwarzacz wideo i ustawiamy źródło na nasz punkt końcowy do przesyłania strumieniowego wideo, ale dynamicznie dołączamy, które wideo odtwarzać do naszego punktu końcowego za pomocą this.$route.params.name , który przechwytuje, który parametr otrzymany link .

 <template> <div class="player"> <video controls muted autoPlay> <source :src="`https://localhost:5000/videos/video/${vidName}`" type="video/mp4"> </video> </div> </template> <script> export default { data() { return { vidName: '' } }, mounted(){ this.vidName = this.$route.params.name } } </script> <style scoped> .player { display: flex; justify-content: center; align-items: center; margin-top: 2em; } </style>

Kiedy klikniemy na którykolwiek z filmów, otrzymamy:

Ostateczny wynik aplikacji do strumieniowego przesyłania wideo Nu
Strumieniowe przesyłanie wideo rozpoczyna się, gdy użytkownik kliknie miniaturę. (duży podgląd)

Dodawanie naszego pliku z napisami

Aby dodać nasz plik śledzenia, upewniamy się, że wszystkie pliki .vtt w folderze podpisów mają taką samą nazwę jak nasz id . Zaktualizuj nasz element wideo o ścieżkę, prosząc o napisy.

 <template> <div class="player"> <video controls muted autoPlay crossOrigin="anonymous"> <source :src="`https://localhost:5000/videos/video/${vidName}`" type="video/mp4"> <track label="English" kind="captions" srcLang="en" :src="`https://localhost:5000/videos/video/${vidName}/caption`" default> </video> </div> </template>

Dodaliśmy crossOrigin="anonymous" do elementu video; w przeciwnym razie żądanie napisów zakończy się niepowodzeniem. Teraz odśwież, a zobaczysz, że napisy zostały pomyślnie dodane.

O czym należy pamiętać podczas tworzenia odpornego przesyłania strumieniowego wideo.

Tworząc aplikacje do przesyłania strumieniowego, takie jak Twitch, Hulu czy Netflix, należy wziąć pod uwagę kilka rzeczy:

  • Potok przetwarzania danych wideo
    Może to stanowić wyzwanie techniczne, ponieważ do obsługi milionów filmów użytkownikom potrzebne są serwery o wysokiej wydajności. Za wszelką cenę należy unikać dużych opóźnień lub przestojów.
  • Buforowanie
    Przy budowie tego typu aplikacji należy wykorzystać mechanizmy buforowania np. Cassandra, Amazon S3, AWS SimpleDB.
  • Geografia użytkowników
    Biorąc pod uwagę geografię użytkowników, należy wziąć pod uwagę dystrybucję.

Wniosek

W tym samouczku zobaczyliśmy, jak utworzyć serwer w Node.js, który przesyła strumieniowo filmy, generuje podpisy do tych filmów i udostępnia metadane filmów. Widzieliśmy również, jak używać Nuxt.js na interfejsie użytkownika do korzystania z punktów końcowych i danych generowanych przez serwer.

W przeciwieństwie do innych frameworków, budowanie aplikacji za pomocą Nuxt.js i Express.js jest dość łatwe i szybkie. Fajną częścią Nuxt.js jest sposób, w jaki zarządza on Twoimi trasami i umożliwia lepszą strukturę aplikacji.

  • Więcej informacji o Nuxt.js znajdziesz tutaj.
  • Możesz pobrać kod źródłowy na Github.

Zasoby

  • „Dodawanie podpisów i napisów do wideo HTML5”, MDN Web Docs
  • „Zrozumienie podpisów i napisów”, Screenfont.ca