# Quiz background playback API

Base URL: `http://127.0.0.1:47831/api/v1` (use the IP and port configured in More settings).
No authentication. Do not expose the listener to the Internet. Browser CORS remains enabled.
All command examples below use POST and `Content-Type: application/json`.
Existing bodyless play/restart/stop, buttons/status and DMX synchronization remain supported.

## Start the background

`POST /buttons/{buttonId}/play`

```json
{"pauseWhenReplaced":true,"allowOverlap":false,"externalSequencing":true}
```

New playback returns HTTP 202:

```json
{"result":"accepted","id":"button-guid","executionId":"media-execution-guid","dmxAllowed":true,"dmxExecutionId":null}
```

Capture `executionId`. It identifies an audio/video run, not a DMX run. Audio-only videos are included.
Images and DMX-only buttons have no multimedia execution ID. `dmxExecutionId` and the DMX sync contract are unchanged.

Overrides are nullable booleans: omitted/null uses the saved button setting. They are kept for this run,
including pause/resume, and never written to the button, backup or cloud. On termination they no longer apply.
`pauseWhenReplaced` controls the outgoing background; `allowOverlap:false` requests replacement through the mixer.
The incoming playlist's Mix on transition and crossfade duration are used for replacement, as in the UI.

`externalSequencing:true` gives sequencing ownership to the external client for this audio/video execution.
It disables automatic Sequence/Shuffle advancement (including the early crossfade trigger), playlist wrapping,
and automatic button loop/restart. At natural completion the run ends normally and leaves Now playing;
the client alone chooses what to start next. This remains effective after pause/resume and through the final
completion callback. It does not change saved playlist/button settings, explicit user commands, manual
replacement crossfades, or DMX timeline looping/synchronization. Omitted or false preserves existing behavior.
Send it again for every new play/restart; it is not a global setting. It cannot be enabled retroactively
with an idempotent play on an already active execution.

`play` on an active run returns 200 `already_active` with its ID, and resumes it if paused, preserving legacy behavior.
It does not change that run's options. `restart` explicitly ends the previous run and creates a new ID;
it also accepts the optional override body. Do not use play/restart to resume a quiz background safely.

## Repeat the current media (runtime loop)

Both play and restart accept optional `"loop": true` or `"loop": false`:

```json
{"pauseWhenReplaced":true,"allowOverlap":false,"externalSequencing":true,"loop":true}
```

The desktop padlock is the saved single-button repeat setting (`IsLoopEnabled`),
not playlist looping. The API override never modifies that setting.

- `loop:true` repeats only this audio/video's configured Start-to-End range
  (or to the source end when End is unset). It takes precedence over
  externalSequencing and prevents automatic Sequence/Shuffle advancement.
- `loop:false` disables the saved button repeat for this run. It does not disable
  ordinary playlist advancement/wrapping; also set externalSequencing:true for that.
- Omitted/null preserves the previous contract: externalSequencing:true suppresses
  saved button looping; without it the saved button/playlist rules still apply.
- Options on play for an already active execution are ignored, as before.
  Explicit restart creates a new execution with the requested options.

All repetitions of an explicit loop retain **one executionId**. They do not emit
completed/natural_end. Position wraps to Start (plus the incoming portion already
consumed when crossfading); duration remains the source duration.
Pause/resume (including pauseWhenReplaced) retains the ID and loop option. Explicit
stop ends it as stopped/explicit_stop; replacement without pause ends it as
stopped/replaced. Decoder failure ends it as failed/playback_error.

This repeats at the decoder boundary rather than issuing another button command.
The initial fade-in and explicit pause/resume/replacement fades remain active.
Both the desktop padlock and API loops use the same audio crossfade engine.
Optional `loopCrossfadeMs` (integer 0-60000) on play/restart overrides the duration:

```json
{"loop":true,"externalSequencing":true,"loopCrossfadeMs":1500}
```

