@karnstack/kino 0.3.0 → 0.4.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/README.md +22 -2
- package/dist/vimeo.d.ts +57 -0
- package/dist/vimeo.js +378 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
> **[Try it live → kino.karnstack.com](https://kino.karnstack.com)** — drop in any public Mux playback ID, pick an accent, and play with the real glass UI.
|
|
26
26
|
|
|
27
|
-
kino ships the player UI and a provider contract. Each provider adapts a streaming engine to that contract, so the same glass chrome can sit on top of different backends.
|
|
27
|
+
kino ships the player UI and a provider contract. Each provider adapts a streaming engine to that contract, so the same glass chrome can sit on top of different backends. Four providers ship today: **Mux** (adaptive HLS via `@mux/mux-video`), **Native** (a plain `<video>` over any raw file URL), **YouTube** (the IFrame Player API wrapped in the same chrome), and **Vimeo** (the Vimeo Player SDK under the same chrome). Each lives behind its own entry point, so you only pull in the engine you use.
|
|
28
28
|
|
|
29
29
|
## Install
|
|
30
30
|
|
|
@@ -132,6 +132,27 @@ Speed, fullscreen, and captions work. The captions menu lists the video's own su
|
|
|
132
132
|
|
|
133
133
|
kino plays YouTube through the official IFrame Player API and, per YouTube's terms, **doesn't obscure the player**: before playback and while paused, YouTube shows its own thumbnail, play button, title, and logo, and kino's controls sit alongside them. A few things the API simply doesn't expose, so kino hides those controls: **manual quality** (YouTube dropped it — playback is always automatic), **picture-in-picture**, and **scrub-preview thumbnails** (storyboards aren't available to embeds).
|
|
134
134
|
|
|
135
|
+
## Playing a Vimeo video
|
|
136
|
+
|
|
137
|
+
For a Vimeo source, use the Vimeo provider. It drives the Vimeo Player SDK under the same glass chrome, with kino owning the controls and keyboard map.
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { VimeoPlayer } from "@karnstack/kino/vimeo"
|
|
141
|
+
import "@karnstack/kino/styles.css"
|
|
142
|
+
|
|
143
|
+
export default function Watch() {
|
|
144
|
+
return <VimeoPlayer videoId="291235566" accentColor="#00adef" />
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
For an unlisted video, pass the hash (or a share URL that contains it):
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
<VimeoPlayer videoId="123456789" hash="abcdef0123" />
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Chromeless playback (kino owning the controls) requires a paid Vimeo plan.
|
|
155
|
+
|
|
135
156
|
## Theming
|
|
136
157
|
|
|
137
158
|
The quickest knob is the `accentColor` prop, which drives the scrubber fill, active menu items, and range controls.
|
|
@@ -201,7 +222,6 @@ pnpm lint # eslint
|
|
|
201
222
|
|
|
202
223
|
## Roadmap
|
|
203
224
|
|
|
204
|
-
- More providers: Vimeo
|
|
205
225
|
- AirPlay support
|
|
206
226
|
- Chapters
|
|
207
227
|
- Documented headless primitives for fully custom chrome
|
package/dist/vimeo.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { a as Provider } from "./types-D0bLitH2.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/vimeo/provider.d.ts
|
|
5
|
+
type VimeoProviderOptions = {
|
|
6
|
+
videoId: string;
|
|
7
|
+
hash?: string;
|
|
8
|
+
metadata?: {
|
|
9
|
+
videoId?: string;
|
|
10
|
+
videoTitle?: string;
|
|
11
|
+
viewerUserId?: string;
|
|
12
|
+
};
|
|
13
|
+
autoPlay?: boolean;
|
|
14
|
+
muted?: boolean;
|
|
15
|
+
loop?: boolean;
|
|
16
|
+
defaultRate?: number;
|
|
17
|
+
};
|
|
18
|
+
declare function parseVimeoSource(input: string): {
|
|
19
|
+
id: string;
|
|
20
|
+
hash?: string;
|
|
21
|
+
};
|
|
22
|
+
declare function playerUrl(id: string, hash: string): string;
|
|
23
|
+
declare function createVimeoProvider(opts: VimeoProviderOptions): Provider;
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/vimeo/vimeo-player.d.ts
|
|
26
|
+
type VimeoPlayerProps = VimeoProviderOptions & {
|
|
27
|
+
accentColor?: string;
|
|
28
|
+
theme?: Record<string, string>;
|
|
29
|
+
className?: string; /** Blur-up still painted behind the video until the first frame loads. */
|
|
30
|
+
placeholder?: string;
|
|
31
|
+
children?: ReactNode;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* kino's glass chrome over a Vimeo video, backed by the Vimeo Player SDK. Pass a
|
|
35
|
+
* numeric id or any vimeo.com / player.vimeo.com URL; for unlisted videos pass
|
|
36
|
+
* the `hash` (or a share URL that already contains it).
|
|
37
|
+
*
|
|
38
|
+
* Only `videoId`, `hash`, and `metadata.videoTitle` are reactive (they flow
|
|
39
|
+
* through `swapSource`). `autoPlay`, `muted`, `loop`, and `defaultRate` are read
|
|
40
|
+
* once at creation — remount (e.g. via `key`) to change them.
|
|
41
|
+
*
|
|
42
|
+
* Chromeless playback (kino owning the controls) requires a **paid** Vimeo plan;
|
|
43
|
+
* on a free-account video Vimeo renders its own controls under kino's overlay.
|
|
44
|
+
*
|
|
45
|
+
* Per Vimeo's embed terms, kino does not obscure the player — no poster-on-pause
|
|
46
|
+
* cover over the embed; kino's controls sit alongside Vimeo's surface.
|
|
47
|
+
*/
|
|
48
|
+
declare function VimeoPlayer({
|
|
49
|
+
accentColor,
|
|
50
|
+
theme,
|
|
51
|
+
className,
|
|
52
|
+
placeholder,
|
|
53
|
+
children,
|
|
54
|
+
...opts
|
|
55
|
+
}: VimeoPlayerProps): import("react").JSX.Element;
|
|
56
|
+
//#endregion
|
|
57
|
+
export { VimeoPlayer, type VimeoPlayerProps, type VimeoProviderOptions, createVimeoProvider, parseVimeoSource, playerUrl };
|
package/dist/vimeo.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { c as Captions, d as Player, l as IdleOverlay, t as ControlBar } from "./control-bar-B-p1rMgm.js";
|
|
2
|
+
import { t as defaultState } from "./fake-provider-Do3fX4-T.js";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/vimeo/provider.ts
|
|
6
|
+
function parseVimeoSource(input) {
|
|
7
|
+
const trimmed = input.trim();
|
|
8
|
+
const q = trimmed.match(/[?&]h=([\w]+)/);
|
|
9
|
+
const path = trimmed.match(/(?:^|\/)(\d+)(?:\/([\w]+))?/);
|
|
10
|
+
if (!path) return { id: trimmed };
|
|
11
|
+
const id = path[1];
|
|
12
|
+
const hash = q?.[1] ?? path[2];
|
|
13
|
+
return hash ? {
|
|
14
|
+
id,
|
|
15
|
+
hash
|
|
16
|
+
} : { id };
|
|
17
|
+
}
|
|
18
|
+
function playerUrl(id, hash) {
|
|
19
|
+
return `https://player.vimeo.com/video/${id}?h=${hash}`;
|
|
20
|
+
}
|
|
21
|
+
const SDK_SRC = "https://player.vimeo.com/api/player.js";
|
|
22
|
+
let apiPromise = null;
|
|
23
|
+
function loadVimeoAPI() {
|
|
24
|
+
const w = window;
|
|
25
|
+
if (w.Vimeo?.Player) return Promise.resolve(w.Vimeo);
|
|
26
|
+
if (apiPromise) return apiPromise;
|
|
27
|
+
apiPromise = new Promise((resolve, reject) => {
|
|
28
|
+
const finish = () => {
|
|
29
|
+
if (w.Vimeo?.Player) resolve(w.Vimeo);
|
|
30
|
+
else reject(/* @__PURE__ */ new Error("Vimeo SDK loaded but window.Vimeo is missing"));
|
|
31
|
+
};
|
|
32
|
+
const existing = document.querySelector(`script[src="${SDK_SRC}"]`);
|
|
33
|
+
if (existing) {
|
|
34
|
+
existing.addEventListener("load", finish);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const script = document.createElement("script");
|
|
38
|
+
script.src = SDK_SRC;
|
|
39
|
+
script.async = true;
|
|
40
|
+
script.addEventListener("load", finish);
|
|
41
|
+
document.head.appendChild(script);
|
|
42
|
+
});
|
|
43
|
+
return apiPromise;
|
|
44
|
+
}
|
|
45
|
+
function readyVimeo() {
|
|
46
|
+
if (typeof window === "undefined") return null;
|
|
47
|
+
const v = window.Vimeo;
|
|
48
|
+
return v && typeof v.Player === "function" ? v : null;
|
|
49
|
+
}
|
|
50
|
+
function mapQualities(raw) {
|
|
51
|
+
return {
|
|
52
|
+
qualities: raw.filter((q) => parseInt(q.id, 10) > 0).map((q) => ({
|
|
53
|
+
id: q.id,
|
|
54
|
+
height: parseInt(q.id, 10),
|
|
55
|
+
bitrate: 0,
|
|
56
|
+
selected: q.active
|
|
57
|
+
})),
|
|
58
|
+
activeId: raw.find((q) => q.active)?.id ?? "auto"
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function mapTracks(raw) {
|
|
62
|
+
const seen = /* @__PURE__ */ new Map();
|
|
63
|
+
return raw.map((t) => {
|
|
64
|
+
const base = `${t.language}.${t.kind}`;
|
|
65
|
+
const n = seen.get(base) ?? 0;
|
|
66
|
+
seen.set(base, n + 1);
|
|
67
|
+
return {
|
|
68
|
+
id: n === 0 ? base : `${base}.${n}`,
|
|
69
|
+
kind: t.kind,
|
|
70
|
+
label: t.label || t.language,
|
|
71
|
+
lang: t.language,
|
|
72
|
+
mode: t.mode === "showing" ? "showing" : "disabled"
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function createVimeoProvider(opts) {
|
|
77
|
+
const explicit = parseVimeoSource(opts.videoId);
|
|
78
|
+
const initial = {
|
|
79
|
+
id: explicit.id,
|
|
80
|
+
hash: opts.hash ?? explicit.hash
|
|
81
|
+
};
|
|
82
|
+
let player = null;
|
|
83
|
+
let destroyed = false;
|
|
84
|
+
let desiredRate = opts.defaultRate ?? 1;
|
|
85
|
+
let state = {
|
|
86
|
+
...defaultState(),
|
|
87
|
+
rate: desiredRate,
|
|
88
|
+
muted: opts.muted ?? false,
|
|
89
|
+
capabilities: {
|
|
90
|
+
canSetRate: true,
|
|
91
|
+
hasStoryboard: false,
|
|
92
|
+
canPiP: false,
|
|
93
|
+
canFullscreen: true,
|
|
94
|
+
canSetQuality: false,
|
|
95
|
+
hasTextTracks: false
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
99
|
+
const emit = () => listeners.forEach((l) => l());
|
|
100
|
+
const patch = (p) => {
|
|
101
|
+
state = {
|
|
102
|
+
...state,
|
|
103
|
+
...p
|
|
104
|
+
};
|
|
105
|
+
emit();
|
|
106
|
+
};
|
|
107
|
+
const onFullscreenChange = () => patch({ fullscreen: document.fullscreenElement != null });
|
|
108
|
+
const actions = {
|
|
109
|
+
play: () => void player?.play().catch(() => {}),
|
|
110
|
+
pause: () => void player?.pause().catch(() => {}),
|
|
111
|
+
seek: (t) => {
|
|
112
|
+
patch({ seeking: true });
|
|
113
|
+
player?.setCurrentTime(t).catch(() => {});
|
|
114
|
+
},
|
|
115
|
+
setRate: (r) => {
|
|
116
|
+
desiredRate = r;
|
|
117
|
+
player?.setPlaybackRate(r).then(() => patch({ rate: r })).catch(() => {});
|
|
118
|
+
},
|
|
119
|
+
setVolume: (v) => void player?.setVolume(v).catch(() => {}),
|
|
120
|
+
setMuted: (m) => void player?.setMuted(m).catch(() => {}),
|
|
121
|
+
setQuality: (id) => void player?.setQuality(id).catch(() => {}),
|
|
122
|
+
setTextTrack: (id) => {
|
|
123
|
+
if (id == null) {
|
|
124
|
+
patch({
|
|
125
|
+
activeTextTrackId: null,
|
|
126
|
+
activeCueText: ""
|
|
127
|
+
});
|
|
128
|
+
player?.disableTextTrack().catch(() => {});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const ref = state.textTracks.find((t) => t.id === id);
|
|
132
|
+
patch({ activeTextTrackId: id });
|
|
133
|
+
if (ref) player?.enableTextTrack(ref.lang, ref.kind, false).catch(() => {});
|
|
134
|
+
},
|
|
135
|
+
enterFullscreen: (wrapper) => {
|
|
136
|
+
if (wrapper.requestFullscreen) wrapper.requestFullscreen();
|
|
137
|
+
},
|
|
138
|
+
exitFullscreen: () => {
|
|
139
|
+
if (document.fullscreenElement) document.exitFullscreen?.();
|
|
140
|
+
},
|
|
141
|
+
enterPiP: () => {},
|
|
142
|
+
exitPiP: () => {}
|
|
143
|
+
};
|
|
144
|
+
const setSessionMetadata = (title) => {
|
|
145
|
+
if (typeof navigator === "undefined" || !("mediaSession" in navigator)) return;
|
|
146
|
+
if (typeof MediaMetadata === "undefined") return;
|
|
147
|
+
try {
|
|
148
|
+
navigator.mediaSession.metadata = new MediaMetadata({ title });
|
|
149
|
+
} catch {}
|
|
150
|
+
};
|
|
151
|
+
const onLoaded = async () => {
|
|
152
|
+
if (!player) return;
|
|
153
|
+
const p = player;
|
|
154
|
+
const [duration, rawQualities, rawTracks, muted] = await Promise.all([
|
|
155
|
+
p.getDuration().catch(() => 0),
|
|
156
|
+
p.getQualities().catch(() => []),
|
|
157
|
+
p.getTextTracks().catch(() => []),
|
|
158
|
+
p.getMuted().catch(() => false)
|
|
159
|
+
]);
|
|
160
|
+
if (destroyed) return;
|
|
161
|
+
p.setPlaybackRate(desiredRate).catch(() => {});
|
|
162
|
+
const { qualities, activeId } = mapQualities(rawQualities);
|
|
163
|
+
const tracks = mapTracks(rawTracks);
|
|
164
|
+
setSessionMetadata(opts.metadata?.videoTitle ?? "Video");
|
|
165
|
+
patch({
|
|
166
|
+
duration: duration || state.duration,
|
|
167
|
+
readyState: 4,
|
|
168
|
+
muted,
|
|
169
|
+
qualities,
|
|
170
|
+
activeQualityId: activeId,
|
|
171
|
+
textTracks: tracks,
|
|
172
|
+
capabilities: {
|
|
173
|
+
...state.capabilities,
|
|
174
|
+
canSetQuality: qualities.length > 0,
|
|
175
|
+
hasTextTracks: tracks.length > 0
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
const bindEvents = (p) => {
|
|
180
|
+
p.on("play", () => patch({
|
|
181
|
+
paused: false,
|
|
182
|
+
ended: false
|
|
183
|
+
}));
|
|
184
|
+
p.on("pause", () => patch({ paused: true }));
|
|
185
|
+
p.on("ended", () => patch({
|
|
186
|
+
paused: true,
|
|
187
|
+
ended: true
|
|
188
|
+
}));
|
|
189
|
+
p.on("bufferstart", () => {
|
|
190
|
+
if (!state.seeking) patch({ paused: false });
|
|
191
|
+
});
|
|
192
|
+
p.on("bufferend", () => {});
|
|
193
|
+
p.on("timeupdate", (d) => {
|
|
194
|
+
const e = d;
|
|
195
|
+
patch({
|
|
196
|
+
currentTime: e.seconds ?? 0,
|
|
197
|
+
duration: e.duration ?? state.duration,
|
|
198
|
+
seeking: false,
|
|
199
|
+
readyState: 4
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
p.on("progress", (d) => {
|
|
203
|
+
const e = d;
|
|
204
|
+
const duration = e.duration ?? state.duration;
|
|
205
|
+
patch({ buffered: duration > 0 ? [[0, e.percent * duration]] : [] });
|
|
206
|
+
});
|
|
207
|
+
p.on("seeking", (d) => {
|
|
208
|
+
patch({
|
|
209
|
+
seeking: true,
|
|
210
|
+
currentTime: d?.seconds ?? state.currentTime
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
p.on("seeked", (d) => {
|
|
214
|
+
patch({
|
|
215
|
+
seeking: false,
|
|
216
|
+
ended: false,
|
|
217
|
+
currentTime: d?.seconds ?? state.currentTime
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
p.on("volumechange", (d) => {
|
|
221
|
+
const e = d;
|
|
222
|
+
patch({
|
|
223
|
+
volume: e.volume,
|
|
224
|
+
muted: e.muted ?? state.muted
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
p.on("playbackratechange", (d) => {
|
|
228
|
+
const e = d;
|
|
229
|
+
desiredRate = e.playbackRate;
|
|
230
|
+
patch({ rate: e.playbackRate });
|
|
231
|
+
});
|
|
232
|
+
p.on("fullscreenchange", (d) => {
|
|
233
|
+
patch({ fullscreen: !!d.fullscreen });
|
|
234
|
+
});
|
|
235
|
+
p.on("enterpictureinpicture", () => patch({ pip: true }));
|
|
236
|
+
p.on("leavepictureinpicture", () => patch({ pip: false }));
|
|
237
|
+
p.on("error", (d) => {
|
|
238
|
+
const e = d;
|
|
239
|
+
patch({ error: {
|
|
240
|
+
code: 0,
|
|
241
|
+
message: e.name ? `${e.name}: ${e.message ?? ""}`.trim() : e.message ?? "Vimeo playback error"
|
|
242
|
+
} });
|
|
243
|
+
});
|
|
244
|
+
p.on("loaded", () => void onLoaded());
|
|
245
|
+
p.on("qualitychange", (d) => patch({ activeQualityId: d.quality }));
|
|
246
|
+
p.on("cuechange", (d) => {
|
|
247
|
+
patch({ activeCueText: d.cues?.[0]?.text ?? "" });
|
|
248
|
+
});
|
|
249
|
+
p.on("texttrackchange", (d) => {
|
|
250
|
+
const e = d;
|
|
251
|
+
if (e.language == null) {
|
|
252
|
+
patch({
|
|
253
|
+
activeTextTrackId: null,
|
|
254
|
+
activeCueText: ""
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
patch({ activeTextTrackId: state.textTracks.find((t) => t.lang === e.language && t.kind === e.kind)?.id ?? state.activeTextTrackId });
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
const createPlayer = (v, host) => {
|
|
262
|
+
const ctorOpts = {
|
|
263
|
+
controls: false,
|
|
264
|
+
autoplay: !!opts.autoPlay,
|
|
265
|
+
muted: !!opts.muted,
|
|
266
|
+
loop: !!opts.loop,
|
|
267
|
+
playsinline: true,
|
|
268
|
+
dnt: true,
|
|
269
|
+
keyboard: false
|
|
270
|
+
};
|
|
271
|
+
if (initial.hash) ctorOpts.url = playerUrl(initial.id, initial.hash);
|
|
272
|
+
else ctorOpts.id = initial.id;
|
|
273
|
+
const p = new v.Player(host, ctorOpts);
|
|
274
|
+
player = p;
|
|
275
|
+
p.ready().then(() => {
|
|
276
|
+
if (destroyed) return;
|
|
277
|
+
});
|
|
278
|
+
bindEvents(p);
|
|
279
|
+
};
|
|
280
|
+
return {
|
|
281
|
+
mount(container) {
|
|
282
|
+
const host = document.createElement("div");
|
|
283
|
+
container.appendChild(host);
|
|
284
|
+
document.addEventListener("fullscreenchange", onFullscreenChange);
|
|
285
|
+
const v = readyVimeo();
|
|
286
|
+
if (v) createPlayer(v, host);
|
|
287
|
+
else loadVimeoAPI().then((loaded) => {
|
|
288
|
+
if (destroyed) return;
|
|
289
|
+
if (host.isConnected) createPlayer(loaded, host);
|
|
290
|
+
});
|
|
291
|
+
},
|
|
292
|
+
getState: () => state,
|
|
293
|
+
subscribe: (l) => {
|
|
294
|
+
listeners.add(l);
|
|
295
|
+
return () => listeners.delete(l);
|
|
296
|
+
},
|
|
297
|
+
swapSource(next) {
|
|
298
|
+
if (!player || next.src == null) return;
|
|
299
|
+
const { id, hash } = parseVimeoSource(next.src);
|
|
300
|
+
player.loadVideo(hash ? { url: playerUrl(id, hash) } : Number(id)).then(() => void player?.setPlaybackRate(desiredRate).catch(() => {})).catch(() => {});
|
|
301
|
+
if (next.metadata?.videoTitle != null) setSessionMetadata(next.metadata.videoTitle);
|
|
302
|
+
patch({
|
|
303
|
+
currentTime: 0,
|
|
304
|
+
duration: 0,
|
|
305
|
+
ended: false,
|
|
306
|
+
seeking: false,
|
|
307
|
+
error: null
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
actions,
|
|
311
|
+
destroy() {
|
|
312
|
+
destroyed = true;
|
|
313
|
+
document.removeEventListener("fullscreenchange", onFullscreenChange);
|
|
314
|
+
try {
|
|
315
|
+
player?.destroy();
|
|
316
|
+
} catch {}
|
|
317
|
+
player = null;
|
|
318
|
+
listeners.clear();
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/vimeo/vimeo-player.tsx
|
|
324
|
+
const packSrc = (videoId, hash) => {
|
|
325
|
+
const parsed = parseVimeoSource(videoId);
|
|
326
|
+
const h = hash ?? parsed.hash;
|
|
327
|
+
return h ? playerUrl(parsed.id, h) : parsed.id;
|
|
328
|
+
};
|
|
329
|
+
/**
|
|
330
|
+
* kino's glass chrome over a Vimeo video, backed by the Vimeo Player SDK. Pass a
|
|
331
|
+
* numeric id or any vimeo.com / player.vimeo.com URL; for unlisted videos pass
|
|
332
|
+
* the `hash` (or a share URL that already contains it).
|
|
333
|
+
*
|
|
334
|
+
* Only `videoId`, `hash`, and `metadata.videoTitle` are reactive (they flow
|
|
335
|
+
* through `swapSource`). `autoPlay`, `muted`, `loop`, and `defaultRate` are read
|
|
336
|
+
* once at creation — remount (e.g. via `key`) to change them.
|
|
337
|
+
*
|
|
338
|
+
* Chromeless playback (kino owning the controls) requires a **paid** Vimeo plan;
|
|
339
|
+
* on a free-account video Vimeo renders its own controls under kino's overlay.
|
|
340
|
+
*
|
|
341
|
+
* Per Vimeo's embed terms, kino does not obscure the player — no poster-on-pause
|
|
342
|
+
* cover over the embed; kino's controls sit alongside Vimeo's surface.
|
|
343
|
+
*/
|
|
344
|
+
function VimeoPlayer({ accentColor, theme, className, placeholder, children, ...opts }) {
|
|
345
|
+
const providerRef = useRef(null);
|
|
346
|
+
if (providerRef.current === null) providerRef.current = createVimeoProvider(opts);
|
|
347
|
+
const provider = providerRef.current;
|
|
348
|
+
const mountedRef = useRef(false);
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
if (!mountedRef.current) {
|
|
351
|
+
mountedRef.current = true;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
provider.swapSource?.({
|
|
355
|
+
src: packSrc(opts.videoId, opts.hash),
|
|
356
|
+
metadata: opts.metadata
|
|
357
|
+
});
|
|
358
|
+
}, [
|
|
359
|
+
opts.videoId,
|
|
360
|
+
opts.hash,
|
|
361
|
+
opts.metadata?.videoTitle
|
|
362
|
+
]);
|
|
363
|
+
return /* @__PURE__ */ jsxs(Player, {
|
|
364
|
+
provider,
|
|
365
|
+
accentColor,
|
|
366
|
+
theme,
|
|
367
|
+
className,
|
|
368
|
+
placeholder,
|
|
369
|
+
children: [
|
|
370
|
+
/* @__PURE__ */ jsx(IdleOverlay, {}),
|
|
371
|
+
/* @__PURE__ */ jsx(Captions, {}),
|
|
372
|
+
/* @__PURE__ */ jsx(ControlBar, {}),
|
|
373
|
+
children
|
|
374
|
+
]
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
//#endregion
|
|
378
|
+
export { VimeoPlayer, createVimeoProvider, parseVimeoSource, playerUrl };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karnstack/kino",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Themeable React video player with pluggable providers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Karn",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"react",
|
|
19
19
|
"mux",
|
|
20
20
|
"youtube",
|
|
21
|
+
"vimeo",
|
|
21
22
|
"hls",
|
|
22
23
|
"video-player"
|
|
23
24
|
],
|
|
@@ -49,6 +50,10 @@
|
|
|
49
50
|
"types": "./dist/youtube.d.ts",
|
|
50
51
|
"import": "./dist/youtube.js"
|
|
51
52
|
},
|
|
53
|
+
"./vimeo": {
|
|
54
|
+
"types": "./dist/vimeo.d.ts",
|
|
55
|
+
"import": "./dist/vimeo.js"
|
|
56
|
+
},
|
|
52
57
|
"./styles.css": "./dist/styles.css"
|
|
53
58
|
},
|
|
54
59
|
"peerDependencies": {
|