Media Reference
This document is the complete working reference for Media: the video item's data model, the upload and transcoding lifecycle, renditions and captions, the library's organizing tools, the playback and stream-signing scheme, the AI layers, and the DVR and Restream data models.
It is the technical companion to Media, which explains the concepts and workflows. Read that first if you are new to the module; come here when you need the exact behavior of a field, a key, or a lifecycle. The AI transcription and dubbing tools that run on these same videos have their own reference: see Dubbing.
Table of Contents
- Overview
- The Video Item
- Upload & Processing
- Renditions
- Captions & Transcripts
- Organizing the Library
- Playback & Streaming
- AI on Media
- DVR
- Restream
- Background Tasks
- Storage & Key Map
- Access & Permissions
1. Overview
Media is three surfaces over one backend, the HMS streaming platform at hms.pfs.gdn:
| Surface | Route | Backing store | Created here? |
|---|---|---|---|
| Videos | /media/videos | DUS media collection (relational) | Yes — by upload |
| DVR | /media/dvr | Project key/value store, dvr:* records | No — provisioned on HMS |
| Restream | /media/restream | DUS liveRestream + liveSources | Restreams yes; sources no |
The console is a control surface. HMS ingests and transcodes uploads, records live channels into time-shiftable buffers, signs and delivers streams, and executes restream pushes. The dashboard reads and writes the records that describe this work and surfaces its status; it does not transcode, record, or push bytes itself.
Everything is scoped to the active project (project IN $CURRENT_USER.projects.project_id) and shared by that project's team.
2. The Video Item
A video is a row in the DUS media collection (numeric primary key). The operator-facing fields:
| Field | Type | Notes |
|---|---|---|
id | number | Primary key; appears in the detail URL /media/videos/{id} |
status | enum | published · draft · archived — the editorial/publish state |
title | string | Required; defaults to the uploaded filename |
description | string | Free-form |
key | string | The HMS streaming key — the handle for signed playback, embeds, and storyboards (§7) |
duration | seconds | Total length; populated by the transcode pipeline (read-only) |
thumbnail | file | Poster image; replaceable in the editor |
thumbnailSquare | file | Square variant, used mainly in XML/podcast feeds |
tags | string list | Free-text tags (§6) |
programmableTags | relation | Shared taxonomy tags (advancedTags, M2M) (§6) |
season / episode | number | Series metadata |
segments | Segment[] | Chapters (§6) |
introStartTime / introEndTime | HH:MM:SS | Intro markers (skip-intro window) |
sourceRawMetadata | JSON | The ffprobe probe of the source — container, bitrate, and per-stream details. The source of truth for resolution, frame rate, and codec (§3) |
mediaStatus | enum | The processing lifecycle, distinct from status (§3) |
sourceFileUrl | string | Where the uploaded original lives (required) |
sourceFileName / sourceFileSize / sourceFileLastModified / sourceFileBucket | — | Provenance of the source upload |
datePublished | datetime | Display date for podcast/feed contexts |
mediaFormats | relation | The transcoded renditions (§4) |
project | relation | Owning project |
Rights-management fields (youtubeContentId, facebookCopyrightId, and their *Status companions) also exist for content-ID workflows but are outside the dashboard's editing surface.
The
mediacollection has no flatwidth/height/fps/codeccolumns. Technical specs are read fromsourceRawMetadata(the ffprobe video/audio streams), which is why a freshly uploaded video shows no resolution until probing completes.
3. Upload & Processing
Upload
Uploads are resumable and chunked (TUS, 50 MB chunks, auto-start), accepting video/*, audio/*, and .mxf. They run through a global upload panel that persists across navigation; a dropped connection resumes from the last chunk. Entry points: the library toolbar's upload button, and full-page drag-and-drop onto the list.
On a successful upload the dashboard creates the media row with the raw source only:
{ title: <filename>, sourceFileUrl, sourceFileName, sourceFileSize,
sourceFileLastModified, mediaStatus: 'uploaded', project }
Everything else — the HMS key, duration, sourceRawMetadata, thumbnail, renditions, and captions — is filled in asynchronously by the HMS pipeline. There is no transcoding code in the console.
The mediaStatus lifecycle
mediaStatus tracks the pipeline, separately from the editorial status:
new (record exists, no source) → uploaded / uploading → transcoding / transferring → ready / completed, with error on failure and reprocess to re-run.
Two "processing" signals
The UI gates on processing in two independent ways — worth distinguishing when diagnosing:
| Signal | Source | Effect |
|---|---|---|
| Card "Processing" badge | sourceRawMetadata has no probed streams yet | Card dims and shows an amber Processing badge instead of its status |
| Player gate | No mediaFormats entry is completed yet | The player shows a processing placeholder (blurred poster + percent) and will not play; it polls every 10 s and starts once a rendition completes |
4. Renditions
Each transcoded output is a row in the mediaFormats collection, a child of media:
| Field | Type | Notes |
|---|---|---|
id | id | Primary key |
parent | relation | The owning media item |
type | enum | The rendition (e.g. 1080, 720, 480, 360, 240) |
transcodingStatus | string | The per-rendition state; completed is what the player waits for (§3) |
processingPercent | number | Progress for in-flight renditions; the placeholder averages the active ones |
calculatedFileSize | bytes | Rendition size; the library sums these for the displayed total |
bucket / fileName | string | Storage location of the rendition |
status | enum | published · draft · archived · transcoding · failed |
transcodingJobId / transcodingTaskId | string | Pipeline correlation handles |
5. Captions & Transcripts
Closed captions and transcript tracks are rows in the mediaVtt collection:
| Field | Type | Notes |
|---|---|---|
key | id | Primary key |
mediaKey | string | Links to the video's media.key (required) |
type | string | Track type — closed-caption tracks are named cc-{lang} (e.g. cc-en) |
order | number | Ordering within the video's track set |
body | text | The WebVTT payload |
Caption tracks are produced by the HMS pipeline. They are also the input to AI chapter generation, which reads every cc-* track for a video, ordered by order (§8).
6. Organizing the Library
The list
The library renders as a responsive grid of cards. List state (search, status, sort, page, size) is reflected in the URL so a view is shareable; default page size is 50. A 30-second visibility-gated poll watches the item count and raises a new content banner (Refresh / Dismiss) when it changes.
- Search matches
title,description, andkey(case-insensitive), plus an exact numericidmatch. - Status filter defaults to "not archived" (
status ≠ archived); can pin to an exact status or show all.
Tags vs programmable tags
tags | programmableTags | |
|---|---|---|
| Storage | csv column on media | M2M to the advancedTags collection |
| Nature | Free text, per-video | Shared, project-scoped taxonomy |
| Editing | Enter to add, Backspace to remove | Debounced typeahead against advancedTags |
An advancedTags entry carries { name, notes, config, status, project } (lowercase names recommended) and is reused across videos.
Chapters (segments)
Each chapter is { startTime, segmentTitle, segmentTitleAr }:
| Field | Format | Notes |
|---|---|---|
startTime | HH:MM:SS | Where the chapter begins |
segmentTitle | string | English title |
segmentTitleAr | string | Arabic title (RTL-aware) |
Chapters are added manually (a row seeds its start from the current player time) or generated by AI (§8); each row plays-from-here and locks/edits independently.
Intro markers
introStartTime / introEndTime (HH:MM:SS) bound the intro for skip-intro behavior; each has a "use current player time" control in the editor.
Bulk edit
A selection can be edited at once across status, season, episode, tags, and programmable tags. Array fields (tags) support add / remove / replace modes; the change is applied per selected video with a success/partial-failure summary.
7. Playback & Streaming
Signed stream URLs
On-demand playback is authorized per-play. The player asks the server to sign the video's key, and signStream returns a short-lived HLS URL:
https://hms.pfs.gdn/stream/{signReference}.m3u8?token={token}
The token is an AES-256-CBC encryption (random 16-byte IV; URL-safe base64) of a signature object:
| Field | Value |
|---|---|
time | Issue time |
signatureExpiry | time + 5 minutes — how long the URL may be presented |
streamExpiry | time + 6 hours — how long the granted stream stays valid |
type | ondemand |
mediaKey | The video's key |
limitToIP / limitToCountry | false (IP/country binding off) |
The signing material — signKey and signReference — comes from the project's HMS feature configuration. Because the signature expires in five minutes, the player re-signs on demand rather than caching a URL.
A second mode, direct URL, plays a fully-formed URL (e.g. an authenticated source file) straight into the player, bypassing both signing and the transcode gate. This is what the picture-in-picture player uses for direct files.
Storyboard scrubbing
The player fetches a sprite-sheet manifest at:
https://hms.pfs.gdn/hms/v1/ondemand/{mediaKey}/storyboard.vtt
Each cue is a sprite with an #xywh= crop; hovering or seeking the progress bar renders the matching crop as a thumbnail. It is progressive enhancement — silent on any failure, and skipped in direct-URL mode.
Embeds and shortcuts
- Embed / open-in-new-tab:
https://hms.pfs.gdn/embed/{key}. - Keyboard:
,/.step ±1 s ·Space/kplay-pause ·mmute ·ffullscreen ·←/→±5 s ·j/l±10 s ·Home/Endstart/end. - Picture-in-picture: any video pops into a floating, draggable player that survives navigation. Volume is shared across every player.
8. AI on Media
Extractions
On ingest, the HMS pipeline analyzes each video into a structured summary stored in the project key/value store under description:{mediaId}. The dashboard reads it read-only (a missing record is simply absent):
| Field | Notes |
|---|---|
suggestedTitle | Proposed title |
short_description / long_description | Two summary lengths |
hosts / guests | Detected people |
subjects / top_topics | Topical tags |
language | Detected spoken language |
speakers_count | Number of distinct speakers |
Extractions are surfaced in a collapsible panel on the detail page and are a prerequisite for chapter generation (below).
Chapter generation
Generation turns a video's caption transcript into bilingual chapters. The flow:
- Read the video's
keyandduration. - Gather every
cc-*track frommediaVttfor thatmediaKey, ordered byorder, and join thebodytext. - Render the chapter prompt (parameterized by video duration) and call the language model with a strict JSON response contract.
- Validate each returned chapter for
startTime,segmentTitle,segmentTitleArand write them tomedia.segments.
The model is instructed to start the first chapter at 00:00:00, target 2–8 minute chapters (~5–10 for a 30-minute video), write specific titles, and translate to Modern Standard Arabic (translation, not transliteration), chronological with no gaps.
Gating: the control is available only when the video is longer than 5 minutes and extractions exist. It runs as a background task keyed generateSegments:{mediaId} (§11); on completion the chapter editor is replaced with the result (after a confirmation when chapters already exist).
Deeper AI (Inference)
Editable transcription (speaker-diarized speech-to-text) and full dubbing (translation, synthetic voices, audio mixing, translated graphics, optional lip-sync) operate on these same media items but live in the Inference product, with their own data model and tasks. See Dubbing.
9. DVR
What a DVR channel is
A DVR channel is a live channel that HMS records into a time-shiftable buffer. It is read-only in the console — there is no create/edit/delete UI; channels are provisioned on HMS. Each channel is a key/value record (dvr:*) whose value an operator-side process populates:
| Field | Notes |
|---|---|
reference | Display name — the card title and detail heading |
embed | The HMS embed-player URL, loaded in an iframe on the detail page |
thumbnail | A still image URL, refreshed periodically on the list |
playbackHls? | Optional low-latency live edge URL, used for the card's hover preview |
DVR records live in a shared system project (
77b81e67-9054-4d5e-b770-0d5e8fc07c5e) — the same store that holds per-user preferences — and are queried by thedvr:key prefix, not from the active workspace project.
The list
Channels render in a 1/2/3-column grid sorted by key. Thumbnails are kept current by refreshing one channel every 3 seconds in rotation (a cache-busting ?t= timestamp), and the rotation pauses entirely while the browser tab is hidden.
Card preview and playback
A card whose record carries playbackHls previews live on hover: after a 300 ms delay the card enlarges and plays the low-latency stream with sound (falling back to muted autoplay if the browser blocks audio). Clicking a card opens /media/dvr/{key}, whose detail page is the HMS embed player in an iframe. All seek, scrub, and time-shift controls over the recorded window come from that HMS player, not from the console.
10. Restream
Sources
A restream's input is a liveSources row — a live channel already ingested into HMS. Only sources with status: published are selectable.
| Field | Notes |
|---|---|
reference | Display name |
streamName / streamServer | The ingest endpoint where the source publishes into HMS |
internalPlayback | HMS playback handle (m4s://… or hlss://…), rewritten client-side into an HLS preview URL |
status | published to be selectable |
The internalPlayback scheme is transformed for the preview player: m4s://host/path → https://host/path/index.m3u8 (thumbnail …/preview.jpg); hlss://host/path/index.m3u8 → the same https://…/index.m3u8 (thumbnail …/preview.jpg).
Restreams
A restream is a liveRestream row:
| Field | Notes |
|---|---|
reference | The restream's name |
source | The liveSources record (required) |
destination | The full push URL, with the stream key embedded; immutable once created |
status | Runtime state, driven by the backend worker — running · starting · stopping · stopped · error · archived |
serverStatus | The worker's own status string (read-only) |
project | Owning project |
Creating a restream
A single-step dialog: choose a source (previewed live), name it, pick a platform, and supply the credentials. The console composes the platform-correct push URL:
| Platform | Composed destination |
|---|---|
| YouTube | rtmp://x.rtmp.youtube.com/live2/{streamKey} |
rtmps://live-api-s.facebook.com:443/rtmp/{streamKey} | |
| Twitch | rtmp://ingest.global-contribute.live-video.net/app/{streamKey} |
| Custom | {server}/{streamName} — server must be rtmp://, rtmps://, or srt:// |
On read, the platform is detected back from the destination URL (by hostname). A stream key that starts with rtmp is rejected as a likely pasted-URL mistake.
The list and lifecycle
Restreams split into Running (anything not archived) and Recent (archived). A running card shows the source → destination flow with a connection indicator, a live-ticking duration, and the worker's serverStatus. The broadcast itself is executed by the HMS worker — the console only writes the record and mirrors state. Stopping a restream sets status: archived (there is no delete and no in-place restart); it moves to Recent. One restream targets one destination, so fanning a source out to several platforms means several restreams.
11. Background Tasks
Long-running AI work runs through a shared NATS task system. The media-relevant jobs are chapter generation (generateSegments) and transcription (transcribeMedia, the ASR/Inference entry point that operates on a media item).
Lifecycle:
- The client creates a task; the request is authorized, the per-type validator runs with the caller's token, a deterministic task key is derived, a
pendingprogress record is written totasks:{type}:{key}, and a message is published to NATS. - The client polls
tasks:{type}:{key}every 2 seconds untilcompleted/failed, skipping fetches while the tab is hidden, and re-attaching to an already-running task on mount. - The consumer routes the message to its handler, patches the record to
processing, reports progress as it runs, and finally patchescompleted(with a result) orfailed. A separate reaper marks tasks stuck without progress for ~5 minutes as failed.
Task keys are deterministic per item and stage — generateSegments:{mediaId} — so anyone viewing the same video sees the same in-flight task, and re-requesting a running stage attaches rather than starting a second one.
12. Storage & Key Map
| Object | Where | Notes |
|---|---|---|
| Video item | DUS media | One row per video (§2) |
| Renditions | DUS mediaFormats | Child of media (§4) |
| Captions | DUS mediaVtt | Keyed to media.key (§5) |
| Programmable tags | DUS advancedTags + media_advancedTags junction | Shared taxonomy (§6) |
| AI extractions | KVS description:{mediaId} | Read-only, written by HMS (§8) |
| Task progress | KVS tasks:{type}:{key} | Background jobs (§11) |
| DVR channel | KVS dvr:* (system project 77b81e67-…) | Read-only (§9) |
| Live source | DUS liveSources | Read-only here (§10) |
| Restream | DUS liveRestream | Create / read / update (stop = archive) (§10) |
13. Access & Permissions
Media access is governed by DUS policies, all project-scoped (project IN $CURRENT_USER.projects.project_id):
| Policy | Grants | Surfaces |
|---|---|---|
| HMS / Media: Write | CRUD on media, mediaFormats, advancedTags, liveSources (read), liveRestream, DVR | Videos, Restream |
| HMS / DVR: Read | Read DVR records | DVR (browse-only) |
liveRestream allows create/read/update but not delete — which is why stopping is implemented as archiving. liveSources is read-only from the console: sources are ingested and managed on HMS.
For the concepts and workflows behind these models, see Media.
