shaka-player 4.2.5 → 4.3.1

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 (143) hide show
  1. package/.github/workflows/build-and-test.yaml +7 -0
  2. package/.github/workflows/sync-labels.yaml +1 -1
  3. package/.github/workflows/update-issues.yaml +1 -1
  4. package/AUTHORS +4 -0
  5. package/CHANGELOG.md +114 -74
  6. package/CONTRIBUTORS +4 -0
  7. package/README.md +14 -0
  8. package/conditional/dummy_cast_proxy.js +120 -0
  9. package/demo/common/assets.js +58 -0
  10. package/demo/common/message_ids.js +22 -1
  11. package/demo/config.js +89 -5
  12. package/demo/demo.less +7 -1
  13. package/demo/index.html +8 -1
  14. package/demo/locales/en.json +21 -1
  15. package/demo/locales/source.json +82 -2
  16. package/demo/main.js +57 -1
  17. package/demo/search.js +2 -0
  18. package/demo/visualizer.js +337 -0
  19. package/demo/visualizer_button.js +103 -0
  20. package/dist/controls.css +1 -1
  21. package/dist/controls.css.map +1 -1
  22. package/dist/demo.compiled.debug.js +107 -88
  23. package/dist/demo.compiled.debug.map +4 -4
  24. package/dist/demo.compiled.js +107 -88
  25. package/dist/demo.compiled.map +4 -4
  26. package/dist/demo.css +1 -1
  27. package/dist/demo.css.map +1 -1
  28. package/dist/deps.js +21 -13
  29. package/dist/locales.js +31 -31
  30. package/dist/receiver.compiled.debug.js +75 -77
  31. package/dist/receiver.compiled.debug.map +3 -3
  32. package/dist/receiver.compiled.js +75 -77
  33. package/dist/receiver.compiled.map +3 -3
  34. package/dist/shaka-player.compiled.d.ts +181 -27
  35. package/dist/shaka-player.compiled.debug.d.ts +181 -27
  36. package/dist/shaka-player.compiled.debug.externs.js +169 -31
  37. package/dist/shaka-player.compiled.debug.js +510 -450
  38. package/dist/shaka-player.compiled.debug.map +4 -4
  39. package/dist/shaka-player.compiled.externs.js +169 -31
  40. package/dist/shaka-player.compiled.js +912 -874
  41. package/dist/shaka-player.compiled.map +4 -4
  42. package/dist/shaka-player.ui.d.ts +181 -27
  43. package/dist/shaka-player.ui.debug.d.ts +181 -27
  44. package/dist/shaka-player.ui.debug.externs.js +169 -31
  45. package/dist/shaka-player.ui.debug.js +533 -471
  46. package/dist/shaka-player.ui.debug.map +4 -4
  47. package/dist/shaka-player.ui.externs.js +169 -31
  48. package/dist/shaka-player.ui.js +1073 -1036
  49. package/dist/shaka-player.ui.map +4 -4
  50. package/docs/design/lcevc-architecture.png +0 -0
  51. package/docs/design/lcevc-demo.png +0 -0
  52. package/docs/design/lcevc-integration.md +72 -0
  53. package/docs/tutorials/fairplay.md +7 -0
  54. package/docs/tutorials/faq.md +1 -1
  55. package/docs/tutorials/ui.md +7 -7
  56. package/docs/tutorials/upgrade.md +6 -0
  57. package/externs/ima.js +13 -1
  58. package/externs/lcevc.js +81 -0
  59. package/externs/mux.js +1 -42
  60. package/externs/shaka/ads.js +11 -1
  61. package/externs/shaka/manifest_parser.js +5 -1
  62. package/externs/shaka/mp4_parser.js +3 -0
  63. package/externs/shaka/player.js +178 -15
  64. package/lib/abr/simple_abr_manager.js +7 -0
  65. package/lib/ads/ad_manager.js +0 -8
  66. package/lib/ads/client_side_ad.js +15 -0
  67. package/lib/ads/client_side_ad_manager.js +21 -8
  68. package/lib/ads/server_side_ad.js +15 -0
  69. package/lib/ads/server_side_ad_manager.js +3 -3
  70. package/lib/cast/cast_utils.js +2 -0
  71. package/lib/cea/dummy_cea_parser.js +28 -0
  72. package/lib/cea/i_cea_parser.js +14 -2
  73. package/lib/cea/mp4_cea_parser.js +108 -5
  74. package/lib/config/auto_show_text.js +33 -0
  75. package/lib/dash/dash_parser.js +1 -1
  76. package/lib/hls/hls_parser.js +808 -384
  77. package/lib/lcevc/lcevc_dil.js +152 -0
  78. package/lib/media/closed_caption_parser.js +9 -6
  79. package/lib/media/drm_engine.js +14 -6
  80. package/lib/media/media_source_capabilities.js +36 -0
  81. package/lib/media/media_source_engine.js +113 -28
  82. package/lib/media/presentation_timeline.js +26 -11
  83. package/lib/media/segment_reference.js +49 -1
  84. package/lib/media/streaming_engine.js +142 -12
  85. package/lib/media/transmuxer.js +78 -21
  86. package/lib/offline/storage.js +1 -0
  87. package/lib/player.js +456 -77
  88. package/lib/polyfill/media_capabilities.js +9 -5
  89. package/lib/polyfill/patchedmediakeys_apple.js +63 -4
  90. package/lib/text/vtt_text_parser.js +35 -1
  91. package/lib/util/dom_utils.js +0 -1
  92. package/lib/util/error.js +17 -0
  93. package/lib/util/fake_event.js +2 -0
  94. package/lib/util/id3_utils.js +300 -0
  95. package/lib/util/mime_utils.js +3 -6
  96. package/lib/util/mp4_box_parsers.js +39 -5
  97. package/lib/util/mp4_parser.js +35 -0
  98. package/lib/util/player_configuration.js +23 -1
  99. package/lib/util/stream_utils.js +5 -3
  100. package/lib/util/ts_parser.js +499 -0
  101. package/maintained-branches.md +2 -2
  102. package/package.json +7 -4
  103. package/roadmap.md +5 -3
  104. package/test/ads/ad_manager_unit.js +182 -29
  105. package/test/cast/cast_utils_unit.js +3 -1
  106. package/test/dash/dash_parser_content_protection_unit.js +1 -0
  107. package/test/dash/dash_parser_live_unit.js +1 -0
  108. package/test/dash/dash_parser_manifest_unit.js +93 -1
  109. package/test/dash/dash_parser_segment_base_unit.js +1 -0
  110. package/test/dash/dash_parser_segment_list_unit.js +1 -0
  111. package/test/dash/dash_parser_segment_template_unit.js +1 -0
  112. package/test/demo/demo_unit.js +2 -1
  113. package/test/hls/hls_live_unit.js +253 -19
  114. package/test/hls/hls_parser_unit.js +631 -12
  115. package/test/media/closed_caption_parser_unit.js +2 -1
  116. package/test/media/drm_engine_integration.js +3 -1
  117. package/test/media/media_source_engine_integration.js +71 -6
  118. package/test/media/media_source_engine_unit.js +10 -5
  119. package/test/media/streaming_engine_integration.js +4 -1
  120. package/test/media/streaming_engine_unit.js +144 -1
  121. package/test/media/transmuxer_integration.js +21 -10
  122. package/test/player_integration.js +224 -82
  123. package/test/player_unit.js +5 -2
  124. package/test/polyfill/media_capabilities_unit.js +21 -23
  125. package/test/polyfill/patchedmediakeys_apple_unit.js +130 -0
  126. package/test/test/assets/id3-metadata.aac +0 -0
  127. package/test/test/assets/id3-metadata.ts +0 -0
  128. package/test/test/assets/thumbnails-sprites.vtt +10 -0
  129. package/test/test/assets/thumbnails.vtt +10 -0
  130. package/test/test/util/dash_parser_util.js +2 -0
  131. package/test/test/util/fake_ad.js +21 -0
  132. package/test/test/util/fake_media_source_engine.js +7 -0
  133. package/test/test/util/id3_generator.js +100 -0
  134. package/test/test/util/manifest_generator.js +3 -6
  135. package/test/test/util/simple_fakes.js +4 -0
  136. package/test/test/util/stream_generator.js +38 -0
  137. package/test/test/util/test_scheme.js +43 -14
  138. package/test/text/vtt_text_parser_unit.js +11 -0
  139. package/test/util/id3_utils_unit.js +116 -0
  140. package/test/util/ts_parser_unit.js +77 -0
  141. package/third_party/closure-uri/uri.js +1 -1
  142. package/ui/less/containers.less +7 -0
  143. package/ui/ui.js +43 -4
