1
0
mirror of https://github.com/ossrs/srs.git synced 2026-10-28 22:59:37 +03:00
Files
srs/skills/srs-develop/scripts/obs-test-lib.sh
Winlin 876411828f Windows: Native x64 build with MSVC. v8.0.65 (#4787)
SRS builds and runs natively on Windows x64 again, now with MSVC and
without Cygwin. This change adds `srs.exe` and `srs_utest.exe`, which
are tested on a Windows host and in CI, and the release attaches a
Windows zip. The Cygwin64 build was removed in v7.0.60 (#4447) because
it was too hard to maintain. This build sits on the native Windows
support that State Threads got in v8.0.56, and every Windows feature has
automated tests so it does not rot again.

## Build

- A Windows-only CMake build, `trunk/cmake/windows/CMakeLists.txt`,
reads the same module list as `./configure`
(`trunk/auto/module_files.txt`). The Unix `./configure` build is
unchanged.
- Dependencies: vcpkg pins OpenSSL 3, libsrtp, libsrt, Opus and
googletest in `trunk/vcpkg.json`. MSYS2 builds the bundled FFmpeg-fit
with MSVC, for AAC and Opus in WebRTC.
- The platform shims for sockets, file IO, time, processes, signals and
system stats live in `srs_core_platform.hpp/.cpp`. App code has no new
`#ifdef` spread.
- `skills/srs-develop/scripts/srs-windows-runner.sh` builds, tests and
packages. The Windows host, CI and the release all use it. Its `vs` step
makes a Visual Studio solution.
- The IDE CMake, `trunk/cmake/CMakeLists.txt`, now takes its sources
from the module list too.

## Features on Windows

- Live: RTMP, RTMPS, HTTP-FLV, HTTP-TS, HTTPS, HLS (TS and fMP4), DASH,
DVR, static files and VOD.
- HTTP API, exporter, hooks, security, edge and forward.
- WebRTC WHIP and WHEP with AAC and Opus conversion, SRT through libsrt
including SRT to RTMP, GB28181 and RTSP.
- Ingest, transcode and exec, which start `ffmpeg.exe` through
`CreateProcessW`. A job object kills the child when SRS exits.
- Control: Ctrl+C quits fast. Ctrl+Break or closing the console quits
gracefully. The RAW API runs `reload` and `reopen-log`. The pid file
uses `LockFileEx`. Daemon mode is refused, because `srs.exe` is a
console program.

## Tests and CI

- **Windows host:** `srs_utest` 3325 of 3325. `cmd/integ` main and timer
cases pass at `-integ.limit-scale 4`. The OBS RTMP, SRT and WHIP cases
now publish to native `srs.exe` instead of WSL.
- **Skipped on Windows:** six timer cases that need Unix signals. Each
names the case that covers it (`cmd/integ/internal/check/skips.go`).
- **CI:** a `windows-utest` job in `test.yml` on `windows-2025`. The
build names `cl` for CMake, because the current image's Visual Studio 18
puts its clang on PATH.
- **Release:** a `windows` job in `release.yml` attaches
`SRS-Windows-x86_64-<ver>.zip`. The zip needs no DLLs beyond Windows'
own.
- **Linux and macOS:** the Full tier is unchanged. A Linux RTMP A/B
against `develop` at 11,520 players measured +1.8% SRS CPU, below the
10% line.

## Fixes found on the way, for every OS

- A forked child that fails to redirect or exec, such as for ffmpeg, now
calls `_exit`. Before, with SRT started, it hung in libsrt's exit
handlers, and SRS treated ffmpeg as running.
- A child's `process ppid=...` banner now reaches its log: stdout is
flushed before `execv`.
- `mkdir_all` can create a missing dir right under the root, such as
`/data/hls`.
- An HEVC sequence header from WebRTC is the same for the same parameter
sets. The hvcC record had uninitialized fields, so RTMP could get two
headers.
- With OpenSSL 3, which a Unix `--use-sys-ssl` build may use too: RSA
DTLS certificates are 2048-bit. `dtls_version dtls1.0` works, but leaves
out the anonymous, null and MD5 suites that security level 1 refuses.
- The utest builds with RTSP or GB28181 off, the configure defaults.
- `openssl3-api-check.sh` works under GCC. Before, every compile failed
and it reported PASS.
- Several `cmd/integ` cases no longer flake: the DASH
`availabilityStartTime` bound, the RTC hook queue cases, the GB publish
waits, and RTC to RTMP keyframe requests.

## Limits

- ST on Windows uses WSAPoll only, with a ~15.6 ms timer. Non-blocking
TCP sends are all-or-nothing.
- No `/proc` stats, inotify reload, `SO_REUSEPORT`, daemon, Windows
service or installer.
- No ASan, gperftools, Valgrind or backtraces.
- UTF-8 file names need Windows 10 1903 or later.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 08:00:41 -04:00

290 lines
13 KiB
Bash

#!/bin/bash
# Sourced OBS test harness, compatible with Bash 3.2. Call init, start, setup,
# assertions and finish. init installs owned-resource EXIT/signal cleanup.
# Slot offsets: RTMP +0, HTTP +1, API +2, RTC UDP +3, SRT UDP +4.
# OBS_PYTHON names the Python command (default python3). OBS_SRS_HOST means
# the caller already runs SRS on the slot at that address, so start never
# spawns or kills SRS. OBS_SRS_BIN names the SRS binary under trunk (default
# objs/srs).
obs_test_ports() {
OBS_RTMP_PORT=$1
OBS_HTTP_PORT=$(($1 + 1))
OBS_API_PORT=$(($1 + 2))
OBS_RTC_PORT=$(($1 + 3))
OBS_SRT_PORT=$(($1 + 4))
OBS_API_URL="http://${OBS_SRS_HOST:-127.0.0.1}:$OBS_API_PORT"
OBS_HTTP_URL="http://${OBS_SRS_HOST:-127.0.0.1}:$OBS_HTTP_PORT"
}
obs_test_fail() { echo "FAIL $*" >&2; return 1; }
obs_test_pass() { OBS_PASS_COUNT=$((OBS_PASS_COUNT + 1)); echo "PASS $*"; }
obs_test_known() { OBS_KNOWN_COUNT=$((OBS_KNOWN_COUNT + 1)); echo "KNOWN $*"; }
obs_test_skip() { echo "SKIP $*"; exit 0; }
obs_test_probe_available() {
echo "$1" | jq -e '.obs_binary != null and .websocket_plugin != null' >/dev/null
}
obs_test_budget() {
local elapsed=$((SECONDS - OBS_STARTED))
[[ $elapsed -le 60 ]] || obs_test_fail "budget ${elapsed}s > 60s"
}
# Bound each external command by both its own timeout and the remaining budget.
obs_test_command() {
local limit=$1 deadline status=0
shift
deadline=$((SECONDS + limit))
OBS_COMMAND_TIMED_OUT=
"$@" &
OBS_COMMAND_PID=$!
while kill -0 "$OBS_COMMAND_PID" 2>/dev/null; do
if [[ $SECONDS -ge $deadline || $((SECONDS - OBS_STARTED)) -ge 60 ]]; then
kill "$OBS_COMMAND_PID" 2>/dev/null || true
sleep 0.1
kill -KILL "$OBS_COMMAND_PID" 2>/dev/null || true
wait "$OBS_COMMAND_PID" 2>/dev/null || true
OBS_COMMAND_PID=
OBS_COMMAND_TIMED_OUT=1
obs_test_fail 'command timed out'
return 1
fi
sleep 0.1
done
wait "$OBS_COMMAND_PID" || status=$?
OBS_COMMAND_PID=
return "$status"
}
obs_test_cleanup() {
local pid attempt status=0
if [[ -n "${OBS_COMMAND_PID:-}" ]]; then
kill "$OBS_COMMAND_PID" 2>/dev/null || true
wait "$OBS_COMMAND_PID" 2>/dev/null || true
fi
if [[ -n "${OBS_LAUNCH_PID:-}" ]]; then
kill "$OBS_LAUNCH_PID" 2>/dev/null || true
wait "$OBS_LAUNCH_PID" 2>/dev/null || true
OBS_LAUNCH_PID=
fi
if [[ -n "${OBS_INSTANCE_ID:-}" ]]; then
"${OBS_PYTHON:-python3}" "$OBS_WS" instance remove --instance "$OBS_INSTANCE_ID" --force --timeout 10 \
>"$OBS_TEST_DIR/cleanup.log" 2>&1 && OBS_INSTANCE_ID= || { obs_test_fail 'OBS cleanup'; status=1; }
fi
if [[ -n "${OBS_SRS_PID:-}" && -f "$OBS_TEST_DIR/srs.pid" ]]; then
pid=$(cat "$OBS_TEST_DIR/srs.pid")
# Only the process spawned by this shell, confirmed by its pid file.
if [[ "$pid" == "$OBS_SRS_PID" ]]; then
kill "$pid" 2>/dev/null || true
for attempt in 1 2 3 4 5 6 7 8 9 10; do
kill -0 "$pid" 2>/dev/null || break
sleep 0.1
done
kill -KILL "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
rm -f "$OBS_TEST_DIR/srs.pid"
else
obs_test_fail 'SRS pid file no longer matches owned process'
return 1
fi
fi
return "$status"
}
obs_test_exit() {
local status=$?
trap - EXIT
obs_test_cleanup || status=1
obs_test_budget || status=1
[[ $status -eq 0 ]] || echo "FAIL $OBS_CASE (logs: $OBS_TEST_DIR)" >&2
exit "$status"
}
obs_test_init() {
[[ "${SRS_TEST_OBS:-1}" != 0 ]] || obs_test_skip SRS_TEST_OBS=0
OBS_STARTED=$SECONDS
OBS_CASE=$1
local base=${SRS_TEST_PORT_BASE:-$2} dep probe script_dir
case "$OBS_CASE" in ''|*[!a-zA-Z0-9_-]*) obs_test_fail 'invalid case name'; return 1;; esac
case "$base" in ''|*[!0-9]*) obs_test_fail 'invalid port base'; return 1;; esac
[[ $base -ge 11000 && $((base + 99)) -lt 32768 ]] || { obs_test_fail 'port slot outside test range'; return 1; }
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
OBS_WORKSPACE=$(cd "$script_dir/../../.." && pwd)
OBS_WS="$OBS_WORKSPACE/skills/internal-obs-control/scripts/obsws.py"
OBS_RECIPE="$OBS_WORKSPACE/skills/internal-obs-control/scripts/obs_srs_test.py"
OBS_TEST_DIR="/tmp/srs-obs-$OBS_CASE"
obs_test_ports "$base"
OBS_PASS_COUNT=0 OBS_KNOWN_COUNT=0
OBS_INSTANCE_ID= OBS_SRS_PID= OBS_LAUNCH_PID= OBS_COMMAND_PID= OBS_COMMAND_TIMED_OUT=
for dep in "${OBS_PYTHON:-python3}" ffmpeg ffprobe jq curl; do
command -v "$dep" >/dev/null || { obs_test_fail "missing dependency $dep"; return 1; }
done
# Probe is read-only; never enable/restart the user's main OBS.
probe=$("${OBS_PYTHON:-python3}" "$OBS_WS" probe --json) || { obs_test_skip 'OBS not available (probe failed)'; }
obs_test_probe_available "$probe" || obs_test_skip 'OBS not available'
[[ -n "${OBS_SRS_HOST:-}" || -x "$OBS_WORKSPACE/trunk/${OBS_SRS_BIN:-objs/srs}" ]] || { obs_test_fail "build trunk/${OBS_SRS_BIN:-objs/srs} first"; return 1; }
mkdir -p "$OBS_TEST_DIR/html"
# Reject another live owner; never terminate a listener to acquire a slot.
if [[ -z "${OBS_SRS_HOST:-}" && -f "$OBS_TEST_DIR/srs.pid" ]] && kill -0 "$(cat "$OBS_TEST_DIR/srs.pid")" 2>/dev/null; then
obs_test_fail 'case already running'; return 1
fi
trap obs_test_exit EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
export OBS_CONTROL_WS_PORTS=15000-15099 PYTHONDONTWRITEBYTECODE=1
# Shared clip: atomic rename prevents a parallel setup reading half a file.
OBS_CLIP=/tmp/srs-obs-test-640x360-30.mp4
if [[ ! -s "$OBS_CLIP" ]]; then
obs_test_command 15 ffmpeg -hide_banner -loglevel error -y \
-f lavfi -i testsrc2=size=640x360:rate=30 -f lavfi -i sine=frequency=440:sample_rate=48000 \
-t 4 -c:v libx264 -preset ultrafast -pix_fmt yuv420p -c:a aac \
"$OBS_TEST_DIR/clip.mp4" || return 1
mv "$OBS_TEST_DIR/clip.mp4" "$OBS_CLIP"
fi
}
obs_test_start_srs() {
(cd "$OBS_WORKSPACE/trunk" && exec env \
SRS_RTMP_LISTEN="$OBS_RTMP_PORT" SRS_HTTP_SERVER_ENABLED=on SRS_HTTP_SERVER_LISTEN="$OBS_HTTP_PORT" \
SRS_HTTP_SERVER_DIR="$OBS_TEST_DIR/html" SRS_HTTP_API_ENABLED=on SRS_HTTP_API_LISTEN="$OBS_API_PORT" \
SRS_RTC_SERVER_ENABLED=on SRS_RTC_SERVER_LISTEN="$OBS_RTC_PORT" SRS_RTC_SERVER_CANDIDATE=127.0.0.1 \
SRS_SRT_SERVER_ENABLED=on SRS_SRT_SERVER_LISTEN="$OBS_SRT_PORT" \
SRS_VHOST_HTTP_REMUX_ENABLED=on SRS_VHOST_HLS_ENABLED=on SRS_VHOST_HLS_HLS_FRAGMENT=1 \
SRS_VHOST_HLS_HLS_WINDOW=4 SRS_VHOST_HLS_HLS_PATH="$OBS_TEST_DIR/html" \
SRS_VHOST_RTC_ENABLED=on SRS_VHOST_RTC_RTMP_TO_RTC=on SRS_VHOST_RTC_RTC_TO_RTMP=on \
SRS_VHOST_SRT_ENABLED=on SRS_VHOST_SRT_SRT_TO_RTMP=on SRS_LOG_TANK=console \
"$@" "./${OBS_SRS_BIN:-objs/srs}" -e) >"$OBS_TEST_DIR/srs.log" 2>&1 &
OBS_SRS_PID=$!
printf '%s\n' "$OBS_SRS_PID" > "$OBS_TEST_DIR/srs.pid"
}
obs_test_start() {
OBS_INSTANCE_ID=$("${OBS_PYTHON:-python3}" "$OBS_WS" instance new) || return 1
# Python on Windows ends its lines with CRLF.
OBS_INSTANCE_ID=${OBS_INSTANCE_ID%$'\r'}
[[ "$OBS_INSTANCE_ID" == t* ]] || { obs_test_fail 'instance claim failed'; return 1; }
[[ -n "${OBS_SRS_HOST:-}" ]] || obs_test_start_srs "$@"
"${OBS_PYTHON:-python3}" "$OBS_WS" launch --instance "$OBS_INSTANCE_ID" --wait 15 --minimize-to-tray \
>"$OBS_TEST_DIR/launch.log" 2>&1 &
OBS_LAUNCH_PID=$!
local deadline=$((SECONDS + 10))
until curl -fsS --max-time 1 "$OBS_API_URL/api/v1/versions" >"$OBS_TEST_DIR/version.json" 2>/dev/null; do
[[ -n "${OBS_SRS_HOST:-}" ]] || kill -0 "$OBS_SRS_PID" 2>/dev/null || { obs_test_fail 'SRS exited during startup'; return 1; }
[[ $SECONDS -lt $deadline ]] || { obs_test_fail 'SRS API startup timeout'; return 1; }
sleep 0.1
done
if ! wait "$OBS_LAUNCH_PID"; then
OBS_LAUNCH_PID=
obs_test_fail 'isolated OBS launch failed; see launch.log'
return 1
fi
OBS_LAUNCH_PID=
}
obs_test_setup() {
local status=0 probe relaunch=
obs_test_setup_once "$@" || status=$?
if [[ -n "${OBS_COMMAND_TIMED_OUT:-}" ]]; then
# When several instances reset video at once, OBS can hang in
# obs_reset_video, joining a graphics thread that keeps rendering; the
# websocket stops answering while the process lives. Replace it once.
echo 'OBS stopped answering during setup; relaunching the isolated instance once'
obs_test_command 10 "${OBS_PYTHON:-python3}" "$OBS_WS" quit --instance "$OBS_INSTANCE_ID" --force --timeout 3 \
>>"$OBS_TEST_DIR/launch.log" 2>&1 || true
relaunch=1
elif [[ $status -eq 2 ]]; then
probe=$("${OBS_PYTHON:-python3}" "$OBS_WS" probe --instance "$OBS_INSTANCE_ID" --json) || probe='{}'
# Some macOS OBS versions exit on their first CreateProfile. Recover once,
# only when the owned process exited; never retry a running/modal OBS.
if echo "$probe" | jq -e '.obs_running == false and .live.connected == false' >/dev/null; then
echo 'OBS exited during setup; relaunching the isolated instance once'
relaunch=1
fi
fi
if [[ -n "$relaunch" ]]; then
obs_test_command 15 "${OBS_PYTHON:-python3}" "$OBS_WS" launch --instance "$OBS_INSTANCE_ID" --wait 10 --minimize-to-tray \
>>"$OBS_TEST_DIR/launch.log" 2>&1 || return 1
status=0
obs_test_setup_once "$@" || status=$?
fi
[[ $status -eq 0 ]] || { obs_test_fail "OBS setup/publish (see $OBS_TEST_DIR/setup.log)"; return 1; }
}
# 20 s bounds a hung OBS early enough for one relaunch inside the 60 s budget.
obs_test_setup_once() {
obs_test_command 20 "${OBS_PYTHON:-python3}" "$OBS_RECIPE" setup --instance "$OBS_INSTANCE_ID" \
--width 640 --height 360 --fps 30 --video-bitrate 800 --preset ultrafast \
--media-file "$OBS_CLIP" --srs-api "$OBS_API_URL" --http-url "$OBS_HTTP_URL" \
"$@" --start --verify >>"$OBS_TEST_DIR/setup.log" 2>&1
}
obs_test_read_stream() { curl -fsS --max-time 1 "$OBS_API_URL/api/v1/streams/"; }
obs_test_assert_api() {
local video=$1 audio=$2 data deadline=$((SECONDS + ${OBS_API_WAIT:-8}))
while :; do
data=$(obs_test_read_stream 2>/dev/null) || data='{}'
if echo "$data" | jq -e --arg name "$OBS_INSTANCE_ID" --arg video "$video" --arg audio "$audio" \
'.code == 0 and any(.streams[]?; .app == "live" and .name == $name and .publish.active == true
and .video.codec == $video and .audio.codec == $audio and .video.width == 640 and .video.height == 360)' >/dev/null; then
echo "$data" > "$OBS_TEST_DIR/streams.json"
obs_test_pass "API $video/$audio 640x360"; return
fi
[[ $SECONDS -lt $deadline ]] || break
obs_test_budget || return 1
sleep 0.1
done
obs_test_fail "API expected active live/$OBS_INSTANCE_ID $video/$audio 640x360"
}
# SRS flushes an RTC publisher's frame count to the statistic only every 288
# frames, and the RTC-to-RTMP bridge counts none, so a fresh WHIP publisher
# reads frames=0: report that as the known finding, never as a pass.
obs_test_assert_frames() {
local frames
frames=$(obs_test_read_stream 2>/dev/null | jq -r --arg name "$OBS_INSTANCE_ID" \
'[.streams[]? | select(.app == "live" and .name == $name) | .frames // 0] | first // 0') || frames=0
case "$frames" in ''|*[!0-9]*) frames=0;; esac
if [[ $frames -gt 0 ]]; then obs_test_pass "API frames $frames"; else obs_test_known 'F2 API frames 0'; fi
}
obs_test_media_matches() {
echo "$1" | jq -e --arg audio "$2" 'any(.streams[]?; .codec_type == "video" and .codec_name == "h264"
and .width == 640 and .height == 360) and any(.streams[]?; .codec_type == "audio" and .codec_name == $audio)' >/dev/null
}
obs_test_play() {
local kind=$1 audio=${2:-aac} suffix data deadline=$((SECONDS + 10))
case "$kind" in http-flv) suffix=flv;; hls) suffix=m3u8;; *) obs_test_fail 'unknown player'; return 1;; esac
while :; do
if obs_test_command 5 ffprobe -v error -rw_timeout 3000000 -analyzeduration 1000000 \
-probesize 1000000 -show_streams -of json "$OBS_HTTP_URL/live/$OBS_INSTANCE_ID.$suffix" \
>"$OBS_TEST_DIR/$kind.json" 2>"$OBS_TEST_DIR/$kind.log"; then
data=$(cat "$OBS_TEST_DIR/$kind.json")
if obs_test_media_matches "$data" "$audio"; then obs_test_pass "$kind H264/$audio"; return; fi
fi
[[ $SECONDS -lt $deadline ]] || break
obs_test_budget || return 1
sleep 0.1
done
obs_test_fail "$kind missing H264/$audio playback"
}
obs_test_whep() {
local tool="$OBS_WORKSPACE/tools/pion-whep/objs/pion-whep" summary
[[ -x "$tool" ]] || { obs_test_fail 'build tools/pion-whep before WHEP checks'; return 1; }
obs_test_command 10 "$tool" -hide_banner -f whep \
-i "$OBS_API_URL/rtc/v1/whep/?app=live&stream=$OBS_INSTANCE_ID&eip=127.0.0.1" \
-t 3 -f null - >"$OBS_TEST_DIR/whep.log" 2>&1 || return 1
summary=$(sed -n 's/^Received video=\([0-9]*\), audio=\([0-9]*\) .*/\1 \2/p' "$OBS_TEST_DIR/whep.log" | tail -1)
[[ "$summary" =~ ^([0-9]+)\ ([0-9]+)$ ]] && [[ ${BASH_REMATCH[1]} -gt 0 && ${BASH_REMATCH[2]} -gt 0 ]] \
|| { obs_test_fail 'WHEP missing video/audio packets'; return 1; }
obs_test_pass 'WHEP video/audio packets'
}
obs_test_finish() {
obs_test_cleanup || return 1
obs_test_budget || return 1
echo "RESULT $OBS_CASE passed=$OBS_PASS_COUNT known=$OBS_KNOWN_COUNT elapsed=$((SECONDS - OBS_STARTED))s"
}