---
title: "media.vod@1 · OpenVibe.Services"
canonical: "https://openvibe.services/docs/contracts/media.vod"
updated: null
---

# media.vod@1 · OpenVibe.Services

HTML: [media.vod@1 · OpenVibe.Services](https://openvibe.services/docs/contracts/media.vod)

# MediaVod `media.vod@1`

Generated at 2026-11-13 06:25:48 UTC from [`openvibe-contracts v0.419.0`](https://github.com/OpenVibers/OpenVibe.Contracts/tree/v0.419.0) and [`openvibe-sdk v0.161.0`](https://github.com/OpenVibers/OpenVibe.SDK/tree/v0.161.0).

- Version: 1.0.0
- Owner: media
- Visibility: first-party
- Status: active
- Compatibility: backward
- Schema: [`https://openvibe.network/contracts/media/vod.v1.json`](/docs/contracts/media.vod.json)

media.vod@1: one recorded stream (VOD) as OpenVibe.Media's API answers it (server/vod/finalize.js vodPublic: GET /api/v1/:app/vods/:id, the list, and every read that returns a row). Its numeric id, its app-local user id, title and description, derived status (pending, recording, ready or failed), duration and size, the file basename, the URL its bytes play from (playback\_url; hls\_url is the master playlist when the object has one), thumbnail, storage provider, visibility, view counts, readiness and times. watch\_id is the id openvibe.video addresses the watch page by (openvibe.video/watch?v=<watch\_id>): 11 characters of URL-safe base64 from 8 random bytes, fixed for the row's life, never derived from the numeric id. More fields may be added; a consumer reads what it knows.

## Fields

| Field | Type | Required | Description | Constraints |
| --- | --- | --- | --- | --- |
| `id` | integer | yes | The VOD's numeric id inside Media. Internal: a watch page is addressed by watch\_id. | - `minimum 1` |
| `watch_id` | string |  | The id of its watch page on openvibe.video (openvibe.video/watch?v=<watch\_id>): 8 random bytes as URL-safe base64, 64 bits. One id space across VODs and clips. Optional: a row written before the ids existed may answer without one until Media's backfill fills it. | - `pattern ^[A-Za-z0-9_-]{11}$` |
| `app_id` | string | yes | The tenant that owns the recording (live, media-hub, games…). |  |
| `stream_id` | integer \| null |  |  |  |
| `managed_stream_id` | integer \| null |  |  |  |
| `user_id` | string \| integer \| null |  | The owning app's local user id; it is not a Network subject id. |  |
| `title` | string | yes |  | - `maxLength 300` |
| `description` | string |  |  |  |
| `status` | enum | yes | Derived (db.vodStatus): recording while it is written, failed when its health quarantined it, ready once a file and a duration or size are there, pending before. | - `one of "pending", "recording", "ready", "failed", "unknown"` |
| `duration` | number | yes |  | - `minimum 0` |
| `duration_seconds` | number |  |  | - `minimum 0` |
| `duration_source` | string \| null |  | probe (ffprobe), remux (packet timeline) or null (stored 0). |  |
| `file_size` | integer |  |  | - `minimum 0` |
| `file_path` | string \| null |  | The file's basename only; a server path is never returned. |  |
| `playback_url` | string | yes | The URL its bytes play from on openvibe.media. |  |
| `thumbnail_url` | string \| null |  |  |  |
| `preview_url` | string \| null |  | The hover preview of this item: about 6 s, muted, 320 px wide, MP4 H.264, stored beside its thumbnail and served at /t/<name>. Null until the preview.make job has made it (audio-only, private and too-short items never get one). |  |
| `preview_webm_url` | string \| null |  | The same moment as preview\_url, encoded as WebM (VP9, VP8 where the encoder has no VP9). Null while preview\_url is. |  |
| `storage_provider` | string |  | local, b2 or r2. |  |
| `visibility` | enum | yes | public: anyone. unlisted: reached only by its exact id, never listed, noindex. private: missing to everyone but its owner and staff. | - `one of "public", "unlisted", "private"` |
| `is_public` | boolean | yes | True only for visibility public (kept beside it for older readers). |  |
| `health_status` | string \| null |  |  |  |
| `clips_only` | boolean |  |  |  |
| `is_recording` | boolean |  |  |  |
| `ai_overview` | string \| null |  |  |  |
| `ai_analyzed_at` | string \| null |  |  |  |
| `view_count` | integer |  |  | - `minimum 0` |
| `unique_views` | integer |  |  | - `minimum 0` |
| `readiness` | object |  | The object's readiness levels (server/objects/readiness.js) when the read asked for them. |  |
| `readiness.metadata` | boolean |  |  |  |
| `readiness.bytes_verified` | boolean |  |  |  |
| `readiness.playable` | boolean |  |  |  |
| `readiness.reason` | string \| null |  |  |  |
| `hls_url` | string |  | The object's master playlist, when it has one (MEDIA\_HLS\_ENABLED). |  |
| `hls_expires_at` | string \| integer |  |  |  |
| `created_at` | string | yes | UTC 'YYYY-MM-DD HH:MM:SS' as the store keeps it. |  |

## Examples

From the contract's own test fixtures: valid ones validate, rejected ones must fail.

 Valid: public-recording-with-watch-id

~~~~
{
  "id": 6203,
  "watch_id": "dQw4w9WgXcQ",
  "app_id": "live",
  "stream_id": 912,
  "managed_stream_id": 88,
  "user_id": 80,
  "title": "Fortnite, and the last ten minutes",
  "description": "",
  "status": "ready",
  "duration": 5400,
  "duration_seconds": 5400,
  "duration_source": "probe",
  "file_size": 734003200,
  "file_path": "vod-live-6203-1759910400000.webm",
  "playback_url": "https://openvibe.media/v/6203",
  "thumbnail_url": "https://openvibe.media/t/vod-6203.jpg",
  "storage_provider": "local",
  "visibility": "public",
  "is_public": true,
  "health_status": "ok",
  "clips_only": false,
  "is_recording": false,
  "ai_overview": null,
  "ai_analyzed_at": null,
  "view_count": 1321,
  "unique_views": 902,
  "created_at": "2026-11-06 21:00:00"
}
~~~~

Valid: unlisted-before-the-backfill

~~~~
{
  "id": 6204,
  "app_id": "live",
  "user_id": 80,
  "title": "An unlisted rehearsal",
  "description": "",
  "status": "ready",
  "duration": 300,
  "duration_seconds": 300,
  "duration_source": "probe",
  "file_size": 18874368,
  "file_path": "vod-live-6204-1759914000000.webm",
  "playback_url": "https://openvibe.media/v/6204",
  "thumbnail_url": null,
  "storage_provider": "b2",
  "visibility": "unlisted",
  "is_public": false,
  "clips_only": false,
  "is_recording": false,
  "view_count": 3,
  "unique_views": 3,
  "created_at": "2026-11-06 22:00:00"
}
~~~~

Rejected: watch-id-invalid-character

~~~~
{
  "id": 6205,
  "watch_id": "dQw4w9WgXc!",
  "app_id": "live",
  "title": "A VOD whose watch id has a character base64url never has",
  "status": "ready",
  "duration": 60,
  "visibility": "public",
  "is_public": true,
  "playback_url": "https://openvibe.media/v/6205",
  "created_at": "2026-11-06 23:00:00"
}
~~~~

Rejected: watch-id-wrong-length

~~~~
{
  "id": 6206,
  "watch_id": "too-short",
  "app_id": "live",
  "title": "A VOD with a 9-character watch id",
  "status": "ready",
  "duration": 60,
  "visibility": "public",
  "is_public": true,
  "playback_url": "https://openvibe.media/v/6206",
  "created_at": "2026-11-06 23:05:00"
}
~~~~

## Validate

~~~~
const contracts = require('openvibe-contracts');
contracts.validate('media.vod@1', value);   // { valid, errors: [{ path, message }] }
~~~~
