Erstellen einer Video-Streaming-App mit Nuxt.js, Node und Express

Veröffentlicht: 2022-03-10
Kurze Zusammenfassung ↬ In diesem Artikel erstellen wir eine Video-Streaming-App mit Nuxt.js und Node.js. Insbesondere werden wir eine serverseitige Node.js-App erstellen, die das Abrufen und Streamen von Videos, das Generieren von Miniaturansichten für Ihre Videos und das Bereitstellen von Bildunterschriften und Untertiteln übernimmt.

Videos funktionieren mit Streams. Das bedeutet, dass anstatt das gesamte Video auf einmal zu senden, ein Video als eine Reihe kleinerer Teile gesendet wird, die das vollständige Video bilden. Dies erklärt, warum Videos beim Ansehen eines Videos auf langsamem Breitband puffern, weil es nur die empfangenen Chunks abspielt und versucht, mehr zu laden.

Dieser Artikel richtet sich an Entwickler, die bereit sind, eine neue Technologie zu erlernen, indem sie ein tatsächliches Projekt erstellen: eine Video-Streaming-App mit Node.js als Backend und Nuxt.js als Client.

  • Node.js ist eine Laufzeitumgebung, die zum Erstellen schneller und skalierbarer Anwendungen verwendet wird. Wir werden es verwenden, um Videos abzurufen und zu streamen, Miniaturansichten für Videos zu generieren und Bildunterschriften und Untertitel für Videos bereitzustellen.
  • Nuxt.js ist ein Vue.js-Framework, das uns hilft, servergerenderte Vue.js-Anwendungen einfach zu erstellen. Wir werden unsere API für die Videos verwenden und diese Anwendung wird zwei Ansichten haben: eine Liste der verfügbaren Videos und eine Player-Ansicht für jedes Video.

Voraussetzungen

  • Ein Verständnis von HTML, CSS, JavaScript, Node/Express und Vue.
  • Ein Texteditor (z. B. VS Code).
  • Ein Webbrowser (z. B. Chrome, Firefox).
  • FFmpeg auf Ihrer Workstation installiert.
  • Node.js. nvm.
  • Den Quellcode erhalten Sie auf GitHub.

Einrichten unserer Anwendung

In dieser Anwendung werden wir die Routen erstellen, um Anfragen vom Frontend zu stellen:

  • videos route, um eine Liste der Videos und ihrer Daten zu erhalten.
  • eine Route, um nur ein Video aus unserer Videoliste abzurufen.
  • streaming Route, um die Videos zu streamen.
  • captions , um den Videos, die wir streamen, Untertitel hinzuzufügen.

Nachdem unsere Routen erstellt wurden, werden wir unser Nuxt Frontend rüsten, wo wir die Home und die dynamische player erstellen werden. Dann fordern wir unsere videos an, die Startseite mit den Videodaten zu füllen, eine weitere Anforderung, die Videos auf unserer player zu streamen, und schließlich eine Anforderung, die von den Videos zu verwendenden Untertiteldateien bereitzustellen.

Um unsere Anwendung einzurichten, erstellen wir unser Projektverzeichnis,

 mkdir streaming-app
Mehr nach dem Sprung! Lesen Sie unten weiter ↓

Einrichten unseres Servers

In unserem streaming-app Verzeichnis erstellen wir einen Ordner namens backend .

 cd streaming-app mkdir backend

In unserem Backend-Ordner initialisieren wir eine package.json -Datei, um Informationen über unser Serverprojekt zu speichern.

 cd backend npm init -y

Wir müssen die folgenden Pakete installieren, um unsere App zu erstellen.

  • nodemon startet unseren Server automatisch neu, wenn wir Änderungen vornehmen.
  • express gibt uns eine schöne Schnittstelle, um Routen zu handhaben.
  • cors ermöglicht es uns, Cross-Origin-Anfragen zu stellen, da unser Client und Server auf verschiedenen Ports laufen.

In unserem Backend-Verzeichnis erstellen wir einen Ordner assets , um unsere Videos zum Streamen zu speichern.

 mkdir assets

Kopieren Sie eine .mp4 -Datei in den Assets-Ordner und nennen Sie sie video1 . Sie können kurze .mp4 Beispielvideos verwenden, die auf Github Repo zu finden sind.

Erstellen Sie eine app.js -Datei und fügen Sie die erforderlichen Pakete für unsere App hinzu.

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

Das fs -Modul wird zum einfachen Lesen und Schreiben in Dateien auf unserem Server verwendet, während das path -Modul eine Möglichkeit bietet, mit Verzeichnissen und Dateipfaden zu arbeiten.

Jetzt erstellen wir eine ./video . Auf Anfrage sendet es eine Videodatei an den Client zurück.

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

