Skip to content

Changelog


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/unknownunknown 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.