使用node.js通过HTML 5进行视频流传输

46
我正在尝试使用node.js搭建一个 Web 服务器,以支持通过 HTML5 视频标签流媒体视频。以下是我的代码:
var range = request.headers.range;
var total = file.length;

var parts = range.replace(/bytes=/, "").split("-");
var partialstart = parts[0];
var partialend = parts[1];

var start = parseInt(partialstart, 10);
var end = partialend ? parseInt(partialend, 10) : total-1;

var chunksize = (end-start)+1;

response.writeHead(206, { "Content-Range": "bytes " + start + "-" + end + "/" + total, "Accept-Ranges": "bytes", "Content-Length": chunksize, "Content-Type": type });
response.end(file);

"request"代表http请求,type可以是"application/ogg"或"video/ogg"(我尝试过两种),而"file"则是从文件系统中读取的.ogv文件。以下是响应头:

Content-Range   bytes 0-14270463/14270464
Accept-Ranges   bytes
Content-Length   14270464
Connection     keep-alive
Content-Type     video/ogg

我已经检查了响应头,这段代码似乎工作正常,但存在几个问题:
  1. 视频在本地网络上加载速度非常慢。从我使用Firebug检查响应的结果来看,文件似乎以约150 kb / sec的速度流入。
  2. 视频根本无法播放。即使我等待整个视频完全加载,Firefox中的HTML 5视频标签也只显示一个大的“x”,而不是电影。

有人有什么想法可以让我通过node.js实现视频流吗?

谢谢!
Chris

8个回答

25

我知道这是一个非常古老的问题,但是由于谷歌似乎很喜欢它,所以我认为值得指出的是,我编写了一个Node.js视频流模块(可以通过Github或NPM获取),希望也值得一看。


6
我喜欢这种回答!谢谢 :) - Vasyl Boroviak
那么回到那个话题,两年后... :) 有没有一种方法可以使用该脚本将已通过UDP接收的实时数据发送到浏览器? - randomuser1
@randomuser1 不好意思,实时流媒体更加复杂,需要对输入进行分段并支持索引文件,而这个脚本无法实现。我很想支持实时流媒体,但是目前还没有时间去开发。非常抱歉。 - meloncholy
1
嘿@meloncholy,实际上我已经做完了-我“分割”了输入,放置了一些索引,然后通过UDP发送它。现在我可以在Node.js中读取它们并查看它们-每个段的数量及其内容。但是我只能在控制台中看到它们,我想在用户浏览器的另一个站点上合并它们-我不知道是否可能。 - randomuser1

19

在nodejs论坛的帮助下,我成功实现了这个功能:

http://groups.google.com/group/nodejs/browse_thread/thread/8339e0dc825c057f/822b2dd48f36e890

以下是Google Groups线程中的要点:

众所周知,Google Chrome首先会以范围0-1024发起请求,然后请求范围为“1024-”。

response.end(file.slice(start, chunksize), "binary");

然后:

通过将“connection”标头设置为“close”,我成功地使视频在Firefox中播放,没有任何问题

然后:

看起来你错误地计算了内容长度:

var chunksize = (end-start)+1;

如果start为0且end为1,则在你的情况下chunksize为2,而应该为1。


我相信块大小是正确的。根据https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html:`byte-range-spec中的first-byte-pos值给出范围内第一个字节的字节偏移量。last-byte-pos值给出范围内最后一个字节的字节偏移量;也就是说,指定的字节位置是包含在内的。字节偏移从零开始。` - sthomps

14
这个解决方案可以异步读取服务器端的视频或音频媒体文件……它会在以下 URL 处启动一个 Node.js 服务器:

http://localhost:8888/

此外,它可以正确处理客户端 HTML5(浏览器/应用)前进/后退 UI 小部件滑块的移动。
请将下面的代码片段保存为服务器端文件:
media_server.js

...在服务器端执行它使用

node media_server.js

享受

var http = require('http'),
    fs = require('fs'),
    util = require('util');

var path = "/path/to/local/video/or/audio/file/on/server.mp4";

var port = 8888;
var host = "localhost";
 
