it-swarm.com.de

Eine Videodatei mit Node.js auf einen HTML5-Videoplayer streamen, damit die Videosteuerung weiterhin funktioniert?

Tl; Dr - Die Frage:

Wie gehe ich mit dem Streaming einer Videodatei auf einen HTML5-Videoplayer mit Node.js um, damit die Videosteuerelemente weiterhin funktionieren?

Ich denke , dass es mit der Art und Weise zu tun hat, wie die Header behandelt werden. Wie auch immer, hier sind die Hintergrundinformationen. Der Code ist ein wenig lang, aber es ist ziemlich einfach.

Das Streamen kleiner Videodateien in HTML5-Videos mit Node ist einfach

Ich habe sehr leicht gelernt, wie kleine Videodateien auf einen HTML5-Videoplayer gestreamt werden können. Mit diesem Setup funktionieren die Steuerelemente ohne mein Zutun und die Videostreams einwandfrei. Eine Arbeitskopie des voll funktionsfähigen Codes mit Beispielvideo lautet hier zum Herunterladen in Google Text & Tabellen .

Kunde:

<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>

Server:

// 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);

Diese Methode ist jedoch auf Dateien mit einer Größe von <1 GB beschränkt.

Streaming von (beliebig großen) Videodateien mit fs.createReadStream

Durch die Verwendung von fs.createReadStream() kann der Server die Datei in einem Stream lesen, anstatt sie alle gleichzeitig in den Speicher einzulesen. Das klingt nach der richtigen Vorgehensweise, und die Syntax ist äußerst einfach:

Server-Snippet:

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);
});

Dadurch wird das Video ganz gut gestreamt! Die Videosteuerung funktioniert aber nicht mehr.

82
WebDeveloper404

Der Header Accept Ranges (Das Bit in writeHead()) ist erforderlich, damit die HTML5-Video-Steuerelemente funktionieren.

Ich denke, anstatt nur blind die vollständige Datei zu senden, sollten Sie zuerst den Header Accept Ranges In der REQUEST überprüfen, dann einlesen und nur dieses Bit senden. fs.createReadStream Unterstützt start und end.

Also habe ich ein Beispiel ausprobiert und es funktioniert. Der Code ist nicht schön, aber leicht zu verstehen. Zuerst verarbeiten wir den Bereichskopf, um die Start-/Endposition zu erhalten. Dann verwenden wir fs.stat, Um die Größe der Datei zu ermitteln, ohne die gesamte Datei in den Speicher einzulesen. Verwenden Sie abschließend fs.createReadStream, Um das angeforderte Teil an den Client zu senden.

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

Die akzeptierte Antwort auf diese Frage ist fantastisch und sollte die akzeptierte Antwort bleiben. Ich bin jedoch auf ein Problem mit dem Code gestoßen, bei dem der Lesestream nicht immer beendet/geschlossen wurde. Ein Teil der Lösung bestand darin, autoClose: true zusammen mit start:start, end:end im zweiten createReadStream arg.

Der andere Teil der Lösung bestand darin, die maximale Anzahl der in der Antwort gesendeten chunksize zu begrenzen. Die andere Antwortmenge end sieht so aus:

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

... was dazu führt, dass der Rest der Datei von der angeforderten Startposition bis zum letzten Byte gesendet wird, unabhängig von der Anzahl der Bytes. Der Client-Browser hat jedoch die Option, nur einen Teil dieses Streams zu lesen, und wird dies tun, wenn er noch nicht alle Bytes benötigt. Dadurch wird das Lesen des Streams blockiert, bis der Browser entscheidet, dass mehr Daten abgerufen werden sollen (z. B. durch eine Benutzeraktion wie Suchen/Scrubben oder durch einfaches Abspielen des Streams).

Ich musste diesen Stream schließen, weil ich das <video> Element auf einer Seite, mit dem der Benutzer die Videodatei löschen konnte. Die Datei wurde jedoch erst aus dem Dateisystem entfernt, nachdem der Client (oder Server) die Verbindung geschlossen hatte, da der Stream nur so beendet/geschlossen wurde.

Meine Lösung bestand lediglich darin, eine maxChunk -Konfigurationsvariable festzulegen, diese auf 1 MB zu setzen und niemals einen Lesestream von mehr als 1 MB gleichzeitig auf die Antwort zu leiten.

// 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;
}

Dadurch wird sichergestellt, dass der Lesestream nach jeder Anforderung beendet/geschlossen wird und nicht vom Browser am Leben gehalten wird.

Ich habe auch einen separaten StackOverflow Frage und Antwort geschrieben, der dieses Problem behandelt.

18
danludwig