代码之家  ›  专栏  ›  技术社区  ›  loretoparisi

Node.js中的流式音频,内容范围

  •  1
  • loretoparisi  · 技术社区  · 6 年前

    我正在使用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',//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

    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]

    因此,我可以创建通过该范围的文件读取流:

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

    问题是我的浏览器在HTML5中没有任何音频 <audio> 内容范围 标题。 Here 你可以看到垃圾堆 ReadStream

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

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

    [更新]

    游猎 谷歌的Chrome ! 然后我可以假设 内容范围 它设计正确,但Chrome有一些缺陷。 现在,规范由 rfc2616 我现在严格遵守这一条 byte-range-resp-spec 所以我通过了

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

    根据RFC规范,这也适用于铬合金。它应该按照Mozilla文档的规定工作 here

    1 回复  |  直到 6 年前
        1
  •  7
  •   num8er    4 年前

    我正在使用 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);
    });
    

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