download
raw
19.1 kB
/**
* Provides methods dealing with playlist sliding and drift
*/
import { stringify } from './safe-json-stringify';
import { DateRange } from '../loader/date-range';
import { assignProgramDateTime, mapDateRanges } from '../loader/m3u8-parser';
import type { ILogger } from './logger';
import type { Fragment, MediaFragment, Part } from '../loader/fragment';
import type { LevelDetails } from '../loader/level-details';
import type { Level } from '../types/level';
type FragmentIntersection = (
oldFrag: MediaFragment,
newFrag: MediaFragment,
newFragIndex: number,
newFragments: MediaFragment[],
) => void;
type PartIntersection = (oldPart: Part, newPart: Part) => void;
export function updatePTS(
fragments: MediaFragment[],
fromIdx: number,
toIdx: number,
): void {
const fragFrom = fragments[fromIdx];
const fragTo = fragments[toIdx];
updateFromToPTS(fragFrom, fragTo);
}
function updateFromToPTS(fragFrom: MediaFragment, fragTo: MediaFragment) {
const fragToPTS = fragTo.startPTS as number;
// if we know startPTS[toIdx]
if (Number.isFinite(fragToPTS)) {
// update fragment duration.
// it helps to fix drifts between playlist reported duration and fragment real duration
let duration: number = 0;
let frag: Fragment;
if (fragTo.sn > fragFrom.sn) {
duration = fragToPTS - fragFrom.start;
frag = fragFrom;
} else {
duration = fragFrom.start - fragToPTS;
frag = fragTo;
}
if (frag.duration !== duration) {
frag.setDuration(duration);
}
// we dont know startPTS[toIdx]
} else if (fragTo.sn > fragFrom.sn) {
const contiguous = fragFrom.cc === fragTo.cc;
// TODO: With part-loading end/durations we need to confirm the whole fragment is loaded before using (or setting) minEndPTS
if (contiguous && fragFrom.minEndPTS) {
fragTo.setStart(fragFrom.start + (fragFrom.minEndPTS - fragFrom.start));
} else {
fragTo.setStart(fragFrom.start + fragFrom.duration);
}
} else {
fragTo.setStart(Math.max(fragFrom.start - fragTo.duration, 0));
}
}
export function updateFragPTSDTS(
details: LevelDetails | undefined,
frag: MediaFragment,
startPTS: number,
endPTS: number,
startDTS: number,
endDTS: number,
logger: ILogger,
): number {
const parsedMediaDuration = endPTS - startPTS;
if (parsedMediaDuration <= 0) {
logger.warn('Fragment should have a positive duration', frag);
endPTS = startPTS + frag.duration;
endDTS = startDTS + frag.duration;
}
let maxStartPTS = startPTS;
let minEndPTS = endPTS;
const fragStartPts = frag.startPTS as number;
const fragEndPts = frag.endPTS as number;
if (Number.isFinite(fragStartPts)) {
// delta PTS between audio and video
const deltaPTS = Math.abs(fragStartPts - startPTS);
if (details && deltaPTS > details.totalduration) {
logger.warn(
`media timestamps and playlist times differ by ${deltaPTS}s for level ${frag.level} ${details.url}`,
);
} else if (!Number.isFinite(frag.deltaPTS as number)) {
frag.deltaPTS = deltaPTS;
} else {
frag.deltaPTS = Math.max(deltaPTS, frag.deltaPTS as number);
}
maxStartPTS = Math.max(startPTS, fragStartPts);
startPTS = Math.min(startPTS, fragStartPts);
startDTS =
frag.startDTS !== undefined
? Math.min(startDTS, frag.startDTS)
: startDTS;
minEndPTS = Math.min(endPTS, fragEndPts);
endPTS = Math.max(endPTS, fragEndPts);
endDTS = frag.endDTS !== undefined ? Math.max(endDTS, frag.endDTS) : endDTS;
}
const drift = startPTS - frag.start;
if (frag.start !== 0) {
frag.setStart(startPTS);
}
frag.setDuration(endPTS - frag.start);
frag.startPTS = startPTS;
frag.maxStartPTS = maxStartPTS;
frag.startDTS = startDTS;
frag.endPTS = endPTS;
frag.minEndPTS = minEndPTS;
frag.endDTS = endDTS;
const sn = frag.sn;
// exit if sn out of range
if (!details || sn < details.startSN || sn > details.endSN) {
return 0;
}
let i: number;
const fragIdx = sn - details.startSN;
const fragments = details.fragments;
// update frag reference in fragments array
// rationale is that fragments array might not contain this frag object.
// this will happen if playlist has been refreshed between frag loading and call to updateFragPTSDTS()
// if we don't update frag, we won't be able to propagate PTS info on the playlist
// resulting in invalid sliding computation
fragments[fragIdx] = frag;
// adjust fragment PTS/duration from seqnum-1 to frag 0
for (i = fragIdx; i > 0; i--) {
updateFromToPTS(fragments[i], fragments[i - 1]);
}
// adjust fragment PTS/duration from seqnum to last frag
for (i = fragIdx; i < fragments.length - 1; i++) {
updateFromToPTS(fragments[i], fragments[i + 1]);
}
if (details.fragmentHint) {
updateFromToPTS(fragments[fragments.length - 1], details.fragmentHint);
}
details.PTSKnown = details.alignedSliding = true;
return drift;
}
export function mergeDetails(
oldDetails: LevelDetails,
newDetails: LevelDetails,
logger: ILogger,
) {
if (oldDetails === newDetails) {
return;
}
// Track the last initSegment processed. Initialize it to the last one on the timeline.
let currentInitSegment: Fragment | null = null;
const oldFragments = oldDetails.fragments;
for (let i = oldFragments.length - 1; i >= 0; i--) {
const oldInit = oldFragments[i].initSegment;
if (oldInit) {
currentInitSegment = oldInit;
break;
}
}
if (oldDetails.fragmentHint) {
// prevent PTS and duration from being adjusted on the next hint
delete oldDetails.fragmentHint.endPTS;
}
// check if old/new playlists have fragments in common
// loop through overlapping SN and update startPTS, cc, and duration if any found
let PTSFrag: MediaFragment | undefined;
mapFragmentIntersection(
oldDetails,
newDetails,
(oldFrag, newFrag, newFragIndex, newFragments) => {
if (
(!newDetails.startCC || newDetails.skippedSegments) &&
newFrag.cc !== oldFrag.cc
) {
const ccOffset = oldFrag.cc - newFrag.cc;
for (let i = newFragIndex; i < newFragments.length; i++) {
newFragments[i].cc += ccOffset;
}
newDetails.endCC = newFragments[newFragments.length - 1].cc;
}
if (
Number.isFinite(oldFrag.startPTS) &&
Number.isFinite(oldFrag.endPTS)
) {
newFrag.setStart((newFrag.startPTS = oldFrag.startPTS!));
newFrag.startDTS = oldFrag.startDTS;
newFrag.maxStartPTS = oldFrag.maxStartPTS;
newFrag.endPTS = oldFrag.endPTS;
newFrag.endDTS = oldFrag.endDTS;
newFrag.minEndPTS = oldFrag.minEndPTS;
newFrag.setDuration(oldFrag.endPTS! - oldFrag.startPTS!);
if (newFrag.duration) {
PTSFrag = newFrag;
}
// PTS is known when any segment has startPTS and endPTS
newDetails.PTSKnown = newDetails.alignedSliding = true;
}
if (oldFrag.hasStreams) {
newFrag.elementaryStreams = oldFrag.elementaryStreams;
}
newFrag.loader = oldFrag.loader;
if (oldFrag.hasStats) {
newFrag.stats = oldFrag.stats;
}
if (oldFrag.initSegment) {
newFrag.initSegment = oldFrag.initSegment;
currentInitSegment = oldFrag.initSegment;
}
},
);
const newFragments = newDetails.fragments;
const fragmentsToCheck = newDetails.fragmentHint
? newFragments.concat(newDetails.fragmentHint)
: newFragments;
if (currentInitSegment) {
fragmentsToCheck.forEach((frag) => {
if (
(frag as any) &&
(!frag.initSegment ||
frag.initSegment.relurl === currentInitSegment?.relurl)
) {
frag.initSegment = currentInitSegment;
}
});
}
if (newDetails.skippedSegments) {
newDetails.deltaUpdateFailed = newFragments.some((frag) => !frag as any);
if (newDetails.deltaUpdateFailed) {
logger.warn(
'[level-helper] Previous playlist missing segments skipped in delta playlist',
);
for (let i = newDetails.skippedSegments; i--; ) {
newFragments.shift();
}
newDetails.startSN = newFragments[0].sn;
} else {
if (newDetails.canSkipDateRanges) {
newDetails.dateRanges = mergeDateRanges(
oldDetails.dateRanges,
newDetails,
logger,
);
}
const programDateTimes = oldDetails.fragments.filter(
(frag) => frag.rawProgramDateTime,
);
if (oldDetails.hasProgramDateTime && !newDetails.hasProgramDateTime) {
for (let i = 1; i < fragmentsToCheck.length; i++) {
if (fragmentsToCheck[i].programDateTime === null) {
assignProgramDateTime(
fragmentsToCheck[i],
fragmentsToCheck[i - 1],
programDateTimes,
);
}
}
}
mapDateRanges(programDateTimes, newDetails);
}
newDetails.endCC = newFragments[newFragments.length - 1].cc;
}
if (!newDetails.startCC) {
const fragPriorToNewStart = getFragmentWithSN(
oldDetails,
newDetails.startSN - 1,
);
newDetails.startCC = fragPriorToNewStart?.cc ?? newFragments[0].cc;
}
// Merge parts
mapPartIntersection(
oldDetails.partList,
newDetails.partList,
(oldPart: Part, newPart: Part) => {
newPart.elementaryStreams = oldPart.elementaryStreams;
newPart.stats = oldPart.stats;
},
);
// if at least one fragment contains PTS info, recompute PTS information for all fragments
if (PTSFrag) {
updateFragPTSDTS(
newDetails,
PTSFrag,
PTSFrag.startPTS as number,
PTSFrag.endPTS as number,
PTSFrag.startDTS as number,
PTSFrag.endDTS as number,
logger,
);
} else {
// ensure that delta is within oldFragments range
// also adjust sliding in case delta is 0 (we could have old=[50-60] and new=old=[50-61])
// in that case we also need to adjust start offset of all fragments
adjustSliding(oldDetails, newDetails);
}
if (newFragments.length) {
newDetails.totalduration = newDetails.edge - newFragments[0].start;
}
newDetails.driftStartTime = oldDetails.driftStartTime;
newDetails.driftStart = oldDetails.driftStart;
const advancedDateTime = newDetails.advancedDateTime;
if (newDetails.advanced && advancedDateTime) {
const edge = newDetails.edge;
if (!newDetails.driftStart) {
newDetails.driftStartTime = advancedDateTime;
newDetails.driftStart = edge;
}
newDetails.driftEndTime = advancedDateTime;
newDetails.driftEnd = edge;
} else {
newDetails.driftEndTime = oldDetails.driftEndTime;
newDetails.driftEnd = oldDetails.driftEnd;
newDetails.advancedDateTime = oldDetails.advancedDateTime;
}
if (newDetails.requestScheduled === -1) {
newDetails.requestScheduled = oldDetails.requestScheduled;
}
}
function mergeDateRanges(
oldDateRanges: Record<string, DateRange | undefined>,
newDetails: LevelDetails,
logger: ILogger,
): Record<string, DateRange | undefined> {
const { dateRanges: deltaDateRanges, recentlyRemovedDateranges } = newDetails;
const dateRanges = Object.assign({}, oldDateRanges);
if (recentlyRemovedDateranges) {
recentlyRemovedDateranges.forEach((id) => {
delete dateRanges[id];
});
}
const mergeIds = Object.keys(dateRanges);
const mergeCount = mergeIds.length;
if (!mergeCount) {
return deltaDateRanges;
}
Object.keys(deltaDateRanges).forEach((id) => {
const mergedDateRange = dateRanges[id];
const dateRange = new DateRange(deltaDateRanges[id]!.attr, mergedDateRange);
if (dateRange.isValid) {
dateRanges[id] = dateRange;
if (!mergedDateRange) {
dateRange.tagOrder += mergeCount;
}
} else {
logger.warn(
`Ignoring invalid Playlist Delta Update DATERANGE tag: "${stringify(
deltaDateRanges[id]!.attr,
)}"`,
);
}
});
return dateRanges;
}
export function mapPartIntersection(
oldParts: Part[] | null,
newParts: Part[] | null,
intersectionFn: PartIntersection,
) {
if (oldParts && newParts) {
let delta = 0;
for (let i = 0, len = oldParts.length; i <= len; i++) {
const oldPart = oldParts[i];
const newPart = newParts[i + delta];
if (
(oldPart as any) &&
(newPart as any) &&
oldPart.index === newPart.index &&
oldPart.fragment.sn === newPart.fragment.sn
) {
intersectionFn(oldPart, newPart);
} else {
delta--;
}
}
}
}
export function mapFragmentIntersection(
oldDetails: LevelDetails,
newDetails: LevelDetails,
intersectionFn: FragmentIntersection,
) {
const skippedSegments = newDetails.skippedSegments;
const start =
Math.max(oldDetails.startSN, newDetails.startSN) - newDetails.startSN;
const end =
(oldDetails.fragmentHint ? 1 : 0) +
(skippedSegments
? newDetails.endSN
: Math.min(oldDetails.endSN, newDetails.endSN)) -
newDetails.startSN;
const delta = newDetails.startSN - oldDetails.startSN;
const newFrags = newDetails.fragmentHint
? newDetails.fragments.concat(newDetails.fragmentHint)
: newDetails.fragments;
const oldFrags = oldDetails.fragmentHint
? oldDetails.fragments.concat(oldDetails.fragmentHint)
: oldDetails.fragments;
for (let i = start; i <= end; i++) {
const oldFrag = oldFrags[delta + i];
let newFrag = newFrags[i];
if (skippedSegments && (!newFrag as any) && (oldFrag as any)) {
// Fill in skipped segments in delta playlist
newFrag = newDetails.fragments[i] = oldFrag;
}
if ((oldFrag as any) && (newFrag as any)) {
intersectionFn(oldFrag, newFrag, i, newFrags);
const uriBefore = oldFrag.relurl;
const uriAfter = newFrag.relurl;
if (uriBefore && notEqualAfterStrippingQueries(uriBefore, uriAfter)) {
newDetails.playlistParsingError = getSequenceError(
`media sequence mismatch ${newFrag.sn}:`,
oldDetails,
newDetails,
oldFrag,
newFrag,
);
return;
} else if (oldFrag.cc !== newFrag.cc) {
newDetails.playlistParsingError = getSequenceError(
`discontinuity sequence mismatch (${oldFrag.cc}!=${newFrag.cc})`,
oldDetails,
newDetails,
oldFrag,
newFrag,
);
return;
}
}
}
}
function getSequenceError(
message: string,
oldDetails: LevelDetails,
newDetails: LevelDetails,
oldFrag: MediaFragment,
newFrag: MediaFragment,
): Error {
return new Error(
`${message} ${newFrag.url}
Playlist starting @${oldDetails.startSN}
${oldDetails.m3u8}
Playlist starting @${newDetails.startSN}
${newDetails.m3u8}`,
);
}
export function adjustSliding(
oldDetails: LevelDetails,
newDetails: LevelDetails,
matchingStableVariantOrRendition: boolean = true,
): void {
const delta =
newDetails.startSN + newDetails.skippedSegments - oldDetails.startSN;
const oldFragments = oldDetails.fragments;
const advancedOrStable = delta >= 0;
let sliding = 0;
if (advancedOrStable && delta < oldFragments.length) {
sliding = oldFragments[delta].start;
} else if (advancedOrStable && newDetails.startSN === oldDetails.endSN + 1) {
sliding = oldDetails.fragmentEnd;
} else if (advancedOrStable && matchingStableVariantOrRendition) {
// align with expected position (updated playlist start sequence is past end sequence of last update)
sliding = oldDetails.fragmentStart + delta * newDetails.levelTargetDuration;
} else if (!newDetails.skippedSegments && newDetails.fragmentStart === 0) {
// align new start with old (playlist switch has a sequence with no overlap and should not be used for alignment)
sliding = oldDetails.fragmentStart;
} else {
// new details already has a sliding offset or has skipped segments
return;
}
addSliding(newDetails, sliding);
}
export function addSliding(details: LevelDetails, sliding: number) {
if (sliding) {
const fragments = details.fragments;
for (let i = details.skippedSegments; i < fragments.length; i++) {
fragments[i].addStart(sliding);
}
if (details.fragmentHint) {
details.fragmentHint.addStart(sliding);
}
}
}
export function computeReloadInterval(
newDetails: LevelDetails,
distanceToLiveEdgeMs: number = Infinity,
): number {
let reloadInterval = 1000 * newDetails.targetduration;
if (newDetails.updated) {
// Use last segment duration when shorter than target duration and near live edge
const fragments = newDetails.fragments;
const liveEdgeMaxTargetDurations = 4;
if (
fragments.length &&
reloadInterval * liveEdgeMaxTargetDurations > distanceToLiveEdgeMs
) {
const lastSegmentDuration =
fragments[fragments.length - 1].duration * 1000;
if (lastSegmentDuration < reloadInterval) {
reloadInterval = lastSegmentDuration;
}
}
} else {
// estimate = 'miss half average';
// follow HLS Spec, If the client reloads a Playlist file and finds that it has not
// changed then it MUST wait for a period of one-half the target
// duration before retrying.
reloadInterval /= 2;
}
return Math.round(reloadInterval);
}
export function getFragmentWithSN(
details: LevelDetails | undefined,
sn: number,
fragCurrent?: Fragment | null,
): MediaFragment | null {
if (!details) {
return null;
}
let fragment = details.fragments[sn - details.startSN] as
| MediaFragment
| undefined;
if (fragment) {
return fragment;
}
fragment = details.fragmentHint;
if (fragment && fragment.sn === sn) {
return fragment;
}
if (sn < details.startSN && fragCurrent && fragCurrent.sn === sn) {
return fragCurrent as MediaFragment;
}
return null;
}
export function getPartWith(
details: LevelDetails | undefined,
sn: number,
partIndex: number,
): Part | null {
if (!details) {
return null;
}
return findPart(details.partList, sn, partIndex);
}
export function findPart(
partList: Part[] | null | undefined,
sn: number,
partIndex: number,
): Part | null {
if (partList) {
for (let i = partList.length; i--; ) {
const part = partList[i];
if (part.index === partIndex && part.fragment.sn === sn) {
return part;
}
}
}
return null;
}
export function reassignFragmentLevelIndexes(levels: Level[]) {
levels.forEach((level, index) => {
level.details?.fragments.forEach((fragment) => {
fragment.level = index;
if (fragment.initSegment) {
fragment.initSegment.level = index;
}
});
});
}
function notEqualAfterStrippingQueries(
uriBefore: string,
uriAfter: string | undefined,
): boolean {
if (uriBefore !== uriAfter && uriAfter) {
return stripQuery(uriBefore) !== stripQuery(uriAfter);
}
return false;
}
function stripQuery(uri: string): string {
return uri.replace(/\?[^?]*$/, '');
}

Xet Storage Details

Size:
19.1 kB
·
Xet hash:
f17eb4da885399ee28e8b348f9e8d1bf6849bc5ab3a610aafe3725d7198a3f27

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