Creazione di un'app per lo streaming video con Nuxt.js, Node ed Express

Pubblicato: 2022-03-10
Riepilogo rapido ↬ In questo articolo creeremo un'app di streaming video utilizzando Nuxt.js e Node.js. In particolare, creeremo un'app Node.js lato server che gestirà il recupero e lo streaming di video, la generazione di miniature per i tuoi video e la pubblicazione di sottotitoli e sottotitoli.

I 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
Altro dopo il salto! Continua a leggere sotto ↓

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 nella Home page del nostro frontend.
  • /video/:id/data
    Restituisce i metadati per un singolo video. Utilizzato dalla pagina Player nel nostro frontend.
  • /video/:id
    Streaming di un video con un determinato ID. Utilizzato dalla pagina Player .

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:

Risultato finale dell'app di streaming video Nuxt
Lo streaming video viene avviato quando l'utente fa clic sulla miniatura. (Grande anteprima)

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