shaka-player 4.2.4 → 4.3.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 (137) hide show
  1. package/.github/workflows/sync-labels.yaml +1 -1
  2. package/.github/workflows/update-issues.yaml +1 -1
  3. package/AUTHORS +4 -0
  4. package/CHANGELOG.md +85 -54
  5. package/CONTRIBUTORS +4 -0
  6. package/README.md +14 -0
  7. package/demo/common/assets.js +58 -0
  8. package/demo/common/message_ids.js +22 -1
  9. package/demo/config.js +89 -5
  10. package/demo/demo.less +7 -1
  11. package/demo/index.html +8 -1
  12. package/demo/locales/en.json +21 -1
  13. package/demo/locales/source.json +82 -2
  14. package/demo/main.js +57 -1
  15. package/demo/search.js +2 -0
  16. package/demo/visualizer.js +337 -0
  17. package/demo/visualizer_button.js +103 -0
  18. package/dist/controls.css +1 -1
  19. package/dist/controls.css.map +1 -1
  20. package/dist/demo.compiled.debug.js +83 -58
  21. package/dist/demo.compiled.debug.map +4 -4
  22. package/dist/demo.compiled.js +83 -58
  23. package/dist/demo.compiled.map +4 -4
  24. package/dist/demo.css +1 -1
  25. package/dist/demo.css.map +1 -1
  26. package/dist/deps.js +16 -9
  27. package/dist/locales.js +32 -32
  28. package/dist/receiver.compiled.debug.js +23 -19
  29. package/dist/receiver.compiled.debug.map +3 -3
  30. package/dist/receiver.compiled.js +23 -19
  31. package/dist/receiver.compiled.map +3 -3
  32. package/dist/shaka-player.compiled.d.ts +173 -26
  33. package/dist/shaka-player.compiled.debug.d.ts +173 -26
  34. package/dist/shaka-player.compiled.debug.externs.js +161 -31
  35. package/dist/shaka-player.compiled.debug.js +464 -401
  36. package/dist/shaka-player.compiled.debug.map +4 -4
  37. package/dist/shaka-player.compiled.externs.js +161 -31
  38. package/dist/shaka-player.compiled.js +897 -862
  39. package/dist/shaka-player.compiled.map +4 -4
  40. package/dist/shaka-player.ui.d.ts +173 -26
  41. package/dist/shaka-player.ui.debug.d.ts +173 -26
  42. package/dist/shaka-player.ui.debug.externs.js +161 -31
  43. package/dist/shaka-player.ui.debug.js +488 -422
  44. package/dist/shaka-player.ui.debug.map +4 -4
  45. package/dist/shaka-player.ui.externs.js +161 -31
  46. package/dist/shaka-player.ui.js +1053 -1018
  47. package/dist/shaka-player.ui.map +4 -4
  48. package/docs/design/lcevc-architecture.png +0 -0
  49. package/docs/design/lcevc-demo.png +0 -0
  50. package/docs/design/lcevc-integration.md +72 -0
  51. package/docs/tutorials/fairplay.md +7 -0
  52. package/docs/tutorials/faq.md +1 -1
  53. package/docs/tutorials/ui.md +7 -7
  54. package/docs/tutorials/upgrade.md +6 -0
  55. package/externs/ima.js +13 -1
  56. package/externs/lcevc.js +81 -0
  57. package/externs/mux.js +1 -42
  58. package/externs/shaka/ads.js +11 -1
  59. package/externs/shaka/manifest_parser.js +5 -1
  60. package/externs/shaka/player.js +178 -15
  61. package/lib/abr/simple_abr_manager.js +7 -0
  62. package/lib/ads/ad_manager.js +0 -8
  63. package/lib/ads/client_side_ad.js +15 -0
  64. package/lib/ads/client_side_ad_manager.js +21 -8
  65. package/lib/ads/server_side_ad.js +15 -0
  66. package/lib/ads/server_side_ad_manager.js +3 -3
  67. package/lib/cast/cast_utils.js +2 -0
  68. package/lib/cea/dummy_cea_parser.js +28 -0
  69. package/lib/cea/i_cea_parser.js +14 -2
  70. package/lib/cea/mp4_cea_parser.js +87 -5
  71. package/lib/config/auto_show_text.js +33 -0
  72. package/lib/dash/dash_parser.js +1 -1
  73. package/lib/hls/hls_parser.js +790 -374
  74. package/lib/lcevc/lcevc_dil.js +152 -0
  75. package/lib/media/closed_caption_parser.js +9 -6
  76. package/lib/media/drm_engine.js +14 -6
  77. package/lib/media/media_source_engine.js +106 -26
  78. package/lib/media/presentation_timeline.js +26 -11
  79. package/lib/media/segment_reference.js +49 -1
  80. package/lib/media/streaming_engine.js +142 -12
  81. package/lib/media/transmuxer.js +73 -18
  82. package/lib/offline/storage.js +1 -0
  83. package/lib/player.js +454 -75
  84. package/lib/polyfill/patchedmediakeys_apple.js +55 -1
  85. package/lib/text/ui_text_displayer.js +5 -5
  86. package/lib/text/vtt_text_parser.js +35 -1
  87. package/lib/util/dom_utils.js +0 -1
  88. package/lib/util/error.js +17 -0
  89. package/lib/util/fake_event.js +2 -0
  90. package/lib/util/id3_utils.js +300 -0
  91. package/lib/util/mime_utils.js +3 -6
  92. package/lib/util/mp4_box_parsers.js +15 -5
  93. package/lib/util/platform.js +9 -1
  94. package/lib/util/player_configuration.js +23 -1
  95. package/lib/util/ts_parser.js +499 -0
  96. package/maintained-branches.md +2 -2
  97. package/package.json +4 -1
  98. package/roadmap.md +5 -3
  99. package/test/ads/ad_manager_unit.js +180 -29
  100. package/test/cast/cast_utils_unit.js +3 -1
  101. package/test/dash/dash_parser_content_protection_unit.js +1 -0
  102. package/test/dash/dash_parser_live_unit.js +1 -0
  103. package/test/dash/dash_parser_manifest_unit.js +93 -1
  104. package/test/dash/dash_parser_segment_base_unit.js +1 -0
  105. package/test/dash/dash_parser_segment_list_unit.js +1 -0
  106. package/test/dash/dash_parser_segment_template_unit.js +1 -0
  107. package/test/demo/demo_unit.js +2 -1
  108. package/test/hls/hls_live_unit.js +253 -19
  109. package/test/hls/hls_parser_unit.js +500 -12
  110. package/test/media/closed_caption_parser_unit.js +2 -1
  111. package/test/media/drm_engine_integration.js +3 -1
  112. package/test/media/media_source_engine_integration.js +71 -6
  113. package/test/media/media_source_engine_unit.js +10 -5
  114. package/test/media/streaming_engine_integration.js +4 -1
  115. package/test/media/streaming_engine_unit.js +144 -1
  116. package/test/media/transmuxer_integration.js +21 -10
  117. package/test/player_integration.js +224 -82
  118. package/test/player_unit.js +5 -2
  119. package/test/polyfill/patchedmediakeys_apple_unit.js +124 -0
  120. package/test/test/assets/id3-metadata.aac +0 -0
  121. package/test/test/assets/id3-metadata.ts +0 -0
  122. package/test/test/assets/thumbnails-sprites.vtt +10 -0
  123. package/test/test/assets/thumbnails.vtt +10 -0
  124. package/test/test/util/dash_parser_util.js +2 -0
  125. package/test/test/util/fake_ad.js +21 -0
  126. package/test/test/util/fake_media_source_engine.js +7 -0
  127. package/test/test/util/id3_generator.js +100 -0
  128. package/test/test/util/manifest_generator.js +3 -6
  129. package/test/test/util/simple_fakes.js +4 -0
  130. package/test/test/util/stream_generator.js +38 -0
  131. package/test/test/util/test_scheme.js +43 -14
  132. package/test/text/vtt_text_parser_unit.js +11 -0
  133. package/test/ui/text_displayer_layout_unit.js +14 -0
  134. package/test/util/id3_utils_unit.js +116 -0
  135. package/test/util/ts_parser_unit.js +77 -0
  136. package/ui/less/containers.less +7 -0
  137. 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,17 @@ 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
