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
@@ -1,8 +1,41 @@
1
1
  # FairPlay Support
2
2
 
3
3
  We support FairPlay with EME on compatible environments or native `src=`.
4
+
5
+ By default Shaka Player supports Modern EME, if your provider doesn't support
6
+ Modern EME yet, you can use legacy Apple Media Keys with:
7
+ ```js
8
+ shaka.polyfill.PatchedMediaKeysApple.install();
9
+ ```
10
+
11
+ The support in each case would be the following:
12
+
13
+ | |Modern EME |legacy Apple Media Keys|
14
+ |:----------:|:---------:|:---------------------:|
15
+ |src= (CMAF) |**Y** |**Y** |
16
+ |src= (TS) |**Y** |**Y** |
17
+ |MSE (CMAF) |**Y** | - |
18
+ |MSE (TS) | - | - |
19
+
20
+
4
21
  Adding FairPlay support involves a bit more work than other key systems.
5
22
 
23
+
24
+ ## Keysystem used in EME
25
+
26
+ Depending on the EME implementation that is being used, the Fairplay keysystem
27
+ varies.
28
+
29
+ For Modern EME:
30
+ ```
31
+ com.apple.fps
32
+ ```
33
+
34
+ For legacy Apple Media Keys:
35
+ ```
36
+ com.apple.fps.1_0
37
+ ```
38
+
6
39
  ## Server certificate
7
40
 
8
41
  All FairPlay content requires setting a server certificate. You can either
@@ -22,6 +55,27 @@ player.configure('drm.advanced.com\\.apple\\.fps\\.serverCertificateUri',
22
55
  'https://example.com/cert.der');
23
56
  ```
24
57
 
58
+ ## Content ID
59
+
60
+ Note: Normally only applies to legacy Apple Media Keys but some providers also
61
+ need it in Modern EME.
62
+
63
+ Some FairPlay content use custom signaling for the content ID. The content ID
64
+ is used by the browser to generate the license request. If you don't use the
65
+ default content ID derivation, you need to specify a custom init data transform:
66
+
67
+ ```js
68
+ player.configure('drm.initDataTransform', (initData, initDataType) => {
69
+ if (initDataType != 'skd')
70
+ return initData;
71
+ // 'initData' is a buffer containing an 'skd://' URL as a UTF-8 string.
72
+ const skdUri = shaka.util.StringUtils.fromBytesAutoDetect(initData);
73
+ const contentId = getMyContentId(skdUri);
74
+ const cert = player.drmInfo().serverCertificate;
75
+ return shaka.util.FairPlayUtils.initDataTransform(initData, contentId, cert);
76
+ });
77
+ ```
78
+
25
79
  ## License wrapping
26
80
 
27
81
  Some FairPlay servers need to accept the license request in a different format
@@ -61,3 +115,98 @@ player.getNetworkingEngine().registerResponseFilter((type, response) => {
61
115
  response.data = shaka.util.Uint8ArrayUtils.fromBase64(responseText).buffer;
62
116
  });
63
117
  ```
