shaka-player 4.2.3 → 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 (147) hide show
  1. package/.github/workflows/sync-labels.yaml +1 -1
  2. package/.github/workflows/update-issues.yaml +1 -1
  3. package/AUTHORS +5 -0
  4. package/CHANGELOG.md +85 -39
  5. package/CONTRIBUTORS +5 -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 +18 -11
  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 +178 -27
  33. package/dist/shaka-player.compiled.debug.d.ts +178 -27
  34. package/dist/shaka-player.compiled.debug.externs.js +163 -32
  35. package/dist/shaka-player.compiled.debug.js +488 -422
  36. package/dist/shaka-player.compiled.debug.map +4 -4
  37. package/dist/shaka-player.compiled.externs.js +163 -32
  38. package/dist/shaka-player.compiled.js +895 -857
  39. package/dist/shaka-player.compiled.map +4 -4
  40. package/dist/shaka-player.ui.d.ts +178 -27
  41. package/dist/shaka-player.ui.debug.d.ts +178 -27
  42. package/dist/shaka-player.ui.debug.externs.js +163 -32
  43. package/dist/shaka-player.ui.debug.js +519 -450
  44. package/dist/shaka-player.ui.debug.map +4 -4
  45. package/dist/shaka-player.ui.externs.js +163 -32
  46. package/dist/shaka-player.ui.js +1053 -1015
  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 +4 -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_proxy.js +2 -1
  68. package/lib/cast/cast_receiver.js +19 -5
  69. package/lib/cast/cast_sender.js +2 -1
  70. package/lib/cast/cast_utils.js +18 -6
  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 +87 -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 +799 -376
  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_engine.js +112 -26
  81. package/lib/media/playhead.js +23 -1
  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 +145 -24
  85. package/lib/media/transmuxer.js +79 -18
  86. package/lib/offline/storage.js +1 -0
  87. package/lib/player.js +477 -85
  88. package/lib/polyfill/patchedmediakeys_apple.js +55 -1
  89. package/lib/text/ui_text_displayer.js +5 -5
  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/manifest_parser_utils.js +4 -0
  96. package/lib/util/mime_utils.js +3 -6
  97. package/lib/util/mp4_box_parsers.js +15 -5
  98. package/lib/util/platform.js +9 -1
  99. package/lib/util/player_configuration.js +23 -1
  100. package/lib/util/ts_parser.js +499 -0
  101. package/maintained-branches.md +2 -2
  102. package/package.json +4 -1
  103. package/roadmap.md +27 -7
  104. package/test/ads/ad_manager_unit.js +180 -29
  105. package/test/cast/cast_receiver_integration.js +97 -16
  106. package/test/cast/cast_receiver_unit.js +17 -12
  107. package/test/cast/cast_utils_unit.js +4 -1
  108. package/test/dash/dash_parser_content_protection_unit.js +1 -0
  109. package/test/dash/dash_parser_live_unit.js +1 -0
  110. package/test/dash/dash_parser_manifest_unit.js +93 -1
  111. package/test/dash/dash_parser_segment_base_unit.js +1 -0
  112. package/test/dash/dash_parser_segment_list_unit.js +1 -0
  113. package/test/dash/dash_parser_segment_template_unit.js +1 -0
  114. package/test/demo/demo_unit.js +2 -1
  115. package/test/hls/hls_live_unit.js +253 -19
  116. package/test/hls/hls_parser_unit.js +500 -12
  117. package/test/media/closed_caption_parser_unit.js +2 -1
  118. package/test/media/drm_engine_integration.js +3 -1
  119. package/test/media/media_source_engine_integration.js +102 -6
  120. package/test/media/media_source_engine_unit.js +10 -5
  121. package/test/media/playhead_unit.js +41 -0
  122. package/test/media/streaming_engine_integration.js +17 -1
  123. package/test/media/streaming_engine_unit.js +180 -24
  124. package/test/media/transmuxer_integration.js +21 -10
  125. package/test/player_integration.js +224 -82
  126. package/test/player_unit.js +92 -2
  127. package/test/polyfill/patchedmediakeys_apple_unit.js +124 -0
  128. package/test/test/assets/id3-metadata.aac +0 -0
  129. package/test/test/assets/id3-metadata.ts +0 -0
  130. package/test/test/assets/thumbnails-sprites.vtt +10 -0
  131. package/test/test/assets/thumbnails.vtt +10 -0
  132. package/test/test/util/dash_parser_util.js +2 -0
  133. package/test/test/util/fake_ad.js +21 -0
  134. package/test/test/util/fake_media_source_engine.js +7 -0
  135. package/test/test/util/id3_generator.js +100 -0
  136. package/test/test/util/manifest_generator.js +3 -6
  137. package/test/test/util/simple_fakes.js +7 -0
  138. package/test/test/util/stream_generator.js +38 -0
  139. package/test/test/util/test_scheme.js +43 -14
  140. package/test/text/vtt_text_parser_unit.js +11 -0
  141. package/test/ui/text_displayer_layout_unit.js +14 -0
  142. package/test/util/id3_utils_unit.js +116 -0
  143. package/test/util/ts_parser_unit.js +77 -0
  144. package/ui/controls.js +4 -1
  145. package/ui/less/containers.less +7 -0
  146. package/ui/settings_menu.js +11 -2
  147. 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
  /**
@@ -831,13 +875,19 @@ shaka.hls.HlsParser = class {
831
875
  shaka.util.ArrayUtils.remove(allCodecs, textCodecs);
832
876
  }
833
877
  if (audioGroupId) {
834
- const codecs = shaka.util.ManifestParserUtils.guessCodecs(
878
+ let codecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
835
879
  ContentType.AUDIO, allCodecs);
880
+ if (!codecs) {
881
+ codecs = this.config_.hls.defaultAudioCodec;
882
+ }
836
883
  this.groupIdToCodecsMap_.set(audioGroupId, codecs);
837
884
  }
838
885
  if (videoGroupId) {
839
- const codecs = shaka.util.ManifestParserUtils.guessCodecs(
886
+ let codecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
840
887
  ContentType.VIDEO, allCodecs);
888
+ if (!codecs) {
889
+ codecs = this.config_.hls.defaultVideoCodec;
890
+ }
841
891
  this.groupIdToCodecsMap_.set(videoGroupId, codecs);
842
892
  }
843
893
  }
@@ -848,21 +898,20 @@ shaka.hls.HlsParser = class {
848
898
  * Create text streams for Subtitles, but not Closed Captions.
849
899
  *
850
900
  * @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
851
- * @return {!Promise.<!Array.<!shaka.extern.Stream>>}
901
+ * @return {!Array.<!shaka.extern.Stream>}
852
902
  * @private
853
903
  */
