我在Node.js中使用流服务器来流MP3文件。虽然可以流式传输整个文件,但我不能使用Content- Range标题将文件流式传输到起始位置并使用终止位置。
Content- Range
我使用ffprobelike 从秒计算开始和结束字节
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',//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
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]
[ 120515, 240260, 4724126 ]
[startBytes,endBytes,totalDurationInBytes]
因此,我可以创建一个通过该范围的文件读取流:
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" }
在将读取流的管道连接到输出之前
file.pipe(res);
问题是浏览器<audio>在不使用任何Content-Range标题时正在流式传输内容时,HTML5 标签中没有任何音频。 在这里,您可以看到ReadStream来自节点api 的对象转储,显示了范围如何
<audio>
Content-Range
ReadStream
start: 120515, end: 240260, autoClose: true, pos: 120515
那么,在浏览器端发生了什么事情而导致无法加载文件?
[更新]
事实证明,它可以在 Safari中运行, 但不能在 Google的Chrome中运行 !然后,我可以假设Content- Range它是正确设计的,但是Chrome确实存在一些缺陷。现在规范是由rfc2616制定的,我严格遵循那个规范,byte- range-resp-spec所以我通过了
byte- range-resp-spec
"Accept-Ranges": "bytes", "Content-Range": "bytes 120515-240260/4724126"
根据RFC规范,这在Chrome上也应适用。这是-它- 作为由Mozilla文档规定,以及它应该工作在这里
我正在使用expressjs框架,并且已经做到了:
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); } 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); });
网站的完整资源在这里
尝试修复您的代码:
这将强制浏览器对资源进行分块处理。
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); }