Skip to content

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