hls.js 1.5.14-0.canary.10668 → 1.5.14-0.canary.10670

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -130,5 +130,5 @@
130
130
  "url-toolkit": "2.2.5",
131
131
  "wrangler": "3.80.0"
132
132
  },
133
- "version": "1.5.14-0.canary.10668"
133
+ "version": "1.5.14-0.canary.10670"
134
134
  }
@@ -634,7 +634,7 @@ transfer tracks: ${JSON.stringify(transferredTracks, (key, value) => (key === 'i
634
634
  if (trackName.slice(0, 5) === 'audio') {
635
635
  trackCodec = getCodecCompatibleName(trackCodec, this.appendSource);
636
636
  }
637
- this.log(`switching codec ${sbCodec} to ${codec}`);
637
+ this.log(`switching codec ${sbCodec} to ${trackCodec}`);
638
638
  if (trackCodec !== (track.pendingCodec || track.codec)) {
639
639
  track.pendingCodec = trackCodec;
640
640
  }
@@ -1431,7 +1431,7 @@ transfer tracks: ${JSON.stringify(transferredTracks, (key, value) => (key === 'i
1431
1431
  }
1432
1432
 
1433
1433
  private getTrackCodec(track: BaseTrack, trackName: SourceBufferName): string {
1434
- const codec = track.codec || track.levelCodec;
1434
+ const codec = pickMostCompleteCodecName(track.codec, track.levelCodec);
1435
1435
  if (codec) {
1436
1436
  if (trackName.slice(0, 5) === 'audio') {
1437
1437
  return getCodecCompatibleName(codec, this.appendSource);
@@ -16,6 +16,7 @@ import {
16
16
  codecsSetSelectionPreferenceValue,
17
17
  convertAVC1ToAVCOTI,
18
18
  getCodecCompatibleName,
19
+ sampleEntryCodesISO,
19
20
  videoCodecPreferenceValue,
20
21
  } from '../utils/codecs';
21
22
  import BasePlaylistController from './base-playlist-controller';
@@ -130,23 +131,36 @@ export default class LevelController extends BasePlaylistController {
130
131
 
131
132
  // only keep levels with supported audio/video codecs
132
133
  const { width, height, unknownCodecs } = levelParsed;
134
+ let unknownUnsupportedCodecCount = unknownCodecs
135
+ ? unknownCodecs.length
136
+ : 0;
137
+ if (unknownCodecs) {
138
+ // Treat unknown codec as audio or video codec based on passing `isTypeSupported` check
139
+ // (allows for playback of any supported codec even if not indexed in utils/codecs)
140
+ for (let i = unknownUnsupportedCodecCount; i--; ) {
141
+ const unknownCodec = unknownCodecs[i];
142
+ if (this.isAudioSupported(unknownCodec)) {
143
+ levelParsed.audioCodec = audioCodec = audioCodec
144
+ ? `${audioCodec},${unknownCodec}`
145
+ : unknownCodec;
146
+ unknownUnsupportedCodecCount--;
147
+ sampleEntryCodesISO.audio[audioCodec.substring(0, 4)] = 2;
148
+ } else if (this.isVideoSupported(unknownCodec)) {
149
+ levelParsed.videoCodec = videoCodec = videoCodec
150
+ ? `${videoCodec},${unknownCodec}`
151
+ : unknownCodec;
152
+ unknownUnsupportedCodecCount--;
153
+ sampleEntryCodesISO.video[videoCodec.substring(0, 4)] = 2;
154
+ }
155
+ }
156
+ }
133
157
  resolutionFound ||= !!(width && height);
134
158
  videoCodecFound ||= !!videoCodec;
135
159
  audioCodecFound ||= !!audioCodec;
136
160
  if (
137
- unknownCodecs?.length ||
138
- (audioCodec &&
139
- !areCodecsMediaSourceSupported(
140
- audioCodec,
141
- 'audio',
142
- preferManagedMediaSource,
143
- )) ||
144
- (videoCodec &&
145
- !areCodecsMediaSourceSupported(
146
- videoCodec,
147
- 'video',
148
- preferManagedMediaSource,
149
- ))
161
+ unknownUnsupportedCodecCount ||
162
+ (audioCodec && !this.isAudioSupported(audioCodec)) ||
163
+ (videoCodec && !this.isVideoSupported(videoCodec))
150
164
  ) {
151
165
  return;
152
166
  }
@@ -193,6 +207,22 @@ export default class LevelController extends BasePlaylistController {
193
207
  );
194
208
  }
195
209
 
210
+ private isAudioSupported(codec: string): boolean {
211
+ return areCodecsMediaSourceSupported(
212
+ codec,
213
+ 'audio',
214
+ this.hls.config.preferManagedMediaSource,
215
+ );
216
+ }
217
+
218
+ private isVideoSupported(codec: string): boolean {
219
+ return areCodecsMediaSourceSupported(
220
+ codec,
221
+ 'video',
222
+ this.hls.config.preferManagedMediaSource,
223
+ );
224
+ }
225
+
196
226
  private filterAndSortMediaOptions(
197
227
  filteredLevels: Level[],
198
228
  data: ManifestLoadedData,
@@ -240,15 +270,8 @@ export default class LevelController extends BasePlaylistController {
240
270
  }
241
271
 
242
272
  if (data.audioTracks) {
243
- const { preferManagedMediaSource } = this.hls.config;
244
273
  audioTracks = data.audioTracks.filter(
245
- (track) =>
246
- !track.audioCodec ||
247
- areCodecsMediaSourceSupported(
248
- track.audioCodec,
249
- 'audio',
250
- preferManagedMediaSource,
251
- ),
274
+ (track) => !track.audioCodec || this.isAudioSupported(track.audioCodec),
252
275
  );
253
276
  // Assign ids after filtering as array indices by group-id
254
277
  assignTrackIdsByGroup(audioTracks);
@@ -14,6 +14,7 @@ import TransmuxerInterface from '../demux/transmuxer-interface';
14
14
  import { ChunkMetadata } from '../types/transmuxer';
15
15
  import GapController, { MAX_START_GAP_JUMP } from './gap-controller';
16
16
  import { ErrorDetails } from '../errors';
17
+ import { pickMostCompleteCodecName } from '../utils/codecs';
17
18
  import type { NetworkComponentAPI } from '../types/component-api';
18
19
  import type Hls from '../hls';
19
20
  import type { Level } from '../types/level';
@@ -1367,7 +1368,16 @@ export default class StreamController
1367
1368
  // include levelCodec in audio and video tracks
1368
1369
  const { audio, video, audiovideo } = tracks;
1369
1370
  if (audio) {
1370
- let audioCodec = currentLevel.audioCodec;
1371
+ let audioCodec = pickMostCompleteCodecName(
1372
+ audio.codec,
1373
+ currentLevel.audioCodec,
1374
+ );
1375
+ // Add level and profile to make up for passthrough-remuxer not being able to parse full codec
1376
+ // (logger warning "Unhandled audio codec...")
1377
+ if (audioCodec === 'mp4a') {
1378
+ audioCodec = 'mp4a.40.5';
1379
+ }
1380
+ // Handle `audioCodecSwitch`
1371
1381
  const ua = navigator.userAgent.toLowerCase();
1372
1382
  if (this.audioCodecSwitch) {
1373
1383
  if (audioCodec) {
@@ -1420,12 +1430,29 @@ export default class StreamController
1420
1430
  if (video) {
1421
1431
  video.levelCodec = currentLevel.videoCodec;
1422
1432
  video.id = 'main';
1433
+ const parsedVideoCodec = video.codec;
1434
+ if (parsedVideoCodec?.length === 4) {
1435
+ // Make up for passthrough-remuxer not being able to parse full codec
1436
+ // (logger warning "Unhandled video codec...")
1437
+ switch (parsedVideoCodec) {
1438
+ case 'hvc1':
1439
+ case 'hev1':
1440
+ video.codec = 'hvc1.1.6.L120.90';
1441
+ break;
1442
+ case 'av01':
1443
+ video.codec = 'av01.0.04M.08';
1444
+ break;
1445
+ case 'avc1':
1446
+ video.codec = 'avc1.42e01e';
1447
+ break;
1448
+ }
1449
+ }
1423
1450
  this.log(
1424
1451
  `Init video buffer, container:${
1425
1452
  video.container
1426
1453
  }, codecs[level/parsed]=[${currentLevel.videoCodec || ''}/${
1427
- video.codec
1428
- }]`,
1454
+ parsedVideoCodec
1455
+ }${video.codec !== parsedVideoCodec ? ' parsed-corrected=' + video.codec : ''}}]`,
1429
1456
  );
1430
1457
  delete tracks.audiovideo;
1431
1458
  }
@@ -1,4 +1,5 @@
1
1
  import BaseVideoParser from './base-video-parser';
2
+ import type { ParsedVideoSample } from '../tsdemuxer';
2
3
  import type {
3
4
  DemuxedVideoTrack,
4
5
  DemuxedUserdataTrack,
@@ -180,7 +181,7 @@ class HevcVideoParser extends BaseVideoParser {
180
181
  track.params[prop] = config[prop];
181
182
  }
182
183
  }
183
- if (this.initVPS !== null || track.pps.length === 0) {
184
+ if (track.vps !== undefined && track.vps[0] === this.initVPS) {
184
185
  track.pps.push(unit.data);
185
186
  }
186
187
  }
@@ -239,6 +240,16 @@ class HevcVideoParser extends BaseVideoParser {
239
240
  return new Uint8Array(dst.buffer, 0, dstIdx);
240
241
  }
241
242
 
243
+ protected pushAccessUnit(
244
+ VideoSample: ParsedVideoSample,
245
+ videoTrack: DemuxedVideoTrack,
246
+ ) {
247
+ super.pushAccessUnit(VideoSample, videoTrack);
248
+ if (this.initVPS) {
249
+ this.initVPS = null; // null initVPS to prevent possible track's sps/pps growth until next VPS
250
+ }
251
+ }
252
+
242
253
  readVPS(vps: Uint8Array): {
243
254
  numTemporalLayers: number;
244
255
  temporalIdNested: boolean;
package/src/hls.ts CHANGED
@@ -54,6 +54,11 @@ import type FragmentLoader from './loader/fragment-loader';
54
54
  import type { LevelDetails } from './loader/level-details';
55
55
  import type TaskLoop from './task-loop';
56
56
  import type TransmuxerInterface from './demux/transmuxer-interface';
57
+ import { getAudioTracksByGroup } from './utils/rendition-helper';
58
+ import {
59
+ getMediaDecodingInfoPromise,
60
+ MediaDecodingInfo,
61
+ } from './utils/mediacapabilities-helper';
57
62
 
58
63
  /**
59
64
  * The `Hls` class is the core of the HLS.js library used to instantiate player instances.
@@ -951,7 +956,7 @@ export default class Hls implements HlsEventEmitter {
951
956
  /**
952
957
  * Get the complete list of audio tracks across all media groups
953
958
  */
954
- get allAudioTracks(): Array<MediaPlaylist> {
959
+ get allAudioTracks(): MediaPlaylist[] {
955
960
  const audioTrackController = this.audioTrackController;
956
961
  return audioTrackController ? audioTrackController.allAudioTracks : [];
957
962
  }
@@ -959,7 +964,7 @@ export default class Hls implements HlsEventEmitter {
959
964
  /**
960
965
  * Get the list of selectable audio tracks
961
966
  */
962
- get audioTracks(): Array<MediaPlaylist> {
967
+ get audioTracks(): MediaPlaylist[] {
963
968
  const audioTrackController = this.audioTrackController;
964
969
  return audioTrackController ? audioTrackController.audioTracks : [];
965
970
  }
@@ -985,7 +990,7 @@ export default class Hls implements HlsEventEmitter {
985
990
  /**
986
991
  * get the complete list of subtitle tracks across all media groups
987
992
  */
988
- get allSubtitleTracks(): Array<MediaPlaylist> {
993
+ get allSubtitleTracks(): MediaPlaylist[] {
989
994
  const subtitleTrackController = this.subtitleTrackController;
990
995
  return subtitleTrackController
991
996
  ? subtitleTrackController.allSubtitleTracks
@@ -995,7 +1000,7 @@ export default class Hls implements HlsEventEmitter {
995
1000
  /**
996
1001
  * get alternate subtitle tracks list from playlist
997
1002
  */
998
- get subtitleTracks(): Array<MediaPlaylist> {
1003
+ get subtitleTracks(): MediaPlaylist[] {
999
1004
  const subtitleTrackController = this.subtitleTrackController;
1000
1005
  return subtitleTrackController
1001
1006
  ? subtitleTrackController.subtitleTracks
@@ -1132,6 +1137,21 @@ export default class Hls implements HlsEventEmitter {
1132
1137
  get interstitialsManager(): InterstitialsManager | null {
1133
1138
  return this.interstitialsController?.interstitialsManager || null;
1134
1139
  }
1140
+
1141
+ /**
1142
+ * returns mediaCapabilities.decodingInfo for a variant/rendition
1143
+ */
1144
+ getMediaDecodingInfo(
1145
+ level: Level,
1146
+ audioTracks: MediaPlaylist[] = this.allAudioTracks,
1147
+ ): Promise<MediaDecodingInfo> {
1148
+ const audioTracksByGroup = getAudioTracksByGroup(audioTracks);
1149
+ return getMediaDecodingInfoPromise(
1150
+ level,
1151
+ audioTracksByGroup,
1152
+ navigator.mediaCapabilities,
1153
+ );
1154
+ }
1135
1155
  }
1136
1156
 
1137
1157
  export type {
@@ -32,7 +32,7 @@ import type {
32
32
  } from '../types/demuxer';
33
33
  import type { DecryptData } from '../loader/level-key';
34
34
  import type { TypeSupported } from '../utils/codecs';
35
- import type { ILogger } from '../utils/logger';
35
+ import { logger, type ILogger } from '../utils/logger';
36
36
  import type { RationalTimestamp } from '../utils/timescale-conversion';
37
37
 
38
38
  class PassThroughRemuxer implements Remuxer {
@@ -86,7 +86,7 @@ class PassThroughRemuxer implements Remuxer {
86
86
  }
87
87
  const initData = (this.initData = parseInitSegment(initSegment));
88
88
 
89
- // Get codec from initSegment or fallback to default
89
+ // Get codec from initSegment
90
90
  if (initData.audio) {
91
91
  audioCodec = getParsedTrackCodec(
92
92
  initData.audio,
@@ -298,21 +298,13 @@ function getParsedTrackCodec(
298
298
  const preferManagedMediaSource = false;
299
299
  return getCodecCompatibleName(parsedCodec, preferManagedMediaSource);
300
300
  }
301
- const result = 'mp4a.40.5';
302
- this.logger.info(
303
- `Parsed audio codec "${parsedCodec}" or audio object type not handled. Using "${result}"`,
304
- );
305
- return result;
301
+
302
+ logger.warn(`Unhandled audio codec "${parsedCodec}" in mp4 MAP`);
303
+ return parsedCodec || 'mp4a';
306
304
  }
307
305
  // Provide defaults based on codec type
308
306
  // This allows for some playback of some fmp4 playlists without CODECS defined in manifest
309
- this.logger.warn(`Unhandled video codec "${parsedCodec}"`);
310
- if (parsedCodec === 'hvc1' || parsedCodec === 'hev1') {
311
- return 'hvc1.1.6.L120.90';
312
- }
313
- if (parsedCodec === 'av01') {
314
- return 'av01.0.04M.08';
315
- }
316
- return 'avc1.42e01e';
307
+ logger.warn(`Unhandled video codec "${parsedCodec}" in mp4 MAP`);
308
+ return parsedCodec || 'avc1';
317
309
  }
318
310
  export default PassThroughRemuxer;
@@ -2,7 +2,7 @@ import { getMediaSource } from './mediasource-helper';
2
2
 
3
3
  // from http://mp4ra.org/codecs.html
4
4
  // values indicate codec selection preference (lower is higher priority)
5
- const sampleEntryCodesISO = {
5
+ export const sampleEntryCodesISO = {
6
6
  audio: {
7
7
  a3ds: 1,
8
8
  'ac-3': 0.95,
@@ -107,7 +107,7 @@ function isCodecMediaSourceSupported(
107
107
  }
108
108
 
109
109
  export function mimeTypeForCodec(codec: string, type: CodecType): string {
110
- return `${type}/mp4;codecs="${codec}"`;
110
+ return `${type}/mp4;codecs=${codec}`;
111
111
  }
112
112
 
113
113
  export function videoCodecPreferenceValue(
@@ -198,16 +198,33 @@ export function pickMostCompleteCodecName(
198
198
  ): string | undefined {
199
199
  // Parsing of mp4a codecs strings in mp4-tools from media is incomplete as of d8c6c7a
200
200
  // so use level codec is parsed codec is unavailable or incomplete
201
- if (parsedCodec && parsedCodec !== 'mp4a') {
201
+ if (
202
+ parsedCodec &&
203
+ (parsedCodec.length > 4 ||
204
+ ['ac-3', 'ec-3', 'alac', 'fLaC', 'Opus'].indexOf(parsedCodec) !== -1)
205
+ ) {
202
206
  return parsedCodec;
203
207
  }
204
- return levelCodec ? levelCodec.split(',')[0] : levelCodec;
208
+ if (levelCodec) {
209
+ const levelCodecs = levelCodec.split(',');
210
+ if (levelCodecs.length > 1) {
211
+ if (parsedCodec) {
212
+ for (let i = levelCodecs.length; i--; ) {
213
+ if (levelCodecs[i].substring(0, 4) === parsedCodec.substring(0, 4)) {
214
+ return levelCodecs[i];
215
+ }
216
+ }
217
+ }
218
+ return levelCodecs[0];
219
+ }
220
+ }
221
+ return levelCodec || parsedCodec;
205
222
  }
206
223
 
207
- export function convertAVC1ToAVCOTI(codec: string) {
224
+ export function convertAVC1ToAVCOTI(videoCodecs: string): string {
208
225
  // Convert avc1 codec string from RFC-4281 to RFC-6381 for MediaSource.isTypeSupported
209
226
  // Examples: avc1.66.30 to avc1.42001e and avc1.77.30,avc1.66.30 to avc1.4d001e,avc1.42001e.
210
- const codecs = codec.split(',');
227
+ const codecs = videoCodecs.split(',');
211
228
  for (let i = 0; i < codecs.length; i++) {
212
229
  const avcdata = codecs[i].split('.');
213
230
  if (avcdata.length > 2) {
@@ -222,6 +239,19 @@ export function convertAVC1ToAVCOTI(codec: string) {
222
239
  return codecs.join(',');
223
240
  }
224
241
 
242
+ export function fillInMissingAV01Params(videoCodec: string): string {
243
+ // Used to fill in incomplete AV1 playlist CODECS strings for mediaCapabilities.decodingInfo queries
244
+ if (videoCodec.startsWith('av01.')) {
245
+ const av1params = videoCodec.split('.');
246
+ const placeholders = ['0', '111', '01', '01', '01', '0'];
247
+ for (let i = av1params.length; i > 4 && i < 10; i++) {
248
+ av1params[i] = placeholders[i - 4];
249
+ }
250
+ return av1params.join('.');
251
+ }
252
+ return videoCodec;
253
+ }
254
+
225
255
  export interface TypeSupported {
226
256
  mpeg: boolean;
227
257
  mp3: boolean;
@@ -1,4 +1,4 @@
1
- import { mimeTypeForCodec } from './codecs';
1
+ import { fillInMissingAV01Params, mimeTypeForCodec } from './codecs';
2
2
  import type { Level, VideoRange } from '../types/level';
3
3
  import type { AudioSelectionOption } from '../types/media-playlist';
4
4
  import type { AudioTracksByGroup } from './rendition-helper';
@@ -96,33 +96,40 @@ export function getMediaDecodingInfoPromise(
96
96
  ): Promise<MediaDecodingInfo> {
97
97
  const videoCodecs = level.videoCodec;
98
98
  const audioCodecs = level.audioCodec;
99
- if (!videoCodecs || !audioCodecs || !mediaCapabilities) {
99
+ if ((!videoCodecs && !audioCodecs) || !mediaCapabilities) {
100
100
  return Promise.resolve(SUPPORTED_INFO_DEFAULT);
101
101
  }
102
102
 
103
- const baseVideoConfiguration: BaseVideoConfiguration = {
104
- width: level.width,
105
- height: level.height,
106
- bitrate: Math.ceil(Math.max(level.bitrate * 0.9, level.averageBitrate)),
107
- // Assume a framerate of 30fps since MediaCapabilities will not accept Level default of 0.
108
- framerate: level.frameRate || 30,
109
- };
103
+ const configurations: MediaDecodingConfiguration[] = [];
110
104
 
111
- const videoRange = level.videoRange;
112
- if (videoRange !== 'SDR') {
113
- baseVideoConfiguration.transferFunction =
114
- videoRange.toLowerCase() as TransferFunction;
115
- }
105
+ if (videoCodecs) {
106
+ const baseVideoConfiguration: BaseVideoConfiguration = {
107
+ width: level.width,
108
+ height: level.height,
109
+ bitrate: Math.ceil(Math.max(level.bitrate * 0.9, level.averageBitrate)),
110
+ // Assume a framerate of 30fps since MediaCapabilities will not accept Level default of 0.
111
+ framerate: level.frameRate || 30,
112
+ };
113
+ const videoRange = level.videoRange;
114
+ if (videoRange !== 'SDR') {
115
+ baseVideoConfiguration.transferFunction =
116
+ videoRange.toLowerCase() as TransferFunction;
117
+ }
116
118
 
117
- const configurations: MediaDecodingConfiguration[] = videoCodecs
118
- .split(',')
119
- .map((videoCodec) => ({
120
- type: 'media-source',
121
- video: {
122
- ...baseVideoConfiguration,
123
- contentType: mimeTypeForCodec(videoCodec, 'video'),
124
- },
125
- }));
119
+ configurations.push.apply(
120
+ configurations,
121
+ videoCodecs.split(',').map((videoCodec) => ({
122
+ type: 'media-source',
123
+ video: {
124
+ ...baseVideoConfiguration,
125
+ contentType: mimeTypeForCodec(
126
+ fillInMissingAV01Params(videoCodec),
127
+ 'video',
128
+ ),
129
+ },
130
+ })),
131
+ );
132
+ }
126
133
 
127
134
  if (audioCodecs && level.audioGroups) {
128
135
  level.audioGroups.forEach((audioGroupId) => {
@@ -152,12 +152,12 @@ function createMediaKeySystemConfigurations(
152
152
  drmSystemOptions.sessionType || 'temporary',
153
153
  ],
154
154
  audioCapabilities: audioCodecs.map((codec) => ({
155
- contentType: `audio/mp4; codecs="${codec}"`,
155
+ contentType: `audio/mp4; codecs=${codec}`,
156
156
  robustness: drmSystemOptions.audioRobustness || '',
157
157
  encryptionScheme: drmSystemOptions.audioEncryptionScheme || null,
158
158
  })),
159
159
  videoCapabilities: videoCodecs.map((codec) => ({
160
- contentType: `video/mp4; codecs="${codec}"`,
160
+ contentType: `video/mp4; codecs=${codec}`,
161
161
  robustness: drmSystemOptions.videoRobustness || '',
162
162
  encryptionScheme: drmSystemOptions.videoEncryptionScheme || null,
163
163
  })),