@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Havikware
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # @hexdn/react
2
+
3
+ Ready-made, customizable playback and upload UI. Import only the entry point you
4
+ need. Styles are included; use labels, CSS variables and custom content to fit
5
+ your application. Keep HexDN environment API keys on your server.
6
+
7
+ ## Player
8
+
9
+ ```tsx
10
+ import { Player } from "@hexdn/react/player";
11
+
12
+ <Player source="https://cdn.example/video/master.m3u8" title="My video" />;
13
+ ```
14
+
15
+ For restricted video, expose a tenant endpoint that authorizes the viewer and
16
+ returns `hexdn.playback.createSource(...)` for protected playback or
17
+ `hexdn.playback.createSignedSource(...)` for signed playback, using
18
+ `@hexdn/sdk`:
19
+
20
+ ```tsx
21
+ <Player sourceEndpoint="/api/videos/42/playback" title="My video" />
22
+ ```
23
+
24
+ The player acquires and renews the source through that endpoint. `getSource`
25
+ supports custom acquisition. Labels, themes and slots customize the controls;
26
+ integration callbacks connect application navigation, preferences and progress
27
+ persistence. Ready public sources need no credential request. The HLS engine
28
+ loads only when required.
29
+
30
+ Playback-origin restrictions are optional. If your HexDN environment has
31
+ playback origins configured, include your application's origin; an empty
32
+ playback-origin list does not restrict origins.
33
+
34
+ To enable independent analytics with HexDN collection, first provision a
35
+ collection key on your trusted server using `@hexdn/sdk`:
36
+
37
+ ```ts
38
+ import { HexDN } from "@hexdn/sdk";
39
+
40
+ const hexdn = new HexDN({ apiKey: configuration.hexdnApiKey });
41
+ const key = await hexdn.analytics.collectionKeys.create({
42
+ origins: ["https://app.example"],
43
+ });
44
+ const collectionKey = key.id;
45
+ const collectionEndpoint = "https://api.hexdn.com/v1/analytics/collect";
46
+ ```
47
+
48
+ Use your application's exact origins (scheme, host and port), without wildcards.
49
+ Provision once per origin configuration, not per view. Publish only
50
+ `collectionKey` and `collectionEndpoint` through your application's public
51
+ configuration; keep the environment API key on the server. If you configure a
52
+ different HexDN API base, use its `/v1/analytics/collect` URL. These collection
53
+ origins are independent of playback and upload origin settings.
54
+
55
+ Pass that public configuration and a stable video identity to the player:
56
+
57
+ ```tsx
58
+ import { createFetchTransport } from "@hexdn/analytics";
59
+
60
+ const analytics = {
61
+ video: { id: "your-video-id" },
62
+ transport: createFetchTransport({
63
+ endpoint: collectionEndpoint,
64
+ collectionKey,
65
+ }),
66
+ };
67
+
68
+ <Player sourceEndpoint="/api/videos/42/playback" analytics={analytics} />;
69
+ ```
70
+
71
+ An existing `beginPlaybackAttempt()` handle can be supplied as
72
+ `analytics.pendingAttempt` when play intent precedes mounting the player.
73
+ Credential changes retain the attempt. Product view rules and resume persistence
74
+ remain application responsibilities.
75
+
76
+ A source descriptor's `playbackId` supplies the default content identity.
77
+ Credential URL changes with the same ID preserve position, playback speed and
78
+ play/pause intent. Use `playbackIdentity` to override it; external sources
79
+ without an ID use their URL. Analytics keeps its separately supplied video
80
+ identity.
81
+
82
+ ## Uploader
83
+
84
+ Before browser uploads, have your HexDN operator register the application's
85
+ origin in the environment with `purpose=upload`, include it in the reviewed
86
+ deployment upload origins, and apply the source-storage CORS policy. The
87
+ environment and storage policies must both allow the origin; an upload
88
+ capability alone does not configure browser access. Origin provisioning is
89
+ currently an operator task, not a method on the public server SDK.
90
+
91
+ ```tsx
92
+ import { Uploader } from "@hexdn/react/upload";
93
+
94
+ <Uploader
95
+ getUpload={async (file, { signal, idempotencyKey }) => {
96
+ const response = await fetch("/api/video-uploads", {
97
+ method: "POST",
98
+ signal,
99
+ headers: {
100
+ "Content-Type": "application/json",
101
+ "Idempotency-Key": idempotencyKey,
102
+ },
103
+ body: JSON.stringify({
104
+ filename: file.name,
105
+ byteLength: file.size,
106
+ mimeType: file.type,
107
+ }),
108
+ });
109
+ if (!response.ok) throw new Error("Upload could not be authorized.");
110
+ const { upload } = await response.json();
111
+ return upload;
112
+ }}
113
+ onComplete={() => refreshApplicationVideo()}
114
+ />;
115
+ ```
116
+
117
+ Your endpoint authenticates the uploader, chooses outputs, and returns a scoped
118
+ `{ upload: { url, token } }` capability. Forward the supplied idempotency key to
119
+ asset creation: the component preserves it through an uncertain creation retry.
120
+ An already-created capability can instead be passed as `upload={capability}`.
121
+
122
+ The default UI supports file selection, dropping a file, progress, pause and
123
+ resume/retry. Media bytes go directly to storage through `@hexdn/upload`. Resume
124
+ uses the same capability and original file, including authoritative part
125
+ reconciliation and completion recovery. Upload completion confirms receipt of
126
+ source bytes; processing/readiness comes from the application's normal API reads
127
+ or webhooks.
128
+
129
+ Pause and default unmount cleanup stop local transfer only. Remote cancellation
130
+ is a separate explicit application action. Set `abortOnUnmount={false}` only
131
+ when the application owns a continuing background workflow. The `accept` prop is
132
+ a file-picker hint; enforce product and content rules on your server.
133
+
134
+ Customize labels and `--hexdn-upload-color`, `--hexdn-upload-background`,
135
+ `--hexdn-upload-border`, `--hexdn-upload-radius`, and `--hexdn-upload-accent`.
136
+ For an existing application, use the same controller with your own presentation:
137
+
138
+ ```tsx
139
+ import { Uploader, useUploader } from "@hexdn/react/upload";
140
+
141
+ const uploader = useUploader({ upload: capability });
142
+
143
+ <Uploader controller={uploader} dropEnabled={false}>
144
+ <button disabled={uploader.isBusy} onClick={uploader.openPicker}>
145
+ Choose video
146
+ </button>
147
+ <progress max={100} value={uploader.state.progress?.percent ?? 0} />
148
+ </Uploader>;
149
+ ```
150
+
151
+ `children` can also be a render function receiving the controller. Advanced
152
+ applications can supply `transfer(file, { signal, onProgress, idempotencyKey })`
153
+ to wrap their existing authorization and product workflow around the upload
154
+ core. That callback owns retry/resume semantics and must distinguish local abort
155
+ from any destructive product action. Errors remain available as
156
+ `controller.state.error`; raw provider errors are not displayed by the default
157
+ UI.
158
+
159
+ Neither uploader entry point nor upload core imports the player, HLS, or
160
+ analytics.
@@ -0,0 +1,8 @@
1
+ export type BoundedHorizontalSurfaceInput = {
2
+ readonly anchorCenter: number;
3
+ readonly boundaryEnd: number;
4
+ readonly boundaryStart: number;
5
+ readonly gutter: number;
6
+ readonly surfaceWidth: number;
7
+ };
8
+ export declare function resolveBoundedHorizontalSurfaceLeft({ anchorCenter, boundaryEnd, boundaryStart, gutter, surfaceWidth, }: BoundedHorizontalSurfaceInput): number;
@@ -0,0 +1,8 @@
1
+ export function resolveBoundedHorizontalSurfaceLeft({ anchorCenter, boundaryEnd, boundaryStart, gutter, surfaceWidth, }) {
2
+ const availableGutter = Math.max((boundaryEnd - boundaryStart - surfaceWidth) / 2, 0);
3
+ const effectiveGutter = Math.min(Math.max(gutter, 0), availableGutter);
4
+ const minimumLeft = boundaryStart + effectiveGutter;
5
+ const maximumLeft = boundaryEnd - surfaceWidth - effectiveGutter;
6
+ const naturalLeft = anchorCenter - surfaceWidth / 2;
7
+ return Math.min(Math.max(naturalLeft, minimumLeft), Math.max(minimumLeft, maximumLeft));
8
+ }
@@ -0,0 +1,113 @@
1
+ .host {
2
+ --hexdn-player-control-popover-header-bg: var(
3
+ --hexdn-popover-header-bg,
4
+ rgba(244, 237, 228, 0.035)
5
+ );
6
+ --hexdn-player-control-popover-surface-bg: var(
7
+ --hexdn-player-floating-surface-bg,
8
+ var(
9
+ --hexdn-popover-surface-bg,
10
+ var(--hexdn-glass-strong, rgba(20, 17, 15, 0.8))
11
+ )
12
+ );
13
+ --hexdn-player-control-popover-bg: var(
14
+ --hexdn-player-control-popover-surface-bg
15
+ );
16
+ --hexdn-player-control-popover-duration: var(--hexdn-fast, 120ms);
17
+ --hexdn-player-control-popover-enter-offset: var(--hexdn-s-2, 8px);
18
+ --hexdn-player-control-popover-scale-x: 1;
19
+ --hexdn-player-control-popover-scale-y: 1;
20
+ --hexdn-player-control-popover-transform-origin: 50% 100%;
21
+
22
+ position: relative;
23
+ display: inline-flex;
24
+ flex: 0 0 auto;
25
+ }
26
+
27
+ .surface {
28
+ position: absolute;
29
+ z-index: var(--hexdn-z-tooltip, 100);
30
+ width: var(--hexdn-player-control-popover-width, auto);
31
+ min-width: var(
32
+ --hexdn-player-control-popover-min-width,
33
+ min(calc(var(--hexdn-s-10, 128px) * 2.5), 78vw)
34
+ );
35
+ max-width: var(
36
+ --hexdn-player-control-popover-max-width,
37
+ min(calc(var(--hexdn-s-10, 128px) * 3.5), 84vw)
38
+ );
39
+ border: 0;
40
+ border-radius: var(
41
+ --hexdn-player-control-popover-radius,
42
+ var(--hexdn-popover-radius, 12px)
43
+ );
44
+ background: var(--hexdn-player-control-popover-bg);
45
+ -webkit-backdrop-filter: blur(var(--hexdn-glass-blur, 14px))
46
+ saturate(var(--hexdn-glass-saturate, 1.2));
47
+ backdrop-filter: blur(var(--hexdn-glass-blur, 14px))
48
+ saturate(var(--hexdn-glass-saturate, 1.2));
49
+ box-shadow: var(--hexdn-glass-shadow, 0 8px 24px rgba(0, 0, 0, 0.5));
50
+ color: var(--hexdn-paper, #f4ede4);
51
+ cursor: inherit;
52
+ opacity: 0;
53
+ pointer-events: none;
54
+ transform: translate(
55
+ var(--hexdn-player-control-popover-transform-x, 0),
56
+ var(--hexdn-player-control-popover-enter-offset)
57
+ )
58
+ scaleX(var(--hexdn-player-control-popover-scale-x))
59
+ scaleY(var(--hexdn-player-control-popover-scale-y));
60
+ transform-origin: var(--hexdn-player-control-popover-transform-origin);
61
+ transition:
62
+ opacity var(--hexdn-player-control-popover-duration)
63
+ var(--hexdn-ease, cubic-bezier(0.2, 0.8, 0.2, 1)),
64
+ transform var(--hexdn-player-control-popover-duration)
65
+ var(--hexdn-ease, cubic-bezier(0.2, 0.8, 0.2, 1));
66
+ }
67
+
68
+ .surface::before {
69
+ position: absolute;
70
+ right: 0;
71
+ left: 0;
72
+ content: "";
73
+ }
74
+
75
+ .host[data-hexdn-side="top"] .surface {
76
+ bottom: 100%;
77
+ margin-bottom: var(--hexdn-s-4, 16px);
78
+ }
79
+
80
+ .host[data-hexdn-side="top"] .surface::before {
81
+ bottom: calc(var(--hexdn-s-4, 16px) * -1);
82
+ height: var(--hexdn-s-4, 16px);
83
+ }
84
+
85
+ .host[data-hexdn-align="center"] .surface {
86
+ left: var(--hexdn-player-control-popover-left, 50%);
87
+ --hexdn-player-control-popover-transform-x: -50%;
88
+ }
89
+
90
+ .host[data-hexdn-align="start"] .surface {
91
+ left: 0;
92
+ }
93
+
94
+ .host[data-hexdn-align="end"] .surface {
95
+ right: 0;
96
+ }
97
+
98
+ .host[data-hexdn-open="true"] .surface {
99
+ opacity: 1;
100
+ pointer-events: auto;
101
+ transform: translate(var(--hexdn-player-control-popover-transform-x, 0), 0)
102
+ scaleX(1) scaleY(1);
103
+ }
104
+
105
+ @media (max-width: 640px) {
106
+ .surface {
107
+ min-width: var(
108
+ --hexdn-player-control-popover-min-width,
109
+ min(calc(var(--hexdn-s-10, 128px) * 2.25), 86vw)
110
+ );
111
+ max-width: var(--hexdn-player-control-popover-max-width, 86vw);
112
+ }
113
+ }
@@ -0,0 +1,25 @@
1
+ import { type PlaybackSource, type PlaybackSourceResolver } from "@hexdn/playback";
2
+ import { type PlayerIntegration } from "./player-host.js";
3
+ import { type ReadyPlayerProps } from "./ready-player.js";
4
+ export type { PlayerLabels } from "./player-labels.js";
5
+ export { defaultPlayerLabels } from "./player-labels.js";
6
+ export type { PlayerSlots, PlayerSlotControls } from "./player-slots.js";
7
+ export type { PlayerIntegration } from "./player-host.js";
8
+ export type { PlayerAnalyticsOptions } from "./use-player-analytics.js";
9
+ export type { MediaVideoPlayerDebugHudComponent } from "./media-video-player-debug-hud-loader-contract.js";
10
+ export type { MediaVideoPlayerProgressSnapshot, MediaVideoPlayerProgressReason, MediaVideoPlayerQualityEvent, } from "./use-media-video-player.js";
11
+ export { PlayerControlPopover } from "./player-control-popover.js";
12
+ export type PlayerTheme = {
13
+ readonly accentColor?: string | undefined;
14
+ readonly backgroundColor?: string | undefined;
15
+ readonly fontFamily?: string | undefined;
16
+ };
17
+ export type PlayerProps = Omit<ReadyPlayerProps, "sourceUrl" | "requestAuthorization" | "previewThumbnails" | "expiresAt" | "sourceError" | "onRetrySource"> & {
18
+ readonly source?: string | PlaybackSource | undefined;
19
+ readonly sourceEndpoint?: string | undefined;
20
+ readonly getSource?: PlaybackSourceResolver | undefined;
21
+ readonly integration?: PlayerIntegration | undefined;
22
+ readonly theme?: PlayerTheme | undefined;
23
+ };
24
+ /** Public URLs are ready immediately. Only an explicit endpoint/resolver acquires credentials. */
25
+ export declare function Player({ source, sourceEndpoint, getSource, integration, theme, autoPlay, ...props }: PlayerProps): import("react").JSX.Element;
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createPlaybackSourceEndpoint, } from "@hexdn/playback";
4
+ import { useMemo } from "react";
5
+ import { PlayerHost } from "./player-host.js";
6
+ import { ReadyPlayer } from "./ready-player.js";
7
+ export { defaultPlayerLabels } from "./player-labels.js";
8
+ export { PlayerControlPopover } from "./player-control-popover.js";
9
+ /** Public URLs are ready immediately. Only an explicit endpoint/resolver acquires credentials. */
10
+ export function Player({ source, sourceEndpoint, getSource, integration, theme, autoPlay = false, ...props }) {
11
+ const readySource = useMemo(() => (typeof source === "string" ? { sourceUrl: source } : source), [source]);
12
+ const resolver = useMemo(() => getSource ??
13
+ (sourceEndpoint
14
+ ? createPlaybackSourceEndpoint({ endpoint: sourceEndpoint })
15
+ : undefined), [getSource, sourceEndpoint]);
16
+ const style = {
17
+ ...(theme?.accentColor
18
+ ? {
19
+ "--hexdn-progress-fill": theme.accentColor,
20
+ "--hexdn-progress-leading-edge": theme.accentColor,
21
+ }
22
+ : {}),
23
+ ...(theme?.backgroundColor
24
+ ? { "--hexdn-player-canvas": theme.backgroundColor }
25
+ : {}),
26
+ ...(theme?.fontFamily
27
+ ? { "--hexdn-player-font-family": theme.fontFamily }
28
+ : {}),
29
+ ...props.style,
30
+ };
31
+ return (_jsx(PlayerHost, { integration: integration, children: _jsx(ReadyPlayer, { ...props, ...readySource, sourceUrl: readySource?.sourceUrl ?? "", playbackIdentity: props.playbackIdentity ?? readySource?.playbackId, getSource: resolver, autoPlay: autoPlay, style: style }) }));
32
+ }
@@ -0,0 +1,2 @@
1
+ export type MediaVideoPlayerControlHoldReason = "bottom" | "extension" | "volume";
2
+ export declare function setMediaVideoPlayerControlHold(activeReasons: Set<MediaVideoPlayerControlHoldReason>, reason: MediaVideoPlayerControlHoldReason, active: boolean): boolean;
@@ -0,0 +1,9 @@
1
+ export function setMediaVideoPlayerControlHold(activeReasons, reason, active) {
2
+ if (active) {
3
+ activeReasons.add(reason);
4
+ }
5
+ else {
6
+ activeReasons.delete(reason);
7
+ }
8
+ return activeReasons.size > 0;
9
+ }
@@ -0,0 +1,5 @@
1
+ import type { ComponentType } from "react";
2
+ import type { MediaVideoPlayerDebugSnapshot } from "./use-media-video-player.js";
3
+ export type MediaVideoPlayerDebugHudComponent = ComponentType<{
4
+ readonly debug: MediaVideoPlayerDebugSnapshot;
5
+ }>;
@@ -0,0 +1,5 @@
1
+ type MediaVideoPlayerKeyboardShortcut = "restart" | "seek-backward" | "seek-forward" | "toggle-playback";
2
+ export declare function readMediaVideoPlayerKeyboardShortcut(event: KeyboardEvent): MediaVideoPlayerKeyboardShortcut | null;
3
+ export declare function shouldIgnoreMediaVideoPlayerKeyboardShortcut(event: KeyboardEvent): boolean;
4
+ export declare function blurFocusedMediaVideoPlayerControl(player: HTMLDivElement | null): void;
5
+ export {};
@@ -0,0 +1,52 @@
1
+ const PLAYER_CONTROL_SELECTOR = "a, button, input, select, textarea, [contenteditable='true'], [role='button'], [role='slider']";
2
+ function isPlaybackSpaceShortcut(event) {
3
+ return (event.code === "Space" || event.key === " " || event.key === "Spacebar");
4
+ }
5
+ export function readMediaVideoPlayerKeyboardShortcut(event) {
6
+ if (isPlaybackSpaceShortcut(event)) {
7
+ return "toggle-playback";
8
+ }
9
+ if (event.key === "0" ||
10
+ event.code === "Digit0" ||
11
+ event.code === "Numpad0") {
12
+ return "restart";
13
+ }
14
+ if (event.key === "ArrowLeft") {
15
+ return "seek-backward";
16
+ }
17
+ if (event.key === "ArrowRight") {
18
+ return "seek-forward";
19
+ }
20
+ return null;
21
+ }
22
+ export function shouldIgnoreMediaVideoPlayerKeyboardShortcut(event) {
23
+ if (event.defaultPrevented || event.repeat) {
24
+ return true;
25
+ }
26
+ if (event.altKey || event.ctrlKey || event.metaKey) {
27
+ return true;
28
+ }
29
+ const target = event.target;
30
+ if (typeof HTMLElement === "undefined" || !(target instanceof HTMLElement)) {
31
+ return false;
32
+ }
33
+ if (target.isContentEditable) {
34
+ return true;
35
+ }
36
+ return Boolean(target.closest(PLAYER_CONTROL_SELECTOR));
37
+ }
38
+ export function blurFocusedMediaVideoPlayerControl(player) {
39
+ if (typeof document === "undefined") {
40
+ return;
41
+ }
42
+ const activeElement = document.activeElement;
43
+ if (!player ||
44
+ !(activeElement instanceof HTMLElement) ||
45
+ !player.contains(activeElement)) {
46
+ return;
47
+ }
48
+ const focusedControl = activeElement.closest(PLAYER_CONTROL_SELECTOR);
49
+ if (focusedControl instanceof HTMLElement) {
50
+ focusedControl.blur();
51
+ }
52
+ }
@@ -0,0 +1,64 @@
1
+ import type { CSSProperties } from "react";
2
+ import type { MediaThumbnailPreviewCue } from "@hexdn/playback/internal";
3
+ export type PlayerSliderStyle = CSSProperties & {
4
+ "--hexdn-video-buffer": string;
5
+ "--hexdn-video-preview": string;
6
+ "--hexdn-video-progress": string;
7
+ };
8
+ export type VolumeSliderStyle = CSSProperties & {
9
+ "--hexdn-video-volume": string;
10
+ "--hexdn-video-volume-ratio": number;
11
+ };
12
+ export type CaptionViewportStyle = CSSProperties & {
13
+ "--hexdn-player-video-bottom": string;
14
+ "--hexdn-player-video-left": string;
15
+ "--hexdn-player-video-right": string;
16
+ };
17
+ export type ScrubberThumbnailPreviewStyle = CSSProperties & {
18
+ "--hexdn-video-preview-image": string;
19
+ "--hexdn-video-preview-x": string;
20
+ "--hexdn-video-preview-y": string;
21
+ "--hexdn-video-preview-width": string;
22
+ "--hexdn-video-preview-height": string;
23
+ };
24
+ export type ScrubberPreview = {
25
+ readonly percent: number;
26
+ readonly time: number;
27
+ };
28
+ export type ScrubberPreviewState = {
29
+ readonly active: boolean;
30
+ readonly preview: ScrubberPreview;
31
+ };
32
+ export type ScrubberPreviewGeometry = {
33
+ readonly boundaryWidth: number;
34
+ readonly surfaceWidth: number;
35
+ };
36
+ export declare const emptyScrubberPreviewGeometry: {
37
+ boundaryWidth: number;
38
+ surfaceWidth: number;
39
+ };
40
+ export declare function areScrubberPreviewStatesEqual(left: ScrubberPreviewState | null, right: ScrubberPreviewState | null): boolean;
41
+ export type VideoViewportInsets = {
42
+ readonly bottom: number;
43
+ readonly left: number;
44
+ readonly right: number;
45
+ };
46
+ export declare const emptyVideoViewportInsets: {
47
+ bottom: number;
48
+ left: number;
49
+ right: number;
50
+ };
51
+ export declare function readVideoViewportInsets(video: HTMLVideoElement): VideoViewportInsets;
52
+ export declare function areVideoViewportInsetsEqual(current: VideoViewportInsets, next: VideoViewportInsets): boolean;
53
+ export declare function clampScrubberRatio(value: number): number;
54
+ export declare function resolveScrubberPointerRatio(rect: Pick<DOMRect, "left" | "top" | "width" | "height">, point: {
55
+ readonly clientX: number;
56
+ readonly clientY: number;
57
+ }): number | null;
58
+ export declare function formatCssImageUrl(value: string): string;
59
+ export declare function buildScrubberThumbnailPreviewStyle(cue: MediaThumbnailPreviewCue): ScrubberThumbnailPreviewStyle;
60
+ export declare function measureScrubberPreviewGeometry(boundary: HTMLElement, surface: HTMLElement): ScrubberPreviewGeometry;
61
+ export declare function syncScrubberPreviewLayout(element: HTMLElement, preview: ScrubberPreview, geometry: ScrubberPreviewGeometry): void;
62
+ export declare function buildPlayerSliderStyle(bufferProgress: string, preview: ScrubberPreview | null, seekProgress: string): PlayerSliderStyle;
63
+ export declare function buildVolumeSliderStyle(effectiveVolume: number, volumeProgress: string): VolumeSliderStyle;
64
+ export declare function buildCaptionViewportStyle(insets: VideoViewportInsets): CaptionViewportStyle;
@@ -0,0 +1,110 @@
1
+ import { resolveBoundedHorizontalSurfaceLeft } from "./bounded-surface-position.js";
2
+ import { resolveContainedMediaViewportInsets } from "@hexdn/playback/internal";
3
+ export const emptyScrubberPreviewGeometry = {
4
+ boundaryWidth: 0,
5
+ surfaceWidth: 0,
6
+ };
7
+ function areScrubberPreviewsEqual(left, right) {
8
+ return left.percent === right.percent && left.time === right.time;
9
+ }
10
+ export function areScrubberPreviewStatesEqual(left, right) {
11
+ if (left === right) {
12
+ return true;
13
+ }
14
+ if (!left || !right || left.active !== right.active) {
15
+ return false;
16
+ }
17
+ return areScrubberPreviewsEqual(left.preview, right.preview);
18
+ }
19
+ export const emptyVideoViewportInsets = {
20
+ bottom: 0,
21
+ left: 0,
22
+ right: 0,
23
+ };
24
+ export function readVideoViewportInsets(video) {
25
+ return resolveContainedMediaViewportInsets({
26
+ containerHeight: video.clientHeight,
27
+ containerWidth: video.clientWidth,
28
+ mediaHeight: video.videoHeight,
29
+ mediaWidth: video.videoWidth,
30
+ });
31
+ }
32
+ export function areVideoViewportInsetsEqual(current, next) {
33
+ return (current.bottom === next.bottom &&
34
+ current.left === next.left &&
35
+ current.right === next.right);
36
+ }
37
+ export function clampScrubberRatio(value) {
38
+ if (!Number.isFinite(value)) {
39
+ return 0;
40
+ }
41
+ return Math.min(Math.max(value, 0), 1);
42
+ }
43
+ export function resolveScrubberPointerRatio(rect, point) {
44
+ if (rect.width <= 0 || rect.height <= 0)
45
+ return null;
46
+ // The portrait Watch frame rotates 90deg clockwise: the track's start/end
47
+ // then run top-to-bottom on screen, while its layout width stays unchanged.
48
+ return clampScrubberRatio(rect.height > rect.width
49
+ ? (point.clientY - rect.top) / rect.height
50
+ : (point.clientX - rect.left) / rect.width);
51
+ }
52
+ export function formatCssImageUrl(value) {
53
+ return `url("${value.replace(/["\\\n\r\f]/gu, "\\$&")}")`;
54
+ }
55
+ export function buildScrubberThumbnailPreviewStyle(cue) {
56
+ return {
57
+ "--hexdn-video-preview-height": `${cue.height}px`,
58
+ "--hexdn-video-preview-image": cue.imageUrl
59
+ ? formatCssImageUrl(cue.imageUrl)
60
+ : "none",
61
+ "--hexdn-video-preview-width": `${cue.width}px`,
62
+ "--hexdn-video-preview-x": `${cue.x}px`,
63
+ "--hexdn-video-preview-y": `${cue.y}px`,
64
+ };
65
+ }
66
+ export function measureScrubberPreviewGeometry(boundary, surface) {
67
+ const boundaryWidth = boundary.clientWidth;
68
+ boundary.style.setProperty("--hexdn-video-preview-surface-max-width", `${boundaryWidth}px`);
69
+ return {
70
+ boundaryWidth,
71
+ surfaceWidth: surface.offsetWidth,
72
+ };
73
+ }
74
+ export function syncScrubberPreviewLayout(element, preview, geometry) {
75
+ element.style.setProperty("--hexdn-video-preview", `${preview.percent}%`);
76
+ if (geometry.boundaryWidth <= 0 || geometry.surfaceWidth <= 0) {
77
+ return;
78
+ }
79
+ const anchorCenter = clampScrubberRatio(preview.percent / 100) * geometry.boundaryWidth;
80
+ const surfaceLeft = resolveBoundedHorizontalSurfaceLeft({
81
+ anchorCenter,
82
+ boundaryEnd: geometry.boundaryWidth,
83
+ boundaryStart: 0,
84
+ gutter: 16,
85
+ surfaceWidth: geometry.surfaceWidth,
86
+ });
87
+ const surfaceOrigin = Math.min(Math.max(anchorCenter - surfaceLeft, 0), geometry.surfaceWidth);
88
+ element.style.setProperty("--hexdn-video-preview-surface-left", `${surfaceLeft}px`);
89
+ element.style.setProperty("--hexdn-video-preview-surface-origin", `${surfaceOrigin}px`);
90
+ }
91
+ export function buildPlayerSliderStyle(bufferProgress, preview, seekProgress) {
92
+ return {
93
+ "--hexdn-video-buffer": bufferProgress,
94
+ "--hexdn-video-preview": preview ? `${preview.percent}%` : seekProgress,
95
+ "--hexdn-video-progress": seekProgress,
96
+ };
97
+ }
98
+ export function buildVolumeSliderStyle(effectiveVolume, volumeProgress) {
99
+ return {
100
+ "--hexdn-video-volume": volumeProgress,
101
+ "--hexdn-video-volume-ratio": effectiveVolume,
102
+ };
103
+ }
104
+ export function buildCaptionViewportStyle(insets) {
105
+ return {
106
+ "--hexdn-player-video-bottom": `${insets.bottom}px`,
107
+ "--hexdn-player-video-left": `${insets.left}px`,
108
+ "--hexdn-player-video-right": `${insets.right}px`,
109
+ };
110
+ }
@@ -0,0 +1,16 @@
1
+ import type { VideoPlaybackRequestAuthorization, VideoPreviewThumbnails } from "@hexdn/playback/internal";
2
+ import { type ChangeEventHandler } from "react";
3
+ export declare function MediaVideoPlayerScrubber({ bufferProgress, currentTime, duration, hasSeekableDuration, onSeekChange, onSeekToTime, onShowControls, previewThumbnails, requestAuthorization, seekLabel, seekProgress, seekValueText, }: {
4
+ readonly bufferProgress: string;
5
+ readonly currentTime: number;
6
+ readonly duration: number;
7
+ readonly hasSeekableDuration: boolean;
8
+ readonly onSeekChange: ChangeEventHandler<HTMLInputElement>;
9
+ readonly onSeekToTime: (timeSeconds: number) => void;
10
+ readonly onShowControls: () => void;
11
+ readonly previewThumbnails?: VideoPreviewThumbnails | undefined;
12
+ readonly requestAuthorization?: VideoPlaybackRequestAuthorization | undefined;
13
+ readonly seekLabel: string;
14
+ readonly seekProgress: string;
15
+ readonly seekValueText: string;
16
+ }): import("react").JSX.Element;