118
+
119
+ ### Integration with some DRMs providers
120
+
121
+ Note: Some providers support both Modern EME and legacy Apple Media Keys.
122
+
123
+ #### EZDRM (Modern EME)
124
+
125
+ For integration with EZDRM the following can be used:
126
+
127
+ ```js
128
+ const FairPlayUtils = shaka.util.FairPlayUtils;
129
+ player.getNetworkingEngine()
130
+ .registerRequestFilter(FairPlayUtils.ezdrmFairPlayRequest);
131
+ player.getNetworkingEngine()
132
+ .registerResponseFilter(FairPlayUtils.commonFairPlayResponse);
133
+ ```
134
+
135
+ Note: If the url of the license server has to undergo any transformation
136
+ (eg: add the contentId), you would have to create your filter manually.
137
+
138
+ ```js
139
+ player.getNetworkingEngine().registerRequestFilter((type, request) => {
140
+ if (type != shaka.net.NetworkingEngine.RequestType.LICENSE) {
141
+ return;
142
+ }
143
+ const uri = request.uris[0];
144
+ const FairPlayUtils = shaka.util.FairPlayUtils;
145
+ const contentId = FairPlayUtils.defaultGetContentId(request.initData);
146
+ const newUri = uri.replace('^assetId^', contentId);
147
+ request.uris = [newUri];
148
+ request.headers['Content-Type'] = 'application/octet-stream'
149
+ });
150
+ ```
151
+
152
+ #### EZDRM (legacy Apple Media Keys)
153
+
154
+ For integration with EZDRM the following can be used:
155
+
156
+ ```js
157
+ shaka.polyfill.PatchedMediaKeysApple.install();
158
+ const FairPlayUtils = shaka.util.FairPlayUtils;
159
+ player.getNetworkingEngine()
160
+ .registerRequestFilter(FairPlayUtils.ezdrmFairPlayRequest);
161
+ player.getNetworkingEngine()
162
+ .registerResponseFilter(FairPlayUtils.commonFairPlayResponse);
163
+ player.configure('drm.initDataTransform',
164
+ FairPlayUtils.ezdrmInitDataTransform);
165
+ ```
166
+
167
+ Note: If the url of the license server has to undergo any transformation
168
+ (eg: add the contentId), you would have to create your filter manually.
169
+
170
+ ```js
171
+ player.getNetworkingEngine().registerRequestFilter((type, request) => {
172
+ if (type != shaka.net.NetworkingEngine.RequestType.LICENSE) {
173
+ return;
174
+ }
175
+ const uri = request.uris[0];
176
+ const FairPlayUtils = shaka.util.FairPlayUtils;
177
+ const contentId = FairPlayUtils.defaultGetContentId(request.initData);
178
+ const newUri = uri.replace('^assetId^', contentId);
179
+ request.uris = [newUri];
180
+ request.headers['Content-Type'] = 'application/octet-stream'
181
+ });
182
+ ```
183
+
184
+ #### Verimatrix (legacy Apple Media Keys)
185
+
186
+ For integration with Verimatrix the following can be used:
187
+
188
+ ```js
189
+ shaka.polyfill.PatchedMediaKeysApple.install();
190
+ const FairPlayUtils = shaka.util.FairPlayUtils;
191
+ player.getNetworkingEngine()
192
+ .registerRequestFilter(FairPlayUtils.verimatrixFairPlayRequest);
193
+ player.getNetworkingEngine()
194
+ .registerResponseFilter(FairPlayUtils.commonFairPlayResponse);
195
+ player.configure('drm.initDataTransform',
196
+ FairPlayUtils.verimatrixInitDataTransform);
197
+ ```
198
+
199
+ #### Conax (legacy Apple Media Keys)
200
+
201
+ For integration with Conax the following can be used:
202
+
203
+ ```js
204
+ shaka.polyfill.PatchedMediaKeysApple.install();
205
+ const FairPlayUtils = shaka.util.FairPlayUtils;
206
+ player.getNetworkingEngine()
207
+ .registerRequestFilter(FairPlayUtils.conaxFairPlayRequest);
208
+ player.getNetworkingEngine()
209
+ .registerResponseFilter(FairPlayUtils.commonFairPlayResponse);
210
+ player.configure('drm.initDataTransform',
211
+ FairPlayUtils.conaxInitDataTransform);
212
+ ```
@@ -30,7 +30,7 @@ Set up controls with HTML data attributes:
30
30
  The data-shaka-player-cast-receiver-id tag allows you to provide a Cast Application ID that
31
31
  the cast button will cast to; the value provided here is the sample cast receiver. -->
32
32
  <div data-shaka-player-container style="max-width:40em"
33
- data-shaka-player-cast-receiver-id="06EDA6EE">
33
+ data-shaka-player-cast-receiver-id="07AEE832">
34
34
  <!-- The data-shaka-player tag will make the UI library use this video element.
35
35
  If no video is provided, the UI will automatically make one inside the container div. -->
36
36
  <video autoplay data-shaka-player id="video" style="width:100%;height:100%"></video>
@@ -111,7 +111,7 @@ set up a listener for the 'caststatuschanged' events.
111
111
  <!-- Add a data-shaka-player-cast-receiver-id tag to provide a Cast Application ID that
