文章目录
项目概述技术分析支持的编码格式传输方式心跳检测机制RTSP 推流安全性 架构分析RtspServer 整体架构流程分析1. 客户端连接和会话建立2. 媒体数据传输3. 心跳检测和连接维护 xop 基础库项目介绍功能特性xop 整体架构 应用场景社区问题收集与解答问题一:刚开始播放时有些花屏问题二:推送 H.265 流时播放器无法播放 Bug 修复与代码分析问题描述分析与解决方案1. 修改 RTP 包的最大负载大小2. 调整 RTP TCP 头部大小3. 修正 H.264 视频源的帧处理4. 修正 H.265 视频源的帧处理 总结 进一步的技术分析深入理解 RTP 分片与 NALU 处理H.264 中的 FU-A 分片H.265 中的 FU 分片 起始码的处理播放器兼容性
项目概述
RtspServer 是由 PHZ76 开发的高效、可定制的实时流媒体服务器解决方案。它基于作者编写的网络基础库 xop,允许开发者轻松处理和分发实时音视频流。项目提供了一个名为 DesktopSharing 的示例应用,可以捕获桌面和麦克风声音,并在编码后通过 RTSP 协议进行转发和推流。该项目支持 Windows 和 Linux 平台,代码量少,相比于 live555 等经典的流媒体库,RtspServer 更加轻量级,易于集成和二次开发,已在公司的项目中应用。
技术分析
支持的编码格式
RtspServer 支持多种音视频编码格式,涵盖了广泛的应用场景:
视频编码:H.264、H.265音频编码:G.711A、AAC传输方式
单播 (Unicast):
RTP_OVER_UDP:通过 UDP 协议传输 RTP 数据,延迟低,但在网络不稳定时可能会丢包。RTP_OVER_RTSP (TCP):通过 RTSP 协议在 TCP 连接上传输 RTP 数据,可靠性高,适用于防火墙或 NAT 环境。组播 (Multicast):
适用于需要将同一流媒体数据发送给多个客户端的场景,节省网络带宽。心跳检测机制
针对单播传输,RtspServer 内置了心跳检测机制,可以及时发现和处理连接异常,确保数据传输的稳定性。
RTSP 推流
RtspServer 支持 RTSP 推流功能,使用 TCP 协议进行数据传输,保证了数据的可靠传输,适用于对传输可靠性要求较高的应用场景。
安全性
RtspServer 内置了摘要认证(Digest Authentication),为服务提供了安全保障,防止未经授权的访问。
架构分析
RtspServer 整体架构
RtspServer 的架构主要包括以下组件:
RTSP Server:处理客户端的 RTSP 请求,包括 SETUP、PLAY、PAUSE、TEARDOWN 等指令,管理会话和媒体流。Media Session:表示一个媒体会话,包含媒体流的相关信息,如流名称、媒体类型、编码格式等。Media Source:媒体源,负责提供音视频帧数据,可以来自文件、摄像头、麦克风或其他实时数据源。RTP Connection:负责通过 RTP 协议发送音视频数据,支持单播和组播传输。流程分析
1. 客户端连接和会话建立
客户端通过 RTSP 协议发送连接请求到服务器。RTSP Server 接收到请求后,解析并创建一个新的 Media Session。为每个媒体流(音频或视频)创建对应的 Media Source。2. 媒体数据传输
Media Source 获取音视频数据帧(可能来自编码器或实时采集设备)。RTP Connection 负责将媒体数据打包成 RTP 包,通过网络传输给客户端。支持的传输方式包括 RTP_OVER_UDP、RTP_OVER_RTSP 和组播。3. 心跳检测和连接维护
为了保持连接的稳定性,服务器会定期发送心跳检测,确认客户端的在线状态。如果检测到连接异常,服务器会及时释放资源,防止资源泄漏。xop 基础库
项目介绍
xop 是 RtspServer 的基础网络库,参考了 muduo 和 live555 的设计,封装了一个简单高效的网络框架,提供了构建高性能网络应用的基础组件。
功能特性
跨平台支持:兼容 Windows 和 Linux 操作系统。 Windows 下:使用select
实现事件循环。Linux 下:使用 epoll
实现高效的事件通知机制。 事件驱动模型:基于 Reactor 模式,实现非阻塞 IO 和事件驱动。定时器:提供高精度的定时任务调度。内存管理:实现了环形缓冲区和内存池,提升内存分配和释放的效率。日志系统:内置简洁的日志功能,方便调试和运行监控。 xop 整体架构
EventLoop:事件循环,核心组件,负责监听和分发 IO 事件、定时器事件等。Channel:通道,封装了文件描述符及其感兴趣的事件类型,如可读、可写等。TimerQueue:定时器队列,管理所有的定时任务。Buffer:缓冲区,提供高效的数据读写接口。Acceptor:监听器,负责接受新的客户端连接。应用场景
在线教育平台:提供高清、流畅的远程教学体验,实现教师与学生的实时互动。视频监控系统:实时传输监控摄像头的视频流,实现远程监控和安全管理。远程会议系统:确保音频和视频的同步传输,提高远程会议的质量和效率。桌面共享与远程协助:通过 DesktopSharing 示例应用,实现桌面实时共享和远程协助功能。社区问题收集与解答
问题一:刚开始播放时有些花屏
问题描述:
在播放开始时,视频出现花屏现象。怀疑是与 GOP(Group of Pictures)有关,没有收到完整的一组 GOP,所以会花屏。是否应该等待下一个 I 帧?
解答:
是的,花屏问题通常是由于解码器没有接收到完整的关键帧(I 帧)导致的。解码器需要从 I 帧开始才能正确解码后续的 P 帧和 B 帧。
解决方案:
确保首帧为关键帧:在客户端连接后,服务器应确保发送的第一帧是关键帧。可以在编码器中设置,让其在新的会话开始时立即生成一个 I 帧。检查帧类型:在编码过程中,可以通过AVFrame
或其他编码库的接口,获取帧类型,根据编码后的类型(I 帧、P 帧、B 帧)进行处理。缓冲策略:在客户端实现一定的缓冲策略,等待接收到第一个 I 帧后再开始解码和播放。 问题二:推送 H.265 流时播放器无法播放
问题描述:
在使用 H.265 推流时,播放器无法正常播放视频。怀疑是在 H265Source
的 handleFrame
函数中处理有误。
解答:
问题可能出在对 NALU(Network Abstraction Layer Unit)的处理上。在 H.265 码流中,每个 NALU 前通常有一个起始码(如 0x00 00 00 01
)。在计算 NALU 类型时,需要正确跳过起始码。
解决方案:
去除起始码:在处理帧数据时,需要跳过起始码,以正确解析 NALU 类型。
uint8_t *frameBuf = frame.buffer.get();uint32_t frameSize = frame.size;frameBuf += 4; // 跳过起始码frameSize -= 4; // 调整帧大小
统一处理方式:建议在构建 VideoFrame
时就去掉起始码,这样后续的处理会更加统一。
区别转发和推流:对于转发和推流的处理,可以有所区别。直接获取 H.265 帧数据后调用 pushFrame
,确保计算时是带有起始码的头部信息。
注意:
NALU 解析:正确解析 NALU 类型对于视频解码非常重要。确保在处理时准确跳过起始码,才能正确识别帧类型。
播放器兼容性:不同的播放器对码流格式的要求可能不同,确保码流符合标准,有助于提高兼容性。
Bug 修复与代码分析
问题描述
在使用 RtspServer 时,发现使用 VLC 和 ffplay 播放时出现错误:
Invalid NAL unit 0, skipping
导致播放器无法正常播放视频流。需要对代码进行修复,解决该问题。
分析与解决方案
经过分析,问题出在 RTP 包的组装和 NALU 的处理上。以下是具体的修复步骤和代码修改。
1. 修改 RTP 包的最大负载大小
文件:rtp.h
问题分析:
原始定义的MAX_RTP_PAYLOAD_SIZE
为 1420,可能导致 RTP 包过小,增加了网络负载。RTP 头部的大小为 12 字节,实际可用的负载大小应该更大。 修改内容:
// 原始定义#define MAX_RTP_PAYLOAD_SIZE 1420// 修改为#define MAX_RTP_PAYLOAD_SIZE 1440
解释:
将MAX_RTP_PAYLOAD_SIZE
增加到 1440,可以减少 RTP 包的数量,提高传输效率。参考了 Live555 中的 MultiFramedRTPSink.cpp
,调整了最大负载大小。 2. 调整 RTP TCP 头部大小
文件:rtp.h
问题分析:
原代码中RTP_TCP_HEAD_SIZE
定义为 4,当传输模式为 RTP_OVER_UDP
时,这个值应该为 0。在 UDP 传输中,不需要额外的 TCP 头部。 修改内容:
// 原始定义//#define RTP_TCP_HEAD_SIZE 4 // when transport_mode_ = RTP_OVER_TCP// 修改为#define RTP_TCP_HEAD_SIZE 0 // when transport_mode_ = RTP_OVER_UDP
解释:
将RTP_TCP_HEAD_SIZE
设置为 0,确保在 UDP 传输时,不会错误地加入额外的头部。 3. 修正 H.264 视频源的帧处理
文件:H264Source.cpp
问题分析:
在处理 H.264 帧时,需要正确跳过起始码(0x00 00 00 01
),否则会导致 NALU 解析错误。当帧大小小于等于最大 RTP 负载时,直接发送整个帧;否则,需要进行分片(FU-A)。 修改内容:
if (frame_size <= MAX_RTP_PAYLOAD_SIZE){ frame_buf += 4; // 跳过起始码 frame_size -= 4; // 构建 RTP 包 RtpPacket rtp_pkt; rtp_pkt.type = frame.type; rtp_pkt.timestamp = frame.timestamp; rtp_pkt.size = frame_size + RTP_TCP_HEAD_SIZE + RTP_HEADER_SIZE; rtp_pkt.last = 1; ...}else{ frame_buf += 4; // 跳过起始码 frame_size -= 4; char FU_A[2] = {0}; FU_A[0] = (frame_buf[0] & 0xE0) | 28; // NALU 头部 FU_A[1] = 0x80 | (frame_buf[0] & 0x1F); // FU-A 起始标志 // 进行分片处理}
解释:
对于小于等于最大负载的帧,直接跳过起始码,发送整个 NALU。对于大于最大负载的帧,跳过起始码,进行 FU-A 分片,需要正确构建 FU-A 头部。4. 修正 H.265 视频源的帧处理
文件:H265Source.cpp
问题分析:
H.265 的 NALU 解析与 H.264 类似,但头部格式不同,需要相应调整。同样需要正确处理起始码和分片。修改内容:
if (frame_size <= MAX_RTP_PAYLOAD_SIZE){ frame_buf += 4; // 跳过起始码 frame_size -= 4; // 构建 RTP 包 RtpPacket rtp_pkt; rtp_pkt.type = frame.type; rtp_pkt.timestamp = frame.timestamp; rtp_pkt.size = frame_size + RTP_TCP_HEAD_SIZE + RTP_HEADER_SIZE; rtp_pkt.last = 1; ...}else{ frame_buf += 4; // 跳过起始码 frame_size -= 4; uint8_t PL_FU[3] = {0}; uint8_t nalUnitType = (frame_buf[0] & 0x7E) >> 1; PL_FU[0] = (frame_buf[0] & 0x81) | (49 << 1); // NALU 头部 PL_FU[1] = frame_buf[1]; // NALU 头部 PL_FU[2] = 0x80 | nalUnitType; // FU 起始标志 // 进行分片处理}
解释:
同样跳过起始码,正确调整frame_buf
和 frame_size
。构建 H.265 的分片头部,确保 NALU 类型和标志位正确。 总结
通过上述修改,修复了播放器无法正常播放的问题。主要原因在于:
起始码处理:需要正确跳过起始码,才能正确解析 NALU。RTP 包组装:调整最大负载大小,确保网络传输的效率和稳定性。分片处理:在帧过大时,正确地进行 RTP 分片,构建正确的 FU-A(H.264)或 FU(H.265)包。这些修改使得 RtspServer 在与常见播放器(如 VLC、ffplay)兼容性方面得到了改善。
进一步的技术分析
深入理解 RTP 分片与 NALU 处理
在 RTP 传输中,当单个 NALU 的大小超过了最大 RTP 负载大小时,需要对其进行分片。分片时,需要构建特殊的 RTP 包,称为 Fragmentation Unit(FU)。
H.264 中的 FU-A 分片
FU-A 头部格式:
+---------------+|0|1|2|3|4|5|6|7|+-+-+-+-+-+-+-+-+|S|E|R| Type |+---------------+
S(Start):分片的开始标志位,1 表示这是第一个分片。
E(End):分片的结束标志位,1 表示这是最后一个分片。
Type:原始 NALU 的类型。
H.265 中的 FU 分片
FU 头部格式:
+---------------+|0|1|2|3|4|5|6|7|+-+-+-+-+-+-+-+-+|S|E| Type |+---------------+
S(Start):分片的开始标志位。
E(End):分片的结束标志位。
Type:原始 NALU 的类型。
起始码的处理
起始码(Start Code):在原始的 H.264/H.265 码流中,起始码用于分隔 NALU,一般为0x00 00 00 01
。RTP 传输中不需要起始码:在 RTP 传输中,起始码不应包含在传输的数据中,需要在发送前去除。解析 NALU 类型:跳过起始码后,才能正确解析 NALU 的头部,获取 NALU 类型。 播放器兼容性
VLC 和 ffplay 等播放器在解析 RTP 流时,严格遵循标准。如果发送的数据不符合 RTP 和 H.264/H.265 的封装规范,就会出现错误。
错误信息:
Invalid NAL unit 0, skipping
表示接收到的 NALU 类型为 0,这是无效的,可能是因为没有正确跳过起始码导致的。