@eyevinn/m3u8
Version:
streaming m3u8 parser for Apple's HTTP Live Streaming protocol
353 lines (316 loc) • 9.66 kB
JavaScript
var util = require('util'),
ChunkedStream = require('chunked-stream'),
M3U = require('./m3u'),
PlaylistItem = require('./m3u/PlaylistItem'),
StreamItem = require('./m3u/StreamItem'),
IframeStreamItem = require('./m3u/IframeStreamItem'),
MediaItem = require('./m3u/MediaItem');
// used for splitting strings by commas not within double quotes
var NON_QUOTED_COMMA = /,(?=(?:[^"]|"[^"]*")*$)/;
var m3uParser = module.exports = function m3uParser() {
ChunkedStream.apply(this, ['\n', true]);
this.linesRead = 0;
this.m3u = new M3U;
this.cueOut = null;
this.cueOutCont = null;
this.cueIn = null;
this.assetData = null;
this.scteData = null;
this.dateRangeData = null;
this.key = null;
this.keys = null;
this.map = null;
this.startOffset = null;
this.on('data', this.parse.bind(this));
var self = this;
this.on('end', function() {
if(this.cueIn == true) {
this.addItem(new PlaylistItem);
this.currentItem.set('cuein', true);
this.cueIn = null;
}
self.emit('m3u', self.m3u);
});
};
util.inherits(m3uParser, ChunkedStream);
m3uParser.M3U = M3U;
m3uParser.createStream = function() {
return new m3uParser;
};
m3uParser.prototype.parse = function parse(line) {
line = line.trim();
if (this.linesRead == 0) {
if (line != '#EXTM3U') {
return this.emit('error', new Error(
'Non-valid M3U file. First line: ' + line
));
}
this.linesRead++;
return true;
}
if (['', '#EXT-X-ENDLIST'].indexOf(line) > -1) return true;
if (line.indexOf('#') == 0) {
this.parseLine(line);
} else {
if (this.currentItem.attributes.uri != undefined) {
this.addItem(new PlaylistItem);
}
if (this.datetime) {
this.currentItem.set('date', this.datetime);
}
this.currentItem.set('uri', line);
this.emit('item', this.currentItem);
}
this.linesRead++;
};
m3uParser.prototype.parseLine = function parseLine(line) {
var parts = line.slice(1).split(/:(.*)/);
var tag = parts[0];
var data = parts[1];
if (typeof this[tag] == 'function') {
this[tag](data, tag);
} else {
this.m3u.set(tag, data);
}
};
m3uParser.prototype.addItem = function addItem(item) {
this.m3u.addItem(item);
this.currentItem = item;
return item;
};
m3uParser.prototype['EXTINF'] = function parseInf(data) {
this.addItem(new PlaylistItem);
data = data.split(',');
this.currentItem.set('duration', parseFloat(data[0]));
this.currentItem.set('title', data[1]);
if (this.playlistDiscontinuity) {
this.currentItem.set('discontinuity', true);
this.playlistDiscontinuity = false;
}
if (this.datetime) {
this.currentItem.set('date', this.datetime);
this.datetime = null;
}
if (this.cueOut !== null) {
this.currentItem.set('cueout', this.cueOut);
this.cueOut = null;
if (this.assetData !== null) {
this.currentItem.set('assetdata', this.assetData);
this.assetData = null;
}
if (this.scteData !== null) {
this.currentItem.set('sctedata', this.scteData);
this.scteData = null;
}
}
if (this.cueOutCont !== null) {
this.currentItem.set('cont-offset', this.cueOutCont.offset);
this.currentItem.set('cont-dur', this.cueOutCont.duration);
if (this.cueOutCont.scteData) {
this.currentItem.set('sctedata', this.cueOutCont.scteData);
}
this.cueOutCont = null;
}
if (this.cueIn !== null) {
this.currentItem.set('cuein', true);
this.cueIn = null;
}
if (this.dateRangeData !== null) {
this.currentItem.set('daterange', this.dateRangeData);
this.dateRangeData = null;
}
if (this.key != null) {
this.currentItem.set('key-method', this.key.method);
if (this.key.uri) {
this.currentItem.set('key-uri', this.key.uri);
}
if (this.key.iv) {
this.currentItem.set('key-iv', this.key.iv);
}
if (this.key.keyId) {
this.currentItem.set('key-id', this.key.keyId);
}
if (this.key.keyFormat) {
this.currentItem.set('key-keyformat', this.key.keyFormat);
}
if (this.key.keyFormatVersions) {
this.currentItem.set('key-keyformatversions', this.key.keyFormatVersions);
}
this.key = null;
}
if (this.keys != null) {
this.currentItem.set('keys', this.keys);
this.keys = null;
}
if (this.map !== null) {
this.currentItem.set('map-uri', this.map.uri);
if (this.map.byterange) {
this.currentItem.set('map-byterange', this.map.byterange);
}
this.map = null;
}
if (this.startOffset !== null) {
this.currentItem.set('start-timeoffset', this.startOffset.timeOffset);
if (this.startOffset.precise) {
this.currentItem.set('start-precise', this.startOffset.precise);
}
this.startOffset = null;
}
};
m3uParser.prototype['EXT-X-DISCONTINUITY'] = function parseInf() {
this.playlistDiscontinuity = true;
};
m3uParser.prototype['EXT-X-PROGRAM-DATE-TIME'] = function parseInf(data) {
this.datetime = data;
};
m3uParser.prototype['EXT-X-CUE-OUT'] = function parseInf(data) {
var attr = this.parseAttributes(data);
var durationAttr = attr.find(elem => elem.key.toLowerCase() === 'duration');
if(durationAttr) {
this.cueOut = durationAttr.value;
} else {
const duration = parseInt(data);
this.cueOut = !isNaN(duration) ? duration : 0;
}
}
m3uParser.prototype['EXT-X-CUE-OUT-CONT'] = function parseInf(data) {
const m = data.match(/(\d+\.?\d*)\/(\d+\.?\d*)/);
if (m) {
const offset = m[1];
const duration = m[2];
this.cueOutCont = { offset: Number(offset), duration: Number(duration) };
}
else {
const cueOutInfo = { offset: false, duration: false }
for (const match of data.matchAll(/(ElapsedTime|Duration|SCTE35)=([^,]*)/g)) {
switch(match[1]) {
case 'ElapsedTime':
cueOutInfo.offset = Number(match[2]);
break;
case 'Duration':
cueOutInfo.duration = Number(match[2]);
break;
case 'SCTE35':
cueOutInfo.scteData = match[2];
break;
}
}
if (cueOutInfo.offset !== false && cueOutInfo.duration !== false) {
this.cueOutCont = cueOutInfo;
}
}
}
m3uParser.prototype['EXT-OATCLS-SCTE35'] = function parseInf(data) {
this.scteData = data;
}
m3uParser.prototype['EXT-X-CUE-IN'] = function parseInf() {
this.cueIn = true;
}
m3uParser.prototype['EXT-X-ASSET'] = function parseInf(data) {
this.assetData = data;
};
m3uParser.prototype['EXT-X-BYTERANGE'] = function parseByteRange(data) {
this.currentItem.set('byteRange', data);
};
m3uParser.prototype['EXT-X-DATERANGE'] = function parseDateRange(data) {
this.dateRangeData = data;
};
m3uParser.prototype['EXT-X-STREAM-INF'] = function(data) {
this.addItem(new StreamItem(this.parseAttributes(data)));
};
m3uParser.prototype['EXT-X-I-FRAME-STREAM-INF'] = function(data) {
this.addItem(new IframeStreamItem(this.parseAttributes(data)));
this.emit('item', this.currentItem);
};
m3uParser.prototype['EXT-X-MEDIA'] = function(data) {
this.addItem(new MediaItem(this.parseAttributes(data)));
this.emit('item', this.currentItem);
};
m3uParser.prototype['EXT-X-KEY'] = function parseInf(data) {
this.key = {
method: null,
uri: null,
iv: null,
keyId: null,
keyFormat: null,
keyFormatVersions: null,
};
var attr = this.parseAttributes(data);
var method = attr.find(elem => elem.key.toLowerCase() === 'method');
if (method) {
this.key.method = method.value;
} else {
this.key.method = 'none';
}
if (method.value.toLowerCase() !== 'none' ) {
var uri = attr.find(elem => elem.key.toLowerCase() === 'uri');
var iv = attr.find(elem => elem.key.toLowerCase() === 'iv');
var keyId = attr.find(elem => elem.key.toLowerCase() === 'keyid');
var keyFormat = attr.find(elem => elem.key.toLowerCase() === 'keyformat');
var keyFormatVersions = attr.find(elem => elem.key.toLowerCase() === 'keyformatversions');
if (uri) {
this.key.uri = uri.value;
}
if (iv) {
this.key.iv = iv.value;
}
if (keyId) {
this.key.keyId = keyId.value;
}
if (keyFormat) {
this.key.keyFormat = keyFormat.value;
}
if (keyFormatVersions) {
this.key.keyFormatVersions = keyFormatVersions.value;
}
}
if (!this.keys) {
this.keys = {};
}
if (this.key.keyFormat) {
this.keys[this.key.keyFormat.slice(1,-1)] = this.key;
}
}
m3uParser.prototype['EXT-X-MAP'] = function parseMap(data) {
this.map = {
uri: null,
byterange: null
}
var attr = this.parseAttributes(data);
var uri = attr.find(elem => elem.key.toLowerCase() === 'uri');
if (uri) {
this.map.uri = uri.value;
}
var byterange = attr.find(elem => elem.key.toLowerCase() === 'byterange');
if (byterange) {
this.map.byterange = byterange.value;
}
};
m3uParser.prototype['EXT-X-START'] = function parseInf(data) {
this.startOffset = {
timeOffset: null,
precise: null
}
var attr = this.parseAttributes(data);
var offset = attr.find(elem => elem.key.toLowerCase() === 'time-offset');
if (offset) {
this.startOffset.timeOffset = offset.value;
}
var precise = attr.find(elem => elem.key.toLowerCase() === 'precise');
if (precise) {
this.startOffset.precise = precise.value;
}
};
m3uParser.prototype.parseAttributes = function parseAttributes(data) {
data = data.split(NON_QUOTED_COMMA);
var self = this;
return data.map(function(attribute) {
var keyValue = attribute.split(/=(.+)/).map(function(str) {
return str.trim();
});
return {
key : keyValue[0],
value : keyValue[1]
};
});
};