shaka-player 4.0.2 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/AUTHORS +1 -0
  2. package/CHANGELOG.md +36 -16
  3. package/CONTRIBUTORS +2 -0
  4. package/build/test.py +1 -1
  5. package/demo/common/assets.js +1 -1
  6. package/demo/common/message_ids.js +1 -0
  7. package/demo/config.js +3 -1
  8. package/demo/locales/en.json +1 -0
  9. package/demo/locales/source.json +4 -0
  10. package/demo/main.js +3 -3
  11. package/dist/demo.compiled.debug.js +7 -7
  12. package/dist/demo.compiled.debug.map +2 -2
  13. package/dist/demo.compiled.js +7 -7
  14. package/dist/demo.compiled.map +2 -2
  15. package/dist/deps.js +2 -2
  16. package/dist/locales.js +30 -30
  17. package/dist/receiver.compiled.debug.js +3 -3
  18. package/dist/receiver.compiled.debug.map +2 -2
  19. package/dist/receiver.compiled.js +3 -3
  20. package/dist/receiver.compiled.map +2 -2
  21. package/dist/shaka-player.compiled.d.ts +14 -8
  22. package/dist/shaka-player.compiled.debug.d.ts +14 -8
  23. package/dist/shaka-player.compiled.debug.externs.js +13 -4
  24. package/dist/shaka-player.compiled.debug.js +296 -282
  25. package/dist/shaka-player.compiled.debug.map +4 -4
  26. package/dist/shaka-player.compiled.externs.js +13 -4
  27. package/dist/shaka-player.compiled.js +781 -773
  28. package/dist/shaka-player.compiled.map +4 -4
  29. package/dist/shaka-player.ui.d.ts +15 -9
  30. package/dist/shaka-player.ui.debug.d.ts +15 -9
  31. package/dist/shaka-player.ui.debug.externs.js +13 -4
  32. package/dist/shaka-player.ui.debug.js +345 -326
  33. package/dist/shaka-player.ui.debug.map +4 -4
  34. package/dist/shaka-player.ui.externs.js +13 -4
  35. package/dist/shaka-player.ui.js +955 -949
  36. package/dist/shaka-player.ui.map +4 -4
  37. package/docs/design/chromecast.md +3 -1
  38. package/docs/tutorials/drm-config.md +16 -0
  39. package/docs/tutorials/ui.md +26 -0
  40. package/externs/chromecast.js +9 -2
  41. package/externs/shaka/manifest.js +7 -0
  42. package/externs/shaka/player.js +25 -4
  43. package/externs/shaka/text.js +7 -0
  44. package/lib/cast/cast_proxy.js +19 -8
  45. package/lib/cast/cast_receiver.js +3 -3
  46. package/lib/cast/cast_sender.js +12 -2
  47. package/lib/hls/hls_parser.js +189 -69
  48. package/lib/media/drm_engine.js +17 -8
  49. package/lib/media/gap_jumping_controller.js +23 -1
  50. package/lib/media/playhead.js +47 -4
  51. package/lib/media/segment_index.js +26 -1
  52. package/lib/media/segment_reference.js +10 -3
  53. package/lib/media/stall_detector.js +19 -2
  54. package/lib/media/streaming_engine.js +9 -2
  55. package/lib/offline/manifest_converter.js +1 -0
  56. package/lib/player.js +175 -69
  57. package/lib/text/cue.js +6 -0
  58. package/lib/text/ui_text_displayer.js +1 -0
  59. package/lib/text/vtt_text_parser.js +4 -0
  60. package/lib/util/error.js +6 -0
  61. package/lib/util/fake_event.js +39 -0
  62. package/lib/util/player_configuration.js +2 -0
  63. package/lib/util/stats.js +28 -1
  64. package/lib/util/stream_utils.js +8 -0
  65. package/package.json +2 -2
  66. package/test/cast/cast_proxy_unit.js +3 -1
  67. package/test/cast/cast_sender_unit.js +21 -6
  68. package/test/demo/demo_unit.js +2 -1
  69. package/test/hls/hls_parser_unit.js +102 -8
  70. package/test/media/adaptation_set_unit.js +1 -0
  71. package/test/media/playhead_unit.js +114 -25
  72. package/test/media/stall_detector_unit.js +20 -2
  73. package/test/media/streaming_engine_integration.js +2 -1
  74. package/test/media/streaming_engine_unit.js +1 -0
  75. package/test/player_integration.js +3 -0
  76. package/test/player_unit.js +142 -1
  77. package/test/test/util/manifest_generator.js +2 -0
  78. package/test/test/util/simple_fakes.js +14 -0
  79. package/test/test/util/streaming_engine_util.js +1 -0
  80. package/test/text/vtt_text_parser_unit.js +4 -0
  81. package/test/ui/ui_unit.js +18 -5
  82. package/test/util/stream_utils_unit.js +108 -0
  83. package/ui/controls.js +12 -8
  84. package/ui/externs/ui.js +10 -1
  85. package/ui/statistics_button.js +10 -0
  86. package/ui/ui.js +12 -1
