Backend: channel on-air / now-playing state API (for Channels screen live treatment) #97

Closed
opened 2026-07-02 18:48:48 +02:00 by timothy · 9 comments
Owner

The Channels prototype (design-system/templates/chicorytv-admin/Channels.jsx) has an On-air filter/segment, an accent-rail + live-soft row treatment for on-air channels (c.live), and a "Now playing" column (current program title + progress %). None of this is in the channel list API — it is runtime playout/session state, not channel config.

Do

  • Expose per-channel on-air state: whether the channel is currently streaming, plus the current PlayoutItem title and start/finish (to derive progress %).
  • Prefer a separate endpoint (e.g. GET /api/channels/state or enrich from active sessions/playout) so the static channel list DTO stays cheap; the SPA can poll it.

Acceptance: #84 can show the On-air count/filter, the live row treatment, and the Now-playing lane from real data — and degrades gracefully (Idle / Off air) when unavailable.

Supports #84 (live treatment). Related: #72 (channel-list health/status states), #60 (browser playback). Parent: #59.

The Channels prototype (`design-system/templates/chicorytv-admin/Channels.jsx`) has an **On-air** filter/segment, an accent-rail + `live-soft` row treatment for on-air channels (`c.live`), and a **"Now playing"** column (current program title + progress %). None of this is in the channel list API — it is runtime playout/session state, not channel config. **Do** - Expose per-channel **on-air state**: whether the channel is currently streaming, plus the current `PlayoutItem` title and start/finish (to derive progress %). - Prefer a **separate endpoint** (e.g. `GET /api/channels/state` or enrich from active sessions/playout) so the static channel list DTO stays cheap; the SPA can poll it. **Acceptance:** #84 can show the On-air count/filter, the live row treatment, and the Now-playing lane from real data — and **degrades gracefully** (Idle / Off air) when unavailable. Supports #84 (live treatment). Related: #72 (channel-list health/status states), #60 (browser playback). Parent: #59.
timothy added the enhancementapi labels 2026-07-02 18:48:48 +02:00
Author
Owner

Picking this up. I’m first checking the existing channel API and playout/session state paths so the implementation can keep /api/channels static and expose runtime state through a separate, pollable endpoint as requested.

Picking this up. I’m first checking the existing channel API and playout/session state paths so the implementation can keep `/api/channels` static and expose runtime state through a separate, pollable endpoint as requested.
Author
Owner

Design clarification accepted:

  • nowPlaying will be the current scheduled PlayoutItem by wall-clock, independent of whether anyone is streaming.
  • onAir will initially mean "has an active HLS segmenter session" via IFFmpegSegmenterService.IsActive(channelNumber).

Important caveat: the current session registry only covers segmenter-backed modes (HttpLiveStreamingSegmenter and TransportStreamHybrid). MPEG-TS direct and HLS-Direct can stream without registering in IFFmpegSegmenterService.Workers, so they will report onAir: false until direct-path session tracking exists.

Opened follow-up #99 for that universal active-session tracking. #97 will stay scoped to the pollable batched state endpoint and current playout lookup.

Design clarification accepted: - `nowPlaying` will be the current scheduled `PlayoutItem` by wall-clock, independent of whether anyone is streaming. - `onAir` will initially mean "has an active HLS segmenter session" via `IFFmpegSegmenterService.IsActive(channelNumber)`. Important caveat: the current session registry only covers segmenter-backed modes (`HttpLiveStreamingSegmenter` and `TransportStreamHybrid`). MPEG-TS direct and HLS-Direct can stream without registering in `IFFmpegSegmenterService.Workers`, so they will report `onAir: false` until direct-path session tracking exists. Opened follow-up #99 for that universal active-session tracking. #97 will stay scoped to the pollable batched state endpoint and current playout lookup.
Author
Owner

Done

What was done: Task 1 contract/route skeleton added for GET /api/channels/state: response records, MediatR query record, controller action, and controller test. Commit: 7c1a1d56 feat(api): add channel state endpoint contract.

Root cause: n/a

