DirectShow的H.264编码RTP协议收发Filter(2018.3.12更新git)

基于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

已更新RTP-Source-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帧顺序不对,

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



  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值