Skip to content

Commit 708e10f

Browse files
authored
Multivariant Playlist parsing fixes (#7523)
* Move unknown codecs handling to playlist-loader * Add `stats.parsing.end` timing prior to emitting MANIFEST_PARSED on MANIFEST_LOADED (or MANIFEST_PARSING_ERROR) Resolves #7518 * Add mvp loading and parsing test with `stats.parsing.end` assertion (#7518) * Fix incorrect LEVEL_LOADED `level` index when lower level removed while loading
1 parent 293c8da commit 708e10f

File tree

7 files changed

+306
-46
lines changed

7 files changed

+306
-46
lines changed

src/controller/interstitials-controller.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,6 +1532,9 @@ export default class InterstitialsController
15321532
return;
15331533
}
15341534
const main = this.hls.levels[data.level];
1535+
if (!main.details) {
1536+
return;
1537+
}
15351538
const currentSelection = {
15361539
...(this.mediaSelection || this.altSelection),
15371540
main,

src/controller/level-controller.ts

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
codecsSetSelectionPreferenceValue,
99
convertAVC1ToAVCOTI,
1010
getCodecCompatibleName,
11-
sampleEntryCodesISO,
1211
videoCodecPreferenceValue,
1312
} from '../utils/codecs';
1413
import { reassignFragmentLevelIndexes } from '../utils/level-helper';
@@ -133,29 +132,8 @@ export default class LevelController extends BasePlaylistController {
133132

134133
// only keep levels with supported audio/video codecs
135134
const { width, height, unknownCodecs } = levelParsed;
136-
let unknownUnsupportedCodecCount = unknownCodecs
137-
? unknownCodecs.length
138-
: 0;
139-
if (unknownCodecs) {
140-
// Treat unknown codec as audio or video codec based on passing `isTypeSupported` check
141-
// (allows for playback of any supported codec even if not indexed in utils/codecs)
142-
for (let i = unknownUnsupportedCodecCount; i--; ) {
143-
const unknownCodec = unknownCodecs[i];
144-
if (this.isAudioSupported(unknownCodec)) {
145-
levelParsed.audioCodec = audioCodec = audioCodec
146-
? `${audioCodec},${unknownCodec}`
147-
: unknownCodec;
148-
unknownUnsupportedCodecCount--;
149-
sampleEntryCodesISO.audio[audioCodec.substring(0, 4)] = 2;
150-
} else if (this.isVideoSupported(unknownCodec)) {
151-
levelParsed.videoCodec = videoCodec = videoCodec
152-
? `${videoCodec},${unknownCodec}`
153-
: unknownCodec;
154-
unknownUnsupportedCodecCount--;
155-
sampleEntryCodesISO.video[videoCodec.substring(0, 4)] = 2;
156-
}
157-
}
158-
}
135+
const unknownUnsupportedCodecCount = unknownCodecs?.length || 0;
136+
159137
resolutionFound ||= !!(width && height);
160138
videoCodecFound ||= !!videoCodec;
161139
audioCodecFound ||= !!audioCodec;
@@ -252,6 +230,7 @@ export default class LevelController extends BasePlaylistController {
252230
let audioTracks: MediaPlaylist[] = [];
253231
let subtitleTracks: MediaPlaylist[] = [];
254232
let levels = filteredLevels;
233+
const statsParsing = data.stats?.parsing || {};
255234

256235
// remove audio-only and invalid video-range levels if we also have levels with video codecs or RESOLUTION signalled
257236
if ((resolutionFound || videoCodecFound) && audioCodecFound) {
@@ -290,6 +269,7 @@ export default class LevelController extends BasePlaylistController {
290269
});
291270
}
292271
});
272+
statsParsing.end = performance.now();
293273
return;
294274
}
295275

