UNPKG

m3u8parse

Version:

Structural parsing of Apple HTTP Live Streaming .m3u8 format

196 lines (195 loc) 8.17 kB
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; } }