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

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

问题描述

我在Node.js中使用流媒体服务器来传输MP3文件。虽然整个文件流是可以的,但是我不能使用 Content-Range 标头来流式传输寻找起始位置的文件并使用最终位置。



我使用 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秒到第一个下一个数据包的确切字节数。



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

  const args = [
'-hide_banner',
'-loglevel',loglevel,
'-show_frames',//显示每个帧的信息
'-show_entries','frame = pkt_pos',/ /仅显示有关字节位置的信息
'-of','default = noprint_wrappers = 1:nokey = 1',//不想打印键和节头和页脚
'-read_intervals',秒+'%+#1',//在寻找位置01:23后只读取1个包
'-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

  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(范围[1]&& range [1] .length){
range [1] = parseInt(范围[1]);
range [1] = range [1]< 16? 16:范围[1];
}
} catch(e){}
}

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

范围[2] =总计;
}

返回范围;
};

此时我将获得此范围 [120515,240260,4724126] ,我喜欢 [startBytes,endBytes,totalDurationInBytes]



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

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

然后使用



<$组成响应标头p $ p> 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部分内容
res.writeHead(206,header);
} else {
res.writeHead(200,header);
}

所以要获得

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

在执行输出到输出的管道之前

  file.pipe(RES); 

问题是浏览器我没有在HTML5中获得任何音频< audio> 标记,同时在不使用任何 Content-Range 标头时播放内容。
这里你可以看到 ReadStream的转储来自节点api的对象,显示范围如何正常

  start:120515,
结束:240260,
autoClose:true,
pos:120515

那么什么是在浏览器端阻止加载文件吗?



[更新]



事实证明它适用于 Safari ,但不适用于 Google的Chrome !我可以假设它正确设计了 Content-Range ,但Chrome有一些缺陷。
现在规范是 rfc2616 我严格遵循 byte-range-resp-spec 的那一个所以我传递

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

根据RFC规范,这也适用于Chrome。它应该按照Mozilla文档的规定工作,以及这里

解决方案

我正在使用 expressjs 框架我已经这样做了:

  //可读流存储类
类FileReadStreams {
constructor(){
this._streams = {};
}

make(file,options = null){
返回选项?
fs.createReadStream(file,options)
:fs.createReadStream(file);
}

get(file){
返回this._streams [file] || this.set(文件);
}

set(file){
return this._streams [file] = this.make(file);
}
}
const readStreams = new FileReadStreams();

//获取文件统计信息并缓存它以避免磁盘i / o
函数getFileStat(文件,回调){
let cacheKey = ['File','stat',文件]。加入( ':');

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

//流媒体整个文件
函数streamFile(file,req,res){
getFileStat(file,function(err,stat){
if(err){
console.error(err);
返回res.status(404);
}

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

//流媒体块
函数streamFileChunked(file,req,res){
getFileStat(file,function(err,stat){
if(错误){
console.error(err);
返回res.status(404);
}

让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,必须重新验证',
'Pragma':'no-cache',
'到期':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);
});

网站的完整来源 here



尝试修复您的代码:



这将强制浏览器将资源作为分块进行操作。

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


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.

I calculate the start and end bytes from seconds using ffprobe like

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

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

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

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

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

and then compose the response header using

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

so to obtain

{
 "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);

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?

[UPDATE]

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"

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

解决方案

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

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

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

Full sources of site here

Try to fix to Your code:

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天全站免登陆