@@ -71,6 +71,24 @@ shaka.hls.HlsParser = class {
71
71
  */
72
72
  this.groupIdToStreamInfosMap_ = new Map();
73
73
 
74
+ /**
75
+ * For media playlist lazy-loading to work in livestreams, we have to assume
76
+ * that each stream of a type (video, audio, etc) has the same mappings of
77
+ * sequence number to start time.
78
+ * This map stores those relationships.
79
+ * Only used during livestreams; we do not assume that VOD content is
80
+ * aligned in that way.
81
+ * @private {!Map.<string, !Map.<number, number>>}
82
+ */
83
+ this.mediaSequenceToStartTimeByType_ = new Map();
84
+
85
+ // Set initial maps.
86
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
87
+ this.mediaSequenceToStartTimeByType_.set(ContentType.VIDEO, new Map());
88
+ this.mediaSequenceToStartTimeByType_.set(ContentType.AUDIO, new Map());
89
+ this.mediaSequenceToStartTimeByType_.set(ContentType.TEXT, new Map());
90
+ this.mediaSequenceToStartTimeByType_.set(ContentType.IMAGE, new Map());
91
+
74
92
  /**
75
93
  * The values are strings of the form "<VIDEO URI> - <AUDIO URI>",
76
94
  * where the URIs are the verbatim media playlist URIs as they appeared in
@@ -122,19 +140,29 @@ shaka.hls.HlsParser = class {
122
140
  this.updatePlaylistDelay_ = 0;
123
141
 
124
142
  /**
125
- * If true, we have already calculated offsets to synchronize streams.
126
- * Offsets are computed in syncStreams*_().
127
- * @private {boolean}
143
+ * The minimum sequence number for generated segments, when ignoring
144
+ * EXT-X-PROGRAM-DATE-TIME.
145
+ *
146
+ * @private {number}
128
147
  */
129
- this.streamsSynced_ = false;
148
+ this.minSequenceNumber_ = -1;
130
149
 
131
150
  /**
132
- * The minimum sequence number for generated segments, when ignoring
133
- * EXT-X-PROGRAM-DATE-TIME.
151
+ * The lowest time value for any of the streams, as defined by the
152
+ * EXT-X-PROGRAM-DATE-TIME value. Measured in seconds since January 1, 1970.
153
+ *
154
+ * @private {number}
155
+ */
156
+ this.lowestSyncTime_ = Infinity;
157
+
158
+ /**
159
+ * Whether the streams have previously been "finalized"; that is to say,
160
+ * whether we have loaded enough streams to know information about the asset
161
+ * such as timing information, live status, etc.
134
162
  *
135
- * @private {?number}
163
+ * @private {boolean}
136
164
  */
137
- this.minSequenceNumber_ = null;
165
+ this.streamsFinalized_ = false;
138
166
 
139
167
  /**
140
168
  * This timer is used to trigger the start of a manifest update. A manifest
@@ -171,9 +199,6 @@ shaka.hls.HlsParser = class {
171
199
  /** @private {shaka.util.OperationManager} */
172
200
  this.operationManager_ = new shaka.util.OperationManager();
173
201
 
174
- /** @private {!Array.<!Array.<!shaka.media.SegmentReference>>} */
175
- this.segmentsToNotifyByStream_ = [];
176
-
177
202
  /** A map from closed captions' group id, to a map of closed captions info.
178
203
  * {group id -> {closed captions channel id -> language}}
179
204
  * @private {Map.<string, Map.<string, string>>}
@@ -220,12 +245,6 @@ shaka.hls.HlsParser = class {
220
245
  goog.asserts.assert(response.data, 'Response data should be non-null!');
221
246
  await this.parseManifest_(response.data, uri);
222
247
 
223
- // Start the update timer if we want updates.
224
- const delay = this.updatePlaylistDelay_;
225
- if (delay > 0) {
226
- this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
227
- }
228
-
229
248
  goog.asserts.assert(this.manifest_, 'Manifest should be non-null');
230
249
  return this.manifest_;
231
250
  }
@@ -274,22 +293,20 @@ shaka.hls.HlsParser = class {
274
293
  /** @type {!Array.<!Promise>} */
275
294
  const updates = [];
276
295
  const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
277
- // Wait for the first stream info created, so that the start time is fetched
278
- // and can be reused.
279
- if (streamInfos.length) {
280
- await this.updateStream_(streamInfos[0]);
281
- }
282
- for (let i = 1; i < streamInfos.length; i++) {
283
- updates.push(this.updateStream_(streamInfos[i]));
284
- }
285
296
 
297
+ // Only update active streams.
298
+ const activeStreamInfos = streamInfos.filter((s) => s.stream.segmentIndex);
299
+ for (const streamInfo of activeStreamInfos) {
300
+ updates.push(this.updateStream_(streamInfo));
301
+ }
286
302
  await Promise.all(updates);
287
303
 
288
- this.notifySegments_();
304
+ // Now that streams have been updated, notify the presentation timeline.
305
+ this.notifySegmentsForStreams_(activeStreamInfos.map((s) => s.stream));
289
306
 
290
307
  // If any hasEndList is false, the stream is still live.
291
- const stillLive = streamInfos.some((s) => s.hasEndList == false);
292
- if (!stillLive) {
308
+ const stillLive = activeStreamInfos.some((s) => s.hasEndList == false);
309
+ if (activeStreamInfos.length && !stillLive) {
293
310
  // Convert the presentation to VOD and set the duration.
294
311
  const PresentationType = shaka.hls.HlsParser.PresentationType_;
295
312
  this.setPresentationType_(PresentationType.VOD);
@@ -301,6 +318,19 @@ shaka.hls.HlsParser = class {
301
318
  }
302
319
  }
303
320
 
321
+ /**
322
+ * @param {!shaka.hls.HlsParser.StreamInfo} streamInfo
323
+ * @return {!Map.<number, number>}
324
+ * @private
325
+ */
326
+ getMediaSequenceToStartTimeFor_(streamInfo) {
327
+ if (this.isLive_()) {
328
+ return this.mediaSequenceToStartTimeByType_.get(streamInfo.type);
329
+ } else {
330
+ return streamInfo.mediaSequenceToStartTime;
331
+ }
332
+ }
333
+
304
334
  /**
305
335
  * Updates a stream.
306
336
  *
@@ -317,6 +347,10 @@ shaka.hls.HlsParser = class {
317
347
  uriObj.setQueryData(new goog.Uri.QueryData('_HLS_skip=YES'));
318
348
  }
319
349
  const response = await this.requestManifest_(uriObj.toString());
350
+ if (!streamInfo.stream.segmentIndex) {
351
+ // The stream was closed since the update was first requested.
352
+ return;
353
+ }
320
354
 
321
355
  /** @type {shaka.hls.Playlist} */
322
356
  const playlist = this.manifestTextParser_.parsePlaylist(
@@ -337,18 +371,29 @@ shaka.hls.HlsParser = class {
337
371
 
338
372
  const stream = streamInfo.stream;
339
373
 
374
+ const mediaSequenceToStartTime =
375
+ this.getMediaSequenceToStartTimeFor_(streamInfo);
376
+ const {keyIds, drmInfos} = this.parseDrmInfo_(playlist, stream.mimeType);
377
+
378
+ const keysAreEqual =
379
+ (a, b) => a.size === b.size && [...a].every((value) => b.has(value));
380
+
381
+ if (!keysAreEqual(stream.keyIds, keyIds)) {
382
+ stream.keyIds = keyIds;
383
+ stream.drmInfos = drmInfos;
384
+ this.playerInterface_.newDrmInfo(stream);
385
+ }
386
+
340
387
  const segments = this.createSegments_(
341
388
  streamInfo.verbatimMediaPlaylistUri, playlist, stream.type,
342
- stream.mimeType, streamInfo.mediaSequenceToStartTime, mediaVariables,
343
- stream.codecs);
344
- this.segmentsToNotifyByStream_.push(segments);
389
+ stream.mimeType, mediaSequenceToStartTime, mediaVariables);
345
390
 
346
391
  stream.segmentIndex.mergeAndEvict(
347
392
  segments, this.presentationTimeline_.getSegmentAvailabilityStart());
348
393
  if (segments.length) {
349
394
  const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
350
395
  playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0);
351
- const playlistStartTime = streamInfo.mediaSequenceToStartTime.get(
396
+ const playlistStartTime = mediaSequenceToStartTime.get(
352
397
  mediaSequenceNumber);
353
398
  stream.segmentIndex.evict(playlistStartTime);
354
399
  }
@@ -379,68 +424,70 @@ shaka.hls.HlsParser = class {
379
424
  }
380
425
 
381
426
  /**
382
- * Align all streams by sequence number by dropping early segments. Then
383
- * offset all streams to begin at presentation time 0.
427
+ * Align the streams by sequence number by dropping early segments. Then
428
+ * offset the streams to begin at presentation time 0.
429
+ * @param {!Array.<!shaka.hls.HlsParser.StreamInfo>} streamInfos
384
430
  * @private
385
431
  */
386
- syncStreamsWithSequenceNumber_() {
387
- if (this.streamsSynced_) {
388
- return;
389
- }
390
-
432
+ syncStreamsWithSequenceNumber_(streamInfos) {
433
+ // We assume that, when this is first called, we have enough info to
434
+ // determine how to use the program date times (e.g. we have both a video
435
+ // and an audio, and all other videos and audios match those).
436
+ // Thus, we only need to calculate this once.
437
+ const updateMinSequenceNumber = this.minSequenceNumber_ == -1;
391
438
  // Sync using media sequence number. Find the highest starting sequence
392
439
  // number among all streams. Later, we will drop any references to
393
440
  // earlier segments in other streams, then offset everything back to 0.
394
- let highestStartingSequenceNumber = -1;
395
- const firstSequenceNumberMap = new Map();
396
-
397
- for (const streamInfo of this.uriToStreamInfosMap_.values()) {
441
+ for (const streamInfo of streamInfos) {
398
442
  const segmentIndex = streamInfo.stream.segmentIndex;
399
- if (segmentIndex) {
400
- const segment0 = segmentIndex.earliestReference();
401
- if (segment0) {
402
- // This looks inefficient, but iteration order is insertion order.
403
- // So the very first entry should be the one we want.
404
- // We assert that this holds true so that we are alerted by debug
405
- // builds and tests if it changes. We still do a loop, though, so
406
- // that the code functions correctly in production no matter what.
407
- if (goog.DEBUG) {
408
- const firstSequenceStartTime =
409
- streamInfo.mediaSequenceToStartTime.values().next().value;
410
- goog.asserts.assert(
411
- firstSequenceStartTime == segment0.startTime,
412
- 'Sequence number map is not ordered as expected!');
413
- }
414
- for (const [sequence, start] of streamInfo.mediaSequenceToStartTime) {
415
- if (start == segment0.startTime) {
416
- firstSequenceNumberMap.set(streamInfo, sequence);
417
-
418
- highestStartingSequenceNumber = Math.max(
419
- highestStartingSequenceNumber, sequence);
420
- break;
443
+ goog.asserts.assert(segmentIndex,
444
+ 'Only loaded streams should be synced');
445
+ const mediaSequenceToStartTime =
446
+ this.getMediaSequenceToStartTimeFor_(streamInfo);
447
+ const segment0 = segmentIndex.earliestReference();
448
+ if (segment0) {
449
+ // This looks inefficient, but iteration order is insertion order.
450
+ // So the very first entry should be the one we want.
451
+ // We assert that this holds true so that we are alerted by debug
452
+ // builds and tests if it changes. We still do a loop, though, so
453
+ // that the code functions correctly in production no matter what.
454
+ if (goog.DEBUG) {
455
+ const firstSequenceStartTime =
456
+ mediaSequenceToStartTime.values().next().value;
457
+ goog.asserts.assert(
458
+ firstSequenceStartTime == segment0.startTime,
459
+ 'Sequence number map is not ordered as expected!');
460
+ }
461
+ for (const [sequence, start] of mediaSequenceToStartTime) {
462
+ if (start == segment0.startTime) {
463
+ if (updateMinSequenceNumber) {
464
+ this.minSequenceNumber_ = Math.max(
465
+ this.minSequenceNumber_, sequence);
421
466
  }
467
+ // Even if we already have decided on a value for
468
+ // |this.minSequenceNumber_|, we still need to determine the first
469
+ // sequence number for the stream, to offset it in the code below.
470
+ streamInfo.firstSequenceNumber = sequence;
471
+ break;
422
472
  }
423
473
  }
424
474
  }
425
475
  }
426
476
 
427
- if (highestStartingSequenceNumber < 0) {
477
+ if (this.minSequenceNumber_ < 0) {
428
478
  // Nothing to sync.
429
479
  return;
430
480
  }
431
481
 
432
- // From now on, updates will ignore any references before this number.
433
- this.minSequenceNumber_ = highestStartingSequenceNumber;
434
-
435
482
  shaka.log.debug('Syncing HLS streams against base sequence number:',
436
483
  this.minSequenceNumber_);
437
484
 
438
- for (const streamInfo of this.uriToStreamInfosMap_.values()) {
485
+ for (const streamInfo of streamInfos) {
439
486
  const segmentIndex = streamInfo.stream.segmentIndex;
440
487
  if (segmentIndex) {
441
488
  // Drop any earlier references.
442
- const numSegmentsToDrop = this.minSequenceNumber_ -
443
- firstSequenceNumberMap.get(streamInfo);
489
+ const numSegmentsToDrop =
490
+ this.minSequenceNumber_ - streamInfo.firstSequenceNumber;
444
491
  segmentIndex.dropFirstReferences(numSegmentsToDrop);
445
492
 
446
493
  // Now adjust timestamps back to begin at 0.
@@ -450,33 +497,34 @@ shaka.hls.HlsParser = class {
450
497
  }
451
498
  }
452
499
  }
453
-
454
- this.streamsSynced_ = true;
455
500
  }
456
501
 
457
502
  /**
458
503
  * Synchronize streams by the EXT-X-PROGRAM-DATE-TIME tags attached to their
459
504
  * segments. Also normalizes segment times so that the earliest segment in
460
505
  * any stream is at time 0.
506
+ * @param {!Array.<!shaka.hls.HlsParser.StreamInfo>} streamInfos
461
507
  * @private
462
508
  */
463
- syncStreamsWithProgramDateTime_() {
464
- if (this.streamsSynced_) {
465
- return;
466
- }
467
-
468
- let lowestSyncTime = Infinity;
469
-
470
- for (const streamInfo of this.uriToStreamInfosMap_.values()) {
471
- const segmentIndex = streamInfo.stream.segmentIndex;
472
- if (segmentIndex) {
509
+ syncStreamsWithProgramDateTime_(streamInfos) {
510
+ // We assume that, when this is first called, we have enough info to
511
+ // determine how to use the program date times (e.g. we have both a video
512
+ // and an audio, and all other videos and audios match those).
513
+ // Thus, we only need to calculate this once.
514
+ if (this.lowestSyncTime_ == Infinity) {
515
+ for (const streamInfo of streamInfos) {
516
+ const segmentIndex = streamInfo.stream.segmentIndex;
517
+ goog.asserts.assert(segmentIndex,
518
+ 'Only loaded streams should be synced');
473
519
  const segment0 = segmentIndex.earliestReference();
474
520
  if (segment0 != null && segment0.syncTime != null) {
475
- lowestSyncTime = Math.min(lowestSyncTime, segment0.syncTime);
521
+ this.lowestSyncTime_ =
522
+ Math.min(this.lowestSyncTime_, segment0.syncTime);
476
523
  }
477
524
  }
478
525
  }
479
526
 
527
+ const lowestSyncTime = this.lowestSyncTime_;
480
528
  if (lowestSyncTime == Infinity) {
481
529
  // Nothing to sync.
482
530
  return;
@@ -506,8 +554,6 @@ shaka.hls.HlsParser = class {
506
554
  }
507
555
  }
508
556
  }
509
-
510
- this.streamsSynced_ = true;
511
557
  }
512
558
 
513
559
  /**
@@ -522,8 +568,10 @@ shaka.hls.HlsParser = class {
522
568
  goog.asserts.assert(streamInfo.maxTimestamp >= 0,
523
569
  'Negative maxTimestamp after adjustment!');
524
570
 
525
- for (const [key, value] of streamInfo.mediaSequenceToStartTime) {
526
- streamInfo.mediaSequenceToStartTime.set(key, value + offset);
571
+ const mediaSequenceToStartTime =
572
+ this.getMediaSequenceToStartTimeFor_(streamInfo);
573
+ for (const [key, value] of mediaSequenceToStartTime) {
574
+ mediaSequenceToStartTime.set(key, value + offset);
527
575
  }
528
576
 
529
577
  shaka.log.debug('Offset', offset, 'applied to',
@@ -539,6 +587,7 @@ shaka.hls.HlsParser = class {
539
587
  * @private
540
588
  */
541
589
  async parseManifest_(data, uri) {
590
+ const HlsParser = shaka.hls.HlsParser;
542
591
  const Utils = shaka.hls.Utils;
543
592
 
544
593
  goog.asserts.assert(this.masterPlaylistUri_,
@@ -564,7 +613,31 @@ shaka.hls.HlsParser = class {
564
613
  // Get necessary info for this stream, from the config. These are things
565
614
  // we would normally find from the master playlist (e.g. from values on
566
615
  // EXT-X-MEDIA tags).
567
- const fullMimeType = this.config_.hls.mediaPlaylistFullMimeType;
616
+ let fullMimeType = this.config_.hls.mediaPlaylistFullMimeType;
617
+ // Try to infer the full mimetype better.
618
+ if (playlist.segments.length) {
619
+ const parsedUri = new goog.Uri(playlist.segments[0].absoluteUri);
620
+ const extension = parsedUri.getPath().split('.').pop();
621
+ let mimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension];
622
+ if (mimeType) {
623
+ fullMimeType = mimeType;
624
+ } else if (extension === 'ts') {
625
+ // TODO: Fetch one segment a use the TsParser to analize if there is
626
+ // video, audio or both.
627
+ } else if (extension === 'mp4') {
628
+ // TODO: Fetch one segment a use the Mp4Parser to analize if there is
629
+ // video, audio or both.
630
+ } else if (HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_[extension]) {
631
+ mimeType = HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_[extension];
632
+ const defaultAudioCodec = this.config_.hls.defaultAudioCodec;
633
+ fullMimeType = `${mimeType}; codecs="${defaultAudioCodec}"`;
634
+ } else if (HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_[extension]) {
635
+ mimeType = HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_[extension];
636
+ const defaultVideoCodec = this.config_.hls.defaultVideoCodec;
637
+ fullMimeType = `${mimeType}; codecs="${defaultVideoCodec}"`;
638
+ }
639
+ }
640
+
568
641
  const mimeType = shaka.util.MimeUtils.getBasicType(fullMimeType);
569
642
  const type = mimeType.split('/')[0];
570
643
  const codecs = shaka.util.MimeUtils.getCodecs(fullMimeType);
@@ -611,6 +684,9 @@ shaka.hls.HlsParser = class {
611
684
  /** @type {!Array.<!shaka.hls.Tag>} */
612
685
  const imageTags = Utils.filterTagsByName(
613
686
  playlist.tags, 'EXT-X-IMAGE-STREAM-INF');
687
+ /** @type {!Array.<!shaka.hls.Tag>} */
688
+ const sessionKeyTags = Utils.filterTagsByName(
689
+ playlist.tags, 'EXT-X-SESSION-KEY');
614
690
 
615
691
  this.parseCodecs_(variantTags);
616
692
 
@@ -641,10 +717,10 @@ shaka.hls.HlsParser = class {
641
717
 
642
718
  // Parse audio and video media tags first, so that we can extract segment
643
719
  // start time from audio/video streams and reuse for text streams.
644
- await this.createStreamInfosFromMediaTags_(mediaTags);
720
+ this.createStreamInfosFromMediaTags_(mediaTags);
645
721
  this.parseClosedCaptions_(mediaTags);
646
- variants = await this.createVariantsForTags_(variantTags);
647
- textStreams = await this.parseTexts_(mediaTags);
722
+ variants = this.createVariantsForTags_(variantTags, sessionKeyTags);
723
+ textStreams = this.parseTexts_(mediaTags);
648
724
  imageStreams = await this.parseImages_(imageTags);
649
725
  }
650
726
 
@@ -656,32 +732,45 @@ shaka.hls.HlsParser = class {
656
732
  shaka.util.Error.Code.OPERATION_ABORTED);
657
733
  }
658
734
 
659
- // Now that we have generated all streams, we can determine the offset to
660
- // apply to sync times.
661
- if (this.config_.hls.ignoreManifestProgramDateTime) {
662
- this.syncStreamsWithSequenceNumber_();
663
- } else {
664
- this.syncStreamsWithProgramDateTime_();
665
- }
666
-
667
- // Find the min and max timestamp of the earliest segment in all streams.
668
- // Find the minimum duration of all streams as well.
669
- let minDuration = Infinity;
670
- for (const streamInfo of this.uriToStreamInfosMap_.values()) {
671
- if (streamInfo.stream.type != 'text') {
672
- // Since everything is already offset to 0 (either by sync or by being
673
- // VOD), only maxTimestamp is necessary to compute the duration.
674
- minDuration = Math.min(minDuration, streamInfo.maxTimestamp);
675
- }
676
- }
677
-
678
735
  // This assert is our own sanity check.
679
736
  goog.asserts.assert(this.presentationTimeline_ == null,
680
737
  'Presentation timeline created early!');
681
- this.createPresentationTimeline_();
738
+ // We don't know if the presentation is VOD or live until we parse at least
739
+ // one media playlist, so make a VOD-style presentation timeline for now
740
+ // and change the type later if we discover this is live.
741
+ // Since the player will load the first variant chosen early in the process,
742
+ // there isn't a window during playback where the live-ness is unknown.
743
+ this.presentationTimeline_ = new shaka.media.PresentationTimeline(
744
+ /* presentationStartTime= */ null, /* delay= */ 0);
745
+ this.presentationTimeline_.setStatic(true);
746
+
747
+ // Single-variant streams aren't lazy-loaded, so for them we already have
748
+ // enough info here to determine the presentation type and duration.
749
+ if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
750
+ if (this.isLive_()) {
751
+ this.changePresentationTimelineToLive_();
752
+ const delay = this.updatePlaylistDelay_;
753
+ this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
754
+ }
755
+ const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
756
+ this.finalizeStreams_(streamInfos);
757
+ this.determineDuration_();
758
+ }
682
759
 
683
- // This assert satisfies the compiler that it is not null for the rest of
684
- // the method.
760
+ this.manifest_ = {
761
+ presentationTimeline: this.presentationTimeline_,
762
+ variants,
763
+ textStreams,
764
+ imageStreams,
765
+ offlineSessionIds: [],
766
+ minBufferTime: 0,
767
+ sequenceMode: true,
768
+ };
769
+ this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
770
+ }
771
+
772
+ /** @private */
773
+ determineDuration_() {
685
774
  goog.asserts.assert(this.presentationTimeline_,
686
775
  'Presentation timeline not created!');
687
776
 
@@ -701,10 +790,14 @@ shaka.hls.HlsParser = class {
701
790
  const PresentationType = shaka.hls.HlsParser.PresentationType_;
702
791
 
703
792
  if (this.presentationType_ == PresentationType.LIVE) {
793
+ let segmentAvailabilityDuration = this.getMinDuration_();
794
+
704
795
  // This defaults to the presentation delay, which has the effect of
705
796
  // making the live stream unseekable. This is consistent with Apple's
706
797
  // HLS implementation.
707
- let segmentAvailabilityDuration = this.presentationTimeline_.getDelay();
798
+ if (this.config_.hls.useSafariBehaviorForLive) {
799
+ segmentAvailabilityDuration = this.presentationTimeline_.getDelay();
800
+ }
708
801
 
709
802
  // The app can override that with a longer duration, to allow seeking.
710
803
  if (!isNaN(this.config_.availabilityWindowOverride)) {
@@ -716,33 +809,9 @@ shaka.hls.HlsParser = class {
716
809
  }
717
810
  } else {
718
811
  // Use the minimum duration as the presentation duration.
719
- this.presentationTimeline_.setDuration(minDuration);
720
-
721
- for (const streamInfo of this.uriToStreamInfosMap_.values()) {
722
- // Fit the segments to the playlist duration.
723
- streamInfo.stream.segmentIndex.fit(/* periodStart= */ 0, minDuration);
724
- }
812
+ this.presentationTimeline_.setDuration(this.getMinDuration_());
725
813
  }
726
814
 
727
- // Now that the content has been fit, notify segments.
728
- this.segmentsToNotifyByStream_ = [];
729
- const streamsToNotify = [];
730
- for (const variant of variants) {
731
- for (const stream of [variant.video, variant.audio]) {
732
- if (stream) {
733
- streamsToNotify.push(stream);
734
- }
735
- }
736
- }
737
- await Promise.all(streamsToNotify.map(async (stream) => {
738
- await stream.createSegmentIndex();
739
- }));
740
- for (const stream of streamsToNotify) {
741
- this.segmentsToNotifyByStream_.push(stream.segmentIndex.references);
742
- }
743
-
744
- this.notifySegments_();
745
-
746
815
  // This is the first point where we have a meaningful presentation start
747
816
  // time, and we need to tell PresentationTimeline that so that it can
748
817
  // maintain consistency from here on.
@@ -753,17 +822,6 @@ shaka.hls.HlsParser = class {
753
822
  goog.asserts.assert(
754
823
  !this.presentationTimeline_.usingPresentationStartTime(),
755
824
  'We should not be using the presentation start time in HLS!');
756
-
757
- this.manifest_ = {
758
- presentationTimeline: this.presentationTimeline_,
759
- variants,
760
- textStreams,
761
- imageStreams,
762
- offlineSessionIds: [],
763
- minBufferTime: 0,
764
- sequenceMode: true,
765
- };
766
- this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
767
825
  }
768
826
 
769
827
  /**
@@ -854,21 +912,20 @@ shaka.hls.HlsParser = class {
854
912
  * Create text streams for Subtitles, but not Closed Captions.
855
913
  *
856
914
  * @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
857
- * @return {!Promise.<!Array.<!shaka.extern.Stream>>}
915
+ * @return {!Array.<!shaka.extern.Stream>}
858
916
  * @private
859
917
  */
860
- async parseTexts_(mediaTags) {
918
+ parseTexts_(mediaTags) {
861
919
  // Create text stream for each Subtitle media tag.
862
920
  const subtitleTags =
863
921
  shaka.hls.Utils.filterTagsByType(mediaTags, 'SUBTITLES');
864
- const textStreamPromises = subtitleTags.map(async (tag) => {
922
+ const textStreams = subtitleTags.map((tag) => {
865
923
  const disableText = this.config_.disableText;
866
924
  if (disableText) {
867
925
  return null;
868
926
  }
869
927
  try {
870
- const streamInfo = await this.createStreamInfoFromMediaTag_(tag);
871
- return streamInfo.stream;
928
+ return this.createStreamInfoFromMediaTag_(tag).stream;
872
929
  } catch (e) {
873
930
  if (this.config_.hls.ignoreTextStreamFailures) {
874
931
  return null;
@@ -876,7 +933,6 @@ shaka.hls.HlsParser = class {
876
933
  throw e;
877
934
  }
878
935
  });
879
- const textStreams = await Promise.all(textStreamPromises);
880
936
 
881
937
  // Set the codecs for text streams.
882
938
  for (const tag of subtitleTags) {
@@ -926,7 +982,7 @@ shaka.hls.HlsParser = class {
926
982
  * @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
927
983
  * @private
928
984
  */
929
- async createStreamInfosFromMediaTags_(mediaTags) {
985
+ createStreamInfosFromMediaTags_(mediaTags) {
930
986
  // Filter out subtitles and media tags without uri.
931
987
  mediaTags = mediaTags.filter((tag) => {
932
988
  const uri = tag.getAttributeValue('URI') || '';
@@ -935,20 +991,52 @@ shaka.hls.HlsParser = class {
935
991
  });
936
992
 
937
993
  // Create stream info for each audio / video media tag.
938
- const promises = mediaTags.map((tag) => {
939
- return this.createStreamInfoFromMediaTag_(tag);
940
- });
941
- await Promise.all(promises);
994
+ for (const tag of mediaTags) {
995
+ this.createStreamInfoFromMediaTag_(tag);
996
+ }
942
997
  }
943
998
 
944
999
  /**
945
1000
  * @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
946
- * @return {!Promise.<!Array.<!shaka.extern.Variant>>}
1001
+ * @param {!Array.<!shaka.hls.Tag>} sessionKeyTags EXT-X-SESSION-KEY tags
1002
+ * from the playlist.
1003
+ * @return {!Array.<!shaka.extern.Variant>}
947
1004
  * @private
948
1005
  */
949
- async createVariantsForTags_(tags) {
1006
+ createVariantsForTags_(tags, sessionKeyTags) {
1007
+ // EXT-X-SESSION-KEY processing
1008
+ const drmInfos = [];
1009
+ const keyIds = new Set();
1010
+ if (sessionKeyTags.length > 0) {
1011
+ for (const drmTag of sessionKeyTags) {
1012
+ const method = drmTag.getRequiredAttrValue('METHOD');
1013
+ if (method != 'NONE' && method != 'AES-128') {
1014
+ // According to the HLS spec, KEYFORMAT is optional and implicitly
1015
+ // defaults to "identity".
1016
+ // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
1017
+ const keyFormat =
1018
+ drmTag.getAttributeValue('KEYFORMAT') || 'identity';
1019
+ const drmParser =
1020
+ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
1021
+
1022
+ const drmInfo = drmParser ?
1023
+ drmParser(drmTag, /* mimeType= */ '') : null;
1024
+ if (drmInfo) {
1025
+ if (drmInfo.keyIds) {
1026
+ for (const keyId of drmInfo.keyIds) {
1027
+ keyIds.add(keyId);
1028
+ }
1029
+ }
1030
+ drmInfos.push(drmInfo);
1031
+ } else {
1032
+ shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
1033
+ }
1034
+ }
1035
+ }
1036
+ }
1037
+
950
1038
  // Create variants for each variant tag.
951
- const variantsPromises = tags.map(async (tag) => {
1039
+ const allVariants = tags.map((tag) => {
952
1040
  const frameRate = tag.getAttributeValue('FRAME-RATE');
953
1041
  const bandwidth = Number(tag.getAttributeValue('AVERAGE-BANDWIDTH')) ||
954
1042
  Number(tag.getRequiredAttrValue('BANDWIDTH'));
@@ -958,7 +1046,7 @@ shaka.hls.HlsParser = class {
958
1046
 
959
1047
  const videoRange = tag.getAttributeValue('VIDEO-RANGE');
960
1048
 
961
- const streamInfos = await this.createStreamInfosForVariantTag_(tag,
1049
+ const streamInfos = this.createStreamInfosForVariantTag_(tag,
962
1050
  resolution, frameRate);
963
1051
 
964
1052
  goog.asserts.assert(streamInfos.audio.length ||
@@ -971,10 +1059,10 @@ shaka.hls.HlsParser = class {
971
1059
  width,
972
1060
  height,
973
1061
  frameRate,
974
- videoRange);
1062
+ videoRange,
1063
+ drmInfos,
1064
+ keyIds);
975
1065
  });
976
-
977
- const allVariants = await Promise.all(variantsPromises);
978
1066
  let variants = allVariants.reduce(shaka.util.Functional.collapseArrays, []);
979
1067
  // Filter out null variants.
980
1068
  variants = variants.filter((variant) => variant != null);
@@ -988,10 +1076,10 @@ shaka.hls.HlsParser = class {
988
1076
  * @param {!shaka.hls.Tag} tag
989
1077
  * @param {?string} resolution
990
1078
  * @param {?string} frameRate
991
- * @return {!Promise.<!shaka.hls.HlsParser.StreamInfos>}
1079
+ * @return {!shaka.hls.HlsParser.StreamInfos}
992
1080
  * @private
993
1081
  */
994
- async createStreamInfosForVariantTag_(tag, resolution, frameRate) {
1082
+ createStreamInfosForVariantTag_(tag, resolution, frameRate) {
995
1083
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
996
1084
  /** @type {!Array.<string>} */
997
1085
  let allCodecs = this.getCodecsForVariantTag_(tag);
@@ -1060,10 +1148,9 @@ shaka.hls.HlsParser = class {
1060
1148
 
1061
1149
  if (!ignoreStream) {
1062
1150
  const streamInfo =
1063
- await this.createStreamInfoFromVariantTag_(tag, allCodecs, type);
1151
+ this.createStreamInfoFromVariantTag_(tag, allCodecs, type);
1064
1152
  res[streamInfo.stream.type] = [streamInfo];
1065
1153
  }
1066
- this.filterLegacyCodecs_(res);
1067
1154
  return res;
1068
1155
  }
1069
1156
 
@@ -1194,7 +1281,7 @@ shaka.hls.HlsParser = class {
1194
1281
 
1195
1282
  /**
1196
1283
  * Get the type value.
1197
- * Shaka recognizes the content types 'audio', 'video' and 'text'.
1284
+ * Shaka recognizes the content types 'audio', 'video', 'text', and 'image'.
1198
1285
  * The HLS 'subtitles' type needs to be mapped to 'text'.
1199
1286
  * @param {!shaka.hls.Tag} tag
1200
1287
  * @return {string}
@@ -1208,28 +1295,6 @@ shaka.hls.HlsParser = class {
1208
1295
  return type;
1209
1296
  }
1210
1297
 
1211
- /**
1212
- * Filters out unsupported codec strings from an array of stream infos.
1213
- * @param {shaka.hls.HlsParser.StreamInfos} streamInfos
1214
- * @private
1215
- */
1216
- filterLegacyCodecs_(streamInfos) {
1217
- for (const streamInfo of streamInfos.audio.concat(streamInfos.video)) {
1218
- if (!streamInfo) {
1219
- continue;
1220
- }
1221
- let codecs = streamInfo.stream.codecs.split(',');
1222
- codecs = codecs.filter((codec) => {
1223
- // mp4a.40.34 is a nonstandard codec string that is sometimes used in
1224
- // HLS for legacy reasons. It is not recognized by non-Apple MSE.
1225
- // See https://bugs.chromium.org/p/chromium/issues/detail?id=489520
1226
- // Therefore, ignore this codec string.
1227
- return codec != 'mp4a.40.34';
1228
- });
1229
- streamInfo.stream.codecs = codecs.join(',');
1230
- }
1231
- }
1232
-
1233
1298
  /**
1234
1299
  * @param {!Array.<shaka.hls.HlsParser.StreamInfo>} audioInfos
1235
1300
  * @param {!Array.<shaka.hls.HlsParser.StreamInfo>} videoInfos
@@ -1238,11 +1303,14 @@ shaka.hls.HlsParser = class {
1238
1303
  * @param {?string} height
1239
1304
  * @param {?string} frameRate
1240
1305
  * @param {?string} videoRange
1306
+ * @param {!Array.<shaka.extern.DrmInfo>} drmInfos
1307
+ * @param {!Set.<string>} keyIds
1241
1308
  * @return {!Array.<!shaka.extern.Variant>}
1242
1309
  * @private
1243
1310
  */
1244
1311
  createVariants_(
1245
- audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange) {
1312
+ audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange,
1313
+ drmInfos, keyIds) {
1246
1314
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
1247
1315
  const DrmEngine = shaka.media.DrmEngine;
1248
1316
 
@@ -1268,7 +1336,15 @@ shaka.hls.HlsParser = class {
1268
1336
  for (const audioInfo of audioInfos) {
1269
1337
  for (const videoInfo of videoInfos) {
1270
1338
  const audioStream = audioInfo ? audioInfo.stream : null;
1339
+ if (audioStream) {
1340
+ audioStream.drmInfos = drmInfos;
1341
+ audioStream.keyIds = keyIds;
1342
+ }
1271
1343
  const videoStream = videoInfo ? videoInfo.stream : null;
1344
+ if (videoStream) {
1345
+ videoStream.drmInfos = drmInfos;
1346
+ videoStream.keyIds = keyIds;
1347
+ }
1272
1348
  const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null;
1273
1349
  const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null;
1274
1350
  const videoStreamUri =
@@ -1353,10 +1429,10 @@ shaka.hls.HlsParser = class {
1353
1429
  * Parse EXT-X-MEDIA media tag into a Stream object.
1354
1430
  *
1355
1431
  * @param {shaka.hls.Tag} tag
1356
- * @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
1432
+ * @return {!shaka.hls.HlsParser.StreamInfo}
1357
1433
  * @private
1358
1434
  */
1359
- async createStreamInfoFromMediaTag_(tag) {
1435
+ createStreamInfoFromMediaTag_(tag) {
1360
1436
  goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
1361
1437
  'Should only be called on media tags!');
1362
1438
  const groupId = tag.getRequiredAttrValue('GROUP-ID');
@@ -1398,7 +1474,7 @@ shaka.hls.HlsParser = class {
1398
1474
  const forced = forcedAttrValue == 'YES';
1399
1475
  // TODO: Should we take into account some of the currently ignored
1400
1476
  // attributes: INSTREAM-ID, Attribute descriptions: https://bit.ly/2lpjOhj
1401
- const streamInfo = await this.createStreamInfo_(
1477
+ const streamInfo = this.createStreamInfo_(
1402
1478
  verbatimMediaPlaylistUri, codecs, type, language, primary, name,
1403
1479
  channelsCount, /* closedCaptions= */ null, characteristics, forced,
1404
1480
  spatialAudio);
@@ -1445,7 +1521,7 @@ shaka.hls.HlsParser = class {
1445
1521
 
1446
1522
  const characteristics = tag.getAttributeValue('CHARACTERISTICS');
1447
1523
 
1448
- const streamInfo = await this.createStreamInfo_(
1524
+ const streamInfo = this.createStreamInfo_(
1449
1525
  verbatimImagePlaylistUri, codecs, type, language, /* primary= */ false,
1450
1526
  name, /* channelsCount= */ null, /* closedCaptions= */ null,
1451
1527
  characteristics, /* forced= */ false, /* spatialAudio= */ false);
@@ -1463,6 +1539,10 @@ shaka.hls.HlsParser = class {
1463
1539
  // of the entire sheet at once (like we expect in the output).
1464
1540
  // So multiply by the layout size.
1465
1541
 
1542
+ // Since we need to have generated the segment index for this, we can't
1543
+ // lazy-load in this situation.
1544
+ await streamInfo.stream.createSegmentIndex();
1545
+
1466
1546
  const reference = streamInfo.stream.segmentIndex.get(0);
1467
1547
  const layout = reference.getTilesLayout();
1468
1548
  if (layout) {
@@ -1489,10 +1569,10 @@ shaka.hls.HlsParser = class {
1489
1569
  * @param {!shaka.hls.Tag} tag
1490
1570
  * @param {!Array.<string>} allCodecs
1491
1571
  * @param {string} type
1492
- * @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
1572
+ * @return {!shaka.hls.HlsParser.StreamInfo}
1493
1573
  * @private
1494
1574
  */
1495
- async createStreamInfoFromVariantTag_(tag, allCodecs, type) {
1575
+ createStreamInfoFromVariantTag_(tag, allCodecs, type) {
1496
1576
  goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
1497
1577
  'Should only be called on variant tags!');
1498
1578
  const verbatimMediaPlaylistUri = this.variableSubstitution_(
@@ -1504,7 +1584,7 @@ shaka.hls.HlsParser = class {
1504
1584
 
1505
1585
  const closedCaptions = this.getClosedCaptions_(tag, type);
1506
1586
  const codecs = shaka.util.ManifestParserUtils.guessCodecs(type, allCodecs);
1507
- const streamInfo = await this.createStreamInfo_(verbatimMediaPlaylistUri,
1587
+ const streamInfo = this.createStreamInfo_(verbatimMediaPlaylistUri,
1508
1588
  codecs, type, /* language= */ 'und', /* primary= */ false,
1509
1589
  /* name= */ null, /* channelcount= */ null, closedCaptions,
1510
1590
  /* characteristics= */ null, /* forced= */ false,
@@ -1532,29 +1612,275 @@ shaka.hls.HlsParser = class {
1532
1612
  * @param {?string} characteristics
1533
1613
  * @param {boolean} forced
1534
1614
  * @param {boolean} spatialAudio
1535
- * @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
1615
+ * @return {!shaka.hls.HlsParser.StreamInfo}
1536
1616
  * @private
1537
1617
  */
1538
- async createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, language,
1618
+ createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, language,
1539
1619
  primary, name, channelsCount, closedCaptions, characteristics, forced,
1540
1620
  spatialAudio) {
1541
1621
  // TODO: Refactor, too many parameters
1542
- let absoluteMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
1622
+ const initialMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
1543
1623
  this.masterPlaylistUri_, verbatimMediaPlaylistUri);
1544
1624
 
1545
- const response = await this.requestManifest_(absoluteMediaPlaylistUri);
1546
- // Record the final URI after redirects.
1547
- absoluteMediaPlaylistUri = response.uri;
1625
+ // This stream is lazy-loaded inside the createSegmentIndex function.
1626
+ // So we start out with a stream object that does not contain the actual
1627
+ // segment index, then download when createSegmentIndex is called.
1628
+ const stream = this.makeStreamObject_(codecs, type, language, primary, name,
1629
+ channelsCount, closedCaptions, characteristics, forced, spatialAudio);
1630
+ if (shaka.media.MediaSourceEngine.RAW_FORMATS.includes(stream.mimeType)) {
1631
+ stream.codecs = '';
1632
+ }
1633
+ const streamInfo = {
1634
+ stream,
1635
+ type,
1636
+ verbatimMediaPlaylistUri,
1637
+ // These values are filled out or updated after lazy-loading:
1638
+ absoluteMediaPlaylistUri: initialMediaPlaylistUri,
1639
+ maxTimestamp: 0,
1640
+ mediaSequenceToStartTime: new Map(),
1641
+ canSkipSegments: false,
1642
+ hasEndList: false,
1643
+ firstSequenceNumber: -1,
1644
+ loadedOnce: false,
1645
+ };
1548
1646
 
1549
- // Record the redirected, final URI of this media playlist when we parse it.
1550
- /** @type {!shaka.hls.Playlist} */
1551
- const playlist = this.manifestTextParser_.parsePlaylist(
1552
- response.data, absoluteMediaPlaylistUri);
1647
+ /** @param {!AbortSignal} abortSignal */
1648
+ const downloadSegmentIndex = async (abortSignal) => {
1649
+ // Download the actual manifest.
1650
+ const response = await this.requestManifest_(
1651
+ streamInfo.absoluteMediaPlaylistUri);
1652
+ if (abortSignal.aborted) {
1653
+ return;
1654
+ }
1655
+
1656
+ // Record the final URI after redirects.
1657
+ const absoluteMediaPlaylistUri = response.uri;
1658
+
1659
+ // Record the redirected, final URI of this media playlist when we parse
1660
+ // it.
1661
+ /** @type {!shaka.hls.Playlist} */
1662
+ const playlist = this.manifestTextParser_.parsePlaylist(
1663
+ response.data, absoluteMediaPlaylistUri);
1664
+
1665
+ const wasLive = this.isLive_();
1666
+ const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
1667
+ playlist, verbatimMediaPlaylistUri, absoluteMediaPlaylistUri, codecs,
1668
+ type, language, primary, name, channelsCount, closedCaptions,
1669
+ characteristics, forced, spatialAudio);
1670
+ if (abortSignal.aborted) {
1671
+ return;
1672
+ }
1673
+
1674
+ const realStream = realStreamInfo.stream;
1675
+
1676
+ if (this.isLive_() && !wasLive) {
1677
+ // Now that we know that the presentation is live, convert the timeline
1678
+ // to live.
1679
+ this.changePresentationTimelineToLive_();
1680
+ }
1681
+
1682
+ // Copy values from the real stream info to our initial one.
1683
+ streamInfo.absoluteMediaPlaylistUri = absoluteMediaPlaylistUri;
1684
+ streamInfo.maxTimestamp = realStreamInfo.maxTimestamp;
1685
+ streamInfo.canSkipSegments = realStreamInfo.canSkipSegments;
1686
+ streamInfo.hasEndList = realStreamInfo.hasEndList;
1687
+ streamInfo.mediaSequenceToStartTime =
1688
+ realStreamInfo.mediaSequenceToStartTime;
1689
+ streamInfo.loadedOnce = true;
1690
+ stream.segmentIndex = realStream.segmentIndex;
1691
+ stream.encrypted = realStream.encrypted;
1692
+ stream.drmInfos = realStream.drmInfos;
1693
+ stream.keyIds = realStream.keyIds;
1694
+ stream.mimeType = realStream.mimeType;
1695
+ if (shaka.media.MediaSourceEngine.RAW_FORMATS.includes(stream.mimeType)) {
1696
+ stream.codecs = '';
1697
+ }
1698
+
1699
+ // Since we lazy-loaded this content, the player may need to create new
1700
+ // sessions for the DRM info in this stream.
1701
+ if (stream.drmInfos.length) {
1702
+ this.playerInterface_.newDrmInfo(stream);
1703
+ }
1704
+
1705
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
1706
+ if (type == ContentType.VIDEO || type == ContentType.AUDIO) {
1707
+ for (const otherStreamInfo of this.uriToStreamInfosMap_.values()) {
1708
+ if (!otherStreamInfo.loadedOnce && otherStreamInfo.type == type) {
1709
+ // To aid manifest filtering, assume before loading that all video
1710
+ // renditions have the same MIME type. (And likewise for audio.)
1711
+ otherStreamInfo.stream.mimeType = realStream.mimeType;
1712
+ if (shaka.media.MediaSourceEngine.RAW_FORMATS
1713
+ .includes(otherStreamInfo.stream.mimeType)) {
1714
+ otherStreamInfo.stream.codecs = '';
1715
+ }
1716
+ }
1717
+ }
1718
+ }
1719
+
1720
+ // Add finishing touches to the stream that can only be done once we have
1721
+ // more full context on the media as a whole.
1722
+ if (this.hasEnoughInfoToFinalizeStreams_()) {
1723
+ if (!this.streamsFinalized_) {
1724
+ // Mark this manifest as having been finalized, so we don't go through
1725
+ // this whole process of finishing touches a second time.
1726
+ this.streamsFinalized_ = true;
1727
+ // Finalize all of the currently-loaded streams.
1728
+ const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
1729
+ const activeStreamInfos =
1730
+ streamInfos.filter((s) => s.stream.segmentIndex);
1731
+ this.finalizeStreams_(activeStreamInfos);
1732
+ // With the addition of this new stream, we now have enough info to
1733
+ // figure out how long the streams should be. So process all streams
1734
+ // we have downloaded up until this point.
1735
+ this.determineDuration_();
1736
+ // Finally, start the update timer, if this asset has been determined
1737
+ // to be a livestream.
1738
+ const delay = this.updatePlaylistDelay_;
1739
+ if (delay > 0) {
1740
+ this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
1741
+ }
1742
+ } else {
1743
+ // We don't need to go through the full process; just finalize this
1744
+ // single stream.
1745
+ this.finalizeStreams_([streamInfo]);
1746
+ }
1747
+ }
1748
+ };
1749
+
1750
+ /** @type {Promise} */
1751
+ let creationPromise = null;
1752
+ /** @type {!AbortController} */
1753
+ let abortController = new AbortController();
1754
+ const safeCreateSegmentIndex = () => {
1755
+ // An operation is already in progress. The second and subsequent
1756
+ // callers receive the same Promise as the first caller, and only one
1757
+ // download operation will occur.
1758
+ if (creationPromise) {
1759
+ return creationPromise;
1760
+ }
1761
+
1762
+ // Create a new AbortController to be able to cancel this specific
1763
+ // download.
1764
+ abortController = new AbortController();
1765
+
1766
+ // Create a Promise tied to the outcome of downloadSegmentIndex(). If
1767
+ // downloadSegmentIndex is rejected, creationPromise will also be
1768
+ // rejected.
1769
+ creationPromise = new Promise((resolve) => {
1770
+ resolve(downloadSegmentIndex(abortController.signal));
1771
+ });
1772
+ return creationPromise;
1773
+ };
1774
+
1775
+ stream.createSegmentIndex = safeCreateSegmentIndex;
1776
+
1777
+ stream.closeSegmentIndex = () => {
1778
+ // If we're mid-creation, cancel it.
1779
+ if (creationPromise && !stream.segmentIndex) {
1780
+ abortController.abort();
1781
+ }
1782
+ // If we have a segment index, release it.
1783
+ if (stream.segmentIndex) {
1784
+ stream.segmentIndex.release();
1785
+ stream.segmentIndex = null;
1786
+ }
1787
+ // Clear the creation Promise so that a new operation can begin.
1788
+ creationPromise = null;
1789
+ };
1553
1790
 
1554
- return this.convertParsedPlaylistIntoStreamInfo_(playlist,
1555
- verbatimMediaPlaylistUri, absoluteMediaPlaylistUri, codecs, type,
1556
- language, primary, name, channelsCount, closedCaptions, characteristics,
1557
- forced, spatialAudio);
1791
+ return streamInfo;
1792
+ }
1793
+
1794
+ /**
1795
+ * @return {number}
1796
+ * @private
1797
+ */
1798
+ getMinDuration_() {
1799
+ let minDuration = Infinity;
1800
+ for (const streamInfo of this.uriToStreamInfosMap_.values()) {
1801
+ if (streamInfo.stream.segmentIndex && streamInfo.stream.type != 'text') {
1802
+ // Since everything is already offset to 0 (either by sync or by being
1803
+ // VOD), only maxTimestamp is necessary to compute the duration.
1804
+ minDuration = Math.min(minDuration, streamInfo.maxTimestamp);
1805
+ }
1806
+ }
1807
+ return minDuration;
1808
+ }
1809
+
1810
+ /**
1811
+ * @param {!Array.<!shaka.extern.Stream>} streams
1812
+ * @private
1813
+ */
1814
+ notifySegmentsForStreams_(streams) {
1815
+ const references = [];
1816
+ for (const stream of streams) {
1817
+ if (!stream.segmentIndex) {
1818
+ // The stream was closed since the list of streams was built.
1819
+ continue;
1820
+ }
1821
+ stream.segmentIndex.forEachTopLevelReference((reference) => {
1822
+ references.push(reference);
1823
+ });
1824
+ }
1825
+ this.presentationTimeline_.notifySegments(references);
1826
+ }
1827
+
1828
+ /**
1829
+ * @param {!Array.<!shaka.hls.HlsParser.StreamInfo>} streamInfos
1830
+ * @private
1831
+ */
1832
+ finalizeStreams_(streamInfos) {
1833
+ if (!this.isLive_()) {
1834
+ const minDuration = this.getMinDuration_();
1835
+ for (const streamInfo of streamInfos) {
1836
+ streamInfo.stream.segmentIndex.fit(/* periodStart= */ 0, minDuration);
1837
+ }
1838
+ }
1839
+ // MediaSource expects no codec strings combined with raw formats.
1840
+ for (const streamInfo of streamInfos) {
1841
+ const stream = streamInfo.stream;
1842
+ if (shaka.media.MediaSourceEngine.RAW_FORMATS.includes(stream.mimeType)) {
1843
+ stream.codecs = '';
1844
+ }
1845
+ }
1846
+ this.notifySegmentsForStreams_(streamInfos.map((s) => s.stream));
1847
+ if (this.config_.hls.ignoreManifestProgramDateTime) {
1848
+ this.syncStreamsWithSequenceNumber_(streamInfos);
1849
+ } else {
1850
+ this.syncStreamsWithProgramDateTime_(streamInfos);
1851
+ }
1852
+ }
1853
+
1854
+ /**
1855
+ * There are some values on streams that can only be set once we know about
1856
+ * both the video and audio content, if present.
1857
+ * This checks if there is at least one video downloaded (if the media has
1858
+ * video), and that there is at least one audio downloaded (if the media has
1859
+ * audio).
1860
+ * @return {boolean}
1861
+ * @private
1862
+ */
1863
+ hasEnoughInfoToFinalizeStreams_() {
1864
+ if (!this.manifest_) {
1865
+ return false;
1866
+ }
1867
+ const videos = [];
1868
+ const audios = [];
1869
+ for (const variant of this.manifest_.variants) {
1870
+ if (variant.video) {
1871
+ videos.push(variant.video);
1872
+ }
1873
+ if (variant.audio) {
1874
+ audios.push(variant.audio);
1875
+ }
1876
+ }
1877
+ if (videos.length > 0 && !videos.some((stream) => stream.segmentIndex)) {
1878
+ return false;
1879
+ }
1880
+ if (audios.length > 0 && !audios.some((stream) => stream.segmentIndex)) {
1881
+ return false;
1882
+ }
1883
+ return true;
1558
1884
  }
1559
1885
 
1560
1886
  /**
@@ -1604,51 +1930,8 @@ shaka.hls.HlsParser = class {
1604
1930
  mediaVariables);
1605
1931
  }
1606
1932
 
1607
- /** @type {!Array.<!shaka.hls.Tag>} */
1608
- const drmTags = [];
1609
- if (playlist.segments) {
1610
- for (const segment of playlist.segments) {
1611
- const segmentKeyTags = shaka.hls.Utils.filterTagsByName(segment.tags,
1612
- 'EXT-X-KEY');
1613
- drmTags.push(...segmentKeyTags);
1614
- }
1615
- }
1616
-
1617
- let encrypted = false;
1618
- let aesEncrypted = false;
1619
-
1620
- /** @type {!Array.<shaka.extern.DrmInfo>}*/
1621
- const drmInfos = [];
1622
- const keyIds = new Set();
1623
-
1624
- // TODO: May still need changes to support key rotation.
1625
- for (const drmTag of drmTags) {
1626
- const method = drmTag.getRequiredAttrValue('METHOD');
1627
- if (method != 'NONE') {
1628
- encrypted = true;
1629
-
1630
- if (method == 'AES-128') {
1631
- // These keys are handled separately.
1632
- aesEncrypted = true;
1633
- } else {
1634
- const keyFormat = drmTag.getRequiredAttrValue('KEYFORMAT');
1635
- const drmParser =
1636
- shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
1637
-
1638
- const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null;
1639
- if (drmInfo) {
1640
- if (drmInfo.keyIds) {
1641
- for (const keyId of drmInfo.keyIds) {
1642
- keyIds.add(keyId);
1643
- }
1644
- }
1645
- drmInfos.push(drmInfo);
1646
- } else {
1647
- shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
1648
- }
1649
- }
1650
- }
1651
- }
1933
+ const {drmInfos, keyIds, encrypted, aesEncrypted} =
1934
+ this.parseDrmInfo_(playlist, mimeType);
1652
1935
 
1653
1936
  if (encrypted && !drmInfos.length && !aesEncrypted) {
1654
1937
  throw new shaka.util.Error(
@@ -1657,17 +1940,11 @@ shaka.hls.HlsParser = class {
1657
1940
  shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
1658
1941
  }
1659
1942
 
1660
- // MediaSource expects no codec strings combined with raw formats.
1661
- if (shaka.media.MediaSourceEngine.RAW_FORMATS.includes(mimeType)) {
1662
- codecs = '';
1663
- }
1664
-
1665
- /** @type {!Map.<number, number>} */
1666
- const mediaSequenceToStartTime = new Map();
1667
-
1943
+ const mediaSequenceToStartTime = this.isLive_() ?
1944
+ this.mediaSequenceToStartTimeByType_.get(type) : new Map();
1668
1945
  const prevLowLatencyMode = this.lowLatencyMode_;
1669
1946
  const segments = this.createSegments_(verbatimMediaPlaylistUri, playlist,
1670
- type, mimeType, mediaSequenceToStartTime, mediaVariables, codecs);
1947
+ type, mimeType, mediaSequenceToStartTime, mediaVariables);
1671
1948
 
1672
1949
  // This happens when autoLowLatencyMode is true, so we need set the
1673
1950
  // correct lowLatencyPresentationDelay_
@@ -1679,33 +1956,71 @@ shaka.hls.HlsParser = class {
1679
1956
  /** @type {!shaka.media.SegmentIndex} */
1680
1957
  const segmentIndex = new shaka.media.SegmentIndex(segments);
1681
1958
 
1682
- const kind = (type == shaka.util.ManifestParserUtils.ContentType.TEXT) ?
1683
- shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE : undefined;
1684
-
1685
- const roles = [];
1686
- if (characteristics) {
1687
- for (const characteristic of characteristics.split(',')) {
1688
- roles.push(characteristic);
1689
- }
1690
- }
1691
-
1692
1959
  const serverControlTag = shaka.hls.Utils.getFirstTagWithName(
1693
1960
  playlist.tags, 'EXT-X-SERVER-CONTROL');
1694
1961
  const canSkipSegments = serverControlTag ?
1695
1962
  serverControlTag.getAttribute('CAN-SKIP-UNTIL') != null : false;
1696
1963
 
1697
- /** @type {shaka.extern.Stream} */
1698
- const stream = {
1964
+ const stream = this.makeStreamObject_(codecs, type, language, primary, name,
1965
+ channelsCount, closedCaptions, characteristics, forced, spatialAudio);
1966
+ stream.segmentIndex = segmentIndex;
1967
+ stream.encrypted = encrypted;
1968
+ stream.drmInfos = drmInfos;
1969
+ stream.keyIds = keyIds;
1970
+ stream.mimeType = mimeType;
1971
+
1972
+ return {
1973
+ stream,
1974
+ type,
1975
+ verbatimMediaPlaylistUri,
1976
+ absoluteMediaPlaylistUri,
1977
+ maxTimestamp: lastEndTime,
1978
+ canSkipSegments,
1979
+ hasEndList: false,
1980
+ firstSequenceNumber: -1,
1981
+ mediaSequenceToStartTime,
1982
+ loadedOnce: false,
1983
+ };
1984
+ }
1985
+
1986
+
1987
+ /**
1988
+ * Creates a stream object with the given parameters.
1989
+ * The parameters that are passed into here are only the things that can be
1990
+ * known without downloading the media playlist; other values must be set
1991
+ * manually on the object after creation.
1992
+ * @param {string} codecs
1993
+ * @param {string} type
1994
+ * @param {string} language
1995
+ * @param {boolean} primary
1996
+ * @param {?string} name
1997
+ * @param {?number} channelsCount
1998
+ * @param {Map.<string, string>} closedCaptions
1999
+ * @param {?string} characteristics
2000
+ * @param {boolean} forced
2001
+ * @param {boolean} spatialAudio
2002
+ * @return {!shaka.extern.Stream}
2003
+ * @private
2004
+ */
2005
+ makeStreamObject_(codecs, type, language, primary, name, channelsCount,
2006
+ closedCaptions, characteristics, forced, spatialAudio) {
2007
+ // Fill out a "best-guess" mimeType, for now. It will be replaced once the
2008
+ // stream is lazy-loaded.
2009
+ const mimeType = this.guessMimeTypeBeforeLoading_(type, codecs) ||
2010
+ this.guessMimeTypeFallback_(type);
2011
+
2012
+ return {
1699
2013
  id: this.globalId_++,
1700
2014
  originalId: name,
1701
2015
  createSegmentIndex: () => Promise.resolve(),
1702
- segmentIndex,
2016
+ segmentIndex: null,
1703
2017
  mimeType,
1704
2018
  codecs,
1705
- kind,
1706
- encrypted,
1707
- drmInfos,
1708
- keyIds,
2019
+ kind: (type == shaka.util.ManifestParserUtils.ContentType.TEXT) ?
2020
+ shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE : undefined,
2021
+ encrypted: false,
2022
+ drmInfos: [],
2023
+ keyIds: new Set(),
1709
2024
  language,
1710
2025
  label: name, // For historical reasons, since before "originalId".
1711
2026
  type,
@@ -1718,27 +2033,80 @@ shaka.hls.HlsParser = class {
1718
2033
  width: undefined,
1719
2034
  height: undefined,
1720
2035
  bandwidth: undefined,
1721
- roles: roles,
1722
- forced: forced,
2036
+ roles: characteristics ? characteristics.split(',') : [],
2037
+ forced,
1723
2038
  channelsCount,
1724
2039
  audioSamplingRate: null,
1725
- spatialAudio: spatialAudio,
2040
+ spatialAudio,
1726
2041
  closedCaptions,
1727
2042
  hdr: undefined,
1728
2043
  tilesLayout: undefined,
1729
2044
  };
1730
-
1731
- return {
1732
- stream,
1733
- verbatimMediaPlaylistUri,
1734
- absoluteMediaPlaylistUri,
1735
- maxTimestamp: lastEndTime,
1736
- mediaSequenceToStartTime,
1737
- canSkipSegments,
1738
- hasEndList: false,
1739
- };
1740
2045
  }
1741
2046
 
2047
+ /**
2048
+ * @param {!shaka.hls.Playlist} playlist
2049
+ * @param {string} mimeType
2050
+ * @return {{
2051
+ * drmInfos: !Array.<shaka.extern.DrmInfo>,
2052
+ * keyIds: !Set.<string>,
2053
+ * encrypted: boolean,
2054
+ * aesEncrypted: boolean
2055
+ * }}
2056
+ * @private
2057
+ */
2058
+ parseDrmInfo_(playlist, mimeType) {
2059
+ /** @type {!Array.<!shaka.hls.Tag>} */
2060
+ const drmTags = [];
2061
+ if (playlist.segments) {
2062
+ for (const segment of playlist.segments) {
2063
+ const segmentKeyTags = shaka.hls.Utils.filterTagsByName(segment.tags,
2064
+ 'EXT-X-KEY');
2065
+ drmTags.push(...segmentKeyTags);
2066
+ }
2067
+ }
2068
+
2069
+ let encrypted = false;
2070
+ let aesEncrypted = false;
2071
+
2072
+ /** @type {!Array.<shaka.extern.DrmInfo>}*/
2073
+ const drmInfos = [];
2074
+ const keyIds = new Set();
2075
+
2076
+ for (const drmTag of drmTags) {
2077
+ const method = drmTag.getRequiredAttrValue('METHOD');
2078
+ if (method != 'NONE') {
2079
+ encrypted = true;
2080
+
2081
+ if (method == 'AES-128') {
2082
+ // These keys are handled separately.
2083
+ aesEncrypted = true;
2084
+ } else {
2085
+ // According to the HLS spec, KEYFORMAT is optional and implicitly
2086
+ // defaults to "identity".
2087
+ // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
2088
+ const keyFormat =
2089
+ drmTag.getAttributeValue('KEYFORMAT') || 'identity';
2090
+ const drmParser =
2091
+ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
2092
+
2093
+ const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null;
2094
+ if (drmInfo) {
2095
+ if (drmInfo.keyIds) {
2096
+ for (const keyId of drmInfo.keyIds) {
2097
+ keyIds.add(keyId);
2098
+ }
2099
+ }
2100
+ drmInfos.push(drmInfo);
2101
+ } else {
2102
+ shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
2103
+ }
2104
+ }
2105
+ }
2106
+ }
2107
+
2108
+ return {drmInfos, keyIds, encrypted, aesEncrypted};
2109
+ }
1742
2110
 
1743
2111
  /**
1744
2112
  * @param {!shaka.hls.Tag} drmTag
@@ -1882,39 +2250,34 @@ shaka.hls.HlsParser = class {
1882
2250
  /**
1883
2251
  * @private
1884
2252
  */
1885
- createPresentationTimeline_() {
1886
- if (this.isLive_()) {
1887
- // The live edge will be calculated from segments, so we don't need to
1888
- // set a presentation start time. We will assert later that this is
1889
- // working as expected.
1890
-
1891
- // The HLS spec (RFC 8216) states in 6.3.3:
1892
- //
1893
- // "The client SHALL choose which Media Segment to play first ... the
1894
- // client SHOULD NOT choose a segment that starts less than three target
1895
- // durations from the end of the Playlist file. Doing so can trigger
1896
- // playback stalls."
1897
- //
1898
- // We accomplish this in our DASH-y model by setting a presentation
1899
- // delay of configured value, or 3 segments duration if not configured.
1900
- // This will be the "live edge" of the presentation.
1901
- let presentationDelay;
1902
- if (this.config_.defaultPresentationDelay) {
1903
- presentationDelay = this.config_.defaultPresentationDelay;
1904
- } else if (this.lowLatencyPresentationDelay_) {
1905
- presentationDelay = this.lowLatencyPresentationDelay_;
1906
- } else {
1907
- presentationDelay = this.maxTargetDuration_ * 3;
1908
- }
1909
-
1910
- this.presentationTimeline_ = new shaka.media.PresentationTimeline(
1911
- /* presentationStartTime= */ 0, /* delay= */ presentationDelay);
1912
- this.presentationTimeline_.setStatic(false);
2253
+ changePresentationTimelineToLive_() {
2254
+ // The live edge will be calculated from segments, so we don't need to
2255
+ // set a presentation start time. We will assert later that this is
2256
+ // working as expected.
2257
+
2258
+ // The HLS spec (RFC 8216) states in 6.3.3:
2259
+ //
2260
+ // "The client SHALL choose which Media Segment to play first ... the
2261
+ // client SHOULD NOT choose a segment that starts less than three target
2262
+ // durations from the end of the Playlist file. Doing so can trigger
2263
+ // playback stalls."
2264
+ //
2265
+ // We accomplish this in our DASH-y model by setting a presentation
2266
+ // delay of configured value, or 3 segments duration if not configured.
2267
+ // This will be the "live edge" of the presentation.
2268
+ let presentationDelay;
2269
+ if (this.config_.defaultPresentationDelay) {
2270
+ presentationDelay = this.config_.defaultPresentationDelay;
2271
+ } else if (this.lowLatencyPresentationDelay_) {
2272
+ presentationDelay = this.lowLatencyPresentationDelay_;
1913
2273
  } else {
1914
- this.presentationTimeline_ = new shaka.media.PresentationTimeline(
1915
- /* presentationStartTime= */ null, /* delay= */ 0);
1916
- this.presentationTimeline_.setStatic(true);
2274
+ const numberOfSegments = this.config_.hls.liveSegmentsDelay;
2275
+ presentationDelay = this.maxTargetDuration_ * numberOfSegments;
1917
2276
  }
2277
+
2278
+ this.presentationTimeline_.setPresentationStartTime(0);
2279
+ this.presentationTimeline_.setDelay(presentationDelay);
2280
+ this.presentationTimeline_.setStatic(false);
1918
2281
  }
1919
2282
 
1920
2283
  /**
@@ -2205,19 +2568,6 @@ shaka.hls.HlsParser = class {
2205
2568
  return [startByte, endByte];
2206
2569
  }
2207
2570
 
2208
- /** @private */
2209
- notifySegments_() {
2210
- // The presentation timeline may or may not be set yet.
2211
- // If it does not yet exist, hold onto the segments until it does.
2212
- if (!this.presentationTimeline_) {
2213
- return;
2214
- }
2215
- for (const segments of this.segmentsToNotifyByStream_) {
2216
- this.presentationTimeline_.notifySegments(segments);
2217
- }
2218
- this.segmentsToNotifyByStream_ = [];
2219
- }
2220
-
2221
2571
  /**
2222
2572
  * Parses shaka.hls.Segment objects into shaka.media.SegmentReferences.
2223
2573
  *
@@ -2227,12 +2577,11 @@ shaka.hls.HlsParser = class {
2227
2577
  * @param {string} mimeType
2228
2578
  * @param {!Map.<number, number>} mediaSequenceToStartTime
2229
2579
  * @param {!Map.<string, string>} variables
2230
- * @param {string} codecs
2231
2580
  * @return {!Array.<!shaka.media.SegmentReference>}
2232
2581
  * @private
2233
2582
  */
2234
2583
  createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType,
2235
- mediaSequenceToStartTime, variables, codecs) {
2584
+ mediaSequenceToStartTime, variables) {
2236
2585
  /** @type {Array.<!shaka.hls.Segment>} */
2237
2586
  const hlsSegments = playlist.segments;
2238
2587
  goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
@@ -2435,6 +2784,64 @@ shaka.hls.HlsParser = class {
2435
2784
  *
2436
2785
  * @param {string} contentType
2437
2786
  * @param {string} codecs
2787
+ * @return {?string}
2788
+ * @private
2789
+ */
2790
+ guessMimeTypeBeforeLoading_(contentType, codecs) {
2791
+ if (contentType == shaka.util.ManifestParserUtils.ContentType.TEXT) {
2792
+ if (codecs == 'vtt' || codecs == 'wvtt') {
2793
+ // If codecs is 'vtt', it's WebVTT.
2794
+ return 'text/vtt';
2795
+ } else if (codecs && codecs !== '') {
2796
+ // Otherwise, assume MP4-embedded text, since text-based formats tend
2797
+ // not to have a codecs string at all.
2798
+ return 'application/mp4';
2799
+ }
2800
+ }
2801
+
2802
+ if (contentType == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
2803
+ if (!codecs || codecs == 'jpeg') {
2804
+ return 'image/jpeg';
2805
+ }
2806
+ }
2807
+
2808
+ if (contentType == shaka.util.ManifestParserUtils.ContentType.AUDIO) {
2809
+ // See: https://bugs.chromium.org/p/chromium/issues/detail?id=489520
2810
+ if (codecs == 'mp4a.40.34') {
2811
+ return 'audio/mpeg';
2812
+ }
2813
+ }
2814
+
2815
+ // Not enough information to guess from the content type and codecs.
2816
+ return null;
2817
+ }
2818
+
2819
+ /**
2820
+ * Get a fallback mime type for the content. Used if all the better methods
2821
+ * for determining the mime type have failed.
2822
+ *
2823
+ * @param {string} contentType
2824
+ * @return {string}
2825
+ * @private
2826
+ */
2827
+ guessMimeTypeFallback_(contentType) {
2828
+ if (contentType == shaka.util.ManifestParserUtils.ContentType.TEXT) {
2829
+ // If there was no codecs string and no content-type, assume HLS text
2830
+ // streams are WebVTT.
2831
+ return 'text/vtt';
2832
+ }
2833
+ // If the HLS content is lacking in both MIME type metadata and
2834
+ // segment file extensions, we fall back to assuming it's MP4.
2835
+ const map = shaka.hls.HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
2836
+ return map['mp4'];
2837
+ }
2838
+
2839
+ /**
2840
+ * Attempts to guess stream's mime type based on content type, URI, and
2841
+ * contents of the playlist.
2842
+ *
2843
+ * @param {string} contentType
2844
+ * @param {string} codecs
2438
2845
  * @param {!shaka.hls.Playlist} playlist
2439
2846
  * @param {!Map.<string, string>} variables
2440
2847
  * @return {!Promise.<string>}
@@ -2442,7 +2849,6 @@ shaka.hls.HlsParser = class {
2442
2849
  */
2443
2850
  async guessMimeType_(contentType, codecs, playlist, variables) {
2444
2851
  const HlsParser = shaka.hls.HlsParser;
2445
- const ContentType = shaka.util.ManifestParserUtils.ContentType;
2446
2852
  const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
2447
2853
 
2448
2854
  // If you wait long enough, requesting the first segment can fail
@@ -2468,22 +2874,10 @@ shaka.hls.HlsParser = class {
2468
2874
  return mimeType;
2469
2875
  }
2470
2876
 
2471
- if (contentType == ContentType.TEXT) {
2472
- // The extension map didn't work.
2473
- if (codecs == 'vtt' || codecs == 'wvtt') {
2474
- // If codecs is 'vtt', it's WebVTT.
2475
- return 'text/vtt';
2476
- } else if (codecs && codecs !== '') {
2477
- // Otherwise, assume MP4-embedded text, since text-based formats tend
2478
- // not to have a codecs string at all.
2479
- return 'application/mp4';
2480
- }
2481
- }
2482
-
2483
- if (contentType == ContentType.IMAGE) {
2484
- if (!codecs || codecs == 'jpeg') {
2485
- return 'image/jpeg';
2486
- }
2877
+ // The extension map didn't work, so guess based on codecs.
2878
+ mimeType = this.guessMimeTypeBeforeLoading_(contentType, codecs);
2879
+ if (mimeType) {
2880
+ return mimeType;
2487
2881
  }
2488
2882
 
2489
2883
  // If unable to guess mime type, request a segment and try getting it
@@ -2496,21 +2890,12 @@ shaka.hls.HlsParser = class {
2496
2890
  headRequest, requestType);
2497
2891
 
2498
2892
  const contentMimeType = response.headers['content-type'];
2499
-
2500
- if (!contentMimeType) {
2501
- if (contentType == ContentType.TEXT) {
2502
- // If there was no codecs string and no content-type, assume HLS text
2503
- // streams are WebVTT.
2504
- return 'text/vtt';
2505
- }
2506
- // If the HLS content is lacking in both MIME type metadata and
2507
- // segment file extensions, we fall back to assuming it's MP4.
2508
- const fallbackMimeType = map['mp4'];
2509
- return fallbackMimeType;
2893
+ if (contentMimeType) {
2894
+ // Split the MIME type in case the server sent additional parameters.
2895
+ return contentMimeType.split(';')[0];
2510
2896
  }
2511
2897
 
2512
- // Split the MIME type in case the server sent additional parameters.
2513
- return contentMimeType.split(';')[0];
2898
+ return this.guessMimeTypeFallback_(contentType);
2514
2899
  }
2515
2900
 
2516
2901
  /**
@@ -2700,7 +3085,7 @@ shaka.hls.HlsParser = class {
2700
3085
  */
2701
3086
  const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
2702
3087
  'com.apple.fps', [
2703
- {initDataType: 'sinf', initData: new Uint8Array(0)},
3088
+ {initDataType: 'sinf', initData: new Uint8Array(0), keyId: null},
2704
3089
  ]);
2705
3090
 
2706
3091
  return drmInfo;
@@ -2779,18 +3164,48 @@ shaka.hls.HlsParser = class {
2779
3164
 
2780
3165
  return drmInfo;
2781
3166
  }
3167
+
3168
+ /**
3169
+ * See: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-5.1
3170
+ *
3171
+ * @param {!shaka.hls.Tag} drmTag
3172
+ * @return {?shaka.extern.DrmInfo}
3173
+ * @private
3174
+ */
3175
+ static identityDrmParser_(drmTag) {
3176
+ const method = drmTag.getRequiredAttrValue('METHOD');
3177
+ const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR'];
3178
+ if (!VALID_METHODS.includes(method)) {
3179
+ shaka.log.error('Identity (ClearKey) in HLS is only supported with [',
3180
+ VALID_METHODS.join(', '), '], not', method);
3181
+ return null;
3182
+ }
3183
+
3184
+ // NOTE: The ClearKey CDM requires a key-id to key mapping. HLS doesn't
3185
+ // provide a key ID anywhere. So although we could use the 'URI' attribute
3186
+ // to fetch the actual 16-byte key, without a key ID, we can't provide this
3187
+ // automatically to the ClearKey CDM. Instead, the application will have
3188
+ // to use player.configure('drm.clearKeys', { ... }) to provide the key IDs
3189
+ // and keys or player.configure('drm.servers.org\.w3\.clearkey', ...) to
3190
+ // provide a ClearKey license server URI.
3191
+ return shaka.util.ManifestParserUtils.createDrmInfo(
3192
+ 'org.w3.clearkey', /* initDatas= */ null);
3193
+ }
2782
3194
  };
2783
3195
 
2784
3196
 
2785
3197
  /**
2786
3198
  * @typedef {{
2787
3199
  * stream: !shaka.extern.Stream,
3200
+ * type: string,
2788
3201
  * verbatimMediaPlaylistUri: string,
2789
3202
  * absoluteMediaPlaylistUri: string,
2790
3203
  * maxTimestamp: number,
2791
3204
  * mediaSequenceToStartTime: !Map.<number, number>,
2792
3205
  * canSkipSegments: boolean,
2793
- * hasEndList: boolean
3206
+ * hasEndList: boolean,
3207
+ * firstSequenceNumber: number,
3208
+ * loadedOnce: boolean
2794
3209
  * }}
2795
3210
  *
2796
3211
  * @description
@@ -2798,6 +3213,8 @@ shaka.hls.HlsParser = class {
2798
3213
  *
2799
3214
  * @property {!shaka.extern.Stream} stream
2800
3215
  * The Stream itself.
3216
+ * @property {string} type
3217
+ * The type value. Could be 'video', 'audio', 'text', or 'image'.
2801
3218
  * @property {string} verbatimMediaPlaylistUri
2802
3219
  * The verbatim media playlist URI, as it appeared in the master playlist.
2803
3220
  * This has not been canonicalized into an absolute URI. This gives us a
@@ -2810,11 +3227,16 @@ shaka.hls.HlsParser = class {
2810
3227
  * The maximum timestamp found in the stream.
2811
3228
  * @property {!Map.<number, number>} mediaSequenceToStartTime
2812
3229
  * A map of media sequence numbers to media start times.
3230
+ * Only used for VOD content.
2813
3231
  * @property {boolean} canSkipSegments
2814
- * True if the server supports delta playlist updates, and we can send a
2815
- * request for a playlist that can skip older media segments.
3232
+ * True if the server supports delta playlist updates, and we can send a
3233
+ * request for a playlist that can skip older media segments.
2816
3234
  * @property {boolean} hasEndList
2817
- * True if the stream has an EXT-X-ENDLIST tag.
3235
+ * True if the stream has an EXT-X-ENDLIST tag.
3236
+ * @property {number} firstSequenceNumber
3237
+ * The sequence number of the first reference. Only calculated if needed.
3238
+ * @property {boolean} loadedOnce
3239
+ * True if the stream has been loaded at least once.
2818
3240
  */
2819
3241
  shaka.hls.HlsParser.StreamInfo;
2820
3242
 
@@ -2936,6 +3358,8 @@ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_ = {
2936
3358
  shaka.hls.HlsParser.widevineDrmParser_,
2937
3359
  'com.microsoft.playready':
2938
3360
  shaka.hls.HlsParser.playreadyDrmParser_,
3361
+ 'identity':
3362
+ shaka.hls.HlsParser.identityDrmParser_,
2939
3363
  };
2940
3364
 
2941
3365