Adapter: Thumbnail Source
IThumbnailSource answers “what sprite sub-region shows this timestamp” for a scrubber’s hover-preview thumbnail, load once per item, then a synchronous lookup(time) on every hover-move. Like Adapter: Chapter Source, this is a standalone contract, not a setup() field: DesktopUiPlugin’s own scrub-preview (see Plugin: Desktop UI) calls the same underlying sprite-parsing helpers directly rather than going through this interface, this adapter exists for custom scrubber UI built outside that plugin.
The interface
interface IThumbnailSource {
/** Fetch/cache thumbnails for item. Returns false when the item has no source. */
load(item: BasePlaylistItem): Promise<boolean>;
/** Frame covering timeSeconds, or null. Synchronous, called on every scrub event. */
lookup(timeSeconds: number): ThumbnailFrame | null;
/** Release cached data for the current item. */
unload(): void;
}
interface ThumbnailFrame {
spriteUrl: string;
x: number; y: number; w: number; h: number; // sub-region within spriteUrl, in pixels
start: number; end: number; // cue time range, in seconds
}
x/y/w/h are all zero for a source that packs one standalone image per cue instead of a sprite sheet, ThumbnailFrame covers both shapes with the same fields.
The shipped implementation
VttSpriteThumbnailSource reads item.previewSpriteUrl, fetches a WebVTT sprite manifest (cue body format sprite.webp#xywh=x,y,w,h, the format NoMercy’s media-server sprite generator produces, and compatible with ffmpeg tileify output and the common web-player sprite convention), and preloads the sprite image so lookup() stays synchronous once loaded:
import nmplayer from '@nomercy-entertainment/nomercy-video-player';
const player = nmplayer('player');
const source = new VttSpriteThumbnailSource();
const available = await source.load(player.item()!); // false if no previewSpriteUrl
if (available) {
const frame = source.lookup(45); // ThumbnailFrame | null
}
Rules & restrictions
load()resolvesfalse(not a rejected promise) when the item has nopreviewSpriteUrl, treat that as “no preview available,” not an error condition.lookup()must stay synchronous in any implementation, it runs on every pointer-move across the scrubber.- The default implementation caches exactly one item’s sprite set at a time, call
load()again on every item change, there’s no automatic per-item cache eviction to configure.
How to extend it
Implement against a different manifest format, or a source that returns one full-size frame per cue instead of sprite sub-regions:
class ApiThumbnailSource implements IThumbnailSource {
private frames: ThumbnailFrame[] = [];
async load(item: BasePlaylistItem): Promise<boolean> {
this.frames = await fetchThumbnailManifest(item.id);
return this.frames.length > 0;
}
lookup(timeSeconds: number): ThumbnailFrame | null {
return this.frames.find((f) => timeSeconds >= f.start && timeSeconds < f.end) ?? null;
}
unload(): void {
this.frames = [];
}
}
Wire load() from a 'mediaReady' listener and lookup() from your scrubber’s pointer-move handler in your own overlay code, same as Adapter: Chapter Source, there is no automatic hook.
Next steps
- Adapter: Subtitle Style Store: the persistence-focused adapter, saving user preferences instead of looking up media metadata.