hls.js 1.5.4 → 1.5.5-0.canary.9978

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.
Files changed (67) hide show
  1. package/README.md +1 -0
  2. package/dist/hls-demo.js +10 -0
  3. package/dist/hls-demo.js.map +1 -1
  4. package/dist/hls.js +1935 -1094
  5. package/dist/hls.js.d.ts +63 -50
  6. package/dist/hls.js.map +1 -1
  7. package/dist/hls.light.js +1059 -838
  8. package/dist/hls.light.js.map +1 -1
  9. package/dist/hls.light.min.js +1 -1
  10. package/dist/hls.light.min.js.map +1 -1
  11. package/dist/hls.light.mjs +846 -626
  12. package/dist/hls.light.mjs.map +1 -1
  13. package/dist/hls.min.js +1 -1
  14. package/dist/hls.min.js.map +1 -1
  15. package/dist/hls.mjs +1640 -814
  16. package/dist/hls.mjs.map +1 -1
  17. package/dist/hls.worker.js +1 -1
  18. package/dist/hls.worker.js.map +1 -1
  19. package/package.json +18 -18
  20. package/src/config.ts +3 -2
  21. package/src/controller/abr-controller.ts +21 -20
  22. package/src/controller/audio-stream-controller.ts +15 -16
  23. package/src/controller/audio-track-controller.ts +1 -1
  24. package/src/controller/base-playlist-controller.ts +7 -7
  25. package/src/controller/base-stream-controller.ts +56 -29
  26. package/src/controller/buffer-controller.ts +11 -11
  27. package/src/controller/cap-level-controller.ts +1 -2
  28. package/src/controller/cmcd-controller.ts +25 -3
  29. package/src/controller/content-steering-controller.ts +8 -6
  30. package/src/controller/eme-controller.ts +9 -22
  31. package/src/controller/error-controller.ts +6 -8
  32. package/src/controller/fps-controller.ts +2 -3
  33. package/src/controller/gap-controller.ts +43 -16
  34. package/src/controller/latency-controller.ts +9 -11
  35. package/src/controller/level-controller.ts +5 -17
  36. package/src/controller/stream-controller.ts +25 -32
  37. package/src/controller/subtitle-stream-controller.ts +13 -14
  38. package/src/controller/subtitle-track-controller.ts +5 -3
  39. package/src/controller/timeline-controller.ts +23 -30
  40. package/src/crypt/aes-crypto.ts +21 -2
  41. package/src/crypt/decrypter-aes-mode.ts +4 -0
  42. package/src/crypt/decrypter.ts +32 -18
  43. package/src/crypt/fast-aes-key.ts +24 -5
  44. package/src/demux/audio/adts.ts +9 -4
  45. package/src/demux/sample-aes.ts +2 -0
  46. package/src/demux/transmuxer-interface.ts +4 -12
  47. package/src/demux/transmuxer-worker.ts +4 -4
  48. package/src/demux/transmuxer.ts +16 -3
  49. package/src/demux/tsdemuxer.ts +71 -37
  50. package/src/demux/video/avc-video-parser.ts +208 -119
  51. package/src/demux/video/base-video-parser.ts +134 -2
  52. package/src/demux/video/exp-golomb.ts +0 -208
  53. package/src/demux/video/hevc-video-parser.ts +746 -0
  54. package/src/events.ts +7 -0
  55. package/src/hls.ts +42 -34
  56. package/src/loader/fragment-loader.ts +9 -2
  57. package/src/loader/key-loader.ts +2 -0
  58. package/src/loader/level-key.ts +10 -9
  59. package/src/remux/mp4-generator.ts +196 -1
  60. package/src/remux/mp4-remuxer.ts +23 -7
  61. package/src/task-loop.ts +5 -2
  62. package/src/types/component-api.ts +2 -0
  63. package/src/types/demuxer.ts +3 -0
  64. package/src/types/events.ts +4 -0
  65. package/src/utils/codecs.ts +33 -4
  66. package/src/utils/encryption-methods-util.ts +21 -0
  67. package/src/utils/logger.ts +53 -24
@@ -49,8 +49,6 @@ export default class StreamController
49
49
  private altAudio: boolean = false;
50
50
  private audioOnly: boolean = false;
51
51
  private fragPlaying: Fragment | null = null;
