rave/AGENTS.md

244 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)](https://github.com/q191201771/lal) and [Monibuca v6 (Rust)](https://monibuca.com/) — 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 from `std::os::unix` / `std::os::windows` if needed
- No `bytes` — use `Vec<u8>`, slices, or custom `Bytes`-like arena
- No `parking_lot`, `dashmap`, `crossbeam` — use `std::sync::{Mutex, RwLock, Arc}` and `atomic` types
- No `serde` — hand-parse/serialize config (TOML/JSON/YAML)
- No `log` / `tracing` — build a minimal logger on `std::io::stderr` or `stdout`
- 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 from `std::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`
```rust
// 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()` — the `Arc<dyn StreamManagerApi>` for stream operations
- `register_service::<T>()` / `get_service::<T>()` — cross-plugin service sharing via TypeId
### PluginRegistry
Manages all registered plugins. Called from `main.rs`:
```rust
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/S0C2/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
```bash
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)
1. **Lock-free SPMC ring buffer**: Publisher writes via `fetch_add` atomic slot index; subscribers read via per-subscriber cursor
2. **Zero-copy**: Frame data shared via `Arc<Vec<u8>>`; never clone media payloads between subscribers
3. **Single-read-broadcast**: Dispatcher reads each frame exactly once from the ring, then distributes to all subscriber queues
4. **Backpressure**: Bounded per-subscriber channel (1024 frames); slow subscribers drop oldest frames rather than blocking the publisher or other subscribers
5. **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 `AVFrame` type. Implemented as `Stream` + `StreamManager`.
- **GOP cache**: `Stream` caches 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 from `core/` directly — they go through `PublisherApi`/`SubscriberApi`/`StreamManagerApi`. This enables future static/dynamic/WASM plugin loading without changing plugin code.
## Coding Conventions
- Pure `std` only. If you catch yourself writing `use <crate_name>::` for a non-std crate, stop.
- **Plugin isolation**: Protocol handlers in `protocol/` must only import from `sdk/`, never from `core/`. The `core/` 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]` in `Cargo.toml`
- Do not use `unsafe` unless absolutely necessary for performance-critical paths (ring buffer, zero-copy) — document every `unsafe` block 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 from `std::sync::mpsc`)
- Do not let protocol plugins import from `core/` — they must use only `sdk/` traits and types