Streaming audio in Node.js with Content-Range
up vote
1
down vote
favorite
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
add a comment |
up vote
1
down vote
favorite
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
add a comment |
up vote
1
down vote
favorite
up vote
1
down vote
favorite
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
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
javascript node.js audio-streaming
edited Nov 9 at 15:07
asked Nov 9 at 13:26
loretoparisi
7,47554770
7,47554770
add a comment |
add a comment |
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);
}
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 theContent-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? MyContent-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
|
show 2 more comments
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);
}
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 theContent-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? MyContent-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
|
show 2 more comments
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);
}
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 theContent-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? MyContent-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
|
show 2 more comments
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);
}
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);
}
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 theContent-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? MyContent-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
|
show 2 more comments
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 theContent-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? MyContent-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
|
show 2 more comments
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
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