+ const mimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension];
622
+ if (mimeType) {
623
+ fullMimeType = mimeType;
624
+ }
625
+ }
626
+
568
627
  const mimeType = shaka.util.MimeUtils.getBasicType(fullMimeType);
569
628
  const type = mimeType.split('/')[0];
570
629
  const codecs = shaka.util.MimeUtils.getCodecs(fullMimeType);
@@ -611,6 +670,9 @@ shaka.hls.HlsParser = class {
611
670
  /** @type {!Array.<!shaka.hls.Tag>} */
612
671
  const imageTags = Utils.filterTagsByName(
613
672
  playlist.tags, 'EXT-X-IMAGE-STREAM-INF');
673
+ /** @type {!Array.<!shaka.hls.Tag>} */
674
+ const sessionKeyTags = Utils.filterTagsByName(
675
+ playlist.tags, 'EXT-X-SESSION-KEY');
614
676
 
615
677
  this.parseCodecs_(variantTags);
616
678
 
@@ -641,10 +703,10 @@ shaka.hls.HlsParser = class {
641
703
 
642
704
  // Parse audio and video media tags first, so that we can extract segment
643
705
  // start time from audio/video streams and reuse for text streams.
644
- await this.createStreamInfosFromMediaTags_(mediaTags);
706
+ this.createStreamInfosFromMediaTags_(mediaTags);
645
707
  this.parseClosedCaptions_(mediaTags);
646
- variants = await this.createVariantsForTags_(variantTags);
647
- textStreams = await this.parseTexts_(mediaTags);
708
+ variants = this.createVariantsForTags_(variantTags, sessionKeyTags);
709
+ textStreams = this.parseTexts_(mediaTags);
648
710
  imageStreams = await this.parseImages_(imageTags);
649
711
  }
650
712
 
@@ -656,32 +718,45 @@ shaka.hls.HlsParser = class {
656
718
  shaka.util.Error.Code.OPERATION_ABORTED);
657
719
  }
658
720
 
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
721
  // This assert is our own sanity check.
679
722
  goog.asserts.assert(this.presentationTimeline_ == null,
680
723
  'Presentation timeline created early!');
681
- this.createPresentationTimeline_();
724
+ // We don't know if the presentation is VOD or live until we parse at least
725
+ // one media playlist, so make a VOD-style presentation timeline for now
726
+ // and change the type later if we discover this is live.
727
+ // Since the player will load the first variant chosen early in the process,
728
+ // there isn't a window during playback where the live-ness is unknown.
729
+ this.presentationTimeline_ = new shaka.media.PresentationTimeline(
730
+ /* presentationStartTime= */ null, /* delay= */ 0);
731
+ this.presentationTimeline_.setStatic(true);
732
+
733
+ // Single-variant streams aren't lazy-loaded, so for them we already have
734
+ // enough info here to determine the presentation type and duration.
735
+ if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
736
+ if (this.isLive_()) {
737
+ this.changePresentationTimelineToLive_();
738
+ const delay = this.updatePlaylistDelay_;
739
+ this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
740
+ }
741
+ const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
742
+ this.finalizeStreams_(streamInfos);
743
+ this.determineDuration_();
744
+ }
745
+
746
+ this.manifest_ = {
747
+ presentationTimeline: this.presentationTimeline_,
748
+ variants,
749
+ textStreams,
750
+ imageStreams,
751
+ offlineSessionIds: [],
752
+ minBufferTime: 0,
753
+ sequenceMode: true,
754
+ };
755
+ this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
756
+ }
682
757
 