Files changed: ErsatzTV.Core/Api/Channels/ChannelNowPlayingResponseModel.cs, ErsatzTV.Core/Api/Channels/ChannelStateResponseModel.cs, ErsatzTV.Application/Channels/Queries/GetChannelStatesForApi.cs, ErsatzTV/Controllers/Api/ChannelController.cs, ErsatzTV.Tests/Controllers/ChannelControllerTests.cs.

Verification: RED observed first with missing ChannelStateResponseModel, ChannelNowPlayingResponseModel, GetChannelStatesForApi, and ChannelController.GetState; GREEN after implementation with dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter "FullyQualifiedName~ChannelControllerTests.GetState_Should_Return_200_With_Channel_State" -v minimal passing 1/1.

Deferred: Handler behavior, OpenAPI contract tests/schema work, and runtime state implementation remain for later tasks.

Follow-up issues: Existing follow-up #99 remains relevant for universal active-session tracking.

Docs updated: n/a for Task 1.

## Done **What was done**: Task 1 contract/route skeleton added for `GET /api/channels/state`: response records, MediatR query record, controller action, and controller test. Commit: `7c1a1d56 feat(api): add channel state endpoint contract`. **Root cause**: n/a **Files changed**: `ErsatzTV.Core/Api/Channels/ChannelNowPlayingResponseModel.cs`, `ErsatzTV.Core/Api/Channels/ChannelStateResponseModel.cs`, `ErsatzTV.Application/Channels/Queries/GetChannelStatesForApi.cs`, `ErsatzTV/Controllers/Api/ChannelController.cs`, `ErsatzTV.Tests/Controllers/ChannelControllerTests.cs`. **Verification**: RED observed first with missing `ChannelStateResponseModel`, `ChannelNowPlayingResponseModel`, `GetChannelStatesForApi`, and `ChannelController.GetState`; GREEN after implementation with `dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter "FullyQualifiedName~ChannelControllerTests.GetState_Should_Return_200_With_Channel_State" -v minimal` passing 1/1. **Deferred**: Handler behavior, OpenAPI contract tests/schema work, and runtime state implementation remain for later tasks. **Follow-up issues**: Existing follow-up #99 remains relevant for universal active-session tracking. **Docs updated**: n/a for Task 1.
Author
Owner

Code Quality Review - Task 1

Commit reviewed: 7c1a1d5672f6eb9eb5d1dbf77d767e03ead88918

Code quality: APPROVED

Findings: No blocking code-quality findings.

Notes: The new GetState action uses IActionResult/OkObjectResult, while nearby collection endpoints such as GetAll return typed lists directly. This is a minor consistency note, not a required fix for Task 1.

Verification: Reviewed the commit diff and relevant controller/test/API model files. Ran dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter FullyQualifiedName~ChannelControllerTests.GetState_Should_Return_200_With_Channel_State -v minimal; result: passed 1/1. Existing NU1903 package vulnerability warnings were reported during restore/build.

## Code Quality Review - Task 1 **Commit reviewed**: `7c1a1d5672f6eb9eb5d1dbf77d767e03ead88918` **Code quality**: APPROVED **Findings**: No blocking code-quality findings. **Notes**: The new `GetState` action uses `IActionResult`/`OkObjectResult`, while nearby collection endpoints such as `GetAll` return typed lists directly. This is a minor consistency note, not a required fix for Task 1. **Verification**: Reviewed the commit diff and relevant controller/test/API model files. Ran `dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter FullyQualifiedName~ChannelControllerTests.GetState_Should_Return_200_With_Channel_State -v minimal`; result: passed 1/1. Existing NU1903 package vulnerability warnings were reported during restore/build.
Author
Owner

Done

What was done: Task 2 handler implementation added for GET /api/channels/state: batched current playout item lookup, active segmenter state via IFFmpegSegmenterService.IsActive(channel.Number), and reuse of the existing playout display-title behavior. Commit: 7b903d4bb41a3858fbae53c14a8e611cc6cdb302 feat(api): return channel runtime state.

Root cause: n/a

