@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.
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/dist/player/bounded-surface-position.d.ts +8 -0
- package/dist/player/bounded-surface-position.js +8 -0
- package/dist/player/hexdn-player-control-popover.module.css +113 -0
- package/dist/player/index.d.ts +25 -0
- package/dist/player/index.js +32 -0
- package/dist/player/media-video-player-control-hold.d.ts +2 -0
- package/dist/player/media-video-player-control-hold.js +9 -0
- package/dist/player/media-video-player-debug-hud-loader-contract.d.ts +5 -0
- package/dist/player/media-video-player-debug-hud-loader-contract.js +1 -0
- package/dist/player/media-video-player-keyboard.d.ts +5 -0
- package/dist/player/media-video-player-keyboard.js +52 -0
- package/dist/player/media-video-player-layout.d.ts +64 -0
- package/dist/player/media-video-player-layout.js +110 -0
- package/dist/player/media-video-player-scrubber.d.ts +16 -0
- package/dist/player/media-video-player-scrubber.js +333 -0
- package/dist/player/media-video-player-surface-click.d.ts +8 -0
- package/dist/player/media-video-player-surface-click.js +14 -0
- package/dist/player/media-video-player-view-state.d.ts +21 -0
- package/dist/player/media-video-player-view-state.js +31 -0
- package/dist/player/media-video-player.module.css +1254 -0
- package/dist/player/playback-landscape-presentation.d.ts +16 -0
- package/dist/player/playback-landscape-presentation.js +39 -0
- package/dist/player/playback-loading-indicator.d.ts +7 -0
- package/dist/player/playback-loading-indicator.js +35 -0
- package/dist/player/player-control-popover.d.ts +16 -0
- package/dist/player/player-control-popover.js +130 -0
- package/dist/player/player-host.d.ts +31 -0
- package/dist/player/player-host.js +29 -0
- package/dist/player/player-icon.d.ts +19 -0
- package/dist/player/player-icon.js +59 -0
- package/dist/player/player-labels.d.ts +27 -0
- package/dist/player/player-labels.js +35 -0
- package/dist/player/player-slots.d.ts +18 -0
- package/dist/player/player-slots.js +1 -0
- package/dist/player/ready-player.d.ts +34 -0
- package/dist/player/ready-player.js +293 -0
- package/dist/player/tooltip-surface.d.ts +2 -0
- package/dist/player/tooltip-surface.js +8 -0
- package/dist/player/ui-primitives.d.ts +17 -0
- package/dist/player/ui-primitives.js +15 -0
- package/dist/player/ui.module.css +113 -0
- package/dist/player/use-media-video-player-controls.d.ts +17 -0
- package/dist/player/use-media-video-player-controls.js +87 -0
- package/dist/player/use-media-video-player-fullscreen.d.ts +24 -0
- package/dist/player/use-media-video-player-fullscreen.js +38 -0
- package/dist/player/use-media-video-player-interactions.d.ts +25 -0
- package/dist/player/use-media-video-player-interactions.js +163 -0
- package/dist/player/use-media-video-player-presentation.d.ts +50 -0
- package/dist/player/use-media-video-player-presentation.js +125 -0
- package/dist/player/use-media-video-player.d.ts +81 -0
- package/dist/player/use-media-video-player.js +263 -0
- package/dist/player/use-player-analytics.d.ts +13 -0
- package/dist/player/use-player-analytics.js +79 -0
- package/dist/player/use-slider-pointer.d.ts +20 -0
- package/dist/player/use-slider-pointer.js +85 -0
- package/dist/player/utils.d.ts +1 -0
- package/dist/player/utils.js +3 -0
- package/dist/upload/index.d.ts +37 -0
- package/dist/upload/index.js +60 -0
- package/dist/upload/upload.module.css +41 -0
- package/dist/upload/use-uploader.d.ts +42 -0
- package/dist/upload/use-uploader.js +179 -0
- 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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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;
|