在 Node.js 中使用 Content-Range 流式传输音频 [英] Streaming audio in Node.js with Content-Range

查看:40
本文介绍了在 Node.js 中使用 Content-Range 流式传输音频的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在 Node.js 中使用流式服务器来流式传输 MP3 文件.虽然整个文件流式传输没问题,但我不能使用 Content-Range 标头将文件流式传输到开始位置并使用结束位置.

I'm using a streaming server in Node.js to stream MP3 files. While the whole file streaming it is ok, I cannot use the Content-Range header to stream the file seeking to a start position and util a end position.

我使用 ffprobe

ffprobe -i /audio/12380187.mp3 -show_frames -show_entries frame=pkt_pos -of default=noprint_wrappers=1:nokey=1 -hide_banner -loglevel panic -read_intervals 20%+#1

这会给我从 10 秒到第一个下一个数据包的确切字节.

That will give me the exact bytes from 10 seconds in this case to the first next packet.

这在 Node.js 中变得如此简单

This becomes in Node.js as simple as

  const args = [
      '-hide_banner',
      '-loglevel', loglevel,
      '-show_frames',//Display information about each frame
      '-show_entries', 'frame=pkt_pos',// Display only information about byte position
      '-of', 'default=noprint_wrappers=1:nokey=1',//Don't want to print the key and the section header and footer
      '-read_intervals', seconds+'%+#1', //Read only 1 packet after seeking to position 01:23
      '-print_format', 'json',
      '-v', 'quiet',
      '-i', fpath
    ];
    const opts = {
      cwd: self._options.tempDir
    };
    const cb = (error, stdout) => {
      if (error)
        return reject(error);
      try {
        const outputObj = JSON.parse(stdout);
        return resolve(outputObj);
      } catch (ex) {
        return reject(ex);
      }
    };
    cp.execFile('ffprobe', args, opts, cb)
      .on('error', reject);
  });

现在我有了开始和结束字节,我的媒体服务器将以这种方式从传递给它的自定义值中获取范围,例如 bytes=120515-240260

Now that I have start and end bytes, my media server will get the ranges in this way from a custom value passed to it like bytes=120515-240260

var getRange = function (req, total) {
  var range = [0, total, 0];
  var rinfo = req.headers ? req.headers.range : null;

  if (rinfo) {
    var rloc = rinfo.indexOf('bytes=');
    if (rloc >= 0) {
      var ranges = rinfo.substr(rloc + 6).split('-');
      try {
        range[0] = parseInt(ranges[0]);
        if (ranges[1] && ranges[1].length) {
          range[1] = parseInt(ranges[1]);
          range[1] = range[1] < 16 ? 16 : range[1];
        }
      } catch (e) {}
    }

    if (range[1] == total)
     range[1]--;

    range[2] = total;
  }

  return range;
};

此时我会得到这个范围[ 120515, 240260, 4724126 ],其中我有[startBytes,endBytes,totalDurationInBytes]

At this point I will get this range [ 120515, 240260, 4724126 ], where I have like [startBytes,endBytes,totalDurationInBytes]

因此,我可以创建一个传递该范围的文件读取流:

I therfore can create a file read stream passing that range:

var file = fs.createReadStream(path, {start: range[0], end: range[1]});

然后使用

  var header = {
    'Content-Length': range[1],
    'Content-Type': type,
    'Access-Control-Allow-Origin': req.headers.origin || "*",
    'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
    'Access-Control-Allow-Headers': 'POST, GET, OPTIONS'
  };

  if (range[2]) {
    header['Expires'] = 0;
    header['Pragma'] = 'no-cache';
    header['Cache-Control']= 'no-cache, no-store, must-revalidate';
    header['Accept-Ranges'] = 'bytes';
    header['Content-Range'] = 'bytes ' + range[0] + '-' + range[1] + '/' + total;
    header['Content-Length'] = range[2];
    //HTTP/1.1 206 Partial Content
    res.writeHead(206, header);
  } else {
    res.writeHead(200, header);
  }

所以获得

{
 "Content-Length": 4724126,
  "Content-Type": "audio/mpeg",
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
  "Access-Control-Allow-Headers": "POST, GET, OPTIONS",
  "Accept-Ranges": "bytes",
  "Content-Range": "bytes 120515-240260/4724126"
}

在做读取流到输出的管道之前

before doing the pipe of the read stream to the output

file.pipe(res);

问题是浏览器我在 HTML5 <audio> 标签中没有得到任何音频,而它在不使用任何 Content-Range 时正在流式传输内容代码>标题.这里你可以看到节点api的ReadStream对象的转储这表明范围是如何确定的

The problem is that the browser I don't get any audio in the HTML5 <audio> tag, while it was streaming the contents when not using any Content-Range header. Here you can see the dump of the ReadStream object from the node api that shows how the range was ok

  start: 120515,
  end: 240260,
  autoClose: true,
  pos: 120515

那么浏览器端发生了什么阻止加载文件?

