A Play is one run of media on a Player:
a film, an album, or a season of episodes, played in order. Its
lifecycle is analogous to a Job’s: it runs once to completion, and
it stays for its status until ttlSecondsAfterFinished passes or a
person deletes it. Create a Play to start it, delete it to stop it
early, and kubectl get plays lists what plays right now.
The operator reconciles a Play into one playback pod and the
claims that pod needs, all owned by the Play, so deleting the
Play is the whole teardown: the garbage collector takes the pod
and the claims with it. A Finished run leaves nothing running.
The spec is immutable, like a Job’s template. A Play whose
player or media changed mid-run would describe a different run;
delete the Play and create another.
apiVersion: media.liken.sh/v1alpha1
kind: Play
metadata:
name: dune
namespace: media
spec:
players: [studio]
items:
- uri: nfs://nas/media/movies/Dune (2021)/Dune.mkv
presentation:
type: video
hint: movie
title: Dune
year: 2021
start: "0:10:00"
A Play is one run of media on a Player: a film, an album, or a season of episodes, played in order. Create a Play to start it; delete the Play to stop it early.
spec
What to play and where. The spec is immutable: a different film or a different player is a different Play.
| Field | Type | Required | Description |
|---|---|---|---|
players |
[]string | yes | The Players this Play runs on, by name, in this namespace. One entry today. |
items |
[]object | yes | The media to play in order. Each entry is a URI and an optional presentation that declares how the display should render it. |
start |
string | no | Where in the first item the run begins, as a time the player accepts, such as 0:10:00 or 600. Omitted, the run begins at the start. Later items always begin at their own start. This is also how a run resumes: a new Play with the position a finished or deleted one reported. |
trickplayInterval |
string | no | The seconds one trickplay tile covers, as a Go duration like 10s. Jellyfin writes no manifest beside the sheets, so the Play declares it. Omitted, it defaults to 10s, the Jellyfin default. |
ttlSecondsAfterFinished |
integer | no | How long this Play stays after it finishes, in seconds, the meaning a Job gives the name. While it stays, kubectl get plays still answers what just played and where it stopped; deleting the Play deletes that record. Omitted, it is 300 seconds. Zero deletes the Play as soon as it finishes. The playback pod does not wait for this window: it is deleted as soon as the run finishes. |
audioLanguages |
[]string | no | A per-Play override of the audio language order, the most specific tier; omit it to inherit the Player. |
subtitleLanguages |
[]string | no | A per-Play override of the subtitle language order, the most specific tier; omit it to inherit the Player. |
subtitles |
string | no | A per-Play override of when subtitles show, the most specific tier; omit it to inherit the Player. One of: on, off, auto. |
volume |
object | no | The level this run starts at. The operator writes it to the Player’s volume topic before it creates the pod, so the value becomes the unit’s state and stays after the film ends. Omitted, the run starts at whatever the unit already holds. |
spec.items[]
One entry in the playlist: the URI to play and, optionally, how it should look.
| Field | Type | Required | Description |
|---|---|---|---|
uri |
string | yes | The operator resolves https:// to a stream the player reads directly, and nfs://host/export/path to a mount on the playback pod. A URI whose scheme the operator does not know fails the Play before any pod exists. |
presentation |
object | no | How the item should look, for the fields the display cannot read from the file. The library that fed the item supplies these, and the display prefers them over the container’s tags. Omit the block for a loose file, and the display falls back to the file’s own tags. |
spec.items[].presentation
How the item should look, for the fields the display cannot read from the file. The library that fed the item supplies these, and the display prefers them over the container’s tags. Omit the block for a loose file, and the display falls back to the file’s own tags.
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | no | The media type the display tunes its layout by. mpv cannot infer this, and the display does not read it from the file name. One of: video, music, image. |
hint |
string | no | The finer kind within the type. A video is a movie or a series, and music is an album. It selects the layout the display draws. One of: movie, series, album. |
title |
string | no | The item’s name, which overrides the file’s own tag. Set it when the tag is wrong or absent. |
series |
string | no | The series this episode belongs to. |
season |
integer | no | The season number of the episode. |
episode |
integer | no | The episode number within its season. |
episodeTitle |
string | no | The title of the episode. |
year |
integer | no | The release year, shown under a movie’s title. |
date |
string | no | The air date of an episode, shown on its line. Give it as an ISO date like 2017-03-05, and the display formats it. |
logo |
string | no | The logo art URI, nfs:// or https://, resolved the way the media URI is. The display shows it in the header in place of the title. |
trickplay |
string | no | The X.trickplay directory URI, nfs:// or https://, resolved the way the media URI is. The display shows a tile from it on the scrub cursor. |
spec.volume
The level this run starts at. The operator writes it to the Player’s volume topic before it creates the pod, so the value becomes the unit’s state and stays after the film ends. Omitted, the run starts at whatever the unit already holds.
| Field | Type | Required | Description |
|---|---|---|---|
level |
integer | no | The listening level, 0 to 100. 100 is unity, the player’s own default, and the cap: a software gain above unity only distorts. Omitted, the level the unit already holds stays. |
muted |
boolean | no | Whether the run starts muted. Omitted, the muted state the unit already holds stays. |
status
What the playback pod reports, written only by the media operator. The playback pod itself holds no API credentials; it reports to the operator, and the operator writes here.
| Field | Type | Required | Description |
|---|---|---|---|
phase |
string | no | Where the run is in its life, in the words Jobs and Pods use. Pending is declared but not yet performing, Running is the pod performing the play, paused or not, and Finished and Failed are the two ends. The word is Running rather than Playing because a phase moves forward only, and a paused film would force Playing to flap; the paused field beside this one says the rest. One of: Pending, Running, Finished, Failed. |
activity |
string | no | The one word for what the Play does right now, the phase and the paused flag folded together. Starting is Pending, Playing and Paused both mean Running, and Finished and Failed match the phase. The phase is the lifecycle; the activity is what a person reads at a glance. One of: Starting, Playing, Paused, Finished, Failed. |
paused |
boolean | no | True while the player holds the current item still. The phase stays Running, because a pause does not advance the lifecycle. |
item |
integer | no | Which URI plays now, counting from 1 in spec order. The third of five episodes shows 3. |
position |
string | no | The playhead inside the current item, as H:MM:SS. |
duration |
string | no | The length of the current item, as H:MM:SS, once the player has read it. |
pod |
string | no | The playback pod’s name, for kubectl describe and logs. The pod is owned by this Play and is deleted with it. |
message |
string | no | The reason for the phase, as one line of text: the resolver refused a URI, the Player does not exist, the pod failed. |
finishedAt |
string | no | When the operator first read this run’s phase as Finished. The time-to-live after finishing counts from here and not from the Play’s creation, so the window measures the end of the film. It is written here rather than held in the operator, so an operator that restarts reads the clock back. |
audioLanguages |
[]string | no | The resolved audio language order this run applied, the record of what the three tiers settled on. |
subtitleLanguages |
[]string | no | The resolved subtitle language order this run applied. |
subtitles |
string | no | The resolved subtitle setting this run applied, one of on, off, or auto. |
audioLanguage |
string | no | The language of the audio track mpv chose, so you can see when a code matched no track. The value is the track’s own tag as the file carries it, for Matroska the three-letter ISO 639-2 code, whatever form the preference used. |
subtitleLanguage |
string | no | The language of the subtitle track mpv chose; empty when none plays. The value is the track’s own tag as the file carries it, the way audioLanguage reports its track. |
On the bus
The plays tree carries one run’s commands, its report, and its
availability. See the media bus for the
rules every topic follows.
| Topic | Writer | Retained | Carries |
|---|---|---|---|
plays/{namespace}/{name}/commands |
any program | no | one named command |
plays/{namespace}/{name}/status |
the playback pod | yes | the run’s report |
plays/{namespace}/{name}/availability |
the playback pod | yes | online or offline |
commands
The topic any program publishes to drive the run. A translator sidecar, a phone, and a Home Assistant integration all reach the run the same way: publish one JSON command, and the playback pod applies it.
{"action": "seek", "amount": -30}
action names a word from the vocabulary below. amount belongs
only to the three actions that move by one, and its sign is the
direction: seconds for seek, a step for volume and chapter.
| Action | What it does |
|---|---|
pause |
toggles pause |
seek |
moves the playhead by amount seconds |
chapter |
jumps by amount chapters |
volume |
steps the unit’s level by amount |
mute |
toggles the unit’s muted flag |
subtitles |
cycles the subtitle track |
audio |
cycles the audio track |
info |
shows the file name and position for a few seconds |
up, down, left, right, select, back |
drive the on-screen display |
A volume or mute command changes no player directly: the pod
computes the unit’s next state and publishes it on the
Player’s volume topic, and every
pod for the unit applies what that topic delivers. An action this
build has no case for does nothing, so a command from a newer
program has no effect rather than a crash. cycle-focus, the one
Keymap action that never travels here,
becomes a focus cycle request on the
Remote’s tree instead.
status
The run’s report, as the playback pod reads it from the player. The
pod publishes it on every change, and every few seconds while the
position advances. It is retained, so a restarted operator reads a
running Play’s place back from the broker.
{
"paused": false,
"item": 1,
"position": "0:41:22",
"duration": "1:58:03",
"audioLanguage": "eng",
"subtitleLanguage": "eng"
}
item counts from 1 in spec order. duration is empty until the
player has read the item’s header, and the two language fields are
absent while no track of that kind plays. The language values are
the track’s own tags as the file carries them, for Matroska the
three-letter ISO 639-2 codes, whatever form the preference used. One more field, ended,
appears when the run is over and stays set in every later report of
the same run. The pod takes seconds to terminate, so the operator
reads this mark and returns the unit to idle at once instead of
waiting out the pod.
The operator folds each report into the Play’s Kubernetes status,
so a program that only needs the current position can read either
one.
availability
online or offline, retained. The pod names this topic as its
MQTT Last Will with offline as the payload, and publishes online
once it connects, so a retained status a killed pod left behind does
not read as a live run.