基于RTP协议的IP电话QoS监测及提高策略

本文转自 http://jxic.jiangxi.gov.cn/Html/2008321143656-1.html

1. 概述 

随着Internet和多媒体技术的飞速发展,Internet已由早期的单一数据传输网向多媒体数据(视频、音频、文本等)综合传输网发展。但Internet提供的只是尽力而为的服务,不能满足多媒体应用程序对传输延迟、包丢失、抖动控制等要求,为了能在传统的IP网上运行多媒体程序,必须考虑服务质量(Ouality of Service,QoS)。QoS可用延迟、抖动、吞吐量、丢包率等参数来描述。为了支持网络的实时传输服务,互联网工作组(Internet Engineering Task Force,IETF)制定了实时传输协议(Real-time Transport Protocol,RTP)。RTP是专门为交互式音频、视频、仿真数据等实时媒体应用而设计的轻型传输协议,已广泛应用于各种多媒体传输系统中。IP电话作为一种新兴业务,因其低廉的话费受到广大用户的欢迎。但IP电话中的通话时延、话音失真一直是制约IP电话迅速发展的“瓶颈”。如何确保IP电话的QoS,是IP电话成功与否的关键。

  结合IP电话系统,从音频实时传输和控制两方面来讨论RTP及实时传输控制协议(Real-time TransportControl Protocol,RTCP)应用技术,分析影响媒体流实时传输的因素。最后从实际实验、应用的角度,讨论如何获得当前Internet可行的QoS监测,并针对QoS质量保证提出切实可行的解决方案。

2 实时传输协议RTP

  RTP是用于Internet上针对多媒体数据流的一种传输协议,被定义为在一对一或一对多的传输情况下工作,其目的是提供时间信息和实现流同步。RTP通常使用用户数据报协议(User Datagram Protocol,UDP)来传送数据,但RTP也可以在传输控制协议(Transmission Control Protocol,TCP)或异步传输模式(Asynchronous Transfer Mode,ATM)等其他协议之上工作。当应用程序开始一个RTP会话时将使用2个端口:1个给RTP,1个给RTCP。RTP本身并不能为按顺序传送数据包提供可靠的传送机制,也不提供流量控制或拥塞控制,它依靠RTCP提供这些服务。通常RTP算法并不作为一个独立的网络层来实现,而是作为应用程序代码的一部分,RTCP和RTP一起提供流量控制和拥塞控制服务。在RTP会话期间,参与者周期性地传送RTCP包,RTCP包中含有已发送的数据包的数量、丢失的数据包的数量等统计资料,因此,服务器可以利用这些信息动态地改变传输速率,甚至改变有效载荷类型。RTP和RTCP配合使用能以有效的反馈和最小的开销使传输效率最佳化,因而特别适合传送网上的实时数据。

2.1 RTP数据包

  RTP数据包由12个字节的固定RTP头和不定长的连续媒体数据(视频帧或音频帧)组成。RTP协议的数据包格式如图1所示。

RTP报文头部分各个参数的意义如下:

  1. 版本(V):2bit版本号置2。
  2. 扩展位(Extension-X):由使用的RTP框架定义。
  3. 填充(P):用以说明包尾是否附有非负荷信息。
  4. 负载类型(PT):对音频或视频等数据类型予以说明,并说明数据的编码方式。
  5. 标志位(Marker-M):标志位由具体的应用框架定义。
  6. 序列号(Sequence Number):为了安全,服务器从一个随机初始化值开始,每发送一个RTP数据包序列号增加1。客户端可根据序列号重新排列数据包的顺序,并对丢失、损坏和重复的数据包进行检测。
  7. 时间戳(Timestamp):RTP时间戳为同步不同的媒体流提供采样时间,用于重新建立原始音频或视频的时序。另外,它还可以帮助接收方确定数据到达时间的一致性或变化(有时被称为抖动)。
  8. 同步源标识(SSRG):帮助接收方利用发送方生成的唯一的数值来区分多个同时的数据流。SRC必须是一个严格的随机数。
  9. 作用标识(CSRC):网络中使用混合器时,混合器会在RTP报文头部之后插入新的同步源标识,其作用是区分多个同时的数据流。

2.2 RTP控制协议——RTCP

  在RTP会话中,RTCP周期性地给所有参与者发送控制包,应用程序或第三方监控者接受RTCP控制包,从中获取控制信息,估计当前QoS,以便进行传输控制、拥塞处理、错误诊断等。

  RTCP报文头部参数首先要区别携带不同控制信息的RTCP报文的类型,RTCP报文的类型主要有以下几种:

(1)SR:发送报告,当前活动发送者发送、接收统计;

(2)RR:接收报告,非活动发送者接收统计;

(3)SDES:源描述项,包括CNAME;

(4)BYB:表示结束;

(5)APP:应用特定函数。

其中最主要的RTCP报文是SR和RR。通常SR报文占总RTCP包数量的25%,RR报文占75%。

通过这5种控制包,RTCP协议实现了以下4个主要功能:

  (1)提供数据发布的质量反馈,这是RTCP最主要的功能。作为RTP传输协议的一部分,与其他传输协议的流和阻塞控制有关。 反馈对自适应编码控制直接起作用。反馈功能由RTCP发送者和接收者报告执行。

  (2)送带有称作规范名字(CNAME)的RTP源持久传输层标识。如发现冲突,或程序重新启动,SSRC标识可改变,接收者需要CNAME跟踪参加者。接收者也需要CNAME与相关RTP连接中给定的几个数据流联系。

  (3)根据参与RTP会话的数量调整RTCP的发送速率。

  (4)传送最小连接控制信息,如参加者辨识。最可能用在“松散控制”连接,那里参加者自由进入或离开,没有成员控制或参数协调,RTCP充当通往所有参加者的方便通道,但不必支持应用的所有控制通信要求。

3 由RTP包分析影响多媒体数据流实时传输的因素

  随着VoIP领域不断发展,满足网络QoS检测需求的应用也成为引人注目的焦点。IP QoS是指IP的服务质量,也是指IP数据流通过网络时的性能。目的就是向用户提供端到端的服务质量保证。有一套度量指标,包括业务可用性、延迟、可变延迟、吞吐量和丢包率等,现就项目中在上海阿尔卡特网络支援系统有限公司NGN实验室中所得到的RTP和RTCP包进行分析,主要研究其中3个因素,从而达到对实时流媒体数据进行监控的目的。

3.1 抖动

   抖动会引起端到端时延的增加(指网络造成的包与包之间的时差),引起语音质量的降低。在音频数据的传输过程中,由于传输延迟的不稳定而造成相邻数据包接收时刻间隔不稳定,从而产生抖动。消除抖动的主要依据就是RTP包的首部中包含的时间戳字段。时间戳标志着该段音频数据中第一个采样点的采样时间。每两个RTP包的抖动可以用其RTP包中的RTP时戳和接收的时刻进行计算。

  关于包的传送时间,接收者最先了解到的是它的时间戳和接收者当前时间之间的差值。该差值是:Di=Ri-Si,表示从包被盖上戳开始,到它在信源的输出链路上被实际发送为止,其中的传送时间和某个机器时间。RFC 1889建议使用 NTP来完成端点的端到端同步,但是也有非同步端点实现存在。

  包i和包j之间增加的延迟差(二阶效应)计算公式如下:设Rj代表第j个包的接受时刻,Sj代表第j个包的RTP时戳值,则第i个RTP报文与第j个RTP报文间的抖动为D(i,j)

  在生成RTCP报文时,其应当传送的时延抖动的值可用以下公式进行递推计算

  其中:J为要传送的时延抖动值。对后一项除以16是为了消除连带噪声。

   抖动是分组交换的必然结果影响抖动的因素一般和 网络的拥塞程度有关。由于语音同数据在同一条物理线上传输,语音数据通常会由于数据报文占用了物理线路而导致阻塞。 解决抖动通常采用 缓冲队列来解决(在网关、IAD上均有JitterBuffer来消除抖动),每收到一个数据包,先将其放人缓冲区,应用程序在缓冲区另一端取数据,只要缓冲区足够大,抖动一定能被 平滑掉。而 错序是由于网络拥挤而使某些后发的数据包先到达收端而引起的,只要 设置足够大的缓冲区来对数据包重新排序,就能 解决这个问题。或者需要IP承载网采用QoS策略,保证语音数据的最高优先级,得到 最先发送获得高带宽也是 解决抖动问题的主要手段。

3.2 时延

   时延是处理和传输导致数据不能按时到达的延迟,是影响流媒体数据传输的一个主要因素。话音信号在端到端传输过程中受到的时延迟滞通常包括:编解码器引入的时延、打包时延、去抖动时延、承载网上的传输节点中排队、服务处理时延。这些时延累计的总和将影响话质,导致回声干扰和交互性的劣化。对于VoIP系统,规定时延一般控制在150 ms内。

  分组语音网络中的延迟可分为固定延迟和可变延迟。前者相对容易得到,笔者不作考虑。在计算丢包率时,主要考虑可变延迟。 丢包判定等待时限Twait设定的大小在很大程度上影响丢包率计算的准确性,也就是可变延迟的影响,它与语音包的传输延迟Ttrf有关,Twait越大等待时限就越长。但不能超过保证语音流连续播放的时间上限Tmax(Tmax一般取250 ms),即:Twait=min(Twait,Tmax)。Ttrf可根据RTCP协议的SR控制包中的NTP(Network Time Protoco1)时间戳计算得到,见图2。

  根据RTP头中的 sequence number域,可以在 接收端轻易发现包丢失,为丢包修复奠定基础。在实际使用中发现, 绝大多数丢包单个丢包,两个或两个以上包丢失的比例较小。针对单个包的丢失,传统的丢包处理方法有两种: 一种方法是重发,但在传输语音数据时,重发将引起播放质量下降,出现无法识别的话音或回音现象; 另一种方法是忽略,这同样会影响播放质量。 更好的方案使用拆分法优化丢包损失。拆分法的 基本思想是: 在发送端把原来要打入一个RTP包的话音数据按照采样间隔分成两块,然后采用相同的压缩算法分别压缩、打入RTP包,并标记相同的时印进行传输。在接收方执行相反的过程,把解压缩后的数据采样、合并、回放。

  如果某个RTP包在传输过程中丢失,那么丢失的只是原数据包按采样间隔的一半信息,接收端可以用接受到的另一半信息, 利用插值等方法恢复出原话音包的大部分信息,从而使话音质量不至于下降太多。拆分法的主要思想如图4所示。

4 结束语

  对音频数据的实时传输问题进行了详细分析,在分析RTP协议的基础上,探讨了基于RTP协议的QoS动态监测的一些方法,并提出了解决在流媒体中存在的语音实时传输质量保证的策略。避免语音通信实时性差的缺点,减小了网络延时使抖动的影响减低,改善了语音传输效果。目前,IP电话用户数每年正以239%的速度增长。下一步将以此为依据设计出基于RTP的一个应用模型,进行深层开发研究。

==========================================================================================

谈谈RTP传输中的负载类型和时间戳

本文转自 http://ticktick.blog.51cto.com/823160/350142

最近被RTP的负载类型和时间戳搞郁闷了,一个问题调试了近一周,终于圆满解决,回头看看,发现其实主要原因还是自己没有真正地搞清楚RTP协议中负载类型和时间戳的含义。虽然做RTP传输,有着Jrtplib和Ortp这两个强大的库支持,一个是c++接口,一个是c语言接口,各有各的特点,各有各的应用环境,但是仅仅有库就能解决一切问题吗?可能仿照着一些例子程序,你能够完成主要的功能,但一旦问题发生了,不清楚原理你是很难定位和解决问题的,所以在此,用我的经验劝劝大家,磨刀不误砍柴工,做应用还是先把原理搞清楚再动手吧……
          看这篇文章之前,首先你应该知道什么是RTP协议,可以去看RTP协议原文(RFC3550协议),也可以看一些网友对RTP协议的讲解的文章,很多,这里我提供一篇我个人觉得写得还不错的:http://blog.csdn.net/bripengandre/archive/2008/04/01/2238818.aspx 。

好,下面言归正传,首先谈谈RTP传输中的负载类型吧。

首先,看RTP协议包头的格式:
          

10~16 Bit为PT域,指的就是负载类型(PayLoad),负载类型定义了RTP负载的格式,协议原文说该域由具体应用决定其解释。
          目前,负载类型主要用来告诉接收端(或者播放器)传输的是哪种类型的媒体(例如G.729,H.264,MPEG-4等),这样接收端(或者播放器)才知道了数据流的格式,才会调用适当的编解码器去解码或者播放,这就是负载类型的主要作用。
          就ORTP库而言,负载类型定义如下:
        

每一种负载类型都有着其独特的参数,这里基本上涵盖了当前主流的一些媒体类型,例如pcmu 、g.729、h.263(很奇怪,竟然没有定义h.264)、mpeg-4等等。Jrtplib库应该也有相类似的定义,你可以去找找源码,在此我就不再赘述了。

在ORTP库和JRTplib库中,都提供了设置RTP负载类型的函数,千万要记得根据实际的应用进行设置,我就是当时没有注意,使用ORTP默认的pcmu音频的负载类型,传输H.264编码的视频数据,结果传输中一直有问题,困扰我好久好久。

好了,再说说RTP的时间戳吧。

首先,了解几个基本概念:

时间戳单位:时间戳计算的单位不是秒之类的单位,而是由采样频率所代替的单位,这样做的目的就是为了是时间戳单位更为精准。比如说一个音频的采样频率为8000Hz,那么我们可以把时间戳单位设为1 / 8000。
          时间戳增量:相邻两个RTP包之间的时间差(以时间戳单位为基准)。
          采样频率:  每秒钟抽取样本的次数,例如音频的采样率一般为8000Hz
          帧率:      每秒传输或者显示帧数,例如25f/s

再看看RTP时间戳课本中的定义:

RTP包头的第2个32Bit即为RTP包的时间戳,Time Stamp ,占32位。
          时间戳反映了RTP分组中的数据的第一个字节的采样时刻。在一次会话开始时的时间戳初值也是随机选择的。即使是没有信号发送时,时间戳的数值也要随时间不断的增加。接收端使用时间戳可准确知道应当在什么时间还原哪一个数据块,从而消除传输中的抖动。时间戳还可用来使视频应用中声音和图像同步。
          在RTP协议中并没有规定时间戳的粒度,这取决于有效载荷的类型。因此RTP的时间戳又称为媒体时间戳,以强调这种时间戳的粒度取决于信号的类型。例如,对于8kHz采样的话音信号,若每隔20ms构成一个数据块,则一个数据块中包含有160个样本(0.02×8000=160)。因此每发送一个RTP分组,其时间戳的值就增加160。

官方的解释看懂没?没看懂?没关系,我刚开始也没看懂,那就听我的解释吧。

          首先,时间戳就是一个值,用来反映某个数据块的产生(采集)时间点的,后采集的数据块的时间戳肯定是大于先采集的数据块的。有了这样一个时间戳,就可以标记数据块的先后顺序。
          第二,在实时流传输中,数据采集后立刻传递到RTP模块进行发送,那么,其实,数据块的采集时间戳就直接作为RTP包的时间戳。
          第三,如果用RTP来传输固定的文件,则这个时间戳就是读文件的时间点,依次递增。这个不再我们当前的讨论范围内,暂时不考虑。
          第四,时间戳的单位采用的是采样频率的倒数,例如采样频率为8000Hz时,时间戳的单位为1 / 8000 ,在Jrtplib库中,有设置时间戳单位的函数接口,而ORTP库中根据负载类型直接给定了时间戳的单位(音频负载1/8000,视频负载1/90000)
          第五,时间戳增量是指两个RTP包之间的时间间隔,详细点说,就是发送第二个RTP包相距发送第一个RTP包时的时间间隔(单位是时间戳单位)。
          如果采样频率为90000Hz,则由上面讨论可知,时间戳单位为1/90000,我们就假设1s钟被划分了90000个时间块,那么,如果每秒发送25帧,那么,每一个帧的发送占多少个时间块呢?当然是 90000/25 = 3600。因此,我们根据定义“时间戳增量是发送第二个RTP包相距发送第一个RTP包时的时间间隔”,故时间戳增量应该为3600。
          在Jrtplib中好像不需要自己管理时间戳的递增,由库内部管理。但在ORTP中每次数据的发送都需要自己传入时间戳的值,即自己需要每次发完一个RTP包后,累加时间戳增量,不是很方便,这就需要自己对RTP的时间戳有比较深刻地理解,我刚开始就是因为没搞清楚,随时设置时间戳增量导致传输一直有问题,困扰我好久。

