@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.
- package/README.md +35 -6
- package/dist/.vite/manifest.json +17 -17
- package/dist/{Analytics-cQC_NR8f.mjs → Analytics-MzZmvYgy.mjs} +1 -1
- package/dist/{Analytics-z6nAtuJx.js → Analytics-jM8HcyUa.js} +1 -1
- package/dist/{dash.all.min-Uvqi9PBX.js → dash.all.min-16Sl6Y0h.js} +1 -1
- package/dist/{dash.all.min-JeIXEd1s.mjs → dash.all.min-2ST8aEXP.mjs} +1 -1
- package/dist/elv-player-js.cjs.js +1 -1
- package/dist/elv-player-js.css +1 -1
- package/dist/elv-player-js.es.js +1 -1
- package/dist/{index-RKrb2ZFL.js → index-BThzGsbn.js} +1 -1
- package/dist/index-Cw8L2-NE.js +367 -0
- package/dist/{index-FpQhGSc8.mjs → index-herSXPMN.mjs} +1 -1
- package/dist/{index-FmpRD8ov.mjs → index-mO9GR6Op.mjs} +25278 -24415
- package/lib/index.js +7 -0
- package/{src → lib/player}/Analytics.js +9 -8
- package/lib/player/Controls.js +912 -0
- package/{src → lib/player}/FairPlay.js +2 -0
- package/lib/player/Player.js +881 -0
- package/lib/player/PlayerParameters.js +173 -0
- package/lib/static/icons/Icons.js +29 -0
- package/lib/static/icons/svgs/backward-circle.svg +5 -0
- package/lib/static/icons/svgs/backward.svg +4 -0
- package/lib/static/icons/svgs/captions-off.svg +7 -0
- package/lib/static/icons/svgs/captions.svg +6 -0
- package/lib/static/icons/svgs/check.svg +1 -0
- package/lib/static/icons/svgs/chevron-left.svg +1 -0
- package/lib/static/icons/svgs/chevron-right.svg +1 -0
- package/lib/static/icons/svgs/forward-circle.svg +5 -0
- package/lib/static/icons/svgs/forward.svg +4 -0
- package/{src/static/icons/media/Full Screen icon.svg → lib/static/icons/svgs/full-screen.svg} +1 -1
- package/lib/static/icons/svgs/large-play-circle.svg +4 -0
- package/lib/static/icons/svgs/list.svg +1 -0
- package/{src/static/icons → lib/static/icons/svgs}/minimize.svg +1 -1
- package/{src/static/icons/media/Pause icon.svg → lib/static/icons/svgs/pause-circle.svg} +3 -3
- package/lib/static/icons/svgs/pause.svg +1 -0
- package/{src/static/icons/media/Play icon.svg → lib/static/icons/svgs/play-circle.svg} +1 -1
- package/lib/static/icons/svgs/play.svg +1 -0
- package/lib/static/icons/svgs/rotate-cw.svg +1 -0
- package/lib/static/icons/svgs/settings.svg +11 -0
- package/{src/static/icons/media/skip back icon.svg → lib/static/icons/svgs/skip-backward.svg} +2 -3
- package/{src/static/icons/media/Skip forward icon.svg → lib/static/icons/svgs/skip-forward.svg} +2 -3
- package/{src/static/icons/media/Volume icon.svg → lib/static/icons/svgs/volume-high.svg} +3 -3
- package/lib/static/icons/svgs/volume-low.svg +10 -0
- package/{src/static/icons/media/low volume icon.svg → lib/static/icons/svgs/volume-medium.svg} +2 -2
- package/{src/static/icons/media/no volume icon.svg → lib/static/icons/svgs/volume-off.svg} +3 -3
- package/lib/static/stylesheets/common.module.scss +486 -0
- package/lib/static/stylesheets/controls-tv.module.scss +488 -0
- package/lib/static/stylesheets/controls-web.module.scss +422 -0
- package/lib/static/stylesheets/player-profile-form.module.scss +141 -0
- package/lib/static/stylesheets/player.module.scss +92 -0
- package/lib/static/stylesheets/reset.module.scss +79 -0
- package/lib/static/stylesheets/ticket-form.module.scss +123 -0
- package/lib/ui/BuildIcons.cjs +44 -0
- package/lib/ui/Common.js +210 -0
- package/lib/ui/Components.jsx +342 -0
- package/lib/ui/Observers.js +449 -0
- package/lib/ui/PlayerProfileForm.jsx +106 -0
- package/lib/ui/PlayerUI.jsx +316 -0
- package/lib/ui/TVControls.jsx +337 -0
- package/lib/ui/TicketForm.jsx +147 -0
- package/lib/ui/WebControls.jsx +290 -0
- package/package.json +35 -47
- package/dist/index-88AgCVwU.js +0 -367
- package/src/BuildIcons.js +0 -27
- package/src/PlayerControls.js +0 -1478
- package/src/index.js +0 -1416
- package/src/static/icons/Icons.js +0 -15
- package/src/static/icons/Settings icon.svg +0 -4
- package/src/static/icons/chat icon collapse.svg +0 -1
- package/src/static/icons/chat icon.svg +0 -11
- package/src/static/icons/chat send.svg +0 -1
- package/src/static/icons/full screen.svg +0 -1
- package/src/static/icons/media/LargePlayIcon.svg +0 -4
- package/src/static/icons/media/Settings icon.svg +0 -4
- package/src/static/icons/media/Skip backward icon.svg +0 -4
- package/src/static/icons/media/list.svg +0 -1
- package/src/static/icons/media/loop icon.svg +0 -12
- package/src/static/icons/media/shuffle icon.svg +0 -13
- package/src/static/icons/muted.svg +0 -11
- package/src/static/icons/pause.svg +0 -1
- package/src/static/icons/play circle.svg +0 -1
- package/src/static/icons/play.svg +0 -1
- package/src/static/icons/settings.svg +0 -1
- package/src/static/icons/slider circle.svg +0 -1
- package/src/static/icons/unmuted.svg +0 -10
- package/src/static/images/ELUV.IO logo embed player.png +0 -0
- package/src/static/images/ELUV.IO logo embed player.svg +0 -1
- package/src/static/images/ELUVIO white.svg +0 -26
- package/src/static/images/Logo.png +0 -0
- package/src/static/stylesheets/player.scss +0 -1065
- package/webpack.config.js +0 -152
- /package/{src/static/icons → lib/static/icons/svgs}/arrow-left.svg +0 -0
- /package/{src/static/icons/live icon.svg → lib/static/icons/svgs/live.svg} +0 -0
- /package/{src/static/icons → lib/static/icons/svgs}/multiview.svg +0 -0
- /package/{src/static/icons/media → lib/static/icons/svgs}/next.svg +0 -0
- /package/{src/static/icons/media → lib/static/icons/svgs}/previous.svg +0 -0
- /package/{src/static/icons → lib/static/icons/svgs}/x.svg +0 -0
- /package/{src/static/images/ELUV.IO white 20 px V2.png → lib/static/images/Logo.png} +0 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
// Observe player size for reactive UI
|
|
2
|
+
import ResizeObserver from "resize-observer-polyfill";
|
|
3
|
+
import EluvioPlayerParameters from "../player/PlayerParameters.js";
|
|
4
|
+
import {ACTIONS, SeekSliderKeyDown} from "./Common.js";
|
|
5
|
+
|
|
6
|
+
export const RegisterModal = ({element, Hide}) => {
|
|
7
|
+
if(!element) { return; }
|
|
8
|
+
|
|
9
|
+
const onEscape = event => {
|
|
10
|
+
if(event && (event.key || "").toLowerCase() === "escape") {
|
|
11
|
+
Hide();
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const onFocusOut = () => {
|
|
16
|
+
// Without timeout, document.activeElement is always body
|
|
17
|
+
setTimeout(() => {
|
|
18
|
+
if(!element.contains(document.activeElement)) {
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
if(!element.contains(document.activeElement)) {
|
|
21
|
+
Hide();
|
|
22
|
+
}
|
|
23
|
+
}, 250);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Wrap handlers in timeout so that the click that spawned the modal does not cause it to close
|
|
29
|
+
let registerTimeout = setTimeout(() => {
|
|
30
|
+
document.body.addEventListener("keydown", onEscape);
|
|
31
|
+
element.addEventListener("focusout", onFocusOut);
|
|
32
|
+
}, 0);
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
clearTimeout(registerTimeout);
|
|
36
|
+
document.body.removeEventListener("keydown", onEscape);
|
|
37
|
+
element.removeEventListener("focusout", onFocusOut);
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const ObserveVideo = ({target, video, setVideoState}) => {
|
|
42
|
+
const UpdateVideoState = function () {
|
|
43
|
+
const buffer = video.buffered;
|
|
44
|
+
let end = 0;
|
|
45
|
+
for(let i = 0; i < buffer.length; i++) {
|
|
46
|
+
if(buffer.start(i) > video.currentTime) { continue; }
|
|
47
|
+
|
|
48
|
+
if(buffer.end(i) > end) {
|
|
49
|
+
end = buffer.end(i);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setVideoState({
|
|
54
|
+
playing: !video.paused,
|
|
55
|
+
duration: video.duration,
|
|
56
|
+
volume: video.volume,
|
|
57
|
+
muted: video.muted,
|
|
58
|
+
rate: video.playbackRate,
|
|
59
|
+
fullscreen: !!(document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement)
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const events = [
|
|
64
|
+
"play",
|
|
65
|
+
"pause",
|
|
66
|
+
"volumechange",
|
|
67
|
+
"seeked",
|
|
68
|
+
"durationchange",
|
|
69
|
+
"ratechange"
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
events.map(event => video.addEventListener(event, UpdateVideoState));
|
|
73
|
+
target.addEventListener("fullscreenchange", UpdateVideoState);
|
|
74
|
+
|
|
75
|
+
return () => {
|
|
76
|
+
events.map(event => video.removeEventListener(event, UpdateVideoState));
|
|
77
|
+
target.removeEventListener("fullscreenchange", UpdateVideoState);
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const ObserveVideoBuffer = ({video, setBufferFraction}) => {
|
|
82
|
+
const UpdateBufferState = () => {
|
|
83
|
+
if(!isFinite(video.duration)) {
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const buffer = video.buffered;
|
|
88
|
+
let end = 0;
|
|
89
|
+
for(let i = 0; i < buffer.length; i++) {
|
|
90
|
+
if(buffer.start(i) > video.currentTime) { continue; }
|
|
91
|
+
|
|
92
|
+
if(buffer.end(i) > end) {
|
|
93
|
+
end = buffer.end(i);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setBufferFraction(1 - (video.duration - end) / video.duration);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
video.addEventListener("progress", UpdateBufferState);
|
|
101
|
+
|
|
102
|
+
return () => video.removeEventListener("progress", UpdateBufferState);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const ObserveVideoTime = ({video, rate=10, setCurrentTime}) => {
|
|
106
|
+
// Current time doesn't update quickly enough from events for smooth movement - use interval instead
|
|
107
|
+
const currentTimeInterval = setInterval(() => {
|
|
108
|
+
setCurrentTime(video.currentTime);
|
|
109
|
+
}, 1000 / rate);
|
|
110
|
+
|
|
111
|
+
return () => {
|
|
112
|
+
clearInterval(currentTimeInterval);
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const ObserveResize = ({target, setSize, setOrientation, setDimensions}) => {
|
|
117
|
+
let dimensionsUpdateTimeout;
|
|
118
|
+
const observer = new ResizeObserver(entries => {
|
|
119
|
+
clearTimeout(dimensionsUpdateTimeout);
|
|
120
|
+
|
|
121
|
+
const dimensions = entries[0].contentRect;
|
|
122
|
+
|
|
123
|
+
let size = "sm";
|
|
124
|
+
let orientation = "landscape";
|
|
125
|
+
// Use actual player size instead of media queries
|
|
126
|
+
if(dimensions.width > 1400) {
|
|
127
|
+
size = "xl";
|
|
128
|
+
} else if(dimensions.width > 1000) {
|
|
129
|
+
size = "lg";
|
|
130
|
+
} else if(dimensions.width > 650) {
|
|
131
|
+
size = "md";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if(dimensions.width < dimensions.height) {
|
|
135
|
+
orientation = "portrait";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setSize(size);
|
|
139
|
+
setOrientation(orientation);
|
|
140
|
+
|
|
141
|
+
dimensionsUpdateTimeout = setTimeout(() => {
|
|
142
|
+
setDimensions({width: dimensions.width, height: dimensions.height});
|
|
143
|
+
}, 500);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
observer.observe(target);
|
|
147
|
+
|
|
148
|
+
return () => observer.disconnect();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// For 'when visible' options (autoplay, muted), handle when the video moves in and out of user visibility
|
|
152
|
+
export const ObserveVisibility = ({player}) => {
|
|
153
|
+
const video = player.video;
|
|
154
|
+
const autoplay = player.playerOptions.autoplay === EluvioPlayerParameters.autoplay.WHEN_VISIBLE;
|
|
155
|
+
const automute = player.playerOptions.muted === EluvioPlayerParameters.muted.WHEN_NOT_VISIBLE;
|
|
156
|
+
|
|
157
|
+
if(!autoplay && !automute) { return; }
|
|
158
|
+
|
|
159
|
+
let lastPlayPauseAction, lastMuteAction;
|
|
160
|
+
const Callback = async ([bodyElement]) => {
|
|
161
|
+
// Play / pause when entering / leaving viewport
|
|
162
|
+
if(autoplay) {
|
|
163
|
+
if(lastPlayPauseAction !== "play" && bodyElement.isIntersecting && video.paused) {
|
|
164
|
+
player.controls.Play();
|
|
165
|
+
lastPlayPauseAction = "play";
|
|
166
|
+
} else if(lastPlayPauseAction !== "pause" && !bodyElement.isIntersecting && !video.paused) {
|
|
167
|
+
player.controls.Pause();
|
|
168
|
+
lastPlayPauseAction = "pause";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Mute / unmute when entering / leaving viewport
|
|
173
|
+
if(automute) {
|
|
174
|
+
if(lastMuteAction !== "unmute" && bodyElement.isIntersecting && video.muted) {
|
|
175
|
+
video.muted = false;
|
|
176
|
+
lastMuteAction = "unmute";
|
|
177
|
+
} else if(lastMuteAction !== "mute" && !bodyElement.isIntersecting && !video.muted) {
|
|
178
|
+
video.muted = true;
|
|
179
|
+
lastMuteAction = "mute";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const intersectionObserver = new window.IntersectionObserver(Callback, { threshold: 0.1 }).observe(video);
|
|
185
|
+
|
|
186
|
+
return () => intersectionObserver && intersectionObserver.disconnect();
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const ObserveInteraction = ({player, inactivityPeriod=3000, onSleep, onWake}) => {
|
|
190
|
+
let autohideTimeout;
|
|
191
|
+
const Wake = event => {
|
|
192
|
+
clearTimeout(autohideTimeout);
|
|
193
|
+
onWake && onWake();
|
|
194
|
+
|
|
195
|
+
autohideTimeout = setTimeout(() => {
|
|
196
|
+
onSleep && onSleep();
|
|
197
|
+
}, event.type === "mouseout" ? 500 : inactivityPeriod);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const videoEvents = [
|
|
201
|
+
"play",
|
|
202
|
+
"pause",
|
|
203
|
+
"volumechange",
|
|
204
|
+
"seeking"
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
videoEvents.forEach(event => player.video.addEventListener(event, Wake));
|
|
208
|
+
|
|
209
|
+
const targetEvents = [
|
|
210
|
+
"click",
|
|
211
|
+
"dblclick",
|
|
212
|
+
"keydown",
|
|
213
|
+
"mousemove",
|
|
214
|
+
"touchmove",
|
|
215
|
+
"blur",
|
|
216
|
+
"mouseout",
|
|
217
|
+
"fullscreenchange"
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
targetEvents.forEach(event => player.target.addEventListener(event, Wake));
|
|
221
|
+
|
|
222
|
+
return () => {
|
|
223
|
+
videoEvents.map(event => player.video.removeEventListener(event, Wake));
|
|
224
|
+
targetEvents.map(event => player.target.removeEventListener(event, Wake));
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export const ObserveKeydown = ({player, setRecentUserAction}) => {
|
|
229
|
+
if(player.playerOptions.keyboardControls === EluvioPlayerParameters.keyboardControls.OFF) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const disableArrowControls = player.playerOptions.keyboardControls === EluvioPlayerParameters.keyboardControls.ARROW_KEYS_DISABLED;
|
|
234
|
+
const SeekHandler = SeekSliderKeyDown(player, setRecentUserAction)();
|
|
235
|
+
|
|
236
|
+
const onKeydown = event => {
|
|
237
|
+
if(
|
|
238
|
+
// Keyboard controls should only fire if the player is in focus
|
|
239
|
+
!(player.target === event.target || player.target.contains(event.target)) ||
|
|
240
|
+
// Ignore keyboard controls if actively focused on a button or input
|
|
241
|
+
["button", "input"].includes(document.activeElement && document.activeElement.tagName.toLowerCase()) ||
|
|
242
|
+
// Or if the player profile form is visible
|
|
243
|
+
player.__showPlayerProfileForm
|
|
244
|
+
) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let result;
|
|
249
|
+
switch (event.key) {
|
|
250
|
+
case " ":
|
|
251
|
+
case "k":
|
|
252
|
+
result = player.controls.TogglePlay();
|
|
253
|
+
setRecentUserAction({action: result ? ACTIONS.PLAY : ACTIONS.PAUSE});
|
|
254
|
+
break;
|
|
255
|
+
case "f":
|
|
256
|
+
player.controls.ToggleFullscreen();
|
|
257
|
+
break;
|
|
258
|
+
case "m":
|
|
259
|
+
result = player.controls.ToggleMuted();
|
|
260
|
+
setRecentUserAction({action: result ? ACTIONS.MUTE : ACTIONS.UNMUTE});
|
|
261
|
+
break;
|
|
262
|
+
case "ArrowDown":
|
|
263
|
+
if(!disableArrowControls) {
|
|
264
|
+
result = player.controls.SetVolume({relativeFraction: -0.1});
|
|
265
|
+
setRecentUserAction({
|
|
266
|
+
action: result === 0 ? ACTIONS.MUTE : ACTIONS.VOLUME_DOWN,
|
|
267
|
+
text: (result * 100).toFixed(0) + "%"
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
break;
|
|
271
|
+
case "ArrowUp":
|
|
272
|
+
if(!disableArrowControls) {
|
|
273
|
+
result = player.controls.SetVolume({relativeFraction: 0.1});
|
|
274
|
+
setRecentUserAction({
|
|
275
|
+
action: ACTIONS.VOLUME_UP,
|
|
276
|
+
text: (result * 100).toFixed(0) + "%"
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
case ",":
|
|
281
|
+
if(player.video.paused) {
|
|
282
|
+
player.controls.Seek({
|
|
283
|
+
relativeSeconds: -1 / 60
|
|
284
|
+
});
|
|
285
|
+
setRecentUserAction({action: ACTIONS.SEEK_BACK});
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
case ".":
|
|
289
|
+
if(player.video.paused) {
|
|
290
|
+
player.controls.Seek({
|
|
291
|
+
relativeSeconds: 1 / 60
|
|
292
|
+
});
|
|
293
|
+
setRecentUserAction({action: ACTIONS.SEEK_FORWARD});
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
case "ArrowLeft":
|
|
297
|
+
case "ArrowRight":
|
|
298
|
+
if(!disableArrowControls) {
|
|
299
|
+
SeekHandler(event);
|
|
300
|
+
}
|
|
301
|
+
break;
|
|
302
|
+
case "j":
|
|
303
|
+
player.controls.Seek({relativeSeconds: -10});
|
|
304
|
+
setRecentUserAction({action: ACTIONS.SEEK_BACK, text: "10 seconds"});
|
|
305
|
+
break;
|
|
306
|
+
case "l":
|
|
307
|
+
player.controls.Seek({relativeSeconds: 10});
|
|
308
|
+
setRecentUserAction({action: ACTIONS.SEEK_FORWARD, text: "10 seconds"});
|
|
309
|
+
break;
|
|
310
|
+
case "<":
|
|
311
|
+
case ">":
|
|
312
|
+
// eslint-disable-next-line no-case-declarations
|
|
313
|
+
const playbackRates = player.controls.GetPlaybackRates();
|
|
314
|
+
|
|
315
|
+
if(!playbackRates.active) {
|
|
316
|
+
result = player.controls.SetPlaybackRate({rate: 1});
|
|
317
|
+
} else {
|
|
318
|
+
result = player.controls.SetPlaybackRate({
|
|
319
|
+
index: playbackRates.active.index + (event.key === "<" ? -1 : 1)
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
setRecentUserAction({
|
|
323
|
+
action: result.increase ? ACTIONS.PLAYBACK_RATE_UP : ACTIONS.PLAYBACK_RATE_DOWN,
|
|
324
|
+
text: `${result.rate.toFixed(2)}x`
|
|
325
|
+
});
|
|
326
|
+
break;
|
|
327
|
+
case "c":
|
|
328
|
+
result = player.controls.ToggleTextTrack();
|
|
329
|
+
setRecentUserAction({action: result ? ACTIONS.SUBTITLES_ON : ACTIONS.SUBTITLES_OFF});
|
|
330
|
+
break;
|
|
331
|
+
case "P":
|
|
332
|
+
player.controls.CollectionPlayPrevious();
|
|
333
|
+
setRecentUserAction({action: ACTIONS.PLAY_PREVIOUS});
|
|
334
|
+
break;
|
|
335
|
+
case "N":
|
|
336
|
+
player.controls.CollectionPlayNext();
|
|
337
|
+
setRecentUserAction({action: ACTIONS.PLAY_NEXT});
|
|
338
|
+
break;
|
|
339
|
+
case "Home":
|
|
340
|
+
player.controls.Seek({fraction: 0});
|
|
341
|
+
setRecentUserAction({action: ACTIONS.SEEK_BACK});
|
|
342
|
+
break;
|
|
343
|
+
case "End":
|
|
344
|
+
player.controls.Seek({fraction: 1});
|
|
345
|
+
setRecentUserAction({action: ACTIONS.SEEK_FORWARD});
|
|
346
|
+
break;
|
|
347
|
+
case "0":
|
|
348
|
+
case "1":
|
|
349
|
+
case "2":
|
|
350
|
+
case "3":
|
|
351
|
+
case "4":
|
|
352
|
+
case "5":
|
|
353
|
+
case "6":
|
|
354
|
+
case "7":
|
|
355
|
+
case "8":
|
|
356
|
+
case "9":
|
|
357
|
+
result = player.controls.Seek({fraction: parseFloat(event.key) * 0.1});
|
|
358
|
+
setRecentUserAction({action: result ? ACTIONS.SEEK_FORWARD : ACTIONS.SEEK_BACK});
|
|
359
|
+
break;
|
|
360
|
+
default:
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
event.preventDefault();
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
document.addEventListener("keydown", onKeydown);
|
|
368
|
+
|
|
369
|
+
return () => document.removeEventListener("keydown", onKeydown);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
export const ObserveMediaSession = ({player}) => {
|
|
373
|
+
if("mediaSession" in navigator) {
|
|
374
|
+
const mediaSessionEvents = [
|
|
375
|
+
"play",
|
|
376
|
+
"pause",
|
|
377
|
+
"stop",
|
|
378
|
+
"seekbackward",
|
|
379
|
+
"seekforward",
|
|
380
|
+
"seekto",
|
|
381
|
+
"previoustrack",
|
|
382
|
+
"nexttrack"
|
|
383
|
+
];
|
|
384
|
+
|
|
385
|
+
// Media button handling
|
|
386
|
+
mediaSessionEvents.forEach(event => {
|
|
387
|
+
navigator.mediaSession.setActionHandler(event, args => {
|
|
388
|
+
switch (event) {
|
|
389
|
+
case "play":
|
|
390
|
+
player.controls.Play();
|
|
391
|
+
break;
|
|
392
|
+
case "pause":
|
|
393
|
+
player.controls.Pause();
|
|
394
|
+
break;
|
|
395
|
+
case "stop":
|
|
396
|
+
player.controls.Stop();
|
|
397
|
+
break;
|
|
398
|
+
case "seekbackward":
|
|
399
|
+
player.controls.Seek({relativeSeconds: (args && args.seekOffset) || -10});
|
|
400
|
+
break;
|
|
401
|
+
case "seekforward":
|
|
402
|
+
player.controls.Seek({relativeSeconds: (args && args.seekOffset) || 10});
|
|
403
|
+
break;
|
|
404
|
+
case "seekto":
|
|
405
|
+
args && typeof args.seekTime !== "undefined" && player.controls.Seek({time: args.seekTime});
|
|
406
|
+
break;
|
|
407
|
+
case "previoustrack":
|
|
408
|
+
player.controls.CollectionPlayPrevious();
|
|
409
|
+
break;
|
|
410
|
+
case "nexttrack":
|
|
411
|
+
player.controls.CollectionPlayNext();
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
// Video playback information
|
|
419
|
+
let positionInterval = setInterval(() => {
|
|
420
|
+
navigator.mediaSession.playbackState = player.video.paused ? "paused" : "playing";
|
|
421
|
+
navigator.mediaSession.setPositionState({
|
|
422
|
+
duration: isFinite(player.video.duration) ? player.video.duration || 0 : player.video.currentTime + 1,
|
|
423
|
+
playbackRate: player.video.playbackRate,
|
|
424
|
+
position: player.video.currentTime
|
|
425
|
+
});
|
|
426
|
+
}, 1000);
|
|
427
|
+
|
|
428
|
+
// Video metadata
|
|
429
|
+
const disposePlayerSettingsListener = player.controls.RegisterSettingsListener(() => {
|
|
430
|
+
const {title} = player.controls.GetContentInfo() || {};
|
|
431
|
+
|
|
432
|
+
if(!navigator.mediaSession.metadata || navigator.mediaSession.metadata.title !== title) {
|
|
433
|
+
navigator.mediaSession.metadata = new MediaMetadata({title});
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
return () => {
|
|
438
|
+
clearInterval(positionInterval);
|
|
439
|
+
|
|
440
|
+
disposePlayerSettingsListener && disposePlayerSettingsListener();
|
|
441
|
+
|
|
442
|
+
navigator.mediaSession.metadata = null;
|
|
443
|
+
mediaSessionEvents.forEach(event => {
|
|
444
|
+
navigator.mediaSession.setActionHandler(event, null);
|
|
445
|
+
});
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import ProfileFormStyles from "../static/stylesheets/player-profile-form.module.scss";
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line no-unused-vars
|
|
4
|
+
import React, {useEffect, useRef, useState} from "react";
|
|
5
|
+
import {Spinner} from "./Components.jsx";
|
|
6
|
+
import {RegisterModal} from "./Observers.js";
|
|
7
|
+
|
|
8
|
+
const PlayerProfileForm = ({player, Close}) => {
|
|
9
|
+
const [playerOptions, setPlayerOptions] = useState(JSON.stringify(player.hlsOptions || "{}", null, 2));
|
|
10
|
+
const [submitting, setSubmitting] = useState(false);
|
|
11
|
+
const [errorMessage, setErrorMessage] = useState("");
|
|
12
|
+
|
|
13
|
+
const formRef = useRef();
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if(!formRef || !formRef.current) { return; }
|
|
17
|
+
|
|
18
|
+
const modalHandlerDisposer = RegisterModal({element: formRef.current, Hide: Close});
|
|
19
|
+
|
|
20
|
+
return () => modalHandlerDisposer && modalHandlerDisposer();
|
|
21
|
+
}, [formRef]);
|
|
22
|
+
|
|
23
|
+
const Submit = async event => {
|
|
24
|
+
event.preventDefault();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
setSubmitting(true);
|
|
28
|
+
await player.controls.SetPlayerProfile({profile: "custom", customHLSOptions: JSON.parse(playerOptions)});
|
|
29
|
+
|
|
30
|
+
Close();
|
|
31
|
+
} catch(error) {
|
|
32
|
+
setErrorMessage(error.toString());
|
|
33
|
+
setSubmitting(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
role="complementary"
|
|
40
|
+
tabIndex={-1}
|
|
41
|
+
className={ProfileFormStyles["container"]}
|
|
42
|
+
>
|
|
43
|
+
<div className={ProfileFormStyles["overlay"]}>
|
|
44
|
+
<form
|
|
45
|
+
onSubmit={Submit}
|
|
46
|
+
className={ProfileFormStyles["form"]}
|
|
47
|
+
ref={formRef}
|
|
48
|
+
>
|
|
49
|
+
<h2 className={ProfileFormStyles["header"]}>
|
|
50
|
+
Custom hls.js Options
|
|
51
|
+
</h2>
|
|
52
|
+
<div className={ProfileFormStyles["input-container"]}>
|
|
53
|
+
<textarea
|
|
54
|
+
disabled={submitting}
|
|
55
|
+
autoFocus
|
|
56
|
+
value={playerOptions}
|
|
57
|
+
title={errorMessage}
|
|
58
|
+
aria-label="Player Options"
|
|
59
|
+
aria-invalid={!!errorMessage}
|
|
60
|
+
aria-errormessage={errorMessage || ""}
|
|
61
|
+
onChange={event => setPlayerOptions(event.currentTarget.value)}
|
|
62
|
+
onFocus={() => setErrorMessage("")}
|
|
63
|
+
onBlur={() => {
|
|
64
|
+
try {
|
|
65
|
+
setErrorMessage("");
|
|
66
|
+
setPlayerOptions(JSON.stringify(JSON.parse(playerOptions || "{}"), null, 2));
|
|
67
|
+
} catch(error) {
|
|
68
|
+
setErrorMessage(error.toString());
|
|
69
|
+
}
|
|
70
|
+
}}
|
|
71
|
+
className={`${ProfileFormStyles["input"]} ${errorMessage ? ProfileFormStyles["input--invalid"] : ""}`}
|
|
72
|
+
/>
|
|
73
|
+
<div className={ProfileFormStyles["player-info"]}>
|
|
74
|
+
<a tabIndex={0} href="https://github.com/video-dev/hls.js/blob/master/docs/API.md" rel="noreferrer" target="_blank" className={ProfileFormStyles["api-link"]}>
|
|
75
|
+
API Docs
|
|
76
|
+
</a>
|
|
77
|
+
<div className={ProfileFormStyles["player-version"]}>
|
|
78
|
+
hls.js { player.HLS.version }
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<div className={ProfileFormStyles["actions"]}>
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
aria-label="Cancel"
|
|
86
|
+
onClick={() => Close()}
|
|
87
|
+
className={ProfileFormStyles["cancel"]}
|
|
88
|
+
>
|
|
89
|
+
Cancel
|
|
90
|
+
</button>
|
|
91
|
+
<button
|
|
92
|
+
type="submit"
|
|
93
|
+
aria-label="Submit"
|
|
94
|
+
disabled={!!errorMessage}
|
|
95
|
+
className={ProfileFormStyles["submit"]}
|
|
96
|
+
>
|
|
97
|
+
{ submitting ? <Spinner light /> : "Submit" }
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
</form>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default PlayerProfileForm;
|