123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591 |
- /**
- * Provides methods dealing with playlist sliding and drift
- */
- import { logger } from './logger';
- import { stringify } from './safe-json-stringify';
- import { DateRange } from '../loader/date-range';
- import { assignProgramDateTime, mapDateRanges } from '../loader/m3u8-parser';
- 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,
- ): 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 (!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 = Math.min(startDTS, frag.startDTS as number);
- minEndPTS = Math.min(endPTS, fragEndPts);
- endPTS = Math.max(endPTS, fragEndPts);
- endDTS = Math.max(endDTS, frag.endDTS as number);
- }
- 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,
- ) {
- 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 &&
- (!frag.initSegment ||
- frag.initSegment.relurl === currentInitSegment?.relurl)
- ) {
- frag.initSegment = currentInitSegment;
- }
- });
- }
- if (newDetails.skippedSegments) {
- newDetails.deltaUpdateFailed = newFragments.some((frag) => !frag);
- 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,
- );
- }
- 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,
- );
- } 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>,
- newDetails: LevelDetails,
- ): Record<string, DateRange> {
- 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) {
- 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 &&
- newPart &&
- 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 && oldFrag) {
- // Fill in skipped segments in delta playlist
- newFrag = newDetails.fragments[i] = oldFrag;
- }
- if (oldFrag && newFrag) {
- intersectionFn(oldFrag, newFrag, i, newFrags);
- if (oldFrag.url && oldFrag.url !== newFrag.url) {
- 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: MediaFragment | undefined =
- details.fragments[sn - details.startSN];
- 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;
- }
- });
- });
- }
|