shaka-player 4.1.3 → 4.2.0

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 (59) hide show
  1. package/CHANGELOG.md +33 -34
  2. package/demo/common/assets.js +25 -0
  3. package/demo/common/message_ids.js +2 -0
  4. package/demo/config.js +5 -1
  5. package/demo/index.html +1 -1
  6. package/demo/locales/en.json +2 -0
  7. package/demo/locales/source.json +8 -0
  8. package/dist/demo.compiled.debug.js +25 -23
  9. package/dist/demo.compiled.debug.map +3 -3
  10. package/dist/demo.compiled.js +25 -23
  11. package/dist/demo.compiled.map +3 -3
  12. package/dist/deps.js +3 -2
  13. package/dist/locales.js +31 -31
  14. package/dist/receiver.compiled.debug.js +24 -22
  15. package/dist/receiver.compiled.debug.map +3 -3
  16. package/dist/receiver.compiled.js +24 -22
  17. package/dist/receiver.compiled.map +3 -3
  18. package/dist/shaka-player.compiled.d.ts +57 -4
  19. package/dist/shaka-player.compiled.debug.d.ts +57 -4
  20. package/dist/shaka-player.compiled.debug.externs.js +62 -4
  21. package/dist/shaka-player.compiled.debug.js +285 -241
  22. package/dist/shaka-player.compiled.debug.map +4 -4
  23. package/dist/shaka-player.compiled.externs.js +62 -4
  24. package/dist/shaka-player.compiled.js +815 -798
  25. package/dist/shaka-player.compiled.map +4 -4
  26. package/dist/shaka-player.ui.d.ts +57 -4
  27. package/dist/shaka-player.ui.debug.d.ts +57 -4
  28. package/dist/shaka-player.ui.debug.externs.js +62 -4
  29. package/dist/shaka-player.ui.debug.js +283 -239
  30. package/dist/shaka-player.ui.debug.map +4 -4
  31. package/dist/shaka-player.ui.externs.js +62 -4
  32. package/dist/shaka-player.ui.js +979 -960
  33. package/dist/shaka-player.ui.map +4 -4
  34. package/docs/tutorials/fairplay.md +149 -0
  35. package/docs/tutorials/ui.md +6 -6
  36. package/externs/shaka/abr_manager.js +8 -0
  37. package/externs/shaka/manifest.js +43 -0
  38. package/externs/shaka/player.js +13 -2
  39. package/lib/abr/simple_abr_manager.js +72 -4
  40. package/lib/hls/hls_parser.js +117 -25
  41. package/lib/media/drm_engine.js +0 -2
  42. package/lib/media/segment_index.js +8 -0
  43. package/lib/media/segment_reference.js +7 -1
  44. package/lib/media/streaming_engine.js +38 -2
  45. package/lib/net/networking_engine.js +1 -0
  46. package/lib/player.js +8 -1
  47. package/lib/polyfill/patchedmediakeys_apple.js +729 -0
  48. package/lib/text/vtt_text_parser.js +61 -4
  49. package/lib/util/error.js +21 -1
  50. package/lib/util/fairplay_utils.js +100 -2
  51. package/lib/util/platform.js +12 -1
  52. package/lib/util/player_configuration.js +3 -10
  53. package/package.json +1 -1
  54. package/shaka-player.uncompiled.js +1 -0
  55. package/test/hls/hls_parser_unit.js +49 -33
  56. package/test/media/streaming_engine_unit.js +79 -5
  57. package/test/test/util/simple_fakes.js +4 -0
  58. package/test/test/util/streaming_engine_util.js +10 -2
  59. package/test/text/vtt_text_parser_unit.js +26 -0
@@ -412,6 +412,7 @@ shaka.text.VttTextParser = class {
412
412
  VttTextParser.addDefaultTextColor_(styles);
413
413
  }
414
414
  payload = VttTextParser.replaceColorPayload_(payload);
415
+ payload = VttTextParser.replaceKaraokeStylePayload_(payload);
415
416
  const xmlPayload = '<span>' + payload + '</span>';
416
417
  const element = shaka.util.XmlUtils.parseXmlString(xmlPayload, 'span');
417
418
  if (element) {
@@ -437,6 +438,44 @@ shaka.text.VttTextParser = class {
437
438
  }
438
439
  }
