react-media-kit 0.1.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/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/index.d.ts +275 -0
- package/dist/index.js +1585 -0
- package/dist/index.js.map +1 -0
- package/dist/rolldown-runtime-w6R9maHv.js +18 -0
- package/package.json +115 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1585 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { t as __exportAll } from "./rolldown-runtime-w6R9maHv.js";
|
|
4
|
+
import { createContext, use, useCallback, useEffect, useEffectEvent, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
|
|
7
|
+
//#region src/utils/time.ts
|
|
8
|
+
function getTimeParts(duration) {
|
|
9
|
+
const timeParts = {
|
|
10
|
+
hours: 0,
|
|
11
|
+
minutes: 0,
|
|
12
|
+
seconds: 0
|
|
13
|
+
};
|
|
14
|
+
if (!Number.isFinite(duration) || duration < 0) return timeParts;
|
|
15
|
+
timeParts.hours = Math.floor(duration / 3600);
|
|
16
|
+
duration %= 3600;
|
|
17
|
+
timeParts.minutes = Math.floor(duration / 60);
|
|
18
|
+
duration %= 60;
|
|
19
|
+
timeParts.seconds = Math.floor(duration);
|
|
20
|
+
return timeParts;
|
|
21
|
+
}
|
|
22
|
+
const padZeros = (value) => String(value).padStart(2, "0");
|
|
23
|
+
function getTimeFormat(time) {
|
|
24
|
+
const { hours, minutes, seconds } = getTimeParts(time);
|
|
25
|
+
return (hours ? [
|
|
26
|
+
hours,
|
|
27
|
+
minutes,
|
|
28
|
+
seconds
|
|
29
|
+
] : [minutes, seconds]).map((v, idx) => idx === 0 ? String(v) : padZeros(v)).join(":");
|
|
30
|
+
}
|
|
31
|
+
function getDurationTimeFormat(time) {
|
|
32
|
+
const { hours, minutes, seconds } = getTimeParts(time);
|
|
33
|
+
return `PT${hours}H${minutes}M${seconds}S`;
|
|
34
|
+
}
|
|
35
|
+
function createTimeLabelFormatter(locale) {
|
|
36
|
+
const createNumberFormatter = (unit) => new Intl.NumberFormat(locale, {
|
|
37
|
+
style: "unit",
|
|
38
|
+
unit,
|
|
39
|
+
unitDisplay: "long"
|
|
40
|
+
});
|
|
41
|
+
const numberFormatters = {
|
|
42
|
+
hour: createNumberFormatter("hour"),
|
|
43
|
+
minute: createNumberFormatter("minute"),
|
|
44
|
+
second: createNumberFormatter("second")
|
|
45
|
+
};
|
|
46
|
+
const listFormatter = new Intl.ListFormat(locale, {
|
|
47
|
+
style: "long",
|
|
48
|
+
type: "conjunction"
|
|
49
|
+
});
|
|
50
|
+
return (time) => {
|
|
51
|
+
const { hours, minutes, seconds } = getTimeParts(time);
|
|
52
|
+
const timeValues = [
|
|
53
|
+
hours && numberFormatters["hour"].format(hours),
|
|
54
|
+
minutes && numberFormatters["minute"].format(minutes),
|
|
55
|
+
seconds && numberFormatters["second"].format(seconds)
|
|
56
|
+
].filter(Boolean);
|
|
57
|
+
if (timeValues.length === 0) return numberFormatters["second"].format(0);
|
|
58
|
+
return listFormatter.format(timeValues);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const normalizeTime = (time) => Number.isFinite(time) ? time : 0;
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/state/playerReducer.ts
|
|
65
|
+
function playerReducer(state, action) {
|
|
66
|
+
switch (action.type) {
|
|
67
|
+
case "PLAY": return {
|
|
68
|
+
...state,
|
|
69
|
+
isPlaying: true
|
|
70
|
+
};
|
|
71
|
+
case "PAUSE": return {
|
|
72
|
+
...state,
|
|
73
|
+
isPlaying: false
|
|
74
|
+
};
|
|
75
|
+
case "TOGGLE": return {
|
|
76
|
+
...state,
|
|
77
|
+
isPlaying: !state.isPlaying
|
|
78
|
+
};
|
|
79
|
+
case "INIT": {
|
|
80
|
+
const { durationInSec, volume, playbackRate } = action.payload;
|
|
81
|
+
return {
|
|
82
|
+
...state,
|
|
83
|
+
durationInSec: normalizeTime(durationInSec),
|
|
84
|
+
volume,
|
|
85
|
+
playbackRate,
|
|
86
|
+
state: "ready"
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
case "TIME_UPDATE": {
|
|
90
|
+
const { time } = action.payload;
|
|
91
|
+
return {
|
|
92
|
+
...state,
|
|
93
|
+
currentTimeInSec: normalizeTime(time),
|
|
94
|
+
optimisticTimeInSec: null
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
case "SEEKING": {
|
|
98
|
+
const { time, bufferedEnd } = action.payload;
|
|
99
|
+
return {
|
|
100
|
+
...state,
|
|
101
|
+
optimisticTimeInSec: normalizeTime(time),
|
|
102
|
+
bufferedEndInSec: bufferedEnd
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
case "FULLSCREEN": {
|
|
106
|
+
const { enabled } = action.payload;
|
|
107
|
+
return {
|
|
108
|
+
...state,
|
|
109
|
+
isFullscreen: enabled
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
case "PIP": {
|
|
113
|
+
const { enabled } = action.payload;
|
|
114
|
+
return {
|
|
115
|
+
...state,
|
|
116
|
+
isPictureInPicture: enabled
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
case "MUTE": {
|
|
120
|
+
const { muted } = action.payload;
|
|
121
|
+
return {
|
|
122
|
+
...state,
|
|
123
|
+
isMuted: muted
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
case "VOLUME_CHANGE": {
|
|
127
|
+
const { volume } = action.payload;
|
|
128
|
+
return {
|
|
129
|
+
...state,
|
|
130
|
+
volume
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
case "PLAYBACK_RATE_CHANGE": {
|
|
134
|
+
const { playbackRate } = action.payload;
|
|
135
|
+
return {
|
|
136
|
+
...state,
|
|
137
|
+
playbackRate
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
case "ERROR": return {
|
|
141
|
+
...state,
|
|
142
|
+
state: "error"
|
|
143
|
+
};
|
|
144
|
+
case "LOADING": return {
|
|
145
|
+
...state,
|
|
146
|
+
state: "loading"
|
|
147
|
+
};
|
|
148
|
+
case "PROGRESS": {
|
|
149
|
+
const { bufferedEnd } = action.payload;
|
|
150
|
+
return {
|
|
151
|
+
...state,
|
|
152
|
+
bufferedEndInSec: bufferedEnd
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
case "BUFFERING": {
|
|
156
|
+
const { isBuffering } = action.payload;
|
|
157
|
+
return {
|
|
158
|
+
...state,
|
|
159
|
+
isBuffering
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/state/seekQueue.ts
|
|
167
|
+
function createSeekQueue() {
|
|
168
|
+
let queue = { isPending: false };
|
|
169
|
+
function set(value) {
|
|
170
|
+
queue = {
|
|
171
|
+
isPending: true,
|
|
172
|
+
value
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function pop() {
|
|
176
|
+
const state = { ...queue };
|
|
177
|
+
queue = { isPending: false };
|
|
178
|
+
return state;
|
|
179
|
+
}
|
|
180
|
+
function get() {
|
|
181
|
+
return queue;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
set,
|
|
185
|
+
pop,
|
|
186
|
+
get
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/constants.ts
|
|
192
|
+
const VOLUME_INTERVAL = .05;
|
|
193
|
+
const KEY_NAMES = {
|
|
194
|
+
SPACE: " ",
|
|
195
|
+
ENTER: "ENTER",
|
|
196
|
+
ARROW_LEFT: "ARROWLEFT",
|
|
197
|
+
ARROW_RIGHT: "ARROWRIGHT",
|
|
198
|
+
ARROW_UP: "ARROWUP",
|
|
199
|
+
ARROW_DOWN: "ARROWDOWN",
|
|
200
|
+
HOME: "HOME",
|
|
201
|
+
END: "END",
|
|
202
|
+
MUTE: "M",
|
|
203
|
+
FULLSCREEN: "F"
|
|
204
|
+
};
|
|
205
|
+
const CSS_VARS = {
|
|
206
|
+
progressPercent: "--progress-percent",
|
|
207
|
+
bufferPercent: "--buffer-percent"
|
|
208
|
+
};
|
|
209
|
+
const DATA_ATTRS = {
|
|
210
|
+
mediaPending: "data-media-pending",
|
|
211
|
+
mediaLoading: "data-media-loading",
|
|
212
|
+
mediaReady: "data-media-ready",
|
|
213
|
+
mediaError: "data-media-error",
|
|
214
|
+
playing: "data-playing",
|
|
215
|
+
muted: "data-muted",
|
|
216
|
+
fullscreen: "data-fullscreen",
|
|
217
|
+
pip: "data-pip",
|
|
218
|
+
direction: "data-direction",
|
|
219
|
+
dragging: "data-dragging",
|
|
220
|
+
active: "data-active",
|
|
221
|
+
elapsedMode: "data-elapsed-mode"
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/utils/volume.ts
|
|
226
|
+
const clampVolume = (volume) => Math.max(Math.min(volume, 1), 0);
|
|
227
|
+
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/utils/buffer.ts
|
|
230
|
+
function getBufferedEnd(buffered, time) {
|
|
231
|
+
if (!buffered) return time;
|
|
232
|
+
for (let i = 1, len = buffered.length; i <= len; i++) {
|
|
233
|
+
const idx = len - i;
|
|
234
|
+
const start = buffered.start(idx);
|
|
235
|
+
const end = buffered.end(idx);
|
|
236
|
+
if (time >= start && time <= end) return end;
|
|
237
|
+
}
|
|
238
|
+
return time;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/state/store.ts
|
|
243
|
+
const initialState = Object.freeze({
|
|
244
|
+
state: "pending",
|
|
245
|
+
isPlaying: false,
|
|
246
|
+
isMuted: false,
|
|
247
|
+
isFullscreen: false,
|
|
248
|
+
isPictureInPicture: false,
|
|
249
|
+
isBuffering: false,
|
|
250
|
+
durationInSec: 0,
|
|
251
|
+
currentTimeInSec: 0,
|
|
252
|
+
optimisticTimeInSec: null,
|
|
253
|
+
bufferedEndInSec: null,
|
|
254
|
+
volume: .5,
|
|
255
|
+
playbackRate: 1
|
|
256
|
+
});
|
|
257
|
+
function createPlayerStore() {
|
|
258
|
+
let state = { ...initialState };
|
|
259
|
+
const seekQueue = createSeekQueue();
|
|
260
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
261
|
+
const errorListeners = /* @__PURE__ */ new Set();
|
|
262
|
+
let abortController = new AbortController();
|
|
263
|
+
let media = null;
|
|
264
|
+
let container = null;
|
|
265
|
+
const dispatch = (action) => {
|
|
266
|
+
state = playerReducer(state, action);
|
|
267
|
+
listeners.forEach((l) => l());
|
|
268
|
+
};
|
|
269
|
+
const handlePlay = () => dispatch({ type: "PLAY" });
|
|
270
|
+
const handlePause = () => dispatch({ type: "PAUSE" });
|
|
271
|
+
const play = async () => {
|
|
272
|
+
try {
|
|
273
|
+
await media?.play();
|
|
274
|
+
} catch (error) {
|
|
275
|
+
notifyAboutError({
|
|
276
|
+
type: "play",
|
|
277
|
+
error
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
const pause = () => media?.pause();
|
|
282
|
+
const toggle = () => state.isPlaying ? pause() : play();
|
|
283
|
+
const mute = () => media && (media.muted = true);
|
|
284
|
+
const unmute = () => media && (media.muted = false);
|
|
285
|
+
const toggleMute = () => state.isMuted ? unmute() : mute();
|
|
286
|
+
const stepVolume = (delta) => {
|
|
287
|
+
if (!media) return;
|
|
288
|
+
const newValue = clampVolume(media.volume + delta);
|
|
289
|
+
media.volume = newValue;
|
|
290
|
+
media.muted = false;
|
|
291
|
+
};
|
|
292
|
+
const setVolume = (volume) => {
|
|
293
|
+
if (!media) return;
|
|
294
|
+
const newValue = clampVolume(volume);
|
|
295
|
+
media.volume = newValue;
|
|
296
|
+
media.muted = false;
|
|
297
|
+
};
|
|
298
|
+
const toggleFullscreen = async () => {
|
|
299
|
+
try {
|
|
300
|
+
if (state.isFullscreen) await document.exitFullscreen();
|
|
301
|
+
else await container?.requestFullscreen();
|
|
302
|
+
} catch (error) {
|
|
303
|
+
notifyAboutError({
|
|
304
|
+
type: "fullscreen",
|
|
305
|
+
error
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const togglePip = async () => {
|
|
310
|
+
try {
|
|
311
|
+
if (!(media instanceof HTMLVideoElement)) throw new Error("Picture-in-picture is only supported for video elements");
|
|
312
|
+
if (state.isPictureInPicture) await document.exitPictureInPicture();
|
|
313
|
+
else await media.requestPictureInPicture();
|
|
314
|
+
} catch (error) {
|
|
315
|
+
notifyAboutError({
|
|
316
|
+
type: "pip",
|
|
317
|
+
error
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
const stepPlaybackRate = (delta) => {
|
|
322
|
+
if (!media) return;
|
|
323
|
+
media.playbackRate = media.playbackRate + delta;
|
|
324
|
+
};
|
|
325
|
+
const setPlaybackRate = (rate) => {
|
|
326
|
+
if (!media) return;
|
|
327
|
+
media.playbackRate = rate;
|
|
328
|
+
};
|
|
329
|
+
function skip(delta) {
|
|
330
|
+
if (!media) return;
|
|
331
|
+
seek(Math.max(Math.min(media.currentTime + delta, state.durationInSec), 0));
|
|
332
|
+
}
|
|
333
|
+
function handleTimeUpdate() {
|
|
334
|
+
dispatch({
|
|
335
|
+
type: "TIME_UPDATE",
|
|
336
|
+
payload: { time: this.currentTime }
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
function seek(time) {
|
|
340
|
+
if (!media) return;
|
|
341
|
+
if (media.seeking) seekQueue.set(time);
|
|
342
|
+
else media.currentTime = time;
|
|
343
|
+
dispatch({
|
|
344
|
+
type: "SEEKING",
|
|
345
|
+
payload: {
|
|
346
|
+
time,
|
|
347
|
+
bufferedEnd: getBufferedEnd(media.buffered, time)
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
function handleSeeking() {
|
|
352
|
+
if (seekQueue.get().isPending) return;
|
|
353
|
+
dispatch({
|
|
354
|
+
type: "SEEKING",
|
|
355
|
+
payload: {
|
|
356
|
+
time: this.currentTime,
|
|
357
|
+
bufferedEnd: getBufferedEnd(this.buffered, this.currentTime)
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
function handleSeeked() {
|
|
362
|
+
const seekQueueState = seekQueue.pop();
|
|
363
|
+
if (seekQueueState.isPending && media) media.currentTime = seekQueueState.value;
|
|
364
|
+
}
|
|
365
|
+
function handleInit() {
|
|
366
|
+
const { duration, volume, playbackRate } = this;
|
|
367
|
+
dispatch({
|
|
368
|
+
type: "INIT",
|
|
369
|
+
payload: {
|
|
370
|
+
durationInSec: duration,
|
|
371
|
+
volume,
|
|
372
|
+
playbackRate
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
function handleFullscreen() {
|
|
377
|
+
dispatch({
|
|
378
|
+
type: "FULLSCREEN",
|
|
379
|
+
payload: { enabled: document.fullscreenElement === container }
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
function handlePipEnter() {
|
|
383
|
+
dispatch({
|
|
384
|
+
type: "PIP",
|
|
385
|
+
payload: { enabled: true }
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
function handlePipLeave() {
|
|
389
|
+
dispatch({
|
|
390
|
+
type: "PIP",
|
|
391
|
+
payload: { enabled: false }
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function handleVolumeChange() {
|
|
395
|
+
if (state.isMuted !== this.muted) dispatch({
|
|
396
|
+
type: "MUTE",
|
|
397
|
+
payload: { muted: this.muted }
|
|
398
|
+
});
|
|
399
|
+
if (state.volume !== this.volume) dispatch({
|
|
400
|
+
type: "VOLUME_CHANGE",
|
|
401
|
+
payload: { volume: this.volume }
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
function handleRateChange() {
|
|
405
|
+
if (state.playbackRate !== this.playbackRate) dispatch({
|
|
406
|
+
type: "PLAYBACK_RATE_CHANGE",
|
|
407
|
+
payload: { playbackRate: this.playbackRate }
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function handleError() {
|
|
411
|
+
dispatch({ type: "ERROR" });
|
|
412
|
+
notifyAboutError({
|
|
413
|
+
type: "media",
|
|
414
|
+
error: this.error
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
function handleLoading() {
|
|
418
|
+
dispatch({ type: "LOADING" });
|
|
419
|
+
}
|
|
420
|
+
function handleBufferingStart() {
|
|
421
|
+
if (!state.isBuffering) dispatch({
|
|
422
|
+
type: "BUFFERING",
|
|
423
|
+
payload: { isBuffering: true }
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
function handleBufferingEnd() {
|
|
427
|
+
if (state.isBuffering) dispatch({
|
|
428
|
+
type: "BUFFERING",
|
|
429
|
+
payload: { isBuffering: false }
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function handleProgress() {
|
|
433
|
+
const time = state.optimisticTimeInSec ?? state.currentTimeInSec;
|
|
434
|
+
dispatch({
|
|
435
|
+
type: "PROGRESS",
|
|
436
|
+
payload: { bufferedEnd: getBufferedEnd(media?.buffered, time) }
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function init(mediaEl, containerEl) {
|
|
440
|
+
media = mediaEl;
|
|
441
|
+
container = containerEl;
|
|
442
|
+
const signalConfig = { signal: abortController.signal };
|
|
443
|
+
mediaEl.addEventListener("loadedmetadata", handleInit, signalConfig);
|
|
444
|
+
mediaEl.addEventListener("error", handleError, signalConfig);
|
|
445
|
+
mediaEl.addEventListener("loadstart", handleLoading, signalConfig);
|
|
446
|
+
mediaEl.addEventListener("play", handlePlay, signalConfig);
|
|
447
|
+
mediaEl.addEventListener("pause", handlePause, signalConfig);
|
|
448
|
+
mediaEl.addEventListener("ratechange", handleRateChange, signalConfig);
|
|
449
|
+
mediaEl.addEventListener("seeking", handleSeeking, signalConfig);
|
|
450
|
+
mediaEl.addEventListener("seeked", handleSeeked, signalConfig);
|
|
451
|
+
mediaEl.addEventListener("timeupdate", handleTimeUpdate, signalConfig);
|
|
452
|
+
mediaEl.addEventListener("volumechange", handleVolumeChange, signalConfig);
|
|
453
|
+
mediaEl.addEventListener("progress", handleProgress, signalConfig);
|
|
454
|
+
mediaEl.addEventListener("waiting", handleBufferingStart, signalConfig);
|
|
455
|
+
mediaEl.addEventListener("playing", handleBufferingEnd, signalConfig);
|
|
456
|
+
mediaEl.addEventListener("canplay", handleBufferingEnd, signalConfig);
|
|
457
|
+
if (mediaEl instanceof HTMLVideoElement) {
|
|
458
|
+
mediaEl.addEventListener("enterpictureinpicture", handlePipEnter, signalConfig);
|
|
459
|
+
mediaEl.addEventListener("leavepictureinpicture", handlePipLeave, signalConfig);
|
|
460
|
+
}
|
|
461
|
+
containerEl.addEventListener("fullscreenchange", handleFullscreen, signalConfig);
|
|
462
|
+
}
|
|
463
|
+
function destroy() {
|
|
464
|
+
abortController.abort();
|
|
465
|
+
abortController = new AbortController();
|
|
466
|
+
listeners.clear();
|
|
467
|
+
errorListeners.clear();
|
|
468
|
+
}
|
|
469
|
+
function subscribe(listener) {
|
|
470
|
+
listeners.add(listener);
|
|
471
|
+
return () => {
|
|
472
|
+
listeners.delete(listener);
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const notifyAboutError = (playerError) => {
|
|
476
|
+
errorListeners.forEach((l) => l(playerError));
|
|
477
|
+
};
|
|
478
|
+
const subscribeToErrors = (cb) => {
|
|
479
|
+
errorListeners.add(cb);
|
|
480
|
+
return () => {
|
|
481
|
+
errorListeners.delete(cb);
|
|
482
|
+
};
|
|
483
|
+
};
|
|
484
|
+
const getSnapshot = () => state;
|
|
485
|
+
function subscribeWithSelector(selector, listener) {
|
|
486
|
+
let prev = selector(getSnapshot());
|
|
487
|
+
return subscribe(() => {
|
|
488
|
+
const next = selector(getSnapshot());
|
|
489
|
+
if (Object.is(next, prev)) return;
|
|
490
|
+
prev = next;
|
|
491
|
+
listener(next);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
controls: {
|
|
496
|
+
play,
|
|
497
|
+
pause,
|
|
498
|
+
toggle,
|
|
499
|
+
seek,
|
|
500
|
+
skip,
|
|
501
|
+
toggleFullscreen,
|
|
502
|
+
togglePip,
|
|
503
|
+
toggleMute,
|
|
504
|
+
mute,
|
|
505
|
+
unmute,
|
|
506
|
+
stepVolume,
|
|
507
|
+
setVolume,
|
|
508
|
+
stepPlaybackRate,
|
|
509
|
+
setPlaybackRate
|
|
510
|
+
},
|
|
511
|
+
subscribe,
|
|
512
|
+
subscribeWithSelector,
|
|
513
|
+
subscribeToErrors,
|
|
514
|
+
init,
|
|
515
|
+
destroy,
|
|
516
|
+
getSnapshot
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region src/state/PlayerContext.tsx
|
|
522
|
+
const PlayerContext = createContext(null);
|
|
523
|
+
const usePlayerSubscription = () => {
|
|
524
|
+
const ctx = use(PlayerContext);
|
|
525
|
+
if (!ctx) throw new Error("usePlayerSubscription used outside of the PlayerProvider!");
|
|
526
|
+
return ctx;
|
|
527
|
+
};
|
|
528
|
+
const usePlayerCtx = () => {
|
|
529
|
+
const ctx = use(PlayerContext);
|
|
530
|
+
if (!ctx) throw new Error("usePlayerCtx used outside of the PlayerProvider!");
|
|
531
|
+
const { lang, mediaEl, containerEl } = ctx;
|
|
532
|
+
return {
|
|
533
|
+
lang,
|
|
534
|
+
mediaEl,
|
|
535
|
+
containerEl
|
|
536
|
+
};
|
|
537
|
+
};
|
|
538
|
+
function usePlayer(selector) {
|
|
539
|
+
const { subscribe, getSnapshot } = usePlayerSubscription();
|
|
540
|
+
return useSyncExternalStore(subscribe, () => selector(getSnapshot()));
|
|
541
|
+
}
|
|
542
|
+
function usePlayerControls() {
|
|
543
|
+
const { controls } = usePlayerSubscription();
|
|
544
|
+
return controls;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
//#endregion
|
|
548
|
+
//#region src/hooks/useMergeRefs.ts
|
|
549
|
+
function useMergeRefs(...refs) {
|
|
550
|
+
return useCallback((node) => {
|
|
551
|
+
const cleanups = refs.map((ref) => {
|
|
552
|
+
if (!ref) return null;
|
|
553
|
+
if (typeof ref === "function") return ref(node);
|
|
554
|
+
ref.current = node;
|
|
555
|
+
return null;
|
|
556
|
+
});
|
|
557
|
+
return () => {
|
|
558
|
+
refs.forEach((ref, idx) => {
|
|
559
|
+
if (!ref) return;
|
|
560
|
+
if (typeof ref === "function") {
|
|
561
|
+
const refCleanup = cleanups[idx];
|
|
562
|
+
if (refCleanup == null) ref(null);
|
|
563
|
+
else refCleanup();
|
|
564
|
+
} else ref.current = null;
|
|
565
|
+
});
|
|
566
|
+
};
|
|
567
|
+
}, refs);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/utils/attributes.ts
|
|
572
|
+
const setDataAttr = (condition) => condition || void 0;
|
|
573
|
+
|
|
574
|
+
//#endregion
|
|
575
|
+
//#region src/hooks/useMediaAttributes.ts
|
|
576
|
+
function useMediaAttributes() {
|
|
577
|
+
const state = usePlayer((s) => s.state);
|
|
578
|
+
return {
|
|
579
|
+
[DATA_ATTRS.mediaPending]: setDataAttr(state === "pending"),
|
|
580
|
+
[DATA_ATTRS.mediaLoading]: setDataAttr(state === "loading"),
|
|
581
|
+
[DATA_ATTRS.mediaReady]: setDataAttr(state === "ready"),
|
|
582
|
+
[DATA_ATTRS.mediaError]: setDataAttr(state === "error")
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
//#endregion
|
|
587
|
+
//#region src/components/audio/player/AudioPlayer.tsx
|
|
588
|
+
function AudioPlayer({ ref, ...props }) {
|
|
589
|
+
const { mediaEl } = usePlayerCtx();
|
|
590
|
+
const mergedRef = useMergeRefs(mediaEl, ref);
|
|
591
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
592
|
+
return /* @__PURE__ */ jsx("audio", {
|
|
593
|
+
ref: mergedRef,
|
|
594
|
+
...props,
|
|
595
|
+
...mediaDataAttrs
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/components/audio/index.parts.ts
|
|
601
|
+
var index_parts_exports = /* @__PURE__ */ __exportAll({ Player: () => AudioPlayer });
|
|
602
|
+
|
|
603
|
+
//#endregion
|
|
604
|
+
//#region src/components/controls/root/ControlsRoot.tsx
|
|
605
|
+
function ControlsRoot(props) {
|
|
606
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
607
|
+
return /* @__PURE__ */ jsx("div", {
|
|
608
|
+
...props,
|
|
609
|
+
...mediaDataAttrs
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region src/components/controls/index.parts.ts
|
|
615
|
+
var index_parts_exports$1 = /* @__PURE__ */ __exportAll({ Root: () => ControlsRoot });
|
|
616
|
+
|
|
617
|
+
//#endregion
|
|
618
|
+
//#region src/utils/handlers.ts
|
|
619
|
+
const composeHandlers = (...handlers) => {
|
|
620
|
+
return (event) => {
|
|
621
|
+
for (const handler of handlers) {
|
|
622
|
+
if (event.defaultPrevented) return;
|
|
623
|
+
handler?.(event);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
};
|
|
627
|
+
const normalizeKeyCode = (key) => key.toUpperCase();
|
|
628
|
+
|
|
629
|
+
//#endregion
|
|
630
|
+
//#region src/components/fullscreenButton/root/FullscreenButtonRoot.tsx
|
|
631
|
+
function FullscreenButtonRoot({ onClick, ...props }) {
|
|
632
|
+
const { toggleFullscreen } = usePlayerControls();
|
|
633
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
634
|
+
const isFullscreen = usePlayer((s) => s.isFullscreen);
|
|
635
|
+
return /* @__PURE__ */ jsx("button", {
|
|
636
|
+
"aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
|
|
637
|
+
...props,
|
|
638
|
+
type: "button",
|
|
639
|
+
onClick: composeHandlers(onClick, toggleFullscreen),
|
|
640
|
+
[DATA_ATTRS.fullscreen]: setDataAttr(isFullscreen),
|
|
641
|
+
...mediaDataAttrs
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
//#endregion
|
|
646
|
+
//#region src/components/fullscreenButton/index.parts.ts
|
|
647
|
+
var index_parts_exports$2 = /* @__PURE__ */ __exportAll({ Root: () => FullscreenButtonRoot });
|
|
648
|
+
|
|
649
|
+
//#endregion
|
|
650
|
+
//#region src/components/pipButton/root/PipButtonRoot.tsx
|
|
651
|
+
function PipButtonRoot({ onClick, ...props }) {
|
|
652
|
+
const { togglePip } = usePlayerControls();
|
|
653
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
654
|
+
const isPictureInPicture = usePlayer((s) => s.isPictureInPicture);
|
|
655
|
+
return /* @__PURE__ */ jsx("button", {
|
|
656
|
+
"aria-label": isPictureInPicture ? "Exit picture-in-picture" : "Enter picture-in-picture",
|
|
657
|
+
...props,
|
|
658
|
+
type: "button",
|
|
659
|
+
onClick: composeHandlers(onClick, togglePip),
|
|
660
|
+
[DATA_ATTRS.pip]: setDataAttr(isPictureInPicture),
|
|
661
|
+
...mediaDataAttrs
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region src/components/pipButton/index.parts.ts
|
|
667
|
+
var index_parts_exports$3 = /* @__PURE__ */ __exportAll({ Root: () => PipButtonRoot });
|
|
668
|
+
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region src/components/playbackRateButton/root/PlaybackRateButtonRoot.tsx
|
|
671
|
+
function PlaybackRateButtonRoot({ playbackRate, onClick, ...props }) {
|
|
672
|
+
const { setPlaybackRate } = usePlayerControls();
|
|
673
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
674
|
+
const activePlaybackRate = usePlayer((s) => s.playbackRate);
|
|
675
|
+
const handleClick = () => setPlaybackRate(playbackRate);
|
|
676
|
+
return /* @__PURE__ */ jsx("button", {
|
|
677
|
+
"aria-label": `Playback speed: ${playbackRate}x`,
|
|
678
|
+
...props,
|
|
679
|
+
type: "button",
|
|
680
|
+
onClick: composeHandlers(onClick, handleClick),
|
|
681
|
+
[DATA_ATTRS.active]: setDataAttr(activePlaybackRate === playbackRate),
|
|
682
|
+
...mediaDataAttrs
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
//#endregion
|
|
687
|
+
//#region src/components/playbackRateButton/index.parts.ts
|
|
688
|
+
var index_parts_exports$5 = /* @__PURE__ */ __exportAll({ Root: () => PlaybackRateButtonRoot });
|
|
689
|
+
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region src/components/playButton/root/PlayButtonRoot.tsx
|
|
692
|
+
function PlayButtonRoot({ onClick, ...props }) {
|
|
693
|
+
const { toggle } = usePlayerControls();
|
|
694
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
695
|
+
const isPlaying = usePlayer((s) => s.isPlaying);
|
|
696
|
+
return /* @__PURE__ */ jsx("button", {
|
|
697
|
+
"aria-label": isPlaying ? "Pause video" : "Play video",
|
|
698
|
+
...props,
|
|
699
|
+
type: "button",
|
|
700
|
+
onClick: composeHandlers(onClick, toggle),
|
|
701
|
+
[DATA_ATTRS.playing]: setDataAttr(isPlaying),
|
|
702
|
+
...mediaDataAttrs
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/components/playButton/index.parts.ts
|
|
708
|
+
var index_parts_exports$4 = /* @__PURE__ */ __exportAll({ Root: () => PlayButtonRoot });
|
|
709
|
+
|
|
710
|
+
//#endregion
|
|
711
|
+
//#region src/components/player/root/PlayerRoot.tsx
|
|
712
|
+
const PlayerRoot = ({ children, lang, onError }) => {
|
|
713
|
+
const [state] = useState(createPlayerStore);
|
|
714
|
+
const mediaRef = useRef(null);
|
|
715
|
+
const containerRef = useRef(null);
|
|
716
|
+
useEffect(() => {
|
|
717
|
+
if (!mediaRef.current || !containerRef.current) return;
|
|
718
|
+
state.init(mediaRef.current, containerRef.current);
|
|
719
|
+
return state.destroy;
|
|
720
|
+
}, [state]);
|
|
721
|
+
const subscribeErrorsEvent = useEffectEvent((state) => {
|
|
722
|
+
if (typeof onError !== "function") return;
|
|
723
|
+
state.subscribeToErrors(onError);
|
|
724
|
+
});
|
|
725
|
+
useEffect(() => {
|
|
726
|
+
return subscribeErrorsEvent(state);
|
|
727
|
+
}, [state]);
|
|
728
|
+
const value = useMemo(() => ({
|
|
729
|
+
...state,
|
|
730
|
+
mediaEl: mediaRef,
|
|
731
|
+
containerEl: containerRef,
|
|
732
|
+
lang
|
|
733
|
+
}), [state, lang]);
|
|
734
|
+
return /* @__PURE__ */ jsx(PlayerContext, {
|
|
735
|
+
value,
|
|
736
|
+
children
|
|
737
|
+
});
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
//#endregion
|
|
741
|
+
//#region src/components/player/container/PlayerContainer.tsx
|
|
742
|
+
const NATIVE_ACTIVATION_TAGS = /* @__PURE__ */ new Set([
|
|
743
|
+
"BUTTON",
|
|
744
|
+
"INPUT",
|
|
745
|
+
"SELECT",
|
|
746
|
+
"TEXTAREA",
|
|
747
|
+
"A"
|
|
748
|
+
]);
|
|
749
|
+
function PlayerContainer({ onKeyDown, style, ref, ...props }) {
|
|
750
|
+
const { containerEl } = usePlayerCtx();
|
|
751
|
+
const { toggle, toggleMute, toggleFullscreen, skip, stepVolume } = usePlayerControls();
|
|
752
|
+
const mergedRef = useMergeRefs(containerEl, ref);
|
|
753
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
754
|
+
const isFullscreen = usePlayer((s) => s.isFullscreen);
|
|
755
|
+
const isPictureInPicture = usePlayer((s) => s.isPictureInPicture);
|
|
756
|
+
const handleKeyDown = (e) => {
|
|
757
|
+
if (e.defaultPrevented) return;
|
|
758
|
+
const key = normalizeKeyCode(e.key);
|
|
759
|
+
if ((key === KEY_NAMES.SPACE || key === KEY_NAMES.ENTER) && NATIVE_ACTIVATION_TAGS.has(e.target.tagName)) return;
|
|
760
|
+
switch (key) {
|
|
761
|
+
case KEY_NAMES.SPACE:
|
|
762
|
+
case KEY_NAMES.ENTER:
|
|
763
|
+
e.preventDefault();
|
|
764
|
+
return toggle();
|
|
765
|
+
case KEY_NAMES.ARROW_LEFT:
|
|
766
|
+
e.preventDefault();
|
|
767
|
+
return skip(-5);
|
|
768
|
+
case KEY_NAMES.ARROW_RIGHT:
|
|
769
|
+
e.preventDefault();
|
|
770
|
+
return skip(5);
|
|
771
|
+
case KEY_NAMES.ARROW_UP:
|
|
772
|
+
e.preventDefault();
|
|
773
|
+
return stepVolume(VOLUME_INTERVAL);
|
|
774
|
+
case KEY_NAMES.ARROW_DOWN:
|
|
775
|
+
e.preventDefault();
|
|
776
|
+
return stepVolume(-VOLUME_INTERVAL);
|
|
777
|
+
case KEY_NAMES.MUTE: return toggleMute();
|
|
778
|
+
case KEY_NAMES.FULLSCREEN: return toggleFullscreen();
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
return /* @__PURE__ */ jsx("div", {
|
|
782
|
+
ref: mergedRef,
|
|
783
|
+
style: {
|
|
784
|
+
...style,
|
|
785
|
+
position: "relative"
|
|
786
|
+
},
|
|
787
|
+
...props,
|
|
788
|
+
onKeyDown: composeHandlers(onKeyDown, handleKeyDown),
|
|
789
|
+
[DATA_ATTRS.fullscreen]: setDataAttr(isFullscreen),
|
|
790
|
+
[DATA_ATTRS.pip]: setDataAttr(isPictureInPicture),
|
|
791
|
+
...mediaDataAttrs
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
//#endregion
|
|
796
|
+
//#region src/components/player/index.parts.ts
|
|
797
|
+
var index_parts_exports$6 = /* @__PURE__ */ __exportAll({
|
|
798
|
+
Container: () => PlayerContainer,
|
|
799
|
+
Root: () => PlayerRoot
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region src/hooks/useAnimateOnPlay.ts
|
|
804
|
+
function useAnimateOnPlay({ draw: onDraw, intervalMs }) {
|
|
805
|
+
const { subscribe, subscribeWithSelector, getSnapshot } = usePlayerSubscription();
|
|
806
|
+
const draw = useEffectEvent(onDraw);
|
|
807
|
+
useEffect(() => {
|
|
808
|
+
let animateFrame = null;
|
|
809
|
+
let prevTime = 0;
|
|
810
|
+
const animate = (time) => {
|
|
811
|
+
if (time - prevTime >= intervalMs) {
|
|
812
|
+
draw();
|
|
813
|
+
prevTime = time;
|
|
814
|
+
}
|
|
815
|
+
animateFrame = requestAnimationFrame(animate);
|
|
816
|
+
};
|
|
817
|
+
const startLoop = () => {
|
|
818
|
+
prevTime = 0;
|
|
819
|
+
animateFrame = requestAnimationFrame(animate);
|
|
820
|
+
};
|
|
821
|
+
const stopLoop = () => {
|
|
822
|
+
if (animateFrame === null) return;
|
|
823
|
+
cancelAnimationFrame(animateFrame);
|
|
824
|
+
animateFrame = null;
|
|
825
|
+
};
|
|
826
|
+
const unsubscribe = subscribeWithSelector((s) => s.isPlaying, (isPlaying) => {
|
|
827
|
+
if (isPlaying) startLoop();
|
|
828
|
+
else {
|
|
829
|
+
stopLoop();
|
|
830
|
+
draw();
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
const unsubscribeAny = subscribe(() => !getSnapshot().isPlaying && draw());
|
|
834
|
+
if (getSnapshot().isPlaying) startLoop();
|
|
835
|
+
else draw();
|
|
836
|
+
return () => {
|
|
837
|
+
unsubscribe();
|
|
838
|
+
unsubscribeAny();
|
|
839
|
+
stopLoop();
|
|
840
|
+
};
|
|
841
|
+
}, [
|
|
842
|
+
intervalMs,
|
|
843
|
+
getSnapshot,
|
|
844
|
+
subscribe,
|
|
845
|
+
subscribeWithSelector
|
|
846
|
+
]);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
//#endregion
|
|
850
|
+
//#region src/utils/math.ts
|
|
851
|
+
const toPercent = (value) => value * 100;
|
|
852
|
+
const safeDivide = (divident, divisor) => divisor === 0 ? 0 : divident / divisor;
|
|
853
|
+
|
|
854
|
+
//#endregion
|
|
855
|
+
//#region src/components/seekbar/root/useSeekbarTime.ts
|
|
856
|
+
const UPDATE_INTERVAL_MS$1 = 25;
|
|
857
|
+
function useSeekbarTime(sliderEl) {
|
|
858
|
+
const { lang, mediaEl } = usePlayerCtx();
|
|
859
|
+
const { getSnapshot } = usePlayerSubscription();
|
|
860
|
+
const getTimeLabel = useMemo(() => createTimeLabelFormatter(lang), [lang]);
|
|
861
|
+
const draw = useCallback(() => {
|
|
862
|
+
if (!mediaEl.current || !sliderEl.current) return;
|
|
863
|
+
const { optimisticTimeInSec, durationInSec } = getSnapshot();
|
|
864
|
+
const { currentTime } = mediaEl.current;
|
|
865
|
+
const time = optimisticTimeInSec ?? currentTime;
|
|
866
|
+
const elapsed = toPercent(safeDivide(time, durationInSec));
|
|
867
|
+
const totalElapsedTimeLabel = `${getTimeLabel(time)} / ${getTimeLabel(durationInSec)}`;
|
|
868
|
+
sliderEl.current.style.setProperty(CSS_VARS.progressPercent, elapsed.toFixed(2));
|
|
869
|
+
sliderEl.current.setAttribute("aria-valuetext", totalElapsedTimeLabel);
|
|
870
|
+
sliderEl.current.setAttribute("aria-valuenow", String(Math.floor(time)));
|
|
871
|
+
sliderEl.current.setAttribute("aria-valuemax", String(Math.floor(durationInSec)));
|
|
872
|
+
}, [
|
|
873
|
+
mediaEl,
|
|
874
|
+
getTimeLabel,
|
|
875
|
+
sliderEl,
|
|
876
|
+
getSnapshot
|
|
877
|
+
]);
|
|
878
|
+
useAnimateOnPlay({
|
|
879
|
+
draw,
|
|
880
|
+
intervalMs: UPDATE_INTERVAL_MS$1
|
|
881
|
+
});
|
|
882
|
+
useLayoutEffect(() => {
|
|
883
|
+
draw();
|
|
884
|
+
}, [lang, draw]);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
//#endregion
|
|
888
|
+
//#region src/hooks/useRectPosition.ts
|
|
889
|
+
function useRectPosition() {
|
|
890
|
+
const rectRef = useRef(null);
|
|
891
|
+
const calcRectPositionX = (clickX) => {
|
|
892
|
+
if (!rectRef.current) return null;
|
|
893
|
+
const { left, width } = rectRef.current;
|
|
894
|
+
return (clickX - left) / width;
|
|
895
|
+
};
|
|
896
|
+
const setRect = (value) => {
|
|
897
|
+
rectRef.current = value;
|
|
898
|
+
};
|
|
899
|
+
return {
|
|
900
|
+
setRect,
|
|
901
|
+
calcRectPositionX
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
//#endregion
|
|
906
|
+
//#region src/components/seekbar/root/useSeekbarInteractivity.ts
|
|
907
|
+
function useSeekbarInteractivity(sliderEl, { skipInterval }) {
|
|
908
|
+
const duration = usePlayer((s) => s.durationInSec);
|
|
909
|
+
const { seek, skip } = usePlayerControls();
|
|
910
|
+
const { setRect: setSliderRect, calcRectPositionX } = useRectPosition();
|
|
911
|
+
function updateVideoTime(clickX) {
|
|
912
|
+
const calculatedPosition = calcRectPositionX(clickX);
|
|
913
|
+
if (calculatedPosition == null) return;
|
|
914
|
+
seek(Math.min(Math.max(calculatedPosition, 0), 1) * duration);
|
|
915
|
+
}
|
|
916
|
+
const handlePointerDown = (e) => {
|
|
917
|
+
if (!sliderEl.current) return;
|
|
918
|
+
setSliderRect(sliderEl.current.getBoundingClientRect());
|
|
919
|
+
sliderEl.current.setPointerCapture(e.pointerId);
|
|
920
|
+
sliderEl.current.setAttribute(DATA_ATTRS.dragging, "true");
|
|
921
|
+
updateVideoTime(e.clientX);
|
|
922
|
+
};
|
|
923
|
+
const handlePointerMove = (e) => {
|
|
924
|
+
if (!sliderEl.current || !sliderEl.current.hasPointerCapture(e.pointerId)) return;
|
|
925
|
+
updateVideoTime(e.clientX);
|
|
926
|
+
};
|
|
927
|
+
const handleLostPointerCapture = () => {
|
|
928
|
+
if (!sliderEl.current) return;
|
|
929
|
+
sliderEl.current.toggleAttribute(DATA_ATTRS.dragging, false);
|
|
930
|
+
};
|
|
931
|
+
const handleKeyDown = (e) => {
|
|
932
|
+
switch (normalizeKeyCode(e.key)) {
|
|
933
|
+
case KEY_NAMES.ARROW_LEFT:
|
|
934
|
+
case KEY_NAMES.ARROW_DOWN:
|
|
935
|
+
e.preventDefault();
|
|
936
|
+
return skip(-skipInterval);
|
|
937
|
+
case KEY_NAMES.ARROW_RIGHT:
|
|
938
|
+
case KEY_NAMES.ARROW_UP:
|
|
939
|
+
e.preventDefault();
|
|
940
|
+
return skip(skipInterval);
|
|
941
|
+
case KEY_NAMES.HOME:
|
|
942
|
+
e.preventDefault();
|
|
943
|
+
return seek(0);
|
|
944
|
+
case KEY_NAMES.END:
|
|
945
|
+
e.preventDefault();
|
|
946
|
+
return seek(duration);
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
return {
|
|
950
|
+
handlePointerDown,
|
|
951
|
+
handleLostPointerCapture,
|
|
952
|
+
handlePointerMove,
|
|
953
|
+
handleKeyDown
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
//#endregion
|
|
958
|
+
//#region src/components/common/Slider.tsx
|
|
959
|
+
function Slider({ style, ...props }) {
|
|
960
|
+
return /* @__PURE__ */ jsx("div", {
|
|
961
|
+
style: {
|
|
962
|
+
...style,
|
|
963
|
+
...defaultStyle$3
|
|
964
|
+
},
|
|
965
|
+
...props,
|
|
966
|
+
role: "slider",
|
|
967
|
+
tabIndex: 0
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
const defaultStyle$3 = {
|
|
971
|
+
touchAction: "none",
|
|
972
|
+
position: "relative"
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
//#endregion
|
|
976
|
+
//#region src/components/seekbar/root/useBufferTimer.ts
|
|
977
|
+
function useBufferTime(sliderEl) {
|
|
978
|
+
const { subscribeWithSelector, getSnapshot } = usePlayerSubscription();
|
|
979
|
+
useEffect(() => {
|
|
980
|
+
if (!sliderEl.current) return;
|
|
981
|
+
return subscribeWithSelector((s) => s.bufferedEndInSec, (bufferedEnd) => {
|
|
982
|
+
if (!sliderEl.current) return;
|
|
983
|
+
const { durationInSec } = getSnapshot();
|
|
984
|
+
const buffered = toPercent(safeDivide(bufferedEnd ?? 0, durationInSec));
|
|
985
|
+
sliderEl.current.style.setProperty(CSS_VARS.bufferPercent, buffered.toFixed(2));
|
|
986
|
+
});
|
|
987
|
+
}, [
|
|
988
|
+
sliderEl,
|
|
989
|
+
subscribeWithSelector,
|
|
990
|
+
getSnapshot
|
|
991
|
+
]);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
//#endregion
|
|
995
|
+
//#region src/components/seekbar/root/SeekbarRoot.tsx
|
|
996
|
+
function SeekbarRoot({ skipInterval = 5, ref, onKeyDown, onPointerMove, onPointerDown, onLostPointerCapture, ...props }) {
|
|
997
|
+
const sliderEl = useRef(null);
|
|
998
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
999
|
+
useSeekbarTime(sliderEl);
|
|
1000
|
+
useBufferTime(sliderEl);
|
|
1001
|
+
const mergedRef = useMergeRefs(sliderEl, ref);
|
|
1002
|
+
const { handlePointerDown, handleLostPointerCapture, handlePointerMove, handleKeyDown } = useSeekbarInteractivity(sliderEl, { skipInterval });
|
|
1003
|
+
return /* @__PURE__ */ jsx(Slider, {
|
|
1004
|
+
ref: mergedRef,
|
|
1005
|
+
"aria-label": "Video player slider",
|
|
1006
|
+
...props,
|
|
1007
|
+
onPointerDown: composeHandlers(onPointerDown, handlePointerDown),
|
|
1008
|
+
onPointerMove: composeHandlers(onPointerMove, handlePointerMove),
|
|
1009
|
+
onLostPointerCapture: composeHandlers(onLostPointerCapture, handleLostPointerCapture),
|
|
1010
|
+
onKeyDown: composeHandlers(onKeyDown, handleKeyDown),
|
|
1011
|
+
"aria-valuemin": 0,
|
|
1012
|
+
...mediaDataAttrs
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
//#endregion
|
|
1017
|
+
//#region src/components/common/Track.tsx
|
|
1018
|
+
function Track({ style, ...props }) {
|
|
1019
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1020
|
+
style: {
|
|
1021
|
+
...style,
|
|
1022
|
+
...defaultStyle$2
|
|
1023
|
+
},
|
|
1024
|
+
...props
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
const defaultStyle$2 = {
|
|
1028
|
+
position: "relative",
|
|
1029
|
+
isolation: "isolate"
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
//#endregion
|
|
1033
|
+
//#region src/components/seekbar/track/SeekbarTrack.tsx
|
|
1034
|
+
function SeekbarTrack(props) {
|
|
1035
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1036
|
+
return /* @__PURE__ */ jsx(Track, {
|
|
1037
|
+
...props,
|
|
1038
|
+
...mediaDataAttrs
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
//#endregion
|
|
1043
|
+
//#region src/components/common/Progress.tsx
|
|
1044
|
+
function Progress({ progressVar = "", style, ...props }) {
|
|
1045
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1046
|
+
style: {
|
|
1047
|
+
...style,
|
|
1048
|
+
...defaultStyle$1,
|
|
1049
|
+
transform: `scaleX(calc(var(${progressVar}, 0) / 100))`
|
|
1050
|
+
},
|
|
1051
|
+
...props
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
const defaultStyle$1 = {
|
|
1055
|
+
width: "100%",
|
|
1056
|
+
height: "100%",
|
|
1057
|
+
position: "absolute",
|
|
1058
|
+
transformOrigin: "left",
|
|
1059
|
+
willChange: "transform"
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
//#endregion
|
|
1063
|
+
//#region src/components/seekbar/progress/SeekbarProgress.tsx
|
|
1064
|
+
function SeekbarProgress({ style, ...props }) {
|
|
1065
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1066
|
+
return /* @__PURE__ */ jsx(Progress, {
|
|
1067
|
+
progressVar: CSS_VARS.progressPercent,
|
|
1068
|
+
style: {
|
|
1069
|
+
zIndex: 2,
|
|
1070
|
+
...style
|
|
1071
|
+
},
|
|
1072
|
+
...props,
|
|
1073
|
+
...mediaDataAttrs
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
//#endregion
|
|
1078
|
+
//#region src/components/seekbar/buffer/SeekbarBuffer.tsx
|
|
1079
|
+
function SeekbarBuffer({ style, ...props }) {
|
|
1080
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1081
|
+
return /* @__PURE__ */ jsx(Progress, {
|
|
1082
|
+
progressVar: CSS_VARS.bufferPercent,
|
|
1083
|
+
style: {
|
|
1084
|
+
zIndex: 1,
|
|
1085
|
+
...style
|
|
1086
|
+
},
|
|
1087
|
+
...props,
|
|
1088
|
+
...mediaDataAttrs
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
//#endregion
|
|
1093
|
+
//#region src/components/common/Thumb.tsx
|
|
1094
|
+
function Thumb({ style, ...props }) {
|
|
1095
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1096
|
+
style: defaultContainerStyle,
|
|
1097
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1098
|
+
style: {
|
|
1099
|
+
...defaultStyle,
|
|
1100
|
+
...style
|
|
1101
|
+
},
|
|
1102
|
+
...props
|
|
1103
|
+
})
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
const defaultContainerStyle = {
|
|
1107
|
+
width: "100%",
|
|
1108
|
+
height: "100%",
|
|
1109
|
+
position: "absolute",
|
|
1110
|
+
transform: `translateX(calc(var(${CSS_VARS.progressPercent}, 0) * 1%))`,
|
|
1111
|
+
willChange: "transform",
|
|
1112
|
+
pointerEvents: "none",
|
|
1113
|
+
left: 0,
|
|
1114
|
+
zIndex: "var(--seekbar-thumb-z-index, 9999)"
|
|
1115
|
+
};
|
|
1116
|
+
const defaultStyle = {
|
|
1117
|
+
position: "absolute",
|
|
1118
|
+
top: "50%",
|
|
1119
|
+
transform: "translate(-50%, -50%)"
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
//#endregion
|
|
1123
|
+
//#region src/components/seekbar/thumb/SeekbarThumb.tsx
|
|
1124
|
+
function SeekbarThumb(props) {
|
|
1125
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1126
|
+
return /* @__PURE__ */ jsx(Thumb, {
|
|
1127
|
+
...props,
|
|
1128
|
+
...mediaDataAttrs
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
//#endregion
|
|
1133
|
+
//#region src/components/seekbar/index.parts.ts
|
|
1134
|
+
var index_parts_exports$7 = /* @__PURE__ */ __exportAll({
|
|
1135
|
+
Buffer: () => SeekbarBuffer,
|
|
1136
|
+
Progress: () => SeekbarProgress,
|
|
1137
|
+
Root: () => SeekbarRoot,
|
|
1138
|
+
Thumb: () => SeekbarThumb,
|
|
1139
|
+
Track: () => SeekbarTrack
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
//#endregion
|
|
1143
|
+
//#region src/components/skipButton/root/SkipButtonRoot.tsx
|
|
1144
|
+
const getTimeLabel = createTimeLabelFormatter("en");
|
|
1145
|
+
function SkipButtonRoot({ direction, skipInterval = 5, onClick, ...props }) {
|
|
1146
|
+
const { skip } = usePlayerControls();
|
|
1147
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1148
|
+
const isForward = direction === "forward";
|
|
1149
|
+
const handleSkip = () => skip(isForward ? skipInterval : -skipInterval);
|
|
1150
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1151
|
+
"aria-label": `Skip ${isForward ? "forward" : "back"} ${getTimeLabel(skipInterval)}`,
|
|
1152
|
+
...props,
|
|
1153
|
+
type: "button",
|
|
1154
|
+
onClick: composeHandlers(onClick, handleSkip),
|
|
1155
|
+
[DATA_ATTRS.direction]: direction,
|
|
1156
|
+
...mediaDataAttrs
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
//#endregion
|
|
1161
|
+
//#region src/components/skipButton/index.parts.ts
|
|
1162
|
+
var index_parts_exports$8 = /* @__PURE__ */ __exportAll({ Root: () => SkipButtonRoot });
|
|
1163
|
+
|
|
1164
|
+
//#endregion
|
|
1165
|
+
//#region src/components/timeDisplay/TimeDisplayContext.ts
|
|
1166
|
+
const TimeDisplayContext = createContext(null);
|
|
1167
|
+
const useTimeDisplay = () => {
|
|
1168
|
+
const ctx = use(TimeDisplayContext);
|
|
1169
|
+
if (!ctx) throw new Error("useTimeDisplay used outside of the TimeDisplayProvider!");
|
|
1170
|
+
return ctx;
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
//#endregion
|
|
1174
|
+
//#region src/components/timeDisplay/root/TimeDisplayRoot.tsx
|
|
1175
|
+
function TimeDisplayRoot({ initialMode = "elapsed", ...props }) {
|
|
1176
|
+
const [isElapsedMode, setIsElapsedMode] = useState(initialMode === "elapsed");
|
|
1177
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1178
|
+
const setMode = useCallback((mode) => setIsElapsedMode(mode === "elapsed"), []);
|
|
1179
|
+
const toggleMode = useCallback(() => setIsElapsedMode((m) => !m), []);
|
|
1180
|
+
const value = useMemo(() => ({
|
|
1181
|
+
isElapsedMode,
|
|
1182
|
+
setMode,
|
|
1183
|
+
toggleMode
|
|
1184
|
+
}), [
|
|
1185
|
+
isElapsedMode,
|
|
1186
|
+
setMode,
|
|
1187
|
+
toggleMode
|
|
1188
|
+
]);
|
|
1189
|
+
return /* @__PURE__ */ jsx(TimeDisplayContext, {
|
|
1190
|
+
value,
|
|
1191
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1192
|
+
...props,
|
|
1193
|
+
...mediaDataAttrs
|
|
1194
|
+
})
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
//#endregion
|
|
1199
|
+
//#region src/components/timeDisplay/toggle/TimeDisplayToggle.tsx
|
|
1200
|
+
function TimeDisplayToggle({ onClick, ...props }) {
|
|
1201
|
+
const { toggleMode, isElapsedMode } = useTimeDisplay();
|
|
1202
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1203
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1204
|
+
"aria-label": `See ${isElapsedMode ? "remaining" : "elapsed"} time`,
|
|
1205
|
+
...props,
|
|
1206
|
+
type: "button",
|
|
1207
|
+
onClick: composeHandlers(onClick, toggleMode),
|
|
1208
|
+
[DATA_ATTRS.elapsedMode]: isElapsedMode,
|
|
1209
|
+
...mediaDataAttrs
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
//#endregion
|
|
1214
|
+
//#region src/components/timeDisplay/timer/useTimeDisplayTimer.ts
|
|
1215
|
+
const UPDATE_INTERVAL_MS = 250;
|
|
1216
|
+
function useTimeDisplayTimer(timerRef) {
|
|
1217
|
+
const { isElapsedMode } = useTimeDisplay();
|
|
1218
|
+
const { mediaEl } = usePlayerCtx();
|
|
1219
|
+
const { getSnapshot } = usePlayerSubscription();
|
|
1220
|
+
const draw = useCallback(() => {
|
|
1221
|
+
if (!mediaEl.current || !timerRef.current) return;
|
|
1222
|
+
const { optimisticTimeInSec, durationInSec } = getSnapshot();
|
|
1223
|
+
const time = optimisticTimeInSec ?? mediaEl.current.currentTime;
|
|
1224
|
+
const remainingTime = durationInSec - time;
|
|
1225
|
+
const displayTime = isElapsedMode ? time : remainingTime;
|
|
1226
|
+
timerRef.current.textContent = `${isElapsedMode ? "" : "-"}${getTimeFormat(displayTime)}`;
|
|
1227
|
+
timerRef.current.dateTime = getDurationTimeFormat(displayTime);
|
|
1228
|
+
}, [
|
|
1229
|
+
isElapsedMode,
|
|
1230
|
+
mediaEl,
|
|
1231
|
+
timerRef,
|
|
1232
|
+
getSnapshot
|
|
1233
|
+
]);
|
|
1234
|
+
useAnimateOnPlay({
|
|
1235
|
+
draw,
|
|
1236
|
+
intervalMs: UPDATE_INTERVAL_MS
|
|
1237
|
+
});
|
|
1238
|
+
useLayoutEffect(() => {
|
|
1239
|
+
draw();
|
|
1240
|
+
}, [isElapsedMode, draw]);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
//#endregion
|
|
1244
|
+
//#region src/components/timeDisplay/timer/TimeDisplayTimer.tsx
|
|
1245
|
+
function TimeDisplayTimer({ ref, ...props }) {
|
|
1246
|
+
const timerRef = useRef(null);
|
|
1247
|
+
const mergedRef = useMergeRefs(timerRef, ref);
|
|
1248
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1249
|
+
useTimeDisplayTimer(timerRef);
|
|
1250
|
+
return /* @__PURE__ */ jsx("time", {
|
|
1251
|
+
...props,
|
|
1252
|
+
ref: mergedRef,
|
|
1253
|
+
...mediaDataAttrs
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
//#endregion
|
|
1258
|
+
//#region src/components/timeDisplay/duration/TimeDisplayDuration.tsx
|
|
1259
|
+
function TimeDisplayDuration(props) {
|
|
1260
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1261
|
+
const duration = usePlayer((s) => s.durationInSec);
|
|
1262
|
+
return /* @__PURE__ */ jsx("time", {
|
|
1263
|
+
...props,
|
|
1264
|
+
dateTime: getDurationTimeFormat(duration),
|
|
1265
|
+
...mediaDataAttrs,
|
|
1266
|
+
children: getTimeFormat(duration)
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
//#endregion
|
|
1271
|
+
//#region src/components/timeDisplay/index.parts.ts
|
|
1272
|
+
var index_parts_exports$9 = /* @__PURE__ */ __exportAll({
|
|
1273
|
+
Duration: () => TimeDisplayDuration,
|
|
1274
|
+
Root: () => TimeDisplayRoot,
|
|
1275
|
+
Timer: () => TimeDisplayTimer,
|
|
1276
|
+
Toggle: () => TimeDisplayToggle
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
//#endregion
|
|
1280
|
+
//#region src/components/video/root/VideoRoot.tsx
|
|
1281
|
+
function VideoRoot({ style, ...props }) {
|
|
1282
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1283
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1284
|
+
style: {
|
|
1285
|
+
...style,
|
|
1286
|
+
position: "relative"
|
|
1287
|
+
},
|
|
1288
|
+
...props,
|
|
1289
|
+
...mediaDataAttrs
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
//#endregion
|
|
1294
|
+
//#region src/components/video/overlay/useOverlayInteractivity.ts
|
|
1295
|
+
function useOverlayInteractivity({ onPointerUp, onPointerDown, onDoubleTouch = "fullscreen", onDoubleClick = "fullscreen", doubleClickInterval = 300 }) {
|
|
1296
|
+
const pointedDownEl = useRef(null);
|
|
1297
|
+
const prevClickTimer = useRef(null);
|
|
1298
|
+
const prevClick = useRef(0);
|
|
1299
|
+
const { containerEl } = usePlayerCtx();
|
|
1300
|
+
const { toggle, toggleFullscreen } = usePlayerControls();
|
|
1301
|
+
useEffect(() => {
|
|
1302
|
+
return () => {
|
|
1303
|
+
if (prevClickTimer.current) clearTimeout(prevClickTimer.current);
|
|
1304
|
+
};
|
|
1305
|
+
}, []);
|
|
1306
|
+
const fireDoubleClick = (e, onDoubleClick) => {
|
|
1307
|
+
if (prevClickTimer.current) {
|
|
1308
|
+
clearTimeout(prevClickTimer.current);
|
|
1309
|
+
prevClickTimer.current = null;
|
|
1310
|
+
}
|
|
1311
|
+
if (typeof onDoubleClick === "function") return onDoubleClick(e);
|
|
1312
|
+
if (onDoubleClick === "fullscreen") return toggleFullscreen();
|
|
1313
|
+
};
|
|
1314
|
+
const handlePointerDown = (e) => {
|
|
1315
|
+
onPointerDown?.(e);
|
|
1316
|
+
if (e.defaultPrevented) return;
|
|
1317
|
+
pointedDownEl.current = e.currentTarget;
|
|
1318
|
+
};
|
|
1319
|
+
const handlePointerUp = (e) => {
|
|
1320
|
+
onPointerUp?.(e);
|
|
1321
|
+
if (e.defaultPrevented || !containerEl.current?.contains(pointedDownEl.current)) return;
|
|
1322
|
+
const now = performance.now();
|
|
1323
|
+
const isSingleClick = now - prevClick.current > doubleClickInterval;
|
|
1324
|
+
pointedDownEl.current = null;
|
|
1325
|
+
prevClick.current = now;
|
|
1326
|
+
if (isSingleClick) prevClickTimer.current = setTimeout(() => {
|
|
1327
|
+
toggle();
|
|
1328
|
+
prevClickTimer.current = null;
|
|
1329
|
+
}, doubleClickInterval);
|
|
1330
|
+
else fireDoubleClick(e, e.pointerType === "mouse" ? onDoubleClick : onDoubleTouch);
|
|
1331
|
+
};
|
|
1332
|
+
const handleKeyDown = (e) => {
|
|
1333
|
+
switch (normalizeKeyCode(e.key)) {
|
|
1334
|
+
case KEY_NAMES.ENTER:
|
|
1335
|
+
case KEY_NAMES.SPACE:
|
|
1336
|
+
e.preventDefault();
|
|
1337
|
+
return toggle();
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
return {
|
|
1341
|
+
handlePointerUp,
|
|
1342
|
+
handlePointerDown,
|
|
1343
|
+
handleKeyDown
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
//#endregion
|
|
1348
|
+
//#region src/components/video/overlay/VideoOverlay.tsx
|
|
1349
|
+
function VideoOverlayRoot({ style, label, onDoubleClick, onDoubleTouch, doubleClickInterval, onPointerDown, onPointerUp, onKeyDown, ...props }) {
|
|
1350
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1351
|
+
const { handlePointerDown, handlePointerUp, handleKeyDown } = useOverlayInteractivity({
|
|
1352
|
+
onPointerDown,
|
|
1353
|
+
onPointerUp,
|
|
1354
|
+
onDoubleClick,
|
|
1355
|
+
onDoubleTouch,
|
|
1356
|
+
doubleClickInterval
|
|
1357
|
+
});
|
|
1358
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1359
|
+
type: "button",
|
|
1360
|
+
"aria-label": label,
|
|
1361
|
+
style: {
|
|
1362
|
+
...style,
|
|
1363
|
+
...requiredStyle
|
|
1364
|
+
},
|
|
1365
|
+
onPointerDown: handlePointerDown,
|
|
1366
|
+
onPointerUp: handlePointerUp,
|
|
1367
|
+
onKeyDown: composeHandlers(onKeyDown, handleKeyDown),
|
|
1368
|
+
...props,
|
|
1369
|
+
onDoubleClick: () => {},
|
|
1370
|
+
...mediaDataAttrs
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
const requiredStyle = {
|
|
1374
|
+
position: "absolute",
|
|
1375
|
+
inset: 0,
|
|
1376
|
+
touchAction: "manipulation"
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
//#endregion
|
|
1380
|
+
//#region src/components/video/player/VideoPlayer.tsx
|
|
1381
|
+
function VideoPlayer({ ref, ...props }) {
|
|
1382
|
+
const { mediaEl } = usePlayerCtx();
|
|
1383
|
+
const mergedRef = useMergeRefs(mediaEl, ref);
|
|
1384
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1385
|
+
return /* @__PURE__ */ jsx("video", {
|
|
1386
|
+
ref: mergedRef,
|
|
1387
|
+
...props,
|
|
1388
|
+
...mediaDataAttrs
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
//#endregion
|
|
1393
|
+
//#region src/components/video/index.parts.ts
|
|
1394
|
+
var index_parts_exports$10 = /* @__PURE__ */ __exportAll({
|
|
1395
|
+
Overlay: () => VideoOverlayRoot,
|
|
1396
|
+
Player: () => VideoPlayer,
|
|
1397
|
+
Root: () => VideoRoot
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
//#endregion
|
|
1401
|
+
//#region src/state/shallow.ts
|
|
1402
|
+
const isEqual = (a, b) => {
|
|
1403
|
+
if (Object.is(a, b)) return true;
|
|
1404
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
1405
|
+
const entriesA = Object.entries(a);
|
|
1406
|
+
const entriesB = Object.entries(b);
|
|
1407
|
+
if (entriesA.length !== entriesB.length) return false;
|
|
1408
|
+
return entriesA.every(([k, v]) => Object.is(v, b[k]));
|
|
1409
|
+
};
|
|
1410
|
+
function shallow(selector) {
|
|
1411
|
+
let prev = null;
|
|
1412
|
+
return (state) => {
|
|
1413
|
+
const next = selector(state);
|
|
1414
|
+
if (prev && isEqual(prev, next)) return prev;
|
|
1415
|
+
prev = next;
|
|
1416
|
+
return next;
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
//#endregion
|
|
1421
|
+
//#region src/components/volume/slider/useVolume.ts
|
|
1422
|
+
function useVolume(sliderEl, { volumeInterval, computeAriaValueText }) {
|
|
1423
|
+
const computeAriaValueTextStable = useRef(computeAriaValueText);
|
|
1424
|
+
const { setRect: setSliderRect, calcRectPositionX } = useRectPosition();
|
|
1425
|
+
const { subscribeWithSelector, getSnapshot } = usePlayerSubscription();
|
|
1426
|
+
const { stepVolume, setVolume } = usePlayerControls();
|
|
1427
|
+
const updateSliderEl = useCallback(() => {
|
|
1428
|
+
if (!sliderEl.current) return;
|
|
1429
|
+
const { volume, isMuted } = getSnapshot();
|
|
1430
|
+
const currentVolume = isMuted ? 0 : volume;
|
|
1431
|
+
const currentVolumePercent = toPercent(currentVolume).toFixed(2);
|
|
1432
|
+
sliderEl.current.style.setProperty(CSS_VARS.progressPercent, String(currentVolumePercent));
|
|
1433
|
+
sliderEl.current.setAttribute("aria-valuenow", String(currentVolume.toFixed(2)));
|
|
1434
|
+
sliderEl.current.setAttribute("aria-valuetext", computeAriaValueTextStable.current?.({
|
|
1435
|
+
volume,
|
|
1436
|
+
isMuted
|
|
1437
|
+
}) ?? `${currentVolumePercent}%`);
|
|
1438
|
+
sliderEl.current.toggleAttribute(DATA_ATTRS.muted, isMuted);
|
|
1439
|
+
}, [getSnapshot, sliderEl]);
|
|
1440
|
+
useEffect(() => {
|
|
1441
|
+
computeAriaValueTextStable.current = computeAriaValueText;
|
|
1442
|
+
}, [computeAriaValueText]);
|
|
1443
|
+
useLayoutEffect(() => {
|
|
1444
|
+
const unsubscribe = subscribeWithSelector(shallow((s) => ({
|
|
1445
|
+
volume: s.volume,
|
|
1446
|
+
isMuted: s.isMuted
|
|
1447
|
+
})), updateSliderEl);
|
|
1448
|
+
updateSliderEl();
|
|
1449
|
+
return unsubscribe;
|
|
1450
|
+
}, [
|
|
1451
|
+
subscribeWithSelector,
|
|
1452
|
+
updateSliderEl,
|
|
1453
|
+
sliderEl
|
|
1454
|
+
]);
|
|
1455
|
+
function updateVolume(clickX) {
|
|
1456
|
+
const calculatedPosition = calcRectPositionX(clickX);
|
|
1457
|
+
if (calculatedPosition == null) return;
|
|
1458
|
+
setVolume(clampVolume(calculatedPosition));
|
|
1459
|
+
updateSliderEl();
|
|
1460
|
+
}
|
|
1461
|
+
const handlePointerDown = (e) => {
|
|
1462
|
+
if (!sliderEl.current) return;
|
|
1463
|
+
setSliderRect(sliderEl.current.getBoundingClientRect());
|
|
1464
|
+
sliderEl.current.setPointerCapture(e.pointerId);
|
|
1465
|
+
sliderEl.current.setAttribute(DATA_ATTRS.dragging, "true");
|
|
1466
|
+
updateVolume(e.clientX);
|
|
1467
|
+
};
|
|
1468
|
+
const handlePointerMove = (e) => {
|
|
1469
|
+
if (!sliderEl.current || !sliderEl.current.hasPointerCapture(e.pointerId)) return;
|
|
1470
|
+
updateVolume(e.clientX);
|
|
1471
|
+
};
|
|
1472
|
+
const handleLostPointerCapture = () => {
|
|
1473
|
+
if (!sliderEl.current) return;
|
|
1474
|
+
sliderEl.current.toggleAttribute(DATA_ATTRS.dragging, false);
|
|
1475
|
+
};
|
|
1476
|
+
const handleKeyDown = (e) => {
|
|
1477
|
+
switch (normalizeKeyCode(e.key)) {
|
|
1478
|
+
case KEY_NAMES.ARROW_LEFT:
|
|
1479
|
+
case KEY_NAMES.ARROW_DOWN:
|
|
1480
|
+
e.preventDefault();
|
|
1481
|
+
return stepVolume(-volumeInterval);
|
|
1482
|
+
case KEY_NAMES.ARROW_RIGHT:
|
|
1483
|
+
case KEY_NAMES.ARROW_UP:
|
|
1484
|
+
e.preventDefault();
|
|
1485
|
+
return stepVolume(volumeInterval);
|
|
1486
|
+
case KEY_NAMES.HOME:
|
|
1487
|
+
e.preventDefault();
|
|
1488
|
+
return setVolume(0);
|
|
1489
|
+
case KEY_NAMES.END:
|
|
1490
|
+
e.preventDefault();
|
|
1491
|
+
return setVolume(1);
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
return {
|
|
1495
|
+
handlePointerDown,
|
|
1496
|
+
handlePointerMove,
|
|
1497
|
+
handleLostPointerCapture,
|
|
1498
|
+
handleKeyDown
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
//#endregion
|
|
1503
|
+
//#region src/components/volume/slider/VolumeSlider.tsx
|
|
1504
|
+
function VolumeSlider({ volumeInterval = VOLUME_INTERVAL, ref, onPointerDown, onPointerMove, onLostPointerCapture, onKeyDown, computeAriaValueText, ...props }) {
|
|
1505
|
+
const sliderEl = useRef(null);
|
|
1506
|
+
const mergedRef = useMergeRefs(sliderEl, ref);
|
|
1507
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1508
|
+
const { handleKeyDown, handlePointerDown, handlePointerMove, handleLostPointerCapture } = useVolume(sliderEl, {
|
|
1509
|
+
volumeInterval,
|
|
1510
|
+
computeAriaValueText
|
|
1511
|
+
});
|
|
1512
|
+
return /* @__PURE__ */ jsx(Slider, {
|
|
1513
|
+
ref: mergedRef,
|
|
1514
|
+
"aria-label": "Video volume slider",
|
|
1515
|
+
...props,
|
|
1516
|
+
"aria-valuemin": 0,
|
|
1517
|
+
"aria-valuemax": 1,
|
|
1518
|
+
onPointerMove: composeHandlers(onPointerMove, handlePointerMove),
|
|
1519
|
+
onPointerDown: composeHandlers(onPointerDown, handlePointerDown),
|
|
1520
|
+
onLostPointerCapture: composeHandlers(onLostPointerCapture, handleLostPointerCapture),
|
|
1521
|
+
onKeyDown: composeHandlers(onKeyDown, handleKeyDown),
|
|
1522
|
+
...mediaDataAttrs
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
//#endregion
|
|
1527
|
+
//#region src/components/volume/mute/VolumeMute.tsx
|
|
1528
|
+
function VolumeMute({ onClick, ...props }) {
|
|
1529
|
+
const { toggleMute } = usePlayerControls();
|
|
1530
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1531
|
+
const isMuted = usePlayer((s) => s.isMuted);
|
|
1532
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1533
|
+
"aria-label": isMuted ? "Unmute video" : "Mute video",
|
|
1534
|
+
...props,
|
|
1535
|
+
type: "button",
|
|
1536
|
+
onClick: composeHandlers(onClick, toggleMute),
|
|
1537
|
+
[DATA_ATTRS.muted]: setDataAttr(isMuted),
|
|
1538
|
+
...mediaDataAttrs
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
//#endregion
|
|
1543
|
+
//#region src/components/volume/track/VolumeTrack.tsx
|
|
1544
|
+
function VolumeTrack(props) {
|
|
1545
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1546
|
+
return /* @__PURE__ */ jsx(Track, {
|
|
1547
|
+
...props,
|
|
1548
|
+
...mediaDataAttrs
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
//#endregion
|
|
1553
|
+
//#region src/components/volume/progress/VolumeProgress.tsx
|
|
1554
|
+
function VolumeProgress(props) {
|
|
1555
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1556
|
+
return /* @__PURE__ */ jsx(Progress, {
|
|
1557
|
+
progressVar: CSS_VARS.progressPercent,
|
|
1558
|
+
...props,
|
|
1559
|
+
...mediaDataAttrs
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
//#endregion
|
|
1564
|
+
//#region src/components/volume/thumb/VolumeThumb.tsx
|
|
1565
|
+
function VolumeThumb(props) {
|
|
1566
|
+
const mediaDataAttrs = useMediaAttributes();
|
|
1567
|
+
return /* @__PURE__ */ jsx(Thumb, {
|
|
1568
|
+
...props,
|
|
1569
|
+
...mediaDataAttrs
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
//#endregion
|
|
1574
|
+
//#region src/components/volume/index.parts.ts
|
|
1575
|
+
var index_parts_exports$11 = /* @__PURE__ */ __exportAll({
|
|
1576
|
+
Mute: () => VolumeMute,
|
|
1577
|
+
Progress: () => VolumeProgress,
|
|
1578
|
+
Slider: () => VolumeSlider,
|
|
1579
|
+
Thumb: () => VolumeThumb,
|
|
1580
|
+
Track: () => VolumeTrack
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
//#endregion
|
|
1584
|
+
export { index_parts_exports as Audio, index_parts_exports$1 as Controls, index_parts_exports$2 as FullscreenButton, index_parts_exports$3 as PipButton, index_parts_exports$4 as PlayButton, index_parts_exports$5 as PlaybackRateButton, index_parts_exports$6 as Player, index_parts_exports$7 as Seekbar, index_parts_exports$8 as SkipButton, index_parts_exports$9 as TimeDisplay, index_parts_exports$10 as Video, index_parts_exports$11 as Volume, usePlayer, usePlayerControls };
|
|
1585
|
+
//# sourceMappingURL=index.js.map
|