Crearea unei aplicații de streaming video cu Nuxt.js, Node și Express
Publicat: 2022-03-10Videoclipurile 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
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 deHome
din frontend-ul nostru. -
/video/:id/data
Returnează metadate pentru un singur videoclip. Folosit de paginaPlayer
din frontend-ul nostru. -
/video/:id
Redă în flux un videoclip cu un anumit ID. Folosit de paginaPlayer
.
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:
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