Seek-preview thumbnails for video players, delivered as sprite sheets and a WebVTT cue track. A player looks up one title, reads one cue for the current playback position, and crops one tile out of a sheet.
OpenSeek indexes films by metadata id and duration, serves the preview assets, and merges new coverage from players that watch.
duration_ms; get back a
WebVTT file and JPEG sprite sheets of 320×180 tiles packed into sheets,
covering that film at a configurable interval (5, 10 or 30 seconds).status and covered_until_ms, so a player knows where
to stop drawing instead of showing a stale frame from the tail of the file.Two packages, both dependency-free:
javax.imageio. Handles the playback half: fetch the lookup, parse
the VTT, resolve a tile for a position, crop it. The contribution half
(ContributeBatch, uploadContribution) is built on a
branch and is not released.Source: github.com/Yatin-Code/openseek-sdk.
Publishing is prepared but not live: the npm package is named
@openseek/sdk and the Maven coordinates are
tv.openseek:openseek-sdk-jvm, but neither is on a public registry
yet. Until then, read the sources in the repo.
import openseek.OpenSeekClient
import openseek.thumbnailFor
val client = OpenSeekClient("https://openseek.duckdns.org",
apiKey = System.getenv("OPENSEEK_KEY"))
val track = client.loadMovieTrack(imdbId = "tt0111161",
durationMs = player.duration) ?: return
// during a scrub, positionMs = current playback position:
val cue = track.thumbnailFor(positionMs) ?: return // null past coverage
val tile = client.cropTile(client.sheetBytes(cue.imageUrl), cue)
import { loadMovieTrack, paintPeek } from "@openseek/sdk";
const track = await loadMovieTrack("https://openseek.duckdns.org",
{ imdb_id: "tt0111161", duration_ms: video.duration * 1000 });
if (!track) return; // not indexed: no preview
// during a scrub, positionMs = current playback position:
const shown = await paintPeek(canvas, track, positionMs);
shown ? popup.show() : popup.hide(); // false = past covered_until_ms
Both packages use floor semantics: the last cue at or before the position,
corrected by the scale the registry reports, and bounded by
covered_until_ms.
The running registry is at https://openseek.duckdns.org.
GET /v1/status — OPEN. One-line JSON service
state.GET /v1/catalog — OPEN. Browsable index of
every indexed title. Each version carries vtt_url,
cue_count, interval_s, status and
covered_until_ms.GET /v1/titles?imdb_id=… — OPEN. All versions
of one title.GET /v1/keys/validate — OPEN. Takes
X-API-Key, returns {valid, name}. Does not count
against quota.GET /v1/sprites?imdb_id=…&duration_ms=… —
KEYED. The main lookup. Returns vtt_url,
version, status, covered_until_ms and
scale.POST /v1/contribute — KEYED. Contributor
upload, multipart: a JSON metadata part plus thumbnails.vtt plus
sheet-*.jpg files.GET /s/… — OPEN. Static VTT and sheet files
when no CDN fronts them.Keys are minted on the registry host and printed once:
python3 mint_key.py <name>
Revoke one by deleting its row:
DELETE FROM keys WHERE key='…';
curl -H 'X-API-Key: <your key>' \
'https://openseek.duckdns.org/v1/sprites?imdb_id=tt0111161&duration_ms=8280000'
The response:
{
"media_type": "movie",
"title": "The Shawshank Redemption",
"imdb_id": "tt0111161",
"vtt_url": "https://openseek.duckdns.org/s/tt0111161-v1/thumbnails.vtt",
"version": 1,
"source_duration_ms": 8280000,
"scale": 1.0,
"status": "complete",
"covered_until_ms": 8280000
}
scale is the ratio of the requested duration to the indexed
source duration, so a player reporting a slightly different runtime still lands
on the right cue. status is pending while coverage is
partial and complete when it reaches the end.
Four components:
thumbnails.vtt with #xywh= cue payloads. Needs ffmpeg
on PATH and pip install Pillow./s/ with long cache headers.sqlite3. Holds
titles, versions, keys and the request ledger, and serves the API above.Shortest path to a working local setup:
# 1. generate previews for a local file (needs ffmpeg + Pillow)
python3 spritegen/spritegen.py --id tt0111161 \
--in movie.mp4 --out store/tt0111161/ --interval 10
# 2. serve the store (standard library only)
python3 server/server.py --store ./store --port 8080
# 3. or run the registry with its own database
python3 registry/init_db.py
python3 registry/registry.py --port 8081
init_db.py is idempotent: it applies the schema and then any
migrations, so it is safe to re-run on an existing database. The generated
store/ is a plain directory of files; no database is needed to
serve it. The end-to-end test runs on the standard library alone, with no
ffmpeg and no Pillow:
python3 tests/test_e2e.py
MIT licensed.
Repositories:
Repo map at a glance: packages/kotlin and
packages/js are the SDKs; docs/wire.md pins the
request and response contract; docs/PUBLISHING.md holds the
release checklist. On the service side the generator, the static server and the
registry are separate top-level directories.
How a contribution actually travels:
POST /v1/contribute carrying a JSON metadata part,
thumbnails.vtt and the sheet-*.jpg files./s/ is served from quarantine.MIT licensed. Everything above is open source and self-hostable: run your own generator, your own store and your own registry, and point your players at it.