// Copyright (c) 2026 Winlin // // SPDX-License-Identifier: MIT package server import ( "fmt" "path/filepath" "sort" "strconv" "strings" ) // Listener is one port a server can listen on. type Listener int const ( RTMP Listener = iota // RTMP over TCP. HTTPAPI // The HTTP API over TCP, always on: the adapter reads it. HTTPServer // The HTTP server for streams and static files, over TCP. RTCUDP // WebRTC media over UDP. RTCTCP // WebRTC media over TCP, needs RTCUDP. SRT // SRT over UDP. RTSP // RTSP over TCP. Exporter // The Prometheus exporter over TCP. GB // GB28181 media over TCP, a stream caster. UDPTS // MPEG-TS over UDP, a stream caster; one caster per server. HTTPSAPI // The HTTPS API over TCP, needs HTTPAPI. HTTPSServer // The HTTPS server over TCP, needs HTTPServer. RTMPS // RTMPS over TCP. ) // UDPTSStream is the stream, app and name, the MPEG-TS over UDP caster // publishes over RTMP: SRS's caster converts what its port gets to one RTMP // URL of its config. const UDPTSStream = "live/udpts" // CommonListeners are the listeners of the Common server: every listener // except UDPTS, because a server has only one stream caster, and those of the // features the platform's build lacks. var CommonListeners = host.builtListeners([]Listener{ RTMP, HTTPAPI, HTTPServer, RTCUDP, RTCTCP, SRT, RTSP, Exporter, GB, HTTPSAPI, HTTPSServer, RTMPS, }) // listenerInfo is how SRS turns a listener on: the env var that enables it, the // one that sets its port, and the listener it needs. type listenerInfo struct { name string udp bool enable string listen string needs Listener tls string // The env prefix of its key and cert, for a TLS listener. } const noListener Listener = -1 var listeners = map[Listener]listenerInfo{ RTMP: {"rtmp", false, "", "SRS_RTMP_LISTEN", noListener, ""}, HTTPAPI: {"http-api", false, "SRS_HTTP_API_ENABLED", "SRS_HTTP_API_LISTEN", noListener, ""}, HTTPServer: {"http-server", false, "SRS_HTTP_SERVER_ENABLED", "SRS_HTTP_SERVER_LISTEN", noListener, ""}, RTCUDP: {"rtc-udp", true, "SRS_RTC_SERVER_ENABLED", "SRS_RTC_SERVER_LISTEN", noListener, ""}, RTCTCP: {"rtc-tcp", false, "SRS_RTC_SERVER_TCP_ENABLED", "SRS_RTC_SERVER_TCP_LISTEN", RTCUDP, ""}, SRT: {"srt", true, "SRS_SRT_SERVER_ENABLED", "SRS_SRT_SERVER_LISTEN", noListener, ""}, RTSP: {"rtsp", false, "SRS_RTSP_SERVER_ENABLED", "SRS_RTSP_SERVER_LISTEN", noListener, ""}, Exporter: {"exporter", false, "SRS_EXPORTER_ENABLED", "SRS_EXPORTER_LISTEN", noListener, ""}, GB: {"gb28181", false, "", "SRS_STREAM_CASTER_LISTEN", RTMP, ""}, UDPTS: {"udp-ts", true, "", "SRS_STREAM_CASTER_LISTEN", RTMP, ""}, HTTPSAPI: {"https-api", false, "SRS_HTTP_API_HTTPS_ENABLED", "SRS_HTTP_API_HTTPS_LISTEN", HTTPAPI, "SRS_HTTP_API_HTTPS_"}, HTTPSServer: {"https-server", false, "SRS_HTTP_SERVER_HTTPS_ENABLED", "SRS_HTTP_SERVER_HTTPS_LISTEN", HTTPServer, "SRS_HTTP_SERVER_HTTPS_"}, RTMPS: {"rtmps", false, "SRS_RTMPS_ENABLED", "SRS_RTMPS_LISTEN", noListener, "SRS_RTMPS_"}, } // UDP reports whether the listener is a UDP port. func (l Listener) UDP() bool { return listeners[l].udp } // String returns the listener's name, such as "rtmp". func (l Listener) String() string { if info, ok := listeners[l]; ok { return info.name } return "listener-" + strconv.Itoa(int(l)) } // Config is what a server turns on. type Config struct { // Listeners to turn on, each with its own port. Every other listener is off. Listeners []Listener // Env holds more SRS_* settings for a variant, such as a feature that must be // off. It cannot change what the adapter sets: the ports, the listeners or the // process settings. Env map[string]string // Hooks starts a mock on a port of its own and points every hook at it. Hooks bool // HookURLs, with Hooks, sets the URLs of the hooks it names, such as // on_publish: each calls every base in order, as /, where a // base is a path on the mock, such as /hooks or /more, or HookDown. A hook // it doesn't name calls the mock at /hooks. HookURLs map[string][]string // ForwardBackend, with Hooks, turns forward on with the mock as its // backend, at /api/v1/forward: every publish asks it for the URLs to // forward to, as an on_forward, and fails unless it answers some or none. ForwardBackend bool // Heartbeat, with Hooks, turns the heartbeat on, POSTing to the mock at // HeartbeatPath; nil leaves it off. Heartbeat *Heartbeat // IPv6 makes RTMP listen on [::] too, on the same port, when the host has // IPv6. IPv6 bool // APIAuth turns the HTTP API's authentication on, nil leaves it off. APIAuth *APIAuth // OneHTTPPort makes the HTTP API and the HTTP server share one port, and // the HTTPS API and the HTTPS server another: SRS then serves the API on // the server's port. OneHTTPPort bool // APIPort is the HTTP API's port, one ReservePort handed out, for a // server whose API others must know before it starts, such as an origin // of an origin cluster; 0 allocates one. APIPort int // Process replaces the adapter's process settings, SRS_PID and // SRS_LOG_TANK for example, for a case of those settings; a key the // adapter doesn't set is refused. Process map[string]string // ListenAlias sets RTMP's port by SRS_LISTEN, the alias of // SRS_RTMP_LISTEN. ListenAlias bool // WorkDir is a dir in the server's own dir, which the adapter makes, for // SRS to change to, SRS_WORK_DIR: SRS's relative paths, such as its pid // file, log file and recordings, are then in it. Empty leaves SRS in the // server's own dir. WorkDir string // TZ is the server's time zone, the TZ env var, such as INTEG-5:45 for // UTC+5:45; empty keeps the host's. TZ string // BelowPeak starts the server only once the process runs fewer SRS than // its peak so far, waiting at most 2s as a run of Exec does, so a group // that may start late doesn't raise the peak. BelowPeak bool // Vhost is config text in the default vhost, for the features that have // no env vars: ingest, transcode and exec. With it, SRS starts with a // config file of only this vhost and the FFmpeg log dir, the server's own // dir, and the env still sets the rest. Vhost string } // TLSFiles are the key and certificate of the TLS listeners. type TLSFiles struct { Key, Cert string } // BuildEnv returns the SRS_* env vars, sorted, that start a server in dir // with cfg, listening on ports, and TZ when cfg sets the time zone. func BuildEnv(cfg Config, ports map[Listener]int, dir string, tls TLSFiles) ([]string, error) { on := map[Listener]bool{} for _, l := range cfg.Listeners { if _, ok := listeners[l]; !ok { return nil, fmt.Errorf("unknown listener %v", l) } on[l] = true } if !on[HTTPAPI] { return nil, fmt.Errorf("the HTTP API must be on, the adapter reads it") } if cfg.ForwardBackend && !cfg.Hooks { return nil, fmt.Errorf("ForwardBackend needs Hooks, whose mock is the backend") } if cfg.Heartbeat != nil && !cfg.Hooks { return nil, fmt.Errorf("Heartbeat needs Hooks, whose mock gets the heartbeats") } if on[GB] && on[UDPTS] { return nil, fmt.Errorf("a server has only one stream caster, not both %v and %v", GB, UDPTS) } if cfg.OneHTTPPort { if err := checkOneHTTPPort(on, ports); err != nil { return nil, err } } if cfg.ListenAlias && !on[RTMP] { return nil, fmt.Errorf("ListenAlias needs RTMP, whose port it sets") } if cfg.WorkDir != "" && !filepath.IsLocal(cfg.WorkDir) { return nil, fmt.Errorf("WorkDir %q is not a dir in the server's own dir", cfg.WorkDir) } env := map[string]string{ // The process: in the foreground, logging to the console the adapter // captures, with its pid file in its own dir. "SRS_DAEMON": "off", "SRS_LOG_TANK": "console", "SRS_PID": filepath.Join(dir, "srs.pid"), "SRS_CIRCUIT_BREAKER_ENABLED": "off", "SRS_STREAM_CASTER_ENABLED": "off", } // A case of a process setting replaces the adapter's. for k, v := range cfg.Process { if _, ok := env[k]; !ok { return nil, fmt.Errorf("env %s is not a process setting of the adapter", k) } env[k] = v } if cfg.WorkDir != "" { env["SRS_WORK_DIR"] = "./" + filepath.Clean(cfg.WorkDir) } // Every listener is set explicitly, on with its port or off, so no default // port is ever left in use. for l := RTMP; l <= RTMPS; l++ { info := listeners[l] if !on[l] { if info.enable != "" { env[info.enable] = "off" } continue } port, ok := ports[l] if !ok || port <= 0 { return nil, fmt.Errorf("listener %v is on but has no port", l) } if info.needs != noListener && !on[info.needs] { return nil, fmt.Errorf("listener %v needs %v", l, info.needs) } if info.enable != "" { env[info.enable] = "on" } listen := info.listen if l == RTMP && cfg.ListenAlias { listen = "SRS_LISTEN" } env[listen] = strconv.Itoa(port) if l == RTMP && cfg.IPv6 { env[listen] += fmt.Sprintf(" [::]:%d", port) } if info.tls != "" { if tls.Key == "" || tls.Cert == "" { return nil, fmt.Errorf("listener %v needs the TLS key and certificate", l) } env[info.tls+"KEY"] = tls.Key env[info.tls+"CERT"] = tls.Cert } } // SRS opens the WebRTC TCP listener only when its protocol is not udp; all // keeps the UDP one too. if on[RTCTCP] { env["SRS_RTC_SERVER_PROTOCOL"] = "all" } for _, l := range []Listener{GB, UDPTS} { if !on[l] { continue } env["SRS_STREAM_CASTER_ENABLED"] = "on" env["SRS_STREAM_CASTER_CASTER"] = map[Listener]string{GB: "gb28181", UDPTS: "mpegts_over_udp"}[l] env["SRS_STREAM_CASTER_OUTPUT"] = fmt.Sprintf("rtmp://127.0.0.1:%d/live/[stream]", ports[RTMP]) if l == UDPTS { env["SRS_STREAM_CASTER_OUTPUT"] = fmt.Sprintf("rtmp://127.0.0.1:%d/%s", ports[RTMP], UDPTSStream) } } // The API's authentication, whose credentials the adapter sends. for k, v := range authEnv(cfg.APIAuth) { env[k] = v } // The variant's own settings come last, and may not change the adapter's. for k, v := range cfg.Env { if !strings.HasPrefix(k, "SRS_") { return nil, fmt.Errorf("env %s is not an SRS_ setting", k) } if _, ok := env[k]; ok || isListenerEnv(k) || isAuthEnv(k) { return nil, fmt.Errorf("env %s is set by the adapter", k) } env[k] = v } if cfg.TZ != "" { env["TZ"] = cfg.TZ } out := make([]string, 0, len(env)) for k, v := range env { out = append(out, k+"="+v) } sort.Strings(out) return out, nil } // isListenerEnv reports whether k turns a listener on or sets its port. func isListenerEnv(k string) bool { if k == "SRS_LISTEN" || k == "SRS_STREAM_CASTER_CASTER" || k == "SRS_STREAM_CASTER_OUTPUT" { return true } for _, info := range listeners { if k == info.enable || k == info.listen { return true } } return false }