shaka-player 2.5.4 → 2.5.5

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 (102) hide show
  1. package/.csslintrc +13 -0
  2. package/CHANGELOG.md +56 -0
  3. package/build/build.py +4 -1
  4. package/build/check.py +17 -0
  5. package/build/compiler.py +40 -0
  6. package/build/types/core +1 -0
  7. package/build/types/ui +4 -1
  8. package/demo/asset_card.js +41 -11
  9. package/demo/cast_receiver/receiver_app.css +19 -13
  10. package/demo/close_button.js +0 -6
  11. package/demo/common/asset.js +6 -1
  12. package/demo/common/assets.js +72 -14
  13. package/demo/demo.less +105 -16
  14. package/demo/index.html +8 -5
  15. package/demo/load.js +5 -7
  16. package/demo/main.js +81 -16
  17. package/dist/.csslintstamp +0 -0
  18. package/dist/controls.css +1 -1
  19. package/dist/controls.css.map +1 -1
  20. package/dist/demo.compiled.debug.js +46 -39
  21. package/dist/demo.compiled.debug.map +3 -3
  22. package/dist/demo.compiled.js +46 -39
  23. package/dist/demo.compiled.map +3 -3
  24. package/dist/demo.css +1 -1
  25. package/dist/demo.css.map +1 -1
  26. package/dist/deps.js +11 -7
  27. package/dist/receiver.compiled.debug.js +10 -6
  28. package/dist/receiver.compiled.debug.map +3 -3
  29. package/dist/receiver.compiled.js +10 -6
  30. package/dist/receiver.compiled.map +3 -3
  31. package/dist/shaka-player.compiled.debug.externs.js +123 -92
  32. package/dist/shaka-player.compiled.debug.js +113 -105
  33. package/dist/shaka-player.compiled.debug.map +4 -4
  34. package/dist/shaka-player.compiled.externs.js +123 -92
  35. package/dist/shaka-player.compiled.js +551 -548
  36. package/dist/shaka-player.compiled.map +4 -4
  37. package/dist/shaka-player.ui.debug.externs.js +170 -101
  38. package/dist/shaka-player.ui.debug.js +187 -188
  39. package/dist/shaka-player.ui.debug.map +4 -4
  40. package/dist/shaka-player.ui.externs.js +170 -101
  41. package/dist/shaka-player.ui.js +651 -651
  42. package/dist/shaka-player.ui.map +3 -3
  43. package/docs/tutorials/drm-config.md +1 -0
  44. package/docs/tutorials/fairplay.md +77 -0
  45. package/docs/tutorials/index.json +1 -0
  46. package/docs/tutorials/ui-customization.md +3 -0
  47. package/docs/tutorials/ui.md +3 -1
  48. package/externs/mediasession.js +16 -1
  49. package/externs/shaka/net.js +5 -1
  50. package/externs/shaka/player.js +136 -90
  51. package/externs/webkitmediakeys.js +4 -0
  52. package/lib/dash/content_protection.js +33 -1
  53. package/lib/dash/dash_parser.js +12 -4
  54. package/lib/hls/hls_parser.js +20 -4
  55. package/lib/media/drm_engine.js +29 -2
  56. package/lib/media/media_source_engine.js +7 -3
  57. package/lib/media/playhead.js +8 -3
  58. package/lib/media/streaming_engine.js +24 -8
  59. package/lib/media/video_wrapper.js +8 -3
  60. package/lib/net/networking_engine.js +1 -0
  61. package/lib/player.js +3 -2
  62. package/lib/polyfill/patchedmediakeys_apple.js +2 -72
  63. package/lib/util/error.js +12 -0
  64. package/lib/util/fairplay_utils.js +122 -0
  65. package/lib/util/player_configuration.js +5 -0
  66. package/lib/util/pssh.js +41 -0
  67. package/package.json +4 -3
  68. package/shaka-player.uncompiled.js +5 -1
  69. package/test/dash/dash_parser_content_protection_unit.js +35 -1
  70. package/test/dash/dash_parser_manifest_unit.js +31 -0
  71. package/test/hls/hls_live_unit.js +7 -1
  72. package/test/hls/hls_parser_unit.js +50 -0
  73. package/test/media/media_source_engine_integration.js +12 -0
  74. package/test/media/playhead_unit.js +2 -0
  75. package/test/player_external.js +3 -2
  76. package/test/player_integration.js +75 -0
  77. package/test/test/externs/jasmine.js +6 -0
  78. package/test/test/util/test_scheme.js +32 -1
  79. package/test/test/util/ui_utils.js +41 -0
  80. package/test/ui/ui_unit.js +129 -3
  81. package/ui/audio_language_selection.js +17 -96
  82. package/ui/big_play_button.js +61 -0
  83. package/ui/controls.js +111 -86
  84. package/ui/controls.less +3 -5
  85. package/ui/externs/ui.js +14 -2
  86. package/ui/fullscreen_button.js +2 -52
  87. package/ui/less/buttons.less +8 -8
  88. package/ui/less/containers.less +21 -21
  89. package/ui/less/general.less +11 -0
  90. package/ui/less/other_elements.less +2 -1
  91. package/ui/less/overflow_menu.less +11 -10
  92. package/ui/less/range_elements.less +6 -0
  93. package/ui/less/spinner.less +2 -0
  94. package/ui/pip_button.js +1 -1
  95. package/ui/{play_pause_button.js → play_button.js} +17 -47
  96. package/ui/resolution_selection.js +41 -104
  97. package/ui/seek_bar.js +15 -17
  98. package/ui/settings_menu.js +113 -0
  99. package/ui/small_play_button.js +73 -0
  100. package/ui/text_displayer.js +2 -2
  101. package/ui/text_selection.js +39 -105
  102. package/ui/ui.js +12 -0