So what is happening on the browser side that prevents to load the file?

[更新]

事实证明它可以在 Safari 中运行,但在 Google 的 Chrome 中不起作用!然后我可以假设 Content-Range 它设计正确,但 Chrome 有一些缺陷.现在规范是 rfc2616 我正在关注严格来说是 byte-range-resp-spec 所以我通过了

It turns out that it works Safari but not in Google's Chrome! I can then assume that the Content-Range it correctly devised, but Chrome has some flawness with it. Now the specification is by rfc2616 and I'm following strictly that one for the byte-range-resp-spec so I pass

  "Accept-Ranges": "bytes",
  "Content-Range": "bytes 120515-240260/4724126"

根据 RFC 规范,这也应该适用于 Chrome.这应该像 Mozilla 文档所指定的那样按原样工作 这里

and this should work on Chrome too according to the RFC specs. This it should work as-it-is as specified by Mozilla docs as well here

推荐答案

我正在使用 expressjs 框架,我已经做到了:

I'm using expressjs framework and I've made it like this:

// Readable Streams Storage Class
class FileReadStreams {
  constructor() {
    this._streams = {};
  }
  
  make(file, options = null) {
    return options ?
      fs.createReadStream(file, options)
      : fs.createReadStream(file);
  }
  
  get(file) {
    return this._streams[file] || this.set(file);
  }
  
  set(file) {
    return this._streams[file] = this.make(file);
  }
}
const readStreams = new FileReadStreams();

// Getting file stats and caching it to avoid disk i/o
function getFileStat(file, callback) {
  let cacheKey = ['File', 'stat', file].join(':');
  
  cache.get(cacheKey, function(err, stat) {
    if(stat) {
      return callback(null, stat);
    }
    
    fs.stat(file, function(err, stat) {
      if(err) {
        return callback(err);
      }
      
      cache.set(cacheKey, stat);
      callback(null, stat);
    });
  });
}

// Streaming whole file
function streamFile(file, req, res) {
  getFileStat(file, function(err, stat) {
    if(err) {
      console.error(err);
      return res.status(404).end();
    }
    
    let bufferSize = 1024 * 1024;
    res.writeHead(200, {
      'Cache-Control': 'no-cache, no-store, must-revalidate',
      'Pragma': 'no-cache',
      'Expires': 0,
      'Content-Type': 'audio/mpeg',
      'Content-Length': stat.size
    });
    readStreams.make(file, {bufferSize}).pipe(res);
  });
}

// Streaming chunk
function streamFileChunked(file, req, res) {
  getFileStat(file, function(err, stat) {
    if(err) {
      console.error(err);
      return res.status(404).end();
    }
    
    let chunkSize = 1024 * 1024;
    if(stat.size > chunkSize * 2) {
      chunkSize = Math.ceil(stat.size * 0.25);
    }
    let range = (req.headers.range) ? req.headers.range.replace(/bytes=/, "").split("-") : [];
    
    range[0] = range[0] ? parseInt(range[0], 10) : 0;
    range[1] = range[1] ? parseInt(range[1], 10) : range[0] + chunkSize;
    if(range[1] > stat.size - 1) {
      range[1] = stat.size - 1;
    }
    range = {start: range[0], end: range[1]};
    
    let stream = readStreams.make(file, range);
    res.writeHead(206, {
      'Cache-Control': 'no-cache, no-store, must-revalidate',
      'Pragma': 'no-cache',
      'Expires': 0,
      'Content-Type': 'audio/mpeg',
      'Accept-Ranges': 'bytes',
      'Content-Range': 'bytes ' + range.start + '-' + range.end + '/' + stat.size,
      'Content-Length': range.end - range.start + 1,
    });
    stream.pipe(res);
  });
}

router.get('/:file/stream', (req, res) => {

  const file = path.join('path/to/mp3/', req.params.file+'.mp3');
    
  if(/firefox/i.test(req.headers['user-agent'])) {
    return streamFile(file, req, res);
  }
  streamFileChunked(file, req, res);
});

网站的完整来源这里

尝试修复您的代码:

这将强制浏览器将资源作为分块处理.

this will enforce browser to act with resource as chunked.

var header = {
    'Content-Length': range[1],
    'Content-Type': type,
    'Access-Control-Allow-Origin': req.headers.origin || "*",
    'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
    'Access-Control-Allow-Headers': 'POST, GET, OPTIONS',
    'Cache-Control': 'no-cache, no-store, must-revalidate',
    'Pragma': 'no-cache',
    'Expires': 0
  };

  if(/firefox/i.test(req.headers['user-agent'])) {  
    res.writeHead(200, header);
  }
  else {
    header['Accept-Ranges'] = 'bytes';
    header['Content-Range'] = 'bytes ' + range[0] + '-' + range[1] + '/' + total;
    header['Content-Length'] = range[2];
    res.writeHead(206, header);
  }

这篇关于在 Node.js 中使用 Content-Range 流式传输音频的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