Tl; Dr - La pregunta:
¿Cuál es la forma correcta de manejar la transmisión de un archivo de video a un reproductor de video html5 con Node.js para que los controles de video continúen funcionando?
Yo creo que tiene que ver con la forma en que los encabezados se manejan. De todos modos, aquí está la información de fondo. El código es un poco extenso, sin embargo, es bastante sencillo.
Transmitir pequeños archivos de video a video HTML5 con Node es fácil
Aprendí a transmitir archivos de video pequeños a un reproductor de video HTML5 muy fácilmente. Con esta configuración, los controles funcionan sin ningún trabajo de mi parte, y el video se transmite sin problemas. Una copia de trabajo del código completamente funcional con un video de muestra está aquí, para descargar en 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);
Pero este método está limitado a archivos de menos de 1 GB de tamaño.
Transmisión de archivos de video (de cualquier tamaño) con fs.createReadStream
Al utilizarlo fs.createReadStream()
, el servidor puede leer el archivo en una secuencia en lugar de leerlo todo en la memoria a la vez. Esta parece la forma correcta de hacer las cosas, y la sintaxis es extremadamente simple:
Fragmento 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);
});
¡Esto transmite el video perfectamente! Pero los controles de video ya no funcionan.
fuente
writeHead()
código comentado, pero ahí por si sirve de ayuda. ¿Debo eliminar eso para que el fragmento de código sea más legible?Respuestas:
El
Accept Ranges
encabezado (el bit inwriteHead()
) es necesario para que funcionen los controles de video HTML5.Creo que en lugar de enviar ciegamente el archivo completo, primero debe verificar el
Accept Ranges
encabezado en la SOLICITUD, luego leer y enviar solo ese bit.fs.createReadStream
soportestart
yend
opción para eso.Así que probé un ejemplo y funciona. El código no es bonito pero es fácil de entender. Primero procesamos el encabezado del rango para obtener la posición inicial / final. Luego usamos
fs.stat
para obtener el tamaño del archivo sin leer el archivo completo en la memoria. Finalmente, utilicefs.createReadStream
para enviar la pieza solicitada al 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);
fuente
La respuesta aceptada a esta pregunta es asombrosa y debería seguir siendo la respuesta aceptada. Sin embargo, encontré un problema con el código en el que la transmisión de lectura no siempre se terminaba / cerraba. Parte de la solución fue enviar
autoClose: true
junto constart:start, end:end
el segundocreateReadStream
argumento.La otra parte de la solución fue limitar el máximo
chunksize
enviado en la respuesta. La otra respuesta se estableceend
así:var end = positions[1] ? parseInt(positions[1], 10) : total - 1;
... que tiene el efecto de enviar el resto del archivo desde la posición inicial solicitada hasta su último byte, sin importar cuántos bytes pueda ser. Sin embargo, el navegador del cliente tiene la opción de leer solo una parte de ese flujo, y lo hará, si aún no necesita todos los bytes. Esto hará que la lectura de la transmisión se bloquee hasta que el navegador decida que es hora de obtener más datos (por ejemplo, una acción del usuario como buscar / eliminar, o simplemente reproducir la transmisión).
Necesitaba cerrar esta transmisión porque estaba mostrando el
<video>
elemento en una página que permitía al usuario eliminar el archivo de video. Sin embargo, el archivo no se estaba eliminando del sistema de archivos hasta que el cliente (o servidor) cerró la conexión, porque esa es la única forma en que la transmisión terminaba / cerraba.Mi solución fue simplemente establecer una
maxChunk
variable de configuración, establecerla en 1 MB y nunca canalizar una lectura de más de 1 MB a la vez a la respuesta.// 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; }
Esto tiene el efecto de asegurarse de que el flujo de lectura finalice / cierre después de cada solicitud y que el navegador no lo mantenga activo.
También escribí una pregunta y respuesta de StackOverflow por separado que cubren este problema.
fuente
En primer lugar, cree el
app.js
archivo en el directorio que desea 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);
Simplemente ejecute
node app.js
y su servidor se ejecutará en el puerto 8080. Además de video, puede transmitir todo tipo de archivos.fuente