mirror of
https://github.com/ossrs/srs.git
synced 2026-10-24 09:59:20 +03:00
Adds `srs-integ`, a black-box integration suite for the SRS media server, and fixes the SRS bugs it found: over 120 of them, in every protocol. The suite tests SRS from outside the process, only through what a client, an operator or a callback server can see, so a rewritten server could pass the same cases. The suite now has about 1,450 cases (1,353 main and 99 timer), with no known bugs left: every bug a case found is fixed in this PR. It also removes some unused features, adds env support for a few more directives, and changes a few hook and API fields. ## srs-integ suite `cmd/integ` is a Go module of its own, `srs-integ`, separate from the root `srsx` module. It imports nothing from the repository. - **Run:** `make integ` makes the test media, then runs the cases in two `go test` processes at once: the main cases and the `TestTimer_` cases. `make integ-unit` runs the unit tests of the clients and analyzers, and `bin/srs-integ` gives `probe`, `publish` and `prepare` from the command line. - **Server adapter:** each server is `objs/srs -e`, configured only by env vars, on ports from `SRS_INTEG_PORTS` (14000-14999 in the Port Plan). Most cases share a Common server, split into 4 shards. Cases that need other settings run on variant servers, and groups share these where they can. - **Case classes:** Fast (≤100ms), Normal (≤500ms) and Timer (≤10s, only in `TestTimer_` groups). A case that runs over its limit fails. `KnownBug` marks a case that fails because of an SRS bug, and the case fails once the bug is fixed, so the mark can't go stale. - **Clients and analyzers written for the suite:** RTMP and RTMPS (both handshakes, any chunk size, enhanced RTMP, malformed input), FLV, TS, AAC/MP3, MP4, HLS (TS, fMP4, AES-128, SAMPLE-AES) and DASH. Each one fills a shared stream report: codecs, picture size, sequence headers, keyframes, timestamps and A/V sync. - **Clients built on libraries:** WebRTC WHIP/WHEP on pion (UDP and TCP, NACK/RTX/TWCC recorder, raw DTLS sessions), SRT on gosrt, RTSP on gortsplib, and a GB28181 PS-over-RTP publisher with no SIP. - **Test media:** generated from `doc/source.flv` by `make integ-prepare` and never committed. The files cover H.264, HEVC, VP9, AV1, AAC, Opus, MP3 and G.711, in FLV, MP4 and TS. A manifest keeps the media from being made again when nothing changed. - **Feature coverage:** the HTTP API, HTTPS API and exporter, reload, heartbeat, RTMP, RTMPS, HTTP-FLV, HTTP-TS, HTTP AAC and MP3, HTTPS, HLS, DASH, WebRTC, SRT, RTSP, GB28181, the MPEG-TS over UDP caster, every HTTP hook, security rules, forward, edge, the origin cluster, DVR, static files and VOD, signals, the command line and env settings. - **Conversion matrix:** each source (RTMP/RTMPS, WHIP, SRT, GB28181) is played by each player (RTMP, HTTP-FLV, HTTP-TS, MP3, AAC, HLS ts and fMP4, DASH, WHEP, SRT, RTSP) with each codec it can carry. The README has the matrix. - **Scale:** `-integ.scale=N` runs each case N times. A run of about 1,000 cases passes in about 11s. ## SRS bug fixes Every fix has a utest and, where a client can see it, an srs-integ case that failed before the fix. The main ones by area: - **HLS:** fMP4: - The default init file is now listed by its dir. - `hls_entry_prefix` now applies to fMP4 URIs. - MP3 streams now get an audio track in the init file. - Each publish now starts with a discontinuity. - `hls_keys` now encrypts the samples with cbcs. - A segment that fails to open is now dropped. Before, it crashed SRS. Other HLS fixes: key URIs, AES padding, HLS starts again after `hls_dispose`, the ratios, a TS republish, session names, and the `hls_ctx` of another stream's session is now checked as a new session. - **DASH:** the config, the MPD (window, update period, HEVC and MP3 codecs), init files of MP3 and of one-track streams, and dispose. - **WebRTC:** - Keyframes with an SEI, and Sender Reports and their mapping. - Opus timing, G.711, and WHEP SSRCs. - A refused WHIP publisher now frees its stream. - The start and hooks, and WebRTC over TCP. - **SRT:** only the configured latencies are set, as ffmpeg does. Also fixed: HEVC headers and CTS, frame counts, and the listener. - **RTSP:** a republish to a kept player, PLAY without SETUP, unknown methods, closes, DESCRIBE/TEARDOWN, request bodies and `on_stop`. - **GB28181:** - SSRC parsing. - HEVC to RTMP is no longer sent in legacy FLV tags. - A crash on a second media connection. - Latency with no audio, sliced frames, CTS, session lifetime, and the muxer and caster output. - **RTMP and HTTP:** - Timestamps that jump back without time correction no longer stop a player. - The republish param and the publish timeout after a pause. - The no-origin close and the RTMPS port. - HTTP ranges, including MP4 suffix ranges and VOD 416. - Redirects, mounts, unmounts and URL `..`. - CORS, API auth paths, JSONP 400 and 403 answers. - Static files and the request echo. - **Hooks:** - The heartbeat URL's query is now sent. - `on_play` clients and client types, and `on_dvr`. - A client that `on_connect` refused no longer gets `on_close`. - **Other:** DVR (MP4 info frame, audio rate, audio cut), listen ports above 65535, the env vhost check, the UDP caster, process stats, the tcmalloc rate, the encoder loop, the edge failover, STUN, coroutine joins, and the resource manager's fast-id cache (WebRTC and GB lookups never used it). ## Behavior changes - **Hooks:** - `on_publish`, `on_unpublish`, `on_play` and `on_stop` have a new `type` field after `client_id`, such as `rtmp-play` or `hls-play`, the same value as `/api/v1/clients`. - Every protocol now sends `param` with its leading `?`. - HTTP players send their own `http(s)://` tcUrl, and WHIP/WHEP clients send `webrtc://<host>/<app>`. - **API:** `/api/v1/features` adds `srt`, `rtsp`, `gb28181`, `rtc`, `simulator` and `sanitizer`. The `/api/v1/` index lists only the URLs SRS serves. - **Env support:** - The vhost security rules. - The edge protocol. - The origin cluster and coworkers. - The DVR apply filter. - **Timers in seconds:** `hls_dispose`, `dash_dispose`, `dvr_duration`, `queue_length`, `stun_timeout` and the heartbeat `interval` now accept fractions such as `0.5`. A fraction used to be cut to whole seconds, so `0.5` read as 0. - **Removed features:** a config or env var that names one of these, on or off, now makes SRS refuse to start (and `-t` fail) with `<name> is removed`: - HDS, including `--hds=on`. - The FLV POST caster and `asprocess`. - `query_latest_version` and `first_wait_for_qlv`. - The vhost `bandcheck`, `refer` and `atc_auto`. - Edge `token_traverse`. Check clients with the edge's HTTP hooks instead. ## Tests, CI and skills - **CI:** a new `integration-test` job builds `trunk/Dockerfile.test` (now with `--simulator=on`) and runs srs-integ. - It caches the test media. - It runs the main cases and the timer cases as separate steps, each retried on its own. - Limits are scaled by 4 with `-integ.limit-scale=4`. - **Unit tests:** more stable. They use random seeds, the test servers no longer share ports or leak fds, and a gtest listener fails a test that leaves a handler on a global resource manager. - **Skills:** - The `srs-develop` Gate and Full tiers run `make integ`. - `integration-tests.md` explains how to add cases. - The new `internal-audit-for-srs` skill audits each commit blind before review. --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
298 lines
10 KiB
Go
298 lines
10 KiB
Go
// 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.
|
|
var CommonListeners = []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 <base>/<name>, 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
|
|
}
|
|
|
|
// 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
|
|
}
|