|
@@ -37,6 +37,11 @@ function HlsStreamRecorder(reader, dst, options) {
|
|
|
this.decrypt = options.decrypt;
|
|
this.decrypt = options.decrypt;
|
|
|
|
|
|
|
|
this.recorders = [];
|
|
this.recorders = [];
|
|
|
|
|
+
|
|
|
|
|
+ this.mapSeq = 0;
|
|
|
|
|
+ this.nextMap = null;
|
|
|
|
|
+
|
|
|
|
|
+ this.writing = null; // tracks writing state
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
HlsStreamRecorder.prototype.start = function() {
|
|
HlsStreamRecorder.prototype.start = function() {
|
|
@@ -171,36 +176,72 @@ HlsStreamRecorder.prototype.updateIndex = function(update) {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-HlsStreamRecorder.prototype.process = function(segmentInfo, done) {
|
|
|
|
|
|
|
+HlsStreamRecorder.prototype.process = function (segmentInfo, next) {
|
|
|
|
|
+
|
|
|
|
|
+ if (segmentInfo.type === 'segment') {
|
|
|
|
|
+ return this.processSegment(segmentInfo, next);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (segmentInfo.type === 'init') {
|
|
|
|
|
+ return this.processInfo(segmentInfo, next);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ debug('unknown segment type: ' + segmentInfo.type);
|
|
|
|
|
+
|
|
|
|
|
+ return next();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+HlsStreamRecorder.prototype.processInfo = function (segmentInfo, callback) {
|
|
|
|
|
+
|
|
|
|
|
+ const meta = segmentInfo.file;
|
|
|
|
|
+ const uri = `${this.segmentName(this.mapSeq, true)}.${Mime.extension(meta.mime)}`;
|
|
|
|
|
+
|
|
|
|
|
+ this.writeStream(segmentInfo.stream, uri, (err, bytesWritten) => {
|
|
|
|
|
+
|
|
|
|
|
+ // only to report errors
|
|
|
|
|
+ if (err) debug('stream error', err.stack || err);
|
|
|
|
|
|
|
|
- let segment = new M3U8Parse.M3U8Segment(segmentInfo.details, true);
|
|
|
|
|
|
|
+ const map = new M3U8Parse.AttrList();
|
|
|
|
|
+
|
|
|
|
|
+ map.quotedString('uri', uri);
|
|
|
|
|
+
|
|
|
|
|
+ // handle byterange
|
|
|
|
|
+ if (this.collect) {
|
|
|
|
|
+ map.quotedString('byterange', `${bytesWritten}@${this.writing.bytes - bytesWritten}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.nextMap = map;
|
|
|
|
|
+ return callback();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.mapSeq++;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+HlsStreamRecorder.prototype.processSegment = function (segmentInfo, callback) {
|
|
|
|
|
+
|
|
|
|
|
+ let segment = new M3U8Parse.M3U8Segment(segmentInfo.segment.details, true);
|
|
|
let meta = segmentInfo.file;
|
|
let meta = segmentInfo.file;
|
|
|
|
|
|
|
|
// mark discontinuities
|
|
// mark discontinuities
|
|
|
if (this.nextSegmentSeq !== -1 &&
|
|
if (this.nextSegmentSeq !== -1 &&
|
|
|
- this.nextSegmentSeq !== segmentInfo.seq) {
|
|
|
|
|
|
|
+ this.nextSegmentSeq !== segmentInfo.segment.seq) {
|
|
|
segment.discontinuity = true;
|
|
segment.discontinuity = true;
|
|
|
}
|
|
}
|
|
|
- this.nextSegmentSeq = segmentInfo.seq + 1;
|
|
|
|
|
|
|
+ this.nextSegmentSeq = segmentInfo.segment.seq + 1;
|
|
|
|
|
|
|
|
// create our own uri
|
|
// create our own uri
|
|
|
segment.uri = `${this.segmentName(this.seq)}.${Mime.extension(meta.mime)}`;
|
|
segment.uri = `${this.segmentName(this.seq)}.${Mime.extension(meta.mime)}`;
|
|
|
|
|
|
|
|
- // handle byterange
|
|
|
|
|
- let first = this.index.segments.length === 0;
|
|
|
|
|
- let newFile = first || this.index.segments[this.index.segments.length - 1].uri !== segment.uri;
|
|
|
|
|
- if (this.collect) {
|
|
|
|
|
- segment.byterange = {
|
|
|
|
|
- length: 0,
|
|
|
|
|
- offset: newFile ? 0 : null
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- else {
|
|
|
|
|
- delete segment.byterange;
|
|
|
|
|
|
|
+ // add map info
|
|
|
|
|
+ if (this.nextMap) {
|
|
|
|
|
+ segment.map = this.nextMap;
|
|
|
|
|
+ this.nextMap = null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ delete segment.byterange;
|
|
|
|
|
+
|
|
|
// save the stream segment
|
|
// save the stream segment
|
|
|
- SegmentDecrypt.decrypt(segmentInfo.stream, segmentInfo.details.keys, this.decrypt, (err, stream, decrypted) => {
|
|
|
|
|
|
|
+ SegmentDecrypt.decrypt(segmentInfo.stream, segmentInfo.segment.details.keys, this.decrypt, (err, stream, decrypted) => {
|
|
|
|
|
|
|
|
if (err) {
|
|
if (err) {
|
|
|
console.error('decrypt failed', err.stack);
|
|
console.error('decrypt failed', err.stack);
|
|
@@ -210,35 +251,57 @@ HlsStreamRecorder.prototype.process = function(segmentInfo, done) {
|
|
|
segment.keys = null;
|
|
segment.keys = null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- stream = Oncemore(stream);
|
|
|
|
|
- stream.pipe(Fs.createWriteStream(Path.join(this.dst, segment.uri), { flags: newFile ? 'w' : 'a' }));
|
|
|
|
|
-
|
|
|
|
|
- let bytesWritten = 0;
|
|
|
|
|
- if (this.collect) {
|
|
|
|
|
- stream.on('data', (chunk) => {
|
|
|
|
|
-
|
|
|
|
|
- bytesWritten += chunk.length;
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- stream.once('end', 'error', (err) => {
|
|
|
|
|
|
|
+ this.writeStream(stream, segment.uri, (err, bytesWritten) => {
|
|
|
|
|
|
|
|
// only to report errors
|
|
// only to report errors
|
|
|
if (err) debug('stream error', err.stack || err);
|
|
if (err) debug('stream error', err.stack || err);
|
|
|
|
|
|
|
|
- if (segment.byterange) {
|
|
|
|
|
- segment.byterange.length = bytesWritten;
|
|
|
|
|
|
|
+ // handle byterange
|
|
|
|
|
+ if (this.collect) {
|
|
|
|
|
+ const isContigious = this.writing.segmentHead > 0 && ((this.writing.segmentHead + bytesWritten) === this.writing.bytes);
|
|
|
|
|
+ segment.byterange = {
|
|
|
|
|
+ length: bytesWritten,
|
|
|
|
|
+ offset: isContigious ? null : this.writing.bytes - bytesWritten
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.writing.segmentHead = this.writing.bytes;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// update index
|
|
// update index
|
|
|
this.index.segments.push(segment);
|
|
this.index.segments.push(segment);
|
|
|
- this.flushIndex(done);
|
|
|
|
|
|
|
+ this.flushIndex(callback);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
this.seq++;
|
|
this.seq++;
|
|
|
});
|
|
});
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+HlsStreamRecorder.prototype.writeStream = function (stream, name, callback) {
|
|
|
|
|
+
|
|
|
|
|
+ if (!this.writing || !this.collect) {
|
|
|
|
|
+ this.writing = {
|
|
|
|
|
+ bytes: 0,
|
|
|
|
|
+ segmentHead: 0
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ stream.pipe(Fs.createWriteStream(Path.join(this.dst, name), { flags: this.writing.bytes === 0 ? 'w' : 'a' }));
|
|
|
|
|
+
|
|
|
|
|
+ let bytesWritten = 0;
|
|
|
|
|
+ if (this.collect) {
|
|
|
|
|
+ stream.on('data', (chunk) => {
|
|
|
|
|
+
|
|
|
|
|
+ bytesWritten += +chunk.length;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Oncemore(stream).once('end', 'error', (err) => {
|
|
|
|
|
+
|
|
|
|
|
+ this.writing.bytes += bytesWritten;
|
|
|
|
|
+ return callback(err, bytesWritten);
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
HlsStreamRecorder.prototype.variantName = function(info, index) {
|
|
HlsStreamRecorder.prototype.variantName = function(info, index) {
|
|
|
|
|
|
|
|
return `v${index}`;
|
|
return `v${index}`;
|
|
@@ -251,7 +314,7 @@ HlsStreamRecorder.prototype.groupSrcName = function(info, index) {
|
|
|
return `grp/${id}/${lang ? lang + '-' : ''}${index}`;
|
|
return `grp/${id}/${lang ? lang + '-' : ''}${index}`;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-HlsStreamRecorder.prototype.segmentName = function(seqNo) {
|
|
|
|
|
|
|
+HlsStreamRecorder.prototype.segmentName = function(seqNo, isInit) {
|
|
|
|
|
|
|
|
const name = (n) => {
|
|
const name = (n) => {
|
|
|
|
|
|
|
@@ -261,7 +324,7 @@ HlsStreamRecorder.prototype.segmentName = function(seqNo) {
|
|
|
return chr;
|
|
return chr;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- return this.collect ? 'stream' : name(seqNo);
|
|
|
|
|
|
|
+ return this.collect ? 'stream' : (isInit ? 'init-' : '') + name(seqNo);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
HlsStreamRecorder.prototype.flushIndex = function(cb) {
|
|
HlsStreamRecorder.prototype.flushIndex = function(cb) {
|