@@ -121,13 +121,19 @@ shaka.hls.HlsParser = class {
121
121
  this.updatePlaylistDelay_ = 0;
122
122
 
123
123
  /**
124
- * A time offset to apply to EXT-X-PROGRAM-DATE-TIME values to normalize
125
- * them so that they start at 0. This is necessary because these times will
126
- * be used to set presentation times for segments.
127
- * null means we don't have enough data yet.
124
+ * If true, we have already calculated offsets to synchronize streams.
125
+ * Offsets are computed in syncStreams*_().
126
+ * @private {boolean}
127
+ */
128
+ this.streamsSynced_ = false;
129
+
130
+ /**
131
+ * The minimum sequence number for generated segments, when ignoring
132
+ * EXT-X-PROGRAM-DATE-TIME.
133
+ *
128
134
  * @private {?number}
129
135
  */
130
- this.syncTimeOffset_ = null;
136
+ this.minSequenceNumber_ = null;
131
137
 
132
138
  /**
133
139
  * This timer is used to trigger the start of a manifest update. A manifest
@@ -361,39 +367,155 @@ shaka.hls.HlsParser = class {
361
367
  }
362
368
 
363
369
  /**
364
- * If necessary, makes sure that sync times will be normalized to 0, so that
365
- * a stream does not start buffering at 50 years in because sync times are
366
- * measured in time since 1970.
370
+ * Align all streams by sequence number by dropping early segments. Then
371
+ * offset all streams to begin at presentation time 0.
367
372
  * @private
368
373
  */
369
- calculateSyncTimeOffset_() {
370
- if (this.syncTimeOffset_ != null) {
371
- // The offset was already calculated.
374
+ syncStreamsWithSequenceNumber_() {
375
+ if (this.streamsSynced_) {
372
376
  return;
373
377
  }
374
378
 
375
- const segments = new Set();
376
- let lowestSyncTime = Infinity;
379
+ // Sync using media sequence number. Find the highest starting sequence
380
+ // number among all streams. Later, we will drop any references to
381
+ // earlier segments in other streams, then offset everything back to 0.
382
+ let highestStartingSequenceNumber = -1;
383
+ const firstSequenceNumberMap = new Map();
384
+
377
385
  for (const streamInfo of this.uriToStreamInfosMap_.values()) {
378
386
  const segmentIndex = streamInfo.stream.segmentIndex;
379
387
  if (segmentIndex) {
380
- segmentIndex.forEachTopLevelReference((segment) => {
381
- if (segment.syncTime != null) {
382
- lowestSyncTime = Math.min(lowestSyncTime, segment.syncTime);
383
- segments.add(segment);
388
+ const segment0 = segmentIndex.earliestReference();
389
+ if (segment0) {
390
+ // This looks inefficient, but iteration order is insertion order.
391
+ // So the very first entry should be the one we want.
392
+ // We assert that this holds true so that we are alerted by debug
393
+ // builds and tests if it changes. We still do a loop, though, so
394
+ // that the code functions correctly in production no matter what.
395
+ if (goog.DEBUG) {
396
+ const firstSequenceStartTime =
397
+ streamInfo.mediaSequenceToStartTime.values().next().value;
398
+ goog.asserts.assert(
399
+ firstSequenceStartTime == segment0.startTime,
400
+ 'Sequence number map is not ordered as expected!');
401
+ }
402
+ for (const [sequence, start] of streamInfo.mediaSequenceToStartTime) {
403
+ if (start == segment0.startTime) {
404
+ firstSequenceNumberMap.set(streamInfo, sequence);
405
+
406
+ highestStartingSequenceNumber = Math.max(
407
+ highestStartingSequenceNumber, sequence);
408
+ break;
409
+ }
384
410
  }
385
- });
411
+ }
412
+ }
413
+ }
414
+
415
+ if (highestStartingSequenceNumber < 0) {
416
+ // Nothing to sync.
417
+ return;
418
+ }
419
+
420
+ // From now on, updates will ignore any references before this number.
421
+ this.minSequenceNumber_ = highestStartingSequenceNumber;
422
+
423
+ shaka.log.debug('Syncing HLS streams against base sequence number:',
424
+ this.minSequenceNumber_);
425
+
426
+ for (const streamInfo of this.uriToStreamInfosMap_.values()) {
427
+ const segmentIndex = streamInfo.stream.segmentIndex;
428
+ if (segmentIndex) {
429
+ // Drop any earlier references.
430
+ const numSegmentsToDrop = this.minSequenceNumber_ -
431
+ firstSequenceNumberMap.get(streamInfo);
432
+ segmentIndex.dropFirstReferences(numSegmentsToDrop);
433
+
434
+ // Now adjust timestamps back to begin at 0.
435
+ const segmentN = segmentIndex.earliestReference();
436
+ if (segmentN) {
437
+ this.offsetStream_(streamInfo, -segmentN.startTime);
438
+ }
439
+ }
440
+ }
441
+
442
+ this.streamsSynced_ = true;
443
+ }
444
+
445
+ /**
446
+ * Synchronize streams by the EXT-X-PROGRAM-DATE-TIME tags attached to their
447
+ * segments. Also normalizes segment times so that the earliest segment in
448
+ * any stream is at time 0.
449
+ * @private
450
+ */
451
+ syncStreamsWithProgramDateTime_() {
452
+ if (this.streamsSynced_) {
453
+ return;
454
+ }
455
+
456
+ let lowestSyncTime = Infinity;
457
+
458
+ for (const streamInfo of this.uriToStreamInfosMap_.values()) {
459
+ const segmentIndex = streamInfo.stream.segmentIndex;
460
+ if (segmentIndex) {
461
+ const segment0 = segmentIndex.earliestReference();
462
+ if (segment0 != null && segment0.syncTime != null) {
463
+ lowestSyncTime = Math.min(lowestSyncTime, segment0.syncTime);
464
+ }
386
465
  }
387
466
  }
388
- if (segments.size > 0) {
389
- this.syncTimeOffset_ = -lowestSyncTime;
390
- for (const segment of segments) {
391
- segment.syncTime += this.syncTimeOffset_;
392
- for (const partial of segment.partialReferences) {
393
- partial.syncTime += this.syncTimeOffset_;
467
+
468
+ if (lowestSyncTime == Infinity) {
469
+ // Nothing to sync.
470
+ return;
471
+ }
472
+
473
+ shaka.log.debug('Syncing HLS streams against base time:', lowestSyncTime);
474
+
475
+ for (const streamInfo of this.uriToStreamInfosMap_.values()) {
476
+ const segmentIndex = streamInfo.stream.segmentIndex;
477
+ if (segmentIndex != null) {
478
+ const segment0 = segmentIndex.earliestReference();
479
+ if (segment0.syncTime == null) {
480
+ shaka.log.alwaysError('Missing EXT-X-PROGRAM-DATE-TIME for stream',
481
+ streamInfo.verbatimMediaPlaylistUri,
482
+ 'Expect AV sync issues!');
483
+ } else {
484
+ // The first segment's target startTime should be based entirely on
485
+ // its syncTime. The rest of the stream will be based on that
486
+ // starting point. The earliest segment sync time from any stream
487
+ // will become presentation time 0. If two streams start e.g. 6
488
+ // seconds apart in syncTime, then their first segments will also
489
+ // start 6 seconds apart in presentation time.
490
+ const segment0TargetTime = segment0.syncTime - lowestSyncTime;
491
+ const streamOffset = segment0TargetTime - segment0.startTime;
492
+
493
+ this.offsetStream_(streamInfo, streamOffset);
394
494
  }
395
495
  }
396
496
  }
497
+
498
+ this.streamsSynced_ = true;
499
+ }
500
+
501
+ /**
502
+ * @param {!shaka.hls.HlsParser.StreamInfo} streamInfo
503
+ * @param {number} offset
504
+ * @private
505
+ */
506
+ offsetStream_(streamInfo, offset) {
507
+ streamInfo.stream.segmentIndex.offset(offset);
508
+
509
+ streamInfo.maxTimestamp += offset;
510
+ goog.asserts.assert(streamInfo.maxTimestamp >= 0,
511
+ 'Negative maxTimestamp after adjustment!');
512
+
513
+ for (const [key, value] of streamInfo.mediaSequenceToStartTime) {
514
+ streamInfo.mediaSequenceToStartTime.set(key, value + offset);
515
+ }
516
+
517
+ shaka.log.debug('Offset', offset, 'applied to',
518
+ streamInfo.verbatimMediaPlaylistUri);
397
519
  }
398
520
 
399
521
  /**
@@ -460,6 +582,7 @@ shaka.hls.HlsParser = class {
460
582
  variants.push({
461
583
  id: 0,
462
584
  language: 'und',
585
+ disabledUntilTime: 0,
463
586
  primary: true,
464
587
  audio: type == 'audio' ? streamInfo.stream : null,
465
588
  video: type == 'video' ? streamInfo.stream : null,
@@ -524,7 +647,11 @@ shaka.hls.HlsParser = class {
524
647
 
525
648
  // Now that we have generated all streams, we can determine the offset to
526
649
  // apply to sync times.
527
- this.calculateSyncTimeOffset_();
650
+ if (this.config_.hls.ignoreManifestProgramDateTime) {
651
+ this.syncStreamsWithSequenceNumber_();
652
+ } else {
653
+ this.syncStreamsWithProgramDateTime_();
654
+ }
528
655
 
529
656
  if (this.aesEncrypted_ && variants.length == 0) {
530
657
  // We do not support AES-128 encryption with HLS yet. Variants is null
@@ -539,15 +666,12 @@ shaka.hls.HlsParser = class {
539
666
 
540
667
  // Find the min and max timestamp of the earliest segment in all streams.
541
668
  // Find the minimum duration of all streams as well.
542
- let minFirstTimestamp = Infinity;
543
669
  let minDuration = Infinity;
544
-
545
670
  for (const streamInfo of this.uriToStreamInfosMap_.values()) {
546
- minFirstTimestamp =
547
- Math.min(minFirstTimestamp, streamInfo.minTimestamp);
548
671
  if (streamInfo.stream.type != 'text') {
549
- minDuration = Math.min(minDuration,
550
- streamInfo.maxTimestamp - streamInfo.minTimestamp);
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);
551
675
  }
552
676
  }
553
677
 
@@ -591,18 +715,11 @@ shaka.hls.HlsParser = class {
591
715
  segmentAvailabilityDuration);
592
716
  }
593
717
  } else {
594
- // For VOD/EVENT content, offset everything back to 0.
595
- // Use the minimum timestamp as the offset for all streams.
596
718
  // Use the minimum duration as the presentation duration.
597
719
  this.presentationTimeline_.setDuration(minDuration);
598
- // Use a negative offset to adjust towards 0.
599
- this.presentationTimeline_.offset(-minFirstTimestamp);
600
720
 
601
721
  for (const streamInfo of this.uriToStreamInfosMap_.values()) {
602
- // The segments were created with actual media times, rather than
603
- // presentation-aligned times, so offset them all now.
604
- streamInfo.stream.segmentIndex.offset(-minFirstTimestamp);
605
- // Finally, fit the segments to the playlist duration.
722
+ // Fit the segments to the playlist duration.
606
723
  streamInfo.stream.segmentIndex.fit(/* periodStart= */ 0, minDuration);
607
724
  }
608
725
  }
@@ -810,12 +927,7 @@ shaka.hls.HlsParser = class {
810
927
  });
811
928
 
812
929
  // Create stream info for each audio / video media tag.
813
- // Wait for the first stream info created, so that the start time is fetched
814
- // and can be reused.
815
- if (mediaTags.length) {
816
- await this.createStreamInfoFromMediaTag_(mediaTags[0]);
817
- }
818
- const promises = mediaTags.slice(1).map((tag) => {
930
+ const promises = mediaTags.map((tag) => {
819
931
  return this.createStreamInfoFromMediaTag_(tag);
820
932
  });
821
933
  await Promise.all(promises);
@@ -1583,7 +1695,6 @@ shaka.hls.HlsParser = class {
1583
1695
  throw error;
1584
1696
  }
1585
1697
 
1586
- const minTimestamp = segments[0].startTime;
1587
1698
  const lastEndTime = segments[segments.length - 1].endTime;
1588
1699
  /** @type {!shaka.media.SegmentIndex} */
1589
1700
  const segmentIndex = new shaka.media.SegmentIndex(segments);
@@ -1641,7 +1752,6 @@ shaka.hls.HlsParser = class {
1641
1752
  stream,
1642
1753
  verbatimMediaPlaylistUri,
1643
1754
  absoluteMediaPlaylistUri,
1644
- minTimestamp,
1645
1755
  maxTimestamp: lastEndTime,
1646
1756
  mediaSequenceToStartTime,
1647
1757
  canSkipSegments,
@@ -1853,15 +1963,9 @@ shaka.hls.HlsParser = class {
1853
1963
  const dateTimeTag =
1854
1964
  shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-PROGRAM-DATE-TIME');
1855
1965
  if (dateTimeTag && dateTimeTag.value) {
1856
- const time = shaka.util.XmlUtils.parseDate(dateTimeTag.value);
1857
- goog.asserts.assert(time != null,
1966
+ syncTime = shaka.util.XmlUtils.parseDate(dateTimeTag.value);
1967
+ goog.asserts.assert(syncTime != null,
1858
1968
  'EXT-X-PROGRAM-DATE-TIME format not valid');
1859
- // Sync time offset is null on the first go-through. This indicates that
1860
- // we have not yet seen every stream, and thus do not yet have enough
1861
- // information to determine how to normalize the sync times.
1862
- // For that first go-through, the sync time will be applied after the
1863
- // references are all created. Until then, just offset by 0.
1864
- syncTime = time + (this.syncTimeOffset_ || 0);
1865
1969
  }
1866
1970
  }
1867
1971
 
@@ -1926,6 +2030,11 @@ shaka.hls.HlsParser = class {
1926
2030
  const pAbsoluteUri = shaka.hls.Utils.constructAbsoluteUri(
1927
2031
  absoluteMediaPlaylistUri, pUri);
1928
2032
 
2033
+ let partialStatus = shaka.media.SegmentReference.Status.AVAILABLE;
2034
+ if (item.getAttributeValue('GAP') == 'YES') {
2035
+ partialStatus = shaka.media.SegmentReference.Status.MISSING;
2036
+ }
2037
+
1929
2038
  const partial = new shaka.media.SegmentReference(
1930
2039
  pStartTime,
1931
2040
  pEndTime,
@@ -1935,7 +2044,12 @@ shaka.hls.HlsParser = class {
1935
2044
  initSegmentReference,
1936
2045
  /* timestampOffset= */ 0, // This value is ignored in sequence mode.
1937
2046
  /* appendWindowStart= */ 0,
1938
- /* appendWindowEnd= */ Infinity);
2047
+ /* appendWindowEnd= */ Infinity,
2048
+ /* partialReferences= */ [],
2049
+ /* tilesLayout= */ '',
2050
+ /* tileDuration= */ null,
2051
+ /* syncTime= */ null,
2052
+ partialStatus);
1939
2053
  partialSegmentRefs.push(partial);
1940
2054
  } // for-loop of hlsSegment.partialSegments
1941
2055
  }
@@ -2085,16 +2199,13 @@ shaka.hls.HlsParser = class {
2085
2199
  firstStartTime = mediaSequenceToStartTime.get(position);
2086
2200
  }
2087
2201
 
2088
- const firstSegmentUri = hlsSegments[0].absoluteUri;
2089
- shaka.log.debug('First segment', firstSegmentUri.split('/').pop(),
2090
- 'starts at', firstStartTime);
2091
-
2092
2202
  /** @type {!Array.<!shaka.media.SegmentReference>} */
2093
2203
  const references = [];
2094
2204
 
2205
+ let previousReference = null;
2206
+
2095
2207
  for (let i = 0; i < hlsSegments.length; i++) {
2096
2208
  const item = hlsSegments[i];
2097
- const previousReference = references[references.length - 1];
2098
2209
  const startTime =
2099
2210
  (i == 0) ? firstStartTime : previousReference.endTime;
2100
2211
  position = mediaSequenceNumber + skippedSegments + i;
@@ -2124,9 +2235,17 @@ shaka.hls.HlsParser = class {
2124
2235
  variables,
2125
2236
  playlist.absoluteUri,
2126
2237
  type);
2238
+ previousReference = reference;
2127
2239
 
2128
2240
  if (reference) {
2129
- references.push(reference);
2241
+ if (this.config_.hls.ignoreManifestProgramDateTime &&
2242
+ this.minSequenceNumber_ != null &&
2243
+ position < this.minSequenceNumber_) {
2244
+ // This segment is ignored as part of our fallback synchronization
2245
+ // method.
2246
+ } else {
2247
+ references.push(reference);
2248
+ }
2130
2249
  }
2131
2250
  }
2132
2251
 
@@ -2256,12 +2375,16 @@ shaka.hls.HlsParser = class {
2256
2375
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
2257
2376
  const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
2258
2377
 
2378
+ // If you wait long enough, requesting the first segment can fail
2379
+ // because it has fallen off the left edge of DVR, so to be safer,
2380
+ // let's request the middle segment.
2259
2381
  goog.asserts.assert(playlist.segments.length,
2260
2382
  'Playlist should have segments!');
2261
- const firstSegmentUri = this.variableSubstitution_(
2262
- playlist.segments[0].absoluteUri, variables);
2383
+ const middleSegmentIdx = Math.trunc((playlist.segments.length - 1) / 2);
2384
+ const middleSegmentUri = this.variableSubstitution_(
2385
+ playlist.segments[middleSegmentIdx].absoluteUri, variables);
2263
2386
 
2264
- const parsedUri = new goog.Uri(firstSegmentUri);
2387
+ const parsedUri = new goog.Uri(middleSegmentUri);
2265
2388
  const extension = parsedUri.getPath().split('.').pop();
2266
2389
  const map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
2267
2390
 
@@ -2291,7 +2414,7 @@ shaka.hls.HlsParser = class {
2291
2414
  // If unable to guess mime type, request a segment and try getting it
2292
2415
  // from the response.
2293
2416
  const headRequest = shaka.net.NetworkingEngine.makeRequest(
2294
- [firstSegmentUri], this.config_.retryParameters);
2417
+ [middleSegmentUri], this.config_.retryParameters);
2295
2418
  headRequest.method = 'HEAD';
2296
2419
 
2297
2420
  const response = await this.makeNetworkRequest_(
@@ -2578,7 +2701,6 @@ shaka.hls.HlsParser = class {
2578
2701
  * stream: !shaka.extern.Stream,
2579
2702
  * verbatimMediaPlaylistUri: string,
2580
2703
  * absoluteMediaPlaylistUri: string,
2581
- * minTimestamp: number,
2582
2704
  * maxTimestamp: number,
2583
2705
  * mediaSequenceToStartTime: !Map.<number, number>,
2584
2706
  * canSkipSegments: boolean
@@ -2597,8 +2719,6 @@ shaka.hls.HlsParser = class {
2597
2719
  * @property {string} absoluteMediaPlaylistUri
2598
2720
  * The absolute media playlist URI, resolved relative to the master playlist
2599
2721
  * and updated to reflect any redirects.
2600
- * @property {number} minTimestamp
2601
- * The minimum timestamp found in the stream.
2602
2722
  * @property {number} maxTimestamp
2603
2723
  * The maximum timestamp found in the stream.
2604
2724
  * @property {!Map.<number, number>} mediaSequenceToStartTime
@@ -355,7 +355,8 @@ shaka.media.DrmEngine = class {
355
355
  shaka.media.DrmEngine.fillInDrmInfoDefaults_(
356
356
  info,
357
357
  shaka.util.MapUtils.asMap(this.config_.servers),
358
- shaka.util.MapUtils.asMap(this.config_.advanced || {}));
358
+ shaka.util.MapUtils.asMap(this.config_.advanced || {}),
359
+ this.config_.keySystemsMapping);
359
360
  }
360
361
  }
361
362
 
@@ -2224,16 +2225,20 @@ shaka.media.DrmEngine = class {
2224
2225
  * that the parser left blank. Before working with any drmInfo, it should be
2225
2226
  * passed through here as it is uncommon for drmInfo to be complete when
2226
2227
  * fetched from a manifest because most manifest formats do not have the
2227
- * required information.
2228
+ * required information. Also applies the key systems mapping.
2228
2229
  *
2229
2230
  * @param {shaka.extern.DrmInfo} drmInfo
2230
2231
  * @param {!Map.<string, string>} servers
2231
2232
  * @param {!Map.<string, shaka.extern.AdvancedDrmConfiguration>}
2232
2233
  * advancedConfigs
2234
+ * @param {!Object.<string, string>} keySystemsMapping
2233
2235
  * @private
2234
2236
  */
2235
- static fillInDrmInfoDefaults_(drmInfo, servers, advancedConfigs) {
2236
- if (!drmInfo.keySystem) {
2237
+ static fillInDrmInfoDefaults_(drmInfo, servers, advancedConfigs,
2238
+ keySystemsMapping) {
2239
+ const originalKeySystem = drmInfo.keySystem;
2240
+
2241
+ if (!originalKeySystem) {
2237
2242
  // This is a placeholder from the manifest parser for an unrecognized key
2238
2243
  // system. Skip this entry, to avoid logging nonsensical errors.
2239
2244
  return;
@@ -2254,14 +2259,14 @@ shaka.media.DrmEngine = class {
2254
2259
  // The only way to get license servers from the manifest is not to specify
2255
2260
  // any in your player config.
2256
2261
 
2257
- if (drmInfo.keySystem == 'org.w3.clearkey' && drmInfo.licenseServerUri) {
2262
+ if (originalKeySystem == 'org.w3.clearkey' && drmInfo.licenseServerUri) {
2258
2263
  // Preference 1: Clear Key with pre-configured keys will have a data URI
2259
2264
  // assigned as its license server. Don't change anything.
2260
2265
  return;
2261
2266
  } else if (servers.size) {
2262
2267
  // Preference 2: If anything is configured at the application level,
2263
2268
  // override whatever was in the manifest.
2264
- const server = servers.get(drmInfo.keySystem) || '';
2269
+ const server = servers.get(originalKeySystem) || '';
2265
2270
 
2266
2271
  drmInfo.licenseServerUri = server;
2267
2272
  } else {
@@ -2273,7 +2278,7 @@ shaka.media.DrmEngine = class {
2273
2278
  drmInfo.keyIds = new Set();
2274
2279
  }
2275
2280
 
2276
- const advancedConfig = advancedConfigs.get(drmInfo.keySystem);
2281
+ const advancedConfig = advancedConfigs.get(originalKeySystem);
2277
2282
 
2278
2283
  if (advancedConfig) {
2279
2284
  if (!drmInfo.distinctiveIdentifierRequired) {
@@ -2307,6 +2312,10 @@ shaka.media.DrmEngine = class {
2307
2312
  }
2308
2313
  }
2309
2314
 
2315
+ if (keySystemsMapping[originalKeySystem]) {
2316
+ drmInfo.keySystem = keySystemsMapping[originalKeySystem];
2317
+ }
2318
+
2310
2319
  // Chromecast has a variant of PlayReady that uses a different key
2311
2320
  // system ID. Since manifest parsers convert the standard PlayReady
2312
2321
  // UUID to the standard PlayReady key system ID, here we will switch
@@ -2315,7 +2324,7 @@ shaka.media.DrmEngine = class {
2315
2324
  // player config uses the standard PlayReady ID for license server
2316
2325
  // configuration.
2317
2326
  if (window.cast && window.cast.__platform__) {
2318
- if (drmInfo.keySystem == 'com.microsoft.playready') {
2327
+ if (originalKeySystem == 'com.microsoft.playready') {
2319
2328
  drmInfo.keySystem = 'com.chromecast.playready';
2320
2329
  }
2321
2330
  }
@@ -11,6 +11,7 @@ goog.require('shaka.media.PresentationTimeline');
11
11
  goog.require('shaka.media.StallDetector');
12
12
  goog.require('shaka.media.TimeRangesUtils');
13
13
  goog.require('shaka.util.EventManager');
14
+ goog.require('shaka.util.FakeEvent');
14
15
  goog.require('shaka.util.IReleasable');
15
16
  goog.require('shaka.util.Timer');
16
17
 
@@ -32,8 +33,13 @@ shaka.media.GapJumpingController = class {
32
33
  * playable region. The gap jumping controller takes ownership over the
33
34
  * stall detector.
34
35
  * If no stall detection logic is desired, |null| may be provided.
36
+ * @param {function(!Event)} onEvent
37
+ * Called when an event is raised to be sent to the application.
35
38
  */
36
- constructor(video, timeline, config, stallDetector) {
39
+ constructor(video, timeline, config, stallDetector, onEvent) {
40
+ /** @private {?function(!Event)} */
41
+ this.onEvent_ = onEvent;
42
+
37
43
  /** @private {HTMLMediaElement} */
38
44
  this.video_ = video;
39
45
 
@@ -52,6 +58,9 @@ shaka.media.GapJumpingController = class {
52
58
  /** @private {number} */
53
59
  this.prevReadyState_ = video.readyState;
54
60
 
61
+ /** @private {number} */
62
+ this.gapsJumped_ = 0;
63
+
55
64
  /**
56
65
  * The stall detector tries to keep the playhead moving forward. It is
57
66
  * managed by the gap-jumping controller to avoid conflicts. On some
@@ -98,6 +107,7 @@ shaka.media.GapJumpingController = class {
98
107
  this.stallDetector_ = null;
99
108
  }
100
109
 
110
+ this.onEvent_ = null;
101
111
  this.timeline_ = null;
102
112
  this.video_ = null;
103
113
  }
@@ -121,6 +131,15 @@ shaka.media.GapJumpingController = class {
121
131
  }
122
132
 
123
133
 
134
+ /**
135
+ * Returns the total number of playback gaps jumped.
136
+ * @return {number}
137
+ */
138
+ getGapsJumped() {
139
+ return this.gapsJumped_;
140
+ }
141
+
142
+
124
143
  /**
125
144
  * Called on a recurring timer to check for gaps in the media. This is also
126
145
  * called in a 'waiting' event.
@@ -209,6 +228,9 @@ shaka.media.GapJumpingController = class {
209
228
  }
210
229
 
211
230
  this.video_.currentTime = jumpTo;
231
+ this.gapsJumped_++;
232
+ this.onEvent_(
233
+ new shaka.util.FakeEvent(shaka.util.FakeEvent.EventName.GapJumped));
212
234
  }
213
235
  };
214
236
 
@@ -41,6 +41,20 @@ shaka.media.Playhead = class {
41
41
  */
42
42
  setStartTime(startTime) {}
43
43
 
44
+ /**
45
+ * Get the number of playback stalls detected by the StallDetector.
46
+ *
47
+ * @return {number}
48
+ */
49
+ getStallsDetected() {}
50
+
51
+ /**
52
+ * Get the number of playback gaps jumped by the GapJumpingController.
53
+ *
54
+ * @return {number}
55
+ */
56
+ getGapsJumped() {}
57
+
44
58
  /**
45
59
  * Get the current playhead position. The position will be restricted to valid
46
60
  * time ranges.
@@ -133,6 +147,16 @@ shaka.media.SrcEqualsPlayhead = class {
133
147
  return time || 0;
134
148
  }
135
149
 
150
+ /** @override */
151
+ getStallsDetected() {
152
+ return 0;
153
+ }
154
+
155
+ /** @override */
156
+ getGapsJumped() {
157
+ return 0;
158
+ }
159
+
136
160
  /** @override */
137
161
  notifyOfBufferingChange() {}
138
162
  };
@@ -160,8 +184,10 @@ shaka.media.MediaSourcePlayhead = class {
160
184
  * @param {function()} onSeek
161
185
  * Called when the user agent seeks to a time within the presentation
162
186
  * timeline.
187
+ * @param {function(!Event)} onEvent
188
+ * Called when an event is raised to be sent to the application.
163
189
  */
164
- constructor(mediaElement, manifest, config, startTime, onSeek) {
190
+ constructor(mediaElement, manifest, config, startTime, onSeek, onEvent) {
165
191
  /**
166
192
  * The seek range must be at least this number of seconds long. If it is
167
193
  * smaller than this, change it to be this big so we don't repeatedly seek
@@ -192,12 +218,17 @@ shaka.media.MediaSourcePlayhead = class {
192
218
  /** @private {?number} */
193
219
  this.lastCorrectiveSeek_ = null;
194
220
 
221
+ /** @private {shaka.media.StallDetector} */
222
+ this.stallDetector_ =
223
+ this.createStallDetector_(mediaElement, config, onEvent);
224
+
195
225
  /** @private {shaka.media.GapJumpingController} */
196
226
  this.gapController_ = new shaka.media.GapJumpingController(
197
227
  mediaElement,
198
228
  manifest.presentationTimeline,
199
229
  config,
200
- this.createStallDetector_(mediaElement, config));
230
+ this.stallDetector_,
231
+ onEvent);
201
232
 
202
233
  /** @private {shaka.media.VideoWrapper} */
203
234
  this.videoWrapper_ = new shaka.media.VideoWrapper(
@@ -261,6 +292,16 @@ shaka.media.MediaSourcePlayhead = class {
261
292
  return time;
262
293
  }
263
294
 
295
+ /** @override */
296
+ getStallsDetected() {
297
+ return this.stallDetector_.getStallsDetected();
298
+ }
299
+
300
+ /** @override */
301
+ getGapsJumped() {
302
+ return this.gapController_.getGapsJumped();
303
+ }
304
+
264
305
  /**
265
306
  * Gets the playhead's initial position in seconds.
266
307
  *
@@ -475,10 +516,12 @@ shaka.media.MediaSourcePlayhead = class {
475
516
  *
476
517
  * @param {!HTMLMediaElement} mediaElement
477
518
  * @param {shaka.extern.StreamingConfiguration} config
519
+ * @param {function(!Event)} onEvent
520
+ * Called when an event is raised to be sent to the application.
478
521
  * @return {shaka.media.StallDetector}
479
522
  * @private
480
523
  */
481
- createStallDetector_(mediaElement, config) {
524
+ createStallDetector_(mediaElement, config, onEvent) {
482
525
  if (!config.stallEnabled) {
483
526
  return null;
484
527
  }
@@ -492,7 +535,7 @@ shaka.media.MediaSourcePlayhead = class {
492
535
  // playhead forward.
493
536
  const detector = new shaka.media.StallDetector(
494
537
  new shaka.media.StallDetector.MediaElementImplementation(mediaElement),
495
- threshold);
538
+ threshold, onEvent);
496
539
 
497
540
  detector.onStall((at, duration) => {
498
541
  shaka.log.debug(`Stall detected at ${at} for ${duration} seconds.`);