|
@@ -6,6 +6,7 @@ var util = require('util'),
|
|
|
assert = require('assert');
|
|
assert = require('assert');
|
|
|
|
|
|
|
|
var request = require('request'),
|
|
var request = require('request'),
|
|
|
|
|
+ extend = require('xtend'),
|
|
|
oncemore = require('./oncemore'),
|
|
oncemore = require('./oncemore'),
|
|
|
uristream = require('./uristream'),
|
|
uristream = require('./uristream'),
|
|
|
debug = require('debug')('hls:reader');
|
|
debug = require('debug')('hls:reader');
|
|
@@ -22,21 +23,84 @@ var m3u8 = require('./m3u8');
|
|
|
function noop() {};
|
|
function noop() {};
|
|
|
|
|
|
|
|
module.exports = hlsreader;
|
|
module.exports = hlsreader;
|
|
|
|
|
+hlsreader.HlsSegmentObject = HlsSegmentObject;
|
|
|
hlsreader.HlsStreamReader = HlsStreamReader;
|
|
hlsreader.HlsStreamReader = HlsStreamReader;
|
|
|
|
|
|
|
|
-/*
|
|
|
|
|
-options:
|
|
|
|
|
- startSeq*
|
|
|
|
|
- noData // don't emit any data - useful for analyzing the stream structure
|
|
|
|
|
|
|
+function HlsSegmentObject(seq, segment, meta, stream) {
|
|
|
|
|
+ this.seq = seq;
|
|
|
|
|
+ this.segment = segment;
|
|
|
|
|
+ this.meta = meta;
|
|
|
|
|
+ this.stream = stream;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function checknext(reader) {
|
|
|
|
|
+ var state = reader.readState;
|
|
|
|
|
+ var index = reader.index;
|
|
|
|
|
+ if (!state.active || state.fetching || !index)
|
|
|
|
|
+ return;
|
|
|
|
|
+
|
|
|
|
|
+ var seq = state.nextSeq;
|
|
|
|
|
+ var segment = index.getSegment(seq);
|
|
|
|
|
+
|
|
|
|
|
+ if (segment) {
|
|
|
|
|
+ state.fetching = fetchfrom(reader, seq, segment, function(err, object) {
|
|
|
|
|
+ state.fetching = null;
|
|
|
|
|
+ if (err) reader.emit('error', err);
|
|
|
|
|
+
|
|
|
|
|
+ if (seq === state.nextSeq)
|
|
|
|
|
+ state.nextSeq++;
|
|
|
|
|
+
|
|
|
|
|
+ state.active = reader.push(object);
|
|
|
|
|
+ checknext(reader);
|
|
|
|
|
+ });
|
|
|
|
|
+ } else if (index.ended) {
|
|
|
|
|
+ reader.push(null);
|
|
|
|
|
+ } else if (!index.type && (index.lastSeqNo() < state.nextSeq-1)) {
|
|
|
|
|
+ // handle live stream restart
|
|
|
|
|
+ state.nextSeq = index.startSeqNo(true);
|
|
|
|
|
+ checknext(reader);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function fetchfrom(reader, seqNo, segment, cb) {
|
|
|
|
|
+ var segmentUrl = url.resolve(reader.baseUrl, segment.uri)
|
|
|
|
|
+ var probe = !!reader.noData;
|
|
|
|
|
+
|
|
|
|
|
+ debug('fetching segment', segmentUrl);
|
|
|
|
|
+ var stream = uristream(segmentUrl, { probe:probe, highWaterMark:100*1000*1000 });
|
|
|
|
|
+ stream.on('meta', onmeta);
|
|
|
|
|
+ stream.on('end', onfail);
|
|
|
|
|
+ stream.on('error', onfail);
|
|
|
|
|
+
|
|
|
|
|
+ function cleanup() {
|
|
|
|
|
+ stream.removeListener('meta', onmeta);
|
|
|
|
|
+ stream.removeListener('end', onfail);
|
|
|
|
|
+ stream.removeListener('error', onfail);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function onmeta(meta) {
|
|
|
|
|
+ debug('got segment info', meta);
|
|
|
|
|
+
|
|
|
|
|
+ if (meta.mime !== 'video/mp2t'/* &&
|
|
|
|
|
+ meta.mime !== 'audio/aac' && meta.mime !== 'audio/x-aac' &&
|
|
|
|
|
+ meta.mime !== 'audio/ac3'*/) {
|
|
|
|
|
+ if (stream.abort) stream.abort();
|
|
|
|
|
+ return stream.emit(new Error('Unsupported segment MIME type: '+meta.mime));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ cleanup();
|
|
|
|
|
+ cb(null, new HlsSegmentObject(seqNo, segment, meta, stream));
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- maxRedirects*
|
|
|
|
|
- cacheDir*
|
|
|
|
|
- headers* // allows for custom user-agent, cookies, auth, etc
|
|
|
|
|
-
|
|
|
|
|
-emits:
|
|
|
|
|
- index (m3u8)
|
|
|
|
|
- segment (seqNo, duration, datetime, size?, )
|
|
|
|
|
-*/
|
|
|
|
|
|
|
+ function onfail(err) {
|
|
|
|
|
+ if (!err) err = new Error('No metadata');
|
|
|
|
|
+
|
|
|
|
|
+ cleanup();
|
|
|
|
|
+ cb(err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return stream;
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
function HlsStreamReader(src, options) {
|
|
function HlsStreamReader(src, options) {
|
|
|
var self = this;
|
|
var self = this;
|
|
@@ -49,15 +113,12 @@ function HlsStreamReader(src, options) {
|
|
|
this.baseUrl = src;
|
|
this.baseUrl = src;
|
|
|
|
|
|
|
|
this.fullStream = !!options.fullStream;
|
|
this.fullStream = !!options.fullStream;
|
|
|
- this.keepConnection = !!options.keepConnection;
|
|
|
|
|
this.noData = !!options.noData;
|
|
this.noData = !!options.noData;
|
|
|
|
|
|
|
|
- this.indexStream = null;
|
|
|
|
|
this.index = null;
|
|
this.index = null;
|
|
|
this.readState = {
|
|
this.readState = {
|
|
|
- currentSeq:-1,
|
|
|
|
|
|
|
+ nextSeq:-1,
|
|
|
currentSegment:null,
|
|
currentSegment:null,
|
|
|
- stream:null
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function getUpdateInterval(updated) {
|
|
function getUpdateInterval(updated) {
|
|
@@ -69,17 +130,19 @@ function HlsStreamReader(src, options) {
|
|
|
|
|
|
|
|
function updatecheck(updated) {
|
|
function updatecheck(updated) {
|
|
|
if (updated) {
|
|
if (updated) {
|
|
|
- if (self.readState.currentSeq===-1)
|
|
|
|
|
- self.readState.currentSeq = self.index.startSeqNo(self.fullStream);
|
|
|
|
|
- else if (self.readState.currentSeq < self.index.startSeqNo(true))
|
|
|
|
|
- self.readState.currentSeq = self.index.startSeqNo(true);
|
|
|
|
|
|
|
+ if (self.readState.nextSeq===-1)
|
|
|
|
|
+ self.readState.nextSeq = self.index.startSeqNo(self.fullStream);
|
|
|
|
|
+ else if (self.readState.nextSeq < self.index.startSeqNo(true)) {
|
|
|
|
|
+ debug('skipping '+(self.index.startSeqNo(true)-self.readState.nextSeq)+' invalidated segments');
|
|
|
|
|
+ self.readState.nextSeq = self.index.startSeqNo(true);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
self.emit('index', self.index);
|
|
self.emit('index', self.index);
|
|
|
|
|
|
|
|
if (self.index.variant)
|
|
if (self.index.variant)
|
|
|
return self.end();
|
|
return self.end();
|
|
|
}
|
|
}
|
|
|
- checkcurrent();
|
|
|
|
|
|
|
+ checknext(self);
|
|
|
|
|
|
|
|
if (!self.index.ended) {
|
|
if (!self.index.ended) {
|
|
|
var updateInterval = getUpdateInterval(updated);
|
|
var updateInterval = getUpdateInterval(updated);
|
|
@@ -120,102 +183,13 @@ function HlsStreamReader(src, options) {
|
|
|
}
|
|
}
|
|
|
updateindex();
|
|
updateindex();
|
|
|
|
|
|
|
|
- function checkcurrent() {
|
|
|
|
|
- if (self.readState.currentSegment) return; // already processing
|
|
|
|
|
-
|
|
|
|
|
- self.readState.currentSegment = self.index.getSegment(self.readState.currentSeq);
|
|
|
|
|
- if (self.readState.currentSegment) {
|
|
|
|
|
- var url = self.readState.currentSegment.uri;
|
|
|
|
|
-
|
|
|
|
|
- function tryfetch(start) {
|
|
|
|
|
- var seq = self.readState.currentSeq;
|
|
|
|
|
- fetchfrom(seq, self.readState.currentSegment, start, function(err) {
|
|
|
|
|
- if (err) {
|
|
|
|
|
- if (!self.keepConnection) return self.emit('error', err);
|
|
|
|
|
- console.error('While fetching '+url+':', err.stack || err);
|
|
|
|
|
-
|
|
|
|
|
- // retry with missing range if it is still relevant
|
|
|
|
|
- if (err instanceof uristream.PartialError && err.processed > 0 &&
|
|
|
|
|
- self.index.getSegment(seq))
|
|
|
|
|
- return tryfetch(start + err.processed);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- self.readState.currentSegment = null;
|
|
|
|
|
- if (seq === self.readState.currentSeq)
|
|
|
|
|
- self.readState.currentSeq++;
|
|
|
|
|
-
|
|
|
|
|
- checkcurrent();
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- tryfetch(0);
|
|
|
|
|
- } else if (self.index.ended)
|
|
|
|
|
- self.end();
|
|
|
|
|
- else if (!self.index.type && (self.index.lastSeqNo() < self.readState.currentSeq-1)) {
|
|
|
|
|
- // handle live stream restart
|
|
|
|
|
- self.readState.currentSeq = self.index.startSeqNo(true);
|
|
|
|
|
- checkcurrent();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function fetchfrom(seqNo, segment, start, cb) {
|
|
|
|
|
- var segmentUrl = url.resolve(self.baseUrl, segment.uri)
|
|
|
|
|
- var probe = !!self.noData;
|
|
|
|
|
-
|
|
|
|
|
- debug('fetching segment', segmentUrl);
|
|
|
|
|
- var stream = uristream(segmentUrl, { probe:probe, start:start, highWaterMark:100*1000*1000 });
|
|
|
|
|
- stream.on('meta', function(meta) {
|
|
|
|
|
- debug('got segment info', meta);
|
|
|
|
|
- if (meta.mime !== 'video/mp2t'/* &&
|
|
|
|
|
- meta.mime !== 'audio/aac' && meta.mime !== 'audio/x-aac' &&
|
|
|
|
|
- meta.mime !== 'audio/ac3'*/) {
|
|
|
|
|
- if (stream.abort) stream.abort();
|
|
|
|
|
- return stream.emit(new Error('Unsupported segment MIME type: '+meta.mime));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // abort() indicates a temporal stream. Ie. ensure it is completed in a timely fashion
|
|
|
|
|
- if (self.index.isLive() && typeof stream.abort == 'function') {
|
|
|
|
|
- var duration = segment.duration || self.index.target_duration || 10;
|
|
|
|
|
- duration = Math.min(duration, self.index.target_duration || 10);
|
|
|
|
|
- self.readState.timer = setTimeout(function() {
|
|
|
|
|
- if (self.readState.stream) {
|
|
|
|
|
- debug('timed out waiting for data');
|
|
|
|
|
- self.readState.stream.abort();
|
|
|
|
|
- }
|
|
|
|
|
- // TODO: ensure Done() is always called
|
|
|
|
|
- self.readState.timer = null;
|
|
|
|
|
- }, 1.5*duration*1000);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (start === 0)
|
|
|
|
|
- self.emit('segment', seqNo, segment.duration, meta);
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- stream.pipe(self);
|
|
|
|
|
- oncemore(stream).once('end', 'error', function(err) {
|
|
|
|
|
- clearTimeout(self.readState.timer);
|
|
|
|
|
-
|
|
|
|
|
- stream.unpipe(self);
|
|
|
|
|
- self.readState.stream = null;
|
|
|
|
|
-
|
|
|
|
|
- if (err) debug('stream error', err);
|
|
|
|
|
- else debug('finished with input stream', stream.meta.url);
|
|
|
|
|
-
|
|
|
|
|
- cb(err);
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- self.readState.stream = stream;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // allow piping content to self
|
|
|
|
|
- this.write = this.push.bind(this);
|
|
|
|
|
- this.end = function() {};
|
|
|
|
|
-
|
|
|
|
|
- Readable.call(this, options);
|
|
|
|
|
|
|
+ Readable.call(this, extend(options, {objectMode:true}));
|
|
|
}
|
|
}
|
|
|
util.inherits(HlsStreamReader, Readable);
|
|
util.inherits(HlsStreamReader, Readable);
|
|
|
|
|
|
|
|
-HlsStreamReader.prototype._read = function(n, cb) {
|
|
|
|
|
- this.emit('drain');
|
|
|
|
|
|
|
+HlsStreamReader.prototype._read = function(n) {
|
|
|
|
|
+ this.readState.active = true;
|
|
|
|
|
+ checknext(this);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
function hlsreader(url, options) {
|
|
function hlsreader(url, options) {
|