hls.js 1.6.0-beta.4.0.canary.11066 → 1.6.0-beta.4.0.canary.11069

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
@@ -74,7 +74,7 @@
74
74
  "@babel/preset-env": "7.26.9",
75
75
  "@babel/preset-typescript": "7.26.0",
76
76
  "@babel/register": "7.25.9",
77
- "@microsoft/api-documenter": "7.26.16",
77
+ "@microsoft/api-documenter": "7.26.17",
78
78
  "@microsoft/api-extractor": "7.52.1",
79
79
  "@rollup/plugin-alias": "5.1.1",
80
80
  "@rollup/plugin-babel": "6.0.4",
@@ -134,5 +134,5 @@
134
134
  "url-toolkit": "2.2.5",
135
135
  "wrangler": "3.114.1"
136
136
  },
137
- "version": "1.6.0-beta.4.0.canary.11066"
137
+ "version": "1.6.0-beta.4.0.canary.11069"
138
138
  }
@@ -6,6 +6,7 @@ import { ElementaryStreamTypes } from '../loader/fragment';
6
6
  import { PlaylistLevelType } from '../types/loader';
7
7
  import { BufferHelper } from '../utils/buffer-helper';
8
8
  import {
9
+ areCodecsMediaSourceSupported,
9
10
  getCodecCompatibleName,
10
11
  pickMostCompleteCodecName,
11
12
  } from '../utils/codecs';
@@ -607,7 +608,8 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
607
608
  }
