@eluvio/elv-player-js 1.0.139 → 2.0.0

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 (98) hide show
  1. package/README.md +35 -6
  2. package/dist/.vite/manifest.json +17 -17
  3. package/dist/{Analytics-cQC_NR8f.mjs → Analytics-MzZmvYgy.mjs} +1 -1
  4. package/dist/{Analytics-z6nAtuJx.js → Analytics-jM8HcyUa.js} +1 -1
  5. package/dist/{dash.all.min-Uvqi9PBX.js → dash.all.min-16Sl6Y0h.js} +1 -1
  6. package/dist/{dash.all.min-JeIXEd1s.mjs → dash.all.min-2ST8aEXP.mjs} +1 -1
  7. package/dist/elv-player-js.cjs.js +1 -1
  8. package/dist/elv-player-js.css +1 -1
  9. package/dist/elv-player-js.es.js +1 -1
  10. package/dist/{index-RKrb2ZFL.js → index-BThzGsbn.js} +1 -1
  11. package/dist/index-Cw8L2-NE.js +367 -0
  12. package/dist/{index-FpQhGSc8.mjs → index-herSXPMN.mjs} +1 -1
  13. package/dist/{index-FmpRD8ov.mjs → index-mO9GR6Op.mjs} +25278 -24415
  14. package/lib/index.js +7 -0
  15. package/{src → lib/player}/Analytics.js +9 -8
  16. package/lib/player/Controls.js +912 -0
  17. package/{src → lib/player}/FairPlay.js +2 -0
  18. package/lib/player/Player.js +881 -0
  19. package/lib/player/PlayerParameters.js +173 -0
  20. package/lib/static/icons/Icons.js +29 -0
  21. package/lib/static/icons/svgs/backward-circle.svg +5 -0
  22. package/lib/static/icons/svgs/backward.svg +4 -0
  23. package/lib/static/icons/svgs/captions-off.svg +7 -0
  24. package/lib/static/icons/svgs/captions.svg +6 -0
  25. package/lib/static/icons/svgs/check.svg +1 -0
  26. package/lib/static/icons/svgs/chevron-left.svg +1 -0
  27. package/lib/static/icons/svgs/chevron-right.svg +1 -0
  28. package/lib/static/icons/svgs/forward-circle.svg +5 -0
  29. package/lib/static/icons/svgs/forward.svg +4 -0
  30. package/{src/static/icons/media/Full Screen icon.svg → lib/static/icons/svgs/full-screen.svg} +1 -1
  31. package/lib/static/icons/svgs/large-play-circle.svg +4 -0
  32. package/lib/static/icons/svgs/list.svg +1 -0
  33. package/{src/static/icons → lib/static/icons/svgs}/minimize.svg +1 -1
  34. package/{src/static/icons/media/Pause icon.svg → lib/static/icons/svgs/pause-circle.svg} +3 -3
  35. package/lib/static/icons/svgs/pause.svg +1 -0
  36. package/{src/static/icons/media/Play icon.svg → lib/static/icons/svgs/play-circle.svg} +1 -1
  37. package/lib/static/icons/svgs/play.svg +1 -0
  38. package/lib/static/icons/svgs/rotate-cw.svg +1 -0
  39. package/lib/static/icons/svgs/settings.svg +11 -0
  40. package/{src/static/icons/media/skip back icon.svg → lib/static/icons/svgs/skip-backward.svg} +2 -3
  41. package/{src/static/icons/media/Skip forward icon.svg → lib/static/icons/svgs/skip-forward.svg} +2 -3
  42. package/{src/static/icons/media/Volume icon.svg → lib/static/icons/svgs/volume-high.svg} +3 -3
  43. package/lib/static/icons/svgs/volume-low.svg +10 -0
  44. package/{src/static/icons/media/low volume icon.svg → lib/static/icons/svgs/volume-medium.svg} +2 -2
  45. package/{src/static/icons/media/no volume icon.svg → lib/static/icons/svgs/volume-off.svg} +3 -3
  46. package/lib/static/stylesheets/common.module.scss +486 -0
  47. package/lib/static/stylesheets/controls-tv.module.scss +488 -0
  48. package/lib/static/stylesheets/controls-web.module.scss +422 -0
  49. package/lib/static/stylesheets/player-profile-form.module.scss +141 -0
  50. package/lib/static/stylesheets/player.module.scss +92 -0
  51. package/lib/static/stylesheets/reset.module.scss +79 -0
  52. package/lib/static/stylesheets/ticket-form.module.scss +123 -0
  53. package/lib/ui/BuildIcons.cjs +44 -0
  54. package/lib/ui/Common.js +210 -0
  55. package/lib/ui/Components.jsx +342 -0
  56. package/lib/ui/Observers.js +449 -0
  57. package/lib/ui/PlayerProfileForm.jsx +106 -0
  58. package/lib/ui/PlayerUI.jsx +316 -0
  59. package/lib/ui/TVControls.jsx +337 -0
  60. package/lib/ui/TicketForm.jsx +147 -0
  61. package/lib/ui/WebControls.jsx +290 -0
  62. package/package.json +35 -47
  63. package/dist/index-88AgCVwU.js +0 -367
  64. package/src/BuildIcons.js +0 -27
  65. package/src/PlayerControls.js +0 -1478
  66. package/src/index.js +0 -1416
  67. package/src/static/icons/Icons.js +0 -15
  68. package/src/static/icons/Settings icon.svg +0 -4
  69. package/src/static/icons/chat icon collapse.svg +0 -1
  70. package/src/static/icons/chat icon.svg +0 -11
  71. package/src/static/icons/chat send.svg +0 -1
  72. package/src/static/icons/full screen.svg +0 -1
  73. package/src/static/icons/media/LargePlayIcon.svg +0 -4
  74. package/src/static/icons/media/Settings icon.svg +0 -4
  75. package/src/static/icons/media/Skip backward icon.svg +0 -4
  76. package/src/static/icons/media/list.svg +0 -1
  77. package/src/static/icons/media/loop icon.svg +0 -12
  78. package/src/static/icons/media/shuffle icon.svg +0 -13
  79. package/src/static/icons/muted.svg +0 -11
  80. package/src/static/icons/pause.svg +0 -1
  81. package/src/static/icons/play circle.svg +0 -1
  82. package/src/static/icons/play.svg +0 -1
  83. package/src/static/icons/settings.svg +0 -1
  84. package/src/static/icons/slider circle.svg +0 -1
  85. package/src/static/icons/unmuted.svg +0 -10
  86. package/src/static/images/ELUV.IO logo embed player.png +0 -0
  87. package/src/static/images/ELUV.IO logo embed player.svg +0 -1
  88. package/src/static/images/ELUVIO white.svg +0 -26
  89. package/src/static/images/Logo.png +0 -0
  90. package/src/static/stylesheets/player.scss +0 -1065
  91. package/webpack.config.js +0 -152
  92. /package/{src/static/icons → lib/static/icons/svgs}/arrow-left.svg +0 -0
  93. /package/{src/static/icons/live icon.svg → lib/static/icons/svgs/live.svg} +0 -0
  94. /package/{src/static/icons → lib/static/icons/svgs}/multiview.svg +0 -0
  95. /package/{src/static/icons/media → lib/static/icons/svgs}/next.svg +0 -0
  96. /package/{src/static/icons/media → lib/static/icons/svgs}/previous.svg +0 -0
  97. /package/{src/static/icons → lib/static/icons/svgs}/x.svg +0 -0
  98. /package/{src/static/images/ELUV.IO white 20 px V2.png → lib/static/images/Logo.png} +0 -0