112
112
  the cast button will cast to; the value provided here is the sample cast receiver. -->
113
113
  <div data-shaka-player-container style="max-width:40em"
114
- data-shaka-player-cast-receiver-id="06EDA6EE">
114
+ data-shaka-player-cast-receiver-id="07AEE832">
115
115
  </div>
116
116
  ```
117
117
 
@@ -154,7 +154,7 @@ or a `<source>` tag inside it to enable auto loading of the specified content.
154
154
 
155
155
  ```html
156
156
  <div data-shaka-player-container style="max-width:40em"
157
- data-shaka-player-cast-receiver-id="06EDA6EE">
157
+ data-shaka-player-cast-receiver-id="07AEE832">
158
158
  <!-- The manifest url in the src attribute will be automatically loaded -->
159
159
  <video autoplay data-shaka-player id="video" style="width:100%;height:100%"
160
160
  src="https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd"></video>
@@ -165,7 +165,7 @@ or
165
165
 
166
166
  ```html
167
167
  <div data-shaka-player-container style="max-width:40em"
168
- data-shaka-player-cast-receiver-id="06EDA6EE">
168
+ data-shaka-player-cast-receiver-id="07AEE832">
169
169
  <video autoplay data-shaka-player id="video" style="width:100%;height:100%">
170
170
  <!-- The manifest url in the src attribute will be auto loaded -->
171
171
  <source src="https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd"/>
@@ -178,7 +178,7 @@ call to the first one fails.
178
178
 
179
179
  ```html
180
180
  <div data-shaka-player-container style="max-width:40em"
181
- data-shaka-player-cast-receiver-id="06EDA6EE">
181
+ data-shaka-player-cast-receiver-id="07AEE832">
182
182
  <video autoplay data-shaka-player id="video" style="width:100%;height:100%">
183
183
  <!-- Try this first -->
184
184
  <source src="https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd"/>
@@ -222,7 +222,7 @@ const video = controls.getVideo();
222
222
  // Receiver Compatability.
223
223
  ui.configure({
224
224
  // Set the castReceiverAppId
225
- 'castReceiverAppId': '06EDA6EE',
225
+ 'castReceiverAppId': '07AEE832',
226
226
  // Enable casting to native Android Apps (e.g. Android TV Apps)
227
227
  'castAndroidReceiverCompatible': true,
228
228
  });