608
609
  trackNames.forEach((trackName: SourceBufferName) => {
609
610
  const parsedTrack = data[trackName] as ParsedTrack;
610
- const { id, codec, levelCodec, container, metadata } = parsedTrack;
611
+ const { id, codec, levelCodec, container, metadata, supplemental } =
612
+ parsedTrack;
611
613
  let track = tracks[trackName];
612
614
  const transferredTrack = this.transferData?.tracks?.[trackName];
613
615
  const sbTrack = transferredTrack?.buffer ? transferredTrack : track;
@@ -618,6 +620,7 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
618
620
  buffer: undefined,
619
621
  listeners: [],
620
622
  codec,
623
+ supplemental,
621
624
  container,
622
625
  levelCodec,
623
626
  metadata,
@@ -1350,6 +1353,7 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
1350
1353
  buffer,
1351
1354
  container: track.container,
1352
1355
  codec: track.codec,
1356
+ supplemental: track.supplemental,
1353
1357
  levelCodec: track.levelCodec,
1354
1358
  id: track.id,
1355
1359
  metadata: track.metadata,
@@ -1430,6 +1434,15 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
1430
1434
  }
1431
1435
 
1432
1436
  private getTrackCodec(track: BaseTrack, trackName: SourceBufferName): string {
1437
+ // Use supplemental video codec when supported when adding SourceBuffer (#5558)
1438
+ const supplementalCodec = track.supplemental;
1439
+ if (
1440
+ supplementalCodec &&
1441
+ trackName === 'video' &&
1442
+ areCodecsMediaSourceSupported(supplementalCodec, trackName)
1443
+ ) {
1444
+ return supplementalCodec;
1445
+ }
1433
1446
  const codec = pickMostCompleteCodecName(track.codec, track.levelCodec);
1434
1447
  if (codec) {
1435
1448
  if (trackName.slice(0, 5) === 'audio') {
@@ -1451,6 +1464,7 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
1451
1464
  codec,
1452
1465
  container: track.container,
1453
1466
  levelCodec: track.levelCodec,
1467
+ supplemental: track.supplemental,
1454
1468
  metadata: track.metadata,
1455
1469
  id: track.id,
1456
1470
  listeners: [],
@@ -12,6 +12,7 @@ import {
12
12
  videoCodecPreferenceValue,
13
13
  } from '../utils/codecs';
14
14
  import { reassignFragmentLevelIndexes } from '../utils/level-helper';
15
+ import { getUnsupportedResult } from '../utils/mediacapabilities-helper';
15
16
  import { stringify } from '../utils/safe-json-stringify';
16
17
  import type ContentSteeringController from './content-steering-controller';
17
18
  import type Hls from '../hls';
@@ -163,6 +164,7 @@ export default class LevelController extends BasePlaylistController {
163
164
  (audioCodec && !this.isAudioSupported(audioCodec)) ||
164
165
  (videoCodec && !this.isVideoSupported(videoCodec))
165
166
  ) {
167
+ this.log(`Some or all CODECS not supported "${attributes.CODECS}"`);
166
168
  return;
167
169
  }
168
170
 
@@ -178,7 +180,7 @@ export default class LevelController extends BasePlaylistController {
178
180
  const levelKey = `${contentSteeringPrefix}${levelParsed.bitrate}-${RESOLUTION}-${FRAMERATE}-${CODECS}-${VIDEO_RANGE}-${HDCP}`;
179
181
 
180
182
  if (!redundantSet[levelKey]) {
181
- const level = new Level(levelParsed);
183
+ const level = this.createLevel(levelParsed);
182
184
  redundantSet[levelKey] = level;
183
185
  generatePathwaySet[levelKey] = 1;
184
186
  levels.push(level);
@@ -190,7 +192,7 @@ export default class LevelController extends BasePlaylistController {
190
192
  // Content Steering controller to handles Pathway fallback on error
191
193
  const pathwayCount = (generatePathwaySet[levelKey] += 1);
192
194
  levelParsed.attrs['PATHWAY-ID'] = new Array(pathwayCount + 1).join('.');
193
- const level = new Level(levelParsed);
195
+ const level = this.createLevel(levelParsed);
194
196
  redundantSet[levelKey] = level;
195
197
  levels.push(level);
196
198
  } else {
@@ -208,6 +210,22 @@ export default class LevelController extends BasePlaylistController {
208
210
  );
209
211
  }
210
212
 
213
+ private createLevel(levelParsed: LevelParsed): Level {
214
+ const level = new Level(levelParsed);
215
+ const supplemental = levelParsed.supplemental;
216
+ if (
217
+ supplemental?.videoCodec &&
218
+ !this.isVideoSupported(supplemental.videoCodec)
219
+ ) {
220
+ const error = new Error(
221
+ `SUPPLEMENTAL-CODECS not supported "${supplemental.videoCodec}"`,
222
+ );
223
+ this.log(error.message);
224
+ level.supportedResult = getUnsupportedResult(error, []);
225
+ }
226
+ return level;
227
+ }
228
+
211
229
  private isAudioSupported(codec: string): boolean {
212
230
  return areCodecsMediaSourceSupported(
213
231
  codec,
@@ -247,23 +265,27 @@ export default class LevelController extends BasePlaylistController {
247
265
  // Dispatch error after MANIFEST_LOADED is done propagating
248
266
  Promise.resolve().then(() => {
249
267
  if (this.hls) {
268
+ let message = 'no level with compatible codecs found in manifest';
269
+ let reason = message;
250
270
  if (data.levels.length) {
251
- this.warn(
252
- `One or more CODECS in variant not supported: ${stringify(
253
- data.levels[0].attrs,
254
- )}`,
255
- );
271
+ reason = `one or more CODECS in variant not supported: ${stringify(
272
+ data.levels
273
+ .map((level) => level.attrs.CODECS)
274
+ .filter(
275
+ (value, index, array) => array.indexOf(value) === index,
276
+ ),
277
+ )}`;
278
+ this.warn(reason);
279
+ message += ` (${reason})`;
256
280
  }
257
- const error = new Error(
258
- 'no level with compatible codecs found in manifest',
259
- );
281
+ const error = new Error(message);
260
282
  this.hls.trigger(Events.ERROR, {
261
283
  type: ErrorTypes.MEDIA_ERROR,
262
284
  details: ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR,
263
285
  fatal: true,
264
286
  url: data.url,
265
287
  error,
266
- reason: error.message,
288
+ reason,
267
289
  });
268
290
  }
269
291
  });
@@ -1453,7 +1453,7 @@ export default class StreamController
1453
1453
  video.container
1454
1454
  }, codecs[level/parsed]=[${currentLevel.videoCodec || ''}/${
1455
1455
  parsedVideoCodec
1456
- }${video.codec !== parsedVideoCodec ? ' parsed-corrected=' + video.codec : ''}}]`,
1456
+ }]${video.codec !== parsedVideoCodec ? ' parsed-corrected=' + video.codec : ''}${video.supplemental ? ' supplemental=' + video.supplemental : ''}`,
1457
1457
  );
1458
1458
  delete tracks.audiovideo;
1459
1459
  }
@@ -71,10 +71,11 @@ class MP4Demuxer implements Demuxer {
71
71
  const initData = parseInitSegment(initSegment);
72
72
 
73
73
  if (initData.video) {
74
- const { id, timescale, codec } = initData.video;
74
+ const { id, timescale, codec, supplemental } = initData.video;
75
75
  videoTrack.id = id;
76
76
  videoTrack.timescale = captionTrack.timescale = timescale;
77
77
  videoTrack.codec = codec;
78
+ videoTrack.supplemental = supplemental;
78
79
  }
79
80
 
80
81
  if (initData.audio) {
package/src/hls.ts CHANGED
@@ -1477,6 +1477,7 @@ export type { Bufferable } from './utils/buffer-helper';
1477
1477
  export type { CaptionScreen } from './utils/cea-608-parser';
1478
1478
  export type { CuesInterface } from './utils/cues';
1479
1479
  export type {
1480
+ CodecsParsed,
1480
1481
  HdcpLevels,
1481
1482
  HlsSkip,
1482
1483
  HlsUrlParameters,
@@ -14,7 +14,12 @@ import {
14
14
  } from '../utils/variable-substitution';
15
15
  import type { MediaFragment } from './fragment';
16
16
  import type { ContentSteeringOptions } from '../types/events';
17
- import type { LevelAttributes, LevelParsed, VariableMap } from '../types/level';
17
+ import type {
18
+ CodecsParsed,
19
+ LevelAttributes,
20
+ LevelParsed,
21
+ VariableMap,
22
+ } from '../types/level';
18
23
  import type { PlaylistLevelType } from '../types/loader';
19
24
  import type { MediaAttributes, MediaPlaylist } from '../types/media-playlist';
20
25
  import type { CodecType } from '../utils/codecs';
@@ -138,6 +143,11 @@ export default class M3U8Parser {
138
143
  }
139
144
 
140
145
  setCodecs(attrs.CODECS, level);
146
+ const supplementalCodecs = attrs['SUPPLEMENTAL-CODECS'];
147
+ if (supplementalCodecs) {
148
+ level.supplemental = {};
149
+ setCodecs(supplementalCodecs, level.supplemental);
150
+ }
141
151
 
142
152
  if (!level.unknownCodecs?.length) {
143
153
  levelsWithKnownCodecs.push(level);
@@ -822,14 +832,14 @@ function parseStartTimeOffset(startAttributes: string): number | null {
822
832
 
823
833
  function setCodecs(
824
834
  codecsAttributeValue: string | undefined,
825
- level: LevelParsed,
835
+ level: CodecsParsed,
826
836
  ) {
827
837
  let codecs = (codecsAttributeValue || '').split(/[ ,]+/).filter((c) => c);
828
838
  ['video', 'audio', 'text'].forEach((type: CodecType) => {
829
839
  const filtered = codecs.filter((codec) => isCodecType(codec, type));
830
840
  if (filtered.length) {
831
841
  // Comma separated list of all codecs for type
832
- level[`${type}Codec`] = filtered.join(',');
842
+ level[`${type}Codec`] = filtered.map((c) => c.split('/')[0]).join(',');
833
843
  // Remove known codecs so that only unknownCodecs are left after iterating through each type
834
844
  codecs = codecs.filter((codec) => filtered.indexOf(codec) === -1);
835
845
  }
@@ -117,6 +117,7 @@ class PassThroughRemuxer implements Remuxer {
117
117
  tracks.video = {
118
118
  container: 'video/mp4',
119
119
  codec: videoCodec,
120
+ supplemental: initData.video.supplemental,
120
121
  initSegment,
121
122
  id: 'main',
122
123
  };
@@ -20,6 +20,7 @@ export interface BaseTrack {
20
20
  id: 'audio' | 'main';
21
21
  container: string;
22
22
  codec?: string;
23
+ supplemental?: string;
23
24
  levelCodec?: string;
24
25
  pendingCodec?: string;
25
26
  metadata?: {
@@ -57,6 +57,7 @@ export interface PassthroughTrack extends DemuxedTrack {
57
57
  timescale: number;
58
58
  duration: number;
59
59
  codec: string;
60
+ supplemental: string | undefined;
60
61
  }
61
62
  export interface DemuxedAudioTrack extends DemuxedTrack {
62
63
  type: 'audio';
@@ -3,21 +3,25 @@ import type { LevelDetails } from '../loader/level-details';
3
3
  import type { AttrList } from '../utils/attr-list';
4
4
  import type { MediaDecodingInfo } from '../utils/mediacapabilities-helper';
5
5
 
6
- export interface LevelParsed {
6
+ export interface LevelParsed extends CodecsParsed {
7
7
  attrs: LevelAttributes;
8
- audioCodec?: string;
9
8
  bitrate: number;
10
9
  details?: LevelDetails;
11
10
  height?: number;
12
11
  id?: number;
13
12
  name: string;
14
- textCodec?: string;
15
- unknownCodecs?: string[];
13
+ supplemental?: CodecsParsed;
16
14
  url: string;
17
- videoCodec?: string;
18
15
  width?: number;
19
16
  }
20
17
 
18
+ export interface CodecsParsed {
19
+ audioCodec?: string;
20
+ videoCodec?: string;
21
+ textCodec?: string;
22
+ unknownCodecs?: string[];
23
+ }
24
+
21
25
  export interface LevelAttributes extends AttrList {
22
26
  'ALLOWED-CPC'?: string;
23
27
  AUDIO?: string;
@@ -110,6 +114,7 @@ export class Level {
110
114
  public readonly height: number;
111
115
  public readonly id: number;
112
116
  public readonly name: string;
117
+ public readonly supplemental: CodecsParsed | undefined;
113
118
  public readonly videoCodec: string | undefined;
114
119
  public readonly width: number;
115
120
  public details?: LevelDetails;
@@ -144,6 +149,13 @@ export class Level {
144
149
  .filter((c) => !!c)
145
150
  .map((s: string) => s.substring(0, 4))
146
151
  .join(',');
152
+ if ('supplemental' in data) {
153
+ this.supplemental = data.supplemental;
154
+ const supplementalVideo = data.supplemental?.videoCodec;
155
+ if (supplementalVideo && supplementalVideo !== data.videoCodec) {
156
+ this.codecSet += `,${supplementalVideo.substring(0, 4)}`;
157
+ }
158
+ }
147
159
  this.addGroupId('audio', data.attrs.AUDIO);
148
160
  this.addGroupId('text', data.attrs.SUBTITLES);
149
161
  }
@@ -36,7 +36,7 @@ export class AttrList {
36
36
  return intValue;
37
37
  }
38
38
 
39
- hexadecimalInteger(attrName: string): Uint8Array | null {
39
+ hexadecimalInteger(attrName: string) {
40
40
  if (this[attrName]) {
41
41
  let stringValue = (this[attrName] || '0x').slice(2);
42
42
  stringValue = (stringValue.length & 1 ? '0' : '') + stringValue;
@@ -53,6 +53,7 @@ export const sampleEntryCodesISO = {
53
53
  avc4: 1,
54
54
  avcp: 1,
55
55
  av01: 0.8,
56
+ dav1: 0.8,
56
57
  drac: 1,
57
58
  dva1: 1,
58
59
  dvav: 1,
@@ -30,6 +30,24 @@ export const SUPPORTED_INFO_DEFAULT: MediaDecodingInfo = {
30
30
  ],
31
31
  } as const;
32
32
 
33
+ export function getUnsupportedResult(
34
+ error: Error,
35
+ configurations: MediaDecodingConfiguration[],
36
+ ): MediaDecodingInfo {
37
+ return {
38
+ supported: false,
39
+ configurations,
40
+ decodingInfoResults: [
41
+ {
42
+ supported: false,
43
+ smooth: false,
44
+ powerEfficient: false,
45
+ },
46
+ ],
47
+ error,
48
+ };
49
+ }
50
+
33
51
  export const SUPPORTED_INFO_CACHE: Record<
34
52
  string,
35
53
  Promise<MediaCapabilitiesDecodingInfo>
@@ -128,20 +146,14 @@ export function getMediaDecodingInfoPromise(
128
146
  videoCodecsArray.some((videoCodec) => isHEVC(videoCodec)) &&
129
147
  userAgentHevcSupportIsInaccurate()
130
148
  ) {
131
- return Promise.resolve({
132
- supported: false,
133
- configurations,
134
- decodingInfoResults: [
135
- {
136
- supported: false,
137
- smooth: false,
138
- powerEfficient: false,
139
- },
140
- ],
141
- error: new Error(
142
- `Overriding Windows Firefox HEVC MediaCapabilities result based on user-agent sting: (${ua})`,
149
+ return Promise.resolve(
150
+ getUnsupportedResult(
151
+ new Error(
152
+ `Overriding Windows Firefox HEVC MediaCapabilities result based on user-agent sting: (${ua})`,
153
+ ),
154
+ configurations,
143
155
  ),
144
- });
156
+ );
145
157
  }
146
158
  configurations.push.apply(
147
159
  configurations,
@@ -223,6 +223,7 @@ export interface InitDataTrack {
223
223
  timescale: number;
224
224
  id: number;
225
225
  codec: string;
226
+ supplemental: string | undefined;
226
227
  }
227
228
 
228
229
  type HdlrType = ElementaryStreamTypes.AUDIO | ElementaryStreamTypes.VIDEO;
@@ -290,11 +291,16 @@ export function parseInitSegment(initSegment: Uint8Array): InitData {
290
291
  return result;
291
292
  }
292
293
 
293
- function parseStsd(stsd: Uint8Array): { codec: string; encrypted: boolean } {
294
+ function parseStsd(stsd: Uint8Array): {
295
+ codec: string;
296
+ encrypted: boolean;
297
+ supplemental: string | undefined;
298
+ } {
294
299
  const sampleEntries = stsd.subarray(8);
295
300
  const sampleEntriesEnd = sampleEntries.subarray(8 + 78);
296
301
  const fourCC = bin2str(sampleEntries.subarray(4, 8));
297
302
  let codec = fourCC;
303
+ let supplemental;
298
304
  const encrypted = fourCC === 'enca' || fourCC === 'encv';
299
305
  if (encrypted) {
300
306
  const encBox = findBox(sampleEntries, [fourCC])[0];
@@ -314,6 +320,7 @@ function parseStsd(stsd: Uint8Array): { codec: string; encrypted: boolean } {
314
320
  }
315
321
  });
316
322
  }
323
+ const codecFourCC = codec;
317
324
  switch (codec) {
318
325
  case 'avc1':
319
326
  case 'avc2':
@@ -322,6 +329,10 @@ function parseStsd(stsd: Uint8Array): { codec: string; encrypted: boolean } {
322
329
  // extract profile + compatibility + level out of avcC box
323
330
  const avcCBox = findBox(sampleEntriesEnd, ['avcC'])[0];
324
331
  codec += '.' + toHex(avcCBox[1]) + toHex(avcCBox[2]) + toHex(avcCBox[3]);
332
+ supplemental = parseSupplementalDoViCodec(
333
+ codecFourCC === 'avc1' ? 'dva1' : 'dvav',
334
+ sampleEntriesEnd,
335
+ );
325
336
  break;
326
337
  }
327
338
  case 'mp4a': {
@@ -371,34 +382,41 @@ function parseStsd(stsd: Uint8Array): { codec: string; encrypted: boolean } {
371
382
  }
372
383
  case 'hvc1':
373
384
  case 'hev1': {
374
- const hvcCBox = findBox(sampleEntriesEnd, ['hvcC'])[0];
375
- const profileByte = hvcCBox[1];
376
- const profileSpace = ['', 'A', 'B', 'C'][profileByte >> 6];
377
- const generalProfileIdc = profileByte & 0x1f;
378
- const profileCompat = readUint32(hvcCBox, 2);
379
- const tierFlag = (profileByte & 0x20) >> 5 ? 'H' : 'L';
380
- const levelIDC = hvcCBox[12];
381
- const constraintIndicator = hvcCBox.subarray(6, 12);
382
- codec += '.' + profileSpace + generalProfileIdc;
383
- codec += '.' + profileCompat.toString(16).toUpperCase();
384
- codec += '.' + tierFlag + levelIDC;
385
- let constraintString = '';
386
- for (let i = constraintIndicator.length; i--; ) {
387
- const byte = constraintIndicator[i];
388
- if (byte || constraintString) {
389
- const encodedByte = byte.toString(16).toUpperCase();
390
- constraintString = '.' + encodedByte + constraintString;
385
+ const hvcCBoxes = findBox(sampleEntriesEnd, ['hvcC']);
386
+ if (hvcCBoxes) {
387
+ const hvcCBox = hvcCBoxes[0];
388
+ const profileByte = hvcCBox[1];
389
+ const profileSpace = ['', 'A', 'B', 'C'][profileByte >> 6];
390
+ const generalProfileIdc = profileByte & 0x1f;
391
+ const profileCompat = readUint32(hvcCBox, 2);
392
+ const tierFlag = (profileByte & 0x20) >> 5 ? 'H' : 'L';
393
+ const levelIDC = hvcCBox[12];
394
+ const constraintIndicator = hvcCBox.subarray(6, 12);
395
+ codec += '.' + profileSpace + generalProfileIdc;
396
+ codec += '.' + profileCompat.toString(16).toUpperCase();
397
+ codec += '.' + tierFlag + levelIDC;
398
+ let constraintString = '';
399
+ for (let i = constraintIndicator.length; i--; ) {
400
+ const byte = constraintIndicator[i];
401
+ if (byte || constraintString) {
402
+ const encodedByte = byte.toString(16).toUpperCase();
403
+ constraintString = '.' + encodedByte + constraintString;
404
+ }
391
405
  }
406
+ codec += constraintString;
392
407
  }
393
- codec += constraintString;
408
+ supplemental = parseSupplementalDoViCodec(
409
+ codecFourCC == 'hev1' ? 'dvhe' : 'dvh1',
410
+ sampleEntriesEnd,
411
+ );
394
412
  break;
395
413
  }
396
414
  case 'dvh1':
397
- case 'dvhe': {
398
- const dvcCBox = findBox(sampleEntriesEnd, ['dvcC'])[0];
399
- const profile = (dvcCBox[2] >> 1) & 0x7f;
400
- const level = ((dvcCBox[2] << 5) & 0x20) | ((dvcCBox[3] >> 3) & 0x1f);
401
- codec += '.' + addLeadingZero(profile) + '.' + addLeadingZero(level);
415
+ case 'dvhe':
416
+ case 'dvav':
417
+ case 'dva1':
418
+ case 'dav1': {
419
+ codec = parseSupplementalDoViCodec(codec, sampleEntriesEnd) || codec;
402
420
  break;
403
421
  }
404
422
  case 'vp09': {
@@ -463,6 +481,7 @@ function parseStsd(stsd: Uint8Array): { codec: string; encrypted: boolean } {
463
481
  addLeadingZero(matrixCoefficients) +
464
482
  '.' +
465
483
  videoFullRangeFlag;
484
+ supplemental = parseSupplementalDoViCodec('dav1', sampleEntriesEnd);
466
485
  break;
467
486
  }
468
487
  case 'ac-3':
@@ -473,7 +492,28 @@ function parseStsd(stsd: Uint8Array): { codec: string; encrypted: boolean } {
473
492
  default:
474
493
  break;
475
494
  }
476
- return { codec, encrypted };
495
+ return { codec, encrypted, supplemental };
496
+ }
497
+
498
+ function parseSupplementalDoViCodec(
499
+ fourCC: string,
500
+ sampleEntriesEnd: Uint8Array,
501
+ ): string | undefined {
502
+ const dvvCResult = findBox(sampleEntriesEnd, ['dvvC']); // used by DoVi Profile 8 to 10
503
+ const dvXCBox = dvvCResult.length
504
+ ? dvvCResult[0]
505
+ : findBox(sampleEntriesEnd, ['dvcC'])[0]; // used by DoVi Profiles up to 7 and 20
506
+ if (dvXCBox) {
507
+ const doViProfile = (dvXCBox[2] >> 1) & 0x7f;
508
+ const doViLevel = ((dvXCBox[2] << 5) & 0x20) | ((dvXCBox[3] >> 3) & 0x1f);
509
+ return (
510
+ fourCC +
511
+ '.' +
512
+ addLeadingZero(doViProfile) +
513
+ '.' +
514
+ addLeadingZero(doViLevel)
515
+ );
516
+ }
477
517
  }
478
518
 
479
519
  function skipBERInteger(bytes: Uint8Array, i: number): number {