shaka-player 3.0.11 → 3.0.15

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 (142) hide show
  1. package/AUTHORS +5 -0
  2. package/CHANGELOG.md +132 -0
  3. package/CONTRIBUTORS +5 -0
  4. package/README.md +3 -0
  5. package/build/apps.py +2 -2
  6. package/build/check.py +2 -2
  7. package/build/conformance.textproto +0 -9
  8. package/build/generateExterns.js +1 -1
  9. package/build/shakaBuildHelpers.py +6 -5
  10. package/build/subprocessWindowsPatch.py +103 -0
  11. package/demo/common/assets.js +1 -1
  12. package/demo/common/message_ids.js +1 -0
  13. package/demo/config.js +2 -1
  14. package/demo/demo.less +4 -0
  15. package/demo/demo_utils.js +1 -0
  16. package/demo/input_container.js +2 -1
  17. package/demo/locales/en.json +1 -0
  18. package/demo/locales/source.json +4 -0
  19. package/dist/controls.css +2 -2
  20. package/dist/controls.css.map +1 -1
  21. package/dist/demo.compiled.debug.js +63 -60
  22. package/dist/demo.compiled.debug.map +3 -3
  23. package/dist/demo.compiled.js +63 -60
  24. package/dist/demo.compiled.map +3 -3
  25. package/dist/demo.css +1 -1
  26. package/dist/demo.css.map +1 -1
  27. package/dist/deps.js +9 -9
  28. package/dist/locales.js +35 -33
  29. package/dist/receiver.compiled.debug.js +59 -56
  30. package/dist/receiver.compiled.debug.map +3 -3
  31. package/dist/receiver.compiled.js +59 -56
  32. package/dist/receiver.compiled.map +3 -3
  33. package/dist/shaka-player.compiled.d.ts +131 -5
  34. package/dist/shaka-player.compiled.debug.d.ts +131 -5
  35. package/dist/shaka-player.compiled.debug.externs.js +142 -8
  36. package/dist/shaka-player.compiled.debug.js +330 -312
  37. package/dist/shaka-player.compiled.debug.map +4 -4
  38. package/dist/shaka-player.compiled.externs.js +142 -8
  39. package/dist/shaka-player.compiled.js +707 -702
  40. package/dist/shaka-player.compiled.map +4 -4
  41. package/dist/shaka-player.ui.d.ts +131 -5
  42. package/dist/shaka-player.ui.debug.d.ts +131 -5
  43. package/dist/shaka-player.ui.debug.externs.js +144 -10
  44. package/dist/shaka-player.ui.debug.js +354 -333
  45. package/dist/shaka-player.ui.debug.map +4 -4
  46. package/dist/shaka-player.ui.externs.js +144 -10
  47. package/dist/shaka-player.ui.js +860 -855
  48. package/dist/shaka-player.ui.map +4 -4
  49. package/docs/tutorials/fairplay.md +1 -1
  50. package/docs/tutorials/faq.md +25 -0
  51. package/externs/aria.js +18 -0
  52. package/externs/ima.js +4 -0
  53. package/externs/shaka/ads.js +2 -0
  54. package/karma.conf.js +1 -0
  55. package/lib/ads/ad_manager.js +29 -11
  56. package/lib/ads/client_side_ad_manager.js +76 -86
  57. package/lib/ads/server_side_ad_manager.js +26 -11
  58. package/lib/cast/cast_proxy.js +3 -2
  59. package/lib/cast/cast_receiver.js +6 -2
  60. package/lib/cast/cast_sender.js +2 -2
  61. package/lib/dash/dash_parser.js +63 -1
  62. package/lib/dash/segment_list.js +1 -1
  63. package/lib/dash/segment_template.js +41 -14
  64. package/lib/hls/hls_parser.js +41 -11
  65. package/lib/media/content_workarounds.js +18 -0
  66. package/lib/media/drm_engine.js +8 -0
  67. package/lib/media/gap_jumping_controller.js +8 -4
  68. package/lib/media/media_source_engine.js +7 -0
  69. package/lib/media/playhead.js +1 -1
  70. package/lib/media/presentation_timeline.js +3 -2
  71. package/lib/media/segment_index.js +41 -11
  72. package/lib/media/streaming_engine.js +34 -21
  73. package/lib/net/networking_engine.js +2 -1
  74. package/lib/offline/download_manager.js +1 -2
  75. package/lib/player.js +99 -44
  76. package/lib/polyfill/encryption_scheme.js +2 -0
  77. package/lib/polyfill/fullscreen.js +2 -0
  78. package/lib/polyfill/mathround.js +2 -0
  79. package/lib/polyfill/mediasource.js +2 -0
  80. package/lib/polyfill/patchedmediakeys_apple.js +6 -4
  81. package/lib/polyfill/patchedmediakeys_ms.js +6 -4
  82. package/lib/polyfill/patchedmediakeys_nop.js +2 -0
  83. package/lib/polyfill/patchedmediakeys_webkit.js +6 -4
  84. package/lib/polyfill/pip_webkit.js +2 -0
  85. package/lib/polyfill/video_play_promise.js +2 -0
  86. package/lib/polyfill/videoplaybackquality.js +2 -0
  87. package/lib/polyfill/vttcue.js +2 -0
  88. package/lib/text/mp4_ttml_parser.js +3 -0
  89. package/lib/text/ttml_text_parser.js +5 -0
  90. package/lib/text/ui_text_displayer.js +37 -3
  91. package/lib/util/error.js +1 -4
  92. package/lib/util/fake_event.js +53 -7
  93. package/lib/util/mime_utils.js +33 -0
  94. package/lib/util/periods.js +25 -3
  95. package/lib/util/stream_utils.js +11 -0
  96. package/package.json +3 -3
  97. package/test/cast/cast_proxy_unit.js +12 -6
  98. package/test/cast/cast_receiver_integration.js +1 -1
  99. package/test/cast/cast_utils_unit.js +1 -1
  100. package/test/dash/dash_parser_manifest_unit.js +73 -0
  101. package/test/demo/demo_unit.js +0 -1
  102. package/test/hls/hls_parser_unit.js +45 -1
  103. package/test/media/drm_engine_unit.js +56 -0
  104. package/test/media/playhead_unit.js +27 -1
  105. package/test/media/presentation_timeline_unit.js +19 -0
  106. package/test/media/segment_index_unit.js +79 -6
  107. package/test/media/streaming_engine_unit.js +3 -3
  108. package/test/player_integration.js +38 -0
  109. package/test/player_unit.js +19 -1
  110. package/test/test/assets/3675/chunk-stream0-00001.m4s +0 -0
  111. package/test/test/assets/3675/chunk-stream0-00002.m4s +0 -0
  112. package/test/test/assets/3675/chunk-stream0-00003.m4s +0 -0
  113. package/test/test/assets/3675/chunk-stream0-00004.m4s +0 -0
  114. package/test/test/assets/3675/chunk-stream0-00005.m4s +0 -0
  115. package/test/test/assets/3675/chunk-stream0-00006.m4s +0 -0
  116. package/test/test/assets/3675/chunk-stream0-00007.m4s +0 -0
  117. package/test/test/assets/3675/dash_0.mpd +30 -0
  118. package/test/test/assets/3675/dash_1.mpd +30 -0
  119. package/test/test/assets/3675/dash_2.mpd +30 -0
  120. package/test/test/assets/3675/dash_3.mpd +30 -0
  121. package/test/test/assets/3675/dash_4.mpd +30 -0
  122. package/test/test/assets/3675/dash_5.mpd +26 -0
  123. package/test/test/assets/3675/init-stream0.m4s +0 -0
  124. package/test/test/util/fake_ad_manager.js +4 -1
  125. package/test/test/util/fake_media_source_engine.js +4 -0
  126. package/test/test/util/simple_fakes.js +5 -3
  127. package/test/test/util/waiter.js +36 -2
  128. package/test/text/ttml_text_parser_unit.js +9 -0
  129. package/test/text/ui_text_displayer_unit.js +19 -3
  130. package/test/ui/localization_unit.js +15 -14
  131. package/test/ui/ui_unit.js +4 -0
  132. package/ui/big_play_button.js +10 -0
  133. package/ui/cast_button.js +2 -3
  134. package/ui/controls.js +9 -7
  135. package/ui/less/containers.less +3 -0
  136. package/ui/locales/en.json +1 -0
  137. package/ui/locales/ru.json +6 -6
  138. package/ui/locales/source.json +4 -0
  139. package/ui/localization.js +28 -51
  140. package/ui/pip_button.js +2 -3
  141. package/ui/play_button.js +10 -10
  142. package/ui/small_play_button.js +12 -0
