资讯详情

资讯详情

Delphi7 实现 RTSP 拉流与 H264 解码:从信令到渲染的完整链路

简介这份资源面向需要在 Delphi 7 环境下处理实时音视频流的开发者提供一套可运行的 RTSP 客户端实现代码与演示工程帮助解决传统 VCL 项目难以接入网络流媒体的问题。内容围绕 RTSP 协议交互展开涉及 DESCRIBE、SETUP、PLAY、PAUSE、TEARDOWN 等请求方法并结合 SDP 信息解析、H.264 视频流处理、Indy 或 Winsock 网络通信、多线程接收与解码播放等关键环节适合具备一定 Delphi 基础、希望深入理解流媒体协议实现细节的中级开发者参考。压缩包为 rar 格式整体约 9.38MB文件总数暂未提供明细从标签与描述看应包含源码、演示工程及必要的依赖说明便于直接编译调试与二次开发。目前已有 1003 人学习下载读者可通过分析 DEMO 掌握 RTSP 会话维护、错误处理与内存优化思路并将其迁移到自己的监控、直播或视频会议类项目中。1. Delphi7 接 RTSP 流一个被低估的老工具怎么把 H264 拉回来手上还有 Delphi7 项目要维护的人大概率遇到过这种需求客户现场一台海康或大华的摄像头要求把实时画面嵌进现有的 Win32 桌面程序里不能换框架不能上 Electron最好连第三方控件都少引。这时候搜到demo_rtsp_rtspfordelphi_delphi7rtsp_delphih264_d这类资源第一反应往往是怀疑——Delphi7 是 2002 年的东西RTSP 是实时流协议H264 是视频编码这三样凑一起能跑通吗能跑而且思路比想象中直接。RTSP 负责和摄像头协商会话、拿到 RTP 包H264 负责把裸流解码成 YUV 或 RGBDelphi7 负责把解码后的帧画到窗口上。这份资源的价值不在于它用了多新的技术而在于它把「RTSP 信令 RTP 收包 H264 解码 画面渲染」这条链路用 Pascal 串了起来适合需要在老项目里快速验证拉流可行性的场景。如果你正在做工业监控、医疗设备界面、或者内网视频上墙又不想重写整个客户端这套代码值得拆开看。2. RTSP 会话与 RTP 收包Delphi7 里怎么把流拉稳2.1 为什么不用现成播放器控件而是自己走 RTSP很多人第一反应是塞一个 VLC ActiveX 或者 FFmpeg 的 dll 进去。能跑但有两个现实问题一是部署环境往往不允许装额外运行时二是 ActiveX 在 Win32 下的窗口消息循环和 Delphi7 的 VCL 容易打架表现为画面卡死或者拖动窗口后黑屏。自己实现 RTSP 的好处是链路完全可控收包、解码、渲染都在自己的线程里出问题能定位到具体哪一步。RTSP 本身是文本协议类似 HTTP方法有 OPTIONS、DESCRIBE、SETUP、PLAY、TEARDOWN。摄像头返回的 SDP 里会描述媒体流信息重点是mvideo行和acontrol行后者告诉你 RTP 流的 URL。常见做法是用一个 TCP 连接发 RTSP 命令然后根据 SETUP 时协商的传输方式决定 RTP 走 UDP 还是 TCP。内网环境优先 UDP延迟低如果现场网络有丢包或者跨网段TCP 更稳但延迟会高一些。下面是一个 RTSP 请求构造的简化示例用 Delphi7 的TIdTCPClient或者原生TClientSocket都可以// 构造 RTSP DESCRIBE 请求 function BuildDescribeRequest(const Url, CSeq: string): string; begin Result : DESCRIBE Url RTSP/1.0 #13#10 CSeq: CSeq #13#10 Accept: application/sdp #13#10 User-Agent: DelphiRTSPDemo #13#10 #13#10; end; // 解析 SDP 中的 control 行拿到 track URL function ParseControlUrl(const Sdp, BaseUrl: string): string; var Lines: TStringList; I: Integer; P: Integer; begin Result : ; Lines : TStringList.Create; try Lines.Text : Sdp; for I : 0 to Lines.Count - 1 do begin if Pos(acontrol:, Lines[I]) 1 then begin Result : Copy(Lines[I], 11, MaxInt); // 如果是相对路径拼上 BaseUrl if Pos(rtsp://, Result) 1 then begin P : Pos(rtsp://, BaseUrl); if P 1 then Result : BaseUrl / Result; end; Break; end; end; finally Lines.Free; end; end;这段代码的逻辑很直白DESCRIBE 拿 SDP从 SDP 里找acontrol行确定视频轨道的 URL。参数上要注意CSeq必须递增摄像头会校验User-Agent有些设备不校验但填上更规范。如果返回 401说明需要认证常见的是 Basic 认证在请求头里加Authorization: Basic base64(user:pass)即可。海康的 RTSP 地址格式一般是rtsp://user:passip:554/Streaming/Channels/101101 是主码流102 是子码流子码流分辨率低、码率小适合预览。2.2 RTP 收包与 H264 分片重组SETUP 完成后发 PLAY摄像头开始往你指定的端口发 RTP 包。RTP 头是 12 字节固定部分后面跟负载。H264 在 RTP 里的打包方式有几种最常见的是 FU-A 分片。一个大的 NALU 被拆成多个 RTP 包每个包有 FU indicator 和 FU header你需要根据 marker 位和序列号把它们拼回完整的 NALU。// 处理 RTP 包重组 H264 NALU procedure TForm1.ProcessRtpPacket(const Buf: array of Byte; Len: Integer); var PayloadType, SeqNum: Word; Marker: Boolean; Payload: array of Byte; FuIndicator, FuHeader: Byte; NalType: Byte; begin if Len 12 then Exit; // RTP 头解析 PayloadType : Buf[1] and $7F; SeqNum : (Buf[2] shl 8) or Buf[3]; Marker : (Buf[1] and $80) 0; // 负载从第 13 字节开始12 字节头 可能的扩展这里简化处理 SetLength(Payload, Len - 12); Move(Buf[12], Payload[0], Len - 12); if (PayloadType 96) or (PayloadType 98) then // 动态负载类型常见 96 begin FuIndicator : Payload[0]; NalType : FuIndicator and $1F; if NalType 28 then // FU-A begin FuHeader : Payload[1]; if (FuHeader and $80) 0 then begin // 起始分片开始新的 NALU 缓冲 FCurrentNalu.Clear; FCurrentNalu.Add(Byte((FuIndicator and $E0) or (FuHeader and $1F))); end; // 追加分片数据 FCurrentNalu.Append(Payload, 2, Length(Payload) - 2); if (FuHeader and $40) 0 then begin // 结束分片送去解码 DecodeNalu(FCurrentNalu.Data, FCurrentNalu.Size); end; end else if NalType 1 then // 单个 NALU begin DecodeNalu(Payload, Length(Payload)); end; end; end;这里的关键参数是负载类型海康一般用 96大华可能用 98具体看 SDP 里的artpmap行。FU-A 的判断依据是 NALU 类型 28起始分片的 FU header 最高位为 1结束分片次高位为 1。重组时要把 FU indicator 的高 3 位和 FU header 的低 5 位拼成原始 NALU 头。如果这一步错了解码器会报错或者花屏。常见坑是忘了处理 marker 位导致最后一帧丢包或者序列号回绕时没做处理长时间运行后画面卡住。3. H264 解码与画面渲染从裸流到窗口3.1 解码器的选择与初始化Delphi7 本身不带 H264 解码能力必须借助外部库。常见做法有三种一是调用 FFmpeg 的 avcodec dll功能最全但接口复杂二是用开源的 H264 解码库如 OpenH264 或 JM 的简化版三是直接用摄像头 SDK 自带的解码回调。这份资源里delphih264相关的部分大概率是封装了一个轻量解码器或者调用了系统自带的解码器。如果走 FFmpeg 路线初始化流程是avcodec_find_decoder→avcodec_alloc_context3→avcodec_open2然后每收到一个完整 NALU 就调avcodec_send_packet和avcodec_receive_frame。Delphi7 调 dll 要用external声明注意调用约定FFmpeg 默认是 cdecl。// FFmpeg 解码器初始化简化声明 type TAVCodec record end; PAVCodec ^TAVCodec; TAVCodecContext record end; PAVCodecContext ^TAVCodecContext; TAVFrame record end; PAVFrame ^TAVFrame; TAVPacket record end; PAVPacket ^TAVPacket; function avcodec_find_decoder(id: Integer): PAVCodec; cdecl; external avcodec.dll; function avcodec_alloc_context3(codec: PAVCodec): PAVCodecContext; cdecl; external avcodec.dll; function avcodec_open2(ctx: PAVCodecContext; codec: PAVCodec; options: Pointer): Integer; cdecl; external avcodec.dll; function avcodec_send_packet(ctx: PAVCodecContext; pkt: PAVPacket): Integer; cdecl; external avcodec.dll; function avcodec_receive_frame(ctx: PAVCodecContext; frame: PAVFrame): Integer; cdecl; external avcodec.dll; // 初始化 procedure TForm1.InitDecoder; begin FCodec : avcodec_find_decoder(27); // AV_CODEC_ID_H264 27 if FCodec nil then begin ShowMessage(找不到 H264 解码器); Exit; end; FCodecCtx : avcodec_alloc_context3(FCodec); if avcodec_open2(FCodecCtx, FCodec, nil) 0 then begin ShowMessage(解码器打开失败); Exit; end; end;参数说明avcodec_find_decoder的参数是编码 IDH264 对应 27这个数字在不同 FFmpeg 版本里可能变化最好用avcodec_find_decoder_by_name(h264)更稳。avcodec_open2的第三个参数可以传字典设置线程数比如threads4多核解码能明显降低延迟。如果返回负值用av_strerror转成文字看具体原因常见的是 dll 版本不匹配。3.2 渲染到 Delphi7 窗口的两种方式解码出来的是 YUV 帧要显示到 TImage 或者 TPaintBox 上需要转成 RGB 再画。方式一用 FFmpeg 的sws_scale做 YUV420P 到 RGB24 的转换然后构造一个 TBitmap把 RGB 数据拷进去最后Canvas.Draw。方式二如果显卡支持用 DirectShow 或者 OpenGL 直接渲染 YUV省掉转换开销但 Delphi7 下集成成本高。我一般用方式一简单可控。转换时注意 stride 对齐FFmpeg 解码出来的 frame 每行可能有 paddinglinesize[0]不一定等于宽度。拷贝时要按行拷不能一次性 Move 整个缓冲区。// YUV420P 转 RGB24 并绘制到 PaintBox procedure TForm1.DrawFrame(frame: PAVFrame); var RgbBuf: array of Byte; Stride: Integer; Bmp: TBitmap; Src, Dst: PByte; Y, X: Integer; begin Stride : frame.linesize[0]; SetLength(RgbBuf, frame.width * frame.height * 3); // sws_scale 转换这里省略 sws_context 的初始化 sws_scale(FSwsCtx, frame.data[0], frame.linesize[0], 0, frame.height, RgbBuf[0], Stride); Bmp : TBitmap.Create; try Bmp.PixelFormat : pf24bit; Bmp.Width : frame.width; Bmp.Height : frame.height; for Y : 0 to frame.height - 1 do begin Src : RgbBuf[Y * Stride]; Dst : Bmp.ScanLine[Y]; Move(Src^, Dst^, frame.width * 3); end; PaintBox1.Canvas.Draw(0, 0, Bmp); finally Bmp.Free; end; end;这段代码里sws_scale的输入是 YUV 三个平面输出是 RGB 缓冲区。Stride是输出每行的字节数可能被对齐到 4 的倍数。绘制时用ScanLine逐行拷贝避免 TBitmap 内部格式差异导致颜色错乱。如果画面出现绿色或紫色多半是 YUV 和 RGB 的排列顺序搞反了检查sws_scale的像素格式参数。4. 避坑与排查那些让画面卡住、花屏、延迟的细节4.1 现象连接成功但一直黑屏原因SETUP 时传输方式选错或者 RTP 端口被防火墙挡住。UDP 模式下摄像头往你指定的端口发流如果 Windows 防火墙没放行包全丢。解决先临时关防火墙测试确认后加规则放行或者改用 TCP 传输在 SETUP 的Transport头里写RTP/AVP/TCP;unicast;interleaved0-1这样 RTP 和 RTCP 会复用 RTSP 的 TCP 连接不用额外开端口。4.2 现象画面花屏、马赛克过几秒恢复原因RTP 丢包或者 NALU 重组时丢了起始分片。UDP 在内网也可能因为缓冲区满丢包。解决加大 socket 接收缓冲区Delphi7 里用setsockopt设SO_RCVBUF到 1MB 以上同时在重组逻辑里如果收到结束分片但没有对应的起始分片直接丢弃这一帧不要硬拼。另外检查FCurrentNalu的清空时机收到新的起始分片时必须清空旧缓冲。4.3 现象延迟越来越大几分钟后画面落后十几秒原因解码和渲染在同一个线程里收包线程被阻塞RTP 包在缓冲区堆积。解决收包、解码、渲染分三个线程用环形缓冲区传递 NALU。收包线程只管收解码线程从队列取渲染线程用PostMessage通知主线程刷新。注意 Delphi7 的 VCL 不是线程安全的画图必须在主线程做。4.4 现象程序运行一段时间后崩溃报访问违例原因FFmpeg 的AVFrame和AVPacket没有正确释放或者多线程同时操作解码器上下文。解决每次avcodec_receive_frame后调av_frame_unrefavcodec_send_packet前确保 packet 已经av_packet_alloc并正确填充。解码器上下文不要跨线程共享一个解码器只在一个线程里用。Delphi7 里用try...finally保证异常时也能释放。4.5 现象海康摄像头返回 401但用户名密码是对的原因海康的 RTSP 认证方式可能是 Digest 而不是 Basic或者密码里有特殊字符没转义。解决先确认地址格式rtsp://admin:password192.168.1.64:554/Streaming/Channels/101密码里的和:要 URL 编码。如果还是 401抓包看WWW-Authenticate头如果是 Digest需要按 RFC 2617 计算 response比 Basic 麻烦但也不复杂。5. 进阶把拉流封装成可复用的 Delphi7 组件5.1 设计一个 TRtspClient 组件与其每次新建项目都复制粘贴代码不如封装成一个TComponent派生类暴露Url、UserName、Password、OnFrame事件。内部管理 RTSP 会话线程、RTP 收包线程、解码线程外部只需要设置属性、调Open和Close。这样在多个项目里复用也方便单元测试。type TFrameEvent procedure(Sender: TObject; Bmp: TBitmap) of object; TRtspClient class(TComponent) private FUrl: string; FUserName: string; FPassword: string; FOnFrame: TFrameEvent; FSessionThread: TRtspSessionThread; FDecodeThread: TDecodeThread; procedure DoFrame(Bmp: TBitmap); public procedure Open; procedure Close; property Url: string read FUrl write FUrl; property UserName: string read FUserName write FUserName; property Password: string read FPassword write FPassword; property OnFrame: TFrameEvent read FOnFrame write FOnFrame; end; procedure TRtspClient.DoFrame(Bmp: TBitmap); begin if Assigned(FOnFrame) then FOnFrame(Self, Bmp); end;组件化的关键是线程安全。OnFrame事件在解码线程里触发但使用方可能直接操作 VCL 控件所以要么在组件内部用Synchronize切回主线程要么在文档里明确要求使用方自己PostMessage。我一般选后者因为Synchronize会阻塞解码线程影响吞吐。5.2 验证拉流稳定性的三个指标封装完之后怎么判断它能不能上生产看三个数首帧时间、丢包率、CPU 占用。首帧时间从Open到第一次OnFrame内网 UDP 应该在 500ms 以内TCP 会多 200ms 左右。丢包率可以在 RTP 层统计序列号间隙超过 1% 就要考虑换 TCP 或者调缓冲区。CPU 占用在 1080p 25fps 下单核解码应该在 30% 以下如果跑满说明解码器没开多线程或者像素格式转换太耗时。我习惯在组件里加一个Stats属性返回这些计数现场调试时直接读比抓包快。从那以后我每次接新摄像头都先跑一遍这个统计确认三个指标正常再往下做业务逻辑。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →