Créer une application de streaming vidéo avec Nuxt.js, Node et Express
Publié: 2022-03-10Les vidéos fonctionnent avec des flux. Cela signifie qu'au lieu d'envoyer la vidéo entière en une seule fois, une vidéo est envoyée sous la forme d'un ensemble de petits morceaux qui composent la vidéo complète. Cela explique pourquoi les vidéos se mettent en mémoire tampon lorsque vous regardez une vidéo sur un haut débit lent, car elles ne lisent que les morceaux qu'elles ont reçus et essaient d'en charger davantage.
Cet article est destiné aux développeurs qui souhaitent apprendre une nouvelle technologie en créant un projet réel : une application de streaming vidéo avec Node.js comme backend et Nuxt.js comme client.
- Node.js est un runtime utilisé pour créer des applications rapides et évolutives. Nous l'utiliserons pour gérer la récupération et la diffusion de vidéos, la génération de vignettes pour les vidéos et la diffusion de légendes et de sous-titres pour les vidéos.
- Nuxt.js est un framework Vue.js qui nous aide à créer facilement des applications Vue.js rendues par le serveur. Nous consommerons notre API pour les vidéos et cette application aura deux vues : une liste des vidéos disponibles et une vue lecteur pour chaque vidéo.
Prérequis
- Compréhension de HTML, CSS, JavaScript, Node/Express et Vue.
- Un éditeur de texte (par exemple VS Code).
- Un navigateur Web (par exemple Chrome, Firefox).
- FFmpeg installé sur votre poste de travail.
- Node.js. nvm.
- Vous pouvez obtenir le code source sur GitHub.
Configuration de notre application
Dans cette application, nous allons construire les routes pour faire des requêtes depuis le frontend :
- route des
videos
pour obtenir une liste des vidéos et de leurs données. - un itinéraire pour récupérer une seule vidéo de notre liste de vidéos.
- route de
streaming
pour diffuser les vidéos. - route des sous-
captions
pour ajouter des sous-titres aux vidéos que nous diffusons.
Une fois nos routes créées, nous échafauderons notre interface Nuxt
, où nous créerons la page Home
et le player
dynamique. Ensuite, nous demandons à notre route de videos
de remplir la page d'accueil avec les données vidéo, une autre demande pour diffuser les vidéos sur notre page de player
, et enfin une demande pour servir les fichiers de sous-titres à utiliser par les vidéos.
Pour mettre en place notre application, nous créons notre répertoire de projet,
mkdir streaming-app
Configuration de notre serveur
Dans notre répertoire streaming-app
, nous créons un dossier nommé backend
.
cd streaming-app mkdir backend
Dans notre dossier backend, nous initialisons un fichier package.json
pour stocker des informations sur notre projet de serveur.
cd backend npm init -y
nous devons installer les packages suivants pour créer notre application.
-
nodemon
redémarre automatiquement notre serveur lorsque nous apportons des modifications. -
express
nous donne une belle interface pour gérer les routes. -
cors
nous permettra de faire des requêtes cross-origin puisque notre client et notre serveur fonctionneront sur des ports différents.
Dans notre répertoire backend, nous créons un dossier assets
pour contenir nos vidéos en streaming.
mkdir assets
Copiez un fichier .mp4
dans le dossier assets et nommez-le video1
. Vous pouvez utiliser de courts exemples de vidéos .mp4
disponibles sur Github Repo.
Créez un fichier app.js
et ajoutez les packages nécessaires pour notre application.
const express = require('express'); const fs = require('fs'); const cors = require('cors'); const path = require('path'); const app = express(); app.use(cors())
Le module fs
est utilisé pour lire et écrire facilement dans des fichiers sur notre serveur, tandis que le module path
fournit un moyen de travailler avec des répertoires et des chemins de fichiers.
Nous créons maintenant une route ./video
. Sur demande, il renverra un fichier vidéo au client.
// add after 'const app = express();' app.get('/video', (req, res) => { res.sendFile('assets/video1.mp4', { root: __dirname }); });
Cette route sert le fichier vidéo video1.mp4
sur demande. Nous écoutons ensuite notre serveur au port 3000
.
// add to end of app.js file app.listen(5000, () => { console.log('Listening on port 5000!') });
Un script est ajouté dans le fichier package.json
pour démarrer notre serveur en utilisant nodemon.
"scripts": { "start": "nodemon app.js" },
Ensuite, sur votre terminal, exécutez :
npm run start
Si vous voyez le message Listening on port 3000!
dans le terminal, alors le serveur fonctionne correctement. Accédez à https://localhost:5000/video dans votre navigateur et vous devriez voir la vidéo jouer.
Demandes à traiter par le frontend
Vous trouverez ci-dessous les requêtes que nous ferons au backend depuis notre frontend et que nous avons besoin que le serveur gère.
-
/videos
Renvoie un tableau de données de maquette vidéo qui seront utilisées pour remplir la liste des vidéos sur la page d'Home
de notre interface. -
/video/:id/data
Renvoie les métadonnées d'une seule vidéo. Utilisé par la pagePlayer
dans notre interface. -
/video/:id
Diffuse une vidéo avec un ID donné. Utilisé par la pagePlayer
.
Créons les routes.
Renvoyer les données de maquette pour la liste des vidéos
Pour cette application de démonstration, nous allons créer un tableau d'objets qui contiendront les métadonnées et les enverrons à l'interface à la demande. Dans une application réelle, vous seriez probablement en train de lire les données d'une base de données, qui seraient ensuite utilisées pour générer un tableau comme celui-ci. Par souci de simplicité, nous ne le ferons pas dans ce tutoriel.
Dans notre dossier principal, créez un fichier mockdata.js
et remplissez-le avec les métadonnées de notre liste de vidéos.
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
On peut voir d'en haut, chaque objet contient des informations sur la vidéo. Remarquez l'attribut poster
qui contient le lien vers une image d'affiche de la vidéo.
Créons une route de videos
puisque toutes nos requêtes à faire par le frontend sont précédées de /videos
.
Pour ce faire, créons un dossier routes
et ajoutons un fichier Video.js
pour notre route /videos
. Dans ce fichier, nous aurons besoin d' express
et utiliserons le routeur express pour créer notre itinéraire.
const express = require('express') const router = express.Router()
Lorsque nous allons sur la route /videos
, nous voulons obtenir notre liste de vidéos, alors exigeons le fichier mockData.js
dans notre fichier Video.js
et faisons notre demande.
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;
La route /videos
est maintenant déclarée, enregistrez le fichier et il devrait redémarrer automatiquement le serveur. Une fois démarré, accédez à https://localhost:3000/videos et notre tableau est renvoyé au format JSON.
Renvoyer les données d'une seule vidéo
Nous voulons pouvoir faire une demande pour une vidéo particulière dans notre liste de vidéos. Nous pouvons récupérer une donnée vidéo particulière dans notre tableau en utilisant l' id
que nous lui avons donné. Faisons une requête, toujours dans notre fichier 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]) })
Le code ci-dessus obtient l' id
à partir des paramètres de route et le convertit en un entier. Ensuite, nous renvoyons l'objet qui correspond à l' id
du tableau de videos
au client.
Diffuser les vidéos
Dans notre fichier app.js
, nous avons créé une route /video
qui sert une vidéo au client. Nous voulons que ce point de terminaison envoie de plus petits morceaux de la vidéo, au lieu de diffuser un fichier vidéo entier sur demande.
Nous voulons être en mesure de diffuser dynamiquement l'une des trois vidéos qui se trouvent dans le tableau allVideos
et de diffuser les vidéos en morceaux, donc :
Supprimez la route /video
de app.js
.
Nous avons besoin de trois vidéos, alors copiez les exemples de vidéos du code source du didacticiel dans le répertoire assets/
de votre projet de server
. Assurez-vous que les noms de fichiers des vidéos correspondent à l' id
dans le tableau videos
:
De retour dans notre fichier Video.js
, créez la route pour les vidéos en streaming.
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); } });
Si nous naviguons vers https://localhost:5000/videos/video/outside-the-wire dans notre navigateur, nous pouvons voir le streaming vidéo.
Comment fonctionne l'itinéraire vidéo en streaming
Il y a pas mal de code écrit dans notre route de flux vidéo, alors regardons-le ligne par ligne.
const videoPath = `assets/${req.params.id}.mp4`; const videoStat = fs.statSync(videoPath); const fileSize = videoStat.size; const videoRange = req.headers.range;
Tout d'abord, à partir de notre requête, nous obtenons l' id
de la route à l'aide de req.params.id
et l'utilisons pour générer le videoPath
vers la vidéo. Nous lisons ensuite le fileSize
en utilisant le système de fichiers fs
que nous avons importé. Pour les vidéos, le navigateur d'un utilisateur enverra un paramètre de range
dans la requête. Cela permet au serveur de savoir quel morceau de la vidéo renvoyer au client.
Certains navigateurs envoient une plage dans la requête initiale, mais d'autres non. Pour ceux qui ne le font pas, ou si pour toute autre raison le navigateur n'envoie pas de plage, nous gérons cela dans le bloc else
. Ce code obtient la taille du fichier et envoie les premiers morceaux de la vidéo :
else { const head = { 'Content-Length': fileSize, 'Content-Type': 'video/mp4', }; res.writeHead(200, head); fs.createReadStream(path).pipe(res); }
Nous traiterons les requêtes suivantes, y compris la plage dans 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); }
Ce code ci-dessus crée un flux de lecture en utilisant les valeurs de start
et de end
de la plage. Définissez Content-Length
des en-têtes de réponse sur la taille de bloc calculée à partir des valeurs de start
et de end
. Nous utilisons également le code HTTP 206, ce qui signifie que la réponse contient du contenu partiel. Cela signifie que le navigateur continuera à faire des demandes jusqu'à ce qu'il ait récupéré tous les morceaux de la vidéo.
Que se passe-t-il sur les connexions instables
Si l'utilisateur est sur une connexion lente, le flux réseau le signalera en demandant que la source d'E/S fasse une pause jusqu'à ce que le client soit prêt pour plus de données. C'est ce qu'on appelle la contre-pression . Nous pouvons pousser cet exemple un peu plus loin et voir à quel point il est facile d'étendre le flux. Nous pouvons aussi facilement ajouter de la compression !
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});
Nous pouvons voir ci-dessus qu'un ReadStream
est créé et sert la vidéo morceau par morceau.
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'en-tête de la requête contient le Content-Range
, qui est le changement de début et de fin pour obtenir le prochain morceau de vidéo à diffuser sur le frontend, la content-length
est le morceau de vidéo envoyé. Nous spécifions également le type de contenu que nous diffusons qui est mp4
. La tête d'écriture de 206 est configurée pour répondre uniquement avec des flux nouvellement créés.
Créer un fichier de sous-titres pour nos vidéos
Voici à quoi ressemble un fichier de légende .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.
Les fichiers de sous-titres contiennent du texte pour ce qui est dit dans une vidéo. Il contient également des codes temporels indiquant quand chaque ligne de texte doit être affichée. Nous voulons que nos vidéos aient des sous-titres, et nous ne créerons pas notre propre fichier de sous-titres pour ce didacticiel, vous pouvez donc vous diriger vers le dossier des sous-titres dans le répertoire des assets
du référentiel et télécharger les sous-titres.
Créons une nouvelle route qui gérera la demande de sous-titre :
router.get('/video/:id/caption', (req, res) => res.sendFile(`assets/captions/${req.params.id}.vtt`, { root: __dirname }));
Construire notre interface
Pour commencer la partie visuelle de notre système, nous devions construire notre échafaudage frontal.
Note : Vous avez besoin de vue-cli pour créer notre application. Si vous ne l'avez pas installé sur votre ordinateur, vous pouvez exécuter npm install -g @vue/cli
pour l'installer.
Installation
A la racine de notre projet, créons notre dossier front-end :
mkdir frontend cd frontend
et dans celui-ci, nous initialisons notre fichier package.json
, copions et collons ce qui suit :
{ "name": "my-app", "scripts": { "dev": "nuxt", "build": "nuxt build", "generate": "nuxt generate", "start": "nuxt start" } }
puis installez nuxt
:
npm add nuxt
et exécutez la commande suivante pour exécuter l'application Nuxt.js :
npm run dev
Notre structure de fichiers Nuxt
Maintenant que nous avons installé Nuxt, nous pouvons commencer à configurer notre interface.
Tout d'abord, nous devons créer un dossier de layouts
en page à la racine de notre application. Ce dossier définit la disposition de l'application, quelle que soit la page vers laquelle nous naviguons. Des choses comme notre barre de navigation et notre pied de page se trouvent ici. Dans le dossier frontal, nous créons default.vue
pour notre mise en page par défaut lorsque nous démarrons notre application frontale.
mkdir layouts cd layouts touch default.vue
Puis un dossier de components
pour créer tous nos composants. Nous n'aurons besoin que de deux composants, NavBar
et composant video
. Ainsi, dans notre dossier racine du frontend, nous :
mkdir components cd components touch NavBar.vue touch Video.vue
Enfin, un dossier de pages où toutes nos pages comme home
et about
peuvent être créées. Les deux pages dont nous avons besoin dans cette application sont la page home
accueil affichant toutes nos vidéos et informations vidéo et une page de lecteur dynamique qui redirige vers la vidéo sur laquelle nous cliquons.
mkdir pages cd pages touch index.vue mkdir player cd player touch _name.vue
Notre répertoire frontal ressemble maintenant à ceci :
|-frontend |-components |-NavBar.vue |-Video.vue |-layouts |-default.vue |-pages |-index.vue |-player |-_name.vue |-package.json |-yarn.lock
Composant de la barre de navigation
Notre NavBar.vue
ressemble à ceci :
<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
a une balise h1
qui affiche Streaming App , avec un peu de style.
Importons la NavBar
dans notre mise en page default.vue
.
// default.vue <template> <div> <NavBar /> <nuxt /> </div> </template> <script> import NavBar from "@/components/NavBar.vue" export default { components: { NavBar, } } </script>
La mise en page default.vue
contient maintenant notre composant NavBar
et la <nuxt />
après qu'elle signifie où toute page que nous créons sera affichée.
Dans notre index.vue
(qui est notre page d'accueil), faisons une requête à https://localhost:5000/videos
pour obtenir toutes les vidéos de notre serveur. Transmettre les données en tant qu'accessoire à notre composant video.vue
que nous créerons plus tard. Mais pour l'instant, nous l'avons déjà importé.
<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>
Composant vidéo
Ci-dessous, nous déclarons d'abord notre accessoire. Puisque les données vidéo sont maintenant disponibles dans le composant, en utilisant le v-for
de Vue, nous itérons sur toutes les données reçues et pour chacune, nous affichons les informations. Nous pouvons utiliser la directive v-for
pour parcourir les données et les afficher sous forme de liste. Certains styles de base ont également été ajoutés.
<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>
Nous remarquons également que le NuxtLink
a une route dynamique, c'est-à-dire le routage vers le /player/video.id
.
La fonctionnalité que nous voulons est que lorsqu'un utilisateur clique sur l'une des vidéos, la diffusion commence. Pour y parvenir, nous utilisons la nature dynamique de la route _name.vue
.
Dans celui-ci, nous créons un lecteur vidéo et définissons la source sur notre point de terminaison pour diffuser la vidéo, mais nous ajoutons dynamiquement la vidéo à lire à notre point de terminaison à l'aide de this.$route.params.name
qui capture le paramètre reçu par le lien .
<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>
Lorsque nous cliquons sur l'une des vidéos, nous obtenons :
Ajout de notre fichier de légende
Pour ajouter notre fichier de piste, nous nous assurons que tous les fichiers .vtt
du dossier captions portent le même nom que notre id
. Mettez à jour notre élément vidéo avec la piste, en faisant une demande pour les sous-titres.
<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>
Nous avons ajouté crossOrigin="anonymous"
à l'élément vidéo ; sinon, la demande de sous-titres échouera. Maintenant, actualisez et vous verrez que les sous-titres ont été ajoutés avec succès.
Ce qu'il faut garder à l'esprit lors de la création d'un streaming vidéo résilient.
Lors de la création d'applications de streaming comme Twitch, Hulu ou Netflix, un certain nombre de choses sont prises en compte :
- Pipeline de traitement des données vidéo
Cela peut être un défi technique car des serveurs très performants sont nécessaires pour diffuser des millions de vidéos aux utilisateurs. Une latence élevée ou des temps d'arrêt doivent être évités à tout prix. - Mise en cache
Des mécanismes de mise en cache doivent être utilisés lors de la création de ce type d'application, par exemple Cassandra, Amazon S3, AWS SimpleDB. - Géographie des utilisateurs
La répartition géographique de vos utilisateurs doit être prise en compte pour la distribution.
Conclusion
Dans ce didacticiel, nous avons vu comment créer un serveur dans Node.js qui diffuse des vidéos, génère des sous-titres pour ces vidéos et diffuse les métadonnées des vidéos. Nous avons également vu comment utiliser Nuxt.js en frontend pour consommer les endpoints et les données générées par le serveur.
Contrairement à d'autres frameworks, créer une application avec Nuxt.js et Express.js est assez simple et rapide. La partie intéressante de Nuxt.js est la façon dont il gère vos itinéraires et vous permet de mieux structurer vos applications.
- Vous pouvez obtenir plus d'informations sur Nuxt.js ici.
- Vous pouvez obtenir le code source sur Github.
Ressources
- « Ajouter des légendes et des sous-titres à une vidéo HTML5 », MDN Web Docs
- "Comprendre les légendes et les sous-titres", Screenfont.ca