shaka-player 4.10.21 → 4.10.23

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.
@@ -1322,11 +1322,13 @@ shaka.dash.DashParser = class {
1322
1322
  for (let i = 0; i < periodNodes.length; i++) {
1323
1323
  const elem = periodNodes[i];
1324
1324
  const next = periodNodes[i + 1];
1325
- const start = /** @type {number} */ (
1325
+ let start = /** @type {number} */ (
1326
1326
  TXml.parseAttr(elem, 'start', TXml.parseDuration, prevEnd));
1327
1327
  const periodId = elem.attributes['id'];
1328
1328
  const givenDuration =
1329
1329
  TXml.parseAttr(elem, 'duration', TXml.parseDuration);
1330
+ start = (i == 0 && start == 0 && this.isTransitionFromDynamicToStatic_) ?
1331
+ seekRangeStart : start;
1330
1332
 
1331
1333
  let periodDuration = null;
1332
1334
  if (next) {
@@ -1336,7 +1338,7 @@ shaka.dash.DashParser = class {
1336
1338
  const nextStart =
1337
1339
  TXml.parseAttr(next, 'start', TXml.parseDuration);
1338
1340
  if (nextStart != null) {
1339
- periodDuration = nextStart - start;
1341
+ periodDuration = nextStart - start + seekRangeStart;
1340
1342
  }
1341
1343
  } else if (presentationDuration != null) {
1342
1344
  // "The Period extends until the Period.start of the next Period, or
@@ -2626,25 +2628,51 @@ shaka.dash.DashParser = class {
2626
2628
  return intValue;
2627
2629
  }
2628
2630
 
2631
+ case 'tag:dolby.com,2015:dash:audio_channel_configuration:2015': {
2632
+ // ETSI TS 103 190-2 v1.2.1, Annex G.3
2633
+ // LSB-to-MSB order
2634
+ const channelCountMapping =
2635
+ [2, 1, 2, 2, 2, 2, 1, 2, 2, 1, 1, 1, 1, 2, 1, 1, 2, 2];
2636
+ const hexValue = parseInt(value, 16);
2637
+ if (!hexValue) { // 0 or NaN
2638
+ shaka.log.warning('Channel parsing failure! ' +
2639
+ 'Ignoring scheme and value', scheme, value);
2640
+ continue;
2641
+ }
2642
+ let numBits = 0;
2643
+ for (let i = 0; i < channelCountMapping.length; i++) {
2644
+ if (hexValue & (1<<i)) {
2645
+ numBits += channelCountMapping[i];
2646
+ }
2647
+ }
2648
+ if (numBits) {
2649
+ return numBits;
2650
+ }
2651
+ continue;
2652
+ }
2653
+
2629
2654
  case 'tag:dolby.com,2014:dash:audio_channel_configuration:2011':
2630
2655
  case 'urn:dolby:dash:audio_channel_configuration:2011': {
2631
- // A hex-encoded 16-bit integer, in which each bit represents a
2632
- // channel.
2633
- let hexValue = parseInt(value, 16);
2656
+ // Defined by https://ott.dolby.com/OnDelKits/DDP/Dolby_Digital_Plus_Online_Delivery_Kit_v1.5/Documentation/Content_Creation/SDM/help_files/topics/ddp_mpeg_dash_c_mpd_auchlconfig.html
2657
+ // keep list in order of the spec; reverse for LSB-to-MSB order
2658
+ const channelCountMapping =
2659
+ [1, 1, 1, 1, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 1, 1].reverse();
2660
+ const hexValue = parseInt(value, 16);
2634
2661
  if (!hexValue) { // 0 or NaN
2635
2662
  shaka.log.warning('Channel parsing failure! ' +
2636
2663
  'Ignoring scheme and value', scheme, value);
2637
2664
  continue;
2638
2665
  }
2639
- // Count the 1-bits in hexValue.
2640
2666
  let numBits = 0;
2641
- while (hexValue) {
2642
- if (hexValue & 1) {
2643
- ++numBits;
2667
+ for (let i = 0; i < channelCountMapping.length; i++) {
2668
+ if (hexValue & (1<<i)) {
2669
+ numBits += channelCountMapping[i];
2644
2670
  }
2645
- hexValue >>= 1;
2646
2671
  }
2647
- return numBits;
2672
+ if (numBits) {
2673
+ return numBits;
2674
+ }
2675
+ continue;
2648
2676
  }
2649
2677
 
2650
2678
  // Defined by https://dashif.org/identifiers/audio_source_metadata/ and clause 8.2, in ISO/IEC 23001-8.
@@ -787,7 +787,7 @@ shaka.dash.TimelineSegmentIndex = class extends shaka.media.SegmentIndex {
787
787
  if (currentTimeline.length) {
788
788
  const lastCurrentEntry = currentTimeline[currentTimeline.length - 1];
789
789
  newEntries = info.timeline.filter((entry) => {
790
- return entry.start >= lastCurrentEntry.end;
790
+ return entry.end > lastCurrentEntry.end;
791
791
  });
792
792
  } else {
793
793
  newEntries = info.timeline.slice();
@@ -9,7 +9,6 @@ goog.provide('shaka.media.DrmEngine');
9
9
  goog.require('goog.asserts');
10
10
  goog.require('shaka.log');
11
11
  goog.require('shaka.net.NetworkingEngine');
12
- goog.require('shaka.transmuxer.TransmuxerEngine');
13
12
  goog.require('shaka.util.BufferUtils');
14
13
  goog.require('shaka.util.Destroyer');
15
14
  goog.require('shaka.util.DrmUtils');
@@ -20,7 +19,6 @@ goog.require('shaka.util.IDestroyable');
20
19
  goog.require('shaka.util.Iterables');
21
20
  goog.require('shaka.util.ManifestParserUtils');
22
21
  goog.require('shaka.util.MapUtils');
23
- goog.require('shaka.util.MimeUtils');
24
22
  goog.require('shaka.util.ObjectUtils');
25
23
  goog.require('shaka.util.Platform');
26
24
  goog.require('shaka.util.Pssh');
@@ -41,9 +39,6 @@ shaka.media.DrmEngine = class {
41
39
  /** @private {?shaka.media.DrmEngine.PlayerInterface} */
42
40
  this.playerInterface_ = playerInterface;
43
41
 
44
- /** @private {!Set.<string>} */
45
- this.supportedTypes_ = new Set();
46
-
47
42
  /** @private {MediaKeys} */
48
43
  this.mediaKeys_ = null;
49
44
 
@@ -197,7 +192,6 @@ shaka.media.DrmEngine = class {
197
192
 
198
193
  // Break references to everything else we hold internally.
199
194
  this.currentDrmInfo_ = null;
200
- this.supportedTypes_.clear();
201
195
  this.mediaKeys_ = null;
202
196
  this.storedPersistentSessions_ = new Map();
203
197
  this.config_ = null;
@@ -743,6 +737,17 @@ shaka.media.DrmEngine = class {
743
737
  return;
744
738
  }
745
739
  }
740
+ let duplicate = false;
741
+ this.storedPersistentSessions_.forEach((metadata, sessionId) => {
742
+ if (!duplicate &&
743
+ shaka.util.BufferUtils.equal(initData, metadata.initData)) {
744
+ duplicate = true;
745
+ }
746
+ });
747
+ if (duplicate) {
748
+ shaka.log.debug('Ignoring duplicate init data.');
749
+ return;
750
+ }
746
751
  }
747
752
 
748
753
  // If there are pre-existing sessions that have all been loaded
@@ -762,40 +767,6 @@ shaka.media.DrmEngine = class {
762
767
  return this.initialized_;
763
768
  }
764
769
 
765
- /**
766
- * Check if DrmEngine (as initialized) will likely be able to support the
767
- * given content type.
768
- *
769
- * @param {string} contentType
770
- * @return {boolean}
771
- */
772
- willSupport(contentType) {
773
- // Edge 14 does not report correct capabilities. It will only report the
774
- // first MIME type even if the others are supported. To work around this,
775
- // we say that Edge supports everything.
776
- //
777
- // See https://github.com/shaka-project/shaka-player/issues/1495 for details.
778
- if (shaka.util.Platform.isLegacyEdge()) {
779
- return true;
780
- }
781
-
782
- contentType = contentType.toLowerCase();
783
-
784
- if (shaka.util.Platform.isTizen() &&
785
- contentType.includes('codecs="ac-3"')) {
786
- // Some Tizen devices seem to misreport AC-3 support. This works around
787
- // the issue, by falling back to EC-3, which seems to be supported on the
788
- // same devices and be correctly reported in all cases we have observed.
789
- // See https://github.com/shaka-project/shaka-player/issues/2989 for
790
- // details.
791
- const fallback = contentType.replace('ac-3', 'ec-3');
792
- return this.supportedTypes_.has(contentType) ||
793
- this.supportedTypes_.has(fallback);
794
- }
795
-
796
- return this.supportedTypes_.has(contentType);
797
- }
798
-
799
770
  /**
800
771
  * Returns the ID of the sessions currently active.
801
772
  *
@@ -896,25 +867,6 @@ shaka.media.DrmEngine = class {
896
867
  return Array.from(this.activeSessions_.keys());
897
868
  }
898
869
 
899
-
900
- /**
901
- * @param {shaka.extern.Stream} stream
902
- * @param {string=} codecOverride
903
- * @return {string}
904
- * @private
905
- */
906
- static computeMimeType_(stream, codecOverride) {
907
- const realMimeType = shaka.util.MimeUtils.getFullType(stream.mimeType,
908
- codecOverride || stream.codecs);
909
- const TransmuxerEngine = shaka.transmuxer.TransmuxerEngine;
910
- if (TransmuxerEngine.isSupported(realMimeType, stream.type)) {
911
- // This will be handled by the Transmuxer, so use the MIME type that the
912
- // Transmuxer will produce.
913
- return TransmuxerEngine.convertCodecs(stream.type, realMimeType);
914
- }
915
- return realMimeType;
916
- }
917
-
918
870
  /**
919
871
  * @param {!Map.<string, MediaKeySystemConfiguration>} configsByKeySystem
920
872
  * A dictionary of configs, indexed by key system, with an iteration order
@@ -939,11 +891,6 @@ shaka.media.DrmEngine = class {
939
891
  this.destroyer_.ensureNotDestroyed();
940
892
 
941
893
  try {
942
- // Get the set of supported content types from the audio and video
943
- // capabilities. Avoid duplicates so that it is easier to read what is
944
- // supported.
945
- this.supportedTypes_.clear();
946
-
947
894
  // Store the capabilities of the key system.
948
895
  const realConfig = mediaKeySystemAccess.getConfiguration();
949
896
 
@@ -951,20 +898,6 @@ shaka.media.DrmEngine = class {
951
898
  'Got MediaKeySystemAccess with configuration',
952
899
  realConfig);
953
900
 
954
- const audioCaps = realConfig.audioCapabilities || [];
955
- const videoCaps = realConfig.videoCapabilities || [];
956
-
957
- for (const cap of audioCaps) {
958
- this.supportedTypes_.add(cap.contentType.toLowerCase());
959
- }
960
-
961
- for (const cap of videoCaps) {
962
- this.supportedTypes_.add(cap.contentType.toLowerCase());
963
- }
964
-
965
- goog.asserts.assert(this.supportedTypes_.size,
966
- 'We should get at least one supported MIME type');
967
-
968
901
  if (variants.length) {
969
902
  this.currentDrmInfo_ = this.createDrmInfoByInfos_(
970
903
  mediaKeySystemAccess.keySystem,
@@ -1021,7 +954,6 @@ shaka.media.DrmEngine = class {
1021
954
 
1022
955
  // Don't rewrap a shaka.util.Error from earlier in the chain:
1023
956
  this.currentDrmInfo_ = null;
1024
- this.supportedTypes_.clear();
1025
957
  if (exception instanceof shaka.util.Error) {
1026
958
  throw exception;
1027
959
  }
@@ -2063,41 +1995,6 @@ shaka.media.DrmEngine = class {
2063
1995
  }));
2064
1996
  }
2065
1997
 
2066
- /**
2067
- * Check if a variant is likely to be supported by DrmEngine. This will err on
2068
- * the side of being too accepting and may not reject a variant that it will
2069
- * later fail to play.
2070
- *
2071
- * @param {!shaka.extern.Variant} variant
2072
- * @return {boolean}
2073
- */
2074
- supportsVariant(variant) {
2075
- /** @type {?shaka.extern.Stream} */
2076
- const audio = variant.audio;
2077
- /** @type {?shaka.extern.Stream} */
2078
- const video = variant.video;
2079
-
2080
- if (audio && audio.encrypted) {
2081
- const audioContentType = shaka.media.DrmEngine.computeMimeType_(audio);
2082
- if (!this.willSupport(audioContentType)) {
2083
- return false;
2084
- }
2085
- }
2086
-
2087
- if (video && video.encrypted) {
2088
- const videoContentType = shaka.media.DrmEngine.computeMimeType_(video);
2089
- if (!this.willSupport(videoContentType)) {
2090
- return false;
2091
- }
2092
- }
2093
-
2094
- const keySystem = shaka.util.DrmUtils.keySystem(this.currentDrmInfo_);
2095
- const drmInfos = this.getVariantDrmInfos_(variant);
2096
-
2097
- return drmInfos.length == 0 ||
2098
- drmInfos.some((drmInfo) => drmInfo.keySystem == keySystem);
2099
- }
2100
-
2101
1998
  /**
2102
1999
  * Concat the audio and video drmInfos in a variant.
2103
2000
  * @param {shaka.extern.Variant} variant
@@ -116,7 +116,7 @@ shaka.media.MediaSourceEngine = class {
116
116
  /** @private {boolean} */
117
117
  this.playbackHasBegun_ = false;
118
118
 
119
- /** @private {(MediaSource|ManagedMediaSource)} */
119
+ /** @private {MediaSource} */
120
120
  this.mediaSource_ = this.createMediaSource(this.mediaSourceOpen_);
121
121
 
122
122
  /** @private {boolean} */
@@ -175,13 +175,16 @@ shaka.media.MediaSourceEngine = class {
175
175
  * Replaced by unit tests.
176
176
  *
177
177
  * @param {!shaka.util.PublicPromise} p
178
- * @return {!(MediaSource|ManagedMediaSource)}
178
+ * @return {!MediaSource}
179
179
  */
180
180
  createMediaSource(p) {
181
+ /** @type {!MediaSource} */
182
+ let mediaSource;
183
+
181
184
  if (window.ManagedMediaSource) {
182
185
  this.video_.disableRemotePlayback = true;
183
186
 
184
- const mediaSource = new ManagedMediaSource();
187
+ mediaSource = new ManagedMediaSource();
185
188
 
186
189
  this.eventManager_.listen(
187
190
  mediaSource, 'startstreaming', () => {
@@ -192,39 +195,25 @@ shaka.media.MediaSourceEngine = class {
192
195
  mediaSource, 'endstreaming', () => {
193
196
  this.streamingAllowed_ = false;
194
197
  });
195
-
196
- this.eventManager_.listenOnce(
197
- mediaSource, 'sourceopen', () => this.onSourceOpen_(p));
198
-
199
- // Correctly set when playback has begun.
200
- this.eventManager_.listenOnce(this.video_, 'playing', () => {
201
- this.playbackHasBegun_ = true;
202
- });
203
-
204
- this.url_ = shaka.media.MediaSourceEngine.createObjectURL(mediaSource);
205
-
206
- this.video_.src = this.url_;
207
-
208
- return mediaSource;
209
198
  } else {
210
- const mediaSource = new MediaSource();
199
+ mediaSource = new MediaSource();
200
+ }
211
201
 
212
- // Set up MediaSource on the video element.
213
- this.eventManager_.listenOnce(
214
- mediaSource, 'sourceopen', () => this.onSourceOpen_(p));
202
+ // Set up MediaSource on the video element.
203
+ this.eventManager_.listenOnce(
204
+ mediaSource, 'sourceopen', () => this.onSourceOpen_(p));
215
205
 
216
- // Correctly set when playback has begun.
217
- this.eventManager_.listenOnce(this.video_, 'playing', () => {
218
- this.playbackHasBegun_ = true;
219
- });
206
+ // Correctly set when playback has begun.
207
+ this.eventManager_.listenOnce(this.video_, 'playing', () => {
208
+ this.playbackHasBegun_ = true;
209
+ });
220
210
 
221
- // Store the object URL for releasing it later.
222
- this.url_ = shaka.media.MediaSourceEngine.createObjectURL(mediaSource);
211
+ // Store the object URL for releasing it later.
212
+ this.url_ = shaka.media.MediaSourceEngine.createObjectURL(mediaSource);
223
213
 
224
- this.video_.src = this.url_;
214
+ this.video_.src = this.url_;
225
215
 
226
- return mediaSource;
227
- }
216
+ return mediaSource;
228
217
  }
229
218
 
230
219
  /**
@@ -427,7 +427,9 @@ shaka.media.MediaSourcePlayhead = class {
427
427
  // infinite loop on systems where changing currentTime takes a
428
428
  // significant amount of time (e.g. Chromecast).
429
429
  const time = Date.now() / 1000;
430
- if (!this.lastCorrectiveSeek_ || this.lastCorrectiveSeek_ < time - 1) {
430
+ const seekDelay = shaka.util.Platform.isFuchsiaCastDevice() ? 3 : 1;
431
+ if (!this.lastCorrectiveSeek_ ||
432
+ this.lastCorrectiveSeek_ < time - seekDelay) {
431
433
  this.lastCorrectiveSeek_ = time;
432
434
  canCorrectiveSeek = true;
433
435
  }
package/lib/player.js CHANGED
@@ -2890,6 +2890,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2890
2890
  }
2891
2891
  }
2892
2892
  mediaElement.src = playbackUri;
2893
+ // If ManagedMediaSource exists, we've disabled remote playback on our own.
2894
+ // Reenable it so AirPlay can be used via RemotePlayback API.
2895
+ if (this.mediaSourceEngine_ && window.ManagedMediaSource) {
2896
+ mediaElement.disableRemotePlayback = false;
2897
+ }
2893
2898
 
2894
2899
  // Tizen 3 / WebOS won't load anything unless you call load() explicitly,
2895
2900
  // no matter the value of the preload attribute. This is harmful on some
@@ -7859,7 +7864,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
7859
7864
  * @export
7860
7865
  */
7861
7866
  // eslint-disable-next-line no-useless-concat, max-len
7862
- shaka.Player.version = 'v4.10.21' + '-uncompiled'; // x-release-please-version
7867
+ shaka.Player.version = 'v4.10.23' + '-uncompiled'; // x-release-please-version
7863
7868
 
7864
7869
  // Initialize the deprecation system using the version string we just set
7865
7870
  // on the player.
@@ -629,7 +629,7 @@ shaka.util.Platform = class {
629
629
  *
630
630
  * Some devices are known not to support `MediaSource.changeType`
631
631
  * well. These devices should use the reload strategy. If a device
632
- * reports that it supports `changeType` but support it reliabley
632
+ * reports that it supports `changeType` but support it unreliably
633
633
  * it should be added to this list.
634
634
  *
635
635
  * @return {boolean}
@@ -638,7 +638,8 @@ shaka.util.Platform = class {
638
638
  const Platform = shaka.util.Platform;
639
639
  if (Platform.isTizen2() || Platform.isTizen3() || Platform.isTizen4() ||
640
640
  Platform.isTizen5() || Platform.isTizen6() || Platform.isWebOS3() ||
641
- Platform.isWebOS4() || Platform.isWebOS5()) {
641
+ Platform.isWebOS4() || Platform.isWebOS5() || Platform.isPS4() ||
642
+ Platform.isPS5()) {
642
643
  return false;
643
644
  }
644
645
  // Older chromecasts without GoogleTV seem to not support SMOOTH properly.
@@ -11,6 +11,7 @@ goog.require('shaka.abr.SimpleAbrManager');
11
11
  goog.require('shaka.config.AutoShowText');
12
12
  goog.require('shaka.config.CodecSwitchingStrategy');
13
13
  goog.require('shaka.log');
14
+ goog.require('shaka.media.Capabilities');
14
15
  goog.require('shaka.net.NetworkingEngine');
15
16
  goog.require('shaka.util.ConfigUtils');
16
17
  goog.require('shaka.util.FairPlayUtils');
@@ -103,7 +104,8 @@ shaka.util.PlayerConfiguration = class {
103
104
 
104
105
  let codecSwitchingStrategy = shaka.config.CodecSwitchingStrategy.RELOAD;
105
106
  let multiTypeVariantsAllowed = false;
106
- if (shaka.util.Platform.supportsSmoothCodecSwitching()) {
107
+ if (shaka.media.Capabilities.isChangeTypeSupported() &&
108
+ shaka.util.Platform.supportsSmoothCodecSwitching()) {
107
109
  codecSwitchingStrategy = shaka.config.CodecSwitchingStrategy.SMOOTH;
108
110
  multiTypeVariantsAllowed = true;
109
111
  }
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.10.21",
4
+ "version": "4.10.23",
5
5
  "homepage": "https://github.com/shaka-project/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
package/ui/externs/ui.js CHANGED
@@ -229,7 +229,8 @@ shaka.extern.UIVolumeBarColors;
229
229
  * @property {boolean} seekOnTaps
230
230
  * Indicates whether or not a fast-forward and rewind tap button that seeks
231
231
  * video some seconds.
232
- * Defaults to true.
232
+ * Defaults to true if the browser <code>navigator.maxTouchPoints > 0</code>
233
+ * is true.
233
234
  * @property {number} tapSeekDistance
234
235
  * The time interval, in seconds, to seek when the user presses the left or
235
236
  * right part of the video. If less than or equal to 0,
@@ -72,7 +72,7 @@ shaka.ui.RemoteButton = class extends shaka.ui.Element {
72
72
 
73
73
  shaka.ui.Utils.setDisplay(this.remoteButton_, false);
74
74
 
75
- if (!this.video.remote || this.video.disableRemotePlayback) {
75
+ if (!this.video.remote) {
76
76
  this.remoteButton_.classList.add('shaka-hidden');
77
77
  } else {
78
78
  this.eventManager.listen(
@@ -138,10 +138,11 @@ shaka.ui.RemoteButton = class extends shaka.ui.Element {
138
138
  } else if (this.video.remote.state == 'disconnected') {
139
139
  const handleAvailabilityChange = (availability) => {
140
140
  if (this.player) {
141
+ const disableRemote = this.video.disableRemotePlayback;
141
142
  const loadMode = this.player.getLoadMode();
142
143
  const srcMode = loadMode == shaka.Player.LoadMode.SRC_EQUALS;
143
144
  shaka.ui.Utils.setDisplay(
144
- this.remoteButton_, srcMode && availability);
145
+ this.remoteButton_, srcMode && availability && !disableRemote);
145
146
  } else {
146
147
  shaka.ui.Utils.setDisplay(this.remoteButton_, false);
147
148
  }
@@ -159,6 +159,9 @@ shaka.ui.SaveVideoFrameButton = class extends shaka.ui.Element {
159
159
  if (this.ad) {
160
160
  available = false;
161
161
  }
162
+ if (this.video.remote && this.video.remote.state != 'disconnected') {
163
+ available = false;
164
+ }
162
165
  shaka.ui.Utils.setDisplay(this.button_, available);
163
166
  }
164
167
 
package/ui/ui.js CHANGED
@@ -192,7 +192,6 @@ shaka.ui.Overlay = class {
192
192
  'playback_rate',
193
193
  'recenter_vr',
194
194
  'toggle_stereoscopic',
195
- 'save_video_frame',
196
195
  ],
197
196
  statisticsList: [
198
197
  'width',
@@ -267,7 +266,7 @@ shaka.ui.Overlay = class {
267
266
  fullScreenElement: this.videoContainer_,
268
267
  preferDocumentPictureInPicture: true,
269
268
  showAudioChannelCountVariants: true,
270
- seekOnTaps: true,
269
+ seekOnTaps: navigator.maxTouchPoints > 0,
271
270
  tapSeekDistance: 10,
272
271
  refreshTickInSeconds: 0.125,
273
272
  displayInVrMode: false,
@@ -290,6 +289,9 @@ shaka.ui.Overlay = class {
290
289
  (name) => name != 'play_pause' && name != 'volume');
291
290
  }
292
291
 
292
+ // Set this button here to push it at the end.
293
+ config.overflowMenuButtons.push('save_video_frame');
294
+
293
295
  return config;
294
296
  }
295
297
 
@@ -491,6 +493,8 @@ shaka.ui.Overlay = class {
491
493
  video.removeAttribute('src');
492
494
  }
493
495
 
496
+ await player.attach(shaka.util.Dom.asHTMLMediaElement(video));
497
+
494
498
  for (const elem of video.querySelectorAll('source')) {
495
499
  try { // eslint-disable-next-line no-await-in-loop
496
500
  await ui.getControls().getPlayer().load(elem.getAttribute('src'));
@@ -499,8 +503,6 @@ shaka.ui.Overlay = class {
499
503
  shaka.log.error('Error auto-loading asset', e);
500
504
  }
501
505
  }
502
-
503
- await player.attach(shaka.util.Dom.asHTMLMediaElement(video));
504
506
  }
505
507
 
506
508