UNPKG

m3u8parse

Version:

Structural parsing of Apple HTTP Live Streaming .m3u8 format

335 lines (334 loc) 11.9 kB
import { AttrList } from "./attrlist.js"; import { MediaSegment } from "./media-segment.js"; import { BasePlaylist, cloneAttrArray, rewriteAttrs } from "./playlist-base.js"; var PlaylistType; (function (PlaylistType) { PlaylistType["EVENT"] = "EVENT"; PlaylistType["VOD"] = "VOD"; })(PlaylistType || (PlaylistType = {})); var ArrayMetas; (function (ArrayMetas) { ArrayMetas["DATERANGE"] = "ranges"; ArrayMetas["PRELOAD-HINT"] = "preload_hints"; ArrayMetas["RENDITION-REPORT"] = "rendition_reports"; })(ArrayMetas || (ArrayMetas = {})); const formatMsn = function (obj) { const type = typeof obj; return (obj === undefined || type === 'number' || type === 'bigint') ? obj : +obj; }; const useBigInt = typeof BigInt !== 'undefined' && typeof BigInt(0) === 'bigint'; const toBigInt = useBigInt ? BigInt : (value) => { const number = Number(value); if (isNaN(number) || Math.floor(number) !== number) { throw new SyntaxError(`Cannot convert ${value} to a (fake) BigInt`); } return number; }; const tryBigInt = function (value) { try { if (typeof value === 'bigint') { return value; } if (typeof value === 'number' || typeof value === 'string') { return toBigInt(value); } } catch { } return undefined; }; export class MediaPlaylist extends BasePlaylist { static cast(index) { if (index.master) { throw new Error('Cannot cast a main playlist'); } return index; } constructor(obj) { obj ?? (obj = {}); super(obj); this.master = false; if (obj.master !== undefined && !!obj.master !== this.master) { throw new Error('Cannot create from main playlist'); } this.target_duration = +obj.target_duration || Number.NaN; this.media_sequence = formatMsn(obj.media_sequence) ?? formatMsn(obj.first_seq_no) ?? 0; this.discontinuity_sequence = formatMsn(obj.discontinuity_sequence); this.type = obj.type !== undefined ? `${obj.type}` : undefined; this.i_frames_only = !!obj.i_frames_only; this.ended = !!obj.ended; this.segments = []; if (obj.segments) { this.segments = obj.segments.map((segment) => new MediaSegment(segment)); } this.meta = Object.create(null); if (obj.meta) { if (obj.meta.skip) { this.meta.skip = new AttrList(obj.meta.skip); } for (const key of MediaPlaylist._metas.values()) { if (obj.meta[key]) { this.meta[key] = cloneAttrArray(obj.meta[key]); } } } if (obj.server_control) { this.server_control = new AttrList(obj.server_control); } if (obj.part_info) { this.part_info = new AttrList(obj.part_info); } } _lastSegmentProperty(key, msn, incrFn) { let segment; while ((segment = this.getSegment(msn--)) !== null) { if (incrFn && incrFn(segment)) { return undefined; } const val = segment[key]; if (val) { return val; } } return undefined; } isLive() { return !(this.ended || this.type === PlaylistType.VOD); } totalDuration(includePartial = false) { const segments = this.segments; return segments.reduce((sum, segment) => { if (segment.isPartial()) { if (includePartial) { for (const part of segment.parts ?? []) { sum += part.get('duration', AttrList.Types.Float); } } } else { sum += segment.duration; } return sum; }, 0); } startMsn(full = false) { if (this.segments.length === 0) { return -1; } if (!this.isLive() || full) { return this.media_sequence; } let i; let duration = (this.target_duration || 0) * 3; for (i = ~~this.segments.length - 1; i > 0; --i) { duration -= this.segments[i].duration || 0; if (duration < 0) { break; } } return this.media_sequence + i; } lastMsn(includePartial = true) { if (this.segments.length === 0) { return -1; } const msn = this.media_sequence + this.segments.length - 1; return includePartial ? msn : msn - +this.getSegment(msn).isPartial(); } isValidMsn(msn, part) { msn = tryBigInt(msn); if (msn < toBigInt(this.media_sequence)) { return false; } const lastMsn = toBigInt(this.lastMsn(true)); if (msn > lastMsn) { return false; } if (msn !== lastMsn) { return true; } if (part !== undefined) { if (part < 0) { return this.isValidMsn(msn - toBigInt(1)); } const { parts = { length: -1 } } = this.getSegment(lastMsn); return part <= parts.length; } return !this.getSegment(lastMsn).isPartial(); } dateForMsn(msn, lookahead = false) { let elapsed = 0; const program_time = this._lastSegmentProperty('program_time', msn, ({ duration = 0, discontinuity }) => { elapsed += duration; return discontinuity; }); if (!program_time && lookahead) { } return program_time ? new Date(program_time.getTime() + (elapsed - (this.getSegment(msn).duration || 0)) * 1000) : null; } msnForDate(date, findNearestAfter = false) { if (typeof date === 'boolean') { findNearestAfter = date; date = null; } let startTime = date; if (typeof date !== 'number') { startTime = date ? +new Date(date) : Date.now(); } startTime = startTime; const firstValid = { msn: -1, delta: null, duration: 0 }; let segmentEndTime = -1; const segments = this.segments; const count = ~~segments.length; for (let i = 0; i < count; ++i) { const segment = segments[i]; if (segment.program_time) { segmentEndTime = segment.program_time.getTime(); } if (segment.discontinuity) { segmentEndTime = -1; } const segmentDuration = 1000 * (segment.duration || 0); if (segmentEndTime !== -1 && segmentDuration > 0) { segmentEndTime += segmentDuration; const delta = segmentEndTime - startTime - 1; if (delta >= 0 && (firstValid.delta === null || delta < firstValid.delta || delta < segmentDuration)) { firstValid.msn = this.media_sequence + i; firstValid.delta = delta; firstValid.duration = segmentDuration; } } } if (!findNearestAfter && firstValid.delta >= firstValid.duration) { return -1; } return firstValid.msn; } keysForMsn(msn) { msn = tryBigInt(msn); const keys = new Map(); const initialMsn = msn; let segment; while ((segment = this.getSegment(msn--)) !== null) { if (!segment.keys) { continue; } for (const key of segment.keys) { const keyformat = key.get('keyformat') || 'identity'; if (!keys.has(keyformat)) { const keymethod = key.get('method'); if (keymethod === 'NONE') { return undefined; } keys.set(keyformat, new AttrList(key)); if (this.version < 5) { break; } } } } const identity = keys.get('identity'); if (identity && !identity.has('iv')) { identity.set('iv', initialMsn, AttrList.Types.HexInt); } return keys.size > 0 ? [...keys.values()] : undefined; } byterangeForMsn(msn) { msn = tryBigInt(msn); if (msn === undefined) { return undefined; } const segmentIdx = Number(msn - toBigInt(this.media_sequence)); const segment = this.segments[segmentIdx]; if (!segment || !segment.byterange) { return undefined; } const length = parseInt(segment.byterange.length, 10); if (isNaN(length)) { return undefined; } let offset = parseInt(segment.byterange.offset, 10); if (isNaN(offset)) { offset = 0; for (let i = segmentIdx - 1; i >= 0; --i) { const { uri, byterange } = this.segments[i]; if (uri !== segment.uri) { continue; } if (!byterange) { break; } const segmentLength = parseInt(byterange.length, 10); const segmentOffset = parseInt(byterange.offset, 10); if (isNaN(segmentLength)) { break; } offset += segmentLength; if (!isNaN(segmentOffset)) { offset += segmentOffset; break; } } } return { length, offset }; } mapForMsn(msn) { return this._lastSegmentProperty('map', msn); } bitrateForMsn(msn) { return this._lastSegmentProperty('bitrate', msn); } getSegment(msn, independent = false) { msn = tryBigInt(msn); if (msn === undefined) { return null; } const index = Number(msn - toBigInt(this.media_sequence)); const rawSegment = this.segments[index] ?? null; if (!independent || !rawSegment) { return rawSegment; } const segment = new MediaSegment(rawSegment); segment.program_time = this.dateForMsn(msn); segment.keys = this.keysForMsn(msn); if (this.version >= 4) { segment.byterange = this.byterangeForMsn(msn); } if (this.version >= 5) { segment.map = this.mapForMsn(msn); } if (this.version >= 8) { segment.bitrate = this.bitrateForMsn(msn); } if (segment.parts) { let lastPart; for (const part of segment.parts) { if (lastPart) { const byterange = part.get('byterange', AttrList.Types.Byterange); if (byterange && byterange.offset === undefined && part.get('uri') === lastPart.get('uri')) { const lastByterange = lastPart.get('byterange', AttrList.Types.Byterange); if (lastByterange?.offset !== undefined) { byterange.offset = lastByterange.offset + lastByterange.length; part.set('byterange', byterange, AttrList.Types.Byterange); } } } lastPart = part; } } return segment; } rewriteUris(mapFn) { for (const segment of this.segments) { segment.rewriteUris(mapFn); } if (this.meta) { rewriteAttrs(mapFn, this.meta.preload_hints, 'preload-hint'); rewriteAttrs(mapFn, this.meta.rendition_reports, 'rendition-report'); } return this; } } MediaPlaylist.Type = PlaylistType; MediaPlaylist._metas = new Map(Object.entries(ArrayMetas));