Files changed: ErsatzTV.Application/Channels/Queries/GetChannelStatesForApiHandler.cs, ErsatzTV.Application/Playouts/Mapper.cs, ErsatzTV.Tests/Application/Channels/GetChannelStatesForApiHandlerTests.cs.

Verification: RED observed first with missing GetChannelStatesForApiHandler. GREEN after implementation with dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter "FullyQualifiedName~GetChannelStatesForApiHandlerTests" -v minimal passing 3/3. Existing NU1903 package vulnerability warnings were reported during restore/build.

Deferred: OpenAPI/schema work and full feature/final verification remain for later tasks. Issue remains open.

Follow-up issues: Existing follow-up #99 remains relevant for universal active-session tracking beyond segmenter-backed modes.

Docs updated: n/a for Task 2.

## Done **What was done**: Task 2 handler implementation added for `GET /api/channels/state`: batched current playout item lookup, active segmenter state via `IFFmpegSegmenterService.IsActive(channel.Number)`, and reuse of the existing playout display-title behavior. Commit: `7b903d4bb41a3858fbae53c14a8e611cc6cdb302 feat(api): return channel runtime state`. **Root cause**: n/a **Files changed**: `ErsatzTV.Application/Channels/Queries/GetChannelStatesForApiHandler.cs`, `ErsatzTV.Application/Playouts/Mapper.cs`, `ErsatzTV.Tests/Application/Channels/GetChannelStatesForApiHandlerTests.cs`. **Verification**: RED observed first with missing `GetChannelStatesForApiHandler`. GREEN after implementation with `dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter "FullyQualifiedName~GetChannelStatesForApiHandlerTests" -v minimal` passing 3/3. Existing NU1903 package vulnerability warnings were reported during restore/build. **Deferred**: OpenAPI/schema work and full feature/final verification remain for later tasks. Issue remains open. **Follow-up issues**: Existing follow-up #99 remains relevant for universal active-session tracking beyond segmenter-backed modes. **Docs updated**: n/a for Task 2.
Author
Owner

Review

Scope: Task 3 spec compliance re-review after code-quality fix, commits 6057bcea and f01ffc62.

Result: APPROVED for Task 3 spec compliance.

Findings: No remaining required fixes before code-quality review.

Verification: TZ=UTC dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter FullyQualifiedName~OpenApiErrorResponseContractTests.Static_OpenApi_Should_Document_Channel_State_Response -v minimal passed: 1 test passed, 0 failed. The run emitted existing NU1903 vulnerability warnings for Microsoft.OpenApi and Scriban.Signed.

Notes: The static OpenAPI document now defines GET /api/channels/state with a 200 array of ChannelStateResponseModel; includes ChannelStateResponseModel with channelId, channelNumber, onAir, and nullable nowPlaying; includes ChannelNowPlayingResponseModel with title, startUtc, and finishUtc. The nullable $ref shape uses the repo's existing oneOf: [{ type: "null" }, { $ref: ... }] style.

## Review **Scope**: Task 3 spec compliance re-review after code-quality fix, commits `6057bcea` and `f01ffc62`. **Result**: APPROVED for Task 3 spec compliance. **Findings**: No remaining required fixes before code-quality review. **Verification**: `TZ=UTC dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter FullyQualifiedName~OpenApiErrorResponseContractTests.Static_OpenApi_Should_Document_Channel_State_Response -v minimal` passed: 1 test passed, 0 failed. The run emitted existing NU1903 vulnerability warnings for `Microsoft.OpenApi` and `Scriban.Signed`. **Notes**: The static OpenAPI document now defines `GET /api/channels/state` with a 200 array of `ChannelStateResponseModel`; includes `ChannelStateResponseModel` with `channelId`, `channelNumber`, `onAir`, and nullable `nowPlaying`; includes `ChannelNowPlayingResponseModel` with `title`, `startUtc`, and `finishUtc`. The nullable `$ref` shape uses the repo's existing `oneOf: [{ type: "null" }, { $ref: ... }]` style.
Author
Owner

Done

