Skip to content

Changelog


2026-08-21

Channel metadata UI wired up (ContentProvider, Genres, Service List Membership)

ContentProvider has been a real Channel column since 2026-07-14 and UpdateChannel already accepted it, but no field for it existed anywhere in PlayoutAdmin — added to ChannelGeneralTab.jsx's Discovery & EPG section.

Additional Genres changed from free-text tags to a fixed toggle-chip grid. No Genre vocabulary table was added — options are a live client-side aggregation of distinct values already in use across all channels (same approach the Channel Groups Genres tab below uses), seeded with a small starter list so the grid isn't empty on a fresh install. Keywords/Tags/ Languages/CustomGroups got the same treatment as an addable combobox (pick an existing aggregated value or type a new one).

Service List Membership — GetChannelGroupMemberships (GET /channels/:id/groups) existed server-side already (see Domain Structure) but was never called from the frontend. Now wired into ChannelGeneralTab.jsx, showing every ChannelGroup a channel belongs to, that group's status, and the channel's LCN within it.

/channels/:id/watch removed — real On-Air Status tab added instead

The standalone live-preview page (ChannelPreview.jsx) is gone. It wasn't decorative — real Shaka Player DASH/HLS playback — so its useful pieces (all-origins playback-URL list, live player-event log, UDP-publisher badge) moved into the existing ChannelStatusPanel sidebar's Origin Playback tab, which already had its own working Shaka Player instance.

The sidebar's On-Air Status tab, previously a placeholder, is now real — backed by the existing GetDashboardStats (2026-06-30). Shows live ingest- detected/healthy status, the probed ingest URL, and per-origin streamer online/offline. Honest about the one real limitation carried over from that endpoint: only a channel's first origin ever gets ingest-probed. Source Status and Activities tabs remain placeholders — no backing data exists yet.

Channel Group creation — 3 steps, real filters, Genres aggregation tab

ChannelGroupCreateSheet.jsx restructured from 2 steps (Channels, LCN) to 3 (Basic Info, Selection, LCN) — Basic Info split out of the old combined first step. The Selection step's channel picker gained a Filters panel (Content Providers / Genres / Languages / Custom Groups), each a real, live-aggregated distinct-value list from existing channels, AND-combined when multiple are active. A "Templates" filter chip is shown but disabled — no backing concept exists for it anywhere (not even a hardcoded list), so it isn't faked.

ChannelGroupList.jsx gained a Genres tab alongside the existing groups table — a real aggregation (genre → channel count), not a managed list. No genre create/edit/delete UI, since genre isn't a real entity.

Multi-rendition SMIL duplication fixed (transcode-mode channels)

Feeding the streamer a full multi-rendition ladder.smil on a channel that actually transcodes (not passthrough) caused it to build an independent decoder per source rendition, each re-encoding into the entire target ladder — confirmed live against the real streamer: a 2-rendition source SMIL fed into a 4-rung transcode config produced 8 output representations, not 4. The streamer has no track-selection logic of its own; it just decodes and re-encodes everything it's handed.

Fix: resolvePlaylistFilePaths now resolves to Media.ProgressiveKey (the single top-rung merged MP4, already produced by the transcode pipeline) instead of Media.SmilKey, whenever the channel isn't full passthrough — derived from the same len(TranscodeConfig) == 0 signal ValidateCreateChannel already used. Full passthrough is the one case that still gets the real multi-rendition SMIL, since that's genuinely what it needs (an untouched ABR ladder delivered to viewers). Applies to both a scheduled channel's default content and a live channel's fallback content.

DELETE added for Transcoding Presets, Ingest Presets, and Media Playlists

DeleteTranscodingPreset — real in-use guard (Channel.TranscodingPresetID count, 409 if referenced). DeleteIngestPreset — guards only on IsDefault, since no real FK-backed usage relation exists for it. Both wired as DELETE /transcoding-presets/:id / DELETE /ingest-presets/:id (2026-08-18).

