ADD: plugin
This commit is contained in:
parent
6a3ccda626
commit
a4ec0e25fd
90
AGENTS.md
90
AGENTS.md
|
|
@ -27,19 +27,19 @@
|
||||||
|
|
||||||
## Project Identity
|
## 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).
|
**Rave** is a full-protocol streaming media server engine written in **pure Rust, zero third-party crates** (except `tokio` for async runtime). It draws architectural inspiration from [lal (Go)](https://github.com/q191201771/lal) and [Monibuca v6 (Rust)](https://monibuica.com/) — but must not depend on any external crate beyond `tokio`.
|
||||||
|
|
||||||
## Hard Constraint: No Third-Party Dependencies
|
## Hard Constraint: Minimal Third-Party Dependencies
|
||||||
|
|
||||||
```
|
```
|
||||||
# Cargo.toml [dependencies] MUST stay empty
|
# Cargo.toml [dependencies] — only tokio is allowed
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "time", "sync", "signal"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
Everything beyond `tokio` 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:
|
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 `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 `parking_lot`, `dashmap`, `crossbeam` — use `std::sync::{Mutex, RwLock, Arc}` and `atomic` types
|
||||||
- No `serde` — hand-parse/serialize config (TOML/JSON/YAML)
|
- No `serde` — hand-parse/serialize config (TOML/JSON/YAML)
|
||||||
|
|
@ -80,13 +80,14 @@ Every plugin follows: `Created → init() → start() → [running] → stop()
|
||||||
use rave::sdk::plugin::{Plugin, PluginMeta, PluginState, ProtocolPlugin};
|
use rave::sdk::plugin::{Plugin, PluginMeta, PluginState, ProtocolPlugin};
|
||||||
use rave::sdk::context::EngineContext;
|
use rave::sdk::context::EngineContext;
|
||||||
use rave::sdk::traits::{ConfigProvider, StreamManagerApi};
|
use rave::sdk::traits::{ConfigProvider, StreamManagerApi};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub struct RtmpPlugin { /* ... */ }
|
pub struct RtmpPlugin { /* ... */ }
|
||||||
|
|
||||||
impl Plugin for RtmpPlugin {
|
impl Plugin for RtmpPlugin {
|
||||||
fn meta(&self) -> &PluginMeta { /* ... */ }
|
fn meta(&self) -> &PluginMeta { /* ... */ }
|
||||||
fn init(&mut self, ctx: &EngineContext, cfg: &dyn ConfigProvider) -> Result<(), String> { /* ... */ }
|
fn init(&mut self, ctx: Arc<EngineContext>, cfg: &dyn ConfigProvider) -> Result<(), String> { /* ... */ }
|
||||||
fn start(&mut self, ctx: &EngineContext) -> Result<(), String> { /* ... */ }
|
fn start(&mut self, ctx: Arc<EngineContext>) -> Result<(), String> { /* ... */ }
|
||||||
fn stop(&mut self) -> Result<(), String> { /* ... */ }
|
fn stop(&mut self) -> Result<(), String> { /* ... */ }
|
||||||
fn state(&self) -> PluginState { /* ... */ }
|
fn state(&self) -> PluginState { /* ... */ }
|
||||||
fn as_any(&self) -> &dyn std::any::Any { self }
|
fn as_any(&self) -> &dyn std::any::Any { self }
|
||||||
|
|
@ -133,15 +134,37 @@ Plugins receive `EngineContext` during `init()` and `start()`. It provides:
|
||||||
### PluginRegistry
|
### PluginRegistry
|
||||||
|
|
||||||
Manages all registered plugins. Called from `main.rs`:
|
Manages all registered plugins. Called from `main.rs`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let registry = PluginRegistry::new(context);
|
// 内置插件:通过 all_plugins() 自动注册
|
||||||
registry.register(Box::new(RtmpPlugin::new()));
|
for plugin in protocol::all_plugins() {
|
||||||
registry.init_all(&config)?; // calls init() on each
|
registry.register(plugin);
|
||||||
registry.start_all()?; // calls start() on each
|
}
|
||||||
|
|
||||||
|
// 外部插件:手动注册
|
||||||
|
// registry.register(Box::new(my_external_plugin::XxxPlugin::new()));
|
||||||
|
|
||||||
|
registry.init_all(&config)?; // calls init() on each, checks <name>.enabled
|
||||||
|
registry.start_all()?; // calls start() on each (spawns async listeners)
|
||||||
// ... server runs ...
|
// ... server runs ...
|
||||||
registry.stop_all()?; // reverse-order stop
|
registry.stop_all()?; // reverse-order stop
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 内置插件自动注册
|
||||||
|
|
||||||
|
内置协议插件通过 `protocol::all_plugins()` 清单函数统一注册:
|
||||||
|
- 新增内置协议:在 `protocol/<name>/plugin.rs` 实现插件 → 在 `protocol/mod.rs` 的 `all_plugins()` 加一行
|
||||||
|
- 外部插件:在 `main.rs` 中手动 `registry.register()`
|
||||||
|
|
||||||
|
### 配置级插件开关
|
||||||
|
|
||||||
|
通过 `<插件名>.enabled = false` 在配置中禁用指定插件:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
rtmp.enabled = false # 禁用 RTMP 插件
|
||||||
|
rtsp.enabled = false # 禁用 RTSP 插件
|
||||||
|
```
|
||||||
|
|
||||||
## Target Protocol Support
|
## Target Protocol Support
|
||||||
|
|
||||||
### Phase 1 — Core Protocols
|
### Phase 1 — Core Protocols
|
||||||
|
|
@ -170,7 +193,7 @@ registry.stop_all()?; // reverse-order stop
|
||||||
|
|
||||||
```
|
```
|
||||||
rave/
|
rave/
|
||||||
├── Cargo.toml # [dependencies] = empty, edition = "2024"
|
├── Cargo.toml # [dependencies] = tokio only, edition = "2024"
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── main.rs # CLI entry, config load, PluginRegistry bootstrap
|
│ ├── main.rs # CLI entry, config load, PluginRegistry bootstrap
|
||||||
│ ├── lib.rs # Module root: core, sdk, config, logger
|
│ ├── lib.rs # Module root: core, sdk, config, logger
|
||||||
|
|
@ -190,9 +213,44 @@ rave/
|
||||||
│ │ ├── traits.rs # PublisherApi, SubscriberApi, StreamManagerApi, EventHandler
|
│ │ ├── traits.rs # PublisherApi, SubscriberApi, StreamManagerApi, EventHandler
|
||||||
│ │ ├── plugin.rs # Plugin trait, ProtocolPlugin trait, PluginMeta, PluginState
|
│ │ ├── plugin.rs # Plugin trait, ProtocolPlugin trait, PluginMeta, PluginState
|
||||||
│ │ ├── context.rs # EngineContext — IoC service locator
|
│ │ ├── context.rs # EngineContext — IoC service locator
|
||||||
│ │ └── registry.rs # PluginRegistry — lifecycle management
|
│ │ └── registry.rs # PluginRegistry — lifecycle management, config enable/disable
|
||||||
|
│ ├── codec/
|
||||||
|
│ │ ├── h264.rs # H.264 NALU 解析、AVCC 编码
|
||||||
|
│ │ ├── hevc.rs # H.265 NALU 类型
|
||||||
|
│ │ ├── aac.rs # AAC ADTS 解析
|
||||||
|
│ │ ├── flv.rs # FLV tag 编解码
|
||||||
|
│ │ └── ts.rs # MPEG-TS 打包
|
||||||
|
│ ├── protocol/
|
||||||
|
│ │ ├── mod.rs # all_plugins() 内置插件清单函数
|
||||||
|
│ │ ├── rtmp/
|
||||||
|
│ │ │ ├── plugin.rs # RtmpPlugin (Plugin + ProtocolPlugin)
|
||||||
|
│ │ │ ├── server.rs # RtmpServer TCP 监听
|
||||||
|
│ │ │ ├── session.rs # RTMP 会话状态机
|
||||||
|
│ │ │ ├── handshake.rs# RTMP 握手 (C0/S0–C2/S2)
|
||||||
|
│ │ │ ├── chunk.rs # Chunk 协议解析
|
||||||
|
│ │ │ ├── message.rs # RTMP 消息类型
|
||||||
|
│ │ │ └── amf0.rs # AMF0 编解码
|
||||||
|
│ │ ├── rtsp/
|
||||||
|
│ │ │ ├── plugin.rs # RtspPlugin (Plugin + ProtocolPlugin)
|
||||||
|
│ │ │ ├── server.rs # RtspServer TCP 监听
|
||||||
|
│ │ │ ├── session.rs # RTSP 会话(DESCRIBE/SETUP/PLAY/RECORD)
|
||||||
|
│ │ │ ├── rtp.rs # RTP 封装/解包
|
||||||
|
│ │ │ ├── rtcp.rs # RTCP Sender Report
|
||||||
|
│ │ │ ├── depacketizer.rs # RTP→AVFrame 解包器(H.264 FU-A、AAC)
|
||||||
|
│ │ │ ├── sdps.rs # SDP 生成
|
||||||
|
│ │ │ ├── request.rs # RTSP 请求解析
|
||||||
|
│ │ │ └── response.rs # RTSP 响应构造
|
||||||
|
│ │ ├── httpflv.rs # HTTP-FLV 请求解析、FLV 流式输出
|
||||||
|
│ │ ├── httpflv_server.rs # HTTP-FLV TCP 服务器
|
||||||
|
│ │ ├── httpflv_server_plugin.rs # HttpFlvPlugin (Plugin + ProtocolPlugin)
|
||||||
|
│ │ ├── hls.rs # HLS 框架
|
||||||
|
│ │ └── wsflv.rs # WebSocket-FLV 框架
|
||||||
|
│ ├── remux/
|
||||||
|
│ │ ├── rtmp2flv.rs # RTMP ↔ FLV
|
||||||
|
│ │ └── flv2ts.rs # FLV ↔ MPEG-TS
|
||||||
│ ├── config.rs # Hand-written TOML parser, implements ConfigProvider
|
│ ├── config.rs # Hand-written TOML parser, implements ConfigProvider
|
||||||
│ └── logger.rs # Minimal stderr logger with level filtering, #[macro_export] macros
|
│ ├── logger.rs # Minimal stderr logger with level filtering, #[macro_export] macros
|
||||||
|
│ └── stats.rs # ServerStats — bandwidth, fps, connection counters
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build & Run
|
## Build & Run
|
||||||
|
|
@ -226,10 +284,8 @@ Logger macros are `#[macro_export]` — use `rave::log_info!(...)` from the bina
|
||||||
|
|
||||||
## Coding Conventions
|
## Coding Conventions
|
||||||
|
|
||||||
- Pure `std` only. If you catch yourself writing `use <crate_name>::` for a non-std crate, stop.
|
- Only `tokio` from crates.io; all other code uses `std` and `core` only. If you catch yourself writing `use <crate_name>::` for a non-std crate (other than tokio), stop.
|
||||||
- **Plugin isolation**: Protocol handlers in `protocol/` must only import from `sdk/`, never from `core/`. The `core/` module is engine-private.
|
- **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).
|
- 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.
|
- 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.
|
- Test protocol parsers with hand-crafted byte fixtures, not external libraries.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,16 @@ version = "1.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.186"
|
version = "0.2.186"
|
||||||
|
|
@ -56,6 +66,16 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||||
|
dependencies = [
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
|
|
@ -87,6 +107,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,6 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "time", "sync"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "time", "sync", "signal"] }
|
||||||
# ffmpeg -re -i video.mp4 -c copy -f flv rtmp://localhost:1935/live/test
|
# ffmpeg -re -i video.mp4 -c copy -f flv rtmp://localhost:1935/live/test
|
||||||
# ffplay http://localhost:8080/live/test.flv
|
# ffplay http://localhost:8080/live/test.flv
|
||||||
36
README.md
36
README.md
|
|
@ -9,7 +9,7 @@
|
||||||
| 协议 | 推流 | 拉流 | 状态 |
|
| 协议 | 推流 | 拉流 | 状态 |
|
||||||
|------|:----:|:----:|:----:|
|
|------|:----:|:----:|:----:|
|
||||||
| RTMP | ✅ | ✅ | 握手、Chunk 协议、AMF0、Publish/Play |
|
| RTMP | ✅ | ✅ | 握手、Chunk 协议、AMF0、Publish/Play |
|
||||||
| RTSP/RTP | ✅ | ⚠️ | ANNOUNCE/RECORD 推流完成;DESCRIBE/PLAY 拉流开发中 |
|
| RTSP/RTP | ✅ | ✅ | ANNOUNCE/RECORD 推流;DESCRIBE/PLAY 拉流 |
|
||||||
| HTTP-FLV | — | ✅ | HTTP 长连接 + FLV 实时流 |
|
| HTTP-FLV | — | ✅ | HTTP 长连接 + FLV 实时流 |
|
||||||
| WebSocket-FLV | — | 🚧 | 框架已搭建 |
|
| WebSocket-FLV | — | 🚧 | 框架已搭建 |
|
||||||
| HLS | — | 🚧 | 框架已搭建 |
|
| HLS | — | 🚧 | 框架已搭建 |
|
||||||
|
|
@ -58,8 +58,8 @@ ffplay rtsp://localhost:5544/live/test
|
||||||
|
|
||||||
| 推流 → 拉流 | RTMP | HTTP-FLV | RTSP |
|
| 推流 → 拉流 | RTMP | HTTP-FLV | RTSP |
|
||||||
|:-----------:|:----:|:--------:|:----:|
|
|:-----------:|:----:|:--------:|:----:|
|
||||||
| **RTMP** | ✅ | ✅ | ⚠️ |
|
| **RTMP** | ✅ | ✅ | ✅ |
|
||||||
| **RTSP** | ✅ | ✅ | ⚠️ |
|
| **RTSP** | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
## HTTP API 接口
|
## HTTP API 接口
|
||||||
|
|
||||||
|
|
@ -75,20 +75,24 @@ ffplay rtsp://localhost:5544/live/test
|
||||||
|
|
||||||
## 配置
|
## 配置
|
||||||
|
|
||||||
默认端口可在 `rave.conf` (TOML 格式) 中配置:
|
默认端口和插件开关可在 `rave.conf` (TOML 格式) 中配置:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
rtmp.port = 1935
|
rtmp.port = 1935
|
||||||
httpflv.port = 8080
|
httpflv.port = 8080
|
||||||
rtsp.port = 5544
|
rtsp.port = 5544
|
||||||
log.level = "info"
|
log.level = "info"
|
||||||
|
|
||||||
|
# 插件开关(设为 false 禁用对应协议)
|
||||||
|
# rtmp.enabled = false
|
||||||
|
# rtsp.enabled = false
|
||||||
```
|
```
|
||||||
|
|
||||||
## 架构
|
## 架构
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── main.rs # 入口:配置加载、服务器启动
|
├── main.rs # 入口:配置加载、插件注册、banner、优雅关闭
|
||||||
├── lib.rs # 模块根
|
├── lib.rs # 模块根
|
||||||
├── config.rs # 手写 TOML 解析器
|
├── config.rs # 手写 TOML 解析器
|
||||||
├── logger.rs # 最小化 stderr 日志
|
├── logger.rs # 最小化 stderr 日志
|
||||||
|
|
@ -106,23 +110,23 @@ src/
|
||||||
│ ├── traits.rs # PublisherApi、SubscriberApi、StreamManagerApi
|
│ ├── traits.rs # PublisherApi、SubscriberApi、StreamManagerApi
|
||||||
│ ├── plugin.rs # Plugin / ProtocolPlugin trait
|
│ ├── plugin.rs # Plugin / ProtocolPlugin trait
|
||||||
│ ├── context.rs # EngineContext(IoC 容器)
|
│ ├── context.rs # EngineContext(IoC 容器)
|
||||||
│ └── registry.rs # PluginRegistry(生命周期管理)
|
│ └── registry.rs # PluginRegistry(生命周期管理 + 配置开关)
|
||||||
├── codec/ # 编解码器
|
├── codec/ # 编解码器
|
||||||
│ ├── h264.rs # H.264 NALU 解析、AVCC 编码
|
│ ├── h264.rs # H.264 NALU 解析、AVCC 编码
|
||||||
│ ├── h265.rs # H.265 NALU 类型
|
│ ├── h265.rs # H.265 NALU 类型
|
||||||
│ ├── aac.rs # AAC ADTS 解析
|
│ ├── aac.rs # AAC ADTS 解析
|
||||||
│ ├── flv.rs # FLV tag 编解码
|
│ ├── flv.rs # FLV tag 编解码
|
||||||
│ └── mpegts.rs # MPEG-TS 打包
|
│ └── ts.rs # MPEG-TS 打包
|
||||||
├── protocol/ # 协议实现
|
├── protocol/ # 协议实现
|
||||||
│ ├── rtmp/ # RTMP 握手、Chunk、AMF0、Session
|
│ ├── mod.rs # all_plugins() 内置插件清单函数
|
||||||
│ ├── rtsp/ # RTSP 信令、RTP、Depacketizer
|
│ ├── rtmp/ # RTMP 插件、握手、Chunk、AMF0、Session
|
||||||
│ ├── httpflv*.rs # HTTP-FLV 服务端
|
│ ├── rtsp/ # RTSP 插件、信令、RTP、Depacketizer
|
||||||
|
│ ├── httpflv*.rs # HTTP-FLV 插件 + 服务端
|
||||||
│ ├── hls.rs # HLS 框架
|
│ ├── hls.rs # HLS 框架
|
||||||
│ └── wsflv.rs # WebSocket-FLV 框架
|
│ └── wsflv.rs # WebSocket-FLV 框架
|
||||||
└── remux/ # 协议转换
|
└── remux/ # 协议转换
|
||||||
├── rtmp2flv.rs # RTMP ↔ FLV
|
├── rtmp2flv.rs # RTMP ↔ FLV
|
||||||
├── rtmp2ts.rs # RTMP ↔ MPEG-TS
|
└── flv2ts.rs # FLV ↔ MPEG-TS
|
||||||
└── rtmp_to_rtp.rs # AVFrame → RTP
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 性能设计
|
### 性能设计
|
||||||
|
|
@ -144,10 +148,16 @@ core/ (实现 sdk traits) |
|
||||||
protocol/ (仅依赖 sdk:: traits + types) ───────────────────┘
|
protocol/ (仅依赖 sdk:: traits + types) ───────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**内置插件**通过 `protocol::all_plugins()` 自动注册,新增内置协议只需:
|
||||||
|
1. 在 `protocol/<name>/plugin.rs` 实现 `Plugin` + `ProtocolPlugin` trait
|
||||||
|
2. 在 `protocol/mod.rs` 的 `all_plugins()` 中添加一行
|
||||||
|
|
||||||
|
**外部插件**通过 `registry.register()` 手动注册。
|
||||||
|
|
||||||
## 测试
|
## 测试
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 运行全部测试(485 个)
|
# 运行全部测试(513 个)
|
||||||
cargo test
|
cargo test
|
||||||
|
|
||||||
# 运行特定测试
|
# 运行特定测试
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use crate::core::stream::Stream;
|
use crate::core::stream::Stream;
|
||||||
use crate::sdk::traits::{PublisherApi, StreamManagerApi, SubscriberApi};
|
use crate::sdk::traits::{PublisherApi, StreamManagerApi, SubscriberApi};
|
||||||
use crate::sdk::types::{AVFrame, StreamPath, StreamSummary};
|
use crate::sdk::types::{AVFrame, StreamCodecMeta, StreamPath, StreamSummary};
|
||||||
|
|
||||||
/// 流管理器
|
/// 流管理器
|
||||||
///
|
///
|
||||||
|
|
@ -110,12 +110,19 @@ impl StreamManagerApi for StreamManager {
|
||||||
let streams = self.streams.read().unwrap();
|
let streams = self.streams.read().unwrap();
|
||||||
streams.values().map(|s| s.summary()).collect()
|
streams.values().map(|s| s.summary()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取流的编解码器元数据
|
||||||
|
fn get_codec_metadata(&self, path: &StreamPath) -> Option<StreamCodecMeta> {
|
||||||
|
let key = path.full_path();
|
||||||
|
let streams = self.streams.read().unwrap();
|
||||||
|
streams.get(&key).map(|s| s.codec_metadata())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::sdk::types::{FrameType, StreamPath, VideoCodec, AudioCodec, AVFrame};
|
use crate::sdk::types::{FrameType, StreamPath, VideoCodec, AudioCodec, AVFrame, CodecExtraInfo, H264SeqHeader, AacSeqHeader};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -396,4 +403,46 @@ mod tests {
|
||||||
}
|
}
|
||||||
assert_eq!(last_ts, 1099, "last frame should be the most recent");
|
assert_eq!(last_ts, 1099, "last frame should be the most recent");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 测试:get_codec_metadata 对不存在的流返回 None
|
||||||
|
#[test]
|
||||||
|
fn test_stream_manager_get_codec_metadata_missing_returns_none() {
|
||||||
|
let mgr = StreamManager::new();
|
||||||
|
let path = StreamPath::new("live", "missing");
|
||||||
|
assert!(mgr.get_codec_metadata(&path).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:get_codec_metadata 从流中提取编解码器元数据
|
||||||
|
#[test]
|
||||||
|
fn test_stream_manager_get_codec_metadata_extracts_meta() {
|
||||||
|
let mgr = StreamManager::new();
|
||||||
|
let path = StreamPath::new("live", "meta");
|
||||||
|
|
||||||
|
let stream = create_stream_and_get_inner(&mgr, &path);
|
||||||
|
|
||||||
|
// 写入带 SPS/PPS 的视频 seq_header
|
||||||
|
let sps = std::sync::Arc::new(vec![0x67, 0x64, 0x00, 0x29, 0xAC]);
|
||||||
|
let pps = std::sync::Arc::new(vec![0x68, 0xEE, 0x31, 0x12]);
|
||||||
|
let mut vframe = AVFrame::new_video(0, std::sync::Arc::new(vec![]), VideoCodec::H264, FrameType::KeyFrame);
|
||||||
|
vframe.codec_info = Some(CodecExtraInfo::H264SeqHeader(H264SeqHeader {
|
||||||
|
sps: sps.clone(),
|
||||||
|
pps: pps.clone(),
|
||||||
|
}));
|
||||||
|
stream.dispatch_frame(vframe);
|
||||||
|
|
||||||
|
// 写入带 AAC config 的音频 seq_header
|
||||||
|
let aac_config = std::sync::Arc::new(vec![0x12, 0x10]); // 44100Hz, 2ch
|
||||||
|
let mut aframe = AVFrame::new_audio(0, std::sync::Arc::new(vec![]), AudioCodec::Aac);
|
||||||
|
aframe.codec_info = Some(CodecExtraInfo::AacSeqHeader(AacSeqHeader {
|
||||||
|
audio_specific_config: aac_config.clone(),
|
||||||
|
}));
|
||||||
|
stream.dispatch_frame(aframe);
|
||||||
|
|
||||||
|
let meta = mgr.get_codec_metadata(&path).expect("应返回元数据");
|
||||||
|
assert!(meta.h264_sps.is_some(), "应有 SPS");
|
||||||
|
assert!(meta.h264_pps.is_some(), "应有 PPS");
|
||||||
|
assert!(meta.aac_config.is_some(), "应有 AAC config");
|
||||||
|
assert_eq!(meta.audio_sample_rate, 44100, "采样率应为 44100");
|
||||||
|
assert_eq!(meta.audio_channels, 2, "通道数应为 2");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use std::sync::{Arc, Mutex};
|
||||||
use crate::core::dispatcher::Dispatcher;
|
use crate::core::dispatcher::Dispatcher;
|
||||||
use crate::core::publisher::Publisher;
|
use crate::core::publisher::Publisher;
|
||||||
use crate::core::subscriber::Subscriber;
|
use crate::core::subscriber::Subscriber;
|
||||||
use crate::sdk::types::{AudioCodec, AVFrame, StreamPath, StreamSummary, VideoCodec};
|
use crate::sdk::types::{AudioCodec, AVFrame, CodecExtraInfo, StreamCodecMeta, StreamPath, StreamSummary, VideoCodec};
|
||||||
|
|
||||||
/// 流实例
|
/// 流实例
|
||||||
///
|
///
|
||||||
|
|
@ -203,13 +203,80 @@ impl Stream {
|
||||||
total_audio_frames: self.total_audio_frames.load(Ordering::Relaxed),
|
total_audio_frames: self.total_audio_frames.load(Ordering::Relaxed),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取流的编解码器元数据
|
||||||
|
///
|
||||||
|
/// 扫描 GOP 缓存中的 seq_header 帧,提取 H.264 SPS/PPS 和 AAC AudioSpecificConfig。
|
||||||
|
/// 用于 RTSP DESCRIBE 响应生成 SDP 描述。
|
||||||
|
///
|
||||||
|
/// AAC AudioSpecificConfig 2 字节解析:
|
||||||
|
/// - bits [4:0] byte0 + [7:5] byte1 → audioObjectType(5 bits 从 byte0 高 5 位)
|
||||||
|
/// - bits [4:1] byte1 → samplingFrequencyIndex(4 bits)
|
||||||
|
/// - bit [0] byte1 + [7:5] byte2 → channelConfiguration(4 bits)
|
||||||
|
pub fn codec_metadata(&self) -> StreamCodecMeta {
|
||||||
|
let cache = self.gop_cache.lock().unwrap();
|
||||||
|
let mut meta = StreamCodecMeta::default();
|
||||||
|
|
||||||
|
for frame in cache.iter() {
|
||||||
|
if let Some(ref info) = frame.codec_info {
|
||||||
|
match info {
|
||||||
|
CodecExtraInfo::H264SeqHeader(sh) => {
|
||||||
|
meta.h264_sps = Some(sh.sps.clone());
|
||||||
|
meta.h264_pps = Some(sh.pps.clone());
|
||||||
|
}
|
||||||
|
CodecExtraInfo::AacSeqHeader(ah) => {
|
||||||
|
meta.aac_config = Some(ah.audio_specific_config.clone());
|
||||||
|
// 解析 2 字节 AudioSpecificConfig 提取采样率和通道数
|
||||||
|
let config = &ah.audio_specific_config;
|
||||||
|
if config.len() >= 2 {
|
||||||
|
// samplingFrequencyIndex: bits [7:3] of byte[1]
|
||||||
|
// 格式: audioObjectType(5)<<11 | samplingFreqIndex(4)<<7 | channelConfig(3)<<4 | ...
|
||||||
|
// 实际 layout: byte0[7:3]=audioObjectType(5), byte0[2:0]+byte1[7]=samplingFreqIndex(4)
|
||||||
|
// byte1[6:3]=channelConfiguration(4), ...
|
||||||
|
// 简化:byte0 低 3 位 << 1 | byte1 高 1 位 = samplingFreqIndex
|
||||||
|
let sfi = (((config[0] & 0x07) as u32) << 1)
|
||||||
|
| (((config[1] >> 7) & 0x01) as u32);
|
||||||
|
// channelConfiguration: byte1 bits [6:3]
|
||||||
|
let ch = (config[1] >> 3) & 0x0F;
|
||||||
|
meta.audio_sample_rate = sampling_rate_from_index(sfi);
|
||||||
|
meta.audio_channels = ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
meta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据 samplingFrequencyIndex 查表返回采样率
|
||||||
|
///
|
||||||
|
/// AAC AudioSpecificConfig 中的采样率索引表(ISO 14496-3 Table 1.16)
|
||||||
|
fn sampling_rate_from_index(index: u32) -> u32 {
|
||||||
|
match index {
|
||||||
|
0 => 96000,
|
||||||
|
1 => 88200,
|
||||||
|
2 => 64000,
|
||||||
|
3 => 48000,
|
||||||
|
4 => 44100,
|
||||||
|
5 => 32000,
|
||||||
|
6 => 24000,
|
||||||
|
7 => 22050,
|
||||||
|
8 => 16000,
|
||||||
|
9 => 12000,
|
||||||
|
10 => 11025,
|
||||||
|
11 => 8000,
|
||||||
|
12 => 7350,
|
||||||
|
_ => 44100, // 未知时默认 44100
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::sdk::traits::{PublisherApi, SubscriberApi};
|
use crate::sdk::traits::{PublisherApi, SubscriberApi};
|
||||||
use crate::sdk::types::{AudioCodec, FrameType, VideoCodec};
|
use crate::sdk::types::{AudioCodec, CodecExtraInfo, FrameType, VideoCodec, AacSeqHeader, H264SeqHeader};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
fn make_path() -> StreamPath {
|
fn make_path() -> StreamPath {
|
||||||
|
|
@ -347,4 +414,106 @@ mod tests {
|
||||||
let _sub2 = stream.subscribe();
|
let _sub2 = stream.subscribe();
|
||||||
assert_eq!(stream.summary().subscriber_count, 2);
|
assert_eq!(stream.summary().subscriber_count, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 测试:codec_metadata 初始状态返回空元数据
|
||||||
|
#[test]
|
||||||
|
fn test_stream_codec_metadata_initial_state_empty() {
|
||||||
|
let stream = Stream::new(make_path());
|
||||||
|
let meta = stream.codec_metadata();
|
||||||
|
assert!(meta.h264_sps.is_none(), "初始状态不应有 SPS");
|
||||||
|
assert!(meta.h264_pps.is_none(), "初始状态不应有 PPS");
|
||||||
|
assert!(meta.aac_config.is_none(), "初始状态不应有 AAC config");
|
||||||
|
assert_eq!(meta.audio_sample_rate, 0);
|
||||||
|
assert_eq!(meta.audio_channels, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:codec_metadata 从 GOP 缓存中的 seq_header 帧提取 H.264 SPS/PPS
|
||||||
|
#[test]
|
||||||
|
fn test_stream_codec_metadata_extracts_h264_sps_pps() {
|
||||||
|
let stream = Stream::new(make_path());
|
||||||
|
let sps_data = Arc::new(vec![0x67, 0x64, 0x00, 0x29, 0xAC, 0xD9, 0x40, 0x78, 0x02]);
|
||||||
|
let pps_data = Arc::new(vec![0x68, 0xEE, 0x31, 0x12]);
|
||||||
|
|
||||||
|
let mut frame = AVFrame::new_video(0, Arc::new(vec![]), VideoCodec::H264, FrameType::KeyFrame);
|
||||||
|
frame.codec_info = Some(CodecExtraInfo::H264SeqHeader(H264SeqHeader {
|
||||||
|
sps: sps_data.clone(),
|
||||||
|
pps: pps_data.clone(),
|
||||||
|
}));
|
||||||
|
stream.dispatch_frame(frame);
|
||||||
|
|
||||||
|
let meta = stream.codec_metadata();
|
||||||
|
assert!(meta.h264_sps.is_some(), "应有 SPS");
|
||||||
|
assert!(meta.h264_pps.is_some(), "应有 PPS");
|
||||||
|
assert_eq!(&*meta.h264_sps.unwrap(), &*sps_data);
|
||||||
|
assert_eq!(&*meta.h264_pps.unwrap(), &*pps_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:codec_metadata 从 GOP 缓存中的 seq_header 帧提取 AAC config
|
||||||
|
#[test]
|
||||||
|
fn test_stream_codec_metadata_extracts_aac_config() {
|
||||||
|
let stream = Stream::new(make_path());
|
||||||
|
// AAC AudioSpecificConfig: audioObjectType=2(AAC-LC), samplingFreqIndex=3(48000), channelConfig=2
|
||||||
|
// byte0 = (audioObjectType=2) << 3 | (sfi=3) >> 1 = 00010_001 = 0x11
|
||||||
|
// byte1 = (sfi=3 & 1) << 7 | (channelConfig=2) << 3 = 1_0010_000 = 0x90
|
||||||
|
let aac_config = Arc::new(vec![0x11, 0x90]);
|
||||||
|
|
||||||
|
let mut frame = AVFrame::new_audio(0, Arc::new(vec![]), AudioCodec::Aac);
|
||||||
|
frame.codec_info = Some(CodecExtraInfo::AacSeqHeader(AacSeqHeader {
|
||||||
|
audio_specific_config: aac_config.clone(),
|
||||||
|
}));
|
||||||
|
stream.dispatch_frame(frame);
|
||||||
|
|
||||||
|
let meta = stream.codec_metadata();
|
||||||
|
assert!(meta.aac_config.is_some(), "应有 AAC config");
|
||||||
|
assert_eq!(&*meta.aac_config.unwrap(), &*aac_config);
|
||||||
|
assert_eq!(meta.audio_sample_rate, 48000, "采样率应为 48000");
|
||||||
|
assert_eq!(meta.audio_channels, 2, "通道数应为 2");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:codec_metadata 同时包含视频和音频元数据
|
||||||
|
#[test]
|
||||||
|
fn test_stream_codec_metadata_both_video_and_audio() {
|
||||||
|
let stream = Stream::new(make_path());
|
||||||
|
let sps_data = Arc::new(vec![0x67, 0x42, 0xC0, 0x1E, 0xD9]);
|
||||||
|
let pps_data = Arc::new(vec![0x68, 0xCE, 0x38, 0x80]);
|
||||||
|
// AAC AudioSpecificConfig: samplingFreqIndex=4(44100), channelConfig=2
|
||||||
|
// byte0 = (2<<3) | (4>>1) = 0x12, byte1 = (4<<7) | (2<<3) = 0x90... wait
|
||||||
|
// audioObjectType=2(5bits) = 00010
|
||||||
|
// samplingFreqIndex=4(4bits) = 0100
|
||||||
|
// channelConfiguration=2(4bits) = 0010
|
||||||
|
// layout: [00010][0100][0010][000]
|
||||||
|
// byte0 = 00010_010 = 0x12
|
||||||
|
// byte1 = 0_0010_000 = 0x10
|
||||||
|
let aac_config = Arc::new(vec![0x12, 0x10]);
|
||||||
|
|
||||||
|
let mut video_frame = AVFrame::new_video(0, Arc::new(vec![]), VideoCodec::H264, FrameType::KeyFrame);
|
||||||
|
video_frame.codec_info = Some(CodecExtraInfo::H264SeqHeader(H264SeqHeader {
|
||||||
|
sps: sps_data.clone(),
|
||||||
|
pps: pps_data.clone(),
|
||||||
|
}));
|
||||||
|
stream.dispatch_frame(video_frame);
|
||||||
|
|
||||||
|
let mut audio_frame = AVFrame::new_audio(0, Arc::new(vec![]), AudioCodec::Aac);
|
||||||
|
audio_frame.codec_info = Some(CodecExtraInfo::AacSeqHeader(AacSeqHeader {
|
||||||
|
audio_specific_config: aac_config.clone(),
|
||||||
|
}));
|
||||||
|
stream.dispatch_frame(audio_frame);
|
||||||
|
|
||||||
|
let meta = stream.codec_metadata();
|
||||||
|
assert!(meta.h264_sps.is_some());
|
||||||
|
assert!(meta.h264_pps.is_some());
|
||||||
|
assert!(meta.aac_config.is_some());
|
||||||
|
assert_eq!(meta.audio_sample_rate, 44100, "采样率应为 44100");
|
||||||
|
assert_eq!(meta.audio_channels, 2, "通道数应为 2");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:sampling_rate_from_index 查表正确
|
||||||
|
#[test]
|
||||||
|
fn test_sampling_rate_from_index_known_values() {
|
||||||
|
assert_eq!(super::sampling_rate_from_index(0), 96000);
|
||||||
|
assert_eq!(super::sampling_rate_from_index(3), 48000);
|
||||||
|
assert_eq!(super::sampling_rate_from_index(4), 44100);
|
||||||
|
assert_eq!(super::sampling_rate_from_index(11), 8000);
|
||||||
|
assert_eq!(super::sampling_rate_from_index(15), 44100); // 未知索引默认 44100
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
95
src/main.rs
95
src/main.rs
|
|
@ -1,17 +1,29 @@
|
||||||
//! Rave 流媒体服务器入口
|
//! Rave 流媒体服务器入口
|
||||||
//!
|
//!
|
||||||
//! 基于 tokio 多线程异步运行时
|
//! 基于 PluginRegistry 的插件注册模式启动服务器。
|
||||||
|
//!
|
||||||
|
//! **内置插件**通过 `protocol::all_plugins()` 自动注册,无需手动列举。
|
||||||
|
//! **外部插件**通过 `registry.register()` 手动注册。
|
||||||
|
//!
|
||||||
|
//! 添加新的内置协议只需:
|
||||||
|
//! 1. 在 `protocol/<name>/` 下实现 `Plugin` + `ProtocolPlugin` trait
|
||||||
|
//! 2. 在 `protocol/mod.rs` 的 `all_plugins()` 中添加一行
|
||||||
|
//!
|
||||||
|
//! 添加外部插件只需:
|
||||||
|
//! 1. 依赖 `rave-sdk` trait,实现 `Plugin` trait
|
||||||
|
//! 2. 在此处 `registry.register(Box::new(XxxPlugin::new()))`
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use rave::config::Config;
|
use rave::config::Config;
|
||||||
use rave::core::group::StreamManager;
|
use rave::core::group::StreamManager;
|
||||||
|
use rave::log_error;
|
||||||
|
use rave::log_info;
|
||||||
use rave::log_warn;
|
use rave::log_warn;
|
||||||
use rave::logger;
|
use rave::logger;
|
||||||
use rave::protocol::rtmp::server::{RtmpServer, RtmpServerConfig};
|
use rave::protocol;
|
||||||
use rave::protocol::httpflv_server::{HttpFlvServer, HttpFlvServerConfig};
|
|
||||||
use rave::protocol::rtsp::server::{RtspServer, RtspServerConfig};
|
|
||||||
use rave::sdk::context::EngineContext;
|
use rave::sdk::context::EngineContext;
|
||||||
|
use rave::sdk::registry::PluginRegistry;
|
||||||
use rave::stats::ServerStats;
|
use rave::stats::ServerStats;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|
@ -19,59 +31,58 @@ async fn main() {
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
let config_path = args.get(1).map(|s| s.as_str()).unwrap_or("rave.conf");
|
let config_path = args.get(1).map(|s| s.as_str()).unwrap_or("rave.conf");
|
||||||
|
|
||||||
|
// 加载配置(失败则使用默认值)
|
||||||
let config = Config::from_file(config_path).unwrap_or_else(|e| {
|
let config = Config::from_file(config_path).unwrap_or_else(|e| {
|
||||||
log_warn!("config load failed ({}), using defaults", e);
|
log_warn!("config load failed ({}), using defaults", e);
|
||||||
Config::new()
|
Config::new()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 设置日志级别
|
||||||
let log_level = config.get_or("log.level", "info");
|
let log_level = config.get_or("log.level", "info");
|
||||||
logger::set_level(&log_level);
|
logger::set_level(&log_level);
|
||||||
|
|
||||||
rave::log_info!("rave server starting (tokio async)");
|
log_info!("rave server starting (plugin registry mode)");
|
||||||
|
|
||||||
|
// 创建引擎上下文:StreamManager + 全局服务
|
||||||
let stream_manager = Arc::new(StreamManager::new());
|
let stream_manager = Arc::new(StreamManager::new());
|
||||||
let context = Arc::new(EngineContext::new(stream_manager));
|
let context = Arc::new(EngineContext::new(stream_manager));
|
||||||
|
|
||||||
context.register_service(Arc::new(ServerStats::new()));
|
context.register_service(Arc::new(ServerStats::new()));
|
||||||
|
|
||||||
let rtmp_port: u16 = config.get_or("rtmp.port", "1935").parse().unwrap_or(1935);
|
// 创建插件注册表
|
||||||
let httpflv_port: u16 = config.get_or("httpflv.port", "8080").parse().unwrap_or(8080);
|
let registry = PluginRegistry::new(context);
|
||||||
let rtsp_port: u16 = config.get_or("rtsp.port", "5544").parse().unwrap_or(5544);
|
|
||||||
|
|
||||||
let rtmp_config = RtmpServerConfig {
|
// === 内置插件:自动注册 ===
|
||||||
listen_addr: "0.0.0.0".to_string(),
|
for plugin in protocol::all_plugins() {
|
||||||
port: rtmp_port,
|
registry.register(plugin);
|
||||||
};
|
}
|
||||||
let httpflv_config = HttpFlvServerConfig {
|
|
||||||
listen_addr: "0.0.0.0".to_string(),
|
|
||||||
port: httpflv_port,
|
|
||||||
};
|
|
||||||
let rtsp_config = RtspServerConfig {
|
|
||||||
listen_addr: "0.0.0.0".to_string(),
|
|
||||||
port: rtsp_port,
|
|
||||||
};
|
|
||||||
|
|
||||||
rave::log_info!("======================================");
|
// === 外部插件:手动注册 ===
|
||||||
rave::log_info!(" Rave Streaming Media Server");
|
// 示例:registry.register(Box::new(my_external_plugin::XxxPlugin::new()));
|
||||||
rave::log_info!(" RTMP: rtmp://0.0.0.0:{}/live/test", rtmp_port);
|
|
||||||
rave::log_info!(" HTTP-FLV: http://0.0.0.0:{}/live/test.flv", httpflv_port);
|
|
||||||
rave::log_info!(" RTSP: rtsp://0.0.0.0:{}/live/test", rtsp_port);
|
|
||||||
rave::log_info!("======================================");
|
|
||||||
rave::log_info!("push: ffmpeg -re -i video.mp4 -c copy -f flv rtmp://localhost:{}/live/test", rtmp_port);
|
|
||||||
rave::log_info!("push: ffmpeg -re -i video.mp4 -c copy -f rtsp rtsp://localhost:{}/live/test", rtsp_port);
|
|
||||||
rave::log_info!("watch: ffplay rtmp://localhost:{}/live/test", rtmp_port);
|
|
||||||
rave::log_info!("watch: ffplay http://localhost:{}/live/test.flv", httpflv_port);
|
|
||||||
rave::log_info!("watch: ffplay rtsp://localhost:{}/live/test", rtsp_port);
|
|
||||||
rave::log_info!("======================================");
|
|
||||||
|
|
||||||
let rtmp_server = RtmpServer::new(rtmp_config, context.clone());
|
// 初始化所有插件(读取配置,检查 enabled 标志)
|
||||||
let httpflv_server = HttpFlvServer::new(httpflv_config, context.clone());
|
if let Err(e) = registry.init_all(&config) {
|
||||||
let rtsp_server = RtspServer::new(rtsp_config, context.clone());
|
log_error!("plugin init failed: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 并发运行三个服务器
|
// 打印启动 banner(在 start 之前,确保 banner 不被插件日志打断)
|
||||||
let _ = tokio::join!(
|
log_info!("======================================");
|
||||||
tokio::spawn(async move { rtmp_server.listen().await }),
|
log_info!(" Rave Streaming Media Server");
|
||||||
tokio::spawn(async move { httpflv_server.listen().await }),
|
for meta in registry.list_plugins() {
|
||||||
tokio::spawn(async move { rtsp_server.listen().await }),
|
log_info!(" {} v{} - {}", meta.name, meta.version, meta.description);
|
||||||
);
|
}
|
||||||
|
log_info!("======================================");
|
||||||
|
|
||||||
|
// 启动所有已启用的插件(各插件打印各自的监听地址和使用提示)
|
||||||
|
if let Err(e) = registry.start_all() {
|
||||||
|
log_error!("plugin start failed: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log_info!("======================================");
|
||||||
|
|
||||||
|
// 主线程等待(服务器在 tokio::spawn 中运行)
|
||||||
|
// 监听 Ctrl+C 信号,触发优雅关闭
|
||||||
|
tokio::signal::ctrl_c().await.ok();
|
||||||
|
log_info!("shutting down...");
|
||||||
|
let _ = registry.stop_all();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,6 @@ impl HttpFlvServer {
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("httpflv bind {} failed: {}", addr, e))?;
|
.map_err(|e| format!("httpflv bind {} failed: {}", addr, e))?;
|
||||||
|
|
||||||
log_info!("[httpflv] listening on {}", addr);
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match listener.accept().await {
|
match listener.accept().await {
|
||||||
Ok((tcp_stream, _addr)) => {
|
Ok((tcp_stream, _addr)) => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
//! HTTP-FLV 协议插件
|
||||||
|
//!
|
||||||
|
//! 实现 `Plugin` + `ProtocolPlugin` trait,
|
||||||
|
//! 通过 `PluginRegistry` 统一管理 HTTP-FLV 服务器的生命周期。
|
||||||
|
//!
|
||||||
|
//! 职责:
|
||||||
|
//! - `init()` 阶段从 ConfigProvider 读取 httpflv.port / httpflv.listen_addr
|
||||||
|
//! - `start()` 阶段创建 HttpFlvServer 并 spawn 异步监听任务
|
||||||
|
//! - `stop()` 阶段标记停止
|
||||||
|
|
||||||
|
use std::any::Any;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::log_error;
|
||||||
|
use crate::log_info;
|
||||||
|
use crate::sdk::context::EngineContext;
|
||||||
|
use crate::sdk::plugin::{Plugin, PluginMeta, PluginState, ProtocolPlugin};
|
||||||
|
use crate::sdk::traits::ConfigProvider;
|
||||||
|
|
||||||
|
use super::httpflv_server::{HttpFlvServer, HttpFlvServerConfig};
|
||||||
|
|
||||||
|
/// HTTP-FLV 协议插件
|
||||||
|
///
|
||||||
|
/// 包装 `HttpFlvServer`,提供标准化的插件生命周期管理。
|
||||||
|
/// HTTP-FLV 仅支持拉流(sub only),同时承载 /api/stats 和 /api/streams HTTP API。
|
||||||
|
pub struct HttpFlvPlugin {
|
||||||
|
/// 插件元数据(名称、版本、描述)
|
||||||
|
meta: PluginMeta,
|
||||||
|
/// 当前插件状态
|
||||||
|
state: PluginState,
|
||||||
|
/// 监听配置(从 ConfigProvider 读取)
|
||||||
|
config: HttpFlvServerConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpFlvPlugin {
|
||||||
|
/// 创建 HTTP-FLV 插件(使用默认配置)
|
||||||
|
///
|
||||||
|
/// 默认监听 `0.0.0.0:8080`,在 `init()` 阶段会被配置文件中的值覆盖
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
meta: PluginMeta::new("httpflv", "0.1.0", "HTTP-FLV 拉流协议"),
|
||||||
|
state: PluginState::Created,
|
||||||
|
config: HttpFlvServerConfig::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for HttpFlvPlugin {
|
||||||
|
fn meta(&self) -> &PluginMeta {
|
||||||
|
&self.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初始化:从 ConfigProvider 读取 HTTP-FLV 配置
|
||||||
|
///
|
||||||
|
/// 读取配置项:
|
||||||
|
/// - `httpflv.port` — 监听端口(默认 8080)
|
||||||
|
/// - `httpflv.listen_addr` — 监听地址(默认 "0.0.0.0")
|
||||||
|
fn init(
|
||||||
|
&mut self,
|
||||||
|
_context: Arc<EngineContext>,
|
||||||
|
config: &dyn ConfigProvider,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let port: u16 = config
|
||||||
|
.get("httpflv.port")
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(8080);
|
||||||
|
let addr = config
|
||||||
|
.get("httpflv.listen_addr")
|
||||||
|
.unwrap_or_else(|| "0.0.0.0".to_string());
|
||||||
|
self.config = HttpFlvServerConfig {
|
||||||
|
listen_addr: addr,
|
||||||
|
port,
|
||||||
|
};
|
||||||
|
self.state = PluginState::Initialized;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动:创建 HttpFlvServer 并 spawn 异步监听任务
|
||||||
|
///
|
||||||
|
/// listen() 是 async 方法,通过 tokio::spawn 在后台运行,
|
||||||
|
/// start() 本身立即返回,不阻塞调用者
|
||||||
|
fn start(&mut self, context: Arc<EngineContext>) -> Result<(), String> {
|
||||||
|
let server = HttpFlvServer::new(self.config.clone(), context);
|
||||||
|
let port = self.config.port;
|
||||||
|
let addr = self.config.listen_addr.clone();
|
||||||
|
// 异步启动:spawn listen 循环,start() 立即返回
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = server.listen().await {
|
||||||
|
log_error!("[httpflv] listen error: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log_info!("[httpflv] listening on {}:{}", addr, port);
|
||||||
|
log_info!("[httpflv] watch: ffplay http://localhost:{}/live/test.flv", port);
|
||||||
|
self.state = PluginState::Running;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停止:标记插件为已停止状态
|
||||||
|
fn stop(&mut self) -> Result<(), String> {
|
||||||
|
self.state = PluginState::Stopped;
|
||||||
|
log_info!("[httpflv] stopped");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> PluginState {
|
||||||
|
self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProtocolPlugin for HttpFlvPlugin {
|
||||||
|
/// 协议名称
|
||||||
|
fn protocol_name(&self) -> &str {
|
||||||
|
"httpflv"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 默认端口
|
||||||
|
fn default_port(&self) -> u16 {
|
||||||
|
8080
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::core::group::StreamManager;
|
||||||
|
|
||||||
|
/// 测试用的空配置
|
||||||
|
struct DummyConfig;
|
||||||
|
|
||||||
|
impl ConfigProvider for DummyConfig {
|
||||||
|
fn get(&self, _key: &str) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn get_section(&self, _section: &str) -> Vec<(String, String)> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:插件创建后状态为 Created
|
||||||
|
#[test]
|
||||||
|
fn test_httpflv_plugin_new_state_is_created() {
|
||||||
|
let plugin = HttpFlvPlugin::new();
|
||||||
|
assert_eq!(plugin.state(), PluginState::Created);
|
||||||
|
assert_eq!(plugin.protocol_name(), "httpflv");
|
||||||
|
assert_eq!(plugin.default_port(), 8080);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:init 后状态为 Initialized,使用默认配置
|
||||||
|
#[test]
|
||||||
|
fn test_httpflv_plugin_init_uses_default_config() {
|
||||||
|
let mut plugin = HttpFlvPlugin::new();
|
||||||
|
let ctx = Arc::new(EngineContext::new(Arc::new(StreamManager::new())));
|
||||||
|
let config = DummyConfig;
|
||||||
|
assert!(plugin.init(ctx, &config).is_ok());
|
||||||
|
assert_eq!(plugin.state(), PluginState::Initialized);
|
||||||
|
assert_eq!(plugin.config.port, 8080);
|
||||||
|
assert_eq!(plugin.config.listen_addr, "0.0.0.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:stop 后状态为 Stopped
|
||||||
|
#[test]
|
||||||
|
fn test_httpflv_plugin_stop_changes_state() {
|
||||||
|
let mut plugin = HttpFlvPlugin::new();
|
||||||
|
assert!(plugin.stop().is_ok());
|
||||||
|
assert_eq!(plugin.state(), PluginState::Stopped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,51 @@
|
||||||
//! 协议处理器模块
|
//! 协议处理器模块
|
||||||
//!
|
//!
|
||||||
//! 实现各流媒体协议的握手、解析、会话管理:
|
//! 实现各流媒体协议的握手、解析、会话管理:
|
||||||
//! - `rtmp/` — RTMP 协议:握手、chunk 协议、AMF0 编解码、会话状态机
|
//! - `rtmp/` — RTMP 协议:握手、chunk 协议、AMF0 编解码、会话状态机
|
||||||
//! - `rtsp/` — RTSP/RTP/RTCP 协议:信令交互、RTP 传输、SDP 协商
|
//! - `rtsp/` — RTSP/RTP/RTCP 协议:信令交互、RTP 传输、SDP 协商
|
||||||
//! - `httpflv` — HTTP-FLV 拉流协议:HTTP 请求解析、FLV 流式输出
|
//! - `httpflv` — HTTP-FLV 拉流协议:HTTP 请求解析、FLV 流式输出
|
||||||
//! - `httpflv_server` — HTTP-FLV TCP 服务器:订阅流并转发 FLV 数据
|
//! - `httpflv_server` — HTTP-FLV TCP 服务器:订阅流并转发 FLV 数据
|
||||||
//! - `wsflv` — WebSocket-FLV 拉流协议:WS 握手、二进制帧传输
|
//! - `httpflv_server_plugin` — HTTP-FLV 协议插件(Plugin + ProtocolPlugin 实现)
|
||||||
//! - `hls` — HLS 协议:M3U8 播放列表生成、TS 分片管理
|
//! - `wsflv` — WebSocket-FLV 拉流协议:WS 握手、二进制帧传输
|
||||||
|
//! - `hls` — HLS 协议:M3U8 播放列表生成、TS 分片管理
|
||||||
//!
|
//!
|
||||||
//! 所有协议处理器只依赖 `sdk/` trait 和 `codec/` 类型,不直接引用 `core/`
|
//! 所有协议处理器只依赖 `sdk/` trait 和 `codec/` 类型,不直接引用 `core/`
|
||||||
|
//!
|
||||||
|
//! ## 内置插件自动注册
|
||||||
|
//!
|
||||||
|
//! 本模块通过 [`all_plugins`] 函数提供所有内置协议插件的一次性注册能力。
|
||||||
|
//! 新增内置协议时,只需:
|
||||||
|
//! 1. 在对应协议目录下实现 `Plugin` + `ProtocolPlugin` trait
|
||||||
|
//! 2. 在此文件的 `all_plugins()` 中添加一行 `Box::new(XxxPlugin::new())`
|
||||||
|
//!
|
||||||
|
//! 外部插件仍需通过 `registry.register()` 手动注册。
|
||||||
|
|
||||||
|
use crate::sdk::plugin::Plugin;
|
||||||
|
|
||||||
pub mod rtmp;
|
pub mod rtmp;
|
||||||
pub mod rtsp;
|
pub mod rtsp;
|
||||||
pub mod httpflv;
|
pub mod httpflv;
|
||||||
pub mod httpflv_server;
|
pub mod httpflv_server;
|
||||||
|
pub mod httpflv_server_plugin;
|
||||||
pub mod wsflv;
|
pub mod wsflv;
|
||||||
pub mod hls;
|
pub mod hls;
|
||||||
|
|
||||||
|
/// 返回所有内置协议插件实例
|
||||||
|
///
|
||||||
|
/// 引擎启动时调用此函数获取内置插件列表并批量注册到 PluginRegistry。
|
||||||
|
/// 新增内置协议时在此函数中添加一行即可。
|
||||||
|
///
|
||||||
|
/// # 示例
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// for plugin in protocol::all_plugins() {
|
||||||
|
/// registry.register(plugin);
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn all_plugins() -> Vec<Box<dyn Plugin>> {
|
||||||
|
vec![
|
||||||
|
Box::new(rtmp::plugin::RtmpPlugin::new()),
|
||||||
|
Box::new(rtsp::plugin::RtspPlugin::new()),
|
||||||
|
Box::new(httpflv_server_plugin::HttpFlvPlugin::new()),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
//! - `message` — RTMP 消息类型定义(connect/publish/play/onMetaData 等)
|
//! - `message` — RTMP 消息类型定义(connect/publish/play/onMetaData 等)
|
||||||
//! - `session` — RTMP 会话状态机(握手→连接→推流/拉流)
|
//! - `session` — RTMP 会话状态机(握手→连接→推流/拉流)
|
||||||
//! - `server` — RTMP TCP 监听器,接收连接并分发会话
|
//! - `server` — RTMP TCP 监听器,接收连接并分发会话
|
||||||
|
//! - `plugin` — RTMP 协议插件(Plugin + ProtocolPlugin 实现)
|
||||||
|
|
||||||
pub mod amf0;
|
pub mod amf0;
|
||||||
pub mod handshake;
|
pub mod handshake;
|
||||||
|
|
@ -14,3 +15,4 @@ pub mod chunk;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
pub mod plugin;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
//! RTMP 协议插件
|
||||||
|
//!
|
||||||
|
//! 实现 `Plugin` + `ProtocolPlugin` trait,
|
||||||
|
//! 通过 `PluginRegistry` 统一管理 RTMP 服务器的生命周期。
|
||||||
|
//!
|
||||||
|
//! 职责:
|
||||||
|
//! - `init()` 阶段从 ConfigProvider 读取 rtmp.port / rtmp.listen_addr
|
||||||
|
//! - `start()` 阶段创建 RtmpServer 并 spawn 异步监听任务
|
||||||
|
//! - `stop()` 阶段标记停止(实际 TCP 监听随 tokio 任务结束而关闭)
|
||||||
|
|
||||||
|
use std::any::Any;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::log_error;
|
||||||
|
use crate::log_info;
|
||||||
|
use crate::sdk::context::EngineContext;
|
||||||
|
use crate::sdk::plugin::{Plugin, PluginMeta, PluginState, ProtocolPlugin};
|
||||||
|
use crate::sdk::traits::ConfigProvider;
|
||||||
|
|
||||||
|
use super::server::{RtmpServer, RtmpServerConfig};
|
||||||
|
|
||||||
|
/// RTMP 协议插件
|
||||||
|
///
|
||||||
|
/// 包装 `RtmpServer`,提供标准化的插件生命周期管理。
|
||||||
|
/// 添加新协议只需实现类似的插件包装器并注册到 PluginRegistry。
|
||||||
|
pub struct RtmpPlugin {
|
||||||
|
/// 插件元数据(名称、版本、描述)
|
||||||
|
meta: PluginMeta,
|
||||||
|
/// 当前插件状态
|
||||||
|
state: PluginState,
|
||||||
|
/// 监听配置(从 ConfigProvider 读取)
|
||||||
|
config: RtmpServerConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RtmpPlugin {
|
||||||
|
/// 创建 RTMP 插件(使用默认配置)
|
||||||
|
///
|
||||||
|
/// 默认监听 `0.0.0.0:1935`,在 `init()` 阶段会被配置文件中的值覆盖
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
meta: PluginMeta::new("rtmp", "0.1.0", "RTMP 推拉流协议"),
|
||||||
|
state: PluginState::Created,
|
||||||
|
config: RtmpServerConfig::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for RtmpPlugin {
|
||||||
|
fn meta(&self) -> &PluginMeta {
|
||||||
|
&self.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初始化:从 ConfigProvider 读取 RTMP 配置
|
||||||
|
///
|
||||||
|
/// 读取配置项:
|
||||||
|
/// - `rtmp.port` — 监听端口(默认 1935)
|
||||||
|
/// - `rtmp.listen_addr` — 监听地址(默认 "0.0.0.0")
|
||||||
|
fn init(
|
||||||
|
&mut self,
|
||||||
|
_context: Arc<EngineContext>,
|
||||||
|
config: &dyn ConfigProvider,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let port: u16 = config
|
||||||
|
.get("rtmp.port")
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(1935);
|
||||||
|
let addr = config
|
||||||
|
.get("rtmp.listen_addr")
|
||||||
|
.unwrap_or_else(|| "0.0.0.0".to_string());
|
||||||
|
self.config = RtmpServerConfig {
|
||||||
|
listen_addr: addr,
|
||||||
|
port,
|
||||||
|
};
|
||||||
|
self.state = PluginState::Initialized;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动:创建 RtmpServer 并 spawn 异步监听任务
|
||||||
|
///
|
||||||
|
/// listen() 是 async 方法,通过 tokio::spawn 在后台运行,
|
||||||
|
/// start() 本身立即返回,不阻塞调用者
|
||||||
|
fn start(&mut self, context: Arc<EngineContext>) -> Result<(), String> {
|
||||||
|
let server = RtmpServer::new(self.config.clone(), context);
|
||||||
|
let port = self.config.port;
|
||||||
|
let addr = self.config.listen_addr.clone();
|
||||||
|
// 异步启动:spawn listen 循环,start() 立即返回
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = server.listen().await {
|
||||||
|
log_error!("[rtmp] listen error: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log_info!("[rtmp] listening on {}:{}", addr, port);
|
||||||
|
log_info!("[rtmp] push: ffmpeg -re -i video.mp4 -c copy -f flv rtmp://localhost:{}/live/test", port);
|
||||||
|
log_info!("[rtmp] watch: ffplay rtmp://localhost:{}/live/test", port);
|
||||||
|
self.state = PluginState::Running;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停止:标记插件为已停止状态
|
||||||
|
fn stop(&mut self) -> Result<(), String> {
|
||||||
|
self.state = PluginState::Stopped;
|
||||||
|
log_info!("[rtmp] stopped");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> PluginState {
|
||||||
|
self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProtocolPlugin for RtmpPlugin {
|
||||||
|
/// 协议名称
|
||||||
|
fn protocol_name(&self) -> &str {
|
||||||
|
"rtmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 默认端口
|
||||||
|
fn default_port(&self) -> u16 {
|
||||||
|
1935
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::core::group::StreamManager;
|
||||||
|
|
||||||
|
/// 测试用的空配置
|
||||||
|
struct DummyConfig;
|
||||||
|
|
||||||
|
impl ConfigProvider for DummyConfig {
|
||||||
|
fn get(&self, _key: &str) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn get_section(&self, _section: &str) -> Vec<(String, String)> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:插件创建后状态为 Created
|
||||||
|
#[test]
|
||||||
|
fn test_rtmp_plugin_new_state_is_created() {
|
||||||
|
let plugin = RtmpPlugin::new();
|
||||||
|
assert_eq!(plugin.state(), PluginState::Created);
|
||||||
|
assert_eq!(plugin.protocol_name(), "rtmp");
|
||||||
|
assert_eq!(plugin.default_port(), 1935);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:init 后状态为 Initialized,使用默认配置
|
||||||
|
#[test]
|
||||||
|
fn test_rtmp_plugin_init_uses_default_config() {
|
||||||
|
let mut plugin = RtmpPlugin::new();
|
||||||
|
let ctx = Arc::new(EngineContext::new(Arc::new(StreamManager::new())));
|
||||||
|
let config = DummyConfig;
|
||||||
|
assert!(plugin.init(ctx, &config).is_ok());
|
||||||
|
assert_eq!(plugin.state(), PluginState::Initialized);
|
||||||
|
assert_eq!(plugin.config.port, 1935);
|
||||||
|
assert_eq!(plugin.config.listen_addr, "0.0.0.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:stop 后状态为 Stopped
|
||||||
|
#[test]
|
||||||
|
fn test_rtmp_plugin_stop_changes_state() {
|
||||||
|
let mut plugin = RtmpPlugin::new();
|
||||||
|
assert!(plugin.stop().is_ok());
|
||||||
|
assert_eq!(plugin.state(), PluginState::Stopped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,6 @@ use tokio::net::TcpListener;
|
||||||
|
|
||||||
use crate::log_error;
|
use crate::log_error;
|
||||||
use crate::log_info;
|
use crate::log_info;
|
||||||
use crate::log_warn;
|
|
||||||
|
|
||||||
use crate::protocol::rtmp::chunk::{
|
use crate::protocol::rtmp::chunk::{
|
||||||
self, ChunkFmt, RtmpMessageHeader, message_to_chunks, msg_type_id,
|
self, ChunkFmt, RtmpMessageHeader, message_to_chunks, msg_type_id,
|
||||||
|
|
@ -66,8 +65,6 @@ impl RtmpServer {
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("failed to bind {}: {}", addr, e))?;
|
.map_err(|e| format!("failed to bind {}: {}", addr, e))?;
|
||||||
|
|
||||||
log_info!("[rtmp] listening on {}", addr);
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match listener.accept().await {
|
match listener.accept().await {
|
||||||
Ok((tcp_stream, _addr)) => {
|
Ok((tcp_stream, _addr)) => {
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,6 @@ pub struct RtpDepacketizer {
|
||||||
/// RTP 时间戳从随机值开始,转换为毫秒后可能超过 RTMP 扩展时间戳阈值 (0xFFFFFF),
|
/// RTP 时间戳从随机值开始,转换为毫秒后可能超过 RTMP 扩展时间戳阈值 (0xFFFFFF),
|
||||||
/// 导致 ffmpeg 解析失败。记录首个帧的 RTP 时间戳,后续帧减去此基准值。
|
/// 导致 ffmpeg 解析失败。记录首个帧的 RTP 时间戳,后续帧减去此基准值。
|
||||||
video_ts_base: Option<u32>,
|
video_ts_base: Option<u32>,
|
||||||
/// 音频时间戳归一化基准
|
|
||||||
audio_ts_base: Option<u32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RtpDepacketizer {
|
impl RtpDepacketizer {
|
||||||
|
|
@ -100,7 +98,6 @@ impl RtpDepacketizer {
|
||||||
pending_nalus: Vec::new(),
|
pending_nalus: Vec::new(),
|
||||||
pending_timestamp: 0,
|
pending_timestamp: 0,
|
||||||
video_ts_base: None,
|
video_ts_base: None,
|
||||||
audio_ts_base: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -419,14 +416,6 @@ impl RtpDepacketizer {
|
||||||
// 使用 wrapping_sub 处理 RTP 时间戳回绕(32-bit 溢出)
|
// 使用 wrapping_sub 处理 RTP 时间戳回绕(32-bit 溢出)
|
||||||
rtp_ts.wrapping_sub(self.video_ts_base.unwrap())
|
rtp_ts.wrapping_sub(self.video_ts_base.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 归一化音频 RTP 时间戳
|
|
||||||
fn normalize_audio_ts(&mut self, rtp_ts: u32) -> u32 {
|
|
||||||
if self.audio_ts_base.is_none() {
|
|
||||||
self.audio_ts_base = Some(rtp_ts);
|
|
||||||
}
|
|
||||||
rtp_ts.wrapping_sub(self.audio_ts_base.unwrap())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── RTP 音频解包器 ─────────────────────────────────────────
|
// ── RTP 音频解包器 ─────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
//! - `rtcp` — RTCP 报文(Sender Report)
|
//! - `rtcp` — RTCP 报文(Sender Report)
|
||||||
//! - `session` — RTSP 会话状态机
|
//! - `session` — RTSP 会话状态机
|
||||||
//! - `server` — RTSP TCP 监听器
|
//! - `server` — RTSP TCP 监听器
|
||||||
|
//! - `plugin` — RTSP 协议插件(Plugin + ProtocolPlugin 实现)
|
||||||
|
|
||||||
pub mod request;
|
pub mod request;
|
||||||
pub mod response;
|
pub mod response;
|
||||||
|
|
@ -17,3 +18,4 @@ pub mod rtcp;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod depacketizer;
|
pub mod depacketizer;
|
||||||
|
pub mod plugin;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
//! RTSP 协议插件
|
||||||
|
//!
|
||||||
|
//! 实现 `Plugin` + `ProtocolPlugin` trait,
|
||||||
|
//! 通过 `PluginRegistry` 统一管理 RTSP 服务器的生命周期。
|
||||||
|
//!
|
||||||
|
//! 职责:
|
||||||
|
//! - `init()` 阶段从 ConfigProvider 读取 rtsp.port / rtsp.listen_addr
|
||||||
|
//! - `start()` 阶段创建 RtspServer 并 spawn 异步监听任务
|
||||||
|
//! - `stop()` 阶段标记停止
|
||||||
|
|
||||||
|
use std::any::Any;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::log_error;
|
||||||
|
use crate::log_info;
|
||||||
|
use crate::sdk::context::EngineContext;
|
||||||
|
use crate::sdk::plugin::{Plugin, PluginMeta, PluginState, ProtocolPlugin};
|
||||||
|
use crate::sdk::traits::ConfigProvider;
|
||||||
|
|
||||||
|
use super::server::{RtspServer, RtspServerConfig};
|
||||||
|
|
||||||
|
/// RTSP 协议插件
|
||||||
|
///
|
||||||
|
/// 包装 `RtspServer`,提供标准化的插件生命周期管理。
|
||||||
|
/// RTSP 同时支持推流(ANNOUNCE/RECORD)和拉流(DESCRIBE/PLAY),
|
||||||
|
/// 传输层支持 TCP interleaved 和 UDP 两种模式。
|
||||||
|
pub struct RtspPlugin {
|
||||||
|
/// 插件元数据(名称、版本、描述)
|
||||||
|
meta: PluginMeta,
|
||||||
|
/// 当前插件状态
|
||||||
|
state: PluginState,
|
||||||
|
/// 监听配置(从 ConfigProvider 读取)
|
||||||
|
config: RtspServerConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RtspPlugin {
|
||||||
|
/// 创建 RTSP 插件(使用默认配置)
|
||||||
|
///
|
||||||
|
/// 默认监听 `0.0.0.0:5544`,在 `init()` 阶段会被配置文件中的值覆盖
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
meta: PluginMeta::new("rtsp", "0.1.0", "RTSP/RTP 推拉流协议"),
|
||||||
|
state: PluginState::Created,
|
||||||
|
config: RtspServerConfig::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for RtspPlugin {
|
||||||
|
fn meta(&self) -> &PluginMeta {
|
||||||
|
&self.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初始化:从 ConfigProvider 读取 RTSP 配置
|
||||||
|
///
|
||||||
|
/// 读取配置项:
|
||||||
|
/// - `rtsp.port` — 监听端口(默认 5544)
|
||||||
|
/// - `rtsp.listen_addr` — 监听地址(默认 "0.0.0.0")
|
||||||
|
fn init(
|
||||||
|
&mut self,
|
||||||
|
_context: Arc<EngineContext>,
|
||||||
|
config: &dyn ConfigProvider,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let port: u16 = config
|
||||||
|
.get("rtsp.port")
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(5544);
|
||||||
|
let addr = config
|
||||||
|
.get("rtsp.listen_addr")
|
||||||
|
.unwrap_or_else(|| "0.0.0.0".to_string());
|
||||||
|
self.config = RtspServerConfig {
|
||||||
|
listen_addr: addr,
|
||||||
|
port,
|
||||||
|
};
|
||||||
|
self.state = PluginState::Initialized;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动:创建 RtspServer 并 spawn 异步监听任务
|
||||||
|
///
|
||||||
|
/// listen() 是 async 方法,通过 tokio::spawn 在后台运行,
|
||||||
|
/// start() 本身立即返回,不阻塞调用者
|
||||||
|
fn start(&mut self, context: Arc<EngineContext>) -> Result<(), String> {
|
||||||
|
let server = RtspServer::new(self.config.clone(), context);
|
||||||
|
let port = self.config.port;
|
||||||
|
let addr = self.config.listen_addr.clone();
|
||||||
|
// 异步启动:spawn listen 循环,start() 立即返回
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = server.listen().await {
|
||||||
|
log_error!("[rtsp] listen error: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log_info!("[rtsp] listening on {}:{}", addr, port);
|
||||||
|
log_info!("[rtsp] push: ffmpeg -re -i video.mp4 -c copy -f rtsp rtsp://localhost:{}/live/test", port);
|
||||||
|
log_info!("[rtsp] watch: ffplay rtsp://localhost:{}/live/test", port);
|
||||||
|
self.state = PluginState::Running;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停止:标记插件为已停止状态
|
||||||
|
fn stop(&mut self) -> Result<(), String> {
|
||||||
|
self.state = PluginState::Stopped;
|
||||||
|
log_info!("[rtsp] stopped");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> PluginState {
|
||||||
|
self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProtocolPlugin for RtspPlugin {
|
||||||
|
/// 协议名称
|
||||||
|
fn protocol_name(&self) -> &str {
|
||||||
|
"rtsp"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 默认端口
|
||||||
|
fn default_port(&self) -> u16 {
|
||||||
|
5544
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::core::group::StreamManager;
|
||||||
|
|
||||||
|
/// 测试用的空配置
|
||||||
|
struct DummyConfig;
|
||||||
|
|
||||||
|
impl ConfigProvider for DummyConfig {
|
||||||
|
fn get(&self, _key: &str) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn get_section(&self, _section: &str) -> Vec<(String, String)> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:插件创建后状态为 Created
|
||||||
|
#[test]
|
||||||
|
fn test_rtsp_plugin_new_state_is_created() {
|
||||||
|
let plugin = RtspPlugin::new();
|
||||||
|
assert_eq!(plugin.state(), PluginState::Created);
|
||||||
|
assert_eq!(plugin.protocol_name(), "rtsp");
|
||||||
|
assert_eq!(plugin.default_port(), 5544);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:init 后状态为 Initialized,使用默认配置
|
||||||
|
#[test]
|
||||||
|
fn test_rtsp_plugin_init_uses_default_config() {
|
||||||
|
let mut plugin = RtspPlugin::new();
|
||||||
|
let ctx = Arc::new(EngineContext::new(Arc::new(StreamManager::new())));
|
||||||
|
let config = DummyConfig;
|
||||||
|
assert!(plugin.init(ctx, &config).is_ok());
|
||||||
|
assert_eq!(plugin.state(), PluginState::Initialized);
|
||||||
|
assert_eq!(plugin.config.port, 5544);
|
||||||
|
assert_eq!(plugin.config.listen_addr, "0.0.0.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:stop 后状态为 Stopped
|
||||||
|
#[test]
|
||||||
|
fn test_rtsp_plugin_stop_changes_state() {
|
||||||
|
let mut plugin = RtspPlugin::new();
|
||||||
|
assert!(plugin.stop().is_ok());
|
||||||
|
assert_eq!(plugin.state(), PluginState::Stopped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -221,6 +221,14 @@ impl H264RtpPacketizer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取当前序列号(下一个将要使用的序列号)
|
||||||
|
///
|
||||||
|
/// 用于 RTSP PLAY 响应的 RTP-Info 头中,
|
||||||
|
/// 返回初始序列号(首个 RTP 包的 seq)。
|
||||||
|
pub fn initial_seq(&self) -> u16 {
|
||||||
|
self.seq
|
||||||
|
}
|
||||||
|
|
||||||
/// 将 H.264 NALU 打包为一个或多个 RTP 包
|
/// 将 H.264 NALU 打包为一个或多个 RTP 包
|
||||||
///
|
///
|
||||||
/// 输入 NALU 不含起始码(AnnexB start code 已去除)。
|
/// 输入 NALU 不含起始码(AnnexB start code 已去除)。
|
||||||
|
|
@ -333,6 +341,14 @@ impl AacRtpPacketizer {
|
||||||
Self { seq: 0, ssrc, pt }
|
Self { seq: 0, ssrc, pt }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取当前序列号(下一个将要使用的序列号)
|
||||||
|
///
|
||||||
|
/// 用于 RTSP PLAY 响应的 RTP-Info 头中,
|
||||||
|
/// 返回初始序列号(首个 RTP 包的 seq)。
|
||||||
|
pub fn initial_seq(&self) -> u16 {
|
||||||
|
self.seq
|
||||||
|
}
|
||||||
|
|
||||||
/// 将 AAC 帧打包为 RTP 包
|
/// 将 AAC 帧打包为 RTP 包
|
||||||
///
|
///
|
||||||
/// AAC-hbr 模式负载格式:
|
/// AAC-hbr 模式负载格式:
|
||||||
|
|
@ -637,4 +653,27 @@ mod tests {
|
||||||
assert_eq!(pkt2.sequence, 1);
|
assert_eq!(pkt2.sequence, 1);
|
||||||
assert_eq!(pkt3.sequence, 2, "序列号应递增");
|
assert_eq!(pkt3.sequence, 2, "序列号应递增");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 测试:H.264 打包器 initial_seq 返回当前序列号
|
||||||
|
#[test]
|
||||||
|
fn test_h264_packetizer_initial_seq_returns_current_seq() {
|
||||||
|
let mut packetizer = H264RtpPacketizer::new(0x11111111, 96);
|
||||||
|
assert_eq!(packetizer.initial_seq(), 0, "初始序列号应为 0");
|
||||||
|
|
||||||
|
// 打包一帧后序列号应递增
|
||||||
|
let nalu = vec![0x65, 0x01, 0x02];
|
||||||
|
packetizer.packetize(&nalu, 90000, true);
|
||||||
|
assert_eq!(packetizer.initial_seq(), 1, "打包一帧后 initial_seq 应为 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:AAC 打包器 initial_seq 返回当前序列号
|
||||||
|
#[test]
|
||||||
|
fn test_aac_packetizer_initial_seq_returns_current_seq() {
|
||||||
|
let mut packetizer = AacRtpPacketizer::new(0x77777777, 97);
|
||||||
|
assert_eq!(packetizer.initial_seq(), 0, "初始序列号应为 0");
|
||||||
|
|
||||||
|
let aac_data = vec![0x01, 0x02];
|
||||||
|
packetizer.packetize(&aac_data, 100);
|
||||||
|
assert_eq!(packetizer.initial_seq(), 1, "打包一帧后 initial_seq 应为 1");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ pub struct SdpInfo {
|
||||||
pub conn_addr: String,
|
pub conn_addr: String,
|
||||||
/// 媒体描述列表
|
/// 媒体描述列表
|
||||||
pub medias: Vec<SdpMedia>,
|
pub medias: Vec<SdpMedia>,
|
||||||
|
/// 是否为 recvonly 会话(拉流会话标记为 recvonly)
|
||||||
|
pub recvonly: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SdpInfo {
|
impl SdpInfo {
|
||||||
|
|
@ -80,6 +82,7 @@ impl SdpInfo {
|
||||||
stream_name: stream_name.to_string(),
|
stream_name: stream_name.to_string(),
|
||||||
conn_addr: addr.to_string(),
|
conn_addr: addr.to_string(),
|
||||||
medias: Vec::new(),
|
medias: Vec::new(),
|
||||||
|
recvonly: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,23 +91,52 @@ impl SdpInfo {
|
||||||
/// # 参数
|
/// # 参数
|
||||||
/// - `control`: 控制轨道 ID(如 "track1")
|
/// - `control`: 控制轨道 ID(如 "track1")
|
||||||
/// - `sprop_parameter_sets`: SPS+PPS 的 Base64 编码(可选)
|
/// - `sprop_parameter_sets`: SPS+PPS 的 Base64 编码(可选)
|
||||||
pub fn add_h264_video(&mut self, control: &str, sprop_parameter_sets: Option<&str>) {
|
/// - `profile_level_id`: profile-level-id 十六进制字符串(如 "640029"),可选
|
||||||
// 构建 fmtp 参数:包含 profile-level-id、sprop-parameter-sets、packetization-mode
|
pub fn add_h264_video(&mut self, control: &str, sprop_parameter_sets: Option<&str>, profile_level_id: Option<&str>) {
|
||||||
let fmtp = sprop_parameter_sets.map(|sps| {
|
// 构建 fmtp 参数
|
||||||
format!(
|
// 如果有 sprop_parameter_sets,需要完整的 fmtp;否则可省略
|
||||||
"profile-level-id=640029;sprop-parameter-sets={};packetization-mode=1",
|
if let Some(sprop) = sprop_parameter_sets {
|
||||||
sps
|
let pli = profile_level_id.unwrap_or("640029");
|
||||||
)
|
let fmtp = format!(
|
||||||
});
|
"profile-level-id={};sprop-parameter-sets={};packetization-mode=1",
|
||||||
self.medias.push(SdpMedia {
|
pli, sprop
|
||||||
media_type: "video".to_string(),
|
);
|
||||||
payload_type: 96, // H.264 动态 payload type
|
self.medias.push(SdpMedia {
|
||||||
codec_name: "H264".to_string(),
|
media_type: "video".to_string(),
|
||||||
clock_rate: 90000, // 视频固定 90kHz
|
payload_type: 96,
|
||||||
channels: 0, // 视频不使用通道数
|
codec_name: "H264".to_string(),
|
||||||
fmtp,
|
clock_rate: 90000,
|
||||||
control: control.to_string(),
|
channels: 0,
|
||||||
});
|
fmtp: Some(fmtp),
|
||||||
|
control: control.to_string(),
|
||||||
|
});
|
||||||
|
} else if let Some(pli) = profile_level_id {
|
||||||
|
// 有 profile-level-id 但无 sprop
|
||||||
|
let fmtp = format!(
|
||||||
|
"profile-level-id={};packetization-mode=1",
|
||||||
|
pli
|
||||||
|
);
|
||||||
|
self.medias.push(SdpMedia {
|
||||||
|
media_type: "video".to_string(),
|
||||||
|
payload_type: 96,
|
||||||
|
codec_name: "H264".to_string(),
|
||||||
|
clock_rate: 90000,
|
||||||
|
channels: 0,
|
||||||
|
fmtp: Some(fmtp),
|
||||||
|
control: control.to_string(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 无任何参数,使用默认 fmtp
|
||||||
|
self.medias.push(SdpMedia {
|
||||||
|
media_type: "video".to_string(),
|
||||||
|
payload_type: 96,
|
||||||
|
codec_name: "H264".to_string(),
|
||||||
|
clock_rate: 90000,
|
||||||
|
channels: 0,
|
||||||
|
fmtp: None,
|
||||||
|
control: control.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 添加 H.265/HEVC 视频媒体描述
|
/// 添加 H.265/HEVC 视频媒体描述
|
||||||
|
|
@ -129,11 +161,18 @@ impl SdpInfo {
|
||||||
/// - `control`: 控制轨道 ID(如 "track2")
|
/// - `control`: 控制轨道 ID(如 "track2")
|
||||||
/// - `sample_rate`: 采样率(如 44100、48000、8000)
|
/// - `sample_rate`: 采样率(如 44100、48000、8000)
|
||||||
/// - `channels`: 通道数(如 2 表示立体声)
|
/// - `channels`: 通道数(如 2 表示立体声)
|
||||||
pub fn add_aac_audio(&mut self, control: &str, sample_rate: u32, channels: u8) {
|
/// - `config_hex`: AAC AudioSpecificConfig 的十六进制编码字符串(可选)
|
||||||
|
pub fn add_aac_audio(&mut self, control: &str, sample_rate: u32, channels: u8, config_hex: Option<&str>) {
|
||||||
// AAC-hbr 模式的 fmtp 参数
|
// AAC-hbr 模式的 fmtp 参数
|
||||||
let fmtp = format!(
|
let fmtp = if let Some(hex) = config_hex {
|
||||||
"streamtype=5; profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config="
|
format!(
|
||||||
);
|
"streamtype=5; profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config={}",
|
||||||
|
hex
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 无 config 时仍生成有效 fmtp(某些客户端支持带内 config)
|
||||||
|
"streamtype=5; profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3".to_string()
|
||||||
|
};
|
||||||
self.medias.push(SdpMedia {
|
self.medias.push(SdpMedia {
|
||||||
media_type: "audio".to_string(),
|
media_type: "audio".to_string(),
|
||||||
payload_type: 97, // AAC 动态 payload type
|
payload_type: 97, // AAC 动态 payload type
|
||||||
|
|
@ -169,6 +208,11 @@ impl SdpInfo {
|
||||||
// 时间描述(0 0 表示永久会话)
|
// 时间描述(0 0 表示永久会话)
|
||||||
s.push_str("t=0 0\r\n");
|
s.push_str("t=0 0\r\n");
|
||||||
|
|
||||||
|
// 拉流会话标记为 recvonly
|
||||||
|
if self.recvonly {
|
||||||
|
s.push_str("a=recvonly\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
// 逐个输出媒体描述
|
// 逐个输出媒体描述
|
||||||
for media in &self.medias {
|
for media in &self.medias {
|
||||||
// m= 行:端口 0 表示服务器选择(interleaved 模式下不使用 UDP 端口)
|
// m= 行:端口 0 表示服务器选择(interleaved 模式下不使用 UDP 端口)
|
||||||
|
|
@ -306,7 +350,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sdp_generate_h264_video_only_succeeds() {
|
fn test_sdp_generate_h264_video_only_succeeds() {
|
||||||
let mut sdp = SdpInfo::new("live/test", "192.168.1.100");
|
let mut sdp = SdpInfo::new("live/test", "192.168.1.100");
|
||||||
sdp.add_h264_video("track1", Some("Z0LAHtkA2Q=,aMsg"));
|
sdp.add_h264_video("track1", Some("Z0LAHtkA2Q=,aMsg"), None);
|
||||||
|
|
||||||
let text = sdp.to_sdp();
|
let text = sdp.to_sdp();
|
||||||
|
|
||||||
|
|
@ -340,21 +384,21 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sdp_generate_h264_aac_succeeds() {
|
fn test_sdp_generate_h264_aac_succeeds() {
|
||||||
let mut sdp = SdpInfo::new("live/test", "10.0.0.1");
|
let mut sdp = SdpInfo::new("live/test", "10.0.0.1");
|
||||||
sdp.add_h264_video("track1", None);
|
sdp.add_h264_video("track1", None, None);
|
||||||
sdp.add_aac_audio("track2", 44100, 2);
|
sdp.add_aac_audio("track2", 44100, 2, None);
|
||||||
|
|
||||||
let text = sdp.to_sdp();
|
let text = sdp.to_sdp();
|
||||||
|
|
||||||
// 验证视频部分
|
// 验证视频部分
|
||||||
assert!(text.contains("m=video 0 RTP/AVP 96\r\n"));
|
assert!(text.contains("m=video 0 RTP/AVP 96\r\n"));
|
||||||
assert!(text.contains("a=rtpmap:96 H264/90000\r\n"));
|
assert!(text.contains("a=rtpmap:96 H264/90000\r\n"));
|
||||||
// 没有 fmtp(sprop_parameter_sets 为 None)
|
// 没有 fmtp(sprop_parameter_sets 为 None 且 profile_level_id 也为 None)
|
||||||
let lines: Vec<&str> = text.lines().collect();
|
let lines: Vec<&str> = text.lines().collect();
|
||||||
let video_fmtp_count = lines
|
let video_fmtp_count = lines
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|l| l.starts_with("a=fmtp:96"))
|
.filter(|l| l.starts_with("a=fmtp:96"))
|
||||||
.count();
|
.count();
|
||||||
assert_eq!(video_fmtp_count, 0, "H.264 无 sprop 参数时不应有 fmtp 行");
|
assert_eq!(video_fmtp_count, 0, "H.264 无参数时不应有 fmtp 行");
|
||||||
|
|
||||||
// 验证音频部分
|
// 验证音频部分
|
||||||
assert!(text.contains("m=audio 0 RTP/AVP 97\r\n"));
|
assert!(text.contains("m=audio 0 RTP/AVP 97\r\n"));
|
||||||
|
|
@ -431,8 +475,8 @@ a=control:track2\r\n";
|
||||||
let mut original = SdpInfo::new("live/roundtrip", "10.0.0.1");
|
let mut original = SdpInfo::new("live/roundtrip", "10.0.0.1");
|
||||||
original.session_id = 111;
|
original.session_id = 111;
|
||||||
original.session_version = 222;
|
original.session_version = 222;
|
||||||
original.add_h264_video("track1", None);
|
original.add_h264_video("track1", None, None);
|
||||||
original.add_aac_audio("track2", 48000, 2);
|
original.add_aac_audio("track2", 48000, 2, None);
|
||||||
|
|
||||||
// 生成 SDP 文本
|
// 生成 SDP 文本
|
||||||
let sdp_text = original.to_sdp();
|
let sdp_text = original.to_sdp();
|
||||||
|
|
@ -472,7 +516,7 @@ a=control:track2\r\n";
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sdp_rtpmap_format_succeeds() {
|
fn test_sdp_rtpmap_format_succeeds() {
|
||||||
let mut sdp = SdpInfo::new("test", "127.0.0.1");
|
let mut sdp = SdpInfo::new("test", "127.0.0.1");
|
||||||
sdp.add_h264_video("track1", None);
|
sdp.add_h264_video("track1", None, None);
|
||||||
|
|
||||||
let text = sdp.to_sdp();
|
let text = sdp.to_sdp();
|
||||||
// 视频 rtpmap 不应包含通道数
|
// 视频 rtpmap 不应包含通道数
|
||||||
|
|
@ -482,7 +526,7 @@ a=control:track2\r\n";
|
||||||
);
|
);
|
||||||
|
|
||||||
// 添加音频后验证通道数
|
// 添加音频后验证通道数
|
||||||
sdp.add_aac_audio("track2", 44100, 2);
|
sdp.add_aac_audio("track2", 44100, 2, None);
|
||||||
let text = sdp.to_sdp();
|
let text = sdp.to_sdp();
|
||||||
assert!(
|
assert!(
|
||||||
text.contains("a=rtpmap:97 mpeg4-generic/44100/2\r\n"),
|
text.contains("a=rtpmap:97 mpeg4-generic/44100/2\r\n"),
|
||||||
|
|
@ -547,4 +591,58 @@ a=control:track1\r\n";
|
||||||
"session_id 应为当前 Unix 时间戳"
|
"session_id 应为当前 Unix 时间戳"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 测试:add_aac_audio 带 config hex 参数生成正确的 fmtp
|
||||||
|
#[test]
|
||||||
|
fn test_sdp_aac_audio_with_config_hex_succeeds() {
|
||||||
|
let mut sdp = SdpInfo::new("test", "127.0.0.1");
|
||||||
|
sdp.add_aac_audio("track2", 48000, 2, Some("1190"));
|
||||||
|
|
||||||
|
let text = sdp.to_sdp();
|
||||||
|
assert!(text.contains("config=1190"), "应包含 config 参数值");
|
||||||
|
assert!(text.contains("a=rtpmap:97 mpeg4-generic/48000/2"), "采样率应为 48000");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:add_h264_video 带 profile-level-id 参数生成正确的 fmtp
|
||||||
|
#[test]
|
||||||
|
fn test_sdp_h264_video_with_profile_level_id_succeeds() {
|
||||||
|
let mut sdp = SdpInfo::new("test", "127.0.0.1");
|
||||||
|
sdp.add_h264_video("track1", Some("Z0LAHtkA2Q=,aMsg"), Some("640029"));
|
||||||
|
|
||||||
|
let text = sdp.to_sdp();
|
||||||
|
assert!(text.contains("profile-level-id=640029"), "应包含指定的 profile-level-id");
|
||||||
|
assert!(text.contains("sprop-parameter-sets=Z0LAHtkA2Q=,aMsg"), "应包含 sprop");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:recvonly 标记生成 a=recvonly 行
|
||||||
|
#[test]
|
||||||
|
fn test_sdp_recvonly_flag_generates_attribute() {
|
||||||
|
let mut sdp = SdpInfo::new("test", "127.0.0.1");
|
||||||
|
sdp.recvonly = true;
|
||||||
|
sdp.add_h264_video("track1", None, None);
|
||||||
|
|
||||||
|
let text = sdp.to_sdp();
|
||||||
|
assert!(text.contains("a=recvonly\r\n"), "recvonly 标记应生成 a=recvonly 行");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:recvonly 默认为 false 时不生成 a=recvonly 行
|
||||||
|
#[test]
|
||||||
|
fn test_sdp_no_recvonly_by_default() {
|
||||||
|
let mut sdp = SdpInfo::new("test", "127.0.0.1");
|
||||||
|
sdp.add_h264_video("track1", None, None);
|
||||||
|
|
||||||
|
let text = sdp.to_sdp();
|
||||||
|
assert!(!text.contains("a=recvonly"), "默认不应包含 recvonly");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:add_aac_audio 无 config hex 时 fmtp 不含 config=
|
||||||
|
#[test]
|
||||||
|
fn test_sdp_aac_audio_without_config_hex_succeeds() {
|
||||||
|
let mut sdp = SdpInfo::new("test", "127.0.0.1");
|
||||||
|
sdp.add_aac_audio("track2", 44100, 2, None);
|
||||||
|
|
||||||
|
let text = sdp.to_sdp();
|
||||||
|
assert!(!text.contains("config="), "无 config 时不包含 config= 参数");
|
||||||
|
assert!(text.contains("mode=AAC-hbr"), "仍应有 AAC-hbr 模式");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,12 @@ use crate::stats::ServerStats;
|
||||||
|
|
||||||
use super::request::RtspRequest;
|
use super::request::RtspRequest;
|
||||||
use super::session::{RtspSession, TransportMode};
|
use super::session::{RtspSession, TransportMode};
|
||||||
|
use super::rtcp::SenderReport;
|
||||||
|
|
||||||
/// RTSP 服务器配置
|
/// RTSP 服务器配置
|
||||||
///
|
///
|
||||||
/// 控制监听地址和端口
|
/// 控制监听地址和端口
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct RtspServerConfig {
|
pub struct RtspServerConfig {
|
||||||
/// 监听地址(如 "0.0.0.0")
|
/// 监听地址(如 "0.0.0.0")
|
||||||
pub listen_addr: String,
|
pub listen_addr: String,
|
||||||
|
|
@ -75,8 +77,6 @@ impl RtspServer {
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("RTSP bind {} failed: {}", addr, e))?;
|
.map_err(|e| format!("RTSP bind {} failed: {}", addr, e))?;
|
||||||
|
|
||||||
log_info!("[rtsp] listening on {}", addr);
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match listener.accept().await {
|
match listener.accept().await {
|
||||||
Ok((tcp_stream, _addr)) => {
|
Ok((tcp_stream, _addr)) => {
|
||||||
|
|
@ -129,6 +129,14 @@ async fn handle_rtsp_client(mut stream: tokio::net::TcpStream, context: Arc<Engi
|
||||||
let mut audio_udp: Option<tokio::net::UdpSocket> = None;
|
let mut audio_udp: Option<tokio::net::UdpSocket> = None;
|
||||||
// UDP 读取缓冲区
|
// UDP 读取缓冲区
|
||||||
let mut udp_buf = [0u8; 65536];
|
let mut udp_buf = [0u8; 65536];
|
||||||
|
// RTCP 发送间隔追踪(每 5 秒发送一次 Sender Report)
|
||||||
|
let mut last_rtcp_time = tokio::time::Instant::now();
|
||||||
|
// RTCP 发送统计(视频)
|
||||||
|
let mut video_rtcp_pkt_count: u32 = 0;
|
||||||
|
let mut video_rtcp_octet_count: u32 = 0;
|
||||||
|
// RTCP 发送统计(音频)
|
||||||
|
let mut audio_rtcp_pkt_count: u32 = 0;
|
||||||
|
let mut audio_rtcp_octet_count: u32 = 0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// === Playing 模式:从订阅者队列读取帧并发送 RTP ===
|
// === Playing 模式:从订阅者队列读取帧并发送 RTP ===
|
||||||
|
|
@ -146,6 +154,14 @@ async fn handle_rtsp_client(mut stream: tokio::net::TcpStream, context: Arc<Engi
|
||||||
// TCP interleaved 模式:写入 TCP 流
|
// TCP interleaved 模式:写入 TCP 流
|
||||||
let rtp_data = session.frame_to_rtp_interleaved(&frame);
|
let rtp_data = session.frame_to_rtp_interleaved(&frame);
|
||||||
for (_channel, data) in rtp_data {
|
for (_channel, data) in rtp_data {
|
||||||
|
// 累计 RTCP 统计
|
||||||
|
if frame.is_video() {
|
||||||
|
video_rtcp_pkt_count += 1;
|
||||||
|
video_rtcp_octet_count += data.len() as u32;
|
||||||
|
} else if frame.is_audio() {
|
||||||
|
audio_rtcp_pkt_count += 1;
|
||||||
|
audio_rtcp_octet_count += data.len() as u32;
|
||||||
|
}
|
||||||
if stream.write_all(&data).await.is_err() {
|
if stream.write_all(&data).await.is_err() {
|
||||||
session.cleanup(&sm);
|
session.cleanup(&sm);
|
||||||
log_info!("[rtsp:{}] client disconnected", peer);
|
log_info!("[rtsp:{}] client disconnected", peer);
|
||||||
|
|
@ -157,6 +173,16 @@ async fn handle_rtsp_client(mut stream: tokio::net::TcpStream, context: Arc<Engi
|
||||||
// UDP 模式:通过 UDP socket 发送 RTP 包
|
// UDP 模式:通过 UDP socket 发送 RTP 包
|
||||||
let rtp_packets = session.frame_to_rtp_packets(&frame);
|
let rtp_packets = session.frame_to_rtp_packets(&frame);
|
||||||
for (is_video, rtp_pkt) in rtp_packets {
|
for (is_video, rtp_pkt) in rtp_packets {
|
||||||
|
// 累计 RTCP 统计
|
||||||
|
let rtp_bytes = rtp_pkt.encode();
|
||||||
|
if is_video {
|
||||||
|
video_rtcp_pkt_count += 1;
|
||||||
|
video_rtcp_octet_count += rtp_bytes.len() as u32;
|
||||||
|
} else {
|
||||||
|
audio_rtcp_pkt_count += 1;
|
||||||
|
audio_rtcp_octet_count += rtp_bytes.len() as u32;
|
||||||
|
}
|
||||||
|
|
||||||
let udp_socket = if is_video {
|
let udp_socket = if is_video {
|
||||||
video_udp.as_ref()
|
video_udp.as_ref()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -169,7 +195,6 @@ async fn handle_rtsp_client(mut stream: tokio::net::TcpStream, context: Arc<Engi
|
||||||
};
|
};
|
||||||
|
|
||||||
if let (Some(socket), Some(dest_addr)) = (udp_socket, dest) {
|
if let (Some(socket), Some(dest_addr)) = (udp_socket, dest) {
|
||||||
let rtp_bytes = rtp_pkt.encode();
|
|
||||||
if socket.send_to(&rtp_bytes, dest_addr).await.is_err() {
|
if socket.send_to(&rtp_bytes, dest_addr).await.is_err() {
|
||||||
log_warn!("[rtsp:{}] UDP send failed", peer);
|
log_warn!("[rtsp:{}] UDP send failed", peer);
|
||||||
}
|
}
|
||||||
|
|
@ -186,6 +211,71 @@ async fn handle_rtsp_client(mut stream: tokio::net::TcpStream, context: Arc<Engi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 定期发送 RTCP Sender Report(每 5 秒)
|
||||||
|
if last_rtcp_time.elapsed() >= Duration::from_secs(5) {
|
||||||
|
match session.transport_mode() {
|
||||||
|
TransportMode::TcpInterleaved => {
|
||||||
|
// 发送视频 RTCP SR
|
||||||
|
if session.h264_packetizer().is_some() {
|
||||||
|
let rtp_ts = (video_rtcp_pkt_count as u64 * 90 * 33) as u32; // 近似时间戳
|
||||||
|
let mut sr = SenderReport::with_timestamp(session.video_ssrc(), rtp_ts);
|
||||||
|
sr.sender_packet_count = video_rtcp_pkt_count;
|
||||||
|
sr.sender_octet_count = video_rtcp_octet_count;
|
||||||
|
let data = sr.encode_interleaved(session.video_rtcp_channel());
|
||||||
|
if stream.write_all(&data).await.is_err() {
|
||||||
|
session.cleanup(&sm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 发送音频 RTCP SR
|
||||||
|
if session.aac_packetizer().is_some() {
|
||||||
|
let audio_clock = session.audio_sample_rate();
|
||||||
|
let rtp_ts = (audio_rtcp_pkt_count as u64 * 1024 * audio_clock as u64 / 44100) as u32;
|
||||||
|
let mut sr = SenderReport::with_timestamp(session.audio_ssrc(), rtp_ts);
|
||||||
|
sr.sender_packet_count = audio_rtcp_pkt_count;
|
||||||
|
sr.sender_octet_count = audio_rtcp_octet_count;
|
||||||
|
let data = sr.encode_interleaved(session.audio_rtcp_channel());
|
||||||
|
if stream.write_all(&data).await.is_err() {
|
||||||
|
session.cleanup(&sm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TransportMode::Udp => {
|
||||||
|
// UDP 模式下发送 RTCP SR 到客户端的 RTCP 端口
|
||||||
|
// 视频SR
|
||||||
|
if session.h264_packetizer().is_some() {
|
||||||
|
if let Some(ref udp_sock) = video_udp {
|
||||||
|
let dest = session.video_udp_dest();
|
||||||
|
if let Some(dest_addr) = dest {
|
||||||
|
let rtcp_port = dest_addr.port() + 1;
|
||||||
|
let rtcp_dest = std::net::SocketAddr::new(dest_addr.ip(), rtcp_port);
|
||||||
|
let mut sr = SenderReport::with_timestamp(session.video_ssrc(), 0);
|
||||||
|
sr.sender_packet_count = video_rtcp_pkt_count;
|
||||||
|
sr.sender_octet_count = video_rtcp_octet_count;
|
||||||
|
let _ = udp_sock.send_to(&sr.encode(), rtcp_dest).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 音频SR
|
||||||
|
if session.aac_packetizer().is_some() {
|
||||||
|
if let Some(ref udp_sock) = audio_udp {
|
||||||
|
let dest = session.audio_udp_dest();
|
||||||
|
if let Some(dest_addr) = dest {
|
||||||
|
let rtcp_port = dest_addr.port() + 1;
|
||||||
|
let rtcp_dest = std::net::SocketAddr::new(dest_addr.ip(), rtcp_port);
|
||||||
|
let mut sr = SenderReport::with_timestamp(session.audio_ssrc(), 0);
|
||||||
|
sr.sender_packet_count = audio_rtcp_pkt_count;
|
||||||
|
sr.sender_octet_count = audio_rtcp_octet_count;
|
||||||
|
let _ = udp_sock.send_to(&sr.encode(), rtcp_dest).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last_rtcp_time = tokio::time::Instant::now();
|
||||||
|
}
|
||||||
// 检查流是否仍然存在(推流端可能已断开)
|
// 检查流是否仍然存在(推流端可能已断开)
|
||||||
if let Some(sp) = session.stream_path() {
|
if let Some(sp) = session.stream_path() {
|
||||||
if !sm.has_stream(sp) {
|
if !sm.has_stream(sp) {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use crate::log_error;
|
use crate::log_error;
|
||||||
use crate::log_info;
|
use crate::log_info;
|
||||||
use crate::log_warn;
|
|
||||||
use crate::sdk::traits::{StreamManagerApi, SubscriberApi};
|
use crate::sdk::traits::{StreamManagerApi, SubscriberApi};
|
||||||
use crate::sdk::types::StreamPath;
|
use crate::sdk::types::StreamPath;
|
||||||
|
|
||||||
|
|
@ -138,6 +137,10 @@ pub struct RtspSession {
|
||||||
announce_sps: Option<Vec<u8>>,
|
announce_sps: Option<Vec<u8>>,
|
||||||
/// 缓存的 PPS NALU(不含起始码,含 NALU header 0x68)
|
/// 缓存的 PPS NALU(不含起始码,含 NALU header 0x68)
|
||||||
announce_pps: Option<Vec<u8>>,
|
announce_pps: Option<Vec<u8>>,
|
||||||
|
|
||||||
|
// === 拉流端音频采样率(从 DESCRIBE 元数据获取) ===
|
||||||
|
/// 音频采样率,用于 RTP 时间戳计算(默认 44100)
|
||||||
|
audio_sample_rate: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RtspSession {
|
impl RtspSession {
|
||||||
|
|
@ -176,6 +179,8 @@ impl RtspSession {
|
||||||
// 推流端 SPS/PPS(从 ANNOUNCE SDP 解析)
|
// 推流端 SPS/PPS(从 ANNOUNCE SDP 解析)
|
||||||
announce_sps: None,
|
announce_sps: None,
|
||||||
announce_pps: None,
|
announce_pps: None,
|
||||||
|
// 拉流端音频采样率
|
||||||
|
audio_sample_rate: 44100,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,6 +278,52 @@ impl RtspSession {
|
||||||
self.audio_udp_socket.take()
|
self.audio_udp_socket.take()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取音频采样率(用于 RTP 时间戳计算)
|
||||||
|
pub fn audio_sample_rate(&self) -> u32 {
|
||||||
|
self.audio_sample_rate
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 H.264 打包器引用(用于 RTCP SR 生成)
|
||||||
|
pub fn h264_packetizer(&self) -> Option<&H264RtpPacketizer> {
|
||||||
|
self.h264_packetizer.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 AAC 打包器引用(用于 RTCP SR 生成)
|
||||||
|
pub fn aac_packetizer(&self) -> Option<&AacRtpPacketizer> {
|
||||||
|
self.aac_packetizer.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置 H.264 打包器的包计数和字节计数(用于 RTCP SR)
|
||||||
|
pub fn h264_packetizer_stats(&self) -> (u16, u32, u32) {
|
||||||
|
// 返回 (seq, packet_count_estimate, octet_count_estimate)
|
||||||
|
// 注意:实际实现需要更精确的追踪
|
||||||
|
if let Some(ref pkt) = self.h264_packetizer {
|
||||||
|
(pkt.initial_seq(), 0, 0)
|
||||||
|
} else {
|
||||||
|
(0, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取视频 SSRC
|
||||||
|
pub fn video_ssrc(&self) -> u32 {
|
||||||
|
self.video_ssrc
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取音频 SSRC
|
||||||
|
pub fn audio_ssrc(&self) -> u32 {
|
||||||
|
self.audio_ssrc
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取视频 RTCP interleaved 通道号(RTP 通道 + 1)
|
||||||
|
pub fn video_rtcp_channel(&self) -> u8 {
|
||||||
|
self.video_channel + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取音频 RTCP interleaved 通道号(RTP 通道 + 1)
|
||||||
|
pub fn audio_rtcp_channel(&self) -> u8 {
|
||||||
|
self.audio_channel + 1
|
||||||
|
}
|
||||||
|
|
||||||
/// 处理 RTSP 请求,返回响应字节
|
/// 处理 RTSP 请求,返回响应字节
|
||||||
///
|
///
|
||||||
/// 根据当前状态和请求方法执行状态转换,生成响应。
|
/// 根据当前状态和请求方法执行状态转换,生成响应。
|
||||||
|
|
@ -328,8 +379,11 @@ impl RtspSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 处理 DESCRIBE 请求 — 返回流的 SDP 描述
|
/// 处理 DESCRIBE 请求 — 返回流的 SDP 描述
|
||||||
|
///
|
||||||
|
/// 从流的 GOP 缓存中提取编解码器元数据(SPS/PPS、AAC config),
|
||||||
|
/// 生成包含正确 SDP 参数的描述响应。
|
||||||
fn handle_describe(
|
fn handle_describe(
|
||||||
&self,
|
&mut self,
|
||||||
req: &RtspRequest,
|
req: &RtspRequest,
|
||||||
cseq: u32,
|
cseq: u32,
|
||||||
sm: &Arc<dyn StreamManagerApi>,
|
sm: &Arc<dyn StreamManagerApi>,
|
||||||
|
|
@ -347,10 +401,66 @@ impl RtspSession {
|
||||||
return RtspResponse::new(status::NOT_FOUND).cseq(cseq).encode();
|
return RtspResponse::new(status::NOT_FOUND).cseq(cseq).encode();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成 SDP — 简化实现:假定 H.264 视频 + AAC 音频
|
// 查询流的编解码器元数据
|
||||||
|
let meta = sm.get_codec_metadata(&sp);
|
||||||
|
|
||||||
|
// 生成 SDP
|
||||||
let mut sdp = SdpInfo::new(&format!("{}/{}", app, stream_name), "0.0.0.0");
|
let mut sdp = SdpInfo::new(&format!("{}/{}", app, stream_name), "0.0.0.0");
|
||||||
sdp.add_h264_video("track1", None);
|
|
||||||
sdp.add_aac_audio("track2", 44100, 2);
|
// 视频轨道:如果有 SPS/PPS,生成 sprop-parameter-sets 和 profile-level-id
|
||||||
|
let sprop = if let Some(ref m) = meta {
|
||||||
|
if let (Some(sps), Some(pps)) = (&m.h264_sps, &m.h264_pps) {
|
||||||
|
Some(format!(
|
||||||
|
"{},{}",
|
||||||
|
base64_encode_simple(sps),
|
||||||
|
base64_encode_simple(pps)
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从 SPS 计算 profile-level-id
|
||||||
|
let profile_level_id = if let Some(ref m) = meta {
|
||||||
|
if let Some(ref sps) = m.h264_sps {
|
||||||
|
if sps.len() >= 4 {
|
||||||
|
// SPS NALU: byte[0]=NALU header(0x67), byte[1]=profile_idc, byte[2]=constraint_set_flags, byte[3]=level_idc
|
||||||
|
let profile_idc = sps[1];
|
||||||
|
let constraint = sps[2];
|
||||||
|
let level_idc = sps[3];
|
||||||
|
Some(format!("{:02X}{:02X}{:02X}", profile_idc, constraint, level_idc))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
sdp.add_h264_video("track1", sprop.as_deref(), profile_level_id.as_deref());
|
||||||
|
|
||||||
|
// 音频轨道:如果有 AAC config,传入 hex 编码
|
||||||
|
let aac_config_hex = meta.as_ref().and_then(|m| m.aac_config.as_ref()).map(|c| {
|
||||||
|
c.iter().map(|b| format!("{:02x}", b)).collect::<String>()
|
||||||
|
});
|
||||||
|
let sample_rate = meta.as_ref().map(|m| m.audio_sample_rate).unwrap_or(44100);
|
||||||
|
let channels = meta.as_ref().map(|m| m.audio_channels).unwrap_or(2);
|
||||||
|
|
||||||
|
// 如果采样率为 0(元数据中未设置),使用默认值
|
||||||
|
let sample_rate = if sample_rate == 0 { 44100 } else { sample_rate };
|
||||||
|
let channels = if channels == 0 { 2 } else { channels };
|
||||||
|
|
||||||
|
// 存储音频采样率到 session,供后续 RTP 时间戳计算使用
|
||||||
|
self.audio_sample_rate = sample_rate;
|
||||||
|
|
||||||
|
sdp.add_aac_audio("track2", sample_rate, channels, aac_config_hex.as_deref());
|
||||||
|
|
||||||
|
// 拉流会话标记为 recvonly
|
||||||
|
sdp.recvonly = true;
|
||||||
|
|
||||||
RtspResponse::new(status::OK)
|
RtspResponse::new(status::OK)
|
||||||
.cseq(cseq)
|
.cseq(cseq)
|
||||||
|
|
@ -505,9 +615,12 @@ impl RtspSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 处理 PLAY 请求 — 开始拉流,状态从 Ready → Playing
|
/// 处理 PLAY 请求 — 开始拉流,状态从 Ready → Playing
|
||||||
|
///
|
||||||
|
/// 订阅流并在响应中包含 RTP-Info 头,
|
||||||
|
/// 以便客户端知道初始序列号和 RTP 时间戳。
|
||||||
fn handle_play(
|
fn handle_play(
|
||||||
&mut self,
|
&mut self,
|
||||||
_req: &RtspRequest,
|
req: &RtspRequest,
|
||||||
cseq: u32,
|
cseq: u32,
|
||||||
sm: &Arc<dyn StreamManagerApi>,
|
sm: &Arc<dyn StreamManagerApi>,
|
||||||
) -> Vec<u8> {
|
) -> Vec<u8> {
|
||||||
|
|
@ -529,9 +642,37 @@ impl RtspSession {
|
||||||
self.subscriber = Some(sub);
|
self.subscriber = Some(sub);
|
||||||
self.state = SessionState::Playing;
|
self.state = SessionState::Playing;
|
||||||
log_info!("[rtsp] pull start {}", sp);
|
log_info!("[rtsp] pull start {}", sp);
|
||||||
|
|
||||||
|
// 构建 RTP-Info 头
|
||||||
|
// 从请求 URI 提取基础 URL(rtsp://host:port)
|
||||||
|
let base_url: String = req.uri.split('/').take(3).collect::<Vec<_>>().join("/");
|
||||||
|
|
||||||
|
let mut rtp_info_parts = Vec::new();
|
||||||
|
|
||||||
|
// 视频轨道 RTP-Info
|
||||||
|
if let Some(ref pkt) = self.h264_packetizer {
|
||||||
|
rtp_info_parts.push(format!(
|
||||||
|
"url={}/track1;seq={};rtptime=0",
|
||||||
|
base_url,
|
||||||
|
pkt.initial_seq()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 音频轨道 RTP-Info
|
||||||
|
if let Some(ref pkt) = self.aac_packetizer {
|
||||||
|
rtp_info_parts.push(format!(
|
||||||
|
"url={}/track2;seq={};rtptime=0",
|
||||||
|
base_url,
|
||||||
|
pkt.initial_seq()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let rtp_info = rtp_info_parts.join(",");
|
||||||
|
|
||||||
RtspResponse::new(status::OK)
|
RtspResponse::new(status::OK)
|
||||||
.cseq(cseq)
|
.cseq(cseq)
|
||||||
.session(&self.session_id)
|
.session(&self.session_id)
|
||||||
|
.header("RTP-Info", &rtp_info)
|
||||||
.header("Range", "npt=0.000-")
|
.header("Range", "npt=0.000-")
|
||||||
.encode()
|
.encode()
|
||||||
}
|
}
|
||||||
|
|
@ -722,8 +863,10 @@ impl RtspSession {
|
||||||
} else {
|
} else {
|
||||||
&frame.data
|
&frame.data
|
||||||
};
|
};
|
||||||
|
// 使用实际的音频采样率计算 RTP 时间戳
|
||||||
|
let audio_clock_rate = self.audio_sample_rate;
|
||||||
let rtp_packet =
|
let rtp_packet =
|
||||||
pkt.packetize(aac_data, (frame.timestamp_ms * 44100 / 1000) as u32);
|
pkt.packetize(aac_data, (frame.timestamp_ms * audio_clock_rate as u64 / 1000) as u32);
|
||||||
result.push((
|
result.push((
|
||||||
self.audio_channel,
|
self.audio_channel,
|
||||||
rtp_packet.encode_interleaved(self.audio_channel),
|
rtp_packet.encode_interleaved(self.audio_channel),
|
||||||
|
|
@ -773,8 +916,10 @@ impl RtspSession {
|
||||||
} else {
|
} else {
|
||||||
&frame.data
|
&frame.data
|
||||||
};
|
};
|
||||||
|
// 使用实际的音频采样率计算 RTP 时间戳
|
||||||
|
let audio_clock_rate = self.audio_sample_rate;
|
||||||
let rtp_packet =
|
let rtp_packet =
|
||||||
pkt.packetize(aac_data, (frame.timestamp_ms * 44100 / 1000) as u32);
|
pkt.packetize(aac_data, (frame.timestamp_ms * audio_clock_rate as u64 / 1000) as u32);
|
||||||
result.push((false, rtp_packet));
|
result.push((false, rtp_packet));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1776,4 +1921,98 @@ mod tests {
|
||||||
let result = base64_decode("abc!!!");
|
let result = base64_decode("abc!!!");
|
||||||
assert!(result.is_err(), "无效字符应返回错误");
|
assert!(result.is_err(), "无效字符应返回错误");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 测试:DESCRIBE 有编解码器元数据时 SDP 包含 sprop-parameter-sets
|
||||||
|
#[test]
|
||||||
|
fn test_session_describe_with_metadata_includes_sprop() {
|
||||||
|
use crate::sdk::types::{CodecExtraInfo, H264SeqHeader, AacSeqHeader, VideoCodec, FrameType};
|
||||||
|
|
||||||
|
let mut session = RtspSession::new();
|
||||||
|
let sm = test_sm();
|
||||||
|
let sp = StreamPath::new("live", "test");
|
||||||
|
sm.create_stream(sp.clone()).unwrap();
|
||||||
|
|
||||||
|
// 获取内部 Stream 并 dispatch seq_header 帧
|
||||||
|
let stream = {
|
||||||
|
// 通过 StreamManagerApi::dispatch_frame 来写入帧
|
||||||
|
// 构造 H.264 seq_header
|
||||||
|
let sps = Arc::new(vec![0x67, 0x64, 0x00, 0x29, 0xAC, 0xD9, 0x40]);
|
||||||
|
let pps = Arc::new(vec![0x68, 0xEE, 0x31, 0x12]);
|
||||||
|
let mut vframe = crate::sdk::types::AVFrame::new_video(
|
||||||
|
0, Arc::new(vec![]), VideoCodec::H264, FrameType::KeyFrame
|
||||||
|
);
|
||||||
|
vframe.codec_info = Some(CodecExtraInfo::H264SeqHeader(H264SeqHeader {
|
||||||
|
sps, pps,
|
||||||
|
}));
|
||||||
|
sm.dispatch_frame(&sp, vframe);
|
||||||
|
|
||||||
|
// 构造 AAC seq_header(48000Hz, 2ch)
|
||||||
|
// byte0 = 0x11, byte1 = 0x90
|
||||||
|
let aac_config = Arc::new(vec![0x11, 0x90]);
|
||||||
|
let mut aframe = crate::sdk::types::AVFrame::new_audio(
|
||||||
|
0, Arc::new(vec![]), crate::sdk::types::AudioCodec::Aac
|
||||||
|
);
|
||||||
|
aframe.codec_info = Some(CodecExtraInfo::AacSeqHeader(AacSeqHeader {
|
||||||
|
audio_specific_config: aac_config,
|
||||||
|
}));
|
||||||
|
sm.dispatch_frame(&sp, aframe);
|
||||||
|
};
|
||||||
|
|
||||||
|
// DESCRIBE
|
||||||
|
let req = parse_req(b"DESCRIBE rtsp://localhost:554/live/test RTSP/1.0\r\nCSeq: 2\r\n\r\n");
|
||||||
|
let resp = session.handle_request(&req, &sm);
|
||||||
|
let resp_str = std::str::from_utf8(&resp).unwrap();
|
||||||
|
|
||||||
|
assert!(resp_str.starts_with("RTSP/1.0 200"), "DESCRIBE 应返回 200");
|
||||||
|
assert!(resp_str.contains("sprop-parameter-sets="), "应包含 sprop-parameter-sets");
|
||||||
|
assert!(resp_str.contains("profile-level-id="), "应包含 profile-level-id");
|
||||||
|
assert!(resp_str.contains("config="), "应包含 AAC config");
|
||||||
|
assert!(resp_str.contains("a=recvonly"), "拉流会话应标记 recvonly");
|
||||||
|
assert!(resp_str.contains("mpeg4-generic/48000/2"), "采样率应为 48000");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:PLAY 响应包含 RTP-Info 头
|
||||||
|
#[test]
|
||||||
|
fn test_session_play_response_includes_rtp_info() {
|
||||||
|
let mut session = RtspSession::new();
|
||||||
|
let sm = test_sm();
|
||||||
|
let sp = StreamPath::new("live", "test");
|
||||||
|
sm.create_stream(sp).expect("创建流应成功");
|
||||||
|
|
||||||
|
// SETUP 视频(初始化 packetizer)
|
||||||
|
let setup1 = b"SETUP rtsp://localhost:554/live/test/trackID=0 RTSP/1.0\r\n\
|
||||||
|
CSeq: 3\r\n\
|
||||||
|
Transport: RTP/AVP;unicast;interleaved=0-1\r\n\
|
||||||
|
\r\n";
|
||||||
|
session.handle_request(&parse_req(setup1), &sm);
|
||||||
|
|
||||||
|
// SETUP 音频
|
||||||
|
let setup2 = b"SETUP rtsp://localhost:554/live/test/trackID=1 RTSP/1.0\r\n\
|
||||||
|
CSeq: 4\r\n\
|
||||||
|
Transport: RTP/AVP;unicast;interleaved=2-3\r\n\
|
||||||
|
\r\n";
|
||||||
|
session.handle_request(&parse_req(setup2), &sm);
|
||||||
|
|
||||||
|
// PLAY
|
||||||
|
let play_raw = format!(
|
||||||
|
"PLAY rtsp://localhost:554/live/test RTSP/1.0\r\nCSeq: 5\r\nSession: {}\r\n\r\n",
|
||||||
|
session.session_id()
|
||||||
|
);
|
||||||
|
let resp = session.handle_request(&parse_req(play_raw.as_bytes()), &sm);
|
||||||
|
let resp_str = std::str::from_utf8(&resp).unwrap();
|
||||||
|
|
||||||
|
assert!(resp_str.starts_with("RTSP/1.0 200"), "PLAY 应返回 200");
|
||||||
|
assert!(resp_str.contains("RTP-Info:"), "PLAY 响应应包含 RTP-Info 头");
|
||||||
|
assert!(resp_str.contains("seq=0"), "RTP-Info 应包含初始序列号");
|
||||||
|
assert!(resp_str.contains("rtptime=0"), "RTP-Info 应包含 rtptime");
|
||||||
|
assert!(resp_str.contains("track1"), "RTP-Info 应包含 track1");
|
||||||
|
assert!(resp_str.contains("track2"), "RTP-Info 应包含 track2");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:audio_sample_rate 默认为 44100,DESCRIBE 后更新
|
||||||
|
#[test]
|
||||||
|
fn test_session_audio_sample_rate_default_and_updated() {
|
||||||
|
let session = RtspSession::new();
|
||||||
|
assert_eq!(session.audio_sample_rate(), 44100, "默认应为 44100");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
//! - `ProtocolPlugin` — 协议插件扩展 trait(增加 protocol_name + default_port)
|
//! - `ProtocolPlugin` — 协议插件扩展 trait(增加 protocol_name + default_port)
|
||||||
|
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::context::EngineContext;
|
use super::context::EngineContext;
|
||||||
use super::traits::ConfigProvider;
|
use super::traits::ConfigProvider;
|
||||||
|
|
@ -59,16 +60,20 @@ pub trait Plugin: Send + Sync {
|
||||||
|
|
||||||
/// 初始化阶段:加载配置、注入依赖
|
/// 初始化阶段:加载配置、注入依赖
|
||||||
///
|
///
|
||||||
|
/// 接收 `Arc<EngineContext>` 以便插件在异步任务中持有上下文引用。
|
||||||
|
///
|
||||||
/// 默认实现为空操作
|
/// 默认实现为空操作
|
||||||
fn init(&mut self, context: &EngineContext, config: &dyn ConfigProvider) -> Result<(), String> {
|
fn init(&mut self, context: Arc<EngineContext>, config: &dyn ConfigProvider) -> Result<(), String> {
|
||||||
let _ = (context, config);
|
let _ = (context, config);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 启动阶段:开始监听端口、注册任务
|
/// 启动阶段:开始监听端口、注册任务
|
||||||
///
|
///
|
||||||
|
/// 接收 `Arc<EngineContext>` 以便插件 spawn 异步任务时移动上下文所有权。
|
||||||
|
///
|
||||||
/// 默认实现为空操作
|
/// 默认实现为空操作
|
||||||
fn start(&mut self, context: &EngineContext) -> Result<(), String> {
|
fn start(&mut self, context: Arc<EngineContext>) -> Result<(), String> {
|
||||||
let _ = context;
|
let _ = context;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,14 +60,27 @@ impl PluginRegistry {
|
||||||
/// 初始化所有已注册且启用的插件(调用 init())
|
/// 初始化所有已注册且启用的插件(调用 init())
|
||||||
///
|
///
|
||||||
/// 按注册顺序依次调用。任一插件 init 失败则立即返回错误。
|
/// 按注册顺序依次调用。任一插件 init 失败则立即返回错误。
|
||||||
|
/// 每个插件接收 `Arc<EngineContext>` 的克隆,便于在异步任务中持有。
|
||||||
|
///
|
||||||
|
/// 支持配置级开关:检查 `<插件名>.enabled` 配置项,
|
||||||
|
/// 设为 `"false"` 则跳过初始化并将插件标记为 `Disabled` 状态。
|
||||||
pub fn init_all(&self, config: &dyn ConfigProvider) -> Result<(), String> {
|
pub fn init_all(&self, config: &dyn ConfigProvider) -> Result<(), String> {
|
||||||
let mut entries = self.entries.lock().unwrap();
|
let mut entries = self.entries.lock().unwrap();
|
||||||
for entry in entries.iter_mut() {
|
for entry in entries.iter_mut() {
|
||||||
|
let name = entry.plugin.meta().name.clone();
|
||||||
|
|
||||||
|
// 检查配置中的 enabled 标志:`<plugin_name>.enabled = false` 则跳过
|
||||||
|
if let Some(val) = config.get(&format!("{}.enabled", name)) {
|
||||||
|
if val == "false" {
|
||||||
|
entry.enabled = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !entry.enabled {
|
if !entry.enabled {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Err(e) = entry.plugin.init(&self.context, config) {
|
if let Err(e) = entry.plugin.init(Arc::clone(&self.context), config) {
|
||||||
let name = entry.plugin.meta().name.clone();
|
|
||||||
return Err(format!("plugin '{}' init failed: {}", name, e));
|
return Err(format!("plugin '{}' init failed: {}", name, e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -77,13 +90,14 @@ impl PluginRegistry {
|
||||||
/// 启动所有已注册且启用的插件(调用 start())
|
/// 启动所有已注册且启用的插件(调用 start())
|
||||||
///
|
///
|
||||||
/// 按注册顺序依次调用。任一插件 start 失败则立即返回错误。
|
/// 按注册顺序依次调用。任一插件 start 失败则立即返回错误。
|
||||||
|
/// 每个插件接收 `Arc<EngineContext>` 的克隆,便于 spawn 异步任务。
|
||||||
pub fn start_all(&self) -> Result<(), String> {
|
pub fn start_all(&self) -> Result<(), String> {
|
||||||
let mut entries = self.entries.lock().unwrap();
|
let mut entries = self.entries.lock().unwrap();
|
||||||
for entry in entries.iter_mut() {
|
for entry in entries.iter_mut() {
|
||||||
if !entry.enabled {
|
if !entry.enabled {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Err(e) = entry.plugin.start(&self.context) {
|
if let Err(e) = entry.plugin.start(Arc::clone(&self.context)) {
|
||||||
let name = entry.plugin.meta().name.clone();
|
let name = entry.plugin.meta().name.clone();
|
||||||
return Err(format!("plugin '{}' start failed: {}", name, e));
|
return Err(format!("plugin '{}' start failed: {}", name, e));
|
||||||
}
|
}
|
||||||
|
|
@ -166,12 +180,12 @@ mod tests {
|
||||||
|
|
||||||
impl Plugin for DummyPlugin {
|
impl Plugin for DummyPlugin {
|
||||||
fn meta(&self) -> &PluginMeta { &self.meta }
|
fn meta(&self) -> &PluginMeta { &self.meta }
|
||||||
fn init(&mut self, _context: &EngineContext, _config: &dyn ConfigProvider) -> Result<(), String> {
|
fn init(&mut self, _context: Arc<EngineContext>, _config: &dyn ConfigProvider) -> Result<(), String> {
|
||||||
*self.init_called.lock().unwrap() = true;
|
*self.init_called.lock().unwrap() = true;
|
||||||
self.state = PluginState::Initialized;
|
self.state = PluginState::Initialized;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn start(&mut self, _context: &EngineContext) -> Result<(), String> {
|
fn start(&mut self, _context: Arc<EngineContext>) -> Result<(), String> {
|
||||||
*self.start_called.lock().unwrap() = true;
|
*self.start_called.lock().unwrap() = true;
|
||||||
self.state = PluginState::Running;
|
self.state = PluginState::Running;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -203,10 +217,10 @@ mod tests {
|
||||||
|
|
||||||
impl Plugin for FailPlugin {
|
impl Plugin for FailPlugin {
|
||||||
fn meta(&self) -> &PluginMeta { &self.meta }
|
fn meta(&self) -> &PluginMeta { &self.meta }
|
||||||
fn init(&mut self, _context: &EngineContext, _config: &dyn ConfigProvider) -> Result<(), String> {
|
fn init(&mut self, _context: Arc<EngineContext>, _config: &dyn ConfigProvider) -> Result<(), String> {
|
||||||
Err("init error".to_string())
|
Err("init error".to_string())
|
||||||
}
|
}
|
||||||
fn start(&mut self, _context: &EngineContext) -> Result<(), String> {
|
fn start(&mut self, _context: Arc<EngineContext>) -> Result<(), String> {
|
||||||
Err("start error".to_string())
|
Err("start error".to_string())
|
||||||
}
|
}
|
||||||
fn stop(&mut self) -> Result<(), String> {
|
fn stop(&mut self) -> Result<(), String> {
|
||||||
|
|
@ -224,6 +238,30 @@ mod tests {
|
||||||
fn get_section(&self, _section: &str) -> Vec<(String, String)> { Vec::new() }
|
fn get_section(&self, _section: &str) -> Vec<(String, String)> { Vec::new() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 测试用配置:支持按插件名设置 enabled 标志
|
||||||
|
struct SelectiveConfig {
|
||||||
|
disabled: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectiveConfig {
|
||||||
|
fn new(disabled: &[&str]) -> Self {
|
||||||
|
Self { disabled: disabled.iter().map(|s| s.to_string()).collect() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigProvider for SelectiveConfig {
|
||||||
|
fn get(&self, key: &str) -> Option<String> {
|
||||||
|
// key 格式:<plugin_name>.enabled
|
||||||
|
if let Some(name) = key.strip_suffix(".enabled") {
|
||||||
|
if self.disabled.iter().any(|d| d == name) {
|
||||||
|
return Some("false".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn get_section(&self, _section: &str) -> Vec<(String, String)> { Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
fn make_registry() -> PluginRegistry {
|
fn make_registry() -> PluginRegistry {
|
||||||
let ctx = Arc::new(EngineContext::new(Arc::new(StreamManager::new())));
|
let ctx = Arc::new(EngineContext::new(Arc::new(StreamManager::new())));
|
||||||
PluginRegistry::new(ctx)
|
PluginRegistry::new(ctx)
|
||||||
|
|
@ -294,4 +332,31 @@ mod tests {
|
||||||
assert!(registry.get_plugin("found").is_some());
|
assert!(registry.get_plugin("found").is_some());
|
||||||
assert!(registry.get_plugin("missing").is_none());
|
assert!(registry.get_plugin("missing").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 测试:配置中 enabled=false 的插件被跳过
|
||||||
|
#[test]
|
||||||
|
fn test_registry_config_disable_skips_init() {
|
||||||
|
let registry = make_registry();
|
||||||
|
registry.register(Box::new(DummyPlugin::new("rtmp")));
|
||||||
|
registry.register(Box::new(DummyPlugin::new("hls")));
|
||||||
|
// 禁用 hls 插件
|
||||||
|
let config = SelectiveConfig::new(&["hls"]);
|
||||||
|
assert!(registry.init_all(&config).is_ok());
|
||||||
|
assert!(registry.start_all().is_ok());
|
||||||
|
// 两个插件都已注册,但 hls 未被初始化
|
||||||
|
assert_eq!(registry.list_plugins().len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试:禁用插件不影响其他插件的完整生命周期
|
||||||
|
#[test]
|
||||||
|
fn test_registry_config_disable_partial() {
|
||||||
|
let registry = make_registry();
|
||||||
|
let rtmp = DummyPlugin::new("rtmp");
|
||||||
|
registry.register(Box::new(rtmp));
|
||||||
|
registry.register(Box::new(DummyPlugin::new("rtsp")));
|
||||||
|
// 只禁用 rtsp
|
||||||
|
let config = SelectiveConfig::new(&["rtsp"]);
|
||||||
|
assert!(registry.init_all(&config).is_ok());
|
||||||
|
assert!(registry.start_all().is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::types::{AVFrame, StreamPath, StreamSummary, TrackInfo, TrackId};
|
use super::types::{AVFrame, StreamCodecMeta, StreamPath, StreamSummary, TrackInfo, TrackId};
|
||||||
|
|
||||||
/// 发布者 API
|
/// 发布者 API
|
||||||
///
|
///
|
||||||
|
|
@ -93,6 +93,18 @@ pub trait StreamManagerApi: Send + Sync {
|
||||||
fn list_streams(&self) -> Vec<StreamPath>;
|
fn list_streams(&self) -> Vec<StreamPath>;
|
||||||
/// 获取所有活跃流的详细信息摘要
|
/// 获取所有活跃流的详细信息摘要
|
||||||
fn stream_summaries(&self) -> Vec<StreamSummary>;
|
fn stream_summaries(&self) -> Vec<StreamSummary>;
|
||||||
|
/// 获取流的编解码器元数据(SPS/PPS, AAC config 等)
|
||||||
|
///
|
||||||
|
/// 从 GOP 缓存中的 seq_header 帧提取编解码器初始化参数,
|
||||||
|
/// 用于 RTSP DESCRIBE 响应生成 SDP 描述。
|
||||||
|
///
|
||||||
|
/// # 参数
|
||||||
|
/// - `path` — 流路径
|
||||||
|
///
|
||||||
|
/// # 返回
|
||||||
|
/// - `Some(StreamCodecMeta)` — 找到流的编解码器元数据
|
||||||
|
/// - `None` — 流不存在或无编解码器信息
|
||||||
|
fn get_codec_metadata(&self, path: &StreamPath) -> Option<StreamCodecMeta>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 流生命周期事件
|
/// 流生命周期事件
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,24 @@ pub struct StreamPath {
|
||||||
pub stream_name: String,
|
pub stream_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 流的编解码器元数据(用于 RTSP DESCRIBE 等 SDP 生成场景)
|
||||||
|
///
|
||||||
|
/// 从 GOP 缓存中的 seq_header 帧提取,包含 H.264 SPS/PPS 和 AAC 配置信息。
|
||||||
|
/// RTSP 拉流时 DESCRIBE 响应需要这些信息来生成正确的 SDP 描述。
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct StreamCodecMeta {
|
||||||
|
/// H.264 SPS NALU(不含起始码,含 NALU header 0x67)
|
||||||
|
pub h264_sps: Option<Arc<Vec<u8>>>,
|
||||||
|
/// H.264 PPS NALU(不含起始码,含 NALU header 0x68)
|
||||||
|
pub h264_pps: Option<Arc<Vec<u8>>>,
|
||||||
|
/// AAC AudioSpecificConfig 原始字节
|
||||||
|
pub aac_config: Option<Arc<Vec<u8>>>,
|
||||||
|
/// 音频采样率(如 44100, 48000)
|
||||||
|
pub audio_sample_rate: u32,
|
||||||
|
/// 音频通道数
|
||||||
|
pub audio_channels: u8,
|
||||||
|
}
|
||||||
|
|
||||||
/// 流摘要信息(用于 API 查询)
|
/// 流摘要信息(用于 API 查询)
|
||||||
///
|
///
|
||||||
/// 包含活跃流的元数据快照,供 HTTP API 返回
|
/// 包含活跃流的元数据快照,供 HTTP API 返回
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue