Streaming audio in Node.js with Content-Range











up vote
1
down vote

favorite
4












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










share|improve this question




























    up vote
    1
    down vote

    favorite
    4












    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










    share|improve this question


























      up vote
      1
      down vote

      favorite
      4









      up vote
      1
      down vote

      favorite
      4






      4





      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










      share|improve this question















      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







      javascript node.js audio-streaming






      share|improve this question















      share|improve this question













      share|improve this question




      share|improve this question








      edited Nov 9 at 15:07

























      asked Nov 9 at 13:26









      loretoparisi

      7,47554770




      7,47554770
























          1 Answer
          1






          active

          oldest

          votes

















          up vote
          1
          down vote



          accepted










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





          share|improve this answer



















          • 1




            Thank you, so cool this web site!!! Let me check how to adapt your solution, it seems it should work!
            – loretoparisi
            Nov 9 at 14:49






          • 1




            @loretoparisi keep in mind js player in site does not allow to jump not-cached part. But by sending content range header from client side You can jump anywhere. I've checked in Safari using touch-bar and seek to un-cached part.
            – num8er
            Nov 9 at 14:52










          • The very strange thing is that Chrome it does not ignore the Content-Range. It seems that in my case, it will break the default player that Chrome builds when you put in the browser the streaming url like yours here m.saray.az/track/atb-could-you-believe/stream - that it works. In my case Chrome displays a <video> tag!!! What's going on? My Content-Type is "audio/mpeg" like in your case...
            – loretoparisi
            Nov 9 at 15:00








          • 1




            I think headers being cached, try to add this headers: 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': 0,
            – num8er
            Nov 9 at 15:02










          • done! Updated the code above with header setup, it seems someting it is still missing.
            – loretoparisi
            Nov 9 at 15:07











          Your Answer






          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "1"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          convertImagesToLinks: true,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: 10,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });














           

          draft saved


          draft discarded


















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53226595%2fstreaming-audio-in-node-js-with-content-range%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown

























          1 Answer
          1






          active

          oldest

          votes








          1 Answer
          1






          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes








          up vote
          1
          down vote



          accepted










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





          share|improve this answer



















          • 1




            Thank you, so cool this web site!!! Let me check how to adapt your solution, it seems it should work!
            – loretoparisi
            Nov 9 at 14:49






          • 1




            @loretoparisi keep in mind js player in site does not allow to jump not-cached part. But by sending content range header from client side You can jump anywhere. I've checked in Safari using touch-bar and seek to un-cached part.
            – num8er
            Nov 9 at 14:52










          • The very strange thing is that Chrome it does not ignore the Content-Range. It seems that in my case, it will break the default player that Chrome builds when you put in the browser the streaming url like yours here m.saray.az/track/atb-could-you-believe/stream - that it works. In my case Chrome displays a <video> tag!!! What's going on? My Content-Type is "audio/mpeg" like in your case...
            – loretoparisi
            Nov 9 at 15:00








          • 1




            I think headers being cached, try to add this headers: 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': 0,
            – num8er
            Nov 9 at 15:02










          • done! Updated the code above with header setup, it seems someting it is still missing.
            – loretoparisi
            Nov 9 at 15:07















          up vote
          1
          down vote



          accepted










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





          share|improve this answer



















          • 1




            Thank you, so cool this web site!!! Let me check how to adapt your solution, it seems it should work!
            – loretoparisi
            Nov 9 at 14:49






          • 1




            @loretoparisi keep in mind js player in site does not allow to jump not-cached part. But by sending content range header from client side You can jump anywhere. I've checked in Safari using touch-bar and seek to un-cached part.
            – num8er
            Nov 9 at 14:52










          • The very strange thing is that Chrome it does not ignore the Content-Range. It seems that in my case, it will break the default player that Chrome builds when you put in the browser the streaming url like yours here m.saray.az/track/atb-could-you-believe/stream - that it works. In my case Chrome displays a <video> tag!!! What's going on? My Content-Type is "audio/mpeg" like in your case...
            – loretoparisi
            Nov 9 at 15:00








          • 1




            I think headers being cached, try to add this headers: 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': 0,
            – num8er
            Nov 9 at 15:02










          • done! Updated the code above with header setup, it seems someting it is still missing.
            – loretoparisi
            Nov 9 at 15:07













          up vote
          1
          down vote



          accepted







          up vote
          1
          down vote



          accepted






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





          share|improve this answer














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






          share|improve this answer














          share|improve this answer



          share|improve this answer








          edited Nov 9 at 17:09

























          answered Nov 9 at 14:39









          num8er

          11k21738




          11k21738








          • 1




            Thank you, so cool this web site!!! Let me check how to adapt your solution, it seems it should work!
            – loretoparisi
            Nov 9 at 14:49






          • 1




            @loretoparisi keep in mind js player in site does not allow to jump not-cached part. But by sending content range header from client side You can jump anywhere. I've checked in Safari using touch-bar and seek to un-cached part.
            – num8er
            Nov 9 at 14:52










          • The very strange thing is that Chrome it does not ignore the Content-Range. It seems that in my case, it will break the default player that Chrome builds when you put in the browser the streaming url like yours here m.saray.az/track/atb-could-you-believe/stream - that it works. In my case Chrome displays a <video> tag!!! What's going on? My Content-Type is "audio/mpeg" like in your case...
            – loretoparisi
            Nov 9 at 15:00








          • 1




            I think headers being cached, try to add this headers: 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': 0,
            – num8er
            Nov 9 at 15:02










          • done! Updated the code above with header setup, it seems someting it is still missing.
            – loretoparisi
            Nov 9 at 15:07














          • 1




            Thank you, so cool this web site!!! Let me check how to adapt your solution, it seems it should work!
            – loretoparisi
            Nov 9 at 14:49






          • 1




            @loretoparisi keep in mind js player in site does not allow to jump not-cached part. But by sending content range header from client side You can jump anywhere. I've checked in Safari using touch-bar and seek to un-cached part.
            – num8er
            Nov 9 at 14:52










          • The very strange thing is that Chrome it does not ignore the Content-Range. It seems that in my case, it will break the default player that Chrome builds when you put in the browser the streaming url like yours here m.saray.az/track/atb-could-you-believe/stream - that it works. In my case Chrome displays a <video> tag!!! What's going on? My Content-Type is "audio/mpeg" like in your case...
            – loretoparisi
            Nov 9 at 15:00








          • 1




            I think headers being cached, try to add this headers: 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': 0,
            – num8er
            Nov 9 at 15:02










          • done! Updated the code above with header setup, it seems someting it is still missing.
            – loretoparisi
            Nov 9 at 15:07








          1




          1




          Thank you, so cool this web site!!! Let me check how to adapt your solution, it seems it should work!
          – loretoparisi
          Nov 9 at 14:49




          Thank you, so cool this web site!!! Let me check how to adapt your solution, it seems it should work!
          – loretoparisi
          Nov 9 at 14:49




          1




          1




          @loretoparisi keep in mind js player in site does not allow to jump not-cached part. But by sending content range header from client side You can jump anywhere. I've checked in Safari using touch-bar and seek to un-cached part.
          – num8er
          Nov 9 at 14:52




          @loretoparisi keep in mind js player in site does not allow to jump not-cached part. But by sending content range header from client side You can jump anywhere. I've checked in Safari using touch-bar and seek to un-cached part.
          – num8er
          Nov 9 at 14:52












          The very strange thing is that Chrome it does not ignore the Content-Range. It seems that in my case, it will break the default player that Chrome builds when you put in the browser the streaming url like yours here m.saray.az/track/atb-could-you-believe/stream - that it works. In my case Chrome displays a <video> tag!!! What's going on? My Content-Type is "audio/mpeg" like in your case...
          – loretoparisi
          Nov 9 at 15:00






          The very strange thing is that Chrome it does not ignore the Content-Range. It seems that in my case, it will break the default player that Chrome builds when you put in the browser the streaming url like yours here m.saray.az/track/atb-could-you-believe/stream - that it works. In my case Chrome displays a <video> tag!!! What's going on? My Content-Type is "audio/mpeg" like in your case...
          – loretoparisi
          Nov 9 at 15:00






          1




          1




          I think headers being cached, try to add this headers: 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': 0,
          – num8er
          Nov 9 at 15:02




          I think headers being cached, try to add this headers: 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': 0,
          – num8er
          Nov 9 at 15:02












          done! Updated the code above with header setup, it seems someting it is still missing.
          – loretoparisi
          Nov 9 at 15:07




          done! Updated the code above with header setup, it seems someting it is still missing.
          – loretoparisi
          Nov 9 at 15:07


















           

          draft saved


          draft discarded



















































           


          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53226595%2fstreaming-audio-in-node-js-with-content-range%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown





















































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown

































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown







          Popular posts from this blog

          Schultheiß

          Android Play Services Check

          Where to put API Key in Google Cloud Vision for PHP