DeletePlaylist (domain/media, DELETE /playlists/:id) — MediaPlaylist had no delete at any layer until now (no handler, no route, no frontend call). Same in-use-guard shape, checked via a raw query against channels (not a typed join — domain/media doesn't import domain/playback).

Channel and ChannelGroup delete endpoints already existed server-side (since the 2026-07-14 introduction) — only their frontend delete buttons were missing; added to ChannelList.jsx/ChannelGroupList.jsx.

Publish UDP moved from TranscodingPreset to the channel itself

TranscodingPreset.Publishers (added 2026-08-18, below) was removed again the same day, once it became clear a preset can be reused across many channels — baking a UDP publish destination into it would make every channel sharing that preset collide on the same multicast/unicast address. Publish UDP output is now channel-level state in PlayoutAdmin's channel creation wizard (useChannelForm.js), folded straight into that channel's ingestConfig.publishers at creation. No CoreAPI change was needed either direction — Channel.IngestConfig is stored as an opaque jsonb blob.

Passthrough mode added to live TranscodingPreset

TranscodingPreset.PassthroughMode string ("" / "full" / "video", nullable, no schema risk on the existing populated table). "full" means genuine no-transcode passthrough — VideoRenditions/FilterChain are force-cleared to [] server-side, and PlayoutAdmin's buildPayload omits the transcoders key entirely (confirmed against the real streamer source: no Decoder is ever constructed for an omitted/empty transcoders block). "video" means video passthrough with audio still encoded — confirmed against the real streamer source that this is enforced per-track inside its Transcoder stage, not an approximation.

CreateTranscodingPreset/UpdateTranscodingPreset skip the "renditions required" validation for either passthrough mode. PassthroughMode was added to UpdateTranscodingPreset's explicit Select() column list for the same zero-value-skip reason ForceClean needed it (2026-08-03) — reverting "full"/"video" back to "" must not be silently dropped.

GET /transcoding-presets/:id and GET /ingest-presets/:id added (2026-08-17)

No single-preset fetch existed before this — only list and history reads. Backs the new preset detail pages/sidebars (see PlayoutAdmin changes below). The transcoding-preset variant also returns real InUse channel data (the same query the delete guard above uses); the ingest-preset variant doesn't, since no real usage relation exists for it.

IngestPreset probe-result caching (2026-08-06)

Probe streams are throwaway (deleted right after each request) — added LastProbeResult/LastProbedAt on IngestPreset so "what did this source look like" survives past the probe itself, for compatibility checks against a selected TranscodingPreset.

Deinterlace filter chain on Transcoding Presets (2026-08-05)

FilterChain (bwdif/weave) added to TranscodingPreset, applied once before the rendition ladder in the streamer payload.

PlayoutAdmin — sidebar summary + full detail pages for presets

TranscodingPresetDetail.jsx/IngestPresetDetail.jsx — new full-page :id detail views, plus PresetInfoSheet.jsx/ChannelStatusPanel-style sidebar summaries. IngestPresetDetail.jsx also carries several not-yet-functional mock sections (Container/Decoder, Multicast/SRT access, Audio pass-through, Subtitles, Timing, SCTE-35) matching design intent for fields IngestPreset doesn't have columns for yet — clearly banner-flagged as preview-only, matching the pattern TranscodingPresetDetail.jsx's own mock sections (Encryption/DRM, Ad Signaling, Compute Target, Source Requirements) already established.


2026-08-03

ForceClean persistence bug fixed (TranscodingPreset)

forceClean on TranscodingPreset could be set to true but never actually persisted as false, on both create and update.

Root cause: the field carried a gorm default:true tag. GORM omits zero-value fields with a default tag from INSERT (letting the DB default apply) — so forceClean: false from the frontend was silently written as true on CreateTranscodingPreset. Separately, UpdateTranscodingPreset used a bare h.db.Model(&preset).Updates(in) — GORM's struct-based Updates skips all zero-value fields (not just ones with a default tag), so flipping forceClean true → false on an existing preset silently did nothing either.

Fix: dropped default:true from the ForceClean gorm tag in models.go, and changed UpdateTranscodingPreset to .Select(...) an explicit column list instead of a bare Updates(in), so zero-value fields (ForceClean, DropSecondAudio) are actually written.

CreateChannel no longer treats a streamer 500 as success

CreateChannel's per-origin loop previously excluded 500 from its failure check (res.StatusCode != 200 && ... && res.StatusCode != 500) — a genuine streamer-side rejection (bad payload, invalid config) was silently treated as a successful origin, and the ChannelOrigin row got persisted even though the streamer never actually created the .stream file.

Fix: only 200/201/204 count as success now. The streamer's actual response body is captured and returned in failures[].response (in addition to the existing request/response logging in pkg/StreamerClient), so the real rejection reason is visible in the API response itself.

Passthrough channels no longer require a TranscodingPreset

Reverts a short-lived change where every channel — including passthrough — had to select a TranscodingPreset just to supply chunkDuration/fragmentDuration/netTimeout/forceClean/dropSecondAudio. Forcing a full ABR ladder selection onto a passthrough channel (which never builds a transcoders block at all) was the wrong tradeoff.

CreateChannelRequest's ingestConfig now accepts these five fields directly for passthrough channels with no preset selected — PlayoutAdmin exposes them as a standalone "Ingest Tuning" section in that case (see SectionTranscode.jsx in PlayoutAdmin). No backend/model change was needed — ingestConfig was always just a client-supplied JSON blob; only the frontend's validation and preset requirement changed.


2026-07-31

Ingest/Transcode preset merge

IngestPreset domain added (GET/POST /ingest-presets, PUT /ingest-presets/:id, version history), alongside a restructuring of where ingest-tuning fields live:

  • ChunkDuration, FragmentDuration, NetTimeout, ForceClean, DropSecondAudio moved from IngestPreset onto TranscodingPreset. These are top-level fields in the streamer's .stream file (sessionAttribute.chunkDuration, etc.) regardless of whether anything is actually transcoded, so TranscodingPreset became their sole source — required on every channel at the time (see the 2026-08-03 revert above).
  • IngestPreset is address-only as of this merge: Transport, UDPHost, UDPPort, Status. Its purpose is "one already-known-good multicast address, reusable across N streamers" — not ingest tuning.
  • Both TranscodingPreset and IngestPreset follow the same snapshot-before-mutate versioning pattern: Version int, a *History table (TranscodingPresetHistory / IngestPresetHistory) written before every update, Update* handlers bump Version and mutate the row in place. Safe because Channel.IngestConfig/TranscodeConfig are copied from the resolved preset at channel-creation time — a channel never holds a live reference back to the preset row.

Note: UpdateIngestPreset still uses the same bare Updates(in) pattern that caused the ForceClean bug above — IsDefault: false on an update will likely hit the same zero-value-skip issue. Not yet fixed; flagged here since it's the same bug class.


2026-07-23

Channel draft/ready status lifecycle

Channel.Status added (draft | ready), defaulting to draft on every CreateChannel regardless of what the client sends. PATCH /channels/:id/status (UpdateChannelStatus) flips it explicitly — no other side effects on Origins/streamer state. SetChannelGroupMemberships is guarded against assigning a draft channel to a ChannelGroup.

This is orthogonal to ChannelType (live/scheduled), which describes the ingest shape — Status describes whether the channel is considered finished/confirmed.


2026-07-14

Channel Groups + Transcoding Presets introduced

Two significant additions landed together:

Channel Groups (/channel-groups) — a curated, ordered list of channels with a per-group LCN (logical channel number), the artifact an OTT client fetches to render its channel guide: - GET/POST /channel-groups, GET/PUT/DELETE /channel-groups/:id - PUT /channel-groups/:id/channels (SetChannelGroupMemberships) — full replace: deletes all existing memberships for the group and recreates from the request body, rather than diffing add/remove - ChannelGroupMembership enforces a channel can't appear twice in the same group (idx_group_channel) and no two channels in a group can share an LCN (idx_group_lcn) - GetChannelGroupByID/SetChannelGroupMemberships preload Channels.Channel.Origins.Streamer and call RewritePublicPlaybackURL so the frontend gets streamer.publicHost-based playback URLs, not the (possibly Docker-internal) stored PlaybackURL string - Status: draft | published, plus a simple Version counter — no snapshot/rollback history yet (deliberately simplest-shape-for-now, unlike the preset versioning below)

Transcoding Presets (/transcoding-presets) — reusable rendition ladders, replacing hand-built per-channel CMAF profile arrays: - GET/POST /transcoding-presets, PUT /transcoding-presets/:id, GET /transcoding-presets/:id/history - A ladder is always an array of renditions (no single/ABR split — a length-1 ladder behaves like the old "single" case) - Channel.TranscodingPresetID — nullable FK, nil on the legacy custom-profile path

G1 OTT metadata fields added to Channel: ContentProvider, Description, Genres, Keywords, Tags, Languages, CustomGroups, PrimaryGenre — all optional, all pq.StringArray where plural.


2026-06-30

Dashboard stats endpoint added

POST /stats/dashboard (GetDashboardStats) — pings every registered streamer and probes UDP ingest for one origin per channel, all in parallel, within a 10s context timeout. Returns channel counts (total/live/scheduled — "scheduled" here is heuristic: any channel whose IngestConfig has a defaultUrl key), origin health per streamer, and per-channel ingest health (healthy/unhealthy/unknown — unknown for pl:///-only origins with no real UDP address to probe).

Note this predates the ChannelType live/scheduled split (2026-07-14) — the "scheduled" heuristic here (presence of defaultUrl) is a proxy, not a direct Type check, and hasn't been revisited since Type became the real source of truth.


2026-06-25 and earlier

Domain established

Extracted from a flat domain/service/ package into the current nested domain/playback/ structure (handlers/, routes/wire.go + routes.go, models.go, validation.go, helper.go, config.go) — following the same domain-driven pattern as distribution. Channel/ChannelOrigin migrated to reference distribution.Streamer directly rather than a local duplicate.

Core channel CRUD, multi-origin fan-out to Castis Streamer nodes via streamerclient.CreateStream/DeleteStream, and pkg/response-based pagination were already in place from the domain's original introduction (2026-05-18) and carried forward through this restructuring largely intact.