Omitted/null inherits the owning playlist's Mix on transition setting: if enabled,
use the button's positive Fade out duration, otherwise the playlist Crossfade duration;
if disabled, no loop crossfade. Explicit zero requests a hard loop. An explicit
positive value enables the overlap even when Mix is off. The duration is captured
when the execution starts; change it with an intentional restart, not pause/resume.
It does not enable looping on its own. The effective overlap is clamped to half the
Start/End range and a 32 MiB decoded-head cache (as well as the 60-second maximum).
The incoming head and outgoing tail are mixed with complementary linear gains,
then playback continues after the already-consumed head. No new execution, button
command or DMX restart is created for each repetition. Unlocking the desktop padlock
stops user-controlled looping; an explicit API loop override still takes precedence.

This is an audio crossfade, including the audio of video files. It does not blend
two video images. Seamless/sample-accurate video looping is not guaranteed:
the secondary renderer seeks on the next
position check. A valid non-empty seekable media range is required. Test the actual
file/codec and output hardware before live use. Images and DMX-only buttons are not
media loops. This option does not restart, seek or change the associated DMX timeline;
its loop and synchronization lifecycle remain independent.

## Pause for a question

`POST /buttons/{buttonId}/pause`

```json
{"executionId":"media-execution-guid","fadeOutMs":500}
```

HTTP 200 returns the execution state. A nonzero fade is asynchronous (`pausing`); audio keeps advancing during
the fade and is paused at the position reached at its end. Repeated pauses while pausing/paused are harmless
and do not restart the fade.

## Resume at the next cue

`POST /buttons/{buttonId}/resume`

```json
{"executionId":"media-execution-guid","fadeInMs":500}
```

HTTP 200, same execution ID and preserved media position. If already playing, this is a no-op.
Resume during a pending pause cancels that pause. Other active media are replaced/paused using their own
effective settings and normal crossfade rules. A terminated or superseded execution is never restarted.

For explicit pause/resume, fade precedence is:

1. Supplied `fadeOutMs` / `fadeInMs`, including zero for immediate operation.
2. Positive Fade out / Fade in saved on the target button.
3. Owning playlist Crossfade duration, if Mix on transition is enabled.
4. Zero.

Fade values are integer milliseconds from 0 to 60000. An explicit resume fade controls the incoming track;
the outgoing track still uses its own Fade out / playlist Mix settings. Fade-in is reported as `playing`.

## Poll execution status

`GET /executions/{executionId}`

```json
{"executionId":"media-execution-guid","buttonId":"button-guid","state":"paused","positionMs":12500,"durationMs":180000,"endReason":null}
```

Position is the absolute position in the source file, not relative to configured Start.
Duration is the source duration; zero means not yet known. Configured End can complete a run before this duration.
States: `starting`, `playing`, `pausing`, `paused`, `fading_out`, `completed`, `stopped`, `failed`.

| Terminal state | endReason | Meaning |
| --- | --- | --- |
| completed | natural_end | Source/configured End reached normally |
| stopped | explicit_stop | Explicit stop or restart |
| stopped | replaced | Replaced without preserving it in pause |
| failed | playback_error | Decoder/output failure |

Terminal snapshots are retained in memory for at least 10 minutes after termination, even if the Now playing
row disappears. They are not persisted across application restarts. Active and paused runs do not expire.
Buttons/status responses also include `executionId`; a historical ID may no longer be queryable after retention.

**Advance the quiz's background list only for `state == "completed" && endReason == "natural_end"`.**
Never treat HTTP 404, network errors, timeouts, `stopped` or `failed` as a natural end.
Poll every 500-1000 ms. A 202 response acknowledges a start request, not successful decoding; poll the returned ID.

## Safe stop

`POST /buttons/{buttonId}/stop`

```json
{"executionId":"media-execution-guid"}
```

