@hexdn/react 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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/dist/player/bounded-surface-position.d.ts +8 -0
  4. package/dist/player/bounded-surface-position.js +8 -0
  5. package/dist/player/hexdn-player-control-popover.module.css +113 -0
  6. package/dist/player/index.d.ts +25 -0
  7. package/dist/player/index.js +32 -0
  8. package/dist/player/media-video-player-control-hold.d.ts +2 -0
  9. package/dist/player/media-video-player-control-hold.js +9 -0
  10. package/dist/player/media-video-player-debug-hud-loader-contract.d.ts +5 -0
  11. package/dist/player/media-video-player-debug-hud-loader-contract.js +1 -0
  12. package/dist/player/media-video-player-keyboard.d.ts +5 -0
  13. package/dist/player/media-video-player-keyboard.js +52 -0
  14. package/dist/player/media-video-player-layout.d.ts +64 -0
  15. package/dist/player/media-video-player-layout.js +110 -0
  16. package/dist/player/media-video-player-scrubber.d.ts +16 -0
  17. package/dist/player/media-video-player-scrubber.js +333 -0
  18. package/dist/player/media-video-player-surface-click.d.ts +8 -0
  19. package/dist/player/media-video-player-surface-click.js +14 -0
  20. package/dist/player/media-video-player-view-state.d.ts +21 -0
  21. package/dist/player/media-video-player-view-state.js +31 -0
  22. package/dist/player/media-video-player.module.css +1254 -0
  23. package/dist/player/playback-landscape-presentation.d.ts +16 -0
  24. package/dist/player/playback-landscape-presentation.js +39 -0
  25. package/dist/player/playback-loading-indicator.d.ts +7 -0
  26. package/dist/player/playback-loading-indicator.js +35 -0
  27. package/dist/player/player-control-popover.d.ts +16 -0
  28. package/dist/player/player-control-popover.js +130 -0
  29. package/dist/player/player-host.d.ts +31 -0
  30. package/dist/player/player-host.js +29 -0
  31. package/dist/player/player-icon.d.ts +19 -0
  32. package/dist/player/player-icon.js +59 -0
  33. package/dist/player/player-labels.d.ts +27 -0
  34. package/dist/player/player-labels.js +35 -0
  35. package/dist/player/player-slots.d.ts +18 -0
  36. package/dist/player/player-slots.js +1 -0
  37. package/dist/player/ready-player.d.ts +34 -0
  38. package/dist/player/ready-player.js +293 -0
  39. package/dist/player/tooltip-surface.d.ts +2 -0
  40. package/dist/player/tooltip-surface.js +8 -0
  41. package/dist/player/ui-primitives.d.ts +17 -0
  42. package/dist/player/ui-primitives.js +15 -0
  43. package/dist/player/ui.module.css +113 -0
  44. package/dist/player/use-media-video-player-controls.d.ts +17 -0
  45. package/dist/player/use-media-video-player-controls.js +87 -0
  46. package/dist/player/use-media-video-player-fullscreen.d.ts +24 -0
  47. package/dist/player/use-media-video-player-fullscreen.js +38 -0
  48. package/dist/player/use-media-video-player-interactions.d.ts +25 -0
  49. package/dist/player/use-media-video-player-interactions.js +163 -0
  50. package/dist/player/use-media-video-player-presentation.d.ts +50 -0
  51. package/dist/player/use-media-video-player-presentation.js +125 -0
  52. package/dist/player/use-media-video-player.d.ts +81 -0
  53. package/dist/player/use-media-video-player.js +263 -0
  54. package/dist/player/use-player-analytics.d.ts +13 -0
  55. package/dist/player/use-player-analytics.js +79 -0
  56. package/dist/player/use-slider-pointer.d.ts +20 -0
  57. package/dist/player/use-slider-pointer.js +85 -0
  58. package/dist/player/utils.d.ts +1 -0
  59. package/dist/player/utils.js +3 -0
  60. package/dist/upload/index.d.ts +37 -0
  61. package/dist/upload/index.js +60 -0
  62. package/dist/upload/upload.module.css +41 -0
  63. package/dist/upload/use-uploader.d.ts +42 -0
  64. package/dist/upload/use-uploader.js +179 -0
  65. package/package.json +51 -0
