4. Live555进阶:H.264文件推送、多线程处理、访问控制、日志配置、性能调优

基础功能跑通了,接下来才是真正有意思的部分。Live555 默认的 testOnDemandRTSPServer 只能播个测试流,真要上生产环境,你得自己动手改造。这一章我带你过一遍实际项目中必须掌握的五个进阶点。

本章核心目标: 让 Live555 从“能跑”变成“好用”。你会学会推送真正的 H.264 文件、用多线程处理并发、加一层访问控制、把日志管起来,最后再压榨一下性能。

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,否则解码器会花屏。我在项目中遇到过这个问题,当时排查了整整一个下午,最后发现就是少传了这个参数。

避坑指南: 我曾经因为文件路径写成了相对路径,导致服务器启动后一直报 "file not found"。后来我统一用绝对路径 + 配置文件管理,再也没出过这种低级错误。

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 是否有效。

小技巧: 测试阶段可以用明文密码,但生产环境一定要用 MD5 或 SHA256 的哈希值。Live555 的 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 性能调优:把每一毫秒都榨出来

性能调优是流媒体服务器的必修课。我总结了几条实战经验:

  1. 调整 socket 缓冲区大小:默认 64KB 太小,高码流视频容易丢包。我一般设到 512KB 或 1MB。
  2. 关闭 Nagle 算法:RTP 包是小包,Nagle 会延迟发送,导致延迟飙升。用 TCP_NODELAY 关掉它。
  3. 使用 sendfile 零拷贝:如果文件在磁盘上,用 sendfile() 系统调用,避免数据在内核态和用户态之间来回拷贝。
  4. 减少内存分配:预分配好 RTP 包的内存池,不要在每次发送时 new/delete。
调优项 默认值 推荐值 效果
Socket 缓冲区 64 KB 512 KB 减少丢包
Nagle 算法 开启 关闭 降低延迟
内存池大小 动态分配 预分配 1024 个包 减少 GC 压力
线程优先级 默认 实时优先级 (SCHED_RR) 提高响应速度

我记得有一次线上直播卡顿,用 perf 一分析,发现 30% 的 CPU 时间花在了内存分配上。换成内存池之后,CPU 占用直接降到了 10% 以下。

警告: 设置实时优先级要小心。如果某个线程死循环了,整个系统都可能卡死。建议只在关键线程上设置,并且加上 watchdog 监控。

4.6 本章知识体系总览

下面这张图把本章的五个进阶点串起来了。你可以看到,从文件输入到客户端输出,每一层都有对应的优化点。

Live555 进阶架构图 H.264 文件输入 访问控制 (Auth) 多线程处理 日志配置 性能调优 RTP 流输出 说明: 实线箭头表示数据流方向,虚线表示配置/监控关系。 日志配置和性能调优贯穿整个系统,不是独立模块。

这五个点不是孤立的。比如你加了访问控制,每次请求都要查数据库,这就会影响性能,反过来又需要调优。日志配置也是,如果日志写得太频繁,磁盘 I/O 会成为瓶颈。所以实际项目中,你得通盘考虑,找到最适合自己场景的平衡点。