Tl; Dr - Pytanie:
Jaki jest właściwy sposób obsługi przesyłania strumieniowego pliku wideo do odtwarzacza wideo HTML5 za pomocą Node.js, aby elementy sterujące wideo nadal działały?
Myślę , że ma to związek ze sposobem obsługi nagłówków. W każdym razie, oto podstawowe informacje. Kod jest trochę długi, jednak jest dość prosty.
Przesyłanie strumieniowe małych plików wideo do wideo HTML5 za pomocą Node jest łatwe
Nauczyłem się, jak bardzo łatwo przesyłać strumieniowo małe pliki wideo do odtwarzacza wideo HTML5. W tej konfiguracji elementy sterujące działają bez żadnej pracy z mojej strony, a strumień wideo jest bezbłędny. Kopia robocza w pełni działającego kodu z przykładowym filmem jest tutaj do pobrania w Dokumentach Google .
Klient:
<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>
Serwer:
// 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);
Ale ta metoda jest ograniczona do plików o rozmiarze <1 GB.
Przesyłanie strumieniowe (dowolnego rozmiaru) plików wideo w formacie fs.createReadStream
Wykorzystując fs.createReadStream()
, serwer może odczytać plik w strumieniu, zamiast wczytywać go w całości do pamięci naraz. Brzmi to jak właściwy sposób robienia rzeczy, a składnia jest niezwykle prosta:
Fragment serwera:
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);
});
To dobrze przesyła wideo! Ale sterowanie wideo już nie działa.
źródło
writeHead()
kod skomentowany, ale na wszelki wypadek to pomoże. Czy powinienem to usunąć, aby fragment kodu był bardziej czytelny?Odpowiedzi:
Accept Ranges
Header (bit wwriteHead()
) jest wymagana dla kontroli HTML5 video do pracy.Myślę, że zamiast po prostu ślepo wysłać pełny plik, powinieneś najpierw sprawdzić
Accept Ranges
nagłówek w WNIOSKU, a następnie przeczytać i wysłać tylko ten bit.fs.createReadStream
wsparciestart
iend
opcja do tego.Spróbowałem więc na przykładzie i działa. Kod nie jest ładny, ale jest łatwy do zrozumienia. Najpierw przetwarzamy nagłówek zakresu, aby uzyskać pozycję początkową / końcową. Następnie używamy,
fs.stat
aby uzyskać rozmiar pliku bez wczytywania całego pliku do pamięci. Na koniec użyj,fs.createReadStream
aby wysłać żądaną część do klienta.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);
źródło
Przyjęta odpowiedź na to pytanie jest niesamowita i powinna pozostać zaakceptowaną odpowiedzią. Jednak napotkałem problem z kodem, w którym strumień odczytu nie zawsze był kończony / zamykany. Część rozwiązania polegała na wysłaniu
autoClose: true
wraz zstart:start, end:end
drugimcreateReadStream
argumentem.Drugą częścią rozwiązania było ograniczenie maksymalnej liczby
chunksize
wysyłanej w odpowiedzi. Druga odpowiedź brzmiend
tak:var end = positions[1] ? parseInt(positions[1], 10) : total - 1;
... co skutkuje wysłaniem reszty pliku od żądanej pozycji początkowej do ostatniego bajtu, bez względu na to, ile może to być bajtów. Jednak przeglądarka klienta ma opcję odczytu tylko części tego strumienia i zrobi to, jeśli nie potrzebuje jeszcze wszystkich bajtów. Spowoduje to zablokowanie odczytu strumienia, dopóki przeglądarka nie zdecyduje, że nadszedł czas, aby uzyskać więcej danych (na przykład działanie użytkownika, takie jak wyszukiwanie / przewijanie lub po prostu odtwarzając strumień).
Potrzebowałem tego strumienia, aby został zamknięty, ponieważ wyświetlałem
<video>
element na stronie, który pozwolił użytkownikowi usunąć plik wideo. Jednak plik nie był usuwany z systemu plików, dopóki klient (lub serwer) nie zamknął połączenia, ponieważ tylko w ten sposób strumień był kończony / zamykany.Moim rozwiązaniem było po prostu ustawienie
maxChunk
zmiennej konfiguracyjnej, ustawienie jej na 1 MB i nigdy nie przesyłanie do odpowiedzi strumienia więcej niż 1 MB na raz.// 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; }
Skutkuje to upewnieniem się, że strumień odczytu jest kończony / zamykany po każdym żądaniu i nie jest utrzymywany przy życiu przez przeglądarkę.
Napisałem również osobne pytanie dotyczące StackOverflow i odpowiedź na ten problem.
źródło
Najpierw utwórz
app.js
plik w katalogu, który chcesz opublikować.var http = require('http'); var fs = require('fs'); var mime = require('mime'); http.createServer(function(req,res){ if (req.url != '/app.js') { var url = __dirname + req.url; fs.stat(url,function(err,stat){ if (err) { res.writeHead(404,{'Content-Type':'text/html'}); res.end('Your requested URI('+req.url+') wasn\'t found on our server'); } else { var type = mime.getType(url); var fileSize = stat.size; var range = req.headers.range; if (range) { var parts = range.replace(/bytes=/, "").split("-"); var start = parseInt(parts[0], 10); var end = parts[1] ? parseInt(parts[1], 10) : fileSize-1; var chunksize = (end-start)+1; var file = fs.createReadStream(url, {start, end}); var head = { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': type } res.writeHead(206, head); file.pipe(res); } else { var head = { 'Content-Length': fileSize, 'Content-Type': type } res.writeHead(200, head); fs.createReadStream(url).pipe(res); } } }); } else { res.writeHead(403,{'Content-Type':'text/html'}); res.end('Sorry, access to that file is Forbidden'); } }).listen(8080);
Po prostu uruchom,
node app.js
a Twój serwer będzie działał na porcie 8080. Oprócz wideo może przesyłać strumieniowo wszystkie rodzaje plików.źródło