439
440
 
441
+ /**
442
+ * Converts karaoke style tag to be valid for xml parsing
443
+ * For example,
444
+ * input: Text <00:00:00.450> time <00:00:01.450> 1
445
+ * output: Text <div time="00:00:00.450"> time
446
+ * <div time="00:00:01.450"> 1</div></div>
447
+ *
448
+ * @param {string} payload
449
+ * @return {string} processed payload
450
+ * @private
451
+ */
452
+ static replaceKaraokeStylePayload_(payload) {
453
+ const names = [];
454
+ let nameStart = -1;
455
+ for (let i = 0; i < payload.length; i++) {
456
+ if (payload[i] === '<') {
457
+ nameStart = i + 1;
458
+ } else if (payload[i] === '>') {
459
+ if (nameStart > 0) {
460
+ const name = payload.substr(nameStart, i - nameStart);
461
+ if (name.match(shaka.text.VttTextParser.timeFormat_)) {
462
+ names.push(name);
463
+ }
464
+ nameStart = -1;
465
+ }
466
+ }
467
+ }
468
+ let newPayload = payload;
469
+ for (const name of names) {
470
+ const replaceTag = '<' + name + '>';
471
+ const startTag = '<div time="' + name + '">';
472
+ const endTag = '</div>';
473
+ newPayload = newPayload.replace(replaceTag, startTag);
474
+ newPayload += endTag;
475
+ }
476
+ return newPayload;
477
+ }
478
+
440
479
  /**
441
480
  * Converts color end tag to be valid for xml parsing
442
481
  * For example,
@@ -560,6 +599,18 @@ shaka.text.VttTextParser = class {
560
599
  case 'u':
561
600
  nestedCue.textDecoration.push(underline);
562
601
  break;
602
+ case 'div': {
603
+ const time = /** @type {!Element} */(element).getAttribute('time');
604
+ if (!time) {
605
+ break;
606
+ }
607
+ const parser = new shaka.util.TextParser(time);
608
+ const cueTime = shaka.text.VttTextParser.parseTime_(parser);
609
+ if (cueTime) {
610
+ nestedCue.startTime = cueTime;
611
+ }
612
+ break;
613
+ }
563
614
  default:
564
615
  break;
565
616
  }
