Skip to content

Commit

Permalink
Handle DateRange mapping with Delta Playlist updates
Browse files Browse the repository at this point in the history
Error (but parse and merge) multiple EXT-X-SKIP tags
  • Loading branch information
robwalch committed Apr 11, 2024
1 parent aaf0791 commit 59d2c1e
Show file tree
Hide file tree
Showing 4 changed files with 429 additions and 79 deletions.
130 changes: 76 additions & 54 deletions src/loader/m3u8-parser.ts
Expand Up @@ -427,23 +427,29 @@ export default class M3U8Parser {
currentSN = level.startSN = parseInt(value1);
break;
case 'SKIP': {
if (level.skippedSegments) {
level.playlistParsingError = new Error(
`#EXT-X-SKIP MUST NOT appear more than once in a Playlist`,
);
}
const skipAttrs = new AttrList(value1, level);
const skippedSegments =
skipAttrs.decimalInteger('SKIPPED-SEGMENTS');
if (Number.isFinite(skippedSegments)) {
level.skippedSegments = 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.unshift(null);
fragments.push(null);
}
currentSN += skippedSegments;
}
const recentlyRemovedDateranges = skipAttrs.enumeratedString(
'RECENTLY-REMOVED-DATERANGES',
);
if (recentlyRemovedDateranges) {
level.recentlyRemovedDateranges =
recentlyRemovedDateranges.split('\t');
level.recentlyRemovedDateranges = (
level.recentlyRemovedDateranges || []
).concat(recentlyRemovedDateranges.split('\t'));
}
break;
}
Expand Down Expand Up @@ -660,74 +666,90 @@ export default class M3U8Parser {
if (level.fragmentHint) {
totalduration += level.fragmentHint.duration;
}
const programDateTimeCount = programDateTimes.length;
if (programDateTimeCount) {
/**
* 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);
}
level.totalduration = totalduration;
if (programDateTimes.length && firstFragment) {
mapDateRanges(programDateTimes, dateRangeMapping, level);
}
/**
* 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);
}
// Make sure DateRanges are mapped to a ProgramDateTime tag that applies a date to a segment that overlaps with its start date
const lastProgramDateTime = programDateTimes[programDateTimeCount - 1];
for (let i = dateRangeMapping.length; i--; ) {
const dateRange = dateRangeMapping[i][0];
const pdtIndex = dateRange.tagAnchor
? dateRangeMapping[i][1]
: programDateTimeCount - 1;
if (!dateRange.tagAnchor) {
dateRange.tagAnchor = lastProgramDateTime;
}
const startDateTime = dateRange.startDate.getTime();
}

level.endCC = discontinuityCounter;

return level;
}
}

export function mapDateRanges(
programDateTimes: Fragment[],
dateRangeMapping: [DateRange, number][],
details: LevelDetails,
) {
// Make sure DateRanges are mapped to a ProgramDateTime tag that applies a date to a segment that overlaps with its start date
const programDateTimeCount = programDateTimes.length;
const lastProgramDateTime = programDateTimes[programDateTimeCount - 1];
const playlistEnd = details.live ? Infinity : details.totalduration;
for (let i = dateRangeMapping.length; i--; ) {
const dateRange = dateRangeMapping[i][0];
const pdtIndex = dateRange.tagAnchor
? dateRangeMapping[i][1]
: programDateTimeCount - 1;
if (!dateRange.tagAnchor) {
dateRange.tagAnchor = lastProgramDateTime;
}
const startDateTime = dateRange.startDate.getTime();
if (
!dateRangeMapsToProgramDateTime(
startDateTime,
programDateTimes,
pdtIndex,
playlistEnd,
)
) {
// DateRange not mappable to segments between surrounding ProgramDateTime tags, find alternate starting at anchor and looping backwards:
for (let j = programDateTimeCount; j--; ) {
if (
!dateRangeMapsToProgramDateTime(
dateRangeMapsToProgramDateTime(
startDateTime,
programDateTimes,
pdtIndex,
j,
playlistEnd,
)
) {
// DateRange not mappable to segments between surrounding ProgramDateTime tags, find alternate starting at anchor and looping backwards:
for (let j = programDateTimeCount; j--; ) {
const k = (j + pdtIndex) % programDateTimeCount;
if (
dateRangeMapsToProgramDateTime(startDateTime, programDateTimes, k)
) {
dateRange.tagAnchor = programDateTimes[k];
break;
}
}
dateRange.tagAnchor = programDateTimes[j];
break;
}
}
}
level.totalduration = totalduration;
level.endCC = discontinuityCounter;

return level;
}
}

function dateRangeMapsToProgramDateTime(
startDateTime: number,
programDateTimes: Fragment[],
index: number,
endTime: number,
): boolean {
const pdtFragment = programDateTimes[index];
if (pdtFragment) {
const durationBetweenPdt =
(programDateTimes[index + 1]?.start || Infinity) - pdtFragment.start;
(programDateTimes[index + 1]?.start || endTime) - pdtFragment.start;
const pdtStart = pdtFragment.programDateTime as number;
return (
startDateTime >= pdtStart &&
startDateTime <= pdtStart + durationBetweenPdt
(startDateTime >= pdtStart || index === 0) &&
startDateTime <= pdtStart + durationBetweenPdt * 1000
);
}
return false;
Expand Down Expand Up @@ -823,22 +845,22 @@ function backfillProgramDateTimes(
}
}

function assignProgramDateTime(
export function assignProgramDateTime(
frag: Fragment,
prevFrag: Fragment | null,
programDateTimeMap: Fragment[],
programDateTimes: Fragment[],
dateRangeMapping: [DateRange, number][],
) {
if (frag.rawProgramDateTime) {
frag.programDateTime = Date.parse(frag.rawProgramDateTime);
if (frag.sn !== 'initSegment' && Number.isFinite(frag.programDateTime)) {
programDateTimeMap.push(frag);
programDateTimes.push(frag);
// Anchor DateRanges to last ProgramDateTime initially. Mapping to segments is finalized at the end of Playlist parsing.
for (let i = dateRangeMapping.length; i--; ) {
const dateRange = dateRangeMapping[i][0];
if (!dateRange.tagAnchor) {
dateRange.tagAnchor = frag;
dateRangeMapping[i][1] = programDateTimeMap.length - 1;
dateRangeMapping[i][1] = programDateTimes.length - 1;
}
}
}
Expand Down
82 changes: 57 additions & 25 deletions src/utils/level-helper.ts
Expand Up @@ -3,10 +3,11 @@
*/

import { logger } from './logger';
import { Fragment, Part } from '../loader/fragment';
import { LevelDetails } from '../loader/level-details';
import type { Level } from '../types/level';
import { DateRange } from '../loader/date-range';
import { assignProgramDateTime, mapDateRanges } from '../loader/m3u8-parser';
import type { Fragment, Part } from '../loader/fragment';
import type { LevelDetails } from '../loader/level-details';
import type { Level } from '../types/level';

type FragmentIntersection = (oldFrag: Fragment, newFrag: Fragment) => void;
type PartIntersection = (oldPart: Part, newPart: Part) => void;
Expand Down Expand Up @@ -196,10 +197,10 @@ export function mergeDetails(
},
);

const fragmentsToCheck = newDetails.fragmentHint
? newDetails.fragments.concat(newDetails.fragmentHint)
: newDetails.fragments;
if (currentInitSegment) {
const fragmentsToCheck = newDetails.fragmentHint
? newDetails.fragments.concat(newDetails.fragmentHint)
: newDetails.fragments;
fragmentsToCheck.forEach((frag) => {
if (
frag &&
Expand All @@ -222,12 +223,32 @@ export function mergeDetails(
}
newDetails.startSN = newDetails.fragments[0].sn as number;
newDetails.startCC = newDetails.fragments[0].cc;
} else if (newDetails.canSkipDateRanges) {
newDetails.dateRanges = mergeDateRanges(
oldDetails.dateRanges,
newDetails.dateRanges,
newDetails.recentlyRemovedDateranges,
} else {
if (newDetails.canSkipDateRanges) {
newDetails.dateRanges = mergeDateRanges(
oldDetails.dateRanges,
newDetails,
);
}
const programDateTimes = oldDetails.fragments.filter(
(frag) => frag.rawProgramDateTime,
);
const dateRangeMapping: [DateRange, number][] = Object.keys(
newDetails.dateRanges,
).map((id) => [newDetails.dateRanges[id], -1]);
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,
dateRangeMapping,
);
}
}
}
mapDateRanges(programDateTimes, dateRangeMapping, newDetails);
}
}