@@ -0,0 +1,316 @@
1
+ import ResetStyle from "../static/stylesheets/reset.module.scss";
2
+ import PlayerStyles from "../static/stylesheets/player.module.scss";
3
+
4
+ import React, {useEffect, useRef, useState} from "react";
5
+ import ReactDOM from "react-dom/client";
6
+
7
+ import EluvioPlayer from "../player/Player.js";
8
+ import EluvioPlayerParameters from "../player/PlayerParameters.js";
9
+ import {
10
+ ObserveInteraction,
11
+ ObserveKeydown,
12
+ ObserveMediaSession,
13
+ ObserveResize,
14
+ ObserveVisibility
15
+ } from "./Observers.js";
16
+ import WebControls from "./WebControls.jsx";
17
+ import TicketForm from "./TicketForm.jsx";
18
+ import {Spinner, UserActionIndicator} from "./Components.jsx";
19
+ import TVControls from "./TVControls.jsx";
20
+ import PlayerProfileForm from "./PlayerProfileForm.jsx";
21
+ import {ImageUrl, MergeDefaultParameters} from "./Common.js";
22
+
23
+ const Poster = ({player}) => {
24
+ const [imageUrl, setImageUrl] = useState(undefined);
25
+
26
+ const { posterImage } = (player.controls.GetContentInfo() || {});
27
+
28
+ useEffect(() => {
29
+ setImageUrl(undefined);
30
+
31
+ if(!posterImage) { return; }
32
+
33
+ ImageUrl({player, pathOrUrl: posterImage, width: 200})
34
+ .then(imageUrl => setImageUrl(imageUrl));
35
+ }, [posterImage]);
36
+
37
+ if(!imageUrl) { return null; }
38
+
39
+ return (
40
+ <img
41
+ alt="Video Poster"
42
+ src={imageUrl}
43
+ className={PlayerStyles["poster"]}
44
+ />
45
+ );
46
+ };
47
+
48
+ const PlayerUI = ({target, parameters, InitCallback, ErrorCallback, Unmount, Reset}) => {
49
+ const [player, setPlayer] = useState(undefined);
50
+ const [client, setClient] = useState(undefined);
51
+ const [size, setSize] = useState("lg");
52
+ const [orientation, setOrientation] = useState("landscape");
53
+ const [dimensions, setDimensions] = useState({
54
+ width: target.getBoundingClientRect().width,
55
+ height: target.getBoundingClientRect().height
56
+ });
57
+ const [errorMessage, setErrorMessage] = useState(undefined);
58
+ const [playerInitialized, setPlayerInitialized] = useState(false);
59
+ const [playbackStarted, setPlaybackStarted] = useState(false);
60
+ const [canPlay, setCanPlay] = useState(false);
61
+ const [mounted, setMounted] = useState(false);
62
+ const [showPlayerProfileForm, setShowPlayerProfileForm] = useState(false);
63
+ const [recentlyInteracted, setRecentlyInteracted] = useState(true);
64
+ const [recentUserAction, setRecentUserAction] = useState(undefined);
65
+ const [allowRotation, setAllowRotation] = useState(undefined);
66
+ const videoRef = useRef();
67
+
68
+ const playerSet = !!player;
69
+
70
+ const onUserAction = ({action, text}) => setRecentUserAction({action, text, key: Math.random()});
71
+
72
+ useEffect(() => {
73
+ setMounted(true);
74
+
75
+ // Observe target portal size
76
+ const disposeResizeObserver = ObserveResize({target, setSize, setOrientation, setDimensions});
77
+
78
+ return () => {
79
+ setMounted(false);
80
+ disposeResizeObserver && disposeResizeObserver();
81
+ };
82
+ }, []);
83
+
84
+ useEffect(() => {
85
+ if(!videoRef || !videoRef.current || !mounted) {
86
+ return;
87
+ }
88
+
89
+ try {
90
+ setPlayerInitialized(false);
91
+ setPlaybackStarted(false);
92
+
93
+ // Use ticket client if present
94
+ parameters.clientOptions.client = client || parameters.clientOptions.client;
95
+
96
+ const newPlayer = new EluvioPlayer({
97
+ target,
98
+ video: videoRef.current,
99
+ parameters,
100
+ SetErrorMessage: setErrorMessage
101
+ });
102
+
103
+ window.__elvPlayer = newPlayer;
104
+
105
+ // Observe player settings to keep track of whether playback has started
106
+ const disposePlayerSettingsListener = newPlayer.controls.RegisterSettingsListener(
107
+ () => {
108
+ setAllowRotation(newPlayer.controls.AllowRotation());
109
+ setPlaybackStarted(newPlayer.playbackStarted);
110
+ setPlayerInitialized(!newPlayer.loading);
111
+ setShowPlayerProfileForm(newPlayer.__showPlayerProfileForm);
112
+ }
113
+ );
114
+
115
+ // Destroy method for external use - destroys internal player and unmounts react
116
+ newPlayer.Destroy = () => {
117
+ newPlayer.__DestroyPlayer();
118
+ Unmount();
119
+ };
120
+
121
+ newPlayer.Reset = () => {
122
+ Reset();
123
+ };
124
+
125
+ // Observe whether player is visible for autoplay/mute on visibility functionality
126
+ const disposeVisibilityObserver = ObserveVisibility({player: newPlayer});
127
+
128
+ // Observe interaction for autohiding control elements
129
+ const disposeInteractionObserver = ObserveInteraction({
130
+ player: newPlayer,
131
+ inactivityPeriod: 5000,
132
+ onWake: () => setRecentlyInteracted(true),
133
+ onSleep: () => setRecentlyInteracted(false)
134
+ });
135
+
136
+ // Keyboard controls
137
+ const disposeKeyboardControls = ObserveKeydown({
138
+ player: newPlayer,
139
+ setRecentUserAction: onUserAction
140
+ });
141
+
142
+ // Media session
143
+ const disposeMediaSessionObserver = ObserveMediaSession({player: newPlayer});
144
+
145
+ InitCallback(newPlayer);
146
+ setPlayer(newPlayer);
147
+
148
+ // Can play
149
+ const disposeCanPlayListener = newPlayer.controls.RegisterVideoEventListener("canplay", () => setCanPlay(true));
150
+
151
+ return () => {
152
+ videoRef && videoRef.current && videoRef.current.removeEventListener("play", setPlaybackStarted);
153
+
154
+ disposePlayerSettingsListener && disposePlayerSettingsListener();
155
+ disposeVisibilityObserver && disposeVisibilityObserver();
156
+ disposeInteractionObserver && disposeInteractionObserver();
157
+ disposeKeyboardControls && disposeKeyboardControls();
158
+ disposeMediaSessionObserver && disposeMediaSessionObserver();
159
+ disposeCanPlayListener && disposeCanPlayListener();
160
+
161
+ newPlayer && newPlayer.__DestroyPlayer();
162
+ };
163
+ } catch(error) {
164
+ ErrorCallback(error);
165
+ Unmount();
166
+ }
167
+ }, [videoRef, mounted, client]);
168
+
169
+ useEffect(() => {
170
+ if(!playerSet) { return; }
171
+
172
+ // Clean up player when unmounting
173
+ return () => {
174
+ player && player.__DestroyPlayer();
175
+ setPlayer(undefined);
176
+ };
177
+ }, [playerSet]);
178
+
179
+ if(parameters.clientOptions.promptTicket && !client) {
180
+ return (
181
+ <TicketForm
182
+ parameters={parameters}
183
+ dimensions={{size, orientation, ...dimensions}}
184
+ onComplete={ticketClient => setClient(ticketClient)}
185
+ />
186
+ );
187
+ }
188
+
189
+ const shouldRotate =
190
+ player &&
191
+ player.controls.IsRotatable() &&
192
+ allowRotation;
193
+
194
+ return (
195
+ <div
196
+ role="complementary"
197
+ aria-label="Eluvio Video Player"
198
+ tabIndex={0}
199
+ style={{
200
+ backgroundColor: parameters.playerOptions.backgroundColor || "transparent",
201
+ "--portal-width": `${dimensions.width}px`,
202
+ "--portal-height": `${dimensions.height}px`
203
+ }}
204
+ className={[PlayerStyles["player-container"], shouldRotate ? PlayerStyles["player-container--rotated"] : "", `__eluvio-player--size-${size}`, `__eluvio-player--orientation-${orientation}`].join(" ")}
205
+ >
206
+ {
207
+ !showPlayerProfileForm || !playerInitialized ? null :
208
+ <PlayerProfileForm player={player} Close={() => player.controls.HidePlayerProfileForm()} />
209
+ }
210
+ <video
211
+ playsInline
212
+ disablePictureInPicture
213
+ ref={videoRef}
214
+ muted={[EluvioPlayerParameters.muted.ON, EluvioPlayerParameters.muted.WHEN_NOT_VISIBLE].includes(parameters.playerOptions.muted)}
215
+ controls={parameters.playerOptions.controls === EluvioPlayerParameters.controls.DEFAULT}
216
+ loop={parameters.playerOptions.loop === EluvioPlayerParameters.loop.ON}
217
+ className={PlayerStyles.video}
218
+ />
219
+ {
220
+ !player || playbackStarted || !canPlay ? null :
221
+ <Poster player={player} />
222
+ }
223
+ {
224
+ playerInitialized || errorMessage ? null :
225
+ <div className={PlayerStyles["spinner-container"]}>
226
+ <Spinner className={PlayerStyles["spinner"]} />
227
+ </div>
228
+ }
229
+ {
230
+ !errorMessage ? null :
231
+ <div className={PlayerStyles["error-message"]}>{ errorMessage }</div>
232
+ }
233
+ {
234
+ !player ? null :
235
+ parameters.playerOptions.ui === EluvioPlayerParameters.ui.WEB ?
236
+ <WebControls
237
+ player={player}
238
+ playbackStarted={!!playbackStarted}
239
+ canPlay={canPlay}
240
+ recentlyInteracted={recentlyInteracted}
241
+ setRecentUserAction={onUserAction}
242
+ className={PlayerStyles.controls}
243
+ /> :
244
+ <TVControls
245
+ player={player}
246
+ playbackStarted={!!playbackStarted}
247
+ canPlay={canPlay}
248
+ recentlyInteracted={recentlyInteracted}
249
+ setRecentUserAction={onUserAction}
250
+ className={PlayerStyles.controls}
251
+ />
252
+ }
253
+ {
254
+ !recentUserAction ? null :
255
+ <UserActionIndicator
256
+ action={recentUserAction}
257
+ key={`action-indicator-${recentUserAction && recentUserAction.key}`}
258
+ />
259
+ }
260
+ </div>
261
+ );
262
+ };
263
+
264
+ const PlayerWrapper = (args) => {
265
+ const [playerKey, setPlayerKey] = useState(Math.random());
266
+
267
+ return (
268
+ <PlayerUI
269
+ {...args}
270
+ key={`player-${playerKey}`}
271
+ Reset={() => setPlayerKey(Math.random())}
272
+ />
273
+ );
274
+ };
275
+
276
+ const Initialize = (target, parameters) => {
277
+ target.innerHTML = "";
278
+ target.classList.add(ResetStyle.reset);
279
+ target.classList.add(PlayerStyles["player-target"]);
280
+
281
+ parameters = MergeDefaultParameters(parameters);
282
+
283
+ if(parameters.playerOptions && parameters.playerOptions.backgroundColor) {
284
+ target.style.backgroundColor = parameters.playerOptions.backgroundColor;
285
+ }
286
+
287
+ return new Promise((resolve, reject) => {
288
+ const root = ReactDOM.createRoot(target);
289
+
290
+ root.render(
291
+ <React.StrictMode>
292
+ <PlayerWrapper
293
+ target={target}
294
+ parameters={parameters}
295
+ InitCallback={resolve}
296
+ ErrorCallback={reject}
297
+ Unmount={async () => {
298
+ try {
299
+ await root.unmount();
300
+ } catch(error) {
301
+ // eslint-disable-next-line no-console
302
+ console.error("Failed to unmount Eluvio Player");
303
+ // eslint-disable-next-line no-console
304
+ console.error(error);
305
+ }
306
+ }
307
+ }
308
+ />
309
+ </React.StrictMode>
310
+ );
311
+ });
312
+ };
313
+
314
+
315
+ export default Initialize;
316
+
@@ -0,0 +1,337 @@
1
+ import ControlStyles from "../static/stylesheets/controls-tv.module.scss";
2
+
3
+ // eslint-disable-next-line no-unused-vars
4
+ import React, {useEffect, useState} from "react";
5
+ import * as Icons from "../static/icons/Icons.js";
6
+ import {ObserveVideo, ObserveVideoTime} from "./Observers.js";
7
+ import "focus-visible";
8
+ import {ImageUrl, PlayerClick, Time} from "./Common.js";
9
+ import EluvioPlayerParameters from "../player/PlayerParameters.js";
10
+
11
+ import EluvioLogo from "../static/images/Logo.png";
12
+ import {CollectionMenu, SeekBar, SettingsMenu, SVG, VolumeControls} from "./Components.jsx";
13
+
14
+ export const IconButton = ({icon, ...props}) => {
15
+ return (
16
+ <button {...props} className={`${ControlStyles["icon-button"]} ${props.className || ""}`} dangerouslySetInnerHTML={{__html: icon}} />
17
+ );
18
+ };
19
+
20
+ const TimeIndicator = ({player, videoState}) => {
21
+ const [currentTime, setCurrentTime] = useState(player.video.currentTime);
22
+
23
+ useEffect(() => {
24
+ const disposeVideoTimeObserver = ObserveVideoTime({video: player.video, setCurrentTime, rate: 10});
25
+
26
+ return () => disposeVideoTimeObserver && disposeVideoTimeObserver();
27
+ }, []);
28
+
29
+ if(player.isLive) {
30
+ return null;
31
+ }
32
+
33
+ return (
34
+ <div className={ControlStyles["time-container"]}>
35
+ <div className={ControlStyles["time"]}>
36
+ { Time(currentTime, videoState.duration) }
37
+ </div>
38
+ <div className={ControlStyles["spacer"]} />
39
+ <div className={ControlStyles["time"]}>
40
+ { Time(videoState.duration, videoState.duration) }
41
+ </div>
42
+ </div>
43
+ );
44
+ };
45
+
46
+ const CenterButtons = ({player, videoState}) => {
47
+ const collectionInfo = player.controls.GetCollectionInfo();
48
+
49
+ const previousMedia = collectionInfo && collectionInfo.isPlaylist && collectionInfo.content[collectionInfo.mediaIndex - 1];
50
+ const nextMedia = collectionInfo && collectionInfo.isPlaylist && collectionInfo.content[collectionInfo.mediaIndex + 1];
51
+
52
+ const playerReady = player.controls.IsReady();
53
+ return (
54
+ <div className={ControlStyles["center-buttons"]}>
55
+ {
56
+ !previousMedia && !nextMedia ? null :
57
+ <IconButton
58
+ disabled={!playerReady || !previousMedia}
59
+ icon={Icons.PreviousTrackIcon}
60
+ onClick={() => player.controls.CollectionPlayPrevious()}
61
+ className={`${ControlStyles["track-button"]} ${ControlStyles["icon-button--drop-shadow-focus"]}`}
62
+ />
63
+ }
64
+ <IconButton
65
+ aria-label="Back 10 Seconds"
66
+ icon={Icons.BackwardCircleIcon}
67
+ onClick={() => player.controls.Seek({relativeSeconds: -10})}
68
+ className={ControlStyles["icon-button--drop-shadow-focus"]}
69
+ />
70
+ <IconButton
71
+ aria-label={videoState.playing ? "Pause" : "Play"}
72
+ icon={videoState.playing ? Icons.PauseCircleIcon : Icons.PlayCircleIcon}
73
+ onClick={() => player.controls.TogglePlay()}
74
+ className={`${ControlStyles["play-pause-button"]} ${ControlStyles["icon-button--drop-shadow-focus"]}`}
75
+ />
76
+ <IconButton
77
+ aria-label="Forward 10 Seconds"
78
+ icon={Icons.ForwardCircleIcon}
79
+ onClick={() => player.controls.Seek({relativeSeconds: 10})}
80
+ className={ControlStyles["icon-button--drop-shadow-focus"]}
81
+ />
82
+ {
83
+ !previousMedia && !nextMedia ? null :
84
+ <IconButton
85
+ disabled={!playerReady || !nextMedia}
86
+ icon={Icons.NextTrackIcon}
87
+ onClick={() => player.controls.CollectionPlayNext()}
88
+ className={`${ControlStyles["track-button"]} ${ControlStyles["icon-button--drop-shadow-focus"]}`}
89
+ />
90
+ }
91
+ </div>
92
+ );
93
+ };
94
+
95
+ const MenuButton = ({label, icon, children, player, setMenuActive, MenuComponent}) => {
96
+ const [show, setShow] = useState(false);
97
+
98
+ return (
99
+ <div className={ControlStyles["menu-control-container"]}>
100
+ {
101
+ icon ?
102
+ <IconButton
103
+ aria-label={show ? `Hide ${label} Menu` : label}
104
+ aria-haspopup
105
+ icon={icon}
106
+ onClick={() => {
107
+ setMenuActive(!show);
108
+ setShow(!show);
109
+ }}
110
+ className={`${ControlStyles["icon-button--circle-focus"]} ${show ? ControlStyles["icon-button-active"] : ""}`}
111
+ /> :
112
+ <button
113
+ onClick={() => {
114
+ setMenuActive(!show);
115
+ setShow(!show);
116
+ }}
117
+ className={`${ControlStyles["text-button"]} ${show ? ControlStyles["text-button--active"] : ""}`}
118
+ >
119
+ { children }
120
+ </button>
121
+ }
122
+ {
123
+ !show ? null :
124
+ <MenuComponent
125
+ player={player}
126
+ Hide={() => {
127
+ setShow(false);
128
+ setMenuActive(false);
129
+ }}
130
+ className={ControlStyles["menu"]}
131
+ />
132
+ }
133
+ </div>
134
+ );
135
+ };
136
+
137
+ const InfoBox = ({player, Hide}) => {
138
+ const [imageUrl, setImageUrl] = useState(undefined);
139
+
140
+ const {title, description, image, headers} = player.controls.GetContentInfo() || {};
141
+
142
+ useEffect(() => {
143
+ setImageUrl(undefined);
144
+
145
+ if(!image) { return; }
146
+
147
+ ImageUrl({player, pathOrUrl: image, width: 200})
148
+ .then(imageUrl => setImageUrl(imageUrl));
149
+ }, [image]);
150
+
151
+ useEffect(() => {
152
+ const onEscape = event => {
153
+ if(event && (event.key || "").toLowerCase() === "escape") {
154
+ Hide();
155
+ }
156
+ };
157
+
158
+ document.body.addEventListener("keydown", onEscape);
159
+
160
+ return () => document.body.removeEventListener("keydown", onEscape);
161
+ }, []);
162
+
163
+ return (
164
+ <div className={ControlStyles["info-box-container"]}>
165
+ <button
166
+ autoFocus
167
+ onClick={() => Hide()}
168
+ className={`${ControlStyles["info-box-button"]} ${ControlStyles["info-box-button--info"]}`}
169
+ >
170
+ Info
171
+ </button>
172
+ <div className={ControlStyles["info-box"]}>
173
+ {
174
+ !imageUrl ? null :
175
+ <div className={ControlStyles["info-box-image-container"]}>
176
+ <img src={imageUrl} alt="Image" className={ControlStyles["info-box-image"]}/>
177
+ </div>
178
+ }
179
+ <div className={`${ControlStyles["info-box-text"]} ${imageUrl && headers && headers.length === 0 ? ControlStyles["info-box-text--top-padding"] : ""}`}>
180
+ {
181
+ !headers || headers.length === 0 ? null :
182
+ <div className={ControlStyles["info-box-headers"]}>
183
+ {headers.map((text, index) =>
184
+ <div key={`header-${index}`} className={ControlStyles["info-box-header"]}>
185
+ { text }
186
+ </div>
187
+ )}
188
+ </div>
189
+ }
190
+ <div className={ControlStyles["info-box-title"]}>
191
+ { title || "" }
192
+ </div>
193
+ <div className={ControlStyles["info-box-description"]}>
194
+ { description || "" }
195
+ </div>
196
+ </div>
197
+ <div className={ControlStyles["info-box-actions"]}>
198
+ <button
199
+ onClick={() => {
200
+ player.controls.Seek({time: 0});
201
+ Hide();
202
+ }}
203
+ className={`${ControlStyles["info-box-button"]} ${ControlStyles["info-box-button--restart"]}`}
204
+ >
205
+ <SVG icon={Icons.PlayIcon} />
206
+ From Beginning
207
+ </button>
208
+ </div>
209
+ </div>
210
+ </div>
211
+ );
212
+ };
213
+
214
+
215
+ const TVControls = ({player, playbackStarted, canPlay, recentlyInteracted, setRecentUserAction, className=""}) => {
216
+ const [videoState, setVideoState] = useState(undefined);
217
+ const [playerClickHandler, setPlayerClickHandler] = useState(undefined);
218
+ const [menuActive, setMenuActive] = useState(false);
219
+ const [showInfo, setShowInfo] = useState(false);
220
+
221
+ useEffect(() => {
222
+ setPlayerClickHandler(PlayerClick({player, setRecentUserAction}));
223
+
224
+ const disposeVideoObserver = ObserveVideo({target: player.target, video: player.video, setVideoState});
225
+
226
+ return () => disposeVideoObserver && disposeVideoObserver();
227
+ }, []);
228
+
229
+ if(!videoState) {
230
+ return null;
231
+ }
232
+
233
+ const { title } = (player.controls.GetContentInfo() || {});
234
+
235
+ const collectionInfo = player.controls.GetCollectionInfo();
236
+
237
+ // Title autohide is not dependent on controls settings
238
+ const showUI = recentlyInteracted || !playbackStarted || menuActive;
239
+ const hideControls = !showUI && player.playerOptions.controls === EluvioPlayerParameters.controls.AUTO_HIDE;
240
+
241
+ player.__SetControlsVisibility(!hideControls);
242
+
243
+ return (
244
+ <div
245
+ key="controls"
246
+ onClick={playerClickHandler}
247
+ className={[
248
+ className,
249
+ ControlStyles["container"],
250
+ showUI ? "" : ControlStyles["autohide"],
251
+ player.playerOptions.controls !== EluvioPlayerParameters.controls.DEFAULT ? "" : ControlStyles["container--default-controls"],
252
+ menuActive ? "menu-active" : ""
253
+ ].join(" ")}
254
+ >
255
+ <IconButton
256
+ aria-label="Play"
257
+ tabIndex={playbackStarted ? -1 : 0}
258
+ icon={Icons.CenterPlayCircleIcon}
259
+ onClick={() => {
260
+ player.controls.Play();
261
+ // Take focus off of this button because it should no longer be selectable after playback starts
262
+ player.target.firstChild.focus();
263
+ }}
264
+ className={`${ControlStyles["center-play-button"]} ${ControlStyles["icon-button--drop-shadow-focus"]} ${canPlay && !playbackStarted ? "" : ControlStyles["center-play-button--hidden"]}`}
265
+ />
266
+ {
267
+ showInfo ?
268
+ <InfoBox player={player} Hide={() => setShowInfo(false)} /> :
269
+ <div className={`${ControlStyles["bottom-controls-container"]} ${hideControls ? ControlStyles["bottom-controls-container--autohide"] : ""}`}>
270
+ <div className={ControlStyles["bottom-controls-gradient"]} />
271
+ <div className={ControlStyles["title-container"]}>
272
+ <div className={ControlStyles["title"]}>
273
+ {
274
+ (
275
+ player.playerOptions.title === EluvioPlayerParameters.title.OFF ||
276
+ (player.playerOptions.title === EluvioPlayerParameters.title.FULLSCREEN_ONLY && !player.controls.IsFullscreen())
277
+ ) ? "" : title || ""
278
+ }
279
+ </div>
280
+ <div className={ControlStyles["spacer"]}/>
281
+ {
282
+ !player.isLive ? null :
283
+ <div className={ControlStyles["live-indicator"]}>
284
+ Live
285
+ </div>
286
+ }
287
+ {
288
+ !collectionInfo ? null :
289
+ <MenuButton
290
+ label="Collection Menu"
291
+ icon={Icons.CollectionIcon}
292
+ player={player}
293
+ setMenuActive={setMenuActive}
294
+ MenuComponent={CollectionMenu}
295
+ />
296
+ }
297
+
298
+ </div>
299
+ <SeekBar player={player} videoState={videoState} setRecentUserAction={setRecentUserAction} />
300
+ <TimeIndicator player={player} videoState={videoState}/>
301
+ <div className={ControlStyles["bottom-controls"]}>
302
+ <div className={ControlStyles["bottom-left-controls"]}>
303
+ {
304
+ !title || player.playerOptions.title === EluvioPlayerParameters.title.OFF ? null :
305
+ <button className={ControlStyles["text-button"]} onClick={() => setShowInfo(true)}>Info</button>
306
+ }
307
+ </div>
308
+ <CenterButtons player={player} videoState={videoState}/>
309
+ <div className={ControlStyles["bottom-right-controls"]}>
310
+ {
311
+ !player.controls.GetOptions().hasAnyOptions ? null :
312
+ <MenuButton
313
+ key="settings-button"
314
+ label="Settings"
315
+ player={player}
316
+ MenuComponent={SettingsMenu}
317
+ setMenuActive={setMenuActive}
318
+ >
319
+ Settings
320
+ </MenuButton>
321
+ }
322
+ </div>
323
+ </div>
324
+ </div>
325
+ }
326
+ {
327
+ // Watermark
328
+ player.playerOptions.watermark === EluvioPlayerParameters.watermark.OFF ? null :
329
+ <div className={ControlStyles["watermark"]}>
330
+ <img src={EluvioLogo} alt="Eluvio" />
331
+ </div>
332
+ }
333
+ </div>
334
+ );
335
+ };
336
+
337
+ export default TVControls;