
简介面向具备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 丢给 testOnDemandRTSPServerlive555 的点播服务器本质是一个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 里有 stblsample table box核心是 stts时间-样本、stsz样本大小、stco/co64chunk 偏移、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/stblparse_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”的游程编码累加得到每帧 DTSstss直接列出关键帧的样本索引没有stss的 MP4 表示所有帧都是关键帧常见于全 I 帧编码。把这两样和帧列表对齐后每个帧记录至少包含文件偏移、字节数、DTS、是否关键帧。live555 侧的自定义FramedSource在doGetNextFrame()里按这个列表读文件、填fPresentationTime关键帧信息用于ServerMediaSubsession的duration()和 seek 逻辑。提示DTS 单位是 timescale不是秒。填fPresentationTime前要除以mdhd里的 timescale 换成秒否则客户端时间轴会整体偏移表现为进度条走得飞快或几乎不动。3. 把解析结果接进 live555自定义 FramedSource 与 ServerMediaSubsession3.1 继承 FramedSource按帧喂数据live555 的取流模型是拉模式ServerMediaSubsession创建FramedSourceRTPSink不断调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::vectorFrameInfo 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返回你的 sourcecreateNewRTPSink返回对应 sink。然后主程序里建RTSPServer为每个文件建ServerMediaSessionaddSubsession后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 时每个客户端独立 sourceseek 互不干扰但内存翻倍。streamName就是 URL 路径段别带斜杠和空格。sink 选错编码类型是最常见的翻车点——H.264 的 MP4 用MPEG4ESVideoRTPSink发出去客户端能连上但解不出画面因为 RTP 负载格式对不上。3.3 编译与联调先本机 ffplay 验证改完代码在 live555 源码树里把新文件加进对应 Makefile 的OBJSmake后启动服务器。验证顺序建议固定先ffplay rtsp://127.0.0.1:8554/test看能不能出画面再用 VLC 拖进度条测 seek最后用ffprobe -v trace看 RTSP 交互里 DESCRIBE/SETUP/PLAY 的 SDP 是否带对了acontrol和arange。本机通了再换局域网 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 的代码这个习惯救过好几次。希望帮到你。本文还有配套的精品资源点击获取