mirror of
https://github.com/musix-org/musix-oss
synced 2025-06-16 18:56:00 +00:00
fix
This commit is contained in:
178
node_modules/m3u8stream/lib/dash-mpd-parser.js
generated
vendored
Normal file
178
node_modules/m3u8stream/lib/dash-mpd-parser.js
generated
vendored
Normal file
@ -0,0 +1,178 @@
|
||||
const Writable = require('stream').Writable;
|
||||
const sax = require('sax');
|
||||
const parseTime = require('./parse-time');
|
||||
|
||||
|
||||
/**
|
||||
* A wrapper around sax that emits segments.
|
||||
*
|
||||
* @extends WRitableStream
|
||||
* @constructor
|
||||
*/
|
||||
module.exports = class DashMPDParser extends Writable {
|
||||
constructor(targetID) {
|
||||
super();
|
||||
this._parser = sax.createStream(false, { lowercasetags: true });
|
||||
this._parser.on('error', this.emit.bind(this, 'error'));
|
||||
|
||||
let lastTag;
|
||||
let currtime = 0;
|
||||
let seq = 0;
|
||||
let segmentTemplate;
|
||||
let timescale, offset, duration, baseURL;
|
||||
let timeline = [];
|
||||
let getSegments = false;
|
||||
let isStatic;
|
||||
let treeLevel;
|
||||
let periodStart;
|
||||
|
||||
const tmpl = (str) => {
|
||||
const context = {
|
||||
RepresentationID: targetID,
|
||||
Number: seq,
|
||||
Time: currtime,
|
||||
};
|
||||
return str.replace(/\$(\w+)\$/g, (m, p1) => context[p1]);
|
||||
};
|
||||
|
||||
this._parser.on('opentag', (node) => {
|
||||
switch (node.name) {
|
||||
case 'mpd':
|
||||
currtime =
|
||||
new Date(node.attributes.availabilitystarttime).getTime();
|
||||
isStatic = node.attributes.type !== 'dynamic';
|
||||
break;
|
||||
case 'period':
|
||||
// Reset everything on <Period> tag.
|
||||
seq = 0;
|
||||
timescale = 1000;
|
||||
duration = 0;
|
||||
offset = 0;
|
||||
baseURL = [];
|
||||
treeLevel = 0;
|
||||
periodStart = parseTime.durationStr(node.attributes.start) || 0;
|
||||
break;
|
||||
case 'segmentlist':
|
||||
seq = parseInt(node.attributes.startnumber) || seq;
|
||||
timescale = parseInt(node.attributes.timescale) || timescale;
|
||||
duration = parseInt(node.attributes.duration) || duration;
|
||||
offset = parseInt(node.attributes.presentationtimeoffset) || offset;
|
||||
break;
|
||||
case 'segmenttemplate':
|
||||
segmentTemplate = node.attributes;
|
||||
seq = parseInt(node.attributes.startnumber) || seq;
|
||||
timescale = parseInt(node.attributes.timescale) || timescale;
|
||||
break;
|
||||
case 'segmenttimeline':
|
||||
case 'baseurl':
|
||||
lastTag = node.name;
|
||||
break;
|
||||
case 's':
|
||||
timeline.push([
|
||||
parseInt(node.attributes.d),
|
||||
parseInt(node.attributes.r)
|
||||
]);
|
||||
break;
|
||||
case 'adaptationset':
|
||||
case 'representation':
|
||||
treeLevel++;
|
||||
if (targetID == null) {
|
||||
targetID = node.attributes.id;
|
||||
}
|
||||
getSegments = node.attributes.id === targetID + '';
|
||||
if (getSegments) {
|
||||
if (periodStart) {
|
||||
currtime += periodStart;
|
||||
}
|
||||
if (offset) {
|
||||
currtime -= offset / timescale * 1000;
|
||||
}
|
||||
this.emit('starttime', currtime);
|
||||
}
|
||||
if (getSegments && segmentTemplate && timeline.length) {
|
||||
if (segmentTemplate.initialization) {
|
||||
this.emit('item', {
|
||||
url: baseURL.filter(s => !!s).join('') +
|
||||
tmpl(segmentTemplate.initialization),
|
||||
seq: seq - 1,
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
for (let [duration, repeat] of timeline) {
|
||||
duration = duration / timescale * 1000;
|
||||
repeat = repeat || 1;
|
||||
for (let i = 0; i < repeat; i++) {
|
||||
this.emit('item', {
|
||||
url: baseURL.filter(s => !!s).join('') +
|
||||
tmpl(segmentTemplate.media),
|
||||
seq: seq++,
|
||||
duration,
|
||||
});
|
||||
currtime += duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'initialization':
|
||||
if (getSegments) {
|
||||
this.emit('item', {
|
||||
url: baseURL.filter(s => !!s).join('') + node.attributes.sourceurl,
|
||||
seq: seq++,
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'segmenturl':
|
||||
if (getSegments) {
|
||||
let tl = timeline.shift();
|
||||
let segmentDuration = (tl && tl[0] || duration) / timescale * 1000;
|
||||
this.emit('item', {
|
||||
url: baseURL.filter(s => !!s).join('') + node.attributes.media,
|
||||
seq: seq++,
|
||||
duration: segmentDuration,
|
||||
});
|
||||
currtime += segmentDuration;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const onEnd = () => {
|
||||
if (isStatic) { this.emit('endlist'); }
|
||||
if (!getSegments) {
|
||||
this.emit('error', Error(`Representation '${targetID}' not found`));
|
||||
}
|
||||
this.emit('end');
|
||||
};
|
||||
|
||||
this._parser.on('closetag', (tagName) => {
|
||||
switch (tagName) {
|
||||
case 'adaptationset':
|
||||
case 'representation':
|
||||
treeLevel--;
|
||||
break;
|
||||
case 'segmentlist':
|
||||
if (getSegments) {
|
||||
this.emit('endearly');
|
||||
onEnd();
|
||||
this._parser.removeAllListeners();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this._parser.on('text', (text) => {
|
||||
if (lastTag === 'baseurl') {
|
||||
baseURL[treeLevel] = text;
|
||||
lastTag = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.on('finish', onEnd);
|
||||
}
|
||||
|
||||
_write(chunk, encoding, callback) {
|
||||
this._parser.write(chunk, encoding);
|
||||
callback();
|
||||
}
|
||||
};
|
178
node_modules/m3u8stream/lib/index.js
generated
vendored
Normal file
178
node_modules/m3u8stream/lib/index.js
generated
vendored
Normal file
@ -0,0 +1,178 @@
|
||||
const PassThrough = require('stream').PassThrough;
|
||||
const urlResolve = require('url').resolve;
|
||||
const miniget = require('miniget');
|
||||
const m3u8Parser = require('./m3u8-parser');
|
||||
const DashMPDParser = require('./dash-mpd-parser');
|
||||
const Queue = require('./queue');
|
||||
const parseTime = require('./parse-time');
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} playlistURL
|
||||
* @param {Object} options
|
||||
* @return {stream.Readable}
|
||||
*/
|
||||
module.exports = (playlistURL, options) => {
|
||||
const stream = new PassThrough();
|
||||
options = options || {};
|
||||
const chunkReadahead = options.chunkReadahead || 3;
|
||||
const liveBuffer = options.liveBuffer || 20000; // 20 seconds
|
||||
const requestOptions = options.requestOptions;
|
||||
const Parser = {
|
||||
'm3u8': m3u8Parser,
|
||||
'dash-mpd': DashMPDParser,
|
||||
}[options.parser || (/\.mpd$/.test(playlistURL) ? 'dash-mpd' : 'm3u8')];
|
||||
if (!Parser) {
|
||||
throw TypeError(`parser '${options.parser}' not supported`);
|
||||
}
|
||||
let relativeBegin = typeof options.begin === 'string';
|
||||
let begin = relativeBegin ?
|
||||
parseTime.humanStr(options.begin) :
|
||||
Math.max(options.begin - liveBuffer, 0) || 0;
|
||||
let liveBegin = Date.now() - liveBuffer;
|
||||
|
||||
let currSegment;
|
||||
const streamQueue = new Queue((req, callback) => {
|
||||
currSegment = req;
|
||||
// Count the size manually, since the `content-length` header is not
|
||||
// always there.
|
||||
let size = 0;
|
||||
req.on('data', (chunk) => size += chunk.length);
|
||||
req.pipe(stream, { end: false });
|
||||
req.on('end', () => callback(size));
|
||||
}, { concurrency: 1 });
|
||||
|
||||
let segmentNumber = 0;
|
||||
let downloaded = 0;
|
||||
const requestQueue = new Queue((segment, callback) => {
|
||||
let req = miniget(urlResolve(playlistURL, segment.url), requestOptions);
|
||||
req.on('error', callback);
|
||||
streamQueue.push(req, (size) => {
|
||||
downloaded += size;
|
||||
stream.emit('progress', {
|
||||
num: ++segmentNumber,
|
||||
size: size,
|
||||
url: segment.url,
|
||||
duration: segment.duration,
|
||||
}, requestQueue.total, downloaded);
|
||||
callback();
|
||||
});
|
||||
}, { concurrency: chunkReadahead });
|
||||
|
||||
const onError = (err) => {
|
||||
if (ended) { return; }
|
||||
stream.emit('error', err);
|
||||
// Stop on any error.
|
||||
stream.end();
|
||||
};
|
||||
|
||||
// When to look for items again.
|
||||
let refreshThreshold;
|
||||
let minRefreshTime;
|
||||
let refreshTimeout;
|
||||
let fetchingPlaylist = false;
|
||||
let ended = false;
|
||||
let isStatic = false;
|
||||
let lastRefresh;
|
||||
|
||||
const onQueuedEnd = (err) => {
|
||||
currSegment = null;
|
||||
if (err) {
|
||||
onError(err);
|
||||
} else if (!fetchingPlaylist && !ended && !isStatic &&
|
||||
requestQueue.tasks.length + requestQueue.active === refreshThreshold) {
|
||||
let ms = Math.max(0, minRefreshTime - (Date.now() - lastRefresh));
|
||||
refreshTimeout = setTimeout(refreshPlaylist, ms);
|
||||
} else if ((ended || isStatic) &&
|
||||
!requestQueue.tasks.length && !requestQueue.active) {
|
||||
stream.end();
|
||||
}
|
||||
};
|
||||
|
||||
let currPlaylist;
|
||||
let lastSeq;
|
||||
const refreshPlaylist = () => {
|
||||
fetchingPlaylist = true;
|
||||
lastRefresh = Date.now();
|
||||
currPlaylist = miniget(playlistURL, requestOptions);
|
||||
currPlaylist.on('error', onError);
|
||||
const parser = currPlaylist.pipe(new Parser(options.id));
|
||||
let starttime = null;
|
||||
parser.on('starttime', (a) => {
|
||||
starttime = a;
|
||||
if (relativeBegin && begin >= 0) {
|
||||
begin += starttime;
|
||||
}
|
||||
});
|
||||
parser.on('endlist', () => { isStatic = true; });
|
||||
parser.on('endearly', () => { currPlaylist.unpipe(parser); });
|
||||
|
||||
let addedItems = [];
|
||||
let liveAddedItems = [];
|
||||
const addItem = (item, isLive) => {
|
||||
if (item.seq <= lastSeq) { return; }
|
||||
lastSeq = item.seq;
|
||||
begin = item.time;
|
||||
requestQueue.push(item, onQueuedEnd);
|
||||
addedItems.push(item);
|
||||
if (isLive) {
|
||||
liveAddedItems.push(item);
|
||||
}
|
||||
};
|
||||
|
||||
let tailedItems = [], tailedItemsDuration = 0;
|
||||
parser.on('item', (item) => {
|
||||
item.time = starttime;
|
||||
if (!starttime || begin <= item.time) {
|
||||
addItem(item, liveBegin <= item.time);
|
||||
} else {
|
||||
tailedItems.push(item);
|
||||
tailedItemsDuration += item.duration;
|
||||
// Only keep the last `liveBuffer` of items.
|
||||
while (tailedItems.length > 1 &&
|
||||
tailedItemsDuration - tailedItems[0].duration > liveBuffer) {
|
||||
tailedItemsDuration -= tailedItems.shift().duration;
|
||||
}
|
||||
}
|
||||
starttime += item.duration;
|
||||
});
|
||||
|
||||
parser.on('end', () => {
|
||||
currPlaylist = null;
|
||||
// If we are too ahead of the stream, make sure to get the
|
||||
// latest available items with a small buffer.
|
||||
if (!addedItems.length && tailedItems.length) {
|
||||
tailedItems.forEach((item) => { addItem(item, true); });
|
||||
}
|
||||
|
||||
// Refresh the playlist when remaining segments get low.
|
||||
refreshThreshold = Math.max(1, Math.ceil(addedItems.length * 0.01));
|
||||
|
||||
// Throttle refreshing the playlist by looking at the duration
|
||||
// of live items added on this refresh.
|
||||
minRefreshTime =
|
||||
addedItems.reduce(((total, item) => item.duration + total), 0);
|
||||
|
||||
fetchingPlaylist = false;
|
||||
});
|
||||
};
|
||||
refreshPlaylist();
|
||||
|
||||
stream.end = () => {
|
||||
ended = true;
|
||||
streamQueue.die();
|
||||
requestQueue.die();
|
||||
clearTimeout(refreshTimeout);
|
||||
if (currPlaylist) {
|
||||
currPlaylist.unpipe();
|
||||
currPlaylist.abort();
|
||||
}
|
||||
if (currSegment) {
|
||||
currSegment.unpipe();
|
||||
currSegment.abort();
|
||||
}
|
||||
PassThrough.prototype.end.call(stream);
|
||||
};
|
||||
|
||||
return stream;
|
||||
};
|
67
node_modules/m3u8stream/lib/m3u8-parser.js
generated
vendored
Normal file
67
node_modules/m3u8stream/lib/m3u8-parser.js
generated
vendored
Normal file
@ -0,0 +1,67 @@
|
||||
const Writable = require('stream').Writable;
|
||||
|
||||
|
||||
/**
|
||||
* A very simple m3u8 playlist file parser that detects tags and segments.
|
||||
*
|
||||
* @extends WritableStream
|
||||
* @constructor
|
||||
*/
|
||||
module.exports = class m3u8Parser extends Writable {
|
||||
constructor() {
|
||||
super();
|
||||
this._lastLine = '';
|
||||
this._seq = 0;
|
||||
this._nextItemDuration = null;
|
||||
this.on('finish', () => {
|
||||
this._parseLine(this._lastLine);
|
||||
this.emit('end');
|
||||
});
|
||||
}
|
||||
|
||||
_parseLine(line) {
|
||||
let match = line.match(/^#(EXT[A-Z0-9-]+)(?::(.*))?/);
|
||||
if (match) {
|
||||
// This is a tag.
|
||||
const tag = match[1];
|
||||
const value = match[2] || null;
|
||||
switch (tag) {
|
||||
case 'EXT-X-PROGRAM-DATE-TIME':
|
||||
this.emit('starttime', new Date(value).getTime());
|
||||
break;
|
||||
case 'EXT-X-MEDIA-SEQUENCE':
|
||||
this._seq = parseInt(value);
|
||||
break;
|
||||
case 'EXTINF':
|
||||
this._nextItemDuration =
|
||||
Math.round(parseFloat(value.split(',')[0]) * 1000);
|
||||
break;
|
||||
case 'EXT-X-ENDLIST':
|
||||
this.emit('endlist');
|
||||
break;
|
||||
}
|
||||
|
||||
} else if (!/^#/.test(line) && line.trim()) {
|
||||
// This is a segment
|
||||
this.emit('item', {
|
||||
url: line.trim(),
|
||||
seq: this._seq++,
|
||||
duration: this._nextItemDuration,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_write(chunk, encoding, callback) {
|
||||
let lines = chunk.toString('utf8').split('\n');
|
||||
if (this._lastLine) { lines[0] = this._lastLine + lines[0]; }
|
||||
lines.forEach((line, i) => {
|
||||
if (i < lines.length - 1) {
|
||||
this._parseLine(line);
|
||||
} else {
|
||||
// Save the last line in case it has been broken up.
|
||||
this._lastLine = line;
|
||||
}
|
||||
});
|
||||
callback();
|
||||
}
|
||||
};
|
51
node_modules/m3u8stream/lib/parse-time.js
generated
vendored
Normal file
51
node_modules/m3u8stream/lib/parse-time.js
generated
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Converts human friendly time to milliseconds. Supports the format
|
||||
* 00:00:00.000 for hours, minutes, seconds, and milliseconds respectively.
|
||||
* And 0ms, 0s, 0m, 0h, and together 1m1s.
|
||||
*
|
||||
* @param {string|number} time
|
||||
* @return {number}
|
||||
*/
|
||||
const numberFormat = /^\d+$/;
|
||||
const timeFormat = /^(?:(?:(\d+):)?(\d{1,2}):)?(\d{1,2})(?:\.(\d{3}))?$/;
|
||||
const timeUnits = {
|
||||
ms: 1,
|
||||
s: 1000,
|
||||
m: 60000,
|
||||
h: 3600000,
|
||||
};
|
||||
exports.humanStr = (time) => {
|
||||
if (typeof time === 'number') { return time; }
|
||||
if (numberFormat.test(time)) { return +time; }
|
||||
const firstFormat = timeFormat.exec(time);
|
||||
if (firstFormat) {
|
||||
return +(firstFormat[1] || 0) * timeUnits.h +
|
||||
+(firstFormat[2] || 0) * timeUnits.m +
|
||||
+firstFormat[3] * timeUnits.s +
|
||||
+(firstFormat[4] || 0);
|
||||
} else {
|
||||
let total = 0;
|
||||
const r = /(-?\d+)(ms|s|m|h)/g;
|
||||
let rs;
|
||||
while ((rs = r.exec(time)) != null) {
|
||||
total += +rs[1] * timeUnits[rs[2]];
|
||||
}
|
||||
return total;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a duration string in the form of "123.456S", returns milliseconds.
|
||||
*
|
||||
* @param {string} time
|
||||
* @return {number}
|
||||
*/
|
||||
exports.durationStr = (time) => {
|
||||
let total = 0;
|
||||
const r = /(\d+(?:\.\d+)?)(S|M|H)/g;
|
||||
let rs;
|
||||
while ((rs = r.exec(time)) != null) {
|
||||
total += +rs[1] * timeUnits[rs[2].toLowerCase()];
|
||||
}
|
||||
return total;
|
||||
};
|
55
node_modules/m3u8stream/lib/queue.js
generated
vendored
Normal file
55
node_modules/m3u8stream/lib/queue.js
generated
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
module.exports = class Queue {
|
||||
/**
|
||||
* A really simple queue with concurrency.
|
||||
*
|
||||
* @param {Function(Object, Function)} worker
|
||||
* @param {Object} options
|
||||
*/
|
||||
constructor(worker, options) {
|
||||
this._worker = worker;
|
||||
options = options || {};
|
||||
this._concurrency = options.concurrency || 1;
|
||||
this.tasks = [];
|
||||
this.total = 0;
|
||||
this.active = 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Push a task to the queue.
|
||||
*
|
||||
* @param {Object} item
|
||||
* @param {Function(Error)} callback
|
||||
*/
|
||||
push(item, callback) {
|
||||
this.tasks.push({ item, callback });
|
||||
this.total++;
|
||||
this._next();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process next job in queue.
|
||||
*/
|
||||
_next() {
|
||||
if (this.active >= this._concurrency || !this.tasks.length) { return; }
|
||||
const { item, callback } = this.tasks.shift();
|
||||
let callbackCalled = false;
|
||||
this.active++;
|
||||
this._worker(item, (err) => {
|
||||
if (callbackCalled) { return; }
|
||||
this.active--;
|
||||
callbackCalled = true;
|
||||
if (callback) { callback(err); }
|
||||
this._next();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Stops processing queued jobs.
|
||||
*/
|
||||
die() {
|
||||
this.tasks = [];
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user