简体   繁体   English

使用 Content-Range 在 Node.js 中流式传输音频

[英]Streaming audio in Node.js with Content-Range

I'm using a streaming server in Node.js to stream MP3 files.我在 Node.js 中使用流式服务器来流式传输 MP3 文件。 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.虽然整个文件流式传输没问题,但我无法使用Content-Range标头将文件流式传输到开始位置并使用结束位置。

I calculate the start and end bytes from seconds using ffprobe like我使用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

That will give me the exact bytes from 10 seconds in this case to the first next packet.在这种情况下,这将为我提供从 10 秒到第一个下一个数据包的确切字节。

This becomes in Node.js as simple as这在 Node.js 中变得非常简单

  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现在我有了开始和结束字节,我的媒体服务器将以这种方式从传递给它的自定义值中获取范围,例如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]此时我会得到这个范围[ 120515, 240260, 4724126 ] ,我喜欢[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.问题是浏览器在 HTML5 <audio>标签中没有收到任何音频,而它在不使用任何Content-Range标头时正在流式传输内容。 Here you can see the dump of the ReadStream object from the node api that shows how the range was ok 在这里,您可以从节点 api 中看到ReadStream对象的转储,该转储显示了范围是否正常

  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 !事实证明它可以在Safari 中运行,但不能在Google 的 Chrome 中运行 I can then assume that the Content-Range it correctly devised, but Chrome has some flawness with it.然后我可以假设它正确设计了Content-Range ,但 Chrome 有一些缺陷。 Now the specification is by rfc2616 and I'm following strictly that one for the byte-range-resp-spec so I pass现在规范是由rfc2616 制定的,我严格遵循byte-range-resp-spec所以我通过了

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

and this should work on Chrome too according to the RFC specs.根据 RFC 规范,这也应该适用于 Chrome。 This it should work as-it-is as specified by Mozilla docs as well here这是-它-作为由Mozilla文档规定,以及它应该工作在这里

I'm using expressjs framework and I've made it like this:我正在使用expressjs框架,我已经做到了:

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

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM