将视频文件传输到具有节点的html5视频播放器。让视频控件继续工作?

时间:2022-06-20 00:11:03

Tl;Dr - The Question:

What is the right way to handle streaming a video file to an html5 video player with Node.js so that the video controls continue to work?

什么是正确的方式处理视频文件到一个html5视频播放器与节点。让视频控件继续工作?

I think it has to do with the way that the headers are handled. Anyway, here's the background information. The code is a little lengthy, however, it's pretty straightforward.

我认为这与头的处理方式有关。总之,这是背景资料。代码有点长,但是,非常简单。

Streaming small video files to HTML5 video with Node is easy

I learned how to stream small video files to an HTML5 video player very easily. With this setup, the controls work without any work on my part, and the video streams flawlessly. A working copy of the fully working code with sample video is here, for download on Google Docs.

我学会了如何很容易地将小型视频文件传输到HTML5视频播放器。有了这个设置,控件就可以在我的部分不需要任何工作的情况下工作,而且视频可以完美地传输。这里有一个完整的示例视频代码的工作拷贝,可以在谷歌文档中下载。

Client:

客户:

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

Server:

服务器:

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

But this method is limited to files < 1GB in size.

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

Streaming (any size) video files with fs.createReadStream

By utilizing fs.createReadStream(), the server can read the file in a stream rather than reading it all into memory at once. This sounds like the right way to do things, and the syntax is extremely simple:

通过使用fs.createReadStream(),服务器可以在流中读取文件,而不是一次将所有文件读入内存。这听起来是正确的做法,语法非常简单:

Server Snippet:

服务器代码片段:

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

This streams the video just fine! But the video controls no longer work.

这个视频播放得很好!但是视频控制不再有效。

2 个解决方案

#1


94  

The Accept Ranges header (the bit in writeHead()) is required for the HTML5 video controls to work.

接受范围标头(writeHead()中的位)是HTML5视频控件工作所必需的。

I think instead of just blindly send the full file, you should first check the Accept Ranges header in the REQUEST, then read in and send just that bit. fs.createReadStream support start, and end option for that.

我认为,与其盲目地发送完整的文件,不如先检查请求中的Accept Ranges标头,然后读取并发送该位。fs。createReadStream支持启动和结束选项。

So I tried an example and it works. The code is not pretty but it is easy to understand. First we process the range header to get the start/end position. Then we use fs.stat to get the size of the file without reading the whole file into memory. Finally, use fs.createReadStream to send the requested part to the client.

我尝试了一个例子,它是有效的。代码并不漂亮,但是很容易理解。首先,我们处理范围标头以获得开始/结束位置。然后我们使用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);

#2


15  

The accepted answer to this question is awesome and should remain the accepted answer. However I ran into an issue with the code where the read stream was not always being ended/closed. Part of the solution was to send autoClose: true along with start:start, end:end in the second createReadStream arg.

这个问题的公认答案是令人敬畏的,应该是公认的答案。然而,我遇到了一个问题:读取流并不总是结束/关闭。解决方案的一部分是在第二个createReadStream arg中发送autoClose: true和start:start、end:end。

The other part of the solution was to limit the max chunksize being sent in the response. The other answer set end like so:

解决方案的另一部分是限制响应中发送的最大chunksize。另一个答案的结尾是:

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

...which has the effect of sending the rest of the file from the requested start position through its last byte, no matter how many bytes that may be. However the client browser has the option to only read a portion of that stream, and will, if it doesn't need all of the bytes yet. This will cause the stream read to get blocked until the browser decides it's time to get more data (for example a user action like seek/scrub, or just by playing the stream).

…它的作用是将文件的其余部分从请求的开始位置通过它的最后一个字节发送出去,无论这个字节有多少个字节。但是,客户端浏览器可以选择只读取流的一部分,如果它还不需要所有的字节,就会读取。这将导致流读取被阻塞,直到浏览器决定获取更多数据的时候(例如,用户行为如查找/删除,或者仅仅通过播放流)。