683
- // This assert satisfies the compiler that it is not null for the rest of
684
- // the method.
758
+ /** @private */
759
+ determineDuration_() {
685
760
  goog.asserts.assert(this.presentationTimeline_,
686
761
  'Presentation timeline not created!');
687
762
 
@@ -701,10 +776,14 @@ shaka.hls.HlsParser = class {
701
776
  const PresentationType = shaka.hls.HlsParser.PresentationType_;
702
777
 
703
778
  if (this.presentationType_ == PresentationType.LIVE) {
779
+ let segmentAvailabilityDuration = this.getMinDuration_();
780
+
704
781
  // This defaults to the presentation delay, which has the effect of
705
782
  // making the live stream unseekable. This is consistent with Apple's
706
783
  // HLS implementation.
707
- let segmentAvailabilityDuration = this.presentationTimeline_.getDelay();
784
+ if (this.config_.hls.useSafariBehaviorForLive) {
785
+ segmentAvailabilityDuration = this.presentationTimeline_.getDelay();
786
+ }
708
787
 
709
788
  // The app can override that with a longer duration, to allow seeking.
710
789
  if (!isNaN(this.config_.availabilityWindowOverride)) {
@@ -716,33 +795,9 @@ shaka.hls.HlsParser = class {
716
795
  }
717
796
  } else {
718
797
  // 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
- }
798
+ this.presentationTimeline_.setDuration(this.getMinDuration_());
725
799
  }
726
800
 
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
801
  // This is the first point where we have a meaningful presentation start
747
802
  // time, and we need to tell PresentationTimeline that so that it can
748
803
  // maintain consistency from here on.
@@ -753,17 +808,6 @@ shaka.hls.HlsParser = class {
753
808
  goog.asserts.assert(
754
809
  !this.presentationTimeline_.usingPresentationStartTime(),
755
810
  '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
811
  }
768
812
 
769
813
  /**
@@ -854,21 +898,20 @@ shaka.hls.HlsParser = class {
854
898
  * Create text streams for Subtitles, but not Closed Captions.
855
899
  *
856
900
  * @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
857
- * @return {!Promise.<!Array.<!shaka.extern.Stream>>}
901
+ * @return {!Array.<!shaka.extern.Stream>}
858
902
  * @private
859
903
  */
860
- async parseTexts_(mediaTags) {
904
+ parseTexts_(mediaTags) {
861
905
  // Create text stream for each Subtitle media tag.
862
906
  const subtitleTags =
863
907
  shaka.hls.Utils.filterTagsByType(mediaTags, 'SUBTITLES');
864
- const textStreamPromises = subtitleTags.map(async (tag) => {
908
+ const textStreams = subtitleTags.map((tag) => {
865
909
  const disableText = this.config_.disableText;
866
910
  if (disableText) {
867
911
  return null;
868
912
  }
869
913
  try {
870
- const streamInfo = await this.createStreamInfoFromMediaTag_(tag);
871
- return streamInfo.stream;
914
+ return this.createStreamInfoFromMediaTag_(tag).stream;
872
915
  } catch (e) {
873
916
  if (this.config_.hls.ignoreTextStreamFailures) {
874
917
  return null;
@@ -876,7 +919,6 @@ shaka.hls.HlsParser = class {
876
919
  throw e;
877
920
  }
878
921
  });
879
- const textStreams = await Promise.all(textStreamPromises);
880
922
 
881
923
  // Set the codecs for text streams.
882
924
  for (const tag of subtitleTags) {
@@ -926,7 +968,7 @@ shaka.hls.HlsParser = class {
926
968
  * @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
927
969
  * @private
928
970
  */
929
- async createStreamInfosFromMediaTags_(mediaTags) {
971
+ createStreamInfosFromMediaTags_(mediaTags) {
930
972
  // Filter out subtitles and media tags without uri.
931
973
  mediaTags = mediaTags.filter((tag) => {
932
974
  const uri = tag.getAttributeValue('URI') || '';
@@ -935,20 +977,52 @@ shaka.hls.HlsParser = class {
935
977
  });
936
978
 
937
979
  // 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);
980
+ for (const tag of mediaTags) {
981
+ this.createStreamInfoFromMediaTag_(tag);
982
+ }
942
983
  }
943
984
 
944
985
  /**
945
986
  * @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
946
- * @return {!Promise.<!Array.<!shaka.extern.Variant>>}
987
+ * @param {!Array.<!shaka.hls.Tag>} sessionKeyTags EXT-X-SESSION-KEY tags
988
+ * from the playlist.
989
+ * @return {!Array.<!shaka.extern.Variant>}
947
990
  * @private
948
991
  */
949
- async createVariantsForTags_(tags) {
992
+ createVariantsForTags_(tags, sessionKeyTags) {
993
+ // EXT-X-SESSION-KEY processing
994
+ const drmInfos = [];
995
+ const keyIds = new Set();
996
+ if (sessionKeyTags.length > 0) {
997
+ for (const drmTag of sessionKeyTags) {
998
+ const method = drmTag.getRequiredAttrValue('METHOD');
999
+ if (method != 'NONE' && method != 'AES-128') {
1000
+ // According to the HLS spec, KEYFORMAT is optional and implicitly
1001
+ // defaults to "identity".
1002
+ // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
1003
+ const keyFormat =
1004
+ drmTag.getAttributeValue('KEYFORMAT') || 'identity';
1005
+ const drmParser =
1006
+ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
1007
+
1008
+ const drmInfo = drmParser ?
1009
+ drmParser(drmTag, /* mimeType= */ '') : null;
1010
+ if (drmInfo) {
1011
+ if (drmInfo.keyIds) {
1012
+ for (const keyId of drmInfo.keyIds) {
1013
+ keyIds.add(keyId);
1014
+ }
1015
+ }
1016
+ drmInfos.push(drmInfo);
1017
+ } else {
1018
+ shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
1019
+ }
1020
+ }
1021
+ }
1022
+ }
1023
+
950
1024
  // Create variants for each variant tag.
951
- const variantsPromises = tags.map(async (tag) => {
1025
+ const allVariants = tags.map((tag) => {
952
1026
  const frameRate = tag.getAttributeValue('FRAME-RATE');
953
1027
  const bandwidth = Number(tag.getAttributeValue('AVERAGE-BANDWIDTH')) ||
954
1028
  Number(tag.getRequiredAttrValue('BANDWIDTH'));
@@ -958,7 +1032,7 @@ shaka.hls.HlsParser = class {
958
1032
 
959
1033
  const videoRange = tag.getAttributeValue('VIDEO-RANGE');
960
1034
 
961
- const streamInfos = await this.createStreamInfosForVariantTag_(tag,
1035
+ const streamInfos = this.createStreamInfosForVariantTag_(tag,
962
1036
  resolution, frameRate);
963
1037
 
964
1038
  goog.asserts.assert(streamInfos.audio.length ||
@@ -971,10 +1045,10 @@ shaka.hls.HlsParser = class {
971
1045
  width,
972
1046
  height,
973
1047
  frameRate,
974
- videoRange);
1048
+ videoRange,
1049
+ drmInfos,
1050
+ keyIds);
975
1051
  });
976
-
977
- const allVariants = await Promise.all(variantsPromises);
978
1052
  let variants = allVariants.reduce(shaka.util.Functional.collapseArrays, []);
979
1053
  // Filter out null variants.
980
1054
  variants = variants.filter((variant) => variant != null);
@@ -988,10 +1062,10 @@ shaka.hls.HlsParser = class {
988
1062
  * @param {!shaka.hls.Tag} tag
989
1063
  * @param {?string} resolution
990
1064
  * @param {?string} frameRate
991
- * @return {!Promise.<!shaka.hls.HlsParser.StreamInfos>}
1065
+ * @return {!shaka.hls.HlsParser.StreamInfos}
992
1066
  * @private
993
1067
  */
994
- async createStreamInfosForVariantTag_(tag, resolution, frameRate) {
1068
+ createStreamInfosForVariantTag_(tag, resolution, frameRate) {
995
1069
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
996
1070
  /** @type {!Array.<string>} */
997
1071
  let allCodecs = this.getCodecsForVariantTag_(tag);
@@ -1060,10 +1134,9 @@ shaka.hls.HlsParser = class {
1060
1134
 
1061
1135
  if (!ignoreStream) {
1062
1136
  const streamInfo =
1063
- await this.createStreamInfoFromVariantTag_(tag, allCodecs, type);
1137
+ this.createStreamInfoFromVariantTag_(tag, allCodecs, type);
1064
1138
  res[streamInfo.stream.type] = [streamInfo];
1065
1139
  }
1066
- this.filterLegacyCodecs_(res);
1067
1140
  return res;
1068
1141
  }
1069
1142
 
@@ -1194,7 +1267,7 @@ shaka.hls.HlsParser = class {
1194
1267
 
1195
1268
  /**
1196
1269
  * Get the type value.
1197
- * Shaka recognizes the content types 'audio', 'video' and 'text'.
1270
+ * Shaka recognizes the content types 'audio', 'video', 'text', and 'image'.
1198
1271
  * The HLS 'subtitles' type needs to be mapped to 'text'.
1199
1272
  * @param {!shaka.hls.Tag} tag
1200
1273
  * @return {string}
@@ -1208,28 +1281,6 @@ shaka.hls.HlsParser = class {
1208
1281
  return type;
1209
1282
  }
1210
1283
 
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
1284
  /**
1234
1285
  * @param {!Array.<shaka.hls.HlsParser.StreamInfo>} audioInfos
1235
1286
  * @param {!Array.<shaka.hls.HlsParser.StreamInfo>} videoInfos
@@ -1238,11 +1289,14 @@ shaka.hls.HlsParser = class {
1238
1289
  * @param {?string} height
1239
1290
  * @param {?string} frameRate
1240
1291
  * @param {?string} videoRange
1292
+ * @param {!Array.<shaka.extern.DrmInfo>} drmInfos
1293
+ * @param {!Set.<string>} keyIds
1241
1294
  * @return {!Array.<!shaka.extern.Variant>}
1242
1295
  * @private
1243
1296
  */
1244
1297
  createVariants_(
1245
- audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange) {
1298
+ audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange,
1299
+ drmInfos, keyIds) {
1246
1300
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
1247
1301
  const DrmEngine = shaka.media.DrmEngine;
1248
1302
 
@@ -1268,7 +1322,15 @@ shaka.hls.HlsParser = class {
1268
1322
  for (const audioInfo of audioInfos) {
1269
1323
  for (const videoInfo of videoInfos) {
1270
1324
  const audioStream = audioInfo ? audioInfo.stream : null;
1325
+ if (audioStream) {
1326
+ audioStream.drmInfos = drmInfos;
1327
+ audioStream.keyIds = keyIds;
1328
+ }
1271
1329
  const videoStream = videoInfo ? videoInfo.stream : null;
1330
+ if (videoStream) {
1331
+ videoStream.drmInfos = drmInfos;
1332
+ videoStream.keyIds = keyIds;
1333
+ }
1272
1334
  const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null;
1273
1335
  const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null;
1274
1336
  const videoStreamUri =
@@ -1353,10 +1415,10 @@ shaka.hls.HlsParser = class {
1353
1415
  * Parse EXT-X-MEDIA media tag into a Stream object.
1354
1416
  *
1355
1417
  * @param {shaka.hls.Tag} tag
1356
- * @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
1418
+ * @return {!shaka.hls.HlsParser.StreamInfo}
1357
1419
  * @private
1358
1420
  */
1359
- async createStreamInfoFromMediaTag_(tag) {
1421
+ createStreamInfoFromMediaTag_(tag) {
1360
1422
  goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
1361
1423
  'Should only be called on media tags!');
1362
1424
  const groupId = tag.getRequiredAttrValue('GROUP-ID');
@@ -1398,7 +1460,7 @@ shaka.hls.HlsParser = class {
1398
1460
  const forced = forcedAttrValue == 'YES';
1399
1461
  // TODO: Should we take into account some of the currently ignored
1400
1462
  // attributes: INSTREAM-ID, Attribute descriptions: https://bit.ly/2lpjOhj
1401
- const streamInfo = await this.createStreamInfo_(
1463
+ const streamInfo = this.createStreamInfo_(
1402
1464
  verbatimMediaPlaylistUri, codecs, type, language, primary, name,
1403
1465
  channelsCount, /* closedCaptions= */ null, characteristics, forced,
1404
1466
  spatialAudio);
@@ -1445,7 +1507,7 @@ shaka.hls.HlsParser = class {
1445
1507
 
1446
1508
  const characteristics = tag.getAttributeValue('CHARACTERISTICS');
1447
1509
 
1448
- const streamInfo = await this.createStreamInfo_(
1510
+ const streamInfo = this.createStreamInfo_(
1449
1511
  verbatimImagePlaylistUri, codecs, type, language, /* primary= */ false,
1450
1512
  name, /* channelsCount= */ null, /* closedCaptions= */ null,
1451
1513
  characteristics, /* forced= */ false, /* spatialAudio= */ false);
@@ -1463,6 +1525,10 @@ shaka.hls.HlsParser = class {
1463
1525
  // of the entire sheet at once (like we expect in the output).
1464
1526
  // So multiply by the layout size.
1465
1527
 
1528
+ // Since we need to have generated the segment index for this, we can't
1529
+ // lazy-load in this situation.
1530
+ await streamInfo.stream.createSegmentIndex();
1531
+
1466
1532
  const reference = streamInfo.stream.segmentIndex.get(0);
1467
1533
  const layout = reference.getTilesLayout();
1468
1534
  if (layout) {
@@ -1489,10 +1555,10 @@ shaka.hls.HlsParser = class {
1489
1555
  * @param {!shaka.hls.Tag} tag
1490
1556
  * @param {!Array.<string>} allCodecs
1491
1557
  * @param {string} type
1492
- * @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
1558
+ * @return {!shaka.hls.HlsParser.StreamInfo}
1493
1559
  * @private
1494
1560
  */
1495
- async createStreamInfoFromVariantTag_(tag, allCodecs, type) {
1561
+ createStreamInfoFromVariantTag_(tag, allCodecs, type) {
1496
1562
  goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
1497
1563
  'Should only be called on variant tags!');
1498
1564
  const verbatimMediaPlaylistUri = this.variableSubstitution_(
@@ -1504,7 +1570,7 @@ shaka.hls.HlsParser = class {
1504
1570
 
1505
1571
  const closedCaptions = this.getClosedCaptions_(tag, type);
1506
1572
  const codecs = shaka.util.ManifestParserUtils.guessCodecs(type, allCodecs);
1507
- const streamInfo = await this.createStreamInfo_(verbatimMediaPlaylistUri,
1573
+ const streamInfo = this.createStreamInfo_(verbatimMediaPlaylistUri,
1508
1574
  codecs, type, /* language= */ 'und', /* primary= */ false,
1509
1575
  /* name= */ null, /* channelcount= */ null, closedCaptions,
1510
1576
  /* characteristics= */ null, /* forced= */ false,
@@ -1532,29 +1598,270 @@ shaka.hls.HlsParser = class {
1532
1598
  * @param {?string} characteristics
1533
1599
  * @param {boolean} forced
1534
1600
  * @param {boolean} spatialAudio
1535
- * @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
1601
+ * @return {!shaka.hls.HlsParser.StreamInfo}
1536
1602
  * @private
1537
1603
  */
1538
- async createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, language,
1604
+ createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, language,
1539
1605
  primary, name, channelsCount, closedCaptions, characteristics, forced,
1540
1606
  spatialAudio) {
1541
1607
  // TODO: Refactor, too many parameters
1542
- let absoluteMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
1608
+ const initialMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
1543
1609
  this.masterPlaylistUri_, verbatimMediaPlaylistUri);
1544
1610
 
1545
- const response = await this.requestManifest_(absoluteMediaPlaylistUri);
1546
- // Record the final URI after redirects.
1547
- absoluteMediaPlaylistUri = response.uri;
1611
+ // This stream is lazy-loaded inside the createSegmentIndex function.
1612
+ // So we start out with a stream object that does not contain the actual
1613
+ // segment index, then download when createSegmentIndex is called.
1614
+ const stream = this.makeStreamObject_(codecs, type, language, primary, name,
1615
+ channelsCount, closedCaptions, characteristics, forced, spatialAudio);
1616
+ if (shaka.media.MediaSourceEngine.RAW_FORMATS.includes(stream.mimeType)) {
1617
+ stream.codecs = '';
1618
+ }
1619
+ const streamInfo = {
1620
+ stream,
1621
+ type,
1622
+ verbatimMediaPlaylistUri,
1623
+ // These values are filled out or updated after lazy-loading:
1624
+ absoluteMediaPlaylistUri: initialMediaPlaylistUri,
1625
+ maxTimestamp: 0,
1626
+ mediaSequenceToStartTime: new Map(),
1627
+ canSkipSegments: false,
1628
+ hasEndList: false,
1629
+ firstSequenceNumber: -1,
1630
+ loadedOnce: false,
1631
+ };
1548
1632
 
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);
1633
+ /** @param {!AbortSignal} abortSignal */
1634
+ const downloadSegmentIndex = async (abortSignal) => {
1635
+ // Download the actual manifest.
1636
+ const response = await this.requestManifest_(
1637
+ streamInfo.absoluteMediaPlaylistUri);
1638
+ if (abortSignal.aborted) {
1639
+ return;
1640
+ }
1641
+
1642
+ // Record the final URI after redirects.
1643
+ const absoluteMediaPlaylistUri = response.uri;
1644
+
1645
+ // Record the redirected, final URI of this media playlist when we parse
1646
+ // it.
1647
+ /** @type {!shaka.hls.Playlist} */
1648
+ const playlist = this.manifestTextParser_.parsePlaylist(
1649
+ response.data, absoluteMediaPlaylistUri);
1650
+
1651
+ const wasLive = this.isLive_();
1652
+ const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
1653
+ playlist, verbatimMediaPlaylistUri, absoluteMediaPlaylistUri, codecs,
1654
+ type, language, primary, name, channelsCount, closedCaptions,
1655
+ characteristics, forced, spatialAudio);
1656
+ if (abortSignal.aborted) {
1657
+ return;
1658
+ }
1659
+
1660
+ const realStream = realStreamInfo.stream;
1661
+
1662
+ if (this.isLive_() && !wasLive) {
1663
+ // Now that we know that the presentation is live, convert the timeline
1664
+ // to live.
1665
+ this.changePresentationTimelineToLive_();
1666
+ }
1667
+
1668
+ // Copy values from the real stream info to our initial one.
1669
+ streamInfo.absoluteMediaPlaylistUri = absoluteMediaPlaylistUri;
1670
+ streamInfo.maxTimestamp = realStreamInfo.maxTimestamp;
1671
+ streamInfo.canSkipSegments = realStreamInfo.canSkipSegments;
1672
+ streamInfo.hasEndList = realStreamInfo.hasEndList;
1673
+ streamInfo.mediaSequenceToStartTime =
1674
+ realStreamInfo.mediaSequenceToStartTime;
1675
+ streamInfo.loadedOnce = true;
1676
+ stream.segmentIndex = realStream.segmentIndex;
1677
+ stream.encrypted = realStream.encrypted;
1678
+ stream.drmInfos = realStream.drmInfos;
1679
+ stream.keyIds = realStream.keyIds;
1680
+ stream.kind = realStream.kind;
1681
+ stream.roles = realStream.roles;
1682
+ stream.mimeType = realStream.mimeType;
1683
+
1684
+ // Since we lazy-loaded this content, the player may need to create new
1685
+ // sessions for the DRM info in this stream.
1686
+ if (stream.drmInfos.length) {
1687
+ this.playerInterface_.newDrmInfo(stream);
1688
+ }
1689
+
1690
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
1691
+ if (type == ContentType.VIDEO || type == ContentType.AUDIO) {
1692
+ for (const otherStreamInfo of this.uriToStreamInfosMap_.values()) {
1693
+ if (!otherStreamInfo.loadedOnce && otherStreamInfo.type == type) {
1694
+ // To aid manifest filtering, assume before loading that all video
1695
+ // renditions have the same MIME type. (And likewise for audio.)
1696
+ otherStreamInfo.stream.mimeType = realStream.mimeType;
1697
+ }
1698
+ }
1699
+ }
1700
+
1701
+ // Add finishing touches to the stream that can only be done once we have
1702
+ // more full context on the media as a whole.
1703
+ if (this.hasEnoughInfoToFinalizeStreams_()) {
1704
+ if (!this.streamsFinalized_) {
1705
+ // Mark this manifest as having been finalized, so we don't go through
1706
+ // this whole process of finishing touches a second time.
1707
+ this.streamsFinalized_ = true;
1708
+ // Finalize all of the currently-loaded streams.
1709
+ const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
1710
+ const activeStreamInfos =
1711
+ streamInfos.filter((s) => s.stream.segmentIndex);
1712
+ this.finalizeStreams_(activeStreamInfos);
1713
+ // With the addition of this new stream, we now have enough info to
1714
+ // figure out how long the streams should be. So process all streams
1715
+ // we have downloaded up until this point.
1716
+ this.determineDuration_();
1717
+ // Finally, start the update timer, if this asset has been determined
1718
+ // to be a livestream.
1719
+ const delay = this.updatePlaylistDelay_;
1720
+ if (delay > 0) {
1721
+ this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
1722
+ }
1723
+ } else {
1724
+ // We don't need to go through the full process; just finalize this
1725
+ // single stream.
1726
+ this.finalizeStreams_([streamInfo]);
1727
+ }
1728
+ }
1729
+ };
1730
+
1731
+ /** @type {Promise} */
1732
+ let creationPromise = null;
1733
+ /** @type {!AbortController} */
1734
+ let abortController = new AbortController();
1735
+ const safeCreateSegmentIndex = () => {
1736
+ // An operation is already in progress. The second and subsequent
1737
+ // callers receive the same Promise as the first caller, and only one
1738
+ // download operation will occur.
1739
+ if (creationPromise) {
1740
+ return creationPromise;
1741
+ }
1742
+
1743
+ // Create a new AbortController to be able to cancel this specific
1744
+ // download.
1745
+ abortController = new AbortController();
1746
+
1747
+ // Create a Promise tied to the outcome of downloadSegmentIndex(). If
1748
+ // downloadSegmentIndex is rejected, creationPromise will also be
1749
+ // rejected.
1750
+ creationPromise = new Promise((resolve) => {
1751
+ resolve(downloadSegmentIndex(abortController.signal));
1752
+ });
1753
+ return creationPromise;
1754
+ };
1755
+
1756
+ stream.createSegmentIndex = safeCreateSegmentIndex;
1757
+
1758
+ stream.closeSegmentIndex = () => {
1759
+ // If we're mid-creation, cancel it.
1760
+ if (creationPromise && !stream.segmentIndex) {
1761
+ abortController.abort();
1762
+ }
1763
+ // If we have a segment index, release it.
1764
+ if (stream.segmentIndex) {
1765
+ stream.segmentIndex.release();
1766
+ stream.segmentIndex = null;
1767
+ }
1768
+ // Clear the creation Promise so that a new operation can begin.
1769
+ creationPromise = null;
1770
+ };
1771
+
1772
+ return streamInfo;
1773
+ }
1774
+
1775
+ /**
1776
+ * @return {number}
1777
+ * @private
1778
+ */
1779
+ getMinDuration_() {
1780
+ let minDuration = Infinity;
1781
+ for (const streamInfo of this.uriToStreamInfosMap_.values()) {
1782
+ if (streamInfo.stream.segmentIndex && streamInfo.stream.type != 'text') {
1783
+ // Since everything is already offset to 0 (either by sync or by being
1784
+ // VOD), only maxTimestamp is necessary to compute the duration.
1785
+ minDuration = Math.min(minDuration, streamInfo.maxTimestamp);
1786
+ }
1787
+ }
1788
+ return minDuration;
1789
+ }
1790
+
1791
+ /**
1792
+ * @param {!Array.<!shaka.extern.Stream>} streams
1793
+ * @private
1794
+ */
1795
+ notifySegmentsForStreams_(streams) {
1796
+ const references = [];
1797
+ for (const stream of streams) {
1798
+ if (!stream.segmentIndex) {
1799
+ // The stream was closed since the list of streams was built.
1800
+ continue;
1801
+ }
1802
+ stream.segmentIndex.forEachTopLevelReference((reference) => {
1803
+ references.push(reference);
1804
+ });
1805
+ }
1806
+ this.presentationTimeline_.notifySegments(references);
1807
+ }
1808
+
1809
+ /**
1810
+ * @param {!Array.<!shaka.hls.HlsParser.StreamInfo>} streamInfos
1811
+ * @private
1812
+ */
1813
+ finalizeStreams_(streamInfos) {
1814
+ if (!this.isLive_()) {
1815
+ const minDuration = this.getMinDuration_();
1816
+ for (const streamInfo of streamInfos) {
1817
+ streamInfo.stream.segmentIndex.fit(/* periodStart= */ 0, minDuration);
1818
+ }
1819
+ }
1820
+ // MediaSource expects no codec strings combined with raw formats.
1821
+ for (const streamInfo of streamInfos) {
1822
+ const stream = streamInfo.stream;
1823
+ if (shaka.media.MediaSourceEngine.RAW_FORMATS.includes(stream.mimeType)) {
1824
+ stream.codecs = '';
1825
+ }
1826
+ }
1827
+ this.notifySegmentsForStreams_(streamInfos.map((s) => s.stream));
1828
+ if (this.config_.hls.ignoreManifestProgramDateTime) {
1829
+ this.syncStreamsWithSequenceNumber_(streamInfos);
1830
+ } else {
1831
+ this.syncStreamsWithProgramDateTime_(streamInfos);
1832
+ }
1833
+ }
1553
1834
 
1554
- return this.convertParsedPlaylistIntoStreamInfo_(playlist,
1555
- verbatimMediaPlaylistUri, absoluteMediaPlaylistUri, codecs, type,
1556
- language, primary, name, channelsCount, closedCaptions, characteristics,
1557
- forced, spatialAudio);
1835
+ /**
1836
+ * There are some values on streams that can only be set once we know about
1837
+ * both the video and audio content, if present.
1838
+ * This checks if there is at least one video downloaded (if the media has
1839
+ * video), and that there is at least one audio downloaded (if the media has
1840
+ * audio).
1841
+ * @return {boolean}
1842
+ * @private
1843
+ */
1844
+ hasEnoughInfoToFinalizeStreams_() {
1845
+ if (!this.manifest_) {
1846
+ return false;
1847
+ }
1848
+ const videos = [];
1849
+ const audios = [];
1850
+ for (const variant of this.manifest_.variants) {
1851
+ if (variant.video) {
1852
+ videos.push(variant.video);
1853
+ }
1854
+ if (variant.audio) {
1855
+ audios.push(variant.audio);
1856
+ }
1857
+ }
1858
+ if (videos.length > 0 && !videos.some((stream) => stream.segmentIndex)) {
1859
+ return false;
1860
+ }
1861
+ if (audios.length > 0 && !audios.some((stream) => stream.segmentIndex)) {
1862
+ return false;
1863
+ }
1864
+ return true;
1558
1865
  }
1559
1866
 
1560
1867
  /**
@@ -1604,51 +1911,8 @@ shaka.hls.HlsParser = class {
1604
1911
  mediaVariables);
1605
1912
  }
1606
1913
 
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
- }
1914
+ const {drmInfos, keyIds, encrypted, aesEncrypted} =
1915
+ this.parseDrmInfo_(playlist, mimeType);
1652
1916
 
1653
1917
  if (encrypted && !drmInfos.length && !aesEncrypted) {
1654
1918
  throw new shaka.util.Error(
@@ -1657,16 +1921,10 @@ shaka.hls.HlsParser = class {
1657
1921
  shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
1658
1922
  }
1659
1923
 
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
-
1924
+ const mediaSequenceToStartTime = this.isLive_() ?
1925
+ this.mediaSequenceToStartTimeByType_.get(type) : new Map();
1668
1926
  const segments = this.createSegments_(verbatimMediaPlaylistUri, playlist,
1669
- type, mimeType, mediaSequenceToStartTime, mediaVariables, codecs);
1927
+ type, mimeType, mediaSequenceToStartTime, mediaVariables);
1670
1928
 
1671
1929
  const lastEndTime = segments[segments.length - 1].endTime;
1672
1930
  /** @type {!shaka.media.SegmentIndex} */
@@ -1687,18 +1945,67 @@ shaka.hls.HlsParser = class {
1687
1945
  const canSkipSegments = serverControlTag ?
1688
1946
  serverControlTag.getAttribute('CAN-SKIP-UNTIL') != null : false;
1689
1947
 
1690
- /** @type {shaka.extern.Stream} */
1691
- const stream = {
1948
+ const stream = this.makeStreamObject_(codecs, type, language, primary, name,
1949
+ channelsCount, closedCaptions, characteristics, forced, spatialAudio);
1950
+ stream.segmentIndex = segmentIndex;
1951
+ stream.encrypted = encrypted;
1952
+ stream.drmInfos = drmInfos;
1953
+ stream.keyIds = keyIds;
1954
+ stream.kind = kind;
1955
+ stream.roles = roles;
1956
+ stream.mimeType = mimeType;
1957
+
1958
+ return {
1959
+ stream,
1960
+ type,
1961
+ verbatimMediaPlaylistUri,
1962
+ absoluteMediaPlaylistUri,
1963
+ maxTimestamp: lastEndTime,
1964
+ canSkipSegments,
1965
+ hasEndList: false,
1966
+ firstSequenceNumber: -1,
1967
+ mediaSequenceToStartTime,
1968
+ loadedOnce: false,
1969
+ };
1970
+ }
1971
+
1972
+
1973
+ /**
1974
+ * Creates a stream object with the given parameters.
1975
+ * The parameters that are passed into here are only the things that can be
1976
+ * known without downloading the media playlist; other values must be set
1977
+ * manually on the object after creation.
1978
+ * @param {string} codecs
1979
+ * @param {string} type
1980
+ * @param {string} language
1981
+ * @param {boolean} primary
1982
+ * @param {?string} name
1983
+ * @param {?number} channelsCount
1984
+ * @param {Map.<string, string>} closedCaptions
1985
+ * @param {?string} characteristics
1986
+ * @param {boolean} forced
1987
+ * @param {boolean} spatialAudio
1988
+ * @return {!shaka.extern.Stream}
1989
+ * @private
1990
+ */
1991
+ makeStreamObject_(codecs, type, language, primary, name, channelsCount,
1992
+ closedCaptions, characteristics, forced, spatialAudio) {
1993
+ // Fill out a "best-guess" mimeType, for now. It will be replaced once the
1994
+ // stream is lazy-loaded.
1995
+ const mimeType = this.guessMimeTypeBeforeLoading_(type, codecs) ||
1996
+ this.guessMimeTypeFallback_(type);
1997
+
1998
+ return {
1692
1999
  id: this.globalId_++,
1693
2000
  originalId: name,
1694
2001
  createSegmentIndex: () => Promise.resolve(),
1695
- segmentIndex,
2002
+ segmentIndex: null,
1696
2003
  mimeType,
1697
2004
  codecs,
1698
- kind,
1699
- encrypted,
1700
- drmInfos,
1701
- keyIds,
2005
+ kind: undefined,
2006
+ encrypted: false,
2007
+ drmInfos: [],
2008
+ keyIds: new Set(),
1702
2009
  language,
1703
2010
  label: name, // For historical reasons, since before "originalId".
1704
2011
  type,
@@ -1711,27 +2018,80 @@ shaka.hls.HlsParser = class {
1711
2018
  width: undefined,
1712
2019
  height: undefined,
1713
2020
  bandwidth: undefined,
1714
- roles: roles,
1715
- forced: forced,
2021
+ roles: [],
2022
+ forced,
1716
2023
  channelsCount,
1717
2024
  audioSamplingRate: null,
1718
- spatialAudio: spatialAudio,
2025
+ spatialAudio,
1719
2026
  closedCaptions,
1720
2027
  hdr: undefined,
1721
2028
  tilesLayout: undefined,
1722
2029
  };
1723
-
1724
- return {
1725
- stream,
1726
- verbatimMediaPlaylistUri,
1727
- absoluteMediaPlaylistUri,
1728
- maxTimestamp: lastEndTime,
1729
- mediaSequenceToStartTime,
1730
- canSkipSegments,
1731
- hasEndList: false,
1732
- };
1733
2030
  }
1734
2031
 
2032
+ /**
2033
+ * @param {!shaka.hls.Playlist} playlist
2034
+ * @param {string} mimeType
2035
+ * @return {{
2036
+ * drmInfos: !Array.<shaka.extern.DrmInfo>,
2037
+ * keyIds: !Set.<string>,
2038
+ * encrypted: boolean,
2039
+ * aesEncrypted: boolean
2040
+ * }}
2041
+ * @private
2042
+ */
2043
+ parseDrmInfo_(playlist, mimeType) {
2044
+ /** @type {!Array.<!shaka.hls.Tag>} */
2045
+ const drmTags = [];
2046
+ if (playlist.segments) {
2047
+ for (const segment of playlist.segments) {
2048
+ const segmentKeyTags = shaka.hls.Utils.filterTagsByName(segment.tags,
2049
+ 'EXT-X-KEY');
2050
+ drmTags.push(...segmentKeyTags);
2051
+ }
2052
+ }
2053
+
2054
+ let encrypted = false;
2055
+ let aesEncrypted = false;
2056
+
2057
+ /** @type {!Array.<shaka.extern.DrmInfo>}*/
2058
+ const drmInfos = [];
2059
+ const keyIds = new Set();
2060
+
2061
+ for (const drmTag of drmTags) {
2062
+ const method = drmTag.getRequiredAttrValue('METHOD');
2063
+ if (method != 'NONE') {
2064
+ encrypted = true;
2065
+
2066
+ if (method == 'AES-128') {
2067
+ // These keys are handled separately.
2068
+ aesEncrypted = true;
2069
+ } else {
2070
+ // According to the HLS spec, KEYFORMAT is optional and implicitly
2071
+ // defaults to "identity".
2072
+ // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
2073
+ const keyFormat =
2074
+ drmTag.getAttributeValue('KEYFORMAT') || 'identity';
2075
+ const drmParser =
2076
+ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
2077
+
2078
+ const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null;
2079
+ if (drmInfo) {
2080
+ if (drmInfo.keyIds) {
2081
+ for (const keyId of drmInfo.keyIds) {
2082
+ keyIds.add(keyId);
2083
+ }
2084
+ }
2085
+ drmInfos.push(drmInfo);
2086
+ } else {
2087
+ shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
2088
+ }
2089
+ }
2090
+ }
2091
+ }
2092
+
2093
+ return {drmInfos, keyIds, encrypted, aesEncrypted};
2094
+ }
1735
2095
 
1736
2096
  /**
1737
2097
  * @param {!shaka.hls.Tag} drmTag
@@ -1872,39 +2232,34 @@ shaka.hls.HlsParser = class {
1872
2232
  /**
1873
2233
  * @private
1874
2234
  */
1875
- createPresentationTimeline_() {
1876
- if (this.isLive_()) {
1877
- // The live edge will be calculated from segments, so we don't need to
1878
- // set a presentation start time. We will assert later that this is
1879
- // working as expected.
1880
-
1881
- // The HLS spec (RFC 8216) states in 6.3.3:
1882
- //
1883
- // "The client SHALL choose which Media Segment to play first ... the
1884
- // client SHOULD NOT choose a segment that starts less than three target
1885
- // durations from the end of the Playlist file. Doing so can trigger
1886
- // playback stalls."
1887
- //
1888
- // We accomplish this in our DASH-y model by setting a presentation
1889
- // delay of configured value, or 3 segments duration if not configured.
1890
- // This will be the "live edge" of the presentation.
1891
- let presentationDelay;
1892
- if (this.config_.defaultPresentationDelay) {
1893
- presentationDelay = this.config_.defaultPresentationDelay;
1894
- } else if (this.lowLatencyPresentationDelay_) {
1895
- presentationDelay = this.lowLatencyPresentationDelay_;
1896
- } else {
1897
- presentationDelay = this.maxTargetDuration_ * 3;
1898
- }
1899
-
1900
- this.presentationTimeline_ = new shaka.media.PresentationTimeline(
1901
- /* presentationStartTime= */ 0, /* delay= */ presentationDelay);
1902
- this.presentationTimeline_.setStatic(false);
2235
+ changePresentationTimelineToLive_() {
2236
+ // The live edge will be calculated from segments, so we don't need to
2237
+ // set a presentation start time. We will assert later that this is
2238
+ // working as expected.
2239
+
2240
+ // The HLS spec (RFC 8216) states in 6.3.3:
2241
+ //
2242
+ // "The client SHALL choose which Media Segment to play first ... the
2243
+ // client SHOULD NOT choose a segment that starts less than three target
2244
+ // durations from the end of the Playlist file. Doing so can trigger
2245
+ // playback stalls."
2246
+ //
2247
+ // We accomplish this in our DASH-y model by setting a presentation
2248
+ // delay of configured value, or 3 segments duration if not configured.
2249
+ // This will be the "live edge" of the presentation.
2250
+ let presentationDelay;
2251
+ if (this.config_.defaultPresentationDelay) {
2252
+ presentationDelay = this.config_.defaultPresentationDelay;
2253
+ } else if (this.lowLatencyPresentationDelay_) {
2254
+ presentationDelay = this.lowLatencyPresentationDelay_;
1903
2255
  } else {
1904
- this.presentationTimeline_ = new shaka.media.PresentationTimeline(
1905
- /* presentationStartTime= */ null, /* delay= */ 0);
1906
- this.presentationTimeline_.setStatic(true);
2256
+ const numberOfSegments = this.config_.hls.liveSegmentsDelay;
2257
+ presentationDelay = this.maxTargetDuration_ * numberOfSegments;
1907
2258
  }
2259
+
2260
+ this.presentationTimeline_.setPresentationStartTime(0);
2261
+ this.presentationTimeline_.setDelay(presentationDelay);
2262
+ this.presentationTimeline_.setStatic(false);
1908
2263
  }
1909
2264
 
1910
2265
  /**
@@ -2195,19 +2550,6 @@ shaka.hls.HlsParser = class {
2195
2550
  return [startByte, endByte];
2196
2551
  }
2197
2552
 
2198
- /** @private */
2199
- notifySegments_() {
2200
- // The presentation timeline may or may not be set yet.
2201
- // If it does not yet exist, hold onto the segments until it does.
2202
- if (!this.presentationTimeline_) {
2203
- return;
2204
- }
2205
- for (const segments of this.segmentsToNotifyByStream_) {
2206
- this.presentationTimeline_.notifySegments(segments);
2207
- }
2208
- this.segmentsToNotifyByStream_ = [];
2209
- }
2210
-
2211
2553
  /**
2212
2554
  * Parses shaka.hls.Segment objects into shaka.media.SegmentReferences.
2213
2555
  *
@@ -2217,12 +2559,11 @@ shaka.hls.HlsParser = class {
2217
2559
  * @param {string} mimeType
2218
2560
  * @param {!Map.<number, number>} mediaSequenceToStartTime
2219
2561
  * @param {!Map.<string, string>} variables
2220
- * @param {string} codecs
2221
2562
  * @return {!Array.<!shaka.media.SegmentReference>}
2222
2563
  * @private
2223
2564
  */
2224
2565
  createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType,
2225
- mediaSequenceToStartTime, variables, codecs) {
2566
+ mediaSequenceToStartTime, variables) {
2226
2567
  /** @type {Array.<!shaka.hls.Segment>} */
2227
2568
  const hlsSegments = playlist.segments;
2228
2569
  goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
@@ -2425,6 +2766,64 @@ shaka.hls.HlsParser = class {
2425
2766
  *
2426
2767
  * @param {string} contentType
2427
2768
  * @param {string} codecs
2769
+ * @return {?string}
2770
+ * @private
2771
+ */
2772
+ guessMimeTypeBeforeLoading_(contentType, codecs) {
2773
+ if (contentType == shaka.util.ManifestParserUtils.ContentType.TEXT) {
2774
+ if (codecs == 'vtt' || codecs == 'wvtt') {
2775
+ // If codecs is 'vtt', it's WebVTT.
2776
+ return 'text/vtt';
2777
+ } else if (codecs && codecs !== '') {
2778
+ // Otherwise, assume MP4-embedded text, since text-based formats tend
2779
+ // not to have a codecs string at all.
2780
+ return 'application/mp4';
2781
+ }
2782
+ }
2783
+
2784
+ if (contentType == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
2785
+ if (!codecs || codecs == 'jpeg') {
2786
+ return 'image/jpeg';
2787
+ }
2788
+ }
2789
+
2790
+ if (contentType == shaka.util.ManifestParserUtils.ContentType.AUDIO) {
2791
+ // See: https://bugs.chromium.org/p/chromium/issues/detail?id=489520
2792
+ if (codecs == 'mp4a.40.34') {
2793
+ return 'audio/mpeg';
2794
+ }
2795
+ }
2796
+
2797
+ // Not enough information to guess from the content type and codecs.
2798
+ return null;
2799
+ }
2800
+
2801
+ /**
2802
+ * Get a fallback mime type for the content. Used if all the better methods
2803
+ * for determining the mime type have failed.
2804
+ *
2805
+ * @param {string} contentType
2806
+ * @return {string}
2807
+ * @private
2808
+ */
2809
+ guessMimeTypeFallback_(contentType) {
2810
+ if (contentType == shaka.util.ManifestParserUtils.ContentType.TEXT) {
2811
+ // If there was no codecs string and no content-type, assume HLS text
2812
+ // streams are WebVTT.
2813
+ return 'text/vtt';
2814
+ }
2815
+ // If the HLS content is lacking in both MIME type metadata and
2816
+ // segment file extensions, we fall back to assuming it's MP4.
2817
+ const map = shaka.hls.HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
2818
+ return map['mp4'];
2819
+ }
2820
+
2821
+ /**
2822
+ * Attempts to guess stream's mime type based on content type, URI, and
2823
+ * contents of the playlist.
2824
+ *
2825
+ * @param {string} contentType
2826
+ * @param {string} codecs
2428
2827
  * @param {!shaka.hls.Playlist} playlist
2429
2828
  * @param {!Map.<string, string>} variables
2430
2829
  * @return {!Promise.<string>}
@@ -2432,7 +2831,6 @@ shaka.hls.HlsParser = class {
2432
2831
  */
2433
2832
  async guessMimeType_(contentType, codecs, playlist, variables) {
2434
2833
  const HlsParser = shaka.hls.HlsParser;
2435
- const ContentType = shaka.util.ManifestParserUtils.ContentType;
2436
2834
  const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
2437
2835
 
2438
2836
  // If you wait long enough, requesting the first segment can fail
@@ -2458,22 +2856,10 @@ shaka.hls.HlsParser = class {
2458
2856
  return mimeType;
2459
2857
  }
2460
2858
 
2461
- if (contentType == ContentType.TEXT) {
2462
- // The extension map didn't work.
2463
- if (codecs == 'vtt' || codecs == 'wvtt') {
2464
- // If codecs is 'vtt', it's WebVTT.
2465
- return 'text/vtt';
2466
- } else if (codecs && codecs !== '') {
2467
- // Otherwise, assume MP4-embedded text, since text-based formats tend
2468
- // not to have a codecs string at all.
2469
- return 'application/mp4';
2470
- }
2471
- }
2472
-
2473
- if (contentType == ContentType.IMAGE) {
2474
- if (!codecs || codecs == 'jpeg') {
2475
- return 'image/jpeg';
2476
- }
2859
+ // The extension map didn't work, so guess based on codecs.
2860
+ mimeType = this.guessMimeTypeBeforeLoading_(contentType, codecs);
2861
+ if (mimeType) {
2862
+ return mimeType;
2477
2863
  }
2478
2864
 
2479
2865
  // If unable to guess mime type, request a segment and try getting it
@@ -2486,21 +2872,12 @@ shaka.hls.HlsParser = class {
2486
2872
  headRequest, requestType);
2487
2873
 
2488
2874
  const contentMimeType = response.headers['content-type'];
2489
-
2490
- if (!contentMimeType) {
2491
- if (contentType == ContentType.TEXT) {
2492
- // If there was no codecs string and no content-type, assume HLS text
2493
- // streams are WebVTT.
2494
- return 'text/vtt';
2495
- }
2496
- // If the HLS content is lacking in both MIME type metadata and
2497
- // segment file extensions, we fall back to assuming it's MP4.
2498
- const fallbackMimeType = map['mp4'];
2499
- return fallbackMimeType;
2875
+ if (contentMimeType) {
2876
+ // Split the MIME type in case the server sent additional parameters.
2877
+ return contentMimeType.split(';')[0];
2500
2878
  }
2501
2879
 
2502
- // Split the MIME type in case the server sent additional parameters.
2503
- return contentMimeType.split(';')[0];
2880
+ return this.guessMimeTypeFallback_(contentType);
2504
2881
  }
2505
2882
 
2506
2883
  /**
@@ -2690,7 +3067,7 @@ shaka.hls.HlsParser = class {
2690
3067
  */
2691
3068
  const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
2692
3069
  'com.apple.fps', [
2693
- {initDataType: 'sinf', initData: new Uint8Array(0)},
3070
+ {initDataType: 'sinf', initData: new Uint8Array(0), keyId: null},
2694
3071
  ]);
2695
3072
 
2696
3073
  return drmInfo;
@@ -2769,18 +3146,48 @@ shaka.hls.HlsParser = class {
2769
3146
 
2770
3147
  return drmInfo;
2771
3148
  }
3149
+
3150
+ /**
3151
+ * See: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-5.1
3152
+ *
3153
+ * @param {!shaka.hls.Tag} drmTag
3154
+ * @return {?shaka.extern.DrmInfo}
3155
+ * @private
3156
+ */
3157
+ static identityDrmParser_(drmTag) {
3158
+ const method = drmTag.getRequiredAttrValue('METHOD');
3159
+ const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR'];
3160
+ if (!VALID_METHODS.includes(method)) {
3161
+ shaka.log.error('Identity (ClearKey) in HLS is only supported with [',
3162
+ VALID_METHODS.join(', '), '], not', method);
3163
+ return null;
3164
+ }
3165
+
3166
+ // NOTE: The ClearKey CDM requires a key-id to key mapping. HLS doesn't
3167
+ // provide a key ID anywhere. So although we could use the 'URI' attribute
3168
+ // to fetch the actual 16-byte key, without a key ID, we can't provide this
3169
+ // automatically to the ClearKey CDM. Instead, the application will have
3170
+ // to use player.configure('drm.clearKeys', { ... }) to provide the key IDs
3171
+ // and keys or player.configure('drm.servers.org\.w3\.clearkey', ...) to
3172
+ // provide a ClearKey license server URI.
3173
+ return shaka.util.ManifestParserUtils.createDrmInfo(
3174
+ 'org.w3.clearkey', /* initDatas= */ null);
3175
+ }
2772
3176
  };
2773
3177
 
2774
3178
 
2775
3179
  /**
2776
3180
  * @typedef {{
2777
3181
  * stream: !shaka.extern.Stream,
3182
+ * type: string,
2778
3183
  * verbatimMediaPlaylistUri: string,
2779
3184
  * absoluteMediaPlaylistUri: string,
2780
3185
  * maxTimestamp: number,
2781
3186
  * mediaSequenceToStartTime: !Map.<number, number>,
2782
3187
  * canSkipSegments: boolean,
2783
- * hasEndList: boolean
3188
+ * hasEndList: boolean,
3189
+ * firstSequenceNumber: number,
3190
+ * loadedOnce: boolean
2784
3191
  * }}
2785
3192
  *
2786
3193
  * @description
@@ -2788,6 +3195,8 @@ shaka.hls.HlsParser = class {
2788
3195
  *
2789
3196
  * @property {!shaka.extern.Stream} stream
2790
3197
  * The Stream itself.
3198
+ * @property {string} type
3199
+ * The type value. Could be 'video', 'audio', 'text', or 'image'.
2791
3200
  * @property {string} verbatimMediaPlaylistUri
2792
3201
  * The verbatim media playlist URI, as it appeared in the master playlist.
2793
3202
  * This has not been canonicalized into an absolute URI. This gives us a
@@ -2800,11 +3209,16 @@ shaka.hls.HlsParser = class {
2800
3209
  * The maximum timestamp found in the stream.
2801
3210
  * @property {!Map.<number, number>} mediaSequenceToStartTime
2802
3211
  * A map of media sequence numbers to media start times.
3212
+ * Only used for VOD content.
2803
3213
  * @property {boolean} canSkipSegments
2804
- * True if the server supports delta playlist updates, and we can send a
2805
- * request for a playlist that can skip older media segments.
3214
+ * True if the server supports delta playlist updates, and we can send a
3215
+ * request for a playlist that can skip older media segments.
2806
3216
  * @property {boolean} hasEndList
2807
- * True if the stream has an EXT-X-ENDLIST tag.
3217
+ * True if the stream has an EXT-X-ENDLIST tag.
3218
+ * @property {number} firstSequenceNumber
3219
+ * The sequence number of the first reference. Only calculated if needed.
3220
+ * @property {boolean} loadedOnce
3221
+ * True if the stream has been loaded at least once.
2808
3222
  */
2809
3223
  shaka.hls.HlsParser.StreamInfo;
2810
3224
 
@@ -2926,6 +3340,8 @@ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_ = {
2926
3340
  shaka.hls.HlsParser.widevineDrmParser_,
2927
3341
  'com.microsoft.playready':
2928
3342
  shaka.hls.HlsParser.playreadyDrmParser_,
3343
+ 'identity':
3344
+ shaka.hls.HlsParser.identityDrmParser_,
2929
3345
  };
2930
3346
 
2931
3347