@@ -21,6 +21,7 @@ goog.require('goog.asserts');
21
21
  goog.require('shaka.log');
22
22
  goog.require('shaka.util.Error');
23
23
  goog.require('shaka.util.ManifestParserUtils');
24
+ goog.require('shaka.util.Pssh');
24
25
  goog.require('shaka.util.Uint8ArrayUtils');
25
26
  goog.require('shaka.util.XmlUtils');
26
27
 
@@ -442,6 +443,36 @@ shaka.dash.ContentProtection.getPlayReadyLicenseUrl = function(element) {
442
443
  };
443
444
 
444
445
 
446
+ /**
447
+ * Gets a PlayReady initData from a content protection element
448
+ * containing a PlayReady Pro Object
449
+ *
450
+ * @param {shaka.dash.ContentProtection.Element} element
451
+ * @return {?Array.<shaka.extern.InitDataOverride>}
452
+ * @private
453
+ */
454
+ shaka.dash.ContentProtection.getInitDataFromPro_ = function(element) {
455
+ const proNode = shaka.util.XmlUtils.findChildNS(
456
+ element.node, 'urn:microsoft:playready', 'pro');
457
+ if (!proNode) {
458
+ return null;
459
+ }
460
+ const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
461
+ const data = Uint8ArrayUtils.fromBase64(proNode.textContent);
462
+ const systemId = new Uint8Array([
463
+ 0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86,
464
+ 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95,
465
+ ]);
466
+ const pssh = shaka.util.Pssh.createPssh(data, systemId);
467
+ return [
468
+ {
469
+ initData: pssh,
470
+ initDataType: 'cenc',
471
+ keyId: element.keyId,
472
+ },
473
+ ];
474
+ };
475
+
445
476
  /**
446
477
  * Creates DrmInfo objects from the given element.
447
478
  *
@@ -468,7 +499,8 @@ shaka.dash.ContentProtection.convertElements_ = function(
468
499
  !element.init || element.init.length,
469
500
  'Init data must be null or non-empty.');
470
501
 
471
- const initData = element.init || defaultInit;
502
+ const proInitData = ContentProtection.getInitDataFromPro_(element);
503
+ const initData = element.init || defaultInit || proInitData;
472
504
  const info = ManifestParserUtils.createDrmInfo(keySystem, initData);
473
505
  const licenseParser = licenseUrlParsers.get(keySystem);
474
506
  if (licenseParser) {
@@ -1016,7 +1016,7 @@ shaka.dash.DashParser.prototype.parseAdaptationSet_ = function(context, elem) {
1016
1016
  case 'subtitle':
1017
1017
  kind = value;
1018
1018
  break;
1019
- }
1019
+ }
1020
1020
  }
1021
1021
  }
1022
1022
 
@@ -1038,12 +1038,12 @@ shaka.dash.DashParser.prototype.parseAdaptationSet_ = function(context, elem) {
1038
1038
  let closedCaptions = new Map();
1039
1039
  for (const prop of accessibilities) {
1040
1040
  let schemeId = prop.getAttribute('schemeIdUri');
1041
+ let value = prop.getAttribute('value');
1041
1042
  if (schemeId == 'urn:scte:dash:cc:cea-608:2015' ||
1042
1043
  schemeId == 'urn:scte:dash:cc:cea-708:2015') {
1043
1044
  let channelId = 1;
1044
- let closedCaptionsValue = prop.getAttribute('value');
1045
- if (closedCaptionsValue != null) {
1046
- closedCaptionsValue.split(';').forEach((captionStr) => {
1045
+ if (value != null) {
1046
+ value.split(';').forEach((captionStr) => {
1047
1047
  let channel;
1048
1048
  let language;
1049
1049
  // Some closed caption descriptions have channel number and language,
@@ -1075,6 +1075,14 @@ shaka.dash.DashParser.prototype.parseAdaptationSet_ = function(context, elem) {
1075
1075
  // 'CC1' as channel id and 'und' as language info.
1076
1076
  closedCaptions.set('CC1', 'und');
1077
1077
  }
1078
+ } else if (schemeId == 'urn:mpeg:dash:role:2011') {
1079
+ // See DASH IOP 3.9.2 Table 4.
1080
+ if (value != null) {
1081
+ roleValues.push(value);
1082
+ if (value == 'captions') {
1083
+ kind = ManifestParserUtils.TextStreamKind.CLOSED_CAPTION;
1084
+ }
1085
+ }
1078
1086
  }
1079
1087
  }
1080
1088
 
@@ -539,8 +539,15 @@ shaka.hls.HlsParser.prototype.createPeriod_ = async function(playlist) {
539
539
  return type == 'SUBTITLES';
540
540
  }.bind(this));
541
541
 
542
- let textStreamPromises = textStreamTags.map(function(tag) {
543
- return this.createTextStream_(tag, playlist);
542
+ let textStreamPromises = textStreamTags.map(async function(tag) {
543
+ try {
544
+ return await this.createTextStream_(tag, playlist);
545
+ } catch (e) {
546
+ if (this.config_.hls.ignoreTextStreamFailures) {
547
+ return null;
548
+ }
549
+ throw e;
550
+ }
544
551
  }.bind(this));
545
552
 
546
553
  const closedCaptionsTags = mediaTags.filter((tag) => {
@@ -567,7 +574,7 @@ shaka.hls.HlsParser.prototype.createPeriod_ = async function(playlist) {
567
574
  return {
568
575
  startTime: 0,
569
576
  variants: variants,
570
- textStreams: textStreams,
577
+ textStreams: textStreams.filter((t) => t != null),
571
578
  };
572
579
  };
573
580
 
@@ -660,7 +667,9 @@ shaka.hls.HlsParser.prototype.createVariantsForTag_ =
660
667
  // attached to the stream.
661
668
  const textStreamInfo =
662
669
  this.mediaTagsToStreamInfosMap_.get(textTags[0].id);
663
- textStreamInfo.stream.codecs = textCodecs;
670
+ if (textStreamInfo) {
671
+ textStreamInfo.stream.codecs = textCodecs;
672
+ }
664
673
  }
665
674
  }
666
675
 
@@ -1672,6 +1681,13 @@ shaka.hls.HlsParser.prototype.getStartTime_ = async function(
1672
1681
  return this.getStartTimeFromTextSegment_(mimeType, codecs, response.data);
1673
1682
  }
1674
1683
 
1684
+ if (mimeType == 'video/webm') {
1685
+ shaka.log.warning(
1686
+ 'Hls+WebM combination is not supported at the moment. Skipping.');
1687
+ return null;
1688
+ }
1689
+
1690
+
1675
1691
  // TODO(vaage): Add support for additional formats.
1676
1692
  //
1677
1693
  // Formats:
@@ -22,6 +22,7 @@ goog.require('shaka.log');
22
22
  goog.require('shaka.net.NetworkingEngine');
23
23
  goog.require('shaka.util.Error');
24
24
  goog.require('shaka.util.EventManager');
25
+ goog.require('shaka.util.FairPlayUtils');
25
26
  goog.require('shaka.util.FakeEvent');
26
27
  goog.require('shaka.util.IDestroyable');
27
28
  goog.require('shaka.util.Iterables');
@@ -1115,6 +1116,29 @@ shaka.media.DrmEngine.prototype.createTemporarySession_ =
1115
1116
  };
1116
1117
  this.activeSessions_.set(session, metadata);
1117
1118
 
1119
+ try {
1120
+ if (this.config_.initDataTransform) {
1121
+ initData = this.config_.initDataTransform(initData);
1122
+ } else if (this.keySystem().startsWith('com.apple.fps')) {
1123
+ const cert = this.currentDrmInfo_.serverCertificate;
1124
+ const contentId =
1125
+ shaka.util.FairPlayUtils.defaultGetContentId(initData);
1126
+ initData = shaka.util.FairPlayUtils.initDataTransform(
1127
+ initData, contentId, cert);
1128
+ }
1129
+ } catch (error) {
1130
+ let shakaError = error;
1131
+ if (!(error instanceof shaka.util.Error)) {
1132
+ shakaError = new shaka.util.Error(
1133
+ shaka.util.Error.Severity.CRITICAL,
1134
+ shaka.util.Error.Category.DRM,
1135
+ shaka.util.Error.Code.INIT_DATA_TRANSFORM_ERROR,
1136
+ error);
1137
+ }
1138
+ this.onError_(shakaError);
1139
+ return;
1140
+ }
1141
+
1118
1142
  session.generateRequest(initDataType, initData.buffer).catch((error) => {
1119
1143
  if (this.isDestroying_) { return; }
1120
1144
 
@@ -1186,13 +1210,15 @@ shaka.media.DrmEngine.prototype.sendLicenseRequest_ = function(event) {
1186
1210
  request.body = event.message;
1187
1211
  request.method = 'POST';
1188
1212
  request.licenseRequestType = event.messageType;
1213
+ request.sessionId = session.sessionId;
1189
1214
  // NOTE: allowCrossSiteCredentials can be set in a request filter.
1190
1215
 
1191
1216
  if (this.currentDrmInfo_.keySystem == 'com.microsoft.playready' ||
1192
1217
  this.currentDrmInfo_.keySystem == 'com.chromecast.playready') {
1193
1218
  this.unpackPlayReadyRequest_(request);
1194
1219
  }
1195
- if (this.currentDrmInfo_.keySystem.startsWith('com.apple.fps')) {
1220
+ if (this.currentDrmInfo_.keySystem.startsWith('com.apple.fps') &&
1221
+ this.config_.fairPlayTransform) {
1196
1222
  this.formatFairPlayRequest_(request);
1197
1223
  }
1198
1224
 
@@ -1200,7 +1226,8 @@ shaka.media.DrmEngine.prototype.sendLicenseRequest_ = function(event) {
1200
1226
  .then(function(response) {
1201
1227
  if (this.isDestroying_) { return Promise.reject(); }
1202
1228
 
1203
- if (this.currentDrmInfo_.keySystem.startsWith('com.apple.fps')) {
1229
+ if (this.currentDrmInfo_.keySystem.startsWith('com.apple.fps') &&
1230
+ this.config_.fairPlayTransform) {
1204
1231
  this.parseFairPlayResponse_(response);
1205
1232
  }
1206
1233
 
@@ -704,9 +704,13 @@ shaka.media.MediaSourceEngine.prototype.setStreamProperties = function(
704
704
  */