What was done: Fixed the final review finding for mirror channel state resolution. GetChannelStatesForApiHandler now resolves nowPlaying against MirrorSourceChannelId ?? channel.Id, applies PlayoutOffset when looking up the source playout item, and returns startUtc/finishUtc adjusted back onto the channel wall-clock timeline. Commit: 07e9d9838d66385339732a4693cd6de1ffbba359.

Root cause: The handler looked up current playout items only by the API channel id and wall-clock now, while playback resolves mirror channels through their source channel and subtracts the channel playout offset before locating the source item.

Files changed: ErsatzTV.Application/Channels/Queries/GetChannelStatesForApiHandler.cs, ErsatzTV.Tests/Application/Channels/GetChannelStatesForApiHandlerTests.cs.

Verification: RED observed first with dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter "FullyQualifiedName~GetChannelStatesForApiHandlerTests.Handle_Should_Resolve_Mirror_Channel_NowPlaying_From_Source_With_Offset" -v minimal, failing because mirrorState.NowPlaying was null. GREEN after implementation with the same focused test passing 1/1, then dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter "FullyQualifiedName~GetChannelStatesForApiHandlerTests" -v minimal passing 4/4. Existing NU1903 package vulnerability warnings were reported during restore/build.

Deferred: Push/merge and issue closure left unchanged per the requested final-review-fix workflow.

Follow-up issues: Existing #99 remains relevant for universal active-session tracking beyond segmenter-backed modes.

Docs updated: n/a.

## Done **What was done**: Fixed the final review finding for mirror channel state resolution. `GetChannelStatesForApiHandler` now resolves `nowPlaying` against `MirrorSourceChannelId ?? channel.Id`, applies `PlayoutOffset` when looking up the source playout item, and returns `startUtc`/`finishUtc` adjusted back onto the channel wall-clock timeline. Commit: `07e9d9838d66385339732a4693cd6de1ffbba359`. **Root cause**: The handler looked up current playout items only by the API channel id and wall-clock `now`, while playback resolves mirror channels through their source channel and subtracts the channel playout offset before locating the source item. **Files changed**: `ErsatzTV.Application/Channels/Queries/GetChannelStatesForApiHandler.cs`, `ErsatzTV.Tests/Application/Channels/GetChannelStatesForApiHandlerTests.cs`. **Verification**: RED observed first with `dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter "FullyQualifiedName~GetChannelStatesForApiHandlerTests.Handle_Should_Resolve_Mirror_Channel_NowPlaying_From_Source_With_Offset" -v minimal`, failing because `mirrorState.NowPlaying` was null. GREEN after implementation with the same focused test passing 1/1, then `dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter "FullyQualifiedName~GetChannelStatesForApiHandlerTests" -v minimal` passing 4/4. Existing NU1903 package vulnerability warnings were reported during restore/build. **Deferred**: Push/merge and issue closure left unchanged per the requested final-review-fix workflow. **Follow-up issues**: Existing #99 remains relevant for universal active-session tracking beyond segmenter-backed modes. **Docs updated**: n/a.
Author
Owner

Final verification + review complete — PR #112 opened against docs/59-ui-redesign-brief (stacked; this issue stays open until the stack reaches main).

Verification (after the interrupted runs): resolved the crash leftovers — the committed DTO was missing the nullable annotation the schema test expected, so NowPlaying is now ChannelNowPlayingResponseModel? (e66f49e5; off-air ⇒ null), and the dubious draft ChannelNumber? was confirmed wrong and dropped. Full build, 295 ErsatzTV.Tests + 488 Core.Tests, OpenAPI regen, web generate:api + check:api all green.

Adversarial review (high-effort agent over the full #97 diff): verdict — no blockers; offset math, mirror resolution, boundary semantics, error contract, and honest #99 degradation all check out against the streaming/XMLTV precedents. Findings fixed in 73455aae:

  • Major: nowPlaying surfaced raw filler (bumpers retitled the channel and reset progress every ad break) — now resolves the guide group's program like XMLTV; filler-only entries report null.
  • Major: one offset channel widened the metadata-hydrating query window (up to 24h) for all channels — now a cheap covering projection per distinct offset + one guide-group-scoped metadata query.
  • Minors/nits: Now on the request (boundary tests now pin start/finish semantics), non-nullable title in the schema, typed controller return, Playouts.Mapper back to internal, OnDemand-drift + filler semantics documented in the spec. The review's mirror-test nit exposed a real regression in my rewrite (source channel inheriting its mirror's lookup result) — fixed and pinned by a test.

