简介:面向具备C++基础、希望掌握流媒体点播实现的中高级开发者,这份资源围绕live555 Media Server与MP4点播展开,可用于搭建RTSP点播服务、理解RTP封装与音视频同步机制,也可作为ffmpeg解析MP4元数据、处理客户端Range请求与断点续传的实践参考。压缩包共1487个文件,约31.69MB,以cpp与hh源码、h头文件为主体,辅以obj、tlog等编译中间产物,以及dll、lib、pdb等库与调试文件,并包含vcxproj、mak、sln等工程构建脚本和sdp示例、readme说明,覆盖多平台配置。已有675人学习下载。资源保留完整源码目录与示例程序,便于读者对照RTSP会话建立、媒体段流化与错误恢复流程逐层调试,是理解live555框架与MP4点播链路的实用素材。
1. live555-mp4点播:把 MP4 文件变成 RTSP 流的最小可用路径
手头有一堆 MP4 素材,想让播放器用 RTSP 地址直接拉流,而不是下载完再播——这个需求在局域网演示、机顶盒联调、监控回放这类场景里反复出现。live555 的testOnDemandRTSPServer自带 MPEG-4 文件点播能力,但默认只认它自己那套索引格式,直接丢一个普通 MP4 进去大概率起不来。这份 live555-mp4点播资源要解决的就是这段落差:让标准 MP4 文件通过 live555 以 RTSP 方式点播出去,客户端用 VLC、ffplay 或自研播放器都能拉。适合已经编译过 live555、懂基本 RTSP 交互、但卡在“文件怎么喂进去”的从业者。下面按“先跑通、再改参数、最后排坑”的顺序拆。
2. 先搞清楚 live555 点播 MP4 的链路:从文件到 RTP 包
2.1 为什么不能直接把 MP4 丢给 testOnDemandRTSPServer
live555 的点播服务器本质是一个ServerMediaSession挂多个ServerMediaSubsession,每个 subsession 负责一路媒体(视频或音频)的 RTP 打包与发送。对 MPEG-4 视频,它期望的输入是“按帧边界可随机访问”的字节流,并且需要知道每帧的时长、大小、是否关键帧。普通 MP4 是容器格式,音视频样本交错存放在 mdat 里,索引在 moov 里,live555 自带的MPEG4VideoFileServerMediaSubsession读的是它自己的.mpg式索引文件,不是 MP4 的 sample table。所以直接改个文件名传进去,服务器要么报“无法打开文件”,要么能打开但发不出正确的帧边界,客户端花屏或直接超时。
常见做法有两条路:一是把 MP4 解封装成 live555 认识的帧索引格式再喂给它;二是在 live555 里挂一个自定义的FramedSource,运行时解析 MP4 的 sample table,按帧读出 NAL 单元或视频帧。前者改动小、适合批量转存量素材;后者灵活、适合边转边播或需要精确控制时间戳的场景。这份资源走的是第一条路为主、第二条路留了扩展点,下面分别说清楚。
2.2 解封装与索引:把 MP4 的 sample table 读出来
MP4 的 moov box 里有 stbl(sample table box),核心是 stts(时间-样本)、stsz(样本大小)、stco/co64(chunk 偏移)、stsc(样本到 chunk 映射)、stss(关键帧)。要把 MP4 变成可点播的帧序列,就是把这五张表联合起来,算出每一帧在文件里的偏移、字节数、解码时间戳(DTS)和是否关键帧。这一步用任何能读 MP4 结构的库都行,Python 里用struct手撸 box 解析,或者用现成的解析库,逻辑是一样的。
下面这段是解析 stbl 关键表的骨架,重点看它怎么把三张表拼成帧列表:
import struct def read_box_header(f): # 每个 box 前 8 字节:4 字节大小 + 4 字节类型 raw = f.read(8) if len(raw) < 8: return None, None size, btype = struct.unpack(">I4s", raw) if size == 1: # 64 位扩展大小 size = struct.unpack(">Q", f.read(8))[0] return size, btype.decode("latin-1") def parse_stsz(f, size): # stsz: version/flags(4) + sample_size(4) + sample_count(4) + 每样本大小 f.read(4) default_size, count = struct.unpack(">II", f.read(8)) sizes = [] if default_size == 0: for _ in range(count): sizes.append(struct.unpack(">I", f.read(4))[0]) else: sizes = [default_size] * count return sizes def parse_stco(f, size): # stco: version/flags(4) + entry_count(4) + 每 chunk 偏移(4) f.read(4) count = struct.unpack(">I", f.read(4))[0] return [struct.unpack(">I", f.read(4))[0] for _ in range(count)] def parse_stsc(f, size): # stsc: version/flags(4) + entry_count(4) + 每项(first_chunk, samples_per_chunk, desc_index) f.read(4) count = struct.unpack(">I", f.read(4))[0] entries = [] for _ in range(count): entries.append(struct.unpack(">III", f.read(12))) return entries逻辑说明:read_box_header负责在文件里逐层下钻到moov/trak/mdia/minf/stbl;parse_stsz给出每帧字节数;parse_stco给出每个 chunk 在文件中的绝对偏移;parse_stsc说明“第几个 chunk 里装几帧”。三者联合后,从第一个 chunk 开始累加样本大小,就能还原出每一帧的(offset, size)。参数上要注意stsz的default_size非零时所有帧等长,这时不用逐帧读;stco是 32 位偏移,超过 4GB 的文件要用co64,解析函数要对应换 64 位读法,否则偏移会截断,表现为大文件后半段全花屏。
2.3 时间戳与关键帧:让客户端能 seek
光有帧偏移不够,RTSP 点播要能响应 PLAY 的 range 头,客户端拖进度条时服务器得从最近的关键帧开始发。所以解析时还要读stts(每段样本的时长)和stss(关键帧样本号)。stts是“count + delta”的游程编码,累加得到每帧 DTS;stss直接列出关键帧的样本索引,没有stss的 MP4 表示所有帧都是关键帧(常见于全 I 帧编码)。把这两样和帧列表对齐后,每个帧记录至少包含:文件偏移、字节数、DTS、是否关键帧。live555 侧的自定义FramedSource在doGetNextFrame()里按这个列表读文件、填fPresentationTime,关键帧信息用于ServerMediaSubsession的duration()和 seek 逻辑。
提示:DTS 单位是 timescale,不是秒。填
fPresentationTime前要除以mdhd里的 timescale 换成秒,否则客户端时间轴会整体偏移,表现为进度条走得飞快或几乎不动。
3. 把解析结果接进 live555:自定义 FramedSource 与 ServerMediaSubsession
3.1 继承 FramedSource,按帧喂数据
live555 的取流模型是拉模式:ServerMediaSubsession创建FramedSource,RTPSink不断调doGetNextFrame()要下一帧。自定义 source 要做的事很固定——打开文件、维护当前帧索引、每次调用读一帧到fTo、设置fFrameSize和fPresentationTime、调afterGetting(this)。下面是最小实现骨架:
class MP4FileSource : public FramedSource { public: static MP4FileSource* createNew(UsageEnvironment& env, char const* fileName) { return new MP4FileSource(env, fileName); } protected: MP4FileSource(UsageEnvironment& env, char const* fileName) : FramedSource(env), fFileName(strDup(fileName)), fCurFrame(0) { fFid = OpenInputFile(env, fileName); // live555 自带封装 parseIndex(); // 填充 fFrames 列表 } virtual void doGetNextFrame() { if (fCurFrame >= fFrames.size()) { handleClosure(); return; } FrameInfo& fi = fFrames[fCurFrame]; fFrameSize = fi.size; if (fFrameSize > fMaxSize) { // 缓冲区不够,截断并报错 fFrameSize = fMaxSize; envir() << "frame too large\n"; } fseek(fFid, fi.offset, SEEK_SET); fread(fTo, 1, fFrameSize, fFid); fPresentationTime = fi.pts; // 已换算成秒的 struct timeval fDurationInMicroseconds = 0; ++fCurFrame; afterGetting(this); // 通知 sink 可以取走了 } private: char* fFileName; FILE* fFid; std::vector<FrameInfo> fFrames; size_t fCurFrame; };逻辑说明:doGetNextFrame是唯一必须重写的方法,读帧、填元数据、回调三步不能少。fMaxSize是 sink 给的缓冲区上限,帧比它大时必须截断并记录,否则会越界写。fPresentationTime用gettimeofday风格的结构,从 DTS 换算。参数上,fDurationInMicroseconds一般置 0,让 sink 按帧率推算;如果素材是变帧率,这里要填真实帧间隔,否则客户端播放节奏会漂。handleClosure()表示流结束,不调它客户端会一直等。
3.2 挂上 ServerMediaSubsession 并注册到服务器
有了 source,还要一个OnDemandServerMediaSubsession子类把它包起来,负责创建RTPSink(视频一般是H264VideoRTPSink或MPEG4ESVideoRTPSink,取决于 MP4 里编码是 H.264 还是 MPEG-4 Part 2)和RTCPInstance。关键在createNewStreamSource返回你的 source,createNewRTPSink返回对应 sink。然后主程序里建RTSPServer,为每个文件建ServerMediaSession,addSubsession后addServerMediaSession,客户端就能用rtsp://ip:8554/文件名拉流。
ServerMediaSession* sms = ServerMediaSession::createNew( *env, streamName, streamName, "MP4 on demand"); sms->addSubsession(MP4FileServerMediaSubsession::createNew(*env, filePath, reuseFirstSource)); rtspServer->addServerMediaSession(sms);参数说明:reuseFirstSource为 True 时多个客户端共享同一个 source,适合点播同一文件;为 False 时每个客户端独立 source,seek 互不干扰但内存翻倍。streamName就是 URL 路径段,别带斜杠和空格。sink 选错编码类型是最常见的翻车点——H.264 的 MP4 用MPEG4ESVideoRTPSink发出去,客户端能连上但解不出画面,因为 RTP 负载格式对不上。
3.3 编译与联调:先本机 ffplay 验证
改完代码,在 live555 源码树里把新文件加进对应 Makefile 的OBJS,make后启动服务器。验证顺序建议固定:先ffplay rtsp://127.0.0.1:8554/test看能不能出画面,再用 VLC 拖进度条测 seek,最后用ffprobe -v trace看 RTSP 交互里 DESCRIBE/SETUP/PLAY 的 SDP 是否带对了a=control和a=range。本机通了再换局域网 IP,避免把网络问题误判成代码问题。
注意:live555 默认单线程事件循环,
doGetNextFrame里做同步文件读会阻塞整个服务器。素材在机械盘或网络盘上时,读一帧几十毫秒,多客户端并发就卡顿。常见做法是把文件读放到独立线程预读,或者至少用内存映射减少寻道。
4. 避坑与排查:点播起不来时先看这五处
4.1 现象:服务器启动正常,客户端 DESCRIBE 后无 SETUP
原因多半是ServerMediaSubsession的sdpLines()返回空或 track 数不对,常见于 sink 创建失败但没检查返回值。解决:在createNewRTPSink里判空并打日志,确认编码类型和 MP4 实际编码一致;同时检查auxSDPLine()是否被正确调用,H.264 的 SPS/PPS 要从 MP4 的 avcC box 里取出来填进 SDP 的sprop-parameter-sets,缺了它部分客户端直接放弃。
4.2 现象:能出画面但每隔几秒卡一下
原因是fPresentationTime用了墙钟而不是媒体时间,或者 DTS 换算漏了 timescale。表现是 RTP 时间戳跳变,客户端缓冲反复重建。解决:统一用媒体时间轴,fPresentationTime从 0 开始按帧累加;检查mdhd的 timescale 是否被误当成 1000。另一个诱因是fMaxSize截断了大关键帧,日志里会有 “frame too large”,把 sink 的rtpPayloadSize调大或改分片发送。
4.3 现象:拖进度条后花屏或黑屏
seek 时服务器没从关键帧起发,或者stss解析错位。解决:在seekStreamSource里把当前帧索引回退到最近的关键帧,而不是精确到目标帧;核对stss的样本号是从 1 开始还是从 0 开始,不同封装工具写法不一致,差一位就全错。验证方法是拖到任意位置后用ffprobe看首帧是否 IDR。
4.4 现象:大文件(超过 4GB)后半段全花
stco是 32 位偏移,超限必须用co64。解析时只认了stco,后半段偏移回绕到文件头,读出来的是垃圾数据。解决:在 stbl 里同时匹配stco和co64,后者每项 8 字节。这个坑在监控长录像里特别常见,短素材测不出来。
4.5 现象:多客户端同时拉流,第二个连上就断
reuseFirstSource为 True 时所有客户端共享一个 source 和播放位置,第二个客户端加入会打乱第一个的帧索引。解决:点播场景设 False,每个会话独立 source;如果确实要共享,得在 source 里做多播分发而不是简单复用。另外检查RTSPServer的reclamationSeconds,客户端异常断开后会话没及时回收也会占满连接。
5. 进阶:用 ffmpeg 预处理把兼容性拉满
自定义 source 能跑之后,真正决定“能不能被各种客户端稳定拉起来”的往往是素材本身。我一般会在入库前用 ffmpeg 统一转一遍,把编码、关键帧间隔、时间戳基准都规整掉,再交给 live555。这一步不是必须,但能省掉大量“某个播放器能播、另一个不能”的玄学问题。
ffmpeg -i input.mp4 \ -c:v libx264 -profile:v baseline -level 3.1 \ -g 30 -keyint_min 30 -sc_threshold 0 \ -c:a aac -b:a 128k \ -movflags +faststart \ output.mp4参数说明:-profile:v baseline兼容老客户端;-g 30 -keyint_min 30 -sc_threshold 0强制固定 GOP,让stss规整、seek 粒度可预期;-movflags +faststart把 moov 挪到文件头,解析索引不用扫全文件。转完再用前面的解析脚本核对帧数和关键帧数,和ffprobe -show_frames的输出对一遍,对不上就说明解析逻辑有边界没覆盖。
验证方法我固定走三步:ffprobe看编码和时长是否与源一致;本机ffplay拉流看首帧时间和拖拽响应;最后用两个不同客户端同时拉,观察服务器日志里 source 创建和销毁是否成对。这套流程跑顺之后,新素材入库基本不用再人工盯。
血泪经验是:别在 source 里做任何“聪明”的容错,比如帧读失败就跳过继续。跳过会让时间戳和帧索引错位,客户端表现是画面越来越慢或音画不同步,排查时根本看不出是哪一帧出的问题。宁可让doGetNextFrame在异常时直接handleClosure()并打日志,把问题暴露在服务器侧。从那以后我每次接新的 MP4 点播需求,都先把索引解析结果和 ffprobe 对一遍再动 live555 的代码,这个习惯救过好几次。希望帮到你。
本文还有配套的精品资源,点击获取