StreamerClient
HTTP client for controlling Castis Streamer nodes — creating/deleting streams, building playback payloads, probing UDP sources.
Talks to http://{node.Host}:{node.APIPort}/api/.... Knows nothing about Fiber, Postgres, or CoreAPI's own response envelope — it's a pure translation layer between our domain logic and the Streamer's REST API.
Structure
pkg/StreamerClient/
├── client.go — CreateStream, DeleteStream, GetStreams, GetTraffic,
│ BuildStreamID, BuildPayload, PlaybackURL, ProbeUDP
│ + private: doGet, doPost, readResponse
└── models.go — StreamerResponse{StatusCode, Body}
Who calls this
domain/playback — CreateChannel / DeleteChannel orchestrate the full flow: validate input → look up the target service.Streamer row → call into StreamerClient → persist the result as ChannelOrigin.
StreamerClient itself never touches the database. Every function takes a service.Streamer (or the specific host/port fields off it) as a parameter — the client is stateless with respect to which node it's hitting.
Function reference
| Function | Purpose |
|---|---|
BuildStreamID(name string) string |
Converts a display name into the Streamer's slug convention — "BBC World" → "bbc_world.stream" |
BuildPayload(ingestURL string, ingestConfig, transcodeConfig json.RawMessage) (json.RawMessage, error) |
Merges ingest URL + ingest config + transcode config into the single JSON body the Streamer's POST /api/streams/:id expects. This is where Castis-specific field names ("transcoders", top-level "url") live — callers never need to know that shape. |
CreateStream(node service.Streamer, streamID string, payload json.RawMessage) (*StreamerResponse, error) |
POST /api/streams/{streamID} on the target node |
DeleteStream(node service.Streamer, streamID string) (*StreamerResponse, error) |
DELETE /api/streams/{streamID} |
GetStreams(node service.Streamer) (*StreamerResponse, error) |
GET /api/streams — list active streams on the node |
GetTraffic(node service.Streamer) (*StreamerResponse, error) |
Traffic/stats endpoint for the node |
PlaybackURL(node service.Streamer, streamID string) string |
Constructs the DASH manifest URL for playback — uses node.PublicHost, not node.Host, so it's browser-reachable rather than Docker-internal |
ProbeUDP(...) |
Checks whether a UDP ingest source is live before creating a stream against it |
Node targeting fields
service.Streamer (the DB record) carries everything a call needs to know where to go:
type Streamer struct {
Name string
Host string // Docker-internal name (e.g. "streamer_ntb") or DNS (staging)
PublicHost string // browser-reachable — "localhost" locally, same as Host on staging
HTTPPort int // streaming port
APIPort int // control-plane API port
HasGPU bool
Status string
}
Host/APIPort are used for the server-to-server API calls this client makes. PublicHost/HTTPPort are used only in PlaybackURL, since that URL is handed to a browser, not called by CoreAPI itself.
Example call from a domain handler
// domain/playback/handlers/channels.go
streamID := streamerclient.BuildStreamID(req.Name)
payload, err := streamerclient.BuildPayload(req.IngestURL, req.IngestConfig, req.TranscodeConfig)
if err != nil {
return response.Error(c, 400, err.Error())
}
resp, err := streamerclient.CreateStream(node, streamID, payload)
if err != nil {
return response.Error(c, 502, "streamer node unreachable")
}
playbackURL := streamerclient.PlaybackURL(node, streamID)
// persist ChannelOrigin{StreamID: streamID, PlaybackURL: playbackURL, ...}
return response.Created(c, origin)
Note the boundary: StreamerClient returns a raw *StreamerResponse or error; the handler is what turns that into a response.Error/response.Created for the frontend.
See changelog for history.