Open Video Menu 1.0 · Specification 1.0 · Server API · DVD profile · Compatibility · License · JSON Schema

Contents

Open Video Menu server API — v1 (preview)

Status: preview. It follows the manifest format's versioning: from v1.0 the compatibility charter applies to this API too: v1 endpoints and fields are never removed or redefined.

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as described in RFC 2119.

1. Purpose

A TV app needs two things to show a menu: the manifest and the menu art it names. This API is how any server hands them to any app. Video is never part of it: the app plays media from the media server it already uses.

Two kinds of server implement the same API:

KindExampleBase URL B
Native: the media server itselfJellyfin with the Open Video Menu plugin (plugins/jellyfin){media server}/OpenVideoMenu
Standalone asset server, for media servers that don't support Open Video Menu yetpackages/asset-serverhttp://{host}:{port}

An app written against this API works with both, and keeps working unchanged when a media server adopts Open Video Menu natively.

2. Finding a server (client behaviour)

Apps SHOULD look in this order and use the first that answers:

  1. The media server itself: GET {media server}/OpenVideoMenu/v1/health. Native implementations MUST serve the API under that base path, so this one request tells an app whether its server supports menus.

  2. An asset server the viewer chose before (a saved address).

  3. An asset server on the network: DNS-SD service type _openvideomenu._tcp. The instance's address and port form B (http://{address}:{port}). A host may advertise several addresses; apps SHOULD use the first whose /v1/health answers. If a saved address stops answering, apps SHOULD look on the network again before asking the viewer (the host may have a new address).

    Servers that advertise MUST also answer legacy unicast queries (RFC 6762 §6.7): a query from a port other than 5353 is answered directly to that address and port, repeating the query's ID and question. TV platforms often can't bind port 5353 or join the multicast group, so this is how their apps browse. Apple's mDNSResponder and Avahi already do this; some mDNS libraries don't. An app then takes the server's address from where the answer came from, which also avoids addresses the TV can't reach (e.g. a container's own address), and the port from the SRV record.

If none answers, titles MUST still play, just without menus.

3. Sign-in

The API uses the viewer's existing media-server sign-in. There are no Open Video Menu accounts.

  • Native servers use their own authentication, exactly as for their other endpoints (e.g. Jellyfin's Authorization: MediaBrowser …, Token="…" header).

  • Standalone servers accept the sign-in of the media servers they are configured for, and check it with that media server:

    Media serverHeader the app sendsHow the asset server checks it
    PlexX-Plex-Token: … (or Authorization: Bearer …)The configured Plex server accepts the token (it answers GET /library/sections with it). When that server lets its network in without signing in (it accepts a made-up token too), or can't be reached, plex.tv must list this Plex server among the token's resources instead.
    JellyfinAuthorization: MediaBrowser …, Token="…"The configured Jellyfin server answers GET /Users/Me
  • Credentials MUST travel in headers, never in URLs.

  • A request without a valid sign-in MUST get 401.

  • Servers MUST NOT store viewers' credentials. They MAY cache verdicts briefly in memory.

  • A standalone server MAY keep one credential of its own, given by whoever sets it up: a media-server token or API key it uses to read that media server's library folders and items, so it scans them, follows them when libraries change, and says which item each menu's media is (the index's items). It MUST NOT log it, and SHOULD keep it where only the server and the machine's administrators can read it. It uses it only to read; it MUST NOT change anything on the media server.

4. Endpoints

GET {B}/v1/health

No sign-in. Tells apps the server is there and what it speaks.

{ "ok": true, "name": "Open Video Menu for Jellyfin", "api": "1", "format": "1.0", "manifests": 3, "auth": "jellyfin" }
FieldMeaning
apiAPI major version, "1" here.
formatNewest manifest format version the server reads.
manifestsManifests found so far. It MAY be 0 while a first scan runs.
authWhat sign-in the server accepts, as a human-readable hint.

Servers MAY add fields; apps MUST ignore fields they don't know.

GET {B}/v1/index

Signed in. Every manifest the server found, and the media each one references.

{
  "api": "1",
  "format": "1.0",
  "roots": 2,
  "manifests": [
    {
      "id": "3f1c0d9a8b7e6f50",
      "root": 0,
      "path": "The Matrix (1999)/The Matrix (1999).openvideomenu.json",
      "url": "/v1/files/0/The%20Matrix%20(1999)/The%20Matrix%20(1999).openvideomenu.json",
      "valid": true,
      "version": "1.0",
      "title": { "name": "The Matrix", "year": 1999 },
      "source": { "kind": "dvd" },
      "hasMenus": true,
      "media": [
        { "id": "feature", "kind": "feature", "label": "The Matrix", "ref": "The Matrix (1999).mp4", "path": "The Matrix (1999)/The Matrix (1999).mp4" }
      ],
      "modified": "2026-09-26T23:39:12.000Z"
    }
  ]
}
FieldRequiredMeaning
idyesStable id for this manifest on this server.
rootyesWhich media folder it is in (index into the server's folders). A server that follows the media server's libraries may number its folders differently after a library changes; apps take root and url from the latest index.
pathyesThe manifest's path under that folder, with forward slashes.
urlyesWhere to fetch the manifest, relative to B. Relative asset paths in the manifest resolve against this URL.
validyesWhether the manifest could be read. Invalid entries carry errors (strings) instead of the fields below.
version, title, sourceif validCopied from the manifest. When title has no season or volume (manifest §3.1), servers SHOULD fill them in from the manifest's path, the name nearest the file winning: "Season N" or "Specials" (0) for the season, "Disc N" for the volume, or "SnnDnn" for both. So Example Show/Season 05/Example Show - Disc 2.ovmz is season 5, volume 2.
hasMenusif validWhether it has any menus. A manifest with only a fallback list is still valid.
mediaif validOne entry per media item: id, kind, label and ref (when it has one) from the manifest, plus path.
media[].pathwhen ref is a relative pathThe media file's path under the same folder, with forward slashes. Apps match it against the media server's file paths by their trailing segments (at least the folder and file name), because the two machines may mount the folder at different places.
media[].segmentswhen the media is made of segments(Level 2) The media ids it plays, in order; such media have no ref or path (the files are those of the media listed).
media[].angleswhen the media has angle files(Level 2) One { ref, path } per other angle (angle 2, 3, …), path as for the media itself, so apps can match those files too.
modifiedyesWhen the manifest last changed (ISO 8601).
itemsnoThe media server's items that play the manifest's media, when the server knows them: one { media, server, id, type, show? } per media and media server. media is the manifest's media id; server the media server's id (Plex's machineIdentifier, Jellyfin's server Id); id the item's id there (Plex ratingKey, Jellyfin item Id); type "movie" or "episode"; show an episode's show id.

Items. Native servers know which of their items each media[].path is; a standalone server knows when it has the media server's token (§3). Servers SHOULD then include items. Apps use an item only when its server is the server they are signed in to, and MUST fall back to matching media[].path themselves when items is absent or has nothing for that server. An item lets an app show a menu's title without paging through the library to find it, and find a TV menu's show by its id.

The index MUST NOT list menu asset paths; the server keeps its allowlist to itself. Apps MUST ignore fields they don't know.

Caching. Responses SHOULD carry an ETag and Cache-Control: private, no-cache, and answer If-None-Match with 304, so an unchanged index costs one small request. Servers SHOULD gzip the index when the app accepts it. ?refresh asks the server to rescan before answering. Without it, servers answer from their latest scan and SHOULD rescan in the background.

GET {B}/v1/files/{root}/{path}

Signed in. One manifest, or one menu asset a manifest names.

  • The server MUST serve only the manifests in its index and the menu assets they name (the assets menuAssets() in packages/core returns). Everything else, in particular media files, MUST get 404, the same answer as for a file that doesn't exist.
  • Paths MUST NOT escape their folder (.., absolute paths).
  • Range requests MUST be supported, because players seek in motion menus and transitions.
  • Content-Type SHOULD match the file. Servers MAY let apps cache art briefly (Cache-Control: private, max-age=60).
  • Bundles (format spec §2.1) are served as if they were folders: the manifest at {path to bundle}/manifest.json and each asset at {path to bundle}/{asset path}, for example /v1/files/0/Movies/The%20Matrix%20(1999)/The%20Matrix%20(1999).ovmz/menus/main.jpg. Relative asset URLs then resolve inside the bundle on their own. Range requests work the same way (members are stored uncompressed, so a range maps straight onto the bundle file). The index lists a bundle with path = the bundle's own path and url = its manifest.json; media paths are relative to the folder that holds the bundle, as for a plain manifest.

5. Network

Standalone servers SHOULD answer only home-network and VPN addresses by default (loopback, RFC 1918, link-local, IPv6 unique-local, 100.64.0.0/10). Native servers follow their own remote-access settings.

6. Versions

  • The path carries the API major version (/v1/) and health.api repeats it.
  • Within v1, changes are additive: new optional fields and new endpoints only.
  • A breaking change would be /v2/, served alongside /v1/.