转帖地址:http://blog.csdn.net/fan2273/article/details/77653700

基于DirectShow的框架H.264 RTP Sender Filter

开发框架与环境:

1.VS2017——工具集为V120-VS2013

2.jrtplib-3.11.1 jthread-1.3.3 编译为32位版

3.程序为32位程序

4.DirectShow链路图如下(控制台为RTP发送地址与本机端口)

5.RTP拆包方式为FU-A

6.x264编码filter为ffdshow codec(本filter支持输入为H264的sample)

7.本程序开源(部分实现借鉴了许多CSDN博客与大牛的程序),请遵守开源协议

git地址:https://github.com/EthanXzhang/RTP-Sender-Filter

实现部分

有空的话应该会整理个更详细的版本,这里主要就说一下遇到的问题好了。

实现DirectShow+jrtplib的H264收发包程序,总共用时约两周,主要的精力和时间花在了RTP协议、RTP拆包与H.264

字节流处理、H.264格式的解析上。

1.H.264的NAL单元(NALU)

H.264编码实现了网络层也就是NAL,其中每一个单元(NALU)适用于网络传输,详细的这里不阐述了,捡重点的方

便大家快速理解。

编码器对原始采集视频(图像)进行处理后,输出的每一帧即是一个NALU。这里的每一帧包括编码器初始化输出的

PPS和SPS。

实际上,我们需要用RTP实现的传输,就是从输入pin中的数据,提取每一帧(NALU),对NALU进行打包发送。

H.264编解码,怎样完成?需要怎样保持数据的发送?

首先,H.264的编码,除了初始化编码器后,输出PPS和SPS外,之后的所有NALU单元,都为I-P-B帧的组合。其中,

I帧为关键帧,解码器遇到这一帧后,会清除重置解码器的预测基准(具体请参考H.264编码与运动预测模型),而P

帧和B帧则是前向预测帧和双向预测帧。H.264由于进行了运动预测,因此除了I帧外,P帧和B帧仅需要较少的bit进行

编码,从而减少传输的数据量。

既然考虑网络传输,肯定需要知道接收端/解码器,需要什么信息才能完成解码。网上一些资料说到,需要PPS和

SPS,解码器才能获得编码的信息。这部分可能是基于封装后的H.264编码视频,而不是指实时采集的H.264视频流。

这里可以告诉大家的是,解码器需要的一切信息,都在编码器定时编码输出的I帧中。也就是,接收端开启后,收到到

来的第一个I帧后,便可以开始正常的解码工作。之后遇上任何网络波动、丢包、延迟等情况,都会在下一个I帧后重

置。I帧的速率,由编码器控制。由于I帧需要较大的编码量,因此一般编码器缺省I帧速率居中,通常受输出压缩率与

质量影响。

较高的I帧速率,可以保证较好的动态分辨率,降低延迟、丢包等带来的视频模糊、拖影、花屏影响,但相对的会增

加网络传输的数据量。(通常一个I帧的数据量是P B帧的五倍以上)

如果网络丢包严重、延迟较高,有需要较好的动态分辨率和画质,可以通过提高I帧的编码速率来解决。

这一部分,简单来说,编码器输出一个个NALU,而我们不需要管这个NALU是具体什么帧,只需要对它进行RTP

拆包并发送就可以了。解码器会自动等待I帧,并开始之后的解码工作。

2.NALU的结构

无论这个NALU是PPS还是SPS,又或者I P B帧,他们都具有一样的结构。

startcode+NALU头+NALU数据。

startcode,起始码,主要是帮助解码器从数据流中分辨NALU用。startcode格式十分固定,但根据编码器规范,

可能具有两种不同的形式。

三位的0x000001与四位的0x00000001。无论哪种格式,都可以通过读0后读1判断。当遇到一个startcode后,紧接

着startcode后的数据就是该NALU的数据,直到遇到下一个startcode。