854
- async parseTexts_(mediaTags) {
904
+ parseTexts_(mediaTags) {
855
905
  // Create text stream for each Subtitle media tag.
856
906
  const subtitleTags =
857
907
  shaka.hls.Utils.filterTagsByType(mediaTags, 'SUBTITLES');
858
- const textStreamPromises = subtitleTags.map(async (tag) => {
908
+ const textStreams = subtitleTags.map((tag) => {
859
909
  const disableText = this.config_.disableText;
860
910
  if (disableText) {
861
911
  return null;
862
912
  }
863
913
  try {
864
- const streamInfo = await this.createStreamInfoFromMediaTag_(tag);
865
- return streamInfo.stream;
914
+ return this.createStreamInfoFromMediaTag_(tag).stream;
866
915
  } catch (e) {
867
916
  if (this.config_.hls.ignoreTextStreamFailures) {
868
917
  return null;
@@ -870,7 +919,6 @@ shaka.hls.HlsParser = class {
870
919
  throw e;
871
920
  }
872
921
  });
873
- const textStreams = await Promise.all(textStreamPromises);
874
922
 
875
923
  // Set the codecs for text streams.
876
924
  for (const tag of subtitleTags) {
@@ -920,7 +968,7 @@ shaka.hls.HlsParser = class {
920
968
  * @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
921
969
  * @private
922
970
  */
923
- async createStreamInfosFromMediaTags_(mediaTags) {
971
+ createStreamInfosFromMediaTags_(mediaTags) {
924
972
  // Filter out subtitles and media tags without uri.
925
973
  mediaTags = mediaTags.filter((tag) => {
926
974
  const uri = tag.getAttributeValue('URI') || '';
@@ -929,20 +977,52 @@ shaka.hls.HlsParser = class {
929
977
  });
930
978
 
931
979
  // Create stream info for each audio / video media tag.
932
- const promises = mediaTags.map((tag) => {
933
- return this.createStreamInfoFromMediaTag_(tag);
934
- });
935
- await Promise.all(promises);
980
+ for (const tag of mediaTags) {
981
+ this.createStreamInfoFromMediaTag_(tag);
982
+ }
936
983
  }
937
984
 
938
985
  /**
939
986
  * @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
940
- * @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>}
941
990
  * @private
942
991
  */
943
- 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
+
944
1024
  // Create variants for each variant tag.
945
- const variantsPromises = tags.map(async (tag) => {
1025
+ const allVariants = tags.map((tag) => {
946
1026
  const frameRate = tag.getAttributeValue('FRAME-RATE');
947
1027
  const bandwidth = Number(tag.getAttributeValue('AVERAGE-BANDWIDTH')) ||
948
1028
  Number(tag.getRequiredAttrValue('BANDWIDTH'));
@@ -952,7 +1032,7 @@ shaka.hls.HlsParser = class {
952
1032
 
953
1033
  const videoRange = tag.getAttributeValue('VIDEO-RANGE');
954
1034
 
955
- const streamInfos = await this.createStreamInfosForVariantTag_(tag,
1035
+ const streamInfos = this.createStreamInfosForVariantTag_(tag,
956
1036
  resolution, frameRate);
957
1037
 
958
1038
  goog.asserts.assert(streamInfos.audio.length ||
@@ -965,10 +1045,10 @@ shaka.hls.HlsParser = class {
965
1045
  width,
966
1046
  height,
967
1047
  frameRate,
968
- videoRange);
1048
+ videoRange,
1049
+ drmInfos,
1050
+ keyIds);
969
1051
  });
970
-
971
- const allVariants = await Promise.all(variantsPromises);
972
1052
  let variants = allVariants.reduce(shaka.util.Functional.collapseArrays, []);
973
1053
  // Filter out null variants.
974
1054
  variants = variants.filter((variant) => variant != null);
@@ -982,10 +1062,10 @@ shaka.hls.HlsParser = class {
982
1062
  * @param {!shaka.hls.Tag} tag
983
1063
  * @param {?string} resolution
984
1064
  * @param {?string} frameRate
985
- * @return {!Promise.<!shaka.hls.HlsParser.StreamInfos>}
1065
+ * @return {!shaka.hls.HlsParser.StreamInfos}
986
1066
  * @private
987
1067
  */
988
- async createStreamInfosForVariantTag_(tag, resolution, frameRate) {
1068
+ createStreamInfosForVariantTag_(tag, resolution, frameRate) {
989
1069
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
990
1070
  /** @type {!Array.<string>} */
991
1071
  let allCodecs = this.getCodecsForVariantTag_(tag);
@@ -1054,10 +1134,9 @@ shaka.hls.HlsParser = class {
1054
1134
 
1055
1135
  if (!ignoreStream) {
1056
1136
  const streamInfo =
1057
- await this.createStreamInfoFromVariantTag_(tag, allCodecs, type);
1137
+ this.createStreamInfoFromVariantTag_(tag, allCodecs, type);
1058
1138
  res[streamInfo.stream.type] = [streamInfo];
1059
1139
  }
1060
- this.filterLegacyCodecs_(res);
1061
1140
  return res;
1062
1141
  }
1063
1142
 
@@ -1188,7 +1267,7 @@ shaka.hls.HlsParser = class {
1188
1267
 
1189
1268
  /**
1190
1269
  * Get the type value.
1191
- * Shaka recognizes the content types 'audio', 'video' and 'text'.
1270
+ * Shaka recognizes the content types 'audio', 'video', 'text', and 'image'.
1192
1271
  * The HLS 'subtitles' type needs to be mapped to 'text'.
1193
1272
  * @param {!shaka.hls.Tag} tag
1194
1273
  * @return {string}
@@ -1202,28 +1281,6 @@ shaka.hls.HlsParser = class {
1202
1281
  return type;
1203
1282
  }
1204
1283
 
1205
- /**
1206
- * Filters out unsupported codec strings from an array of stream infos.
1207
- * @param {shaka.hls.HlsParser.StreamInfos} streamInfos
1208
- * @private
1209
- */
1210
- filterLegacyCodecs_(streamInfos) {
1211
- for (const streamInfo of streamInfos.audio.concat(streamInfos.video)) {
1212
- if (!streamInfo) {
1213
- continue;
1214
- }
1215
- let codecs = streamInfo.stream.codecs.split(',');
1216
- codecs = codecs.filter((codec) => {
1217
- // mp4a.40.34 is a nonstandard codec string that is sometimes used in
1218
- // HLS for legacy reasons. It is not recognized by non-Apple MSE.
1219
- // See https://bugs.chromium.org/p/chromium/issues/detail?id=489520
1220
- // Therefore, ignore this codec string.
1221
- return codec != 'mp4a.40.34';
1222
- });
1223
- streamInfo.stream.codecs = codecs.join(',');
1224
- }
1225
- }
1226
-
1227
1284
  /**
1228
1285
  * @param {!Array.<shaka.hls.HlsParser.StreamInfo>} audioInfos
1229
1286
  * @param {!Array.<shaka.hls.HlsParser.StreamInfo>} videoInfos
@@ -1232,11 +1289,14 @@ shaka.hls.HlsParser = class {
1232
1289
  * @param {?string} height
1233
1290
  * @param {?string} frameRate
1234
1291
  * @param {?string} videoRange
1292
+ * @param {!Array.<shaka.extern.DrmInfo>} drmInfos
1293
+ * @param {!Set.<string>} keyIds
1235
1294
  * @return {!Array.<!shaka.extern.Variant>}
1236
1295
  * @private
1237
1296
  */
1238
1297
  createVariants_(
1239
- audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange) {
1298
+ audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange,
1299
+ drmInfos, keyIds) {
1240
1300
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
1241
1301
  const DrmEngine = shaka.media.DrmEngine;
1242
1302
 
@@ -1262,7 +1322,15 @@ shaka.hls.HlsParser = class {
1262
1322
  for (const audioInfo of audioInfos) {
1263
1323
  for (const videoInfo of videoInfos) {
1264
1324
  const audioStream = audioInfo ? audioInfo.stream : null;
1325
+ if (audioStream) {
1326
+ audioStream.drmInfos = drmInfos;
1327
+ audioStream.keyIds = keyIds;
1328
+ }
1265
1329
  const videoStream = videoInfo ? videoInfo.stream : null;
1330
+ if (videoStream) {
1331
+ videoStream.drmInfos = drmInfos;
1332
+ videoStream.keyIds = keyIds;
1333
+ }
1266
1334
  const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null;
1267
1335
  const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null;
1268
1336
  const videoStreamUri =
@@ -1347,10 +1415,10 @@ shaka.hls.HlsParser = class {
1347
1415
  * Parse EXT-X-MEDIA media tag into a Stream object.
1348
1416
  *
1349
1417
  * @param {shaka.hls.Tag} tag
1350
- * @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
1418
+ * @return {!shaka.hls.HlsParser.StreamInfo}
1351
1419
  * @private
1352
1420
  */
1353
- async createStreamInfoFromMediaTag_(tag) {
1421
+ createStreamInfoFromMediaTag_(tag) {
1354
1422
  goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
1355
1423
  'Should only be called on media tags!');
1356
1424
  const groupId = tag.getRequiredAttrValue('GROUP-ID');
@@ -1392,7 +1460,7 @@ shaka.hls.HlsParser = class {
1392
1460
  const forced = forcedAttrValue == 'YES';
1393
1461
  // TODO: Should we take into account some of the currently ignored
1394
1462
  // attributes: INSTREAM-ID, Attribute descriptions: https://bit.ly/2lpjOhj
1395
- const streamInfo = await this.createStreamInfo_(
1463
+ const streamInfo = this.createStreamInfo_(
1396
1464
  verbatimMediaPlaylistUri, codecs, type, language, primary, name,
1397
1465
  channelsCount, /* closedCaptions= */ null, characteristics, forced,
1398
1466
  spatialAudio);
@@ -1439,7 +1507,7 @@ shaka.hls.HlsParser = class {
1439
1507
 
1440
1508
  const characteristics = tag.getAttributeValue('CHARACTERISTICS');
1441
1509
 
1442
- const streamInfo = await this.createStreamInfo_(
1510
+ const streamInfo = this.createStreamInfo_(
1443
1511
  verbatimImagePlaylistUri, codecs, type, language, /* primary= */ false,
1444
1512
  name, /* channelsCount= */ null, /* closedCaptions= */ null,
1445
1513
  characteristics, /* forced= */ false, /* spatialAudio= */ false);
@@ -1457,6 +1525,10 @@ shaka.hls.HlsParser = class {
1457
1525
  // of the entire sheet at once (like we expect in the output).
1458
1526
  // So multiply by the layout size.
1459
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
+
1460
1532
  const reference = streamInfo.stream.segmentIndex.get(0);
1461
1533
  const layout = reference.getTilesLayout();
1462
1534
  if (layout) {
@@ -1483,10 +1555,10 @@ shaka.hls.HlsParser = class {
1483
1555
  * @param {!shaka.hls.Tag} tag
1484
1556
  * @param {!Array.<string>} allCodecs
1485
1557
  * @param {string} type
1486
- * @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
1558
+ * @return {!shaka.hls.HlsParser.StreamInfo}
1487
1559
  * @private
1488
1560
  */
1489
- async createStreamInfoFromVariantTag_(tag, allCodecs, type) {
1561
+ createStreamInfoFromVariantTag_(tag, allCodecs, type) {
1490
1562
  goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
1491
1563
  'Should only be called on variant tags!');
1492
1564
  const verbatimMediaPlaylistUri = this.variableSubstitution_(
@@ -1498,7 +1570,7 @@ shaka.hls.HlsParser = class {
1498
1570
 
1499
1571
  const closedCaptions = this.getClosedCaptions_(tag, type);
1500
1572
  const codecs = shaka.util.ManifestParserUtils.guessCodecs(type, allCodecs);
1501
- const streamInfo = await this.createStreamInfo_(verbatimMediaPlaylistUri,
1573
+ const streamInfo = this.createStreamInfo_(verbatimMediaPlaylistUri,
1502
1574
  codecs, type, /* language= */ 'und', /* primary= */ false,
1503
1575
  /* name= */ null, /* channelcount= */ null, closedCaptions,
1504
1576
  /* characteristics= */ null, /* forced= */ false,
@@ -1526,29 +1598,270 @@ shaka.hls.HlsParser = class {
1526
1598
  * @param {?string} characteristics
1527
1599
  * @param {boolean} forced
1528
1600
  * @param {boolean} spatialAudio
1529
- * @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
1601
+ * @return {!shaka.hls.HlsParser.StreamInfo}
1530
1602
  * @private
1531
1603
  */
1532
- async createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, language,
1604
+ createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, language,
1533
1605
  primary, name, channelsCount, closedCaptions, characteristics, forced,
1534
1606
  spatialAudio) {
1535
1607
  // TODO: Refactor, too many parameters
1536
- let absoluteMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
1608
+ const initialMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
1537
1609
  this.masterPlaylistUri_, verbatimMediaPlaylistUri);
1538
1610
 
1539
- const response = await this.requestManifest_(absoluteMediaPlaylistUri);
1540
- // Record the final URI after redirects.
1541
- 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
+ };
1542
1632
 
1543
- // Record the redirected, final URI of this media playlist when we parse it.
1544
- /** @type {!shaka.hls.Playlist} */
1545
- const playlist = this.manifestTextParser_.parsePlaylist(
1546
- 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
+ }
1547
1834
 
1548
- return this.convertParsedPlaylistIntoStreamInfo_(playlist,
1549
- verbatimMediaPlaylistUri, absoluteMediaPlaylistUri, codecs, type,
1550
- language, primary, name, channelsCount, closedCaptions, characteristics,
1551
- 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;
1552
1865
  }
1553
1866
 
1554
1867
  /**
@@ -1598,51 +1911,8 @@ shaka.hls.HlsParser = class {
1598
1911
  mediaVariables);
1599
1912
  }
1600
1913
 
1601
- /** @type {!Array.<!shaka.hls.Tag>} */
1602
- const drmTags = [];
1603
- if (playlist.segments) {
1604
- for (const segment of playlist.segments) {
1605
- const segmentKeyTags = shaka.hls.Utils.filterTagsByName(segment.tags,
1606
- 'EXT-X-KEY');
1607
- drmTags.push(...segmentKeyTags);
1608
- }
1609
- }
1610
-
1611
- let encrypted = false;
1612
- let aesEncrypted = false;
1613
-
1614
- /** @type {!Array.<shaka.extern.DrmInfo>}*/
1615
- const drmInfos = [];
1616
- const keyIds = new Set();
1617
-
1618
- // TODO: May still need changes to support key rotation.
1619
- for (const drmTag of drmTags) {
1620
- const method = drmTag.getRequiredAttrValue('METHOD');
1621
- if (method != 'NONE') {
1622
- encrypted = true;
1623
-
1624
- if (method == 'AES-128') {
1625
- // These keys are handled separately.
1626
- aesEncrypted = true;
1627
- } else {
1628
- const keyFormat = drmTag.getRequiredAttrValue('KEYFORMAT');
1629
- const drmParser =
1630
- shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
1631
-
1632
- const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null;
1633
- if (drmInfo) {
1634
- if (drmInfo.keyIds) {
1635
- for (const keyId of drmInfo.keyIds) {
1636
- keyIds.add(keyId);
1637
- }
1638
- }
1639
- drmInfos.push(drmInfo);
1640
- } else {
1641
- shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
1642
- }
1643
- }
1644
- }
1645
- }
1914
+ const {drmInfos, keyIds, encrypted, aesEncrypted} =
1915
+ this.parseDrmInfo_(playlist, mimeType);
1646
1916
 
1647
1917
  if (encrypted && !drmInfos.length && !aesEncrypted) {
1648
1918
  throw new shaka.util.Error(
@@ -1651,16 +1921,10 @@ shaka.hls.HlsParser = class {
1651
1921
  shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
1652
1922
  }
1653
1923
 
1654
- // MediaSource expects no codec strings combined with raw formats.
1655
- if (shaka.media.MediaSourceEngine.RAW_FORMATS.includes(mimeType)) {
1656
- codecs = '';
1657
- }
1658
-
1659
- /** @type {!Map.<number, number>} */
1660
- const mediaSequenceToStartTime = new Map();
1661
-
1924
+ const mediaSequenceToStartTime = this.isLive_() ?
1925
+ this.mediaSequenceToStartTimeByType_.get(type) : new Map();
1662
1926
  const segments = this.createSegments_(verbatimMediaPlaylistUri, playlist,
1663
- type, mimeType, mediaSequenceToStartTime, mediaVariables, codecs);
1927
+ type, mimeType, mediaSequenceToStartTime, mediaVariables);
1664
1928
 
1665
1929
  const lastEndTime = segments[segments.length - 1].endTime;
1666
1930
  /** @type {!shaka.media.SegmentIndex} */
@@ -1681,18 +1945,67 @@ shaka.hls.HlsParser = class {
1681
1945
  const canSkipSegments = serverControlTag ?
1682
1946
  serverControlTag.getAttribute('CAN-SKIP-UNTIL') != null : false;
1683
1947
 
1684
- /** @type {shaka.extern.Stream} */
1685
- 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 {
1686
1999
  id: this.globalId_++,
1687
2000
  originalId: name,
1688
2001
  createSegmentIndex: () => Promise.resolve(),
1689
- segmentIndex,
2002
+ segmentIndex: null,
1690
2003
  mimeType,
1691
2004
  codecs,
1692
- kind,
1693
- encrypted,
1694
- drmInfos,
1695
- keyIds,
2005
+ kind: undefined,
2006
+ encrypted: false,
2007
+ drmInfos: [],
2008
+ keyIds: new Set(),
1696
2009
  language,
1697
2010
  label: name, // For historical reasons, since before "originalId".
1698
2011
  type,
@@ -1705,27 +2018,80 @@ shaka.hls.HlsParser = class {
1705
2018
  width: undefined,
1706
2019
  height: undefined,
1707
2020
  bandwidth: undefined,
1708
- roles: roles,
1709
- forced: forced,
2021
+ roles: [],
2022
+ forced,
1710
2023
  channelsCount,
1711
2024
  audioSamplingRate: null,
1712
- spatialAudio: spatialAudio,
2025
+ spatialAudio,
1713
2026
  closedCaptions,
1714
2027
  hdr: undefined,
1715
2028
  tilesLayout: undefined,
1716
2029
  };
1717
-
1718
- return {
1719
- stream,
1720
- verbatimMediaPlaylistUri,
1721
- absoluteMediaPlaylistUri,
1722
- maxTimestamp: lastEndTime,
1723
- mediaSequenceToStartTime,
1724
- canSkipSegments,
1725
- hasEndList: false,
1726
- };
1727
2030
  }
1728
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
+ }
1729
2095
 
1730
2096
  /**
1731
2097
  * @param {!shaka.hls.Tag} drmTag
@@ -1866,39 +2232,34 @@ shaka.hls.HlsParser = class {
1866
2232
  /**
1867
2233
  * @private
1868
2234
  */
1869
- createPresentationTimeline_() {
1870
- if (this.isLive_()) {
1871
- // The live edge will be calculated from segments, so we don't need to
1872
- // set a presentation start time. We will assert later that this is
1873
- // working as expected.
1874
-
1875
- // The HLS spec (RFC 8216) states in 6.3.3:
1876
- //
1877
- // "The client SHALL choose which Media Segment to play first ... the
1878
- // client SHOULD NOT choose a segment that starts less than three target
1879
- // durations from the end of the Playlist file. Doing so can trigger
1880
- // playback stalls."
1881
- //
1882
- // We accomplish this in our DASH-y model by setting a presentation
1883
- // delay of configured value, or 3 segments duration if not configured.
1884
- // This will be the "live edge" of the presentation.
1885
- let presentationDelay;
1886
- if (this.config_.defaultPresentationDelay) {
1887
- presentationDelay = this.config_.defaultPresentationDelay;
1888
- } else if (this.lowLatencyPresentationDelay_) {
1889
- presentationDelay = this.lowLatencyPresentationDelay_;
1890
- } else {
1891
- presentationDelay = this.maxTargetDuration_ * 3;
1892
- }
1893
-
1894
- this.presentationTimeline_ = new shaka.media.PresentationTimeline(
1895
- /* presentationStartTime= */ 0, /* delay= */ presentationDelay);
1896
- 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_;
1897
2255
  } else {
1898
- this.presentationTimeline_ = new shaka.media.PresentationTimeline(
1899
- /* presentationStartTime= */ null, /* delay= */ 0);
1900
- this.presentationTimeline_.setStatic(true);
2256
+ const numberOfSegments = this.config_.hls.liveSegmentsDelay;
2257
+ presentationDelay = this.maxTargetDuration_ * numberOfSegments;
1901
2258
  }
2259
+
2260
+ this.presentationTimeline_.setPresentationStartTime(0);
2261
+ this.presentationTimeline_.setDelay(presentationDelay);
2262
+ this.presentationTimeline_.setStatic(false);
1902
2263
  }
1903
2264
 
1904
2265
  /**
@@ -2189,19 +2550,6 @@ shaka.hls.HlsParser = class {
2189
2550
  return [startByte, endByte];
2190
2551
  }
2191
2552
 
2192
- /** @private */
2193
- notifySegments_() {
2194
- // The presentation timeline may or may not be set yet.
2195
- // If it does not yet exist, hold onto the segments until it does.
2196
- if (!this.presentationTimeline_) {
2197
- return;
2198
- }
2199
- for (const segments of this.segmentsToNotifyByStream_) {
2200
- this.presentationTimeline_.notifySegments(segments);
2201
- }
2202
- this.segmentsToNotifyByStream_ = [];
2203
- }
2204
-
2205
2553
  /**
2206
2554
  * Parses shaka.hls.Segment objects into shaka.media.SegmentReferences.
2207
2555
  *
@@ -2211,12 +2559,11 @@ shaka.hls.HlsParser = class {
2211
2559
  * @param {string} mimeType
2212
2560
  * @param {!Map.<number, number>} mediaSequenceToStartTime
2213
2561
  * @param {!Map.<string, string>} variables
2214
- * @param {string} codecs
2215
2562
  * @return {!Array.<!shaka.media.SegmentReference>}
2216
2563
  * @private
2217
2564
  */
2218
2565
  createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType,
2219
- mediaSequenceToStartTime, variables, codecs) {
2566
+ mediaSequenceToStartTime, variables) {
2220
2567
  /** @type {Array.<!shaka.hls.Segment>} */
2221
2568
  const hlsSegments = playlist.segments;
2222
2569
  goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
@@ -2419,6 +2766,64 @@ shaka.hls.HlsParser = class {
2419
2766
  *
2420
2767
  * @param {string} contentType
2421
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
2422
2827
  * @param {!shaka.hls.Playlist} playlist
2423
2828
  * @param {!Map.<string, string>} variables
2424
2829
  * @return {!Promise.<string>}
@@ -2426,7 +2831,6 @@ shaka.hls.HlsParser = class {
2426
2831
  */
2427
2832
  async guessMimeType_(contentType, codecs, playlist, variables) {
2428
2833
  const HlsParser = shaka.hls.HlsParser;
2429
- const ContentType = shaka.util.ManifestParserUtils.ContentType;
2430
2834
  const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
2431
2835
 
2432
2836
  // If you wait long enough, requesting the first segment can fail
@@ -2452,22 +2856,10 @@ shaka.hls.HlsParser = class {
2452
2856
  return mimeType;
2453
2857
  }
2454
2858
 
2455
- if (contentType == ContentType.TEXT) {
2456
- // The extension map didn't work.
2457
- if (codecs == 'vtt' || codecs == 'wvtt') {
2458
- // If codecs is 'vtt', it's WebVTT.
2459
- return 'text/vtt';
2460
- } else if (codecs && codecs !== '') {
2461
- // Otherwise, assume MP4-embedded text, since text-based formats tend
2462
- // not to have a codecs string at all.
2463
- return 'application/mp4';
2464
- }
2465
- }
2466
-
2467
- if (contentType == ContentType.IMAGE) {
2468
- if (!codecs || codecs == 'jpeg') {
2469
- return 'image/jpeg';
2470
- }
2859
+ // The extension map didn't work, so guess based on codecs.
2860
+ mimeType = this.guessMimeTypeBeforeLoading_(contentType, codecs);
2861
+ if (mimeType) {
2862
+ return mimeType;
2471
2863
  }
2472
2864
 
2473
2865
  // If unable to guess mime type, request a segment and try getting it
@@ -2480,21 +2872,12 @@ shaka.hls.HlsParser = class {
2480
2872
  headRequest, requestType);
2481
2873
 
2482
2874
  const contentMimeType = response.headers['content-type'];
2483
-
2484
- if (!contentMimeType) {
2485
- if (contentType == ContentType.TEXT) {
2486
- // If there was no codecs string and no content-type, assume HLS text
2487
- // streams are WebVTT.
2488
- return 'text/vtt';
2489
- }
2490
- // If the HLS content is lacking in both MIME type metadata and
2491
- // segment file extensions, we fall back to assuming it's MP4.
2492
- const fallbackMimeType = map['mp4'];
2493
- return fallbackMimeType;
2875
+ if (contentMimeType) {
2876
+ // Split the MIME type in case the server sent additional parameters.
2877
+ return contentMimeType.split(';')[0];
2494
2878
  }
2495
2879
 
2496
- // Split the MIME type in case the server sent additional parameters.
2497
- return contentMimeType.split(';')[0];
2880
+ return this.guessMimeTypeFallback_(contentType);
2498
2881
  }
2499
2882
 
2500
2883
  /**
@@ -2684,7 +3067,7 @@ shaka.hls.HlsParser = class {
2684
3067
  */
2685
3068
  const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
2686
3069
  'com.apple.fps', [
2687
- {initDataType: 'sinf', initData: new Uint8Array(0)},
3070
+ {initDataType: 'sinf', initData: new Uint8Array(0), keyId: null},
2688
3071
  ]);
2689
3072
 
2690
3073
  return drmInfo;
@@ -2763,18 +3146,48 @@ shaka.hls.HlsParser = class {
2763
3146
 
2764
3147
  return drmInfo;
2765
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
+ }
2766
3176
  };
2767
3177
 
2768
3178
 
2769
3179
  /**
2770
3180
  * @typedef {{
2771
3181
  * stream: !shaka.extern.Stream,
3182
+ * type: string,
2772
3183
  * verbatimMediaPlaylistUri: string,
2773
3184
  * absoluteMediaPlaylistUri: string,
2774
3185
  * maxTimestamp: number,
2775
3186
  * mediaSequenceToStartTime: !Map.<number, number>,
2776
3187
  * canSkipSegments: boolean,
2777
- * hasEndList: boolean
3188
+ * hasEndList: boolean,
3189
+ * firstSequenceNumber: number,
3190
+ * loadedOnce: boolean
2778
3191
  * }}
2779
3192
  *
2780
3193
  * @description
@@ -2782,6 +3195,8 @@ shaka.hls.HlsParser = class {
2782
3195
  *
2783
3196
  * @property {!shaka.extern.Stream} stream
2784
3197
  * The Stream itself.
3198
+ * @property {string} type
3199
+ * The type value. Could be 'video', 'audio', 'text', or 'image'.
2785
3200
  * @property {string} verbatimMediaPlaylistUri
2786
3201
  * The verbatim media playlist URI, as it appeared in the master playlist.
2787
3202
  * This has not been canonicalized into an absolute URI. This gives us a
@@ -2794,11 +3209,16 @@ shaka.hls.HlsParser = class {
2794
3209
  * The maximum timestamp found in the stream.
2795
3210
  * @property {!Map.<number, number>} mediaSequenceToStartTime
2796
3211
  * A map of media sequence numbers to media start times.
3212
+ * Only used for VOD content.
2797
3213
  * @property {boolean} canSkipSegments
2798
- * True if the server supports delta playlist updates, and we can send a
2799
- * 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.
2800
3216
  * @property {boolean} hasEndList
2801
- * 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.
2802
3222
  */
2803
3223
  shaka.hls.HlsParser.StreamInfo;
2804
3224
 
@@ -2872,6 +3292,7 @@ shaka.hls.HlsParser.TEXT_EXTENSIONS_TO_MIME_TYPES_ = {
2872
3292
  'm4f': 'application/mp4',
2873
3293
  'cmft': 'application/mp4',
2874
3294
  'vtt': 'text/vtt',
3295
+ 'webvtt': 'txt/vtt',
2875
3296
  'ttml': 'application/ttml+xml',
2876
3297
  };
2877
3298
 
@@ -2919,6 +3340,8 @@ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_ = {
2919
3340
  shaka.hls.HlsParser.widevineDrmParser_,
2920
3341
  'com.microsoft.playready':
2921
3342
  shaka.hls.HlsParser.playreadyDrmParser_,
3343
+ 'identity':
3344
+ shaka.hls.HlsParser.identityDrmParser_,
2922
3345
  };
2923
3346
 
2924
3347