@@ -776,10 +827,7 @@ shaka.text.VttTextParser = class {
776
827
  * @private
777
828
  */
778
829
  static parseTime_(parser) {
779
- // 00:00.000 or 00:00:00.000 or 0:00:00.000 or
780
- // 00:00.00 or 00:00:00.00 or 0:00:00.00
781
- const regexExpresion = /(?:(\d{1,}):)?(\d{2}):(\d{2})\.(\d{2,3})/g;
782
- const results = parser.readRegex(regexExpresion);
830
+ const results = parser.readRegex(shaka.text.VttTextParser.timeFormat_);
783
831
  if (results == null) {
784
832
  return null;
785
833
  }
@@ -810,6 +858,15 @@ shaka.text.VttTextParser.MPEG_TIMESCALE_ = 90000;
810
858
  */
811
859
  shaka.text.VttTextParser.TS_ROLLOVER_ = 0x200000000;
812
860
 
861
+ /**
862
+ * @const
863
+ * @private {!RegExp}
864
+ * @example 00:00.000 or 00:00:00.000 or 0:00:00.000 or
865
+ * 00:00.00 or 00:00:00.00 or 0:00:00.00
866
+ */
867
+ shaka.text.VttTextParser.timeFormat_ =
868
+ /(?:(\d{1,}):)?(\d{2}):(\d{2})\.(\d{2,3})/g;
869
+
813
870
  shaka.text.TextEngine.registerParser(
814
871
  'text/vtt', () => new shaka.text.VttTextParser());
815
872
 
package/lib/util/error.js CHANGED
@@ -693,7 +693,27 @@ shaka.util.Error.Code = {
693
693
  */
694
694
  'HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED': 4040,
695
695
 
696
- // RETIRED: 'HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED': 4041,
696
+ /**
697
+ * We do not support playing encrypted content (different than mp2t) with MSE
698
+ * and legacy Apple MediaKeys API.
699
+ */
700
+ 'HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED': 4041,
701
+
702
+ /**
703
+ * Web Crypto API is not available (to decrypt AES-128 streams). Web Crypto
704
+ * only exists in secure origins like https.
705
+ */
706
+ 'NO_WEB_CRYPTO_API': 4042,
707
+
708
+ /**
709
+ * AES-128 iv length should be 16 bytes.
710
+ */
711
+ 'HLS_AES_128_INVALID_IV_LENGTH': 4043,
712
+
713
+ /**
714
+ * AES-128 encryption key length should be 16 bytes.
715
+ */
716
+ 'HLS_AES_128_INVALID_KEY_LENGTH': 4044,
697
717
 
698
718
 
699
719
  // RETIRED: 'INCONSISTENT_BUFFER_STATE': 5000,
@@ -133,13 +133,83 @@ shaka.util.FairPlayUtils = class {
133
133
  }
134
134
 
135
135
  /**
136
- * SPC FairPlay request.
136
+ * Verimatrix initDataTransform configuration.
137
+ *
138
+ * @param {!Uint8Array} initData
139
+ * @param {string} initDataType
140
+ * @param {?shaka.extern.DrmInfo} drmInfo
141
+ * @export
142
+ */
143
+ static verimatrixInitDataTransform(initData, initDataType, drmInfo) {
144
+ if (initDataType !== 'skd') {
145
+ return initData;
146
+ }
147
+ const StringUtils = shaka.util.StringUtils;
148
+ const FairPlayUtils = shaka.util.FairPlayUtils;
149
+ const cert = drmInfo.serverCertificate;
150
+ const initDataAsString = StringUtils.fromBytesAutoDetect(initData);
151
+ const contentId = initDataAsString.split('skd://').pop();
152
+ return FairPlayUtils.initDataTransform(initData, contentId, cert);
153
+ }
154
+
155
+ /**
156
+ * EZDRM initDataTransform configuration.
157
+ *
158
+ * @param {!Uint8Array} initData
159
+ * @param {string} initDataType
160
+ * @param {?shaka.extern.DrmInfo} drmInfo
161
+ * @export
162
+ */
163
+ static ezdrmInitDataTransform(initData, initDataType, drmInfo) {
164
+ if (initDataType !== 'skd') {
165
+ return initData;
166
+ }
167
+ const StringUtils = shaka.util.StringUtils;
168
+ const FairPlayUtils = shaka.util.FairPlayUtils;
169
+ const cert = drmInfo.serverCertificate;
170
+ const initDataAsString = StringUtils.fromBytesAutoDetect(initData);
171
+ const contentId = initDataAsString.split(';').pop();
172
+ return FairPlayUtils.initDataTransform(initData, contentId, cert);
173
+ }
174
+
175
+ /**
176
+ * Conax initDataTransform configuration.
177
+ *
178
+ * @param {!Uint8Array} initData
179
+ * @param {string} initDataType
180
+ * @param {?shaka.extern.DrmInfo} drmInfo
181
+ * @export
182
+ */
183
+ static conaxInitDataTransform(initData, initDataType, drmInfo) {
184
+ if (initDataType !== 'skd') {
185
+ return initData;
186
+ }
187
+ const StringUtils = shaka.util.StringUtils;
188
+ const FairPlayUtils = shaka.util.FairPlayUtils;
189
+ const cert = drmInfo.serverCertificate;
190
+ const initDataAsString = StringUtils.fromBytesAutoDetect(initData);
191
+ const skdValue = initDataAsString.split('skd://').pop().split('?').shift();
192
+ const stringToArray = (string) => {
193
+ // 2 bytes for each char
194
+ const buffer = new ArrayBuffer(string.length * 2);
195
+ const array = new Uint16Array(buffer);
196
+ for (let i = 0, strLen = string.length; i < strLen; i++) {
197
+ array[i] = string.charCodeAt(i);
198
+ }
199
+ return array;
200
+ };
201
+ const contentId = stringToArray(window.atob(skdValue));
202
+ return FairPlayUtils.initDataTransform(initData, contentId, cert);
203
+ }
204
+
205
+ /**
206
+ * Verimatrix FairPlay request.
137
207
  *
138
208
  * @param {shaka.net.NetworkingEngine.RequestType} type
139
209
  * @param {shaka.extern.Request} request
140
210
  * @export
141
211
  */
142
- static spcFairPlayRequest(type, request) {
212
+ static verimatrixFairPlayRequest(type, request) {
143
213
  if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
144
214
  return;
145
215
  }
@@ -150,6 +220,34 @@ shaka.util.FairPlayUtils = class {
150
220
  request.body = shaka.util.StringUtils.toUTF8('spc=' + base64Payload);
151
221
  }
152
222
 
223
+ /**
224
+ * EZDRM FairPlay request.
225
+ *
226
+ * @param {shaka.net.NetworkingEngine.RequestType} type
227
+ * @param {shaka.extern.Request} request
228
+ * @export
229
+ */
230
+ static ezdrmFairPlayRequest(type, request) {
231
+ if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
232
+ return;
233
+ }
234
+ request.headers['Content-Type'] = 'application/octet-stream';
235
+ }
236
+
237
+ /**
238
+ * Conax FairPlay request.
239
+ *
240
+ * @param {shaka.net.NetworkingEngine.RequestType} type
241
+ * @param {shaka.extern.Request} request
242
+ * @export
243
+ */
244
+ static conaxFairPlayRequest(type, request) {
245
+ if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
246
+ return;
247
+ }
248
+ request.headers['Content-Type'] = 'application/octet-stream';
249
+ }
250
+
153
251
  /**
154
252
  * Common FairPlay response transform for some DRMs providers.
155
253
  *
@@ -199,7 +199,8 @@ shaka.util.Platform = class {
199
199
  return !!navigator.vendor && navigator.vendor.includes('Apple') &&
200
200
  !shaka.util.Platform.isTizen() &&
201
201
  !shaka.util.Platform.isEOS() &&
202
- !shaka.util.Platform.isPS4();
202
+ !shaka.util.Platform.isPS4() &&
203
+ !shaka.util.Platform.isAmazonFireTV();
203
204
  }
204
205
 
205
206
  /**
@@ -222,6 +223,16 @@ shaka.util.Platform = class {
222
223
  return shaka.util.Platform.userAgentContains_('PlayStation 4');
223
224
  }
224
225
 
226
+ /**
227
+ * Check if the current platform is Amazon Fire TV.
228
+ * https://developer.amazon.com/docs/fire-tv/identify-amazon-fire-tv-devices.html
229
+ *
230
+ * @return {boolean}
231
+ */
232
+ static isAmazonFireTV() {
233
+ return shaka.util.Platform.userAgentContains_('AFT');
234
+ }
235
+
225
236
  /**
226
237
  * Returns a major version number for Safari, or Safari-based iOS browsers.
227
238
  *
@@ -147,7 +147,7 @@ shaka.util.PlayerConfiguration = class {
147
147
  ignoreTextStreamFailures: false,
148
148
  alwaysStreamText: false,
149
149
  startAtSegmentBoundary: false,
150
- gapDetectionThreshold: 0.1,
150
+ gapDetectionThreshold: 0.5,
151
151
  durationBackoff: 1,
152
152
  forceTransmuxTS: false,
153
153
  // Offset by 5 seconds since Chromecast takes a few seconds to start
@@ -173,15 +173,6 @@ shaka.util.PlayerConfiguration = class {
173
173
  maxDisabledTime: 30,
174
174
  };
175
175
 
176
- // Some browsers will stop earlier than others before a gap (e.g., Edge
177
- // stops 0.5 seconds before a gap). So for some browsers we need to use a
178
- // larger threshold. See: https://bit.ly/2K5xmJO
179
- if (shaka.util.Platform.isLegacyEdge() ||
180
- shaka.util.Platform.isTizen() ||
181
- shaka.util.Platform.isChromecast()) {
182
- streaming.gapDetectionThreshold = 0.5;
183
- }
184
-
185
176
  // WebOS, Tizen, and Chromecast have long hardware pipelines that respond
186
177
  // slowly to seeking. Therefore we should not seek when we detect a stall
187
178
  // on one of these platforms. Instead, default stallSkip to 0 to force the
@@ -253,6 +244,8 @@ shaka.util.PlayerConfiguration = class {
253
244
  fastHalfLife: 2,
254
245
  slowHalfLife: 5,
255
246
  },
247
+ restrictToElementSize: false,
248
+ ignoreDevicePixelRatio: false,
256
249
  };
257
250
 
258
251
  const cmcd = {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shaka-player",
3
3
  "description": "DASH/EME video player library",
4
- "version": "4.1.3",
4
+ "version": "4.2.0",
5
5
  "homepage": "https://github.com/shaka-project/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
@@ -38,6 +38,7 @@ goog.require('shaka.polyfill.Fullscreen');
38
38
  goog.require('shaka.polyfill.MediaSource');
39
39
  goog.require('shaka.polyfill.MediaCapabilities');
40
40
  goog.require('shaka.polyfill.Orientation');
41
+ goog.require('shaka.polyfill.PatchedMediaKeysApple');
41
42
  goog.require('shaka.polyfill.PatchedMediaKeysNop');
42
43
  goog.require('shaka.polyfill.PatchedMediaKeysWebkit');
43
44
  goog.require('shaka.polyfill.PiPWebkit');
@@ -33,6 +33,8 @@ describe('HlsParser', () => {
33
33
  let segmentData;
34
34
  /** @type {!Uint8Array} */
35
35
  let selfInitializingSegmentData;
36
+ /** @type {!Uint8Array} */
37
+ let aes128Key;
36
38
 
37
39
  afterEach(() => {
38
40
  shaka.log.alwaysWarn = originalAlwaysWarn;
@@ -75,6 +77,11 @@ describe('HlsParser', () => {
75
77
  selfInitializingSegmentData =
76
78
  shaka.util.Uint8ArrayUtils.concat(initSegmentData, segmentData);
77
79
 
80
+ aes128Key = new Uint8Array([
81
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
82
+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
83
+ ]);
84
+
78
85
  fakeNetEngine = new shaka.test.FakeNetworkingEngine();
79
86
 
80
87
  config = shaka.util.PlayerConfiguration.createDefault().manifest;
@@ -2432,7 +2439,7 @@ describe('HlsParser', () => {
2432
2439
  expect(initSegments[1].getUris()[0]).toBe('test:/init2.mp4');
2433
2440
  });
2434
2441
 
2435
- it('drops variants encrypted with AES-128', async () => {
2442
+ it('parses variants encrypted with AES-128', async () => {
2436
2443
  const master = [
2437
2444
  '#EXTM3U\n',
2438
2445
  '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
@@ -2441,10 +2448,15 @@ describe('HlsParser', () => {
2441
2448
  '#EXT-X-STREAM-INF:BANDWIDTH=300,CODECS="avc1,mp4a",',
2442
2449
  'RESOLUTION=960x540,FRAME-RATE=120,AUDIO="aud2"\n',
2443
2450
  'video2\n',
2451
+ '#EXT-X-STREAM-INF:BANDWIDTH=300,CODECS="avc1,mp4a",',
2452
+ 'RESOLUTION=960x540,FRAME-RATE=120,AUDIO="aud3"\n',
2453
+ 'video3\n',
2444
2454
  '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
2445
2455
  'URI="audio"\n',
2446
2456
  '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud2",LANGUAGE="fr",',
2447
2457
  'URI="audio2"\n',
2458
+ '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud3",LANGUAGE="de",',
2459
+ 'URI="audio3"\n',
2448
2460
  ].join('');
2449
2461
 
2450
2462
  const media = [
@@ -2456,17 +2468,26 @@ describe('HlsParser', () => {
2456
2468
  'main.mp4',
2457
2469
  ].join('');
2458
2470
 
2459
- const mediaWithAesEncryption = [
2471
+ const mediaWithMp4AesEncryption = [
2460
2472
  '#EXTM3U\n',
2461
2473
  '#EXT-X-PLAYLIST-TYPE:VOD\n',
2462
2474
  '#EXT-X-KEY:METHOD=AES-128,',
2463
- 'URI="800k.key\n',
2475
+ 'URI="800k.key"\n',
2464
2476
  '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
2465
2477
  '#EXTINF:5,\n',
2466
2478
  '#EXT-X-BYTERANGE:121090@616\n',
2467
2479
  'main.mp4',
2468
2480
  ].join('');
2469
2481
 
2482
+ const mediaWithTSAesEncryption = [
2483
+ '#EXTM3U\n',
2484
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
2485
+ '#EXT-X-KEY:METHOD=AES-128,',
2486
+ 'URI="800k.key"\n',
2487
+ '#EXTINF:5,\n',
2488
+ 'main.ts',
2489
+ ].join('');
2490
+
2470
2491
  const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
2471
2492
  manifest.anyTimeline();
2472
2493
  manifest.addPartialVariant((variant) => {
@@ -2478,6 +2499,24 @@ describe('HlsParser', () => {
2478
2499
  stream.language = 'en';
2479
2500
  });
2480
2501
  });
2502
+ manifest.addPartialVariant((variant) => {
2503
+ variant.bandwidth = 300;
2504
+ variant.addPartialStream(ContentType.VIDEO, (stream) => {
2505
+ stream.size(960, 540);
2506
+ });
2507
+ variant.addPartialStream(ContentType.AUDIO, (stream) => {
2508
+ stream.language = 'fr';
2509
+ });
2510
+ });
2511
+ manifest.addPartialVariant((variant) => {
2512
+ variant.bandwidth = 300;
2513
+ variant.addPartialStream(ContentType.VIDEO, (stream) => {
2514
+ stream.size(960, 540);
2515
+ });
2516
+ variant.addPartialStream(ContentType.AUDIO, (stream) => {
2517
+ stream.language = 'de';
2518
+ });
2519
+ });
2481
2520
  manifest.sequenceMode = true;
2482
2521
  });
2483
2522
 
@@ -2485,12 +2524,15 @@ describe('HlsParser', () => {
2485
2524
  .setResponseText('test:/master', master)
2486
2525
  .setResponseText('test:/audio', media)
2487
2526
  .setResponseText('test:/audio2', media)
2527
+ .setResponseText('test:/audio3', media)
2488
2528
  .setResponseText('test:/video', media)
2489
- .setResponseText('test:/video2', mediaWithAesEncryption)
2529
+ .setResponseText('test:/video2', mediaWithMp4AesEncryption)
2530
+ .setResponseText('test:/video3', mediaWithTSAesEncryption)
2490
2531
  .setResponseText('test:/main.vtt', vttText)
2491
2532
  .setResponseValue('test:/init.mp4', initSegmentData)
2492
2533
  .setResponseValue('test:/main.mp4', segmentData)
2493
2534
  .setResponseValue('test:/main.test', segmentData)
2535
+ .setResponseValue('test:/800k.key', aes128Key)
2494
2536
  .setResponseValue('test:/selfInit.mp4', selfInitializingSegmentData);
2495
2537
 
2496
2538
  const actual = await parser.start('test:/master', playerInterface);
@@ -2674,7 +2716,9 @@ describe('HlsParser', () => {
2674
2716
  .setResponseText('test:/video', media)
2675
2717
  .setResponseValue('test:/main.exe', segmentData)
2676
2718
  .setResponseValue('test:/init.mp4', initSegmentData)
2677
- .setResponseValue('test:/main.mp4', segmentData);
2719
+ .setResponseValue('test:/main.mp4', segmentData)
2720
+ .setResponseValue('data:text/plain;base64,AAECAwQFBgcICQoLDA0ODw==',
2721
+ aes128Key);
2678
2722
 
2679
2723
  await expectAsync(parser.start('test:/master', playerInterface))
2680
2724
  .toBeRejectedWith(Util.jasmineError(error));
@@ -2708,34 +2752,6 @@ describe('HlsParser', () => {
2708
2752
  await verifyError(master, media, error);
2709
2753
  });
2710
2754
 
2711
- it('if all variants are encrypted with AES-128', async () => {
2712
- const master = [
2713
- '#EXTM3U\n',
2714
- '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
2715
- 'RESOLUTION=960x540,FRAME-RATE=60\n',
2716
- 'video\n',
2717
- ].join('');
2718
-
2719
- const media = [
2720
- '#EXTM3U\n',
2721
- '#EXT-X-TARGETDURATION:6\n',
2722
- '#EXT-X-PLAYLIST-TYPE:VOD\n',
2723
- '#EXT-X-KEY:METHOD=AES-128,',
2724
- 'URI="data:text/plain;base64\n',
2725
- '#EXT-X-MAP:URI="init.mp4"\n',
2726
- '#EXTINF:5,\n',
2727
- '#EXT-X-BYTERANGE:121090@616\n',
2728
- 'main.mp4',
2729
- ].join('');
2730
-
2731
- const error = new shaka.util.Error(
2732
- shaka.util.Error.Severity.CRITICAL,
2733
- shaka.util.Error.Category.MANIFEST,
2734
- Code.HLS_AES_128_ENCRYPTION_NOT_SUPPORTED);
2735
-
2736
- await verifyError(master, media, error);
2737
- });
2738
-
2739
2755
  describe('if required attributes are missing', () => {
2740
2756
  /**
2741
2757
  * @param {string} master
@@ -100,10 +100,13 @@ describe('StreamingEngine', () => {
100
100
  jasmine.clock().mockDate();
101
101
  });
102
102
 
103
- /** @param {boolean=} trickMode
103
+ /**
104
+ * @param {boolean=} trickMode
104
105
  * @param {number=} mediaOffset The offset from 0 for the segment start times
106
+ * @param {shaka.extern.HlsAes128Key=} hlsAes128Key The AES-128 key to put in
107
+ * the manifest, if one should exist
105
108
  */
106
- function setupVod(trickMode, mediaOffset) {
109
+ function setupVod(trickMode, mediaOffset, hlsAes128Key) {
107
110
  // For VOD, we fake a presentation that has 2 Periods of equal duration
108
111
  // (20 seconds), where each Period has 1 Variant and 1 text stream.
109
112
  //
@@ -209,7 +212,8 @@ describe('StreamingEngine', () => {
209
212
  setupManifest(
210
213
  /* firstPeriodStartTime= */ 0,
211
214
  /* secondPeriodStartTime= */ 20,
212
- /* presentationDuration= */ 40);
215
+ /* presentationDuration= */ 40,
216
+ hlsAes128Key);
213
217
  }
214
218
 
215
219
  function setupLive() {
@@ -356,8 +360,15 @@ describe('StreamingEngine', () => {
356
360
  /* delays= */ netEngineDelays);
357
361
  }
358
362
 
363
+ /**
364
+ * @param {number} firstPeriodStartTime
365
+ * @param {number} secondPeriodStartTime
366
+ * @param {number} presentationDuration
367
+ * @param {shaka.extern.HlsAes128Key=} hlsAes128Key
368
+ */
359
369
  function setupManifest(
360
- firstPeriodStartTime, secondPeriodStartTime, presentationDuration) {
370
+ firstPeriodStartTime, secondPeriodStartTime, presentationDuration,
371
+ hlsAes128Key) {
361
372
  const segmentDurations = {
362
373
  audio: segmentData[ContentType.AUDIO].segmentDuration,
363
374
  video: segmentData[ContentType.VIDEO].segmentDuration,
@@ -380,7 +391,7 @@ describe('StreamingEngine', () => {
380
391
  /** @type {!shaka.media.PresentationTimeline} */(timeline),
381
392
  [firstPeriodStartTime, secondPeriodStartTime],
382
393
  presentationDuration, segmentDurations, initSegmentRanges,
383
- timestampOffsets);
394
+ timestampOffsets, hlsAes128Key);
384
395
 
385
396
  audioStream = manifest.variants[0].audio;
386
397
  videoStream = manifest.variants[0].video;
@@ -3430,6 +3441,69 @@ describe('StreamingEngine', () => {
3430
3441
  expect(alternateVideoStream.createSegmentIndex).not.toHaveBeenCalled();
3431
3442
  });
3432
3443
 
3444
+ describe('AES-128', () => {
3445
+ let key;
3446
+ /** @type {!shaka.extern.HlsAes128Key} */
3447
+ let hlsAes128Key;
3448
+
3449
+ beforeEach(async () => {
3450
+ // Get a key.
3451
+ const keyData = new ArrayBuffer(16);
3452
+ const keyDataView = new DataView(keyData);
3453
+ keyDataView.setInt16(0, 31710); // 0111 1011 1101 1110
3454
+ key = await window.crypto.subtle.importKey(
3455
+ 'raw', keyData, 'AES-CBC', true, ['decrypt']);
3456
+
3457
+ // Set up a manifest with AES-128 key info.
3458
+ // We don't actually provide the imported key OR the key fetching function
3459
+ // here, though, so that the individual tests can choose what the starting
3460
+ // state of the hlsAes128Key object is.
3461
+ hlsAes128Key = {method: 'AES-128', firstMediaSequenceNumber: 0};
3462
+
3463
+ setupVod(false, 0, hlsAes128Key);
3464
+ mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
3465
+ presentationTimeInSeconds = 0;
3466
+ createStreamingEngine();
3467
+ streamingEngine.switchVariant(variant);
3468
+ });
3469
+
3470
+ afterEach(async () => {
3471
+ await streamingEngine.destroy();
3472
+ });
3473
+
3474
+ async function runTest() {
3475
+ spyOn(window.crypto.subtle, 'decrypt').and.callThrough();
3476
+
3477
+ await streamingEngine.start();
3478
+ playing = true;
3479
+ await Util.fakeEventLoop(10);
3480
+
3481
+ expect(mediaSourceEngine.appendBuffer).toHaveBeenCalledTimes(2);
3482
+ expect(window.crypto.subtle.decrypt).toHaveBeenCalledTimes(2);
3483
+ expect(window.crypto.subtle.decrypt).toHaveBeenCalledWith(
3484
+ {name: 'AES-CBC', iv: jasmine.any(Object)}, key, jasmine.any(Object));
3485
+ }
3486
+
3487
+ it('decrypts segments', async () => {
3488
+ hlsAes128Key.cryptoKey = key;
3489
+ await runTest();
3490
+ });
3491
+
3492
+ it('downloads key if not pre-filled', async () => {
3493
+ hlsAes128Key.fetchKey = () => {
3494
+ hlsAes128Key.cryptoKey = key;
3495
+ hlsAes128Key.fetchKey = undefined;
3496
+ return Promise.resolve();
3497
+ };
3498
+
3499
+ await runTest();
3500
+
3501
+ // The key should have been fetched.
3502
+ expect(hlsAes128Key.cryptoKey).not.toBeUndefined();
3503
+ expect(hlsAes128Key.fetchKey).toBeUndefined();
3504
+ });
3505
+ });
3506
+
3433
3507
  describe('destroy', () => {
3434
3508
  it('aborts pending network operations', async () => {
3435
3509
  setupVod();
@@ -481,6 +481,10 @@ shaka.test.FakeSegmentIndex = class {
481
481
  return this.get(nextPosition - 1);
482
482
  },
483
483
 
484
+ currentPosition: () => {
485
+ return nextPosition;
486
+ },
487
+
484
488
  seek: (time) => {
485
489
  nextPosition = this.find(time);
486
490
  return this.get(nextPosition++);
@@ -149,11 +149,13 @@ shaka.test.StreamingEngineUtil = class {
149
149
  * ranges for each type of init segment.
150
150
  * @param {!Object.<string,number>=} timestampOffsets The timestamp offset
151
151
  * for each type of segment
152
+ * @param {shaka.extern.HlsAes128Key=} hlsAes128Key The AES-128 key to provide
153
+ * to streams, if desired.
152
154
  * @return {shaka.extern.Manifest}
153
155
  */
154
156
  static createManifest(
155
157
  presentationTimeline, periodStartTimes, presentationDuration,
156
- segmentDurations, initSegmentRanges, timestampOffsets) {
158
+ segmentDurations, initSegmentRanges, timestampOffsets, hlsAes128Key) {
157
159
  const Util = shaka.test.Util;
158
160
 
159
161
  /**
@@ -255,7 +257,7 @@ shaka.test.StreamingEngineUtil = class {
255
257
  const appendWindowEnd = periodIndex == periodStartTimes.length - 1?
256
258
  presentationDuration : periodStartTimes[periodIndex + 1];
257
259
 
258
- return new shaka.media.SegmentReference(
260
+ const ref = new shaka.media.SegmentReference(
259
261
  /* startTime= */ periodStart + positionWithinPeriod * d,
260
262
  /* endTime= */ periodStart + (positionWithinPeriod + 1) * d,
261
263
  getUris,
@@ -265,6 +267,12 @@ shaka.test.StreamingEngineUtil = class {
265
267
  timestampOffset,
266
268
  appendWindowStart,
267
269
  appendWindowEnd);
270
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
271
+ if (hlsAes128Key &&
272
+ (type == ContentType.AUDIO || type == ContentType.VIDEO)) {
273
+ ref.hlsAes128Key = hlsAes128Key;
274
+ }
275
+ return ref;
268
276
  };
269
277
 
270
278
  /** @type {shaka.extern.Manifest} */