Scrubber Preview
Headless seek-bar preview: sprite, WebVTT, or captured video thumbnails with hover time.
|
|
| Package |
@vizejs/ui/scrubber-preview |
| Maturity |
stable |
| Own the source |
vize lib pull scrubber-preview |
| Requires |
context, controllable-state |
| Aliases |
scrubber preview, video thumbnail, seek preview, storyboard, trick play, thumbnail track |
| Covers |
WebVTT thumbnail tracks (JW Player / Video.js convention), Media Fragments URI xywh, Vidstack slider thumbnails, HTMLVideoElement + canvas frame capture |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { ScrubberPreview, ScrubberPreviewRoot, ScrubberPreviewThumbnail, ScrubberPreviewTime, ScrubberPreviewTrack } from "@vizejs/ui/scrubber-preview";
Or copy the source into your project with vize lib pull scrubber-preview (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Seek bar that previews sprite-sheet thumbnails and the hovered time, seeking on release. -->
<script setup lang="ts">
import { ref, useId } from "vue";
import {
ScrubberPreview,
ScrubberPreviewThumbnail,
ScrubberPreviewTime,
ScrubberPreviewTrack,
} from "@vizejs/ui/scrubber-preview";
import type { ScrubberPreviewSprite } from "@vizejs/ui/scrubber-preview";
const duration = 180;
const currentTime = ref(0);
const seekId = useId();
const sprite: ScrubberPreviewSprite = {
src: "/media/trailer-thumbnails.jpg",
columns: 6,
rows: 6,
interval: 5,
width: 160,
height: 90,
};
function seek(time: number): void {
currentTime.value = Math.round(time);
}
</script>
<template>
<ScrubberPreview :duration :sprite @seek="seek">
<ScrubberPreviewTrack>
<label :for="seekId">Seek</label>
<input
:id="seekId"
v-model.number="currentTime"
type="range"
min="0"
:max="duration"
step="1"
/>
</ScrubberPreviewTrack>
<ScrubberPreviewThumbnail />
<ScrubberPreviewTime />
</ScrubberPreview>
<output>{{ currentTime }} s</output>
</template>
API
ScrubberPreview
Source: src/families/media/scrubber-preview/scrubber-preview-root.vue
Props
| Prop |
Type |
Default |
Description |
duration (required) |
number |
required |
Media duration in seconds. |
time |
number | null |
undefined |
Controlled preview time (v-model:time). null hides the preview; undefined is uncontrolled. |
dir |
ScrubberPreviewDirection |
"ltr" |
Reading direction; RTL tracks map the right edge to time zero. |
disabled |
boolean |
false |
Stop previewing and seeking. |
sprite |
ScrubberPreviewSprite |
undefined |
Sprite-sheet thumbnails. Takes precedence over thumbnails and videoSrc. |
thumbnails |
string | readonly ScrubberPreviewCue[] |
undefined |
WebVTT thumbnails track text, or pre-parsed cues. Takes precedence over videoSrc. |
thumbnailsBaseUrl |
string |
undefined |
Base URL that relative image URLs in VTT text resolve against. |
videoSrc |
string |
undefined |
Video URL used to capture frames on the fly in the browser (same-origin or CORS-enabled). |
captureWidth |
number |
160 |
Width of captured frames in pixels. |
captureInterval |
number |
1 |
Captured frames are shared across this many seconds (cache granularity). |
cacheSize |
number |
40 |
Maximum captured frames kept; older object URLs are revoked. |
Events
| Event |
Payload |
Description |
update:time |
[time: number | null] |
Fired when the preview time requests a new controlled value. |
seek |
[time: number, nativeEvent: Event] |
Fired when the user releases a click or scrub on the track. |
Slots
| Slot |
Slot props |
Description |
default |
ScrubberPreviewSlotState |
Track, thumbnail, and time parts. Receives the preview state. |
Exposed
| Member |
Type |
Description |
active |
— |
|
duration |
— |
|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
ratio |
— |
|
setTime |
— |
|
time |
— |
|
ScrubberPreviewRoot
Source: src/families/media/scrubber-preview/scrubber-preview-root.vue
Props
| Prop |
Type |
Default |
Description |
duration (required) |
number |
required |
Media duration in seconds. |
time |
number | null |
undefined |
Controlled preview time (v-model:time). null hides the preview; undefined is uncontrolled. |
dir |
ScrubberPreviewDirection |
"ltr" |
Reading direction; RTL tracks map the right edge to time zero. |
disabled |
boolean |
false |
Stop previewing and seeking. |
sprite |
ScrubberPreviewSprite |
undefined |
Sprite-sheet thumbnails. Takes precedence over thumbnails and videoSrc. |
thumbnails |
string | readonly ScrubberPreviewCue[] |
undefined |
WebVTT thumbnails track text, or pre-parsed cues. Takes precedence over videoSrc. |
thumbnailsBaseUrl |
string |
undefined |
Base URL that relative image URLs in VTT text resolve against. |
videoSrc |
string |
undefined |
Video URL used to capture frames on the fly in the browser (same-origin or CORS-enabled). |
captureWidth |
number |
160 |
Width of captured frames in pixels. |
captureInterval |
number |
1 |
Captured frames are shared across this many seconds (cache granularity). |
cacheSize |
number |
40 |
Maximum captured frames kept; older object URLs are revoked. |
Events
| Event |
Payload |
Description |
update:time |
[time: number | null] |
Fired when the preview time requests a new controlled value. |
seek |
[time: number, nativeEvent: Event] |
Fired when the user releases a click or scrub on the track. |
Slots
| Slot |
Slot props |
Description |
default |
ScrubberPreviewSlotState |
Track, thumbnail, and time parts. Receives the preview state. |
Exposed
| Member |
Type |
Description |
time |
number | null |
Previewed time in seconds, or null while no preview is shown. |
ratio |
number | null |
Previewed position 0..1 along the track, or null. |
active |
boolean |
Whether a preview is shown. |
duration |
number |
Media duration in seconds. |
element |
HTMLDivElement | null |
Rendered root element. |
setTime |
(time: number | null) => boolean |
Show a preview for a time (e.g. while a seek slider has keyboard focus), or hide it. |
ScrubberPreviewThumbnail
Source: src/families/media/scrubber-preview/scrubber-preview-thumbnail.vue
Events
| Event |
Payload |
Description |
error |
[error: ScrubberPreviewError] |
Fired when a thumbnail cannot be produced. |
Slots
| Slot |
Slot props |
Description |
default |
ScrubberPreviewThumbnailSlotState |
Custom thumbnail rendering. Defaults to an <img> for captured frames; sprite and VTT frames are published as custom properties for CSS backgrounds. |
Exposed
| Member |
Type |
Description |
kind |
ScrubberPreviewKind |
Source of preview images. |
status |
ScrubberPreviewStatus |
Availability of the current frame. |
frame |
ScrubberPreviewFrame | null |
Resolved frame, or null. |
time |
number | null |
Previewed time in seconds, or null. |
error |
ScrubberPreviewErrorCode | null |
Failure code after an error, or null. |
element |
HTMLDivElement | null |
Rendered thumbnail element. |
ScrubberPreviewTime
Source: src/families/media/scrubber-preview/scrubber-preview-time.vue
Props
| Prop |
Type |
Default |
Description |
format |
(seconds: number) => string |
undefined |
Custom formatter. Defaults to m:ss, or h:mm:ss for media of an hour or longer. |
Slots
| Slot |
Slot props |
Description |
default |
ScrubberPreviewTimeSlotState |
Custom time rendering. Receives the previewed time and its formatted text. |
Exposed
| Member |
Type |
Description |
time |
number | null |
Previewed time in seconds, or null. |
text |
string |
Formatted time, e.g. "1:05" or "1:02:03"; empty while inactive. |
element |
HTMLSpanElement | null |
Rendered time element. |
ScrubberPreviewTrack
Source: src/families/media/scrubber-preview/scrubber-preview-track.vue
Slots
| Slot |
Slot props |
Description |
default |
ScrubberPreviewSlotState |
Track contents, typically the consumer's seek bar. Receives the preview state. |
Exposed
| Member |
Type |
Description |
element |
HTMLDivElement | null |
Rendered track element. |
scrubbing |
boolean |
Whether a pointer is scrubbing (pressed) on the track. |
Behavior
Normative state x input -> outcome table for scrubber-preview-root.vue,
scrubber-preview-track.vue, scrubber-preview-thumbnail.vue, and
scrubber-preview-time.vue (@vizejs/ui/scrubber-preview). Every row is proven by the named
test.
The preview is a visual affordance layered on the consumer's seek control: the track maps pointer
positions to a preview time, the thumbnail resolves a frame for that time, and the time part shows
it. All parts are aria-hidden; keyboard users hear the seek slider's own value text, and
setTime() lets a focused slider drive the same preview. Frame sources, in precedence order, are a
sprite sheet, a WebVTT thumbnails track, or frames captured on the fly from a hidden client-only
<video>.
| ID |
State |
Input |
Outcome |
Evidence |
| SP1 |
idle |
render |
root is idle with ratio 0; thumbnail is hidden and decorative; time text is empty |
renders an idle preview with hidden, decorative thumbnail and time parts |
| SP2 |
mouse hover |
pointerenter / move / leave |
the pointer position maps to a time and ratio (--vize-ui-scrubber-preview-ratio); leaving hides the preview; update:time reports each |
hovering the track previews the time under the pointer and leaving hides it |
| SP3 |
press on track |
pointerdown, move, up / cancel / touch |
pressing captures the pointer and keeps the preview while outside; release emits seek(time, event); cancel never seeks; touch hides on lift; RTL maps the right edge to zero; secondary buttons are ignored |
clicking or scrubbing the track emits seek on release; touch hides on lift |
| SP4 |
controlled / disabled |
time prop, setTime(), disabled |
a controlled time wins and clamps to the duration; setTime() shows or hides the preview and reports changes; disabled roots neither preview nor seek |
controlled time, setTime, and disabled roots |
| SP5 |
sprite |
preview time |
the frame's sheet URL and offset/size are published as custom properties; unsafe sheet URLs render nothing and report error |
sprite thumbnails publish background geometry for the previewed frame |
| SP6 |
WebVTT |
preview time |
cues (text or parsed) resolve relative to thumbnailsBaseUrl; regions become offsets; slots receive the frame; gaps are idle |
WebVTT thumbnails resolve cues and expose frames to slots |
| SP7 |
capture |
preview time |
a client-only hidden video captures one frame per captureInterval slot into an LRU cache of object URLs; evicted and unmounted frames are revoked; the default slot renders an <img alt=""> |
captured thumbnails are generated once per interval, cached, and revoked |
| SP8 |
capture busy |
rapid time changes |
only the latest pending slot is captured after the current one; intermediate requests are skipped |
rapid capture requests keep only the latest pending frame |
| SP9 |
capture failure |
seek error / unsafe videoSrc |
the thumbnail reports error with a typed ScrubberPreviewError; unsafe video URLs disable capture (kind="none") |
capture failures surface typed errors |
| SP10 |
captureVideoFrame |
call |
waits for metadata, clamps and seeks, scales to the requested width keeping aspect, and encodes a Blob or data URL |
captureVideoFrame waits for metadata, seeks, scales, and encodes |
| SP11 |
captureVideoFrame |
failure |
no 2D context is …_UNSUPPORTED, draw failures are …_CAPTURE_FAILED, cross-origin encoding is …_TAINTED, empty encodes are …_CAPTURE_FAILED |
captureVideoFrame reports unsupported, tainted, and failed encodes |
| SP12 |
missing provider |
setup |
parts fail closed with the shared context diagnostic |
compound parts require a matching root provider |
| SP13 |
VTT parser |
text |
cues with #xywh= (optionally pixel:) regions parse; malformed cues are skipped; URLs resolve against a base |
parses WebVTT thumbnail cues with sprite regions and resolves relative URLs |
| SP14 |
cue lookup |
time |
binary search finds the covering cue with an exclusive end; gaps return nothing |
finds the cue covering a time by binary search |
| SP15 |
sprite math |
time |
frames fill rows then sheets, clamp to frameCount, and invalid geometry throws a typed RangeError |
maps times to sprite frames across rows and sheets |
| SP16 |
helpers |
pointer, quantize, format |
ratios clamp and mirror for RTL; times snap to capture slots; times format as m:ss or h:mm:ss |
maps pointers to ratios, quantizes times, and formats durations |
| SP17 |
frame cache |
get / set / clear |
least-recently-used entries are evicted and every released URL is reported |
the frame cache evicts least-recently-used URLs |
| SP18 |
SSR |
isolated requests |
markup is byte-identical, never renders the capture <video> or an <img>, and renders sprite geometry for a controlled time |
renders byte-identical scrubber preview markup across isolated SSR requests |
| SP19 |
SSR / hydration |
hydrate |
server markup hydrates without warnings or node replacement |
hydrates scrubber preview markup without warnings or node replacement |
| SP20 |
types |
compile |
kinds, statuses, and error codes are closed; captureVideoFrame returns Blob or string by output |
scrubber-preview.types.test-d.ts |
Captured thumbnails need a same-origin video or CORS headers (the capture video sets
crossorigin="anonymous"); otherwise canvases are tainted and capture reports …_TAINTED.