OpenSeek SDK

This page describes one contract: the lifecycle from a lookup request to a rendered tile, in five steps. The Kotlin and JavaScript packages implement exactly this, in the standard library and in the browser, with no dependencies.

The lifecycle

  1. Player ready. Before the first seek, the player issues one lookup request carrying the title's metadata id and its reported duration, with the API key in an X-API-Key header. Do this once per title, not once per scrub: the lookup does not depend on the playback position.

    GET /v1/sprites?imdb_id=tt0111161&duration_ms=8280000
    X-API-Key: <your key>

    The header is required. GET /v1/sprites is a keyed route and answers 401 without a valid key.

  2. Lookup. The registry matches the nearest indexed version to the reported duration and returns JSON. version identifies which one was chosen, source_duration_ms is the runtime of the indexed copy, and scale is the ratio between the requested duration and that source duration, so a player reporting a slightly different runtime still lands on the right cue. status and covered_until_ms describe how far coverage reaches.

    {
      "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
    }
  3. VTT fetched. The player fetches vtt_url. No API key is needed for this request, and for GET /s/… paths the response is cacheable. The file is WebVTT: a WEBVTT header, then one cue per frame consisting of HH:MM:SS.mmm --> HH:MM:SS.mmm followed by a payload.

    WEBVTT
    
    00:00:00.000 --> 00:00:10.000
    sheet-320x180-0.jpg#xywh=0,0,320,180
    
    00:00:10.000 --> 00:00:20.000
    sheet-320x180-0.jpg#xywh=320,0,320,180

    Past the one-hour mark the timestamps start with 01:, so a parser must not assume two components. A full bundle is positional — cue i covers second i × interval_s — but a partial bundle does not have to be: a bundle captured part-way through a title starts at the timestamp it started at, and one first cue at 00:01:00 is ordinary. Do not index cues by position. Keep them in the order they appear and, for a scrub position, take the last cue whose start is less than or equal to the position.

  4. Sheet fetched. Each cue payload names a sheet and the tile box inside it, separated by #xywh=: sheet-320x180-N.jpg#xywh=x,y,w,h. The sheet name is relative. Resolve it against the directory of the vtt_url that handed it to you, not against the registry origin — vtt_url is frequently an object-storage URL on a different host, and building a sheet URL from the origin instead of from the VTT will fetch the wrong thing or nothing.

    Each sheet is a grid of 320×180 tiles. Read x, y, w and h from the cue and do not derive them from a grid size: sheets written by the generator are 10×10, and sheets uploaded by clients are 5×5. Both are served.

  5. Render. Crop the tile described by x, y, w, h from the sheet and show it above the scrubber. Both SDKs cache decoded sheets in memory, 32 entries, so a scrub across one sheet is one fetch rather than one per position.

Coverage

status is pending or complete. A pending version has real previews only up to covered_until_ms, and everything after that point is simply not there. A floor lookup on its own returns the last cue at or before the position, which past coverage means a stale tile from the tail of the file. A player must compare the position against covered_until_ms and show nothing past it. Both packages apply that bound for you: thumbnailFor returns null there, and paintPeek returns false.

Kotlin

import openseek.OpenSeekClient
import openseek.thumbnailFor

// /v1/sprites is keyed: apiKey is sent as X-API-Key.
val client = OpenSeekClient("https://openseek.duckdns.org",
                            apiKey = System.getenv("OPENSEEK_KEY"))

val track = client.loadMovieTrack(imdbId = "tt0111161",
                                 durationMs = player.durationMs) ?: return
// during a scrub, positionMs is the current playback position:
val cue = track.thumbnailFor(positionMs) ?: return   // null past coverage
val tile = client.cropTile(client.sheetBytes(cue.imageUrl), cue)

loadMovieTrack returns null on 404, meaning the title is not indexed. cropTile returns a BufferedImage and uses javax.imageio; on Android, decode the same bytes with BitmapFactory and take the sub-bitmap instead.

JavaScript

import { loadMovieTrack, paintPeek } from "@openseek/sdk";

// /v1/sprites is keyed: wrap fetch so the lookup carries X-API-Key.
const key = "<your key>";
const authedFetch = (url, init = {}) =>
  fetch(url, { ...init, headers: { ...init.headers, "X-API-Key": key } });

const track = await loadMovieTrack("https://openseek.duckdns.org",
  { imdb_id: "tt0111161", duration_ms: video.durationMs },
  { fetchFn: authedFetch });
if (!track) return;                        // 404: title not indexed

// during a scrub, positionMs is the current playback position:
const shown = await paintPeek(canvas, track, positionMs);
shown ? preview.show() : preview.hide();  // false = past coverage

paintPeek crops into a canvas you supply, so the element is yours to place and size. Sheet requests are made with crossOrigin set, which is what keeps the sheet untainted.

Packages

Source: github.com/Yatin-Code/openseek-sdk. Both packages are MIT licensed and dependency-free. Publishing is prepared but not live: the npm package is @openseek/sdk at version 0.2.0-unreleased, and the Maven coordinates are tv.openseek:openseek-sdk-jvm at the same version. Neither is on a public registry yet — the Maven one has a publish template but no Gradle build behind it — so read the sources in the repo instead of installing.

Elsewhere