@@ -103,6 +103,14 @@ shaka.extern.AbrManager = class {
103
103
  */
104
104
  playbackRateChanged(rate) {}
105
105
 
106
+ /**
107
+ * Set media element.
108
+ *
109
+ * @param {HTMLMediaElement} mediaElement
110
+ * @exportDoc
111
+ */
112
+ setMediaElement(mediaElement) {}
113
+
106
114
  /**
107
115
  * Sets the ABR configuration.
108
116
  *
@@ -244,6 +244,49 @@ shaka.extern.Variant;
244
244
  shaka.extern.CreateSegmentIndexFunction;
245
245
 
246
246
 
247
+ /**
248
+ * @typedef {{
249
+ * method: string,
250
+ * cryptoKey: (webCrypto.CryptoKey|undefined),
251
+ * fetchKey: (shaka.extern.CreateSegmentIndexFunction|undefined),
252
+ * iv: (!Uint8Array|undefined),
253
+ * firstMediaSequenceNumber: number
254
+ * }}
255
+ *
256
+ * @description
257
+ * AES-128 key and iv info from the HLS manifest.
258
+ *
259
+ * @property {string} method
260
+ * The key method defined in the HLS manifest.
261
+ * @property {webCrypto.CryptoKey|undefined} cryptoKey
262
+ * Web crypto key object of the AES-128 CBC key. If unset, the "fetchKey"
263
+ * property should be provided.
264
+ * @property {shaka.extern.FetchCryptoKeysFunction|undefined} fetchKey
265
+ * A function that fetches the key.
266
+ * Should be provided if the "cryptoKey" property is unset.
267
+ * Should update this object in-place, to set "cryptoKey".
268
+ * @property {(!Uint8Array|undefined)} iv
269
+ * The IV in the HLS manifest, if defined. See HLS RFC 8216 Section 5.2 for
270
+ * handling undefined IV.
271
+ * @property {number} firstMediaSequenceNumber
272
+ * The starting Media Sequence Number of the playlist, used when IV is
273
+ * undefined.
274
+ *
275
+ * @exportDoc
276
+ */
277
+ shaka.extern.HlsAes128Key;
278
+
279
+
280
+ /**
281
+ * A function that fetches the crypto keys for AES-128.
282
+ * Returns a promise that resolves when the keys have been fetched.
283
+ *
284
+ * @typedef {function(): !Promise}
285
+ * @exportDoc
286
+ */
287
+ shaka.extern.FetchCryptoKeysFunction;
288
+
289
+
247
290
  /**
248
291
  * @typedef {{
249
292
  * id: number,
@@ -1000,7 +1000,9 @@ shaka.extern.StreamingConfiguration;
1000
1000
  * switchInterval: number,
1001
1001
  * bandwidthUpgradeTarget: number,
1002
1002
  * bandwidthDowngradeTarget: number,
1003
- * advanced: shaka.extern.AdvancedAbrConfiguration
1003
+ * advanced: shaka.extern.AdvancedAbrConfiguration,
1004
+ * restrictToElementSize: boolean,
1005
+ * ignoreDevicePixelRatio: boolean
1004
1006
  * }}
1005
1007
  *
1006
1008
  * @property {boolean} enabled
@@ -1029,7 +1031,16 @@ shaka.extern.StreamingConfiguration;
1029
1031
  * The largest fraction of the estimated bandwidth we should use. We should
1030
1032
  * downgrade to avoid this.
1031
1033
  * @property {shaka.extern.AdvancedAbrConfiguration} advanced
1032
- * Advanced ABR configuration.
1034
+ * Advanced ABR configuration
1035
+ * @property {boolean} restrictToElementSize
1036
+ * If true, restrict the quality to media element size.
1037
+ * Note: The use of ResizeObserver is required for it to work properly. If
1038
+ * true without ResizeObserver, it behaves as false.
1039
+ * Defaults false.
1040
+ * @property {boolean} ignoreDevicePixelRatio
1041
+ * If true,device pixel ratio is ignored when restricting the quality to
1042
+ * media element size.
1043
+ * Defaults false.
1033
1044
  * @exportDoc
1034
1045
  */
1035
1046
  shaka.extern.AbrConfiguration;
@@ -10,6 +10,7 @@ goog.require('goog.asserts');
10
10
  goog.require('shaka.abr.EwmaBandwidthEstimator');
11
11
  goog.require('shaka.log');
12
12
  goog.require('shaka.util.StreamUtils');
13
+ goog.require('shaka.util.Timer');
13
14
 
14
15
 
15
16
  /**
@@ -85,6 +86,22 @@ shaka.abr.SimpleAbrManager = class {
85
86
 
86
87
  /** @private {?shaka.extern.AbrConfiguration} */
87
88
  this.config_ = null;
89
+
90
+ /** @private {HTMLMediaElement} */
91
+ this.mediaElement_ = null;
92
+
93
+ /** @private {ResizeObserver} */
94
+ this.resizeObserver_ = null;
95
+
96
+ /** @private {shaka.util.Timer} */
97
+ this.resizeObserverTimer_ = new shaka.util.Timer(() => {
98
+ if (this.config_.restrictToElementSize) {
99
+ const chosenVariant = this.chooseVariant();
100
+ if (chosenVariant) {
101
+ this.switch_(chosenVariant);
102
+ }
103
+ }
104
+ });
88
105
  }
89
106
 
90
107
 
