簡體   English   中英

使用 Content-Range 在 Node.js 中流式傳輸音頻

[英]Streaming audio in Node.js with 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',//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>標簽中沒有收到任何音頻,而它在不使用任何Content-Range標頭時正在流式傳輸內容。 在這里,您可以從節點 api 中看到ReadStream對象的轉儲,該轉儲顯示了范圍是否正常

  start: 120515,
  end: 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框架,我已經做到了:

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

網站的完整來源在這里

嘗試修復您的代碼:

這將強制瀏覽器將資源作為分塊處理。

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