对于实时采集编码来说,由于我们使用的filter处理数据单位为Sample,每一个MediaSample中携带的数据即是一帧

数据,因此,永远都是以startcode开头,并且无需判断该NALU结束(直接使用actualsize数据长度取数据)

但是,这里有一个问题。

由于MediaSample的getPointer(BYTE
**pb)方法,获得指向内存的指针。而BYTE为unsigned char型,0x00在

char型中默认为NULL。也就是,此时返回的指针会提示指向的数据为空('0'
\0),但实际只是因为指向的第一个char

型内存单元为0x00。

此时,不要慌张,通过循环判断*pb==0x00(或NULL);pb++的方法,使指针后移,便可取到startcode后的NALU

数据。紧接着startcode,是一个字节NALU头,8位bit组成。由高位到低位依次是F(1bit)-NRI(2bit)-TYPE(5bit)。

这个部分后面的RTP拆包需要用到,因此需要保存下来(我使用了一个结构体,方便赋值)

NALU头之后,便是H.264的帧数据,这部分原封不动保留下来,直接装载到RTP包的playload中就好了。

PS:这一部分,主要可能遇到的问题是startcode的处理。由于startcode开头有2-3位的0x00,很多人使用

IMediaSample->getPointer方法会以为取得了空指针,而不断怀疑filter与Sample的问题。实际上,只要对指针pb进行

后移处理就好。(其实,我这里就被坑了3天,才反应过来)

3.RTP发包

JRTPLib使用的UDP协议进行发包。UDP协议是不可靠传送协议,因此,装载数据量大的UDP包容易被路由丢弃。

因此,根据RTP协议,通常将每一个RTP包的最大装载量限制在1400(JRTPLIB中用MAX定义为1360大小)。

RTP拆包协议主要由FU-A和FU-B两种,这里我主要使用的是FU-A拆包方式,具体的可以百度更详细的资料,不过

度展开。FU-A的好处是,可以直接使用VLC等播放器,对你的发送端进行测试,而不需要开发出接收端。

当你需要对NALU进行拆包时,假设拆了N个包,那么这里面只有两种包,即前N-1种和最后一包N。(最后一包不

同)FU-A协议,需要在每一个NALU数据前(去掉NALU header),额外添加FU indicator和FU header,各一个字节。

不需要拆包的NALU单元,直接发送即可(包括NALU头)。

FU
indicator有以下格式:
      +---------------+
      |0|1|2|3|4|5|6|7|(注意,左边为高位,右边为低位,此处0-7表示比特流的起始到终止的方向)
      +-+-+-+-+-+-+-+-+
      |F|NRI|  Type   |
      +---------------+
   FU指示字节的类型域 Type=28表示FU-A。。NRI域的值必须根据分片NAL单元的NRI域的值设置。
 
   FU header的格式如下:
      +---------------+
      |0|1|2|3|4|5|6|7|
      +-+-+-+-+-+-+-+-+
      |S|E|R|  Type   |
      +---------------+
   S: 1 bit
   当设置成1,开始位指示分片NAL单元的开始。当跟随的FU荷载不是分片NAL单元荷载的开始,开始位设为0。
   E: 1 bit
   当设置成1, 结束位指示分片NAL单元的结束,即, 荷载的最后字节也是分片NAL单元的最后一个字节。当跟随的FU

荷载不是分片NAL单元的最后分片,结束位设置为0。

R: 1 bit
   保留位必须设置为0,接收者必须忽略该位。
   Type: 5 bits
   NAL单元荷载类型定义见下表

表1.  单元类型以及荷载结构总结
      Type  Packet      Typename                      
     ---------------------------------------------------------
      0     undefined                                   -
      1-23   NALunit    Single NAL unit packet per H.264  
      24    STAP-A     Single-time aggregation packet   
      25    STAP-B     Single-time aggregation packet   
      26    MTAP16    Multi-time aggregation packet     
      27    MTAP24    Multi-time aggregation packet     
      28     FU-A     Fragmentation unit               
      29    FU-B      Fragmentationunit                
      30-31 undefined

