1
0
mirror of https://github.com/ossrs/srs.git synced 2026-10-24 09:59:20 +03:00
Files
srs/cmd/integ/internal/server/server_test.go
Winlin 551ee22d1f Integ: Add the srs-integ test suite, and fix 135 bugs. v8.0.61 (#4772)
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>
2026-10-17 15:20:30 -04:00

1050 lines
39 KiB
Go

// Copyright (c) 2026 Winlin
//
// SPDX-License-Identifier: MIT
package server
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"reflect"
"slices"
"strings"
"syscall"
"testing"
"time"
"srs-integ/internal/mock"
)
func TestMissingFeatures(t *testing.T) {
all := `{"code":0,"data":{"features":{"ssl":true,"srt":true,"rtsp":true,"gb28181":true,"rtc":true,"simulator":true,"sanitizer":true}}}`
if got, err := missingFeatures([]byte(all)); err != nil || len(got) != 0 {
t.Errorf("missingFeatures(all) = %v, %v", got, err)
}
// A false field and a field the server does not report are both missing.
some := `{"code":0,"data":{"features":{"srt":false,"rtsp":true,"gb28181":true,"rtc":true,"sanitizer":true}}}`
if got, err := missingFeatures([]byte(some)); err != nil || !reflect.DeepEqual(got, []string{"srt", "simulator"}) {
t.Errorf("missingFeatures(some) = %v, %v; want [srt simulator]", got, err)
}
if _, err := missingFeatures([]byte(`not json`)); err == nil {
t.Error("missingFeatures(not json) has no error")
}
if _, err := missingFeatures([]byte(`{"code":1}`)); err == nil {
t.Error("missingFeatures(code 1) has no error")
}
}
func TestCleanStaleRuns(t *testing.T) {
root := t.TempDir()
for _, name := range []string{"100", "200", "keep", "300"} {
if err := os.MkdirAll(filepath.Join(root, name, "srs-1"), 0o755); err != nil {
t.Fatal(err)
}
}
if err := os.WriteFile(filepath.Join(root, "400"), nil, 0o644); err != nil {
t.Fatal(err)
}
cleanStaleRuns(root, func(pid int) bool { return pid == 200 })
for name, want := range map[string]bool{"100": false, "200": true, "keep": true, "300": false, "400": true} {
_, err := os.Stat(filepath.Join(root, name))
if exists := err == nil; exists != want {
t.Errorf("%s exists=%v, want %v", name, exists, want)
}
}
}
// TestCommonConfig checks the Common server plays with the lowest latency, so
// a player gets each frame at once rather than in 350ms batches, serves
// HTTP-FLV, HLS and DASH with 10s segments, calls every hook, serves the
// vhost's static files at /static/, and that its env builds.
func TestCommonConfig(t *testing.T) {
cfg := CommonConfig()
if !cfg.Hooks {
t.Error("hooks are off, want every hook on the mock")
}
if !cfg.IPv6 {
t.Error("IPv6 is off, want RTMP on [::] too")
}
if !reflect.DeepEqual(cfg.Listeners, CommonListeners) {
t.Errorf("listeners %v, want %v", cfg.Listeners, CommonListeners)
}
want := map[string]string{
"SRS_VHOST_MIN_LATENCY": "on",
"SRS_VHOST_PLAY_MW_LATENCY": "0",
"SRS_VHOST_HTTP_REMUX_ENABLED": "on",
"SRS_VHOST_HTTP_REMUX_MOUNT": "[vhost]/[app]/[stream].flv",
"SRS_VHOST_HLS_ENABLED": "on",
"SRS_VHOST_DASH_ENABLED": "on",
"SRS_VHOST_DASH_DASH_FRAGMENT": "10",
"SRS_VHOST_RTC_ENABLED": "on",
"SRS_RTC_SERVER_CANDIDATE": "127.0.0.1",
"SRS_RTC_SERVER_KEEP_API_DOMAIN": "on",
"SRS_RTC_SERVER_RESOLVE_API_DOMAIN": "off",
"SRS_VHOST_SRT_ENABLED": "on",
"SRS_VHOST_SRT_TO_RTMP": "on",
"SRS_SRT_SERVER_RECVLATENCY": "0",
"SRS_VHOST_RTSP_ENABLED": "on",
"SRS_VHOST_HTTP_STATIC_ENABLED": "on",
"SRS_VHOST_HTTP_STATIC_MOUNT": "[vhost]/static/",
"SRS_VHOST_HTTP_STATIC_DIR": "./static/[vhost]",
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
ports := map[Listener]int{}
for i, l := range cfg.Listeners {
ports[l] = 14000 + i
}
if _, err := BuildEnv(cfg, ports, "/tmp/x", TLSFiles{Key: "k", Cert: "c"}); err != nil {
t.Errorf("build env: %v", err)
}
}
// TestRemuxConfig checks a remux variant serves its extension, plays with
// Common's latency, has only the listeners it needs, and that its env builds.
func TestRemuxConfig(t *testing.T) {
cfg := RemuxConfig("ts")
if want := []Listener{RTMP, HTTPAPI, HTTPServer}; !reflect.DeepEqual(cfg.Listeners, want) {
t.Errorf("listeners %v, want %v", cfg.Listeners, want)
}
want := map[string]string{
"SRS_VHOST_MIN_LATENCY": "on",
"SRS_VHOST_PLAY_MW_LATENCY": "0",
"SRS_VHOST_HTTP_REMUX_ENABLED": "on",
"SRS_VHOST_HTTP_REMUX_MOUNT": "[vhost]/[app]/[stream].ts",
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
ports := map[Listener]int{RTMP: 14000, HTTPAPI: 14001, HTTPServer: 14002}
if _, err := BuildEnv(cfg, ports, "/tmp/x", TLSFiles{Key: "k", Cert: "c"}); err != nil {
t.Errorf("build env: %v", err)
}
}
// TestPlayConfig checks a play variant has RTMP and the HTTP API, Common's
// latency, SRS's play defaults for the zero Play, and each setting it asks
// for, and that its env builds.
func TestPlayConfig(t *testing.T) {
latency := map[string]string{
"SRS_VHOST_MIN_LATENCY": "on",
"SRS_VHOST_PLAY_MW_LATENCY": "0",
}
with := func(kv ...string) map[string]string {
m := map[string]string{}
for k, v := range latency {
m[k] = v
}
for i := 0; i < len(kv); i += 2 {
m[kv[i]] = kv[i+1]
}
return m
}
cases := []struct {
name string
play Play
env map[string]string
}{
{"Defaults", Play{}, with()},
{"GopCacheOff", Play{GopCacheOff: true}, with("SRS_VHOST_PLAY_GOP_CACHE", "off")},
{"GopCacheMaxFrames", Play{GopCacheMaxFrames: 10}, with("SRS_VHOST_PLAY_GOP_CACHE_MAX_FRAMES", "10")},
{"QueueLength", Play{QueueLength: 2 * time.Second}, with("SRS_VHOST_PLAY_QUEUE_LENGTH", "2")},
{"QueueLengthFraction", Play{QueueLength: 1500 * time.Millisecond}, with("SRS_VHOST_PLAY_QUEUE_LENGTH", "1.5")},
{"ReduceSequenceHeader", Play{ReduceSequenceHeader: true}, with("SRS_VHOST_PLAY_REDUCE_SEQUENCE_HEADER", "on")},
{"TimeJitterZero", Play{TimeJitter: "zero"}, with("SRS_VHOST_PLAY_TIME_JITTER", "zero")},
{"TimeJitterOff", Play{TimeJitter: "off"}, with("SRS_VHOST_PLAY_TIME_JITTER", "off")},
{"ATC", Play{ATC: true, MixCorrect: true}, with("SRS_VHOST_PLAY_ATC", "on", "SRS_VHOST_PLAY_MIX_CORRECT", "on")},
{"SendMinInterval", Play{SendMinInterval: 20 * time.Millisecond}, with("SRS_VHOST_PLAY_SEND_MIN_INTERVAL", "20")},
{"MergedWrite", Play{MergedWrite: &Merged{WriteLatency: 200 * time.Millisecond, WriteMsgs: 4, ReadLatency: 100 * time.Millisecond}}, map[string]string{
"SRS_VHOST_MIN_LATENCY": "off",
"SRS_VHOST_PLAY_MW_LATENCY": "200",
"SRS_VHOST_PLAY_MW_MSGS": "4",
"SRS_VHOST_PUBLISH_MR": "on",
"SRS_VHOST_PUBLISH_MR_LATENCY": "100",
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg := PlayConfig(tc.play)
if want := []Listener{RTMP, HTTPAPI}; !reflect.DeepEqual(cfg.Listeners, want) {
t.Errorf("listeners %v, want %v", cfg.Listeners, want)
}
if !reflect.DeepEqual(cfg.Env, tc.env) {
t.Errorf("env %v, want %v", cfg.Env, tc.env)
}
if _, err := BuildEnv(cfg, map[Listener]int{RTMP: 14000, HTTPAPI: 14001}, "/tmp/x", TLSFiles{}); err != nil {
t.Errorf("build env: %v", err)
}
})
}
}
// TestPublishConfig checks the env of each publish setting, beside Common's
// lowest play latency, and that it builds.
func TestPublishConfig(t *testing.T) {
with := func(kv ...string) map[string]string {
m := map[string]string{
"SRS_VHOST_MIN_LATENCY": "on",
"SRS_VHOST_PLAY_MW_LATENCY": "0",
}
for i := 0; i < len(kv); i += 2 {
m[kv[i]] = kv[i+1]
}
return m
}
cases := []struct {
name string
publish Publish
env map[string]string
}{
{"Defaults", Publish{}, with()},
{"FirstPacketTimeout", Publish{FirstPacketTimeout: 100 * time.Millisecond}, with("SRS_VHOST_PUBLISH_FIRSTPKT_TIMEOUT", "100")},
{"NormalTimeout", Publish{NormalTimeout: 200 * time.Millisecond}, with("SRS_VHOST_PUBLISH_NORMAL_TIMEOUT", "200")},
{"KickoffForIdle", Publish{KickoffForIdle: 300 * time.Millisecond}, with("SRS_VHOST_PUBLISH_KICKOFF_FOR_IDLE", "0.3")},
{"KickoffWholeSeconds", Publish{KickoffForIdle: 2 * time.Second}, with("SRS_VHOST_PUBLISH_KICKOFF_FOR_IDLE", "2")},
{"All", Publish{FirstPacketTimeout: time.Second, NormalTimeout: 1500 * time.Millisecond, KickoffForIdle: 1500 * time.Millisecond}, with(
"SRS_VHOST_PUBLISH_FIRSTPKT_TIMEOUT", "1000",
"SRS_VHOST_PUBLISH_NORMAL_TIMEOUT", "1500",
"SRS_VHOST_PUBLISH_KICKOFF_FOR_IDLE", "1.5",
)},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg := PublishConfig(tc.publish)
if want := []Listener{RTMP, HTTPAPI}; !reflect.DeepEqual(cfg.Listeners, want) {
t.Errorf("listeners %v, want %v", cfg.Listeners, want)
}
if !reflect.DeepEqual(cfg.Env, tc.env) {
t.Errorf("env %v, want %v", cfg.Env, tc.env)
}
if _, err := BuildEnv(cfg, map[Listener]int{RTMP: 14000, HTTPAPI: 14001}, "/tmp/x", TLSFiles{}); err != nil {
t.Errorf("build env: %v", err)
}
})
}
}
// TestRemuxVariant checks that the zero Remux is RemuxConfig, and that each
// remux setting it asks for is set, and that its env builds.
func TestRemuxVariant(t *testing.T) {
if got, want := RemuxVariant("flv", Remux{}), RemuxConfig("flv"); !reflect.DeepEqual(got, want) {
t.Errorf("zero Remux %v, want %v", got, want)
}
cases := []struct {
name string
r Remux
want map[string]string
}{
{"GuessOff", Remux{GuessOff: true}, map[string]string{"SRS_VHOST_HTTP_REMUX_GUESS_HAS_AV": "off"}},
{"NoAudio", Remux{NoAudio: true}, map[string]string{"SRS_VHOST_HTTP_REMUX_HAS_AUDIO": "off"}},
{"NoVideo", Remux{NoVideo: true}, map[string]string{"SRS_VHOST_HTTP_REMUX_HAS_VIDEO": "off"}},
{"KeepUnmatched", Remux{KeepUnmatched: true}, map[string]string{"SRS_VHOST_HTTP_REMUX_DROP_IF_NOT_MATCH": "off"}},
{"FastCache", Remux{FastCache: 1500 * time.Millisecond}, map[string]string{"SRS_VHOST_HTTP_REMUX_FAST_CACHE": "1.5"}},
{"CrossdomainOff", Remux{CrossdomainOff: true}, map[string]string{"SRS_HTTP_SERVER_CROSSDOMAIN": "off"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg := RemuxVariant("flv", tc.r)
want := RemuxConfig("flv").Env
for k, v := range tc.want {
want[k] = v
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
})
}
t.Run("Off", func(t *testing.T) {
cfg := RemuxVariant("flv", Remux{Off: true})
if want := []Listener{RTMP, HTTPAPI, HTTPServer}; !reflect.DeepEqual(cfg.Listeners, want) {
t.Errorf("listeners %v, want %v", cfg.Listeners, want)
}
want := map[string]string{
"SRS_VHOST_MIN_LATENCY": "on",
"SRS_VHOST_PLAY_MW_LATENCY": "0",
"SRS_VHOST_HTTP_REMUX_ENABLED": "off",
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
})
all := Remux{GuessOff: true, NoAudio: true, KeepUnmatched: true, FastCache: time.Second, CrossdomainOff: true}
ports := map[Listener]int{RTMP: 14000, HTTPAPI: 14001, HTTPServer: 14002}
if _, err := BuildEnv(RemuxVariant("flv", all), ports, "/tmp/x", TLSFiles{Key: "k", Cert: "c"}); err != nil {
t.Errorf("build env: %v", err)
}
}
// TestHLSConfig checks that the zero HLS turns HLS on with SRS's defaults,
// that each HLS setting it asks for is set, and that its env builds.
func TestHLSConfig(t *testing.T) {
base := map[string]string{
"SRS_VHOST_MIN_LATENCY": "on",
"SRS_VHOST_PLAY_MW_LATENCY": "0",
"SRS_VHOST_HLS_ENABLED": "on",
}
cases := []struct {
name string
h HLS
want map[string]string
}{
{"Defaults", HLS{}, nil},
{"Fragment", HLS{Fragment: 2 * time.Second}, map[string]string{"SRS_VHOST_HLS_HLS_FRAGMENT": "2"}},
{"Window", HLS{Window: 7500 * time.Millisecond}, map[string]string{"SRS_VHOST_HLS_HLS_WINDOW": "7.5"}},
{"TDRatio", HLS{TDRatio: 1.5}, map[string]string{"SRS_VHOST_HLS_HLS_TD_RATIO": "1.5"}},
{"AOFRatio", HLS{AOFRatio: 2.5}, map[string]string{"SRS_VHOST_HLS_HLS_AOF_RATIO": "2.5"}},
{"Path", HLS{Path: "./hls"}, map[string]string{"SRS_VHOST_HLS_HLS_PATH": "./hls", "SRS_HTTP_SERVER_DIR": "./hls"}},
{"M3U8File", HLS{M3U8File: "[app]/[stream]/index.m3u8"}, map[string]string{"SRS_VHOST_HLS_HLS_M3U8_FILE": "[app]/[stream]/index.m3u8"}},
{"TSFile", HLS{TSFile: "[app]/[stream]/seg-[seq].ts"}, map[string]string{"SRS_VHOST_HLS_HLS_TS_FILE": "[app]/[stream]/seg-[seq].ts"}},
{"EntryPrefix", HLS{EntryPrefix: "http://cdn.example.com"}, map[string]string{"SRS_VHOST_HLS_HLS_ENTRY_PREFIX": "http://cdn.example.com"}},
{"TSFloor", HLS{TSFloor: true}, map[string]string{"SRS_VHOST_HLS_HLS_TS_FLOOR": "on"}},
{"FMP4", HLS{FMP4: true}, map[string]string{"SRS_VHOST_HLS_HLS_USE_FMP4": "on"}},
{"FMP4File", HLS{FMP4File: "[app]/[stream]-seg-[seq].m4s"}, map[string]string{"SRS_VHOST_HLS_HLS_FMP4_FILE": "[app]/[stream]-seg-[seq].m4s"}},
{"InitFile", HLS{InitFile: "[app]/[stream]-init.mp4"}, map[string]string{"SRS_VHOST_HLS_HLS_INIT_FILE": "[app]/[stream]-init.mp4"}},
{"Keys", HLS{Keys: true}, map[string]string{"SRS_VHOST_HLS_HLS_KEYS": "on"}},
{"FragmentsPerKey", HLS{Keys: true, FragmentsPerKey: 2}, map[string]string{"SRS_VHOST_HLS_HLS_KEYS": "on", "SRS_VHOST_HLS_HLS_FRAGMENTS_PER_KEY": "2"}},
{"KeyURL", HLS{KeyURL: "http://cdn.example.com/?token=a&sig=b"}, map[string]string{"SRS_VHOST_HLS_HLS_KEY_URL": "http://cdn.example.com/?token=a&sig=b"}},
{"NoCtx", HLS{NoCtx: true}, map[string]string{"SRS_VHOST_HLS_HLS_CTX": "off"}},
{"NoTSCtx", HLS{NoTSCtx: true}, map[string]string{"SRS_VHOST_HLS_HLS_TS_CTX": "off"}},
{"MasterRelative", HLS{MasterRelative: true}, map[string]string{"SRS_VHOST_HLS_HLS_MASTER_M3U8_PATH_RELATIVE": "on"}},
{"Dispose", HLS{Dispose: time.Second}, map[string]string{"SRS_VHOST_HLS_HLS_DISPOSE": "1"}},
{"DisposeFraction", HLS{Dispose: 500 * time.Millisecond}, map[string]string{"SRS_VHOST_HLS_HLS_DISPOSE": "0.5"}},
{"NoCleanup", HLS{NoCleanup: true}, map[string]string{"SRS_VHOST_HLS_HLS_CLEANUP": "off"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg := HLSConfig(tc.h)
if want := []Listener{RTMP, HTTPAPI, HTTPServer}; !reflect.DeepEqual(cfg.Listeners, want) {
t.Errorf("listeners %v, want %v", cfg.Listeners, want)
}
want := map[string]string{}
for k, v := range base {
want[k] = v
}
for k, v := range tc.want {
want[k] = v
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
if _, err := BuildEnv(cfg, map[Listener]int{RTMP: 14000, HTTPAPI: 14001, HTTPServer: 14002}, "/tmp/x", TLSFiles{}); err != nil {
t.Errorf("build env: %v", err)
}
})
}
}
func TestWithHLS(t *testing.T) {
// A play variant gets HLS on with the settings, and the HTTP server to
// serve the files; its own env stays.
base := PlayConfig(Play{QueueLength: time.Second})
cfg := WithHLS(base, HLS{Fragment: time.Second, Dispose: time.Second})
if want := []Listener{RTMP, HTTPAPI, HTTPServer}; !reflect.DeepEqual(cfg.Listeners, want) {
t.Errorf("listeners %v, want %v", cfg.Listeners, want)
}
want := map[string]string{
"SRS_VHOST_HLS_ENABLED": "on",
"SRS_VHOST_HLS_HLS_FRAGMENT": "1",
"SRS_VHOST_HLS_HLS_DISPOSE": "1",
}
for k, v := range base.Env {
want[k] = v
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
if _, ok := base.Env["SRS_VHOST_HLS_ENABLED"]; ok || len(base.Listeners) != 2 {
t.Errorf("the base config changed: %+v", base)
}
// A config that has the HTTP server keeps its listeners as they are.
remux := RemuxConfig("flv")
if cfg := WithHLS(remux, HLS{}); !reflect.DeepEqual(cfg.Listeners, remux.Listeners) {
t.Errorf("listeners %v, want %v", cfg.Listeners, remux.Listeners)
}
}
func TestWithDASH(t *testing.T) {
cases := []struct {
name string
d DASH
want map[string]string
}{
{"Defaults", DASH{}, nil},
{"Fragment", DASH{Fragment: 2500 * time.Millisecond}, map[string]string{"SRS_VHOST_DASH_DASH_FRAGMENT": "2.5"}},
{"WindowSize", DASH{WindowSize: 3}, map[string]string{"SRS_VHOST_DASH_DASH_WINDOW_SIZE": "3"}},
{"Path", DASH{Path: "./objs/nginx/html/dash"}, map[string]string{"SRS_VHOST_DASH_DASH_PATH": "./objs/nginx/html/dash"}},
{"MPDFile", DASH{MPDFile: "[app]/[stream]/index.mpd"}, map[string]string{"SRS_VHOST_DASH_DASH_MPD_FILE": "[app]/[stream]/index.mpd"}},
{"Dispose", DASH{Dispose: 2 * time.Second}, map[string]string{"SRS_VHOST_DASH_DASH_DISPOSE": "2"}},
{"DisposeFraction", DASH{Dispose: 500 * time.Millisecond}, map[string]string{"SRS_VHOST_DASH_DASH_DISPOSE": "0.5"}},
{"NoCleanup", DASH{NoCleanup: true}, map[string]string{"SRS_VHOST_DASH_DASH_CLEANUP": "off"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
base := RemuxConfig("flv")
cfg := WithDASH(base, tc.d)
if !reflect.DeepEqual(cfg.Listeners, base.Listeners) {
t.Errorf("listeners %v, want %v", cfg.Listeners, base.Listeners)
}
want := map[string]string{"SRS_VHOST_DASH_ENABLED": "on"}
for k, v := range base.Env {
want[k] = v
}
for k, v := range tc.want {
want[k] = v
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
if _, ok := base.Env["SRS_VHOST_DASH_ENABLED"]; ok {
t.Errorf("the base config changed: %v", base.Env)
}
})
}
// The HTTP server keeps its dir, which serves the DASH files under it.
t.Run("KeepsHTTPDir", func(t *testing.T) {
cfg := WithDASH(HLSConfig(HLS{Path: "./hls"}), DASH{Path: "./hls/dash"})
if cfg.Env["SRS_HTTP_SERVER_DIR"] != "./hls" || cfg.Env["SRS_VHOST_DASH_DASH_PATH"] != "./hls/dash" {
t.Errorf("env %v", cfg.Env)
}
})
}
// TestWithRTC checks a variant with WebRTC on: over UDP only, with
// 127.0.0.1 as its candidate like Common, and each WebRTC setting.
func TestWithRTC(t *testing.T) {
cases := []struct {
name string
r RTC
want map[string]string
}{
{"Defaults", RTC{}, nil},
{"DTLSRole", RTC{DTLSRole: "active"}, map[string]string{"SRS_VHOST_RTC_DTLS_ROLE": "active"}},
{"StunStrictCheck", RTC{StunStrictCheck: true}, map[string]string{"SRS_VHOST_RTC_STUN_STRICT_CHECK": "on"}},
{"NoAPICandidates", RTC{NoAPICandidates: true}, map[string]string{"SRS_RTC_SERVER_API_AS_CANDIDATES": "off"}},
{"NackPreferRTX", RTC{NackPreferRTX: true}, map[string]string{"SRS_VHOST_RTC_NACK_PREFER_RTX": "on"}},
{"NoNack", RTC{NoNack: true}, map[string]string{"SRS_VHOST_RTC_NACK": "off"}},
{"NoTWCC", RTC{NoTWCC: true}, map[string]string{"SRS_VHOST_RTC_TWCC": "off"}},
{"RTMPToRTC", RTC{RTMPToRTC: true}, map[string]string{"SRS_VHOST_RTC_RTMP_TO_RTC": "on"}},
{"RTCToRTMP", RTC{RTCToRTMP: true}, map[string]string{"SRS_VHOST_RTC_RTC_TO_RTMP": "on"}},
{"InitRateFromSDP", RTC{InitRateFromSDP: true}, map[string]string{"SRS_VHOST_RTC_INIT_RATE_FROM_SDP": "on"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
base := RemuxConfig("flv")
cfg := WithRTC(base, tc.r)
if want := append(slices.Clone(base.Listeners), RTCUDP); !reflect.DeepEqual(cfg.Listeners, want) {
t.Errorf("listeners %v, want %v", cfg.Listeners, want)
}
want := map[string]string{"SRS_VHOST_RTC_ENABLED": "on", "SRS_RTC_SERVER_CANDIDATE": "127.0.0.1"}
for k, v := range base.Env {
want[k] = v
}
for k, v := range tc.want {
want[k] = v
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
if _, ok := base.Env["SRS_VHOST_RTC_ENABLED"]; ok || len(base.Listeners) != 3 {
t.Errorf("the base config changed: %v %v", base.Listeners, base.Env)
}
ports := map[Listener]int{}
for i, l := range cfg.Listeners {
ports[l] = 14000 + i
}
env, err := BuildEnv(cfg, ports, "/tmp/x", TLSFiles{})
if err != nil {
t.Fatalf("build env: %v", err)
}
if slices.Contains(env, "SRS_RTC_SERVER_PROTOCOL=all") {
t.Errorf("env %v turns on WebRTC over TCP", env)
}
})
}
// A config with WebRTC on keeps its listeners.
if cfg := WithRTC(CommonConfig(), RTC{}); !reflect.DeepEqual(cfg.Listeners, CommonListeners) {
t.Errorf("listeners %v, want Common's", cfg.Listeners)
}
}
// TestWithSRT checks an SRT variant: the listener, SRT and its conversion to
// RTMP on, Common's latency, and its own settings, on a copy of the base.
func TestWithSRT(t *testing.T) {
cases := []struct {
name string
s SRTOptions
want map[string]string
}{
{"Defaults", SRTOptions{}, nil},
{"DefaultApp", SRTOptions{DefaultApp: "srtapp"}, map[string]string{"SRS_SRT_SERVER_DEFAULT_APP": "srtapp"}},
{"DefaultMode", SRTOptions{DefaultMode: "publish"}, map[string]string{"SRS_SRT_SERVER_DEFAULT_MODE": "publish"}},
{"DefaultStreamID", SRTOptions{DefaultStreamID: "#!::r=srtapp/default,m=request"},
map[string]string{"SRS_SRT_SERVER_DEFAULT_STREAMID": "#!::r=srtapp/default,m=request"}},
{"Passphrase", SRTOptions{Passphrase: "srs-integ-passphrase", PBKeyLen: 16},
map[string]string{"SRS_SRT_SERVER_PASSPHRASE": "srs-integ-passphrase", "SRS_SRT_SERVER_PBKEYLEN": "16"}},
{"Latencies", SRTOptions{RecvLatency: 250 * time.Millisecond, PeerLatency: 150 * time.Millisecond},
map[string]string{"SRS_SRT_SERVER_RECVLATENCY": "250", "SRS_SRT_SERVER_PEERLATENCY": "150"}},
// An empty value is a variable left out, so SRS's default applies.
{"LatencyOnly", SRTOptions{Latency: 300 * time.Millisecond, KeepRecvLatency: true},
map[string]string{"SRS_SRT_SERVER_LATENCY": "300", "SRS_SRT_SERVER_RECVLATENCY": ""}},
{"PeerIdleTimeout", SRTOptions{PeerIdleTimeout: 1500 * time.Millisecond},
map[string]string{"SRS_SRT_SERVER_PEER_IDLE_TIMEOUT": "1500"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
base := RemuxConfig("flv")
cfg := WithSRT(base, tc.s)
if want := append(slices.Clone(base.Listeners), SRT); !reflect.DeepEqual(cfg.Listeners, want) {
t.Errorf("listeners %v, want %v", cfg.Listeners, want)
}
want := map[string]string{
"SRS_VHOST_SRT_ENABLED": "on",
"SRS_VHOST_SRT_TO_RTMP": "on",
"SRS_SRT_SERVER_RECVLATENCY": "0",
}
for k, v := range base.Env {
want[k] = v
}
for k, v := range tc.want {
if v == "" {
delete(want, k)
} else {
want[k] = v
}
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
if _, ok := base.Env["SRS_VHOST_SRT_ENABLED"]; ok || len(base.Listeners) != 3 {
t.Errorf("the base config changed: %v %v", base.Listeners, base.Env)
}
ports := map[Listener]int{}
for i, l := range cfg.Listeners {
ports[l] = 14000 + i
}
if _, err := BuildEnv(cfg, ports, "/tmp/x", TLSFiles{}); err != nil {
t.Fatalf("build env: %v", err)
}
})
}
// A config with SRT on keeps its listeners.
if cfg := WithSRT(CommonConfig(), SRTOptions{}); !reflect.DeepEqual(cfg.Listeners, CommonListeners) {
t.Errorf("listeners %v, want Common's", cfg.Listeners)
}
}
// TestWithRTSP checks a variant gets the RTSP listener and the vhost's RTSP
// settings, and that the base config is kept.
func TestWithRTSP(t *testing.T) {
cases := []struct {
name string
r RTSPOptions
want map[string]string
}{
{"Defaults", RTSPOptions{}, map[string]string{"SRS_VHOST_RTSP_ENABLED": "on"}},
{"NoRTMPToRTSP", RTSPOptions{NoRTMPToRTSP: true},
map[string]string{"SRS_VHOST_RTSP_ENABLED": "on", "SRS_VHOST_RTSP_RTMP_TO_RTSP": "off"}},
{"VhostOff", RTSPOptions{VhostOff: true}, map[string]string{"SRS_VHOST_RTSP_ENABLED": "off"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
base := RemuxConfig("flv")
cfg := WithRTSP(base, tc.r)
if want := append(slices.Clone(base.Listeners), RTSP); !reflect.DeepEqual(cfg.Listeners, want) {
t.Errorf("listeners %v, want %v", cfg.Listeners, want)
}
want := map[string]string{}
for k, v := range base.Env {
want[k] = v
}
for k, v := range tc.want {
want[k] = v
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
if _, ok := base.Env["SRS_VHOST_RTSP_ENABLED"]; ok || len(base.Listeners) != 3 {
t.Errorf("the base config changed: %v %v", base.Listeners, base.Env)
}
ports := map[Listener]int{}
for i, l := range cfg.Listeners {
ports[l] = 14000 + i
}
if _, err := BuildEnv(cfg, ports, "/tmp/x", TLSFiles{}); err != nil {
t.Fatalf("build env: %v", err)
}
})
}
// A config with RTSP on keeps its listeners.
if cfg := WithRTSP(CommonConfig(), RTSPOptions{}); !reflect.DeepEqual(cfg.Listeners, CommonListeners) {
t.Errorf("listeners %v, want Common's", cfg.Listeners)
}
}
func TestWithGB(t *testing.T) {
cases := []struct {
name string
g GBOptions
want map[string]string
}{
{"Defaults", GBOptions{}, map[string]string{}},
{"MediaConnectTimeout", GBOptions{MediaConnectTimeout: 1500 * time.Millisecond},
map[string]string{"SRS_STREAM_CASTER_MEDIA_CONNECT_TIMEOUT": "1.5"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
base := PlayConfig(Play{})
cfg := WithGB(base, tc.g)
if want := append(slices.Clone(base.Listeners), GB); !reflect.DeepEqual(cfg.Listeners, want) {
t.Errorf("listeners %v, want %v", cfg.Listeners, want)
}
want := map[string]string{}
for k, v := range base.Env {
want[k] = v
}
for k, v := range tc.want {
want[k] = v
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
if len(base.Listeners) != 2 {
t.Errorf("the base config changed: %v", base.Listeners)
}
ports := map[Listener]int{}
for i, l := range cfg.Listeners {
ports[l] = 14000 + i
}
env, err := BuildEnv(cfg, ports, "/tmp/x", TLSFiles{})
if err != nil {
t.Fatalf("build env: %v", err)
}
if !slices.Contains(env, "SRS_STREAM_CASTER_CASTER=gb28181") {
t.Errorf("env %v has no GB caster", env)
}
})
}
// A config with GB on keeps its listeners.
if cfg := WithGB(CommonConfig(), GBOptions{}); !reflect.DeepEqual(cfg.Listeners, CommonListeners) {
t.Errorf("listeners %v, want Common's", cfg.Listeners)
}
}
func TestWithRawAPI(t *testing.T) {
cases := []struct {
name string
r RawAPI
want map[string]string
}{
{"ReloadOff", RawAPI{},
map[string]string{"SRS_HTTP_API_RAW_API_ENABLED": "on", "SRS_HTTP_API_RAW_API_ALLOW_RELOAD": "off"}},
{"AllowReload", RawAPI{AllowReload: true},
map[string]string{"SRS_HTTP_API_RAW_API_ENABLED": "on", "SRS_HTTP_API_RAW_API_ALLOW_RELOAD": "on"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
base := PlayConfig(Play{})
cfg := WithRawAPI(base, tc.r)
if !reflect.DeepEqual(cfg.Listeners, base.Listeners) {
t.Errorf("listeners %v, want %v", cfg.Listeners, base.Listeners)
}
want := map[string]string{}
for k, v := range base.Env {
want[k] = v
}
for k, v := range tc.want {
want[k] = v
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
if _, ok := base.Env["SRS_HTTP_API_RAW_API_ENABLED"]; ok {
t.Errorf("the base config changed: %v", base.Env)
}
ports := map[Listener]int{}
for i, l := range cfg.Listeners {
ports[l] = 14000 + i
}
if _, err := BuildEnv(cfg, ports, "/tmp/x", TLSFiles{}); err != nil {
t.Fatalf("build env: %v", err)
}
})
}
}
func TestWithSecurity(t *testing.T) {
cases := []struct {
name string
s Security
want map[string]string
}{
{"NoRules", Security{}, map[string]string{"SRS_VHOST_SECURITY_ENABLED": "on"}},
{"Rules", Security{
AllowPublish: []string{"127.0.0.0/8", "10.0.0.1"}, AllowPlay: []string{"all"},
DenyPublish: []string{"127.0.0.1"}, DenyPlay: []string{"192.168.0.0/16"},
}, map[string]string{
"SRS_VHOST_SECURITY_ENABLED": "on",
"SRS_VHOST_SECURITY_ALLOW_PUBLISH": "127.0.0.0/8 10.0.0.1",
"SRS_VHOST_SECURITY_ALLOW_PLAY": "all",
"SRS_VHOST_SECURITY_DENY_PUBLISH": "127.0.0.1",
"SRS_VHOST_SECURITY_DENY_PLAY": "192.168.0.0/16",
}},
{"OnlyDenyPlay", Security{DenyPlay: []string{"all"}}, map[string]string{
"SRS_VHOST_SECURITY_ENABLED": "on", "SRS_VHOST_SECURITY_DENY_PLAY": "all",
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
base := PlayConfig(Play{})
cfg := WithSecurity(base, tc.s)
if !reflect.DeepEqual(cfg.Listeners, base.Listeners) {
t.Errorf("listeners %v, want %v", cfg.Listeners, base.Listeners)
}
want := map[string]string{}
for k, v := range base.Env {
want[k] = v
}
for k, v := range tc.want {
want[k] = v
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
if _, ok := base.Env["SRS_VHOST_SECURITY_ENABLED"]; ok {
t.Errorf("the base config changed: %v", base.Env)
}
})
}
}
// TestWithForward checks a config's forward to destinations: on with the
// destinations, off with them kept, and only the destinations with
// ForwardBackend, which turns forward on itself, so with both set the hooks
// accept the config.
func TestWithForward(t *testing.T) {
dests := []string{"127.0.0.1:14010", "127.0.0.1:14020"}
cases := []struct {
name string
base Config
f Forward
want map[string]string
}{
{"One", PlayConfig(Play{}), Forward{Destinations: dests[:1]}, map[string]string{
"SRS_VHOST_FORWARD_ENABLED": "on", "SRS_VHOST_FORWARD_DESTINATION": "127.0.0.1:14010",
}},
{"Two", PlayConfig(Play{}), Forward{Destinations: dests}, map[string]string{
"SRS_VHOST_FORWARD_ENABLED": "on", "SRS_VHOST_FORWARD_DESTINATION": "127.0.0.1:14010 127.0.0.1:14020",
}},
{"Off", PlayConfig(Play{}), Forward{Destinations: dests[:1], Off: true}, map[string]string{
"SRS_VHOST_FORWARD_ENABLED": "off", "SRS_VHOST_FORWARD_DESTINATION": "127.0.0.1:14010",
}},
{"WithBackend", Config{Listeners: []Listener{RTMP, HTTPAPI}, Hooks: true, ForwardBackend: true},
Forward{Destinations: dests[1:]}, map[string]string{"SRS_VHOST_FORWARD_DESTINATION": "127.0.0.1:14020"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg := WithForward(tc.base, tc.f)
want := map[string]string{}
for k, v := range tc.base.Env {
want[k] = v
}
for k, v := range tc.want {
want[k] = v
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
if _, ok := tc.base.Env["SRS_VHOST_FORWARD_DESTINATION"]; ok {
t.Errorf("the base config changed: %v", tc.base.Env)
}
if !cfg.Hooks {
return
}
got, err := withHooks(cfg, "http://127.0.0.1:14005")
if err != nil {
t.Fatalf("with hooks: %v", err)
}
if got.Env["SRS_VHOST_FORWARD_ENABLED"] != "on" || got.Env["SRS_VHOST_FORWARD_DESTINATION"] != tc.want["SRS_VHOST_FORWARD_DESTINATION"] {
t.Errorf("env with the backend %v, want forward on and the destinations", got.Env)
}
})
}
}
// TestRestartNeedsExit checks Restart refuses a server that still runs and
// one with hooks, whose mock closed with it.
func TestRestartNeedsExit(t *testing.T) {
running := &Server{done: make(chan struct{})}
if err := running.Restart(); err == nil || !strings.Contains(err.Error(), "running") {
t.Errorf("restart of a running server: %v, want an error saying it runs", err)
}
done := make(chan struct{})
close(done)
hooked := &Server{done: done, Hooks: &mock.Server{}}
if err := hooked.Restart(); err == nil || !strings.Contains(err.Error(), "hooks") {
t.Errorf("restart of a server with hooks: %v, want an error naming the hooks", err)
}
}
// TestWithEdge checks the env of an edge: the cluster mode, its origins in
// order, the protocol only when set, and the base config unchanged.
func TestWithEdge(t *testing.T) {
origins := []string{"127.0.0.1:14010", "127.0.0.1:14020"}
cases := []struct {
name string
e Edge
want map[string]string
}{
{"OneOrigin", Edge{Origins: origins[:1]}, map[string]string{
"SRS_VHOST_CLUSTER_MODE": "remote", "SRS_VHOST_CLUSTER_ORIGIN": "127.0.0.1:14010",
}},
{"Failover", Edge{Origins: origins}, map[string]string{
"SRS_VHOST_CLUSTER_MODE": "remote", "SRS_VHOST_CLUSTER_ORIGIN": "127.0.0.1:14010 127.0.0.1:14020",
}},
{"Flv", Edge{Origins: origins[1:], Protocol: "flv"}, map[string]string{
"SRS_VHOST_CLUSTER_MODE": "remote", "SRS_VHOST_CLUSTER_ORIGIN": "127.0.0.1:14020",
"SRS_VHOST_CLUSTER_PROTOCOL": "flv",
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
base := PlayConfig(Play{})
cfg := WithEdge(base, tc.e)
if !reflect.DeepEqual(cfg.Listeners, base.Listeners) {
t.Errorf("listeners %v, want %v", cfg.Listeners, base.Listeners)
}
want := map[string]string{}
for k, v := range base.Env {
want[k] = v
}
for k, v := range tc.want {
want[k] = v
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
if _, ok := base.Env["SRS_VHOST_CLUSTER_MODE"]; ok {
t.Errorf("the base config changed: %v", base.Env)
}
})
}
}
// TestWithOriginCluster checks the env of an origin of an origin cluster:
// the cluster on, and the coworkers' APIs space separated.
func TestWithOriginCluster(t *testing.T) {
base := PlayConfig(Play{})
cfg := WithOriginCluster(base, OriginCluster{Coworkers: []string{"127.0.0.1:14010", "127.0.0.1:14020"}})
if !reflect.DeepEqual(cfg.Listeners, base.Listeners) {
t.Errorf("listeners %v, want %v", cfg.Listeners, base.Listeners)
}
want := map[string]string{}
for k, v := range base.Env {
want[k] = v
}
want["SRS_VHOST_CLUSTER_ORIGIN_CLUSTER"] = "on"
want["SRS_VHOST_CLUSTER_COWORKERS"] = "127.0.0.1:14010 127.0.0.1:14020"
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
if _, ok := base.Env["SRS_VHOST_CLUSTER_ORIGIN_CLUSTER"]; ok {
t.Errorf("the base config changed: %v", base.Env)
}
}
func TestWithDVR(t *testing.T) {
base := PlayConfig(Play{})
cases := []struct {
name string
dvr DVR
want map[string]string
}{
{"session", DVR{Path: "./dvr/[app]/[stream].[timestamp].flv"}, map[string]string{
"SRS_VHOST_DVR_ENABLED": "on",
"SRS_VHOST_DVR_DVR_PATH": "./dvr/[app]/[stream].[timestamp].flv",
}},
{"segment", DVR{Plan: "segment", Path: "./dvr/[stream].mp4", Duration: 2 * time.Second, NoWaitKeyframe: true, Apply: []string{"live/a", "live/b"}}, map[string]string{
"SRS_VHOST_DVR_ENABLED": "on",
"SRS_VHOST_DVR_DVR_PLAN": "segment",
"SRS_VHOST_DVR_DVR_PATH": "./dvr/[stream].mp4",
"SRS_VHOST_DVR_DVR_DURATION": "2",
"SRS_VHOST_DVR_DVR_WAIT_KEYFRAME": "off",
"SRS_VHOST_DVR_DVR_APPLY": "live/a live/b",
}},
{"fraction", DVR{Plan: "segment", Path: "./dvr/[stream].flv", Duration: 1500 * time.Millisecond}, map[string]string{
"SRS_VHOST_DVR_ENABLED": "on",
"SRS_VHOST_DVR_DVR_PLAN": "segment",
"SRS_VHOST_DVR_DVR_PATH": "./dvr/[stream].flv",
"SRS_VHOST_DVR_DVR_DURATION": "1.5",
}},
}
for _, tc := range cases {
cfg := WithDVR(base, tc.dvr)
if !reflect.DeepEqual(cfg.Listeners, base.Listeners) {
t.Errorf("%v: listeners %v, want %v", tc.name, cfg.Listeners, base.Listeners)
}
want := map[string]string{}
for k, v := range base.Env {
want[k] = v
}
for k, v := range tc.want {
want[k] = v
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("%v: env %v, want %v", tc.name, cfg.Env, want)
}
}
if _, ok := base.Env["SRS_VHOST_DVR_ENABLED"]; ok {
t.Errorf("the base config changed: %v", base.Env)
}
}
func TestWithStatic(t *testing.T) {
base := Config{Listeners: []Listener{RTMP, HTTPAPI}, Env: map[string]string{"SRS_VHOST_MIN_LATENCY": "on"}}
cfg := WithStatic(base, Static{Mount: "[vhost]/files/", Dir: "./files/[vhost]"})
want := map[string]string{
"SRS_VHOST_MIN_LATENCY": "on",
"SRS_VHOST_HTTP_STATIC_ENABLED": "on",
"SRS_VHOST_HTTP_STATIC_MOUNT": "[vhost]/files/",
"SRS_VHOST_HTTP_STATIC_DIR": "./files/[vhost]",
}
if !reflect.DeepEqual(cfg.Env, want) {
t.Errorf("env %v, want %v", cfg.Env, want)
}
if !reflect.DeepEqual(cfg.Listeners, []Listener{RTMP, HTTPAPI, HTTPServer}) {
t.Errorf("listeners %v, want the HTTP server added", cfg.Listeners)
}
if len(base.Env) != 1 || len(base.Listeners) != 2 {
t.Errorf("the base config changed: %v", base)
}
if got := CommonConfig().Env["SRS_VHOST_HTTP_STATIC_MOUNT"]; got != CommonStatic.Mount {
t.Errorf("Common's static mount %q, want %q", got, CommonStatic.Mount)
}
}
func TestStaticPaths(t *testing.T) {
s := &Server{Dir: "/run/common-1"}
cases := []struct {
st Static
path, dir string
}{
{CommonStatic, "/static/", "/run/common-1/static/__defaultVhost__"},
{Static{Mount: "[vhost]/a/b", Dir: "./www"}, "/a/b/", "/run/common-1/www"},
{Static{Mount: "[vhost]/", Dir: "files"}, "/", "/run/common-1/files"},
}
for _, tc := range cases {
if got := tc.st.URLPath(); got != tc.path {
t.Errorf("%v: path %q, want %q", tc.st, got, tc.path)
}
if got := tc.st.DirIn(s); got != tc.dir {
t.Errorf("%v: dir %q, want %q", tc.st, got, tc.dir)
}
}
if got := s.WebRoot(); got != "/run/common-1/objs/nginx/html" {
t.Errorf("web root %q", got)
}
}
// TestProcessEnv checks the env a server or a command run gets from this
// process: no SRS_* var, so only the adapter configures SRS, and the sanitizer
// quarantine bounded, before the user's own ASAN_OPTIONS, so theirs win where
// they overlap.
func TestProcessEnv(t *testing.T) {
t.Setenv("SRS_LISTEN", "1935")
t.Setenv("ASAN_OPTIONS", "")
os.Unsetenv("ASAN_OPTIONS")
want := fmt.Sprintf("ASAN_OPTIONS=quarantine_size_mb=%d", asanQuarantineMB)
env := processEnv()
if got := withPrefix(env, "ASAN_OPTIONS="); !reflect.DeepEqual(got, []string{want}) {
t.Errorf("ASAN_OPTIONS %v, want %q", got, want)
}
if got := withPrefix(env, "SRS_"); len(got) != 0 {
t.Errorf("SRS_ vars %v, want none", got)
}
if !slices.Contains(env, "MallocNanoZone=0") {
t.Error("no MallocNanoZone=0")
}
t.Setenv("ASAN_OPTIONS", "detect_container_overflow=1")
env = processEnv()
if got, want := withPrefix(env, "ASAN_OPTIONS="), want+":detect_container_overflow=1"; !reflect.DeepEqual(got, []string{want}) {
t.Errorf("ASAN_OPTIONS %v, want %q, the suite's then the user's", got, want)
}
}
// withPrefix returns the vars of env that start with prefix.
func withPrefix(env []string, prefix string) []string {
var out []string
for _, kv := range env {
if strings.HasPrefix(kv, prefix) {
out = append(out, kv)
}
}
return out
}
// TestExitSignal checks the signal that ended a process: SIGKILL for a killed
// one, none for one that exited.
func TestExitSignal(t *testing.T) {
killed := exec.Command("sleep", "30")
if err := killed.Start(); err != nil {
t.Fatal(err)
}
killed.Process.Kill()
killed.Wait()
if got := exitSignal(killed.ProcessState); got != syscall.SIGKILL {
t.Errorf("a killed process: signal %v, want SIGKILL", got)
}
if code := killed.ProcessState.ExitCode(); code != -1 {
t.Errorf("a killed process: exit code %d, want -1", code)
}
exited := exec.Command("true")
exited.Run()
if got := exitSignal(exited.ProcessState); got != 0 {
t.Errorf("an exited process: signal %v, want none", got)
}
}
// TestStopNamesTheSignal checks what Stop says of a server that already
// ended: a SIGKILL names the signal and the OOM killer, the one thing that
// sends it to a server the suite runs, so a run on a host short of memory is
// not taken for a flake; an exit code is reported as before.
func TestStopNamesTheSignal(t *testing.T) {
done := make(chan struct{})
close(done)
killed := &Server{done: done, code: -1, signal: syscall.SIGKILL}
err := killed.Stop()
if err == nil || !strings.Contains(err.Error(), "SIGKILL") || !strings.Contains(err.Error(), "OOM") || !strings.Contains(err.Error(), "dmesg") {
t.Errorf("a killed server: %v, want SIGKILL, the OOM killer and dmesg named", err)
}
crashed := &Server{done: done, code: -1, signal: syscall.SIGSEGV}
err = crashed.Stop()
if err == nil || !strings.Contains(err.Error(), "signal 11") || strings.Contains(err.Error(), "OOM") {
t.Errorf("a crashed server: %v, want signal 11 and no OOM killer", err)
}
failed := &Server{done: done, code: 1}
if err := failed.Stop(); err == nil || err.Error() != "SRS exited with code 1" {
t.Errorf("a server that exited 1: %v", err)
}
if err := (&Server{done: done}).Stop(); err != nil {
t.Errorf("a server that exited 0: %v", err)
}
}