Créer une application de streaming vidéo avec Nuxt.js, Node et Express

Publié: 2022-03-10
Résumé rapide ↬ Dans cet article, nous allons créer une application de streaming vidéo en utilisant Nuxt.js et Node.js. Plus précisément, nous allons créer une application Node.js côté serveur qui gérera la récupération et la diffusion de vidéos, la génération de vignettes pour vos vidéos et la diffusion de légendes et de sous-titres.

Les 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
Plus après saut! Continuez à lire ci-dessous ↓

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 page Player dans notre interface.
  • /video/:id
    Diffuse une vidéo avec un ID donné. Utilisé par la page Player .

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 :

Résultat final de l'application de streaming vidéo Nuxt
Le streaming vidéo démarre lorsque l'utilisateur clique sur la vignette. ( Grand aperçu )

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