diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e314d9ce8..65321c25c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,6 +1,9 @@ { "permissions": { "allow": [ + "Read", + "Glob", + "Grep", "Bash(find:*)", "Bash(ls:*)", "Bash(cat:*)", @@ -19,9 +22,8 @@ "Bash(realpath:*)", "Bash(dirname:*)", "Bash(basename:*)", - "Read", - "Glob", - "Grep" + "Bash(echo \"PID=$!\")", + "Bash(make)" ] }, "hooks": { diff --git a/.openclaw/.gitignore b/.openclaw/.gitignore index 7bc63699a..62ec8e268 100644 --- a/.openclaw/.gitignore +++ b/.openclaw/.gitignore @@ -10,3 +10,4 @@ /personal* /support* /*srs-consults* +/memory/202*.md diff --git a/.openclaw/memory/srs-codebase-map.md b/.openclaw/memory/srs-codebase-map.md index 573798f0f..0e4ef6a50 100644 --- a/.openclaw/memory/srs-codebase-map.md +++ b/.openclaw/memory/srs-codebase-map.md @@ -8,7 +8,7 @@ This file helps AI navigate the SRS project. Instead of grepping blindly, AI sho SRS has three codebases: -### C++ Media Server +### C++ Media Server Code The C++ media server (`trunk/src/`) serves as both origin server and edge server. It uses the ST (State Threads) coroutine library and is the full-featured server with all SRS capabilities. @@ -192,7 +192,7 @@ Config (`trunk/conf/`) is only for the C++ media server. The next-generation Go - `full.conf` — Full reference config with all options documented - Feature-specific configs: `rtc.conf`, `srt.conf`, `hls.conf`, `dvr.*.conf`, `edge.conf`, `forward.*.conf`, `gb28181.conf`, `dash.conf`, etc. -### State Threads +### State Threads Code State Threads (`trunk/3rdparty/st-srs/`) is the coroutine library used by the C++ media server only. The Go server does not use ST. @@ -207,7 +207,7 @@ State Threads (`trunk/3rdparty/st-srs/`) is the coroutine library used by the C+ - `md.h` — Platform detection and context switch macros - `md_linux.S`, `md_linux2.S`, `md_darwin.S`, `md_cygwin64.S` — Assembly context switch per platform -### Next-Generation Server +### Next-Generation Server Code The next-generation server (`cmd/` + `internal/`) is written in Go and maintained by AI. It is the future of SRS. Currently it only supports proxy, but work is ongoing to support the full feature set including origin, edge, and proxy servers. @@ -247,9 +247,13 @@ The knowledge base (`memory/srs-*.md`) captures William's knowledge about SRS ## Documentation Structure +### Tracking Docs + **Tracking** — Project changelog and version history: - `trunk/doc/CHANGELOG.md` — Full changelog of all SRS versions, one entry per merged PR with version bump +### C++ Media Server Docs + **C++ Media Server Docs** (`trunk/3rdparty/srs-docs/doc/`) — User-facing documentation: - `introduction.md` — SRS overview: what it is, supported protocols (RTMP/WebRTC/HLS/SRT/etc.), features list, ST coroutine architecture, and learning path - `getting-started.md` — Quick start with Docker: live streaming (RTMP publish, HTTP-FLV/HLS play), WebRTC publish/play, RTMP-to-WebRTC conversion, HTTPS for WebRTC on remote servers, SRT publishing, and multiple streams with flexible URL patterns @@ -288,6 +292,8 @@ The knowledge base (`memory/srs-*.md`) captures William's knowledge about SRS - `product-en.md` — Release history: milestones from v1.0 (2013) to v7.0 (2025+), codenames, key achievements per release, AI contributor era (2025+) - `security-advisories-en.md` — CVE reports: CVE-2024-29882 (JSONP XSS), CVE-2023-34105 (command injection in demo api-server), with patch versions and links +### Next-Generation Server Docs + **Next-Generation Server Docs** (`docs/proxy/`) — Documentation for the Go server: - `proxy-design.md` — Architecture: stateless proxy with built-in load balancing, single-proxy vs multi-proxy (Redis) deployment modes, horizontal scaling behind NLB - `proxy-protocol.md` — Backend registration protocol: default backend for debugging, automatic registration via SRS system API, heartbeat mechanism, env vars diff --git a/.openclaw/skills/srs-develop/SKILL.md b/.openclaw/skills/srs-develop/SKILL.md new file mode 100644 index 000000000..3b8bac662 --- /dev/null +++ b/.openclaw/skills/srs-develop/SKILL.md @@ -0,0 +1,126 @@ +--- +name: srs-develop +description: Develop, modify, debug, and maintain the next-generation SRS media server written in Go — including the proxy, origin, and edge servers. This is the AI-maintained successor to the first-generation C++ SRS server. Use for all development tasks, for example, adding features, fixing bugs, refactoring code, understanding code architecture, reviewing changes, and writing tests for the Go codebase. NOT for end-user support, usage questions, configuration help, or learning how to use SRS — use the srs-support skill for those. Only activate when the task is explicitly about developing or modifying the Go SRS codebase. +--- + +# SRS Development + +## Core Principle + +**Code and documents are the only truth.** Issue descriptions may be inaccurate. Pull requests may be misleading. Feature descriptions may be insufficient. Always ground your understanding in the actual source code and project documentation. Documents capture design intent, architecture rationale, and complex background that code alone cannot express — they are another form of code. When code and documents conflict, investigate rather than assume one is wrong. + +--- + +## Task Router + +⚠️ **MANDATORY — Always execute this step first.** Never skip the Task Router. Never jump directly to a task. Every request must be routed through this table before any work begins. + +Route the user's request to exactly ONE task type. Follow that task only. Do not combine tasks. + +| Task | When | Route To | Status | +|---|---|---|---| +| **Develop Code** | User wants to add, modify, refactor code, or update docs — any planned change | → [Develop Code](#task-develop-code) | ✅ Supported | +| **Fix a Bug** | User reports something broken, unexpected behavior, or an error | → [Fix a Bug](#task-fix-a-bug) | ❌ Not yet supported | +| **Learn Code** | User wants to understand how code works — no changes intended | → [Learn Code](#task-learn-code) | ❌ Not yet supported | +| **Review a PR** | User wants to review an existing pull request | → [Review a PR](#task-review-a-pr) | ❌ Not yet supported | + +**If the routed task is not yet supported**, stop and tell the user: +- What task type you routed to +- That this task type is not supported yet +- That support will be added in the future + +Do NOT attempt unsupported tasks. + +--- + +## Task: Fix a Bug + +**Prerequisite:** You must arrive here via the [Task Router](#task-router). Do not execute this task directly — always complete the Task Router first to confirm this is the correct task type. + +**Not yet supported.** Will be added in a future update. + +--- + +## Task: Learn Code + +**Prerequisite:** You must arrive here via the [Task Router](#task-router). Do not execute this task directly — always complete the Task Router first to confirm this is the correct task type. + +**Not yet supported.** Will be added in a future update. + +--- + +## Task: Review a PR + +**Prerequisite:** You must arrive here via the [Task Router](#task-router). Do not execute this task directly — always complete the Task Router first to confirm this is the correct task type. + +**Not yet supported.** Will be added in a future update. + +--- + +## Task: Develop Code + +**Prerequisite:** You must arrive here via the [Task Router](#task-router). Do not execute this task directly — always complete the Task Router first to confirm this is the correct task type. + +**Scope:** This task covers any planned code or documentation change — adding new features, modifying existing functionality, refactoring code, and updating documentation. + +**Important:** The C++ media server (origin + edge) is in **maintenance mode** — only bug fixes are accepted, no new features. All new feature development happens in the **next-generation Go server**. You may reference the C++ server's code to understand how things were done before, but do not add features to it. + +**Service Router** — Determine which Go service the feature targets. Route to exactly ONE service. Do not guess — if unclear, ask the user to clarify. + +| Service | Route To | Status | +|---|---|---| +| **Proxy server** | → [Proxy Server](#proxy-server) | ✅ Supported | +| **Origin server** | → [Origin Server](#origin-server) | ✅ Supported | +| **Edge server** | → [Edge Server](#edge-server) | ❌ Not yet supported | + +**If the routed service is not yet supported**, stop and tell the user: +- What service you routed to +- That this service is not supported yet + +### Proxy Server + +The proxy server is a complex, growing product — not a small app. It has many modules, and more will be added over time. You cannot load all the code into context at once. The key to working on it is **routing to the correct module first**. + +**Step 1: Module Routing (MANDATORY)** + +1. Read the codebase map: `memory/srs-codebase-map.md` — both the **Next-Generation Server Code** section (code modules: `cmd/` + `internal/`) and the **Next-Generation Server Docs** section (documentation: `docs/proxy/`). +2. Study the module descriptions and doc descriptions. Understand what each covers and its boundaries. +3. Reason about which module(s) and which doc(s) are relevant to the user's request. Consider: + - Which module owns the functionality being changed? + - Which modules might be affected as dependencies? + - Which docs cover the design/architecture of this area? + - Is this a new module or a change to an existing one? +4. **Present your reasoning to the user — both the module(s) and doc(s) you identified — and ask for confirmation.** Even if you are confident, you MUST ask. Do not proceed without confirmation. +5. If you are unsure, stop and ask the user to clarify. Do not guess. + +Only after the user confirms the routing do you proceed to Step 2. + +**Step 2: Understand the Module** + +1. **Read the confirmed docs** (if any were identified) — understand the design intent, architecture rationale, and how the module is organized internally. This is the *why*. +2. **Based on doc understanding, identify the specific file(s)** within the module that are relevant to the feature. Not the whole module — only the files that matter. +3. **Read only those specific files.** Code gives you the implementation details: function signatures, patterns, conventions, edge cases. This is the *how*. +4. If no relevant docs exist, scan the module directory listing (filenames only) to locate the right files, then read them. + +**Step 3: Implement and Verify** + +1. Implement the code change. +2. Run the proxy unit tests to verify: + ``` + bash scripts/proxy-utest.sh + ``` +3. Run the proxy E2E test (starts proxy + SRS origin, publishes RTMP, verifies playback): + ``` + bash scripts/proxy-e2e-test.sh + ``` +4. If any tests fail, fix the issues and re-run until all tests pass. + +All script paths are relative to this skill's directory. + +### Origin Server + +*(workflow steps to be defined)* + +### Edge Server + +**Not yet supported.** Will be added in a future update. diff --git a/.openclaw/skills/srs-develop/scripts/proxy-e2e-test.sh b/.openclaw/skills/srs-develop/scripts/proxy-e2e-test.sh new file mode 100755 index 000000000..17c66e4a1 --- /dev/null +++ b/.openclaw/skills/srs-develop/scripts/proxy-e2e-test.sh @@ -0,0 +1,184 @@ +#!/bin/bash +# E2E test for RTMP proxy: starts proxy + SRS origin, publishes an RTMP stream, +# verifies playback via ffprobe, then cleans up all processes. +set -e + +SCRIPT_DIR="$(cd -P "$(dirname "$0")" && pwd)" +# Navigate: scripts/ -> srs-develop/ -> skills/ -> .openclaw/ -> srs +WORKSPACE="$(cd -P "$SCRIPT_DIR/../../../.." && pwd)" + +if [[ ! -f "$WORKSPACE/go.mod" ]]; then + echo "Error: go.mod not found in WORKSPACE: $WORKSPACE" >&2 + exit 1 +fi + +# Ports — use high ports to avoid conflicts with running services. +# The proxy starts ALL servers, so we must assign unique ports for each. +PROXY_RTMP_PORT=11935 +PROXY_HTTP_API_PORT=11985 +PROXY_HTTP_SERVER_PORT=18080 +PROXY_WEBRTC_PORT=18000 +PROXY_SRT_PORT=20080 +PROXY_SYSTEM_API_PORT=12025 + +SOURCE_FLV="$WORKSPACE/trunk/doc/source.flv" +SRS_BINARY="$WORKSPACE/trunk/objs/srs" +ORIGIN_CONF="$WORKSPACE/trunk/conf/origin1-for-proxy.conf" + +# PIDs to clean up on exit. +PROXY_PID="" +ORIGIN_PID="" +FFMPEG_PID="" + +cleanup() { + echo "" + echo "=== Cleaning up ===" + for pid in $PROXY_PID $ORIGIN_PID $FFMPEG_PID; do + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null || true + fi + done + sleep 1 + for pid in $PROXY_PID $ORIGIN_PID $FFMPEG_PID; do + if kill -0 "$pid" 2>/dev/null; then + kill -9 "$pid" 2>/dev/null || true + fi + done + echo "Cleanup done." +} +trap cleanup EXIT + +echo "=== E2E RTMP Proxy Test ===" +echo "Workspace: $WORKSPACE" +echo "" + +# --- Pre-checks --- +if [[ ! -f "$SOURCE_FLV" ]]; then + echo "Error: test source not found: $SOURCE_FLV" >&2 + exit 1 +fi +if ! command -v ffmpeg &>/dev/null; then + echo "Error: ffmpeg not found in PATH" >&2 + exit 1 +fi +if ! command -v ffprobe &>/dev/null; then + echo "Error: ffprobe not found in PATH" >&2 + exit 1 +fi + +# Origin ports (from origin1-for-proxy.conf). +ORIGIN_RTMP_PORT=19351 +ORIGIN_HTTP_PORT=8081 +ORIGIN_API_PORT=19851 +ORIGIN_RTC_PORT=8001 +ORIGIN_SRT_PORT=10081 + +# --- Step 0: Clean up stale state --- +# Remove stale SRS PID file that prevents restart. +rm -f "$WORKSPACE/trunk/objs/origin1.pid" +# Kill any leftover processes on our ports (proxy + origin). +ALL_PORTS="$PROXY_RTMP_PORT $PROXY_HTTP_API_PORT $PROXY_HTTP_SERVER_PORT $PROXY_WEBRTC_PORT $PROXY_SRT_PORT $PROXY_SYSTEM_API_PORT $ORIGIN_RTMP_PORT $ORIGIN_HTTP_PORT $ORIGIN_API_PORT $ORIGIN_RTC_PORT $ORIGIN_SRT_PORT" +for port in $ALL_PORTS; do + lsof -ti :"$port" 2>/dev/null | xargs kill 2>/dev/null || true +done +sleep 1 + +# --- Step 1: Build proxy --- +echo "=== Step 1: Building proxy ===" +cd "$WORKSPACE" +make -s 2>&1 +echo "Proxy built: $WORKSPACE/srs-proxy" + +# --- Step 2: Build SRS origin (if not already built) --- +if [[ ! -f "$SRS_BINARY" ]]; then + echo "=== Step 2: Building SRS origin ===" + cd "$WORKSPACE/trunk" + ./configure && make 2>&1 | tail -3 + echo "SRS origin built: $SRS_BINARY" +else + echo "=== Step 2: SRS origin already built ===" +fi + +# --- Step 3: Start proxy --- +echo "=== Step 3: Starting proxy (RTMP :$PROXY_RTMP_PORT, System API :$PROXY_SYSTEM_API_PORT) ===" +cd "$WORKSPACE" +env PROXY_RTMP_SERVER=$PROXY_RTMP_PORT \ + PROXY_HTTP_API=$PROXY_HTTP_API_PORT \ + PROXY_HTTP_SERVER=$PROXY_HTTP_SERVER_PORT \ + PROXY_WEBRTC_SERVER=$PROXY_WEBRTC_PORT \ + PROXY_SRT_SERVER=$PROXY_SRT_PORT \ + PROXY_SYSTEM_API=$PROXY_SYSTEM_API_PORT \ + PROXY_LOAD_BALANCER_TYPE=memory \ + ./srs-proxy >/tmp/srs-proxy-e2e.log 2>&1 & +PROXY_PID=$! +echo "Proxy PID: $PROXY_PID" +sleep 1 + +if ! kill -0 "$PROXY_PID" 2>/dev/null; then + echo "Error: proxy failed to start. Logs:" >&2 + cat /tmp/srs-proxy-e2e.log >&2 + exit 1 +fi +echo "Proxy started." + +# --- Step 4: Start SRS origin --- +echo "=== Step 4: Starting SRS origin ===" +ulimit -n 10000 2>/dev/null || true +cd "$WORKSPACE/trunk" +./objs/srs -c conf/origin1-for-proxy.conf >/tmp/srs-origin-e2e.log 2>&1 & +ORIGIN_PID=$! +echo "SRS origin PID: $ORIGIN_PID" + +# Wait for SRS to start and register with proxy (heartbeat interval is 9s). +echo "Waiting for SRS origin to register with proxy (up to 15s)..." +sleep 12 + +if ! kill -0 "$ORIGIN_PID" 2>/dev/null; then + echo "Error: SRS origin failed to start. Logs:" >&2 + cat /tmp/srs-origin-e2e.log >&2 + exit 1 +fi +echo "SRS origin started and registered." + +# --- Step 5: Publish RTMP stream --- +echo "=== Step 5: Publishing RTMP stream to proxy ===" +ffmpeg -stream_loop -1 -re -i "$SOURCE_FLV" -c copy -f flv \ + "rtmp://localhost:$PROXY_RTMP_PORT/live/livestream" >/tmp/srs-ffmpeg-e2e.log 2>&1 & +FFMPEG_PID=$! +echo "FFmpeg publisher PID: $FFMPEG_PID" + +# Wait for stream to stabilize. +sleep 5 + +if ! kill -0 "$FFMPEG_PID" 2>/dev/null; then + echo "Error: FFmpeg publisher failed. Logs:" >&2 + cat /tmp/srs-ffmpeg-e2e.log >&2 + exit 1 +fi +echo "Stream publishing." + +# --- Step 6: Verify RTMP playback --- +echo "=== Step 6: Verifying RTMP playback via proxy ===" +PROBE_OUTPUT=$(ffprobe -v error -show_streams \ + "rtmp://localhost:$PROXY_RTMP_PORT/live/livestream" 2>&1 || true) + +if echo "$PROBE_OUTPUT" | grep -q "codec_type=video"; then + echo "PASS: Video stream detected." +else + echo "FAIL: No video stream detected." >&2 + echo "ffprobe output:" >&2 + echo "$PROBE_OUTPUT" >&2 + exit 1 +fi + +if echo "$PROBE_OUTPUT" | grep -q "codec_type=audio"; then + echo "PASS: Audio stream detected." +else + echo "FAIL: No audio stream detected." >&2 + echo "ffprobe output:" >&2 + echo "$PROBE_OUTPUT" >&2 + exit 1 +fi + +echo "" +echo "=== E2E RTMP Proxy Test PASSED ===" diff --git a/.openclaw/skills/srs-develop/scripts/proxy-utest.sh b/.openclaw/skills/srs-develop/scripts/proxy-utest.sh new file mode 100755 index 000000000..0327e861e --- /dev/null +++ b/.openclaw/skills/srs-develop/scripts/proxy-utest.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Run unit tests for the proxy server (cmd/ and internal/ packages). +set -e + +SCRIPT_DIR="$(cd -P "$(dirname "$0")" && pwd)" +# Navigate: scripts/ -> srs-develop/ -> skills/ -> .openclaw/ -> srs +WORKSPACE="$(cd -P "$SCRIPT_DIR/../../../.." && pwd)" + +if [[ ! -f "$WORKSPACE/go.mod" ]]; then + echo "Error: go.mod not found in WORKSPACE: $WORKSPACE" >&2 + exit 1 +fi + +cd "$WORKSPACE" +echo "Running proxy unit tests in: $WORKSPACE" + +go test ./cmd/... ./internal/... -v diff --git a/.openclaw/skills/srs-support/evals/evals.json b/.openclaw/skills/srs-support/evals/evals.json index 841ff9633..05b97c56d 100644 --- a/.openclaw/skills/srs-support/evals/evals.json +++ b/.openclaw/skills/srs-support/evals/evals.json @@ -838,6 +838,64 @@ "type": "absence" } ] + }, + { + "id": 21, + "prompt": "I'm getting this error from SRS: [ERROR][1][48960q0o][4] serve error code=3001(HlsDecode)... avc demux annexb : not annexb. My camera pushes RTMP to SRS and I play via HLS. What causes this and how do I fix it?", + "expected_output": "Should explain the error comes from H.264 AnnexB format detection in the HLS muxing path, caused by the camera encoder producing malformed frames. Should NOT blame network packet loss — RTMP uses TCP which guarantees delivery. Should suggest camera firmware update, FFmpeg relay, or camera restart.", + "files": [], + "assertions": [ + { + "name": "annexb_format_detection", + "description": "Answer explains SRS detects AnnexB format (start code 00 00 00 01 or 00 00 01) on the first H.264 frame and locks in that format for subsequent frames", + "type": "contains_concept" + }, + { + "name": "annexb_vs_ibmf_both_supported", + "description": "Answer mentions SRS supports both AnnexB and IBMF (AVCC) formats, auto-detecting from the first frame", + "type": "contains_concept" + }, + { + "name": "camera_encoder_is_root_cause", + "description": "Answer identifies the camera encoder as the root cause — it produces a frame that doesn't conform to the AnnexB format after running for some time", + "type": "contains_concept" + }, + { + "name": "hls_muxing_path", + "description": "Answer explains the error surfaces during HLS segment generation (error code HlsDecode/3001), when SRS demuxes H.264 data to remux into TS segments", + "type": "contains_concept" + }, + { + "name": "not_network_packet_loss", + "description": "Answer does NOT blame network packet loss or TCP packet loss as a cause — RTMP runs over TCP which guarantees ordered, complete delivery. If TCP fails, the connection drops entirely rather than delivering corrupted data", + "type": "absence" + }, + { + "name": "not_srs_bug", + "description": "Answer clarifies this is not an SRS bug — SRS correctly detects and reports the malformed data", + "type": "contains_concept" + }, + { + "name": "fix_camera_firmware", + "description": "Answer suggests upgrading camera firmware as the most direct fix", + "type": "contains_concept" + }, + { + "name": "fix_ffmpeg_relay", + "description": "Answer suggests using FFmpeg as a relay between camera and SRS to repackage the stream", + "type": "contains_concept" + }, + { + "name": "fix_camera_restart", + "description": "Answer suggests periodic camera restart as a workaround if firmware can't be updated", + "type": "contains_concept" + }, + { + "name": "no_hallucination", + "description": "Answer does not hallucinate SRS config options to fix this, does not claim network issues cause this error, and does not invent workarounds that don't exist", + "type": "absence" + } + ] } ] } diff --git a/.openclaw/skills/st-develop/SKILL.md b/.openclaw/skills/st-develop/SKILL.md deleted file mode 100644 index 279801665..000000000 --- a/.openclaw/skills/st-develop/SKILL.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -name: st-develop -description: Anything related to coroutines, State Threads (ST), or SRS's concurrency model. Use when discussing coroutine concepts, updating coroutine knowledge (srs-coroutines.md), developing/debugging/porting ST source code, porting ST to new CPU architectures or OSes, debugging coroutine context switching, analyzing ST scheduler behavior, adding new platform assembly, fixing ASAN/Valgrind/SEH issues, or understanding ST internals (sched, stk, sync, key, io, event, context switch ASM). ---- - -# ST Development - -State Threads (ST) is a C coroutine library. - -## Setup - -All files are in the current working directory. Find everything from here — no discovery logic needed. - -Available directories: `trunk/`, `cmd/`, `internal/`, `cmake/`, `docs/`, `memory/` - -All AI tools — OpenClaw, Codex, Claude Code, Kiro CLI — see the same relative paths. - -## Load Knowledge Base (MANDATORY) - -Before any ST work, use the `read` tool to load the knowledge base. Do NOT use memory_search — read the full file directly. - -- `memory/srs-coroutines.md` -- `memory/srs-codebase-map.md` — Load this when you need to find or navigate source files. Always use the codebase map to reason about which files are relevant before searching. Never grep from the repository root blindly. - -## Loading ST Source Code (ON REQUEST) - -When the user asks to load the ST codebase (or needs you to work directly with the source), load **ALL** ST source files — no partial loads. - -All under `trunk/3rdparty/st-srs/`: - -Headers: `public.h`, `common.h`, `md.h` - -Core C: `sched.c`, `stk.c`, `sync.c`, `key.c`, `io.c`, `event.c`, `common.c` - -Platform ASM: `md_darwin.S`, `md_linux.S`, `md_linux2.S`, `md_cygwin64.S` - -Build: `Makefile` - -**Load every single file listed above — no shortcuts, no skipping.** - -## Unit Tests (utest) - -ST has a Google Test-based unit test suite in `trunk/3rdparty/st-srs/utest/`: - -- `st_utest.cpp` / `st_utest.hpp` — Test main and shared helpers -- `st_utest_coroutines.cpp` — Coroutine tests (start, params, multiple coroutines, addition across yields) -- `st_utest_tcp.cpp` — TCP connection test -- `gtest-fit/` — Embedded Google Test framework - -**Build targets** (in the ST Makefile): -- `darwin-debug-utest` — macOS debug build + utest -- `linux-debug-utest` — Linux debug build + utest -- `cygwin64-debug-utest` — Cygwin64 debug build + utest - -Coverage variants: `darwin-debug-gcov`, `linux-debug-gcov` (adds `-fprofile-arcs -ftest-coverage`). - -The build compiles ST as a static library first, then builds and links the utest binary at `obj/st_utest`. - -## Verifying Changes - -After any ST change (including utest-only changes), run the verifier script in this skill folder (not in the ST codebase): - -- `scripts/verify.sh` - -This script runs unit tests in `trunk/3rdparty/st-srs`. -Always run verification before considering a change complete. diff --git a/.openclaw/skills/st-develop/scripts/verify.sh b/.openclaw/skills/st-develop/scripts/verify.sh deleted file mode 100755 index 7187c3ce1..000000000 --- a/.openclaw/skills/st-develop/scripts/verify.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Verify ST changes by building and running unit tests. -set -e - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -# Navigate: scripts/ -> st-develop/ -> skills/ -> .openclaw/ -WORKSPACE="$(cd "$SCRIPT_DIR/../../.." && pwd)" -ST_DIR="$WORKSPACE/trunk/3rdparty/st-srs" - -if [[ ! -d "$ST_DIR" ]]; then - echo "Error: ST_DIR does not exist: $ST_DIR" >&2 - exit 1 -fi - -echo "ST source: $ST_DIR" - -CMAKE_DIR="$WORKSPACE/cmake" -BUILD_DIR="$WORKSPACE/cmake/build" - -mkdir -p "$BUILD_DIR" -cd "$BUILD_DIR" - -cmake .. -DCMAKE_BUILD_TYPE=Debug -cmake --build . --target st_utest - -./st-build/st_utest diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index f15ee141c..3f9a5454b 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -11,8 +11,9 @@ import ( ) func main() { - bs := bootstrap.NewBootstrap() + bs := bootstrap.NewProxyBootstrap() if err := bs.Start(context.Background()); err != nil { + // Error already logged in bootstrap.Start(). os.Exit(-1) } } diff --git a/cmd/proxy/main_test.go b/cmd/proxy/main_test.go new file mode 100644 index 000000000..a50330444 --- /dev/null +++ b/cmd/proxy/main_test.go @@ -0,0 +1,9 @@ +// Copyright (c) 2025 Winlin +// +// SPDX-License-Identifier: MIT +package main + +import "testing" + +func TestExample(t *testing.T) { +} diff --git a/docs/proxy/proxy-usage.md b/docs/proxy/proxy-usage.md index b03a5604b..5eff6de90 100644 --- a/docs/proxy/proxy-usage.md +++ b/docs/proxy/proxy-usage.md @@ -65,13 +65,13 @@ Both commands should successfully detect the stream and display video/audio code ## Code Conventions ## Factory Functions -- Factory functions should use explicit interface names: `NewBootstrap()`, `NewMemoryLoadBalancer()`, etc. +- Factory functions should use explicit interface names: `NewProxyBootstrap()`, `NewMemoryLoadBalancer()`, etc. - **Do not** use generic `New()` function names - This improves code clarity and makes the constructed type explicit at the call site - Example: ```go // Good - bs := bootstrap.NewBootstrap() + bs := bootstrap.NewProxyBootstrap() // Avoid bs := bootstrap.New() diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index a6b716e9c..46cee230f 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -5,16 +5,6 @@ package bootstrap import ( "context" - "time" - - "srsx/internal/debug" - "srsx/internal/env" - "srsx/internal/errors" - "srsx/internal/lb" - "srsx/internal/logger" - "srsx/internal/protocol" - "srsx/internal/signal" - "srsx/internal/version" ) // Bootstrap defines the interface for application bootstrap operations. @@ -22,137 +12,4 @@ type Bootstrap interface { // Start initializes the context with logger and signal handlers, then runs the bootstrap. // Returns any error encountered during startup. Start(ctx context.Context) error - - // Run initializes and starts all proxy servers and the load balancer. - // It blocks until the context is cancelled. - Run(ctx context.Context) error -} - -// bootstrapImpl implements the Bootstrap interface. -type bootstrapImpl struct{} - -// NewBootstrap creates a new Bootstrap instance. -func NewBootstrap() Bootstrap { - return &bootstrapImpl{} -} - -// Start initializes the context with logger and signal handlers, then runs the bootstrap. -// Returns any error encountered during startup. -func (b *bootstrapImpl) Start(ctx context.Context) error { - ctx = logger.WithContext(ctx) - logger.Df(ctx, "%v/%v started", version.Signature(), version.Version()) - - // Install signals. - ctx, cancel := context.WithCancel(ctx) - signal.InstallSignals(ctx, cancel) - - // Run the main loop, ignore the user cancel error. - err := b.Run(ctx) - if err != nil && ctx.Err() != context.Canceled { - logger.Ef(ctx, "main: %+v", err) - return err - } - - logger.Df(ctx, "%v done", version.Signature()) - return nil -} - -// Run initializes and starts all proxy servers and the load balancer. -// It blocks until the context is cancelled. -func (b *bootstrapImpl) Run(ctx context.Context) error { - // Setup the environment variables. - environment, err := env.NewEnvironment(ctx) - if err != nil { - return errors.Wrapf(err, "create environment") - } - - // When cancelled, the program is forced to exit due to a timeout. Normally, this doesn't occur - // because the main thread exits after the context is cancelled. However, sometimes the main thread - // may be blocked for some reason, so a forced exit is necessary to ensure the program terminates. - if err := signal.InstallForceQuit(ctx, environment); err != nil { - return errors.Wrapf(err, "install force quit") - } - - // Start the Go pprof if enabled. - debug.HandleGoPprof(ctx, environment) - - // Initialize the load balancer. - if err := b.initializeLoadBalancer(ctx, environment); err != nil { - return err - } - - // Parse the gracefully quit timeout. - gracefulQuitTimeout, err := time.ParseDuration(environment.GraceQuitTimeout()) - if err != nil { - return errors.Wrapf(err, "parse gracefully quit timeout") - } - - // Start all servers and block until context is cancelled. - return b.startServers(ctx, environment, gracefulQuitTimeout) -} - -// initializeLoadBalancer sets up the load balancer based on configuration. -func (b *bootstrapImpl) initializeLoadBalancer(ctx context.Context, environment env.Environment) error { - switch environment.LoadBalancerType() { - case "redis": - lb.SrsLoadBalancer = lb.NewRedisLoadBalancer(environment) - default: - lb.SrsLoadBalancer = lb.NewMemoryLoadBalancer(environment) - } - - if err := lb.SrsLoadBalancer.Initialize(ctx); err != nil { - return errors.Wrapf(err, "initialize srs load balancer") - } - - return nil -} - -// startServers initializes and starts all protocol servers. -func (b *bootstrapImpl) startServers(ctx context.Context, environment env.Environment, gracefulQuitTimeout time.Duration) error { - // Start the RTMP server. - srsRTMPServer := protocol.NewSRSRTMPServer(environment) - if err := srsRTMPServer.Run(ctx); err != nil { - return errors.Wrapf(err, "rtmp server") - } - defer srsRTMPServer.Close() - - // Start the WebRTC server. - srsWebRTCServer := protocol.NewSRSWebRTCServer(environment) - if err := srsWebRTCServer.Run(ctx); err != nil { - return errors.Wrapf(err, "rtc server") - } - defer srsWebRTCServer.Close() - - // Start the HTTP API server. - srsHTTPAPIServer := protocol.NewSRSHTTPAPIServer(environment, gracefulQuitTimeout, srsWebRTCServer) - if err := srsHTTPAPIServer.Run(ctx); err != nil { - return errors.Wrapf(err, "http api server") - } - defer srsHTTPAPIServer.Close() - - // Start the SRT server. - srsSRTServer := protocol.NewSRSSRTServer(environment) - if err := srsSRTServer.Run(ctx); err != nil { - return errors.Wrapf(err, "srt server") - } - defer srsSRTServer.Close() - - // Start the System API server. - systemAPI := protocol.NewSystemAPI(environment, gracefulQuitTimeout) - if err := systemAPI.Run(ctx); err != nil { - return errors.Wrapf(err, "system api server") - } - defer systemAPI.Close() - - // Start the HTTP web server. - srsHTTPStreamServer := protocol.NewSRSHTTPStreamServer(environment, gracefulQuitTimeout) - if err := srsHTTPStreamServer.Run(ctx); err != nil { - return errors.Wrapf(err, "http server") - } - defer srsHTTPStreamServer.Close() - - // Wait for the main loop to quit. - <-ctx.Done() - - return nil } diff --git a/internal/bootstrap/proxy.go b/internal/bootstrap/proxy.go new file mode 100644 index 000000000..d39d372ce --- /dev/null +++ b/internal/bootstrap/proxy.go @@ -0,0 +1,147 @@ +// Copyright (c) 2025 Winlin +// +// SPDX-License-Identifier: MIT +package bootstrap + +import ( + "context" + "time" + + "srsx/internal/debug" + "srsx/internal/env" + "srsx/internal/errors" + "srsx/internal/lb" + "srsx/internal/logger" + "srsx/internal/protocol" + "srsx/internal/signal" + "srsx/internal/version" +) + +// NewProxyBootstrap creates a new Bootstrap instance for the proxy server. +func NewProxyBootstrap() Bootstrap { + return &proxyBootstrap{} +} + +// proxyBootstrap implements the Bootstrap interface for the proxy server. +type proxyBootstrap struct{} + +// Start initializes the context with logger and signal handlers, then runs the bootstrap. +// Returns any error encountered during startup. +func (b *proxyBootstrap) Start(ctx context.Context) error { + ctx = logger.WithContext(ctx) + logger.Df(ctx, "%v-Proxy/%v started", version.Signature(), version.Version()) + + // Install signals. + ctx, cancel := context.WithCancel(ctx) + signal.InstallSignals(ctx, cancel) + + // Run the main loop, ignore the user cancel error. + err := b.run(ctx) + if err != nil && ctx.Err() != context.Canceled { + logger.Ef(ctx, "main: %+v", err) + return err + } + + logger.Df(ctx, "%v done", version.Signature()) + return nil +} + +// Run initializes and starts all proxy servers and the load balancer. +// It blocks until the context is cancelled. +func (b *proxyBootstrap) run(ctx context.Context) error { + // Setup the environment variables. + environment, err := env.NewEnvironment(ctx) + if err != nil { + return errors.Wrapf(err, "create environment") + } + + // When cancelled, the program is forced to exit due to a timeout. Normally, this doesn't occur + // because the main thread exits after the context is cancelled. However, sometimes the main thread + // may be blocked for some reason, so a forced exit is necessary to ensure the program terminates. + if err := signal.InstallForceQuit(ctx, environment); err != nil { + return errors.Wrapf(err, "install force quit") + } + + // Start the Go pprof if enabled. + debug.HandleGoPprof(ctx, environment) + + // Initialize the load balancer. + if err := b.initializeLoadBalancer(ctx, environment); err != nil { + return err + } + + // Parse the gracefully quit timeout. + gracefulQuitTimeout, err := time.ParseDuration(environment.GraceQuitTimeout()) + if err != nil { + return errors.Wrapf(err, "parse gracefully quit timeout") + } + + // Start all servers and block until context is cancelled. + return b.startServers(ctx, environment, gracefulQuitTimeout) +} + +// initializeLoadBalancer sets up the load balancer based on configuration. +func (b *proxyBootstrap) initializeLoadBalancer(ctx context.Context, environment env.Environment) error { + switch environment.LoadBalancerType() { + case "redis": + lb.SrsLoadBalancer = lb.NewRedisLoadBalancer(environment) + default: + lb.SrsLoadBalancer = lb.NewMemoryLoadBalancer(environment) + } + + if err := lb.SrsLoadBalancer.Initialize(ctx); err != nil { + return errors.Wrapf(err, "initialize srs load balancer") + } + + return nil +} + +// startServers initializes and starts all protocol servers. +func (b *proxyBootstrap) startServers(ctx context.Context, environment env.Environment, gracefulQuitTimeout time.Duration) error { + // Start the RTMP server. + srsRTMPServer := protocol.NewSRSRTMPServer(environment) + if err := srsRTMPServer.Run(ctx); err != nil { + return errors.Wrapf(err, "rtmp server") + } + defer srsRTMPServer.Close() + + // Start the WebRTC server. + srsWebRTCServer := protocol.NewSRSWebRTCServer(environment) + if err := srsWebRTCServer.Run(ctx); err != nil { + return errors.Wrapf(err, "rtc server") + } + defer srsWebRTCServer.Close() + + // Start the HTTP API server. + srsHTTPAPIServer := protocol.NewSRSHTTPAPIServer(environment, gracefulQuitTimeout, srsWebRTCServer) + if err := srsHTTPAPIServer.Run(ctx); err != nil { + return errors.Wrapf(err, "http api server") + } + defer srsHTTPAPIServer.Close() + + // Start the SRT server. + srsSRTServer := protocol.NewSRSSRTServer(environment) + if err := srsSRTServer.Run(ctx); err != nil { + return errors.Wrapf(err, "srt server") + } + defer srsSRTServer.Close() + + // Start the System API server. + systemAPI := protocol.NewSystemAPI(environment, gracefulQuitTimeout) + if err := systemAPI.Run(ctx); err != nil { + return errors.Wrapf(err, "system api server") + } + defer systemAPI.Close() + + // Start the HTTP web server. + srsHTTPStreamServer := protocol.NewSRSHTTPStreamServer(environment, gracefulQuitTimeout) + if err := srsHTTPStreamServer.Run(ctx); err != nil { + return errors.Wrapf(err, "http server") + } + defer srsHTTPStreamServer.Close() + + // Wait for the main loop to quit. + <-ctx.Done() + + return nil +} diff --git a/internal/logger/context.go b/internal/logger/context.go index fb15b767e..bafc58605 100644 --- a/internal/logger/context.go +++ b/internal/logger/context.go @@ -12,7 +12,7 @@ import ( type key string -var cidKey key = "cid.proxy.ossrs.org" +var cidKey key = "cid.srsx.ossrs.org" // generateContextID generates a random context id in string. func GenerateContextID() string { diff --git a/internal/version/version.go b/internal/version/version.go index 14479a2ab..5c739a589 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -23,5 +23,5 @@ func Version() string { } func Signature() string { - return "SRSProxy" + return "SRSX" } diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md index 57a2266f0..2600578e2 100644 --- a/trunk/doc/CHANGELOG.md +++ b/trunk/doc/CHANGELOG.md @@ -7,6 +7,7 @@ The changelog for SRS. ## SRS 7.0 Changelog +* v7.0, 2026-04-06, Merge [#4657](https://github.com/ossrs/srs/pull/4657): Proxy: Refactor bootstrap for multi-server support and rebrand to SRSX. v7.0.142 (#4657) * v7.0, 2026-03-26, Merge [#4654](https://github.com/ossrs/srs/pull/4654): OpenClaw: Restructure workspace with symlinks, add codebase map, and rewrite AI docs. v7.0.141 (#4654) * v7.0, 2026-03-22, Merge [#4653](https://github.com/ossrs/srs/pull/4653): OpenClaw: unify AI agent configs with shared persona symlinks. v7.0.140 (#4653) * v7.0, 2026-03-22, Merge [#4652](https://github.com/ossrs/srs/pull/4652): Proxy: restructure repo as Go project with proxy as first module. v7.0.139 (#4652) diff --git a/trunk/src/core/srs_core_version7.hpp b/trunk/src/core/srs_core_version7.hpp index 442e25ba1..8cbc293f7 100644 --- a/trunk/src/core/srs_core_version7.hpp +++ b/trunk/src/core/srs_core_version7.hpp @@ -9,6 +9,6 @@ #define VERSION_MAJOR 7 #define VERSION_MINOR 0 -#define VERSION_REVISION 141 +#define VERSION_REVISION 142 #endif