web-dev-qa-db-fra.com

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

Tl; Dr - La 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 de fonctionner?

Je pense cela a à voir avec la façon dont les en-têtes sont gérés. Quoi qu'il en soit, voici les informations de base. Le code est un petit ​​long, cependant, c'est assez simple.

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

J'ai appris à diffuser très 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 la vidéo est parfaitement diffusée. Une copie de travail du code entièrement fonctionnel avec un exemple de vidéo est ici, à télécharger sur Google Documents .

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 de moins de 1 Go.

Diffuser des fichiers vidéo (toutes tailles) avec fs.createReadStream

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

Extrait de 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 bien la vidéo! Mais les contrôles vidéo ne fonctionnent plus.

82
WebDeveloper404

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

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

Alors j'ai essayé un exemple et ça marche. Le code n'est pas joli mais il est facile à comprendre. Nous traitons d'abord l'en-tête de plage pour obtenir la position de début/fin. Ensuite, nous utilisons fs.stat pour obtenir la taille du fichier sans lire le fichier entier en 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);
107
tungd

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 dans lequel le flux de lecture n'était pas toujours terminé/fermé. Une partie de la solution consistait à envoyer autoClose: true avec start:start, end:end dans la seconde createReadStream arg.

L’autre partie de la solution consistait à limiter le nombre maximal de chunksize envoyés dans la réponse. L’autre ensemble de réponses end ressemble à ceci:

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 jusqu'au dernier octet, quel que soit le nombre d'octets pouvant être. Cependant, le navigateur client a l'option de ne lire qu'une partie de ce flux, et le fera s'il n'a pas encore besoin de tous les octets. Cela entraînera le blocage du flux de lecture jusqu'à ce que le navigateur décide qu'il est temps d'obtenir plus de données (par exemple, une action de l'utilisateur telle que search/scrub ou tout simplement en lisant le flux).

J'avais besoin que ce flux soit fermé car j'affichais l'élément <video> sur une page permettant à l'utilisateur de supprimer le fichier vidéo. Cependant, le fichier n'a pas été supprimé du système de fichiers jusqu'à ce que le client (ou le serveur) ferme la connexion, car c'est la seule façon dont le flux était interrompu/fermé.

Ma solution consistait simplement à définir une variable de configuration maxChunk, à 1 Mo, et à ne jamais diriger un flux de lecture d'un flux de plus de 1 Mo à la fois vers 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 un StackOverflow séparé question et réponse couvrant cette question.

18
danludwig