|
|
@@ -9,13 +9,10 @@ var async = require('async'),
|
|
|
debug = require('debug')('hls:reader');
|
|
|
|
|
|
try {
|
|
|
- var Readable = require('stream').Readable,
|
|
|
- Transform = require('stream').Transform;
|
|
|
+ var Readable = require('stream').Readable;
|
|
|
assert(Readable);
|
|
|
- assert(Transform);
|
|
|
} catch (e) {
|
|
|
- var Readable = require('readable-stream'),
|
|
|
- Transform = require('readable-stream/transform');
|
|
|
+ var Readable = require('readable-stream');
|
|
|
}
|
|
|
|
|
|
var m3u8 = require('./m3u8');
|
|
|
@@ -39,6 +36,16 @@ emits:
|
|
|
segment (seqNo, duration, datetime, size?, )
|
|
|
*/
|
|
|
|
|
|
+// ensure function is never run more than once
|
|
|
+function once(fn) {
|
|
|
+ var called = false;
|
|
|
+ return function() {
|
|
|
+ var call = !called;
|
|
|
+ called = true;
|
|
|
+ if(call) fn.apply(this, arguments);
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
function getFileStream(srcUrl, options, cb) {
|
|
|
assert(srcUrl.protocol);
|
|
|
|
|
|
@@ -51,7 +58,8 @@ function getFileStream(srcUrl, options, cb) {
|
|
|
var headers = options.headers || {};
|
|
|
if (!headers['user-agent']) headers['user-agent'] = DEFAULT_AGENT;
|
|
|
|
|
|
- (options.probe ? http.head : http.get)({url:url.format(srcUrl), stream:true, headers:headers}, function(err, res) {
|
|
|
+ // http-get will occasionally call the callback multiple times... :–(
|
|
|
+ (options.probe ? http.head : http.get)({url:url.format(srcUrl), stream:true, headers:headers}, once(function(err, res) {
|
|
|
if (err) return cb(err);
|
|
|
|
|
|
var statusCode = res.code || res.stream.statusCode;
|
|
|
@@ -66,10 +74,9 @@ function getFileStream(srcUrl, options, cb) {
|
|
|
size = res.headers['content-length'] ? parseInt(res.headers['content-length'], 10) : -1,
|
|
|
modified = res.headers['last-modified'] ? new Date(res.headers['last-modified']) : null;
|
|
|
|
|
|
- if (res.stream)
|
|
|
- res.stream.resume(); // for some reason http-get pauses the stream for the callback
|
|
|
+ res.stream.resume(); // for some reason http-get pauses the stream for the callback
|
|
|
cb(null, res.stream, {url:res.url || srcUrl, mime:mimetype, size:size, modified:modified});
|
|
|
- });
|
|
|
+ }));
|
|
|
} else {
|
|
|
process.nextTick(function() {
|
|
|
cb(new Error('Unsupported protocol: '+srcUrl.protocol));
|
|
|
@@ -88,28 +95,41 @@ function getFileStream(srcUrl, options, cb) {
|
|
|
|
|
|
function HlsStreamReader(src, options) {
|
|
|
var self = this;
|
|
|
- Transform.call(this, options);
|
|
|
|
|
|
+ options = options || {};
|
|
|
if (typeof src === 'string')
|
|
|
src = url.parse(src);
|
|
|
|
|
|
this.url = src;
|
|
|
this.baseUrl = src;
|
|
|
- this.options = options || {};
|
|
|
+
|
|
|
+ this.prebufferSize = options.prebufferSize || 0;
|
|
|
+ this.fullStream = !!options.fullStream;
|
|
|
+ this.keepConnection = !!options.keepConnection;
|
|
|
+ this.noData = !!options.noData;
|
|
|
|
|
|
this.indexStream = null;
|
|
|
this.index = null;
|
|
|
-
|
|
|
this.readState = {
|
|
|
currentSeq:-1,
|
|
|
currentSegment:null,
|
|
|
- readable:null
|
|
|
+ stream:null
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.prebufferSize) {
|
|
|
+ var lwm = options.lowWaterMark || 0;
|
|
|
+ var hwm = options.highWaterMark || this.prebufferSize;
|
|
|
+ options.lowWaterMark = Math.max(this.prebufferSize, lwm);
|
|
|
+ options.highWaterMark = Math.max(hwm, lwm);
|
|
|
+ this.once('readable', function() {
|
|
|
+ self._readableState.lowWaterMark = ~~lwm;
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
function updatecheck(updated) {
|
|
|
if (updated) {
|
|
|
if (self.readState.currentSeq===-1)
|
|
|
- self.readState.currentSeq = self.index.startSeqNo(self.options.fullStream);
|
|
|
+ self.readState.currentSeq = self.index.startSeqNo(self.fullStream);
|
|
|
else if (self.readState.currentSeq < self.index.startSeqNo(true))
|
|
|
self.readState.currentSeq = self.index.startSeqNo(true);
|
|
|
|
|
|
@@ -118,8 +138,7 @@ function HlsStreamReader(src, options) {
|
|
|
if (self.index.variant)
|
|
|
return self.end();
|
|
|
}
|
|
|
- if (!self.readState.currentSegment)
|
|
|
- checkcurrent();
|
|
|
+ checkcurrent();
|
|
|
|
|
|
if (!self.index.ended) {
|
|
|
var updateInterval = updated ? self.index.segments[self.index.segments.length-1].duration : self.index.target_duration / 2;
|
|
|
@@ -131,7 +150,7 @@ function HlsStreamReader(src, options) {
|
|
|
function updateindex() {
|
|
|
getFileStream(self.url, function(err, stream, meta) {
|
|
|
if (err) {
|
|
|
- if (self.index && self.options.keepConnection) {
|
|
|
+ if (self.index && self.keepConnection) {
|
|
|
console.error('Failed to update index at '+url.format(self.url)+':', err.stack || err);
|
|
|
return updatecheck();
|
|
|
}
|
|
|
@@ -162,15 +181,17 @@ function HlsStreamReader(src, options) {
|
|
|
updateindex();
|
|
|
|
|
|
function checkcurrent() {
|
|
|
+ if (self.readState.currentSegment) return; // already processing
|
|
|
+
|
|
|
self.readState.currentSegment = self.index.getSegment(self.readState.currentSeq);
|
|
|
if (self.readState.currentSegment) {
|
|
|
- fetchfrom(self.readState.currentSeq, self.readState.currentSegment, function(err) {
|
|
|
- var url = self.readState.currentSegment.uri;
|
|
|
+ var url = self.readState.currentSegment.uri;
|
|
|
+ fetchfrom(self.readState.currentSeq, self.readState.currentSegment, function(err, transferred) {
|
|
|
self.readState.currentSegment = null;
|
|
|
if (err) {
|
|
|
- if (!self.options.keepConnection) return self.emit('error', err);
|
|
|
+ if (!self.keepConnection) return self.emit('error', err);
|
|
|
console.error('While fetching '+url+':', err.stack || err);
|
|
|
- return;
|
|
|
+ if (!transferred) return; // TODO: retry with a range header
|
|
|
}
|
|
|
self.readState.currentSeq++;
|
|
|
checkcurrent();
|
|
|
@@ -188,9 +209,10 @@ function HlsStreamReader(src, options) {
|
|
|
var segmentUrl = url.resolve(self.baseUrl, segment.uri)
|
|
|
|
|
|
debug('fetching segment', segmentUrl);
|
|
|
- getFileStream(url.parse(segmentUrl), {probe:!!self.options.noData}, function(err, stream, meta) {
|
|
|
+ getFileStream(url.parse(segmentUrl), {probe:!!self.noData}, function(err, stream, meta) {
|
|
|
if (err) return cb(err);
|
|
|
|
|
|
+ debug('got segment info', meta);
|
|
|
if (meta.mime !== 'video/mp2t'/* &&
|
|
|
meta.mime !== 'audio/aac' && meta.mime !== 'audio/x-aac' &&
|
|
|
meta.mime !== 'audio/ac3'*/)
|
|
|
@@ -198,34 +220,76 @@ function HlsStreamReader(src, options) {
|
|
|
|
|
|
self.emit('segment', seqNo, segment.duration, meta);
|
|
|
|
|
|
- // TODO: handle aborted downloads
|
|
|
if (stream) {
|
|
|
- var r = stream;
|
|
|
- if (!(stream instanceof Readable)) {
|
|
|
- r = new Readable();
|
|
|
- r.wrap(stream);
|
|
|
- }
|
|
|
- self.readState.readable = r;
|
|
|
- r.pipe(self, {end:false});
|
|
|
+ debug('pushing input stream to reader');
|
|
|
|
|
|
- r.on('end', function() {
|
|
|
- self.readState.readable = null;
|
|
|
- r.unpipe(self);
|
|
|
- cb();
|
|
|
+ var totalBytes = 0;
|
|
|
+ stream.on('data', function(chunk) {
|
|
|
+ totalBytes += chunk.length;
|
|
|
+ self.push(chunk); // intentionally ignore the result to buffer input as fast as possible
|
|
|
});
|
|
|
+ stream.on('error', Done);
|
|
|
+ stream.on('end', Done);
|
|
|
+ stream.on('close', Done);
|
|
|
+
|
|
|
+ self.readState.stream = stream;
|
|
|
+ self.stream_start(true, !self.push(new Buffer(0)));
|
|
|
+
|
|
|
+ function Done(err) {
|
|
|
+ debug('finished with input stream');
|
|
|
+
|
|
|
+ stream.removeListener('error', Done);
|
|
|
+ stream.removeListener('end', Done);
|
|
|
+ stream.removeListener('close', Done);
|
|
|
+
|
|
|
+ self.readState.stream = null;
|
|
|
+
|
|
|
+ // FIXME: is this required? or already handled by http-get?
|
|
|
+ if (!err && (totalBytes !== meta.size))
|
|
|
+ err = new Error('Invalid returned stream length');
|
|
|
+
|
|
|
+ cb(err, totalBytes);
|
|
|
+ }
|
|
|
} else {
|
|
|
process.nextTick(cb);
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- return this;
|
|
|
+ this.stream_start = function(fresh, blocked) {
|
|
|
+ if (fresh) {
|
|
|
+ self.readState.stream_started = false;
|
|
|
+ if (self.readState.timer) {
|
|
|
+ clearTimeout(self.readState.timer);
|
|
|
+ self.readState.timer = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (blocked) return self.readState.stream.pause();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (self.readState.stream_started) return;
|
|
|
+
|
|
|
+ var stream = self.readState.stream;
|
|
|
+ if (!stream) return;
|
|
|
+
|
|
|
+ if (typeof stream.destroy == 'function') {
|
|
|
+ var duration = self.readState.currentSegment.duration || self.index.target_duration || 10;
|
|
|
+ self.readState.timer = setTimeout(function() {
|
|
|
+ if (self.readState.stream)
|
|
|
+ stream.destroy();
|
|
|
+ self.readState.timer = null;
|
|
|
+ }, 1.5*duration*1000);
|
|
|
+ }
|
|
|
+ self.readState.stream_started = true;
|
|
|
+ stream.resume();
|
|
|
+ }
|
|
|
+
|
|
|
+ Readable.call(this, options);
|
|
|
}
|
|
|
-util.inherits(HlsStreamReader, Transform);
|
|
|
+util.inherits(HlsStreamReader, Readable);
|
|
|
|
|
|
-HlsStreamReader.prototype._transform = function(chunk, output, cb) {
|
|
|
- // TODO: decrypt here
|
|
|
- cb(null, chunk);
|
|
|
+HlsStreamReader.prototype._read = function(n, cb) {
|
|
|
+ this.stream_start();
|
|
|
};
|
|
|
|
|
|
function hlsreader(url, options) {
|