hls.js 1.6.6-0.canary.11352 → 1.6.6-0.canary.11353

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.
@@ -8,7 +8,6 @@ import {
8
8
  requiresMediaCapabilitiesDecodingInfo,
9
9
  SUPPORTED_INFO_DEFAULT,
10
10
  } from '../utils/mediacapabilities-helper';
11
- import { isHEVC } from '../utils/mp4-tools';
12
11
  import {
13
12
  type AudioTracksByGroup,
14
13
  type CodecSetTier,
@@ -47,6 +46,10 @@ class AbrController extends Logger implements AbrComponentAPI {
47
46
  private partCurrent: Part | null = null;
48
47
  private bitrateTestDelay: number = 0;
49
48
  private rebufferNotice: number = -1;
49
+ private supportedCache: Record<
50
+ string,
51
+ Promise<MediaCapabilitiesDecodingInfo>
52
+ > = {};
50
53
 
51
54
  public bwEstimator: EwmaBandWidthEstimator;
52
55
 
@@ -108,7 +111,7 @@ class AbrController extends Logger implements AbrComponentAPI {
108
111
  this.unregisterListeners();
109
112
  this.clearTimer();
110
113
  // @ts-ignore
111
- this.hls = this._abandonRulesCheck = null;
114
+ this.hls = this._abandonRulesCheck = this.supportedCache = null;
112
115
  this.fragCurrent = this.partCurrent = null;
113
116
  }
114
117
 
@@ -119,6 +122,7 @@ class AbrController extends Logger implements AbrComponentAPI {
119
122
  this.lastLoadedFragLevel = -1;
120
123
  this.firstSelection = -1;
121
124
  this.lastLevelLoadSec = 0;
125
+ this.supportedCache = {};
122
126
  this.fragCurrent = this.partCurrent = null;
123
127
  this.onLevelsUpdated();
124
128
  this.clearTimer();
@@ -826,20 +830,20 @@ class AbrController extends Logger implements AbrComponentAPI {
826
830
  | undefined;
827
831
  if (
828
832
  typeof mediaCapabilities?.decodingInfo === 'function' &&
829
- (requiresMediaCapabilitiesDecodingInfo(
833
+ requiresMediaCapabilitiesDecodingInfo(
830
834
  levelInfo,
831
835
  audioTracksByGroup,
832
836
  currentVideoRange,
833
837
  currentFrameRate,
834
838
  currentBw,
835
839
  audioPreference,
836
- ) ||
837
- isHEVC(levelInfo.videoCodec)) // Force media capabilities check for HEVC to avoid failure on Windows
840
+ )
838
841
  ) {
839
842
  levelInfo.supportedPromise = getMediaDecodingInfoPromise(
840
843
  levelInfo,
841
844
  audioTracksByGroup,
842
845
  mediaCapabilities,
846
+ this.supportedCache,
843
847
  );
844
848
  levelInfo.supportedPromise.then((decodingInfo) => {
845
849
  if (!this.hls) {
@@ -867,6 +871,15 @@ class AbrController extends Logger implements AbrComponentAPI {
867
871
  this.hls.nextLoadLevel = 0;
868
872
  }
869
873
  }
874
+ } else if (
875
+ decodingInfo.decodingInfoResults.some(
876
+ (info) =>
877
+ info.smooth === false || info.powerEfficient === false,
878
+ )
879
+ ) {
880
+ this.log(
881
+ `MediaCapabilities decodingInfo for level ${index} not smooth or powerEfficient: ${stringify(decodingInfo)}`,
882
+ );
870
883
  }
871
884
  });
872
885
  } else {
@@ -883,8 +896,9 @@ class AbrController extends Logger implements AbrComponentAPI {
883
896
  (!upSwitch &&
884
897
  currentFrameRate > 0 &&
885
898
  currentFrameRate < levelInfo.frameRate) ||
886
- (levelInfo.supportedResult &&
887
- !levelInfo.supportedResult.decodingInfoResults?.[0].smooth)
899
+ levelInfo.supportedResult?.decodingInfoResults?.some(
900
+ (info) => info.smooth === false,
901
+ )
888
902
  ) {
889
903
  if (!firstSelection || i !== minStartIndex) {
890
904
  levelsSkipped.push(i);
@@ -16,8 +16,6 @@ export type MediaDecodingInfo = {
16
16
  error?: Error;
17
17
  };
18
18
 
19
- type BaseVideoConfiguration = Omit<VideoConfiguration, 'contentType'>;
20
-
21
19
  export const SUPPORTED_INFO_DEFAULT: MediaDecodingInfo = {
22
20
  supported: true,
23
21
  configurations: [] as MediaDecodingConfiguration[],
@@ -48,11 +46,6 @@ export function getUnsupportedResult(
48
46
  };
49
47
  }
50
48
 
51
- export const SUPPORTED_INFO_CACHE: Record<
52
- string,
53
- Promise<MediaCapabilitiesDecodingInfo>
54
- > = {};
55
-
56
49
  export function requiresMediaCapabilitiesDecodingInfo(
57
50
  level: Level,
58
51
  audioTracksByGroup: AudioTracksByGroup,
@@ -62,6 +55,7 @@ export function requiresMediaCapabilitiesDecodingInfo(
62
55
  audioPreference: AudioSelectionOption | undefined,
63
56
  ): boolean {
64
57
  // Only test support when configuration is exceeds minimum options
58
+ const videoCodecs = level.videoCodec;
65
59
  const audioGroups = level.audioCodec ? level.audioGroups : null;
66
60
  const audioCodecPreference = audioPreference?.audioCodec;
67
61
  const channelsPreference = audioPreference?.channels;
@@ -98,8 +92,10 @@ export function requiresMediaCapabilitiesDecodingInfo(
98
92
  }
99
93
  }
100
94
  return (
101
- (level.videoCodec !== undefined &&
102
- ((level.width > 1920 && level.height > 1088) ||
95
+ (videoCodecs !== undefined &&
96
+ // Force media capabilities check for HEVC to avoid failure on Windows
97
+ (videoCodecs.split(',').some((videoCodec) => isHEVC(videoCodec)) ||
98
+ (level.width > 1920 && level.height > 1088) ||
103
99
  (level.height > 1920 && level.width > 1088) ||
104
100
  level.frameRate > Math.max(currentFrameRate, 30) ||
105
101
  (level.videoRange !== 'SDR' &&
@@ -117,85 +113,56 @@ export function getMediaDecodingInfoPromise(
117
113
  level: Level,
118
114
  audioTracksByGroup: AudioTracksByGroup,
119
115
  mediaCapabilities: MediaCapabilities | undefined,
116
+ cache: Record<string, Promise<MediaCapabilitiesDecodingInfo>> = {},
120
117
  ): Promise<MediaDecodingInfo> {
121
118
  const videoCodecs = level.videoCodec;
122
- const audioCodecs = level.audioCodec;
123
- if ((!videoCodecs && !audioCodecs) || !mediaCapabilities) {
119
+ if ((!videoCodecs && !level.audioCodec) || !mediaCapabilities) {
124
120
  return Promise.resolve(SUPPORTED_INFO_DEFAULT);
125
121
  }
126
122
 
127
123
  const configurations: MediaDecodingConfiguration[] = [];
128
124
 
129
- if (videoCodecs) {
130
- const baseVideoConfiguration: BaseVideoConfiguration = {
131
- width: level.width,
132
- height: level.height,
133
- bitrate: Math.ceil(Math.max(level.bitrate * 0.9, level.averageBitrate)),
134
- // Assume a framerate of 30fps since MediaCapabilities will not accept Level default of 0.
135
- framerate: level.frameRate || 30,
125
+ const videoDecodeList = makeVideoConfigurations(level);
126
+ const videoCount = videoDecodeList.length;
127
+ const audioDecodeList = makeAudioConfigurations(
128
+ level,
129
+ audioTracksByGroup,
130
+ videoCount > 0,
131
+ );
132
+ const audioCount = audioDecodeList.length;
133
+ for (let i = videoCount || 1 * audioCount || 1; i--; ) {
134
+ const configuration: MediaDecodingConfiguration = {
135
+ type: 'media-source',
136
136
  };
137
- const videoRange = level.videoRange;
138
- if (videoRange !== 'SDR') {
139
- baseVideoConfiguration.transferFunction =
140
- videoRange.toLowerCase() as TransferFunction;
137
+ if (videoCount) {
138
+ configuration.video = videoDecodeList[i % videoCount];
139
+ }
140
+ if (audioCount) {
141
+ configuration.audio = audioDecodeList[i % audioCount];
142
+ const audioBitrate = configuration.audio.bitrate;
143
+ if (configuration.video && audioBitrate) {
144
+ configuration.video.bitrate -= audioBitrate;
145
+ }
141
146
  }
142
- const videoCodecsArray = videoCodecs.split(',');
147
+ configurations.push(configuration);
148
+ }
149
+
150
+ if (videoCodecs) {
143
151
  // Override Windows Firefox HEVC MediaCapabilities result (https://github.com/video-dev/hls.js/issues/7046)
144
152
  const ua = navigator.userAgent;
145
153
  if (
146
- videoCodecsArray.some((videoCodec) => isHEVC(videoCodec)) &&
154
+ videoCodecs.split(',').some((videoCodec) => isHEVC(videoCodec)) &&
147
155
  userAgentHevcSupportIsInaccurate()
148
156
  ) {
149
157
  return Promise.resolve(
150
158
  getUnsupportedResult(
151
159
  new Error(
152
- `Overriding Windows Firefox HEVC MediaCapabilities result based on user-agent sting: (${ua})`,
160
+ `Overriding Windows Firefox HEVC MediaCapabilities result based on user-agent string: (${ua})`,
153
161
  ),
154
162
  configurations,
155
163
  ),
156
164
  );
157
165
  }
158
- configurations.push.apply(
159
- configurations,
160
- videoCodecsArray.map((videoCodec) => ({
161
- type: 'media-source',
162
- video: {
163
- ...baseVideoConfiguration,
164
- contentType: mimeTypeForCodec(
165
- fillInMissingAV01Params(videoCodec),
166
- 'video',
167
- ),
168
- },
169
- })),
170
- );
171
- }
172
-
173
- if (audioCodecs && level.audioGroups) {
174
- level.audioGroups.forEach((audioGroupId) => {
175
- if (!audioGroupId) {
176
- return;
177
- }
178
- audioTracksByGroup.groups[audioGroupId]?.tracks.forEach((audioTrack) => {
179
- if (audioTrack.groupId === audioGroupId) {
180
- const channels = audioTrack.channels || '';
181
- const channelsNumber = parseFloat(channels);
182
- if (Number.isFinite(channelsNumber) && channelsNumber > 2) {
183
- configurations.push.apply(
184
- configurations,
185
- audioCodecs.split(',').map((audioCodec) => ({
186
- type: 'media-source',
187
- audio: {
188
- contentType: mimeTypeForCodec(audioCodec, 'audio'),
189
- channels: '' + channelsNumber,
190
- // spatialRendering:
191
- // audioCodec === 'ec-3' && channels.indexOf('JOC'),
192
- },
193
- })),
194
- );
195
- }
196
- }
197
- });
198
- });
199
166
  }
200
167
 
201
168
  return Promise.all(
@@ -203,9 +170,8 @@ export function getMediaDecodingInfoPromise(
203
170
  // Cache MediaCapabilities promises
204
171
  const decodingInfoKey = getMediaDecodingInfoKey(configuration);
205
172
  return (
206
- SUPPORTED_INFO_CACHE[decodingInfoKey] ||
207
- (SUPPORTED_INFO_CACHE[decodingInfoKey] =
208
- mediaCapabilities.decodingInfo(configuration))
173
+ cache[decodingInfoKey] ||
174
+ (cache[decodingInfoKey] = mediaCapabilities.decodingInfo(configuration))
209
175
  );
210
176
  }),
211
177
  )
@@ -222,19 +188,107 @@ export function getMediaDecodingInfoPromise(
222
188
  }));
223
189
  }
224
190
 
191
+ function makeVideoConfigurations(level: Level): VideoConfiguration[] {
192
+ const videoCodecs = level.videoCodec?.split(',');
193
+ const bitrate = getVariantDecodingBitrate(level);
194
+ const width = level.width || 640;
195
+ const height = level.height || 480;
196
+ // Assume a framerate of 30fps since MediaCapabilities will not accept Level default of 0.
197
+ const framerate = level.frameRate || 30;
198
+ const videoRange = level.videoRange.toLowerCase() as 'sdr' | 'pq' | 'hlg';
199
+ return videoCodecs
200
+ ? videoCodecs.map((videoCodec: string) => {
201
+ const videoConfiguration: VideoConfiguration = {
202
+ contentType: mimeTypeForCodec(
203
+ fillInMissingAV01Params(videoCodec),
204
+ 'video',
205
+ ),
206
+ width,
207
+ height,
208
+ bitrate,
209
+ framerate,
210
+ };
211
+ if (videoRange !== 'sdr') {
212
+ videoConfiguration.transferFunction = videoRange as TransferFunction;
213
+ }
214
+ return videoConfiguration;
215
+ })
216
+ : [];
217
+ }
218
+
219
+ function makeAudioConfigurations(
220
+ level: Level,
221
+ audioTracksByGroup: AudioTracksByGroup,
222
+ hasVideo: boolean,
223
+ ): AudioConfiguration[] {
224
+ const audioCodecs = level.audioCodec?.split(',');
225
+ const combinedBitrate = getVariantDecodingBitrate(level);
226
+ if (audioCodecs && level.audioGroups) {
227
+ return level.audioGroups.reduce((configurations, audioGroupId) => {
228
+ const tracks = audioGroupId
229
+ ? audioTracksByGroup.groups[audioGroupId]?.tracks
230
+ : null;
231
+ if (tracks) {
232
+ return tracks.reduce((configs, audioTrack) => {
233
+ if (audioTrack.groupId === audioGroupId) {
234
+ const channelsNumber = parseFloat(audioTrack.channels || '');
235
+ audioCodecs.forEach((audioCodec) => {
236
+ const audioConfiguration: AudioConfiguration = {
237
+ contentType: mimeTypeForCodec(audioCodec, 'audio'),
238
+ bitrate: hasVideo
239
+ ? estimatedAudioBitrate(audioCodec, combinedBitrate)
240
+ : combinedBitrate,
241
+ };
242
+ if (channelsNumber) {
243
+ audioConfiguration.channels = '' + channelsNumber;
244
+ }
245
+ configs.push(audioConfiguration);
246
+ });
247
+ }
248
+ return configs;
249
+ }, configurations);
250
+ }
251
+ return configurations;
252
+ }, [] as AudioConfiguration[]);
253
+ }
254
+ return [];
255
+ }
256
+
257
+ function estimatedAudioBitrate(
258
+ audioCodec: string,
259
+ levelBitrate: number,
260
+ ): number {
261
+ if (levelBitrate <= 1) {
262
+ return 1;
263
+ }
264
+ let audioBitrate = 128000;
265
+ if (audioCodec === 'ec-3') {
266
+ audioBitrate = 768000;
267
+ } else if (audioCodec === 'ac-3') {
268
+ audioBitrate = 640000;
269
+ }
270
+ return Math.min(levelBitrate / 2, audioBitrate); // Don't exceed some % of level bitrate
271
+ }
272
+
273
+ function getVariantDecodingBitrate(level: Level): number {
274
+ return (
275
+ Math.ceil(Math.max(level.bitrate * 0.9, level.averageBitrate) / 1000) *
276
+ 1000 || 1
277
+ );
278
+ }
279
+
225
280
  function getMediaDecodingInfoKey(config: MediaDecodingConfiguration): string {
281
+ let key = '';
226
282
  const { audio, video } = config;
227
- const mediaConfig = video || audio;
228
- if (mediaConfig) {
229
- const codec = getCodecsForMimeType(mediaConfig.contentType);
230
- if (video) {
231
- return `r${video.height}x${video.width}f${Math.ceil(video.framerate)}${
232
- video.transferFunction || 'sd'
233
- }_${codec}_${Math.ceil(video.bitrate / 1e5)}`;
234
- }
235
- if (audio) {
236
- return `c${audio.channels}${audio.spatialRendering ? 's' : 'n'}_${codec}`;
237
- }
283
+ if (video) {
284
+ const codec = getCodecsForMimeType(video.contentType);
285
+ key += `${codec}_r${video.height}x${video.width}f${Math.ceil(video.framerate)}${
286
+ video.transferFunction || 'sd'
287
+ }_${Math.ceil(video.bitrate / 1e5)}`;
288
+ }
289
+ if (audio) {
290
+ const codec = getCodecsForMimeType(audio.contentType);
291
+ key += `${video ? '_' : ''}${codec}_c${audio.channels}`;
238
292
  }
239
- return '';
293
+ return key;
240
294
  }