@@ -408,6 +388,7 @@ export default class LevelController extends BasePlaylistController {
408388
altAudio:
409389
altAudioEnabled && !audioOnly && audioTracks.some((t) => !!t.url),
410390
};
391+
statsParsing.end = performance.now();
411392
this.hls.trigger(Events.MANIFEST_PARSED, edata);
412393
}
413394

src/loader/playlist-loader.ts

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import { ErrorDetails, ErrorTypes } from '../errors';
1111
import { Events } from '../events';
1212
import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
1313
import { AttrList } from '../utils/attr-list';
14+
import {
15+
areCodecsMediaSourceSupported,
16+
sampleEntryCodesISO,
17+
} from '../utils/codecs';
1418
import { computeReloadInterval } from '../utils/level-helper';
1519
import type { LevelDetails } from './level-details';
1620
import type { LoaderConfig, RetryConfig } from '../config';
@@ -424,6 +428,7 @@ class PlaylistLoader implements NetworkComponentAPI {
424428
const parsedResult = M3U8Parser.parseMasterPlaylist(string, url);
425429

426430
if (parsedResult.playlistParsingError) {
431+
stats.parsing.end = performance.now();
427432
this.handleManifestParsingError(
428433
response,
429434
context,
@@ -445,6 +450,44 @@ class PlaylistLoader implements NetworkComponentAPI {
445450

446451
this.variableList = variableList;
447452

453+
// Treat unknown codec as audio or video codec based on passing `isTypeSupported` check
454+
// (allows for playback of any supported codec even if not indexed in utils/codecs)
455+
levels.forEach((levelParsed: LevelParsed) => {
456+
const { unknownCodecs } = levelParsed;
457+
if (unknownCodecs) {
458+
const { preferManagedMediaSource } = this.hls.config;
459+
let { audioCodec, videoCodec } = levelParsed;
460+
for (let i = unknownCodecs.length; i--; ) {
461+
const unknownCodec = unknownCodecs[i];
462+
if (
463+
areCodecsMediaSourceSupported(
464+
unknownCodec,
465+
'audio',
466+
preferManagedMediaSource,
467+
)
468+
) {
469+
levelParsed.audioCodec = audioCodec = audioCodec
470+
? `${audioCodec},${unknownCodec}`
471+
: unknownCodec;
472+
sampleEntryCodesISO.audio[audioCodec.substring(0, 4)] = 2;
473+
unknownCodecs.splice(i, 1);
474+
} else if (
475+
areCodecsMediaSourceSupported(
476+
unknownCodec,
477+
'video',
478+
preferManagedMediaSource,
479+
)
480+
) {
481+
levelParsed.videoCodec = videoCodec = videoCodec
482+
? `${videoCodec},${unknownCodec}`
483+
: unknownCodec;
484+
sampleEntryCodesISO.video[videoCodec.substring(0, 4)] = 2;
485+
unknownCodecs.splice(i, 1);
486+
}
487+
}
488+
}
489+
});
490+
448491
const {
449492
AUDIO: audioTracks = [],
450493
SUBTITLES: subtitles,
@@ -683,10 +726,11 @@ class PlaylistLoader implements NetworkComponentAPI {
683726
loader: Loader<PlaylistLoaderContext> | undefined,
684727
): void {
685728
const hls = this.hls;
686-
const { type, level, id, groupId, deliveryDirectives } = context;
729+
const { type, level, levelOrTrack, id, groupId, deliveryDirectives } =
730+
context;
687731
const url = getResponseUrl(response, context);
688732
const parent = mapContextToLevelType(context);
689-
const levelIndex =
733+
let levelIndex =
690734
typeof context.level === 'number' && parent === PlaylistLevelType.MAIN
691735
? (level as number)
692736
: undefined;
@@ -748,9 +792,23 @@ class PlaylistLoader implements NetworkComponentAPI {
748792
switch (type) {
749793
case PlaylistContextType.MANIFEST:
750794
case PlaylistContextType.LEVEL:
795+
if (levelIndex) {
796+
if (!levelOrTrack) {
797+
// fall-through to hls.levels[0]
798+
levelIndex = 0;
799+
} else {
800+
if (levelOrTrack !== hls.levels[levelIndex]) {
801+
// correct levelIndex when lower levels were removed from hls.levels
802+
const updatedIndex = hls.levels.indexOf(levelOrTrack as Level);
803+
if (updatedIndex > -1) {
804+
levelIndex = updatedIndex;
805+
}
806+
}
807+
}
808+
}
751809
hls.trigger(Events.LEVEL_LOADED, {
752810
details: levelDetails,
753-
levelInfo: (context.levelOrTrack as Level) || hls.levels[0],
811+
levelInfo: (levelOrTrack as Level | null) || hls.levels[0],
754812
level: levelIndex || 0,
755813
id: id || 0,
756814
stats,
@@ -762,7 +820,7 @@ class PlaylistLoader implements NetworkComponentAPI {
762820
case PlaylistContextType.AUDIO_TRACK:
763821
hls.trigger(Events.AUDIO_TRACK_LOADED, {
764822
details: levelDetails,
765-
track: context.levelOrTrack as MediaPlaylist,
823+
track: levelOrTrack as MediaPlaylist,
766824
id: id || 0,
767825
groupId: groupId || '',
768826
stats,
@@ -773,7 +831,7 @@ class PlaylistLoader implements NetworkComponentAPI {
773831
case PlaylistContextType.SUBTITLE_TRACK:
774832
hls.trigger(Events.SUBTITLE_TRACK_LOADED, {
775833
details: levelDetails,
776-
track: context.levelOrTrack as MediaPlaylist,
834+
track: levelOrTrack as MediaPlaylist,
777835
id: id || 0,
778836
groupId: groupId || '',
779837
stats,

tests/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import './unit/loader/fragment-loader';
3636
import './unit/loader/fragment';
3737
import './unit/loader/level';
3838
import './unit/loader/m3u8-parser';
39+
import './unit/loader/playlist-loader';
3940
import './unit/remux/mp4-remuxer';
4041
import './unit/utils/attr-list';
4142
import './unit/utils/binary-search';

tests/unit/controller/content-steering-controller.ts

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ type ConentSteeringControllerTestable = Omit<
5454
audioTracks: MediaPlaylist[] | null;
5555
subtitleTracks: MediaPlaylist[] | null;
5656
onManifestLoading: () => void;
57-
onManifestLoaded: (event: string, data: Partial<ManifestLoadedData>) => void;
57+
onManifestLoaded: (event: string, data: ManifestLoadedData) => void;
5858
};
5959

6060
describe('ContentSteeringController', function () {
@@ -100,6 +100,16 @@ describe('ContentSteeringController', function () {
100100
uri: 'http://example.com/manifest.json',
101101
pathwayId: 'pathway-2',
102102
},
103+
levels: [],
104+
audioTracks: [],
105+
subtitles: [],
106+
networkDetails: new Response('ok'),
107+
url: 'https://example.com/prog.m3u8',
108+
stats: new LoadStats(),
109+
sessionData: null,
110+
sessionKeys: null,
111+
startTimeOffset: null,
112+
variableList: null,
103113
});
104114
expect(contentSteeringController.uri).to.equal(
105115
'http://example.com/manifest.json',
@@ -114,6 +124,16 @@ describe('ContentSteeringController', function () {
114124
uri: 'http://example.com/manifest.json',
115125
pathwayId: 'pathway-2',
116126
},
127+
levels: [],
128+
audioTracks: [],
129+
subtitles: [],
130+
networkDetails: new Response('ok'),
131+
url: 'https://example.com/prog.m3u8',
132+
stats: new LoadStats(),
133+
sessionData: null,
134+
sessionKeys: null,
135+
startTimeOffset: null,
136+
variableList: null,
117137
});
118138
contentSteeringController.stopLoad();
119139
expect(contentSteeringController).to.have.property('loader').that.is.null;
@@ -128,6 +148,16 @@ describe('ContentSteeringController', function () {
128148
uri: 'http://example.com/manifest.json',
129149
pathwayId: 'pathway-2',
130150
},
151+
levels: [],
152+
audioTracks: [],
153+
subtitles: [],
154+
networkDetails: new Response('ok'),
155+
url: 'https://example.com/prog.m3u8',
156+
stats: new LoadStats(),
157+
sessionData: null,
158+
sessionKeys: null,
159+
startTimeOffset: null,
160+
variableList: null,
131161
});
132162
expect(contentSteeringController.stopLoad).to.be.a('function');
133163
contentSteeringController.stopLoad();
@@ -149,6 +179,16 @@ describe('ContentSteeringController', function () {
149179
uri: 'http://example.com/manifest.json',
150180
pathwayId: 'pathway-2',
151181
},
182+
levels: [],
183+
audioTracks: [],
184+
subtitles: [],
185+
networkDetails: new Response('ok'),
186+
url: 'https://example.com/prog.m3u8',
187+
stats: new LoadStats(),
188+
sessionData: null,
189+
sessionKeys: null,
190+
startTimeOffset: null,
191+
variableList: null,
152192
});
153193
expect(contentSteeringController.loader)
154194
.to.have.property('context')
@@ -166,6 +206,16 @@ describe('ContentSteeringController', function () {
166206
uri: 'http://example.com/manifest.json',
167207
pathwayId: 'pathway-2',
168208
},
209+
levels: [],
210+
audioTracks: [],
211+
subtitles: [],
212+
networkDetails: new Response('ok'),
213+
url: 'https://example.com/prog.m3u8',
214+
stats: new LoadStats(),
215+
sessionData: null,
216+
sessionKeys: null,
217+
startTimeOffset: null,
218+
variableList: null,
169219
});
170220
expect(contentSteeringController.uri).to.equal(
171221
'http://example.com/manifest.json',
@@ -209,11 +259,18 @@ http://a.example.com/md/prog_index.m3u8`;
209259
'http://example.com/main.m3u8',
210260
parsedMultivariant,
211261
);
212-
const manifestLoadedData = {
262+
const manifestLoadedData: ManifestLoadedData = {
213263
contentSteering: parsedMultivariant.contentSteering,
214264
levels: parsedMultivariant.levels,
215-
audioTracks: parsedMediaOptions.AUDIO,
265+
audioTracks: parsedMediaOptions.AUDIO!,
216266
subtitles: parsedMediaOptions.SUBTITLES,
267+
networkDetails: new Response('ok'),
268+
url: 'https://example.com/prog.m3u8',
269+
stats: new LoadStats(),
270+
sessionData: null,
271+
sessionKeys: null,
272+
startTimeOffset: null,
273+
variableList: null,
217274
};
218275
const levelController: any = (hls.levelController = new LevelController(
219276
hls as any,
@@ -310,11 +367,18 @@ http://a.example.com/md/prog_index.m3u8`;
310367
'http://example.com/main.m3u8',
311368
parsedMultivariant,
312369
);
313-
const manifestLoadedData = {
370+
const manifestLoadedData: ManifestLoadedData = {
314371
contentSteering: parsedMultivariant.contentSteering,
315372
levels: parsedMultivariant.levels,
316-
audioTracks: parsedMediaOptions.AUDIO,
373+
audioTracks: parsedMediaOptions.AUDIO!,
317374
subtitles: parsedMediaOptions.SUBTITLES,
375+
networkDetails: new Response('ok'),
376+
url: 'https://example.com/prog.m3u8',
377+
stats: new LoadStats(),
378+
sessionData: null,
379+
sessionKeys: null,
380+
startTimeOffset: null,
381+
variableList: null,
318382
};
319383
levelController = hls.levelController = new LevelController(
320384
hls as any,

0 commit comments

Comments
 (0)