Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/scenes-storyboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@karnstack/kino": minor
---

Scenes storyboard passthrough: `ScenesProviderOptions` (and therefore
`ScenesPlayer`) accept `storyboard: { vttUrl }`, surfacing
`capabilities.hasStoryboard` and `state.storyboard` so the scrubber shows
thumbnail previews on hover, exactly as it does for Mux sources. The VTT's
cues point into a sprite image via `#xywh` fragments (the same format the Mux
storyboard track uses). `SceneManifest` gains an optional `storyboard` field
recording where that VTT lives.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ export function Watch() {

Captions are a sidecar VTT fetched and rendered by the parent in kino's own caption overlay, so the host page never deals with text tracks.

Storyboards work the same way: pass `storyboard={{ vttUrl }}` and the scrubber shows thumbnail previews on hover, exactly as it does for Mux sources. The VTT's cues point into a sprite image via `#xywh` fragments (the format Mux storyboard tracks use); whatever produces the sequence generates both files, kino only fetches the VTT and paints the tiles while you scrub. As with `src`, the URL may carry an auth token, already encoded by the caller.

kino sets `allow="autoplay; fullscreen"` on the iframe it creates. That delegates the parent page's user activation (the click on kino's play button) into the frame; without it, `audio.play()` inside the host is blocked by autoplay policy. If your app itself runs inside an iframe, the outer frame needs the same `allow` list.

### The host page
Expand Down Expand Up @@ -240,6 +242,7 @@ type SceneManifest = {
audio: Array<{ bitrate: number; src: string }>
captions?: string
poster?: string
storyboard?: string // thumbnail VTT with #xywh fragments; read by embedders, not the host
chapters?: Array<{ id: string; title: string; start: number }>
}
```
Expand Down
4 changes: 4 additions & 0 deletions src/scenes/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export type SceneManifest = {
audio: Array<{ bitrate: number; src: string }>
captions?: string
poster?: string
// Relative URL of a thumbnail VTT whose cues point into a sprite image via
// #xywh fragments, the same format the Mux storyboard track uses. Consumed
// by embedding players (scrubber hover previews), not by the host.
storyboard?: string
chapters?: Array<{ id: string; title: string; start: number }>
}

Expand Down
16 changes: 16 additions & 0 deletions src/scenes/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ test("capabilities: rate yes, quality/storyboard/pip no, captions when provided"
p2.destroy()
})

test("storyboard option reports the capability and exposes the vtt url", () => {
const vttUrl = "https://scenes.example.com/l/demo/storyboard.vtt?token=abc"
const p = createScenesProvider({ src: SRC, storyboard: { vttUrl } })
const s = p.getState()
expect(s.capabilities.hasStoryboard).toBe(true)
expect(s.storyboard).toEqual({ vttUrl })
p.destroy()
})

test("without a storyboard option the capability is off and state is null", () => {
const p = createScenesProvider({ src: SRC })
expect(p.getState().capabilities.hasStoryboard).toBe(false)
expect(p.getState().storyboard).toBe(null)
p.destroy()
})

test("ready handshake replies with init carrying rate and autoplay", () => {
const p = createScenesProvider({ src: SRC, defaultRate: 1.5, autoPlay: true })
const { iframe } = mount(p)
Expand Down
7 changes: 6 additions & 1 deletion src/scenes/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export type ScenesProviderOptions = {
// Full URL of the host page, token and sequence already encoded by the caller.
src: string
captions?: { src: string; label: string; srclang: string }
// Thumbnail VTT for scrubber hover previews: cues whose payload points into
// a sprite image via #xywh fragments, same format as the Mux storyboard
// track. Full URL, any auth token already encoded by the caller.
storyboard?: { vttUrl: string }
metadata?: { videoId?: string; videoTitle?: string; viewerUserId?: string }
defaultRate?: number
autoPlay?: boolean
Expand Down Expand Up @@ -46,10 +50,11 @@ export function createScenesProvider(opts: ScenesProviderOptions): Provider {
...defaultState(),
rate: desiredRate,
muted: opts.muted ?? false,
storyboard: opts.storyboard ? { vttUrl: opts.storyboard.vttUrl } : null,
capabilities: {
// The stage is resolution independent DOM; there is no rendition ladder.
canSetQuality: false,
hasStoryboard: false,
hasStoryboard: opts.storyboard != null,
// No parent-side media element to promote into PiP.
canPiP: false,
canFullscreen: true,
Expand Down
Loading