Creazione di un'app per lo streaming video con Nuxt.js, Node ed Express
Pubblicato: 2022-03-10I video funzionano con gli stream. Ciò significa che invece di inviare l'intero video in una volta, un video viene inviato come un insieme di blocchi più piccoli che compongono il video completo. Questo spiega perché i video vengono caricati durante la visione di un video su una banda larga lenta perché riproduce solo i blocchi che ha ricevuto e cerca di caricarne di più.
Questo articolo è rivolto agli sviluppatori che desiderano apprendere una nuova tecnologia costruendo un progetto reale: un'app di streaming video con Node.js come back-end e Nuxt.js come client.
- Node.js è un runtime utilizzato per la creazione di applicazioni veloci e scalabili. Lo useremo per gestire il recupero e lo streaming di video, la generazione di miniature per i video e la pubblicazione di didascalie e sottotitoli per i video.
- Nuxt.js è un framework Vue.js che ci aiuta a creare facilmente applicazioni Vue.js con rendering del server. Useremo la nostra API per i video e questa applicazione avrà due visualizzazioni: un elenco di video disponibili e una visualizzazione giocatore per ogni video.
Prerequisiti
- Una comprensione di HTML, CSS, JavaScript, Node/Express e Vue.
- Un editor di testo (ad esempio VS Code).
- Un browser web (es. Chrome, Firefox).
- FFmpeg installato sulla tua workstation.
- Node.js. nvm.
- Puoi ottenere il codice sorgente su GitHub.
Configurazione della nostra applicazione
In questa applicazione, costruiremo i percorsi per effettuare richieste dal frontend:
-
videos
route per ottenere un elenco di video e dei relativi dati. - un percorso per recuperare un solo video dal nostro elenco di video.
- percorso di
streaming
per lo streaming dei video. -
captions
per aggiungere didascalie ai video che stiamo trasmettendo in streaming.
Dopo che i nostri percorsi sono stati creati, impalcheremo il nostro frontend Nuxt
, dove creeremo la Home
e la pagina dinamica del player
. Quindi richiediamo il percorso dei nostri videos
per riempire la home page con i dati del video, un'altra richiesta per lo streaming dei video sulla nostra pagina del player
e infine una richiesta per servire i file didascalia che devono essere utilizzati dai video.
Per configurare la nostra applicazione, creiamo la nostra directory di progetto,
mkdir streaming-app
Configurazione del nostro server
Nella nostra directory streaming-app
, creiamo una cartella denominata backend
.
cd streaming-app mkdir backend
Nella nostra cartella back-end, inizializziamo un file package.json
per memorizzare informazioni sul nostro progetto server.
cd backend npm init -y
dobbiamo installare i seguenti pacchetti per creare la nostra app.
-
nodemon
riavvia automaticamente il nostro server quando apportiamo modifiche. -
express
ci offre una bella interfaccia per gestire i percorsi. -
cors
ci consentirà di effettuare richieste multiorigine poiché il nostro client e server funzioneranno su porte diverse.
Nella nostra directory back-end, creiamo una cartella di assets
per contenere i nostri video per lo streaming.
mkdir assets
Copia un file .mp4
nella cartella delle risorse e video1
. Puoi utilizzare brevi video di esempio .mp4
che possono essere trovati su Github Repo.
Crea un file app.js
e aggiungi i pacchetti necessari per la nostra app.
const express = require('express'); const fs = require('fs'); const cors = require('cors'); const path = require('path'); const app = express(); app.use(cors())
Il modulo fs
viene utilizzato per leggere e scrivere facilmente nei file sul nostro server, mentre il modulo path
fornisce un modo per lavorare con directory e percorsi di file.
Ora creiamo un percorso ./video
. Quando richiesto, invierà un file video al cliente.
// add after 'const app = express();' app.get('/video', (req, res) => { res.sendFile('assets/video1.mp4', { root: __dirname }); });
Questo percorso serve il file video video1.mp4
quando richiesto. Quindi ascoltiamo il nostro server alla porta 3000
.
// add to end of app.js file app.listen(5000, () => { console.log('Listening on port 5000!') });
Uno script viene aggiunto nel file package.json
per avviare il nostro server utilizzando nodemon.
"scripts": { "start": "nodemon app.js" },
Quindi sul tuo terminale esegui:
npm run start
Se vedi il messaggio In Listening on port 3000!
nel terminale, il server funziona correttamente. Passa a https://localhost:5000/video nel tuo browser e dovresti vedere il video in riproduzione.
Richieste di essere gestite dal frontend
Di seguito sono riportate le richieste che faremo al back-end dal nostro front-end che abbiamo bisogno che il server gestisca.
-
/videos
Restituisce un array di dati di mockup video che verranno utilizzati per popolare l'elenco dei video nellaHome
page del nostro frontend. -
/video/:id/data
Restituisce i metadati per un singolo video. Utilizzato dalla paginaPlayer
nel nostro frontend. -
/video/:id
Streaming di un video con un determinato ID. Utilizzato dalla paginaPlayer
.
Creiamo i percorsi.
Restituisci i dati del mockup per l'elenco dei video
Per questa applicazione demo, creeremo un array di oggetti che conterranno i metadati e li invieranno al frontend quando richiesto. In un'applicazione reale, probabilmente leggeresti i dati da un database, che verrebbe quindi utilizzato per generare un array come questo. Per semplicità, non lo faremo in questo tutorial.
Nella nostra cartella back-end crea un file mockdata.js
e popolalo con i metadati per il nostro elenco di video.
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
Possiamo vedere dall'alto, ogni oggetto contiene informazioni sul video. Notare l'attributo poster
che contiene il collegamento a un'immagine poster del video.
Creiamo un percorso videos
poiché tutte le nostre richieste da fare dal frontend sono precedute da /videos
.
Per fare ciò, creiamo una cartella routes
e aggiungiamo un file Video.js
per il nostro percorso /videos
. In questo file, avremo bisogno di express
e useremo il router express per creare il nostro percorso.
const express = require('express') const router = express.Router()
Quando andiamo al percorso /videos
, vogliamo ottenere il nostro elenco di video, quindi richiediamo il file mockData.js
nel nostro file Video.js
e facciamo la nostra richiesta.
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;
Il percorso /videos
è ora dichiarato, salva il file e dovrebbe riavviare automaticamente il server. Una volta avviato, vai a https://localhost:3000/videos e il nostro array viene restituito in formato JSON.
Restituisci i dati per un singolo video
Vogliamo essere in grado di fare una richiesta per un video particolare nel nostro elenco di video. Possiamo recuperare un particolare dato video nel nostro array usando l' id
che gli abbiamo fornito. Facciamo una richiesta, sempre nel nostro file 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]) })
Il codice sopra ottiene l' id
dai parametri del percorso e lo converte in un numero intero. Quindi inviamo al client l'oggetto che corrisponde id
dall'array videos
.
Streaming dei video
Nel nostro file app.js
, abbiamo creato un percorso /video
che serve un video al client. Vogliamo che questo endpoint invii porzioni più piccole del video, invece di servire un intero file video su richiesta.
Vogliamo essere in grado di servire dinamicamente uno dei tre video che si trovano nell'array allVideos
e di riprodurre in streaming i video in blocchi, quindi:
Elimina il percorso /video
da app.js
.
Abbiamo bisogno di tre video, quindi copia i video di esempio dal codice sorgente del tutorial nella directory assets/
del tuo progetto server
. Assicurati che i nomi dei file per i video corrispondano id
nell'array videos
:
Nel nostro file Video.js
, crea il percorso per lo streaming di video.
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); } });
Se andiamo a https://localhost:5000/videos/video/outside-the-wire nel nostro browser, possiamo vedere lo streaming video.
Come funziona il percorso video in streaming
C'è un bel po' di codice scritto nel nostro percorso di streaming video, quindi diamo un'occhiata riga per riga.
const videoPath = `assets/${req.params.id}.mp4`; const videoStat = fs.statSync(videoPath); const fileSize = videoStat.size; const videoRange = req.headers.range;
Innanzitutto, dalla nostra richiesta, otteniamo l' id
dal percorso utilizzando req.params.id
e lo usiamo per generare il videoPath
al video. Quindi leggiamo il fileSize
utilizzando il file system fs
che abbiamo importato. Per i video, il browser di un utente invierà un parametro di range
nella richiesta. Ciò consente al server di sapere quale parte del video inviare al client.
Alcuni browser inviano un intervallo nella richiesta iniziale, ma altri no. Per coloro che non lo fanno, o se per qualsiasi altro motivo il browser non invia un intervallo, lo gestiamo nel blocco else
. Questo codice ottiene la dimensione del file e invia i primi pezzi del video:
else { const head = { 'Content-Length': fileSize, 'Content-Type': 'video/mp4', }; res.writeHead(200, head); fs.createReadStream(path).pipe(res); }
Gestiremo le richieste successive, incluso l'intervallo in un blocco 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); }
Questo codice sopra crea un flusso di lettura usando i valori di start
e end
dell'intervallo. Impostare Content-Length
delle intestazioni di risposta sulla dimensione del blocco calcolata dai valori di start
e end
. Utilizziamo anche il codice HTTP 206, a significare che la risposta contiene contenuto parziale. Ciò significa che il browser continuerà a fare richieste fino a quando non avrà recuperato tutti i blocchi del video.
Cosa succede alle connessioni instabili
Se l'utente ha una connessione lenta, il flusso di rete lo segnalerà richiedendo che l'origine I/O si metta in pausa finché il client non è pronto per ulteriori dati. Questo è noto come contropressione . Possiamo fare un ulteriore passo avanti in questo esempio e vedere quanto sia facile estendere il flusso. Possiamo facilmente aggiungere anche la compressione!
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});
Possiamo vedere sopra che viene creato un ReadStream
e serve il video pezzo per pezzo.
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);
L'intestazione della richiesta contiene Content-Range
, che è l'inizio e la fine che cambiano per ottenere il successivo blocco di video da trasmettere in streaming al frontend, la content-length
è il blocco di video inviato. Specifichiamo anche il tipo di contenuto che stiamo trasmettendo in streaming che è mp4
. La testina di scrittura di 206 è impostata per rispondere solo con flussi di nuova creazione.
Creazione di un file didascalia per i nostri video
Ecco come appare un file didascalia .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.
I file dei sottotitoli contengono testo per ciò che viene detto in un video. Contiene anche codici temporali per quando deve essere visualizzata ogni riga di testo. Vogliamo che i nostri video abbiano didascalie e non creeremo il nostro file di sottotitoli per questo tutorial, quindi puoi andare alla cartella didascalie nella directory delle assets
nel repository e scaricare i sottotitoli.
Creiamo un nuovo percorso che gestirà la richiesta di didascalia:
router.get('/video/:id/caption', (req, res) => res.sendFile(`assets/captions/${req.params.id}.vtt`, { root: __dirname }));
Costruire il nostro frontend
Per iniziare con la parte visiva del nostro sistema, dovremmo costruire il nostro scaffold front-end.
Nota : per creare la nostra app è necessario vue-cli. Se non lo hai installato sul tuo computer, puoi eseguire npm install -g @vue/cli
per installarlo.
Installazione
Alla radice del nostro progetto, creiamo la nostra cartella front-end:
mkdir frontend cd frontend
e in esso inizializziamo il nostro file package.json
, copiamo e incolliamo quanto segue:
{ "name": "my-app", "scripts": { "dev": "nuxt", "build": "nuxt build", "generate": "nuxt generate", "start": "nuxt start" } }
quindi installa nuxt
:
npm add nuxt
ed eseguire il comando seguente per eseguire l'app Nuxt.js:
npm run dev
La nostra struttura di file Nuxt
Ora che abbiamo installato Nuxt, possiamo iniziare a progettare il nostro frontend.
Innanzitutto, dobbiamo creare una cartella di layouts
nella radice della nostra app. Questa cartella definisce il layout dell'app, indipendentemente dalla pagina in cui navighiamo. Cose come la nostra barra di navigazione e il piè di pagina si trovano qui. Nella cartella frontend, creiamo default.vue
per il nostro layout predefinito quando avviamo la nostra app frontend.
mkdir layouts cd layouts touch default.vue
Quindi una cartella dei components
per creare tutti i nostri componenti. Avremo bisogno solo di due componenti, NavBar
e componente video
. Quindi nella nostra cartella principale di frontend noi:
mkdir components cd components touch NavBar.vue touch Video.vue
Infine, una cartella delle pagine in cui è possibile creare tutte le nostre pagine come home
e about
. Le due pagine di cui abbiamo bisogno in questa app sono la home
page che mostra tutti i nostri video e le informazioni sui video e una pagina del lettore dinamico che indirizza al video su cui facciamo clic.
mkdir pages cd pages touch index.vue mkdir player cd player touch _name.vue
La nostra directory frontend ora si presenta così:
|-frontend |-components |-NavBar.vue |-Video.vue |-layouts |-default.vue |-pages |-index.vue |-player |-_name.vue |-package.json |-yarn.lock
Componente della barra di navigazione
Il nostro NavBar.vue
si presenta così:
<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>
La NavBar
ha un tag h1
che mostra l' app di streaming , con un po' di stile.
Importiamo la NavBar
nel nostro layout default.vue
.
// default.vue <template> <div> <NavBar /> <nuxt /> </div> </template> <script> import NavBar from "@/components/NavBar.vue" export default { components: { NavBar, } } </script>
Il layout default.vue
ora contiene il nostro componente NavBar
e il <nuxt />
dopo che indica dove verrà visualizzata qualsiasi pagina che creiamo.
Nel nostro index.vue
(che è la nostra homepage), facciamo una richiesta a https://localhost:5000/videos
per ottenere tutti i video dal nostro server. Passando i dati come supporto al nostro componente video.vue
che creeremo in seguito. Ma per ora, l'abbiamo già importato.
<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>
Componente video
Di seguito, prima dichiariamo il nostro prop. Poiché i dati video sono ora disponibili nel componente, utilizzando v-for
di Vue ripetiamo su tutti i dati ricevuti e per ciascuno visualizziamo le informazioni. Possiamo usare la direttiva v-for
per scorrere i dati e visualizzarli come un elenco. Sono stati aggiunti anche alcuni stili di base.
<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>
Notiamo anche che il NuxtLink
ha un percorso dinamico, ovvero il routing al /player/video.id
.
La funzionalità che desideriamo è che quando un utente fa clic su uno qualsiasi dei video, inizia lo streaming. Per raggiungere questo obiettivo, utilizziamo la natura dinamica del percorso _name.vue
.
In esso, creiamo un video player e impostiamo la sorgente sul nostro endpoint per lo streaming del video, ma aggiungiamo dinamicamente quale video riprodurre al nostro endpoint con l'aiuto di this.$route.params.name
che cattura il parametro ricevuto dal collegamento .
<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>
Quando clicchiamo su uno qualsiasi dei video otteniamo:
Aggiunta del nostro file didascalia
Per aggiungere il nostro file di traccia, ci assicuriamo che tutti i file .vtt
nella cartella delle didascalie abbiano lo stesso nome del nostro id
. Aggiorna il nostro elemento video con la traccia, facendo una richiesta per le didascalie.
<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>
Abbiamo aggiunto crossOrigin="anonymous"
all'elemento video; in caso contrario, la richiesta di didascalie avrà esito negativo. Ora aggiorna e vedrai che i sottotitoli sono stati aggiunti correttamente.
Cosa tenere a mente quando si crea uno streaming video resiliente.
Quando si creano applicazioni di streaming come Twitch, Hulu o Netflix, ci sono una serie di cose che vengono prese in considerazione:
- Pipeline di elaborazione dati video
Questa può essere una sfida tecnica poiché sono necessari server ad alte prestazioni per fornire milioni di video agli utenti. L'elevata latenza o i tempi di inattività dovrebbero essere evitati a tutti i costi. - Memorizzazione nella cache
I meccanismi di memorizzazione nella cache devono essere utilizzati durante la creazione di questo tipo di esempio di applicazione Cassandra, Amazon S3, AWS SimpleDB. - Geografia degli utenti
Considerando la geografia dei tuoi utenti dovrebbe essere pensato per la distribuzione.
Conclusione
In questo tutorial, abbiamo visto come creare un server in Node.js che trasmetta video in streaming, generi didascalie per quei video e serva i metadati dei video. Abbiamo anche visto come utilizzare Nuxt.js sul frontend per consumare gli endpoint ei dati generati dal server.
A differenza di altri framework, la creazione di un'applicazione con Nuxt.js ed Express.js è abbastanza facile e veloce. La parte interessante di Nuxt.js è il modo in cui gestisce i tuoi percorsi e ti fa strutturare meglio le tue app.
- Puoi ottenere maggiori informazioni su Nuxt.js qui.
- Puoi ottenere il codice sorgente su Github.
Risorse
- "Aggiunta di didascalie e sottotitoli a video HTML5", MDN Web Docs
- "Capire didascalie e sottotitoli", Screenfont.ca