52
- private onvplaying: EventListener | null = null;
53
- private onvseeked: EventListener | null = null;
54
52
  private fragLastKbps: number = 0;
55
53
  private couldBacktrack: boolean = false;
56
54
  private backtrackFragment: Fragment | null = null;
@@ -66,17 +64,15 @@ export default class StreamController
66
64
  hls,
67
65
  fragmentTracker,
68
66
  keyLoader,
69
- '[stream-controller]',
67
+ 'stream-controller',
70
68
  PlaylistLevelType.MAIN,
71
69
  );
72
- this._registerListeners();
70
+ this.registerListeners();
73
71
  }
74
72
 
75
- private _registerListeners() {
73
+ protected registerListeners() {
74
+ super.registerListeners();
76
75
  const { hls } = this;
77
- hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
78
- hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
79
- hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
80
76
  hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this);
81
77
  hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this);
82
78
  hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
@@ -85,7 +81,6 @@ export default class StreamController
85
81
  this.onFragLoadEmergencyAborted,
86
82
  this,
87
83
  );
88
- hls.on(Events.ERROR, this.onError, this);
89
84
  hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this);
90
85
  hls.on(Events.AUDIO_TRACK_SWITCHED, this.onAudioTrackSwitched, this);
91
86
  hls.on(Events.BUFFER_CREATED, this.onBufferCreated, this);
@@ -94,11 +89,9 @@ export default class StreamController
94
89
  hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
95
90
  }
96
91
 
97
- protected _unregisterListeners() {
92
+ protected unregisterListeners() {
93
+ super.unregisterListeners();
98
94
  const { hls } = this;
99
- hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
100
- hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
101
- hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
102
95
  hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this);
103
96
  hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
104
97
  hls.off(
@@ -106,7 +99,6 @@ export default class StreamController
106
99
  this.onFragLoadEmergencyAborted,
107
100
  this,
108
101
  );
109
- hls.off(Events.ERROR, this.onError, this);
110
102
  hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this);
111
103
  hls.off(Events.AUDIO_TRACK_SWITCHED, this.onAudioTrackSwitched, this);
112
104
  hls.off(Events.BUFFER_CREATED, this.onBufferCreated, this);
@@ -116,7 +108,9 @@ export default class StreamController
116
108
  }
117
109
 
118
110
  protected onHandlerDestroying() {
119
- this._unregisterListeners();
111
+ // @ts-ignore
112
+ this.onMediaPlaying = this.onMediaSeeked = null;
113
+ this.unregisterListeners();
120
114
  super.onHandlerDestroying();
121
115
  }
122
116
 
@@ -238,7 +232,7 @@ export default class StreamController
238
232
  return;
239
233
  }
240
234
 
