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/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