Compare commits

..

2 Commits

Author SHA1 Message Date
winlin
133f66afba Edge: Fix HTTP-FLV 404 and RTMP late-join missing sequence headers. v7.0.150 (#4678)
Cherry-pick of the v8.0.2 fix to the 7.0 release line. Two edge-cluster
regressions surface when validating an RTMP origin/edge setup:

- **HTTP-FLV play on edge always 404'd.**
`SrsHttpStreamServer::assemble()` registered the dynamic matcher only
when the mux cast was `NULL` (inverted guard), so the matcher was never
wired up. On edge the FLV mount is created lazily by the dynamic
matcher, so every HTTP-FLV client got 404. Invert the guard to register
when the mux is valid, mirroring the destructor.
(`trunk/src/app/srs_app_http_stream.cpp`)

- **RTMP players that join an edge stream after the first player fail to
decode.** After v7.0.94 (#4513) stopped creating `SrsOriginHub` on edge,
the `hub_active` gate in `SrsLiveSource::consumer_dumps()` always
evaluated false on edge. That gate guards the dump of cached
`onMetaData` + AVC sequence header + AAC sequence header + GOP cache to
a new consumer. Result: the first player attaches before the edge-pull
starts and gets headers via the live fan-out, but every subsequent
player gets coded payload with no codec config and ffmpeg aborts with
`dimensions not set` / `Could not write header`. Fall back to the meta
cache state when `hub_` is `NULL`, so the dump path runs once the
edge-pull has populated the cache.
(`trunk/src/app/srs_app_rtmp_source.cpp`)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:20:25 -04:00
Winlin
386a3768df Proxy: Fix RTC/SRT reader leak, legacy WHEP unwrap, WHEP perf guide. v7.0.149 (#4676)
- Fix a goroutine leak on the WHEP path: the backend→client reader was
being spawned on every inbound client packet (STUN keepalives + RTCP
feedback), leaking tens of thousands of goroutines under steady-state
load. Now spawned exactly once per connection via `sync.Once` on both
the RTC and SRT proxies. Listener and reader receive buffers are also
reused across iterations.
- Make the legacy SRS `/rtc/v1/play/` and `/rtc/v1/publish/` APIs work
end-to-end through the proxy. Those endpoints wrap the SDP in a JSON
envelope (`{"sdp":"v=0\r\n..."}` where `\r\n` is the literal 2-byte JSON
escape, not real CRLF), so ICE parsing previously absorbed the rest of
the body into the ufrag. Added `unwrapSDPEnvelope` for ICE extraction
and tightened `ParseIceUfragPwd`'s value class to stop at `\`. The bytes
forwarded to the client and the in-body candidate-port rewrite still
operate on the raw envelope.
- Enable `net/http/pprof` endpoints when `GO_PPROF` is set (blank import
in `internal/debug/pprof.go`) and add `docs/perf/proxy-whep.md` walking
through CPU/alloc/heap/goroutine/trace collection and `pprof -base`
before/after diffs for the WHEP workload (1 publisher + N players).
- Tighten `SRTHandshakePacket.UnmarshalBinary` to
`bytes.Clone(ExtraData)` so decoded handshakes kept on the connection
(`handshake0`, `handshake2`) stay valid once the receive buffer is
reused.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:18:35 -04:00
13 changed files with 65 additions and 1456 deletions

View File

@ -10,7 +10,7 @@
[![](https://img.shields.io/docker/pulls/ossrs/srs)](https://hub.docker.com/r/ossrs/srs/tags)
[![](https://codecov.io/gh/ossrs/srs/graph/badge.svg?token=Zx2LhdtA39)](https://codecov.io/gh/ossrs/srs)
SRS/8.0 ([Free](https://ossrs.io/lts/en-us/product#release-80)) is a simple, high-efficiency, and real-time video server,
SRS/7.0 ([Kai](https://ossrs.io/lts/en-us/product#release-70)) is a simple, high-efficiency, and real-time video server,
supporting RTMP/WebRTC/HLS/HTTP-FLV/SRT/MPEG-DASH/GB28181, Linux/macOS, X86_64/ARMv7/AARCH64/M1/RISCV/LOONGARCH/MIPS,
with codec support for H.264, H.265, AV1, VP9, AAC, Opus, and G.711,
and essential [features](trunk/doc/Features.md#features).

View File

@ -136,21 +136,12 @@ func newSettings() *settings {
// The chunk stream which transport a message once.
type chunkStream struct {
format formatType
cid chunkID
header messageHeader
message *message
count uint64
// Whether the chunk carries an extended timestamp, set when the (delta) timestamp in
// the message header equals 0xffffff. Type-3 continuation chunks inherit this from the
// preceding Type-0/1/2 chunk.
hasExtendedTimestamp bool
// The raw value last read from the extended timestamp field. Kept separately from
// header.Timestamp (the accumulated message timestamp) so we can both detect Type-3
// chunks that omit the extended timestamp and use it as a delta for fmt=1/2 chunks.
// See readMessageHeader.
extendedTimestamp uint32
format formatType
cid chunkID
header messageHeader
message *message
count uint64
extendedTimestamp bool
}
func newChunkStream() *chunkStream {
@ -549,7 +540,29 @@ func (v *protocol) readMessageHeader(ctx context.Context, chunk *chunkStream, fo
// 0x00ffffff), this value MUST be 16777215, and the 'extended
// timestamp header' MUST be present. Otherwise, this value SHOULD be
// the entire delta.
chunk.hasExtendedTimestamp = uint64(chunk.header.timestampDelta) >= extendedTimestamp
chunk.extendedTimestamp = uint64(chunk.header.timestampDelta) >= extendedTimestamp
if !chunk.extendedTimestamp {
// Extended timestamp: 0 or 4 bytes
// This field MUST be sent when the normal timsestamp is set to
// 0xffffff, it MUST NOT be sent if the normal timestamp is set to
// anything else. So for values less than 0xffffff the normal
// timestamp field SHOULD be used in which case the extended timestamp
// MUST NOT be present. For values greater than or equal to 0xffffff
// the normal timestamp field MUST NOT be used and MUST be set to
// 0xffffff and the extended timestamp MUST be sent.
if format == formatType0 {
// 6.1.2.1. Type 0
// For a type-0 chunk, the absolute timestamp of the message is sent
// here.
chunk.header.Timestamp = uint64(chunk.header.timestampDelta)
} else {
// 6.1.2.2. Type 1
// 6.1.2.3. Type 2
// For a type-1 or type-2 chunk, the difference between the previous
// chunk's timestamp and the current chunk's timestamp is sent here.
chunk.header.Timestamp += uint64(chunk.header.timestampDelta)
}
}
if format <= formatType1 {
payloadLength := uint32(p[0])<<16 | uint32(p[1])<<8 | uint32(p[2])
@ -572,58 +585,27 @@ func (v *protocol) readMessageHeader(ctx context.Context, chunk *chunkStream, fo
p = p[4:]
}
}
} else {
// Update the timestamp even fmt=3 for first chunk packet
if isFirstChunkOfMsg && !chunk.extendedTimestamp {
chunk.header.Timestamp += uint64(chunk.header.timestampDelta)
}
}
// Read extended-timestamp, present when the (delta) timestamp in the message header is
// 0xffffff. Type-3 chunks inherit hasExtendedTimestamp from the preceding chunk.
if chunk.hasExtendedTimestamp {
// Peek instead of read, so the 4 bytes can be left in place when a sender omits the
// extended timestamp on a Type-3 chunk (see the detection below).
var b []byte
if b, err = v.r.Peek(4); err != nil {
// Read extended-timestamp
if chunk.extendedTimestamp {
var timestamp uint32
if err = binary.Read(v.r, binary.BigEndian, &timestamp); err != nil {
return errors.Wrapf(err, "read ext-ts, pkt-ts=%v", chunk.header.Timestamp)
}
// We always use 31bits timestamp, for some server may use 32bits extended timestamp.
// @see https://github.com/ossrs/srs/issues/111
timestamp := binary.BigEndian.Uint32(b) & 0x7fffffff
timestamp &= 0x7fffffff
// For the RTMP v1 2009 version (6.1.3. Extended Timestamp), Type 3 chunks MUST NOT
// have this field. For the RTMP v1 2012 version (5.3.1.3. Extended Timestamp), it is
// present in Type 3 chunks when the most recent Type 0/1/2 chunk indicated one.
//
// FMLE/FMS/Flash Player follow the 2012 version and always send the extended
// timestamp in Type 3 chunks; librtmp/ffmpeg may not. So detect it: if this is not
// the first chunk of the message and the peeked value differs from the previously
// stored extended timestamp, the sender omitted it and these 4 bytes are payload, so
// leave them in the reader. Otherwise consume and store them.
// TODO: FIXME: Support detect the extended timestamp.
// @see http://blog.csdn.net/win_lin/article/details/13363699
// @see https://github.com/veovera/enhanced-rtmp/issues/42
if !isFirstChunkOfMsg && chunk.extendedTimestamp > 0 && chunk.extendedTimestamp != timestamp {
// No extended timestamp on this Type-3 chunk; the 4 bytes belong to the payload.
} else {
if _, err = v.r.Discard(4); err != nil {
return errors.Wrapf(err, "discard ext-ts, pkt-ts=%v", chunk.header.Timestamp)
}
chunk.extendedTimestamp = timestamp
}
}
// Compute the message timestamp. The source is the extended timestamp when present,
// otherwise the 3-byte (delta) timestamp from the message header.
//
// fmt=0: the value is the absolute timestamp of the message.
// fmt=1/2 (and a fmt=3 first chunk continuing them): the value is a delta and is
// accumulated onto the previous timestamp. This is required when the delta is >= 0xffffff
// and is therefore carried in the extended timestamp.
timestamp := chunk.header.timestampDelta
if chunk.hasExtendedTimestamp {
timestamp = chunk.extendedTimestamp
}
if format == formatType0 {
chunk.header.Timestamp = uint64(timestamp)
} else if isFirstChunkOfMsg {
chunk.header.Timestamp += uint64(timestamp)
}
// The extended-timestamp must be unsigned-int,
@ -714,11 +696,6 @@ func (v *protocol) readBasicHeader(ctx context.Context) (format formatType, cid
return
}
// Here cid is 0 or 1: a marker selecting the 2B or 3B form, not the real cid. Keep it,
// because cid is overwritten below and the marker decides whether a third byte (the
// high-order part of the cid) follows. Do not test the overwritten cid for this.
marker := cid
// 64-319, 2B chunk header
if err = binary.Read(v.r, binary.BigEndian, &t); err != nil {
return format, cid, errors.Wrapf(err, "read basic header for cid=%v", cid)
@ -726,7 +703,7 @@ func (v *protocol) readBasicHeader(ctx context.Context) (format formatType, cid
cid = chunkID(64 + uint32(t))
// 64-65599, 3B chunk header
if marker == 1 {
if cid == 1 {
if err = binary.Read(v.r, binary.BigEndian, &t); err != nil {
return format, cid, errors.Wrapf(err, "read basic header for cid=%v", cid)
}
@ -1306,12 +1283,6 @@ func (v *variantCallPacket) UnmarshalBinary(data []byte) (err error) {
}
p = p[v.TransactionID.Size():]
// Reset the optional command object before deciding whether it is present.
// A New*Packet constructor may have pre-set it to a default (e.g. Null), but
// when the wire data is exhausted here the object is absent. Leaving the stale
// default would make Size() count bytes that were never parsed, overflowing the
// caller's p = p[Size():] advance on truncated, untrusted input.
v.CommandObject = nil
if len(p) > 0 {
if v.CommandObject, err = Amf0Discovery(p); err != nil {
return errors.WithMessage(err, "discovery command object")
@ -1382,32 +1353,14 @@ func (v *CallPacket) Size() int {
return size
}
// advanceBytes returns p[n:] after verifying n lies within p. Packet
// UnmarshalBinary advances its cursor by each embedded field's decoded Size();
// on untrusted wire input a malformed length can make Size() exceed the bytes
// actually present, so this guard turns a slice-out-of-range panic into a clean
// error. See the RTMP test plan, P8 (adversarial resource-safety).
func advanceBytes(p []byte, n int) ([]byte, error) {
if n < 0 || n > len(p) {
return nil, errors.Errorf("advance %v exceeds remaining %v bytes", n, len(p))
}
return p[n:], nil
}
func (v *CallPacket) UnmarshalBinary(data []byte) (err error) {
p := data
if err = v.variantCallPacket.UnmarshalBinary(p); err != nil {
return errors.WithMessage(err, "unmarshal call")
}
if p, err = advanceBytes(p, v.variantCallPacket.Size()); err != nil {
return errors.WithMessage(err, "advance call")
}
p = p[v.variantCallPacket.Size():]
// Reset the optional args before deciding whether they are present, for the
// same reason as variantCallPacket.CommandObject: a stale constructor default
// would be counted by Size() and overflow a later advance.
v.Args = nil
if len(p) > 0 {
if v.Args, err = Amf0Discovery(p); err != nil {
return errors.WithMessage(err, "discovery args")
@ -1483,9 +1436,7 @@ func (v *CreateStreamResPacket) UnmarshalBinary(data []byte) (err error) {
if err = v.variantCallPacket.UnmarshalBinary(p); err != nil {
return errors.WithMessage(err, "unmarshal call")
}
if p, err = advanceBytes(p, v.variantCallPacket.Size()); err != nil {
return errors.WithMessage(err, "advance call")
}
p = p[v.variantCallPacket.Size():]
if err = v.StreamID.UnmarshalBinary(p); err != nil {
return errors.WithMessage(err, "unmarshal sid")
@ -1535,9 +1486,7 @@ func (v *PublishPacket) UnmarshalBinary(data []byte) (err error) {
if err = v.variantCallPacket.UnmarshalBinary(p); err != nil {
return errors.WithMessage(err, "unmarshal call")
}
if p, err = advanceBytes(p, v.variantCallPacket.Size()); err != nil {
return errors.WithMessage(err, "advance call")
}
p = p[v.variantCallPacket.Size():]
v.StreamName = newAmf0String("")
if err = v.StreamName.UnmarshalBinary(p); err != nil {
@ -1597,9 +1546,7 @@ func (v *PlayPacket) UnmarshalBinary(data []byte) (err error) {
if err = v.variantCallPacket.UnmarshalBinary(p); err != nil {
return errors.WithMessage(err, "unmarshal call")
}
if p, err = advanceBytes(p, v.variantCallPacket.Size()); err != nil {
return errors.WithMessage(err, "advance call")
}
p = p[v.variantCallPacket.Size():]
v.StreamName = newAmf0String("")
if err = v.StreamName.UnmarshalBinary(p); err != nil {

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ package version
import "fmt"
func VersionMajor() int {
return 8
return 7
}
// VersionMinor specifies the typical version of SRS we adapt to.
@ -15,7 +15,7 @@ func VersionMinor() int {
}
func VersionRevision() int {
return 3
return 150
}
func Version() string {

View File

@ -9,14 +9,14 @@ import (
)
func TestVersionComponents(t *testing.T) {
if got := VersionMajor(); got != 8 {
t.Fatalf("VersionMajor = %d, want 8", got)
if got := VersionMajor(); got != 7 {
t.Fatalf("VersionMajor = %d, want 7", got)
}
if got := VersionMinor(); got != 0 {
t.Fatalf("VersionMinor = %d, want 0", got)
}
if got := VersionRevision(); got < 0 {
t.Fatalf("VersionRevision = %d, want >= 0", got)
if got := VersionRevision(); got <= 0 {
t.Fatalf("VersionRevision = %d, want > 0", got)
}
}

View File

@ -17,7 +17,7 @@ The C++ media server (`trunk/src/`) serves as both origin server and edge server
`core/` — Foundational definitions:
- `core` — Core includes, macros, config generated by configure
- `core_version` through `core_version8` — Version definitions per major release
- `core_version` through `core_version7` — Version definitions per major release
- `core_autofree` — SrsUniquePtr smart pointer (RAII)
- `core_deprecated` — Deprecated SrsAutoFree, kept for compat
- `core_performance` — Performance tuning constants (merge-read, etc.)
@ -227,7 +227,7 @@ The next-generation server (`cmd/` + `internal/`) is written in Go and maintaine
`internal/env` — Environment-based configuration. All settings via env vars (or `.env` file parsed by an in-tree custom parser — no third-party dep; supports comments, `export` prefix, quoted values, escape sequences, and inline comments). Exposes a `ProxyEnvironment` interface (with a counterfeiter-generated fake in `envfakes/` for downstream tests) with methods for each config value. Default ports: RTMP=11935, HTTP API=11985, HTTP Stream=18080, WebRTC=18000, SRT=20080, System API=12025. Timeouts: grace=20s, force=30s. Supports Redis config and default backend config for debugging.
`internal/version` — Version constants. Signature `SRSX`, version tracks the SRS project version (currently 8.0.x). Used in HTTP API responses and startup logging.
`internal/version` — Version constants. Signature `SRSX`, version tracks the SRS project version (currently 7.0.x). Used in HTTP API responses and startup logging.
`internal/errors` — Error handling with stack traces, thin wrapper over stdlib `errors`. Provides `New`, `Errorf`, `Wrap`, `Wrapf`, `WithMessage`, `WithStack`, `Cause`, and re-exports `Is`/`As`/`Unwrap`/`Join`. Every error captures a stack trace at creation; `%+v` prints the full trace. `Cause()` walks the error chain to find the root error.

View File

@ -77,13 +77,13 @@ Do NOT attempt unsupported tasks.
1. Ask the user for the PR number if they haven't given it.
2. Bump revision by one in **both** version files, keeping them in sync:
- `internal/version/version.go``VersionRevision()`
- `trunk/src/core/srs_core_version8.hpp` — `VERSION_REVISION`
3. Add a new top entry to `trunk/doc/CHANGELOG.md` under `## SRS 8.0 Changelog`, matching the existing format:
- `trunk/src/core/srs_core_version7.hpp` — `VERSION_REVISION`
3. Add a new top entry to `trunk/doc/CHANGELOG.md` under `## SRS 7.0 Changelog`, matching the existing format:
```
* v8.0, YYYY-MM-DD, Merge [#PR](URL): <Prefix>: <one-line summary>. v8.0.<rev> (#PR)
* v7.0, YYYY-MM-DD, Merge [#PR](URL): <Prefix>: <one-line summary>. v7.0.<rev> (#PR)
```
Propose the summary to the user; don't invent one unilaterally.
4. Stop. Let the user review. When they `git add` the version files and changelog, commit with a short message like `Proxy: Bump to v8.0.<rev> for #<PR>.`.
4. Stop. Let the user review. When they `git add` the version files and changelog, commit with a short message like `Proxy: Bump to v7.0.<rev> for #<PR>.`.
---
@ -143,7 +143,7 @@ Only after the user confirms the routing do you proceed to Step 2.
```
bash scripts/proxy-utest.sh --coverage
```
4. Run **all** of the proxy E2E tests below — every one, not just the first. Run them one at a time (they bind fixed ports, so they cannot run in parallel), and do not stop early: a later test can fail even when the earlier ones pass.
4. Run the proxy E2E tests:
- Single-origin RTMP proxy test (starts proxy + one SRS origin, publishes RTMP, verifies playback):
```
bash scripts/proxy-e2e-test.sh

2
trunk/configure vendored
View File

@ -237,7 +237,7 @@ fi
MODULE_ID="CORE"
MODULE_DEPENDS=()
ModuleLibIncs=(${SRS_OBJS})
MODULE_FILES=("srs_core" "srs_core_version" "srs_core_version8" "srs_core_autofree"
MODULE_FILES=("srs_core" "srs_core_version" "srs_core_version7" "srs_core_autofree"
"srs_core_time" "srs_core_platform" "srs_core_deprecated" "srs_core_performance")
CORE_INCS="src/core"; MODULE_DIR=${CORE_INCS} . $SRS_WORKDIR/auto/modules.sh
CORE_OBJS="${MODULE_OBJS[@]}"

View File

@ -4,17 +4,11 @@
The changelog for SRS.
<a name="v8-changes"></a>
## SRS 8.0 Changelog
* v8.0, 2026-05-28, Merge [#4680](https://github.com/ossrs/srs/pull/4680): RTMP: Fix chunk timestamp/basic-header decoding and harden packet unmarshal. v8.0.3 (#4680)
* v8.0, 2026-05-19, Merge [#4678](https://github.com/ossrs/srs/pull/4678): Edge: Fix HTTP-FLV 404 and RTMP late-join missing sequence headers. v8.0.2 (#4678)
* v8.0, 2026-05-17, Merge [#4676](https://github.com/ossrs/srs/pull/4676): Proxy: Fix RTC/SRT reader goroutine leak; unwrap legacy WHEP JSON envelope; add WHEP pprof guide. v8.0.1 (#4676)
* v8.0, 2026-05-17, Init SRS 8.0, code Free. v8.0.0
<a name="v7-changes"></a>
## SRS 7.0 Changelog
* v7.0, 2026-05-19, Merge [#4678](https://github.com/ossrs/srs/pull/4678): Edge: Fix HTTP-FLV 404 and RTMP late-join missing sequence headers. v7.0.150 (#4678)
* v7.0, 2026-05-17, Merge [#4676](https://github.com/ossrs/srs/pull/4676): Proxy: Fix RTC/SRT reader goroutine leak; unwrap legacy WHEP JSON envelope; add WHEP pprof guide. v7.0.149 (#4676)
* v7.0, 2026-05-17, Merge [#4675](https://github.com/ossrs/srs/pull/4675): Proxy: Refactor for testability; add SRT/WHIP E2E and unit tests. v7.0.148 (#4675)
* v7.0, 2026-05-02, Merge [#4672](https://github.com/ossrs/srs/pull/4672): Proxy: Refactor server APIs and expand RTMP test coverage. v7.0.147 (#4672)
* v7.0, 2026-04-28, Merge [#4670](https://github.com/ossrs/srs/pull/4670): Proxy: Refine logger and environment APIs. v7.0.146 (#4670)

View File

@ -7,6 +7,6 @@
#ifndef SRS_CORE_VERSION_HPP
#define SRS_CORE_VERSION_HPP
#include <srs_core_version8.hpp>
#include <srs_core_version7.hpp>
#endif

View File

@ -9,6 +9,6 @@
#define VERSION_MAJOR 7
#define VERSION_MINOR 0
#define VERSION_REVISION 148
#define VERSION_REVISION 150
#endif

View File

@ -1,7 +0,0 @@
//
// Copyright (c) 2013-2026 The SRS Authors
//
// SPDX-License-Identifier: MIT
//
#include <srs_core_version8.hpp>

View File

@ -1,14 +0,0 @@
//
// Copyright (c) 2013-2026 The SRS Authors
//
// SPDX-License-Identifier: MIT
//
#ifndef SRS_CORE_VERSION8_HPP
#define SRS_CORE_VERSION8_HPP
#define VERSION_MAJOR 8
#define VERSION_MINOR 0
#define VERSION_REVISION 3
#endif