shaka-player 4.12.4 → 4.12.6

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 (55) hide show
  1. package/dist/deps.js +2 -2
  2. package/dist/locales.js +35 -35
  3. package/dist/shaka-player.compiled.d.ts +11 -3
  4. package/dist/shaka-player.compiled.debug.d.ts +11 -3
  5. package/dist/shaka-player.compiled.debug.externs.js +8 -5
  6. package/dist/shaka-player.compiled.debug.js +340 -331
  7. package/dist/shaka-player.compiled.debug.map +3 -3
  8. package/dist/shaka-player.compiled.externs.js +8 -5
  9. package/dist/shaka-player.compiled.js +1383 -1378
  10. package/dist/shaka-player.compiled.map +4 -4
  11. package/dist/shaka-player.ui.d.ts +11 -3
  12. package/dist/shaka-player.ui.debug.d.ts +11 -3
  13. package/dist/shaka-player.ui.debug.externs.js +8 -5
  14. package/dist/shaka-player.ui.debug.js +357 -348
  15. package/dist/shaka-player.ui.debug.map +3 -3
  16. package/dist/shaka-player.ui.externs.js +8 -5
  17. package/dist/shaka-player.ui.js +1605 -1599
  18. package/dist/shaka-player.ui.map +3 -3
  19. package/externs/shaka/codecs.js +21 -0
  20. package/externs/shaka/offline.js +4 -0
  21. package/lib/cast/cast_utils.js +1 -0
  22. package/lib/dash/dash_parser.js +3 -0
  23. package/lib/hls/hls_parser.js +26 -5
  24. package/lib/media/drm_engine.js +70 -29
  25. package/lib/media/manifest_filterer.js +2 -1
  26. package/lib/media/media_source_engine.js +138 -129
  27. package/lib/media/segment_utils.js +50 -44
  28. package/lib/media/streaming_engine.js +9 -1
  29. package/lib/offline/download_info.js +5 -0
  30. package/lib/offline/download_manager.js +54 -0
  31. package/lib/offline/manifest_converter.js +1 -0
  32. package/lib/offline/storage.js +12 -3
  33. package/lib/player.js +52 -29
  34. package/lib/polyfill/media_capabilities.js +1 -1
  35. package/lib/text/simple_text_displayer.js +9 -1
  36. package/lib/text/ui_text_displayer.js +2 -14
  37. package/lib/transmuxer/aac_transmuxer.js +11 -3
  38. package/lib/transmuxer/ac3_transmuxer.js +11 -3
  39. package/lib/transmuxer/ec3_transmuxer.js +11 -3
  40. package/lib/transmuxer/h264.js +143 -70
  41. package/lib/transmuxer/mp3_transmuxer.js +11 -3
  42. package/lib/transmuxer/ts_transmuxer.js +68 -28
  43. package/lib/util/drm_utils.js +20 -0
  44. package/lib/util/mime_utils.js +3 -0
  45. package/lib/util/object_utils.js +83 -0
  46. package/lib/util/platform.js +48 -40
  47. package/lib/util/stream_utils.js +37 -113
  48. package/lib/util/ts_parser.js +111 -85
  49. package/package.json +1 -1
  50. package/ui/ad_counter.js +5 -0
  51. package/ui/controls.js +4 -2
  52. package/ui/fast_forward_button.js +7 -0
  53. package/ui/play_button.js +1 -1
  54. package/ui/playback_rate_selection.js +1 -1
  55. package/ui/rewind_button.js +7 -0
@@ -146,9 +146,10 @@ shaka.util.StreamUtils = class {
146
146
  'vp09': 0.9,
147
147
  'hevc': 0.85,
148
148
  'dovi-hevc': 0.8,
149
- 'av01': 0.75,
150
- 'dovi-av1': 0.7,
151
- 'vvc': 0.65,
149
+ 'dovi-p5': 0.75,
150
+ 'av01': 0.7,
151
+ 'dovi-av1': 0.65,
152
+ 'vvc': 0.6,
152
153
  };
153
154
 
154
155
  const videoStreams = Array.from(videoStreamsSet)
