@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,293 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { usePlayerAnalytics, } from "./use-player-analytics.js";
4
+ import { createPlayerTranslator } from "./player-labels.js";
5
+ import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
6
+ import { Button } from "./ui-primitives.js";
7
+ import { DsIcon, resolveDsVolumeLevel, } from "./player-icon.js";
8
+ import { EmptyState } from "./ui-primitives.js";
9
+ import { Notice } from "./ui-primitives.js";
10
+ import { cn } from "./utils.js";
11
+ import styles from "./media-video-player.module.css";
12
+ import { areVideoViewportInsetsEqual, buildCaptionViewportStyle, buildVolumeSliderStyle, clampScrubberRatio, emptyVideoViewportInsets, readVideoViewportInsets, } from "./media-video-player-layout.js";
13
+ import { MediaVideoPlayerScrubber } from "./media-video-player-scrubber.js";
14
+ import { PlaybackLoadingIndicator } from "./playback-loading-indicator.js";
15
+ import { PlayerControlPopover } from "./player-control-popover.js";
16
+ import { formatMediaVideoTime, useMediaVideoPlayer, } from "./use-media-video-player.js";
17
+ const playbackFeedbackIconNames = {
18
+ "back-10s": "back10",
19
+ "fwd-10s": "fwd10",
20
+ pause: "pause",
21
+ play: "play",
22
+ };
23
+ function MediaVideoPlayPauseGlyph({ isPlaying, }) {
24
+ return (_jsxs("span", { "aria-hidden": "true", className: styles.playPauseGlyph, "data-hexdn-state": isPlaying ? "playing" : "paused", children: [_jsx("span", { className: cn(styles.playPauseGlyphIcon, styles.playPauseGlyphPlay), children: _jsx(DsIcon, { name: "play" }) }), _jsx("span", { className: cn(styles.playPauseGlyphIcon, styles.playPauseGlyphPause), children: _jsx(DsIcon, { name: "pause" }) })] }));
25
+ }
26
+ export function ReadyPlayer({ autoPlay = true, getSource, analytics, style, sourceError, onRetrySource, className, initialPositionMs, onBack, onProgress, onQualityEvent, onProtectedPlaybackRefresh, playbackIdentity, posterUrl, previewThumbnails, requestAuthorization, sourceUrl, expiresAt, subtitle, title = "Video", labels, slots, debugControls = false, loadDebugHud, }) {
27
+ const t = useMemo(() => createPlayerTranslator(labels), [labels]);
28
+ const debugControlVisible = debugControls && Boolean(loadDebugHud);
29
+ const [debugRequested, setDebugRequested] = useState(false);
30
+ const [DebugHud, setDebugHud] = useState(null);
31
+ const debugEnabled = debugControlVisible && debugRequested;
32
+ const toggleDebug = useCallback(() => {
33
+ if (!debugControlVisible) {
34
+ return;
35
+ }
36
+ if (debugRequested) {
37
+ setDebugRequested(false);
38
+ return;
39
+ }
40
+ setDebugRequested(true);
41
+ if (DebugHud) {
42
+ return;
43
+ }
44
+ void loadDebugHud?.()
45
+ .then((component) => {
46
+ if (!component) {
47
+ setDebugRequested(false);
48
+ return;
49
+ }
50
+ setDebugHud(() => component);
51
+ })
52
+ .catch(() => {
53
+ // Diagnostics are optional and must never take down core playback.
54
+ setDebugRequested(false);
55
+ });
56
+ }, [DebugHud, debugControlVisible, debugRequested, loadDebugHud]);
57
+ const analyticsMonitorRef = useRef(null);
58
+ const handleIntent = useCallback(() => {
59
+ analyticsMonitorRef.current?.intent();
60
+ }, []);
61
+ const handleQualityEvent = useCallback((event) => {
62
+ if (event.type === "first-frame")
63
+ analyticsMonitorRef.current?.recordFirstFrame();
64
+ if (event.type === "error")
65
+ analyticsMonitorRef.current?.recordError(event.errorCode, {
66
+ fatal: event.errorCode !== "fullscreen_unavailable",
67
+ });
68
+ onQualityEvent?.(event);
69
+ }, [onQualityEvent]);
70
+ const [videoViewportInsets, setVideoViewportInsets] = useState(emptyVideoViewportInsets);
71
+ const volumePointerIdRef = useRef(null);
72
+ const playerCopy = useMemo(() => ({
73
+ fullscreenUnavailableError: t("playback.fullscreenUnavailableError"),
74
+ playbackFailedError: t("playback.playbackFailedError"),
75
+ playbackStartFailedError: t("playback.playbackStartFailedError"),
76
+ protectedPlaybackUnsupportedError: t("playback.protectedPlaybackUnsupportedError"),
77
+ }), [t]);
78
+ const { debug, derived, handlers, playerRef, state, videoRef, source: currentSource, } = useMediaVideoPlayer({
79
+ copy: playerCopy,
80
+ debugEnabled,
81
+ initialPositionMs,
82
+ getSource,
83
+ onBack,
84
+ onIntent: handleIntent,
85
+ onProgress,
86
+ onQualityEvent: handleQualityEvent,
87
+ onProtectedPlaybackRefresh,
88
+ playbackIdentity: playbackIdentity ?? (sourceUrl || undefined),
89
+ autoPlay,
90
+ requestAuthorization,
91
+ previewThumbnails,
92
+ sourceUrl,
93
+ expiresAt,
94
+ });
95
+ const playbackSourceState = currentSource ?? {
96
+ sourceUrl,
97
+ expiresAt,
98
+ requestAuthorization,
99
+ previewThumbnails,
100
+ };
101
+ usePlayerAnalytics({
102
+ analytics,
103
+ autoPlay,
104
+ monitorRef: analyticsMonitorRef,
105
+ sourceError,
106
+ videoRef,
107
+ });
108
+ const volumeSliderStyle = buildVolumeSliderStyle(derived.effectiveVolume, derived.volumeProgress);
109
+ const volumeLevel = resolveDsVolumeLevel(derived.effectiveVolume);
110
+ const captionViewportStyle = buildCaptionViewportStyle(videoViewportInsets);
111
+ const captionLines = useMemo(() => {
112
+ if (!state.captionText) {
113
+ return [];
114
+ }
115
+ const seenLines = new Map();
116
+ return state.captionText
117
+ .split("\n")
118
+ .map((line) => line.trim())
119
+ .filter(Boolean)
120
+ .map((line) => {
121
+ const occurrence = seenLines.get(line) ?? 0;
122
+ seenLines.set(line, occurrence + 1);
123
+ return {
124
+ id: occurrence === 0 ? line : `${line}:${occurrence}`,
125
+ text: line,
126
+ };
127
+ });
128
+ }, [state.captionText]);
129
+ const terminalError = sourceError
130
+ ? {
131
+ code: "playback_start_failed",
132
+ message: sourceError,
133
+ terminal: true,
134
+ retryable: true,
135
+ }
136
+ : state.error?.terminal
137
+ ? state.error
138
+ : null;
139
+ const retryPlayback = sourceError && onRetrySource ? onRetrySource : handlers.retryPlayback;
140
+ const controlError = state.error && !state.error.terminal ? state.error : null;
141
+ useEffect(() => {
142
+ if (!debugControlVisible) {
143
+ // Close optional diagnostics when the host removes their capability.
144
+ // eslint-disable-next-line react-hooks/set-state-in-effect
145
+ setDebugRequested(false);
146
+ }
147
+ }, [debugControlVisible]);
148
+ useEffect(() => {
149
+ const video = videoRef.current;
150
+ if (!video) {
151
+ setVideoViewportInsets(emptyVideoViewportInsets);
152
+ return;
153
+ }
154
+ const targetVideo = video;
155
+ let frameId = null;
156
+ function syncVideoViewportInsets() {
157
+ frameId = null;
158
+ setVideoViewportInsets((currentInsets) => {
159
+ const nextInsets = readVideoViewportInsets(targetVideo);
160
+ return areVideoViewportInsetsEqual(currentInsets, nextInsets)
161
+ ? currentInsets
162
+ : nextInsets;
163
+ });
164
+ }
165
+ function scheduleVideoViewportSync() {
166
+ if (frameId !== null) {
167
+ return;
168
+ }
169
+ frameId = window.requestAnimationFrame(syncVideoViewportInsets);
170
+ }
171
+ const resizeObserver = typeof ResizeObserver === "undefined"
172
+ ? null
173
+ : new ResizeObserver(scheduleVideoViewportSync);
174
+ resizeObserver?.observe(targetVideo);
175
+ targetVideo.addEventListener("emptied", scheduleVideoViewportSync);
176
+ targetVideo.addEventListener("loadedmetadata", scheduleVideoViewportSync);
177
+ targetVideo.addEventListener("loadstart", scheduleVideoViewportSync);
178
+ targetVideo.addEventListener("resize", scheduleVideoViewportSync);
179
+ window.addEventListener("resize", scheduleVideoViewportSync);
180
+ scheduleVideoViewportSync();
181
+ return () => {
182
+ if (frameId !== null) {
183
+ window.cancelAnimationFrame(frameId);
184
+ }
185
+ resizeObserver?.disconnect();
186
+ targetVideo.removeEventListener("emptied", scheduleVideoViewportSync);
187
+ targetVideo.removeEventListener("loadedmetadata", scheduleVideoViewportSync);
188
+ targetVideo.removeEventListener("loadstart", scheduleVideoViewportSync);
189
+ targetVideo.removeEventListener("resize", scheduleVideoViewportSync);
190
+ window.removeEventListener("resize", scheduleVideoViewportSync);
191
+ };
192
+ }, [videoRef]);
193
+ const { setVolumeFromRatio, commitVolumePreference } = handlers;
194
+ const readVolumeRatio = useCallback((event) => {
195
+ const rect = event.currentTarget.getBoundingClientRect();
196
+ if (rect.height <= 0) {
197
+ return null;
198
+ }
199
+ return clampScrubberRatio((rect.bottom - event.clientY) / rect.height);
200
+ }, []);
201
+ const setVolumeFromPointer = useCallback((event) => {
202
+ const ratio = readVolumeRatio(event);
203
+ if (ratio === null) {
204
+ return;
205
+ }
206
+ setVolumeFromRatio(ratio);
207
+ }, [setVolumeFromRatio, readVolumeRatio]);
208
+ const handleVolumePointerDown = useCallback((event) => {
209
+ if (event.button !== 0) {
210
+ return;
211
+ }
212
+ event.preventDefault();
213
+ event.currentTarget.setPointerCapture(event.pointerId);
214
+ volumePointerIdRef.current = event.pointerId;
215
+ setVolumeFromPointer(event);
216
+ }, [setVolumeFromPointer]);
217
+ const handleVolumePointerMove = useCallback((event) => {
218
+ if (volumePointerIdRef.current !== event.pointerId) {
219
+ return;
220
+ }
221
+ setVolumeFromPointer(event);
222
+ }, [setVolumeFromPointer]);
223
+ const releaseVolumePointer = useCallback((event) => {
224
+ if (volumePointerIdRef.current !== event.pointerId) {
225
+ return;
226
+ }
227
+ setVolumeFromPointer(event);
228
+ commitVolumePreference();
229
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
230
+ event.currentTarget.releasePointerCapture(event.pointerId);
231
+ }
232
+ volumePointerIdRef.current = null;
233
+ }, [commitVolumePreference, setVolumeFromPointer]);
234
+ const cancelVolumePointer = useCallback((event) => {
235
+ if (volumePointerIdRef.current !== event.pointerId) {
236
+ return;
237
+ }
238
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
239
+ event.currentTarget.releasePointerCapture(event.pointerId);
240
+ }
241
+ volumePointerIdRef.current = null;
242
+ }, []);
243
+ const slotControls = {
244
+ showControls: handlers.showControls,
245
+ holdControls: handlers.holdExtensionControls,
246
+ releaseControls: handlers.releaseExtensionControls,
247
+ };
248
+ return (_jsxs("div", { "aria-busy": state.isLoading ? true : undefined, className: cn(styles.player, className), "data-hexdn-chrome-entered": derived.chromeHasEntered ? "true" : "false", "data-hexdn-controls-visible": derived.controlsAreVisible ? "true" : "false", "data-hexdn-presentation-ready": state.isPresentationReady ? "true" : "false", "data-hexdn-terminal-error": terminalError ? "true" : "false", "data-hexdn-player-control-popover-boundary": "", onPointerEnter: handlers.showControls, ref: playerRef, style: style, tabIndex: -1, children: [_jsx("video", { "aria-label": title, className: styles.video, inert: state.isPresentationReady ? undefined : true, onClick: handlers.handleVideoClick, onDoubleClick: handlers.handleVideoDoubleClick, onPointerDown: () => {
249
+ playerRef.current?.focus({ preventScroll: true });
250
+ handlers.showControls();
251
+ }, onPointerMove: handlers.showControls, disablePictureInPicture: true, playsInline: true, poster: posterUrl, preload: autoPlay ? "auto" : "metadata", ref: videoRef }), _jsx("span", { "aria-hidden": "true", className: styles.presentationCurtain }), state.isPresentationReady && state.playbackFeedback ? (_jsx("div", { "aria-hidden": "true", className: styles.playbackFeedback, children: _jsx("span", { className: cn(styles.playbackFeedbackIcon, styles.glassSurface), children: _jsx(DsIcon, { name: playbackFeedbackIconNames[state.playbackFeedback.action] }) }) }, state.playbackFeedback.id)) : null, _jsxs("div", { className: styles.chrome, inert: state.isPresentationReady && !terminalError ? undefined : true, children: [_jsxs("div", { className: styles.top, children: [onBack ? (_jsx("button", { "aria-label": t("playback.backAction"), className: styles.backButton, onFocus: handlers.showControls, onPointerDown: handlers.showControls, onPointerMove: handlers.showControls, onClick: () => {
252
+ void handlers.handleBack();
253
+ }, type: "button", children: _jsx(DsIcon, { name: "chevron-l" }) })) : null, onBack ? (_jsxs("button", { "aria-label": t("playback.backAction"), className: cn(styles.titleBlock, styles.titleBackTarget), onFocus: handlers.showControls, onPointerDown: handlers.showControls, onPointerMove: handlers.showControls, onClick: () => {
254
+ void handlers.handleBack();
255
+ }, type: "button", children: [_jsx("span", { className: styles.title, children: title }), subtitle ? (_jsx("span", { className: styles.subtitle, children: subtitle })) : null] })) : (_jsxs("div", { className: styles.titleBlock, children: [_jsx("p", { className: styles.title, children: title }), subtitle ? _jsx("p", { className: styles.subtitle, children: subtitle }) : null] })), slots?.headerTrailing?.(slotControls), debugControlVisible ? (_jsx("div", { className: styles.topActions, children: _jsx("button", { "aria-label": debugEnabled
256
+ ? t("playback.hideStatsAction")
257
+ : t("playback.showStatsAction"), "aria-pressed": debugEnabled, className: styles.controlButton, "data-hexdn-active": debugEnabled ? "true" : "false", onFocus: handlers.showControls, onPointerDown: handlers.showControls, onPointerMove: handlers.showControls, onClick: toggleDebug, type: "button", children: _jsx(DsIcon, { name: "info" }) }) })) : null] }), _jsx("div", { className: styles.center, children: _jsx(PlaybackLoadingIndicator, { content: slots?.loading, active: state.isLoading, deferUntilTransitionIdle: !derived.chromeHasEntered, delayMs: state.isPresentationReady ? 0 : undefined }) }), derived.chromeHasEntered &&
258
+ state.isPresentationReady &&
259
+ captionLines.length > 0 ? (_jsx("div", { "aria-hidden": "true", className: styles.captions, style: captionViewportStyle, children: captionLines.map((line) => (_jsx("span", { className: styles.captionLine, children: line.text }, line.id))) })) : null, _jsxs("div", { className: styles.bottom, onPointerEnter: handlers.holdControls, onPointerLeave: handlers.releaseControls, children: [_jsx("div", { className: styles.meta, children: _jsxs("p", { className: styles.time, children: [_jsx("span", { children: formatMediaVideoTime(state.currentTime) }), _jsx("span", { "aria-hidden": "true", className: styles.timeSeparator, children: "/" }), _jsx("span", { className: styles.timeMuted, children: formatMediaVideoTime(state.duration) })] }) }), _jsx(MediaVideoPlayerScrubber, { bufferProgress: derived.bufferProgress, currentTime: state.currentTime, duration: state.duration, hasSeekableDuration: derived.hasSeekableDuration, onSeekChange: handlers.handleSeekChange, onSeekToTime: handlers.seekToTime, onShowControls: handlers.showControls, previewThumbnails: playbackSourceState.previewThumbnails, requestAuthorization: playbackSourceState.requestAuthorization, seekLabel: t("playback.seekLabel"), seekProgress: derived.seekProgress, seekValueText: t("playback.seekValueText", {
260
+ current: formatMediaVideoTime(state.currentTime),
261
+ duration: formatMediaVideoTime(state.duration),
262
+ }) }), _jsxs("div", { className: styles.controls, children: [_jsx("div", { className: styles.controlsLeft, children: slots?.leadingControls?.(slotControls) }), _jsxs("div", { className: styles.controlsCenter, children: [_jsx("button", { "aria-label": t("playback.rewind10Action"), className: styles.controlButton, disabled: !derived.hasSeekableDuration, onFocus: handlers.showControls, onPointerDown: handlers.showControls, onPointerMove: handlers.showControls, onClick: handlers.seekBackward, type: "button", children: _jsx(DsIcon, { name: "back10" }) }), _jsx("button", { "aria-label": t(state.isPlaying
263
+ ? "playback.pauseVideoAction"
264
+ : "playback.playVideoAction"), className: cn(styles.controlButton, styles.primaryControlButton), onFocus: handlers.showControls, onPointerDown: handlers.showControls, onPointerMove: handlers.showControls, onClick: () => {
265
+ void handlers.togglePlayback();
266
+ }, type: "button", children: _jsx(MediaVideoPlayPauseGlyph, { isPlaying: state.isPlaying }) }), _jsx("button", { "aria-label": t("playback.forward10Action"), className: styles.controlButton, disabled: !derived.hasSeekableDuration, onFocus: handlers.showControls, onPointerDown: handlers.showControls, onPointerMove: handlers.showControls, onClick: handlers.seekForward, type: "button", children: _jsx(DsIcon, { name: "fwd10" }) })] }), _jsxs("div", { className: styles.controlsRight, children: [_jsx(PlayerControlPopover, { align: "center", className: styles.volume, content: _jsxs("span", { className: styles.volumeTrack, onPointerCancel: cancelVolumePointer, onPointerDown: handleVolumePointerDown, onPointerMove: handleVolumePointerMove, onPointerUp: releaseVolumePointer, style: volumeSliderStyle, children: [_jsx("span", { className: styles.volumeFill }), _jsx("input", { "aria-label": t("playback.volumeLabel"), className: styles.volumeSlider, max: 1, min: 0, onChange: handlers.handleVolumeChange, onFocus: handlers.showControls, step: 0.01, type: "range", value: derived.effectiveVolume })] }), contentClassName: styles.volumePopover, onInteraction: handlers.holdVolumeControls, onInteractionEnd: handlers.releaseVolumeControls, openDelayMs: 0, children: _jsx("button", { "aria-label": t(state.isMuted
267
+ ? "playback.unmuteVideoAction"
268
+ : "playback.muteVideoAction"), className: styles.controlButton, onFocus: handlers.showControls, onPointerDown: handlers.showControls, onPointerMove: handlers.showControls, onClick: handlers.toggleMute, type: "button", children: _jsx(DsIcon, { name: "volume", volumeLevel: volumeLevel }) }) }), state.captionsAvailable ? (_jsx("button", { "aria-label": t(state.captionsEnabled
269
+ ? "playback.disableCaptionsAction"
270
+ : "playback.enableCaptionsAction"), "aria-pressed": state.captionsEnabled, className: styles.controlButton, "data-hexdn-active": state.captionsEnabled ? "true" : "false", onFocus: handlers.showControls, onPointerDown: handlers.showControls, onPointerMove: handlers.showControls, onClick: handlers.toggleCaptions, type: "button", children: _jsx(DsIcon, { name: "captions" }) })) : null, _jsx("button", { "aria-label": state.isFullscreen
271
+ ? t("playback.exitFullscreenAction")
272
+ : t("playback.enterFullscreenAction"), className: styles.controlButton, onFocus: handlers.showControls, onPointerDown: handlers.showControls, onPointerMove: handlers.showControls, onClick: () => {
273
+ void handlers.toggleFullscreen();
274
+ }, type: "button", children: _jsx(DsIcon, { name: "fullscreen" }) })] })] })] })] }), terminalError
275
+ ? (slots?.terminalError?.({
276
+ error: terminalError,
277
+ retry: retryPlayback,
278
+ back: onBack ? handlers.handleBack : undefined,
279
+ }) ?? (_jsx(EmptyState, { action: terminalError.retryable || onBack ? (_jsxs(_Fragment, { children: [terminalError.retryable ? (_jsx(Button, { onClick: () => {
280
+ void retryPlayback();
281
+ }, size: "md", type: "button", children: t("playback.retryAction") })) : null, onBack ? (_jsx(Button, { onClick: () => {
282
+ void handlers.handleBack();
283
+ }, size: "md", type: "button", variant: "ghost", children: t("playback.backAction") })) : null] })) : undefined, body: terminalError.code === "protected_playback_unsupported"
284
+ ? terminalError.message
285
+ : t("playback.sessionUnavailableBody"), className: styles.terminalError, icon: _jsx(DsIcon, { name: "error" }), role: "alert", title: t("playback.sessionUnavailableTitle") })))
286
+ : null, controlError
287
+ ? (slots?.controlError?.(controlError.message) ?? (_jsx(Notice, { className: styles.controlError, inline: true, role: "alert", children: controlError.message })))
288
+ : null, debugEnabled &&
289
+ DebugHud &&
290
+ debug &&
291
+ state.isPresentationReady &&
292
+ !terminalError ? (_jsx(DebugHud, { debug: debug })) : null] }));
293
+ }
@@ -0,0 +1,2 @@
1
+ import { type HTMLAttributes } from "react";
2
+ export declare const TooltipSurface: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLSpanElement> & import("react").RefAttributes<HTMLSpanElement>>;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { cn } from "./utils.js";
4
+ import styles from "./ui.module.css";
5
+ export const TooltipSurface = forwardRef(function TooltipSurface({ className, ...props }, ref) {
6
+ return (_jsx("span", { className: cn("hexdn-tooltip-surface", styles.tooltipSurface, className), ref: ref, ...props }));
7
+ });
8
+ TooltipSurface.displayName = "TooltipSurface";
@@ -0,0 +1,17 @@
1
+ import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react";
2
+ export declare function Button({ className, variant, size, ...props }: ButtonHTMLAttributes<HTMLButtonElement> & {
3
+ readonly variant?: "ghost" | undefined;
4
+ readonly size?: "md" | undefined;
5
+ }): import("react").JSX.Element;
6
+ export declare function EmptyState({ action, body, className, icon, role, title, }: {
7
+ readonly action?: ReactNode;
8
+ readonly body: string;
9
+ readonly className?: string | undefined;
10
+ readonly icon?: ReactNode;
11
+ readonly role?: "alert" | undefined;
12
+ readonly title: string;
13
+ }): import("react").JSX.Element;
14
+ export declare function Notice({ className, inline, ...props }: HTMLAttributes<HTMLDivElement> & {
15
+ readonly inline?: boolean | undefined;
16
+ }): import("react").JSX.Element;
17
+ export declare function LoadingIndicator(): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "./utils.js";
3
+ import styles from "./ui.module.css";
4
+ export function Button({ className, variant, size, ...props }) {
5
+ return (_jsx("button", { ...props, "data-hexdn-size": size, className: cn(styles.button, variant === "ghost" && styles.ghostButton, className) }));
6
+ }
7
+ export function EmptyState({ action, body, className, icon, role, title, }) {
8
+ return (_jsxs("div", { className: cn(styles.errorSurface, className), role: role, children: [icon, _jsx("strong", { children: title }), _jsx("p", { children: body }), _jsx("div", { className: styles.errorActions, children: action })] }));
9
+ }
10
+ export function Notice({ className, inline, ...props }) {
11
+ return (_jsx("div", { ...props, "data-hexdn-inline": inline ? "true" : undefined, className: cn(styles.notice, className) }));
12
+ }
13
+ export function LoadingIndicator() {
14
+ return _jsx("span", { "aria-hidden": "true", className: styles.spinner });
15
+ }
@@ -0,0 +1,113 @@
1
+ .icon {
2
+ display: block;
3
+ width: 1.5rem;
4
+ height: 1.5rem;
5
+ flex: none;
6
+ }
7
+
8
+ .button {
9
+ display: inline-flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ min-height: 44px;
13
+ padding: 0.6rem 1rem;
14
+ border: 1px solid transparent;
15
+ border-radius: 8px;
16
+ background: #f5f4f1;
17
+ color: #121212;
18
+ font: inherit;
19
+ cursor: pointer;
20
+ }
21
+
22
+ .button:focus-visible {
23
+ outline: 2px solid currentColor;
24
+ outline-offset: 3px;
25
+ }
26
+
27
+ .ghostButton {
28
+ color: inherit;
29
+ background: transparent;
30
+ border-color: currentColor;
31
+ }
32
+
33
+ .errorSurface {
34
+ display: flex;
35
+ flex-direction: column;
36
+ align-items: center;
37
+ justify-content: center;
38
+ gap: 1rem;
39
+ padding: 2rem;
40
+ text-align: center;
41
+ }
42
+
43
+ .errorSurface p {
44
+ margin: 0;
45
+ max-width: 30rem;
46
+ }
47
+
48
+ .errorActions {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: 0.75rem;
52
+ }
53
+
54
+ .notice {
55
+ padding: 0.75rem 1rem;
56
+ border-radius: 8px;
57
+ background: rgb(20 20 20 / 85%);
58
+ }
59
+
60
+ .spinner {
61
+ display: block;
62
+ width: 2.5rem;
63
+ height: 2.5rem;
64
+ border: 3px solid rgb(255 255 255 / 25%);
65
+ border-top-color: white;
66
+ border-radius: 50%;
67
+ animation: spin 850ms linear infinite;
68
+ }
69
+
70
+ @keyframes spin {
71
+ to {
72
+ transform: rotate(360deg);
73
+ }
74
+ }
75
+
76
+ @media (prefers-reduced-motion: reduce) {
77
+ .spinner {
78
+ animation: none;
79
+ }
80
+ }
81
+
82
+ .tooltipSurface {
83
+ padding: 0.35rem 0.55rem;
84
+ border-radius: 6px;
85
+ background: rgb(20 20 20 / 90%);
86
+ color: white;
87
+ font: inherit;
88
+ }
89
+
90
+ .icon :global(.hexdn-volume-glyph-speaker),
91
+ .icon :global(.hexdn-volume-glyph-wave),
92
+ .icon :global(.hexdn-volume-glyph-mute) {
93
+ transform-box: fill-box;
94
+ transform-origin: center;
95
+ transition:
96
+ fill-opacity 220ms cubic-bezier(0.2, 0.8, 0.2, 1),
97
+ opacity 220ms cubic-bezier(0.2, 0.8, 0.2, 1),
98
+ transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
99
+ }
100
+ .icon :global(.hexdn-volume-glyph-wave),
101
+ .icon :global(.hexdn-volume-glyph-mute) {
102
+ opacity: 0;
103
+ transform: scale(0.72);
104
+ }
105
+ .icon[data-hexdn-volume-level="muted"] :global(.hexdn-volume-glyph-speaker) {
106
+ fill-opacity: 0;
107
+ }
108
+ .icon[data-hexdn-volume-level="muted"] :global(.hexdn-volume-glyph-mute),
109
+ .icon[data-hexdn-volume-level="low"] :global(.hexdn-volume-glyph-wave--low),
110
+ .icon[data-hexdn-volume-level="high"] :global(.hexdn-volume-glyph-wave) {
111
+ opacity: 1;
112
+ transform: scale(1);
113
+ }
@@ -0,0 +1,17 @@
1
+ import { type RefObject } from "react";
2
+ export declare function useMediaVideoPlayerControls({ hasPresentedMediaFrameRef, playerRef, videoRef, }: {
3
+ readonly hasPresentedMediaFrameRef: RefObject<boolean>;
4
+ readonly playerRef: RefObject<HTMLDivElement | null>;
5
+ readonly videoRef: RefObject<HTMLVideoElement | null>;
6
+ }): {
7
+ clearControlsHideTimer: () => void;
8
+ holdControls: () => void;
9
+ holdExtensionControls: () => void;
10
+ holdVolumeControls: () => void;
11
+ isControlsVisible: boolean;
12
+ releaseControls: () => void;
13
+ releaseExtensionControls: () => void;
14
+ releaseVolumeControls: () => void;
15
+ showControls: () => void;
16
+ showControlsPersistently: () => void;
17
+ };
@@ -0,0 +1,87 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useRef, useState, } from "react";
3
+ import { setMediaVideoPlayerControlHold, } from "./media-video-player-control-hold.js";
4
+ import { blurFocusedMediaVideoPlayerControl } from "./media-video-player-keyboard.js";
5
+ const CONTROLS_HIDE_DELAY_MS = 2_800;
6
+ const SHOW_CONTROLS_EVENT_INTERVAL_MS = 120;
7
+ export function useMediaVideoPlayerControls({ hasPresentedMediaFrameRef, playerRef, videoRef, }) {
8
+ const controlsHideTimerRef = useRef(null);
9
+ const controlHoldReasonsRef = useRef(new Set());
10
+ const showControlsLastHandledAtRef = useRef(0);
11
+ const [isControlsVisible, setIsControlsVisible] = useState(true);
12
+ const clearControlsHideTimer = useCallback(() => {
13
+ if (controlsHideTimerRef.current === null) {
14
+ return;
15
+ }
16
+ window.clearTimeout(controlsHideTimerRef.current);
17
+ controlsHideTimerRef.current = null;
18
+ }, []);
19
+ useEffect(() => clearControlsHideTimer, [clearControlsHideTimer]);
20
+ const showControlsPersistently = useCallback(() => {
21
+ clearControlsHideTimer();
22
+ setIsControlsVisible(true);
23
+ }, [clearControlsHideTimer]);
24
+ const showControls = useCallback(() => {
25
+ const nowMs = window.performance.now();
26
+ const recentlyHandled = nowMs - showControlsLastHandledAtRef.current <
27
+ SHOW_CONTROLS_EVENT_INTERVAL_MS;
28
+ if (recentlyHandled && controlsHideTimerRef.current !== null) {
29
+ return;
30
+ }
31
+ showControlsLastHandledAtRef.current = nowMs;
32
+ clearControlsHideTimer();
33
+ setIsControlsVisible((current) => (current ? current : true));
34
+ const video = videoRef.current;
35
+ if (video &&
36
+ hasPresentedMediaFrameRef.current &&
37
+ !video.ended &&
38
+ controlHoldReasonsRef.current.size === 0) {
39
+ controlsHideTimerRef.current = window.setTimeout(() => {
40
+ blurFocusedMediaVideoPlayerControl(playerRef.current);
41
+ setIsControlsVisible(false);
42
+ controlsHideTimerRef.current = null;
43
+ }, CONTROLS_HIDE_DELAY_MS);
44
+ }
45
+ }, [clearControlsHideTimer, hasPresentedMediaFrameRef, playerRef, videoRef]);
46
+ const holdControlsForReason = useCallback((reason) => {
47
+ setMediaVideoPlayerControlHold(controlHoldReasonsRef.current, reason, true);
48
+ clearControlsHideTimer();
49
+ setIsControlsVisible(true);
50
+ }, [clearControlsHideTimer]);
51
+ const releaseControlsForReason = useCallback((reason) => {
52
+ const controlsRemainHeld = setMediaVideoPlayerControlHold(controlHoldReasonsRef.current, reason, false);
53
+ if (!controlsRemainHeld) {
54
+ showControls();
55
+ }
56
+ }, [showControls]);
57
+ const holdControls = useCallback(() => {
58
+ holdControlsForReason("bottom");
59
+ }, [holdControlsForReason]);
60
+ const releaseControls = useCallback(() => {
61
+ releaseControlsForReason("bottom");
62
+ }, [releaseControlsForReason]);
63
+ const holdExtensionControls = useCallback(() => {
64
+ holdControlsForReason("extension");
65
+ }, [holdControlsForReason]);
66
+ const releaseExtensionControls = useCallback(() => {
67
+ releaseControlsForReason("extension");
68
+ }, [releaseControlsForReason]);
69
+ const holdVolumeControls = useCallback(() => {
70
+ holdControlsForReason("volume");
71
+ }, [holdControlsForReason]);
72
+ const releaseVolumeControls = useCallback(() => {
73
+ releaseControlsForReason("volume");
74
+ }, [releaseControlsForReason]);
75
+ return {
76
+ clearControlsHideTimer,
77
+ holdControls,
78
+ holdExtensionControls,
79
+ holdVolumeControls,
80
+ isControlsVisible,
81
+ releaseControls,
82
+ releaseExtensionControls,
83
+ releaseVolumeControls,
84
+ showControls,
85
+ showControlsPersistently,
86
+ };
87
+ }
@@ -0,0 +1,24 @@
1
+ type MutableRef<T> = {
2
+ current: T;
3
+ };
4
+ type FullscreenDocumentTarget = {
5
+ readonly fullscreenElement: Element | null;
6
+ readonly addEventListener: (type: "fullscreenchange", listener: () => void) => void;
7
+ readonly removeEventListener: (type: "fullscreenchange", listener: () => void) => void;
8
+ };
9
+ export declare function connectMediaVideoPlayerFullscreenListener(input: {
10
+ readonly documentTarget: FullscreenDocumentTarget;
11
+ readonly onFullscreenChange: (isFullscreen: boolean) => void;
12
+ readonly playerRef: MutableRef<HTMLDivElement | null>;
13
+ }): {
14
+ dispose(): void;
15
+ };
16
+ export declare function useMediaVideoPlayerFullscreen(): {
17
+ isFullscreen: boolean;
18
+ setIsFullscreen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
19
+ };
20
+ export declare function useMediaVideoPlayerFullscreenLifecycle(input: {
21
+ readonly playerRef: MutableRef<HTMLDivElement | null>;
22
+ readonly setIsFullscreen: (isFullscreen: boolean) => void;
23
+ }): void;
24
+ export {};
@@ -0,0 +1,38 @@
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+ import { isPlayerFullscreen } from "./playback-landscape-presentation.js";
4
+ export function connectMediaVideoPlayerFullscreenListener(input) {
5
+ function handleFullscreenChange() {
6
+ input.onFullscreenChange(isPlayerFullscreen({
7
+ fullscreenElement: input.documentTarget.fullscreenElement,
8
+ player: input.playerRef.current,
9
+ }));
10
+ }
11
+ handleFullscreenChange();
12
+ input.documentTarget.addEventListener("fullscreenchange", handleFullscreenChange);
13
+ return {
14
+ dispose() {
15
+ input.documentTarget.removeEventListener("fullscreenchange", handleFullscreenChange);
16
+ },
17
+ };
18
+ }
19
+ export function useMediaVideoPlayerFullscreen() {
20
+ const [isFullscreen, setIsFullscreen] = useState(false);
21
+ return { isFullscreen, setIsFullscreen };
22
+ }
23
+ export function useMediaVideoPlayerFullscreenLifecycle(input) {
24
+ useEffect(() => {
25
+ const lifecycle = connectMediaVideoPlayerFullscreenListener({
26
+ documentTarget: {
27
+ addEventListener: (_type, listener) => document.addEventListener("fullscreenchange", listener),
28
+ get fullscreenElement() {
29
+ return document.fullscreenElement;
30
+ },
31
+ removeEventListener: (_type, listener) => document.removeEventListener("fullscreenchange", listener),
32
+ },
33
+ onFullscreenChange: input.setIsFullscreen,
34
+ playerRef: input.playerRef,
35
+ });
36
+ return () => lifecycle.dispose();
37
+ }, [input.playerRef, input.setIsFullscreen]);
38
+ }
@@ -0,0 +1,25 @@
1
+ import { type MouseEvent, type ChangeEvent, type RefObject } from "react";
2
+ import type { PlaybackController } from "@hexdn/playback";
3
+ export declare function useMediaVideoPlayerInteractions(input: {
4
+ readonly controllerRef: RefObject<PlaybackController | null>;
5
+ readonly videoRef: RefObject<HTMLVideoElement | null>;
6
+ readonly playerRef: RefObject<HTMLDivElement | null>;
7
+ readonly hasPresentedMediaFrameRef: RefObject<boolean>;
8
+ readonly isControlsVisible: boolean;
9
+ readonly onBack?: (() => void) | undefined;
10
+ readonly showControls: () => void;
11
+ readonly showPlaybackFeedback: (action: "back-10s" | "fwd-10s" | "pause" | "play") => void;
12
+ readonly onFullscreenError: () => void;
13
+ }): {
14
+ handleBack: () => Promise<void>;
15
+ handleSeekChange: (event: ChangeEvent<HTMLInputElement>) => void;
16
+ handleVideoClick: (event: MouseEvent<HTMLVideoElement>) => void;
17
+ handleVideoDoubleClick: (event: MouseEvent<HTMLVideoElement>) => void;
18
+ seekBackward: () => void;
19
+ seekForward: () => void;
20
+ seekToTime: (time: number) => void;
21
+ toggleCaptions: () => void;
22
+ toggleFullscreen: () => Promise<void>;
23
+ togglePlayback: () => Promise<void>;
24
+ clearVideoClickTimer: () => void;
25
+ };