开发者问题收集

如何动态加载和传输大型视频文件到不发送范围标头的 IE?

2017-05-25
2314

不久前,我在一些流式视频教程中发现了这个函数,这样就不需要将整个文件加载到 RAM 中来提供文件(因此,您可以提供大视频文件,而不会因为超出 Node.js 的内存上限而崩溃 - 对于电影长度的视频文件来说,超出这个上限并不难,增加内存分配只是一个权宜之计)。

var fs = require("fs"), 
    http = require("http"), 
    url = require("url"), 
    path = require("path");
var dirPath = process.cwd();
var videoReqHandler = function(req, res, pathname) {
    var file = dirPath + "/client" + pathname;
    var range = req.headers.range;
    var positions = range.replace(/bytes=/, "").split("-");
    var start = parseInt(positions[0], 10);
    fs.stat(file, function(err, stats) {
        if (err) {
            throw err;
        } else {
            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);
            });
        }
    });
};
module.exports.handle = videoReqHandler;

它在 Chrome 和 FF 中运行良好,但是,当 Internet Explorer 或 Edge(新名称,同样可怜的功能支持)请求 mp4 时,它会导致服务器崩溃。

var positions = range.replace(/bytes=/, "").split("-");
                     ^
TypeError: Cannot read property 'replace' of undefined

我怀疑这是因为 范围标头不是强制性的,而此功能需要它们

我的问题是,如何修改此功能以避免在未发送 range 标头时崩溃?我对标头或视频流了解不多,而且我对分块读取文件的理解也不太清楚,所以我真的需要一些关于这个涉及所有这三个功能的帮助。


根据评论我尝试过的方法

到目前为止,基于 @AndréDion 链接的答案

Figured it out. IE11 does support pseudo streaming but it wants the "Accept-Ranges: bytes" header before it will bother requesting a range, so the server needs to respond with that regardless of whether it is actually sending a byte range. I had to modify my vid-streamer module to do that.

我尝试将处理程序代码包装在:

if (req.headers.range) {
    console.log('Video request sent WITH range header!');
    // ... handler code ...
} else {
    console.log('Video request sent without range header!');
    res.writeHead(206, {
        "Accept-Ranges": "bytes"
    });
    res.end();
}

但似乎什么也没发生 - 服务器停止崩溃,并继续在其他浏览器上运行,但 IE 似乎没有加载视频。我原本希望 IE 等待该响应,然后发送另一个请求,这次包含一个范围标头,但似乎没有发生这种情况。

也许我需要在向 IE 发送 accept-ranges 标头时发送不同于 206 的响应代码?我不知道它会是什么,但是当我使用 accept-rages 标头进行响应时,IE 似乎会重复请求两次(但不包含范围标头,这是我需要的)。

就目前情况而言,如果我使用如上所示的条件运行服务器,然后尝试在 IE 中加载视频一次,然后尝试在 Chrome 中加载它,我会收到以下日志:

Video request sent without range header!
Video request sent without range header!
Video request sent without range header!
Video request sent WITH range header!

IE 发送三个没有范围标头的请求,Chrome 当然总是按预期发送范围标头,发送一个请求并成功加载视频。我只是不确定接下来该怎么做。

2个回答

看起来 Internet Explorer 需要的不仅仅是 Accept-Ranges 标头。 此 MSDN 论坛帖子 表明您还需要使用 If-Range 标头 进行响应,该标头返回 ETag 。类似这样的代码:

console.log('Video request sent without range header!');
res.writeHead(206, {
    "Accept-Ranges": "bytes",
    "If-Range": "\"<ETag#>\""
});

很遗憾,我现在无法进行任何测试,所以请告诉我进展如何,有机会时我会进一步调查并编辑我的帖子。


更新:

我进行了进一步调查,发现 If-Range 不是必需的。事实上,似乎这个请求处理程序对于 Internet Explorer 来说不够强大。这是我的运行代码:

var videoReqHandler = function(req, res, pathname) {
    var file = path.resolve(__dirname, pathname);

    fs.stat(file, function(err, stats) {
        if (err) {
            if (err.code === 'ENOENT'){
                //404 Error if file not found
                res.writeHead(404, {
                    "Accept-Ranges" : "bytes",
                    "Content-Range" : "bytes " + start + "-" + end + "/" + total,
                    "Content-Length" : chunksize,
                    "Content-Type" : "video/mp4"
                });
            }
            res.end(err);
        }

        var start;
        var end;
        var chunksize;
        var total = stats.size;

        var range = req.headers.range;
        if (range) {

            var positions = range.replace(/bytes=/, "").split("-");
            end = positions[1] ? parseInt(positions[1], 10) : total - 1;
            start = parseInt(positions[0], 10);
        }else{
            start = 0;
            end = total - 1;
        }


        if (start > end || start < 0 || end > total - 1){
            //error 416 is "Range Not Satisfiable"
            res.writeHead(416, {
                "Accept-Ranges" : "bytes",
                "Content-Range" : "*/" + stats.size,
                "Content-Type" : "video/mp4"
            });
            res.end();
            return;
        }

        if (start == 0 && end == total -1){
            res.writeHead(200, {
                "Accept-Ranges" : "bytes",
                "Content-Range" : "bytes " + start + "-" + end + "/" + total,
                "Content-Length" : total,
                "Content-Type" : "video/mp4"
            });
        }else{
            res.writeHead(206, {
                "Accept-Ranges" : "bytes",
                "Content-Range" : "bytes " + start + "-" + end + "/" + total,
                "Content-Length" : end - start + 1,
                "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);
        });
    });
};

这增加了一些缺失的错误检查,例如确保请求的范围是有效范围,并且如果请求没有范围标头,则返回带有代码 200 的整个文件。不过,还有几件事要做,才能让它在生产服务器上坚如磐石:

  • 更多研究表明, If-Range 标头 的目的是处理有人请求文件的一部分、它在服务器上发生更改、然后他们请求文件的另一部分的情况。If-Range 允许检测到这种情况,服务器可以发送整个新文件,而不是破坏所有内容,并让可怜的请求者对新文件的一部分感到困惑。
  • 此代码不会检查以确保请求的范围以字节为单位。如果请求指定任何其他度量单位,此代码可能会使服务器崩溃。应该检查一下。
  • 范围标头允许一次指定多个范围。请求多个范围时的响应由标头和由可指定的拆分字符串分隔的请求数据组成。 有关 MDN 的更多信息 。这一点值得商榷,您可以说,只要有更多内容不会导致任何崩溃,您就可以在生产服务器上只处理第一部分,但我认为这是规范的一部分。要完全符合规范,您需要实现它,而我厌倦了遇到不完全符合规范的服务。

最后一点说明:此代码使 IE 和 Edge 中的功能正常运行,但我确实注意到,出于某种原因,它们最终请求了两次完整文件!在我的设置下,请求如下:

  1. 使用“Range bytes=0-”请求部分文件,换句话说,请求完整文件
  2. 使用正常 Range 语句请求文件的一小部分
  3. 请求完整文件,但不使用 Range 标头

最后要说明的是,在我添加使用完整文件和 200 状态进行响应的功能之前,当没有 Range 标头时,IE 会显示视频的黑框,上面印有“解码错误”。如果有人能告诉我原因,那就太好了!

ashbygeek
2017-06-01

我理解 Viziionary 希望坚持使用原生 Node.js,但对于看到此消息但又不太想坚持使用 Node 的其他用户,这里有一些无需手动提供静态文件的替代方案。

  1. 如果您想坚持使用 Node,但又不介意添加框架, Express 框架 具有提供静态文件和目录的功能。使用 Express,只需一两行代码即可提供具有范围请求和 etag 支持的静态文件。这比编写自己的请求处理程序要容易得多。

  2. 如果您不介意脱离 Node,您可以 将 Node 与 Nginx 配对 。在此配置中,Nginx 是一个前端,它提供静态文件并将 Web 请求代理到您的 Node 服务器。事实上, Nginx 就是按这种方式设计的 。请注意,您可以代理到任意数量的完全不同的服务器,

  3. 您也可以在前端使用 Apache 执行此操作,我留给您自己查找如何操作。我从未这样做过,所以我所能做的就是引导您找到一个有用的教程。

我确信还有其他配置,但这应该会给您一些思考的空间。对于大多数中小型网站,您很难注意到使用一个系统或另一个系统的任何负面影响。如果您正在运营一个大型网站或服务,那么您最好做好功课,因为还有很多其他选项和因素需要考虑:CDN、其他脚本语言、缓存、可扩展性等等。

ashbygeek
2017-06-05