shaka-player 4.7.12 → 4.8.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 (120) hide show
  1. package/AUTHORS +1 -0
  2. package/CONTRIBUTORS +1 -0
  3. package/LICENSE +54 -0
  4. package/README.md +22 -1
  5. package/dist/controls.css +1 -1
  6. package/dist/controls.css.map +1 -1
  7. package/dist/deps.js +36 -25
  8. package/dist/locales.js +40 -30
  9. package/dist/shaka-player.compiled.d.ts +196 -46
  10. package/dist/shaka-player.compiled.debug.d.ts +196 -46
  11. package/dist/shaka-player.compiled.debug.externs.js +474 -337
  12. package/dist/shaka-player.compiled.debug.js +992 -871
  13. package/dist/shaka-player.compiled.debug.map +4 -4
  14. package/dist/shaka-player.compiled.externs.js +474 -337
  15. package/dist/shaka-player.compiled.js +1248 -1174
  16. package/dist/shaka-player.compiled.map +4 -4
  17. package/dist/shaka-player.ui.d.ts +259 -49
  18. package/dist/shaka-player.ui.debug.d.ts +259 -49
  19. package/dist/shaka-player.ui.debug.externs.js +591 -376
  20. package/dist/shaka-player.ui.debug.js +1208 -1003
  21. package/dist/shaka-player.ui.debug.map +4 -4
  22. package/dist/shaka-player.ui.externs.js +591 -376
  23. package/dist/shaka-player.ui.js +1533 -1408
  24. package/dist/shaka-player.ui.map +4 -4
  25. package/externs/device_sensor_event.js +18 -0
  26. package/externs/hisense.js +16 -0
  27. package/externs/playstation.js +31 -0
  28. package/externs/shaka/abr_manager.js +3 -1
  29. package/externs/shaka/ads.js +5 -0
  30. package/externs/shaka/codecs.js +13 -0
  31. package/externs/shaka/manifest.js +69 -7
  32. package/externs/shaka/net.js +13 -1
  33. package/externs/shaka/offline.js +7 -1
  34. package/externs/shaka/player.js +184 -13
  35. package/externs/shaka/text.js +19 -0
  36. package/externs/tizen.js +41 -0
  37. package/externs/webos.js +19 -0
  38. package/externs/xbox.js +43 -0
  39. package/lib/abr/simple_abr_manager.js +9 -1
  40. package/lib/ads/ad_manager.js +19 -2
  41. package/lib/ads/client_side_ad_manager.js +5 -3
  42. package/lib/ads/media_tailor_ad_manager.js +9 -0
  43. package/lib/cast/cast_receiver.js +0 -14
  44. package/lib/cast/cast_utils.js +1 -0
  45. package/lib/cea/mp4_cea_parser.js +6 -1
  46. package/lib/dash/content_protection.js +97 -53
  47. package/lib/dash/dash_parser.js +240 -179
  48. package/lib/dash/mpd_utils.js +72 -65
  49. package/lib/dash/segment_base.js +35 -22
  50. package/lib/dash/segment_list.js +39 -31
  51. package/lib/dash/segment_template.js +47 -23
  52. package/lib/hls/hls_parser.js +376 -165
  53. package/lib/media/buffering_observer.js +25 -1
  54. package/lib/media/drm_engine.js +91 -71
  55. package/lib/media/manifest_filterer.js +220 -0
  56. package/lib/media/media_source_engine.js +108 -29
  57. package/lib/media/playhead.js +3 -3
  58. package/lib/media/preload_manager.js +827 -0
  59. package/lib/media/presentation_timeline.js +11 -0
  60. package/lib/media/segment_index.js +60 -49
  61. package/lib/media/segment_prefetch.js +140 -43
  62. package/lib/media/segment_reference.js +31 -8
  63. package/lib/media/segment_utils.js +208 -25
  64. package/lib/media/streaming_engine.js +321 -77
  65. package/lib/mss/content_protection.js +31 -25
  66. package/lib/mss/mss_parser.js +78 -55
  67. package/lib/net/networking_engine.js +49 -4
  68. package/lib/offline/indexeddb/v1_storage_cell.js +2 -0
  69. package/lib/offline/indexeddb/v2_storage_cell.js +2 -0
  70. package/lib/offline/manifest_converter.js +10 -3
  71. package/lib/offline/storage.js +5 -3
  72. package/lib/player.js +896 -674
  73. package/lib/polyfill/media_capabilities.js +16 -1
  74. package/lib/text/simple_text_displayer.js +9 -0
  75. package/lib/text/stub_text_displayer.js +7 -0
  76. package/lib/text/text_engine.js +16 -3
  77. package/lib/text/ttml_text_parser.js +116 -114
  78. package/lib/text/ui_text_displayer.js +28 -11
  79. package/lib/text/vtt_text_parser.js +54 -115
  80. package/lib/transmuxer/opus.js +91 -0
  81. package/lib/transmuxer/ts_transmuxer.js +115 -0
  82. package/lib/util/cmcd_manager.js +152 -3
  83. package/lib/util/dom_utils.js +11 -0
  84. package/lib/util/error.js +5 -0
  85. package/lib/util/fake_event.js +2 -0
  86. package/lib/util/manifest_parser_utils.js +97 -3
  87. package/lib/util/mime_utils.js +10 -5
  88. package/lib/util/mp4_box_parsers.js +346 -17
  89. package/lib/util/mp4_generator.js +23 -0
  90. package/lib/util/periods.js +24 -37
  91. package/lib/util/platform.js +161 -9
  92. package/lib/util/player_configuration.js +61 -3
  93. package/lib/util/stats.js +16 -0
  94. package/lib/util/stream_utils.js +350 -277
  95. package/lib/util/string_utils.js +36 -0
  96. package/lib/util/tXml.js +814 -0
  97. package/lib/util/ts_parser.js +50 -0
  98. package/lib/util/xml_utils.js +10 -338
  99. package/package.json +2 -2
  100. package/shaka-player.uncompiled.js +8 -0
  101. package/ui/controls.js +187 -5
  102. package/ui/enums.js +2 -0
  103. package/ui/externs/ui.js +30 -3
  104. package/ui/gl_matrix/matrix_4x4.js +568 -0
  105. package/ui/gl_matrix/matrix_quaternion.js +45 -0
  106. package/ui/less/containers.less +11 -0
  107. package/ui/locales/en-GB.json +2 -0
  108. package/ui/locales/en.json +2 -0
  109. package/ui/locales/es-419.json +2 -0
  110. package/ui/locales/es.json +2 -0
  111. package/ui/locales/source.json +8 -0
  112. package/ui/recenter_vr.js +126 -0
  113. package/ui/resolution_selection.js +25 -5
  114. package/ui/statistics_button.js +12 -0
  115. package/ui/text_selection.js +1 -1
  116. package/ui/toggle_stereoscopic.js +127 -0
  117. package/ui/ui.js +74 -38
  118. package/ui/vr_manager.js +557 -0
  119. package/ui/vr_utils.js +232 -0
  120. package/ui/vr_webgl.js +697 -0
@@ -37,9 +37,9 @@ goog.require('shaka.util.OperationManager');
37
37
  goog.require('shaka.util.Pssh');
38
38
  goog.require('shaka.media.SegmentUtils');
