13 KiB
AGENTS.md — Rave (Rust 全协议流媒体引擎)
强制规范 (Mandatory Rules)
注释要求
- 所有代码必须包含中文注释:每个
struct、enum、trait、fn、impl块都要有文档注释 - 模块级注释 (
//!) 说明该模块的职责和设计思路 - 公开 API 必须有
///文档注释,说明参数、返回值、用途 - 内部实现 (
//) 注释解释"为什么"而非"做了什么" unsafe块必须附带 Safety invariant 说明- 注释语言:中文为主,技术术语保留英文原文
TDD 测试驱动开发
- 先写测试,再写实现:每个公开函数/API 必须有对应测试
- 测试放置位置:与源文件同目录,使用
#[cfg(test)] mod tests { ... } - 测试命名规范:
test_<功能>_<场景>_<预期结果>,例如test_buffer_write_wrap_around_succeeds - 必须覆盖的测试类别:
- 单元测试:每个 pub fn / pub struct 的基本行为
- 边界测试:空输入、满队列、环绕 (wrap-around)、并发
- 流媒体推拉流测试:模拟 Publisher 推流 → Subscriber 拉流 的完整数据通路
- GOP 缓存测试:验证新订阅者能收到缓存的最近关键帧
- 背压测试:验证慢订阅者丢帧而不阻塞发布者
- 协议编解码测试:用手工构造的字节 fixture 测试解析器 (RTMP/FLV/TS)
- 运行:
cargo test必须全部通过才能视为完成
Project Identity
Rave is a full-protocol streaming media server engine written in pure Rust, zero third-party crates. It draws architectural inspiration from lal (Go) and Monibuca v6 (Rust) — but must not depend on any external crate (no tokio, no bytes, no parking_lot, nothing from crates.io).
Hard Constraint: No Third-Party Dependencies
# Cargo.toml [dependencies] MUST stay empty
[dependencies]
Everything — async runtime, networking, concurrency primitives, codec parsers — must be hand-written using only std and core. This is a deliberate design choice, not a temporary state.
Consequences an agent must remember:
- No
tokio,async-std,smol— build epoll/kqueue async I/O fromstd::os::unix/std::os::windowsif needed - No
bytes— useVec<u8>, slices, or customBytes-like arena - No
parking_lot,dashmap,crossbeam— usestd::sync::{Mutex, RwLock, Arc}andatomictypes - No
serde— hand-parse/serialize config (TOML/JSON/YAML) - No
log/tracing— build a minimal logger onstd::io::stderrorstdout - No
tonic/prost— gRPC/protobuf must be hand-implemented if needed - No
rustls/native-tls— TLS must be hand-implemented or omitted - No
clap/structopt— parse CLI args fromstd::env::args
SDK 契约层架构 (Plugin Contract Layer)
Inspired by Monibuca v6's monibuca-sdk pattern — plugins depend only on trait contracts, never on engine internals. The SDK layer is the sole interface between protocol plugins and the engine core.
Dependency Direction (strict)
sdk/types.rs ◀── shared types only (AVFrame, codecs, StreamPath)
▲
sdk/traits.rs ◀── trait contracts (PublisherApi, SubscriberApi, StreamManagerApi)
▲
sdk/plugin.rs ◀── Plugin trait, lifecycle, ProtocolPlugin trait
▲
sdk/context.rs ◀── EngineContext (IoC-like service locator)
sdk/registry.rs◀── PluginRegistry (init/start/stop lifecycle manager)
▲
core/ ◀── Engine internals (RingBuffer, Dispatcher, Stream, StreamManager)
(implements the sdk traits, but plugins NEVER import from core/)
▲
protocol/ ◀── Protocol handlers (rtmp/, rtsp/, httpflv/, hls/, wsflv/)
(depend ONLY on sdk:: traits + types, never on core:: directly)
Plugin Lifecycle
Every plugin follows: Created → init() → start() → [running] → stop() → Stopped
// To create a new protocol plugin, implement the Plugin trait:
use rave::sdk::plugin::{Plugin, PluginMeta, PluginState, ProtocolPlugin};
use rave::sdk::context::EngineContext;
use rave::sdk::traits::{ConfigProvider, StreamManagerApi};
pub struct RtmpPlugin { /* ... */ }
impl Plugin for RtmpPlugin {
fn meta(&self) -> &PluginMeta { /* ... */ }
fn init(&mut self, ctx: &EngineContext, cfg: &dyn ConfigProvider) -> Result<(), String> { /* ... */ }
fn start(&mut self, ctx: &EngineContext) -> Result<(), String> { /* ... */ }
fn stop(&mut self) -> Result<(), String> { /* ... */ }
fn state(&self) -> PluginState { /* ... */ }
fn as_any(&self) -> &dyn std::any::Any { self }
fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
}
impl ProtocolPlugin for RtmpPlugin {
fn protocol_name(&self) -> &str { "rtmp" }
fn default_port(&self) -> u16 { 1935 }
}
Key SDK Traits (what plugins use)
| Trait | Location | Purpose |
|---|---|---|
PublisherApi |
sdk/traits.rs |
Write frames into a stream, add tracks |
SubscriberApi |
sdk/traits.rs |
Read frames from a stream |
StreamManagerApi |
sdk/traits.rs |
Create/remove/subscribe to streams |
EventHandler |
sdk/traits.rs |
Receive stream lifecycle events (PublishStart, etc.) |
ConfigProvider |
sdk/traits.rs |
Read plugin-specific config |
Plugin |
sdk/plugin.rs |
Base plugin lifecycle trait |
ProtocolPlugin |
sdk/plugin.rs |
Extends Plugin with protocol_name + default_port |
Key SDK Types (shared across all layers)
| Type | Location | Purpose |
|---|---|---|
AVFrame |
sdk/types.rs |
Universal audio/video frame (timestamp, codec, data as Arc<Vec<u8>>) |
VideoCodec |
sdk/types.rs |
H264, H265, Unknown |
AudioCodec |
sdk/types.rs |
Aac, G711A, G711U, Opus, Unknown |
FrameType |
sdk/types.rs |
KeyFrame, InterFrame, etc. |
StreamPath |
sdk/types.rs |
app_name + stream_name (e.g., "live/test") |
TrackInfo / TrackId |
sdk/types.rs |
Track metadata and identifier |
StreamEvent |
sdk/traits.rs |
PublishStart/Stop, SubscribeStart/Stop, FrameReceived |
EngineContext |
sdk/context.rs |
IoC container: access StreamManager + registered services |
EngineContext (Service Locator)
Plugins receive EngineContext during init() and start(). It provides:
stream_manager()— theArc<dyn StreamManagerApi>for stream operationsregister_service::<T>()/get_service::<T>()— cross-plugin service sharing via TypeId
PluginRegistry
Manages all registered plugins. Called from main.rs:
let registry = PluginRegistry::new(context);
registry.register(Box::new(RtmpPlugin::new()));
registry.init_all(&config)?; // calls init() on each
registry.start_all()?; // calls start() on each
// ... server runs ...
registry.stop_all()?; // reverse-order stop
Target Protocol Support
Phase 1 — Core Protocols
| Protocol | Direction | Key Spec Elements |
|---|---|---|
| RTMP | Pub + Sub | Handshake (C0/S0–C2/S2), Chunk protocol, AMF0/AMF3, message types |
| RTSP/RTP/RTCP | Pub + Sub | DESCRIBE/SETUP/PLAY/RECORD, SDP parse, RTP packetization, interleaved+UDP |
| HTTP-FLV | Sub only | HTTP long-poll, FLV tag header + body |
| HLS | Sub only | M3U8 master/media playlist, MPEG-TS segments (.ts), optional fMP4 (CMAF) |
| WebSocket-FLV | Sub only | WS handshake (RFC 6455, hand-written), then FLV tag frames |
Phase 2 — Extended Protocols
| Protocol | Direction | Notes |
|---|---|---|
| WebRTC | Pub + Sub | WHIP/WHEP, DTLS-SRTP, ICE, SDP — extremely complex without libs |
| SRT | Pub + Sub | URIPacket-based, ARQ, encryption |
| GB28181 | Pub only | SIP signaling + PS payload demux |
| WebTransport | Pub + Sub | QUIC-based; requires hand-written QUIC |
Codec Support Required
- Video: H.264 (AnnexB + AVCC), H.265/HEVC
- Audio: AAC (ADTS + raw), G.711 (A-law / μ-law), Opus (for WebRTC)
- Container parsing: FLV, MPEG-TS, fMP4/CMAF
Architecture (Current Implementation)
rave/
├── Cargo.toml # [dependencies] = empty, edition = "2024"
├── src/
│ ├── main.rs # CLI entry, config load, PluginRegistry bootstrap
│ ├── lib.rs # Module root: core, sdk, config, logger
│ ├── core/
│ │ ├── mod.rs
│ │ ├── buffer.rs # Lock-free SPMC ring buffer (atomic write cursor)
│ │ ├── frame.rs # Re-exports AVFrame from sdk/types
│ │ ├── track.rs # VideoTrack / AudioTrack — owns a Buffer
│ │ ├── publisher.rs # Publisher — implements PublisherApi, writes into tracks
│ │ ├── subscriber.rs # Subscriber — implements SubscriberApi, bounded queue
│ │ ├── dispatcher.rs # Single-read-broadcast: reads once, pushes to all subs
│ │ ├── stream.rs # Stream: publisher + dispatcher + GOP cache + subscriber list
│ │ └── group.rs # StreamManager: implements StreamManagerApi, HashMap registry
│ ├── sdk/
│ │ ├── mod.rs # Re-exports all public SDK items
│ │ ├── types.rs # AVFrame, VideoCodec, AudioCodec, FrameType, StreamPath, TrackId
│ │ ├── traits.rs # PublisherApi, SubscriberApi, StreamManagerApi, EventHandler
│ │ ├── plugin.rs # Plugin trait, ProtocolPlugin trait, PluginMeta, PluginState
│ │ ├── context.rs # EngineContext — IoC service locator
│ │ └── registry.rs # PluginRegistry — lifecycle management
│ ├── config.rs # Hand-written TOML parser, implements ConfigProvider
│ └── logger.rs # Minimal stderr logger with level filtering, #[macro_export] macros
Build & Run
cargo build # debug build
cargo build --release # optimized build
cargo run # start server (optionally: cargo run -- path/to/rave.conf)
cargo test # run all tests
cargo test <test_name> # run a single test
Edition is 2024 — use latest Rust idioms (let chains, gen blocks when stable, etc.).
Logger macros are #[macro_export] — use rave::log_info!(...) from the binary crate, or log_info!(...) from within the library crate.
Performance Design Principles (from Monibuca v6 reference)
- Lock-free SPMC ring buffer: Publisher writes via
fetch_addatomic slot index; subscribers read via per-subscriber cursor - Zero-copy: Frame data shared via
Arc<Vec<u8>>; never clone media payloads between subscribers - Single-read-broadcast: Dispatcher reads each frame exactly once from the ring, then distributes to all subscriber queues
- Backpressure: Bounded per-subscriber channel (1024 frames); slow subscribers drop oldest frames rather than blocking the publisher or other subscribers
- Object pool: Reuse
Vec<u8>buffers and frame structs on hot paths; avoid per-frame allocation
Key Reference Architecture Facts (from lal + Monibuca)
- Group pattern (lal): Each stream path maps to a "group" that owns one publisher and N subscribers. Protocols interoperate at the group level via a common
AVFrametype. Implemented asStream+StreamManager. - GOP cache:
Streamcaches the last complete GOP (keyframe + following delta frames) so new HTTP-FLV/HLS subscribers can start with a keyframe. Cleared on each new keyframe. - Remux layer: Protocol conversion happens through a remux layer that transforms between
AVFrame↔ protocol-specific formats (FLV tags, RTP packets, MPEG-TS packets). Each protocol plugin handles its own remux. - SDK decoupling (Monibuca v6 pattern): Plugins depend only on
sdk::trait contracts. Engine core (core/) implements those traits. Protocol handlers never import fromcore/directly — they go throughPublisherApi/SubscriberApi/StreamManagerApi. This enables future static/dynamic/WASM plugin loading without changing plugin code.
Coding Conventions
- Pure
stdonly. If you catch yourself writinguse <crate_name>::for a non-std crate, stop. - Plugin isolation: Protocol handlers in
protocol/must only import fromsdk/, never fromcore/. Thecore/module is engine-private. - All networking must be non-blocking; implement a minimal reactor on
epoll(Linux) /kqueue(macOS/BSD) if an async runtime is needed. - Use
#[cfg(target_os = "linux")]/#[cfg(target_os = "macos")]for platform-specific I/O. - RTMP chunk size defaults to 128 bytes; handle chunk size negotiation (SetChunkSize message).
- Timestamps are in milliseconds (RTMP) or 90kHz clock (RTP/RTSP) — always normalize to a common clock internally.
- Test protocol parsers with hand-crafted byte fixtures, not external libraries.
What NOT To Do
- Do not add any entry to
[dependencies]inCargo.toml - Do not use
unsafeunless absolutely necessary for performance-critical paths (ring buffer, zero-copy) — document everyunsafeblock with a safety invariant - Do not reference Monibuca v6's crate dependency graph — it uses many third-party crates; Rave must not
- Do not copy Go idioms from lal (goroutines, channels,
select) — translate to Rust idioms (tasks,select!, channels fromstd::sync::mpsc) - Do not let protocol plugins import from
core/— they must use onlysdk/traits and types