@@ -98,6 +115,14 @@ shaka.abr.SimpleAbrManager = class {
98
115
  this.variants_ = [];
99
116
  this.playbackRate_ = 1;
100
117
  this.lastTimeChosenMs_ = null;
118
+ this.mediaElement_ = null;
119
+
120
+ if (this.resizeObserver_) {
121
+ this.resizeObserver_.disconnect();
122
+ this.resizeObserver_ = null;
123
+ }
124
+
125
+ this.resizeObserverTimer_.stop();
101
126
 
102
127
  // Don't reset |startupComplete_|: if we've left the startup interval, we
103
128
  // can start using bandwidth estimates right away after init() is called.
@@ -120,9 +145,19 @@ shaka.abr.SimpleAbrManager = class {
120
145
  chooseVariant() {
121
146
  const SimpleAbrManager = shaka.abr.SimpleAbrManager;
122
147
 
148
+ let maxHeight = Infinity;
149
+ let maxWidth = Infinity;
150
+
151
+ if (this.resizeObserver_ && this.config_.restrictToElementSize) {
152
+ const devicePixelRatio =
153
+ this.config_.ignoreDevicePixelRatio ? 1 : window.devicePixelRatio;
154
+ maxHeight = this.mediaElement_.clientWidth * devicePixelRatio;
155
+ maxWidth = this.mediaElement_.clientHeight * devicePixelRatio;
156
+ }
157
+
123
158
  // Get sorted Variants.
124
159
  let sortedVariants = SimpleAbrManager.filterAndSortVariants_(
125
- this.config_.restrictions, this.variants_);
160
+ this.config_.restrictions, this.variants_, maxHeight, maxWidth);
126
161
 
127
162
  const defaultBandwidthEstimate = this.getDefaultBandwidth_();
128
163
  const currentBandwidth = this.bandwidthEstimator_.getBandwidthEstimate(
@@ -137,7 +172,8 @@ shaka.abr.SimpleAbrManager = class {
137
172
  shaka.log.warning('No variants met the ABR restrictions. ' +
138
173
  'Choosing a variant by lowest bandwidth.');
139
174
  sortedVariants = SimpleAbrManager.filterAndSortVariants_(
140
- /* restrictions= */ null, this.variants_);
175
+ /* restrictions= */ null, this.variants_,
176
+ /* maxHeight= */ Infinity, /* maxWidth= */ Infinity);
141
177
  sortedVariants = [sortedVariants[0]];
142
178
  }
143
179
 
@@ -243,6 +279,28 @@ shaka.abr.SimpleAbrManager = class {
243
279
  }
244
280
 
245
281
 
282
+ /**
283
+ * @override
284
+ * @export
285
+ */
286
+ setMediaElement(mediaElement) {
287
+ this.mediaElement_ = mediaElement;
288
+ if (this.resizeObserver_) {
289
+ this.resizeObserver_.disconnect();
290
+ this.resizeObserver_ = null;
291
+ }
292
+ if (this.mediaElement_ && 'ResizeObserver' in window) {
293
+ this.resizeObserver_ = new ResizeObserver(() => {
294
+ const SimpleAbrManager = shaka.abr.SimpleAbrManager;
295
+ // Batch up resize changes before checking them.
296
+ this.resizeObserverTimer_.tickAfter(
297
+ /* seconds= */ SimpleAbrManager.RESIZE_OBSERVER_BATCH_TIME);
298
+ });
299
+ this.resizeObserver_.observe(this.mediaElement_);
300
+ }
301
+ }
302
+
303
+
246
304
  /**
247
305
  * @override
248
306
  * @export
@@ -320,11 +378,13 @@ shaka.abr.SimpleAbrManager = class {
320
378
  /**
321
379
  * @param {?shaka.extern.Restrictions} restrictions
322
380
  * @param {!Array.<shaka.extern.Variant>} variants
381
+ * @param {!number} maxHeight
382
+ * @param {!number} maxWidth
323
383
  * @return {!Array.<shaka.extern.Variant>} variants filtered according to
324
384
  * |restrictions| and sorted in ascending order of bandwidth.
325
385
  * @private
326
386
  */
327
- static filterAndSortVariants_(restrictions, variants) {
387
+ static filterAndSortVariants_(restrictions, variants, maxHeight, maxWidth) {
328
388
  if (restrictions) {
329
389
  variants = variants.filter((variant) => {
330
390
  // This was already checked in another scope, but the compiler doesn't
@@ -333,7 +393,7 @@ shaka.abr.SimpleAbrManager = class {
333
393
 
334
394
  return shaka.util.StreamUtils.meetsRestrictions(
335
395
  variant, restrictions,
336
- /* maxHwRes= */ {width: Infinity, height: Infinity});
396
+ /* maxHwRes= */ {width: maxWidth, height: maxHeight});
337
397
  });
338
398
  }
339
399
 
@@ -342,3 +402,11 @@ shaka.abr.SimpleAbrManager = class {
342
402
  });
343
403
  }
344
404
  };
405
+
406
+
407
+ /**
408
+ * The amount of time, in seconds, we wait to batch up rapid resize changes.
409
+ * This allows us to avoid multiple resize events in most cases.
410
+ * @type {number}
411
+ */
412
+ shaka.abr.SimpleAbrManager.RESIZE_OBSERVER_BATCH_TIME = 1;
@@ -37,6 +37,7 @@ goog.require('shaka.util.OperationManager');
37
37
  goog.require('shaka.util.Pssh');
38
38
  goog.require('shaka.util.Timer');
39
39
  goog.require('shaka.util.Platform');
40
+ goog.require('shaka.util.Uint8ArrayUtils');
40
41
  goog.require('shaka.util.XmlUtils');
41
42
  goog.requireType('shaka.hls.Segment');
42
43
 
@@ -1644,34 +1645,30 @@ shaka.hls.HlsParser = class {
1644
1645
  if (method != 'NONE') {
1645
1646
  encrypted = true;
1646
1647
 
1647
- // We do not support AES-128 encryption with HLS yet. So, do not create
1648
- // StreamInfo for the playlist encrypted with AES-128.
1649
- // TODO: Remove the error message once we add support for AES-128.
1650
1648
  if (method == 'AES-128') {
1651
- shaka.log.warning('Unsupported HLS Encryption', method);
1649
+ // These keys are handled separately.
1652
1650
  this.aesEncrypted_ = true;
1653
- return null;
1654
- }
1655
-
1656
- const keyFormat = drmTag.getRequiredAttrValue('KEYFORMAT');
1657
- const drmParser =
1658
- shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
1659
-
1660
- const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null;
1661
- if (drmInfo) {
1662
- if (drmInfo.keyIds) {
1663
- for (const keyId of drmInfo.keyIds) {
1664
- keyIds.add(keyId);
1651
+ } else {
1652
+ const keyFormat = drmTag.getRequiredAttrValue('KEYFORMAT');
1653
+ const drmParser =
1654
+ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
1655
+
1656
+ const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null;
1657
+ if (drmInfo) {
1658
+ if (drmInfo.keyIds) {
1659
+ for (const keyId of drmInfo.keyIds) {
1660
+ keyIds.add(keyId);
1661
+ }
1665
1662
  }
1663
+ drmInfos.push(drmInfo);
1664
+ } else {
1665
+ shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
1666
1666
  }
1667
- drmInfos.push(drmInfo);
1668
- } else {
1669
- shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
1670
1667
  }
1671
1668
  }
1672
1669
  }
1673
1670
 
1674
- if (encrypted && !drmInfos.length) {
1671
+ if (encrypted && !drmInfos.length && !this.aesEncrypted_) {
1675
1672
  throw new shaka.util.Error(
1676
1673
  shaka.util.Error.Severity.CRITICAL,
1677
1674
  shaka.util.Error.Category.MANIFEST,
@@ -1688,9 +1685,8 @@ shaka.hls.HlsParser = class {
1688
1685
 
1689
1686
  let segments;
1690
1687
  try {
1691
- segments = this.createSegments_(verbatimMediaPlaylistUri,
1692
- playlist, type, mimeType, mediaSequenceToStartTime, mediaVariables,
1693
- codecs);
1688
+ segments = this.createSegments_(verbatimMediaPlaylistUri, playlist, type,
1689
+ mimeType, mediaSequenceToStartTime, mediaVariables, codecs);
1694
1690
  } catch (error) {
1695
1691
  if (error.code == shaka.util.Error.Code.HLS_INTERNAL_SKIP_STREAM) {
1696
1692
  shaka.log.alwaysWarn('Skipping unsupported HLS stream',
@@ -1765,6 +1761,78 @@ shaka.hls.HlsParser = class {
1765
1761
  }
1766
1762
 
1767
1763
 
1764
+ /**
1765
+ * @param {!shaka.hls.Tag} drmTag
1766
+ * @param {!shaka.hls.Playlist} playlist
1767
+ * @return {!shaka.extern.HlsAes128Key}
1768
+ * @private
1769
+ */
1770
+ parseAES128DrmTag_(drmTag, playlist) {
1771
+ // Check if the Web Crypto API is available.
1772
+ if (!window.crypto || !window.crypto.subtle) {
1773
+ shaka.log.alwaysWarn('Web Crypto API is not available to decrypt ' +
1774
+ 'AES-128. (Web Crypto only exists in secure origins like https)');
1775
+ throw new shaka.util.Error(
1776
+ shaka.util.Error.Severity.CRITICAL,
1777
+ shaka.util.Error.Category.MANIFEST,
1778
+ shaka.util.Error.Code.NO_WEB_CRYPTO_API);
1779
+ }
1780
+
1781
+ // HLS RFC 8216 Section 5.2:
1782
+ // An EXT-X-KEY tag with a KEYFORMAT of "identity" that does not have an IV
1783
+ // attribute indicates that the Media Sequence Number is to be used as the
1784
+ // IV when decrypting a Media Segment, by putting its big-endian binary
1785
+ // representation into a 16-octet (128-bit) buffer and padding (on the left)
1786
+ // with zeros.
1787
+ let firstMediaSequenceNumber = 0;
1788
+ let iv;
1789
+ const ivHex = drmTag.getAttributeValue('IV', '');
1790
+ if (!ivHex) {
1791
+ // Media Sequence Number will be used as IV.
1792
+ firstMediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
1793
+ playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0);
1794
+ } else {
1795
+ // Exclude 0x at the start of string.
1796
+ iv = shaka.util.Uint8ArrayUtils.fromHex(ivHex.substr(2));
1797
+ if (iv.byteLength != 16) {
1798
+ throw new shaka.util.Error(
1799
+ shaka.util.Error.Severity.CRITICAL,
1800
+ shaka.util.Error.Category.MANIFEST,
1801
+ shaka.util.Error.Code.HLS_AES_128_INVALID_IV_LENGTH);
1802
+ }
1803
+ }
1804
+
1805
+ const keyUri = shaka.hls.Utils.constructAbsoluteUri(
1806
+ playlist.absoluteUri, drmTag.getRequiredAttrValue('URI'));
1807
+
1808
+ const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
1809
+ const request = shaka.net.NetworkingEngine.makeRequest(
1810
+ [keyUri], this.config_.retryParameters);
1811
+
1812
+ const keyInfo = {method: 'AES-128', iv, firstMediaSequenceNumber};
1813
+
1814
+ // Don't download the key object until the segment is parsed, to avoid a
1815
+ // startup delay for long manifests with lots of keys.
1816
+ keyInfo.fetchKey = async () => {
1817
+ const keyResponse = await this.makeNetworkRequest_(request, requestType);
1818
+
1819
+ // keyResponse.status is undefined when URI is "data:text/plain;base64,"
1820
+ if (!keyResponse.data || keyResponse.data.byteLength != 16) {
1821
+ throw new shaka.util.Error(
1822
+ shaka.util.Error.Severity.CRITICAL,
1823
+ shaka.util.Error.Category.MANIFEST,
1824
+ shaka.util.Error.Code.HLS_AES_128_INVALID_KEY_LENGTH);
1825
+ }
1826
+
1827
+ keyInfo.cryptoKey = await window.crypto.subtle.importKey(
1828
+ 'raw', keyResponse.data, 'AES-CBC', true, ['decrypt']);
1829
+ keyInfo.fetchKey = undefined; // No longer needed.
1830
+ };
1831
+
1832
+ return keyInfo;
1833
+ }
1834
+
1835
+
1768
1836
  /**
1769
1837
  * @param {!shaka.hls.Playlist} playlist
1770
1838
  * @private
@@ -1939,12 +2007,13 @@ shaka.hls.HlsParser = class {
1939
2007
  * @param {!Map.<string, string>} variables
1940
2008
  * @param {string} absoluteMediaPlaylistUri
1941
2009
  * @param {string} type
2010
+ * @param {shaka.extern.HlsAes128Key=} hlsAes128Key
1942
2011
  * @return {shaka.media.SegmentReference}
1943
2012
  * @private
1944
2013
  */
1945
2014
  createSegmentReference_(
1946
2015
  initSegmentReference, previousReference, hlsSegment, startTime,
1947
- variables, absoluteMediaPlaylistUri, type) {
2016
+ variables, absoluteMediaPlaylistUri, type, hlsAes128Key) {
1948
2017
  const tags = hlsSegment.tags;
1949
2018
  const absoluteSegmentUri = this.variableSubstitution_(
1950
2019
  hlsSegment.absoluteUri, variables);
@@ -2039,6 +2108,8 @@ shaka.hls.HlsParser = class {
2039
2108
  partialStatus = shaka.media.SegmentReference.Status.MISSING;
2040
2109
  }
2041
2110
 
2111
+ // We do not set the AES-128 key information for partial segments, as we
2112
+ // do not support AES-128 and low-latency at the same time.
2042
2113
  const partial = new shaka.media.SegmentReference(
2043
2114
  pStartTime,
2044
2115
  pEndTime,
@@ -2120,6 +2191,7 @@ shaka.hls.HlsParser = class {
2120
2191
  tileDuration,
2121
2192
  syncTime,
2122
2193
  status,
2194
+ hlsAes128Key,
2123
2195
  );
2124
2196
  }
2125
2197
 
@@ -2186,6 +2258,9 @@ shaka.hls.HlsParser = class {
2186
2258
  /** @type {shaka.media.InitSegmentReference} */
2187
2259
  let initSegmentRef;
2188
2260
 
2261
+ /** @type {shaka.extern.HlsAes128Key|undefined} */
2262
+ let hlsAes128Key = undefined;
2263
+
2189
2264
  // We may need to look at the media itself to determine a segment start
2190
2265
  // time.
2191
2266
  const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
@@ -2214,6 +2289,14 @@ shaka.hls.HlsParser = class {
2214
2289
  (i == 0) ? firstStartTime : previousReference.endTime;
2215
2290
  position = mediaSequenceNumber + skippedSegments + i;
2216
2291
 
2292
+ // Apply new AES-128 tags as you see them, keeping a running total.
2293
+ for (const drmTag of item.tags) {
2294
+ if (drmTag.name == 'EXT-X-KEY' &&
2295
+ drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
2296
+ hlsAes128Key = this.parseAES128DrmTag_(drmTag, playlist);
2297
+ }
2298
+ }
2299
+
2217
2300
  mediaSequenceToStartTime.set(position, startTime);
2218
2301
 
2219
2302
  initSegmentRef = this.getInitSegmentReference_(playlist.absoluteUri,
@@ -2238,7 +2321,8 @@ shaka.hls.HlsParser = class {
2238
2321
  startTime,
2239
2322
  variables,
2240
2323
  playlist.absoluteUri,
2241
- type);
2324
+ type,
2325
+ hlsAes128Key);
2242
2326
  previousReference = reference;
2243
2327
 
2244
2328
  if (reference) {
@@ -2611,6 +2695,14 @@ shaka.hls.HlsParser = class {
2611
2695
  shaka.util.Error.Code.HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED);
2612
2696
  }
2613
2697
 
2698
+ if (shaka.util.Platform.isMediaKeysPolyfilled()) {
2699
+ throw new shaka.util.Error(
2700
+ shaka.util.Error.Severity.CRITICAL,
2701
+ shaka.util.Error.Category.MANIFEST,
2702
+ shaka.util.Error.Code
2703
+ .HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED);
2704
+ }
2705
+
2614
2706
  /*
2615
2707
  * Even if we're not able to construct initData through the HLS tag, adding
2616
2708
  * a DRMInfo will allow DRM Engine to request a media key system access