Skip to content

Commit a3c63bd

Browse files
committed
Multivariant Playlist parsing fixes (video-dev#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 video-dev#7518 * Add mvp loading and parsing test with `stats.parsing.end` assertion (video-dev#7518) * Fix incorrect LEVEL_LOADED `level` index when lower level removed while loading
1 parent 5a62280 commit a3c63bd

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
@@ -1533,6 +1533,9 @@ export default class InterstitialsController
15331533
return;
15341534
}
15351535
const main = this.hls.levels[data.level];
1536+
if (!main.details) {
1537+
return;
1538+
}
15361539
const currentSelection = {
15371540
...(this.mediaSelection || this.altSelection),
15381541
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';
@@ -423,6 +427,7 @@ class PlaylistLoader implements NetworkComponentAPI {
423427
const parsedResult = M3U8Parser.parseMasterPlaylist(string, url);
424428

425429
if (parsedResult.playlistParsingError) {
430+
stats.parsing.end = performance.now();
426431
this.handleManifestParsingError(
427432
response,
428433
context,
@@ -444,6 +449,44 @@ class PlaylistLoader implements NetworkComponentAPI {
444449

445450
this.variableList = variableList;
446451

452+
// Treat unknown codec as audio or video codec based on passing `isTypeSupported` check
453+
// (allows for playback of any supported codec even if not indexed in utils/codecs)
454+
levels.forEach((levelParsed: LevelParsed) => {
455+
const { unknownCodecs } = levelParsed;
456+
if (unknownCodecs) {
457+
const { preferManagedMediaSource } = this.hls.config;
458+
let { audioCodec, videoCodec } = levelParsed;
459+
for (let i = unknownCodecs.length; i--; ) {
460+
const unknownCodec = unknownCodecs[i];
461+
if (
462+
areCodecsMediaSourceSupported(
463+
unknownCodec,
464+
'audio',
465+
preferManagedMediaSource,
466+
)
467+
) {
468+
levelParsed.audioCodec = audioCodec = audioCodec
469+
? `${audioCodec},${unknownCodec}`
470+
: unknownCodec;
471+
sampleEntryCodesISO.audio[audioCodec.substring(0, 4)] = 2;
472+
unknownCodecs.splice(i, 1);
473+
} else if (
474+
areCodecsMediaSourceSupported(
475+
unknownCodec,
476+
'video',
477+
preferManagedMediaSource,
478+
)
479+
) {
480+
levelParsed.videoCodec = videoCodec = videoCodec
481+
? `${videoCodec},${unknownCodec}`
482+
: unknownCodec;
483+
sampleEntryCodesISO.video[videoCodec.substring(0, 4)] = 2;
484+
unknownCodecs.splice(i, 1);
485+
}
486+
}
487+
}
488+
});
489+
447490
const {
448491
AUDIO: audioTracks = [],
449492
SUBTITLES: subtitles,
@@ -679,10 +722,11 @@ class PlaylistLoader implements NetworkComponentAPI {
679722
loader: Loader<PlaylistLoaderContext> | undefined,
680723
): void {
681724
const hls = this.hls;
682-
const { type, level, id, groupId, deliveryDirectives } = context;
725+
const { type, level, levelOrTrack, id, groupId, deliveryDirectives } =
726+
context;
683727
const url = getResponseUrl(response, context);
684728
const parent = mapContextToLevelType(context);
685-
const levelIndex =
729+
let levelIndex =
686730
typeof context.level === 'number' && parent === PlaylistLevelType.MAIN
687731
? (level as number)
688732
: undefined;
@@ -744,9 +788,23 @@ class PlaylistLoader implements NetworkComponentAPI {
744788
switch (type) {
745789
case PlaylistContextType.MANIFEST:
746790
case PlaylistContextType.LEVEL:
791+
if (levelIndex) {
792+
if (!levelOrTrack) {
793+
// fall-through to hls.levels[0]
794+
levelIndex = 0;
795+
} else {
796+
if (levelOrTrack !== hls.levels[levelIndex]) {
797+
// correct levelIndex when lower levels were removed from hls.levels
798+
const updatedIndex = hls.levels.indexOf(levelOrTrack as Level);
799+
if (updatedIndex > -1) {
800+
levelIndex = updatedIndex;
801+
}
802+
}
803+
}
804+
}
747805
hls.trigger(Events.LEVEL_LOADED, {
748806
details: levelDetails,
749-
levelInfo: (context.levelOrTrack as Level) || hls.levels[0],
807+
levelInfo: (levelOrTrack as Level | null) || hls.levels[0],
750808
level: levelIndex || 0,
751809
id: id || 0,
752810
stats,
@@ -758,7 +816,7 @@ class PlaylistLoader implements NetworkComponentAPI {
758816
case PlaylistContextType.AUDIO_TRACK:
759817
hls.trigger(Events.AUDIO_TRACK_LOADED, {
760818
details: levelDetails,
761-
track: context.levelOrTrack as MediaPlaylist,
819+
track: levelOrTrack as MediaPlaylist,
762820
id: id || 0,
763821
groupId: groupId || '',
764822
stats,
@@ -769,7 +827,7 @@ class PlaylistLoader implements NetworkComponentAPI {
769827
case PlaylistContextType.SUBTITLE_TRACK:
770828
hls.trigger(Events.SUBTITLE_TRACK_LOADED, {
771829
details: levelDetails,
772-
track: context.levelOrTrack as MediaPlaylist,
830+
track: levelOrTrack as MediaPlaylist,
773831
id: id || 0,
774832
groupId: groupId || '',
775833
stats,

tests/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import './unit/loader/fragment-loader';
3535
import './unit/loader/fragment';
3636
import './unit/loader/level';
3737
import './unit/loader/m3u8-parser';
38+
import './unit/loader/playlist-loader';
3839
import './unit/remux/mp4-remuxer';
3940
import './unit/utils/attr-list';
4041
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)