download
raw
34.5 kB
import { buildAbsoluteURL } from 'url-toolkit';
import { DateRange } from './date-range';
import { Fragment, Part } from './fragment';
import { LevelDetails } from './level-details';
import { LevelKey } from './level-key';
import { AttrList } from '../utils/attr-list';
import { isCodecType } from '../utils/codecs';
import { logger } from '../utils/logger';
import {
addVariableDefinition,
hasVariableReferences,
importVariableDefinition,
substituteVariables,
} from '../utils/variable-substitution';
import type { MediaFragment } from './fragment';
import type { ContentSteeringOptions } from '../types/events';
import type {
CodecsParsed,
LevelAttributes,
LevelParsed,
VariableMap,
} from '../types/level';
import type { PlaylistLevelType } from '../types/loader';
import type { MediaAttributes, MediaPlaylist } from '../types/media-playlist';
import type { CodecType } from '../utils/codecs';
type M3U8ParserFragments = Array<Fragment | null>;
export type ParsedMultivariantPlaylist = {
contentSteering: ContentSteeringOptions | null;
levels: LevelParsed[];
playlistParsingError: Error | null;
sessionData: Record<string, AttrList> | null;
sessionKeys: LevelKey[] | null;
startTimeOffset: number | null;
variableList: VariableMap | null;
hasVariableRefs: boolean;
};
type ParsedMultivariantMediaOptions = {
AUDIO?: MediaPlaylist[];
SUBTITLES?: MediaPlaylist[];
'CLOSED-CAPTIONS'?: MediaPlaylist[];
};
type LevelKeys = { [key: string]: LevelKey | undefined };
const MASTER_PLAYLIST_REGEX =
/#EXT-X-STREAM-INF:([^\r\n]*)(?:[\r\n](?:#[^\r\n]*)?)*([^\r\n]+)|#EXT-X-(SESSION-DATA|SESSION-KEY|DEFINE|CONTENT-STEERING|START):([^\r\n]*)[\r\n]+/g;
const MASTER_PLAYLIST_MEDIA_REGEX = /#EXT-X-MEDIA:(.*)/g;
const IS_MEDIA_PLAYLIST = /^#EXT(?:INF|-X-TARGETDURATION):/m; // Handle empty Media Playlist (first EXTINF not signaled, but TARGETDURATION present)
const LEVEL_PLAYLIST_REGEX_FAST = new RegExp(
[
/#EXTINF:\s*(\d*(?:\.\d+)?)(?:,(.*)\s+)?/.source, // duration (#EXTINF:<duration>,<title>), group 1 => duration, group 2 => title
/(?!#) *(\S[^\r\n]*)/.source, // segment URI, group 3 => the URI (note newline is not eaten)
/#.*/.source, // All other non-segment oriented tags will match with all groups empty
].join('|'),
'g',
);
const LEVEL_PLAYLIST_REGEX_SLOW = new RegExp(
[
/#EXT-X-(PROGRAM-DATE-TIME|BYTERANGE|DATERANGE|DEFINE|KEY|MAP|PART|PART-INF|PLAYLIST-TYPE|PRELOAD-HINT|RENDITION-REPORT|SERVER-CONTROL|SKIP|START):(.+)/
.source,
/#EXT-X-(BITRATE|DISCONTINUITY-SEQUENCE|MEDIA-SEQUENCE|TARGETDURATION|VERSION): *(\d+)/
.source,
/#EXT-X-(DISCONTINUITY|ENDLIST|GAP|INDEPENDENT-SEGMENTS)/.source,
/(#)([^:]*):(.*)/.source,
/(#)(.*)(?:.*)\r?\n?/.source,
].join('|'),
);
export default class M3U8Parser {
static findGroup(
groups: (
| { id?: string; audioCodec?: string }
| { id?: string; textCodec?: string }
)[],
mediaGroupId: string,
):
| { id?: string; audioCodec?: string }
| { id?: string; textCodec?: string }
| undefined {
for (let i = 0; i < groups.length; i++) {
const group = groups[i];
if (group.id === mediaGroupId) {
return group;
}
}
}
static resolve(url, baseUrl) {
return buildAbsoluteURL(baseUrl, url, { alwaysNormalize: true });
}
static isMediaPlaylist(str: string): boolean {
return IS_MEDIA_PLAYLIST.test(str);
}
static parseMasterPlaylist(
string: string,
baseurl: string,
): ParsedMultivariantPlaylist {
const hasVariableRefs = __USE_VARIABLE_SUBSTITUTION__
? hasVariableReferences(string)
: false;
const parsed: ParsedMultivariantPlaylist = {
contentSteering: null,
levels: [],
playlistParsingError: null,
sessionData: null,
sessionKeys: null,
startTimeOffset: null,
variableList: null,
hasVariableRefs,
};
const levelsWithKnownCodecs: LevelParsed[] = [];
MASTER_PLAYLIST_REGEX.lastIndex = 0;
if (!string.startsWith('#EXTM3U')) {
parsed.playlistParsingError = new Error('no EXTM3U delimiter');
return parsed;
}
let result: RegExpExecArray | null;
while ((result = MASTER_PLAYLIST_REGEX.exec(string)) != null) {
if (result[1]) {
// '#EXT-X-STREAM-INF' is found, parse level tag in group 1
const attrs = new AttrList(result[1], parsed) as LevelAttributes;
const uri = __USE_VARIABLE_SUBSTITUTION__
? substituteVariables(parsed, result[2])
: result[2];
const level: LevelParsed = {
attrs,
bitrate:
attrs.decimalInteger('BANDWIDTH') ||
attrs.decimalInteger('AVERAGE-BANDWIDTH'),
name: attrs.NAME,
url: M3U8Parser.resolve(uri, baseurl),
};
const resolution = attrs.decimalResolution('RESOLUTION');
if (resolution) {
level.width = resolution.width;
level.height = resolution.height;
}
setCodecs(attrs.CODECS, level);
const supplementalCodecs = attrs['SUPPLEMENTAL-CODECS'];
if (supplementalCodecs) {
level.supplemental = {};
setCodecs(supplementalCodecs, level.supplemental);
}
if (!level.unknownCodecs?.length) {
levelsWithKnownCodecs.push(level);
}
parsed.levels.push(level);
} else if (result[3]) {
const tag = result[3];
const attributes = result[4];
switch (tag) {
case 'SESSION-DATA': {
// #EXT-X-SESSION-DATA
const sessionAttrs = new AttrList(attributes, parsed);
const dataId = sessionAttrs['DATA-ID'];
if (dataId) {
if (parsed.sessionData === null) {
parsed.sessionData = {};
}
parsed.sessionData[dataId] = sessionAttrs;
}
break;
}
case 'SESSION-KEY': {
// #EXT-X-SESSION-KEY
const sessionKey = parseKey(attributes, baseurl, parsed);
if (sessionKey.encrypted && sessionKey.isSupported()) {
if (parsed.sessionKeys === null) {
parsed.sessionKeys = [];
}
parsed.sessionKeys.push(sessionKey);
} else {
logger.warn(
`[Keys] Ignoring invalid EXT-X-SESSION-KEY tag: "${attributes}"`,
);
}
break;
}
case 'DEFINE': {
// #EXT-X-DEFINE
if (__USE_VARIABLE_SUBSTITUTION__) {
const variableAttributes = new AttrList(attributes, parsed);
addVariableDefinition(parsed, variableAttributes, baseurl);
}
break;
}
case 'CONTENT-STEERING': {
// #EXT-X-CONTENT-STEERING
const contentSteeringAttributes = new AttrList(attributes, parsed);
parsed.contentSteering = {
uri: M3U8Parser.resolve(
contentSteeringAttributes['SERVER-URI'],
baseurl,
),
pathwayId: contentSteeringAttributes['PATHWAY-ID'] || '.',
};
break;
}
case 'START': {
// #EXT-X-START
parsed.startTimeOffset = parseStartTimeOffset(attributes);
break;
}
default:
break;
}
}
}
// Filter out levels with unknown codecs if it does not remove all levels
const stripUnknownCodecLevels =
levelsWithKnownCodecs.length > 0 &&
levelsWithKnownCodecs.length < parsed.levels.length;
parsed.levels = stripUnknownCodecLevels
? levelsWithKnownCodecs
: parsed.levels;
if (parsed.levels.length === 0) {
parsed.playlistParsingError = new Error('no levels found in manifest');
}
return parsed;
}
static parseMasterPlaylistMedia(
string: string,
baseurl: string,
parsed: ParsedMultivariantPlaylist,
): ParsedMultivariantMediaOptions {
let result: RegExpExecArray | null;
const results: ParsedMultivariantMediaOptions = {};
const levels = parsed.levels;
const groupsByType = {
AUDIO: levels.map((level: LevelParsed) => ({
id: level.attrs.AUDIO,
audioCodec: level.audioCodec,
})),
SUBTITLES: levels.map((level: LevelParsed) => ({
id: level.attrs.SUBTITLES,
textCodec: level.textCodec,
})),
'CLOSED-CAPTIONS': [],
};
let id = 0;
MASTER_PLAYLIST_MEDIA_REGEX.lastIndex = 0;
while ((result = MASTER_PLAYLIST_MEDIA_REGEX.exec(string)) !== null) {
const attrs = new AttrList(result[1], parsed) as MediaAttributes;
const type = attrs.TYPE;
if (type) {
const groups:
| (typeof groupsByType)[keyof typeof groupsByType]
| undefined = groupsByType[type];
const medias: MediaPlaylist[] = results[type] || [];
results[type] = medias;
const lang = attrs.LANGUAGE;
const assocLang = attrs['ASSOC-LANGUAGE'];
const channels = attrs.CHANNELS;
const characteristics = attrs.CHARACTERISTICS;
const instreamId = attrs['INSTREAM-ID'];
const media: MediaPlaylist = {
attrs,
bitrate: 0,
id: id++,
groupId: attrs['GROUP-ID'] || '',
name: attrs.NAME || lang || '',
type,
default: attrs.bool('DEFAULT'),
autoselect: attrs.bool('AUTOSELECT'),
forced: attrs.bool('FORCED'),
lang,
url: attrs.URI ? M3U8Parser.resolve(attrs.URI, baseurl) : '',
};
if (assocLang) {
media.assocLang = assocLang;
}
if (channels) {
media.channels = channels;
}
if (characteristics) {
media.characteristics = characteristics;
}
if (instreamId) {
media.instreamId = instreamId;
}
if (groups?.length) {
// If there are audio or text groups signalled in the manifest, let's look for a matching codec string for this track
// If we don't find the track signalled, lets use the first audio groups codec we have
// Acting as a best guess
const groupCodec =
M3U8Parser.findGroup(groups, media.groupId as string) || groups[0];
assignCodec(media, groupCodec, 'audioCodec');
assignCodec(media, groupCodec, 'textCodec');
}
medias.push(media);
}
}
return results;
}
static parseLevelPlaylist(
string: string,
baseurl: string,
id: number,
type: PlaylistLevelType,
levelUrlId: number,
multivariantVariableList: VariableMap | null,
): LevelDetails {
const base = { url: baseurl };
const level = new LevelDetails(baseurl);
const fragments: M3U8ParserFragments = level.fragments;
const programDateTimes: MediaFragment[] = [];
// The most recent init segment seen (applies to all subsequent segments)
let currentInitSegment: Fragment | null = null;
let currentSN = 0;
let currentPart = 0;
let totalduration = 0;
let discontinuityCounter = 0;
let currentBitrate = 0;
let prevFrag: Fragment | null = null;
let frag: Fragment = new Fragment(type, base);
let result: RegExpExecArray | RegExpMatchArray | null;
let i: number;
let levelkeys: LevelKeys | undefined;
let firstPdtIndex = -1;
let createNextFrag = false;
let nextByteRange: string | null = null;
let serverControlAttrs: AttrList | undefined;
LEVEL_PLAYLIST_REGEX_FAST.lastIndex = 0;
level.m3u8 = string;
level.hasVariableRefs = __USE_VARIABLE_SUBSTITUTION__
? hasVariableReferences(string)
: false;
if (LEVEL_PLAYLIST_REGEX_FAST.exec(string)?.[0] !== '#EXTM3U') {
level.playlistParsingError = new Error(
'Missing format identifier #EXTM3U',
);
return level;
}
while ((result = LEVEL_PLAYLIST_REGEX_FAST.exec(string)) !== null) {
if (createNextFrag) {
createNextFrag = false;
frag = new Fragment(type, base);
// setup the next fragment for part loading
frag.playlistOffset = totalduration;
frag.setStart(totalduration);
frag.sn = currentSN;
frag.cc = discontinuityCounter;
if (currentBitrate) {
frag.bitrate = currentBitrate;
}
frag.level = id;
if (currentInitSegment) {
frag.initSegment = currentInitSegment;
if (currentInitSegment.rawProgramDateTime) {
frag.rawProgramDateTime = currentInitSegment.rawProgramDateTime;
currentInitSegment.rawProgramDateTime = null;
}
if (nextByteRange) {
frag.setByteRange(nextByteRange);
nextByteRange = null;
}
}
}
const duration = result[1];
if (duration) {
// INF
frag.duration = parseFloat(duration);
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
const title = (' ' + result[2]).slice(1);
frag.title = title || null;
frag.tagList.push(title ? ['INF', duration, title] : ['INF', duration]);
} else if (result[3]) {
// url
if (Number.isFinite(frag.duration)) {
frag.playlistOffset = totalduration;
frag.setStart(totalduration);
if (levelkeys) {
setFragLevelKeys(frag, levelkeys, level);
}
frag.sn = currentSN;
frag.level = id;
frag.cc = discontinuityCounter;
fragments.push(frag);
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
const uri = (' ' + result[3]).slice(1);
frag.relurl = __USE_VARIABLE_SUBSTITUTION__
? substituteVariables(level, uri)
: uri;
assignProgramDateTime(
frag as MediaFragment,
prevFrag as MediaFragment,
programDateTimes,
);
prevFrag = frag;
totalduration += frag.duration;
currentSN++;
currentPart = 0;
createNextFrag = true;
}
} else {
result = result[0].match(LEVEL_PLAYLIST_REGEX_SLOW);
if (!result) {
logger.warn('No matches on slow regex match for level playlist!');
continue;
}
for (i = 1; i < result.length; i++) {
if ((result[i] as any) !== undefined) {
break;
}
}
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
const tag = (' ' + result[i]).slice(1);
const value1 = (' ' + result[i + 1]).slice(1);
const value2 = result[i + 2] ? (' ' + result[i + 2]).slice(1) : null;
switch (tag) {
case 'BYTERANGE':
if (prevFrag) {
frag.setByteRange(value1, prevFrag);
} else {
frag.setByteRange(value1);
}
break;
case 'PROGRAM-DATE-TIME':
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
frag.rawProgramDateTime = value1;
frag.tagList.push(['PROGRAM-DATE-TIME', value1]);
if (firstPdtIndex === -1) {
firstPdtIndex = fragments.length;
}
break;
case 'PLAYLIST-TYPE':
if (level.type) {
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
}
level.type = value1.toUpperCase();
break;
case 'MEDIA-SEQUENCE':
if (level.startSN !== 0) {
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
} else if (fragments.length > 0) {
assignMustAppearBeforeSegmentsError(level, tag, result);
}
currentSN = level.startSN = parseInt(value1);
break;
case 'SKIP': {
if (level.skippedSegments) {
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
}
const skipAttrs = new AttrList(value1, level);
const skippedSegments =
skipAttrs.decimalInteger('SKIPPED-SEGMENTS');
if (Number.isFinite(skippedSegments)) {
level.skippedSegments += skippedSegments;
// This will result in fragments[] containing undefined values, which we will fill in with `mergeDetails`
for (let i = skippedSegments; i--; ) {
fragments.push(null);
}
currentSN += skippedSegments;
}
const recentlyRemovedDateranges = skipAttrs.enumeratedString(
'RECENTLY-REMOVED-DATERANGES',
);
if (recentlyRemovedDateranges) {
level.recentlyRemovedDateranges = (
level.recentlyRemovedDateranges || []
).concat(recentlyRemovedDateranges.split('\t'));
}
break;
}
case 'TARGETDURATION':
if (level.targetduration !== 0) {
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
}
level.targetduration = Math.max(parseInt(value1), 1);
break;
case 'VERSION':
if (level.version !== null) {
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
}
level.version = parseInt(value1);
break;
case 'INDEPENDENT-SEGMENTS':
break;
case 'ENDLIST':
if (!level.live) {
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
}
level.live = false;
break;
case '#':
if (value1 || value2) {
frag.tagList.push(value2 ? [value1, value2] : [value1]);
}
break;
case 'DISCONTINUITY':
discontinuityCounter++;
frag.tagList.push(['DIS']);
break;
case 'GAP':
frag.gap = true;
frag.tagList.push([tag]);
break;
case 'BITRATE':
frag.tagList.push([tag, value1]);
currentBitrate = parseInt(value1) * 1000;
if (Number.isFinite(currentBitrate)) {
frag.bitrate = currentBitrate;
} else {
currentBitrate = 0;
}
break;
case 'DATERANGE': {
const dateRangeAttr = new AttrList(value1, level);
const dateRange = new DateRange(
dateRangeAttr,
level.dateRanges[dateRangeAttr.ID],
level.dateRangeTagCount,
);
level.dateRangeTagCount++;
if (dateRange.isValid || level.skippedSegments) {
level.dateRanges[dateRange.id] = dateRange;
} else {
logger.warn(`Ignoring invalid DATERANGE tag: "${value1}"`);
}
// Add to fragment tag list for backwards compatibility (< v1.2.0)
frag.tagList.push(['EXT-X-DATERANGE', value1]);
break;
}
case 'DEFINE': {
if (__USE_VARIABLE_SUBSTITUTION__) {
const variableAttributes = new AttrList(value1, level);
if ('IMPORT' in variableAttributes) {
importVariableDefinition(
level,
variableAttributes,
multivariantVariableList,
);
} else {
addVariableDefinition(level, variableAttributes, baseurl);
}
}
break;
}
case 'DISCONTINUITY-SEQUENCE':
if (level.startCC !== 0) {
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
} else if (fragments.length > 0) {
assignMustAppearBeforeSegmentsError(level, tag, result);
}
level.startCC = discontinuityCounter = parseInt(value1);
break;
case 'KEY': {
const levelKey = parseKey(value1, baseurl, level);
if (levelKey.isSupported()) {
if (levelKey.method === 'NONE') {
levelkeys = undefined;
break;
}
if (!levelkeys) {
levelkeys = {};
}
const currentKey = levelkeys[levelKey.keyFormat];
// Ignore duplicate playlist KEY tags
if (!currentKey?.matches(levelKey)) {
if (currentKey) {
levelkeys = Object.assign({}, levelkeys);
}
levelkeys[levelKey.keyFormat] = levelKey;
}
} else {
logger.warn(
`[Keys] Ignoring unsupported EXT-X-KEY tag: "${value1}"${__USE_EME_DRM__ ? '' : ' (light build)'}`,
);
}
break;
}
case 'START':
level.startTimeOffset = parseStartTimeOffset(value1);
break;
case 'MAP': {
const mapAttrs = new AttrList(value1, level);
if (frag.duration) {
// Initial segment tag is after segment duration tag.
// #EXTINF: 6.0
// #EXT-X-MAP:URI="init.mp4
const init = new Fragment(type, base);
setInitSegment(init, mapAttrs, id, levelkeys);
currentInitSegment = init;
frag.initSegment = currentInitSegment;
if (
currentInitSegment.rawProgramDateTime &&
!frag.rawProgramDateTime
) {
frag.rawProgramDateTime = currentInitSegment.rawProgramDateTime;
}
} else {
// Initial segment tag is before segment duration tag
// Handle case where EXT-X-MAP is declared after EXT-X-BYTERANGE
const end = frag.byteRangeEndOffset;
if (end) {
const start = frag.byteRangeStartOffset as number;
nextByteRange = `${end - start}@${start}`;
} else {
nextByteRange = null;
}
setInitSegment(frag, mapAttrs, id, levelkeys);
currentInitSegment = frag;
createNextFrag = true;
}
currentInitSegment.cc = discontinuityCounter;
break;
}
case 'SERVER-CONTROL': {
if (serverControlAttrs) {
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
}
serverControlAttrs = new AttrList(value1);
level.canBlockReload = serverControlAttrs.bool('CAN-BLOCK-RELOAD');
level.canSkipUntil = serverControlAttrs.optionalFloat(
'CAN-SKIP-UNTIL',
0,
);
level.canSkipDateRanges =
level.canSkipUntil > 0 &&
serverControlAttrs.bool('CAN-SKIP-DATERANGES');
level.partHoldBack = serverControlAttrs.optionalFloat(
'PART-HOLD-BACK',
0,
);
level.holdBack = serverControlAttrs.optionalFloat('HOLD-BACK', 0);
break;
}
case 'PART-INF': {
if (level.partTarget) {
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
}
const partInfAttrs = new AttrList(value1);
level.partTarget = partInfAttrs.decimalFloatingPoint('PART-TARGET');
break;
}
case 'PART': {
let partList = level.partList;
if (!partList) {
partList = level.partList = [];
}
const previousFragmentPart =
currentPart > 0 ? partList[partList.length - 1] : undefined;
const index = currentPart++;
const partAttrs = new AttrList(value1, level);
const part = new Part(
partAttrs,
frag as MediaFragment,
base,
index,
previousFragmentPart,
);
partList.push(part);
frag.duration += part.duration;
break;
}
case 'PRELOAD-HINT': {
const preloadHintAttrs = new AttrList(value1, level);
level.preloadHint = preloadHintAttrs;
break;
}
case 'RENDITION-REPORT': {
const renditionReportAttrs = new AttrList(value1, level);
level.renditionReports = level.renditionReports || [];
level.renditionReports.push(renditionReportAttrs);
break;
}
default:
logger.warn(`line parsed but not handled: ${result}`);
break;
}
}
}
if (prevFrag && !prevFrag.relurl) {
fragments.pop();
totalduration -= prevFrag.duration;
if (level.partList) {
level.fragmentHint = prevFrag as MediaFragment;
}
} else if (level.partList) {
assignProgramDateTime(
frag as MediaFragment,
prevFrag as MediaFragment,
programDateTimes,
);
frag.cc = discontinuityCounter;
level.fragmentHint = frag as MediaFragment;
if (levelkeys) {
setFragLevelKeys(frag, levelkeys, level);
}
}
if (!level.targetduration) {
level.playlistParsingError = new Error(`Missing Target Duration`);
}
const fragmentLength = fragments.length;
const firstFragment = fragments[0];
const lastFragment = fragments[fragmentLength - 1];
totalduration += level.skippedSegments * level.targetduration;
if (totalduration > 0 && fragmentLength && lastFragment) {
level.averagetargetduration = totalduration / fragmentLength;
const lastSn = lastFragment.sn;
level.endSN = lastSn !== 'initSegment' ? lastSn : 0;
if (!level.live) {
lastFragment.endList = true;
}
/**
* Backfill any missing PDT values
* "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after
* one or more Media Segment URIs, the client SHOULD extrapolate
* backward from that tag (using EXTINF durations and/or media
* timestamps) to associate dates with those segments."
* We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs
* computed.
*/
if (firstPdtIndex > 0) {
backfillProgramDateTimes(fragments, firstPdtIndex);
if (firstFragment) {
programDateTimes.unshift(firstFragment as MediaFragment);
}
}
}
if (level.fragmentHint) {
totalduration += level.fragmentHint.duration;
}
level.totalduration = totalduration;
if (programDateTimes.length && level.dateRangeTagCount && firstFragment) {
mapDateRanges(programDateTimes, level);
}
level.endCC = discontinuityCounter;
return level;
}
}
export function mapDateRanges(
programDateTimes: MediaFragment[],
details: LevelDetails,
) {
// Make sure DateRanges are mapped to a ProgramDateTime tag that applies a date to a segment that overlaps with its start date
let programDateTimeCount = programDateTimes.length;
if (!programDateTimeCount) {
if (details.hasProgramDateTime) {
const lastFragment = details.fragments[details.fragments.length - 1];
programDateTimes.push(lastFragment);
programDateTimeCount++;
} else {
// no segments with EXT-X-PROGRAM-DATE-TIME references in playlist history
return;
}
}
const lastProgramDateTime = programDateTimes[programDateTimeCount - 1];
const playlistEnd = details.live ? Infinity : details.totalduration;
const dateRangeIds = Object.keys(details.dateRanges);
for (let i = dateRangeIds.length; i--; ) {
const dateRange = details.dateRanges[dateRangeIds[i]]!;
const startDateTime = dateRange.startDate.getTime();
dateRange.tagAnchor = lastProgramDateTime.ref;
for (let j = programDateTimeCount; j--; ) {
if (programDateTimes[j]?.sn < details.startSN) {
break;
}
const fragIndex = findFragmentWithStartDate(
details,
startDateTime,
programDateTimes,
j,
playlistEnd,
);
if (fragIndex !== -1) {
dateRange.tagAnchor = details.fragments[fragIndex].ref;
break;
}
}
}
}
function findFragmentWithStartDate(
details: LevelDetails,
startDateTime: number,
programDateTimes: MediaFragment[],
index: number,
endTime: number,
): number {
const pdtFragment = programDateTimes[index] as MediaFragment | undefined;
if (pdtFragment) {
// find matching range between PDT tags
const pdtStart = pdtFragment.programDateTime as number;
if (startDateTime >= pdtStart || index === 0) {
const durationBetweenPdt =
(programDateTimes[index + 1]?.start || endTime) - pdtFragment.start;
if (startDateTime <= pdtStart + durationBetweenPdt * 1000) {
// map to fragment with date-time range
const startIndex = programDateTimes[index].sn - details.startSN;
if (startIndex < 0) {
return -1;
}
const fragments = details.fragments;
if (fragments.length > programDateTimes.length) {
const endSegment =
programDateTimes[index + 1] || fragments[fragments.length - 1];
const endIndex = endSegment.sn - details.startSN;
for (let i = endIndex; i > startIndex; i--) {
const fragStartDateTime = fragments[i].programDateTime as number;
if (
startDateTime >= fragStartDateTime &&
startDateTime < fragStartDateTime + fragments[i].duration * 1000
) {
return i;
}
}
}
return startIndex;
}
}
}
return -1;
}
function parseKey(
keyTagAttributes: string,
baseurl: string,
parsed: ParsedMultivariantPlaylist | LevelDetails,
): LevelKey {
// https://tools.ietf.org/html/rfc8216#section-4.3.2.4
const keyAttrs = new AttrList(keyTagAttributes, parsed);
const decryptmethod = keyAttrs.METHOD ?? '';
const decrypturi = keyAttrs.URI;
const decryptiv = keyAttrs.hexadecimalInteger('IV');
const decryptkeyformatversions = keyAttrs.KEYFORMATVERSIONS;
// From RFC: This attribute is OPTIONAL; its absence indicates an implicit value of "identity".
const decryptkeyformat = keyAttrs.KEYFORMAT ?? 'identity';
if (decrypturi && keyAttrs.IV && !decryptiv) {
logger.error(`Invalid IV: ${keyAttrs.IV}`);
}
// If decrypturi is a URI with a scheme, then baseurl will be ignored
// No uri is allowed when METHOD is NONE
const resolvedUri = decrypturi ? M3U8Parser.resolve(decrypturi, baseurl) : '';
const keyFormatVersions = (
decryptkeyformatversions ? decryptkeyformatversions : '1'
)
.split('/')
.map(Number)
.filter(Number.isFinite);
return new LevelKey(
decryptmethod,
resolvedUri,
decryptkeyformat,
keyFormatVersions,
decryptiv,
keyAttrs.KEYID,
);
}
function parseStartTimeOffset(startAttributes: string): number | null {
const startAttrs = new AttrList(startAttributes);
const startTimeOffset = startAttrs.decimalFloatingPoint('TIME-OFFSET');
if (Number.isFinite(startTimeOffset)) {
return startTimeOffset;
}
return null;
}
function setCodecs(
codecsAttributeValue: string | undefined,
level: CodecsParsed,
) {
let codecs = (codecsAttributeValue || '').split(/[ ,]+/).filter((c) => c);
['video', 'audio', 'text'].forEach((type: CodecType) => {
const filtered = codecs.filter((codec) => isCodecType(codec, type));
if (filtered.length) {
// Comma separated list of all codecs for type
level[`${type}Codec`] = filtered.map((c) => c.split('/')[0]).join(',');
// Remove known codecs so that only unknownCodecs are left after iterating through each type
codecs = codecs.filter((codec) => filtered.indexOf(codec) === -1);
}
});
level.unknownCodecs = codecs;
}
function assignCodec(
media: MediaPlaylist,
groupItem: { audioCodec?: string; textCodec?: string },
codecProperty: 'audioCodec' | 'textCodec',
) {
const codecValue = groupItem[codecProperty];
if (codecValue) {
media[codecProperty] = codecValue;
}
}
function backfillProgramDateTimes(
fragments: M3U8ParserFragments,
firstPdtIndex: number,
) {
let fragPrev = fragments[firstPdtIndex] as Fragment;
for (let i = firstPdtIndex; i--; ) {
const frag = fragments[i];
// Exit on delta-playlist skipped segments
if (!frag) {
return;
}
frag.programDateTime =
(fragPrev.programDateTime as number) - frag.duration * 1000;
fragPrev = frag;
}
}
export function assignProgramDateTime(
frag: MediaFragment,
prevFrag: MediaFragment | null,
programDateTimes: MediaFragment[],
) {
if (frag.rawProgramDateTime) {
programDateTimes.push(frag);
} else if (prevFrag?.programDateTime) {
frag.programDateTime = prevFrag.endProgramDateTime;
}
}
function setInitSegment(
frag: Fragment,
mapAttrs: AttrList,
id: number,
levelkeys: LevelKeys | undefined,
) {
frag.relurl = mapAttrs.URI;
if (mapAttrs.BYTERANGE) {
frag.setByteRange(mapAttrs.BYTERANGE);
}
frag.level = id;
frag.sn = 'initSegment';
if (levelkeys) {
frag.levelkeys = levelkeys;
}
frag.initSegment = null;
}
function setFragLevelKeys(
frag: Fragment,
levelkeys: LevelKeys,
level: LevelDetails,
) {
frag.levelkeys = levelkeys;
const { encryptedFragments } = level;
if (
(!encryptedFragments.length ||
encryptedFragments[encryptedFragments.length - 1].levelkeys !==
levelkeys) &&
Object.keys(levelkeys).some(
(format) => levelkeys[format]!.isCommonEncryption,
)
) {
encryptedFragments.push(frag);
}
}
function assignMultipleMediaPlaylistTagOccuranceError(
level: LevelDetails,
tag: string,
result: string[],
) {
level.playlistParsingError = new Error(
`#EXT-X-${tag} must not appear more than once (${result[0]})`,
);
}
function assignMustAppearBeforeSegmentsError(
level: LevelDetails,
tag: string,
result: string[],
) {
level.playlistParsingError = new Error(
`#EXT-X-${tag} must appear before the first Media Segment (${result[0]})`,
);
}

Xet Storage Details

Size:
34.5 kB
·
Xet hash:
e58ae3c09b195a3ba66ff88058eeaae23a04f40bd5ede488d02042d59629c5b4

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.