http.createServer(function (req, res) {

  var stat = fs.statSync(path);
  var total = stat.size;

  if (req.headers.range) { // meaning client (browser) has moved the forward/back slider
                           // which has sent this request back to this server logic ... cool
    var range = req.headers.range;
    var parts = range.replace(/bytes=/, "").split("-");
    var partialstart = parts[0];
    var partialend = parts[1];
 
    var start = parseInt(partialstart, 10);
    var end = partialend ? parseInt(partialend, 10) : total-1;
    var chunksize = (end-start)+1;
    console.log('RANGE: ' + start + ' - ' + end + ' = ' + chunksize);
 
    var file = fs.createReadStream(path, {start: start, end: end});
    res.writeHead(206, { 'Content-Range': 'bytes ' + start + '-' + end + '/' + total, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4' });
    file.pipe(res);

  } else {

    console.log('ALL: ' + total);
    res.writeHead(200, { 'Content-Length': total, 'Content-Type': 'video/mp4' });
    fs.createReadStream(path).pipe(res);
  }
}).listen(port, host);

console.log("Server running at http://" + host + ":" + port + "/");

2
有没有一种方法可以从UDP流而不是源文件向浏览器流式传输视频文件? - randomuser1
我们能从Express中实现吗?我是Node的新手 :) - Vikas Bansal

6

根据Sam9291的回答,我使用createReadStream()重写了这个函数,并解决了一些问题:

/**
 * Sends a static file to the HTTP client, supporting partial transfers.
 * 
 * @req HTTP request object
 * @res HTTP response object
 * @fn Path to file that should be sent
 * @contentType MIME type for the response (defaults to HTML)
 */      
function sendFile(req, res, fn, contentType) {

  contentType = contentType || "text/html";

  fs.stat(fn, function(err, stats) {
    var headers;

    if (err) {
      res.writeHead(404, {"Content-Type":"text/plain"});
      res.end("Could not read file");
      return;
    }

    var range = req.headers.range || "";    
    var total = stats.size;

    if (range) {

      var parts = range.replace(/bytes=/, "").split("-");
      var partialstart = parts[0];
      var partialend = parts[1];

      var start = parseInt(partialstart, 10);
      var end = partialend ? parseInt(partialend, 10) : total-1;

      var chunksize = (end-start)+1;

      headers = { 
        "Content-Range": "bytes " + start + "-" + end + "/" + total, 
        "Accept-Ranges": "bytes", 
        "Content-Length": chunksize, 
        "Content-Type": contentType 
      };
      res.writeHead(206, headers);

    } else {

      headers = { 
        "Accept-Ranges": "bytes", 
        "Content-Length": stats.size, 
        "Content-Type": contentType 
      };
      res.writeHead(200, headers);

    }

    var readStream = fs.createReadStream(fn, {start:start, end:end});
    readStream.pipe(res);    

  });

}

4

我正在使用基于Node.js的MVC框架sails.js,并且通过以下代码成功地运行了它:

/**
 * VideoController
 *
 * @module      :: Controller
 * @description :: Contains logic for handling requests.
 */

 var fs = require('fs');

module.exports = {

  /* e.g.
  sayHello: function (req, res) {
    res.send('hello world!');
  }
  */

  /**
   * /video/stream
   */ 
  stream: function (req,res) {

    // This will render the view: 
    // C:\Users\sam\Documents\Dev\Fun\mymoviebank/views/video/stream.ejs
    res.view();

  },

  play: function (req,res) {

    fs.readFile('/Users/sam/Videos/big_buck_bunny.mp4', function (err, data) {
      if (err) throw err;

      var range = req.headers.range;
        var total = data.length;

        var parts = range.replace(/bytes=/, "").split("-");
        var partialstart = parts[0];
        var partialend = parts[1];

        var start = parseInt(partialstart, 10);
        var end = partialend ? parseInt(partialend, 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' });
        res.end(data);

    });

  }

};

希望这可以帮到你。

1
npm的fs.statSync(../file/path/...)会处理细节。请参见https://gist.github.com/westonplatter/7559003。 - westonplatter
3
这个解决方案不可扩展——它将整个视频文件加载到内存中,仅提供其中一小部分。使用fs.createReadStream(theFile, { start : $START, end : #END })可以让你将流式传输(pipe)到响应中,而不需要将整个视频文件加载到内存中(想象一下如果有1000个用户同时这么做会怎样)。 - Tim Boudreau

3
我找到了一个解决方案,它似乎更简单,而且对我有用(与被勾选的答案不同)。 (我尝试适应线程末尾的coffeescript解决方案,一旦我处理了最初的请求(“bytes = 0-”),它就起作用了。)
实际的实现:http://elegantcode.com/2011/04/06/taking-baby-steps-with-node-js-pumping-data-between-streams/
function stream_response( res, file_path, content_type ){
    var readStream = fs.createReadStream(file_path);

    readStream.on('data', function(data) {
        var flushed = res.write(data);
        // Pause the read stream when the write stream gets saturated
        console.log( 'streaming data', file_path );
        if(!flushed){
            readStream.pause();
        }
    });

    res.on('drain', function() {
        // Resume the read stream when the write stream gets hungry 
        readStream.resume();
    });

    readStream.on('end', function() {
        res.end();
    });

    readStream.on('error', function(err) {
        console.error('Exception', err, 'while streaming', file_path);
        res.end();
    });

    res.writeHead(200, {'Content-Type': content_type});
}

这个程序可以很好地流媒体...但是它需要处理request.headers才能响应客户端小部件请求,比如在源媒体上跳过前进/后退...做得好。 - Scott Stensland

0

0
当使用express时,请将此放入您的media_server.js或index.js文件中,它将在3000端口上提供媒体服务。
const express = require('express')
const fs = require('fs')
const path = require('path')
const app = express()

app.use(express.static(path.join(__dirname, 'public')))

app.get('/', function(req, res) {
  res.sendFile(path.join(__dirname + '/index.html'))
})

app.get('/video', function(req, res) {
  const path = 'assets/sample.mp4'// your video path
  const stat = fs.statSync(path)
  const fileSize = stat.size
  const range = req.headers.range

  if (range) {
    const parts = range.replace(/bytes=/, "").split("-")
    const start = parseInt(parts[0], 10)
    const end = parts[1]
      ? parseInt(parts[1], 10)
      : fileSize-1

    const chunksize = (end-start)+1
    const file = fs.createReadStream(path, {start, end})
    const head = {
      'Content-Range': `bytes ${start}-${end}/${fileSize}`,
      'Accept-Ranges': 'bytes',
      'Content-Length': chunksize,
      'Content-Type': 'video/mp4',
    }

    res.writeHead(206, head)
    file.pipe(res)
  } else {
    const head = {
      'Content-Length': fileSize,
      'Content-Type': 'video/mp4',
    }
    res.writeHead(200, head)
    fs.createReadStream(path).pipe(res)
  }
})

app.listen(3000, function () {
  console.log('Listening on port 3000!')
})

然后在你的index.html文件中

<html>
  <head>
    <title>Video stream sample</title>
  </head>
  <body>
    <video id="videoPlayer" controls muted="muted" autoplay> 
      <source src="http://localhost:3000/video" type="video/mp4">
    </video>
  </body>
</html>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接