Skip to content

Commit 7829295

Browse files
authored
Improve playlist alignment when PDT across playlists is inconsistent (#7482)
* Improve playlist alignment when PDT across playlists is inconsistent * Add logging for playlist alignment across variants and renditions (different playlist URIs) #7482
1 parent d9b30b1 commit 7829295

File tree

7 files changed

+120
-85
lines changed

7 files changed

+120
-85
lines changed

src/controller/audio-stream-controller.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@ import { ElementaryStreamTypes, isMediaFragment } from '../loader/fragment';
99
import { Level } from '../types/level';
1010
import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
1111
import { ChunkMetadata } from '../types/transmuxer';
12-
import {
13-
alignDiscontinuities,
14-
alignMediaPlaylistByPDT,
15-
} from '../utils/discontinuities';
12+
import { alignStream } from '../utils/discontinuities';
1613
import {
1714
audioMatchPredicate,
1815
matchesOption,
@@ -584,10 +581,7 @@ class AudioStreamController
584581
if (!newDetails.alignedSliding) {
585582
// Align audio rendition with the "main" playlist on discontinuity change
586583
// or program-date-time (PDT)
587-
alignDiscontinuities(newDetails, mainDetails);
588-
if (!newDetails.alignedSliding) {
589-
alignMediaPlaylistByPDT(newDetails, mainDetails);
590-
}
584+
alignStream(mainDetails, newDetails, this);
591585
sliding = newDetails.fragmentStart;
592586
}
593587
}
@@ -1055,7 +1049,7 @@ class AudioStreamController
10551049
mainDetails &&
10561050
mainDetails.fragmentStart !== track.details.fragmentStart
10571051
) {
1058-
alignMediaPlaylistByPDT(track.details, mainDetails);
1052+
alignStream(mainDetails, track.details, this);
10591053
}
10601054
} else {
10611055
super.loadFragment(frag, track, targetBufferTime);

src/controller/base-stream-controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1795,7 +1795,7 @@ export default class BaseStreamController
17951795
const firstLevelLoad = !previousDetails;
17961796
const aligned = details.alignedSliding && Number.isFinite(slidingStart);
17971797
if (firstLevelLoad || (!aligned && !slidingStart)) {
1798-
alignStream(switchDetails, details);
1798+
alignStream(switchDetails, details, this);
17991799
const alignedSlidingStart = details.fragmentStart;
18001800
this.log(
18011801
`Live playlist sliding: ${alignedSlidingStart.toFixed(2)} start-sn: ${

src/controller/subtitle-stream-controller.ts

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@ import {
1111
import { Level } from '../types/level';
1212
import { PlaylistLevelType } from '../types/loader';
1313
import { BufferHelper } from '../utils/buffer-helper';
14-
import { alignMediaPlaylistByPDT } from '../utils/discontinuities';
14+
import { alignStream } from '../utils/discontinuities';
1515
import {
1616
getAesModeFromFullSegmentMethod,
1717
isFullSegmentEncryption,
1818
} from '../utils/encryption-methods-util';
19-
import { addSliding } from '../utils/level-helper';
2019
import { subtitleOptionsIdentical } from '../utils/media-option-attributes';
2120
import type { FragmentTracker } from './fragment-tracker';
2221
import type Hls from '../hls';
@@ -295,46 +294,39 @@ export class SubtitleStreamController
295294
},duration:${newDetails.totalduration}`,
296295
);
297296
this.mediaBuffer = this.mediaBufferTimeRanges;
297+
298+
const mainDetails = this.mainDetails;
298299
let sliding = 0;
299300
if (newDetails.live || track.details?.live) {
300301
if (newDetails.deltaUpdateFailed) {
301302
return;
302303
}
303-
const mainDetails = this.mainDetails;
304304
if (!mainDetails) {
305305
this.startFragRequested = false;
306306
return;
307307
}
308-
const mainSlidingStartFragment = mainDetails.fragments[0];
309-
if (!track.details) {
310-
if (newDetails.hasProgramDateTime && mainDetails.hasProgramDateTime) {
311-
alignMediaPlaylistByPDT(newDetails, mainDetails);
312-
sliding = newDetails.fragmentStart;
313-
} else if (mainSlidingStartFragment) {
314-
// line up live playlist with main so that fragments in range are loaded
315-
sliding = mainSlidingStartFragment.start;
316-
addSliding(newDetails, sliding);
317-
}
318-
} else {
308+
if (track.details) {
319309
sliding = this.alignPlaylists(
320310
newDetails,
321311
track.details,
322312
this.levelLastLoaded?.details,
323313
);
324-
if (sliding === 0 && mainSlidingStartFragment) {
325-
// realign with main when there is no overlap with last refresh
326-
sliding = mainSlidingStartFragment.start;
327-
addSliding(newDetails, sliding);
328-
}
329314
}
330-
// compute start position if we are aligned with the main playlist
331-
if (mainDetails && !this.startFragRequested) {
332-
this.setStartPosition(mainDetails, sliding);
315+
if (!newDetails.alignedSliding) {
316+
// line up live playlist with main so that fragments in range are loaded
317+
alignStream(mainDetails, newDetails, this);
318+
sliding = newDetails.fragmentStart;
333319
}
334320
}
321+
335322
track.details = newDetails;
336323
this.levelLastLoaded = track;
337324

325+
// compute start position if we are aligned with the main playlist
326+
if (mainDetails && !this.startFragRequested) {
327+
this.setStartPosition(mainDetails, sliding);
328+
}
329+
338330
if (trackId !== currentTrackId) {
339331
return;
340332
}

src/utils/discontinuities.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { adjustSliding } from './level-helper';
2-
import { logger } from './logger';
2+
import type { ILogger } from './logger';
33
import type { Fragment } from '../loader/fragment';
44
import type { LevelDetails } from '../loader/level-details';
55

@@ -62,22 +62,23 @@ export function adjustSlidingStart(sliding: number, details: LevelDetails) {
6262
export function alignStream(
6363
switchDetails: LevelDetails | undefined,
6464
details: LevelDetails,
65+
logger: ILogger,
6566
) {
6667
if (!switchDetails) {
6768
return;
6869
}
69-
alignDiscontinuities(details, switchDetails);
70+
alignDiscontinuities(details, switchDetails, logger);
7071
if (!details.alignedSliding) {
7172
// If the PTS wasn't figured out via discontinuity sequence that means there was no CC increase within the level.
7273
// Aligning via Program Date Time should therefore be reliable, since PDT should be the same within the same
7374
// discontinuity sequence.
74-
alignMediaPlaylistByPDT(details, switchDetails);
75+
alignMediaPlaylistByPDT(details, switchDetails, logger);
7576
}
7677
if (!details.alignedSliding && !details.skippedSegments) {
7778
// Try to align on sn so that we pick a better start fragment.
7879
// Do not perform this on playlists with delta updates as this is only to align levels on switch
7980
// and adjustSliding only adjusts fragments after skippedSegments.
80-
adjustSliding(switchDetails, details, false);
81+
adjustSliding(switchDetails, details, false, logger);
8182
}
8283
}
8384

@@ -90,6 +91,7 @@ export function alignStream(
9091
export function alignDiscontinuities(
9192
details: LevelDetails,
9293
refDetails: LevelDetails | undefined,
94+
logger: ILogger,
9395
) {
9496
if (!shouldAlignOnDiscontinuities(refDetails, details)) {
9597
return;
@@ -100,8 +102,10 @@ export function alignDiscontinuities(
100102
if (!refFrag || !frag) {
101103
return;
102104
}
103-
logger.log(`Aligning playlist at start of dicontinuity sequence ${targetCC}`);
104105
const delta = refFrag.start - frag.start;
106+
logger.log(
107+
`Aligning playlists using dicontinuity sequence ${targetCC} (diff: ${delta})`,
108+
);
105109
adjustSlidingStart(delta, details);
106110
}
107111

@@ -121,6 +125,7 @@ export function alignDiscontinuities(
121125
export function alignMediaPlaylistByPDT(
122126
details: LevelDetails,
123127
refDetails: LevelDetails,
128+
logger: ILogger,
124129
) {
125130
if (!details.hasProgramDateTime || !refDetails.hasProgramDateTime) {
126131
return;
@@ -154,6 +159,16 @@ export function alignMediaPlaylistByPDT(
154159
return;
155160
}
156161

157-
const delta = (targetPDT - refPDT) / 1000 - (frag.start - refFrag.start);
162+
const dateDifference = (targetPDT - refPDT) / 1000;
163+
if (Math.abs(dateDifference) > Math.max(60, details.totalduration)) {
164+
// Do not align on PDT if ranges differ significantly
165+
logger.log(
166+
`Cannot align playlists using PDT without overlap (${Math.abs(dateDifference)} > ${details.totalduration})`,
167+
);
168+
return;
169+
}
170+
171+
const delta = dateDifference - (frag.start - refFrag.start);
172+
logger.log(`Aligning playlists using PDT (diff: ${delta})`);
158173
adjustSlidingStart(delta, details);
159174
}

src/utils/level-helper.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -475,27 +475,38 @@ export function adjustSliding(
475475
oldDetails: LevelDetails,
476476
newDetails: LevelDetails,
477477
matchingStableVariantOrRendition: boolean = true,
478-
): void {
478+
logger?: ILogger,
479+
): number {
479480
const delta =
480481
newDetails.startSN + newDetails.skippedSegments - oldDetails.startSN;
481482
const oldFragments = oldDetails.fragments;
482483
const advancedOrStable = delta >= 0;
483484
let sliding = 0;
484485
if (advancedOrStable && delta < oldFragments.length) {
485486
sliding = oldFragments[delta].start;
487+
logger?.log(
488+
`Aligning playlists based on SN ${newDetails.startSN} (diff: ${sliding})`,
489+
);
486490
} else if (advancedOrStable && newDetails.startSN === oldDetails.endSN + 1) {
487491
sliding = oldDetails.fragmentEnd;
492+
logger?.log(
493+
`Aligning playlists based on first/last SN ${newDetails.startSN} (diff: ${sliding})`,
494+
);
488495
} else if (advancedOrStable && matchingStableVariantOrRendition) {
489496
// align with expected position (updated playlist start sequence is past end sequence of last update)
490497
sliding = oldDetails.fragmentStart + delta * newDetails.levelTargetDuration;
491498
} else if (!newDetails.skippedSegments && newDetails.fragmentStart === 0) {
492499
// align new start with old (playlist switch has a sequence with no overlap and should not be used for alignment)
493500
sliding = oldDetails.fragmentStart;
501+
logger?.log(
502+
`Aligning playlists based on first SN ${newDetails.startSN} (diff: ${sliding})`,
503+
);
494504
} else {
495505
// new details already has a sliding offset or has skipped segments
496-
return;
506+
return sliding; // 0
497507
}
498508
addSliding(newDetails, sliding);
509+
return sliding;
499510
}
500511

501512
export function addSliding(details: LevelDetails, sliding: number) {

tests/unit/controller/audio-stream-controller.ts

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import { Events } from '../../../src/events';
1111
import Hls from '../../../src/hls';
1212
import { Fragment } from '../../../src/loader/fragment';
1313
import KeyLoader from '../../../src/loader/key-loader';
14+
import { LevelDetails } from '../../../src/loader/level-details';
1415
import { LoadStats } from '../../../src/loader/load-stats';
1516
import { Level } from '../../../src/types/level';
1617
import { PlaylistLevelType } from '../../../src/types/loader';
1718
import { AttrList } from '../../../src/utils/attr-list';
1819
import { adjustSlidingStart } from '../../../src/utils/discontinuities';
19-
import type { LevelDetails } from '../../../src/loader/level-details';
20+
import type { MediaFragment } from '../../../src/loader/fragment';
2021
import type {
2122
AudioTrackLoadedData,
2223
AudioTrackSwitchingData,
@@ -137,6 +138,12 @@ describe('AudioStreamController', function () {
137138
let audioStreamController: AudioStreamControllerTestable;
138139
let tracks: Level[];
139140

141+
function cloneLevelDetails(options: Partial<LevelDetails> & { url: string }) {
142+
return Object.assign(new LevelDetails(options.url), {
143+
...options,
144+
});
145+
}
146+
140147
beforeEach(function () {
141148
sandbox = sinon.createSandbox();
142149
hls = new Hls();
@@ -187,32 +194,33 @@ describe('AudioStreamController', function () {
187194
live: boolean,
188195
) {
189196
const targetduration = 10;
190-
const fragments: Fragment[] = Array.from(new Array(endSN - startSN)).map(
191-
(u, i) => {
192-
const frag = new Fragment(type, '');
193-
frag.sn = i + startSN;
194-
frag.cc = Math.floor((i + startSN) / 10);
195-
frag.setStart(i * targetduration);
196-
frag.duration = targetduration;
197-
return frag;
198-
},
199-
);
197+
const fragments: MediaFragment[] = Array.from(
198+
new Array(endSN - startSN),
199+
).map((u, i) => {
200+
const frag = new Fragment(type, '') as MediaFragment;
201+
frag.sn = i + startSN;
202+
frag.cc = Math.floor((i + startSN) / 10);
203+
frag.setStart(i * targetduration);
204+
frag.duration = targetduration;
205+
return frag;
206+
});
207+
const details: LevelDetails = new LevelDetails('');
208+
details.live = live;
209+
details.advanced = true;
210+
details.updated = true;
211+
details.fragments = fragments;
212+
details.targetduration = targetduration;
213+
details.totalduration = targetduration * fragments.length;
214+
details.startSN = startSN;
215+
details.endSN = endSN;
216+
Object.defineProperty(details, 'startCC', {
217+
get: () => fragments[0].cc,
218+
});
219+
Object.defineProperty(details, 'endCC', {
220+
get: () => fragments[fragments.length - 1].cc,
221+
});
200222
return {
201-
details: {
202-
live,
203-
advanced: true,
204-
updated: true,
205-
fragments,
206-
get endCC(): number {
207-
return fragments[fragments.length - 1].cc;
208-
},
209-
get startCC(): number {
210-
return fragments[0].cc;
211-
},
212-
targetduration,
213-
startSN,
214-
endSN,
215-
} as unknown as LevelDetails,
223+
details,
216224
id: 0,
217225
networkDetails: new Response('ok'),
218226
stats: new LoadStats(),
@@ -265,7 +273,9 @@ describe('AudioStreamController', function () {
265273

266274
it('should update the audio track LevelDetails from the track loaded data', function () {
267275
audioStreamController.levels = tracks;
268-
audioStreamController.mainDetails = mainLoadedData.details;
276+
audioStreamController.mainDetails = cloneLevelDetails(
277+
mainLoadedData.details,
278+
);
269279

270280
audioStreamController.onAudioTrackLoaded(
271281
Events.AUDIO_TRACK_LOADED,
@@ -319,9 +329,9 @@ describe('AudioStreamController', function () {
319329
// Audio track ends on DISCONTINUITY-SEQUENCE 1 (main ends at 0)
320330
trackLoadedData = getTrackLoadedData(7, 12, true);
321331
mainLoadedData = getLevelLoadedData(1, 6, true);
322-
audioStreamController.mainDetails = {
323-
...mainLoadedData.details,
324-
} as unknown as LevelDetails;
332+
audioStreamController.mainDetails = cloneLevelDetails(
333+
mainLoadedData.details,
334+
);
325335

326336
expect(trackLoadedData.details.endCC).to.equal(1);
327337
expect(audioStreamController.mainDetails.endCC).to.equal(0);
@@ -360,10 +370,10 @@ describe('AudioStreamController', function () {
360370
trackLoadedData.details.live = mainLoadedData.details.live = true;
361371
trackLoadedData.details.updated = mainLoadedData.details.updated = true;
362372
// Main live details are present but expired (see LevelDetails `get expired()` and `get age()`)
363-
audioStreamController.mainDetails = {
373+
audioStreamController.mainDetails = cloneLevelDetails({
364374
...mainLoadedData.details,
365-
expired: true,
366-
} as unknown as LevelDetails;
375+
advancedDateTime: 1, // expired date time (must be > 0)
376+
});
367377

368378
audioStreamController.onAudioTrackLoaded(
369379
Events.AUDIO_TRACK_LOADED,
@@ -397,9 +407,7 @@ describe('AudioStreamController', function () {
397407
trackLoadedData = getTrackLoadedData(7, 12, true);
398408
mainLoadedData = getLevelLoadedData(1, 6, true);
399409

400-
audioStreamController.mainDetails = {
401-
...mainLoadedData.details,
402-
} as unknown as LevelDetails;
410+
audioStreamController.mainDetails = mainLoadedData.details;
403411

404412
expect(trackLoadedData.details.endCC).to.equal(1);
405413
expect(audioStreamController.mainDetails.endCC).to.equal(0);

0 commit comments

Comments
 (0)