Expand Down Expand Up @@ -293,27 +314,38 @@ export function mergeDetails(

function mergeDateRanges(
oldDateRanges: Record<string, DateRange>,
deltaDateRanges: Record<string, DateRange>,
recentlyRemovedDateranges: string[] | undefined,
newDetails: LevelDetails,
): Record<string, DateRange> {
const { dateRanges: deltaDateRanges, recentlyRemovedDateranges } = newDetails;
const dateRanges = Object.assign({}, oldDateRanges);
if (recentlyRemovedDateranges) {
recentlyRemovedDateranges.forEach((id) => {
delete dateRanges[id];
});
}
Object.keys(deltaDateRanges).forEach((id) => {
const dateRange = new DateRange(deltaDateRanges[id].attr, dateRanges[id]);
if (dateRange.isValid) {
dateRanges[id] = dateRange;
} else {
logger.warn(
`Ignoring invalid Playlist Delta Update DATERANGE tag: "${JSON.stringify(
deltaDateRanges[id].attr,
)}"`,
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: "${JSON.stringify(
deltaDateRanges[id].attr,
)}"`,
);
}
});
}
return dateRanges;
}

Expand Down Expand Up @@ -366,7 +398,7 @@ export function mapFragmentIntersection(
for (let i = start; i <= end; i++) {
const oldFrag = oldFrags[delta + i];
let newFrag = newFrags[i];
if (skippedSegments && !newFrag && i < skippedSegments) {
if (skippedSegments && !newFrag && oldFrag) {
// Fill in skipped segments in delta playlist
newFrag = newDetails.fragments[i] = oldFrag;
}
Expand Down

0 comments on commit 59d2c1e

Please sign in to comment.