zhibo/.kiro/specs/live-streaming-system/design.md
2025-12-15 11:21:21 +08:00

353 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# Design Document
## Overview
本设计文档描述了基于 SRS 的个人直播系统的技术架构和实现方案。系统采用前后端分离架构,使用 SRS 作为核心流媒体服务器处理视频流的接收和分发Node.js 后端提供业务 APIReact 前端提供用户界面。
### 技术栈选型
| 组件 | 技术选择 | 理由 |
|------|----------|------|
| 流媒体服务器 | SRS 5.0 | 高性能、低延迟、支持多协议、Docker 部署简单 |
| 后端框架 | Node.js + Express | 轻量级、异步处理能力强、生态丰富 |
| 前端框架 | React 18 | 组件化开发、生态成熟、性能优秀 |
| 视频播放器 | flv.js + hls.js | flv.js 支持 HTTP-FLV 低延迟播放hls.js 提供 HLS 兼容性 |
| 容器化 | Docker + Docker Compose | 简化部署、环境一致性 |
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ 用户层 (Users) │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ 主播 │ │ 观众 │ │
│ │ (OBS/FFmpeg)│ │ (浏览器) │ │
│ └──────┬──────┘ └────────┬────────┘ │
│ │ RTMP 推流 │ HTTP 请求 │
│ │ rtmp://host:1935/live/{streamKey} │ │
└─────────┼─────────────────────────────────────────┼────────────┘
│ │
┌─────────┼─────────────────────────────────────────┼────────────┐
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ SRS Server │ │ React Frontend │ │
│ │ (Docker) │ │ (Port 3000) │ │
│ │ │ │ │ │
│ │ RTMP: 1935 │◄───HTTP-FLV/HLS───│ flv.js/hls.js │ │
│ │ HTTP: 8080 │ │ │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ │ HTTP Callback │ REST API │
│ │ (on_publish/on_unpublish) │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Node.js API Server │ │
│ │ (Port 3001) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ Room API │ │ Callback │ │ Room Store │ │ │
│ │ │ Controller │ │ Handler │ │ (In-Memory) │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ 服务层 (Services) │
└─────────────────────────────────────────────────────────────────┘
```
### 数据流说明
1. **推流流程**: 主播 → RTMP → SRS → HTTP Callback → API Server → 更新房间状态
2. **观看流程**: 观众 → React App → 获取房间信息 → flv.js/hls.js → SRS HTTP-FLV/HLS
3. **管理流程**: 用户 → React App → REST API → API Server → 房间 CRUD
## Components and Interfaces
### 1. SRS 流媒体服务器
**职责**: 接收 RTMP 推流,转换并分发 HTTP-FLV 和 HLS 流
**配置接口**:
```conf
# srs.conf
listen 1935;
max_connections 1000;
daemon off;
srs_log_tank console;
http_server {
enabled on;
listen 8080;
dir ./objs/nginx/html;
}
vhost __defaultVhost__ {
hls {
enabled on;
hls_fragment 2;
hls_window 10;
hls_path ./objs/nginx/html;
hls_m3u8_file [app]/[stream].m3u8;
hls_ts_file [app]/[stream]-[seq].ts;
}
http_remux {
enabled on;
mount [vhost]/[app]/[stream].flv;
}
http_hooks {
enabled on;
on_publish http://api-server:3001/api/srs/on_publish;
on_unpublish http://api-server:3001/api/srs/on_unpublish;
}
}
```
### 2. API Server (Node.js + Express)
**职责**: 提供业务 API处理 SRS 回调,管理房间数据
#### 2.1 Room API Controller
```typescript
// 接口定义
interface RoomController {
// 获取所有房间
GET /api/rooms -> Room[]
// 创建房间
POST /api/rooms { title: string, streamerName: string } -> Room
// 获取单个房间
GET /api/rooms/:id -> Room
// 删除房间
DELETE /api/rooms/:id -> { success: boolean }
}
```
#### 2.2 SRS Callback Handler
```typescript
// SRS 回调接口
interface SRSCallbackHandler {
// 推流开始回调
POST /api/srs/on_publish {
action: 'on_publish',
app: string, // 应用名,如 'live'
stream: string // 流名称,即 streamKey
} -> { code: 0 }
// 推流结束回调
POST /api/srs/on_unpublish {
action: 'on_unpublish',
app: string,
stream: string
} -> { code: 0 }
}
```
### 3. React Frontend
**职责**: 提供用户界面,包括房间列表、直播播放、房间创建
#### 3.1 组件结构
```
src/
├── components/
│ ├── RoomList.jsx # 房间列表组件
│ ├── RoomCard.jsx # 房间卡片组件
│ ├── LivePlayer.jsx # 直播播放器组件
│ ├── CreateRoom.jsx # 创建房间弹窗
│ └── StreamInfo.jsx # 推流信息展示
├── pages/
│ ├── HomePage.jsx # 首页(房间列表)
│ └── RoomPage.jsx # 房间详情页
├── services/
│ └── api.js # API 调用封装
└── App.jsx # 应用入口
```
#### 3.2 LivePlayer 组件接口
```typescript
interface LivePlayerProps {
room: Room;
preferFlv?: boolean; // 优先使用 FLV低延迟
}
// 播放器内部逻辑
// 1. 检测浏览器是否支持 flv.js (需要 MSE 支持)
// 2. 支持则使用 HTTP-FLV否则降级到 HLS
// 3. 监听播放错误,自动重连
```
## Data Models
### Room 数据模型
```typescript
interface Room {
id: string; // 房间唯一标识 (UUID)
title: string; // 房间标题
streamerName: string; // 主播名称
streamKey: string; // 推流密钥 (与 id 相同)
isLive: boolean; // 是否正在直播
viewerCount: number; // 观看人数
createdAt: string; // 创建时间 (ISO 8601)
startedAt?: string; // 开播时间 (ISO 8601)
}
```
### API 响应格式
```typescript
// 成功响应
interface SuccessResponse<T> {
success: true;
data: T;
}
// 错误响应
interface ErrorResponse {
success: false;
error: {
code: string;
message: string;
};
}
// SRS 回调响应
interface SRSCallbackResponse {
code: number; // 0 表示成功
}
```
### 流地址格式
```typescript
interface StreamUrls {
rtmp: string; // rtmp://host:1935/live/{streamKey}
flv: string; // http://host:8080/live/{streamKey}.flv
hls: string; // http://host:8080/live/{streamKey}.m3u8
}
```
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: Room Creation Round-Trip Consistency
*For any* valid room creation request with non-empty title and streamer name, creating the room and then retrieving it by ID SHALL return a room with matching title, streamer name, a unique stream key, and a valid creation timestamp.
**Validates: Requirements 1.1, 1.4**
### Property 2: New Rooms Start Offline
*For any* newly created room, the initial `isLive` status SHALL be `false`.
**Validates: Requirements 1.2**
### Property 3: Empty Title Rejection
*For any* room creation request where the title is empty or consists only of whitespace characters, the system SHALL reject the request and return a validation error without creating a room.
**Validates: Requirements 1.3**
### Property 4: Publish Callback Status Transition
*For any* room that exists in the system, when a publish callback is received with that room's stream key, the room's `isLive` status SHALL transition to `true`. When an unpublish callback is received, the status SHALL transition to `false`.
**Validates: Requirements 2.3, 2.5**
### Property 5: Stream URLs Contain Required Formats
*For any* room returned by the API, the stream URLs SHALL include both HTTP-FLV (`.flv`) and HLS (`.m3u8`) format URLs containing the room's stream key.
**Validates: Requirements 3.1**
### Property 6: Room List Completeness
*For any* set of created rooms, requesting the room list SHALL return all rooms with their current status, title, and streamer name intact.
**Validates: Requirements 4.1, 4.2**
## Error Handling
### API Error Codes
| 错误码 | 描述 | HTTP 状态码 |
|--------|------|-------------|
| `VALIDATION_ERROR` | 请求参数验证失败 | 400 |
| `ROOM_NOT_FOUND` | 房间不存在 | 404 |
| `STREAM_KEY_INVALID` | 推流密钥无效 | 401 |
| `INTERNAL_ERROR` | 服务器内部错误 | 500 |
### 错误处理策略
1. **API 层**: 统一错误响应格式,包含错误码和消息
2. **SRS 回调**: 返回 `{ code: 0 }` 表示成功,非零表示失败
3. **前端**: 显示友好的错误提示,支持重试操作
4. **播放器**: 自动重连机制,最多重试 3 次
## Testing Strategy
### 单元测试
使用 Jest 进行单元测试,覆盖以下模块:
1. **Room Service**: 房间 CRUD 操作
2. **Validation**: 输入验证逻辑
3. **URL Generator**: 流地址生成
### 属性测试 (Property-Based Testing)
使用 fast-check 库进行属性测试:
1. **Property 1**: 房间创建往返一致性
2. **Property 2**: 新房间初始状态
3. **Property 3**: 空标题拒绝
4. **Property 4**: 回调状态转换
5. **Property 5**: 流地址格式
6. **Property 6**: 房间列表完整性
### 测试配置
```javascript
// jest.config.js
module.exports = {
testEnvironment: 'node',
testMatch: ['**/*.test.js', '**/*.property.test.js'],
collectCoverageFrom: ['server/**/*.js'],
coverageThreshold: {
global: { branches: 80, functions: 80, lines: 80 }
}
};
```
### 属性测试示例
```javascript
// 每个属性测试运行 100 次迭代
// 标注格式: **Feature: live-streaming-system, Property {number}: {property_text}**
import fc from 'fast-check';
// **Feature: live-streaming-system, Property 2: New Rooms Start Offline**
test('newly created rooms should have isLive = false', () => {
fc.assert(
fc.property(
fc.string({ minLength: 1 }), // title
fc.string({ minLength: 1 }), // streamerName
(title, streamerName) => {
const room = createRoom({ title, streamerName });
return room.isLive === false;
}
),
{ numRuns: 100 }
);
});
```