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