Scope caveat unchanged: on-air is HLS-segmenter-only; MPEG-TS/HLS-Direct tracking is #99.

Final verification + review complete — **PR #112** opened against `docs/59-ui-redesign-brief` (stacked; this issue stays open until the stack reaches `main`). **Verification** (after the interrupted runs): resolved the crash leftovers — the committed DTO was missing the nullable annotation the schema test expected, so `NowPlaying` is now `ChannelNowPlayingResponseModel?` (`e66f49e5`; off-air ⇒ `null`), and the dubious draft `ChannelNumber?` was confirmed wrong and dropped. Full build, 295 ErsatzTV.Tests + 488 Core.Tests, OpenAPI regen, `web` `generate:api` + `check:api` all green. **Adversarial review** (high-effort agent over the full #97 diff): verdict — no blockers; offset math, mirror resolution, boundary semantics, error contract, and honest #99 degradation all check out against the streaming/XMLTV precedents. Findings fixed in `73455aae`: - **Major**: `nowPlaying` surfaced raw filler (bumpers retitled the channel and reset progress every ad break) — now resolves the guide group's program like XMLTV; filler-only entries report `null`. - **Major**: one offset channel widened the metadata-hydrating query window (up to 24h) for all channels — now a cheap covering projection per distinct offset + one guide-group-scoped metadata query. - Minors/nits: `Now` on the request (boundary tests now pin `start`/`finish` semantics), non-nullable `title` in the schema, typed controller return, `Playouts.Mapper` back to `internal`, OnDemand-drift + filler semantics documented in the spec. The review's mirror-test nit exposed a real regression in my rewrite (source channel inheriting its mirror's lookup result) — fixed and pinned by a test. Scope caveat unchanged: on-air is HLS-segmenter-only; MPEG-TS/HLS-Direct tracking is #99.
Author
Owner

Closed via PR #120 (stack merge to main, commit 1b6de047).

What was done: GET /api/channels/state (per-channel onAir + nowPlaying title/start/end) implemented in PR #112 (branch issue-97-channel-state-api), merged into the SPA stack, then landed on main with the #59 foundation via PR #120.

Files: GetChannelStatesForApi(+Handler), ChannelStateResponseModel/ChannelNowPlayingResponseModel (Core/Api, #nullable enable compliant), ChannelController.GetState, handler + controller + OpenAPI contract tests; v1.json + SPA types regenerated.

Deferred: onAir is currently segmenter-session-based (HLS segmenter / TS hybrid only). Universal coverage for MPEG-TS direct + HLS-Direct is #99 — implementation in flight (PR #121, under review); the final wiring of the tracker into this endpoint happens as a small follow-up once #121 merges.

Docs: docs/handoffs/chicorytv-issue-queue.md updated as part of the merge-pass session close-out.

Closed via PR #120 (stack merge to main, commit 1b6de047). **What was done**: `GET /api/channels/state` (per-channel `onAir` + `nowPlaying` title/start/end) implemented in PR #112 (branch `issue-97-channel-state-api`), merged into the SPA stack, then landed on main with the #59 foundation via PR #120. **Files**: GetChannelStatesForApi(+Handler), ChannelStateResponseModel/ChannelNowPlayingResponseModel (Core/Api, `#nullable enable` compliant), ChannelController.GetState, handler + controller + OpenAPI contract tests; v1.json + SPA types regenerated. **Deferred**: `onAir` is currently segmenter-session-based (HLS segmenter / TS hybrid only). Universal coverage for MPEG-TS direct + HLS-Direct is #99 — implementation in flight (PR #121, under review); the final wiring of the tracker into this endpoint happens as a small follow-up once #121 merges. **Docs**: docs/handoffs/chicorytv-issue-queue.md updated as part of the merge-pass session close-out.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: timothy/ersatztv#97