@@ -0,0 +1,163 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useLayoutEffect, useRef, } from "react";
3
+ import { enterPlaybackFullscreen } from "./playback-landscape-presentation.js";
4
+ import { isCoarsePlaybackPointer, isPlayerFullscreen, readPlaybackScreenOrientation, } from "./playback-landscape-presentation.js";
5
+ import { readMediaVideoPlayerKeyboardShortcut, shouldIgnoreMediaVideoPlayerKeyboardShortcut, } from "./media-video-player-keyboard.js";
6
+ import { resolveMediaVideoPlayerSurfaceClickAction } from "./media-video-player-surface-click.js";
7
+ export function useMediaVideoPlayerInteractions(input) {
8
+ const latest = useRef(input);
9
+ useLayoutEffect(() => {
10
+ latest.current = input;
11
+ });
12
+ const videoClickTimerRef = useRef(null);
13
+ const clearVideoClickTimer = useCallback(() => {
14
+ if (videoClickTimerRef.current !== null)
15
+ window.clearTimeout(videoClickTimerRef.current);
16
+ videoClickTimerRef.current = null;
17
+ }, []);
18
+ const togglePlayback = useCallback(async () => {
19
+ const { controllerRef, hasPresentedMediaFrameRef, showControls, showPlaybackFeedback, videoRef, } = latest.current;
20
+ const controller = controllerRef.current;
21
+ if (!controller)
22
+ return;
23
+ if (!hasPresentedMediaFrameRef.current &&
24
+ !controller.getSnapshot().errorCode)
25
+ return;
26
+ showControls();
27
+ const action = videoRef.current?.paused || videoRef.current?.ended ? "play" : "pause";
28
+ await controller.togglePlayback();
29
+ if (!controller.getSnapshot().errorCode)
30
+ showPlaybackFeedback(action);
31
+ }, []);
32
+ const seekToTime = useCallback((time) => {
33
+ latest.current.showControls();
34
+ latest.current.controllerRef.current?.seek(time);
35
+ }, []);
36
+ const seekBackward = useCallback(() => {
37
+ const { videoRef, showPlaybackFeedback } = latest.current;
38
+ seekToTime((videoRef.current?.currentTime ?? 0) - 10);
39
+ showPlaybackFeedback("back-10s");
40
+ }, [seekToTime]);
41
+ const seekForward = useCallback(() => {
42
+ const { videoRef, showPlaybackFeedback } = latest.current;
43
+ seekToTime((videoRef.current?.currentTime ?? 0) + 10);
44
+ showPlaybackFeedback("fwd-10s");
45
+ }, [seekToTime]);
46
+ const toggleCaptions = useCallback(() => {
47
+ latest.current.showControls();
48
+ latest.current.controllerRef.current?.toggleCaptions();
49
+ }, []);
50
+ const toggleFullscreen = useCallback(async () => {
51
+ const { playerRef, controllerRef, showControls, onFullscreenError } = latest.current;
52
+ showControls();
53
+ try {
54
+ if (document.fullscreenElement) {
55
+ await document.exitFullscreen();
56
+ return;
57
+ }
58
+ const player = playerRef.current;
59
+ if (player?.requestFullscreen) {
60
+ await enterPlaybackFullscreen({
61
+ lockLandscape: isCoarsePlaybackPointer(),
62
+ orientation: readPlaybackScreenOrientation(),
63
+ requestFullscreen: () => player.requestFullscreen(),
64
+ });
65
+ return;
66
+ }
67
+ controllerRef.current?.enterNativeFullscreen();
68
+ }
69
+ catch {
70
+ controllerRef.current?.rollbackNativeFullscreen();
71
+ onFullscreenError();
72
+ }
73
+ }, []);
74
+ const handleVideoClick = useCallback((event) => {
75
+ if (!latest.current.hasPresentedMediaFrameRef.current) {
76
+ clearVideoClickTimer();
77
+ return;
78
+ }
79
+ const target = event.currentTarget;
80
+ const action = resolveMediaVideoPlayerSurfaceClickAction({
81
+ clickCount: event.detail,
82
+ controlsVisible: latest.current.isControlsVisible,
83
+ ended: target.ended,
84
+ isCoarsePointer: isCoarsePlaybackPointer(),
85
+ paused: target.paused,
86
+ });
87
+ clearVideoClickTimer();
88
+ if (action === "ignore")
89
+ return;
90
+ latest.current.showControls();
91
+ if (action === "reveal-controls")
92
+ return;
93
+ if (action === "play-now") {
94
+ void togglePlayback();
95
+ return;
96
+ }
97
+ videoClickTimerRef.current = window.setTimeout(() => {
98
+ videoClickTimerRef.current = null;
99
+ void togglePlayback();
100
+ }, 220);
101
+ }, [clearVideoClickTimer, togglePlayback]);
102
+ const handleVideoDoubleClick = useCallback((event) => {
103
+ event.preventDefault();
104
+ clearVideoClickTimer();
105
+ if (latest.current.hasPresentedMediaFrameRef.current)
106
+ void toggleFullscreen();
107
+ }, [clearVideoClickTimer, toggleFullscreen]);
108
+ const handleBack = useCallback(async () => {
109
+ latest.current.showControls();
110
+ try {
111
+ if (isPlayerFullscreen({
112
+ fullscreenElement: document.fullscreenElement,
113
+ player: latest.current.playerRef.current,
114
+ }))
115
+ await document.exitFullscreen();
116
+ }
117
+ catch {
118
+ /* Navigation can continue if browser fullscreen exit fails. */
119
+ }
120
+ latest.current.onBack?.();
121
+ }, []);
122
+ const handleSeekChange = useCallback((event) => seekToTime(Number(event.currentTarget.value)), [seekToTime]);
123
+ useEffect(() => {
124
+ function keydown(event) {
125
+ const shortcut = readMediaVideoPlayerKeyboardShortcut(event);
126
+ if (!shortcut || shouldIgnoreMediaVideoPlayerKeyboardShortcut(event))
127
+ return;
128
+ const player = latest.current.playerRef.current, active = document.activeElement;
129
+ if (player &&
130
+ active instanceof HTMLElement &&
131
+ active !== document.body &&
132
+ !player.contains(active))
133
+ return;
134
+ event.preventDefault();
135
+ if (!latest.current.hasPresentedMediaFrameRef.current)
136
+ return;
137
+ if (shortcut === "restart")
138
+ seekToTime(0);
139
+ else if (shortcut === "seek-backward")
140
+ seekBackward();
141
+ else if (shortcut === "seek-forward")
142
+ seekForward();
143
+ else
144
+ void togglePlayback();
145
+ }
146
+ document.addEventListener("keydown", keydown);
147
+ return () => document.removeEventListener("keydown", keydown);
148
+ }, [seekBackward, seekForward, seekToTime, togglePlayback]);
149
+ useEffect(() => clearVideoClickTimer, [clearVideoClickTimer]);
150
+ return {
151
+ handleBack,
152
+ handleSeekChange,
153
+ handleVideoClick,
154
+ handleVideoDoubleClick,
155
+ seekBackward,
156
+ seekForward,
157
+ seekToTime,
158
+ toggleCaptions,
159
+ toggleFullscreen,
160
+ togglePlayback,
161
+ clearVideoClickTimer,
162
+ };
163
+ }
@@ -0,0 +1,50 @@
1
+ type MutableRef<T> = {
2
+ current: T;
3
+ };
4
+ export type MediaVideoPlayerPlaybackFeedback = {
5
+ readonly action: "back-10s" | "fwd-10s" | "pause" | "play";
6
+ readonly id: number;
7
+ };
8
+ export declare function createMediaVideoPlayerFeedbackController(input: {
9
+ readonly canShow: () => boolean;
10
+ readonly clearTimeout: (handle: number) => void;
11
+ readonly onFeedback: (feedback: MediaVideoPlayerPlaybackFeedback | null) => void;
12
+ readonly setTimeout: (listener: () => void, durationMs: number) => number;
13
+ }): {
14
+ clearTimer: () => void;
15
+ dispose: () => void;
16
+ show: (action: MediaVideoPlayerPlaybackFeedback["action"]) => void;
17
+ };
18
+ export declare function createMediaVideoPlayerChromeEntrySchedule(input: {
19
+ readonly cancelAnimationFrame: (handle: number) => void;
20
+ readonly onEnter: () => void;
21
+ readonly requestAnimationFrame: (listener: () => void) => number;
22
+ readonly waitForTransitionIdle: () => Promise<void>;
23
+ }): {
24
+ dispose(): void;
25
+ };
26
+ export declare function useMediaVideoPlayerPresentation(input: {
27
+ readonly hasPresentedMediaFrameRef: MutableRef<boolean>;
28
+ }): {
29
+ clearChromeEntryFrame: () => void;
30
+ clearPlaybackFeedbackTimer: () => void;
31
+ chromeEntryScheduleRef: import("react").RefObject<{
32
+ dispose: () => void;
33
+ } | null>;
34
+ hasChromeEntered: boolean;
35
+ playbackFeedback: MediaVideoPlayerPlaybackFeedback | null;
36
+ setHasChromeEntered: import("react").Dispatch<import("react").SetStateAction<boolean>>;
37
+ setPlaybackFeedback: import("react").Dispatch<import("react").SetStateAction<MediaVideoPlayerPlaybackFeedback | null>>;
38
+ showPlaybackFeedback: (action: MediaVideoPlayerPlaybackFeedback["action"]) => void;
39
+ };
40
+ export declare function useMediaVideoPlayerChromeEntryLifecycle(input: {
41
+ readonly chromeEntryScheduleRef: MutableRef<{
42
+ dispose: () => void;
43
+ } | null>;
44
+ readonly hasError: boolean;
45
+ readonly isAutoplayPending: boolean;
46
+ readonly isMediaReady: boolean;
47
+ readonly setHasChromeEntered: (hasEntered: boolean) => void;
48
+ readonly sourceUrl: string;
49
+ }): void;
50
+ export {};
@@ -0,0 +1,125 @@
1
+ "use client";
2
+ import { usePlayerHost } from "./player-host.js";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ const playbackFeedbackDurationMs = 720;
5
+ export function createMediaVideoPlayerFeedbackController(input) {
6
+ let sequence = 0;
7
+ let timerHandle = null;
8
+ function clearTimer() {
9
+ if (timerHandle === null) {
10
+ return;
11
+ }
12
+ input.clearTimeout(timerHandle);
13
+ timerHandle = null;
14
+ }
15
+ function show(action) {
16
+ if (!input.canShow()) {
17
+ return;
18
+ }
19
+ clearTimer();
20
+ sequence += 1;
21
+ input.onFeedback({ action, id: sequence });
22
+ timerHandle = input.setTimeout(() => {
23
+ timerHandle = null;
24
+ input.onFeedback(null);
25
+ }, playbackFeedbackDurationMs);
26
+ }
27
+ return {
28
+ clearTimer,
29
+ dispose: clearTimer,
30
+ show,
31
+ };
32
+ }
33
+ export function createMediaVideoPlayerChromeEntrySchedule(input) {
34
+ let cancelled = false;
35
+ let frameHandle = null;
36
+ void input.waitForTransitionIdle().then(() => {
37
+ if (cancelled) {
38
+ return;
39
+ }
40
+ frameHandle = input.requestAnimationFrame(() => {
41
+ frameHandle = input.requestAnimationFrame(() => {
42
+ frameHandle = null;
43
+ if (!cancelled) {
44
+ input.onEnter();
45
+ }
46
+ });
47
+ });
48
+ });
49
+ return {
50
+ dispose() {
51
+ cancelled = true;
52
+ if (frameHandle !== null) {
53
+ input.cancelAnimationFrame(frameHandle);
54
+ frameHandle = null;
55
+ }
56
+ },
57
+ };
58
+ }
59
+ export function useMediaVideoPlayerPresentation(input) {
60
+ const [hasChromeEntered, setHasChromeEntered] = useState(false);
61
+ const [playbackFeedback, setPlaybackFeedback] = useState(null);
62
+ const chromeEntryScheduleRef = useRef(null);
63
+ const feedbackControllerRef = useRef(null);
64
+ feedbackControllerRef.current ??= createMediaVideoPlayerFeedbackController({
65
+ canShow: () => input.hasPresentedMediaFrameRef.current,
66
+ clearTimeout: (handle) => window.clearTimeout(handle),
67
+ onFeedback: setPlaybackFeedback,
68
+ setTimeout: (listener, durationMs) => window.setTimeout(listener, durationMs),
69
+ });
70
+ const clearChromeEntryFrame = useCallback(() => {
71
+ chromeEntryScheduleRef.current?.dispose();
72
+ chromeEntryScheduleRef.current = null;
73
+ }, []);
74
+ const clearPlaybackFeedbackTimer = useCallback(() => {
75
+ feedbackControllerRef.current?.clearTimer();
76
+ }, []);
77
+ const showPlaybackFeedback = useCallback((action) => {
78
+ feedbackControllerRef.current?.show(action);
79
+ }, []);
80
+ useEffect(() => {
81
+ return () => feedbackControllerRef.current?.dispose();
82
+ }, []);
83
+ return {
84
+ clearChromeEntryFrame,
85
+ clearPlaybackFeedbackTimer,
86
+ chromeEntryScheduleRef,
87
+ hasChromeEntered,
88
+ playbackFeedback,
89
+ setHasChromeEntered,
90
+ setPlaybackFeedback,
91
+ showPlaybackFeedback,
92
+ };
93
+ }
94
+ export function useMediaVideoPlayerChromeEntryLifecycle(input) {
95
+ const { chromeEntryScheduleRef, hasError, isAutoplayPending, isMediaReady, setHasChromeEntered, sourceUrl, } = input;
96
+ const { transition } = usePlayerHost();
97
+ const { waitForIdle: waitForPlaybackPowerTransitionIdle } = transition;
98
+ useEffect(() => {
99
+ setHasChromeEntered(false);
100
+ if ((!hasError && !isMediaReady) || isAutoplayPending) {
101
+ return;
102
+ }
103
+ const schedule = createMediaVideoPlayerChromeEntrySchedule({
104
+ cancelAnimationFrame: (handle) => window.cancelAnimationFrame(handle),
105
+ onEnter: () => setHasChromeEntered(true),
106
+ requestAnimationFrame: (listener) => window.requestAnimationFrame(listener),
107
+ waitForTransitionIdle: waitForPlaybackPowerTransitionIdle,
108
+ });
109
+ chromeEntryScheduleRef.current = schedule;
110
+ return () => {
111
+ schedule.dispose();
112
+ if (chromeEntryScheduleRef.current === schedule) {
113
+ chromeEntryScheduleRef.current = null;
114
+ }
115
+ };
116
+ }, [
117
+ chromeEntryScheduleRef,
118
+ hasError,
119
+ isAutoplayPending,
120
+ isMediaReady,
121
+ setHasChromeEntered,
122
+ sourceUrl,
123
+ waitForPlaybackPowerTransitionIdle,
124
+ ]);
125
+ }
@@ -0,0 +1,81 @@
1
+ import { type ChangeEvent, type MouseEvent } from "react";
2
+ import { type PlaybackSourceResolver, type PlaybackSource, type PlaybackQualityEvent, type PlaybackRequestAuthorization } from "@hexdn/playback";
3
+ import { type MediaVideoPlayerCopy, type MediaVideoPlayerError, type MediaVideoPlayerDebugSnapshot } from "@hexdn/playback/internal";
4
+ import { type MediaVideoPlayerPlaybackFeedback } from "./use-media-video-player-presentation.js";
5
+ import type { MediaVideoPlayerProgressSnapshot } from "@hexdn/playback/internal";
6
+ export type { MediaVideoPlayerDebugSnapshot, MediaVideoPlayerCopy, MediaVideoPlayerProgressReason, MediaVideoPlayerProgressSnapshot, } from "@hexdn/playback/internal";
7
+ export { formatMediaVideoTime } from "@hexdn/playback/internal";
8
+ export type { MediaVideoPlayerPlaybackFeedback } from "./use-media-video-player-presentation.js";
9
+ export type MediaVideoPlayerQualityEvent = PlaybackQualityEvent;
10
+ export declare function useMediaVideoPlayer(input: {
11
+ readonly copy?: MediaVideoPlayerCopy | undefined;
12
+ readonly debugEnabled?: boolean | undefined;
13
+ readonly initialPositionMs?: number | undefined;
14
+ readonly onBack?: (() => void) | undefined;
15
+ readonly onProgress?: ((snapshot: MediaVideoPlayerProgressSnapshot) => void) | undefined;
16
+ readonly onIntent?: (() => void) | undefined;
17
+ readonly onQualityEvent?: ((event: PlaybackQualityEvent) => void) | undefined;
18
+ readonly getSource?: PlaybackSourceResolver | undefined;
19
+ readonly onProtectedPlaybackRefresh?: (() => Promise<PlaybackSource | null | undefined>) | undefined;
20
+ readonly playbackIdentity: string | undefined;
21
+ readonly autoPlay: boolean;
22
+ readonly requestAuthorization?: PlaybackRequestAuthorization | undefined;
23
+ readonly previewThumbnails?: import("@hexdn/playback").VideoPreviewThumbnails | undefined;
24
+ readonly sourceUrl: string;
25
+ readonly expiresAt?: string | undefined;
26
+ }): {
27
+ playerRef: import("react").RefObject<HTMLDivElement | null>;
28
+ videoRef: import("react").RefObject<HTMLVideoElement | null>;
29
+ debug: MediaVideoPlayerDebugSnapshot | null;
30
+ source: PlaybackSource | null;
31
+ derived: {
32
+ bufferProgress: string;
33
+ chromeHasEntered: boolean;
34
+ controlsAreVisible: boolean;
35
+ effectiveVolume: number;
36
+ hasSeekableDuration: boolean;
37
+ seekProgress: string;
38
+ volumeProgress: string;
39
+ };
40
+ state: {
41
+ captionsAvailable: boolean;
42
+ captionsEnabled: boolean;
43
+ captionText: string | null;
44
+ currentTime: number;
45
+ duration: number;
46
+ error: MediaVideoPlayerError | null;
47
+ isAutoplayPending: boolean;
48
+ isBuffering: boolean;
49
+ isFullscreen: boolean;
50
+ isLoading: boolean;
51
+ isMuted: boolean;
52
+ isPlaying: boolean;
53
+ isPresentationReady: boolean;
54
+ playbackFeedback: MediaVideoPlayerPlaybackFeedback | null;
55
+ };
56
+ handlers: {
57
+ holdControls: () => void;
58
+ holdExtensionControls: () => void;
59
+ holdVolumeControls: () => void;
60
+ releaseControls: () => void;
61
+ releaseExtensionControls: () => void;
62
+ releaseVolumeControls: () => void;
63
+ showControls: () => void;
64
+ commitVolumePreference: () => void | undefined;
65
+ handleVolumeChange: (event: ChangeEvent<HTMLInputElement>) => void;
66
+ setVolumeFromRatio: (ratio: number) => void;
67
+ toggleMute: () => void | undefined;
68
+ retryPlayback: () => Promise<void>;
69
+ handleBack: () => Promise<void>;
70
+ handleSeekChange: (event: ChangeEvent<HTMLInputElement>) => void;
71
+ handleVideoClick: (event: MouseEvent<HTMLVideoElement>) => void;
72
+ handleVideoDoubleClick: (event: MouseEvent<HTMLVideoElement>) => void;
73
+ seekBackward: () => void;
74
+ seekForward: () => void;
75
+ seekToTime: (time: number) => void;
76
+ toggleCaptions: () => void;
77
+ toggleFullscreen: () => Promise<void>;
78
+ togglePlayback: () => Promise<void>;
79
+ clearVideoClickTimer: () => void;
80
+ };
81
+ };
@@ -0,0 +1,263 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState, } from "react";
3
+ import { createPlaybackController, } from "@hexdn/playback";
4
+ import { resolveMediaVideoPlayerError, } from "@hexdn/playback/internal";
5
+ import { resolveMediaVideoPlayerViewState } from "./media-video-player-view-state.js";
6
+ import { usePlayerHost } from "./player-host.js";
7
+ import { useMediaVideoPlayerControls } from "./use-media-video-player-controls.js";
8
+ import { useMediaVideoPlayerFullscreen, useMediaVideoPlayerFullscreenLifecycle, } from "./use-media-video-player-fullscreen.js";
9
+ import { useMediaVideoPlayerInteractions } from "./use-media-video-player-interactions.js";
10
+ import { useMediaVideoPlayerPresentation, useMediaVideoPlayerChromeEntryLifecycle, } from "./use-media-video-player-presentation.js";
11
+ export { formatMediaVideoTime } from "@hexdn/playback/internal";
12
+ const defaultMediaVideoPlayerCopy = {
13
+ fullscreenUnavailableError: "Fullscreen is not available.",
14
+ playbackFailedError: "Video playback failed. Try again.",
15
+ playbackStartFailedError: "Video playback could not start.",
16
+ protectedPlaybackUnsupportedError: "Protected playback is not supported in this browser.",
17
+ };
18
+ const initialState = {
19
+ source: null,
20
+ currentTime: 0,
21
+ duration: 0,
22
+ bufferedEnd: 0,
23
+ errorCode: null,
24
+ isPlaying: false,
25
+ isAutoplayPending: false,
26
+ isMediaReady: false,
27
+ isMediaWaiting: true,
28
+ isPresentationReady: false,
29
+ captionsAvailable: false,
30
+ captionsEnabled: false,
31
+ captionText: null,
32
+ volume: 1,
33
+ isMuted: false,
34
+ };
35
+ export function useMediaVideoPlayer(input) {
36
+ const playerRef = useRef(null), videoRef = useRef(null), controllerRef = useRef(null);
37
+ const hasPresentedMediaFrameRef = useRef(false);
38
+ const [snapshot, setSnapshot] = useState(initialState);
39
+ const { isFullscreen, setIsFullscreen } = useMediaVideoPlayerFullscreen();
40
+ const controls = useMediaVideoPlayerControls({
41
+ hasPresentedMediaFrameRef,
42
+ playerRef,
43
+ videoRef,
44
+ });
45
+ const presentation = useMediaVideoPlayerPresentation({
46
+ hasPresentedMediaFrameRef,
47
+ });
48
+ const [fullscreenError, setFullscreenError] = useState(false), [debug, setDebug] = useState(null);
49
+ const host = usePlayerHost();
50
+ const latest = useRef({ input, controls, presentation, host });
51
+ useLayoutEffect(() => {
52
+ latest.current = { input, controls, presentation, host };
53
+ });
54
+ const canRefresh = Boolean(input.onProtectedPlaybackRefresh);
55
+ const { clearPlaybackFeedbackTimer, setPlaybackFeedback } = presentation;
56
+ const { transition, loadHlsEngine, preferences } = host;
57
+ useEffect(() => {
58
+ const video = videoRef.current;
59
+ if (!video)
60
+ return;
61
+ const current = latest.current.input;
62
+ const resolve = current.getSource ??
63
+ (current.onProtectedPlaybackRefresh
64
+ ? async () => {
65
+ const next = await latest.current.input.onProtectedPlaybackRefresh?.();
66
+ if (!next)
67
+ throw new Error("Playback refresh returned no source.");
68
+ return next;
69
+ }
70
+ : undefined);
71
+ const controller = createPlaybackController(video, {
72
+ source: current.sourceUrl
73
+ ? {
74
+ sourceUrl: current.sourceUrl,
75
+ expiresAt: current.expiresAt,
76
+ requestAuthorization: current.requestAuthorization,
77
+ previewThumbnails: current.previewThumbnails,
78
+ }
79
+ : undefined,
80
+ getSource: resolve,
81
+ identity: current.playbackIdentity,
82
+ autoPlay: current.autoPlay,
83
+ initialPositionMs: current.initialPositionMs,
84
+ transition,
85
+ loadHlsEngine,
86
+ preferences,
87
+ onIntent: () => latest.current.input.onIntent?.(),
88
+ onProgress: (event) => latest.current.input.onProgress?.(event),
89
+ onQualityEvent: (event) => latest.current.input.onQualityEvent?.(event),
90
+ onNativeFullscreenChange: setIsFullscreen,
91
+ onControlsRequest: (persistent) => persistent
92
+ ? latest.current.controls.showControlsPersistently()
93
+ : latest.current.controls.showControls(),
94
+ });
95
+ controllerRef.current = controller;
96
+ const sync = () => {
97
+ hasPresentedMediaFrameRef.current =
98
+ controller.getSnapshot().isPresentationReady;
99
+ setSnapshot(controller.getSnapshot());
100
+ };
101
+ sync();
102
+ const unsubscribe = controller.subscribe(sync);
103
+ return () => {
104
+ unsubscribe();
105
+ controller.destroy();
106
+ if (controllerRef.current === controller)
107
+ controllerRef.current = null;
108
+ };
109
+ }, [
110
+ transition,
111
+ loadHlsEngine,
112
+ preferences,
113
+ input.getSource,
114
+ canRefresh,
115
+ setIsFullscreen,
116
+ ]);
117
+ const applied = useRef(null);
118
+ useEffect(() => {
119
+ const previous = applied.current;
120
+ const next = {
121
+ sourceUrl: input.sourceUrl,
122
+ expiresAt: input.expiresAt,
123
+ authorization: input.requestAuthorization,
124
+ previews: input.previewThumbnails,
125
+ identity: input.playbackIdentity,
126
+ initialPositionMs: input.initialPositionMs,
127
+ autoPlay: input.autoPlay,
128
+ };
129
+ applied.current = next;
130
+ if (!input.sourceUrl ||
131
+ !previous ||
132
+ (previous.sourceUrl === next.sourceUrl &&
133
+ previous.expiresAt === next.expiresAt &&
134
+ previous.authorization === next.authorization &&
135
+ previous.previews === next.previews &&
136
+ previous.identity === next.identity &&
137
+ previous.initialPositionMs === next.initialPositionMs &&
138
+ previous.autoPlay === next.autoPlay))
139
+ return;
140
+ controllerRef.current?.setSource({
141
+ sourceUrl: input.sourceUrl,
142
+ expiresAt: input.expiresAt,
143
+ requestAuthorization: input.requestAuthorization,
144
+ previewThumbnails: input.previewThumbnails,
145
+ }, {
146
+ identity: input.playbackIdentity,
147
+ initialPositionMs: (previous.identity ??
148
+ controllerRef.current?.getSnapshot().source?.playbackId) !==
149
+ next.identity ||
150
+ previous.initialPositionMs !== next.initialPositionMs
151
+ ? input.initialPositionMs
152
+ : undefined,
153
+ autoPlay: input.autoPlay,
154
+ });
155
+ clearPlaybackFeedbackTimer();
156
+ setPlaybackFeedback(null);
157
+ }, [
158
+ input.sourceUrl,
159
+ input.expiresAt,
160
+ input.requestAuthorization,
161
+ input.previewThumbnails,
162
+ input.playbackIdentity,
163
+ input.initialPositionMs,
164
+ input.autoPlay,
165
+ clearPlaybackFeedbackTimer,
166
+ setPlaybackFeedback,
167
+ ]);
168
+ const onFullscreenError = useCallback(() => setFullscreenError(true), []);
169
+ const interactions = useMediaVideoPlayerInteractions({
170
+ controllerRef,
171
+ videoRef,
172
+ playerRef,
173
+ hasPresentedMediaFrameRef,
174
+ isControlsVisible: controls.isControlsVisible,
175
+ onBack: input.onBack,
176
+ showControls: controls.showControls,
177
+ showPlaybackFeedback: presentation.showPlaybackFeedback,
178
+ onFullscreenError,
179
+ });
180
+ useMediaVideoPlayerFullscreenLifecycle({ playerRef, setIsFullscreen });
181
+ useMediaVideoPlayerChromeEntryLifecycle({
182
+ chromeEntryScheduleRef: presentation.chromeEntryScheduleRef,
183
+ hasError: Boolean(snapshot.errorCode),
184
+ isAutoplayPending: snapshot.isAutoplayPending,
185
+ isMediaReady: snapshot.isMediaReady,
186
+ setHasChromeEntered: presentation.setHasChromeEntered,
187
+ sourceUrl: snapshot.source?.sourceUrl ?? input.sourceUrl,
188
+ });
189
+ useEffect(() => {
190
+ if (!input.debugEnabled) {
191
+ return;
192
+ }
193
+ const capture = () => setDebug(controllerRef.current?.getDebugSnapshot() ?? null);
194
+ // Snapshot browser media state before the first polling interval.
195
+ capture();
196
+ const timer = window.setInterval(capture, 500);
197
+ return () => window.clearInterval(timer);
198
+ }, [input.debugEnabled]);
199
+ const error = resolveMediaVideoPlayerError(snapshot.errorCode ?? (fullscreenError ? "fullscreen_unavailable" : null), input.copy ?? defaultMediaVideoPlayerCopy);
200
+ const viewState = resolveMediaVideoPlayerViewState({
201
+ ...snapshot,
202
+ hasError: Boolean(error),
203
+ isControlsVisible: controls.isControlsVisible,
204
+ });
205
+ return {
206
+ playerRef,
207
+ videoRef,
208
+ debug: input.debugEnabled ? debug : null,
209
+ source: snapshot.source,
210
+ derived: {
211
+ bufferProgress: viewState.bufferProgress,
212
+ chromeHasEntered: presentation.hasChromeEntered,
213
+ controlsAreVisible: viewState.controlsAreVisible,
214
+ effectiveVolume: viewState.effectiveVolume,
215
+ hasSeekableDuration: viewState.hasSeekableDuration,
216
+ seekProgress: viewState.seekProgress,
217
+ volumeProgress: viewState.volumeProgress,
218
+ },
219
+ state: {
220
+ captionsAvailable: snapshot.captionsAvailable,
221
+ captionsEnabled: snapshot.captionsEnabled,
222
+ captionText: snapshot.captionText,
223
+ currentTime: snapshot.currentTime,
224
+ duration: snapshot.duration,
225
+ error,
226
+ isAutoplayPending: snapshot.isAutoplayPending,
227
+ isBuffering: viewState.isBuffering,
228
+ isFullscreen,
229
+ isLoading: viewState.isLoading,
230
+ isMuted: snapshot.isMuted,
231
+ isPlaying: snapshot.isPlaying,
232
+ isPresentationReady: snapshot.isPresentationReady,
233
+ playbackFeedback: presentation.playbackFeedback,
234
+ },
235
+ handlers: {
236
+ ...interactions,
237
+ holdControls: controls.holdControls,
238
+ holdExtensionControls: controls.holdExtensionControls,
239
+ holdVolumeControls: controls.holdVolumeControls,
240
+ releaseControls: controls.releaseControls,
241
+ releaseExtensionControls: controls.releaseExtensionControls,
242
+ releaseVolumeControls: controls.releaseVolumeControls,
243
+ showControls: controls.showControls,
244
+ commitVolumePreference: () => controllerRef.current?.commitVolumePreference(),
245
+ handleVolumeChange: (event) => {
246
+ controllerRef.current?.setVolume(Number(event.currentTarget.value));
247
+ controllerRef.current?.commitVolumePreference();
248
+ controls.showControls();
249
+ },
250
+ setVolumeFromRatio: (ratio) => {
251
+ controllerRef.current?.setVolume(ratio);
252
+ controls.showControls();
253
+ },
254
+ toggleMute: () => controllerRef.current?.toggleMute(),
255
+ retryPlayback: async () => {
256
+ setFullscreenError(false);
257
+ presentation.clearPlaybackFeedbackTimer();
258
+ presentation.setPlaybackFeedback(null);
259
+ await controllerRef.current?.retry();
260
+ },
261
+ },
262
+ };
263
+ }