Crearea unei aplicații de streaming video cu Nuxt.js, Node și Express

Publicat: 2022-03-10
Rezumat rapid ↬ În acest articol, vom construi o aplicație de streaming video folosind Nuxt.js și Node.js. Mai exact, vom construi o aplicație Node.js pe server, care se va ocupa de preluarea și transmiterea în flux a videoclipurilor, de generarea de miniaturi pentru videoclipurile dvs. și de difuzarea subtitrărilor.

Videoclipurile funcționează cu fluxuri. Aceasta înseamnă că, în loc să trimiți întregul videoclip simultan, un videoclip este trimis ca un set de bucăți mai mici care alcătuiesc întregul videoclip. Acest lucru explică de ce videoclipurile sunt în tampon atunci când vizionați un videoclip în bandă largă lentă, deoarece redă doar bucățile pe care le-a primit și încearcă să încarce mai multe.

Acest articol este pentru dezvoltatorii care doresc să învețe o nouă tehnologie prin construirea unui proiect real: o aplicație de streaming video cu Node.js ca backend și Nuxt.js ca client.

  • Node.js este un runtime folosit pentru construirea de aplicații rapide și scalabile. Îl vom folosi pentru a prelua și transmite videoclipuri, pentru a genera miniaturi pentru videoclipuri și pentru a difuza subtitrări pentru videoclipuri.
  • Nuxt.js este un cadru Vue.js care ne ajută să construim cu ușurință aplicații Vue.js redate pe server. Vom consuma API-ul nostru pentru videoclipuri și această aplicație va avea două vizualizări: o listă a videoclipurilor disponibile și o vizualizare a playerului pentru fiecare videoclip.

Cerințe preliminare

  • O înțelegere a HTML, CSS, JavaScript, Node/Express și Vue.
  • Un editor de text (de ex. VS Code).
  • Un browser web (de exemplu, Chrome, Firefox).
  • FFmpeg instalat pe stația dvs. de lucru.
  • Node.js. nvm.
  • Puteți obține codul sursă pe GitHub.

Configurarea aplicației noastre

În această aplicație, vom construi rutele pentru a face solicitări de la frontend:

  • traseul videos pentru a obține o listă de videoclipuri și datele acestora.
  • o rută pentru a prelua un singur videoclip din lista noastră de videoclipuri.
  • traseu de streaming pentru a transmite videoclipuri.
  • ruta captions pentru a adăuga subtitrări la videoclipurile pe care le transmitem în flux.

După ce rutele noastre au fost create, vom crea interfața Nuxt , unde vom crea pagina Home și player dinamic. Apoi solicităm traseul videos noastre să umple pagina de pornire cu datele video, o altă solicitare de a difuza videoclipurile pe pagina noastră de player și, în final, o solicitare de a difuza fișierele de subtitrări pentru a fi utilizate de videoclipuri.

Pentru a configura aplicația noastră, creăm directorul nostru de proiecte,

 mkdir streaming-app
Mai multe după săritură! Continuați să citiți mai jos ↓

Configurarea serverului nostru

În directorul nostru streaming-app , creăm un folder numit backend .

 cd streaming-app mkdir backend

În dosarul nostru backend, inițializam un fișier package.json pentru a stoca informații despre proiectul serverului nostru.

 cd backend npm init -y

trebuie să instalăm următoarele pachete pentru a construi aplicația noastră.

  • nodemon repornește automat serverul nostru când facem modificări.
  • express ne oferă o interfață plăcută pentru a gestiona rutele.
  • cors ne va permite să facem cereri de origine încrucișată, deoarece clientul și serverul nostru vor rula pe porturi diferite.

În directorul nostru de backend, creăm un folder cu assets pentru a păstra videoclipurile noastre pentru streaming.

 mkdir assets

Copiați un fișier .mp4 în folderul assets și numiți-l video1 . Puteți utiliza videoclipuri scurte .mp4 care pot fi găsite pe Github Repo.

Creați un fișier app.js și adăugați pachetele necesare pentru aplicația noastră.

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

Modulul fs este folosit pentru a citi și scrie cu ușurință în fișiere de pe serverul nostru, în timp ce modulul path oferă o modalitate de a lucra cu directoare și căi de fișiere.

