@eluvio/elv-player-js 1.0.136 → 1.0.138

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.
package/README.md CHANGED
@@ -96,6 +96,7 @@ The library includes a helpful collection of configuration options in `EluvioPla
96
96
  ### Player Options
97
97
 
98
98
  ```javascript
99
+ // All player options and their defaults
99
100
  playerOptions: {
100
101
  controls: EluvioPlayerParameters.controls.AUTO_HIDE,
101
102
  autoplay: EluvioPlayerParameters.autoplay.OFF,
@@ -103,11 +104,13 @@ The library includes a helpful collection of configuration options in `EluvioPla
103
104
  loop: EluvioPlayerParameters.loop.OFF,
104
105
  watermark: EluvioPlayerParameters.watermark.ON,
105
106
  capLevelToPlayerSize: EluvioPlayerParameters.capLevelToPlayerSize.OFF,
107
+ collectVideoAnalytics: EluvioPlayerParameters.collectVideoAnalytics.ON,
106
108
  posterUrl: undefined,
107
109
  className: undefined,
108
110
  controlsClassName: undefined,
109
111
  hlsjsOptions: undefined,
110
112
  dashjsOptions: undefined,
113
+ maxBitrate: undefined,
111
114
  playerCallback: ({player, videoElement, hlsPlayer, dashPlayer, posterUrl}) => {},
112
115
  errorCallback: (error, player) => {},
113
116
  restartCallback: async (error) => {}
@@ -115,30 +118,35 @@ The library includes a helpful collection of configuration options in `EluvioPla
115
118
  ```
116
119
 
117
120
  ##### Values
118
- * `controls` - How the controls should be displayed. Default AUTOHIDE
121
+ * `controls` - How the controls should be displayed
122
+ * `AUTOHIDE (default)`: Player controls will be shown. Will automatically hide when not in use
119
123
  * `ON`: Player controls will be shown
120
- * `AUTOHIDE`: Player controls will be shown. Will automatically hide when not in use
121
124
  * `DEFAULT`: Default HTML video controls will be shown
122
125
  * `OFF`: No controls will be shown
123
126
  * `OFF_WITH_VOLUME_TOGGLE`: No controls will be shown except a volume on/off toggle
124
- * `autoplay` - Whether or not the video should autoplay. Default OFF. NOTE: Browsers may block autoplay video with audio
127
+ * `autoplay` - Whether or not the video should autoplay. NOTE: Browsers may block autoplay video with audio
128
+ * `OFF (default)`: Video will not autoplay
125
129
  * `ON`: Video will autoplay
126
- * `OFF`: Video will not autoplay
127
130
  * `WHEN_VISIBLE`: Video will autoplay only when the video element is visible, and will stop when the element is no longer visible
128
- * `muted` - Whether or not the video will be muted. Default OFF
131
+ * `muted` - Whether or not the video will be muted.
132
+ * `OFF (default)`: Video will not be muted
129
133
  * `ON`: Video will be muted
130
- * `OFF`: Video will not be muted
131
134
  * `WHEN_NOT_VISIBLE`: Video will be muted when the video element is not visible
132
135
  * `OFF_IF_POSSIBLE`: Video will not be muted unless playback is blocked due to audio (useful for autoplay)
133
- * `loop` - Whether or not the video will loop. Default OFF
136
+ * `loop` - Whether or not the video will loop.
137
+ * `OFF (default)` - Video will not loop
134
138
  * `ON` - Video will loop
135
- * `OFF` - Video will not loop
136
- * `watermark`: Whether or not the Eluvio watermark will be shown. Default ON
137
- * `ON` - Watermark will be shown
139
+ * `watermark`: Whether or not the Eluvio watermark will be shown.
140
+ * `ON (default)` - Watermark will be shown
138
141
  * `OFF` - Watermark will not be shown
139
- * `capLevelToPlayerSize`: Whether or not the playback quality should be limited by the size of the video element. Useful for reducing bandwidth usage for smaller video elements where a higher quality would not be beneficial. Default OFF
142
+ * `capLevelToPlayerSize`: Whether or not the playback quality should be limited by the size of the video element. Useful for reducing bandwidth usage for smaller video elements where a higher quality would not be beneficial.
143
+ * `OFF (default)` - Playback quality will not be affected by the size of the video element (default)
140
144
  * `ON` - Playback quality will be limited by the size of the video element
141
- * `OFF` - Playback quality will not be affected by the size of the video element
145
+ * `collectVideoAnalytics` - By default, the player will collect anonymized playback analytics to help improve the performance of the Eluvio Content Fabric.
146
+ * `ON (default)` - Player performance analytics will be collected
147
+ * `DISABLE_COOKIES`- Player performance analytics will be collected, but browser cookies will not be used
148
+ * `OFF` - Player performance analytics will not be collected
149
+ * `maxBitrate` - Maximum bitrate that the player will automatically use, in bits/second.
142
150
  * `posterUrl` - Specify a URL for the poster image for the player
143
151
  * `className` - HTML class to be added to the player
144
152
  * `controlsClassName` - HTML class to be added to the player controls container
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eluvio/elv-player-js",
3
- "version": "1.0.136",
3
+ "version": "1.0.138",
4
4
  "description": "![Eluvio Logo](src/static/images/Logo.png \"Eluvio Logo\")",
5
5
  "main": "src/index.js",
6
6
  "license": "MIT",
@@ -9,7 +9,7 @@
9
9
  "bump-version": "npm --git-tag-version --no-commit-hooks version patch",
10
10
  "serve": "TEST_PAGE=true webpack-dev-server --hot --port 8089 --host=0.0.0.0",
11
11
  "serve-example": "EXAMPLE_PAGE=true webpack-dev-server --hot --port 8089 --host=0.0.0.0",
12
- "build": "node ./node_modules/webpack-cli/bin/cli.js --mode=production --devtool false",
12
+ "build": "BUILD_LIBRARY=true node ./node_modules/webpack-cli/bin/cli.js --mode=production --devtool false",
13
13
  "build-test": "TEST_PAGE=true node ./node_modules/webpack-cli/bin/cli.js --mode=production --devtool false",
14
14
  "build-example": "EXAMPLE_PAGE=true node ./node_modules/webpack-cli/bin/cli.js --mode=production --devtool false",
15
15
  "build-analyze": "ANALYZE_BUNDLE=true node ./node_modules/webpack-cli/bin/cli.js --mode=production --devtool false",
@@ -36,11 +36,12 @@
36
36
  "webpack.config.js"
37
37
  ],
38
38
  "dependencies": {
39
- "@eluvio/elv-client-js": "^4.0.62",
39
+ "@eluvio/elv-client-js": "^4.0.76",
40
40
  "dashjs": "~4.7.0",
41
41
  "focus-visible": "^5.2.0",
42
42
  "hls.js": "~1.4.12",
43
43
  "lodash": "^4.17.21",
44
+ "mux-embed": "^4.30.0",
44
45
  "resize-observer-polyfill": "^1.5.1",
45
46
  "url-join": "^4.0.1"
46
47
  },
@@ -0,0 +1,114 @@
1
+ import Mux from "mux-embed";
2
+ const {version} = require("../package.json");
3
+
4
+ export const InitializeMuxMonitoring = async ({
5
+ appName="elv-player-js",
6
+ elvPlayer,
7
+ playoutUrl,
8
+ authorizationToken,
9
+ disableCookies
10
+ }) => {
11
+ playoutUrl = new URL(playoutUrl);
12
+
13
+ const client = await elvPlayer.Client();
14
+ const muxKey = (await client.NetworkInfo()).name === "main" ?
15
+ "aq4mdjn7qo5sbkf89pkvfv93j" :
16
+ "2i5480sms8vdgj0sv9bv6lpk5";
17
+
18
+ const versionHash = playoutUrl.pathname.split("/").find(token => token.startsWith("hq__"));
19
+ const objectId = client.utils.DecodeVersionHash(versionHash).objectId;
20
+ const offering = playoutUrl.toString().match(/\/rep\/playout\/([^/]+)/)[1] || "default";
21
+ const sessionId = playoutUrl.searchParams.get("sid");
22
+
23
+ let name = versionHash;
24
+ try {
25
+ const metadata = (await client.ContentObjectMetadata({
26
+ versionHash,
27
+ metadataSubtree: "/public",
28
+ select: [
29
+ "name",
30
+ "asset_metadata/display_title",
31
+ "asset_metadata/title"
32
+ ],
33
+ authorizationToken
34
+ })) || {};
35
+
36
+ name =
37
+ (metadata.asset_metadata || {}).display_title ||
38
+ (metadata.asset_metadata || {}).title ||
39
+ metadata.name || versionHash;
40
+ // eslint-disable-next-line no-empty
41
+ } catch (error) {}
42
+
43
+ let tenantId = undefined;
44
+ try {
45
+ tenantId = await client.ContentObjectTenantId({versionHash});
46
+ // eslint-disable-next-line no-empty
47
+ } catch (error) {}
48
+
49
+ let address = await client.CurrentAccountAddress();
50
+ if(authorizationToken || client.staticToken) {
51
+ try {
52
+ const {payload} = client.utils.DecodeSignedToken(authorizationToken);
53
+ address = payload.adr || address;
54
+ // eslint-disable-next-line no-empty
55
+ } catch (error) {}
56
+ }
57
+
58
+ let addressDigest = address;
59
+ if(typeof crypto !== "undefined") {
60
+ try {
61
+ const encoder = new TextEncoder();
62
+ addressDigest = Buffer.from(
63
+ await crypto.subtle.digest("SHA-256", encoder.encode(address))
64
+ ).toString("hex");
65
+ // eslint-disable-next-line no-empty
66
+ } catch (error) {}
67
+ }
68
+
69
+ const options = {
70
+ debug: false,
71
+ disableCookies,
72
+ data: {
73
+ env_key: muxKey,
74
+ video_id: objectId,
75
+ video_variant_id: versionHash,
76
+ video_variant_name: offering,
77
+ video_title: name,
78
+ video_cdn: playoutUrl.hostname,
79
+ viewer_user_id: addressDigest,
80
+ sub_property_id: tenantId,
81
+ player_name: appName,
82
+ player_version: version,
83
+ player_init_time: elvPlayer.initTime
84
+ }
85
+ };
86
+
87
+ if(sessionId) {
88
+ options.data.view_session_id = sessionId;
89
+ }
90
+
91
+ if(elvPlayer.player) {
92
+ if(elvPlayer.HLS) {
93
+ options.hlsjs = elvPlayer.player;
94
+ options.Hls = elvPlayer.HLS;
95
+ } else if(elvPlayer.Dash) {
96
+ options.dashjs = elvPlayer.player;
97
+ }
98
+ }
99
+ try {
100
+ Mux.monitor(elvPlayer.video, options);
101
+
102
+ // eslint-disable-next-line no-console
103
+ console.info("elv-player-js: Mux monitoring initialized");
104
+ // eslint-disable-next-line no-console
105
+ console.info(JSON.stringify({...options, hlsjs: {}}));
106
+ } catch (error) {
107
+ // eslint-disable-next-line no-console
108
+ console.warn("elv-player-js: Failed to initialize mux monitoring:");
109
+ // eslint-disable-next-line no-console
110
+ console.warn(JSON.stringify(options, null, 2));
111
+ // eslint-disable-next-line no-console
112
+ console.warn(error);
113
+ }
114
+ };
@@ -10,7 +10,10 @@ import {
10
10
  VolumeLowIcon,
11
11
  VolumeHighIcon,
12
12
  MultiViewIcon,
13
- LeftArrowIcon
13
+ LeftArrowIcon,
14
+ PreviousTrackIcon,
15
+ NextTrackIcon,
16
+ CollectionIcon
14
17
  } from "./static/icons/Icons";
15
18
  // Icons are generated from .svg files to an importable JS file. To add a new icon, modify and run src/BuildIcons.js
16
19
 
@@ -123,7 +126,23 @@ const Time = (time, total) => {
123
126
  return string;
124
127
  };
125
128
 
126
- export const InitializeTicketPrompt = (target, callback) => {
129
+ export const InitializeTicketPrompt = async (target, initialCode, callback) => {
130
+ // If initial code is provided, attempt to automatically redeem it before rendering the form
131
+ let initialError = "";
132
+ if(initialCode) {
133
+ try {
134
+ await callback(initialCode);
135
+ return;
136
+ } catch (error) {
137
+ // eslint-disable-next-line no-console
138
+ console.error("ELUVIO PLAYER: Invalid Code");
139
+ // eslint-disable-next-line no-console
140
+ console.error(error);
141
+
142
+ initialError = "Invalid Code";
143
+ }
144
+ }
145
+
127
146
  const ticketModal = CreateElement({
128
147
  parent: target,
129
148
  type: "div",
@@ -144,6 +163,8 @@ export const InitializeTicketPrompt = (target, callback) => {
144
163
  classes: ["eluvio-player__ticket-modal__form__error-text", "eluvio-player__ticket-modal__form__text"]
145
164
  });
146
165
 
166
+ errorMessage.innerHTML = initialError;
167
+
147
168
  const text = CreateElement({
148
169
  parent: form,
149
170
  type: "div",
@@ -158,6 +179,8 @@ export const InitializeTicketPrompt = (target, callback) => {
158
179
  classes: ["eluvio-player__ticket-modal__form__input"]
159
180
  });
160
181
 
182
+ input.value = initialCode;
183
+
161
184
  const submit = CreateElement({
162
185
  parent: form,
163
186
  type: "button",
@@ -188,7 +211,8 @@ export const InitializeTicketPrompt = (target, callback) => {
188
211
  };
189
212
 
190
213
  class PlayerControls {
191
- constructor({target, video, playerOptions, posterUrl, className}) {
214
+ constructor({player, target, video, playerOptions, posterUrl, className}) {
215
+ this.player = player;
192
216
  this.target = target;
193
217
  this.video = video;
194
218
  this.playerOptions = playerOptions;
@@ -200,6 +224,8 @@ class PlayerControls {
200
224
  this.SetPosterUrl(posterUrl);
201
225
  }
202
226
 
227
+ this.HandleClickOutsideMenu = this.HandleClickOutsideMenu.bind(this);
228
+
203
229
  this.InitializeControls(className);
204
230
  }
205
231
 
@@ -267,7 +293,7 @@ class PlayerControls {
267
293
  }
268
294
  }
269
295
 
270
- AutohideControls(controls) {
296
+ AutohideControls({controls, titleOnly=false}) {
271
297
  this.video.addEventListener("play", () => {
272
298
  this.played = true;
273
299
  });
@@ -289,14 +315,14 @@ class PlayerControls {
289
315
  const PlayerMove = () => {
290
316
  this.FadeIn({
291
317
  key: "controls",
292
- elements: [controls, this.settingsMenu, this.toolTip],
318
+ elements: titleOnly ? [this.titleContainer] : [controls, this.settingsMenu, this.toolTip, this.titleContainer],
293
319
  callback: () => {
294
320
  this.target.classList.remove("-elv-no-cursor");
295
321
  }
296
322
  });
297
323
  this.FadeOut({
298
324
  key: "controls",
299
- elements: [controls, this.settingsMenu, this.toolTip],
325
+ elements: titleOnly ? [this.titleContainer] : [controls, this.settingsMenu, this.toolTip, this.titleContainer],
300
326
  delay: 3000,
301
327
  unless: () => ControlsShouldShow(),
302
328
  callback: () => {
@@ -350,7 +376,40 @@ class PlayerControls {
350
376
  this.accountWatermark.innerText = address;
351
377
  }
352
378
 
379
+ InitializeContentTitle({title, description}) {
380
+ if(!title && !description) { return; }
381
+
382
+ this.titleContainer = CreateElement({
383
+ parent: this.target,
384
+ type: "div",
385
+ classes: ["eluvio-player__title-container"],
386
+ prepend: true
387
+ });
388
+
389
+ if(title) {
390
+ const titleElement = CreateElement({
391
+ parent: this.titleContainer,
392
+ type: "div",
393
+ classes: ["eluvio-player__title"]
394
+ });
395
+
396
+ titleElement.innerHTML = title;
397
+ }
398
+
399
+ if(description) {
400
+ const descriptionElement = CreateElement({
401
+ parent: this.titleContainer,
402
+ type: "div",
403
+ classes: ["eluvio-player__description"]
404
+ });
405
+
406
+ descriptionElement.innerHTML = description;
407
+ }
408
+ }
409
+
353
410
  InitializeControls(className="") {
411
+ const collectionInfo = this.player.collectionInfo;
412
+
354
413
  this.target.setAttribute("tabindex", "0");
355
414
 
356
415
  if(this.playerOptions.watermark) {
@@ -434,7 +493,7 @@ class PlayerControls {
434
493
  volumeButton.innerHTML = this.video.muted || this.video.volume === 0 ? MutedIcon : (this.video.volume < 0.5 ? VolumeLowIcon : VolumeHighIcon);
435
494
  });
436
495
 
437
- this.AutohideControls(controls);
496
+ this.AutohideControls({controls, titleOnly: false});
438
497
  };
439
498
 
440
499
  const HasAudio = () => (this.video.mozHasAudio || Boolean(this.video.webkitAudioDecodedByteCount) || Boolean(this.video.audioTracks && this.video.audioTracks.length));
@@ -559,6 +618,21 @@ class PlayerControls {
559
618
  volumeBar.value = volumeSlider.value;
560
619
  });
561
620
 
621
+ // Collection previous track
622
+ if(collectionInfo && collectionInfo.isPlaylist) {
623
+ const collectionPreviousButton = CreateImageButton({
624
+ parent: controls,
625
+ svg: PreviousTrackIcon,
626
+ classes: ["eluvio-player__controls__previous-track"],
627
+ label: "Previous Track",
628
+ options: {
629
+ disabled: collectionInfo.mediaIndex === 0
630
+ }
631
+ });
632
+
633
+ collectionPreviousButton.addEventListener("click", () => this.player.CollectionPlayPrevious());
634
+ }
635
+
562
636
  const progressTime = CreateElement({
563
637
  parent: controls,
564
638
  type: "div",
@@ -644,6 +718,36 @@ class PlayerControls {
644
718
 
645
719
  totalTime.innerHTML = "00:00";
646
720
 
721
+ // Collection previous track
722
+ if(collectionInfo && collectionInfo.isPlaylist) {
723
+ const collectionNextButton = CreateImageButton({
724
+ parent: controls,
725
+ svg: NextTrackIcon,
726
+ classes: ["eluvio-player__controls__next-track"],
727
+ label: "Next Track",
728
+ options: {
729
+ disabled: collectionInfo.mediaIndex >= collectionInfo.mediaLength - 1
730
+ }
731
+ });
732
+
733
+ collectionNextButton.addEventListener("click", () => this.player.CollectionPlayNext());
734
+ }
735
+
736
+ if(collectionInfo) {
737
+ this.collectionButton = CreateImageButton({
738
+ parent: controls,
739
+ svg: CollectionIcon,
740
+ classes: ["eluvio-player__controls__collection"],
741
+ label: "Collection Info"
742
+ });
743
+
744
+ this.collectionButton.addEventListener("click", () => {
745
+ this.settingsMenu.dataset.mode === "collection" ?
746
+ this.HideSettingsMenu() :
747
+ this.ShowCollectionMenu();
748
+ });
749
+ }
750
+
647
751
  // Right buttons container
648
752
  this.rightButtonsContainer = CreateElement({
649
753
  parent: controls,
@@ -651,6 +755,20 @@ class PlayerControls {
651
755
  classes: ["eluvio-player__controls__right-buttons"]
652
756
  });
653
757
 
758
+ this.settingsButton = CreateImageButton({
759
+ parent: this.rightButtonsContainer,
760
+ svg: SettingsIcon,
761
+ classes: ["eluvio-player__controls__button-settings"],
762
+ prepend: true,
763
+ label: "Settings"
764
+ });
765
+
766
+ this.settingsButton.addEventListener("click", () => {
767
+ this.settingsMenu.dataset.mode.startsWith("settings") ?
768
+ this.HideSettingsMenu() :
769
+ this.ShowSettingsMenu();
770
+ });
771
+
654
772
  // Fullscreen
655
773
  const fullscreenButton = CreateImageButton({
656
774
  parent: this.rightButtonsContainer,
@@ -805,9 +923,7 @@ class PlayerControls {
805
923
  }
806
924
  });
807
925
 
808
- if(this.playerOptions.controls === EluvioPlayerParameters.controls.AUTO_HIDE) {
809
- this.AutohideControls(controls);
810
- }
926
+ this.AutohideControls({controls, titleOnly: this.playerOptions.controls !== EluvioPlayerParameters.controls.AUTO_HIDE});
811
927
  }
812
928
 
813
929
  ShowHLSOptionsForm({hlsOptions={}, SetPlayerProfile, hlsVersion}) {
@@ -935,7 +1051,14 @@ class PlayerControls {
935
1051
  this.hlsOptionsFormContainer && this.hlsOptionsFormContainer.remove();
936
1052
  }
937
1053
 
1054
+ HandleClickOutsideMenu(event) {
1055
+ if(!this.settingsMenu.contains(event.target)) {
1056
+ this.HideSettingsMenu();
1057
+ }
1058
+ }
1059
+
938
1060
  InitializeMenu(mode) {
1061
+ this.HideSettingsMenu();
939
1062
  this.settingsMenu.innerHTML = "";
940
1063
  this.settingsMenu.classList.remove("eluvio-player__controls__settings-menu-hidden");
941
1064
  this.settingsMenu.setAttribute("data-mode", mode);
@@ -949,6 +1072,16 @@ class PlayerControls {
949
1072
  });
950
1073
 
951
1074
  closeButton.addEventListener("click", () => this.HideSettingsMenu());
1075
+
1076
+ setTimeout(() => {
1077
+ document.addEventListener("click", this.HandleClickOutsideMenu);
1078
+ }, 100);
1079
+
1080
+ if(mode === "collection") {
1081
+ this.collectionButton.classList.add("eluvio-player__controls__button--active");
1082
+ } else if(mode.includes("setting")) {
1083
+ this.settingsButton.classList.add("eluvio-player__controls__button--active");
1084
+ }
952
1085
  }
953
1086
 
954
1087
  AddSetting({Retrieve, Set}) {
@@ -1044,37 +1177,73 @@ class PlayerControls {
1044
1177
  }
1045
1178
 
1046
1179
  HideSettingsMenu() {
1180
+ document.removeEventListener("click", this.HandleClickOutsideMenu);
1181
+
1047
1182
  const mode = this.settingsMenu.dataset.mode;
1048
1183
  if(mode === "settings") {
1049
1184
  this.settingsButton.focus();
1050
1185
  } else if(mode === "multiview") {
1051
1186
  this.multiviewButton.focus();
1187
+ } else if(mode === "collection") {
1188
+ this.collectionButton.focus();
1052
1189
  }
1053
1190
 
1054
1191
  this.settingsMenu.innerHTML = "";
1055
1192
  this.settingsMenu.classList.add("eluvio-player__controls__settings-menu-hidden");
1056
1193
  this.settingsMenu.setAttribute("data-mode", "hidden");
1194
+
1195
+ this.settingsButton.classList.remove("eluvio-player__controls__button--active");
1196
+ this.collectionButton && this.collectionButton.classList.remove("eluvio-player__controls__button--active");
1197
+ this.multiviewButton && this.multiviewButton.classList.remove("eluvio-player__controls__button--active");
1057
1198
  }
1058
1199
 
1200
+ // Settings were updated - if the menu is already open, force it to refresh
1059
1201
  UpdateSettings() {
1060
- if(!this.settingsButton) {
1061
- this.settingsButton = CreateImageButton({
1062
- parent: this.rightButtonsContainer,
1063
- svg: SettingsIcon,
1064
- classes: ["eluvio-player__controls__button-settings"],
1065
- prepend: true,
1066
- label: "Settings"
1067
- });
1202
+ if(this.settingsMenu.dataset.mode === "settings") {
1203
+ this.ShowSettingsMenu();
1204
+ }
1205
+ }
1068
1206
 
1069
- this.settingsButton.addEventListener("click", () => {
1070
- this.settingsMenu.dataset.mode === "hidden" ?
1071
- this.ShowSettingsMenu() :
1207
+ ShowCollectionMenu() {
1208
+ if(
1209
+ !this.player.collectionInfo ||
1210
+ !this.player.collectionInfo.content ||
1211
+ this.player.collectionInfo.content.length <= 1
1212
+ ) {
1213
+ return;
1214
+ }
1215
+
1216
+ this.InitializeMenu("collection");
1217
+
1218
+ const collectionTitle = CreateElement({
1219
+ parent: this.settingsMenu,
1220
+ type: "div",
1221
+ classes: ["eluvio-player__controls__settings-menu__title"]
1222
+ });
1223
+
1224
+ collectionTitle.innerHTML = this.player.collectionInfo.title;
1225
+
1226
+ this.player.collectionInfo.content
1227
+ .forEach((option, index) => {
1228
+ const active = this.player.collectionInfo.mediaIndex === index;
1229
+ const optionButton = CreateElement({
1230
+ parent: this.settingsMenu,
1231
+ type: "button",
1232
+ classes: ["eluvio-player__controls__settings-menu__option", active ? "eluvio-player__controls__settings-menu__option-selected" : ""]
1233
+ });
1234
+
1235
+ optionButton.innerHTML = option.title || option.mediaHash;
1236
+
1237
+ optionButton.addEventListener("click", () => {
1238
+ this.player.CollectionPlay({mediaIndex: index});
1072
1239
  this.HideSettingsMenu();
1240
+ });
1073
1241
  });
1074
- }
1075
1242
 
1076
- if(this.settingsMenu.dataset.mode === "settings") {
1077
- this.ShowSettingsMenu();
1243
+ // Focus on first element in list when menu opened
1244
+ const firstItem = this.settingsMenu.querySelector("button");
1245
+ if(firstItem) {
1246
+ firstItem.focus();
1078
1247
  }
1079
1248
  }
1080
1249
 
@@ -1174,6 +1343,8 @@ class PlayerControls {
1174
1343
  return;
1175
1344
  }
1176
1345
 
1346
+ this.multiviewButton.classList.add("eluvio-player__controls__button--active");
1347
+
1177
1348
  this.settingsMenu.setAttribute("data-mode", "multiview");
1178
1349
  this.settingsMenu.classList.remove("eluvio-player__controls__settings-menu-hidden");
1179
1350