使用Node.js将视频文件流式传输到html5视频播放器,以便视频控件继续工作?


96

Tl; Dr-问题:

使用Node.js处理将视频文件流传输到html5视频播放器以使视频控件继续工作的正确方法是什么

认为这与处理标头的方式有关。无论如何,这是背景信息。该代码有点冗长,但是非常简单。

使用Node将小视频文件流化为HTML5视频很容易

我学习了如何非常轻松地将小型视频文件流式传输到HTML5视频播放器。使用此设置,控件可以正常工作,而我的视频也可以完美播放。此处包含示例视频的完整工作代码的工作副本,可在Google文档中下载

客户:

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

服务器:

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

但是此方法仅限于大小小于1GB的文件。

流式传输(任何大小)视频文件 fs.createReadStream

通过利用fs.createReadStream(),服务器可以读取流中的文件,而不是一次将其全部读取到内存中。这听起来像做事的正确方法,语法非常简单:

服务器片段:

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

这样就可以正常播放视频了!但是视频控件不再起作用。


1
我在writeHead()代码中留下了注释,但有帮助的话。我应该删除它以使代码片段更具可读性吗?
WebDeveloper404 2014年

3
req.headers.range来自哪里?尝试执行replace方法时,我总是变得不确定。谢谢。
乍得沃特金斯2015年

Answers:


118

Accept Ranges报头(在该位writeHead())所需的HTML5视频控制来工作。

我认为,不仅要盲目发送完整文件,还应首先检查Accept RangesREQUEST中的标头,然后读入并发送该位。fs.createReadStreamsupportstartend选项。

因此,我尝试了一个例子,它可以工作。该代码不是很漂亮,但是很容易理解。首先,我们处理范围标头以获取开始/结束位置。然后,我们用于fs.stat获取文件的大小,而无需将整个文件读入内存。最后,用于fs.createReadStream将请求的零件发送给客户端。

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

3
我们可以使用这种策略仅发送电影的一部分,即5秒到7秒之间吗?有没有办法通过ffmpeg之类的库来查找该间隔对应于哪个字节间隔?谢谢。
pembeci 2015年

8
没关系,我的问题。我找到了神奇的词汇来找到如何实现我所要求的:伪流
pembeci 2015年

如果movie.mp4由于某种原因而处于加密格式,并且我们需要在将其流式传输到浏览器之前对其进行解密,那么如何使其工作?
萨拉夫

@saraf:这取决于用于加密的算法。它适用于流还是仅作为整个文件加密?您是否可能只是将视频解密到临时位置并照常提供?一般来说,我有可能,但是可能会很棘手。这里没有通用的解决方案。

嗨,桐桐,谢谢您的回复!该用例是基于树莓派的设备,将用作教育内容开发人员的媒体分发平台。我们可以自由选择加密算法,密钥将在固件中-但内存限制为1GB RAM,内容大小约为200GB(将在可移动介质上-连接USB)。 EME没问题-除了铬在ARM上没有内置EME的问题之外。仅可移动介质本身不足以实现播放/复制。
萨拉夫

24

该问题的可接受答案很棒,应保留为可接受答案。但是,我遇到了一个问题,即读取流并不总是被终止/关闭。解决方案的一部分是autoClose: truestart:start, end:end第二个createReadStreamarg一起发送。

解决方案的另一部分是限制chunksize响应中发送的最大值。另一个答案end如下所示:

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

...的效果是,从请求的起始位置到文件的最后一个字节发送其余文件,无论可能有多少个字节。但是,客户端浏览器可以选择仅读取该流的一部分,如果不需要所有字节,则可以选择。这将导致流读取被阻塞,直到浏览器决定是时候获取更多数据为止(例如,用户操作(例如,搜寻/擦洗),或仅通过播放流)。

我需要关闭此流,因为我在<video>允许用户删除视频文件的页面上显示该元素。但是,直到客户端(或服务器)关闭连接后,才从文件系统中删除文件,因为这是结束/关闭流的唯一方法。

我的解决方案只是设置一个maxChunk配置变量,将其设置为1MB,并且永远不要通过管道将读取的超过1MB的流一次传送给响应。

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

这具有确保读取的流在每个请求之后结束/关闭并且不会被浏览器保持活动的效果。

我还写了一个单独的StackOverflow问题答案来解决此问题。


这对Chrome很有效,但在Safari中似乎不起作用。在野生动物园中,它似乎只有在可以请求整个范围时才起作用。您是否对Safari做任何不同的事情?
f1lt3r

2
进行进一步的挖掘时:Safari在2字节的响应中看到“ / $ {total}”,然后说:“嘿,您如何将整个文件发送给我?”。然后,当系统提示“不,您只获得第一个1Mb!”时,Safari感到不安“尝试引导资源时发生错误”。
f1lt3r

0

首先app.js在您要发布的目录中创建文件。

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

只需运行node app.js,您的服务器即可在端口8080上运行。除视频外,它还可以流式传输各种文件。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.