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

2022-08-30 05:43:19

Tl;Dr - 问题:

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

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

使用Node将小视频文件流式传输到HTML5视频很容易

我学会了如何轻松地将小视频文件流式传输到HTML5视频播放器。通过此设置,控件无需我进行任何工作即可工作,并且视频流完美无缺。带有示例视频的完整工作代码的工作副本在此处,可在Google Docs上下载

客户:

<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

标头(中的位)是 HTML5 视频控件正常工作所必需的。Accept RangeswriteHead()

我认为,与其盲目地发送完整的文件,不如首先检查 REQUEST 中的标头,然后读入并发送该位。 支持 ,以及相应的选项。Accept Rangesfs.createReadStreamstartend

所以我尝试了一个例子,它的工作原理。代码不漂亮,但很容易理解。首先,我们处理范围标头以获取开始/结束位置。然后,我们用于获取文件的大小,而无需将整个文件读取到内存中。最后,使用 将请求的部件发送到客户端。fs.statfs.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);

答案 2

这个问题的公认答案很棒,应该仍然是公认的答案。但是,我遇到了一个代码问题,其中读取流并不总是被结束/关闭。部分解决方案是在第二个参数中一起发送。autoClose: truestart:start, end:endcreateReadStream

解决方案的另一部分是限制响应中发送的最大值。另一个答案是这样设置的:chunksizeend

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

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

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

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

// 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问题答案,涵盖了这个问题。