m3u8parse
Version:
Structural parsing of Apple HTTP Live Streaming .m3u8 format
196 lines (195 loc) • 8.17 kB
JavaScript
import { AttrList, AttrType } from "./attrlist.js";
import deserialize from "./attr-deserialize.js";
import { MediaPlaylist, MainPlaylist, MediaSegment } from "./playlist.js";
export var PlaylistType;
(function (PlaylistType) {
PlaylistType["Main"] = "main";
PlaylistType["Media"] = "media";
})(PlaylistType || (PlaylistType = {}));
const parseDecimalInteger = deserialize[AttrType.Int];
const extParser = new Map([
['VERSION', (_, arg) => _.m3u8.version = parseInt(arg, 10)],
['TARGETDURATION', (_, arg) => _.m3u8.target_duration = parseDecimalInteger(arg)],
['MEDIA-SEQUENCE', (_, arg) => _.m3u8.media_sequence = parseDecimalInteger(arg)],
['DISCONTINUITY-SEQUENCE', (_, arg) => _.m3u8.discontinuity_sequence = parseDecimalInteger(arg)],
['PLAYLIST-TYPE', (_, arg) => _.m3u8.type = arg],
['START', (_, arg) => _.m3u8.start = new AttrList(arg)],
['INDEPENDENT-SEGMENTS', (_) => _.m3u8.independent_segments = true],
['ENDLIST', (_) => _.m3u8.ended = true],
['KEY', (_, arg) => { var _a; return ((_a = _.meta).keys ?? (_a.keys = [])).push(new AttrList(arg)); }],
['PROGRAM-DATE-TIME', (_, arg) => _.meta.program_time = new Date(arg)],
['DISCONTINUITY', (_) => _.meta.discontinuity = true],
['STREAM-INF', (_, arg) => {
_.m3u8.master = true;
_.meta.info = new AttrList(arg);
}],
['MEDIA', (_, arg) => {
var _a;
const attrs = new AttrList(arg);
const id = attrs.get('group-id', AttrList.Types.String) ?? '#';
let list = ((_a = _.m3u8).groups ?? (_a.groups = new Map())).get(id);
if (!list) {
list = [];
_.m3u8.groups.set(id, list);
if (id !== '#') {
list.type = attrs.get('type');
}
}
list.push(attrs);
}],
['I-FRAME-STREAM-INF', (_, arg) => { var _a; return ((_a = _.m3u8).iframes ?? (_a.iframes = [])).push(new AttrList(arg)); }],
['SESSION-DATA', (_, arg) => {
var _a;
const attrs = new AttrList(arg);
const id = attrs.get('data-id', AttrList.Types.String);
if (id) {
let list = ((_a = _.m3u8).data ?? (_a.data = new Map())).get(id);
if (!list) {
list = [];
_.m3u8.data.set(id, list);
}
list.push(attrs);
}
}],
['SESSION-KEY', (_, arg) => { var _a; return ((_a = _.m3u8).session_keys ?? (_a.session_keys = [])).push(new AttrList(arg)); }],
['GAP', (_) => _.meta.gap = true],
['BITRATE', (_, arg) => _.meta.bitrate = parseDecimalInteger(arg)],
['DEFINE', (_, arg) => { var _a; return ((_a = _.m3u8).defines ?? (_a.defines = [])).push(new AttrList(arg)); }],
['PART-INF', (_, arg) => _.m3u8.part_info = new AttrList(arg)],
['PART', (_, arg) => { var _a; return ((_a = _.meta).parts ?? (_a.parts = [])).push(new AttrList(arg)); }],
['SERVER-CONTROL', (_, arg) => _.m3u8.server_control = new AttrList(arg)],
['I-FRAMES-ONLY', (_) => _.m3u8.i_frames_only = true],
['BYTERANGE', (_, arg) => {
const n = arg.split('@');
_.meta.byterange = { length: parseInt(n[0], 10) };
if (n.length > 1) {
_.meta.byterange.offset = parseInt(n[1], 10);
}
}],
['MAP', (_, arg) => _.meta.map = new AttrList(arg)],
['SKIP', (_, arg) => { var _a; return ((_a = _.m3u8).meta ?? (_a.meta = Object.create(null))).skip = new AttrList(arg); }]
]);
for (const [ext, entry] of MediaPlaylist._metas.entries()) {
extParser.set(ext, (_, arg) => {
var _a;
const m3u8meta = (_a = _.m3u8).meta ?? (_a.meta = Object.create(null));
(m3u8meta[entry] ?? (m3u8meta[entry] = [])).push(new AttrList(arg));
});
}
export class M3U8Parser {
static debug(line, ...args) { }
constructor(options = {}) {
this.state = { m3u8: {}, meta: {} };
this.lineNo = 0;
this.debug = options.debug ?? M3U8Parser.debug;
this.extensions = Object.assign({}, options.extensions);
}
feed(line) {
if (typeof line !== 'string') {
throw new TypeError('Passed line must be string');
}
this._parseLine(line);
}
finalize(type) {
var _a;
const { state } = this;
if (this.lineNo === 0) {
throw new ParserError('No line data');
}
if (Object.keys(state.meta).length) {
((_a = state.m3u8).segments ?? (_a.segments = [])).push(new MediaSegment(undefined, state.meta, state.m3u8.version));
state.meta = {};
}
if (type) {
if (type !== PlaylistType.Main && type !== PlaylistType.Media) {
throw new TypeError(`Passed type must be "${PlaylistType.Main}" or "${PlaylistType.Media}"`);
}
if (!!state.m3u8.master !== (type === PlaylistType.Main)) {
throw new ParserError('Incorrect playlist type');
}
}
return state.m3u8.master ? new MainPlaylist(state.m3u8) : new MediaPlaylist(state.m3u8);
}
debug(line, ...args) { }
_parseLine(line) {
var _a, _b;
const { state } = this;
this.lineNo += 1;
if (this.lineNo === 1) {
if (line !== '#EXTM3U') {
throw new ParserError('Missing required #EXTM3U header', { line, line_no: this.lineNo });
}
return;
}
if (!line.length) {
return;
}
if (line[0] === '#') {
const matches = /^(#EXT[^:]*)(:?.*)$/.exec(line);
if (!matches) {
return this.debug('ignoring comment', line);
}
const cmd = matches[1];
const arg = matches[2].length > 1 ? matches[2].slice(1) : null;
try {
if (!this._parseExt(cmd, arg)) {
return this.debug('ignoring unknown #EXT:' + cmd, this.lineNo);
}
}
catch (err) {
throw new ParserError('Ext parsing failed', { line, line_no: this.lineNo, cause: err });
}
}
else if (state.m3u8.master) {
state.meta.uri = line;
((_a = state.m3u8).variants ?? (_a.variants = [])).push({ uri: state.meta.uri, info: state.meta.info });
state.meta = {};
}
else {
if (!('duration' in state.meta)) {
throw new ParserError('Missing #EXTINF before media file URI', { line, line_no: this.lineNo });
}
((_b = state.m3u8).segments ?? (_b.segments = [])).push(new MediaSegment(line, state.meta, state.m3u8.version));
state.meta = {};
}
}
_parseExt(cmd, arg = null) {
const { state } = this;
if (cmd in this.extensions) {
const extObj = this.extensions[cmd] ? state.meta : state.m3u8;
if (!extObj.vendor) {
extObj.vendor = [];
}
extObj.vendor.push([cmd, arg]);
return true;
}
if (!cmd.startsWith('#EXT-X-')) {
if (arg && cmd === '#EXTINF') {
const [duration, ...title] = arg.split(',');
state.meta.duration = parseFloat(duration);
state.meta.title = title.join(',');
if (state.meta.duration <= 0) {
throw new Error('Invalid duration');
}
return true;
}
return false;
}
const name = cmd.slice(7);
const handler = extParser.get(name);
if (!handler) {
return false;
}
this.debug('parsing ext', cmd, arg);
handler(this.state, arg);
return true;
}
}
export class ParserError extends Error {
constructor(msg, options) {
super(msg ?? 'Error', options?.cause ? { cause: options.cause } : undefined);
this.name = 'ParserError';
this.line = options?.line ?? '';
this.lineNumber = options?.line_no ?? -1;
}
}