@@ -393,11 +394,13 @@ shaka.util.StreamUtils = class {
393
394
  * @param {shaka.media.DrmEngine} drmEngine
394
395
  * @param {shaka.extern.Manifest} manifest
395
396
  * @param {!Array<string>=} preferredKeySystems
397
+ * @param {!Object.<string, string>=} keySystemsMapping
396
398
  */
397
- static async filterManifest(drmEngine, manifest, preferredKeySystems = []) {
399
+ static async filterManifest(drmEngine, manifest, preferredKeySystems = [],
400
+ keySystemsMapping = {}) {
398
401
  await shaka.util.StreamUtils.filterManifestByMediaCapabilities(
399
402
  drmEngine, manifest, manifest.offlineSessionIds.length > 0,
400
- preferredKeySystems);
403
+ preferredKeySystems, keySystemsMapping);
401
404
  shaka.util.StreamUtils.filterTextStreams_(manifest);
402
405
  await shaka.util.StreamUtils.filterImageStreams_(manifest);
403
406
  }
@@ -411,9 +414,11 @@ shaka.util.StreamUtils = class {
411
414
  * @param {shaka.extern.Manifest} manifest
412
415
  * @param {boolean} usePersistentLicenses
413
416
  * @param {!Array<string>} preferredKeySystems
417
+ * @param {!Object.<string, string>} keySystemsMapping
414
418
  */
415
419
  static async filterManifestByMediaCapabilities(
416
- drmEngine, manifest, usePersistentLicenses, preferredKeySystems) {
420
+ drmEngine, manifest, usePersistentLicenses, preferredKeySystems,
421
+ keySystemsMapping) {
417
422
  goog.asserts.assert(navigator.mediaCapabilities,
418
423
  'MediaCapabilities should be valid.');
419
424
 
@@ -435,7 +440,8 @@ shaka.util.StreamUtils = class {
435
440
  const StreamUtils = shaka.util.StreamUtils;
436
441
 
437
442
  manifest.variants = manifest.variants.filter((variant) => {
438
- const supported = StreamUtils.checkVariantSupported_(variant, keySystem);
443
+ const supported = StreamUtils.checkVariantSupported_(
444
+ variant, keySystem, keySystemsMapping);
439
445
  // Filter out all unsupported variants.
440
446
  if (!supported) {
441
447
  shaka.log.debug('Dropping variant - not compatible with platform',
@@ -485,15 +491,31 @@ shaka.util.StreamUtils = class {
485
491
  /**
486
492
  * @param {!shaka.extern.Variant} variant
487
493
  * @param {?string} keySystem
494
+ * @param {!Object.<string, string>} keySystemsMapping
488
495
  * @return {boolean}
489
496
  * @private
490
497
  */
491
- static checkVariantSupported_(variant, keySystem) {
492
- const ContentType = shaka.util.ManifestParserUtils.ContentType;
493
- const Capabilities = shaka.media.Capabilities;
494
- const ManifestParserUtils = shaka.util.ManifestParserUtils;
495
- const MimeUtils = shaka.util.MimeUtils;
496
- const StreamUtils = shaka.util.StreamUtils;
498
+ static checkVariantSupported_(variant, keySystem, keySystemsMapping) {
499
+ const variantSupported = variant.decodingInfos.some((decodingInfo) => {
500
+ if (!decodingInfo.supported) {
501
+ return false;
502
+ }
503
+ if (keySystem) {
504
+ const keySystemAccess = decodingInfo.keySystemAccess;
505
+ if (keySystemAccess) {
506
+ const currentKeySystem =
507
+ keySystemsMapping[keySystemAccess.keySystem] ||
508
+ keySystemAccess.keySystem;
509
+ if (currentKeySystem != keySystem) {
510
+ return false;
511
+ }
512
+ }
513
+ }
514
+ return true;
515
+ });
516
+ if (!variantSupported) {
517
+ return false;
518
+ }
497
519
 
498
520
  const isXboxOne = shaka.util.Platform.isXboxOne();
499
521
  const isFirefoxAndroid = shaka.util.Platform.isFirefox() &&
@@ -511,43 +533,6 @@ shaka.util.StreamUtils = class {
511
533
  return false;
512
534
  }
513
535
 
514
- if (video) {
515
- let videoCodecs = StreamUtils.getCorrectVideoCodecs(video.codecs);
516
- // For multiplexed streams. Here we must check the audio of the
517
- // stream to see if it is compatible.
518
- if (video.codecs.includes(',')) {
519
- const allCodecs = video.codecs.split(',');
520
-
521
- videoCodecs = ManifestParserUtils.guessCodecs(
522
- ContentType.VIDEO, allCodecs);
523
- videoCodecs = StreamUtils.getCorrectVideoCodecs(videoCodecs);
524
- let audioCodecs = ManifestParserUtils.guessCodecs(
525
- ContentType.AUDIO, allCodecs);
526
- audioCodecs = StreamUtils.getCorrectAudioCodecs(
527
- audioCodecs, video.mimeType);
528
-
529
- const audioFullType = MimeUtils.getFullOrConvertedType(
530
- video.mimeType, audioCodecs, ContentType.AUDIO);
531
-
532
- if (!Capabilities.isTypeSupported(audioFullType)) {
533
- return false;
534
- }
535
-
536
- // Update the codec string with the (possibly) converted codecs.
537
- videoCodecs = [videoCodecs, audioCodecs].join(',');
538
- }
539
-
540
- const fullType = MimeUtils.getFullOrConvertedType(
541
- video.mimeType, videoCodecs, ContentType.VIDEO);
542
-
543
- if (!Capabilities.isTypeSupported(fullType)) {
544
- return false;
545
- }
546
-
547
- // Update the codec string with the (possibly) converted codecs.
548
- video.codecs = videoCodecs;
549
- }
550
-
551
536
  const audio = variant.audio;
552
537
 
553
538
  // See: https://github.com/shaka-project/shaka-player/issues/6111
@@ -559,68 +544,7 @@ shaka.util.StreamUtils = class {
559
544
  return false;
560
545
  }
561
546
 
562
- if (audio) {
563
- const codecs = StreamUtils.getCorrectAudioCodecs(
564
- audio.codecs, audio.mimeType);
565
- const fullType = MimeUtils.getFullOrConvertedType(
566
- audio.mimeType, codecs, ContentType.AUDIO);
567
-
568
- if (!Capabilities.isTypeSupported(fullType)) {
569
- return false;
570
- }
571
-
572
- // Update the codec string with the (possibly) converted codecs.
573
- audio.codecs = codecs;
574
- }
575
-
576
- return variant.decodingInfos.some((decodingInfo) => {
577
- if (!decodingInfo.supported) {
578
- return false;
579
- }
580
- if (keySystem) {
581
- const keySystemAccess = decodingInfo.keySystemAccess;
582
- if (keySystemAccess) {
583
- if (keySystemAccess.keySystem != keySystem) {
584
- return false;
585
- }
586
- }
587
- }
588
- return true;
589
- });
590
- }
591
-
592
-
593
- /**
594
- * Constructs a string out of an object, similar to the JSON.stringify method.
595
- * Unlike that method, this guarantees that the order of the keys is
596
- * alphabetical, so it can be used as a way to reliably compare two objects.
597
- *
598
- * @param {!Object} obj
599
- * @return {string}
600
- * @private
601
- */
602
- static alphabeticalKeyOrderStringify_(obj) {
603
- const keys = [];
604
- for (const key in obj) {
605
- keys.push(key);
606
- }
607
- // Alphabetically sort the keys, so they will be in a reliable order.
608
- keys.sort();
609
-
610
- const terms = [];
611
- for (const key of keys) {
612
- const escapedKey = JSON.stringify(key);
613
- const value = obj[key];
614
- if (value instanceof Object) {
615
- const stringifiedValue =
616
- shaka.util.StreamUtils.alphabeticalKeyOrderStringify_(value);
617
- terms.push(escapedKey + ':' + stringifiedValue);
618
- } else {
619
- const escapedValue = JSON.stringify(value);
620
- terms.push(escapedKey + ':' + escapedValue);
621
- }
622
- }
623
- return '{' + terms.join(',') + '}';
547
+ return true;
624
548
  }
625
549
 
626
550
 
@@ -659,7 +583,7 @@ shaka.util.StreamUtils = class {
659
583
  const promises = [];
660
584
  for (const decodingConfig of decodingConfigs) {
661
585
  const cacheKey =
662
- StreamUtils.alphabeticalKeyOrderStringify_(decodingConfig);
586
+ shaka.util.ObjectUtils.alphabeticalKeyOrderStringify(decodingConfig);
663
587
 
664
588
  const cache = StreamUtils.decodingConfigCache_;
665
589
  if (cache[cacheKey]) {
@@ -517,7 +517,11 @@ shaka.util.TsParser = class {
517
517
  shaka.Deprecate.deprecateFeature(5,
518
518
  'TsParser.parseAvcNalus',
519
519
  'Please use parseNalus function instead.');
520
- return this.parseNalus(pes);
520
+ const lastInfo = {
521
+ nalu: null,
522
+ state: null,
523
+ };
524
+ return this.parseNalus(pes, lastInfo);
521
525
  }
522
526
 
523
527
  /**
@@ -527,12 +531,11 @@ shaka.util.TsParser = class {
527
531
  * Credit to https://github.com/video-dev/hls.js/blob/master/src/demux/tsdemuxer.ts
528
532
  *
529
533
  * @param {shaka.extern.MPEG_PES} pes
530
- * @param {?shaka.extern.VideoNalu=} lastNalu
531
- * @param {?number=} lastState
534
+ * @param {{nalu: ?shaka.extern.VideoNalu, state: ?number}} lastInfo
532
535
  * @return {!Array.<shaka.extern.VideoNalu>}
533
536
  * @export
534
537
  */
535
- parseNalus(pes, lastNalu, lastState) {
538
+ parseNalus(pes, lastInfo) {
536
539
  const timescale = shaka.util.TsParser.Timescale;
537
540
  const time = pes.pts ? pes.pts / timescale : null;
538
541
  const data = pes.data;
@@ -548,7 +551,12 @@ shaka.util.TsParser = class {
548
551
  // precede each NALU. A start code is 2 or 3 0x00 bytes followed with a
549
552
  // 0x01 byte. e.g. 0x000001 or 0x00000001.
550
553
  // More info in: https://stackoverflow.com/questions/24884827/possible-locations-for-sequence-picture-parameter-sets-for-h-264-stream/24890903#24890903
551
- let numZeros = lastState || 0;
554
+ let numZeros = lastInfo.state || 0;
555
+
556
+ const initialNumZeros = numZeros;
557
+
558
+ /** @type {number} */
559
+ let i = 0;
552
560
 
553
561
  /** @type {!Array.<shaka.extern.VideoNalu>} */
554
562
  const nalus = [];
@@ -559,103 +567,119 @@ shaka.util.TsParser = class {
559
567
  // Extracted from the first byte.
560
568
  let lastNaluType = 0;
561
569
 
562
- /** @type {?shaka.extern.VideoNalu} */
563
- let infoOfLastNalu;
570
+ const getNaluType = (offset) => {
571
+ if (this.videoCodec_ == 'hvc') {
572
+ return (data[offset] >> 1) & 0x3f;
573
+ } else {
574
+ return data[offset] & 0x1f;
575
+ }
576
+ };
564
577
 
565
- for (let i = 0; i < len; ++i) {
566
- const value = data[i];
567
- if (!value) {
568
- numZeros++;
569
- } else if (numZeros >= 2 && value == 1) {
570
- if (lastNalu && !nalus.length && lastNaluStart == -1) {
571
- // If we are scanning the next PES, we need append the data to the
572
- // previous Nalu and don't scan for more nalus.
573
- const startCodeSize = numZeros > 3 ? 3 : numZeros;
574
- const lastByteToKeep = i - startCodeSize;
575
- // Optimization
576
- if (lastState && lastByteToKeep != 0) {
577
- const prevData = data.subarray(0, lastByteToKeep);
578
- lastNalu.data = shaka.util.Uint8ArrayUtils.concat(
579
- lastNalu.data, prevData);
580
- lastNalu.fullData = shaka.util.Uint8ArrayUtils.concat(
581
- lastNalu.fullData, prevData);
582
- }
583
- }
584
- // We just read a start code. Consume the NALU we passed, if any.
585
- if (lastNaluStart >= 0) {
586
- // Because the start position includes the header size.
587
- const firstByteToKeep = lastNaluStart + naluHeaderSize;
578
+ const getLastNalu = () => {
579
+ if (nalus.length) {
580
+ return nalus[nalus.length - 1];
581
+ }
582
+ return lastInfo.nalu;
583
+ };
588
584
 
589
- // Compute the last byte to keep. The start code is at most 3 zeros.
590
- // Any earlier zeros are not part of the start code.
591
- const startCodeSize = (numZeros > 3 ? 3 : numZeros) + 1;
592
- const lastByteToKeep = i - startCodeSize;
585
+ if (numZeros == -1) {
586
+ // special use case where we found 3 or 4-byte start codes exactly at the
587
+ // end of previous PES packet
588
+ lastNaluStart = 0;
589
+ // NALu type is value read from offset 0
590
+ lastNaluType = getNaluType(0);
591
+ numZeros = 0;
592
+ i = 1;
593
+ }
593
594
 
595
+ while (i < len) {
596
+ const value = data[i++];
597
+ // Optimization. numZeros 0 and 1 are the predominant case.
598
+ if (!numZeros) {
599
+ numZeros = value ? 0 : 1;
600
+ continue;
601
+ }
602
+ if (numZeros === 1) {
603
+ numZeros = value ? 0 : 2;
604
+ continue;
605
+ }
606
+ if (!value) {
607
+ numZeros = 3;
608
+ } else if (value == 1) {
609
+ const overflow = i - numZeros - 1;
610
+ if (lastNaluStart >= 0) {
594
611
  /** @type {shaka.extern.VideoNalu} */
595
612
  const nalu = {
596
- // subarray's end position is exclusive, so add one.
597
- data: data.subarray(firstByteToKeep, lastByteToKeep + 1),
598
- fullData: data.subarray(lastNaluStart, lastByteToKeep + 1),
613
+ data: data.subarray(lastNaluStart + naluHeaderSize, overflow),
614
+ fullData: data.subarray(lastNaluStart, overflow),
599
615
  type: lastNaluType,
600
616
  time: time,
617
+ state: null,
601
618
  };
602
619
  nalus.push(nalu);
603
- } else if (lastNalu && !nalus.length) {
604
- const overflow = i - numZeros;
605
- if (overflow > 0) {
606
- const prevData = data.subarray(0, overflow);
607
- lastNalu.data = shaka.util.Uint8ArrayUtils.concat(
608
- lastNalu.data, prevData);
609
- lastNalu.fullData = shaka.util.Uint8ArrayUtils.concat(
610
- lastNalu.fullData, prevData);
620
+ } else {
621
+ const lastNalu = getLastNalu();
622
+ if (lastNalu) {
623
+ if (initialNumZeros && i <= 4 - initialNumZeros) {
624
+ // Start delimiter overlapping between PES packets
625
+ // strip start delimiter bytes from the end of last NAL unit
626
+ // check if lastNalu had a state different from zero
627
+ if (lastNalu.state) {
628
+ // strip last bytes
629
+ lastNalu.data = lastNalu.data.subarray(
630
+ 0, lastNalu.data.byteLength - initialNumZeros);
631
+ lastNalu.fullData = lastNalu.fullData.subarray(
632
+ 0, lastNalu.fullData.byteLength - initialNumZeros);
633
+ }
634
+ }
635
+ // If NAL units are not starting right at the beginning of the PES
636
+ // packet, push preceding data into previous NAL unit.
637
+ if (overflow > 0) {
638
+ const prevData = data.subarray(0, overflow);
639
+ lastNalu.data = shaka.util.Uint8ArrayUtils.concat(
640
+ lastNalu.data, prevData);
641
+ lastNalu.fullData = shaka.util.Uint8ArrayUtils.concat(
642
+ lastNalu.fullData, prevData);
643
+ lastNalu.state = 0;
644
+ }
611
645
  }
612
646
  }
613
647
 
614
- // We just read a start code, so there should be another byte here, at
615
- // least, for the NALU type. Check just in case.
616
- if (i >= len - naluHeaderSize) {
617
- shaka.log.warning('Malformed TS, incomplete NALU, ignoring.');
618
- return nalus;
619
- }
620
-
621
- // Advance and read the type of the next NALU.
622
- i++;
623
- lastNaluStart = i;
624
- if (this.videoCodec_ == 'hvc') {
625
- lastNaluType = (data[i] >> 1) & 0x3f;
648
+ // Check if we can read unit type
649
+ if (i < len) {
650
+ lastNaluType = getNaluType(i);
651
+ lastNaluStart = i;
652
+ numZeros = 0;
626
653
  } else {
627
- lastNaluType = data[i] & 0x1f;
654
+ // Not enough byte to read unit type.
655
+ // Let's read it on next PES parsing.
656
+ numZeros = -1;
628
657
  }
629
- numZeros = 0;
630
658
  } else {
631
659
  numZeros = 0;
632
660
  }
633
- // If we have gone through all the data from the PES and we have an
634
- // unfinished Nalu, we will try to use the next PES to complete the
635
- // unfinished Nalu.
636
- if (i >= (len - 1) && lastNaluStart >= 0 && numZeros >= 0) {
637
- // The rest of the buffer was a NALU.
638
- // Because the start position includes the header size.
639
- const firstByteToKeep = lastNaluStart + naluHeaderSize;
640
- infoOfLastNalu = {
641
- data: data.subarray(firstByteToKeep, len),
642
- fullData: data.subarray(lastNaluStart, len),
643
- type: lastNaluType,
644
- time: time,
645
- };
646
- }
647
661
  }
648
662
 
649
- if (infoOfLastNalu) {
650
- nalus.push(infoOfLastNalu);
663
+ if (lastNaluStart >= 0 && numZeros >= 0) {
664
+ const nalu = {
665
+ data: data.subarray(lastNaluStart + naluHeaderSize, len),
666
+ fullData: data.subarray(lastNaluStart, len),
667
+ type: lastNaluType,
668
+ time: time,
669
+ state: numZeros,
670
+ };
671
+ nalus.push(nalu);
651
672
  }
652
- if (!nalus.length && lastNalu) {
653
- lastNalu.data = shaka.util.Uint8ArrayUtils.concat(
654
- lastNalu.data, data);
655
- lastNalu.fullData = shaka.util.Uint8ArrayUtils.concat(
656
- lastNalu.fullData, data);
673
+ if (!nalus.length && lastInfo.nalu) {
674
+ const lastNalu = getLastNalu();
675
+ if (lastNalu) {
676
+ lastNalu.data = shaka.util.Uint8ArrayUtils.concat(
677
+ lastNalu.data, data);
678
+ lastNalu.fullData = shaka.util.Uint8ArrayUtils.concat(
679
+ lastNalu.fullData, data);
680
+ }
657
681
  }
658
- lastState = Math.min(3, numZeros);
682
+ lastInfo.state = numZeros;
659
683
  return nalus;
660
684
  }
661
685
 
@@ -759,14 +783,16 @@ shaka.util.TsParser = class {
759
783
  }
760
784
  }
761
785
  if (naluProcessing) {
762
- let lastNalu;
763
- let lastState;
786
+ const lastInfo = {
787
+ nalu: null,
788
+ state: null,
789
+ };
764
790
  const pesWithLength = [];
765
791
  for (const pes of this.videoPes_) {
766
- pes.nalus = this.parseNalus(pes, lastNalu, lastState);
792
+ pes.nalus = this.parseNalus(pes, lastInfo);
767
793
  if (pes.nalus.length) {
768
794
  pesWithLength.push(pes);
769
- lastNalu = pes.nalus[pes.nalus.length - 1];
795
+ lastInfo.nalu = pes.nalus[pes.nalus.length - 1];
770
796
  }
771
797
  }
772
798
  this.videoPes_ = pesWithLength;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shaka-player",
3
3
  "description": "DASH/EME video player library",
4
- "version": "4.12.4",
4
+ "version": "4.12.6",
5
5
  "homepage": "https://github.com/shaka-project/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
package/ui/ad_counter.js CHANGED
@@ -100,6 +100,11 @@ shaka.ui.AdCounter = class extends shaka.ui.Element {
100
100
  goog.asserts.assert(this.ad != null,
101
101
  'this.ad should exist at this point');
102
102
 
103
+ if (!this.ad.isLinear()) {
104
+ // Do not show information for non-linear ads.
105
+ return;
106
+ }
107
+
103
108
  const secondsLeft = Math.round(this.ad.getRemainingTime());
104
109
  const adDuration = this.ad.getDuration();
105
110
  if (secondsLeft == -1 || adDuration == -1) {
package/ui/controls.js CHANGED
@@ -868,9 +868,11 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
868
868
  return;
869
869
  }
870
870
 
871
- this.player_.cancelTrickPlay();
872
-
873
871
  if (this.presentationIsPaused()) {
872
+ // If we are at the end, go back to the beginning.
873
+ if (this.player_.isEnded()) {
874
+ this.video_.currentTime = this.player_.seekRange().start;
875
+ }
874
876
  this.video_.play();
875
877
  } else {
876
878
  this.video_.pause();
@@ -81,6 +81,13 @@ shaka.ui.FastForwardButton = class extends shaka.ui.Element {
81
81
  // selected. If no more rates are available, the first one is set.
82
82
  const newRate = (newRateIndex != this.fastForwardRates_.length) ?
83
83
  this.fastForwardRates_[newRateIndex] : this.fastForwardRates_[0];
84
+
85
+ if (this.video.paused) {
86
+ // Our fast forward is implemented with playbackRate and needs the video
87
+ // to be playing (to not be paused) to take immediate effect.
88
+ // If the video is paused, "unpause" it.
89
+ this.video.play();
90
+ }
84
91
  this.player.trickPlay(newRate);
85
92
 
86
93
  this.button_.setAttribute('shaka-status', newRate + 'x');
package/ui/play_button.js CHANGED
@@ -116,7 +116,7 @@ shaka.ui.PlayButton = class extends shaka.ui.Element {
116
116
  return false;
117
117
  }
118
118
 
119
- return this.video.ended;
119
+ return this.player ? this.player.isEnded() : true;
120
120
  }
121
121
 
122
122
  /**
@@ -121,7 +121,7 @@ shaka.ui.PlaybackRateSelection = class extends shaka.ui.SettingsMenu {
121
121
 
122
122
  this.eventManager.listen(button, 'click', () => {
123
123
  const rate = this.playbackRates_.get(rateStr);
124
- if (rate == 1) {
124
+ if (rate == this.video.defaultPlaybackRate) {
125
125
  this.player.cancelTrickPlay();
126
126
  } else {
127
127
  this.player.trickPlay(rate, /* useTrickPlayTrack= */ false);
@@ -82,6 +82,13 @@ shaka.ui.RewindButton = class extends shaka.ui.Element {
82
82
  // selected. If no more rates are available, the first one is set.
83
83
  const newRate = (newRateIndex != this.rewindRates_.length) ?
84
84
  this.rewindRates_[newRateIndex] : this.rewindRates_[0];
85
+
86
+ if (this.video.paused) {
87
+ // Our fast forward is implemented with playbackRate and needs the video
88
+ // to be playing (to not be paused) to take immediate effect.
89
+ // If the video is paused, "unpause" it.
90
+ this.video.play();
91
+ }
85
92
  this.player.trickPlay(newRate);
86
93
 
87
94
  this.button_.setAttribute('shaka-status', newRate + 'x');