简单来说,FU-A的拆包方式,indicator你只需要注意TYPE设置为28,F和NRI全部取NALU的头对应的位。Header你

只需要注意TYPE取得NALU头的type,S
E R全部置0(最后一包的E置1)。

这里说明一下FU-A的工作方式。

接收端根据RTP包中头一位(可能为FU indicator或NALU header)的TYPE位,判断这个包具体是什么。

当紧接着一包的FU header E位为1时候,接收端便知道要进行组包工作。

JRTPLib通过RTP包的mark位判断是否是最后一包,进行组包(具体见代码)。

4.filter的实现

这部分没太多可说的,不过因为DirectShow
filter相关的资料现在越来越难找,因此也大概说一下遇到的问题。

本程序前后使用了CBaseRenderer、CBaseVideoRenderer、CTransformFilter实现。

其实,选用哪个filter,主要看当前filter的目的和在整个链路中的定位。

我当前使用的是Transform,作为中间filter。

实际上,我是被迫这么做的。

早期我打算使用的Renderer,继承实现doRender方法,来进行RTP发包。但由于IMediaSample的getPointer一直为

空,而我的资料又很有限。在前后换了使用Base和Video的父类,使用了pin方法Receive,仍然解决不了后,我就更换

了Transform filter来尝试实现。(这个filter的资料相对多一些)

实际上,只是因为startcode的0x00,字符指针为NULL而已。

关于filter部分,实际上需要实现的基本只有以下几个部分:

1.filter信息与注册(名字、CLSID、pin属性)

2.checktype方法(不同位置的check方法不同,用以检测pin口是否匹配,通常必须实现)

3.关键的处理方法(doRender、Transform、FillBuffer)

4.createInstance与构造函数完成初始化

5.RTP传输问题处理

进行RTP传输的时候,视频经常会出现灰蒙、抖动、花屏。

总结来说,基本就是延迟、丢包、乱序的问题。

但在本地收发测试中,丢包和延迟的问题基本不会存在,一般也不可能存在乱序的情况。

那么,为什么本地VLC测试,还会出现上述状况呢?

请使用秒表在采集端进行测试……

经过我的检测发现,是发送端的时间戳和发送频率、延迟设置的问题。

由于实时采集,一般来说,发送频率和帧率是匹配,播放端才能还原出和采集端同速的图像。

如果发送端,发送速度高于帧率,播放端接收到就会马上播放,因此时常会处于等待状态,出现延迟的情况。

如果发送端,发送速度慢与帧率,播放端则会慢速播放,而下一个I帧到来又会刷新还没播放完的P
B帧,出现卡顿的情况。

而类似画面抖动,帧间预测导致画面中动作往复、影响重叠,则是RTP包乱序,或者P
B帧跟随前面的I帧顺序不对,

也大多是因为上述问题产生的。