好了,关于RTP的负载类型和时间戳的介绍就到这里了,这次通过解决RTP传输中的问题学到了不少知识,在此分享希望对大家有用。有说得不正确的地方欢迎高手指教,也可以来信交流:lujun.hust@gmail.com

========================================================================================

Linux下几种RTP协议实现的比较和JRTPLIB编程讲解

本文转自http://aphrodit.blog.sohu.com/133605028.html

1. 概述

流媒体指的是在网络中使用流技术传输的连续时基媒体,其特点是在播放前不需要下载整个文件,而是采用边下载边播放的方式,它是视频会议、 IP电话等应用场合的技术基础。RTP是进行实时流媒体传输的标准协议和关键技术,本文介绍如何在Linux下利用JRTPLIB进行实时流媒体编程。 
  
         随着Internet的日益普及,在网络上传输的数据已经不再局限于文字和图形,而是逐渐向声音和视频等多媒体格式过渡。目前在网络上传输音频/视频(Audio/Video,简称A/V)等多媒体文件时,基本上只有下载和流式传输两种选择。通常说来,A/V文件占据的存储空间都比较大,在带宽受限的网络环境中下载可能要耗费数分钟甚至数小时,所以这种处理方法的延迟很大。如果换用流式传输的话,声音、影像、动画等多媒体文件将由专门的流媒体服务器负责向用户连续、实时地发送,这样用户可以不必等到整个文件全部下载完毕,而只需要经过几秒钟的启动延时就可以了,当这些多媒体数据在客户机上播放时,文件的剩余部分将继续从流媒体服务器下载。

流(Streaming)是近年在Internet上出现的新概念,其定义非常广泛,主要是指通过网络传输多媒体数据的技术总称。流媒体包含广义和狭义两种内涵:广义上的流媒体指的是使音频和视频形成稳定和连续的传输流和回放流的一系列技术、方法和协议的总称,即流媒体技术;狭义上的流媒体是相对于传统的下载-回放方式而言的,指的是一种从Internet上获取音频和视频等多媒体数据的新方法,它能够支持多媒体数据流的实时传输和实时播放。通过运用流媒体技术,服务器能够向客户机发送稳定和连续的多媒体数据流,客户机在接收数据的同时以一个稳定的速率回放,而不用等数据全部下载完之后再进行回放。

由于受网络带宽、计算机处理能力和协议规范等方面的限制,要想从Internet上下载大量的音频和视频数据,无论从下载时间和存储空间上来讲都是不太现实的,而流媒体技术的出现则很好地解决了这一难题。目前实现流媒体传输主要有两种方法:顺序流(progressive streaming)传输和实时流(realtime streaming)传输,它们分别适合于不同的应用场合。

顺序流传输

顺序流传输采用顺序下载的方式进行传输,在下载的同时用户可以在线回放多媒体数据,但给定时刻只能观看已经下载的部分,不能跳到尚未下载的部分,也不能在传输期间根据网络状况对下载速度进行调整。由于标准的HTTP服务器就可以发送这种形式的流媒体,而不需要其他特殊协议的支持,因此也常常被称作HTTP 流式传输。顺序流式传输比较适合于高质量的多媒体片段,如片头、片尾或者广告等。

实时流传输

实时流式传输保证媒体信号带宽能够与当前网络状况相匹配,从而使得流媒体数据总是被实时地传送,因此特别适合于现场事件。实时流传输支持随机访问,即用户可以通过快进或者后退操作来观看前面或者后面的内容。从理论上讲,实时流媒体一经播放就不会停顿,但事实上仍有可能发生周期性的暂停现象,尤其是在网络状况恶化时更是如此。与顺序流传输不同的是,实时流传输需要用到特定的流媒体服务器,而且还需要特定网络协议的支持。

实时传输协议(Real-time Transport Protocol,PRT)是在Internet上处理多媒体数据流的一种网络协议,利用它能够在一对一(unicast,单播)或者一对多(multicast,多播)的网络环境中实现传流媒体数据的实时传输。RTP通常使用UDP来进行多媒体数据的传输,但如果需要的话可以使用TCP或者 ATM等其它协议,整个RTP协议由两个密切相关的部分组成:RTP数据协议和RTP控制协议。实时流协议(Real Time Streaming Protocol,RTSP)最早由Real Networks和Netscape公司共同提出,它位于RTP和RTCP之上,其目的是希望通过IP网络有效地传输多媒体数据。

        RTSP,RTP,RTCP的区别:RTSP发起/终结流媒体、RTP传输流媒体数据 、RTCP对RTP进行控制,同步。


2. 协议

2.1 RTP数据协议

RTP数据协议负责对流媒体数据进行封包并实现媒体流的实时传输,每一个RTP数据报都由头部(Header)和负载(Payload)两个部分组成,其中头部前12个字节的含义是固定的,而负载则可以是音频或者视频数据。RTP数据报的头部格式如图1所示:
        其中比较重要的几个域及其意义如下:

CSRC记数(CC)  表示CSRC标识的数目。CSRC标识紧跟在RTP固定头部之后,用来表示RTP数据报的来源,RTP协议允许在同一个会话中存在多个数据源,它们可以通过RTP混合器合并为一个数据源。例如,可以产生一个CSRC列表来表示一个电话会议,该会议通过一个 RTP混合器将所有讲话者的语音数据组合为一个RTP数据源。 
        负载类型(PT)  标明RTP负载的格式,包括所采用的编码算法、采样频率、承载通道等。例如,类型2表明该RTP数据包中承载的是用ITU G.721算法编码的语音数据,采样频率为8000Hz,并且采用单声道。 
        序列号  用来为接收方提供探测数据丢失的方法,但如何处理丢失的数据则是应用程序自己的事情,RTP协议本身并不负责数据的重传。 
        时间戳  记录了负载中第一个字节的采样时间,接收方能够时间戳能够确定数据的到达是否受到了延迟抖动的影响,但具体如何来补偿延迟抖动则是应用程序自己的事情。 
        从RTP 数据报的格式不难看出,它包含了传输媒体的类型、格式、序列号、时间戳以及是否有附加数据等信息,这些都为实时的流媒体传输提供了相应的基础。RTP协议的目的是提供实时数据(如交互式的音频和视频)的端到端传输服务,因此在RTP中没有连接的概念,它可以建立在底层的面向连接或面向非连接的传输协议之上;RTP也不依赖于特别的网络地址格式,而仅仅只需要底层传输协议支持组帧(Framing)和分段(Segmentation)就足够了;另外RTP 本身还不提供任何可靠性机制,这些都要由传输协议或者应用程序自己来保证。在典型的应用场合下,RTP 一般是在传输协议之上作为应用程序的一部分加以实现的。

2.2 RTCP控制协议

RTCP 控制协议需要与RTP数据协议一起配合使用,当应用程序启动一个RTP会话时将同时占用两个端口,分别供RTP 和RTCP使用。RTP本身并不能为按序传输数据包提供可靠的保证,也不提供流量控制和拥塞控制,这些都由RTCP来负责完成。通常RTCP会采用与 RTP相同的分发机制,向会话中的所有成员周期性地发送控制信息,应用程序通过接收这些数据,从中获取会话参与者的相关资料,以及网络状况、分组丢失概率等反馈信息,从而能够对服务质量进行控制或者对网络状况进行诊断。

RTCP协议的功能是通过不同的RTCP数据报来实现的,主要有如下几种类型:

SR  发送端报告,所谓发送端是指发出RTP数据报的应用程序或者终端,发送端同时也可以是接收端。 
        RR  接收端报告,所谓接收端是指仅接收但不发送RTP数据报的应用程序或者终端。 
        SDES  源描述,主要功能是作为会话成员有关标识信息的载体,如用户名、邮件地址、电话号码等,此外还具有向会话成员传达会话控制信息的功能。 
        BYE  通知离开,主要功能是指示某一个或者几个源不再有效,即通知会话中的其他成员自己将退出会话。 
        APP  由应用程序自己定义,解决了RTCP的扩展性问题,并且为协议的实现者提供了很大的灵活性。 
        RTCP数据报携带有服务质量监控的必要信息,能够对服务质量进行动态的调整,并能够对网络拥塞进行有效的控制。由于RTCP数据报采用的是多播方式,因此会话中的所有成员都可以通过RTCP数据报返回的控制信息,来了解其他参与者的当前情况。

在一个典型的应用场合下,发送媒体流的应用程序将周期性地产生发送端报告SR,该RTCP数据报含有不同媒体流间的同步信息,以及已经发送的数据报和字节的计数,接收端根据这些信息可以估计出实际的数据传输速率。另一方面,接收端会向所有已知的发送端发送接收端报告RR,该RTCP数据报含有已接收数据报的最大序列号、丢失的数据报数目、延时抖动和时间戳等重要信息,发送端应用根据这些信息可以估计出往返时延,并且可以根据数据报丢失概率和时延抖动情况动态调整发送速率,以改善网络拥塞状况,或者根据网络状况平滑地调整应用程序的服务质量。

2.3 RTSP实时流协议

作为一个应用层协议,RTSP提供了一个可供扩展的框架,它的意义在于使得实时流媒体数据的受控和点播变得可能。总的说来,RTSP是一个流媒体表示协议,主要用来控制具有实时特性的数据发送,但它本身并不传输数据,而是必须依赖于下层传输协议所提供的某些服务。RTSP 可以对流媒体提供诸如播放、暂停、快进等操作,它负责定义具体的控制消息、操作方法、状态码等,此外还描述了与RTP间的交互操作。

RTSP 在制定时较多地参考了HTTP/1.1协议,甚至许多描述与HTTP/1.1完全相同。RTSP之所以特意使用与HTTP/1.1类似的语法和操作,在很大程度上是为了兼容现有的Web基础结构,正因如此,HTTP/1.1的扩展机制大都可以直接引入到RTSP 中。

由RTSP 控制的媒体流集合可以用表示描述(Presentation Description)来定义,所谓表示是指流媒体服务器提供给客户机的一个或者多个媒体流的集合,而表示描述则包含了一个表示中各个媒体流的相关信息,如数据编码/解码算法、网络地址、媒体流的内容等。

虽然RTSP服务器同样也使用标识符来区别每一流连接会话(Session),但RTSP连接并没有被绑定到传输层连接(如TCP等),也就是说在整个 RTSP连接期间,RTSP用户可打开或者关闭多个对RTSP服务器的可靠传输连接以发出RTSP 请求。此外,RTSP连接也可以基于面向无连接的传输协议(如UDP等)。

RTSP协议目前支持以下操作:

检索媒体  允许用户通过HTTP或者其它方法向媒体服务器提交一个表示描述。如表示是组播的,则表示描述就包含用于该媒体流的组播地址和端口号;如果表示是单播的,为了安全在表示描述中应该只提供目的地址。 
         邀请加入  媒体服务器可以被邀请参加正在进行的会议,或者在表示中回放媒体,或者在表示中录制全部媒体或其子集,非常适合于分布式教学。 
         添加媒体  通知用户新加入的可利用媒体流,这对现场讲座来讲显得尤其有用。与HTTP/1.1类似,RTSP请求也可以交由代理、通道或者缓存来进行处理。

RTP 是目前解决流媒体实时传输问题的最好办法,如果需要在Linux平台上进行实时流媒体编程,可以考虑使用一些开放源代码的RTP库,如LIBRTP、 JRTPLIB等。JRTPLIB是一个面向对象的RTP库,它完全遵循RFC 1889设计,在很多场合下是一个非常不错的选择,下面就以JRTPLIB为例,讲述如何在Linux平台上运用RTP协议进行实时流媒体编程。

3. 实例

3.1 环境搭建

