@karnstack/kino 0.1.0 → 0.1.2

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 CHANGED
@@ -57,6 +57,16 @@ Give the player a sized container. It fills `100%` width and height of its paren
57
57
 
58
58
  kino is auth-agnostic. For signed playback you mint the `playback`, `thumbnail`, and `storyboard` tokens server-side and hand them to the player through the `tokens` prop. The player never holds a signing key and never talks to your auth layer; it only appends the tokens you give it to the media, thumbnail, and storyboard URLs. For public playback you can omit `tokens` entirely.
59
59
 
60
+ ### Blur-up placeholder
61
+
62
+ Before the poster and first frame load, the video box is empty. Pass a small `placeholder` (a base64 data URI or a URL) and kino paints it behind the video as a blur-up; the sharp poster covers it once decoded, and it reappears briefly across source swaps.
63
+
64
+ ```tsx
65
+ <MuxPlayer playbackId="..." placeholder={blurDataUrl} />
66
+ ```
67
+
68
+ The poster itself stays the signed Mux thumbnail (kino derives it from `playbackId` + the `thumbnail` token), so `placeholder` is purely the instant low-res layer underneath.
69
+
60
70
  ## Theming
61
71
 
62
72
  The quickest knob is the `accentColor` prop, which drives the scrubber fill, active menu items, and range controls.
@@ -403,7 +403,7 @@ function useIsCompact() {
403
403
  return useContext(CompactContext);
404
404
  }
405
405
  const ControlsVisibilityContext = createContext(null);
406
- function Player({ provider, accentColor, theme, className, children }) {
406
+ function Player({ provider, accentColor, theme, className, placeholder, children }) {
407
407
  const wrapperRef = useRef(null);
408
408
  const videoHostRef = useRef(null);
409
409
  const hoveredRef = useRef(false);
@@ -496,13 +496,23 @@ function Player({ provider, accentColor, theme, className, children }) {
496
496
  className: ["kino", className].filter(Boolean).join(" "),
497
497
  style,
498
498
  tabIndex: 0,
499
- children: [/* @__PURE__ */ jsx("div", {
500
- ref: videoHostRef,
501
- className: "kino-video-host"
502
- }), /* @__PURE__ */ jsx(PlayerChrome, {
503
- compact,
504
- children
505
- })]
499
+ children: [
500
+ placeholder && /* @__PURE__ */ jsx("img", {
501
+ className: "kino-placeholder",
502
+ src: placeholder,
503
+ alt: "",
504
+ "aria-hidden": "true",
505
+ draggable: false
506
+ }),
507
+ /* @__PURE__ */ jsx("div", {
508
+ ref: videoHostRef,
509
+ className: "kino-video-host"
510
+ }),
511
+ /* @__PURE__ */ jsx(PlayerChrome, {
512
+ compact,
513
+ children
514
+ })
515
+ ]
506
516
  })
507
517
  })
508
518
  });
package/dist/index.d.ts CHANGED
@@ -21,6 +21,12 @@ type PlayerProps = {
21
21
  accentColor?: string;
22
22
  theme?: Record<string, string>;
23
23
  className?: string;
24
+ /**
25
+ * Low-res still (data URI or URL) painted behind the video while the poster
26
+ * and first frame load — a blur-up. The sharp poster covers it once decoded,
27
+ * so it only shows during the initial load and across source swaps.
28
+ */
29
+ placeholder?: string;
24
30
  children?: ReactNode;
25
31
  };
26
32
  declare function Player({
@@ -28,6 +34,7 @@ declare function Player({
28
34
  accentColor,
29
35
  theme,
30
36
  className,
37
+ placeholder,
31
38
  children
32
39
  }: PlayerProps): import("react").JSX.Element;
33
40
  declare namespace Player {
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as usePlayer, a as SkipBackButton, c as Captions, d as Player, f as useControlsVisible, g as useMediaSelector, h as PlayerContext, i as PlayPauseButton, l as IdleOverlay, m as useWrapperRef, n as FullscreenButton, o as SkipForwardButton, p as useIsCompact, r as PipButton, s as VolumeControl, t as ControlBar, u as Scrubber, v as usePlayerActions, y as formatTime } from "./control-bar-DWzMIb23.js";
1
+ import { _ as usePlayer, a as SkipBackButton, c as Captions, d as Player, f as useControlsVisible, g as useMediaSelector, h as PlayerContext, i as PlayPauseButton, l as IdleOverlay, m as useWrapperRef, n as FullscreenButton, o as SkipForwardButton, p as useIsCompact, r as PipButton, s as VolumeControl, t as ControlBar, u as Scrubber, v as usePlayerActions, y as formatTime } from "./control-bar-DrAeqaap.js";
2
2
  export { Captions, ControlBar, FullscreenButton, IdleOverlay, PipButton, PlayPauseButton, Player, PlayerContext, Scrubber, SkipBackButton, SkipForwardButton, VolumeControl, formatTime, useControlsVisible, useIsCompact, useMediaSelector, usePlayer, usePlayerActions, useWrapperRef };
package/dist/mux.d.ts CHANGED
@@ -24,13 +24,15 @@ declare function createMuxProvider(opts: MuxProviderOptions): Provider;
24
24
  type MuxPlayerProps = MuxProviderOptions & {
25
25
  accentColor?: string;
26
26
  theme?: Record<string, string>;
27
- className?: string;
27
+ className?: string; /** Blur-up still painted behind the video until the poster/first frame loads. */
28
+ placeholder?: string;
28
29
  children?: ReactNode;
29
30
  };
30
31
  declare function MuxPlayer({
31
32
  accentColor,
32
33
  theme,
33
34
  className,
35
+ placeholder,
34
36
  children,
35
37
  ...opts
36
38
  }: MuxPlayerProps): import("react").JSX.Element;
package/dist/mux.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c as Captions, d as Player, l as IdleOverlay, t as ControlBar } from "./control-bar-DWzMIb23.js";
1
+ import { c as Captions, d as Player, l as IdleOverlay, t as ControlBar } from "./control-bar-DrAeqaap.js";
2
2
  import { useEffect, useRef } from "react";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
4
  import "@mux/mux-video";
@@ -99,6 +99,7 @@ function createMuxProvider(opts) {
99
99
  hasTextTracks: true
100
100
  }
101
101
  };