241
- if (!levels?.[level]) {
235
+ if (!this.buffering || !levels?.[level]) {
242
236
  return;
243
237
  }
244
238
 
@@ -516,10 +510,8 @@ export default class StreamController
516
510
  ) {
517
511
  super.onMediaAttached(event, data);
518
512
  const media = data.media;
519
- this.onvplaying = this.onMediaPlaying.bind(this);
520
- this.onvseeked = this.onMediaSeeked.bind(this);
521
- media.addEventListener('playing', this.onvplaying as EventListener);
522
- media.addEventListener('seeked', this.onvseeked as EventListener);
513
+ media.addEventListener('playing', this.onMediaPlaying);
514
+ media.addEventListener('seeked', this.onMediaSeeked);
523
515
  this.gapController = new GapController(
524
516
  this.config,
525
517
  media,
@@ -530,10 +522,9 @@ export default class StreamController
530
522
 
531
523
  protected onMediaDetaching() {
532
524
  const { media } = this;
533
- if (media && this.onvplaying && this.onvseeked) {
534
- media.removeEventListener('playing', this.onvplaying);
535
- media.removeEventListener('seeked', this.onvseeked);
536
- this.onvplaying = this.onvseeked = null;
525
+ if (media) {
526
+ media.removeEventListener('playing', this.onMediaPlaying);
527
+ media.removeEventListener('seeked', this.onMediaSeeked);
537
528
  this.videoBuffer = null;
538
529
  }
539
530
  this.fragPlaying = null;
@@ -544,12 +535,12 @@ export default class StreamController
544
535
  super.onMediaDetaching();
545
536
  }
546
537
 
547
- private onMediaPlaying() {
538
+ private onMediaPlaying = () => {
548
539
  // tick to speed up FRAG_CHANGED triggering
549
540
  this.tick();
550
- }
541
+ };
551
542
 
552
- private onMediaSeeked() {
543
+ private onMediaSeeked = () => {
553
544
  const media = this.media;
554
545
  const currentTime = media ? media.currentTime : null;
555
546
  if (Number.isFinite(currentTime)) {
@@ -569,9 +560,9 @@ export default class StreamController
569
560
 
570
561
  // tick to speed up FRAG_CHANGED triggering
571
562
  this.tick();
572
- }
563
+ };
573
564
 
574
- private onManifestLoading() {
565
+ protected onManifestLoading() {
575
566
  // reset buffer on manifest loading
576
567
  this.log('Trigger BUFFER_RESET');
577
568
  this.hls.trigger(Events.BUFFER_RESET, undefined);
@@ -884,7 +875,7 @@ export default class StreamController
884
875
  this.fragBufferedComplete(frag, part);
885
876
  }
886
877
 
887
- private onError(event: Events.ERROR, data: ErrorData) {
878
+ protected onError(event: Events.ERROR, data: ErrorData) {
888
879
  if (data.fatal) {
889
880
  this.state = State.ERROR;
890
881
  return;
@@ -942,8 +933,10 @@ export default class StreamController
942
933
 
943
934
  if (this.loadedmetadata || !BufferHelper.getBuffered(media).length) {
944
935
  // Resolve gaps using the main buffer, whose ranges are the intersections of the A/V sourcebuffers
945
- const activeFrag = this.state !== State.IDLE ? this.fragCurrent : null;
946
- gapController.poll(this.lastCurrentTime, activeFrag);
936
+ const state = this.state;
937
+ const activeFrag = state !== State.IDLE ? this.fragCurrent : null;
938
+ const levelDetails = this.getLevelDetails();
939
+ gapController.poll(this.lastCurrentTime, activeFrag, levelDetails, state);
947
940
  }
948
941
 
949
942
  this.lastCurrentTime = media.currentTime;
@@ -9,6 +9,10 @@ import { PlaylistLevelType } from '../types/loader';
9
9
  import { Level } from '../types/level';
10
10
  import { subtitleOptionsIdentical } from '../utils/media-option-attributes';
11
11
  import { ErrorDetails, ErrorTypes } from '../errors';
12
+ import {
13
+ isFullSegmentEncryption,
14
+ getAesModeFromFullSegmentMethod,
15
+ } from '../utils/encryption-methods-util';
12
16
  import type { NetworkComponentAPI } from '../types/component-api';
13
17
  import type Hls from '../hls';
14
18
  import type { FragmentTracker } from './fragment-tracker';
@@ -51,25 +55,22 @@ export class SubtitleStreamController
51
55
  hls,
52
56
  fragmentTracker,
53
57
  keyLoader,
54
- '[subtitle-stream-controller]',
58
+ 'subtitle-stream-controller',
55
59
  PlaylistLevelType.SUBTITLE,
56
60
  );
57
- this._registerListeners();
61
+ this.registerListeners();
58
62
  }
59
63
 
60
64
  protected onHandlerDestroying() {
61
- this._unregisterListeners();
65
+ this.unregisterListeners();
62
66
  super.onHandlerDestroying();
63
67
  this.mainDetails = null;
64
68
  }
65
69
 
66
- private _registerListeners() {
70
+ protected registerListeners() {
71
+ super.registerListeners();
67
72
  const { hls } = this;
68
- hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
69
- hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
70
- hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
71
73
  hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
72
- hls.on(Events.ERROR, this.onError, this);
73
74
  hls.on(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this);
74
75
  hls.on(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this);
75
76
  hls.on(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this);
@@ -78,13 +79,10 @@ export class SubtitleStreamController
78
79
  hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
79
80
  }
80
81
 
81
- private _unregisterListeners() {
82
+ protected unregisterListeners() {
83
+ super.unregisterListeners();
82
84
  const { hls } = this;
83
- hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
84
- hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
85
- hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
86
85
  hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
87
- hls.off(Events.ERROR, this.onError, this);
88
86
  hls.off(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this);
89
87
  hls.off(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this);
90
88
  hls.off(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this);
@@ -360,7 +358,7 @@ export class SubtitleStreamController
360
358
  payload.byteLength > 0 &&
361
359
  decryptData?.key &&
362
360
  decryptData.iv &&
363
- decryptData.method === 'AES-128'
361
+ isFullSegmentEncryption(decryptData.method)
364
362
  ) {
365
363
  const startTime = performance.now();
366
364
  // decrypt the subtitles
@@ -369,6 +367,7 @@ export class SubtitleStreamController
369
367
  new Uint8Array(payload),
370
368
  decryptData.key.buffer,
371
369
  decryptData.iv.buffer,
370
+ getAesModeFromFullSegmentMethod(decryptData.method),
372
371
  )
373
372
  .catch((err) => {
374
373
  hls.trigger(Events.ERROR, {
@@ -35,13 +35,14 @@ class SubtitleTrackController extends BasePlaylistController {
35
35
  private currentTrack: MediaPlaylist | null = null;
36
36
  private selectDefaultTrack: boolean = true;
37
37
  private queuedDefaultTrack: number = -1;
38
- private asyncPollTrackChange: () => void = () => this.pollTrackChange(0);
39
38
  private useTextTrackPolling: boolean = false;
40
39
  private subtitlePollingInterval: number = -1;
41
40
  private _subtitleDisplay: boolean = true;
42
41
 
42
+ private asyncPollTrackChange = () => this.pollTrackChange(0);
43
+
43
44
  constructor(hls: Hls) {
44
- super(hls, '[subtitle-track-controller]');
45
+ super(hls, 'subtitle-track-controller');
45
46
  this.registerListeners();
46
47
  }
47
48
 
@@ -50,7 +51,8 @@ class SubtitleTrackController extends BasePlaylistController {
50
51
  this.tracks.length = 0;
51
52
  this.tracksInGroup.length = 0;
52
53
  this.currentTrack = null;
53
- this.onTextTracksChanged = this.asyncPollTrackChange = null as any;
54
+ // @ts-ignore
55
+ this.onTextTracksChanged = this.asyncPollTrackChange = null;
54
56
  super.destroy();
55
57
  }
56
58
 
@@ -28,7 +28,6 @@ import type {
28
28
  BufferFlushingData,
29
29
  FragLoadingData,
30
30
  } from '../types/events';
31
- import { logger } from '../utils/logger';
32
31
  import type Hls from '../hls';
33
32
  import type { ComponentAPI } from '../types/component-api';
34
33
  import type { HlsConfig } from '../config';
@@ -136,17 +135,12 @@ export class TimelineController implements ComponentAPI {
136
135
  }
137
136
 
138
137
  private initCea608Parsers() {
139
- if (
140
- this.config.enableCEA708Captions &&
141
- (!this.cea608Parser1 || !this.cea608Parser2)
142
- ) {
143
- const channel1 = new OutputFilter(this, 'textTrack1');
144
- const channel2 = new OutputFilter(this, 'textTrack2');
145
- const channel3 = new OutputFilter(this, 'textTrack3');
146
- const channel4 = new OutputFilter(this, 'textTrack4');
147
- this.cea608Parser1 = new Cea608Parser(1, channel1, channel2);
148
- this.cea608Parser2 = new Cea608Parser(3, channel3, channel4);
149
- }
138
+ const channel1 = new OutputFilter(this, 'textTrack1');
139
+ const channel2 = new OutputFilter(this, 'textTrack2');
140
+ const channel3 = new OutputFilter(this, 'textTrack3');
141
+ const channel4 = new OutputFilter(this, 'textTrack4');
142
+ this.cea608Parser1 = new Cea608Parser(1, channel1, channel2);
143
+ this.cea608Parser2 = new Cea608Parser(3, channel3, channel4);
150
144
  }
151
145
 
152
146
  public addCues(
@@ -410,7 +404,7 @@ export class TimelineController implements ComponentAPI {
410
404
  .filter((t) => t !== null)
411
405
  .map((t) => (t as TextTrack).label);
412
406
  if (unusedTextTracks.length) {
413
- logger.warn(
407
+ this.hls.logger.warn(
414
408
  `Media element contains unused subtitle tracks: ${unusedTextTracks.join(
415
409
  ', ',
416
410
  )}. Replace media element for each source to clear TextTracks and captions menu.`,
@@ -468,21 +462,19 @@ export class TimelineController implements ComponentAPI {
468
462
  }
469
463
 
470
464
  private onFragLoading(event: Events.FRAG_LOADING, data: FragLoadingData) {
471
- this.initCea608Parsers();
472
- const { cea608Parser1, cea608Parser2, lastCc, lastSn, lastPartIndex } =
473
- this;
474
- if (!this.enabled || !cea608Parser1 || !cea608Parser2) {
475
- return;
476
- }
477
465
  // if this frag isn't contiguous, clear the parser so cues with bad start/end times aren't added to the textTrack
478
- if (data.frag.type === PlaylistLevelType.MAIN) {
466
+ if (this.enabled && data.frag.type === PlaylistLevelType.MAIN) {
467
+ const { cea608Parser1, cea608Parser2, lastSn } = this;
468
+ if (!cea608Parser1 || !cea608Parser2) {
469
+ return;
470
+ }
479
471
  const { cc, sn } = data.frag;
480
- const partIndex = data?.part?.index ?? -1;
472
+ const partIndex = data.part?.index ?? -1;
481
473
  if (
482
474
  !(
483
475
  sn === lastSn + 1 ||
484
- (sn === lastSn && partIndex === lastPartIndex + 1) ||
485
- cc === lastCc
476
+ (sn === lastSn && partIndex === this.lastPartIndex + 1) ||
477
+ cc === this.lastCc
486
478
  )
487
479
  ) {
488
480
  cea608Parser1.reset();
@@ -550,7 +542,7 @@ export class TimelineController implements ComponentAPI {
550
542
  });
551
543
  },
552
544
  (error) => {
553
- logger.log(`Failed to parse IMSC1: ${error}`);
545
+ hls.logger.log(`Failed to parse IMSC1: ${error}`);
554
546
  hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, {
555
547
  success: false,
556
548
  frag: frag,
@@ -597,7 +589,7 @@ export class TimelineController implements ComponentAPI {
597
589
  this._fallbackToIMSC1(frag, payload);
598
590
  }
599
591
  // Something went wrong while parsing. Trigger event with success false.
600
- logger.log(`Failed to parse VTT cue: ${error}`);
592
+ hls.logger.log(`Failed to parse VTT cue: ${error}`);
601
593
  if (missingInitPTS && maxAvCC > frag.cc) {
602
594
  return;
603
595
  }
@@ -669,9 +661,7 @@ export class TimelineController implements ComponentAPI {
669
661
  event: Events.FRAG_PARSING_USERDATA,
670
662
  data: FragParsingUserdataData,
671
663
  ) {
672
- this.initCea608Parsers();
673
- const { cea608Parser1, cea608Parser2 } = this;
674
- if (!this.enabled || !cea608Parser1 || !cea608Parser2) {
664
+ if (!this.enabled || !this.config.enableCEA708Captions) {
675
665
  return;
676
666
  }
677
667
  const { frag, samples } = data;
@@ -686,9 +676,12 @@ export class TimelineController implements ComponentAPI {
686
676
  for (let i = 0; i < samples.length; i++) {
687
677
  const ccBytes = samples[i].bytes;
688
678
  if (ccBytes) {
679
+ if (!this.cea608Parser1) {
680
+ this.initCea608Parsers();
681
+ }
689
682
  const ccdatas = this.extractCea608Data(ccBytes);
690
- cea608Parser1.addData(samples[i].pts, ccdatas[0]);
691
- cea608Parser2.addData(samples[i].pts, ccdatas[1]);
683
+ this.cea608Parser1!.addData(samples[i].pts, ccdatas[0]);
684
+ this.cea608Parser2!.addData(samples[i].pts, ccdatas[1]);
692
685
  }
693
686
  }
694
687
  }
@@ -1,13 +1,32 @@
1
+ import { DecrypterAesMode } from './decrypter-aes-mode';
2
+
1
3
  export default class AESCrypto {
2
4
  private subtle: SubtleCrypto;
3
5
  private aesIV: Uint8Array;
6
+ private aesMode: DecrypterAesMode;
4
7
 
5
- constructor(subtle: SubtleCrypto, iv: Uint8Array) {
8
+ constructor(subtle: SubtleCrypto, iv: Uint8Array, aesMode: DecrypterAesMode) {
6
9
  this.subtle = subtle;
7
10
  this.aesIV = iv;
11
+ this.aesMode = aesMode;
8
12
  }
9
13
 
10
14
  decrypt(data: ArrayBuffer, key: CryptoKey) {
11
- return this.subtle.decrypt({ name: 'AES-CBC', iv: this.aesIV }, key, data);
15
+ switch (this.aesMode) {
16
+ case DecrypterAesMode.cbc:
17
+ return this.subtle.decrypt(
18
+ { name: 'AES-CBC', iv: this.aesIV },
19
+ key,
20
+ data,
21
+ );
22
+ case DecrypterAesMode.ctr:
23
+ return this.subtle.decrypt(
24
+ { name: 'AES-CTR', counter: this.aesIV, length: 64 }, //64 : NIST SP800-38A standard suggests that the counter should occupy half of the counter block
25
+ key,
26
+ data,
27
+ );
28
+ default:
29
+ throw new Error(`[AESCrypto] invalid aes mode ${this.aesMode}`);
30
+ }
12
31
  }
13
32
  }
@@ -0,0 +1,4 @@
1
+ export const enum DecrypterAesMode {
2
+ cbc = 0,
3
+ ctr = 1,
4
+ }
@@ -4,6 +4,7 @@ import AESDecryptor, { removePadding } from './aes-decryptor';
4
4
  import { logger } from '../utils/logger';
5
5
  import { appendUint8Array } from '../utils/mp4-tools';
6
6
  import { sliceUint8 } from '../utils/typed-array';
7
+ import { DecrypterAesMode } from './decrypter-aes-mode';
7
8
  import type { HlsConfig } from '../config';
8
9
 
9
10
  const CHUNK_SIZE = 16; // 16 bytes, 128 bits
@@ -19,9 +20,10 @@ export default class Decrypter {
19
20
  private currentIV: ArrayBuffer | null = null;
20
21
  private currentResult: ArrayBuffer | null = null;
21
22
  private useSoftware: boolean;
23
+ private enableSoftwareAES: boolean;
22
24
 
23
25
  constructor(config: HlsConfig, { removePKCS7Padding = true } = {}) {
24
- this.useSoftware = config.enableSoftwareAES;
26
+ this.enableSoftwareAES = config.enableSoftwareAES;
25
27
  this.removePKCS7Padding = removePKCS7Padding;
26
28
  // built in decryptor expects PKCS7 padding
27
29
  if (removePKCS7Padding) {
@@ -36,9 +38,7 @@ export default class Decrypter {
36
38
  /* no-op */
37
39
  }
38
40
  }
39
- if (this.subtle === null) {
40
- this.useSoftware = true;
41
- }
41
+ this.useSoftware = this.subtle === null;
42
42
  }
43
43
 
44
44
  destroy() {
@@ -82,10 +82,11 @@ export default class Decrypter {
82
82
  data: Uint8Array | ArrayBuffer,
83
83
  key: ArrayBuffer,
84
84
  iv: ArrayBuffer,
85
+ aesMode: DecrypterAesMode,
85
86
  ): Promise<ArrayBuffer> {
86
87
  if (this.useSoftware) {
87
88
  return new Promise((resolve, reject) => {
88
- this.softwareDecrypt(new Uint8Array(data), key, iv);
89
+ this.softwareDecrypt(new Uint8Array(data), key, iv, aesMode);
89
90
  const decryptResult = this.flush();
90
91
  if (decryptResult) {
91
92
  resolve(decryptResult.buffer);
@@ -94,7 +95,7 @@ export default class Decrypter {
94
95
  }
95
96
  });
96
97
  }
97
- return this.webCryptoDecrypt(new Uint8Array(data), key, iv);
98
+ return this.webCryptoDecrypt(new Uint8Array(data), key, iv, aesMode);
98
99
  }
99
100
 
100
101
  // Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached
@@ -103,8 +104,13 @@ export default class Decrypter {
103
104
  data: Uint8Array,
104
105
  key: ArrayBuffer,
105
106
  iv: ArrayBuffer,
107
+ aesMode: DecrypterAesMode,
106
108
  ): ArrayBuffer | null {
107
109
  const { currentIV, currentResult, remainderData } = this;
110
+ if (aesMode !== DecrypterAesMode.cbc || key.byteLength !== 16) {
111
+ logger.warn('SoftwareDecrypt: can only handle AES-128-CBC');
112
+ return null;
113
+ }
108
114
  this.logOnce('JS AES decrypt');
109
115
  // The output is staggered during progressive parsing - the current result is cached, and emitted on the next call
110
116
  // This is done in order to strip PKCS7 padding, which is found at the end of each segment. We only know we've reached
@@ -147,21 +153,22 @@ export default class Decrypter {
147
153
  data: Uint8Array,
148
154
  key: ArrayBuffer,
149
155
  iv: ArrayBuffer,
156
+ aesMode: DecrypterAesMode,
150
157
  ): Promise<ArrayBuffer> {
151
158
  const subtle = this.subtle;
152
159
  if (this.key !== key || !this.fastAesKey) {
153
160
  this.key = key;
154
- this.fastAesKey = new FastAESKey(subtle, key);
161
+ this.fastAesKey = new FastAESKey(subtle, key, aesMode);
155
162
  }
156
163
  return this.fastAesKey
157
164
  .expandKey()
158
- .then((aesKey) => {
165
+ .then((aesKey: CryptoKey) => {
159
166
  // decrypt using web crypto
160
167
  if (!subtle) {
161
168
  return Promise.reject(new Error('web crypto not initialized'));
162
169
  }
163
170
  this.logOnce('WebCrypto AES decrypt');
164
- const crypto = new AESCrypto(subtle, new Uint8Array(iv));
171
+ const crypto = new AESCrypto(subtle, new Uint8Array(iv), aesMode);
165
172
  return crypto.decrypt(data.buffer, aesKey);
166
173
  })
167
174
  .catch((err) => {
@@ -169,19 +176,26 @@ export default class Decrypter {
169
176
  `[decrypter]: WebCrypto Error, disable WebCrypto API, ${err.name}: ${err.message}`,
170
177
  );
171
178
 
172
- return this.onWebCryptoError(data, key, iv);
179
+ return this.onWebCryptoError(data, key, iv, aesMode);
173
180
  });
174
181
  }
175
182
 
176
- private onWebCryptoError(data, key, iv): ArrayBuffer | never {
177
- this.useSoftware = true;
178
- this.logEnabled = true;
179
- this.softwareDecrypt(data, key, iv);
180
- const decryptResult = this.flush();
181
- if (decryptResult) {
182
- return decryptResult.buffer;
183
+ private onWebCryptoError(data, key, iv, aesMode): ArrayBuffer | never {
184
+ const enableSoftwareAES = this.enableSoftwareAES;
185
+ if (enableSoftwareAES) {
186
+ this.useSoftware = true;
187
+ this.logEnabled = true;
188
+ this.softwareDecrypt(data, key, iv, aesMode);
189
+ const decryptResult = this.flush();
190
+ if (decryptResult) {
191
+ return decryptResult.buffer;
192
+ }
183
193
  }
184
- throw new Error('WebCrypto and softwareDecrypt: failed to decrypt data');
194
+ throw new Error(
195
+ 'WebCrypto' +
196
+ (enableSoftwareAES ? ' and softwareDecrypt' : '') +
197
+ ': failed to decrypt data',
198
+ );
185
199
  }
186
200
 
187
201
  private getValidChunk(data: Uint8Array): Uint8Array {
@@ -1,16 +1,35 @@
1
+ import { DecrypterAesMode } from './decrypter-aes-mode';
2
+
1
3
  export default class FastAESKey {
2
4
  private subtle: any;
3
5
  private key: ArrayBuffer;
6
+ private aesMode: DecrypterAesMode;
4
7
 
5
- constructor(subtle, key) {
8
+ constructor(subtle, key, aesMode: DecrypterAesMode) {
6
9
  this.subtle = subtle;
7
10
  this.key = key;
11
+ this.aesMode = aesMode;
8
12
  }
9
13
 
10
14
  expandKey() {
11
- return this.subtle.importKey('raw', this.key, { name: 'AES-CBC' }, false, [
12
- 'encrypt',
13
- 'decrypt',
14
- ]);
15
+ const subtleAlgoName = getSubtleAlgoName(this.aesMode);
16
+ return this.subtle.importKey(
17
+ 'raw',
18
+ this.key,
19
+ { name: subtleAlgoName },
20
+ false,
21
+ ['encrypt', 'decrypt'],
22
+ );
23
+ }
24
+ }
25
+
26
+ function getSubtleAlgoName(aesMode: DecrypterAesMode) {
27
+ switch (aesMode) {
28
+ case DecrypterAesMode.cbc:
29
+ return 'AES-CBC';
30
+ case DecrypterAesMode.ctr:
31
+ return 'AES-CTR';
32
+ default:
33
+ throw new Error(`[FastAESKey] invalid aes mode ${aesMode}`);
15
34
  }
16
35
  }
@@ -17,6 +17,7 @@ type AudioConfig = {
17
17
  samplerate: number;
18
18
  channelCount: number;
19
19
  codec: string;
20
+ parsedCodec: string;
20
21
  manifestCodec: string;
21
22
  };
22
23
 
@@ -32,6 +33,7 @@ export function getAudioConfig(
32
33
  audioCodec: string,
33
34
  ): AudioConfig | void {
34
35
  let adtsObjectType: number;
36
+ let originalAdtsObjectType: number;
35
37
  let adtsExtensionSamplingIndex: number;
36
38
  let adtsChannelConfig: number;
37
39
  let config: number[];
@@ -42,7 +44,8 @@ export function getAudioConfig(
42
44
  8000, 7350,
43
45
  ];
44
46
  // byte 2
45
- adtsObjectType = ((data[offset + 2] & 0xc0) >>> 6) + 1;
47
+ adtsObjectType = originalAdtsObjectType =
48
+ ((data[offset + 2] & 0xc0) >>> 6) + 1;
46
49
  const adtsSamplingIndex = (data[offset + 2] & 0x3c) >>> 2;
47
50
  if (adtsSamplingIndex > adtsSamplingRates.length - 1) {
48
51
  const error = new Error(`invalid ADTS sampling index:${adtsSamplingIndex}`);
@@ -61,8 +64,8 @@ export function getAudioConfig(
61
64
  logger.log(
62
65
  `manifest codec:${audioCodec}, ADTS type:${adtsObjectType}, samplingIndex:${adtsSamplingIndex}`,
63
66
  );
64
- // firefox: freq less than 24kHz = AAC SBR (HE-AAC)
65
- if (/firefox/i.test(userAgent)) {
67
+ // Firefox and Pale Moon: freq less than 24kHz = AAC SBR (HE-AAC)
68
+ if (/firefox|palemoon/i.test(userAgent)) {
66
69
  if (adtsSamplingIndex >= 6) {
67
70
  adtsObjectType = 5;
68
71
  config = new Array(4);
@@ -167,6 +170,7 @@ export function getAudioConfig(
167
170
  samplerate: adtsSamplingRates[adtsSamplingIndex],
168
171
  channelCount: adtsChannelConfig,
169
172
  codec: 'mp4a.40.' + adtsObjectType,
173
+ parsedCodec: 'mp4a.40.' + originalAdtsObjectType,
170
174
  manifestCodec,
171
175
  };
172
176
  }
@@ -244,8 +248,9 @@ export function initTrackConfig(
244
248
  track.channelCount = config.channelCount;
245
249
  track.codec = config.codec;
246
250
  track.manifestCodec = config.manifestCodec;
251
+ track.parsedCodec = config.parsedCodec;
247
252
  logger.log(
248
- `parsed codec:${track.codec}, rate:${config.samplerate}, channels:${config.channelCount}`,
253
+ `parsed codec:${track.parsedCodec}, codec:${track.codec}, rate:${config.samplerate}, channels:${config.channelCount}`,
249
254
  );
250
255
  }
251
256
  }
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { HlsConfig } from '../config';
6
6
  import Decrypter from '../crypt/decrypter';
7
+ import { DecrypterAesMode } from '../crypt/decrypter-aes-mode';
7
8
  import { HlsEventEmitter } from '../events';
8
9
  import type {
9
10
  AudioSample,
@@ -30,6 +31,7 @@ class SampleAesDecrypter {
30
31
  encryptedData,
31
32
  this.keyData.key.buffer,
32
33
  this.keyData.iv.buffer,
34
+ DecrypterAesMode.cbc,
33
35
  );
34
36
  }
35
37