Diese Route liefert die Videodatei video1.mp4 , wenn sie angefordert wird. Wir hören dann unseren Server auf Port 3000 ab.

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

In der Datei package.json wird ein Skript hinzugefügt, um unseren Server mit nodemon zu starten.

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

Führen Sie dann auf Ihrem Terminal aus:

 npm run start

Wenn Sie die Meldung Listening on port 3000! im Terminal, dann funktioniert der Server korrekt. Navigieren Sie in Ihrem Browser zu https://localhost:5000/video und Sie sollten das Video abspielen sehen.

Vom Frontend zu verarbeitende Anfragen

Nachfolgend sind die Anforderungen aufgeführt, die wir von unserem Frontend an das Backend stellen und die der Server verarbeiten muss.

  • /videos
    Gibt ein Array von Video-Mockup-Daten zurück, die verwendet werden, um die Liste der Videos auf der Home in unserem Frontend zu füllen.
  • /video/:id/data
    Gibt Metadaten für ein einzelnes Video zurück. Wird von der Player -Seite in unserem Frontend verwendet.
  • /video/:id
    Streamt ein Video mit einer bestimmten ID. Wird von der Player -Seite verwendet.

Lassen Sie uns die Routen erstellen.

Mockup-Daten für die Liste der Videos zurückgeben

Für diese Demoanwendung erstellen wir ein Array von Objekten , das die Metadaten enthält, und senden diese auf Anfrage an das Frontend. In einer realen Anwendung würden Sie wahrscheinlich die Daten aus einer Datenbank lesen, die dann verwendet werden würde, um ein solches Array zu generieren. Der Einfachheit halber werden wir das in diesem Tutorial nicht tun.

Erstellen Sie in unserem Backend-Ordner eine Datei mockdata.js und füllen Sie sie mit Metadaten für unsere Videoliste.

 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

Wir können von oben sehen, dass jedes Objekt Informationen über das Video enthält. Beachten Sie das poster -Attribut, das den Link zu einem Posterbild des Videos enthält.

Lassen Sie uns eine videos erstellen, da all unseren Anforderungen, die vom Frontend gestellt werden sollen, /videos vorangestellt wird.

Dazu erstellen wir einen routes und fügen eine Video.js -Datei für unsere /videos -Route hinzu. In dieser Datei benötigen wir express und verwenden den Express-Router, um unsere Route zu erstellen.

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

Wenn wir zur /videos -Route gehen, möchten wir unsere Liste der Videos abrufen, also fordern wir die mockData.js -Datei in unsere Video.js -Datei und stellen unsere Anfrage.

 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;

Die Route /videos ist jetzt deklariert, speichern Sie die Datei und der Server sollte automatisch neu gestartet werden. Navigieren Sie nach dem Start zu https://localhost:3000/videos und unser Array wird im JSON-Format zurückgegeben.

Daten für ein einzelnes Video zurückgeben

Wir möchten in der Lage sein, eine Anfrage für ein bestimmtes Video in unserer Videoliste zu stellen. Wir können bestimmte Videodaten in unserem Array abrufen, indem wir die id verwenden, die wir ihnen gegeben haben. Lassen Sie uns eine Anfrage stellen, immer noch in unserer Video.js -Datei.

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

Der obige Code ruft die id aus den Routenparametern ab und wandelt sie in eine Ganzzahl um. Dann senden wir das Objekt, das der id aus dem videos -Array entspricht, zurück an den Client.

Streaming der Videos

In unserer app.js -Datei haben wir eine /video Route erstellt, die dem Client ein Video liefert. Wir möchten, dass dieser Endpunkt kleinere Teile des Videos sendet, anstatt auf Anfrage eine ganze Videodatei bereitzustellen.

Wir möchten in der Lage sein, eines der drei Videos, die sich im allVideos Array befinden, dynamisch bereitzustellen und die Videos in Blöcken zu streamen, also:

Löschen Sie die /video Route aus app.js .

Wir benötigen drei Videos, kopieren Sie also die Beispielvideos aus dem Quellcode des Tutorials in das Verzeichnis assets/ Ihres server . Stellen Sie sicher, dass die Dateinamen für die videos der id im Video-Array entsprechen:

Erstellen Sie in unserer Video.js -Datei die Route zum Streamen von Videos.

 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); } });

Wenn wir in unserem Browser zu https://localhost:5000/videos/video/outside-the-wire navigieren, können wir das Videostreaming sehen.

So funktioniert die Streaming-Video-Route

In unserer Stream-Video-Route ist ziemlich viel Code geschrieben, also schauen wir uns das Ganze Zeile für Zeile an.

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

