1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| ┌─────────────────────────────────────┐
│ HTTP Server :8080 │
│ │
┌──────────┐ │ ┌─────────┐ ┌─────────────┐ │
│ 推流端 │ ──WHIP──▶ │ │ 认证 │───▶│ SFU │ │
│(OBS/网页) │ │ │ 中间件 │ │ 管理器 │ │
└──────────┘ │ └─────────┘ └──────┬──────┘ │
│ │ │
┌──────────┐ │ ┌───────────────┤ │
│ 观看端 │ ◀──WHEP── │ │ │ │
│ (浏览器) │ ───────▶ │ ▼ ▼ │
└──────────┘ │ ┌─────────────┐ ┌───────────┐ │
│ │ 房间 │ │ 录制 │ │
│ │ (转发) │ │ 与上传 │ │
│ └──────┬──────┘ └─────┬─────┘ │
└─────────┼──────────────┼────────────┘
│ │
┌─────────▼──────────────▼───────────┐
│ 对象存储 │
│ (S3/MinIO) │
└────────────────────────────────────┘
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| HTTP 请求
│
▼
┌─────────────┐
│ CORS │ ← ALLOWED_ORIGIN
└──────┬──────┘
│
▼
┌─────────────┐
│ 限流器 │ ← RATE_LIMIT_RPS, RATE_LIMIT_BURST
└──────┬──────┘
│
▼
┌─────────────┐
│ 认证 │ ← AUTH_TOKEN / ROOM_TOKENS / JWT_SECRET
└──────┬──────┘
│
▼
┌─────────────┐
│ 处理器 │ → 业务逻辑
└─────────────┘
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| ┌─────────────────────────────────────┐
│ Config │
├─────────────────────────────────────┤
│ HTTPAddr string │
│ AllowedOrigin string │
│ AuthToken string │
│ RoomTokens map[string]string │
│ JWTSecret string │
│ RecordEnabled bool │
│ RecordDir string │
│ S3Endpoint string │
│ RateLimitRPS float64 │
│ STUN/TURN []string │
│ ... │
└─────────────────────────────────────┘
|
1
2
3
4
5
6
7
| 1. 房间级 Token (ROOM_TOKENS)
↓ (未找到或失败)
2. 全局 Token (AUTH_TOKEN)
↓ (未找到或失败)
3. JWT (JWT_SECRET)
↓ (未找到或失败)
4. 允许访问 (未配置认证)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| ┌─────────────────────────────────────────────────────┐
│ Manager │
│ - 管理所有 Room 实例 │
│ - 创建/删除 Room │
│ - 统计房间数量 │
└──────────────────────┬──────────────────────────────┘
│ 1:N
▼
┌─────────────────────────────────────────────────────┐
│ Room │
│ - Publisher PeerConnection │
│ - Subscriber PeerConnections │
│ - TrackFeeds (TrackFanout map) │
└──────────────────────┬──────────────────────────────┘
│ 1:N
▼
┌─────────────────────────────────────────────────────┐
│ TrackFanout │
│ - Remote Track (from publisher) │
│ - Local Tracks (to subscribers) │
│ - readLoop: RTP distribution │
│ - Optional: Recorder (IVF/OGG writer) │
└─────────────────────────────────────────────────────┘
|
1
2
3
4
5
6
| 上传流程:
1. 检查 Enabled() → client != nil
2. 打开本地文件
3. 构建对象键 (prefix + filename)
4. client.PutObject()
5. (可选) 删除本地文件
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| 1. 发布者 → POST /api/whip/publish/{room}
│
2. HTTPHandlers.ServeWHIPPublish()
│ ├─ CORS 检查
│ ├─ 限流检查
│ └─ 认证检查
│
3. Manager.Publish(roomName, sdpOffer)
│ ├─ getOrCreateRoom()
│ └─ Room.Publish(sdpOffer)
│
4. Room.Publish()
│ ├─ 创建 MediaEngine + Interceptors
│ ├─ NewPeerConnection(ICEConfig)
│ ├─ SetRemoteDescription(offer)
│ ├─ CreateAnswer()
│ ├─ SetLocalDescription(answer)
│ └─ OnTrack: attachTrackFeed()
│
5. 返回 SDP Answer
│
6. TrackFanout.readLoop() 持续运行
│ ├─ 从 Remote Track 读取 RTP
│ ├─ 写入录制器(如启用)
│ └─ 分发到所有 Local Tracks
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 1. 观看者 → POST /api/whep/play/{room}
│
2. HTTPHandlers.ServeWHEPPlay()
│ ├─ CORS/限流/认证检查
│ └─ Manager.Subscribe()
│
3. Manager.Subscribe(roomName, sdpOffer)
│ └─ Room.Subscribe(sdpOffer)
│
4. Room.Subscribe()
│ ├─ 检查订阅者上限
│ ├─ NewPeerConnection()
│ ├─ 遍历现有 TrackFeeds
│ │ └─ TrackFanout.attachToSubscriber()
│ ├─ SetRemoteDescription/CreateAnswer
│ └─ OnICEStateChange: removeSubscriber()
│
5. 返回 SDP Answer
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| ICE 状态变更 (Failed/Disconnected/Closed)
│
▼
┌─────────────────────────────────────┐
│ 发布者断开 │
├─────────────────────────────────────┤
│ 1. closePublisher() │
│ 2. 关闭所有 TrackFanout │
│ 3. 上传录制文件 │
│ 4. 清空订阅者列表 │
│ 5. pruneIfEmpty() │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 订阅者断开 │
├─────────────────────────────────────┤
│ 1. removeSubscriber() │
│ 2. 从 TrackFanouts 移除绑定 │
│ 3. 关闭 PeerConnection │
│ 4. pruneIfEmpty() │
└─────────────────────────────────────┘
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 优先级 1: 房间 Token (ROOM_TOKENS)
┌─────────────────────────────────────┐
│ ROOM_TOKENS="room1:abc;room2:def" │
│ │
│ 访问 room1 → 检查 token == "abc" │
│ 访问 room2 → 检查 token == "def" │
│ 访问 room3 → 回退到全局 Token │
└─────────────────────────────────────┘
优先级 2: 全局 Token (AUTH_TOKEN)
┌─────────────────────────────────────┐
│ AUTH_TOKEN="secret123" │
│ │
│ 所有房间使用相同 Token │
└─────────────────────────────────────┘
|
1
2
3
| {room}_{trackID}_{unixTimestamp}.{ext}
示例: demo_video0_1710123456.ivf
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| Room.closePublisher()
│
▼
TrackFanout.close() → 返回录制文件路径
│
▼
uploader.Enabled()?
│ Yes
▼
go uploadRecording(path)
│
▼
Upload(ctx, path)
├─ PutObject(S3Bucket, objectKey, file)
└─ (可选) os.Remove(localFile)
|
1
2
3
4
5
6
7
8
9
10
11
| # 活跃房间数
live_rooms
# 每房间订阅者数
live_subscribers{room="demo"}
# RTP 字节数(累计)
live_rtp_bytes_total{room="demo"}
# RTP 包数(累计)
live_rtp_packets_total{room="demo"}
|
1
2
3
4
5
6
7
| 环境变量:
OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
OTEL_SERVICE_NAME=live-webrtc-go
追踪的 span:
- HTTP Handler: {method} {path}
|
1
| GET /healthz → "ok" (200 OK)
|