Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4824,7 +4824,7 @@ export interface Track extends BaseTrack {
// (undocumented)
buffer?: SourceBuffer;
// (undocumented)
initSegment?: Uint8Array;
initSegment?: Uint8Array<ArrayBuffer>;
}

// Warning: (ae-missing-release-tag) "TrackLoadedData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down
12 changes: 3 additions & 9 deletions src/controller/audio-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ import { ElementaryStreamTypes, isMediaFragment } from '../loader/fragment';
import { Level } from '../types/level';
import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
import { ChunkMetadata } from '../types/transmuxer';
import {
alignDiscontinuities,
alignMediaPlaylistByPDT,
} from '../utils/discontinuities';
import { alignStream } from '../utils/discontinuities';
import {
audioMatchPredicate,
matchesOption,
Expand Down Expand Up @@ -573,10 +570,7 @@ class AudioStreamController
if (!newDetails.alignedSliding) {
// Align audio rendition with the "main" playlist on discontinuity change
// or program-date-time (PDT)
alignDiscontinuities(newDetails, mainDetails);
if (!newDetails.alignedSliding) {
alignMediaPlaylistByPDT(newDetails, mainDetails);
}
alignStream(mainDetails, newDetails);
sliding = newDetails.fragmentStart;
}
}
Expand Down Expand Up @@ -1012,7 +1006,7 @@ class AudioStreamController
mainDetails &&
mainDetails.fragmentStart !== track.details.fragmentStart
) {
alignMediaPlaylistByPDT(track.details, mainDetails);
alignStream(mainDetails, track.details);
}
} else {
super.loadFragment(frag, track, targetBufferTime);
Expand Down
35 changes: 14 additions & 21 deletions src/controller/subtitle-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { Level } from '../types/level';
import { PlaylistLevelType } from '../types/loader';
import { BufferHelper } from '../utils/buffer-helper';
import { alignMediaPlaylistByPDT } from '../utils/discontinuities';
import { alignStream } from '../utils/discontinuities';
import {
getAesModeFromFullSegmentMethod,
isFullSegmentEncryption,
Expand Down Expand Up @@ -295,46 +295,39 @@ export class SubtitleStreamController
},duration:${newDetails.totalduration}`,
);
this.mediaBuffer = this.mediaBufferTimeRanges;

const mainDetails = this.mainDetails;
let sliding = 0;
if (newDetails.live || track.details?.live) {
if (newDetails.deltaUpdateFailed) {
return;
}
const mainDetails = this.mainDetails;
if (!mainDetails) {
this.startFragRequested = false;
return;
}
const mainSlidingStartFragment = mainDetails.fragments[0];
if (!track.details) {
if (newDetails.hasProgramDateTime && mainDetails.hasProgramDateTime) {
alignMediaPlaylistByPDT(newDetails, mainDetails);
sliding = newDetails.fragmentStart;
} else if (mainSlidingStartFragment) {
// line up live playlist with main so that fragments in range are loaded
sliding = mainSlidingStartFragment.start;
addSliding(newDetails, sliding);
}
} else {
if (track.details) {
sliding = this.alignPlaylists(
newDetails,
track.details,
this.levelLastLoaded?.details,
);
if (sliding === 0 && mainSlidingStartFragment) {
// realign with main when there is no overlap with last refresh
sliding = mainSlidingStartFragment.start;
addSliding(newDetails, sliding);
}
}
// compute start position if we are aligned with the main playlist
if (mainDetails && !this.startFragRequested) {
this.setStartPosition(mainDetails, sliding);
if (!newDetails.alignedSliding) {
// line up live playlist with main so that fragments in range are loaded
alignStream(mainDetails, newDetails);
sliding = newDetails.fragmentStart;
}
}

track.details = newDetails;
this.levelLastLoaded = track;

// compute start position if we are aligned with the main playlist
if (mainDetails && !this.startFragRequested) {
this.setStartPosition(mainDetails, sliding);
}

if (trackId !== currentTrackId) {
return;
}
Expand Down
2 changes: 1 addition & 1 deletion src/types/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ export interface TrackSet {

export interface Track extends BaseTrack {
buffer?: SourceBuffer; // eslint-disable-line no-restricted-globals
initSegment?: Uint8Array;
initSegment?: Uint8Array<ArrayBuffer>;
}
8 changes: 7 additions & 1 deletion src/utils/discontinuities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ export function alignMediaPlaylistByPDT(
return;
}

const delta = (targetPDT - refPDT) / 1000 - (frag.start - refFrag.start);
const dateDifference = (targetPDT - refPDT) / 1000;
if (Math.abs(dateDifference) > Math.max(60, details.totalduration)) {
// Do not align on PDT if ranges differ significantly
return;
}

const delta = dateDifference - (frag.start - refFrag.start);
adjustSlidingStart(delta, details);
}
80 changes: 44 additions & 36 deletions tests/unit/controller/audio-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import { Events } from '../../../src/events';
import Hls from '../../../src/hls';
import { Fragment } from '../../../src/loader/fragment';
import KeyLoader from '../../../src/loader/key-loader';
import { LevelDetails } from '../../../src/loader/level-details';
import { LoadStats } from '../../../src/loader/load-stats';
import { Level } from '../../../src/types/level';
import { PlaylistLevelType } from '../../../src/types/loader';
import { AttrList } from '../../../src/utils/attr-list';
import { adjustSlidingStart } from '../../../src/utils/discontinuities';
import type { LevelDetails } from '../../../src/loader/level-details';
import type { MediaFragment } from '../../../src/loader/fragment';
import type {
AudioTrackLoadedData,
AudioTrackSwitchingData,
Expand Down Expand Up @@ -135,6 +136,12 @@ describe('AudioStreamController', function () {
let audioStreamController: AudioStreamControllerTestable;
let tracks: Level[];

function cloneLevelDetails(options: Partial<LevelDetails> & { url: string }) {
return Object.assign(new LevelDetails(options.url), {
...options,
});
}

beforeEach(function () {
sandbox = sinon.createSandbox();
hls = new Hls();
Expand Down Expand Up @@ -185,32 +192,33 @@ describe('AudioStreamController', function () {
live: boolean,
) {
const targetduration = 10;
const fragments: Fragment[] = Array.from(new Array(endSN - startSN)).map(
(u, i) => {
const frag = new Fragment(type, '');
frag.sn = i + startSN;
frag.cc = Math.floor((i + startSN) / 10);
frag.setStart(i * targetduration);
frag.duration = targetduration;
return frag;
},
);
const fragments: MediaFragment[] = Array.from(
new Array(endSN - startSN),
).map((u, i) => {
const frag = new Fragment(type, '') as MediaFragment;
frag.sn = i + startSN;
frag.cc = Math.floor((i + startSN) / 10);
frag.setStart(i * targetduration);
frag.duration = targetduration;
return frag;
});
const details: LevelDetails = new LevelDetails('');
details.live = live;
details.advanced = true;
details.updated = true;
details.fragments = fragments;
details.targetduration = targetduration;
details.totalduration = targetduration * fragments.length;
details.startSN = startSN;
details.endSN = endSN;
Object.defineProperty(details, 'startCC', {
get: () => fragments[0].cc,
});
Object.defineProperty(details, 'endCC', {
get: () => fragments[fragments.length - 1].cc,
});
return {
details: {
live,
advanced: true,
updated: true,
fragments,
get endCC(): number {
return fragments[fragments.length - 1].cc;
},
get startCC(): number {
return fragments[0].cc;
},
targetduration,
startSN,
endSN,
} as unknown as LevelDetails,
details,
id: 0,
networkDetails: {},
stats: new LoadStats(),
Expand Down Expand Up @@ -263,7 +271,9 @@ describe('AudioStreamController', function () {

it('should update the audio track LevelDetails from the track loaded data', function () {
audioStreamController.levels = tracks;
audioStreamController.mainDetails = mainLoadedData.details;
audioStreamController.mainDetails = cloneLevelDetails(
mainLoadedData.details,
);

audioStreamController.onAudioTrackLoaded(
Events.AUDIO_TRACK_LOADED,
Expand Down Expand Up @@ -317,9 +327,9 @@ describe('AudioStreamController', function () {
// Audio track ends on DISCONTINUITY-SEQUENCE 1 (main ends at 0)
trackLoadedData = getTrackLoadedData(7, 12, true);
mainLoadedData = getLevelLoadedData(1, 6, true);
audioStreamController.mainDetails = {
...mainLoadedData.details,
} as unknown as LevelDetails;
audioStreamController.mainDetails = cloneLevelDetails(
mainLoadedData.details,
);

expect(trackLoadedData.details.endCC).to.equal(1);
expect(audioStreamController.mainDetails.endCC).to.equal(0);
Expand Down Expand Up @@ -358,10 +368,10 @@ describe('AudioStreamController', function () {
trackLoadedData.details.live = mainLoadedData.details.live = true;
trackLoadedData.details.updated = mainLoadedData.details.updated = true;
// Main live details are present but expired (see LevelDetails `get expired()` and `get age()`)
audioStreamController.mainDetails = {
audioStreamController.mainDetails = cloneLevelDetails({
...mainLoadedData.details,
expired: true,
} as unknown as LevelDetails;
advancedDateTime: 1, // expired date time (must be > 0)
});

audioStreamController.onAudioTrackLoaded(
Events.AUDIO_TRACK_LOADED,
Expand Down Expand Up @@ -395,9 +405,7 @@ describe('AudioStreamController', function () {
trackLoadedData = getTrackLoadedData(7, 12, true);
mainLoadedData = getLevelLoadedData(1, 6, true);

audioStreamController.mainDetails = {
...mainLoadedData.details,
} as unknown as LevelDetails;
audioStreamController.mainDetails = mainLoadedData.details;

expect(trackLoadedData.details.endCC).to.equal(1);
expect(audioStreamController.mainDetails.endCC).to.equal(0);
Expand Down
12 changes: 10 additions & 2 deletions tests/unit/utils/discontinuities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ describe('discontinuities', function () {
expect(details).to.deep.equal(detailsExpected);
});

it('adjusts level fragments without overlapping CC range but with programDateTime info', function () {
it('adjusts level fragments without overlapping CC range but with programDateTime info no more than one minute or the playlist duration apart', function () {
const lastLevel = {
details: objToLevelDetails({
PTSKnown: true,
Expand Down Expand Up @@ -421,7 +421,11 @@ describe('discontinuities', function () {
endCC: 3,
});
alignMediaPlaylistByPDT(details, lastLevel.details);
expect(detailsExpected).to.deep.equal(details, JSON.stringify(details));

expect(detailsExpected).to.deep.equal(
details,
JSON.stringify(details, null, 2),
);
});

describe('alignDiscontinuities', function () {
Expand Down Expand Up @@ -596,6 +600,10 @@ function objToLevelDetails(object: Partial<LevelDetails>): LevelDetails {
details.startCC = details.fragments[0].cc;
details.endCC = details.fragments[fragCount - 1].cc;
}
details.totalduration = details.fragments.reduce(
(acc, { duration }) => acc + duration,
0,
);
return details;
}

Expand Down
Loading