Zuerst erhalten wir aus unserer Anfrage die id von der Route mit req.params.id und verwenden sie, um den videoPath zum Video zu generieren. Wir lesen dann die fileSize mit dem von uns importierten Dateisystem fs . Bei Videos sendet der Browser eines Benutzers einen range in der Anfrage. Dadurch weiß der Server, welcher Teil des Videos an den Client zurückgesendet werden soll.

Einige Browser senden einen Bereich in der ersten Anfrage, andere nicht. Für diejenigen, die dies nicht tun, oder wenn der Browser aus irgendeinem anderen Grund keinen Bereich sendet, behandeln wir dies im else -Block. Dieser Code ruft die Dateigröße ab und sendet die ersten paar Teile des Videos:

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

Wir behandeln nachfolgende Anfragen einschließlich des Bereichs in einem if -Block.

 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); }

Dieser obige Code erstellt einen Lesestrom mit den start und end des Bereichs. Legen Sie die Content-Length der Antwortheader auf die Blockgröße fest, die aus den start und end berechnet wird. Wir verwenden auch den HTTP-Code 206, der anzeigt, dass die Antwort Teilinhalte enthält. Das bedeutet, dass der Browser so lange Anfragen stellt, bis er alle Teile des Videos abgerufen hat.

Was passiert bei instabilen Verbindungen

Wenn der Benutzer eine langsame Verbindung hat, signalisiert der Netzwerkstream dies, indem er anfordert, dass die E/A-Quelle pausiert, bis der Client für weitere Daten bereit ist. Dies wird als Gegendruck bezeichnet . Wir können dieses Beispiel noch einen Schritt weiterführen und sehen, wie einfach es ist, den Stream zu erweitern. Wir können auch ganz einfach Komprimierung hinzufügen!

 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});

Wir können oben sehen, dass ein ReadStream erstellt wird und das Video Chunk für Chunk bereitstellt.

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

Der Anforderungsheader enthält den Content-Range , der sich am Anfang und am Ende ändert, um den nächsten Videoblock zum Streamen an das Frontend zu erhalten. Die content-length ist der gesendete Videoblock. Wir geben auch den Typ des Inhalts an, den wir streamen, mp4 . Der Schreibkopf von 206 ist so eingestellt, dass er nur mit neu erstellten Streams antwortet.

Erstellen einer Untertiteldatei für unsere Videos

So sieht eine .vtt Untertiteldatei aus.

 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.

Untertiteldateien enthalten Text für das, was in einem Video gesagt wird. Es enthält auch Zeitcodes dafür, wann jede Textzeile angezeigt werden soll. Wir möchten, dass unsere Videos Untertitel haben, und wir werden für dieses Tutorial keine eigene Untertiteldatei erstellen, also können Sie zum Untertitelordner im assets -Verzeichnis im Repo gehen und die Untertitel herunterladen.

Lassen Sie uns eine neue Route erstellen, die die Untertitelanforderung verarbeitet:

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

Aufbau unseres Frontends

Um mit dem visuellen Teil unseres Systems zu beginnen, müssten wir unser Frontend-Gerüst aufbauen.

Hinweis : Sie benötigen vue-cli, um unsere App zu erstellen. Wenn Sie es nicht auf Ihrem Computer installiert haben, können Sie npm install -g @vue/cli ausführen, um es zu installieren.

Installation

Lassen Sie uns im Stammverzeichnis unseres Projekts unseren Front-End-Ordner erstellen:

 mkdir frontend cd frontend

und darin initialisieren wir unsere package.json -Datei, kopieren und fügen Folgendes ein:

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

dann installiere nuxt :

 npm add nuxt

und führen Sie den folgenden Befehl aus, um die Nuxt.js-App auszuführen:

 npm run dev

Unsere Nuxt-Dateistruktur

Nachdem wir Nuxt installiert haben, können wir mit dem Layout unseres Frontends beginnen.

Zuerst müssen wir einen layouts -Ordner im Stammverzeichnis unserer App erstellen. Dieser Ordner definiert das Layout der App, unabhängig davon, zu welcher Seite wir navigieren. Dinge wie unsere Navigationsleiste und Fußzeile finden Sie hier. Im Frontend-Ordner erstellen wir default.vue für unser Standardlayout, wenn wir unsere Frontend-App starten.

 mkdir layouts cd layouts touch default.vue

Dann einen components , um alle unsere Komponenten zu erstellen. Wir benötigen nur zwei Komponenten, NavBar und video . Also in unserem Root-Ordner des Frontends machen wir:

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

Schließlich ein Seitenordner, in dem alle unsere Seiten wie home und about erstellt werden können. Die beiden Seiten, die wir in dieser App benötigen, sind die home , auf der alle unsere Videos und Videoinformationen angezeigt werden, und eine dynamische Player-Seite, die zu dem Video führt, auf das wir klicken.

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