The ID prevents an old command from stopping a new run. Bodyless stop targets whatever
is currently associated with that button. Both forms inherit the audio fade settings.
Optional `fadeOutMs` overrides the duration (integer 0-60000; zero means immediate):

```json
{"executionId":"media-execution-guid","fadeOutMs":500}
```

Precedence: explicit value, otherwise positive button Fade out, otherwise the owning
playlist Crossfade duration when Mix on transition is enabled, otherwise zero.
A positive `fadeOutMs` on either pause or stop forces the audio fade even when button
Fade out is zero and playlist Mix on transition is disabled. The caller supplies the
duration per request (integer milliseconds, 1-60000), independently of saved settings;
no fixed client duration is required and no saved settings are changed. This applies
to audio and the audio portion of video. Zero is immediate; omission inherits.
Repeated requests do not restart an in-progress pause or stop fade.
There is no additional minimum duration. A paused run stops immediately without resuming.
Stopping a pausing run cancels the pending pause and fades from its current gain.

HTTP 200 returns `result: "fading_out"` with `id` and `executionId` while the fade runs.
Poll that execution: state is `fading_out`, then `stopped` with `endReason: "explicit_stop"`.
Immediate stop returns `result: "stopped"`; an already inactive button returns `already_stopped`.
Repeated stop during the fade does not restart it or change its duration, even if a new
fadeOutMs is supplied. A terminal stop is harmless. Loop and automatic playlist advancement
are suppressed by the stop; it never reports natural_end. Pause/resume cannot cancel it,
and play on that stopping execution returns 409 execution_not_resumable; restart explicitly
creates a new execution protected from delayed callbacks of the old stop.

This is an audio fade for audio and video, not a visual dissolve. Associated DMX is stopped
immediately when the command is accepted, with the existing stop/keep-alive behavior;
no lighting fade is introduced. Images and DMX-only buttons stop immediately.

## Errors and concurrency

Errors are JSON, for example HTTP 409:

```json
{"error":"stale_execution"}
```

| HTTP | error | Client action |
| --- | --- | --- |
| 400 | execution_id_required | Send an ID for pause/resume |
| 400 | invalid_execution_id / invalid_button_id / invalid_playback_request | Correct the request |
| 404 | execution_not_found | Unknown, expired or app restarted; do not advance |
| 404 | button_not_found | Correct the configured button |
| 409 | stale_execution | ID belongs to another button/run; do not retry against a new run |
| 409 | execution_not_resumable | Run has ended or is being replaced; no restart was performed |
| 409 | app_busy / command_in_progress | Retry after a short delay with the same ID |
| 415 | json_required | Set Content-Type to application/json |
| 422 | media_file_missing | Restore/correct media path |
| 500 | command_failed | Re-query state before deciding whether to retry |
| 503 | shutting_down / playback_unavailable | Wait for the application to become available |

Existing license and DMX errors remain unchanged. Commands are serialized without an unbounded queue;
reads remain available during a command. Transport retries do not provide idempotent new starts:
if a start response is lost, inspect buttons/status before issuing another play/restart.

## Quiz integration sequence

1. Start background with all three options above and save the returned execution ID.
2. Before Flash plays a question, pause that execution with the desired fade.
3. At the next cue, resume that ID (never use play as a fallback on resume error).
4. If SoundBoard starts a ranking track, the background fades to pause; later resume it to fade back in.
5. Poll the saved execution ID. Choose the next background only on `completed/natural_end`.
6. On quiz shutdown, stop with the saved ID. Keep the DMX synchronization ID separate.

## Verification

`dotnet run --project Tools/PlanRegressionChecks/PlanRegressionChecks.csproj -c UiChecks`

The regression harness covers HTTP validation/legacy routing/DMX sync, execution identity and terminal reasons,
serialization without overrides, real decoder scheduling with synthetic silent audio, duplicate commands,
cancellation of pending pauses, and existing audio/video replacement combinations using controlled outputs.
An audible end-to-end check on the target sound device and external quiz is still required before a live show.
