Tl; Dr - A pergunta:
Qual é a maneira certa de lidar com o streaming de um arquivo de vídeo para um player de vídeo html5 com Node.js para que os controles de vídeo continuem funcionando?
Eu acho que tem a ver com a maneira que os cabeçalhos são manipulados. De qualquer forma, aqui estão as informações básicas. O código é um pouco longo, no entanto, é bastante direto.
Transmitir pequenos arquivos de vídeo para vídeo HTML5 com Node é fácil
Aprendi como transmitir pequenos arquivos de vídeo para um reprodutor de vídeo HTML5 com muita facilidade. Com esta configuração, os controles funcionam sem nenhum trabalho da minha parte, e o vídeo é transmitido perfeitamente. Uma cópia de trabalho do código totalmente funcional com vídeo de amostra está aqui, para download no Google Docs .
Cliente:
<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>
Servidor:
// 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);
Mas este método é limitado a arquivos <1 GB de tamanho.
Streaming (qualquer tamanho) de arquivos de vídeo com fs.createReadStream
Ao utilizar fs.createReadStream()
, o servidor pode ler o arquivo em um fluxo, em vez de ler tudo na memória de uma vez. Parece a maneira certa de fazer as coisas, e a sintaxe é extremamente simples:
Snippet de servidor:
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);
});
Isso reproduz o vídeo perfeitamente! Mas os controles de vídeo não funcionam mais.
fonte
writeHead()
código comentado, mas aí caso ajude. Devo remover isso para tornar o trecho de código mais legível?Respostas:
O
Accept Ranges
cabeçalho (o bit inwriteHead()
) é necessário para que os controles de vídeo HTML5 funcionem.Acho que em vez de enviar cegamente o arquivo completo, você deve primeiro verificar o
Accept Ranges
cabeçalho no REQUEST, depois ler e enviar apenas aquele pedaço.fs.createReadStream
suportestart
eend
opção para isso.Tentei um exemplo e funcionou. O código não é bonito, mas é fácil de entender. Primeiro, processamos o cabeçalho do intervalo para obter a posição inicial / final. Em seguida, usamos
fs.stat
para obter o tamanho do arquivo sem ler todo o arquivo na memória. Por fim, utilizefs.createReadStream
para enviar a peça solicitada ao cliente.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);
fonte
A resposta aceita para esta pergunta é impressionante e deve permanecer a resposta aceita. No entanto, tive um problema com o código em que o fluxo de leitura nem sempre estava sendo encerrado / fechado. Parte da solução foi enviar
autoClose: true
junto comstart:start, end:end
o segundocreateReadStream
arg.A outra parte da solução era limitar o máximo
chunksize
enviado na resposta. A outra resposta definidaend
assim:var end = positions[1] ? parseInt(positions[1], 10) : total - 1;
... que tem o efeito de enviar o resto do arquivo da posição inicial solicitada até seu último byte, não importa quantos bytes sejam. No entanto, o navegador do cliente tem a opção de ler apenas uma parte desse fluxo, e o fará, se ainda não precisar de todos os bytes. Isso fará com que o fluxo lido seja bloqueado até que o navegador decida que é hora de obter mais dados (por exemplo, uma ação do usuário como buscar / limpar ou apenas reproduzir o fluxo).
Eu precisava que este fluxo fosse fechado porque estava exibindo o
<video>
elemento em uma página que permitia ao usuário excluir o arquivo de vídeo. No entanto, o arquivo não estava sendo removido do sistema de arquivos até que o cliente (ou servidor) fechasse a conexão, porque essa é a única maneira que o fluxo estava sendo encerrado / fechado.Minha solução foi apenas definir uma
maxChunk
variável de configuração, defini-la para 1 MB e nunca canalizar uma leitura de um fluxo de mais de 1 MB por vez para a resposta.// 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; }
Isso tem o efeito de garantir que o fluxo de leitura seja encerrado / fechado após cada solicitação e não seja mantido ativo pelo navegador.
Eu também escrevi uma pergunta StackOverflow separada e resposta cobrindo esse assunto.
fonte
Em primeiro lugar, crie o
app.js
arquivo no diretório que deseja publicar.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);
Simplesmente execute
node app.js
e seu servidor estará rodando na porta 8080. Além de vídeo, ele pode transmitir todos os tipos de arquivos.fonte