m3u8parse
Version:
Structural parsing of Apple HTTP Live Streaming .m3u8 format
335 lines (334 loc) • 11.9 kB
JavaScript
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));