Acum creăm un traseu ./video . Când este solicitat, va trimite un fișier video înapoi clientului.

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

Această rută servește fișierul video video1.mp4 atunci când este solicitat. Apoi ascultăm serverul nostru de la portul 3000 .

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

Un script este adăugat în fișierul package.json pentru a porni serverul nostru folosind nodemon.

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

Apoi, pe terminal, rulați:

 npm run start

Dacă vedeți mesajul Listening on port 3000! în terminal, atunci serverul funcționează corect. Navigați la https://localhost:5000/video în browser și ar trebui să vedeți redarea videoclipului.

Cererile care urmează să fie tratate de către front-end

Mai jos sunt solicitările pe care le vom face backend-ului de la frontend-ul nostru pe care trebuie să le gestionăm serverul.

  • /videos
    Returnează o serie de date de machetă video care vor fi folosite pentru a completa lista de videoclipuri de pe pagina de Home din frontend-ul nostru.
  • /video/:id/data
    Returnează metadate pentru un singur videoclip. Folosit de pagina Player din frontend-ul nostru.
  • /video/:id
    Redă în flux un videoclip cu un anumit ID. Folosit de pagina Player .

Să creăm rutele.

Returnați datele modelului pentru lista de videoclipuri

Pentru această aplicație demonstrativă, vom crea o serie de obiecte care vor deține metadatele și le vom trimite către front-end atunci când sunt solicitate. Într-o aplicație reală, probabil că ați citi datele dintr-o bază de date, care ar fi apoi folosite pentru a genera o matrice ca aceasta. De dragul simplității, nu vom face asta în acest tutorial.

În folderul nostru backend creați un fișier mockdata.js și completați-l cu metadate pentru lista noastră de videoclipuri.

 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

Putem vedea de sus, fiecare obiect conține informații despre videoclip. Observați atributul poster care conține linkul către o imagine poster a videoclipului.

Să creăm o rută a videos , deoarece toată cererea noastră de a fi făcută de către interfață este prefixată cu /videos .

Pentru a face acest lucru, să creăm un folder routes și să adăugăm un fișier Video.js pentru ruta noastră /videos . În acest fișier, vom solicita express și vom folosi routerul expres pentru a ne crea ruta.

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

Când mergem la ruta /videos , dorim să obținem lista noastră de videoclipuri, așa că haideți să solicităm fișierul mockData.js în fișierul nostru Video.js și să facem cererea noastră.

 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;

Ruta /videos este acum declarată, salvați fișierul și ar trebui să repornească automat serverul. Odată ce a pornit, navigați la https://localhost:3000/videos și matricea noastră este returnată în format JSON.

Datele returnate pentru un singur videoclip

Dorim să putem face o solicitare pentru un anumit videoclip din lista noastră de videoclipuri. Putem prelua anumite date video din matricea noastră folosind id -ul pe care i l-am dat. Să facem o solicitare, încă în fișierul nostru 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]) })

Codul de mai sus primește id -ul din parametrii rutei și îl convertește într-un număr întreg. Apoi trimitem obiectul care se potrivește cu id -ul din matricea de videos înapoi către client.

Redarea în flux a videoclipurilor

În fișierul nostru app.js , am creat o rută /video care oferă un videoclip clientului. Dorim ca acest punct final să trimită bucăți mai mici din videoclip, în loc să difuzeze un întreg fișier video la cerere.

Dorim să putem difuza în mod dinamic unul dintre cele trei videoclipuri care se află în matricea allVideos și să transmitem videoclipurile în bucăți, deci:

Ștergeți traseul /video din app.js

Avem nevoie de trei videoclipuri, așa că copiați exemplele de videoclipuri din codul sursă al tutorialului în directorul assets/ al proiectului dvs. de server . Asigurați-vă că numele fișierelor pentru videoclipuri corespund id -ului din matricea videos :

Înapoi în fișierul nostru Video.js , creați traseul pentru streaming videoclipuri.

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

Dacă navigăm la https://localhost:5000/videos/video/outside-the-wire în browserul nostru, putem vedea fluxul video.

Cum funcționează ruta de streaming video

Există un pic de cod scris în traseul nostru video în flux, așa că haideți să-l privim rând cu linie.

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

