1
0
mirror of https://github.com/musix-org/musix-oss synced 2025-06-16 18:56:00 +00:00
This commit is contained in:
MatteZ02
2019-05-30 12:06:47 +03:00
parent cbdffcf19c
commit 5eb0264906
2502 changed files with 360854 additions and 0 deletions

178
node_modules/m3u8stream/lib/dash-mpd-parser.js generated vendored Normal file
View 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
View 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
View 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
View 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
View 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 = [];
}
};