JRTPLIB 是一个用C++语言实现的RTP库,目前已经可以运行在Windows、Linux、FreeBSD、 Solaris、Unix和VxWorks等多种操作系统上。要为Linux 系统安装JRTPLIB,首先从JRTPLIB的网站(http: //lumumba.luc.ac.be/jori/jrtplib/jrtplib.html)下载最新的源码包,此处使用的是jrtplib- 2.7b.tar.bz2。假设下载后的源码包保存在/usr/local/src目录下,执行下面的命令可以对其进行解压缩:

[root@linuxgam src]# bzip2 -dc jrtplib-2.7b.tar.bz2 | tar xvf -

接下去需要对JRTPLIB进行配置和编译:

[root@linuxgam src]# cd jrtplib-2.7
[root@linuxgam jrtplib-2.7b]# ./configure
[root@linuxgam jrtplib-2.7b]# make

最后再执行如下命令就可以完成JRTPLIB的安装:

[root@linuxgam jrtplib-2.7b]# make install

3.2 初始化

在使用JRTPLIB进行实时流媒体数据传输之前,首先应该生成RTPSession类的一个实例来表示此次RTP会话,然后调用Create() 方法来对其进行初始化操作。RTPSession类的Create()方法只有一个参数,用来指明此次RTP会话所采用的端口号。清单1给出了一个最简单的初始化框架,它只是完成了RTP会话的初始化工作,还不具备任何实际的功能。

#include "rtpsession.h"
int main(void)
{RTPSession sess;sess.Create(5000);return 0;
}

如果RTP会话创建过程失败,Create()方法将会返回一个负数,通过它虽然可以很容易地判断出函数调用究竟是成功的还是失败的,但却很难明白出错的原因到底什么。JRTPLIB采用了统一的错误处理机制,它提供的所有函数如果返回负数就表明出现了某种形式的错误,而具体的出错信息则可以通过调用 RTPGetErrorString()函数得到。RTPGetErrorString()函数将错误代码作为参数传入,然后返回该错误代码所对应的错误信息。清单2给出了一个更加完整的初始化框架,它可以对RTP会话初始化过程中所产生的错误进行更好的处理:

#include <stdio.h>
#include "rtpsession.h"
int main(void)
{RTPSession sess;int status;char* msg;sess.Create(6000);msg = RTPGetErrorString(status);printf("Error String: %s//n", msg);return 0;
}

设置恰当的时戳单元,是RTP会话初始化过程所要进行的另外一项重要工作,这是通过调用RTPSession类的 SetTimestampUnit()方法来实现的,该方法同样也只有一个参数,表示的是以秒为单元的时戳单元。例如,当使用RTP会话传输8000Hz 采样的音频数据时,由于时戳每秒钟将递增8000,所以时戳单元相应地应该被设置成1/8000:

sess.SetTimestampUnit(1.0/8000.0);

3.3 数据发送

当RTP 会话成功建立起来之后,接下去就可以开始进行流媒体数据的实时传输了。首先需要设置好数据发送的目标地址, RTP协议允许同一会话存在多个目标地址,这可以通过调用RTPSession类的AddDestination()、 DeleteDestination()和ClearDestinations()方法来完成。例如,下面的语句表示的是让RTP会话将数据发送到本地主机的6000端口(注意:如果是需要发到另一个NAT设备后面终端,则需要通过NAT穿透,见后):

unsigned long addr = ntohl(inet_addr("127.0.0.1"));
sess.AddDestination(addr, 6000);

目标地址全部指定之后,接着就可以调用RTPSession类的SendPacket()方法,向所有的目标地址发送流媒体数据。SendPacket()是RTPSession类提供的一个重载函数,它具有下列多种形式:

int SendPacket(void *data,int len)
int SendPacket(void *data,int len,unsigned char pt,bool mark,unsigned long timestampinc)
int SendPacket(void *data,int len,unsigned short hdrextID,void *hdrextdata,int numhdrextwords)
int SendPacket(void *data,int len,unsigned char pt,bool mark,unsigned long timestampinc,unsigned short hdrextID,void *hdrextdata,int numhdrextwords)

SendPacket()最典型的用法是类似于下面的语句,其中第一个参数是要被发送的数据,而第二个参数则指明将要发送数据的长度,再往后依次是RTP负载类型、标识和时戳增量。

sess.SendPacket(buffer, 5, 0, false, 10);

对于同一个RTP会话来讲,负载类型、标识和时戳增量通常来讲都是相同的,JRTPLIB允许将它们设置为会话的默认参数,这是通过调用 RTPSession类的SetDefaultPayloadType()、SetDefaultMark()和 SetDefaultTimeStampIncrement()方法来完成的。为RTP会话设置这些默认参数的好处是可以简化数据的发送,例如,如果为 RTP会话设置了默认参数:

sess.SetDefaultPayloadType(0);
sess.SetDefaultMark(false);
sess.SetDefaultTimeStampIncrement(10);

之后在进行数据发送时只需指明要发送的数据及其长度就可以了:

sess.SendPacket(buffer, 5);

3.4 数据接收

对于流媒体数据的接收端,首先需要调用RTPSession类的PollData()方法来接收发送过来的RTP或者 RTCP数据报。由于同一个RTP会话中允许有多个参与者(源),你既可以通过调用RTPSession类的GotoFirstSource()和 GotoNextSource()方法来遍历所有的源,也可以通过调用RTPSession类的GotoFirstSourceWithData()和 GotoNextSourceWithData()方法来遍历那些携带有数据的源。在从RTP会话中检测出有效的数据源之后,接下去就可以调用 RTPSession类的GetNextPacket()方法从中抽取RTP数据报,当接收到的RTP数据报处理完之后,一定要记得及时释放。下面的代码示范了该如何对接收到的RTP数据报进行处理:

if (sess.GotoFirstSourceWithData()) {do {RTPPacket *pack;     pack = sess.GetNextPacket();     // 处理接收到的数据delete pack;} while (sess.GotoNextSourceWithData());
}

JRTPLIB为RTP数据报定义了三种接收模式,其中每种接收模式都具体规定了哪些到达的RTP数据报将会被接受,而哪些到达的RTP数据报将会被拒绝。通过调用RTPSession类的SetReceiveMode()方法可以设置下列这些接收模式:

RECEIVEMODE_ALL  缺省的接收模式,所有到达的RTP数据报都将被接受; 
         RECEIVEMODE_IGNORESOME   除了某些特定的发送者之外,所有到达的RTP数据报都将被接受,而被拒绝的发送者列表可以通过调用               AddToIgnoreList()、 DeleteFromIgnoreList()和ClearIgnoreList()方法来进行设置; 
        RECEIVEMODE_ACCEPTSOME   除了某些特定的发送者之外,所有到达的RTP数据报都将被拒绝,而被接受的发送者列表可以通过调用              AddToAcceptList ()、DeleteFromAcceptList和ClearAcceptList ()方法来进行设置。

3.5 控制信息

JRTPLIB 是一个高度封装后的RTP库,程序员在使用它时很多时候并不用关心RTCP数据报是如何被发送和接收的,因为这些都可以由JRTPLIB自己来完成。只要 PollData()或者SendPacket()方法被成功调用,JRTPLIB就能够自动对到达的 RTCP数据报进行处理,并且还会在需要的时候发送RTCP数据报,从而能够确保整个RTP会话过程的正确性。

而另一方面,通过调用RTPSession类提供的SetLocalName()、SetLocalEMail()、 SetLocalLocation()、SetLocalPhone()、SetLocalTool()和SetLocalNote()方法, JRTPLIB又允许程序员对RTP会话的控制信息进行设置。所有这些方法在调用时都带有两个参数,其中第一个参数是一个char型的指针,指向将要被设置的数据;而第二个参数则是一个int型的数值,表明该数据中的前面多少个字符将会被使用。例如下面的语句可以被用来设置控制信息中的电子邮件地址:

sess.SetLocalEMail("xiaowp@linuxgam.comxiaowp@linuxgam.com",19);

在RTP 会话过程中,不是所有的控制信息都需要被发送,通过调用RTPSession类提供的 EnableSendName()、EnableSendEMail()、EnableSendLocation()、EnableSendPhone ()、EnableSendTool()和EnableSendNote()方法,可以为当前RTP会话选择将被发送的控制信息。

3.6 实际应用

最后通过一个简单的流媒体发送-接收实例,介绍如何利用JRTPLIB来进行实时流媒体的编程。清单3给出了数据发送端的完整代码,它负责向用户指定的IP地址和端口,不断地发送RTP数据包:

#include <stdio.h>
#include <string.h>
#include "rtpsession.h"
// 错误处理函数
void checkerror(int err)
{if (err < 0) {char* errstr = RTPGetErrorString(err);printf("Error:%s//n", errstr);exit(-1);}
}
int main(int argc, char** argv)
{RTPSession sess;unsigned long destip;int destport;int portbase = 6000;int status, index;char buffer[128];if (argc != 3) {printf("Usage: ./sender destip destport//n");return -1;}// 获得接收端的IP地址和端口号destip = inet_addr(argv[1]);if (destip == INADDR_NONE) {printf("Bad IP address specified.//n");return -1;}destip = ntohl(destip);destport = atoi(argv[2]);// 创建RTP会话status = sess.Create(portbase);checkerror(status);// 指定RTP数据接收端status = sess.AddDestination(destip, destport);checkerror(status);// 设置RTP会话默认参数sess.SetDefaultPayloadType(0);sess.SetDefaultMark(false);sess.SetDefaultTimeStampIncrement(10);// 发送流媒体数据index = 1;do {sprintf(buffer, "%d: RTP packet", index ++);sess.SendPacket(buffer, strlen(buffer));printf("Send packet !//n");} while(1);return 0;
}

清单4则给出了数据接收端的完整代码,它负责从指定的端口不断地读取RTP数据包:

#include <stdio.h>
#include "rtpsession.h"
#include "rtppacket.h"
// 错误处理函数
void checkerror(int err)
{if (err < 0) {char* errstr = RTPGetErrorString(err);printf("Error:%s//n", errstr);exit(-1);}
}
int main(int argc, char** argv)
{RTPSession sess;int localport;int status;if (argc != 2) {printf("Usage: ./sender localport//n");return -1;}// 获得用户指定的端口号localport = atoi(argv[1]);// 创建RTP会话status = sess.Create(localport);checkerror(status);do {// 接受RTP数据status = sess.PollData();// 检索RTP数据源if (sess.GotoFirstSourceWithData()) {do {RTPPacket* packet;// 获取RTP数据报while ((packet = sess.GetNextPacket()) != NULL) {printf("Got packet !//n");// 删除RTP数据报delete packet;}} while (sess.GotoNextSourceWithData());}} while(1);return 0;
}

随着多媒体数据在Internet上所承担的作用变得越来越重要,需要实时传输音频和视频等多媒体数据的场合也将变得越来越多,如IP电话、视频点播、在线会议等。RTP是用来在Internet上进行实时流媒体传输的一种协议,目前已经被广泛地应用在各种场合,JRTPLIB是一个面向对象的 RTP封装库,利用它可以很方便地完成Linux平台上的实时流媒体编程。

4 基于jrtplib的NAT穿透

4.1 NAT穿透的基础只是有关于NAT穿透的基础知识

4.2 rtp/rtcp传输涉及到的NAT穿透

rtp/rtcp传输数据的时候,需要两个端口支持。即rtp端口用于传输rtp数据,即传输的多媒体数据;rtcp端口用于传输rtcp控制协议信息。 rtp/rtcp协议默认的端口是rtcp port = rtp port + 1 。详细的说,比如A终端和B终端之间通过rtp/rtcp进行通信,

如上图,

本地IP:PORT                                                        NAT映射后IP:PORT

UACA RTP的发送和接收IP:PORT : 192.168.1.100:8000                                             61.144.174.230:1597

UACA RTCP的发送和接收IP:PORT:192.168.1.100:8001                                             61.144.174.230:1602

UACB RTP的发送和接收IP:PORT : 192.168.1.10:8000                                                61.144.174.12:8357

UACB RTCP的发送和接收IP:PORT:192.168.1.10:8001                                                61.144.174.12:8420

上图可以得到一下一些信息:

(一) 本地端口 RTCP PORT = RTP PORT + 1;但是经过NAT映射之后也有可能满足这个等式,但是并不一定有这个关系。

(二)在NAT设备后面的终端的本地IP:PORT并不被NAT外的设置可知,也就无法通过终端的本地IP:PORT与之通信。而必须通过NAT映射之后的公网IP:PORT作为目的地址进行通信。

如上图的终端A如果要发送RTP数据到终端B,UACA发送的目的地址只能是:61.144.174.12:8357。同理,UACB发送RTP数据给UACA,目的地址只能是: 61.144.174.230:1597 。

(三)也许看到这里,如何得到自己的外网IP:PORT呢?如何得到对方的外网IP:PORT呢?这就是NAT IP:PORT转换和穿孔(puncture),下回分解。

4.3 NAT 地址转换

如上所述,终端需要知道自己的外网IP:port,可以通过STUN、STUNT、TURN、Full Proxy等方式。这里介绍通过STUN方式实现NAT穿透。

STUN: Simple Traversal of UDP Through NAT。即通过UDP对NAT进行穿透。

STUNT:Simple Traversal of UDP Through NATs and TCP too.可以通过TCP对NAT进行穿透。

STUN是一个标准协议,具体的协议内容网络上很多。在此不累述了。

为了通过STUN实现NAT穿透,得到自己的公网IP:PORT,必须有一个公网STUN服务器,以及我们的客户端必须支持STUN Client功能。STUN Client 通过UDP发送一个request到STUN服务器,该请求通过NAT设备的时候会把数据报头中的本地IP:PORT换成该本地IP:PORT对应的公网 IP:PORT,STUN服务器接收到该数据包后就可以把该公网IP:PORT 发送给STUN Client。这样我们就得到了自己的公网IP:PORT。这样别的终端就可以把该公网IP:PORT最为发送UDP数据的目的地址发送UDP数据。

推荐一款STUN client/server 程序代码,http://sourceforge.net/projects/stun/files/

这是一款开源软件。在客户端中的主要函数是下面这个:

NatType stunNatType( StunAddress4& dest,       //in 公网STUN服务器地址,如stun.xten.netbool verbose,                 //in 调试时是否输出调试信息bool* preservePort=0,         //out  if set, is return for if NAT preservers ports or notbool* hairpin=0 ,             //out  if set, is the return for if NAT will hairpin packetsNAT设备是否支持回环int port=0,                   // in 本地测试端口port to use for the test, 0 to choose random portStunAddress4* sAddr=0        // out NIC to use ,返回STUN返回的本地地址的公网IP:PORT
);

输入StunAddress和测试端口port,得到本地IP:PORT对应的公网IP:PORT.

4.4 对jrtplib  的改造

jrtplib中对rtp rtcp端口的处理关系是:rtcp port = rtp port + 1 。这就有问题,本地端口可以按照这个等式来设置端口,但是经过NAT映射之后的公网端口是随机的,有可能并不满足上述等式。

int portbase = 6000;                        //设置本地rtp端口为6000
transparams.SetPortbase(portbase);//默认的本地rtcp端口为6001.因为这里是本地设置,所一这样设置OK
status = sess.Create(sessparams,&transparams);
checkerror(status);
RTPIPv4Address addr(destip,destport); //设置目的地的rtp接收IP:PORT,公网传输的话就要设置为对方的rtp公网IP:PORT
// AddDestination()的内部处理是把addr.ip和addr.port+1赋给rtcp。这样如果对方在公网上,就有问题了。
// 因为对方的rtcp port 可能不等于rtp port +1;这就导致对方收不到rtcp数据包。
status = sess.AddDestination(addr); 

通过跟踪AddDestination()函数的实现,发现在class RTPIPv4Destination的构造函数中是这样构造一个发送目的地址的:

RTPIPv4Destination(uint32_t ip,uint16_t rtpportbase)
{memset(&rtpaddr,0,sizeof(struct sockaddr_in));memset(&rtcpaddr,0,sizeof(struct sockaddr_in));rtpaddr.sin_family = AF_INET;rtpaddr.sin_port = htons(rtpportbase);rtpaddr.sin_addr.s_addr = htonl(ip);rtcpaddr.sin_family = AF_INET;rtcpaddr.sin_port = htons(rtpportbase+1);//默认把rtp的端口+1赋给目的rtcp端口。rtcpaddr.sin_addr.s_addr = htonl(ip);RTPIPv4Destination::ip = ip;
}

为了实现:可以自定义目的IP地址和目的rtp port和rtcp port。为了实现这么目标,自己动手改造下面几个函数:构造函数RTPIPv4Destination() 、RTPSession::AddDestination(),思路是在目的地址设置相关函数中增加一个rtcp ip 和port参数。

RTPIPv4Destination(uint32_t ip,uint16_t rtpportbase,uint32_t rtcpip,uint16_t rtcpport)
{memset(&rtpaddr,0,sizeof(struct sockaddr_in));memset(&rtcpaddr,0,sizeof(struct sockaddr_in));rtpaddr.sin_family = AF_INET;rtpaddr.sin_port = htons(rtpportbase);rtpaddr.sin_addr.s_addr = htonl(ip);/**If rtcport has not been set separately, use the default rtcpport*/if ( 0 == rtcpport ){rtcpaddr.sin_family = AF_INET;rtcpaddr.sin_port = htons(rtpportbase+1);rtcpaddr.sin_addr.s_addr = htonl(ip);}else{rtcpaddr.sin_family = AF_INET;rtcpaddr.sin_port = htons(rtcpport);rtcpaddr.sin_addr.s_addr = htonl(ip);}RTPIPv4Destination::ip = ip;
}int RTPSession::AddDestination(const RTPAddress &addr,const RTPIPv4Address &rtcpaddr)
{if (!created)return ERR_RTP_SESSION_NOTCREATED;return rtptrans->AddDestination(addr,rtcpaddr);
}

在调用RTPSession::AddDestination、定义RTPIPv4Destination的时候实参也相应增加目的rtcp参数。
        这样改造之后就可以自定义独立的设置目的地址rtp ,rtcp端口了。

======================================================================================

按照RFC3984协议实现H264视频RTP打包(附源代码)

转自 http://linfengdu.blog.163.com/blog/static/11771073201092705745724/

相信有不少人和我一样,希望实现H264格式视频的流媒体播放。但是对于一个新手来说,往往不知道从何入手。利用百度,GOOGLE等搜索资料真是沙里淘金。在琢磨了N周之后,才弄出来了点成果,其中费了很多无用的功夫,光看英文协议就费了一周,后来才知道有中文版,并且我所达到的目的很简单,只要让VLC实时播放就行,不需要了解整个协议。我也很希望能直接搜出来一套代码,都一直没找到,还是得自己动手。现在我把代码贴出来,希望对做类似工程的朋友有所帮助。
         一、本示例代码在我的电脑上实现了对标准H264码流的RTP打包发送到本机的1234端口,用VLC播放器从1234端口能接收到该码流并实时播放。代码附有详细的注释,应该很容易理解(前提是大家稍微对RFC3550 RFC3984协议有了解)。
         二、本示例代码是按照RFC3984协议仅完成了RTP打包,并没有完成发送RTCP。原因就引用这位达人的话:“1.RTCP里头有很多关于RTCP发送简隔的时间计算,RTP信息的统计,这种操作不是难,而是烦,我不想去写。2.RTCP和RTP一开始出来的时候并不是因为视频的点播等应用的,而是视频会议。RTCP有管理与会者的层面含义,这一功能在很多场合并不会用到。3.我想简单,没有写多个流间的同步,如一个影片的视频和音频流。这些其实是RTCP来完成的。我懒得去写,因为这些功作RTP的各个库类(例如JRTPLIB库)都做得很好。我觉得用库的最大优点就在这吧”。
         三、和代码相关的原理性的东西,大家应该去看看RFC3550,RFC3984.这两份协议都有热心网友翻译好的中文版。我把他们放在压缩包里,大家就不用再累个半死去搜索注册下载了。如果为了更省事,我觉得看看这位网友总结的RFC3984的内容就够了。网址是http://www.cppblog.com/czanyou/archive/2009/12/25/67940.html。如果打不开网页,就到压缩包里资料文件夹下找吧。我已经把网页保存下来了。
         四、代码并非是我完全原创的,而是我在搜索到得网友的代码的基础上修改的。这里要特别感谢以下几位网友:
        1.猫头上的鹰(他的博客地址http://blog.csdn.net/Tinnal/archive/2008/09/03/2871734.aspx)在他的博客里我第一次找到了有价值的东西,并且他无偿提供的MPEG的RTP打包源码只要拷贝下来建个工程就能实现MPEG的流媒体,对我启发很大。
        2.liming,他提供的代码已经实现了H264的码流分析,将其中的每个NALU单元分离开来,并分析出了NALU的类型,长度等信息。为我实现RTP打包提供了很大的方便,事实上,这份示例代码就是在他的代码上添加了RTP打包部分,我连工程名字都没有改。他的源代码在这里
        3.luny,他提供的SDP文件在关键时候帮了我大忙,我发送的RTP数据包通过Wireshark抓包工具分析一直没错,可VLC播放器就是没任何反应。直到下载了他的SDP文件文件后终于出画面了。某位网友说VLC对H264只能通过TS封包或SDP文件打开RTP码流,在此我这么怀疑。
        4.jessiepan和他的帖子,http://topic.csdn.net/u/20090725/11/5FBC75B0-1091-4DD4-9154-3E3D59F9B6D1.html,这里提供了很多有用的信息。
           使用方法:直接在VC6上打开工程,编译。(需要注意的是大家要把IP地址改为自己的。在h264.h的#define DEST_IP        "192.168.0.30"和#define DEST_PORT 1234这两行修改就行了。同时w.sdp文件里也要改成一致的IP和端口号,不然VLC是接受不到数据的。在c=IN IP4 192.168.0.30 和m=video 1234 RTP/AVP 96这两行。中间的1234是我设置的端口号。)在执行程序之前,先用VLC打开w.sdp文件,然后执行程序,就可以看到画面了:)。同样需要注意的是VLC1.0以后的版本不支持直接打开h.264视频文件,但是0.97版本就支持。这里我测试用1.03和0.97两份版本]的VLC都可以接受并播放h.264RTP码流。
        目前还有几个问题我没有弄明白,希望有高手在看完这个帖子后能帮我解答:
        1.关于时间戳的设置。RFC3984里没有提到时间戳具体如何计算,我也是按照各方面的小道消息这样设置。unsigned int timestamp_increse=0;timestamp_increse=(unsigned int)(90000.0 / framerate); 即初始值设为0,时间戳增量设为90000.0 / framerate,framerate我设为25,即每秒25帧。每发送一个NALU单元,时间戳增加。若是该NALU大于1400字节,需要分片时,则多个分片拥有相同的时间戳。这样设置是否正确。请牛人给个权威解答。
        2.按照我的理解,SDP文件仅实现了告诉VLC在哪个IP和端口接受264RTP包,同样的信息我也通过在VLC的媒体-》打开网络串流,协议选RTP,然后填写IP和端口号中设置好了,为什么用打开SDP文件的方法能接收,但用后者VLC却没有一点反应。
        3.当我将帧率设为25时(即代码里的float framerate=25;)vlc能接受码流,但会比较卡,常缓冲,提示错误为main error: ES_OUT_SET_(GROUP_)PCR  is called too late, increasing pts_delay to 339 ms。我怀疑是我的电脑发送UDP包速度不够每秒播放25帧的所需要的UDP包数量,因此在SDP文件我添加了a=framerate:15来限制播放器每秒播放15帧,同时在代码里的相应行float framerate=15;也将帧率改为15这样虽然解决了卡的问题,但是视频播放很慢。请问要是我想达到每秒播放25帧,难道只能换台好电脑了?
        4.下一步我想用jrtplib来打包RTP,因为听说用这个库实现RTCP很方便,是不是这个库会根据网络状况自动发送RTCP信息。如果哪位高手有这方面的代码或者是实现了RTSP的代码,希望能拿出来交流,哪怕是部分代码或者是实现部分功能也好。

=====================================================================================

RTP - 视频流广播

转自  http://linfengdu.blog.163.com/blog/static/11771073201092705742165/?suggestedreading&wumii

这是用RTP(RFC3350)按RFC2550封装MPEG ES流数据的发送程序。学习RTP的路真的辛苦。在网上收集的有关RTP的程序都是那种只负责RTP数据包发送的库,如jrtplib等,他们的DEMO程序都只是用来发发字符串,编编聊天程序,无论是国内还是国外,都没有结合真正的应用的DEMO。其实我的目的很简单,就是写发个视频流服务器,不用复杂,只用把基本原理弄懂,因为这样你才能有的放矢。与网上和RTP相关的库没有应用不一让,当你尝试以流媒体服务器、linux来baidu或google时,你搜出来完非就那么几类:

1.FFSERVER 
        FFMPEG2的DEMO,说它有名只是因为这类程序太少了。FFMPEG2是很好用,我现在还在用,但这个DEMO就有很多“炒作”的嫌疑了。好像在做着FFMPEG2库的演示而不是真的视频流服务器。后来想想,这不正是作者想要的吗,但这不是我想要的。编解码部分我会很偏向FFMPEG这个“大杂会”,其它部分我会选择其它的“强者”

2.Darwin、Helix
        两个都是非常有名软件,也只能称之为软件了,因为就算Darwin有源码,这种代码规模,也不适合用于嵌入式。说回软件本身,真的很有名。它们都是很真真拿来商业化运行的软件,但我是研发人员,不是视频流服务商,对不起,Apple,对不起,Microsoft。

3.LIVE555
        如果说上面两个和我都相关性为零(当然了,也是困扰了N周以后痛苦得出的结论),那LIVE555真的给了我一条出路,它是一个代码规模非常合适,又非常强大的媒体解决方案(称之为方案是因为它功能非常的丰富)。有长一段时间,我想去弄懂它的源码,不过和网上的很多人一样,最后软下来了,毕竟,去把这么多东西揉在一起,框架会弄得很复杂,因为我们要把这些完全不同的东西不断一层一层的抽像,最后抽像成一样(哲学呀)。它结构复杂是我中断分析它原来的其中一个原因,但不是主要原因。它结构的复杂程度也没胡像很多人网上说的那样严重,如果你是一个C++的热忱爱好者,你反而会迷上这段代码,当然了,对C的爱好都来说,当然是一种折磨了。暂时把我自己归类在C++爱好者范畴吧,呵呵,我很欣赏这段原码。主要原因是我不希望被某一个库绑死。LIVE555是有编解码能力,但我更希望它只做服务器的工作。

因此,最终后回来的老路上来,没有帮助,就得自己帮自己,从最基础的RFC看起。经过了N天(周)的英文,终于领会了如果在RTP承载MPEG数据包。在这个过程中很得到了一些LIVE555的帮助(通过对Ethereal捕捉的LIVE555数据包进行分析)。先把程序弄上来,原理性的以后有空再写,程序只有一个.cpp文件,在vs.net 2003下编译通过,播放的视频文件在http://www.cnitblog.com/Files/tinnal/ES流解释程序.rar  内,播放的客户端采用VLC,其下载地址为http://www.videolan.org/。选择打开网络串流,然后选择“UDP/RTP”端口,输入程序的输出端口1000,然后才运行程序,你将在VLC内看到测试的广播视频,IP不一样的话自己改改就行。其它所谓的原理性的,也就是看RFC 3350、RFC2550以及iso13818-2的一些重点地方。

// MPEG2RTP.h
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <string.h>

#include <winsock2.h>
#include <winsock2.h>


//#include "mem.h"

//< Start code or other signal>
#define PACK_STARTCODE                     (unsigned int)0x000001ba
#define SYSTEM_HEADER_STARTCODE     (unsigned int)0x000001bb
#define PICTURE_START_CODE               (unsigned int)0x00000100
#define GROUP_START_CODE            (unsigned int)0x000000B8
#define ISO_11172_ENDCODE             (unsigned int)0x000001b9
#define SEQUENCE_HEADER_CODE        (unsigned int)0x000001b3

#define PACKET_BUFFER_END            (unsigned int)0x00000000


#define MAX_RTP_PKT_LENGTH     1440
#define HEADER_LENGTH        16
#define DEST_IP                "192.168.0.98"
#define DEST_PORT            1000
#define MPA                    14 /*MPEG PAYLOAD TYPE */
#define MPV                    32

typedef struct 
{
    /* byte 0 */
    unsigned char csrc_len:4;        /* expect 0 */
    unsigned char extension:1;        /* expect 1, see RTP_OP below */
    unsigned char padding:1;        /* expect 0 */
    unsigned char version:2;        /* expect 2 */
    /* byte 1 */
    unsigned char payload:7;        /* RTP_PAYLOAD_RTSP */
    unsigned char marker:1;        /* expect 1 */
    /* bytes 2, 3 */
    unsigned short seq_no;            
    /* bytes 4-7 */
    unsigned  long timestamp;        
    /* bytes 8-11 */
    unsigned long ssrc;            /* stream number is used here. */
} RTP_FIXED_HEADER;

typedef struct {
    //byte 0
    unsigned char TR_high2:2;    /* Temporal Reference high 2 bits*/
    unsigned char T:1;            /* video specific head extension flag */
    unsigned char MBZ:5;        /* unused */
    //byte1
    unsigned char TR_low8:8;    /* Temporal Reference low 8 bits*/
    //byte3
    unsigned char P:3;            /* picture type; 1=I,2=P,3=B,4=D */
    unsigned char E:1;         /* set if last byte of payload is slice end code */
    unsigned char B:1;            /* set if start of payload is slice start code */    
    unsigned char S:1;            /* sequence header present flag */
    unsigned char N:1;            /* N bit; used in MPEG 2 */
    unsigned char AN:1;        /* Active N bit */
    //byte4
    unsigned char FFC:3;        /* forward_f_code */
    unsigned char FFV:1;        /* full_pel_forward_vector */
    unsigned char BFC:3;        /* backward_f_code */
    unsigned char FBV:1;        /* full_pel_backward_vector */
} MPEG_VID_SPECIFIC_HDR; /* 4 BYTES */


enum reading_status { 
    SLICE_AGAIN,
    SLICE_BREAK,
    UNKNOWN,
    SLICE,
    SEQUENCE_HEADER,
    GROUP_START,
    PICTURE
    };

void validate_file();
float frame_rate(int buffer_index);
unsigned int read_picture_type(int buffer_index);
unsigned int read_FBV(int buffer_index);
unsigned int read_BFC(int buffer_index);
unsigned int read_FFV(int buffer_index);
unsigned int read_FFC(int buffer_index);
unsigned int extract_temporal_reference(int buffer_index);
unsigned int find_next_start_code(unsigned int *buffer_index);
void reset_buffer_index(void);
BOOL InitWinsock();

//MPEG2RTP.cpp
    //这个程序主要用于RTP封装MPEG2数据的学习和测试,不作任何其它用途
//软件在VS.net 2003中编译通过,但在linux下作小量修改也应编译通过。
//通过VLC测试,VLC能正确接收和解码由本程序发送的TEST.MPV编码流。
//
//作者:冯富秋 Tinnal
//邮箱:tinnal@163.com


#include "MPEG2RTP.h"

#pragma   comment(lib,"Ws2_32")

unsigned char            buf[MAX_RTP_PKT_LENGTH + 4]; //input buffer
enum reading_status        state = SEQUENCE_HEADER;
unsigned int            g_index_in_packet_buffer = HEADER_LENGTH;
static unsigned long    g_time_stamp = 0;
static unsigned long    g_time_stamp_current =0;
static float            g_frame_rate = 0;
static unsigned int        g_delay_time = 0;
static unsigned int        g_timetramp_increment = 0;
FILE    *mpfd;
SOCKET    socket1;
RTP_FIXED_HEADER        *rtp_hdr;
MPEG_VID_SPECIFIC_HDR    *mpeg_hdr;

#if 0
void Send_RTP_Packet(unsigned char *buf,int bytes)
{

    int i = 0;
    int count = 0;

    printf("\nPacket length %d\n",bytes);
    printf("RTP Header: [M]:%s [sequence number]:0x%lx [timestamp]:0x%lx\n",
        rtp_hdr->marker == 1?"TRUE":"FALSE",
        rtp_hdr->seq_no,
        rtp_hdr->timestamp);
    printf(" [TR]:%d [AN]:%d [N]:%d [Sequence Header]:%s \
        \n [Begin Slice]:%s [End Slice]:%s \
        \n [Pictute Type]:%d \
        \n [FBV]:%d [BFC]:%d [FFV]:%d [FFC]:%d\n",
        (mpeg_hdr->TR_high2 << 8 | mpeg_hdr->TR_low8), 
        mpeg_hdr->AN, mpeg_hdr->N, mpeg_hdr->S == 1?"TRUE":"FALSE",
        mpeg_hdr->B ==1?"TRUE":"FALSE", mpeg_hdr->E ==1?"TRUE":"FALSE",
        mpeg_hdr->P,
        mpeg_hdr->FBV, mpeg_hdr->BFC, mpeg_hdr->FFV, mpeg_hdr->FFC);

    while(bytes --)
    {
        printf("%02x ",buf[count++]);
        if(++i == 16)
        {
            i=0;
            printf("\n");
        }
    }
    printf("\n");

}

#else

Send_RTP_Packet(unsigned char *buf,int bytes)
{
    return send( socket1,  (char*) buf, bytes, 0 );
}

#endif


void main(int argc, char *argv[])
{
    unsigned int next_start_code;
    unsigned int next_start_code_index;
    unsigned int sent_bytes;
    unsigned short seq_num =0;
    unsigned short stream_num = 10;
    struct sockaddr_in server;
    int len =sizeof(server);


#if 0
    mpfd = fopen("E:\\tinnal\\live555\\vc_proj\\es\\Debug\\test.mpv", "rb");  
#else
    if (argc < 2)
    {
        printf("\nUSAGE: %s mpegfile\nExiting..\n\n",argv[0]);
                exit(0);
    }
    mpfd = fopen(argv[1], "rb");
#endif



    if (mpfd == NULL ) 
    {
        printf("\nERROR: could not open input file %s\n\n",argv[1]);
        exit(0);
    }
    rtp_hdr = (RTP_FIXED_HEADER*)&buf[0];
    mpeg_hdr = (MPEG_VID_SPECIFIC_HDR*)&buf[12];

    memset((void *)rtp_hdr,0,12); //zero-out the rtp fixed hdr
    memset((void *)mpeg_hdr,0,4); //zero-out the video specific hdr
    memset((void *)buf,0,MAX_RTP_PKT_LENGTH + 4);

    InitWinsock();

    server.sin_family=AF_INET;
    server.sin_port=htons(DEST_PORT);          //server的监听端口
    server.sin_addr.s_addr=inet_addr(DEST_IP); //server的地址 

    socket1=socket(AF_INET,SOCK_DGRAM,0);
    connect(socket1, (const sockaddr *)&server, len) ;

    //read the first packet from the mpeg file
    //always read 4 extra bytes in (in case there's a startcode there)
    //but dont send  more than MAX_RTP_PKT_LENGTH in one packet
    fread(&(buf[HEADER_LENGTH]), MAX_RTP_PKT_LENGTH-HEADER_LENGTH+4, 1,mpfd);

    validate_file();

    do
    {    

        /* initialization of the two RTP headers */
        rtp_hdr->seq_no     = htons(seq_num ++);
        rtp_hdr->payload     = MPV;
        rtp_hdr->version     = 2;
        rtp_hdr->marker    = 0;
        rtp_hdr->ssrc        = htonl(stream_num);    

        mpeg_hdr->S = mpeg_hdr->E = mpeg_hdr->B= 0;

        do{
            next_start_code = find_next_start_code(&next_start_code_index);


            if ((next_start_code >0x100) && (next_start_code<0x1b0) )
            {
                //< We reach the first slice start code in current packet buffer>
                //< Set the B flag of the mpeg special header>
                if(state == SEQUENCE_HEADER
                    || state ==GROUP_START
                    || state ==PICTURE
                    || state == UNKNOWN)
                {
                    state = SLICE;
                    mpeg_hdr->B = 1;
                }
                //< We reach slice start code in current packet again>
                //< Set the E flag of the mpeg special header>
                //< and update the sent_bytes to the last slice data end>
                else if (state == SLICE ||state == SLICE_AGAIN)
                {

                    state = SLICE_AGAIN;
                    sent_bytes = next_start_code_index;
                    mpeg_hdr->E     = 1;
                }
                //< We reach slice start codethe previous slice end>
                //< for a broken slice set the E flag>
                //< According to RFC we shouldnt put another slice data to this packet>
                //< instead of sent it out>
                else if (state == SLICE_BREAK)
                {
                    state = UNKNOWN;
                    sent_bytes = next_start_code_index;
                    mpeg_hdr->E     = 1;
                    goto Sent_Packet;
                }

            } 

            switch(next_start_code)
            {
            case SEQUENCE_HEADER_CODE:
                //< SEQUENCE_HEADER_CODE after SLICE_START_CODE>
                //< we must sent the packet now so that the SEQUENCE_HEADER_CODE>
                //< will appear at the start of the next packet>
                if(state == SLICE || state == SLICE_AGAIN)
                {                
                    state = SEQUENCE_HEADER;
                    sent_bytes = next_start_code_index;
                    //< Accord to RFC >
                    //< at the end of a frame we should set RTP marker bit to >
                    rtp_hdr->marker = 1;
                    goto Sent_Packet;
                }

                state = SEQUENCE_HEADER;
                g_frame_rate = frame_rate(next_start_code_index);
                g_delay_time = (unsigned int)(1000.0 / g_frame_rate +0.5); //ms
                g_timetramp_increment = (unsigned int)(90000.0 / g_frame_rate +0.5); //90K Hz
                mpeg_hdr->S=1;
                break;

            case GROUP_START_CODE:
                //< GROUP_START_CODE after SLICE_START_CODE>
                //< we must sent the packet now so that the GROUP_START_CODE>
                //< will appear at the start of the next packet>
                if(state == SLICE || state == SLICE_AGAIN)
                {
                    state = GROUP_START;
                    sent_bytes = next_start_code_index;
                    //< Accord to RFC >
                    //< at the end of a frame we should set RTP marker bit to >
                    rtp_hdr->marker = 1;
                    goto Sent_Packet;
                }    

                state = GROUP_START;

            case PICTURE_START_CODE:
                //< PICTURE_START_CODE after PICTURE_START_CODE>
                //< we must sent the packet now so that the PICTURE_START_CODE>
                //< will appear at the start of the next packet>
                if(state == SLICE || state == SLICE_AGAIN)
                {
                    state = PICTURE;
                    sent_bytes = next_start_code_index;
                    //< Accord to RFC >
                    //< at the end of a frame we should set RTP marker bit to >
                    rtp_hdr->marker = 1;
                    goto Sent_Packet;
                }

                state = PICTURE;

                mpeg_hdr->TR_high2    = (extract_temporal_reference(next_start_code_index) & 0x300 )>> 8;
                mpeg_hdr->TR_low8     =  extract_temporal_reference(next_start_code_index) & 0xff;
                mpeg_hdr->P             = read_picture_type(next_start_code_index);
                //now read the motion vectors information
                if( (mpeg_hdr->P==2) || (mpeg_hdr->P==3))
                { //if B- or P-type picture, need forward mv
                    mpeg_hdr->FFV = read_FFV(next_start_code_index); 
                    mpeg_hdr->FFC = read_FFC(next_start_code_index);
                }
                if( mpeg_hdr->P==3)
                { // if B-type pictue, need backward mv
                    mpeg_hdr->FBV = read_FBV(next_start_code_index);
                    mpeg_hdr->BFC = read_BFC(next_start_code_index);
                }

                //< Time stamp only increate per frame>
                //< But I or P frame>
                if( mpeg_hdr->P== 1 || mpeg_hdr->P == 2 ){
                    g_time_stamp += g_timetramp_increment;
                    g_time_stamp_current = g_time_stamp;
                }else{
                    g_time_stamp += g_timetramp_increment;
                }
                

                break;

            case PACKET_BUFFER_END:
                //< There is one more slice in the packet buffer>
                //< Anyway we only sent the integrated slice>
                if(state == SLICE_AGAIN) {
                    state = UNKNOWN;
                    goto Sent_Packet;
                }

                //< There is one Slice in the packet buffer>
                //< But the Slice is to big so we break the slice>
                if(state == SLICE)
                {
                    state = SLICE_BREAK;
                    sent_bytes = next_start_code_index;
                    goto Sent_Packet;
                }

                //< There if a broke slice but in current packet buffer>
                //< we could not find the end of the slice>
                //< Let it in the broke state>
                if(state == SLICE_BREAK )
                {
                    state = SLICE_BREAK;
                    sent_bytes = next_start_code_index;
                    goto Sent_Packet;
                }

                break;
            }
        }while(next_start_code != PACKET_BUFFER_END);

Sent_Packet:
        rtp_hdr->timestamp = htonl(g_time_stamp_current);
        Send_RTP_Packet(buf, sent_bytes); 
        
        //copy the tail data to the head of the packet buffer
        memmove(&buf[HEADER_LENGTH], &buf[sent_bytes], MAX_RTP_PKT_LENGTH-sent_bytes);
        //reset the buffer index to zero
        reset_buffer_index();
        //reading data into buffer again
        fread(&(buf[(MAX_RTP_PKT_LENGTH-sent_bytes)+HEADER_LENGTH]), sent_bytes -HEADER_LENGTH , 1,mpfd);

        // sleep g_delay_time msec for sending next picture data
        if(rtp_hdr->marker ==1) Sleep( g_delay_time ); 

    }while(!feof(mpfd));
    closesocket(socket1);
    fclose(mpfd);

    printf("stream end.\n");
}

//==================================================================

unsigned int find_next_start_code(unsigned int *next_start_code_index) //NOTE: all start codes ARE byte-aligned
{
    unsigned int byte0=0,byte1=0,byte2=0,byte3=0,startcode=0;

    //while not startcode and have not exceeded max packet length
    while (g_index_in_packet_buffer < MAX_RTP_PKT_LENGTH) 
    { 
        if (buf[g_index_in_packet_buffer+0] == 0
            && buf[g_index_in_packet_buffer+1] == 0
            && buf[g_index_in_packet_buffer+2] ==1)
        {
            //printf("FOUND startcode %d\n",indx);
            byte0=(int)buf[g_index_in_packet_buffer+0];
            byte1=(int)buf[g_index_in_packet_buffer+1];
            byte2=(int)buf[g_index_in_packet_buffer+2];
            byte3=(int)buf[g_index_in_packet_buffer+3];
            startcode=(byte0 << 24) + (byte1 << 16) + (byte2 << 8) + byte3;
            *next_start_code_index = g_index_in_packet_buffer;
            g_index_in_packet_buffer = g_index_in_packet_buffer+4;
            return(startcode);
        }
        else
            g_index_in_packet_buffer++;
    }

    //< reach the end of the packet buffer>
    if (g_index_in_packet_buffer >= (MAX_RTP_PKT_LENGTH))
    {
        *next_start_code_index = g_index_in_packet_buffer -1;
        g_index_in_packet_buffer = HEADER_LENGTH;
        return PACKET_BUFFER_END;
    }

    printf("Error reading buffer..\n");
    exit(-1);
    return -1;
}

void reset_buffer_index(void)
{
    g_index_in_packet_buffer = HEADER_LENGTH;
}



//========================================================
float frame_rate(int buffer_index)
{
    unsigned char frame_rate_code;
    frame_rate_code = (unsigned char)buf[buffer_index +7] & 0xf;
    switch(frame_rate_code)
    {
    case 0x1:
        return 23.976;
    case 0x2:
        return 24.0;
    case 0x3:
        return 25.0;
    case 0x4:
        return 29.97;
    case 0x5:
        return 30.0;
    case 0x6:
        return 50.0;
    case 0x7:
        return 59.94;
    case 0x8:
        return 60.0;
    default:
        return 0;
    }
}
//========================================================
unsigned int extract_temporal_reference(int buffer_index) // 10 bits
{
    unsigned int low2bits=0,TR=0; // TR = temporal reference;

    TR = (unsigned int) (buf[buffer_index+4]); 
    TR <<= 2;
    low2bits = (unsigned int) (buf[buffer_index+5]);
    TR |= (low2bits >> 6);
    return(TR);
}

//========================================================

unsigned int read_picture_type(int buffer_index)
{
    unsigned int pictype=0;

    pictype = (unsigned int) buf[buffer_index+5];
    pictype = (pictype >> 3) & (0x7);
    return (pictype);
}

//=======================================================
unsigned int read_FFV(int buffer_index) // 1 bit
{
    return( (int) ((buf[buffer_index+7] & (0x4)) >> 2));
}
//=======================================================
unsigned int read_FFC(int buffer_index) // 3 bits
{
    unsigned int FFC=0,lowbit=0;
    FFC = (int) (buf[buffer_index+7] & (0x3));
    FFC <<= 1;
    lowbit = (int) ((buf[buffer_index+8]) & (0x80));
    FFC = FFC | (lowbit >> 7 );
    return(FFC);
}

//=======================================================
unsigned int read_FBV(int buffer_index) // 1 bit
{       
    return( (int) ((buf[buffer_index+8] & (0x40))>>6) );
}

//=======================================================
unsigned int read_BFC(int buffer_index) // 3 bits
{                
    return( (int) ( (buf[buffer_index+8] & (0x38) ) >> 3 ) );
}

void validate_file()
{
    /* to validate the file, ensure the existance of a startcode */
    int j=0,valid=0;

    while ((j++<MAX_RTP_PKT_LENGTH) && (!valid))
    {
        if (!((int)buf[j+0] + (int)buf[j+1]) && (((int)buf[j+2])==1))
            valid=1;
    }
    if (!valid)
    {
        printf("\nERROR: start code not found. \
               \nInput file must be a valid MPEG I file.\n");
        exit(0);
    }           
}

BOOL InitWinsock()
{
    int Error;
    WORD VersionRequested;
    WSADATA WsaData;
    VersionRequested=MAKEWORD(2,2);
    Error=WSAStartup(VersionRequested,&WsaData); //启动WinSock2
    if(Error!=0)
    {
        return FALSE;
    }
    else
    {
        if(LOBYTE(WsaData.wVersion)!=2||HIBYTE(WsaData.wHighVersion)!=2)
        {
            WSACleanup();
            return FALSE;
        }
        
    }
    return TRUE;
}

完成这个测试程序后,我有了很大的信心,又重复看了RFC3550几编,其实,如果你真看了程序,你发现我只发送了RTP,并没有发送RTCP数据包,因此,我们是不能同步多个RTP流的。我没去编码下去,因为我觉得已经够了。这里强调,没用说的RTP没有了RTCP就不行!接下来的工作,就是把这个程序的下层发包函数去掉,采用RTP库JRTPLIB,我觉得这才应该是JRTPLIB的DEMO!如果有人问,就这样的一个程序就能完成任务了,要JRTPLIB干嘛,其实,我不写RTCP相关代码的原因为多个:

1.RTCP里头有很多关于RTCP发送简隔的时间计算,RTP信息的统计,这种操作不是难,而是烦,我不想去写
        2.RTCP和RTP一开始出来的时候并不是因为视频的点播等应用的,而是视频会议。RTCP有管理与会者的层面含义,这一功能在很多场合并不会用到。
        3.我想简单,没有写多个流间的同步,如一个影片的视频和音频流。这些其实是RTCP来完成的。

我懒得去写,因为这些功作RTP的各个库类都做得很好。我觉得用库的最大优点就在这吧。

===================================================================================================

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

转自  http://blog.csdn.net/dengzikun/article/details/5807694

最近考虑使用RTP替换原有的高清视频传输协议,遂上网查找有关H264视频RTP打包、解包的文档和代码。功夫不负有心人,找到不少有价值的文档和代码。参考这些资料,写了H264 RTP打包类、解包类,实现了单个NAL单元包和FU_A分片单元包。对于丢包处理,采用简单的策略:丢弃随后的所有数据包,直到收到关键帧。测试效果还不错,代码贴上来,若能为同道中人借鉴一二,足矣。两个类的使用说明如下(省略了错误处理过程):

DWORD H264SSRC ;
CH264_RTP_PACK pack ( H264SSRC ) ;
BYTE *pVideoData ;
DWORD Size, ts ;
bool IsEndOfFrame ;
WORD wLen ;
pack.Set ( pVideoData, Size, ts, IsEndOfFrame ) ;
BYTE *pPacket ;
while ( pPacket = pack.Get ( &wLen ) )
{// rtp packet process// ...
}HRESULT hr ;
CH264_RTP_UNPACK unpack ( hr ) ;
BYTE *pRtpData ;
WORD inSize;
int outSize ;
BYTE *pFrame = unpack.Parse_RTP_Packet ( pRtpData, inSize, &outSize ) ;
if ( pFrame != NULL )
{// frame process// ...
}
[cpp] view plaincopy
  1. //
  2. // class CH264_RTP_PACK start
  3. class CH264_RTP_PACK
  4. {
  5. #define RTP_VERSION 2
  6. typedef struct NAL_msg_s
  7. {
  8. bool eoFrame ;
  9. unsigned char type;     // NAL type
  10. unsigned char *start;   // pointer to first location in the send buffer
  11. unsigned char *end; // pointer to last location in send buffer
  12. unsigned long size ;
  13. } NAL_MSG_t;
  14. typedef struct
  15. {
  16. //LITTLE_ENDIAN
  17. unsigned short   cc:4;      /* CSRC count                 */
  18. unsigned short   x:1;       /* header extension flag      */
  19. unsigned short   p:1;       /* padding flag               */
  20. unsigned short   v:2;       /* packet type                */
  21. unsigned short   pt:7;      /* payload type               */
  22. unsigned short   m:1;       /* marker bit                 */
  23. unsigned short    seq;      /* sequence number            */
  24. unsigned long     ts;       /* timestamp                  */
  25. unsigned long     ssrc;     /* synchronization source     */
  26. } rtp_hdr_t;
  27. typedef struct tagRTP_INFO
  28. {
  29. NAL_MSG_t   nal;        // NAL information
  30. rtp_hdr_t   rtp_hdr;    // RTP header is assembled here
  31. int hdr_len;            // length of RTP header
  32. unsigned char *pRTP;    // pointer to where RTP packet has beem assembled
  33. unsigned char *start;   // pointer to start of payload
  34. unsigned char *end;     // pointer to end of payload
  35. unsigned int s_bit;     // bit in the FU header
  36. unsigned int e_bit;     // bit in the FU header
  37. bool FU_flag;       // fragmented NAL Unit flag
  38. } RTP_INFO;
  39. public:
  40. CH264_RTP_PACK(unsigned long H264SSRC, unsigned char H264PAYLOADTYPE=96, unsigned short MAXRTPPACKSIZE=1472 )
  41. {
  42. m_MAXRTPPACKSIZE = MAXRTPPACKSIZE ;
  43. if ( m_MAXRTPPACKSIZE > 10000 )
  44. {
  45. m_MAXRTPPACKSIZE = 10000 ;
  46. }
  47. if ( m_MAXRTPPACKSIZE < 50 )
  48. {
  49. m_MAXRTPPACKSIZE = 50 ;
  50. }
  51. memset ( &m_RTP_Info, 0, sizeof(m_RTP_Info) ) ;
  52. m_RTP_Info.rtp_hdr.pt = H264PAYLOADTYPE ;
  53. m_RTP_Info.rtp_hdr.ssrc = H264SSRC ;
  54. m_RTP_Info.rtp_hdr.v = RTP_VERSION ;
  55. m_RTP_Info.rtp_hdr.seq = 0 ;
  56. }
  57. ~CH264_RTP_PACK(void)
  58. {
  59. }
  60. //传入Set的数据必须是一个完整的NAL,起始码为0x00000001。
  61. //起始码之前至少预留10个字节,以避免内存COPY操作。
  62. //打包完成后,原缓冲区内的数据被破坏。
  63. bool Set ( unsigned char *NAL_Buf, unsigned long NAL_Size, unsigned long Time_Stamp, bool End_Of_Frame )
  64. {
  65. unsigned long startcode = StartCode(NAL_Buf) ;
  66. if ( startcode != 0x01000000 )
  67. {
  68. return false ;
  69. }
  70. int type = NAL_Buf[4] & 0x1f ;
  71. if ( type < 1 || type > 12 )
  72. {
  73. return false ;
  74. }
  75. m_RTP_Info.nal.start = NAL_Buf ;
  76. m_RTP_Info.nal.size = NAL_Size ;
  77. m_RTP_Info.nal.eoFrame = End_Of_Frame ;
  78. m_RTP_Info.nal.type = m_RTP_Info.nal.start[4] ;
  79. m_RTP_Info.nal.end = m_RTP_Info.nal.start + m_RTP_Info.nal.size ;
  80. m_RTP_Info.rtp_hdr.ts = Time_Stamp ;
  81. m_RTP_Info.nal.start += 4 ; // skip the syncword
  82. if ( (m_RTP_Info.nal.size + 7) > m_MAXRTPPACKSIZE )
  83. {
  84. m_RTP_Info.FU_flag = true ;
  85. m_RTP_Info.s_bit = 1 ;
  86. m_RTP_Info.e_bit = 0 ;
  87. m_RTP_Info.nal.start += 1 ; // skip NAL header
  88. }
  89. else
  90. {
  91. m_RTP_Info.FU_flag = false ;
  92. m_RTP_Info.s_bit = m_RTP_Info.e_bit = 0 ;
  93. }
  94. m_RTP_Info.start = m_RTP_Info.end = m_RTP_Info.nal.start ;
  95. m_bBeginNAL = true ;
  96. return true ;
  97. }
  98. //循环调用Get获取RTP包,直到返回值为NULL
  99. unsigned char* Get ( unsigned short *pPacketSize )
  100. {
  101. if ( m_RTP_Info.end == m_RTP_Info.nal.end )
  102. {
  103. *pPacketSize = 0 ;
  104. return NULL ;
  105. }
  106. if ( m_bBeginNAL )
  107. {
  108. m_bBeginNAL = false ;
  109. }
  110. else
  111. {
  112. m_RTP_Info.start = m_RTP_Info.end;  // continue with the next RTP-FU packet
  113. }
  114. int bytesLeft = m_RTP_Info.nal.end - m_RTP_Info.start ;
  115. int maxSize = m_MAXRTPPACKSIZE - 12 ;   // sizeof(basic rtp header) == 12 bytes
  116. if ( m_RTP_Info.FU_flag )
  117. maxSize -= 2 ;
  118. if ( bytesLeft > maxSize )
  119. {
  120. m_RTP_Info.end = m_RTP_Info.start + maxSize ;   // limit RTP packetsize to 1472 bytes
  121. }
  122. else
  123. {
  124. m_RTP_Info.end = m_RTP_Info.start + bytesLeft ;
  125. }
  126. if ( m_RTP_Info.FU_flag )
  127. {   // multiple packet NAL slice
  128. if ( m_RTP_Info.end == m_RTP_Info.nal.end )
  129. {
  130. m_RTP_Info.e_bit = 1 ;
  131. }
  132. }
  133. m_RTP_Info.rtp_hdr.m =  m_RTP_Info.nal.eoFrame ? 1 : 0 ; // should be set at EofFrame
  134. if ( m_RTP_Info.FU_flag && !m_RTP_Info.e_bit )
  135. {
  136. m_RTP_Info.rtp_hdr.m = 0 ;
  137. }
  138. m_RTP_Info.rtp_hdr.seq++ ;
  139. unsigned char *cp = m_RTP_Info.start ;
  140. cp -= ( m_RTP_Info.FU_flag ? 14 : 12 ) ;
  141. m_RTP_Info.pRTP = cp ;
  142. unsigned char *cp2 = (unsigned char *)&m_RTP_Info.rtp_hdr ;
  143. cp[0] = cp2[0] ;
  144. cp[1] = cp2[1] ;
  145. cp[2] = ( m_RTP_Info.rtp_hdr.seq >> 8 ) & 0xff ;
  146. cp[3] = m_RTP_Info.rtp_hdr.seq & 0xff ;
  147. cp[4] = ( m_RTP_Info.rtp_hdr.ts >> 24 ) & 0xff ;
  148. cp[5] = ( m_RTP_Info.rtp_hdr.ts >> 16 ) & 0xff ;
  149. cp[6] = ( m_RTP_Info.rtp_hdr.ts >>  8 ) & 0xff ;
  150. cp[7] = m_RTP_Info.rtp_hdr.ts & 0xff ;
  151. cp[8] =  ( m_RTP_Info.rtp_hdr.ssrc >> 24 ) & 0xff ;
  152. cp[9] =  ( m_RTP_Info.rtp_hdr.ssrc >> 16 ) & 0xff ;
  153. cp[10] = ( m_RTP_Info.rtp_hdr.ssrc >>  8 ) & 0xff ;
  154. cp[11] = m_RTP_Info.rtp_hdr.ssrc & 0xff ;
  155. m_RTP_Info.hdr_len = 12 ;
  156. /*!
  157. * /n The FU indicator octet has the following format:
  158. * /n
  159. * /n      +---------------+
  160. * /n MSB  |0|1|2|3|4|5|6|7|  LSB
  161. * /n      +-+-+-+-+-+-+-+-+
  162. * /n      |F|NRI|  Type   |
  163. * /n      +---------------+
  164. * /n
  165. * /n The FU header has the following format:
  166. * /n
  167. * /n      +---------------+
  168. * /n      |0|1|2|3|4|5|6|7|
  169. * /n      +-+-+-+-+-+-+-+-+
  170. * /n      |S|E|R|  Type   |
  171. * /n      +---------------+
  172. */
  173. if ( m_RTP_Info.FU_flag )
  174. {
  175. // FU indicator  F|NRI|Type
  176. cp[12] = ( m_RTP_Info.nal.type & 0xe0 ) | 28 ;  //Type is 28 for FU_A
  177. //FU header     S|E|R|Type
  178. cp[13] = ( m_RTP_Info.s_bit << 7 ) | ( m_RTP_Info.e_bit << 6 ) | ( m_RTP_Info.nal.type & 0x1f ) ; //R = 0, must be ignored by receiver
  179. m_RTP_Info.s_bit = m_RTP_Info.e_bit= 0 ;
  180. m_RTP_Info.hdr_len = 14 ;
  181. }
  182. m_RTP_Info.start = &cp[m_RTP_Info.hdr_len] ;    // new start of payload
  183. *pPacketSize = m_RTP_Info.hdr_len + ( m_RTP_Info.end - m_RTP_Info.start ) ;
  184. return m_RTP_Info.pRTP ;
  185. }
  186. private:
  187. unsigned int StartCode( unsigned char *cp )
  188. {
  189. unsigned int d32 ;
  190. d32 = cp[3] ;
  191. d32 <<= 8 ;
  192. d32 |= cp[2] ;
  193. d32 <<= 8 ;
  194. d32 |= cp[1] ;
  195. d32 <<= 8 ;
  196. d32 |= cp[0] ;
  197. return d32 ;
  198. }
  199. private:
  200. RTP_INFO m_RTP_Info ;
  201. bool m_bBeginNAL ;
  202. unsigned short m_MAXRTPPACKSIZE ;
  203. };
  204. // class CH264_RTP_PACK end
  205. //
  206. //
  207. // class CH264_RTP_UNPACK start
  208. class CH264_RTP_UNPACK
  209. {
  210. #define RTP_VERSION 2
  211. #define BUF_SIZE (1024 * 500)
  212. typedef struct
  213. {
  214. //LITTLE_ENDIAN
  215. unsigned short   cc:4;      /* CSRC count                 */
  216. unsigned short   x:1;       /* header extension flag      */
  217. unsigned short   p:1;       /* padding flag               */
  218. unsigned short   v:2;       /* packet type                */
  219. unsigned short   pt:7;      /* payload type               */
  220. unsigned short   m:1;       /* marker bit                 */
  221. unsigned short    seq;      /* sequence number            */
  222. unsigned long     ts;       /* timestamp                  */
  223. unsigned long     ssrc;     /* synchronization source     */
  224. } rtp_hdr_t;
  225. public:
  226. CH264_RTP_UNPACK ( HRESULT &hr, unsigned char H264PAYLOADTYPE = 96 )
  227. : m_bSPSFound(false)
  228. , m_bWaitKeyFrame(true)
  229. , m_bPrevFrameEnd(false)
  230. , m_bAssemblingFrame(false)
  231. , m_wSeq(1234)
  232. , m_ssrc(0)
  233. {
  234. m_pBuf = new BYTE[BUF_SIZE] ;
  235. if ( m_pBuf == NULL )
  236. {
  237. hr = E_OUTOFMEMORY ;
  238. return ;
  239. }
  240. m_H264PAYLOADTYPE = H264PAYLOADTYPE ;
  241. m_pEnd = m_pBuf + BUF_SIZE ;
  242. m_pStart = m_pBuf ;
  243. m_dwSize = 0 ;
  244. hr = S_OK ;
  245. }
  246. ~CH264_RTP_UNPACK(void)
  247. {
  248. delete [] m_pBuf ;
  249. }
  250. //pBuf为H264 RTP视频数据包,nSize为RTP视频数据包字节长度,outSize为输出视频数据帧字节长度。
  251. //返回值为指向视频数据帧的指针。输入数据可能被破坏。
  252. BYTE* Parse_RTP_Packet ( BYTE *pBuf, unsigned short nSize, int *outSize )
  253. {
  254. if ( nSize <= 12 )
  255. {
  256. return NULL ;
  257. }
  258. BYTE *cp = (BYTE*)&m_RTP_Header ;
  259. cp[0] = pBuf[0] ;
  260. cp[1] = pBuf[1] ;
  261. m_RTP_Header.seq = pBuf[2] ;
  262. m_RTP_Header.seq <<= 8 ;
  263. m_RTP_Header.seq |= pBuf[3] ;
  264. m_RTP_Header.ts = pBuf[4] ;
  265. m_RTP_Header.ts <<= 8 ;
  266. m_RTP_Header.ts |= pBuf[5] ;
  267. m_RTP_Header.ts <<= 8 ;
  268. m_RTP_Header.ts |= pBuf[6] ;
  269. m_RTP_Header.ts <<= 8 ;
  270. m_RTP_Header.ts |= pBuf[7] ;
  271. m_RTP_Header.ssrc = pBuf[8] ;
  272. m_RTP_Header.ssrc <<= 8 ;
  273. m_RTP_Header.ssrc |= pBuf[9] ;
  274. m_RTP_Header.ssrc <<= 8 ;
  275. m_RTP_Header.ssrc |= pBuf[10] ;
  276. m_RTP_Header.ssrc <<= 8 ;
  277. m_RTP_Header.ssrc |= pBuf[11] ;
  278. BYTE *pPayload = pBuf + 12 ;
  279. DWORD PayloadSize = nSize - 12 ;
  280. // Check the RTP version number (it should be 2):
  281. if ( m_RTP_Header.v != RTP_VERSION )
  282. {
  283. return NULL ;
  284. }
  285. /*
  286. // Skip over any CSRC identifiers in the header:
  287. if ( m_RTP_Header.cc )
  288. {
  289. long cc = m_RTP_Header.cc * 4 ;
  290. if ( Size < cc )
  291. {
  292. return NULL ;
  293. }
  294. Size -= cc ;
  295. p += cc ;
  296. }
  297. // Check for (& ignore) any RTP header extension
  298. if ( m_RTP_Header.x )
  299. {
  300. if ( Size < 4 )
  301. {
  302. return NULL ;
  303. }
  304. Size -= 4 ;
  305. p += 2 ;
  306. long l = p[0] ;
  307. l <<= 8 ;
  308. l |= p[1] ;
  309. p += 2 ;
  310. l *= 4 ;
  311. if ( Size < l ) ;
  312. {
  313. return NULL ;
  314. }
  315. Size -= l ;
  316. p += l ;
  317. }
  318. // Discard any padding bytes:
  319. if ( m_RTP_Header.p )
  320. {
  321. if ( Size == 0 )
  322. {
  323. return NULL ;
  324. }
  325. long Padding = p[Size-1] ;
  326. if ( Size < Padding )
  327. {
  328. return NULL ;
  329. }
  330. Size -= Padding ;
  331. }*/
  332. // Check the Payload Type.
  333. if ( m_RTP_Header.pt != m_H264PAYLOADTYPE )
  334. {
  335. return NULL ;
  336. }
  337. int PayloadType = pPayload[0] & 0x1f ;
  338. int NALType = PayloadType ;
  339. if ( NALType == 28 ) // FU_A
  340. {
  341. if ( PayloadSize < 2 )
  342. {
  343. return NULL ;
  344. }
  345. NALType = pPayload[1] & 0x1f ;
  346. }
  347. if ( m_ssrc != m_RTP_Header.ssrc )
  348. {
  349. m_ssrc = m_RTP_Header.ssrc ;
  350. SetLostPacket () ;
  351. }
  352. if ( NALType == 0x07 ) // SPS
  353. {
  354. m_bSPSFound = true ;
  355. }
  356. if ( !m_bSPSFound )
  357. {
  358. return NULL ;
  359. }
  360. if ( NALType == 0x07 || NALType == 0x08 ) // SPS PPS
  361. {
  362. m_wSeq = m_RTP_Header.seq ;
  363. m_bPrevFrameEnd = true ;
  364. pPayload -= 4 ;
  365. *((DWORD*)(pPayload)) = 0x01000000 ;
  366. *outSize = PayloadSize + 4 ;
  367. return pPayload ;
  368. }
  369. if ( m_bWaitKeyFrame )
  370. {
  371. if ( m_RTP_Header.m ) // frame end
  372. {
  373. m_bPrevFrameEnd = true ;
  374. if ( !m_bAssemblingFrame )
  375. {
  376. m_wSeq = m_RTP_Header.seq ;
  377. return NULL ;
  378. }
  379. }
  380. if ( !m_bPrevFrameEnd )
  381. {
  382. m_wSeq = m_RTP_Header.seq ;
  383. return NULL ;
  384. }
  385. else
  386. {
  387. if ( NALType != 0x05 ) // KEY FRAME
  388. {
  389. m_wSeq = m_RTP_Header.seq ;
  390. m_bPrevFrameEnd = false ;
  391. return NULL ;
  392. }
  393. }
  394. }
  395. ///
  396. if ( m_RTP_Header.seq != (WORD)( m_wSeq + 1 ) ) // lost packet
  397. {
  398. m_wSeq = m_RTP_Header.seq ;
  399. SetLostPacket () ;
  400. return NULL ;
  401. }
  402. else
  403. {
  404. // 码流正常
  405. m_wSeq = m_RTP_Header.seq ;
  406. m_bAssemblingFrame = true ;
  407. if ( PayloadType != 28 ) // whole NAL
  408. {
  409. *((DWORD*)(m_pStart)) = 0x01000000 ;
  410. m_pStart += 4 ;
  411. m_dwSize += 4 ;
  412. }
  413. else // FU_A
  414. {
  415. if ( pPayload[1] & 0x80 ) // FU_A start
  416. {
  417. *((DWORD*)(m_pStart)) = 0x01000000 ;
  418. m_pStart += 4 ;
  419. m_dwSize += 4 ;
  420. pPayload[1] = ( pPayload[0] & 0xE0 ) | NALType ;
  421. pPayload += 1 ;
  422. PayloadSize -= 1 ;
  423. }
  424. else
  425. {
  426. pPayload += 2 ;
  427. PayloadSize -= 2 ;
  428. }
  429. }
  430. if ( m_pStart + PayloadSize < m_pEnd )
  431. {
  432. CopyMemory ( m_pStart, pPayload, PayloadSize ) ;
  433. m_dwSize += PayloadSize ;
  434. m_pStart += PayloadSize ;
  435. }
  436. else // memory overflow
  437. {
  438. SetLostPacket () ;
  439. return NULL ;
  440. }
  441. if ( m_RTP_Header.m ) // frame end
  442. {
  443. *outSize = m_dwSize ;
  444. m_pStart = m_pBuf ;
  445. m_dwSize = 0 ;
  446. if ( NALType == 0x05 ) // KEY FRAME
  447. {
  448. m_bWaitKeyFrame = false ;
  449. }
  450. return m_pBuf ;
  451. }
  452. else
  453. {
  454. return NULL ;
  455. }
  456. }
  457. }
  458. void SetLostPacket()
  459. {
  460. m_bSPSFound = false ;
  461. m_bWaitKeyFrame = true ;
  462. m_bPrevFrameEnd = false ;
  463. m_bAssemblingFrame = false ;
  464. m_pStart = m_pBuf ;
  465. m_dwSize = 0 ;
  466. }
  467. private:
  468. rtp_hdr_t m_RTP_Header ;
  469. BYTE *m_pBuf ;
  470. bool m_bSPSFound ;
  471. bool m_bWaitKeyFrame ;
  472. bool m_bAssemblingFrame ;
  473. bool m_bPrevFrameEnd ;
  474. BYTE *m_pStart ;
  475. BYTE *m_pEnd ;
  476. DWORD m_dwSize ;
  477. WORD m_wSeq ;
  478. BYTE m_H264PAYLOADTYPE ;
  479. DWORD m_ssrc ;
  480. };
  481. // class CH264_RTP_UNPACK end
  482. ///

===========================================================================

JRTPLIB 3.9.1文档翻译

转自 http://blog.csdn.net/the__blue__sky/article/details/8795275

Main Page

JRTPLIB

Author:
           Jori Liesenborgs
           Developed at the The Expertise Centre for Digital Media (EDM), a research institute of the Hasselt University

Acknowledgment(致谢)

I would like thank the people at the Expertise Centre for Digital Media for giving me the opportunity to create this rewrite of the library.

Introduction

This document describes JRTPLIB, an object-oriented library written in C++ which aims to help developers in using the Real-time Transport Protocol (RTP) as described in RFC 3550.
              译:这个文档描述了JRTPLIB,一个用C++编写的面向对象的库,旨在帮助开发者使用RFC 3550中描述的实时传输协议(RTP)。

The library makes it possible for the user to send and receive data using RTP, without worrying about SSRC collisions, scheduling and transmitting RTCP data etc. The user only needs to provide the library with the payload data to be sent and the library gives the user access to incoming RTP and RTCP data.

译:这个库确保用户使用RTP发送和接收数据成为可能,而不用担心SSRC冲突,调度和发送RTCP数据等。用户只需要提供库和要发送的载荷数据,库就可以对进来的RTP和RTCP数据提供通道。

Design idea(设计理念)

The library provides several classes which can be helpful in creating RTP applications. Most users will probably only need the RTPSession class for building an application. This class provides the necessary functions for sending RTP data and handles the RTCP part internally.

这个库提供了几个可以帮助创建RTP应用程序的类。大多数用户将很可能只需要RTPSession类来建立一个应用程序。这个类(RTPSession类)提供了发送RTP数据和内部处理RTCP部分的必要功能。

Changes from version 2.x

One of the most important changes is probably the fact that this version is based on RFC 3550 and the 2.x versions were based upon RFC 1889 which is now obsolete.
             译:最重要的改变之一实际上是这个版本(3.x版本)是基于RFC 3550而2.x版本是基于现在已经淘汰的RFC 1889.

Also, the 2.x series was created with the idea that the user would only need to use the RTPSession class which meant that the other classes were not very useful by themselves. This version on the other hand, aims to provide many useful components to aid the user in building RTP capable applications.
             译:而且,2.x系列是基于用户将仅仅需要使用 RTPSession 类的想法,而这意味着其他类并不是很有用。另一方面,这个版本(3.x版本)提供了许多有用的东西,旨在帮助用户建立RTP应用程序。

In this version, the code which is specific for the underlying protocol by which RTP packets are transported, is bundled in a class which inherits its interface from a class called RTPTransmitter. This makes it easy for different underlying protocols to be supported. Currently there is support for UDP over IPv4 and UDP over IPv6.
             译:在这个版本中,有一个专门针对传输RTP包的底层协议的代码,这个代码被捆绑成一个类,这个类继承了一个叫RTPTransmitter类的接口。这使得支持不同的底层协议变得容易。目前支持通过IPv4的UDP协议和通过IPv6的UDP协议。

For applications such as a mixer or translator using the RTPSession class will not be a good solution. Other components can be used for this purpose: a transmission component, an SSRC table, an RTCP scheduler etc. Using these, it should be much easier to build all kinds of applications.

译:对于像mixer或translator这样的应用程序,使用RTPSession类并不是一个好的解决方案。其他组件可以被用于以下目的:一个传输组件,一个SSRC表,一个RTCP调度等。使用这些,应该是更容易建立所有类型的应用程序。

Copyright license(版权许可)

The library code uses the following copyright license:
译:库代码使用以下版权许可:

Permission is hereby granted, free of charge, to any person
 obtaining a copy of this software and associated documentation files
 (the "Software"), to deal in the Software without restriction,
 including without limitation the rights to use, copy, modify, merge,
 publish, distribute, sublicense, and/or sell copies of the Software,
 and to permit persons to whom the Software is furnished to do so,
 subject to the following conditions:
译:现准许任何人免费 获得本软件及相关文档文件的副本 (“软件”),对软件的处理没有任何限制, 包括但不限于使用,复制,修改,合并的权利, 出版,分发,再许可和/或销售软件的副本, 并允许该软件的布置,这样做的人, 受限以下条件:

The above copyright notice and this permission notice shall be
 included in all copies or substantial portions of the Software.
译:上述版权声明和本许可通知应 包含在所有的副本或实质性部分的软件。

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
 KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.

该软件提供的“AS IS” ,不附带任何 明示或暗示,包括但不仅限于 适销性,针对特定用途的适用性的 不侵权的。在任何情况下,作者或版权持有人 争取承担任何索赔,损害赔偿或其他责任,无论是在一个 ,合同,侵权行为或其他形式,从中产生,信息或采取行动 连接与软件或使用或其他买卖 软件。

There are two reasons for using this license. First, since this is the license of the 2.x series, it only seemed natural that this rewrite would contain the same license. Second, since the RTP protocol is deliberately incomplete RTP profiles can, for example, define additional header fields. The best way to deal with this is to adapt the library code itself and that's why I like to keep the license as free as possible.

译:使用这个许可证有两个原因。第一,因为这是2.x系列的许可证,这儿的重写将包含相同的许可证似乎是自然而然的。第二,因为RTP协议是故意不完整的RTP配置文件,例如,额外的头字段定义。处理这个问题的最好方式是改变库代码本身,这就是为什么我想尽可能保持免费许可证的原因。

Getting started with the RTPSession class(RTPSession类入门)

All classes and functions are part of the jrtplib namespace, so to simplify the code a bit, we'll declare that we're using this namespace:

译:所有的类和函数都是 jrtplib 命名空间的一部分,所以为了简化代码一点点,我们声明我们将使用这个命名空间:

using namespace jrtplib;


            To use RTP, you'll have to create an RTPSession object. The constructor accepts two parameter, an instance of an RTPRandom object, and an instance of an RTPMemoryManager object. For now, we'll keep it simple and use the default settings, so this is our code so far:
             译:为了使用RTP,你必须创建一个RTPSession对象。构造函数接受两个参数,一个RTPRandom对象实例和一个RTPMemoryManager对象的实例。现在,我们将使它简单点,使用默认设置,所以这就是我们目前的代码:
 RTPSession session; 


            To actually create the session, you'll have to call the Create member function which takes three arguments: the first one is of type RTPSessionParams and specifies the general options for the session. One parameter of this class must be set explicitly, otherwise the session will not be created successfully. This parameter is the timestamp unit of the data you intend to send and can be calculated by dividing a certain time interval (in seconds) by the number of samples in that interval. So, assuming that we'll send 8000 Hz voice data, we can use this code:
           译:为了实际创建会话,你必须调用Create成员函数,它有三个参数:第一个是RTPSessionParams型,指定了会话的一般选项。这个类的一个参数必须明确设置,否则会话将不会创建成功。这个参数是你打算发送数据的时间戳,它可以通过计算某个固定的时间除以固定时间内的抽样数的值来获得。所以,假设我们将发送8000Hz 的音频数据,我们就可以使用这个代码:
 RTPSessionParams sessionparams;

sessionparams.SetOwnTimestampUnit(1.0/8000.0);
           The other session parameters will probably depend on the actual RTP profile you intend to work with.
            译:其他会话参数将很可能取决于你打算使用的RTP profile。

The second argument of the Create function is a pointer to an RTPTransmissionParams instance and describes the parameters for the transmission component. The third parameter selects the type of transmission component which will be used. By default, an UDP over IPv4 transmitter is used, and for this particular transmitter, the transmission parameters should be of type RTPUDPv4TransmissionParams. Assuming that we want our RTP portbase to be 8000, we can do the following:
           译:Create函数的第二个参数是一个指向RTPTransmissionParams实例的指针,描述了transmission组件的参数。第三个参数设置了即将被用的transmission组件的类型。默认情况下,使用一个通过IPv4的UDP的 transmitter,对于这个特定的transmitter,这个transmission参数将会是RTPUDPv4TransmissionParams类型的。假设我们想要RTP的端口是8000,我们像下面这样做:

RTPUDPv4TransmissionParams transparams;
 
 transparams.SetPortbase(8000);

          Now, we're ready to call the Create member function of RTPSession. The return value is stored in the integer status so we can check if something went wrong. If this value is negative, it indicates that some error occurred. A description of what this error code means can be retrieved by calling RTPGetErrorString:
          译:现在,我们准备调用RTPSession的Create成员函数,返回值储存为整数状态(status)。所以我们可以检测是否有什么出错。如果这个值为负数,它表明有错误发生。对错误代码意味着什么的描述可以通过调用RTPGetErrorString来检索。

 int status = session.Create(sessionparams,&transparams);
 if (status < 0)
 {
        std::cerr << RTPGetErrorString(status) << std::endl;
        exit(-1);
 }

If the session was created with success, this is probably a good point to specify to which destinations RTP and RTCP data should be sent. This is done by a call to the RTPSession member function AddDestination. This function takes an argument of type RTPAddress. This is an abstract class and for the UDP over IPv4 transmitter the actual class to be used is RTPIPv4Address. Suppose that we want to send our data to a process running on the same host at port 9000, we can do the following:
           译:如果这个会话创建成功,这将是一个指定要发送的RTP和RTCP数据的目的地的好机会。可以通过调用RTPSession成员函数AddDestination来实现。这个函数拥有一个RTPAddress类型的参数。这是一个抽象的类,对于通过IPv4传输的UDP transmitter来说,真正使用的类是RTPIPv4Address。假设我们想要把数据发送给一个正在相同主机端口号为9000的正在运行的进程,我们向下面这样做:

uint8_t localip[]={127,0,0,1};
 RTPIPv4Address addr(localip,9000);

status = session.AddDestination(addr);
 if (status < 0)
 {
        std::cerr << RTPGetErrorString(status) << std::endl;
        exit(-1);
 }

If the library was compiled with JThread support, incoming data is processed in the background. If JThread support was not enabled at compile time or if you specified in the session parameters that no poll thread should be used, you'll have to call the RTPSession member function Poll regularly to process incoming data and to send RTCP data when necessary. For now, let's assume that we're working with the poll thread enabled.
          译:如果这个库(JRTPLIB)编译时有JThread的支持,那么进来的数据将在后台处理。如果编译时不支持JTread或者你指定会话参数不使用poll thread,那么你将不得不调用RTPSession成员函数Poll处理进来的数据并在必要时发送RTCP数据。现在,我们假设我们可以使用poll thread。

Lets suppose that for a duration of one minute, we want to send packets containing 20 ms (or 160 samples) of silence and we want to indicate when a packet from someone else has been received. Also suppose we have L8 data as defined in RFC 3551 and want to use payload type 96. First, we'll set some default values:
         译:假设一个一分钟的时间,我们想发送包含20ms(或者160个抽样点)的沉默并且想表明一个从别人来的包什么时候收到。同样假设我们有一个在RFC 3551 中定义的L8数据并使用类型为96的载荷。首先,我们将设定一些默认值:

 session.SetDefaultPayloadType(96);
 session.SetDefaultMark(false);
 session.SetDefaultTimestampIncrement(160);

         Next, we'll create the buffer which contains 160 silence samples and create an RTPTime instance which indicates 20 ms or 0.020 seconds. We'll also store the current time so we'll know when one minute has passed.

译:下面,我们将创建包含160个沉默抽样值的缓冲区,并创建一个表明20ms或0.020秒的RTPTime实例。我们将储存当前时间以便我们知道何时1分钟结束。

uint8_t silencebuffer[160];
 
 for (int i = 0 ; i < 160 ; i++)
        silencebuffer[i] = 128;
 
 RTPTime delay(0.020);
 RTPTime starttime = RTPTime::CurrentTime();

Next, the main loop will be shown. In this loop, a packet containing 160 bytes of payload data will be sent. Then, data handling can take place but this part is described later in the text. Finally, we'll wait 20 ms and check if sixty seconds have passed:

译:接下来,将展示主循环。在这个循环中,将发送一个包含160字节的包。接着,开始数据处理,但是这部分将会在本文以后来描述。最终,我们等待20ms并检查60秒是否已过。

bool done = false;
 while (!done)
 {
        status = session.SendPacket(silencebuffer,160);
        if (status < 0)
        {
                std::cerr << RTPGetErrorString(status) << std::endl;
                exit(-1);
        }
        
        //
        // Inspect incoming data here
        //
        
        RTPTime::Wait(delay);
        
        RTPTime t = RTPTime::CurrentTime();
        t -= starttime;
        if (t > RTPTime(60.0))
                done = true;
 }

Information about participants in the session, packet retrieval etc, has to be done between calls to the RTPSession member functions BeginDataAccess and EndDataAccess. This ensures that the background thread doesn't try to change the same data you're trying to access. We'll iterate over the participants using the GotoFirstSource and GotoNextSource member functions. Packets from the currently selected participant can be retrieved using the GetNextPacket member function which returns a pointer to an instance of the RTPPacket class. When you don't need the packet anymore, it has to be deleted. The processing of incoming data will then be as follows:
         译:会话中的参与者,包检索等信息需要调用RTPSession成员函数 BeginDataAccess和EndDataAccess。这确保了后台线程不会尝试改变你试图访问的相同数据。我们通过使用成员函数GotoFirstSource和GotoNextSource遍历会话参与者。来自当前选定的会话参与者的包通过使用成员函数GetNextPacket来检索,返回一个指向RTPPacket类实例的指针。当你不再需要这个包的时候,它不得不删除。传入数据的处理如下:

 session.BeginDataAccess();
 if (session.GotoFirstSource())
 {
        do
        {
                RTPPacket *packet;
                while ((packet = session.GetNextPacket()) != 0)
                {
                        std::cout << "Got packet with extended sequence number " 
                                  << packet->GetExtendedSequenceNumber() 
                                          << " from SSRC " << packet->GetSSRC() 
                                          << std::endl;
                        session.DeletePacket(packet);
                }
        } while (session.GotoNextSource());
 }
 session.EndDataAccess();

Information about the currently selected source can be obtained by using the GetCurrentSourceInfo member function of the RTPSession class. This function returns a pointer to an instance of RTPSourceData which contains all information about that source: sender reports from that source, receiver reports, SDES info etc.
          译:当前选定的源的信息可以通过使用RTPSession类的成员函数GetCurrentSourceInfo来获得。这个函数返回一个指向RTPSourceData实例的指针,这个实例包含了关于那个源的所有信息:来自这个源的发送报告,接收报告,SDES信息等。

When the main loop is finished, we'll send a BYE packet to inform other participants of our departure and clean up the RTPSession class. Also, we want to wait at most 10 seconds for the BYE packet to be sent, otherwise we'll just leave the session without sending a BYE packet.

译:当这个主循环完成后,我们将发送一个BYE包通知其他会话参与者清除RTPSession类。同样地,我们只想最多10秒等待即将发送的BYE包,否则我们将不发送BYE包就离开这个会话。

 delay = RTPTime(10.0);
 session.BYEDestroy(delay,"Time's up",9);

The complete code of the program is given in example2.cpp.

译:这个程序的完整代码在example2.cpp中给出。

Error codes

Unless specified otherwise, functions with a return type int will return a negative value when an error occurred and zero or a positive value upon success. A description of the error code can be obtained by using the RTPGetErrorString function, declared in rtperrors.h
           译:除非指定,否则,当发生错误时,带有整型返回值的函数将会返回一个负值,零和正值表示成功。错误代码的描述可以通过使用在rtperrors.h中声明的RTPGetErrorString函数来获得。

Memory management(内存管理)

You can write you own memory manager by deriving a class from RTPMemoryManager. The following example shows a very basic implementation.
               译:你可以通过派生RTPMemoryManager类来写自己的内存管理。下面的例子展示了一个非常基本的实现。

 class MyMemoryManager : public RTPMemoryManager
 {
 public:
        MyMemoryManager() { }
        ~MyMemoryManager() { }
        
        void *AllocateBuffer(size_t numbytes, int memtype)
        {
                return malloc(numbytes);
        }

void FreeBuffer(void *p)
        {
                free(p);
        }
 };

           In the constructor of RTPSession, you can specify that you would like to use this memory manager:
              译:在RTPSession的构造函数中,你可以指定你将会使用这个内存管理。

MyMemoryManager mgr;
 RTPSession session(0, &mgr);

           Now, all memory allocation and deallocation will be done using the AllocateBuffer and FreeBuffer implementations of mgr.
              译:现在,所有的内存分配和释放都将通过使用mgr中的AllocateBuffer 和 FreeBuffer实现来完成。 
 
          The second parameter of the RTPMemoryManager::AllocateBuffer member function indicates what the purpose is of this memory block. This allows you to handle different kinds of data in different ways.
             译:RTPMemoryManager::AllocateBuffer成员函数的第二个参数表明了这个内存块的目的是什么。这允许你以不同的方式处理不同类型的数据。
        
          With the introduction of the memory management system, the RTPSession class was extended with member function RTPSession::DeletePacket and RTPSession::DeleteTransmissionInfo. These functions should be used to deallocate RTPPacket instances and RTPTransmissionInfo instances respectively.
            译:由于内存管理系统的引进,RTPSession类扩展了成员函数RTPSession::DeletePacket 和 RTPSession::DeleteTransmissionInfo。这些成员函数分别被用于释放RTPPacket实例和 RTPTransmissionInfo 实例。

Contact

If you have any questions, remarks or requests about the library or if you think you've discovered a bug, you can contact me at jori(dot)liesenborgs(at)gmail(dot)com

The home page of the library is http://research.edm.uhasselt.be/jori/jrtplib/jrtplib.html

There is also a mailing list for the library. To subscribe to the list, send an e-mail to jrtplib-subscribe(at)edm(dot)uhasselt(dot)be and you'll receive further instructions.

译:如果关于图书馆你有任何问题,言论或要求,或者你认为你已经发现了一个错误的,你可以联系我的JORI.liesenborgs@Gmail.COM

库的主页http://research.edm.uhasselt.be/jori/jrtplib/jrtplib.html

还有库的邮件列表。要订阅到列表中,请发送电子邮件 jrtplib-subscribe@edm.uhasselt.be,您会收到进一步的说明。

=============================================================================================================

RTCP中的ntp时间戳

转自 http://blog.sina.com.cn/s/blog_45021d9a0101ejs8.html

RTP支持传送不同codec的steaming,不同codec的clock rate的也不一样,不同的media之间需要依靠RTCP进行同步。这里简单介绍一下他们的机制。

在每个RTCP SR包中对应有一个RTP时间和一个NTP时间,它表达的意思很明确,那就是这个RTP时间对应的绝对时间, 不同media的RTP时间尽管不同,但可以通过NTP时间映射到同一个时间轴上,从而实现同步。

如下图所示,RTP session 1 send H264 使用90,000HZ,而RTP session 2 send G.711 使用8,000HZ:

也就是是说有3个时间轴,音频时间轴,视频时间轴,ntp时间轴。

音视频的时间轴的单位都是各自的采样率,需要除以采样率才能取得单位为秒的时间单位。

有两个rtcp流,分别为音/视频的,其中有一个当前的音频的timestamp和一个ntp的timestamp。这两个值是在不同轴上的相同时间点,即音/视频轴和ntp轴的重合点。使用这个值可以使音视频轴同步。

当拿到音频NTP时间 (Tan),音频RTP时间(Tar),视频NTP时间(Tvn),视频RTP时间(Tvr),就可以计算音视频时间轴的差距D:

假设使用音频为主轴,视频向音频对齐。D = (Tar-Tvr) - (Tan - Tvn);

新的视频时间戳为Tar = Tar + D;

在rtcp的sr单元中有32位的MSW和32位的LSW。MSW的单位为妙,而LSW的单位为232 picoseconds。1皮秒为1/10^12秒。LSW转为us的公式为(LSW*10^12/2^32)/1000000;

MSW的起始时间点为1900年1月1日00:00,要转换为linux时间的话需要减去从1900年1月1日到1970年1月1日的时间差:

time_t lt;
lt = MSW;
lt -= JAN_1970;(0x83aa7e80 )
char* tmpTimeString = ctime(<);

记得两年前刚开始做RTP/RTCP的时候碰到一个问题,是关于如何计算RTCP中的NTP时间戳,最近又有人问这个问题,于是就想把它贴出来,让大家参考,提提建议,交流促进进步。

记得当时有个客户说用openRTSP(open source ,you can get it from www.live555.com)无法录制我们送出去的RTP流,于是我也去下了一个,试了发现果然不行,于是就把openRTSP的source code捞出来看看,最后发现它必须要收到RTCP包后才开始录制视频,于是我就加了RTCP,结果发现视频录制是没问题,但用VLC播放的时候老是抖动,于是回后去找原因,一个排下来,最后focus到NTP时间戳上来了。

NTP的时间戳有MSW和LSW组成, MSW好算,以秒为单位,LSW就头痛了,查了RTP的文档,讲得很模糊,NTP(RFC1305)中只讲单位大约是200 picoseconds,但我试了用200 picoseconds为单位不行,还是闪。

没办法了,之后去研究Darwin Streaming Server,看看人家是怎么做了,抓了包,找了好几个RTCP的点,画了个数轴,因为抓包工具wireshark会显示NTP时间(如下图),于是我就倒过去算,最后算出来单位大约是232 picoseconds, 把这个值代入到我的source code中,果然不闪了。

问题虽然解决了,但心里一直有个结,就是一直不知道232这个值是怎么来的,纠结啊。 只好回去再看RFC1305, 它只说单位大约是200 picoseconds, 而1 second = 1,000,000,000,000 picoseconds, 这个值貌似有点大啊,而232=4294967296,很明显用32bits无法精确到1 picoseconds, 于是我就想到不能精确到1 picoseconds, 那也应该尽力而为之吧,于是自然就有了把1,000,000,000,000 picoseconds劈成232份:

1,000,000,000,000/4294967296 = 232.83064365386962890625

That's it!!

现在想想其实有更快捷的方法,直接看VLC的source code就可以了:

uint64_t NTPtime64 (void)
{struct timespec ts;
#if defined (CLOCK_REALTIME)clock_gettime (CLOCK_REALTIME, &ts);
#else{struct timeval tv;gettimeofday (&tv, NULL);ts.tv_sec = tv.tv_sec;ts.tv_nsec = tv.tv_usec * 1000;}
#endifuint64_t t = (uint64_t)(ts.tv_nsec) << 32;t /= 1000000000;assert (t < 0x100000000);t |= ((70LL * 365 + 17) * 24 * 60 * 60 + ts.tv_sec) << 32;return t;
}

RTP/RTCP工程实践与问题解决方案(合集)相关推荐

  1. 新书推荐 |《区块链工程实践:行业解决方案与关键技术》

    新书推荐 <区块链工程实践:行业解决方案与关键技术> 点击上图了解及购买 远光软件区块链首席科学家5年项目经验总结,通过5个工程案例给出设计整体解决方案的系统方法,6位专家推荐. 编辑推荐 ...

  2. Oracle Ora 错误解决方案合集

    Oracle Ora 错误解决方案合集 参考文章: (1)Oracle Ora 错误解决方案合集 (2)https://www.cnblogs.com/ios9/p/8627643.html 备忘一下 ...

  3. 解决WIN7有限的访问权限的终极解决方案合集

    碰到同样问题的朋友们有福了,本人技术一般,研究了很久,网上也查了很多资料,看到很多人都没碰到这个问题,也提了一下解决办法, 但我都用不上,于是自己研究了半天,终于找到解决办法! 版本: windows ...

  4. 【推荐】智慧检察公益诉讼辅助快检AI人工智能大数据平台解决方案合集(共183份,928M)

    [推荐]智慧检察公益诉讼辅助快检AI人工智能大数据平台解决方案,检务保障系统,整体解决方案合集,公益诉讼方案,可视化检察管理,概要详细设计交付验收模板. 下载地址:https://download.c ...

  5. 【推荐】智慧油田数字化油井智能入侵监测井口控制系统解决方案合集(共83份,884M)

    [推荐]智慧油田数字化油井智能入侵监测井口控制系统解决方案,标准规范指南,解决方案合集,数字化油田,视频宣传资料,数字油田国际技术交流会. 下载地址:https://download.csdn.net ...

  6. 【推荐】AI智慧安监企业安全生产监督管理平台建设技术解决方案合集(共342份,863M)

    [推荐]AI智慧安监企业安全生产监督管理平台建设技术解决方案,行业背景资料,整体解决方案合集,标准规范指南,售前宣贯方案PPT,概要详细设计交付模板. 下载地址:https://download.cs ...

  7. 智慧检察院公益诉讼云平台解决方案-合集

    智慧检察院公益诉讼云平台解决方案-合集 前言 一.公益诉讼云平台建设内容 二.建设规划 三.功能特点 四.获取智慧检察全套解决方案-大合集 前言 检察公益诉讼,是指人民检察院作为公共利益的代表,根据民 ...

  8. 【推荐】平安乡村乡镇视频监控云平台社会治安综合管治理系统解决方案合集(共43份,302M)

    [推荐]平安乡村乡镇视频监控云平台社会治安综合管治理系统解决方案,标准规范指南,解决方案合集,招投标相关,售前宣贯方案PPT,平安乡村社会治安视频监控系统建设. 下载地址:https://downlo ...

  9. 【推荐】智慧电力智能化巡检在线监控信息化平台系统建设推荐解决方案合集(共60份,363M)

    [推荐]智慧电力智能化巡检在线监控信息化平台系统建设推荐解决方案,标准规范指南,解决方案合集,发展白皮书,产品视频演示,行业报告及发展研究相关. 下载地址:https://download.csdn. ...

最新文章

  1. Spring Boot 中的 RestTemplate不好用?试试 Retrofit !
  2. 彻底搞懂浏览器Event-loop
  3. H3C LMI协议标准
  4. UI组件-UISlider
  5. 浅谈android hook技术
  6. 无线路由器的配置实例
  7. 祝福!微软 46 周年生日快乐!
  8. 算法设计与分析———动态规划———最大子段和
  9. LeetCode 53. 最大子序和(动态规划)
  10. 【笔试/面试】—— 二叉树的最远距离
  11. [Hbase]Hbase常用的优化方法
  12. Servlet — 线程安全问题
  13. 面向对象——三大特性(封装、继承、多态)
  14. Scala基本类型及操作、程序控制结构
  15. 基于C语言开发的教师管理系统
  16. Tesseract调用日文识别模型
  17. 定性和定量大数据分析方法指南
  18. Styler类的变量
  19. xv6操作系统中增加一个系统调用
  20. 为什么架构师工资比运维高?

热门文章

  1. Superset系列8- 制作饼图
  2. Dreaming to Distill: Data-free Knowledge Transfer via DeepInversion
  3. Python全栈(五)Web安全攻防之2.信息收集和sqlmap介绍
  4. iTextSharp,将多张图片合并生成PDF文件
  5. 米转经纬度_经纬度换算米(经纬度精度换算米数)
  6. 从零开始用 Windows C++ 桌面程序制作方舟同人游戏(一)
  7. 华为 面试 c语言 编程题,传说中华为的面试编程题-php 创建ecs-WinFrom控件库|.net开源控件库|HZHControls官网...
  8. 如果黑客转行干活动策划,我再也不怕开会睡着了
  9. js 常用数组操作的方法
  10. BUCT数据结构——图(拓扑排序、关键路径)