În primul rând, din solicitarea noastră, obținem id -ul de pe rută folosind req.params.id și îl folosim pentru a genera videoPath către videoclip. Apoi citim fileSize folosind sistemul de fs pe care l-am importat. Pentru videoclipuri, browserul unui utilizator va trimite un parametru range în cerere. Acest lucru permite serverului să știe ce bucată din videoclip să trimită înapoi clientului.

Unele browsere trimit un interval în cererea inițială, dar altele nu. Pentru cei care nu o fac sau dacă din orice alt motiv browserul nu trimite un interval, ne ocupăm de asta în blocul else . Acest cod primește dimensiunea fișierului și trimite primele câteva bucăți din videoclip:

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

Ne vom ocupa de cererile ulterioare, inclusiv intervalul într-un bloc 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); }

Acest cod de mai sus creează un flux de citire folosind valorile de start și de end ale intervalului. Setați Content-Length antetelor răspunsului la dimensiunea fragmentului care este calculată din valorile de start și de end . De asemenea, folosim codul HTTP 206, ceea ce înseamnă că răspunsul conține conținut parțial. Aceasta înseamnă că browserul va continua să facă cereri până când va prelua toate fragmentele din videoclip.

Ce se întâmplă cu conexiunile instabile

Dacă utilizatorul are o conexiune lentă, fluxul de rețea îl va semnala solicitând ca sursa I/O să întrerupă până când clientul este gata pentru mai multe date. Aceasta este cunoscută sub denumirea de contrapresiune . Putem să ducem acest exemplu cu un pas mai departe și să vedem cât de ușor este să extindeți fluxul. Putem adăuga cu ușurință și compresie!

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

Putem vedea mai sus că un ReadStream este creat și servește videoclipul bucată cu bucată.

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

Antetul solicitării conține Content-Range , care este modificarea începutului și a sfârșitului pentru ca următoarea bucată de videoclip să fie transmisă în flux către front-end, content-length este fragmentul de videoclip trimis. De asemenea, specificăm tipul de conținut pe care îl transmitem în flux, care este mp4 . Capul de scris al lui 206 este setat să răspundă doar cu fluxuri nou create.

Crearea unui fișier de subtitrări pentru videoclipurile noastre

Așa arată un fișier de legendă .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.

Fișierele subtitrări conțin text pentru ceea ce se spune într-un videoclip. Conține, de asemenea, coduri temporale pentru momentul în care trebuie afișată fiecare linie de text. Dorim ca videoclipurile noastre să aibă subtitrări și nu ne vom crea propriul fișier de subtitrări pentru acest tutorial, așa că puteți merge la folderul subtitrări din directorul de assets din depozit și puteți descărca subtitrările.

Să creăm o rută nouă care va gestiona solicitarea de subtitrare:

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

Construirea front-end-ului nostru

Pentru a începe cu partea vizuală a sistemului nostru, ar trebui să ne construim schela frontală.

Notă : aveți nevoie de vue-cli pentru a crea aplicația noastră. Dacă nu îl aveți instalat pe computer, puteți rula npm install -g @vue/cli pentru a-l instala.

Instalare

La rădăcina proiectului nostru, să creăm folderul nostru front-end:

 mkdir frontend cd frontend

și în el, inițializam fișierul package.json , copiam și lipim următoarele în el:

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

apoi instaleaza nuxt :

 npm add nuxt

și executați următoarea comandă pentru a rula aplicația Nuxt.js:

 npm run dev

Structura noastră de fișiere Nuxt

Acum că avem Nuxt instalat, putem începe să ne amenajăm frontend-ul.

În primul rând, trebuie să creăm un folder de layouts la rădăcina aplicației noastre. Acest folder definește aspectul aplicației, indiferent de pagina la care navigăm. Lucruri precum bara de navigare și subsolul nostru se găsesc aici. În folderul frontend, creăm default.vue pentru aspectul nostru implicit atunci când pornim aplicația noastră frontend.

 mkdir layouts cd layouts touch default.vue

Apoi un folder de components pentru a crea toate componentele noastre. Vom avea nevoie de doar două componente, NavBar și componenta video . Deci, în folderul nostru rădăcină de frontend:

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

În cele din urmă, un dosar de pagini în care pot fi create toate paginile noastre precum home și about . Cele două pagini de care avem nevoie în această aplicație sunt pagina home care afișează toate videoclipurile și informațiile noastre video și o pagină de player dinamică care direcționează către videoclipul pe care facem clic.

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