Unser Frontend-Verzeichnis sieht nun so aus:

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

Navbar-Komponente

Unsere NavBar.vue sieht so aus:

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

Die NavBar hat ein h1 -Tag, das Streaming App mit etwas Styling anzeigt.

Lassen Sie uns die NavBar in unser default.vue -Layout importieren.

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

Das default.vue -Layout enthält jetzt unsere NavBar Komponente und das <nuxt /> Tag, nachdem es angibt, wo jede von uns erstellte Seite angezeigt wird.

Lassen Sie uns in unserer index.vue (unserer Homepage) eine Anfrage an https://localhost:5000/videos stellen, um alle Videos von unserem Server abzurufen. Übergeben der Daten als Prop an unsere video.vue Komponente, die wir später erstellen werden. Aber im Moment haben wir es bereits importiert.

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

Videokomponente

Unten deklarieren wir zuerst unsere Stütze. Da die Videodaten nun in der Komponente verfügbar sind, iterieren wir mit v-for von Vue über alle empfangenen Daten und zeigen für jede einzelne die Informationen an. Wir können die v-for Direktive verwenden, um die Daten zu durchlaufen und als Liste anzuzeigen. Einige grundlegende Stile wurden ebenfalls hinzugefügt.

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

Wir stellen auch fest, dass der NuxtLink eine dynamische Route hat, d. h. das Routing zur /player/video.id .

Die Funktionalität, die wir wollen, ist, wenn ein Benutzer auf eines der Videos klickt, beginnt es zu streamen. Um dies zu erreichen, nutzen wir die dynamische Natur der _name.vue Route.

Darin erstellen wir einen Videoplayer und setzen die Quelle auf unseren Endpunkt zum Streamen des Videos, aber wir hängen dynamisch an, welches Video an unserem Endpunkt abgespielt werden soll, mit Hilfe von this.$route.params.name , das erfasst, welchen Parameter der Link erhalten hat .

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

Wenn wir auf eines der Videos klicken, erhalten wir:

Endergebnis der Nuxt-Video-Streaming-App
Das Video-Streaming wird gestartet, wenn der Benutzer auf das Miniaturbild klickt. (Große Vorschau)

Hinzufügen unserer Untertiteldatei

Um unsere Track-Datei hinzuzufügen, stellen wir sicher, dass alle .vtt Dateien im Untertitelordner denselben Namen wie unsere id haben. Aktualisieren Sie unser Videoelement mit dem Track und fordern Sie die Untertitel an.

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

Wir haben dem crossOrigin="anonymous" hinzugefügt; Andernfalls schlägt die Anforderung von Untertiteln fehl. Aktualisieren Sie jetzt und Sie werden sehen, dass Untertitel erfolgreich hinzugefügt wurden.

Was Sie beim Erstellen von ausfallsicherem Video-Streaming beachten sollten.

Beim Erstellen von Streaming-Anwendungen wie Twitch, Hulu oder Netflix gibt es eine Reihe von Dingen, die berücksichtigt werden müssen:

  • Pipeline zur Verarbeitung von Videodaten
    Dies kann eine technische Herausforderung sein, da leistungsstarke Server benötigt werden, um Millionen von Videos für Benutzer bereitzustellen. Hohe Latenzen oder Ausfallzeiten sollten unbedingt vermieden werden.
  • Caching
    Caching-Mechanismen sollten beim Erstellen dieser Art von Anwendungsbeispiel Cassandra, Amazon S3, AWS SimpleDB verwendet werden.
  • Geographie der Benutzer
    Bei der Verteilung sollte die geografische Lage Ihrer Benutzer berücksichtigt werden.

Fazit

In diesem Tutorial haben wir gesehen, wie man einen Server in Node.js erstellt, der Videos streamt, Untertitel für diese Videos generiert und Metadaten der Videos bereitstellt. Wir haben auch gesehen, wie Nuxt.js am Frontend verwendet wird, um die Endpunkte und die vom Server generierten Daten zu nutzen.

Im Gegensatz zu anderen Frameworks ist das Erstellen einer Anwendung mit Nuxt.js und Express.js recht einfach und schnell. Das Coole an Nuxt.js ist die Art und Weise, wie es Ihre Routen verwaltet und Sie Ihre Apps besser strukturieren lässt.

  • Weitere Informationen zu Nuxt.js erhalten Sie hier.
  • Den Quellcode erhalten Sie auf Github.

Ressourcen

  • „Hinzufügen von Beschriftungen und Untertiteln zu HTML5-Videos“, MDN Web Docs
  • „Bildunterschriften und Untertitel verstehen“, Screenfont.ca