353 lines
13 KiB
Markdown
353 lines
13 KiB
Markdown
# Design Document
|
||
|
||
## Overview
|
||
|
||
本设计文档描述了基于 SRS 的个人直播系统的技术架构和实现方案。系统采用前后端分离架构,使用 SRS 作为核心流媒体服务器处理视频流的接收和分发,Node.js 后端提供业务 API,React 前端提供用户界面。
|
||
|
||
### 技术栈选型
|
||
|
||
| 组件 | 技术选择 | 理由 |
|
||
|------|----------|------|
|
||
| 流媒体服务器 | 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 }
|
||
);
|
||
});
|
||
``` |