4. Live555进阶:H.264文件推送、多线程处理、访问控制、日志配置、性能调优
基础功能跑通了,接下来才是真正有意思的部分。Live555 默认的 testOnDemandRTSPServer 只能播个测试流,真要上生产环境,你得自己动手改造。这一章我带你过一遍实际项目中必须掌握的五个进阶点。
4.1 H.264 文件推送:从测试流到真实视频
默认的测试流是 Live555 自己生成的假数据。你要推一个真正的 .264 文件,得自己写一个 ByteStreamFileSource 的子类,或者直接用现成的 H264VideoFileServerMediaSubsession。
我个人习惯直接继承 OnDemandServerMediaSubsession,重写 createNewStreamSource() 和 createNewRTPSink()。这样最灵活,也最容易控制。
// H264FileServerMediaSubsession.cpp 核心片段
H264VideoStreamSource* source = H264VideoStreamSource::createNew(env, file, reuseFirstFrame);
if (source == NULL) {
*env << "无法打开文件: " << file << "\n";
return NULL;
}
// 创建 RTP 接收端
RTPSink* sink = H264VideoRTPSink::createNew(env, rtpGroupsock, 96);
嗯,这里要注意:reuseFirstFrame 这个参数。如果你推的是带 B 帧的码流,必须设为 True,否则解码器会花屏。我在项目中遇到过这个问题,当时排查了整整一个下午,最后发现就是少传了这个参数。
4.2 多线程处理:别让一个客户端卡死全局
Live555 默认是单线程事件循环。你想想看,如果一个客户端网络慢,整个服务器都得等他?这显然不合理。
解决方案其实不复杂:为每个客户端连接创建一个独立的 TaskScheduler 和 UsageEnvironment,跑在单独的线程里。
| 方案 | 优点 | 缺点 |
|---|---|---|
| 单线程 + 非阻塞 I/O | 实现简单,无锁竞争 | 一个慢客户端拖慢全局 |
| 每客户端一线程 | 隔离性好,互不影响 | 线程开销大,连接数多时内存暴涨 |
| 线程池 + 任务队列 | 资源可控,性能均衡 | 实现复杂,需要同步机制 |
我建议中小型项目用第二种方案,简单粗暴。如果并发超过 500,再考虑线程池。
// 伪代码:每客户端一线程
void* clientThread(void* arg) {
TaskScheduler* scheduler = BasicTaskScheduler::createNew();
UsageEnvironment* env = BasicUsageEnvironment::createNew(*scheduler);
// 创建 RTSPServer 实例,绑定到当前线程
RTSPServer* rtspServer = RTSPServer::createNew(*env, port);
env->taskScheduler().doEventLoop(); // 每个线程独立事件循环
return NULL;
}
UsageEnvironment 不是线程安全的。每个线程必须有自己的实例,千万别共享。我曾经犯过这个错,结果日志乱成一锅粥,调试了三天才找到原因。
4.3 访问控制:给流媒体加把锁
流媒体服务器不能裸奔。你得加一层访问控制,至少做到用户名密码认证。
Live555 自带了 DigestAuthentication 支持。你只需要在 RTSPServer 创建时传入一个 UserAuthenticationDatabase 即可。
UserAuthenticationDatabase* authDB = new UserAuthenticationDatabase;
authDB->addUserRecord("admin", "123456"); // 用户名: admin, 密码: 123456
RTSPServer* rtspServer = RTSPServer::createNew(env, 8554, authDB);
说白了,这就是 HTTP 的 Digest 认证。客户端发请求时,服务器返回 401 并带上 nonce,客户端用 MD5 算一遍再发回来。Live555 帮你处理了这些细节。
如果你需要更细粒度的控制(比如按 IP 白名单、按时间限制),那就得自己写一个 GenericMediaServer::ClientConnection 的子类,重写 handleCmd_OPTIONS() 或 handleCmd_DESCRIBE()。我在项目中就做过一个基于 Redis 的令牌校验,每次请求都去查一下 token 是否有效。
addUserRecord 支持直接传哈希值,格式是 "username:realm:password" 的 MD5。
4.4 日志配置:别让调试变成大海捞针
Live555 的日志默认全往 stderr 输出,又乱又没分级。你得自己接管日志系统。
我一般会重写 UsageEnvironment 的 getResultMsg() 和 setResultMsg(),把日志重定向到文件,同时加上时间戳和日志级别。
class MyEnvironment : public BasicUsageEnvironment {
public:
virtual void log(Level level, const char* msg) {
time_t now = time(NULL);
fprintf(logFile, "[%s] [%s] %s\n",
getLevelStr(level), ctime(&now), msg);
}
};
日志级别我建议分四级:
- DEBUG:开发时用,打印每个包的序号和时间戳
- INFO:客户端连接/断开、文件打开/关闭
- WARN:丢包、重传、超时等非致命问题
- ERROR:文件打不开、端口被占用、内存分配失败
嗯,这里有个坑:Live555 内部有些日志是通过 env << ... 输出的,你重写了 operator<< 之后,记得把缓冲区刷新逻辑也改了,否则日志会攒在内存里不写盘。我曾经在线上环境发现日志文件一直是空的,排查了半天才发现是缓冲区没刷新。
4.5 性能调优:把每一毫秒都榨出来
性能调优是流媒体服务器的必修课。我总结了几条实战经验:
- 调整 socket 缓冲区大小:默认 64KB 太小,高码流视频容易丢包。我一般设到 512KB 或 1MB。
- 关闭 Nagle 算法:RTP 包是小包,Nagle 会延迟发送,导致延迟飙升。用
TCP_NODELAY关掉它。 - 使用 sendfile 零拷贝:如果文件在磁盘上,用
sendfile()系统调用,避免数据在内核态和用户态之间来回拷贝。 - 减少内存分配:预分配好 RTP 包的内存池,不要在每次发送时 new/delete。
| 调优项 | 默认值 | 推荐值 | 效果 |
|---|---|---|---|
| Socket 缓冲区 | 64 KB | 512 KB | 减少丢包 |
| Nagle 算法 | 开启 | 关闭 | 降低延迟 |
| 内存池大小 | 动态分配 | 预分配 1024 个包 | 减少 GC 压力 |
| 线程优先级 | 默认 | 实时优先级 (SCHED_RR) | 提高响应速度 |
我记得有一次线上直播卡顿,用 perf 一分析,发现 30% 的 CPU 时间花在了内存分配上。换成内存池之后,CPU 占用直接降到了 10% 以下。
4.6 本章知识体系总览
下面这张图把本章的五个进阶点串起来了。你可以看到,从文件输入到客户端输出,每一层都有对应的优化点。
这五个点不是孤立的。比如你加了访问控制,每次请求都要查数据库,这就会影响性能,反过来又需要调优。日志配置也是,如果日志写得太频繁,磁盘 I/O 会成为瓶颈。所以实际项目中,你得通盘考虑,找到最适合自己场景的平衡点。