Directorul nostru frontend arată acum astfel:

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

Componenta Navbar

NavBar.vue arată astfel:

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

NavBar are o etichetă h1 care afișează aplicația de streaming , cu puțin stil.

Să importăm NavBar în aspectul nostru default.vue .

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

Aspectul default.vue conține acum componenta noastră NavBar și <nuxt /> după ce indică unde va fi afișată orice pagină pe care o creăm.

În index.vue (care este pagina noastră de pornire), să facem o solicitare către https://localhost:5000/videos pentru a obține toate videoclipurile de pe serverul nostru. Trecând datele ca o recuzită componentei noastre video.vue pe care le vom crea mai târziu. Dar deocamdată l-am importat deja.

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

Componenta video

Mai jos, declarăm mai întâi recuzita noastră. Deoarece datele video sunt acum disponibile în componentă, folosind v-for Vue, iterăm pe toate datele primite și pentru fiecare, afișăm informațiile. Putem folosi directiva v-for pentru a parcurge datele și pentru a le afișa ca o listă. Au fost adăugate și unele stiluri de bază.

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

De asemenea, observăm că NuxtLink are o rută dinamică, adică rutarea către /player/video.id .

Funcționalitatea pe care o dorim este că atunci când un utilizator dă clic pe oricare dintre videoclipuri, acesta începe transmiterea în flux. Pentru a realiza acest lucru, folosim natura dinamică a rutei _name.vue .

În acesta, creăm un player video și setăm sursa la punctul nostru final pentru transmiterea în flux a videoclipului, dar adăugăm în mod dinamic ce videoclip să redăm la punctul nostru final cu ajutorul this.$route.params.name care surprinde ce parametru a primit linkul .

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

Când facem clic pe oricare dintre videoclipuri, obținem:

Rezultatul final al aplicației de streaming video Nuxt
Streamingul video începe atunci când utilizatorul face clic pe miniatură. (Previzualizare mare)

Adăugarea fișierului nostru de subtitrări

Pentru a adăuga fișierul nostru de urmărire, ne asigurăm că toate fișierele .vtt din folderul subtitrări au același nume cu id -ul nostru . Actualizați elementul nostru video cu piesa, făcând o solicitare pentru subtitrări.

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

Am adăugat crossOrigin="anonymous" la elementul video; în caz contrar, cererea de subtitrări va eșua. Acum reîmprospătați și veți vedea că subtitrările au fost adăugate cu succes.

De ce să țineți cont atunci când construiți un streaming video rezistent.

Când construiți aplicații de streaming precum Twitch, Hulu sau Netflix, există o serie de lucruri care sunt luate în considerare:

  • Conducta de procesare a datelor video
    Aceasta poate fi o provocare tehnică, deoarece sunt necesare servere de înaltă performanță pentru a difuza milioane de videoclipuri utilizatorilor. Latența mare sau timpul de nefuncționare trebuie evitat cu orice preț.
  • Memorarea în cache
    Mecanismele de stocare în cache ar trebui folosite la construirea acestui tip de aplicație de exemplu Cassandra, Amazon S3, AWS SimpleDB.
  • Geografia utilizatorilor
    Luând în considerare geografia utilizatorilor dvs., ar trebui să fie luată în considerare pentru distribuție.

Concluzie

În acest tutorial, am văzut cum să creăm un server în Node.js care transmite videoclipuri, generează subtitrări pentru acele videoclipuri și oferă metadate ale videoclipurilor. De asemenea, am văzut cum să folosim Nuxt.js pe front-end pentru a consuma punctele finale și datele generate de server.

Spre deosebire de alte cadre, construirea unei aplicații cu Nuxt.js și Express.js este destul de ușoară și rapidă. Partea tare despre Nuxt.js este modul în care vă gestionează rutele și vă face să vă structurați mai bine aplicațiile.

  • Puteți obține mai multe informații despre Nuxt.js aici.
  • Puteți obține codul sursă pe Github.

Resurse

  • „Adăugarea de subtitrări și subtitrări la videoclipurile HTML5”, MDN Web Docs
  • „Înțelegerea subtitrărilor și subtitrărilor”, Screenfont.ca