Transférer un fichier vidéo vers un lecteur vidéo html5 avec Node.js afin que les contrôles vidéo continuent de fonctionner?

Tl; Dr – The Question:

Quelle est la bonne façon de gérer la diffusion d'un fichier vidéo sur un lecteur vidéo html5 avec Node.js afin que les contrôles vidéo continuent à fonctionner?

Je pense qu'il s'agit de la manière dont les en-têtes sont gérés. Quoi qu'il en soit, voici les informations générales. Le code est un peu long, cependant, c'est assez simple.

Le streaming de petits fichiers vidéo vers une vidéo HTML5 avec Node est facile

J'ai appris à diffuser facilement de petits fichiers vidéo sur un lecteur vidéo HTML5. Avec cette configuration, les contrôles fonctionnent sans aucun travail de ma part, et les flux vidéo sont impeccables. Une copie de travail du code entièrement fonctionnel avec une vidéo échantillon est ici, pour téléchargement sur Google Docs .

Client:

<html> <title>Welcome</title> <body> <video controls> <source src="movie.mp4" type="video/mp4"/> <source src="movie.webm" type="video/webm"/> <source src="movie.ogg" type="video/ogg"/> <!-- fallback --> Your browser does not support the <code>video</code> element. </video> </body> </html> 

Serveur:

 // Declare Vars & Read Files var fs = require('fs'), http = require('http'), url = require('url'), path = require('path'); var movie_webm, movie_mp4, movie_ogg; // ... [snip] ... (Read index page) fs.readFile(path.resolve(__dirname,"movie.mp4"), function (err, data) { if (err) { throw err; } movie_mp4 = data; }); // ... [snip] ... (Read two other formats for the video) // Serve & Stream Video http.createServer(function (req, res) { // ... [snip] ... (Serve client files) var total; if (reqResource == "/movie.mp4") { total = movie_mp4.length; } // ... [snip] ... handle two other formats for the video var range = req.headers.range; var positions = range.replace(/bytes=/, "").split("-"); var start = parseInt(positions[0], 10); var end = positions[1] ? parseInt(positions[1], 10) : total - 1; var chunksize = (end - start) + 1; if (reqResource == "/movie.mp4") { res.writeHead(206, { "Content-Range": "bytes " + start + "-" + end + "/" + total, "Accept-Ranges": "bytes", "Content-Length": chunksize, "Content-Type": "video/mp4" }); res.end(movie_mp4.slice(start, end + 1), "binary"); } // ... [snip] ... handle two other formats for the video }).listen(8888); 

Mais cette méthode est limitée aux fichiers <1 Go de taille.

Fichiers vidéo en streaming (n'importe quelle taille) avec fs.createReadStream

En utilisant fs.createReadStream() , le serveur peut lire le fichier dans un flux plutôt que de le lire tout en mémoire à la fois. Cela ressemble à la bonne façon de faire les choses, et la syntaxe est extrêmement simple:

Extrait du serveur:

 movieStream = fs.createReadStream(pathToFile); movieStream.on('open', function () { res.writeHead(206, { "Content-Range": "bytes " + start + "-" + end + "/" + total, "Accept-Ranges": "bytes", "Content-Length": chunksize, "Content-Type": "video/mp4" }); // This just pipes the read stream to the response object (which goes //to the client) movieStream.pipe(res); }); movieStream.on('error', function (err) { res.end(err); }); 

Cela diffuse la vidéo très bien! Mais les contrôles vidéo ne fonctionnent plus.

L'en-tête Accept Ranges (le bit dans writeHead() ) est requis pour que les commandes vidéo HTML5 fonctionnent.

Je pense qu'au lieu de simplement envoyer aveuglément le fichier complet, vous devez d'abord vérifier l'en-tête Accept Ranges dans la DEMANDE, puis lire et envoyer juste ce bit. fs.createReadStream support start et option de end pour cela.

J'ai donc essayé un exemple et ça marche. Le code n'est pas beau, mais il est facile à comprendre. D'abord, nous traitons l'en-tête de gamme pour obtenir la position de début / fin. Ensuite, nous utilisons fs.stat pour obtenir la taille du fichier sans lire l'intégralité du fichier dans la mémoire. Enfin, utilisez fs.createReadStream pour envoyer la partie demandée au client.

 var fs = require("fs"), http = require("http"), url = require("url"), path = require("path"); http.createServer(function (req, res) { if (req.url != "/movie.mp4") { res.writeHead(200, { "Content-Type": "text/html" }); res.end('<video src="http://localhost:8888/movie.mp4" controls></video>'); } else { var file = path.resolve(__dirname,"movie.mp4"); fs.stat(file, function(err, stats) { if (err) { if (err.code === 'ENOENT') { // 404 Error if file not found return res.sendStatus(404); } res.end(err); } var range = req.headers.range; if (!range) { // 416 Wrong range return res.sendStatus(416); } var positions = range.replace(/bytes=/, "").split("-"); var start = parseInt(positions[0], 10); var total = stats.size; var end = positions[1] ? parseInt(positions[1], 10) : total - 1; var chunksize = (end - start) + 1; res.writeHead(206, { "Content-Range": "bytes " + start + "-" + end + "/" + total, "Accept-Ranges": "bytes", "Content-Length": chunksize, "Content-Type": "video/mp4" }); var stream = fs.createReadStream(file, { start: start, end: end }) .on("open", function() { stream.pipe(res); }).on("error", function(err) { res.end(err); }); }); } }).listen(8888); 

La réponse acceptée à cette question est géniale et devrait rester la réponse acceptée. Cependant, j'ai rencontré un problème avec le code où le flux de lecture n'était pas toujours terminé / fermé. Une partie de la solution était d'envoyer autoClose: true avec start:start, end:end dans le second createReadStream arg.

L'autre partie de la solution consistait à limiter le nombre maximal de chunksize envoyés dans la réponse. L'autre paramètre de réponse se end comme suit:

 var end = positions[1] ? parseInt(positions[1], 10) : total - 1; 

… ce qui a pour effet d'envoyer le reste du fichier de la position de départ demandée à travers son dernier octet, quel que soit le nombre d'octets qui peuvent être. Cependant, le navigateur client a la possibilité de ne lire qu'une partie de ce flux et, s'il n'en a pas encore besoin, tous les octets. Cela entraînera le lancement du flux pour être bloqué jusqu'à ce que le navigateur décide qu'il est temps d'obtenir plus de données (par exemple, une action de l'utilisateur comme recherche / scrub, ou simplement en jouant le flux).

J'avais besoin de ce flux pour être fermé parce que j'avais affiché l'élément <video> sur une page qui permettait à l'utilisateur de supprimer le fichier vidéo. Toutefois, le fichier n'a pas été supprimé du système de fichiers jusqu'à ce que le client (ou le serveur) ait fermé la connexion, car c'est la seule façon de terminer / fermer le flux.

Ma solution était juste de définir une variable de configuration maxChunk configurer à 1 Mo, et de ne jamais transmettre une lecture d'un flux de plus de 1 Mo à la fois à la réponse.

 // same code as accepted answer var end = positions[1] ? parseInt(positions[1], 10) : total - 1; var chunksize = (end - start) + 1; // poor hack to send smaller chunks to the browser var maxChunk = 1024 * 1024; // 1MB at a time if (chunksize > maxChunk) { end = start + maxChunk - 1; chunksize = (end - start) + 1; } 

Cela a pour effet de s'assurer que le flux de lecture est terminé / fermé après chaque demande et n'est pas maintenu en vie par le navigateur.

J'ai également écrit une question de StackOverflow et une réponse pour répondre à ce problème.