@@ -65,6 +65,12 @@ shaka.dash.DashParser = class {
65
65
  */
66
66
  this.segmentIndexMap_ = {};
67
67
 
68
+ /**
69
+ * A map of period ids to their durations
70
+ * @private {!Object.<string, number>}
71
+ */
72
+ this.periodDurations_ = {};
73
+
68
74
  /** @private {shaka.util.PeriodCombiner} */
69
75
  this.periodCombiner_ = new shaka.util.PeriodCombiner();
70
76
 
@@ -88,6 +94,18 @@ shaka.dash.DashParser = class {
88
94
 
89
95
  /** @private {!shaka.util.OperationManager} */
90
96
  this.operationManager_ = new shaka.util.OperationManager();
97
+
98
+ /**
99
+ * Largest period start time seen.
100
+ * @private {?number}
101
+ */
102
+ this.largestPeriodStartTime_ = null;
103
+
104
+ /**
105
+ * Period IDs seen in previous manifest.
106
+ * @private {!Array.<number>}
107
+ */
108
+ this.lastManifestUpdatePeriodIds_ = [];
91
109
  }
92
110
 
93
111
  /**
@@ -468,6 +486,7 @@ shaka.dash.DashParser = class {
468
486
  for (const {i, item: elem, next} of enumerate(periodNodes)) {
469
487
  const start = /** @type {number} */ (
470
488
  XmlUtils.parseAttr(elem, 'start', XmlUtils.parseDuration, prevEnd));
489
+ const periodId = elem.id;
471
490
  const givenDuration =
472
491
  XmlUtils.parseAttr(elem, 'duration', XmlUtils.parseDuration);
473
492
 
@@ -501,6 +520,42 @@ shaka.dash.DashParser = class {
501
520
  periodDuration = givenDuration;
502
521
  }
503
522
 
523
+ /**
524
+ * This is to improve robustness when the player observes manifest with
525
+ * past periods that are inconsistent to previous ones.
526
+ *
527
+ * This may happen when a CDN or proxy server switches its upstream from
528
+ * one encoder to another redundant encoder.
529
+ *
530
+ * Skip periods that match all of the following criteria:
531
+ * - Start time is earlier than latest period start time ever seen
532
+ * - Period ID is never seen in the previous manifest
533
+ * - Not the last period in the manifest
534
+ *
535
+ * Periods that meet the aforementioned criteria are considered invalid
536
+ * and should be safe to discard.
537
+ */
538
+
539
+ if (this.largestPeriodStartTime_ !== null &&
540
+ periodId !== null && start !== null &&
541
+ start < this.largestPeriodStartTime_ &&
542
+ !this.lastManifestUpdatePeriodIds_.includes(periodId) &&
543
+ i + 1 != periodNodes.length) {
544
+ shaka.log.debug(
545
+ `Skipping Period with ID ${periodId} as its start time is smaller` +
546
+ ' than the largest period start time that has been seen, and ID ' +
547
+ 'is unseen before');
548
+ continue;
549
+ }
550
+
551
+
552
+ // Save maximum period start time if it is the last period
553
+ if (start !== null &&
554
+ (this.largestPeriodStartTime_ === null ||
555
+ start > this.largestPeriodStartTime_)) {
556
+ this.largestPeriodStartTime_ = start;
557
+ }
558
+
504
559
  // Parse child nodes.
505
560
  const info = {
506
561
  start: start,
@@ -511,6 +566,10 @@ shaka.dash.DashParser = class {
511
566
  const period = this.parsePeriod_(context, baseUris, info);
512
567
  periods.push(period);
513
568
 
569
+ if (context.period.id && periodDuration) {
570
+ this.periodDurations_[context.period.id] = periodDuration;
571
+ }
572
+
514
573
  if (periodDuration == null) {
515
574
  if (next) {
516
575
  // If the duration is still null and we aren't at the end, then we
@@ -528,6 +587,9 @@ shaka.dash.DashParser = class {
528
587
  prevEnd = start + periodDuration;
529
588
  } // end of period parsing loop
530
589
 
590
+ // Replace previous seen periods with the current one.
591
+ this.lastManifestUpdatePeriodIds_ = periods.map((el) => el.id);
592
+
531
593
  if (presentationDuration != null) {
532
594
  if (prevEnd != presentationDuration) {
533
595
  shaka.log.warning(
@@ -958,7 +1020,7 @@ shaka.dash.DashParser = class {
958
1020
  const hasManifest = !!this.manifest_;
959
1021
  streamInfo = shaka.dash.SegmentTemplate.createStreamInfo(
960
1022
  context, requestInitSegment, this.segmentIndexMap_, hasManifest,
961
- this.config_.dash.initialSegmentLimit);
1023
+ this.config_.dash.initialSegmentLimit, this.periodDurations_);
962
1024
  } else {
963
1025
  goog.asserts.assert(isText,
964
1026
  'Must have Segment* with non-text streams.');
@@ -63,12 +63,12 @@ shaka.dash.SegmentList = class {
63
63
  const start = context.presentationTimeline.getSegmentAvailabilityStart();
64
64
  segmentIndex.mergeAndEvict(references, start);
65
65
  } else {
66
- context.presentationTimeline.notifySegments(references);
67
66
  segmentIndex = new shaka.media.SegmentIndex(references);
68
67
  if (id && context.dynamic) {
69
68
  segmentIndexMap[id] = segmentIndex;
70
69
  }
71
70
  }
71
+ context.presentationTimeline.notifySegments(references);
72
72
 
73
73
  if (!context.dynamic || !context.periodInfo.isLastPeriod) {
74
74
  const periodStart = context.periodInfo.start;
@@ -35,11 +35,12 @@ shaka.dash.SegmentTemplate = class {
35
35
  * @param {boolean} isUpdate True if the manifest is being updated.
36
36
  * @param {number} segmentLimit The maximum number of segments to generate for
37
37
  * a SegmentTemplate with fixed duration.
38
+ * @param {!Object.<string, number>} periodDurationMap
38
39
  * @return {shaka.dash.DashParser.StreamInfo}
39
40
  */
40
41
  static createStreamInfo(
41
42
  context, requestInitSegment, segmentIndexMap, isUpdate,
42
- segmentLimit) {
43
+ segmentLimit, periodDurationMap) {
43
44
  goog.asserts.assert(context.representation.segmentTemplate,
44
45
  'Should only be called with SegmentTemplate');
45
46
  const SegmentTemplate = shaka.dash.SegmentTemplate;
@@ -77,7 +78,8 @@ shaka.dash.SegmentTemplate = class {
77
78
  return {
78
79
  generateSegmentIndex: () => {
79
80
  return SegmentTemplate.generateSegmentIndexFromDuration_(
80
- shallowCopyOfContext, info, segmentLimit, initSegmentReference);
81
+ shallowCopyOfContext, info, segmentLimit, initSegmentReference,
82
+ periodDurationMap);
81
83
  },
82
84
  };
83
85
  } else {
@@ -114,12 +116,12 @@ shaka.dash.SegmentTemplate = class {
114
116
  segmentIndex.mergeAndEvict(references,
115
117
  context.presentationTimeline.getSegmentAvailabilityStart());
116
118
  } else {
117
- context.presentationTimeline.notifySegments(references);
118
119
  segmentIndex = new shaka.media.SegmentIndex(references);
119
120
  if (id && context.dynamic) {
120
121
  segmentIndexMap[id] = segmentIndex;
121
122
  }
122
123
  }
124
+ context.presentationTimeline.notifySegments(references);
123
125
 
124
126
  if (shouldFit) {
125
127
  segmentIndex.fit(periodStart, periodEnd);
@@ -259,11 +261,12 @@ shaka.dash.SegmentTemplate = class {
259
261
  * @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
260
262
  * @param {number} segmentLimit The maximum number of segments to generate.
261
263
  * @param {shaka.media.InitSegmentReference} initSegmentReference
264
+ * @param {!Object.<string, number>} periodDurationMap
262
265
  * @return {!Promise.<shaka.media.SegmentIndex>}
263
266
  * @private
264
267
  */
265
268
  static generateSegmentIndexFromDuration_(
266
- context, info, segmentLimit, initSegmentReference) {
269
+ context, info, segmentLimit, initSegmentReference, periodDurationMap) {
267
270
  goog.asserts.assert(info.mediaTemplate,
268
271
  'There should be a media template with duration');
269
272
 
@@ -275,9 +278,20 @@ shaka.dash.SegmentTemplate = class {
275
278
  // Capture values that could change as the parsing context moves on to
276
279
  // other parts of the manifest.
277
280
  const periodStart = context.periodInfo.start;
278
- const periodDuration = context.periodInfo.duration;
279
- const periodEnd = periodDuration ?
280
- periodStart + periodDuration : Infinity;
281
+ const periodId = context.period.id;
282
+ const initialPeriodDuration = context.periodInfo.duration;
283
+
284
+ // For multi-period live streams the period duration may not be known until
285
+ // the following period appears in an updated manifest. periodDurationMap
286
+ // provides the updated period duration.
287
+ const getPeriodEnd = () => {
288
+ const periodDuration =
289
+ (periodId != null && periodDurationMap[periodId]) ||
290
+ initialPeriodDuration;
291
+ const periodEnd = periodDuration ?
292
+ (periodStart + periodDuration) : Infinity;
293
+ return periodEnd;
294
+ };
281
295
 
282
296
  const segmentDuration = info.segmentDuration;
283
297
  goog.asserts.assert(
@@ -304,7 +318,7 @@ shaka.dash.SegmentTemplate = class {
304
318
 
305
319
  Math.min(
306
320
  presentationTimeline.getSegmentAvailabilityEnd(),
307
- periodEnd),
321
+ getPeriodEnd()),
308
322
  ];
309
323
  };
310
324
 
@@ -378,7 +392,8 @@ shaka.dash.SegmentTemplate = class {
378
392
  const segmentStart = segmentPeriodTime + periodStart;
379
393
  // Cap the segment end at the period end so that references from the
380
394
  // next period will fit neatly after it.
381
- const segmentEnd = Math.min(segmentStart + segmentDuration, periodEnd);
395
+ const segmentEnd = Math.min(segmentStart + segmentDuration,
396
+ getPeriodEnd());
382
397
 
383
398
  // This condition will be true unless the segmentStart was >= periodEnd.
384
399
  // If we've done the position calculations correctly, this won't happen.
@@ -394,7 +409,7 @@ shaka.dash.SegmentTemplate = class {
394
409
  initSegmentReference,
395
410
  timestampOffset,
396
411
  /* appendWindowStart= */ periodStart,
397
- /* appendWindowEnd= */ periodEnd);
412
+ /* appendWindowEnd= */ getPeriodEnd());
398
413
  };
399
414
 
400
415
  for (let position = minPosition; position <= maxPosition; ++position) {
@@ -407,7 +422,15 @@ shaka.dash.SegmentTemplate = class {
407
422
 
408
423
  // If the availability timeline currently ends before the period, we will
409
424
  // need to add references over time.
410
- if (presentationTimeline.getSegmentAvailabilityEnd() < periodEnd) {
425
+ const willNeedToAddReferences =
426
+ presentationTimeline.getSegmentAvailabilityEnd() < getPeriodEnd();
427
+
428
+ // When we start a live stream with a period that ends within the
429
+ // availability window we will not need to add more references, but we will
430
+ // need to evict old references.
431
+ const willNeedToEvictReferences = presentationTimeline.isLive();
432
+
433
+ if (willNeedToAddReferences || willNeedToEvictReferences) {
411
434
  // The period continues to get longer over time, so check for new
412
435
  // references once every |segmentDuration| seconds.
413
436
  // We clamp to |minPosition| in case the initial range was reversed and no
@@ -416,7 +439,9 @@ shaka.dash.SegmentTemplate = class {
416
439
  let nextPosition = Math.max(minPosition, maxPosition + 1);
417
440
  segmentIndex.updateEvery(segmentDuration, () => {
418
441
  // Evict any references outside the window.
419
- segmentIndex.evict(presentationTimeline.getSegmentAvailabilityStart());
442
+ const availabilityStartTime =
443
+ presentationTimeline.getSegmentAvailabilityStart();
444
+ segmentIndex.evict(availabilityStartTime);
420
445
 
421
446
  // Compute any new references that need to be added.
422
447
  const [_, maxPosition] = computeAvailablePositionRange();
@@ -426,8 +451,10 @@ shaka.dash.SegmentTemplate = class {
426
451
  references.push(reference);
427
452
  nextPosition++;
428
453
  }
429
- if (presentationTimeline.getSegmentAvailabilityEnd() >= periodEnd &&
430
- !references.length) {
454
+
455
+ // The timer must continue firing until the entire period is
456
+ // unavailable, so that all references will be evicted.
457
+ if (availabilityStartTime > getPeriodEnd() && !references.length) {
431
458
  // Signal stop.
432
459
  return null;
433
460
  }
@@ -475,6 +475,30 @@ shaka.hls.HlsParser = class {
475
475
  }
476
476
  }
477
477
 
478
+ // Now that the content has been fit, notify segments.
479
+ this.segmentsToNotifyByStream_ = [];
480
+ const streamsToNotify = [];
481
+ for (const variant of variants) {
482
+ for (const stream of [variant.video, variant.audio]) {
483
+ if (stream) {
484
+ streamsToNotify.push(stream);
485
+ }
486
+ }
487
+ }
488
+ await Promise.all(streamsToNotify.map(async (stream) => {
489
+ await stream.createSegmentIndex();
490
+ }));
491
+ for (const stream of streamsToNotify) {
492
+ this.segmentsToNotifyByStream_.push(stream.segmentIndex.references);
493
+ }
494
+ this.notifySegments_();
495
+
496
+ // This asserts that the live edge is being calculated from segment times.
497
+ // For VOD and event streams, this check should still pass.
498
+ goog.asserts.assert(
499
+ !this.presentationTimeline_.usingPresentationStartTime(),
500
+ 'We should not be using the presentation start time in HLS!');
501
+
478
502
  this.manifest_ = {
479
503
  presentationTimeline: this.presentationTimeline_,
480
504
  variants,
@@ -642,7 +666,8 @@ shaka.hls.HlsParser = class {
642
666
  // Create variants for each variant tag.
643
667
  const variantsPromises = tags.map(async (tag) => {
644
668
  const frameRate = tag.getAttributeValue('FRAME-RATE');
645
- const bandwidth = Number(tag.getRequiredAttrValue('BANDWIDTH'));
669
+ const bandwidth = Number(tag.getAttributeValue('AVERAGE-BANDWIDTH')) ||
670
+ Number(tag.getRequiredAttrValue('BANDWIDTH'));
646
671
 
647
672
  const resolution = tag.getAttributeValue('RESOLUTION');
648
673
  const [width, height] = resolution ? resolution.split('x') : [null, null];
@@ -1063,7 +1088,7 @@ shaka.hls.HlsParser = class {
1063
1088
  // "AUTOSELECT=YES". A value of "AUTOSELECT=NO" would imply that it may
1064
1089
  // only be selected explicitly by the user, and we don't have a way to
1065
1090
  // represent that in our model.
1066
- const defaultAttrValue = tag.getAttribute('DEFAULT');
1091
+ const defaultAttrValue = tag.getAttributeValue('DEFAULT');
1067
1092
  const primary = defaultAttrValue == 'YES';
1068
1093
 
1069
1094
  const channelsCount = type == 'audio' ? this.getChannelsCount_(tag) : null;
@@ -1396,12 +1421,6 @@ shaka.hls.HlsParser = class {
1396
1421
  }
1397
1422
 
1398
1423
  this.notifySegments_();
1399
-
1400
- // This asserts that the live edge is being calculated from segment times.
1401
- // For VOD and event streams, this check should still pass.
1402
- goog.asserts.assert(
1403
- !this.presentationTimeline_.usingPresentationStartTime(),
1404
- 'We should not be using the presentation start time in HLS!');
1405
1424
  }
1406
1425
 
1407
1426
  /**
@@ -1637,9 +1656,6 @@ shaka.hls.HlsParser = class {
1637
1656
  references.push(reference);
1638
1657
  }
1639
1658
 
1640
- this.segmentsToNotifyByStream_.push(references);
1641
- this.notifySegments_();
1642
-
1643
1659
  return references;
1644
1660
  }
1645
1661
 
@@ -1976,6 +1992,12 @@ shaka.hls.HlsParser = class {
1976
1992
  reader.rewind(1);
1977
1993
  };
1978
1994
 
1995
+ // We will look a few packet-lengths forward to find the first sync byte.
1996
+ // Note that we are using this method on what is already a subset of the
1997
+ // file (the first |shaka.hls.HlsParser.START_OF_SEGMENT_SIZE_| bytes), so
1998
+ // we can't look too far ahead to begin with.
1999
+ let syncByteScanLength = Math.min(reader.getLength() - 188, 5 * 188);
2000
+
1979
2001
  // TODO: refactor this while loop for better readability.
1980
2002
  // eslint-disable-next-line no-constant-condition
1981
2003
  while (true) {
@@ -1984,8 +2006,16 @@ shaka.hls.HlsParser = class {
1984
2006
 
1985
2007
  syncByte = reader.readUint8();
1986
2008
  if (syncByte != 0x47) {
2009
+ if (syncByteScanLength > 0) {
2010
+ // This file could have started with a cut-off TS packet. Scan forward
2011
+ // until we find a sync byte.
2012
+ syncByteScanLength -= 1;
2013
+ continue;
2014
+ }
1987
2015
  fail();
1988
2016
  }
2017
+ // If we've found a sync byte, stop scanning forward for future packets.
2018
+ syncByteScanLength = 0;
1989
2019
 
1990
2020
  const flagsAndPacketId = reader.readUint16();
1991
2021
  const packetId = flagsAndPacketId & 0x1fff;
@@ -75,6 +75,24 @@ shaka.media.ContentWorkarounds = class {
75
75
  newType: ContentWorkarounds.BOX_TYPE_ENCV_,
76
76
  });
77
77
  })
78
+ .fullBox('avc3', (box) => {
79
+ boxesToModify.push({
80
+ box,
81
+ newType: ContentWorkarounds.BOX_TYPE_ENCV_,
82
+ });
83
+ })
84
+ .fullBox('ac-3', (box) => {
85
+ boxesToModify.push({
86
+ box,
87
+ newType: ContentWorkarounds.BOX_TYPE_ENCA_,
88
+ });
89
+ })
90
+ .fullBox('ec-3', (box) => {
91
+ boxesToModify.push({
92
+ box,
93
+ newType: ContentWorkarounds.BOX_TYPE_ENCA_,
94
+ });
95
+ })
78
96
  .fullBox('mp4a', (box) => {
79
97
  boxesToModify.push({
80
98
  box,
@@ -496,6 +496,7 @@ shaka.media.DrmEngine = class {
496
496
  tasks.push(session.remove());
497
497
 
498
498
  await Promise.all(tasks);
499
+ this.activeSessions_.delete(session);
499
500
  }
500
501
 
501
502
  /**
@@ -1750,9 +1751,16 @@ shaka.media.DrmEngine = class {
1750
1751
  // audio adaptations, so we shouldn't have to worry about checking
1751
1752
  // robustness.
1752
1753
  if (drm1.keySystem == drm2.keySystem) {
1754
+ /** @type {Array<shaka.extern.InitDataOverride>} */
1753
1755
  let initData = [];
1754
1756
  initData = initData.concat(drm1.initData || []);
1755
1757
  initData = initData.concat(drm2.initData || []);
1758
+ initData = initData.filter((d, i) => {
1759
+ return d.keyId === undefined || i === initData.findIndex((d2) => {
1760
+ return d2.keyId === d.keyId;
1761
+ });
1762
+ });
1763
+
1756
1764
  const keyIds = drm1.keyIds && drm2.keyIds ?
1757
1765
  new Set([...drm1.keyIds, ...drm2.keyIds]) :
1758
1766
  drm1.keyIds || drm2.keyIds;
@@ -154,8 +154,10 @@ shaka.media.GapJumpingController = class {
154
154
  }
155
155
  // Don't gap jump while paused, so that you don't constantly jump ahead
156
156
  // while paused on a livestream. We make an exception for time 0, since we
157
- // may be _required_ to seek on startup before play can begin.
158
- if (this.video_.paused && this.video_.currentTime != 0) {
157
+ // may be _required_ to seek on startup before play can begin, but only if
158
+ // autoplay is enabled.
159
+ if (this.video_.paused && (this.video_.currentTime != 0 ||
160
+ (!this.video_.autoplay && this.video_.currentTime == 0))) {
159
161
  return;
160
162
  }
161
163
 
@@ -214,8 +216,10 @@ shaka.media.GapJumpingController = class {
214
216
  this.didFireLargeGap_ = true;
215
217
 
216
218
  // Event firing is synchronous.
217
- const event = new shaka.util.FakeEvent(
218
- 'largegap', {'currentTime': currentTime, 'gapSize': jumpSize});
219
+ const data = new Map()
220
+ .set('currentTime', currentTime)
221
+ .set('gapSize', jumpSize);
222
+ const event = new shaka.util.FakeEvent('largegap', data);
219
223
  event.cancelable = true;
220
224
  this.onEvent_(event);
221
225
 
@@ -577,6 +577,13 @@ shaka.media.MediaSourceEngine = class {
577
577
  this.textEngine_.setSelectedClosedCaptionId(id, videoBufferEndTime);
578
578
  }
579
579
 
580
+ /** Disable embedded closed captions. */
581
+ clearSelectedClosedCaptionId() {
582
+ if (this.textEngine_) {
583
+ this.textEngine_.setSelectedClosedCaptionId('', 0);
584
+ }
585
+ }
586
+
580
587
  /**
581
588
  * Enqueue an operation to remove data from the SourceBuffer.
582
589
  *
@@ -80,7 +80,7 @@ shaka.media.SrcEqualsPlayhead = class {
80
80
  // We listen for the loaded-data-event so that we know when we can
81
81
  // interact with |currentTime|.
82
82
  const onLoaded = () => {
83
- if (this.startTime_ == null) {
83
+ if (this.startTime_ == null || this.startTime_ == 0) {
84
84
  this.started_ = true;
85
85
  } else {
86
86
  // Startup is complete only when the video element acknowledges the
@@ -351,9 +351,10 @@ shaka.media.PresentationTimeline = class {
351
351
  */
352
352
  getSegmentAvailabilityEnd() {
353
353
  if (!this.isLive() && !this.isInProgress()) {
354
- return this.duration_;
354
+ // It's a static manifest (can also be a dynamic->static conversion)
355
+ return this.maxSegmentEndTime_ || this.duration_;
355
356
  }
356
-
357
+ // Can be either live or "in-progress recording" (live with known duration)
357
358
  return Math.min(this.getLiveEdge_(), this.duration_);
358
359
  }
359
360
 
@@ -181,7 +181,9 @@ shaka.media.SegmentIndex = class {
181
181
 
182
182
  /**
183
183
  * Merges the given SegmentReferences. Supports extending the original
184
- * references only. Will not replace old references or interleave new ones.
184
+ * references only. Will replace old references with equivalent new ones, and
185
+ * keep any unique old ones.
186
+ *
185
187
  * Used, for example, by the DASH and HLS parser, where manifests may not list
186
188
  * all available references, so we must keep available references in memory to
187
189
  * fill the availability window.
@@ -189,6 +191,8 @@ shaka.media.SegmentIndex = class {
189
191
  * @param {!Array.<!shaka.media.SegmentReference>} references The list of
190
192
  * SegmentReferences, which must be sorted first by their start times
191
193
  * (ascending) and second by their end times (ascending).
194
+ * @deprecated Not used directly by our own parsers, so will become private in
195
+ * v4. Use mergeAndEvict() instead.
192
196
  * @export
193
197
  */
194
198
  merge(references) {
@@ -199,13 +203,13 @@ shaka.media.SegmentIndex = class {
199
203
  return;
200
204
  }
201
205
 
202
- // Partial segments are used for live edge, and should be removed when they
203
- // get older. Remove the old SegmentReferences after the first new
204
- // reference's start time.
205
206
  if (!references.length) {
206
207
  return;
207
208
  }
208
209
 
210
+ // Partial segments are used for live edge, and should be removed when they
211
+ // get older. Remove the old SegmentReferences after the first new
212
+ // reference's start time.
209
213
  this.references = this.references.filter((r) => {
210
214
  return r.startTime < references[0].startTime;
211
215
  });
@@ -233,13 +237,27 @@ shaka.media.SegmentIndex = class {
233
237
  * @export
234
238
  */
235
239
  mergeAndEvict(references, windowStart) {
236
- // FIlter out the references that are no longer available to avoid
240
+ // Filter out the references that are no longer available to avoid
237
241
  // repeatedly evicting them and messing up eviction count.
238
242
  references = references.filter((r) => {
239
- return r.endTime > windowStart;
243
+ return r.endTime > windowStart &&
244
+ (this.references.length == 0 ||
245
+ r.endTime > this.references[0].startTime);
240
246
  });
241
247
 
248
+ const oldFirstRef = this.references[0];
242
249
  this.merge(references);
250
+ const newFirstRef = this.references[0];
251
+
252
+ if (oldFirstRef) {
253
+ // We don't compare the actual ref, since the object could legitimately be
254
+ // replaced with an equivalent. Even the URIs could change due to
255
+ // load-balancing actions taken by the server. However, if the time
256
+ // changes, its not an equivalent reference.
257
+ goog.asserts.assert(oldFirstRef.startTime == newFirstRef.startTime,
258
+ 'SegmentIndex.merge should not change the first reference time!');
259
+ }
260
+
243
261
  this.evict(windowStart);
244
262
  }
245
263
 
@@ -363,20 +381,28 @@ shaka.media.SegmentIndex = class {
363
381
 
364
382
  /** @return {!shaka.media.SegmentIterator} */
365
383
  [Symbol.iterator]() {
366
- return this.getIteratorForTime(0);
384
+ const iter = this.getIteratorForTime(0);
385
+ goog.asserts.assert(iter != null, 'Iterator for 0 should never be null!');
386
+ return iter;
367
387
  }
368
388
 
369
389
  /**
370
390
  * Returns a new iterator that initially points to the segment that contains
371
391
  * the given time. Like the normal iterator, next() must be called first to
372
- * get to the first element.
392
+ * get to the first element. Returns null if we do not find a segment at the
393
+ * requested time.
373
394
  *
374
395
  * @param {number} time
375
- * @return {!shaka.media.SegmentIterator}
396
+ * @return {?shaka.media.SegmentIterator}
376
397
  * @export
377
398
  */
378
399
  getIteratorForTime(time) {
379
- return new shaka.media.SegmentIterator(this, this.find(time) || 0);
400
+ const index = this.find(time);
401
+ if (index == null) {
402
+ return null;
403
+ } else {
404
+ return new shaka.media.SegmentIterator(this, index);
405
+ }
380
406
  }
381
407
 
382
408
 
@@ -463,7 +489,11 @@ shaka.media.SegmentIterator = class {
463
489
  */
464
490
  seek(time) {
465
491
  const iter = this.segmentIndex_.getIteratorForTime(time);
466
- this.nextPosition_ = iter.nextPosition_;
492
+ if (iter) {
493
+ this.nextPosition_ = iter.nextPosition_;
494
+ } else {
495
+ this.nextPosition_ = Number.MAX_VALUE;
496
+ }
467
497
  return this.next().value;
468
498
  }
469
499