I needed this stream to be closed because I was displaying the <video> element on a page that allowed the user to delete the video file. However the file was not being removed from the filesystem until the client (or server) closed the connection, because that is the only way the stream was getting ended/closed.

我需要关闭这个流,因为我在一个允许用户删除视频文件的页面上显示

My solution was just to set a maxChunk configuration variable, set it to 1MB, and never pipe a read a stream of more than 1MB at a time to the response.

我的解决方案是设置一个maxChunk配置变量,将其设置为1MB,并且在响应时从不管道a读取超过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;
}

This has the effect of making sure that the read stream is ended/closed after each request, and not kept alive by the browser.

这样做的效果是确保每个请求后读流结束/关闭,并且浏览器不会让读流保持活动状态。

I also wrote a separate * question and answer covering this issue.

我还写了一个关于这个问题的*的问题和答案。

#1


94  

The Accept Ranges header (the bit in writeHead()) is required for the HTML5 video controls to work.

接受范围标头(writeHead()中的位)是HTML5视频控件工作所必需的。

I think instead of just blindly send the full file, you should first check the Accept Ranges header in the REQUEST, then read in and send just that bit. fs.createReadStream support start, and end option for that.

我认为,与其盲目地发送完整的文件,不如先检查请求中的Accept Ranges标头,然后读取并发送该位。fs。createReadStream支持启动和结束选项。

So I tried an example and it works. The code is not pretty but it is easy to understand. First we process the range header to get the start/end position. Then we use fs.stat to get the size of the file without reading the whole file into memory. Finally, use fs.createReadStream to send the requested part to the client.

我尝试了一个例子,它是有效的。代码并不漂亮,但是很容易理解。首先,我们处理范围标头以获得开始/结束位置。然后我们使用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);

#2


15  

The accepted answer to this question is awesome and should remain the accepted answer. However I ran into an issue with the code where the read stream was not always being ended/closed. Part of the solution was to send autoClose: true along with start:start, end:end in the second createReadStream arg.

这个问题的公认答案是令人敬畏的,应该是公认的答案。然而,我遇到了一个问题:读取流并不总是结束/关闭。解决方案的一部分是在第二个createReadStream arg中发送autoClose: true和start:start、end:end。

The other part of the solution was to limit the max chunksize being sent in the response. The other answer set end like so:

解决方案的另一部分是限制响应中发送的最大chunksize。另一个答案的结尾是:

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

...which has the effect of sending the rest of the file from the requested start position through its last byte, no matter how many bytes that may be. However the client browser has the option to only read a portion of that stream, and will, if it doesn't need all of the bytes yet. This will cause the stream read to get blocked until the browser decides it's time to get more data (for example a user action like seek/scrub, or just by playing the stream).

…它的作用是将文件的其余部分从请求的开始位置通过它的最后一个字节发送出去,无论这个字节有多少个字节。但是,客户端浏览器可以选择只读取流的一部分,如果它还不需要所有的字节,就会读取。这将导致流读取被阻塞,直到浏览器决定获取更多数据的时候(例如,用户行为如查找/删除,或者仅仅通过播放流)。

I needed this stream to be closed because I was displaying the <video> element on a page that allowed the user to delete the video file. However the file was not being removed from the filesystem until the client (or server) closed the connection, because that is the only way the stream was getting ended/closed.

我需要关闭这个流,因为我在一个允许用户删除视频文件的页面上显示

My solution was just to set a maxChunk configuration variable, set it to 1MB, and never pipe a read a stream of more than 1MB at a time to the response.

我的解决方案是设置一个maxChunk配置变量,将其设置为1MB,并且在响应时从不管道a读取超过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;
}

This has the effect of making sure that the read stream is ended/closed after each request, and not kept alive by the browser.

这样做的效果是确保每个请求后读流结束/关闭,并且浏览器不会让读流保持活动状态。

I also wrote a separate * question and answer covering this issue.

我还写了一个关于这个问题的*的问题和答案。