39
39
  goog.require('shaka.util.Timer');
40
+ goog.require('shaka.util.TXml');
40
41
  goog.require('shaka.util.Platform');
41
42
  goog.require('shaka.util.Uint8ArrayUtils');
42
- goog.require('shaka.util.XmlUtils');
43
43
  goog.requireType('shaka.hls.Segment');
44
44
 
45
45
 
@@ -217,6 +217,18 @@ shaka.hls.HlsParser = class {
217
217
  * {!Map.<string, !shaka.media.InitSegmentReference>} */
218
218
  this.mapTagToInitSegmentRefMap_ = new Map();
219
219
 
220
+ /** @private {Map.<string, !shaka.extern.aesKey>} */
221
+ this.aesKeyInfoMap_ = new Map();
222
+
223
+ /** @private {Map.<string, !Promise.<shaka.extern.Response>>} */
224
+ this.aesKeyMap_ = new Map();
225
+
226
+ /** @private {Map.<string, !Promise.<shaka.extern.Response>>} */
227
+ this.identityKeyMap_ = new Map();
228
+
229
+ /** @private {Map.<!shaka.media.InitSegmentReference, ?string>} */
230
+ this.identityKidMap_ = new Map();
231
+
220
232
  /** @private {boolean} */
221
233
  this.lowLatencyMode_ = false;
222
234
 
@@ -297,6 +309,10 @@ shaka.hls.HlsParser = class {
297
309
  this.groupIdToCodecsMap_.clear();
298
310
  this.globalVariables_.clear();
299
311
  this.mapTagToInitSegmentRefMap_.clear();
312
+ this.aesKeyInfoMap_.clear();
313
+ this.aesKeyMap_.clear();
314
+ this.identityKeyMap_.clear();
315
+ this.identityKidMap_.clear();
300
316
 
301
317
  if (this.contentSteeringManager_) {
302
318
  this.contentSteeringManager_.destroy();
@@ -434,7 +450,8 @@ shaka.hls.HlsParser = class {
434
450
 
435
451
  const mediaSequenceToStartTime =
436
452
  this.getMediaSequenceToStartTimeFor_(streamInfo);
437
- const {keyIds, drmInfos} = this.parseDrmInfo_(playlist, stream.mimeType);
453
+ const {keyIds, drmInfos} = await this.parseDrmInfo_(
454
+ playlist, stream.mimeType, streamInfo.getUris, mediaVariables);
438
455
 
439
456
  const keysAreEqual =
440
457
  (a, b) => a.size === b.size && [...a].every((value) => b.has(value));
@@ -447,7 +464,7 @@ shaka.hls.HlsParser = class {
447
464
 
448
465
  const {segments, bandwidth} = this.createSegments_(
449
466
  playlist, stream, mediaSequenceToStartTime, mediaVariables,
450
- streamInfo.getUris);
467
+ streamInfo.getUris, streamInfo.type);
451
468
  if (bandwidth) {
452
469
  stream.bandwidth = bandwidth;
453
470
  }
@@ -581,6 +598,9 @@ shaka.hls.HlsParser = class {
581
598
  this.minSequenceNumber_);
582
599
 
583
600
  for (const streamInfo of streamInfos) {
601
+ if (!this.ignoreManifestProgramDateTimeFor_(streamInfo.type)) {
602
+ continue;
603
+ }
584
604
  const segmentIndex = streamInfo.stream.segmentIndex;
585
605
  if (segmentIndex) {
586
606
  // Drop any earlier references.
@@ -637,6 +657,9 @@ shaka.hls.HlsParser = class {
637
657
  shaka.log.debug('Syncing HLS streams against base time:', lowestSyncTime);
638
658
 
639
659
  for (const streamInfo of this.uriToStreamInfosMap_.values()) {
660
+ if (this.ignoreManifestProgramDateTimeFor_(streamInfo.type)) {
661
+ continue;
662
+ }
640
663
  const segmentIndex = streamInfo.stream.segmentIndex;
641
664
  if (segmentIndex != null) {
642
665
  // A segment's startTime should be based on its syncTime vs the lowest
@@ -718,12 +741,12 @@ shaka.hls.HlsParser = class {
718
741
  /** @type {!Array.<!shaka.extern.Stream>} */
719
742
  let imageStreams = [];
720
743
 
744
+ const getUris = () => {
745
+ return [uri];
746
+ };
747
+
721
748
  // Parsing a media playlist results in a single-variant stream.
722
749
  if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
723
- const getUris = () => {
724
- return [uri];
725
- };
726
-
727
750
  // Get necessary info for this stream. These are things we would normally
728
751
  // find from the master playlist (e.g. from values on EXT-X-MEDIA tags).
729
752
  const basicInfo =
@@ -737,6 +760,7 @@ shaka.hls.HlsParser = class {
737
760
  const channelsCount = basicInfo.channelCount;
738
761
  const sampleRate = basicInfo.sampleRate;
739
762
  const closedCaptions = basicInfo.closedCaptions;
763
+ const videoRange = basicInfo.videoRange;
740
764
 
741
765
  // Some values we cannot figure out, and aren't important enough to ask
742
766
  // the user to provide through config values. A lot of these are only
@@ -757,8 +781,7 @@ shaka.hls.HlsParser = class {
757
781
 
758
782
  if (type == 'video') {
759
783
  this.addVideoAttributes_(streamInfo.stream, width, height,
760
- /* frameRate= */ null, /* videoRange= */ null,
761
- /* videoLayout= */ null);
784
+ /* frameRate= */ null, videoRange, /* videoLayout= */ null);
762
785
  }
763
786
 
764
787
  // Wrap the stream from that stream info with a variant.
@@ -805,8 +828,9 @@ shaka.hls.HlsParser = class {
805
828
  this.parseCodecs_(variantTags);
806
829
 
807
830
  this.parseClosedCaptions_(mediaTags);
808
- variants =
809
- this.createVariantsForTags_(variantTags, sessionKeyTags, mediaTags);
831
+ variants = await this.createVariantsForTags_(
832
+ variantTags, sessionKeyTags, mediaTags, getUris,
833
+ this.globalVariables_);
810
834
  textStreams = this.parseTexts_(mediaTags);
811
835
  imageStreams = await this.parseImages_(imageTags, iFrameTags);
812
836
  }
@@ -876,6 +900,19 @@ shaka.hls.HlsParser = class {
876
900
  }
877
901
 
878
902
  this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
903
+
904
+ if (variants.length == 1) {
905
+ const createSegmentIndexPromises = [];
906
+ const variant = variants[0];
907
+ for (const stream of [variant.video, variant.audio]) {
908
+ if (stream && !stream.segmentIndex) {
909
+ createSegmentIndexPromises.push(stream.createSegmentIndex());
910
+ }
911
+ }
912
+ if (createSegmentIndexPromises.length > 0) {
913
+ await Promise.all(createSegmentIndexPromises);
914
+ }
915
+ }
879
916
  }
880
917
 
881
918
  /**
@@ -986,27 +1023,27 @@ shaka.hls.HlsParser = class {
986
1023
  this.presentationTimeline_.setSegmentAvailabilityDuration(
987
1024
  segmentAvailabilityDuration);
988
1025
  }
989
-
990
- if (!this.presentationTimeline_.isStartTimeLocked()) {
991
- for (const streamInfo of this.uriToStreamInfosMap_.values()) {
992
- if (!streamInfo.stream.segmentIndex) {
993
- continue; // Not active.
994
- }
995
- if (streamInfo.type != 'audio' && streamInfo.type != 'video') {
996
- continue;
997
- }
998
- const firstReference = streamInfo.stream.segmentIndex.get(0);
999
- if (firstReference && firstReference.syncTime) {
1000
- const syncTime = firstReference.syncTime;
1001
- this.presentationTimeline_.setInitialProgramDateTime(syncTime);
1002
- }
1003
- }
1004
- }
1005
1026
  } else {
1006
1027
  // Use the minimum duration as the presentation duration.
1007
1028
  this.presentationTimeline_.setDuration(this.getMinDuration_());
1008
1029
  }
1009
1030
 
1031
+ if (!this.presentationTimeline_.isStartTimeLocked()) {
1032
+ for (const streamInfo of this.uriToStreamInfosMap_.values()) {
1033
+ if (!streamInfo.stream.segmentIndex) {
1034
+ continue; // Not active.
1035
+ }
1036
+ if (streamInfo.type != 'audio' && streamInfo.type != 'video') {
1037
+ continue;
1038
+ }
1039
+ const firstReference = streamInfo.stream.segmentIndex.get(0);
1040
+ if (firstReference && firstReference.syncTime) {
1041
+ const syncTime = firstReference.syncTime;
1042
+ this.presentationTimeline_.setInitialProgramDateTime(syncTime);
1043
+ }
1044
+ }
1045
+ }
1046
+
1010
1047
  // This is the first point where we have a meaningful presentation start
1011
1048
  // time, and we need to tell PresentationTimeline that so that it can
1012
1049
  // maintain consistency from here on.
@@ -1224,6 +1261,7 @@ shaka.hls.HlsParser = class {
1224
1261
  textStreamInfo.stream.mimeType =
1225
1262
  this.guessMimeTypeBeforeLoading_(type, codecs) ||
1226
1263
  this.guessMimeTypeFallback_(type);
1264
+ this.setFullTypeForStream_(textStreamInfo.stream);
1227
1265
  }
1228
1266
  }
1229
1267
  }
@@ -1233,6 +1271,15 @@ shaka.hls.HlsParser = class {
1233
1271
  return textStreams.filter((s) => s);
1234
1272
  }
1235
1273
 
1274
+ /**
1275
+ * @param {!shaka.extern.Stream} stream
1276
+ * @private
1277
+ */
1278
+ setFullTypeForStream_(stream) {
1279
+ stream.fullMimeTypes = new Set([shaka.util.MimeUtils.getFullType(
1280
+ stream.mimeType, stream.codecs)]);
1281
+ }
1282
+
1236
1283
  /**
1237
1284
  * @param {!Array.<!shaka.hls.Tag>} imageTags from the playlist.
1238
1285
  * @param {!Array.<!shaka.hls.Tag>} iFrameTags from the playlist.
@@ -1313,37 +1360,48 @@ shaka.hls.HlsParser = class {
1313
1360
  * from the playlist.
1314
1361
  * @param {!Array.<!shaka.hls.Tag>} mediaTags EXT-X-MEDIA tags from the
1315
1362
  * playlist.
1316
- * @return {!Array.<!shaka.extern.Variant>}
1363
+ * @param {function():!Array.<string>} getUris
1364
+ * @param {?Map.<string, string>=} variables
1365
+ * @return {!Promise.<!Array.<!shaka.extern.Variant>>}
1317
1366
  * @private
1318
1367
  */
1319
- createVariantsForTags_(tags, sessionKeyTags, mediaTags) {
1368
+ async createVariantsForTags_(tags, sessionKeyTags, mediaTags, getUris,
1369
+ variables) {
1320
1370
  // EXT-X-SESSION-KEY processing
1321
1371
  const drmInfos = [];
1322
1372
  const keyIds = new Set();
1323
1373
  if (sessionKeyTags.length > 0) {
1324
1374
  for (const drmTag of sessionKeyTags) {
1325
1375
  const method = drmTag.getRequiredAttrValue('METHOD');
1326
- if (method != 'NONE' && method != 'AES-128') {
1327
- // According to the HLS spec, KEYFORMAT is optional and implicitly
1328
- // defaults to "identity".
1329
- // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
1330
- const keyFormat =
1331
- drmTag.getAttributeValue('KEYFORMAT') || 'identity';
1376
+ // According to the HLS spec, KEYFORMAT is optional and implicitly
1377
+ // defaults to "identity".
1378
+ // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
1379
+ const keyFormat =
1380
+ drmTag.getAttributeValue('KEYFORMAT') || 'identity';
1381
+ let drmInfo = null;
1382
+ if (method == 'NONE' || this.isAesMethod_(method)) {
1383
+ continue;
1384
+ } else if (keyFormat == 'identity') {
1385
+ // eslint-disable-next-line no-await-in-loop
1386
+ drmInfo = await this.identityDrmParser_(
1387
+ drmTag, /* mimeType= */ '', getUris,
1388
+ /* initSegmentRef= */ null, variables);
1389
+ } else {
1332
1390
  const drmParser =
1333
1391
  shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
1334
1392
 
1335
- const drmInfo = drmParser ?
1393
+ drmInfo = drmParser ?
1336
1394
  drmParser(drmTag, /* mimeType= */ '') : null;
1337
- if (drmInfo) {
1338
- if (drmInfo.keyIds) {
1339
- for (const keyId of drmInfo.keyIds) {
1340
- keyIds.add(keyId);
1341
- }
1395
+ }
1396
+ if (drmInfo) {
1397
+ if (drmInfo.keyIds) {
1398
+ for (const keyId of drmInfo.keyIds) {
1399
+ keyIds.add(keyId);
1342
1400
  }
1343
- drmInfos.push(drmInfo);
1344
- } else {
1345
- shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
1346
1401
  }
1402
+ drmInfos.push(drmInfo);
1403
+ } else {
1404
+ shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
1347
1405
  }
1348
1406
  }
1349
1407
  }
@@ -1577,6 +1635,9 @@ shaka.hls.HlsParser = class {
1577
1635
  getCodecsForVariantTag_(tag) {
1578
1636
  let codecsString = tag.getAttributeValue('CODECS') || '';
1579
1637
 
1638
+ const supplementalCodecsString =
1639
+ tag.getAttributeValue('SUPPLEMENTAL-CODECS');
1640
+
1580
1641
  this.codecInfoInManifest_ = codecsString.length > 0;
1581
1642
 
1582
1643
  if (!this.codecInfoInManifest_ && !this.config_.hls.disableCodecGuessing) {
@@ -1597,34 +1658,15 @@ shaka.hls.HlsParser = class {
1597
1658
  /** @type {!Array.<string>} */
1598
1659
  const codecs = codecsString.split(/\s*,\s*/);
1599
1660
 
1600
- return this.filterDuplicateCodecs_(codecs);
1601
- }
1602
-
1603
-
1604
- /**
1605
- * @param {!Array.<string>} codecs
1606
- * @return {!Array.<string>} codecs
1607
- * @private
1608
- */
1609
- filterDuplicateCodecs_(codecs) {
1610
- // Filter out duplicate codecs.
1611
- const seen = new Set();
1612
- const ret = [];
1613
- for (const codec of codecs) {
1614
- // HLS says the CODECS field needs to include all codecs that appear in
1615
- // the content. This means that if the content changes profiles, it should
1616
- // include both. Since all known browsers support changing profiles
1617
- // without any other work, just ignore them. See also:
1618
- // https://github.com/shaka-project/shaka-player/issues/1817
1619
- const shortCodec = shaka.util.MimeUtils.getCodecBase(codec);
1620
- if (!seen.has(shortCodec)) {
1621
- ret.push(codec);
1622
- seen.add(shortCodec);
1623
- } else {
1624
- shaka.log.debug('Ignoring duplicate codec');
1625
- }
1661
+ if (supplementalCodecsString) {
1662
+ const supplementalCodecs = supplementalCodecsString.split(/\s*,\s*/)
1663
+ .map((codec) => {
1664
+ return codec.split('/')[0];
1665
+ });
1666
+ codecs.push(...supplementalCodecs);
1626
1667
  }
1627
- return ret;
1668
+
1669
+ return shaka.media.SegmentUtils.codecsFiltering(codecs);
1628
1670
  }
1629
1671
 
1630
1672
  /**
@@ -2266,6 +2308,7 @@ shaka.hls.HlsParser = class {
2266
2308
  stream.mimeType = realStream.mimeType;
2267
2309
  stream.bandwidth = stream.bandwidth || realStream.bandwidth;
2268
2310
  stream.codecs = stream.codecs || realStream.codecs;
2311
+ this.setFullTypeForStream_(stream);
2269
2312
 
2270
2313
  // Since we lazy-loaded this content, the player may need to create new
2271
2314
  // sessions for the DRM info in this stream.
@@ -2280,6 +2323,7 @@ shaka.hls.HlsParser = class {
2280
2323
  // To aid manifest filtering, assume before loading that all video
2281
2324
  // renditions have the same MIME type. (And likewise for audio.)
2282
2325
  otherStreamInfo.stream.mimeType = realStream.mimeType;
2326
+ this.setFullTypeForStream_(otherStreamInfo.stream);
2283
2327
  }
2284
2328
  }
2285
2329
  }
@@ -2288,6 +2332,7 @@ shaka.hls.HlsParser = class {
2288
2332
  const firstSegment = realStream.segmentIndex.get(0);
2289
2333
  if (firstSegment && firstSegment.initSegmentReference) {
2290
2334
  stream.mimeType = 'application/mp4';
2335
+ this.setFullTypeForStream_(stream);
2291
2336
  }
2292
2337
  }
2293
2338
 
@@ -2431,9 +2476,25 @@ shaka.hls.HlsParser = class {
2431
2476
  this.syncStreamsWithSequenceNumber_(streamInfos);
2432
2477
  } else {
2433
2478
  this.syncStreamsWithProgramDateTime_(streamInfos);
2479
+ if (this.config_.hls.ignoreManifestProgramDateTimeForTypes.length > 0) {
2480
+ this.syncStreamsWithSequenceNumber_(streamInfos);
2481
+ }
2434
2482
  }
2435
2483
  }
2436
2484
 
2485
+ /**
2486
+ * @param {string} type
2487
+ * @return {boolean}
2488
+ * @private
2489
+ */
2490
+ ignoreManifestProgramDateTimeFor_(type) {
2491
+ if (this.config_.hls.ignoreManifestProgramDateTime) {
2492
+ return true;
2493
+ }
2494
+ const forTypes = this.config_.hls.ignoreManifestProgramDateTimeForTypes;
2495
+ return forTypes.includes(type);
2496
+ }
2497
+
2437
2498
  /**
2438
2499
  * There are some values on streams that can only be set once we know about
2439
2500
  * both the video and audio content, if present.
@@ -2521,7 +2582,7 @@ shaka.hls.HlsParser = class {
2521
2582
  }
2522
2583
 
2523
2584
  const {drmInfos, keyIds, encrypted, aesEncrypted} =
2524
- this.parseDrmInfo_(playlist, mimeType);
2585
+ await this.parseDrmInfo_(playlist, mimeType, getUris, mediaVariables);
2525
2586
 
2526
2587
  if (encrypted && !drmInfos.length && !aesEncrypted) {
2527
2588
  throw new shaka.util.Error(
@@ -2537,12 +2598,14 @@ shaka.hls.HlsParser = class {
2537
2598
  stream.drmInfos = drmInfos;
2538
2599
  stream.keyIds = keyIds;
2539
2600
  stream.mimeType = mimeType;
2601
+ this.setFullTypeForStream_(stream);
2540
2602
 
2541
2603
  const mediaSequenceToStartTime = this.isLive_() ?
2542
2604
  this.mediaSequenceToStartTimeByType_.get(type) : new Map();
2543
2605
 
2544
2606
  const {segments, bandwidth} = this.createSegments_(
2545
- playlist, stream, mediaSequenceToStartTime, mediaVariables, getUris);
2607
+ playlist, stream, mediaSequenceToStartTime, mediaVariables, getUris,
2608
+ type);
2546
2609
  if (bandwidth) {
2547
2610
  stream.bandwidth = bandwidth;
2548
2611
  }
@@ -2692,7 +2755,7 @@ shaka.hls.HlsParser = class {
2692
2755
  roles.push(shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE);
2693
2756
  }
2694
2757
 
2695
- return {
2758
+ const stream = {
2696
2759
  id: this.globalId_++,
2697
2760
  originalId: name,
2698
2761
  groupId: null,
@@ -2730,28 +2793,40 @@ shaka.hls.HlsParser = class {
2730
2793
  accessibilityPurpose: accessibilityPurpose,
2731
2794
  external: false,
2732
2795
  fastSwitching: false,
2796
+ fullMimeTypes: new Set(),
2733
2797
  };
2798
+ this.setFullTypeForStream_(stream);
2799
+ return stream;
2734
2800
  }
2735
2801
 
2736
2802
  /**
2737
2803
  * @param {!shaka.hls.Playlist} playlist
2738
2804
  * @param {string} mimeType
2739
- * @return {{
2805
+ * @param {function():!Array.<string>} getUris
2806
+ * @param {?Map.<string, string>=} variables
2807
+ * @return {Promise.<{
2740
2808
  * drmInfos: !Array.<shaka.extern.DrmInfo>,
2741
2809
  * keyIds: !Set.<string>,
2742
2810
  * encrypted: boolean,
2743
2811
  * aesEncrypted: boolean
2744
- * }}
2812
+ * }>}
2745
2813
  * @private
2746
2814
  */
2747
- parseDrmInfo_(playlist, mimeType) {
2748
- /** @type {!Array.<!shaka.hls.Tag>} */
2749
- const drmTags = [];
2815
+ async parseDrmInfo_(playlist, mimeType, getUris, variables) {
2816
+ /** @type {!Map<!shaka.hls.Tag, ?shaka.media.InitSegmentReference>} */
2817
+ const drmTagsMap = new Map();
2750
2818
  if (playlist.segments) {
2751
2819
  for (const segment of playlist.segments) {
2752
2820
  const segmentKeyTags = shaka.hls.Utils.filterTagsByName(segment.tags,
2753
2821
  'EXT-X-KEY');
2754
- drmTags.push(...segmentKeyTags);
2822
+ let initSegmentRef = null;
2823
+ if (segmentKeyTags.length) {
2824
+ initSegmentRef = this.getInitSegmentReference_(playlist,
2825
+ segment.tags, getUris, variables);
2826
+ for (const segmentKeyTag of segmentKeyTags) {
2827
+ drmTagsMap.set(segmentKeyTag, initSegmentRef);
2828
+ }
2829
+ }
2755
2830
  }
2756
2831
  }
2757
2832
 
@@ -2762,34 +2837,43 @@ shaka.hls.HlsParser = class {
2762
2837
  const drmInfos = [];
2763
2838
  const keyIds = new Set();
2764
2839
 
2765
- for (const drmTag of drmTags) {
2840
+ for (const [key, value] of drmTagsMap) {
2841
+ const drmTag = /** @type {!shaka.hls.Tag} */ (key);
2842
+ const initSegmentRef =
2843
+ /** @type {?shaka.media.InitSegmentReference} */ (value);
2766
2844
  const method = drmTag.getRequiredAttrValue('METHOD');
2767
2845
  if (method != 'NONE') {
2768
2846
  encrypted = true;
2769
2847
 
2770
- if (method == 'AES-128') {
2848
+ // According to the HLS spec, KEYFORMAT is optional and implicitly
2849
+ // defaults to "identity".
2850
+ // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
2851
+ const keyFormat =
2852
+ drmTag.getAttributeValue('KEYFORMAT') || 'identity';
2853
+ let drmInfo = null;
2854
+
2855
+ if (this.isAesMethod_(method)) {
2771
2856
  // These keys are handled separately.
2772
2857
  aesEncrypted = true;
2858
+ continue;
2859
+ } else if (keyFormat == 'identity') {
2860
+ // eslint-disable-next-line no-await-in-loop
2861
+ drmInfo = await this.identityDrmParser_(
2862
+ drmTag, mimeType, getUris, initSegmentRef, variables);
2773
2863
  } else {
2774
- // According to the HLS spec, KEYFORMAT is optional and implicitly
2775
- // defaults to "identity".
2776
- // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
2777
- const keyFormat =
2778
- drmTag.getAttributeValue('KEYFORMAT') || 'identity';
2779
2864
  const drmParser =
2780
2865
  shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
2781
-
2782
- const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null;
2783
- if (drmInfo) {
2784
- if (drmInfo.keyIds) {
2785
- for (const keyId of drmInfo.keyIds) {
2786
- keyIds.add(keyId);
2787
- }
2866
+ drmInfo = drmParser ? drmParser(drmTag, mimeType) : null;
2867
+ }
2868
+ if (drmInfo) {
2869
+ if (drmInfo.keyIds) {
2870
+ for (const keyId of drmInfo.keyIds) {
2871
+ keyIds.add(keyId);
2788
2872
  }
2789
- drmInfos.push(drmInfo);
2790
- } else {
2791
- shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
2792
2873
  }
2874
+ drmInfos.push(drmInfo);
2875
+ } else {
2876
+ shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
2793
2877
  }
2794
2878
  }
2795
2879
  }
@@ -2802,14 +2886,14 @@ shaka.hls.HlsParser = class {
2802
2886
  * @param {!shaka.hls.Playlist} playlist
2803
2887
  * @param {function():!Array.<string>} getUris
2804
2888
  * @param {?Map.<string, string>=} variables
2805
- * @return {!shaka.extern.aes128Key}
2889
+ * @return {!shaka.extern.aesKey}
2806
2890
  * @private
2807
2891
  */
2808
- parseAES128DrmTag_(drmTag, playlist, getUris, variables) {
2892
+ parseAESDrmTag_(drmTag, playlist, getUris, variables) {
2809
2893
  // Check if the Web Crypto API is available.
2810
2894
  if (!window.crypto || !window.crypto.subtle) {
2811
2895
  shaka.log.alwaysWarn('Web Crypto API is not available to decrypt ' +
2812
- 'AES-128. (Web Crypto only exists in secure origins like https)');
2896
+ 'AES. (Web Crypto only exists in secure origins like https)');
2813
2897
  throw new shaka.util.Error(
2814
2898
  shaka.util.Error.Severity.CRITICAL,
2815
2899
  shaka.util.Error.Category.MANIFEST,
@@ -2840,36 +2924,63 @@ shaka.hls.HlsParser = class {
2840
2924
  }
2841
2925
  }
2842
2926
 
2843
- const keyInfo = {method: 'AES-128', iv, firstMediaSequenceNumber};
2844
-
2845
- // Don't download the key object until the segment is parsed, to avoid a
2846
- // startup delay for long manifests with lots of keys.
2847
- keyInfo.fetchKey = async () => {
2848
- const keyUris = shaka.hls.Utils.constructSegmentUris(
2849
- getUris(), drmTag.getRequiredAttrValue('URI'), variables);
2850
-
2851
- const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
2852
- const request = shaka.net.NetworkingEngine.makeRequest(
2853
- keyUris, this.config_.retryParameters);
2854
- const keyResponse = await this.makeNetworkRequest_(request, requestType);
2927
+ const aesKeyInfoKey = `${drmTag.toString()}-${firstMediaSequenceNumber}`;
2928
+ if (!this.aesKeyInfoMap_.has(aesKeyInfoKey)) {
2929
+ // Default AES-128
2930
+ const keyInfo = {
2931
+ bitsKey: 128,
2932
+ blockCipherMode: 'CBC',
2933
+ iv,
2934
+ firstMediaSequenceNumber,
2935
+ };
2855
2936
 
2856
- // keyResponse.status is undefined when URI is "data:text/plain;base64,"
2857
- if (!keyResponse.data || keyResponse.data.byteLength != 16) {
2858
- throw new shaka.util.Error(
2859
- shaka.util.Error.Severity.CRITICAL,
2860
- shaka.util.Error.Category.MANIFEST,
2861
- shaka.util.Error.Code.AES_128_INVALID_KEY_LENGTH);
2937
+ const method = drmTag.getRequiredAttrValue('METHOD');
2938
+ switch (method) {
2939
+ case 'AES-256':
2940
+ keyInfo.bitsKey = 256;
2941
+ break;
2942
+ case 'AES-256-CTR':
2943
+ keyInfo.bitsKey = 256;
2944
+ keyInfo.blockCipherMode = 'CTR';
2945
+ break;
2862
2946
  }
2863
2947
 
2864
- const algorithm = {
2865
- name: 'AES-CBC',
2866
- };
2867
- keyInfo.cryptoKey = await window.crypto.subtle.importKey(
2868
- 'raw', keyResponse.data, algorithm, true, ['decrypt']);
2869
- keyInfo.fetchKey = undefined; // No longer needed.
2870
- };
2948
+ // Don't download the key object until the segment is parsed, to avoid a
2949
+ // startup delay for long manifests with lots of keys.
2950
+ keyInfo.fetchKey = async () => {
2951
+ const keyUris = shaka.hls.Utils.constructSegmentUris(
2952
+ getUris(), drmTag.getRequiredAttrValue('URI'), variables);
2953
+
2954
+ const keyMapKey = keyUris.sort().join('');
2955
+ if (!this.aesKeyMap_.has(keyMapKey)) {
2956
+ const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
2957
+ const request = shaka.net.NetworkingEngine.makeRequest(
2958
+ keyUris, this.config_.retryParameters);
2959
+ const keyResponse = this.makeNetworkRequest_(request, requestType);
2960
+ this.aesKeyMap_.set(keyMapKey, keyResponse);
2961
+ }
2962
+ const keyResponse = await this.aesKeyMap_.get(keyMapKey);
2963
+
2964
+ // keyResponse.status is undefined when URI is "data:text/plain;base64,"
2965
+ if (!keyResponse.data ||
2966
+ keyResponse.data.byteLength != (keyInfo.bitsKey / 8)) {
2967
+ throw new shaka.util.Error(
2968
+ shaka.util.Error.Severity.CRITICAL,
2969
+ shaka.util.Error.Category.MANIFEST,
2970
+ shaka.util.Error.Code.AES_128_INVALID_KEY_LENGTH);
2971
+ }
2871
2972
 
2872
- return keyInfo;
2973
+ const algorithm = {
2974
+ name: keyInfo.blockCipherMode == 'CTR' ? 'AES-CTR' : 'AES-CBC',
2975
+ length: keyInfo.bitsKey,
2976
+ };
2977
+ keyInfo.cryptoKey = await window.crypto.subtle.importKey(
2978
+ 'raw', keyResponse.data, algorithm, true, ['decrypt']);
2979
+ keyInfo.fetchKey = undefined; // No longer needed.
2980
+ };
2981
+ this.aesKeyInfoMap_.set(aesKeyInfoKey, keyInfo);
2982
+ }
2983
+ return this.aesKeyInfoMap_.get(aesKeyInfoKey);
2873
2984
  }
2874
2985
 
2875
2986
 
@@ -3049,22 +3160,22 @@ shaka.hls.HlsParser = class {
3049
3160
  mapTag.getAttributeValue('BYTERANGE', ''),
3050
3161
  ].join('-');
3051
3162
  if (!this.mapTagToInitSegmentRefMap_.has(mapTagKey)) {
3052
- /** @type {shaka.extern.aes128Key|undefined} */
3053
- let aes128Key = undefined;
3163
+ /** @type {shaka.extern.aesKey|undefined} */
3164
+ let aesKey = undefined;
3054
3165
  let byteRangeTag = null;
3055
3166
  for (const tag of tags) {
3056
3167
  if (tag.name == 'EXT-X-KEY') {
3057
- if (tag.getRequiredAttrValue('METHOD') == 'AES-128' &&
3168
+ if (this.isAesMethod_(tag.getRequiredAttrValue('METHOD')) &&
3058
3169
  tag.id < mapTag.id) {
3059
- aes128Key =
3060
- this.parseAES128DrmTag_(tag, playlist, getUris, variables);
3170
+ aesKey =
3171
+ this.parseAESDrmTag_(tag, playlist, getUris, variables);
3061
3172
  }
3062
3173
  } else if (tag.name == 'EXT-X-BYTERANGE' && tag.id < mapTag.id) {
3063
3174
  byteRangeTag = tag;
3064
3175
  }
3065
3176
  }
3066
3177
  const initSegmentRef = this.createInitSegmentReference_(
3067
- absoluteInitSegmentUris, mapTag, byteRangeTag, aes128Key);
3178
+ absoluteInitSegmentUris, mapTag, byteRangeTag, aesKey);
3068
3179
  this.mapTagToInitSegmentRefMap_.set(mapTagKey, initSegmentRef);
3069
3180
  }
3070
3181
  return this.mapTagToInitSegmentRefMap_.get(mapTagKey);
@@ -3076,12 +3187,12 @@ shaka.hls.HlsParser = class {
3076
3187
  * @param {!Array.<string>} absoluteInitSegmentUris
3077
3188
  * @param {!shaka.hls.Tag} mapTag EXT-X-MAP
3078
3189
  * @param {shaka.hls.Tag=} byteRangeTag EXT-X-BYTERANGE
3079
- * @param {shaka.extern.aes128Key=} aes128Key
3190
+ * @param {shaka.extern.aesKey=} aesKey
3080
3191
  * @return {!shaka.media.InitSegmentReference}
3081
3192
  * @private
3082
3193
  */
3083
3194
  createInitSegmentReference_(absoluteInitSegmentUris, mapTag, byteRangeTag,
3084
- aes128Key) {
3195
+ aesKey) {
3085
3196
  let startByte = 0;
3086
3197
  let endByte = null;
3087
3198
  let byterange = mapTag.getAttributeValue('BYTERANGE');
@@ -3096,8 +3207,8 @@ shaka.hls.HlsParser = class {
3096
3207
  startByte = Number(blocks[1]);
3097
3208
  endByte = startByte + byteLength - 1;
3098
3209
 
3099
- if (aes128Key) {
3100
- // MAP segment encrypted with method 'AES-128', when served with
3210
+ if (aesKey) {
3211
+ // MAP segment encrypted with method AES, when served with
3101
3212
  // HTTP Range, has the unencrypted size specified in the range.
3102
3213
  // See: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-6.3.6
3103
3214
  const length = (endByte + 1) - startByte;
@@ -3114,7 +3225,7 @@ shaka.hls.HlsParser = class {
3114
3225
  /* mediaQuality= */ null,
3115
3226
  /* timescale= */ null,
3116
3227
  /* segmentData= */ null,
3117
- aes128Key);
3228
+ aesKey);
3118
3229
  return initSegmentRef;
3119
3230
  }
3120
3231
 
@@ -3129,13 +3240,13 @@ shaka.hls.HlsParser = class {
3129
3240
  * @param {!shaka.hls.Playlist} playlist
3130
3241
  * @param {shaka.extern.Stream} stream
3131
3242
  * @param {function():!Array.<string>} getUris
3132
- * @param {shaka.extern.aes128Key=} aes128Key
3243
+ * @param {shaka.extern.aesKey=} aesKey
3133
3244
  * @return {shaka.media.SegmentReference}
3134
3245
  * @private
3135
3246
  */
3136
3247
  createSegmentReference_(
3137
3248
  initSegmentReference, previousReference, hlsSegment, startTime,
3138
- variables, playlist, stream, getUris, aes128Key) {
3249
+ variables, playlist, stream, getUris, aesKey) {
3139
3250
  const tags = hlsSegment.tags;
3140
3251
  const extinfTag =
3141
3252
  shaka.hls.Utils.getFirstTagWithName(tags, 'EXTINF');
@@ -3156,7 +3267,7 @@ shaka.hls.HlsParser = class {
3156
3267
  const dateTimeTag =
3157
3268
  shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-PROGRAM-DATE-TIME');
3158
3269
  if (dateTimeTag && dateTimeTag.value) {
3159
- syncTime = shaka.util.XmlUtils.parseDate(dateTimeTag.value);
3270
+ syncTime = shaka.util.TXml.parseDate(dateTimeTag.value);
3160
3271
  goog.asserts.assert(syncTime != null,
3161
3272
  'EXT-X-PROGRAM-DATE-TIME format not valid');
3162
3273
  }
@@ -3281,7 +3392,7 @@ shaka.hls.HlsParser = class {
3281
3392
  /* tileDuration= */ null,
3282
3393
  partialSyncTime,
3283
3394
  partialStatus,
3284
- aes128Key);
3395
+ aesKey);
3285
3396
  if (item.name == 'EXT-X-PRELOAD-HINT') {
3286
3397
  partial.markAsPreload();
3287
3398
  isPreloadSegment = true;
@@ -3396,7 +3507,7 @@ shaka.hls.HlsParser = class {
3396
3507
  tileDuration,
3397
3508
  syncTime,
3398
3509
  status,
3399
- aes128Key,
3510
+ aesKey,
3400
3511
  allPartialSegments,
3401
3512
  );
3402
3513
 
@@ -3450,12 +3561,13 @@ shaka.hls.HlsParser = class {
3450
3561
  * @param {!Map.<number, number>} mediaSequenceToStartTime
3451
3562
  * @param {!Map.<string, string>} variables
3452
3563
  * @param {function():!Array.<string>} getUris
3564
+ * @param {string} type
3453
3565
  * @return {{segments: !Array.<!shaka.media.SegmentReference>,
3454
3566
  * bandwidth: (number|undefined)}}
3455
3567
  * @private
3456
3568
  */
3457
3569
  createSegments_(playlist, stream, mediaSequenceToStartTime, variables,
3458
- getUris) {
3570
+ getUris, type) {
3459
3571
  /** @type {Array.<!shaka.hls.Segment>} */
3460
3572
  const hlsSegments = playlist.segments;
3461
3573
  goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
@@ -3463,8 +3575,8 @@ shaka.hls.HlsParser = class {
3463
3575
  /** @type {shaka.media.InitSegmentReference} */
3464
3576
  let initSegmentRef;
3465
3577
 
3466
- /** @type {shaka.extern.aes128Key|undefined} */
3467
- let aes128Key = undefined;
3578
+ /** @type {shaka.extern.aesKey|undefined} */
3579
+ let aesKey = undefined;
3468
3580
 
3469
3581
  let discontinuitySequence = shaka.hls.Utils.getFirstTagWithNameAsNumber(
3470
3582
  playlist.tags, 'EXT-X-DISCONTINUITY-SEQUENCE', 0);
@@ -3510,14 +3622,14 @@ shaka.hls.HlsParser = class {
3510
3622
  discontinuitySequence++;
3511
3623
  }
3512
3624
 
3513
- // Apply new AES-128 tags as you see them, keeping a running total.
3625
+ // Apply new AES tags as you see them, keeping a running total.
3514
3626
  for (const drmTag of item.tags) {
3515
3627
  if (drmTag.name == 'EXT-X-KEY') {
3516
- if (drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
3517
- aes128Key =
3518
- this.parseAES128DrmTag_(drmTag, playlist, getUris, variables);
3628
+ if (this.isAesMethod_(drmTag.getRequiredAttrValue('METHOD'))) {
3629
+ aesKey =
3630
+ this.parseAESDrmTag_(drmTag, playlist, getUris, variables);
3519
3631
  } else {
3520
- aes128Key = undefined;
3632
+ aesKey = undefined;
3521
3633
  }
3522
3634
  }
3523
3635
  }
@@ -3548,7 +3660,7 @@ shaka.hls.HlsParser = class {
3548
3660
  playlist,
3549
3661
  stream,
3550
3662
  getUris,
3551
- aes128Key);
3663
+ aesKey);
3552
3664
 
3553
3665
  if (reference) {
3554
3666
  const bitrate = shaka.hls.Utils.getFirstTagWithNameAsNumber(
@@ -3569,7 +3681,7 @@ shaka.hls.HlsParser = class {
3569
3681
  previousReference = reference;
3570
3682
  reference.discontinuitySequence = discontinuitySequence;
3571
3683
 
3572
- if (this.config_.hls.ignoreManifestProgramDateTime &&
3684
+ if (this.ignoreManifestProgramDateTimeFor_(type) &&
3573
3685
  this.minSequenceNumber_ != null &&
3574
3686
  position < this.minSequenceNumber_) {
3575
3687
  // This segment is ignored as part of our fallback synchronization
@@ -3670,8 +3782,10 @@ shaka.hls.HlsParser = class {
3670
3782
  // skip this step, and wait until we have sync time across stream types.
3671
3783
  const lowestSyncTime = this.lowestSyncTime_;
3672
3784
  if (someSyncTime && lowestSyncTime != Infinity) {
3673
- for (const reference of references) {
3674
- reference.syncAgainst(lowestSyncTime);
3785
+ if (!this.ignoreManifestProgramDateTimeFor_(type)) {
3786
+ for (const reference of references) {
3787
+ reference.syncAgainst(lowestSyncTime);
3788
+ }
3675
3789
  }
3676
3790
  }
3677
3791
 
@@ -4020,6 +4134,17 @@ shaka.hls.HlsParser = class {
4020
4134
  return op.promise;
4021
4135
  }
4022
4136
 
4137
+ /**
4138
+ * @param {string} method
4139
+ * @return {boolean}
4140
+ * @private
4141
+ */
4142
+ isAesMethod_(method) {
4143
+ return method == 'AES-128' ||
4144
+ method == 'AES-256' ||
4145
+ method == 'AES-256-CTR';
4146
+ }
4147
+
4023
4148
  /**
4024
4149
  * @param {!shaka.hls.Tag} drmTag
4025
4150
  * @param {string} mimeType
@@ -4042,13 +4167,28 @@ shaka.hls.HlsParser = class {
4042
4167
  .HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED);
4043
4168
  }
4044
4169
 
4170
+ const method = drmTag.getRequiredAttrValue('METHOD');
4171
+ const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR'];
4172
+ if (!VALID_METHODS.includes(method)) {
4173
+ shaka.log.error('FairPlay in HLS is only supported with [',
4174
+ VALID_METHODS.join(', '), '], not', method);
4175
+ return null;
4176
+ }
4177
+
4178
+ let encryptionScheme = 'cenc';
4179
+ if (method == 'SAMPLE-AES') {
4180
+ // It should be 'cbcs-1-9' but Safari doesn't support it.
4181
+ // See: https://github.com/WebKit/WebKit/blob/main/Source/WebCore/Modules/encryptedmedia/MediaKeyEncryptionScheme.idl
4182
+ encryptionScheme = 'cbcs';
4183
+ }
4184
+
4045
4185
  /*
4046
4186
  * Even if we're not able to construct initData through the HLS tag, adding
4047
4187
  * a DRMInfo will allow DRM Engine to request a media key system access
4048
4188
  * with the correct keySystem and initDataType
4049
4189
  */
4050
4190
  const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
4051
- 'com.apple.fps', [
4191
+ 'com.apple.fps', encryptionScheme, [
4052
4192
  {initDataType: 'sinf', initData: new Uint8Array(0), keyId: null},
4053
4193
  ]);
4054
4194
 
@@ -4069,13 +4209,18 @@ shaka.hls.HlsParser = class {
4069
4209
  return null;
4070
4210
  }
4071
4211
 
4212
+ let encryptionScheme = 'cenc';
4213
+ if (method == 'SAMPLE-AES') {
4214
+ encryptionScheme = 'cbcs';
4215
+ }
4216
+
4072
4217
  const uri = drmTag.getRequiredAttrValue('URI');
4073
4218
  const parsedData = shaka.net.DataUriPlugin.parseRaw(uri);
4074
4219
 
4075
4220
  // The data encoded in the URI is a PSSH box to be used as init data.
4076
4221
  const pssh = shaka.util.BufferUtils.toUint8(parsedData.data);
4077
4222
  const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
4078
- 'com.widevine.alpha', [
4223
+ 'com.widevine.alpha', encryptionScheme, [
4079
4224
  {initDataType: 'cenc', initData: pssh},
4080
4225
  ]);
4081
4226
 
@@ -4107,6 +4252,11 @@ shaka.hls.HlsParser = class {
4107
4252
  return null;
4108
4253
  }
4109
4254
 
4255
+ let encryptionScheme = 'cenc';
4256
+ if (method == 'SAMPLE-AES') {
4257
+ encryptionScheme = 'cbcs';
4258
+ }
4259
+
4110
4260
  const uri = drmTag.getRequiredAttrValue('URI');
4111
4261
  const parsedData = shaka.net.DataUriPlugin.parseRaw(uri);
4112
4262
 
@@ -4122,7 +4272,7 @@ shaka.hls.HlsParser = class {
4122
4272
  const pssh =
4123
4273
  shaka.util.Pssh.createPssh(data, systemId, keyIds, psshVersion);
4124
4274
  const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
4125
- 'com.microsoft.playready', [
4275
+ 'com.microsoft.playready', encryptionScheme, [
4126
4276
  {initDataType: 'cenc', initData: pssh},
4127
4277
  ]);
4128
4278
 
@@ -4133,10 +4283,15 @@ shaka.hls.HlsParser = class {
4133
4283
  * See: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-5.1
4134
4284
  *
4135
4285
  * @param {!shaka.hls.Tag} drmTag
4136
- * @return {?shaka.extern.DrmInfo}
4286
+ * @param {string} mimeType
4287
+ * @param {function():!Array.<string>} getUris
4288
+ * @param {?shaka.media.InitSegmentReference} initSegmentRef
4289
+ * @param {?Map.<string, string>=} variables
4290
+ * @return {!Promise.<?shaka.extern.DrmInfo>}
4137
4291
  * @private
4138
4292
  */
4139
- static identityDrmParser_(drmTag, mimeType) {
4293
+ async identityDrmParser_(drmTag, mimeType, getUris, initSegmentRef,
4294
+ variables) {
4140
4295
  if (mimeType == 'video/mp2t') {
4141
4296
  throw new shaka.util.Error(
4142
4297
  shaka.util.Error.Severity.CRITICAL,
@@ -4160,15 +4315,73 @@ shaka.hls.HlsParser = class {
4160
4315
  return null;
4161
4316
  }
4162
4317
 
4318
+ const keyUris = shaka.hls.Utils.constructSegmentUris(
4319
+ getUris(), drmTag.getRequiredAttrValue('URI'), variables);
4320
+
4321
+ let key;
4322
+ if (keyUris[0].startsWith('data:text/plain;base64,')) {
4323
+ key = shaka.util.Uint8ArrayUtils.toHex(
4324
+ shaka.util.Uint8ArrayUtils.fromBase64(
4325
+ keyUris[0].split('data:text/plain;base64,').pop()));
4326
+ } else {
4327
+ const keyMapKey = keyUris.sort().join('');
4328
+ if (!this.identityKeyMap_.has(keyMapKey)) {
4329
+ const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
4330
+ const request = shaka.net.NetworkingEngine.makeRequest(
4331
+ keyUris, this.config_.retryParameters);
4332
+ const keyResponse = this.makeNetworkRequest_(request, requestType);
4333
+ this.identityKeyMap_.set(keyMapKey, keyResponse);
4334
+ }
4335
+ const keyResponse = await this.identityKeyMap_.get(keyMapKey);
4336
+ key = shaka.util.Uint8ArrayUtils.toHex(keyResponse.data);
4337
+ }
4338
+
4163
4339
  // NOTE: The ClearKey CDM requires a key-id to key mapping. HLS doesn't
4164
4340
  // provide a key ID anywhere. So although we could use the 'URI' attribute
4165
4341
  // to fetch the actual 16-byte key, without a key ID, we can't provide this
4166
- // automatically to the ClearKey CDM. Instead, the application will have
4167
- // to use player.configure('drm.clearKeys', { ... }) to provide the key IDs
4342
+ // automatically to the ClearKey CDM. By default we assume that keyId is 0,
4343
+ // but we will try to get key ID from Init Segment.
4344
+ // If the application want override this behavior will have to use
4345
+ // player.configure('drm.clearKeys', { ... }) to provide the key IDs
4168
4346
  // and keys or player.configure('drm.servers.org\.w3\.clearkey', ...) to
4169
4347
  // provide a ClearKey license server URI.
4170
- return shaka.util.ManifestParserUtils.createDrmInfo(
4171
- 'org.w3.clearkey', /* initDatas= */ null);
4348
+ let keyId = '00000000000000000000000000000000';
4349
+
4350
+ if (initSegmentRef) {
4351
+ let defaultKID;
4352
+ if (this.identityKidMap_.has(initSegmentRef)) {
4353
+ defaultKID = this.identityKidMap_.get(initSegmentRef);
4354
+ } else {
4355
+ const initSegmentRequest = shaka.util.Networking.createSegmentRequest(
4356
+ initSegmentRef.getUris(),
4357
+ initSegmentRef.getStartByte(),
4358
+ initSegmentRef.getEndByte(),
4359
+ this.config_.retryParameters);
4360
+ const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
4361
+ const initType =
4362
+ shaka.net.NetworkingEngine.AdvancedRequestType.INIT_SEGMENT;
4363
+ const initResponse = await this.makeNetworkRequest_(
4364
+ initSegmentRequest, requestType, {type: initType});
4365
+
4366
+ defaultKID = shaka.media.SegmentUtils.getDefaultKID(
4367
+ initResponse.data);
4368
+ this.identityKidMap_.set(initSegmentRef, defaultKID);
4369
+ }
4370
+ if (defaultKID) {
4371
+ keyId = defaultKID;
4372
+ }
4373
+ }
4374
+
4375
+ const clearkeys = new Map();
4376
+ clearkeys.set(keyId, key);
4377
+
4378
+ let encryptionScheme = 'cenc';
4379
+ if (method == 'SAMPLE-AES') {
4380
+ encryptionScheme = 'cbcs';
4381
+ }
4382
+
4383
+ return shaka.util.ManifestParserUtils.createDrmInfoFromClearKeys(
4384
+ clearkeys, encryptionScheme);
4172
4385
  }
4173
4386
  };
4174
4387
 
@@ -4349,8 +4562,6 @@ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_ = {
4349
4562
  shaka.hls.HlsParser.widevineDrmParser_,
4350
4563
  'com.microsoft.playready':
4351
4564
  shaka.hls.HlsParser.playreadyDrmParser_,
4352
- 'identity':
4353
- shaka.hls.HlsParser.identityDrmParser_,
4354
4565
  };
4355
4566
 
4356
4567