705
705
  shaka.media.MediaSourceEngine.prototype.endOfStream = function(reason) {
706
706
  return this.enqueueBlockingOperation_(function() {
707
- // Chrome won't let us pass undefined, but it will let us omit the
708
- // argument. Firefox does not have this problem.
709
- // TODO: File a bug about this.
707
+ // If endOfStream() has already been called on the media source,
708
+ // don't call it again.
709
+ if (this.ended()) {
710
+ return;
711
+ }
712
+ // Tizen and IE11 won't let us pass undefined, but it will let us omit the
713
+ // argument.
710
714
  if (reason) {
711
715
  this.mediaSource_.endOfStream(reason);
712
716
  } else {
@@ -86,9 +86,14 @@ shaka.media.SrcEqualsPlayhead = class {
86
86
  // We listen for the loaded-metadata-event so that we know when we can
87
87
  // interact with |currentTime|.
88
88
  const onLoaded = () => {
89
- this.started_ = true;
90
-
91
- if (this.startTime_ != null) {
89
+ if (this.startTime_ == null) {
90
+ this.started_ = true;
91
+ } else {
92
+ // Startup is complete only when the video element acknowledges the
93
+ // seek.
94
+ this.eventManager_.listenOnce(this.mediaElement_, 'seeking', () => {
95
+ this.started_ = true;
96
+ });
92
97
  this.mediaElement_.currentTime = this.startTime_;
93
98
  }
94
99
  };
@@ -1339,11 +1339,7 @@ shaka.media.StreamingEngine.prototype.update_ = function(mediaState) {
1339
1339
  // If it's a text stream and the original id starts with 'CC', it's CEA
1340
1340
  // closed captions. Do not schedule update for closed captions text
1341
1341
  // mediastate, since closed captions are embedded in video streams.
1342
- const isCaptionState = (state) => {
1343
- return state.type == ContentType.TEXT &&
1344
- state.stream.mimeType == shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE;
1345
- };
1346
- if (isCaptionState(mediaState)) {
1342
+ if (shaka.media.StreamingEngine.isEmbeddedText_(mediaState)) {
1347
1343
  this.playerInterface_.mediaSourceEngine.setSelectedClosedCaptionId(
1348
1344
  mediaState.stream.originalId || '');
1349
1345
  return null;
@@ -1443,7 +1439,9 @@ shaka.media.StreamingEngine.prototype.update_ = function(mediaState) {
1443
1439
  mediaStates.forEach((otherState) => {
1444
1440
  // Do not consider embedded captions in this calculation. It could lead
1445
1441
  // to hangs in streaming.
1446
- if (isCaptionState(otherState)) return;
1442
+ if (shaka.media.StreamingEngine.isEmbeddedText_(otherState)) {
1443
+ return;
1444
+ }
1447
1445
 
1448
1446
  const timeNeeded = this.getTimeNeeded_(otherState, presentationTime);
1449
1447
  minTimeNeeded = Math.min(minTimeNeeded, timeNeeded);
@@ -2199,13 +2197,15 @@ shaka.media.StreamingEngine.prototype.handlePeriodTransition_ = function(
2199
2197
  goog.asserts.assert(
2200
2198
  mediaStates.every(function(ms) {
2201
2199
  return ms.needPeriodIndex == needPeriodIndex || ms.hasError ||
2202
- !shaka.media.StreamingEngine.isIdle_(ms);
2200
+ !shaka.media.StreamingEngine.isIdle_(ms) ||
2201
+ shaka.media.StreamingEngine.isEmbeddedText_(ms);
2203
2202
  }),
2204
2203
  'All MediaStates should need the same Period or be performing updates.');
2205
2204
 
2206
2205
  // Only call onChooseStreams() when all MediaStates need the same Period.
2207
2206
  let needSamePeriod = mediaStates.every(function(ms) {
2208
- return ms.needPeriodIndex == needPeriodIndex;
2207
+ return ms.needPeriodIndex == needPeriodIndex ||
2208
+ shaka.media.StreamingEngine.isEmbeddedText_(ms);
2209
2209
  });
2210
2210
  if (!needSamePeriod) {
2211
2211
  shaka.log.debug(
@@ -2239,6 +2239,10 @@ shaka.media.StreamingEngine.prototype.handlePeriodTransition_ = function(
2239
2239
  let allReady = mediaStates.every(function(ms) {
2240
2240
  let isIdle = shaka.media.StreamingEngine.isIdle_(ms);
2241
2241
  let currentPeriodIndex = this.findPeriodContainingStream_(ms.stream);
2242
+ if (shaka.media.StreamingEngine.isEmbeddedText_(ms)) {
2243
+ // Embedded text tracks don't do Period transitions.
2244
+ return true;
2245
+ }
2242
2246
  return isIdle && ms.needPeriodIndex == needPeriodIndex &&
2243
2247
  currentPeriodIndex != needPeriodIndex;
2244
2248
  }.bind(this));
@@ -2325,6 +2329,18 @@ shaka.media.StreamingEngine.prototype.handlePeriodTransition_ = function(
2325
2329
  };
2326
2330
 
2327
2331
 
2332
+ /**
2333
+ * @param {shaka.media.StreamingEngine.MediaState_} mediaState
2334
+ * @return {boolean}
2335
+ * @private
2336
+ */
2337
+ shaka.media.StreamingEngine.isEmbeddedText_ = function(mediaState) {
2338
+ const MimeUtils = shaka.util.MimeUtils;
2339
+ return mediaState.type == shaka.util.ManifestParserUtils.ContentType.TEXT &&
2340
+ mediaState.stream.mimeType == MimeUtils.CLOSED_CAPTION_MIMETYPE;
2341
+ };
2342
+
2343
+
2328
2344
  /**
2329
2345
  * @param {shaka.media.StreamingEngine.MediaState_} mediaState
2330
2346
  * @return {boolean} True if the given MediaState is idle; otherwise, return
@@ -49,6 +49,9 @@ shaka.media.VideoWrapper = function(video, onSeek, startTime) {
49
49
  /** @private {number} */
50
50
  this.startTime_ = startTime;
51
51
 
52
+ /** @private {boolean} */
53
+ this.started_ = false;
54
+
52
55
  /** @private {shaka.util.EventManager} */
53
56
  this.eventManager_ = new shaka.util.EventManager();
54
57
 
@@ -92,9 +95,7 @@ shaka.media.VideoWrapper.prototype.release = function() {
92
95
  * @return {number}
93
96
  */
94
97
  shaka.media.VideoWrapper.prototype.getTime = function() {
95
- return this.video_.readyState > 0 ?
96
- this.video_.currentTime :
97
- this.startTime_;
98
+ return this.started_ ? this.video_.currentTime : this.startTime_;
98
99
  };
99
100
 
100
101
 
@@ -186,6 +187,10 @@ shaka.media.VideoWrapper.prototype.startListeningToSeeks_ = function() {
186
187
  this.video_.readyState > 0,
187
188
  'The media element should be ready before we listen for seeking.');
188
189
 
190
+ // Now that any startup seeking is complete, we can trust the video element
191
+ // for currentTime.
192
+ this.started_ = true;
193
+
189
194
  this.eventManager_.listen(this.video_, 'seeking', () => this.onSeek_());
190
195
  };
191
196
 
@@ -282,6 +282,7 @@ shaka.net.NetworkingEngine.makeRequest = function(uris, retryParams) {
282
282
  allowCrossSiteCredentials: false,
283
283
  retryParameters: retryParams,
284
284
  licenseRequestType: null,
285
+ sessionId: null,
285
286
  };
286
287
  };
287
288
 
package/lib/player.js CHANGED
@@ -429,7 +429,7 @@ shaka.Player.prototype.destroy = async function() {
429
429
  * @define {string} A version number taken from git at compile time.
430
430
  * @export
431
431
  */
432
- shaka.Player.version = 'v2.5.4-uncompiled';
432
+ shaka.Player.version = 'v2.5.5-uncompiled';
433
433
 
434
434
  // Initialize the deprecation system using the version string we just set
435
435
  // on the player.
@@ -1728,7 +1728,8 @@ shaka.Player.prototype.onLoad_ = async function(has, wants) {
1728
1728
  // active one, update abr manager with filtered variants for the current
1729
1729
  // period.
1730
1730
  /** @type {shaka.extern.Period} */
1731
- const currentPeriod = this.getPresentationPeriod_();
1731
+ const currentPeriod =
1732
+ this.getPresentationPeriod_() || this.manifest_.periods[0];
1732
1733
  const hasPrimary = currentPeriod.variants.some((v) => v.primary);
1733
1734
 
1734
1735
  if (!this.config_.preferredAudioLanguage && !hasPrimary) {
@@ -17,7 +17,6 @@
17
17
 
18
18
  goog.provide('shaka.polyfill.PatchedMediaKeysApple');
19
19
 
20
- goog.require('goog.Uri');
21
20
  goog.require('goog.asserts');
22
21
  goog.require('shaka.log');
23
22
  goog.require('shaka.polyfill.register');
@@ -25,7 +24,6 @@ goog.require('shaka.util.EventManager');
25
24
  goog.require('shaka.util.FakeEvent');
26
25
  goog.require('shaka.util.FakeEventTarget');
27
26
  goog.require('shaka.util.PublicPromise');
28
- goog.require('shaka.util.StringUtils');
29
27
  goog.require('shaka.util.Uint8ArrayUtils');
30
28
 
31
29
 
@@ -455,6 +453,7 @@ shaka.polyfill.PatchedMediaKeysApple.MediaKeySession.prototype.
455
453
  // initData into a Uint8Array.
456
454
  this.nativeMediaKeySession_ = this.nativeMediaKeys_.createSession(
457
455
  'video/mp4', new Uint8Array(initData));
456
+ this.sessionId = this.nativeMediaKeySession_.sessionId || '';
458
457
 
459
458
  // Attach session event handlers here.
460
459
  this.eventManager_.listen(this.nativeMediaKeySession_, 'webkitkeymessage',
@@ -533,71 +532,6 @@ shaka.polyfill.PatchedMediaKeysApple.MediaKeySession.prototype.
533
532
  };
534
533
 
535
534
 
536
- /**
537
- * Rebuild FairPlay init data according to Apple's docs. It's unclear why this
538
- * is not done by the browser, or if the unprefixed version will do it for us.
539
- *
540
- * @param {BufferSource} initData
541
- * @param {BufferSource} certificate
542
- * @return {BufferSource}
543
- */
544
- shaka.polyfill.PatchedMediaKeysApple.rebuildInitData_ =
545
- function(initData, certificate) {
546
- // TODO: Move this into DrmEngine if it is still needed with unprefixed EME.
547
- // FairPlay init data is in two parts and must be processed a bit.
548
- // The first part is a 4 byte little-endian int, which is the length of the
549
- // second part.
550
- const initDataArray = new Uint8Array(initData);
551
- const dataview = new DataView(initDataArray.buffer);
552
- const length = dataview.getUint32(
553
- /* position= */ 0, /* littleEndian= */ true);
554
- if (length + 4 != initDataArray.byteLength) {
555
- throw new Error('Malformed init data!');
556
- }
557
-
558
- // The second part is a UTF-16 LE URI from the manifest.
559
- const uriString = shaka.util.StringUtils.fromUTF16(
560
- initDataArray.slice(4), /* littleEndian= */ true);
561
-
562
- // The domain of that URI is the content ID according to Apple's FPS sample.
563
- const uri = new goog.Uri(uriString);
564
- const contentId = uri.getDomain();
565
-
566
- // From that, we build a new init data to use in the session. This is
567
- // composed of several parts. First, the raw init data we already got.
568
- // Second, a 4-byte LE length followed by the content ID in UTF-16-LE.
569
- // Third, a 4-byte LE length followed by the certificate.
570
- const contentIdArray = new Uint8Array(
571
- shaka.util.StringUtils.toUTF16(contentId, /* littleEndian= */ true));
572
-
573
- const rebuiltInitData = new Uint8Array(
574
- initDataArray.byteLength +
575
- 4 + contentIdArray.byteLength +
576
- 4 + certificate.byteLength);
577
-
578
- let offset = 0;
579
- /** @param {!Uint8Array} array */
580
- const append = (array) => {
581
- rebuiltInitData.set(array, offset);
582
- offset += array.byteLength;
583
- };
584
- /** @param {!Uint8Array} array */
585
- const appendWithLength = (array) => {
586
- const view = new DataView(rebuiltInitData.buffer);
587
- const value = array.byteLength;
588
- view.setUint32(offset, value, /* littleEndian= */ true);
589
- offset += 4;
590
- append(array);
591
- };
592
-
593
- append(initDataArray);
594
- appendWithLength(contentIdArray);
595
- appendWithLength(new Uint8Array(certificate));
596
-
597
- return rebuiltInitData;
598
- };
599
-
600
-
601
535
  /**
602
536
  * Handler for the native media elements webkitneedkey event.
603
537
  *
@@ -617,16 +551,12 @@ shaka.polyfill.PatchedMediaKeysApple.onWebkitNeedKey_ = function(event) {
617
551
 
618
552
  goog.asserts.assert(event.initData != null, 'missing init data!');
619
553
 
620
- const certificate = mediaKeys.certificate;
621
- goog.asserts.assert(certificate != null, 'missing certificate!');
622
-
623
554
  // NOTE: Because "this" is a real EventTarget, the event we dispatch here must
624
555
  // also be a real Event.
625
556
  const event2 = new Event('encrypted');
626
557
  // TODO: validate this initDataType against the unprefixed version
627
558
  event2.initDataType = 'cenc';
628
- event2.initData = PatchedMediaKeysApple.rebuildInitData_(
629
- event.initData, certificate);
559
+ event2.initData = event.initData;
630
560
 
631
561
  this.dispatchEvent(event2);
632
562
  };
package/lib/util/error.js CHANGED
@@ -733,6 +733,18 @@ shaka.util.Error.Code = {
733
733
  */
734
734
  'EXPIRED': 6014,
735
735
 
736
+ /**
737
+ * A server certificate wasn't given when it is required. FairPlay requires
738
+ * setting an explicit server certificate in the configuration.
739
+ */
740
+ 'SERVER_CERTIFICATE_REQUIRED': 6015,
741
+
742
+ /**
743
+ * An error was thrown while executing the init data transformation.
744
+ * <br> error.data[0] is the original error.
745
+ */
746
+ 'INIT_DATA_TRANSFORM_ERROR': 6016,
747
+
736
748
 
737
749
  /**
738
750
  * The call to Player.load() was interrupted by a call to Player.unload()
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2016 Google Inc.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ goog.provide('shaka.util.FairPlayUtils');
19
+
20
+ goog.require('goog.Uri');
21
+
22
+
23
+ /**
24
+ * @summary A set of FairPlay utility functions.
25
+ * @exportInterface
26
+ */
27
+ shaka.util.FairPlayUtils = class {
28
+ /**
29
+ * Using the default method, extract a content ID from the init data. This is
30
+ * based on the FairPlay example documentation.
31
+ *
32
+ * @param {!BufferSource} initData
33
+ * @return {string}
34
+ * @export
35
+ */
36
+ static defaultGetContentId(initData) {
37
+ const uint8 = new Uint8Array(initData);
38
+ const dataview = new DataView(
39
+ uint8.buffer, uint8.byteOffset, uint8.byteLength);
40
+ // The first part is a 4 byte little-endian int, which is the length of
41
+ // the second part.
42
+ const length = dataview.getUint32(
43
+ /* position= */ 0, /* littleEndian= */ true);
44
+ if (length + 4 != uint8.byteLength) {
45
+ throw new RangeError('Malformed FairPlay init data');
46
+ }
47
+
48
+ // The second part is a UTF-16 LE URI from the manifest.
49
+ const uriString = shaka.util.StringUtils.fromUTF16(
50
+ uint8.subarray(4), /* littleEndian= */ true);
51
+
52
+ // The domain of that URI is the content ID according to Apple's FPS
53
+ // sample.
54
+ const uri = new goog.Uri(uriString);
55
+ return uri.getDomain();
56
+ }
57
+
58
+ /**
59
+ * Transforms the init data buffer using the given data. The format is:
60
+ *
61
+ * <pre>
62
+ * [4 bytes] initDataSize
63
+ * [initDataSize bytes] initData
64
+ * [4 bytes] contentIdSize
65
+ * [contentIdSize bytes] contentId
66
+ * [4 bytes] certSize
67
+ * [certSize bytes] cert
68
+ * </pre>
69
+ *
70
+ * @param {!BufferSource} initData
71
+ * @param {!BufferSource|string} contentId
72
+ * @param {?BufferSource} cert The server certificate; this will throw if not
73
+ * provided.
74
+ * @return {!Uint8Array}
75
+ * @export
76
+ */
77
+ static initDataTransform(initData, contentId, cert) {
78
+ if (!cert || !cert.byteLength) {
79
+ throw new shaka.util.Error(
80
+ shaka.util.Error.Severity.CRITICAL,
81
+ shaka.util.Error.Category.DRM,
82
+ shaka.util.Error.Code.SERVER_CERTIFICATE_REQUIRED);
83
+ }
84
+
85
+ // From that, we build a new init data to use in the session. This is
86
+ // composed of several parts. First, the raw init data we already got.
87
+ // Second, a 4-byte LE length followed by the content ID in UTF-16-LE.
88
+ // Third, a 4-byte LE length followed by the certificate.
89
+ /** @type {!Uint8Array} */
90
+ let contentIdArray;
91
+ if (typeof contentId == 'string') {
92
+ contentIdArray = new Uint8Array(
93
+ shaka.util.StringUtils.toUTF16(contentId, /* littleEndian= */ true));
94
+ } else {
95
+ contentIdArray = new Uint8Array(contentId);
96
+ }
97
+
98
+ const rebuiltInitData = new Uint8Array(
99
+ 8 + initData.byteLength + contentIdArray.byteLength + cert.byteLength);
100
+
101
+ let offset = 0;
102
+ /** @param {!Uint8Array} array */
103
+ const append = (array) => {
104
+ rebuiltInitData.set(array, offset);
105
+ offset += array.byteLength;
106
+ };
107
+ /** @param {!Uint8Array} array */
108
+ const appendWithLength = (array) => {
109
+ const view = new DataView(rebuiltInitData.buffer);
110
+ const value = array.byteLength;
111
+ view.setUint32(offset, value, /* littleEndian= */ true);
112
+ offset += 4;
113
+ append(array);
114
+ };
115
+
116
+ append(new Uint8Array(initData));
117
+ appendWithLength(contentIdArray);
118
+ appendWithLength(new Uint8Array(cert));
119
+
120
+ return rebuiltInitData;
121
+ }
122
+ };
@@ -83,6 +83,8 @@ shaka.util.PlayerConfiguration = class {
83
83
  clearKeys: {}, // key is arbitrary key system ID, value must be string
84
84
  advanced: {}, // key is arbitrary key system ID, value is a record type
85
85
  delayLicenseRequestUntilPlayed: false,
86
+ initDataTransform: (initData) => initData,
87
+ fairPlayTransform: true,
86
88
  };
87
89
 
88
90
  const manifest = {
@@ -102,6 +104,9 @@ shaka.util.PlayerConfiguration = class {
102
104
  ignoreMinBufferTime: false,
103
105
  autoCorrectDrift: true,
104
106
  },
107
+ hls: {
108
+ ignoreTextStreamFailures: false,
109
+ },
105
110
  };
106
111
 
107
112
  const streaming = {