Erstellen einer Video-Streaming-App mit Nuxt.js, Node und Express
Veröffentlicht: 2022-03-10Videos 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
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 derHome
in unserem Frontend zu füllen. -
/video/:id/data
Gibt Metadaten für ein einzelnes Video zurück. Wird von derPlayer
-Seite in unserem Frontend verwendet. -
/video/:id
Streamt ein Video mit einer bestimmten ID. Wird von derPlayer
-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:
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