102
+ let desiredRate = opts.defaultRate ?? 1;
102
103
  const listeners = /* @__PURE__ */ new Set();
103
104
  const emit = () => listeners.forEach((l) => l());
104
105
  const patch = (p) => {
@@ -193,6 +194,7 @@ function createMuxProvider(opts) {
193
194
  const syncFromEl = () => {
194
195
  if (!el) return;
195
196
  bindRenditions();
197
+ if (el.playbackRate !== desiredRate) el.playbackRate = desiredRate;
196
198
  applyTextTrackModes();
197
199
  const ranges = [];
198
200
  for (let i = 0; i < el.buffered.length; i++) ranges.push([el.buffered.start(i), el.buffered.end(i)]);
@@ -201,7 +203,7 @@ function createMuxProvider(opts) {
201
203
  currentTime: el.currentTime,
202
204
  duration: el.duration || 0,
203
205
  buffered: ranges,
204
- rate: el.playbackRate,
206
+ rate: desiredRate,
205
207
  volume: el.volume,
206
208
  muted: el.muted,
207
209
  readyState: el.readyState,
@@ -275,7 +277,9 @@ function createMuxProvider(opts) {
275
277
  if (el) el.currentTime = t;
276
278
  },
277
279
  setRate: (r) => {
280
+ desiredRate = r;
278
281
  if (el) el.playbackRate = r;
282
+ patch({ rate: r });
279
283
  },
280
284
  setVolume: (v) => {
281
285
  if (el) el.volume = Math.min(1, Math.max(0, v));
@@ -329,7 +333,7 @@ function createMuxProvider(opts) {
329
333
  el.playsInline = true;
330
334
  el.poster = opts.poster ?? buildImageUrl(opts.playbackId, "thumbnail", opts.tokens?.thumbnail);
331
335
  if (opts.autoPlay) el.autoplay = true;
332
- el.playbackRate = state.rate;
336
+ el.playbackRate = desiredRate;
333
337
  if (opts.envKey) el.envKey = opts.envKey;
334
338
  if (opts.metadata) el.metadata = {
335
339
  video_id: opts.metadata.videoId,
@@ -399,7 +403,7 @@ function createMuxProvider(opts) {
399
403
  }
400
404
  //#endregion
401
405
  //#region src/mux/mux-player.tsx
402
- function MuxPlayer({ accentColor, theme, className, children, ...opts }) {
406
+ function MuxPlayer({ accentColor, theme, className, placeholder, children, ...opts }) {
403
407
  const providerRef = useRef(null);
404
408
  if (providerRef.current === null) providerRef.current = createMuxProvider(opts);
405
409
  const provider = providerRef.current;
@@ -427,6 +431,7 @@ function MuxPlayer({ accentColor, theme, className, children, ...opts }) {
427
431
  accentColor,
428
432
  theme,
429
433
  className,
434
+ placeholder,
430
435
  children: [
431
436
  /* @__PURE__ */ jsx(IdleOverlay, {}),
432
437
  /* @__PURE__ */ jsx(Captions, {}),
package/dist/styles.css CHANGED
@@ -34,6 +34,18 @@
34
34
  width: 100%;
35
35
  height: 100%;
36
36
  }
37
+ .kino .kino-placeholder {
38
+ position: absolute;
39
+ inset: 0;
40
+ width: 100%;
41
+ height: 100%;
42
+ object-fit: cover;
43
+ /* Same level as the video host but earlier in the DOM, so the video element
44
+ (and its sharp poster, once decoded) paints over this blur-up. */
45
+ z-index: 0;
46
+ pointer-events: none;
47
+ user-select: none;
48
+ }
37
49
  .kino .kino-video-host {
38
50
  position: absolute;
39
51
  inset: 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karnstack/kino",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Themeable React video player with pluggable providers.",
5
5
  "license": "MIT",
6
6
  "author": "Karn",