DirectShow的RTP发包(H264)Filter <转>的更多相关文章

  1. 最简单的基于DirectShow的示例:获取Filter信息

    ===================================================== 最简单的基于DirectShow的示例文章列表: 最简单的基于DirectShow的示例:视 ...

  2. (转)基于RTP的H264视频数据打包解包类

    最近考虑使用RTP替换原有的高清视频传输协议,遂上网查找有关H264视频RTP打包.解包的文档和代码.功夫不负有心人,找到不少有价值的文档和代码.参考这些资料,写了H264 RTP打包类.解包类,实现 ...

  3. rtp传输h264

    ---恢复内容开始--- 基本概念的理解 H.264的主要目标:1.高的视频压缩比2.良好的网络亲和性 解决方案:VCL video coding layer 视频编码层NAL network abs ...

  4. 对最近的RTP和H264学习进行总结整理-04.20

    虽然还是没有搞出来,但总感觉快了哈哈(哪来的自信) 1.RTP协议接受数据 #region 1-RTP协议变量声明 RTPSession session; RTPReceiver receiver; ...

  5. 基于RTP的H264视频数据打包解包类

    from:http://blog.csdn.net/dengzikun/article/details/5807694 最近考虑使用RTP替换原有的高清视频传输协议,遂上网查找有关H264视频RTP打 ...

  6. RTP封装h264

    网络抽象层单元类型 (NALU): NALU头由一个字节组成,它的语法如下: +---------------+      |0|1|2|3|4|5|6|7|      +-+-+-+-+-+-+-+ ...

  7. RTP 打包H264与AAC

    static int h264_parse(Track *tr, uint8_t *data, size_t len) { h264_priv *priv = tr->private_data; ...

  8. 【FFMPEG】基于RTP的H264视频数据打包解包类

    最近考虑使用RTP替换原有的高清视频传输协议,遂上网查找有关H264视频RTP打包.解包的文档和代码.功夫不负有心人,找到不少有价值的文档和代码.参考这些资料,写了H264 RTP打包类.解包类,实现 ...

  9. 用实例分析H264 RTP payload

    用实例分析H264 RTP payload H264的RTP中有三种不同的基本负载(Single NAL,Non-interleaved,Interleaved) 应用程序可以使用第一个字节来识别. ...

随机推荐

  1. VC dimension and Model complexity

    可以把growth function m_H(N)的upper bound用N^(k-1)来限制, for N large, k>=3 Thus, 定义: VC Dimension: maxim ...

  2. selenium-java,UI自动化截图方法

    截图方法: import java.io.File; import java.io.IOException; import org.apache.commons.io.FileUtils; impor ...

  3. CF1114F Please, another Queries on Array?

    CF1114F Please, another Queries on Array? 考虑用线段树维护取模后的区间积和真正的区间积所含有的质因子. 每次询问查得这两个值后,一乘一除,即可算出该区间积的欧 ...

  4. NSURLSession学习笔记(二)Session Task

    Session Task分为三种Data Task,Upload Task,Download Task.毫无疑问,Session Task是整个NSURLSession架构的核心目标. 下面写了一个简 ...

  5. CentOS 6.6下安装OpenOffice4.0

    最近由于项目需要,要在公司服务器上安装Openoffice,网上搜了一些资料后成功安装,现分享给大家. 1.首先先下载好需要的rpm包:Apache_OpenOffice_4.0.0_Linux_x8 ...

  6. c#开发的程序安装时动态指定windows服务名称

    转自:http://www.jb51.net/article/30549.htm 前段时间由于项目的需求,要在Windows里把同样的组件制作成多个不同名称的服务,这些服务完成类似的功能,仅需要修改业 ...

  7. WPF 使用MahApps.Metro UI库

    在WPF中要想使用Metro风格是很简单的,可以自己画嘛.. 但是为了节省时间,哈,今天给大家推荐一款国外Metro风格的控件库. 本文只起到抛砖引玉的作用,有兴趣还是推荐大家上官网,Thanks,官 ...

  8. ballerina 学习六 xml && json

    ballerina xml && json 参考使用 代码比较简单,使用起来还是比较方便的 xml 代码说明: import ballerina/io; function main ( ...

  9. nginx 启用http2 https 无法访问的问题

    原因:   1. openssl  版本过低     解决方法:进行升级   yum  update openssl   2.ssl_ciphers 配置有问题    解决方法:修改为  ssl_ci ...

  10. Android中执行的错误:java.lang.UnsatisfiedLinkError: Couldn't load locSDK3: findLibrary returned null.

    今天在使用百度地图的时候执行发现报错: 明明已经增加了liblocSDK3.so.但总是无法定位.提示错误java.lang.UnsatisfiedLinkError: Couldn't load l ...