老司机种菜


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 公益404

  • 搜索

Webrtc线程模型

发表于 2017-07-23 | 分类于 webrtc

webrtc的base的 thread,是我见过的封装最帅的c++线程库,根据比qt的还好用,发个例子给你

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 using namespace webrtc;
using namespace rtc;
//std::cout<<"Thread::Current():" << Thread::Current()->GetId();
//Thread::Current()->Start(); 不能调用start,因为不是我创建的,他已经开始了
//Thread::Current()->Run(); //阻塞当前线程进入线程循环

Thread * thread = new Thread();
//MyRunnable run;
//thread->Start(&run);//可以带一个Runnable参数运行,运行完就结束,否则运行Thread::Run进入消息循环
thread->Start();
//std::cout << "Thread::Invoke():"<< thread->Invoke<bool>(RTC_FROM_HERE, &task)<< " at " << Thread::Current()->GetId() << std::endl;
thread->Post(RTC_FROM_HERE, Bind(task2));//将最常用的
auto handler= new MessageClient;
//thread->PostAt(RTC_FROM_HERE, (int64_t)3000,handler);
//thread->PostDelayed(RTC_FROM_HERE, (int64_t)5000, handler);
//thread->Stop();


Thread * thread2 = new Thread();
thread2->Start();
thread2->Post(RTC_FROM_HERE, Bind(task2));//将最常用的
//thread2->Invoke() 非常有用,在任何地方可以指定我的代码运行在某个线程

//api下的proxy机制,实际上就是设置要执行的线程,然后加锁等待线程执行结果。这是我设计对外接口可以在任何线程调用而不出错的常用方法

//base的asyncinvoker与proxy类似的机制。

有ios的gdc,android的handler异曲同工

因为编写复杂稳定的多线程C++项目实在太难,所以一个好的跨平台C++基础库是我最求的目标,目前比较欣赏的项目有:

Boost:大而全,缺少一些可以直接上手的东西如线程消息队列,智能指针并非线程安全。 QT core:非常好 C++11:也需要线程消息队列,线程安全智能指针。 chromium的base库:太大了 当我看到webrtc的base时,非常惊讶的发现它正是我想要的,特点:

小:只有几M 纯:基于c++标准库和各操作系统sdk 跨平台 对智能指针、线程、socket封装非常好。 不断更新(需要一直跟踪官方代码) 移植出来单独使用,方案有三:

把源码拷贝出来用通用的编译工具(makefile,cmake,qmake)管理。(makefile较复杂,cmake简单,qmake最简单) 把源码拷贝出来用基于自带的gn管理 在webrtc项目里面编译和合并需要的静态库和pdb

因为google官方说了:引用计数+引用计数的智能化(scoped_ref_ptr)+弱引用就可以解决问题。 shared_ptr不是线程安全的,因为shared_ptr有两个成员:引用计数,和源对象指针。没办法对两个成员同时实现原子操作。 但unique_ptr是个好东西

智能指针的使用:

  1. 不用再使用delete。
  2. 尽量使用unique_ptr。
  3. 多个线程读写同一个 shared_ptr 对象,那么需要加锁。
  4. shared_ptr 和weak_ptr配合解决循环引用的问题。

weak_ptr必须,oc,swift的ViewControler和控件都是weak关系

内存管理模型的三种级别: 1 手动内存管理(c/c++的malloc与free,new与delete):容易出错。 2 自动内存管理(oc的arc,c++的智能指针,scoped_ptr):存在循环引用问题,通过程序员自己管理强弱引用关系解决。 3 垃圾回收机制(如java,python):后台GC降低了程序效率,好的程序员仍然好考虑java的强引用[表情]引用/软引用/

3 线程模型 1 生产者消费模型(mutex,condition):最最常用的模型。 2 线程池模型:解决大量请求分配太多线程的问题。比如一个android和ios的app,http请求会很多很多。 3 (着重强调)串行模型:ios有GCD(Grand Central Dispatch,global queue是线程池),android有looper, win32有PostMessage,boost有strand 读写锁:特别只有写才会不安全的情况。 再结合其他的手段会让程序简洁优美易读:java的handler,oc的delegate和block、swift的闭包,mvc模式 ,c++的function/bind/lambda,python和javascript的function

而串行模型就成了解决这类多线程问题的首选,就是线程消息模型。 在android 系统里面,无数这样的例子。

模块处理线程

Call构造方法中创建module_process_thread与pacer_thread两个ProcessThread.接着为module_process_thread注册CallStats, ReceiveSideCongestionController, SendSideCongestionController模块,为pacer_thread注册PacedSender, RemoteBitrateEstimator模块.

Call::CreateVideoSendStream创建VideoSendStream时,将module_process_thread做构造参数传入,调用RegisterProcessThread方法,注册所有的rtc_rtcp模块到module_process_thread线程.同样的为VideoReceiveStream中设置.

AndroidStudio常见问题

发表于 2017-07-23 | 分类于 Android

如何解决Unsupported major.minor version 52.0问题?

http://www.jianshu.com/p/5eebd3c609d6

运行./gradlew :PandaAndroidDemo:release出现如下错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
FAILURE: Build failed with an exception.

* Where:
Build file '/Users/shitianci/work/Lab/panda.android/PandaAndroidDemo/build.gradle' line: 1

* What went wrong:
A problem occurred evaluating project ':PandaAndroidDemo'.
> java.lang.UnsupportedClassVersionError: com/android/build/gradle/AppPlugin : Unsupported major.minor version 52.0

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

直接点击 run按钮 或者 Build→Generate Build APK 却运行正常。

这里面有两个问题:

为什么出现Unsupported major.minor version 52.0? 为什么gradle命令和android studio按钮运行结果不一样?

问题一:为什么出现Unsupported major.minor version 52.0?

在网上找了一圈,最后在stackoverflow找到了本质原因

1
2
3
4
5
You get this error because a Java 7 VM tries to load a class compiled for Java 8

Java 8 has the class file version 52.0 but a Java 7 VM can only load class files up to version 51.0

In your case the Java 7 VM is your gradle build and the class is com.android.build.gradle.AppPlugin

简单来说,就是java的编译环境版本太低,java 8 class file的版本是52,Java 7虚拟机只能支持到51。所以需要升级到java 8 vm才行。

问题二:为什么gradle命令和android studio按钮运行结果不一样?

从问题1来看,肯定Android Studio按钮调用的是java 8 vm,所以查找一下系统配置,最终在Project Structure找到了如下设置: Android Studio 2.2.2使用了自带的JDK环境,其地址为

1
/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home

而gradle命令的执行环境是在gradle.properties配置的,其指向为:

1
org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk1.7.0_71.jdk/Contents/home

将其修改为:

1
org.gradle.java.home=/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home

INSTALL_PARSE_FAILED_NO_CERTIFICATES安装问题

Android studio 更新到25后打包问题,打包后的应用安装提示:INSTALL_PARSE_FAILED_NO_CERTIFICATES Android N 引入一项新的应用签名方案 APK Signature Scheme v2,它能提供更快的应用安装时间和更多针对未授权

APK 文件更改的保护。 在默认情况下,Android Studio 2.2 和 Android Gradle 2.2 插件会使用 APK

Signature Scheme v2 和传统签名方案来签署您的应用。

脏的解决方式:使用v1打包

INSTALL FAILED CONFLICTING PROVIDER问题完美解决方案

在安装Android应用时出现INSTALL FAILED CONFLICTING PROVIDER问题,是不是感觉很抓狂呢,下面就跟大家分享一下出现这个问题的原因及解决方案。 问题原因: 在Android中authority要求必须是唯一的,比如你在定义一个provider时需要为它指定一个唯一的authority。如果你在安装一个带有provider的应用时,系统会检查当前已安装应用的authority是否和你要安装应用的authority相同,如果相同则会弹出上述警告,并且安装失败。 解决方案 在定义provider是,使用软编码的形式,如下:

1
2
3
4
5
6
7
8
9
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>

上述代码中通过${applicationId}.fileprovider的形式来指定provider的authorities,所以该provider的authorities会根据applicationId的不同而不同,从而避免了authorities的冲突问题。 那么如何使用刚才定义的authorities呢? 我们在定义authorities是采用了applicationId+fileprovider的形式,在获取authorities的时候,我们就可以通过包名+fileprovider来获取,代码如下:

1
2
3
public final static String getFileProviderName(Context context){
return context.getPackageName()+".fileprovider";
}

RFC3550 RTP中文版

发表于 2017-07-22 | 分类于 RFC

RFC3550

RTP:实时应用程序传输协议

摘要 本文描述RTP(real-time transport protocol),实时传输协议。RTP在多点传送(多播)或单点传送(单播)的网络服务上,提供端对端的网络传输功能,适合应用程序传输实时数据,如:音频,视频或者仿真数据。RTP没有为实时服务提供资源预留的功能,也不能保证QoS(服务质量)。数据传输功能由一个控制协议(RTCP)来扩展,通过扩展,可以用一种方式对数据传输进行监测控制,该协议(RTCP)可以升级到大型的多点传送(多播)网络,并提供最小限度的控制和鉴别功能。RTP和RTCP被设计成和下面的传输层和网络层无关。协议支持RTP标准的转换器和混合器的使用。 本文的大多数内容和旧版的RFC1889相同。在线路里传输的数据包格式没有改变,唯一的改变是使用协议的规则和控制算法。为了最小化传输,发送RTCP数据包时超过了设定的速率,而在这时,很多的参与者同时加入了一个会话,在这样的情况下,一个新加入到(用于计算的可升级的)计时器算法中的元素是最大的改变。

目录(Table of Contents) 1 引言 (Introduction) 1 1 术语(Terminology) 2 RTP使用场景(RTP Use Scenarios) 2 1 简单多播音频会议( Simple Multicast Audio Conference) 2 2 音频和视频会议(Audio and Video Conference) 2 3 混频器和转换器(Mixers and Translators) 2 4 分层编码(Layered Encodings) 3 定义(Definitions) 4 字节序,校正和时间格式(Byte Order, Alignment, and Time Format) 5 RTP数据传输协议(RTP Data Transfer Protocol) 5 1 RTP固定头域(RTP Fixed Header Fields) 5 2 多路复用RTP会话(Multiplexing RTP Sessions) 5 3 RTP头的配置文件详细变更(Profile-Specific Modifications to the RTP Header) 5 3 1 RTP报头扩展(RTP Header Extension)
6 RTP控制协议(RTP Control Protocol) – RTCP
6 1 RTCP包格式(RTCP Packet Format)
6 2 RTCP传输间隔(RTCP Transmission Interval) 6 2 1 维护会话成员数目(Maintaining the number of session members) 6 3 RTCP包的发送与接收规则(RTCP Packet Send and Receive Rules) 6 3 1 计算RTCP传输间隔(Computing the RTCP Transmission Interval) 6 3 2 初始化(Initialization) 6 3 3 接收RTP或RTCP(非BYE)包(Receiving an RTP or Non-BYE RTCP Packet) 6 3 4 接收RTCP(BYE)包(Receiving an RTCP BYE Packet) 6 3 5 SSRC计时失效(Timing Out an SSRC) 6 3 6 关于传输计时器的到期(Expiration of Transmission Timer) 6 3 7 传输一个 BYE 包(Transmitting a BYE Packet) 6 3 8 更新we_sent(Updating we_sent) 6 3 9 分配源描述带宽(Allocation of Source Description Bandwidth) 6 4 发送方和接收方报告(Sender and Receiver Reports) 6 4 1 SR:发送方报告的RTCP包(SR: Sender report RTCP packet)
6 4 2 RR:接收方报告的RTCP包(RR: Receiver Report RTCP Packet) 6 4 3 扩展发送方和接收方报告(Extending the Sender and Receiver Reports )
6 4 4 分析发送方和接收方报告(Analyzing Sender and Receiver Reports )
6 5 SDES:源描述RTCP包(SDES: Source description RTCP packet) 6 5 1 CNAME:规范终端标识符的SDES数据项(CNAME: Canonical End-Point Identifier SDES Item)
6 5 2 NAME:用户名的SDES数据项(NAME: User name SDES item)
6 5 3 EMAIL:电子邮件地址的SDES数据项(EMAIL: Electronic Mail Address SDES Item) 6 5 4 PHONE:电话号码的SDES数据项(PHONE: Phone Number SDES Item) 6 5 5 LOC:地理用户地址的SDES数据项(LOC: Geographic User Location SDES Item) 6 5 6 TOOL:应用程序或工具名字的SDES数据项(TOOL: Application or Tool Name SDES Item) 6 5 7 NOTE:通知/状态的SDES数据项(NOTE: Notice/Status SDES Item) 6 5 8 PRIV:私有扩展的SDES数据项(PRIV: Private Extensions SDES Item) 6 6 BYE:Goodbye RTCP包(BYE: Goodbye RTCP packet) 6 7 APP:定义应用程序的RTCP包(APP: Application-Defined RTCP Packet) 7 RTP转换器和混频器(RTP Translators and Mixers) 7 1 概述(General Description ) 7 2 在转换器中的RTCP数据处理(RTCP Processing in Translators) 7 3 在混频器中的RTCP数据处理(RTCP Processing in Mixers ) 7 4 级联混频器(Cascaded Mixers) 8 SSRC标识符的分配和使用(SSRC Identifier Allocation and Use) 8 1 冲突概率(Probability of Collision ) 8 2 冲突解决和循环检测(Collision Resolution and Loop Detection) 8 3 在分层编码中使用(Use with Layered Encodings) 9 安全(Security ) 9 1 机密性(Confidentiality) 9 2 身份验证和消息完整性(Authentication and Message Integrity) 10 拥塞控制(Congestion Control) 11 网络和传输协议之上的RTP(RTP over Network and Transport Protocols) 12 协议常量摘要(Summary of Protocol Constants) 12 1 RTCP 包类型(RTCP Packet Types) 12 2 SDES 类型(SDES Types) 13 RTP概况和负载格式详细说明     (RTP Profiles and Payload Format Specifications) 14 安全考虑(Security Considerations) 15 IANA考虑(IANA Considerations) 16 知识产权声明(Intellectual Property Rights Statement) 17 鸣谢(Acknowledgments) 附录 A 算法(Algorithms) 附录 A 1 RTP数据头有效性检查(RTP Data Header Validity Checks ) 附录 A 2 RTCP数据头有效性检查(RTCP Header Validity Checks) 附录 A 3 确定RTP包预期数目和丢失数目(Determining Number of Packets Expected and Lost) 附录 A 4 生成SDES RTCP包(Generating RTCP SDES Packets) 附录 A 5 解析RTCP SDES包(Parsing RTCP SDES Packets) 附录 A 6 生成32位随机标识符(Generating a Random 32-bit Identifier 附录 A 7 计算RTCP传输间隔(Computing the RTCP Transmission Interval) 附录 A 8 估测两次到达间隔的抖动(Estimating the Interarrival Jitter) 附录 B 与RFC1889不同之外(Changes from RFC 1889)
参考书目(References)
标准化引用(Normative References )
资料性引用(Informative References) 作者地址
完整的版权声明

1.绪论 本文详细的介绍实时传输协议RTP,RTP提供带有实时特性的端对端数据传输服务,传输的数据如:交互式的音频和视频。那些服务包括有效载荷类型定义,序列号,时间戳和传输监测控制。应用程序在UDP上运行RTP来使用它的多路技术和checksum服务。2种协议都提供传输协议的部分功能。不过,RTP可能被其他适当的下层网络和传输协议使用(见11节)。如果下层网络支持,RTP支持数据使用多播分发机制转发到多个目的地。 注意RTP本身没有提供任何的机制来确保实时的传输或其他的服务质量保证,而是由低层的服务来完成。它不保证传输或防止乱序传输,它不假定下层网络是否可靠,是否按顺序传送数据包。RTP包含的序列号允许接受方重构发送方的数据包顺序,但序列号也用来确定一个数据包的正确位置,例如,在视频解码的时候不用按顺序的对数据包进行解码。 但是RTP原先的设计是用来满足多参与者的多媒体会议的需要,它没有限定于专门的应用。连续数据的储存,交互分布式仿真,动态标记,以及控制和测量应用程序也可能会适合使用RTP。 该文档定义RTP,由2个密切联系的部分组成: ○实时传输协议RTP,用于实时传输数据。 ○RTP控制协议RTCP,用于监控服务质量和传达关于在一个正在进行的会议中的参与者的信息。后者对“宽松控制”的会议可能已经足够,但是并没有必要去支持一个应用程序所有的通讯控制条件。这个功能可能充分的或者部分的被一个单独的会议控制协议所包含,这超过了本文档的范围。 RTP表现了协议的一种新的类型,该类型由Clark和Tennenhouse提出[10],遵循应用级(framing)框架和(integrated layer processing)统一层处理的原则。就是说,RTP被规定为可扩展的,用来提供一个专门的应用程序需要的信息,并将会经常性的被归并到应用程序的处理中,而不是作为一个单独的层被实现。RTP只是一个故意不完成的协议框架。本文档详细说明那些功能,希望这些功能能够普遍贯穿于所有适合使用RTP的应用程序。和常规的协议不同,额外的功能可能通过完善协议本身或者增加一个可能需要分析的选项机制来增加,RTP被规定为可以根据需要通过修改和/或增加操作,“剪裁”到报头。具体的例子见5.3和6.4.3节。 因此,除了本文档,用于专门应用程序的RTP完整的说明将还需要一个或者更多的同类文档(见13节): ○ 一个框架(大致轮廓)的说明文档,该文档定义了一系列的有效载荷类型编码和它们与有效载荷格式之间的映射(例如,媒体编码)。一个框架可能也定义了应用程序对RTP的一些扩展和修改,详细到一个专门的类。典型的情况,一个应用程序将在一个框架下运行。一个用于音频和视频数据的框架可以在同类RFC3551[1]文档里找到。 ○有效载荷格式说明文档,该文档定义了一个像一个音频或者视频编码的特殊载荷,在RTP里是如何被传输的。 一个关于实时服务和算法如何实现的讨论和关于一些RTP设计结果的后台讨论能够在[11]中找到。 1.1术语 在这个文档里的关键词“一定要”,“一定不能”,“必需的”,“会”,“不会”,“应该”,“不应该”,“推荐”,“可能”和“可选” 将会像在BCP 14(Basic Control Program,基本控制程序),RFC2119[2]里描述一样的解释。并指出适合RTP实现的需要的级别。

2 RTP使用场景(RTP Use Scenarios) 2.1 简单多播音频会议( Simple Multicast Audio Conference) 2.2 音频和视频会议(Audio and Video Conference) 2.3 混频器和转换器(Mixers and Translators) 2.4 分层编码(Layered Encodings)   以下章节描述了用到RTP的一些方面。所举例子用来说明RTP应用的基本操作,但RTP的用途不限于此。在这些例子中,RTP运行于IP和UDP之上,并且遵循RFC3551所描述的音频和视频的配置文件中的约定。 2.1 简单多播音频会议(Simple Multicast Audio Conference)   IETF的一个工作组开会讨论最新协议草案时,使用Internet的IP多播服务来进行语音通讯。工作组中心分配到一个多播的组地址和一对端口。一个端口用于音频数据,另一个端口用于控制(RTCP)数据包。该地址和端口信息发布给预定的参与者。如果有私密性要求,则可用章节9.1中说明的方法,对数据和控制包进行加密,这时就需要生成和发布加密密钥。分配和发布机制的精确细节不在RTP的讨论范围之内。   每个与会者所使用的音频会议应用程序,都以小块形式(比方说持续20秒时间)来发送音频数据。每个音频数据块都前导RTP报头;RTP报头和数据依次包含在UDP包里。RTP报头指明了各个包里音频编码的类型(如PCM,ADPCM,LPC),这样发送方可以在会议过程中改变编码方式,以适应状况的变化,例如,要加进一个低带宽接入的参与者,或是要应付网络拥塞。   Internet,像其他的报文分组网络一样,偶而会丢失和重排包,造成时长不等的延迟。为弥补这个不足,RTP报头里包含计时信息和一个序列号,允许接收方重建来自源的计时信息,比如前文例子中音频块以20s的间隔在扬声器中连续播放。会议中,对每个RTP包的源,单独地实施计时重建。序列号还被接收方用来评估丢失包数目。   由于会议期间不断有工作组成员加入或离开,因此有必要知道任一时刻的实际参与者及他们接收音频数据的状况好坏。出于这个目的,会议中每个音频应用程序的实例,都在RTCP(控制)端口上周期性地多播一个附加用户名的接收报告。接收报告指明了当前说话者被收听到的状况,可用于控制自适应性编码。除了用户名外,通过控制带宽限度,可以包含其他标识信息。一个站点在离开会议时发送RTCP BYE包(章节6.5)。 2.2 音频和视频会议(Audio and Video Conference)   一个会议如果同时使用音频和视频媒体,则二者传输时使用不同的RTP会话。也就是说,两种媒体中RTP包和RTCP包的传输,是使用两个不同的UDP端口对和(或)多播地址。在RTP层次,音频和视频会话没有直接的耦合,下面这种情况除外:一个同时参加两个会话的参与者,在两个会话的RTCP包中,使用了相同的规范名,这样两个会话就发生关联(耦合)了。   这样区隔开来的目的之一,是允许一些会议参与者只接受自己选择的单一媒体(或者音频,或者视频)。更进一步的说明在章节5.2给出。尽管两种媒体区分开来了,但通过两个会话RTCP包内载有的计时信息,同源的音频与视频还是能够同步回放。 2.3 混频器和转换器(Mixers and Translators)   到目前为止,我们皆假设所有站点都收到相同格式的媒体数据。然而这并不总是行得通。考虑一下这种情况,一个地方的参与者只能低速接入会议,而其他大部分参与者都能享受高速连接。与其让强迫大家都忍受低带宽,不如在只能低速接入的地方,放置一个减质量音频编码的RTP层次的中继(称作混频器)。混频器将重新同步输入的音频包,重建发送方产生的20ms固定间隔,混频已重建过的音频流为单一的流,转换音频编码为低带宽格式,最后通过低带宽连接转发数据包流(package stream)。这些包可能被单播到一个接收方,也可能多播到另一个的地址而发给多个接收方。RTP报头为混频器提供了一种方法,使其能辨识出对混频后的包有用的源,从而保证提供给接收方正确的说话者指示。   在音频会议中,一些预定参与者尽管有高带宽连接,但不能通过IP多播直接接入会议。例如,他们可能位于一个不允许任何IP包通过的应用层防火墙后面。对这些站点,可能就不需要混频,而需要另一种称为转换器的RTP层次中继。可以在防火墙两侧分别安装一个转换器,外侧转换器将所有多播包通过安全连接转入内侧转换器,内侧转换器再转发给内部网的一个多播组(multicast group)。   混频器和转换器可以设计成用于各种目的。比如,一个视频混频器在测量多个不同视频流中各人的单独影像后,将它们组合成一个单一视频流来模拟群组场景。又如,在只用IP/UDP和只用ST_II的两个主机群之间通过转换建立连接。再如,在没有重新同步或混频时,用packet-by-packet编码转换来自各个独立源的视频流。混频器和转换器的操作细节见章节7。 2.4 分层编码(Layered Encodings)   为了匹配接收方的能力(容量)以及适应网络拥塞,多媒体应用程序应当能够调整其传输速率。许多应用实现把调适传输速率的责任放在源端。这种做法在多播传输中并不好,因为不同接收方对带宽存在着冲突性需求。这经常导致最小公分母的场景,网格中最小的管道支配了全部实况多媒体“广播”的质量和保真度。   相反地,可以把分层编码和分层传输系统组合起来,从而把调适速率的责任放在接收端。在IP多播之上的RTP上下文中,对一个横跨多个RTP会话(每个会话在独自多播组上开展)的分级表示信号(a hierarchically represented signal),源能够把它的分层(layers)分割成条。 接收方仅需合并适当的多播组子集,就能适应异种网络和控制接收带宽。 RTP分层编码的细节在章节6.3.9,8.3和11中给出。

3. 定义(definitions)   RTP负载(RTP payload):通过RTP传输的包中的数据,例如,音频样本或压缩好的视频数据。负载格式与解释不在本文讨论范围。   RTP包(RTP packet):一种数据包,其组成部分有:一个固定RTP报头,一个可能为空的作用源(contributing sources)列表(见下文),以及负载数据。一些下层协议可能要求对RTP包的封装进行定义。一般地,下层协议的一个包包含一个RTP包,但若封装方法允许,也可包含数个RTP包(见章节11)。    RTCP包(RTCP packet):一种控制包,其组成部分有:一个类似RTP包的固定报头,后跟一个结构化的部分,该部分具体元素依不同RTCP包的类型而定。格式的定义见章节6。一般地,多个RTCP包将在一个下层协议的包中以合成RTCP包的形式传输;这依靠RTCP包的固定报头中的长度字段来实现。   端口(Port):“传输协议用来在同一主机中区分不同目的地的一种抽象。TCP/IP协议使用正整数来标识不同端口。”[12] OSI传输层使用的传输选择器(TSEL,the transport selectors)等同于这里的端口。RTP需依靠低层协议提供的多种机制,如“端口”用以多路复用会话中的RTP和RTCP包。   传输地址(Transport address):是网络地址与端口的结合,用来标识一个传输层次的终端,例如一个IP地址与一个UDP端口。包是从源传输地址发送到目的传输地址。   RTP媒体类型(RTP media type):一个RTP媒体类型是一个单独RTP会话所载有的负载类型的集合。RTP配置文件把RTP媒体类型指派给RTP负载类型。   多媒体会话(Multimedia session):在一个参与者公共组中,并发的RTP会话的集合。例如,一个视频会议(为多媒体会话)可能包含一个音频RTP会话和一个视频RTP会话。   RTP会话(RTP session):一群参与者通过RTP进行通信时所产生的关联。一个参与者可能同时参与多个RTP会话。在一个多媒体会话中,除非编码方式把多种媒体多路复用到一个单一数据流中,否则每种媒体都将使用各自的RTCP包,通过单独的RTP会话来传送。通过使用不同的目的传输地址对(一个网络地址加上一对分别用于RTP和RTCP的端口,构成了一个传输地址对)来接收不同的会话,参与者能把多个RTP会话区隔开来。单个RTP会话中的所有参与者,可能共享一个公用目的传输地址对,比如IP多播的情况;也可能各自使用不同的目的传输地址对,比如个体单播网络地址加上一个端口对。对于单播的情况,参与者可能使用相同端口对来收听其他所有参与者,也可能对来其他每个参与者使用不同的端口对来收听。   RTP会话间相互区别的特征,在于每个RTP会话都维护一个用于SSRC标识符的独立完整的空间。RTP会话所包含的参与者组,由能接收SSRC标识符的参与者组成,这些SSRC标识符由RTP(同步源或作用源)或RTCP中的任意参与者传递。例如,考虑下述情况,用单播UDP实现的三方会议,每方都用不同的端口对来收听其他两方。如果收到一方的数据,就只把RTCP反馈发送给那一方,则会议就相当于由三个单独的点到点RTP会话构成;如果收到一方的数据,却把RTCP反馈发送另两方,则会议就是由一个多方(multi-party)RTP会话构成。后者模拟了三方间进行IP多播通信时的行为。   RTP框架允许上述规定发生变化,但一个特定的控制协议或者应用程序在设计时常常对变化作出约束。   同步源(SSRC,Synchronization source):RTP包流的源,用RTP报头中32位数值的SSRC标识符进行标识,使其不依赖于网络地址。一个同步源的所有包构成了相同计时和序列号空间的一部分,这样接收方就可以把一个同步源的包放在一起,来进行重放。举些同步源的例子,像来自同一信号源的包流的发送方,如麦克风、摄影机、RTP混频器(见下文)就是同步源。一个同步源可能随着时间变化而改变其数据格式,如音频编码。SSRC标识符是一个随机选取的值,它在特定的RTP会话中是全局唯一(globally unique)的(见章节8)。参与者并不需要在一个多媒体会议的所有RTP会话中,使用相同的SSRC标识符;SSRC标识符的绑定通过RTCP(见章节6.5.1)。如果参与者在一个RTP会话中生成了多个流,例如来自多个摄影机,则每个摄影机都必须标识成单独的同步源。 作用源(CSRC,Contributing source ):若一个RTP包流的源,对由RTP混频器生成的组合流起了作用,则它就是一个作用源。对特定包的生成起作用的源,其SSRC标识符组成的列表,被混频器插入到包的RTP报头中。这个列表叫做CSRC表。相关应用的例子如,在音频会议中,混频器向所有的说话人(talker)指出,谁的话语(speech)将被组合到即将发出的包中,即便所有的包都包含在同一个(混频器的)SSRC标识符中,也可让听者(接收者)可以清楚谁是当前说话人。     终端系统(End system):一种应用程序,它产生发送出的RTP包中内容,或者使用接收到的RTP包中内容。在一个特定的RTP会话中,一个终端系统可以扮演一个或多个同步源角色,但通常是一个。 混频器(Mixer):一种中间系统,它从一个或多个源中接收RTP包,可能改变其数据格式,再按某种方式把这些包组合成一个新的包,然后转发出去。由于多个输入源的计时一般不会同步,所以混频器会对各个流的计时作出调整,并为组合流生成一个新的计时。因此,混频器将被标识成它所产生所有数据包的同步源。 转换器(Translator):一种中间系统,它转发RTP包而不改变各包的同步源标识符。转换器的例子如下:不作混频地转变编码的设备,把多播复制到单播的重复装置,以及防火墙里应用层次的过滤器。 监视器(Monitor):一种应用程序,它接收RTP会话参与者所发送的RTCP包,特别是接收报告(reception report),而且对当前服务质量进行评估,评估结果用于分配监视任务,故障诊断和长期统计。监视器常常被内建到参与会话的应用程序中,但也可以是一个的独立的应用程序——不参加会话、也不发送或接收RTP数据包(因为它们在不同的端口上)。这些被称作第三方监视器。还有一种情况也是可以接受的,第三方监视器只接收但不发送数据包,或者另外地算入到会话中。   非RTP途径(Non-RTP means):为提供一个可用的服务,可能还需要其他的协议和机制。特别地,对多媒体会议来说,一个控制协议可以发布多播地址,发布加密密钥,协商所用的加密算法,以及为没有预定义负载类型值的格式,建立负载类型值和其所代表的负载格式之间的动态映射。其他协议的例子如下:会话初始化协议(SIRFC3261[13]),ITU推荐的H.323[14],还有使用SDP(RFC2327[15])的应用程序,如RTSP(RFC 2326[16]). 对于简单的应用程序,电子邮件或者会议数据库也可能用到。对这些协议和机制的详细说明已经超出了本文档的讨论范围。

5 RTP数据传输协议 5.1 RTP固定头中的各字段 RTP头有以下格式:

1
2
3
4
5
6
7
8
9
10
11
12
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X| CC |M| PT | sequence number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| synchronization source (SSRC) identifier |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| contributing source (CSRC) identifiers |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
RTP包头格式    

 前12个字节出现在每个RTP包中,仅仅在被混合器插入时,才出现CSRC识别符列表。这些域有以下意义:
 版本(V):2比特 此域定义了RTP的版本。此协议定义的版本是2。(值1被RTP草案版本使用,值0用在最初”vat”语音工具使用的协议中。)
 填充(P):1比特 若填料比特被设置,则此包包含一到多个附加在末端的填充比特,填充比特不算作负载的一部分。填充的最后一个字节指明可以忽略多少个填充比特。填充可能用于某些具有固定长度的加密算法,或者用于在底层数据单元中传输多个RTP包。
 扩展(X):1比特 若设置扩展比特,固定头(仅)后面跟随一个头扩展。
 CSRC计数(CC):4比特 CSRC计数包含了跟在固定头后面CSRC识别符的数目。
 标志(M):1比特 标志的解释由具体协议规定。它用来允许在比特流中标记重要的事件,如帧边界。  负载类型(PT):7比特 此域定义了负载的格式,由具体应用决定其解释。协议可以规定负载类型码和负载格式之间一个默认的匹配。其他的负载类型码可以通过非RTP方法动态定义。RTP发送端在任意给定时间发出一个单独的RTP负载类型;此域不用来复用不同的媒体流。
 序列号(sequence number):16比特 每发送一个RTP数据包,序列号加1,接收端可以据此检测丢包和重建包序列。序列号的初始值是随机的(不可预测),以使即便在源本身不加密时(有时包要通过翻译器,它会这样做),对加密算法泛知的普通文本攻击也会更加困难。
时间戳(timestamp) 32比特时间戳反映了RTP数据包中第一个字节的采样时间。时钟频率依赖于负载数据格式,并在描述文件(profile)中进行描述。也可以通过RTP方法对负载格式动态描述。 如果RTP包是周期性产生的,那么将使用由采样时钟决定的名义上的采样时刻,而不是读取系统时间。例如,对一个固定速率的音频,采样时钟将在每个周期内增加1。如果一个音频从输入设备中读取含有160个采样周期的块,那么对每个块,时间戳的值增加160。 时间戳的初始值应当是随机的,就像序号一样。几个连续的RTP包如果是同时产生的。如:属于同一个视频帧的RTP包,将有相同的序列号。 不同媒体流的RTP时间戳可能以不同的速率增长。而且会有独立的随机偏移量。因此,虽然这些时间戳足以重构一个单独的流的时间,但直接比较不同的媒体流的时间戳不能进行同步。对于每一个媒体,我们把与采样时刻相关联的RTP时间戳与来自于参考时钟上的时间戳(NTP)相关联。因此参考时钟的时间戳就了数据的采样时间。(即:RTP时间戳可用来实现不同媒体流的同步,NTP时间戳解决了RTP时间戳有随机偏移量的问题。)参考时钟用于同步所有媒体的共同时间。这一时间戳对(RTP时间戳和NTP时间戳),用于判断RTP时间戳和NTP时间戳的对应关系,以进行媒体流的同步。它们不是在每一个数据包中都被发送,而在发送速率更低的RTCP的SR(发送者报告)中。 如果传输的数据是存贮好的,而不是实时采样等到的,那么会使用从参考时钟得到的虚的表示时间线(virtual presentation timeline)。以确定存贮数据中的每个媒体下一帧或下一个单元应该呈现的时间。此种情况下RTP时间戳反映了每一个单元应当回放的时间。真正的回放将由接收者决定。 SSRC:32比特 用以识别同步源。标识符被随机生成,以使在同一个RTP会话期中没有任何两个同步源有相同的SSRC识别符。尽管多个源选择同一个SSRC识别符的概率很低,所有RTP实现工具都必须准备检测和解决冲突。若一个源改变本身的源传输地址,必须选择新的SSRC识别符,以避免被当作一个环路源。 CSRC列表:0到15项,每项32比特 CSRC列表识别在此包中负载的所有贡献源。识别符的数目在CC域中给定。若有贡献源多于15个,仅识别15个。CSRC识别符由混合器插入,并列出所有贡献源的SSRC识别符。例如语音包,混合产生新包的所有源的SSRC标识符都被列出,以在接收端处正确指示参与者。
5.3.1 RTP头扩展
RTP提供扩展机制以允许实现个性化:某些新的与负载格式独立的功能要求的附加信息在RTP数据包头中传输。设计此方法可以使其它没有扩展的交互忽略此头扩展。RTP头扩展的格式如下图所示。

1
2
3
4
5
6
7
0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| defined by profile | length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| header extension |
| .... |

若RTP头中的扩展比特位置1,则一个长度可变的头扩展部分被加到RTP固定头之后。头扩展包含16比特的长度域,指示扩展项中32比特字的个数,不包括4个字节扩展头(因此零是有效值)。RTP固定头之后只允许有一个头扩展。为允许多个互操作实现独立生成不同的头扩展,或某种特定实现有多种不同的头扩展,扩展项的前16比特用以识别标识符或参数。这16比特的格式由具体实现的上层协议定义。基本的RTP说明并不定义任何头扩展本身。

6 RTP控制协议RTCP
RTP控制协议(RTCP)向会议中所有成员周期性发送控制包。它使用与数据包相同的传输机制。底层协议必须提供数据包和控制包的复用,例如用不同的UDP端口。RTCP提供以下四个功能:
○基本功能是提供数据传输质量的反馈。这是RTP作为一种传输协议的主要作用,它与其他协议的流量和拥塞控制相关。反馈可能对自适应编码有直接作用,并且IP组播的实验表明它对于从接收端得到反馈信息以诊断传输故障也有决定性作用。向所有成员发送接收反馈可以使”观察员”评估这些问题是局部的还是全局的。利用类似多点广播的传输机制,可以使某些实体,诸如没有加入会议的网络业务观察员,接收到反馈信息并作为第三方监视员来诊断网络故障。反馈功能通过RTCP发送者和接收者报告实现。
○RTCP为每个RTP源传输一个固定的识别符,称为规范名(CNAME)。由于当发生冲突或程序重启时SSRC可能改变,接收者要用CNAME来跟踪每个成员。接收者还要用CNAME来关联一系列相关RTP会话中来自同一个成员的多个数据流,例如同步语音和图像。 ○前两个功能要求所有成员都发送RTCP包,因此必须控制速率以使RTP成员数可以逐级增长。通过让每个成员向所有成员发送控制包,各个成员都可以独立地观察会议中所有成员的数目。此数目可以用来估计发包速率。 ○第四个可选的功能是传输最少的会议控制信息,例如在用户接口中显示参与的成员。这最可能在”松散控制”的会议中起作用,在”松散控制”会议里,成员可以不经过资格控制和参数协商而加入或退出会议。RTCP作为一个延伸到所有成员的方便通路,必须要支持具体应用所需的所有控制信息通信。 ○在RTP用于IP多点广播时,功能1-3是强制的,在所有情况下都推荐使用。建议RTP应用开发商避免使用只能用于单向广播而不能扩充到多用户的方法。 6.1 RTCP包格式
这部分定义了几个RTCP包类型,可以传送不同的控制信息: ○SR:发送者报告,描述作为活跃发送者成员的发送和接收统计数字; ○RR:接收者报告,描述非活跃发送者成员的接收统计数字; ○SDES:源描述项,其中包括规范名CNAME。 ○BYE:表明参与者将结束会话。 ○APP:应用描述功能。 在本文中将详细介绍SR和RR。
每个RTCP包的开始部分是与RTP数据包相类似的固定部分,随后是一块结构化单元,它随负载类型不同长度发生变化,但是总以32比特终止。对齐要求和长度域使RTCP包可”堆栈”,即可以将多个RTCP包形成一个复合RTCP包,在底层协议(如UDP)中,通常都是将复合包作为一个包传输的。
复合包中的每个RTCP单包可以单独处理,而无需考虑包复合的顺序。然而,为了实现某些协议功能,添加以下限制: ○接收数据的统计信息(在SR或RR中)。只要带宽允许应尽可能经常的发送,以达到统计数字的最大分辨率。因此每个周期发送的RTCP包必须包含一个报告包。 ○新的参与者需要尽快接收一个源的规范名以识别数据源并与媒体建立会话。因此,每个包中必须包含源描述项中的规范名。除非复合包进行了分割以进行部分加密(见9.1节的描述)。 ○必须限制首次在复合包中出现的包类型的数目,以增加在第一个字中常数比特的数目,这样可以增加RTCP包的有效性,以区分误传的RTP包和其他无关的包。因此,所有RTCP包必须以复合包的形式发送。复合包中至少有两个单个的RTCP包。具有以下格式:   ○加密前缀:当且仅当复合包被加密时,对每个RTCP复合包加32比特的前缀。
○SR或RR:复合包中的第一个RTCP包必须是一个报告包。即使没有数据发送和接收,此时发送空的RR包,或者复合包中其他的唯一包是BYE包,也必须发送报告包。    ○附加的RR:若被报告的接收统计源数目超过SR/RR包中最大允许的31个,附加的RR必须跟在最初的报告包后面。    ○源描述SDES ○BYE或APP包 每个RTP参与者在一个报告间隔内应只发送一个RTCP复合包,以便正确估计每个参与者的RTCP带宽。除非像9.1节描述的情况——把一个RTCP复合包分割以进行加密。如果数据源的个数太多,以至于不能把所有的RR包都放到同一个RTCP包中而不超过网络路径的最大传输单元(maximum transport unit MTU),那么可在每个间隔中发送其中的一部分包。在多个发送间隔中,所有的包应该被等概率的选中。这样就可以报告所有数据源的接收数据的情况。如果一个RTCP复合包的长度超过了网络路径的MTU,则它应当被分割为多个更短的RTCP包来传输。这不会影响对RTCP带宽的估计,因为每一个复合包至少代表了一个参与者。要注意的是每个RTCP复合包必须以SR或RR包开头。

1
2
3
4
5
6
7
8
9
10
11
|
|[--------- packet --------][---------- packet ----------][-packet-]
|
| receiver chunk chunk
V reports item item item item
--------------------------------------------------------------------
R[SR #sendinfo #site1#site2][SDES #CNAME PHONE #CNAME LOC][BYE##why]
--------------------------------------------------------------------
| |
|<----------------------- compound packet ----------------------->|
|<-------------------------- UDP packet ------------------------->|

#: SSRC/CSRC identifier

图1: RTCP复合包举例

6.2 RTCP传输时间间隔 RTP被设计为允许应用自动适应不同的规模的会话――从几个参与者到几千个参与者的会话。 对每一个会话,我们假定数据传输受到一个上限――会话带宽的限制。会话带宽分配给所有的参与者。这个带宽会被预留,并由网络所限制。如果没有预留,基于环境的其他约束将会确定合理的最大带宽供会话使用,这就是会话带宽。会话带宽在一定程度上独立于媒体编码,但媒体编码却依赖于会话带宽。 在涉及媒体应用时,会话带宽参数最好由一个会话控制应用提供。但媒体应用可能设置一个默认参数。此参数由单个发送者选择的编码方式的数据带宽算出。会话管理可能会基于多播范围的规则或其他标准确定带宽限制。所有的参与者应使用相同的会话带宽值以保证计算出相同的RTCP间隔。 控制传输带宽应当是会话带宽的一小部分,这部分所占总的会话带宽的百分比应是已知的。一小部分:传输协议的首要功能是传输数据;已知:控制传输带宽可以被放进带宽描述中提供给资源预留协议,并且使每个参与者都可以独立的计算出他所占有的带宽份额。 控制传输带宽作为额外的一部分加入到会话带宽中。建议RTCP控制传输带宽为RTCP会话带宽的5%。其中的1/4分配给发送者;当发送者的比例超过所有参与者的1/4时,其RTCP控制带宽相应增加。所有的会话参与者必须使用相同的常数(以上提到的百分比),以便计算出相同的发送时间间隔。这些常数应在一个特殊的描述文件中确定。 计算出的RTCP复合包的发送时间间隔应该有一个下限,以免参与者数量较少时大量发送RTCP包。这也使网络暂时断开时,发送间隔不会太小。在应用开始时,一个延迟应加到第一个的TCP复合包发送之前,以便从其他参与者接收RTCP复合包。这样,发送时间间隔能更快的收敛到正确的值。这个延迟可以设为最小时间间隔的一半。固定的时间间隔建议为5秒。 一个实现可能使RTCP最小发送时间间隔与会话带宽参数成比例。则应满足下列约束: ○对多播会话,只有活动的数据发送者使用减小的最小化的值计算RTCP复合包的发送时间间隔。 ○对单播会话,减小的值也可能被不是活动的数据发送者使用,发送初始的RTCP复合包之前的延迟可能是0。 ○对所有会话,在计算参与者的离开时间时,这个固定最小值会被用到。因此,不使用减小的值进行RTCP包的发送,就不会被其他参与者提前宣布超时。 ○减小的最小时间间隔建议为:360/sb(秒),其中sb:会话带宽(千字节/秒)。当sb>72kb/s时,最小时间间隔将小于5s。 6.3节所描述的算法和附录A.7将实现本节列出的目标: ○计算出的RTCP包的时间间隔与组中参与者的人数成正比。(参与者越多,发送时间间隔越长,每个参与者占有的RTCP带宽越小)。 ○RTCP包的(真实)时间间隔是计算出的时间间隔的0.5~1.5倍之间某个随机的值,以避免所有的参与者意外的同步。 ○RTCP复合包的平均大小将会被动态估计,包括所有发送的包和接收的包。以自动适应携带的控制信息数量的变化。 ○由于计算出的时间间隔依赖于组中的人数。因此,当一个的用户加入一个已经存在的会话或者大量的用户几乎同时加入一个新的会话时,就会有意外的初始化效应。这些新用户将在开始时错误的估计组中的人数(估计太小)。因此他们的RTCP包的发送时间间隔就会太短。如果许多用户同时加入一个会话,这个问题就很重要了。为了处理这处问题考虑了一种叫“时间重估”的算法。这个算法使得组中人数增加时,用户能够支持RTCP包的传输。 当有用户离开会话,不管是发送BYE包还是超时,组中的人数会减少。计算出的时间间隔也应当减少。因此,应用“逆向重估”算法,使组中的成员更快的减少他们的时间间隔,以对组中的人数减少做出响应。 ○BYE包的处理和其他RTCP包的处理不同。BYE包的发送用到一个“放弃支持”算法。以避免大量的BYE包同时发送,使大量参与者同时离开会话。 这个算法适用于所有参与者都允许RTCP包的情况。此时,会话带宽=每个发送者的带宽×会话中参与者的总人数。详细算法见随后小节,附录A.7给出了算法的一个实现。 6.2.1维持会话成员的人数 当侦听到新的站点的时候,应当把他们加入计数。每一个登录都应在表中创建一条记录,并以SSRC或CSRC进行索引。新的登录直到接收到含有SSRC的包或含有与此SSRC相联系的规范名的SDES包才视为有效(见附录A.1)。当一个与SSRC标识符相对RTCP BYE包收到时,登录会被从表中删除。除非一个“掉队”的数据包到达,使登录重新创建。 如果在几个RTCP报告时间间隔内没有RTP或RTCP包收到,一个参与者可能标记另外一个站点静止,并删除它。这是针对丢包提供的一个很强健的机制。所有站点对这个超时时间间隔乘子应大体相同,以使这种超时机制正常工作。因此这个乘子应在特别的描述文件中确定。 对于一个有大量参与者的会话,维持并存贮一个有所有参与者的SSRC及各项信息的表几乎是不可能的因此,只可以只存贮SSRC。其他算法类似。关键的问题就是,任何算法都不应当低估组的规模,虽然它有可能被高估。 6.3 RTCP包的发送和接收规则 下面列出了如何发送RTCP包,当接收到的TCP包时该干什么的规则。 为执行规则,一个会话参与者就维持下列变量: tp: RTCP包发送的最后时间。 tc: 当前时间。 tn: 估计的下一个RTCP包要发送的时间。 pmembers: tn最后被重新计算时,会计的会话成员的人数。 members: 会话成员人数的当前估计。 senders: 会话成员中发送者人数的估计。 rtcp_bw: 目标RTCP带宽。例如用于会话中所有成员的RTCP带宽。单位bit/s。这将是程序开始时,指定给“会话带宽”参数的一部分。 we_sent: 自当前第二个前面的RTCP发送后,应用程序又发送了数据,则此项为true。 avg_rtcp_size: 此参与者收到的和发送的RTCP复合包的平均大小。单位:bit。按6.2节,此大小包括底层传输层和网络层协议头。 initial: 如果应用程序还未发送RTCP包,则标记为true。 许多规则都用到了RTCP包传输的“计算时间间隔”。此时间间隔将在随后的小节描述。 6.3.1计算RTCP传输时间间隔 一个会话参与者包的平均发送时间间隔应当和所在会话组中人数成正比。这个间隔称为计算时间间隔。它由上面提到的各个状态参量结合起来计算得出。计算时间间隔T的计算如下: 1(1)如果发送者人数≤会话总人数×25%。则T取决于此参与者是否是发送者(we_sent的值);否则,发送者和接收者将统一处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
senders<=25%*members
we_sent
c=avg_rtcp_size/(0.25*rtcp_bw);
n=senders;
c=avg_rtcp_size/(0.75*rtcp_bw);
n=members-senders;

c=avg_rtcp_size/rtcp_bw;
n=members;

not
yes
yes
not

图:确定c ,n 如6.2节所述,RTP描述文件可能用两个独立的参数(S,R)确定发送者与非发送者。此时,25%和75%只要相应的换成S/(S+R),R/(S+R)即可。注意R=0的情况。 2 如果initial为true(则未发送过RTCP包),则设Tmin=2.5s;否则设Tmin=5s。 3 决定性的计算时间间隔(deterministic calculated interval)Td=max(Tmin ,nc)。 4 T=Tdλ;其中λ~U(0.5,1.5)。即λ服从0.5到1.5之间的均匀分布。 5 T=T/(e-0.5)≈T/1.21828,补偿时间重估算法,使之收敛到比计算出的平均RTCP带宽小的一个值。 这个算法产生了一个随机的计算时间间隔,并把至少25%的RTCP带宽分配给发送者,其余的分给接收者。若发送者超过会话总人数的25%,此算法将把带宽平均分给所有的参与者。 6 3.2初始化 一加入会话,参与者的各状态参量初始化为:tp=0; tc=0; senders=0; pmembers=1; members=1; vw_sent=false; rtcp_bw:由会话带宽参数的相应部分得到;initial=true;avg_rtcp_size:初始化为应用程序稍后将发送的RTCP包的可能大小;T:如6.3.1节;tn=T(这意味着,一个计时器将经T时间后被唤醒);应用程序可以用任何它需要的方式实现计时器。 参与者把它自己的SSRC加到成员列表中。 6.3.3接收到的TP包或一个非BYE的RTCP包 当收到一个参与者的RTP或RTCP包时,若其SSRC不在成员列表中,将其SSRC加入列表;若此参与者被确认有效(如6.2.1节描述),就把列表中成员的值更新。对每个有效的RTP包中的CSRC执行相同的过程。 当收到一个参与者的RTP包时,若其SSRC不在发送者列表中,则将其SSRC加入发送者列表,更新相应的值。 每收到一个RTCP复合包,avg_rtcp_size更新为avg_rtcp_size = 1/16 * packet_size + 15/16 * avg_rtcp_size ;其中packet_size是刚收到的RTCP复合包的大小。 6.3.4接收RTCP BYE包 除6.3.7小节描述的发送RTCP BYE包之外,如果收到一个RTCP BYE包,则检测成员列表。若SSRC存在;先移除之,并更新成员的值。 另外,为使RTCP包的发送速率与组中人数变化更加协调,当收到一个BYE包使得members的值pmembers时,下面的逆向重估算法应当执行: (1)tn的更新:tn = tc + ( members / pmembers ) * ( tn –tc ); (2)tp的更新:tp = tc – ( members / pmembers ) * ( tc – tp );下一个RTCP包将在时刻tn 被发送,比更新前更早一些。 (3)pmembers的更新:pmembers=members; 这个算法并没有防止组的大小被错误的在短时间内估计为0的情况。如:在一个较多人数的会话中,多数参与者几乎同时离开而少数几个参与者没有离开的情况。这个算法并没有使估计迅速返回正确的值。因为这种情况较罕见,且影响不大。 6.3.5 SSRC超时 在随机的时间间隔中,一个参与者必须检测其他参与者是否已经超时。为此,对接收者(we_sent为false),要计算决定性时间间隔Td,如果从时刻Tc-MTd(M为超时因子,默认为5秒)开始,未发送过RTP或RTCP包,则超时。其SSRC将被从列表中移除,成员被更新。在发送者列表中也要进行类似的检测。发送者列表中,任何从时间tc-2T(在最后两个RTCP报告时间间隔内)未发送RTP包的发送者,其SSRC从发送者列表中移除,列表更新。 如果有成员超时,应该执行6.3.4节中的逆向检测算法。每个参与者在一个RTCP包发送时间间隔内至少要进行一次这样的检测。 6.3.6发送时钟到时了 当包传输的发送时钟到时,参与者执行下列操作: (1)按6.3.1节的办法计算T。 (2)更新发送时钟的定时时间,判断是否发送RTCP包,更新pmembers。如图: tp+T<=tc 发送RTCP包 tp=tc; tn=tc+T; initial=false; avg_rtcp_size=1/16 * packet_size + 15/16 * avg_rtcp_size  tn=tp+T Pmemvers=members yes no //不发送RTCP包 图:发送时钟到时的操作 6.3.7发送一个BTE包 当一个参与者离开会话时,应发送BYE包,通知其他参与者。为避免大量参与者同时离开系统时,大量BYE包的发送,若会话人数超过50,则参与者在要离开会话时,应执行下面的算法。这个算法实际上“篡夺”了一般可变成员的角色来统计BYE包。   (1)tp=tc ; members=1; pmembers=1; sinitial=1; we_sent=false; senders=0; rtcp_size:设置为将要发送的RTCP包大小;计算“计算时间间隔”T;tn=tc+T;(BYE包预计在时刻tn被发送)。 (2)每当从另外一个参与者接收到BYE包时,成员人数加1。不管此成员是否存在于成员列表中,也不管SSRC采样何时使用及BYE包的SSRC是否包含在采样之中。如果收到RTP包或甚的RTCP包(除BYE包之外的RTCP包),成员人数不增加。类似,只有在收到BYE包时,avg_rtcp_size才更新。当RTP包到达时,发送者人数senders不更新,保持为0。 (3)在此基础上,BYE包的传输服从上面规定的一般的RTCP包的传输。 (BYE包的传输,是专注于统计会话中发送BYE包的人数的。) 这允许BYE包被立即发送,并控制总的带宽使用。在最坏情况下上,这可能会使RTCP控制包使用两倍于正常水平的带宽,达到10%――其中5%给BYE包的RTCP包,其余5%给BYE包。 一个参与者若不想用上面的机制进行RTCP包的发送,可以直接离开会话,而根本不发送BYE包。他会被其他参与者因超时而删除。 一个参与者想离开会话时,如果组中的人数会计数目小于50,则参与者可以直接发送BYE包。 另外,一个从未发送过RTP或RTCP包的参与者,在离开会话时,不能发送BYE包。 6.3.8更新we_sent变量 如果一个参与者最近发过RTP包,则变量we_sent值为true,否则为false。相同的机制可以管理发送者中的其他参与者。如果参与者发送了TPT包而此时,其对应的we_sent变量值为false,则就把它自己加到发送者列表中,并设置其we_sent变量为true。6.3.4节中描述的逆向重估算法(reverse reconsideration algorithm)应当被执行。以可能减少发送SR包前的延迟。每次发送一个RTP包,其相应的传输时间都会记录在表中。一般发送者的超时算法应用到参与者自身:从tc-2T时开始,一直没有发送RTP包,则此参与者就从发送者列表中将其自身移除,减少发送者总数,并设置we_sent变量值为false。 6.3.9源描述带宽的分配 这里定义了几种源描述项,强制性的规范名(CNAME)除外。例如,个人姓名(NAME)和电子邮件地址(EMAIL)。它也提供了方法定义新的RTCP包的类型。应用程序在给这些额外信息分配带宽时应额外小心。因为这会降低接收报告及CNAME的发送速率,可能破坏协议发挥作用。建议分配给一个参与者用于传输这些额外信息的带宽不超过总的RTCP带宽的20%。另外,并非所有的源描述项都将包含进每一个应用程序中。包含进应用程序的源描述项应根据其用途分配给相应的带宽百分比。建议不要动态会计这些百分比,而应根据一个源描述项的典型长度将所占带宽的百分比的转化为报告间隔。 例如,一个应用程序可能仅发送CNAME,NAME和EMAIL,而不需要其他项。NAME可能会比EMAIL给予更高的优先级。因为NAME可能会在应用程序的用户界面上持续显示,但EMAIL可能仅仅在需要时才会显示。在每一个RTCP时间间隔内,一个包含CNAME项的SDES包和一个RR包将会被发送。最小的会话时间间隔平均为5秒。每经过3个时间间隔(15秒),一个额外的项将会包含进这个SDES包中。7/8的时间是NAME项,每经过8个这样的间隔(15s8=2min),将会是EMAIL项。 当多个会话考虑使用一个通用的规范名为每个参与者进行绑定时,如在一个RTP会话组成的多媒体会议中,额外的SDES信息可能只在一次RTP会话中被发送。其余的会话将只发送CNAME。特别,这个办法也应该用在分层编码的多个会话中。 6.4 发送者和接收者报告 RTP接收者利用RTCP报告包提供接收质量反馈。根据接收者是否同时还是发送者,RTCP包采取两种不同的形式。发送者报告(SR)和接收者报告(RR)格式中唯一的不同,除包类型码之外,在于发送者报告包括20字节的发送者信息。
SR包和RR包都包括零到多个接收报告块。针对该接收者发出上一个报告块后接收到RTP包的起始同步源,每个源一个块。报告不发送给CSRC列表中的贡献源。每个接收报告块提供从特定数据源接收到数据的统计信息。由于SR/RR包最多允许31个接收报告块,故可以在最初的SR或RR包之后附加RR包,以包含从上一个报告以来的间隔内收听到的所有源的接收报告。如果数据源太多,致使若把所有的RR包放到同一个RTCP复合包中会超出网络的MTU。那么就在一个周期内选择上面RR包的一部分以不超过MTU。这些RR包的选取应让各个包都有同等的几率被取到。这样在几个发送周期间隔中,对所有的数据源就都发送接收报告了。 以下部分定义了两种报告的格式。如果应用程序需要其他信息,他们可以被扩展。 6.4.1 SR:发送者报告RTCP包
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ header |V=2|P| RC | PT=SR=200 | length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SSRC of sender | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ sender | NTP timestamp, most significant word | info +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | NTP timestamp, least significant word | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | RTP timestamp | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | sender’s packet count | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | sender’s octet count | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ report | SSRC_1 (SSRC of first source) | block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 1 | fraction lost | cumulative number of packets lost | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | extended highest sequence number received | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | interarrival jitter | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | last SR (LSR) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | delay since last SR (DLSR) | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ report | SSRC_2 (SSRC of second source) | block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 2 : … : +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ | profile-specific extensions | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 发送者报告包由3部分组成,若定义,可能跟随第4个面向协议的扩展部分。
第一部分,头部,8字节长。该域有以下意义:
版本(V):2比特 RTP版本识别符,在RTCP包内的意义与RTP包中的相同。此协议中定义的版本号为2。
填充(P):1比特 若设置填充比特,该RTCP包在末端包含一些附加填充比特,并不是控制信息的基本部分。填充的最后一个比特统计了多少个字节必须被忽略。填充可能会用于需要固定长度块的加密算法。在复合RTCP包中,复合包作为一个整体加密,填料比特只能加在最后一个单个RTCP包的后面。
接收报告块计数(RC):5比特 该包中所含接收报告块的数目。零值有效。
包类型(PT):8比特 包含常数200,用以识别这个为SR包。
长度:16比特 该RTCP包的长度减1。其单位是32比特字,包括头和任何填充字节。(偏移量1保证零值有效,避免了在扫描RTCP包长度时可能发生的无限循环,同时以32比特为单位避免了对以4为倍数的有效性检测。)
SSRC:32比特 SR包发送者的同步源标识符。 第二部分,发送者信息,20字节长。在每个发送者报告包中出现。它概括了从此发送者发出的数据传输情况。此域有以下意义:
NTP时间戳:64比特 指示了此报告发送时的背景时钟(wallclock)时刻,它可以与从其它接收者返回的接收报告块中的时间标志结合起来,计算往返每个接收者所花的时间。接收者应让NTP时间戳的精度远大于其他时间戳的精度。时间戳测量的不确定性不可知,因此也无需指示。一个系统可能没有背景时钟的概念,而只有系统指定的时钟,如系统时间(system uptime)。在这样的系统中,此时钟可以作为参考计算相对NTP时间戳。选择一个公用的时名是非常重要的。这样多个独立的应用都可以使用相同的时钟。到2036年,相对和绝对NTP时间戳会产生大的差异。到那时,我们希望不再需要相对时钟。一个发送者,如果不用背景时钟时间或逝去时间,可以设置此项为零。
RTP时间戳:32比特 与以上的NTP时间标志对应同一时刻。与数据包中的RTP时间戳具有相同的单位和偏移量。这个一致性可以用来让NTP时间标志已经同步的源之间进行媒体内/间同步,还可以让与媒体无关的接收者估计名义RTP时钟频率。注意在大多数情况下此时间戳不等于任何临近的RTP包中的时间戳。RTP时间戳可以由相应的NTP时间戳计算得到。依据的是“RTP时间戳计数器”和“在采样时通过周期性检测背景时钟时间得到的实际时间”两者之间的关系。  (在RTCP SR包中有NTP时间戳、RTP时间戳,它们可以计算背景时钟和RTP时钟之间的对应关系,通过这个关系,可以由RTP数据包中的RTP时间戳计算也相应的回放时刻。这样就可以进行多个流的同步了。之所以要有NTP时间戳,是因为不同流的RTP时间戳有不同的随机偏移量,无法直接进行同步:笔者注。) 发送的报文数:32比特 从开始传输到此SR包产生时该发送者发送的RTP数据包总数。若发送者改变SSRC识别符,该计数器重设。 发送的字节文数:32比特 从开始传输到此SR包产生时该发送者在RTP数据包发送的字节总数(不包括头和填充)。若发送者改变SSRC识别符,该计数器重设。此域可以用来估计平均的负载数据发送速率。
第三部分:零到多个接收报告块。块数等于从上一个报告以来该发送者侦听到的其它源(不包括自身)的数目。每个接收报告块传输从某个同步源来的数据包的接收统计信息。若数据源因冲突而改变其SSRC标识符,接收者重新设置统计信息。这些统计信息有:
SSRC_n(同步源标识符):32比特 在此接收报告块中信息所属源的SSRC标识符。
丢包率:8比特 自从前一SR包或RR包发送以来,从SSRC_n传来的RTP数据包的丢失比例。以定点小数的形式表示。该值定义为损失包数/期望接收的包数。若由于包重复而导致包丢失数为负值,丢包率设为零。注意在收到上一个包后,接收者无法知道以后的包是否丢失。如:若在上一个接收报告间隔内从某个源发出的所有数据包都丢失,那么将不为此数据源发送接收报告块。 累计包丢失数:24比特 从开始接收到现在,从源SSRC_n发到本源的RTP数据包的丢包总数。该值定义为:期望接收的包数-实际接收的包数。接收的包括复制的或迟到的。由于迟到的包不算作损失,在发生复制时丢包数可能为负值。期望接收的包数定义为:扩展的上一接收序号(随后定义)减去最初接收序号。
接收到的扩展的最高序列号:32比特 低16比特包含从源SSRC_n来的最高接收序列号,高16比特用相应的序列号周期计数器扩展该序列号。注意在同一会议中的不同接收者,若启动时间明显不同,将产生不同的扩展项。
到达间隔抖动:32比特 RTP数据包到达时刻统计方差的估计值。测量单位同时间戳单位,用无符号整数表达。到达时间抖动定义为一对包中接收者相对发送者的时间间隔差值的平均偏差(平滑后的绝对值)。如以下等式所示,该值等于两个包相对传输时间的差值。相对传输时间是指:包的RTP时间戳和到达时刻接收者时钟时间的差值。若Si是包i中的RTP时间戳,Ri是包i到达时刻(单位为:RTP时间戳单位)。对于两个包i和j,D可以表示为 D(i,j)=(Rj-Sj)-(Ri-Si); 到达时刻抖动可以在收到从源SSRC_n来的每个数据包i后连续计算。利用该包和前一包i-1的偏差D(按到达顺序,而非序号顺序),根据公式J=J+(|D(i-1,i)|-J)/16计算。无论何时发送接收报告,都用当前的J值。 此处描述的抖动计算允许与协议独立的监视器对来自不同实现的报告进行有效的解释。
上一SR报文 (LSR):32比特 接收到的来自源SSRC_n的最新RTCP发送者报告(SR)的64位NTP时间标志的中间32位。若还没有接收到SR,该域值为零。 自上一SR的时间(DLSR):32比特 是从收到来自SSRC_n的SR包到发送此接收报告块之间的延时,以1/65536秒为单位。若还未收到来自SSRC_n的SR包,该域值为零。
假设SSRC_r为发出此接收报告块的接收者。源SSRC_n可以通过记录收到此接收报告块的时刻A来计算到SSRC_r的环路传输时延。可以利用最新的SR时间标志(LSR)计算整个环路时间A-LSR,然后减去此DLSR域得到环路传输的时延。  如下图所示。 [10 Nov 1995 11:33:25.125 UTC] [10 Nov 1995 11:33:36.5 UTC] n SR(n) A=b710:8000 (46864.500 s) —————————————————————-> v ^ ntp_sec =0xb44db705 v ^ dlsr=0x0005:4000 ( 5.250s) ntp_frac=0x20000000 v ^ lsr =0xb705:2000 (46853.125s) (3024992005.125 s) v ^ r v ^ RR(n) —————————————————————-> |<-DLSR->| (5.250 s)

A 0xb710:8000 (46864.500 s) DLSR -0x0005:4000 ( 5.250 s)

LSR -0xb705:2000 (46853.125 s)

delay 0x0006:2000 ( 6.125 s)

图2: 往返路程时间的计算举例

可以用此来近似测量到一组接收者的距离,尽管有些连接可能有非常不对称的时延。 6.4.2 RR:接收者报告包
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ header |V=2|P| RC | PT=RR=201 | length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SSRC of packet sender | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ report | SSRC_1 (SSRC of first source) | block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 1 | fraction lost | cumulative number of packets lost | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | extended highest sequence number received | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | interarrival jitter | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | last SR (LSR) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | delay since last SR (DLSR) | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ report | SSRC_2 (SSRC of second source) | block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 2 : … : +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ | profile-specific extensions | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

接收者报告包(RR)与发送者报告包基本相同,除了包类型域包含常数201和没有发送者信息的5个字(NTP和RTP时间标志和发送者包和字节计数)。余下区域与SR包意义相同。若没有发送和接收据报告,在RTCP复合包头部加入空的RR包(RC=0)。 6.4.3发送者和接收者报告扩展 如果有额外的关于发送者和接收者的信息要周期性的,描述文件(profile)应该定义接收者报告和发送者报告描述文件扩展。此时,应采用这里的办法,而不是定义另外的RTCP包。因为这种办法需要的头部信息更少。 扩展部分是发送报告包和接收报告包的第四部分。如果有的话,应紧跟在接收报告块的后面。如果需要更多的发送者信息,它应当跟在发送者报告的开关,而不应在报告中出现。如果要包含进接收者的信息,它应该以块数组的方式放到接收报告块的后面。即这些块也应被计入RC字段中。 6.4.4分析发送者和接收者报告 接收质量反馈不仅对发送者有用,而且对于其它接收者和第三方监视器也有作用。发送者可以基于反馈修正发送信息量;接收者可以判断问题是本地的,区域内的还是全局的;网络管理者可以利用与协议无关的监视器(只接收RTCP包而不接收相应的RTP包)去评估多点传送网络的性能。 在发送者信息和接收者报告块中都连续统计丢包数,因此可以计算任何两个报告块中的差别。在短时间和长时间内都可以进行测算。最近收到的两个包之间差值可以评估当前传输质量。包中有NTP时间戳,可以用两个报告间隔的差值计算传输速率。由于此时间间隔与数据编码速率独立,因此可以实现与编码及协议独立的质量监视。 一个例子是计算两个报告间隔时间内的丢包率。丢包率=此间隔内丢失的包/此间隔内期望收到的包。如果此值与“丢失比例”字段中的值相同,说明包是连续的;若否,说明包不是连续的。间隔时间内的丢包率/间隔时间=每秒的丢包率。 从发送者信息中,第三方监视器可以在一个时间间隔内计算平均负载数据发送速率和平均发包速率,而无需考虑数据接收。两个值的比就是平均负载大小(平均每个包的负载大小)。(即:平均负载大小=平均负载数据发送速率/平均发包率。)若能假定丢包与包的大小无关,那么某个特定接收者收到的包数乘以平均负载大小(或相应的包大小)就得出接收者可得到的外在吞吐量。 除了累计计数允许利用报告间差值进行长期包损测量外,单个报告的“丢包比例”字段提供一个短时测量数据。当会话规模增加到无法为所有接收者保存接收状态信息,或者报告间隔变得足够长以至于从一个特定接收者只能收到一个报告时,短时测量数据变得更重要。 到达间隔抖动字段提供另一个有关网络阻塞的短时测量量。丢包反映了长期阻塞,抖动测量反映出短时间的阻塞。抖动测量可以在导致丢包前预示阻塞。由于到达间隔抖动字段仅仅是发送报告时刻抖动的一个快照,因此需要在一个网络内在一段时间内分析来自某个接收者的报告,或者分析来自多个接收者的报告。 6.5源描述RTCP包 源描述(SDES)包由一个头及0个或多个块组成。每个块都由块中所标识的数据源的标识符及其后的各个描述构成。 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ header |V=2|P| SC | PT=SDES=202 | length | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ chunk | SSRC/CSRC_1 | 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SDES items | | … | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ chunk | SSRC/CSRC_2 | 2 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SDES items | | … | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+

6.6 BYE(BYE包) 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |V=2|P| SC | PT=BYE=203 | length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SSRC/CSRC | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ : … : +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ (opt) | length | reason for leaving … +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ BYE包表明一个或多个源将要离开。如果混合器收到BYE包,混合器应当发送这个BYE包,并保持SSRC/CSRC不变。如果混合器关闭,应向贡献源列表中的所有SSRC,包括它自己的SSRC发送BYE包。BYE包可能会有选择的包含8个字节的统计字段,其后跟上几个字节的文本表明离开的原因。文本字符串编码格式和SDES中描述的相同。

9安全性 底层协议将最终提供由RTP应用要求的所有安全服务,包括真实性、完整性、保密性。这些服务在参考文献[27]中的IP协议有详细描述。由于使用RTP的初始音频和视频应用在IP层可用之前就要求保密性服务,因此,随后的一小节描述了使用RTP和RTCP的保密性服务。新的RTP应用可以实现这里描述的RTP保密性服务,以用于向后兼容,也可以实现替代这里的安全服务。这种安全服务的RTP开销是比较小的。因此,如果这项服务被将来的某种服务所替代,代价也是比较小的。 另一方面,RTP的其他服务,服务的其他实现及其他的算法可能会在将来定义。特别是为RTP负载提供可靠性的实时安全传输协议( Secure Real-time Transport, SRTP)正在制定中。它可以使RTP头部不被加密。这样,链路层的头部压缩算法可以继续使用。SRTP基于高级企业标准(Advanced Encryption Standard, AES)制定。它比这里描述的服务提供更强健的安全性。 密钥和证书分配超出了本文的范围。 9.1 保密性 保密性意味着只有特定的接收者才能够对收到的包进行解码;对其他人,包里含有的都是无用信息。内容的保密性通过加密来实现。 当用这节指定的方法RTP、RTCP加密时,为了传输而封装的所有字节将在底层的包中作为一个单元加密。对RTCP,每个单元在加密之前必须在前面附加一个32字节的随机数。对RTP,不必在前面加前缀,而是让序列号和时间戳字段都用随机偏移量初始化。由于较差的随机性质。这其实是一个弱的初始化向量(initialization vector, IV)。另外,如果其后的SSRC字段被攻击者得到,则加密算法将出现新的薄弱点。 对RTCP,一个应用程序可能将RTCP复合包中的一个RTCP包分割成两个RTCP复合包。其中,一个在发送时加密,另一个发送时不加密。例如,SDES信息可能会被加密,但接收者报告却不加密,以适用于没有密钥的第三方监视者。如图4所示。源描述信息后必须附加没有报告的空RR包,以满足所有RTCP复合包必须以SR或RR包开头的要求。SDES的CNAME字段包含在加密或未加密的包中之一即可,但并不都需要包含。相同的源描述信息不应在两个包中都携带。否则会使加密算法不安全。 UDP packet UDP packet


[random][RR][SDES #CNAME …] [SR #senderinfo #site1 #site2]


encrypted                     not encrypted

#: SSRC identifier 图4: 加密的和未加密的RTCP包 接收者加密的使用和正确密钥的使用通过头或负载的有效性检查进行确认。RTP和RTCP头的有效性检查由附录A.1和A.2给出。 为和RFC1889中RTP初始描述中的实现相一致。默认的算法是链式加密块模式(cipher block chaining (CBC) mode)下的数据加密算法,见RFC1423中1.1节的描述。除非出现由5.1节描述指明的填充多个字节的情况,否则,初始的随机向量是0,因为随机值由RTP头或RTCP复合包的随机前缀提供。CBC初始向量的细节见参考文献[30]。支持本节的加密算法的实现也应当支持CBC下的DES算法。因为此算法可实现最大程度的交互可操作性。采用这种方法的原因是,因特网上通过音频、视频工具做实验证明它简便且有效。但DES被发现很容易被破解。建议用更强健的加密算法,例如三层DES加密算法来代替默认的加密算法。另外,安全CBC模式要求每个包的第一个块和一个随机数求异或。对于RTCP,这通过在每个包前附加一个32位的随机数实现。每个包的随机数相互独立。对RTP,时间戳和序列号将从附加的数值开始,但对连续的包,它们并不是被独立的随机化的。应该注意到对RTP和RTCP,这种随机性都受到了限制。高安全性的应用应当考虑其他更加简捷安全的方法。其他加密算法应通过非RTP方法对一个会话动态指定。特别是基于AES的SRTP描述文件(见参考文献[23])将会是未来的一个不错的选择。以上描述了IP层或RTP层加密。作为它的替代,描述文件可以定义另外的负载类型以用于加密、编码。这些编码必须描述如何填充,以及编码的其他方面如何控制。这种方法可以按照应用的要求,只加密数据,不加密头部。这可能对同时处理解密和解码的硬件服务特别重要。这也可能对RTP和底层头部的链路层的应用很有用。既然头部的加密已经进行了压缩,负载(而不是地址)的保密性就足够了。 9.2 真实性和信息完整性 真实性和信息完整性没有在RTP层定义,因为这些服务离不开密钥管理体系。可以期望真实性和信息完整性将由底层协议完成。

10 拥塞控制 因特网上的所有传输协议都需要通过一些方法进行地址拥塞控制(见参考文献[31]),RTP也不例外。但由于RTP数据传输经常缺少弹性(以固定的或控制好的速率产生包)。因此,RTP的拥塞控制方法和其他的传输协议,如TCP很不相同。在某种程度上,缺乏弹性意味着降低了拥塞的风险。因为RTP流不会像TCP流那样增长到消耗掉所有可用的带宽程度。但是,缺乏弹性也意味着RTP流不能任意减小它在网络上的负载量,以在出现拥塞时消除之。 由于RTP可能会在许多不同的情况下用于相当广的。因此就没有一个全都通用一个拥塞控制机制。因此,拥塞控制应当在描述文件中定义。对于某些描述,可能加上可应用性陈述以限制描述应用在已设计消除拥塞的环境中。对其它描述,可能需要特别的方法,如基于RTCP反馈的自适应数据传输速率。 参考文献: 正式参考文献 [1] Schulzrinne, H. and S. Casner, “音频和视频会议最小控制的RTP描述”, RFC 3551, 2003.6 [2] Bradner, S., “表示需求层的RFC关键字”, BCP 14, RFC 2119, 1997.3 [3] Postel, J., “网络协议”, STD 5, RFC 791, 1981.9 [4] Mills, D., “网络时间协议(第三版)描述、实现和分析”, RFC 1305,1992.3 [5] Yergeau, F., “UTF-8, 一个ISO 10646传输格式”, RFC 2279,1998.1 [6] Mockapetris, P., “域名――概念和工具”, STD 13, RFC 1034,1987.11 [7] Mockapetris, P., “域名――实现和描述”, STD 13, RFC 1035,1987.1 [8] Braden, R., “因特网主机需求――应用和支持”, STD 3, RFC 1123,1989.10 [9] Resnick, P., “因特网信息格式”, RFC 2822,2001.4 非正式参考文献 [10] Clark, D. and D. Tennenhouse, “新一代协议的建构考虑,” 关于通信体系结构和协议的数据通信专业组讨论班, (宾夕法尼亚州,费城), IEEE 计算机通信回顾 卷. 20(4), 200-208页,1990.9 [11] Schulzrinne, H., “关于设计音频、视频会话传输协议及其它多参与者实时应用的讨论”, 1993.10 [12] Comer, D., TCP/IP网络协议 ,卷1. Englewood  Cliffs, New Jersey: Prentice Hall, 1991. [13] Rosenberg, J., Schulzrinne, H., Camarillo, G., Johnston, A.,Peterson, J., Sparks, R., Handley, M. and E. Schooler, “SIP:会话初始协议”, RFC 3261,2002.6 [14] International Telecommunication Union, “对不保证质量的局域网的可视电话系统和设备”, Recommendation H.323,ITU的无线电通讯标准一节, Geneva, Switzerland, 2003.7 [15] Handley, M. and V. Jacobson, “SDP: 会话描述协议”, RFC 2327,1998.4 [16] Schulzrinne, H., Rao, A. and R. Lanphier, “实时流协议(RTSP)”, RFC 2326,1998.4 [17] Eastlake 3rd, D., Crocker, S. and J. Schiller, “关于安全性的随机化建议”, RFC 1750, 1994.12 [18] Bolot, J.-C., Turletti, T. and I. Wakeman, “因特网多播视频分布的可升级的反馈控制”,关于通信体系结构和协议的数据通信专业组讨论班(英国,伦敦), ACM,58—67页, 1994.8 [19] Busse, I., Deffner, B. and H. Schulzrinne, “基于RTP的多媒体应用的动态 QoS控制”, 计算机通讯,卷19,49—58页,1996.1 [20] Floyd, S. and V. Jacobson, “周期性路由信息的同步”,关于通信体系结构和协议的数据通信专业组讨论班 (旧金山,加利福尼亚), 33—44页, ACM,1993.9 并参见[34]. [21] Rosenberg, J. and H. Schulzrinne, “RTP中成员组的采样”, RFC 2762,2000.2 [22] Cadzow, J., “纽约数字信号处理和数据分析基础” 纽约: Macmillan, 1987. [23] Hinden, R. and S. Deering, “IPv6地址结构”, RFC 3513,2003.4 [24] Rekhter, Y., Moskowitz, B., Karrenberg, D., de Groot, G. and E.Lear, “保密因特网中的地址分配”, RFC 1918,1996.2 [25] Lear, E., Fair, E., Crocker, D. and T. Kessler, “考虑可能有害的网络10 (一些实现不应成为标准)”, RFC 1627,1994.7 [26] Feller, W.,概率论及其应用入门,卷1. 纽约: John Wiley and Sons , 1968. [27] Kent, S. and R. Atkinson, “因特网协议的安全体系”, RFC 2401,1998.11 [28] Baugher, M., Blom, R., Carrara, E., McGrew, D., Naslund, M.,Norrman, K. and D. Oran, “安全实时传输协议”,2003.4 [29] Balenson, D., “增强因特网电子邮件的保密性:第三部分”, RFC 1423,1993.2 [30] Voydock, V. and S. Kent, “高层网络协议的安全机制”, ACM 计算调查,卷15,135-171页,1983.6 [31] Floyd, S., “拥塞控制原理”, BCP 41, RFC 2914,2000.9 [32] Rivest, R., “MD5通讯――算法摘要”, RFC 1321,1992.4 [33] Stubblebine, S., “多媒体会话的安全服务”, 第16届国际安全会议,(巴尔的摩,马里兰州),391—395页,1993.9 [34] Floyd, S. and V. Jacobson, “周期路由信息同步”, IEEE/ACM 网络传输,卷2,122—136页,1994.4

webrtc-source-android

发表于 2017-07-15 | 分类于 webrtc

nativeCreateVideoSource

初始化

PeerConnectionFactory(pc/peerconnectionfactory) 创建PeerConnection方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
rtc::scoped_refptr<PeerConnectionInterface>
PeerConnectionFactory::CreatePeerConnection(
const PeerConnectionInterface::RTCConfiguration& configuration,
std::unique_ptr<cricket::PortAllocator> allocator,
std::unique_ptr<rtc::RTCCertificateGeneratorInterface> cert_generator,
PeerConnectionObserver* observer) {
RTC_DCHECK(signaling_thread_->IsCurrent());

if (!cert_generator.get()) {
// No certificate generator specified, use the default one.
cert_generator.reset(
new rtc::RTCCertificateGenerator(signaling_thread_, network_thread_));
}

if (!allocator) {
allocator.reset(new cricket::BasicPortAllocator(
default_network_manager_.get(), default_socket_factory_.get()));
}
network_thread_->Invoke<void>(
RTC_FROM_HERE, rtc::Bind(&cricket::PortAllocator::SetNetworkIgnoreMask,
allocator.get(), options_.network_ignore_mask));

rtc::scoped_refptr<PeerConnection> pc(
new rtc::RefCountedObject<PeerConnection>(this));

if (!pc->Initialize(configuration, std::move(allocator),
std::move(cert_generator), observer)) {
return nullptr;
}
return PeerConnectionProxy::Create(signaling_thread(), pc);
}

构造PeerConnection对象pc,并调用初始化方法Initialize,Initialize中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
ool PeerConnection::Initialize(
const PeerConnectionInterface::RTCConfiguration& configuration,
std::unique_ptr<cricket::PortAllocator> allocator,
std::unique_ptr<rtc::RTCCertificateGeneratorInterface> cert_generator,
PeerConnectionObserver* observer) {
TRACE_EVENT0("webrtc", "PeerConnection::Initialize");
if (!allocator) {
LOG(LS_ERROR) << "PeerConnection initialized without a PortAllocator? "
<< "This shouldn't happen if using PeerConnectionFactory.";
return false;
}
if (!observer) {
// TODO(deadbeef): Why do we do this?
LOG(LS_ERROR) << "PeerConnection initialized without a "
<< "PeerConnectionObserver";
return false;
}
observer_ = observer;
port_allocator_ = std::move(allocator);

// The port allocator lives on the network thread and should be initialized
// there.
if (!network_thread()->Invoke<bool>(
RTC_FROM_HERE, rtc::Bind(&PeerConnection::InitializePortAllocator_n,
this, configuration))) {
return false;
}

// Call must be constructed on the worker thread.
factory_->worker_thread()->Invoke<void>(
RTC_FROM_HERE, rtc::Bind(&PeerConnection::CreateCall_w,
this));

session_.reset(new WebRtcSession(
call_.get(), factory_->channel_manager(), configuration.media_config,
event_log_.get(),
factory_->network_thread(),
factory_->worker_thread(), factory_->signaling_thread(),
port_allocator_.get(),
std::unique_ptr<cricket::TransportController>(
factory_->CreateTransportController(
port_allocator_.get(),
configuration.redetermine_role_on_ice_restart)),
#ifdef HAVE_SCTP
std::unique_ptr<cricket::SctpTransportInternalFactory>(
new cricket::SctpTransportFactory(factory_->network_thread()))
#else
nullptr
#endif
));

stats_.reset(new StatsCollector(this));
stats_collector_ = RTCStatsCollector::Create(this);

// Initialize the WebRtcSession. It creates transport channels etc.
if (!session_->Initialize(factory_->options(), std::move(cert_generator),
configuration)) {
return false;
}

// Register PeerConnection as receiver of local ice candidates.
// All the callbacks will be posted to the application from PeerConnection.
session_->RegisterIceObserver(this);
session_->SignalState.connect(this, &PeerConnection::OnSessionStateChange);
session_->SignalVoiceChannelCreated.connect(
this, &PeerConnection::OnVoiceChannelCreated);
session_->SignalVoiceChannelDestroyed.connect(
this, &PeerConnection::OnVoiceChannelDestroyed);
session_->SignalVideoChannelCreated.connect(
this, &PeerConnection::OnVideoChannelCreated);
session_->SignalVideoChannelDestroyed.connect(
this, &PeerConnection::OnVideoChannelDestroyed);
session_->SignalDataChannelCreated.connect(
this, &PeerConnection::OnDataChannelCreated);
session_->SignalDataChannelDestroyed.connect(
this, &PeerConnection::OnDataChannelDestroyed);
session_->SignalDataChannelOpenMessage.connect(
this, &PeerConnection::OnDataChannelOpenMessage);

configuration_ = configuration;
return true;
}

调用CreateCall_w创建call对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void PeerConnection::CreateCall_w() {
RTC_DCHECK(!call_);

const int kMinBandwidthBps = 30000;
const int kStartBandwidthBps = 300000;
const int kMaxBandwidthBps = 2000000;

webrtc::Call::Config call_config(event_log_.get());
call_config.audio_state =
factory_->channel_manager() ->media_engine()->GetAudioState();
call_config.bitrate_config.min_bitrate_bps = kMinBandwidthBps;
call_config.bitrate_config.start_bitrate_bps = kStartBandwidthBps;
call_config.bitrate_config.max_bitrate_bps = kMaxBandwidthBps;

call_.reset(webrtc::Call::Create(call_config));
}

使用call对象以及PeerConnectionFactory中channel_manager(PeerConnectionFactory中Initialize中创建)构造WebRtcSession对象session_,调用Initialize方法初始化session_,初始化session_槽函数等.session_初始化方法中创建WebRtcSessionDescriptionFactory对象webrtc_session_desc_factory_.

创建Channel

WebRtcSession::SetLocalDescription:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
bool WebRtcSession::SetLocalDescription(SessionDescriptionInterface* desc,
std::string* err_desc) {
RTC_DCHECK(signaling_thread()->IsCurrent());

// Takes the ownership of |desc| regardless of the result.
std::unique_ptr<SessionDescriptionInterface> desc_temp(desc);

// Validate SDP.
if (!ValidateSessionDescription(desc, cricket::CS_LOCAL, err_desc)) {
return false;
}

// Update the initial_offerer flag if this session is the initial_offerer.
Action action = GetAction(desc->type());
if (state() == STATE_INIT && action == kOffer) {
initial_offerer_ = true;
transport_controller_->SetIceRole(cricket::ICEROLE_CONTROLLING);
}

if (action == kAnswer) {
current_local_description_.reset(desc_temp.release());
pending_local_description_.reset(nullptr);
current_remote_description_.reset(pending_remote_description_.release());
} else {
pending_local_description_.reset(desc_temp.release());
}

// Transport and Media channels will be created only when offer is set.
if (action == kOffer && !CreateChannels(local_description()->description())) {
// TODO(mallinath) - Handle CreateChannel failure, as new local description
// is applied. Restore back to old description.
return BadLocalSdp(desc->type(), kCreateChannelFailed, err_desc);
}

// Remove unused channels if MediaContentDescription is rejected.
RemoveUnusedChannels(local_description()->description());

if (!UpdateSessionState(action, cricket::CS_LOCAL, err_desc)) {
return false;
}
if (remote_description()) {
// Now that we have a local description, we can push down remote candidates.
UseCandidatesInSessionDescription(remote_description());
}

pending_ice_restarts_.clear();
if (error() != ERROR_NONE) {
return BadLocalSdp(desc->type(), GetSessionErrorMsg(), err_desc);
}
return true;
}

action为offer时CreateChannel创建channels:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
bool WebRtcSession::CreateChannels(const SessionDescription* desc) {
const cricket::ContentGroup* bundle_group = nullptr;
if (bundle_policy_ == PeerConnectionInterface::kBundlePolicyMaxBundle) {
bundle_group = desc->GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
if (!bundle_group) {
LOG(LS_WARNING) << "max-bundle specified without BUNDLE specified";
return false;
}
}
// Creating the media channels and transport proxies.
const cricket::ContentInfo* voice = cricket::GetFirstAudioContent(desc);
if (voice && !voice->rejected && !voice_channel_) {
if (!CreateVoiceChannel(voice,
GetBundleTransportName(voice, bundle_group))) {
LOG(LS_ERROR) << "Failed to create voice channel.";
return false;
}
}

const cricket::ContentInfo* video = cricket::GetFirstVideoContent(desc);
if (video && !video->rejected && !video_channel_) {
if (!CreateVideoChannel(video,
GetBundleTransportName(video, bundle_group))) {
LOG(LS_ERROR) << "Failed to create video channel.";
return false;
}
}

const cricket::ContentInfo* data = cricket::GetFirstDataContent(desc);
if (data_channel_type_ != cricket::DCT_NONE && data && !data->rejected &&
!rtp_data_channel_ && !sctp_transport_) {
if (!CreateDataChannel(data, GetBundleTransportName(data, bundle_group))) {
LOG(LS_ERROR) << "Failed to create data channel.";
return false;
}
}

return true;
}

CreateChannels中创建三个Channel,其中CreateVideoChannel创建视频Channel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
bool WebRtcSession::CreateVideoChannel(const cricket::ContentInfo* content,
const std::string* bundle_transport) {
bool require_rtcp_mux =
rtcp_mux_policy_ == PeerConnectionInterface::kRtcpMuxPolicyRequire;

std::string transport_name =
bundle_transport ? *bundle_transport : content->name;

cricket::DtlsTransportInternal* rtp_dtls_transport =
transport_controller_->CreateDtlsTransport(
transport_name, cricket::ICE_CANDIDATE_COMPONENT_RTP);
cricket::DtlsTransportInternal* rtcp_dtls_transport = nullptr;
if (!require_rtcp_mux) {
rtcp_dtls_transport = transport_controller_->CreateDtlsTransport(
transport_name, cricket::ICE_CANDIDATE_COMPONENT_RTCP);
}

video_channel_.reset(channel_manager_->CreateVideoChannel(
call_, media_config_, rtp_dtls_transport, rtcp_dtls_transport,
transport_controller_->signaling_thread(), content->name, SrtpRequired(),
video_options_));

if (!video_channel_) {
transport_controller_->DestroyDtlsTransport(
transport_name, cricket::ICE_CANDIDATE_COMPONENT_RTP);
if (rtcp_dtls_transport) {
transport_controller_->DestroyDtlsTransport(
transport_name, cricket::ICE_CANDIDATE_COMPONENT_RTP);
}
return false;
}

video_channel_->SignalRtcpMuxFullyActive.connect(
this, &WebRtcSession::DestroyRtcpTransport_n);
video_channel_->SignalDtlsSrtpSetupFailure.connect(
this, &WebRtcSession::OnDtlsSrtpSetupFailure);

SignalVideoChannelCreated();
video_channel_->SignalSentPacket.connect(this,
&WebRtcSession::OnSentPacket_w);
return true;
}

调用channel_manager的CreateVideoChannel创建BaseChannel基类的cricket::VideoChannel. VideoChannel需要传入VideoMediaChannel作为构造参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//pc/channelmanager.h/cc
VideoChannel* ChannelManager::CreateVideoChannel_w(
webrtc::Call* call,
const cricket::MediaConfig& media_config,
DtlsTransportInternal* rtp_dtls_transport,
DtlsTransportInternal* rtcp_dtls_transport,
rtc::PacketTransportInternal* rtp_packet_transport,
rtc::PacketTransportInternal* rtcp_packet_transport,
rtc::Thread* signaling_thread,
const std::string& content_name,
bool srtp_required,
const VideoOptions& options) {
RTC_DCHECK(initialized_);
RTC_DCHECK(worker_thread_ == rtc::Thread::Current());
RTC_DCHECK(nullptr != call);
VideoMediaChannel* media_channel = media_engine_->CreateVideoChannel(
call, media_config, options);
if (media_channel == NULL) {
return NULL;
}

VideoChannel* video_channel = new VideoChannel(
worker_thread_, network_thread_, signaling_thread, media_channel,
content_name, rtcp_packet_transport == nullptr, srtp_required);
if (!video_channel->Init_w(rtp_dtls_transport, rtcp_dtls_transport,
rtp_packet_transport, rtcp_packet_transport)) {
delete video_channel;
return NULL;
}
video_channels_.push_back(video_channel);
return video_channel;
}

VideoMediaChannel实例media_channel由MediaEngineInterface对象media_engine创建,media_engine由ChannelManager构造方法传入并初始化,ChannelManager由PeerConnectionFactory创建,在PeerConnection初始化方法中,media_engine被创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//pc/peerconnectionfactory.cc
bool PeerConnectionFactory::Initialize() {
RTC_DCHECK(signaling_thread_->IsCurrent());
rtc::InitRandom(rtc::Time32());

default_network_manager_.reset(new rtc::BasicNetworkManager());
if (!default_network_manager_) {
return false;
}

default_socket_factory_.reset(
new rtc::BasicPacketSocketFactory(network_thread_));
if (!default_socket_factory_) {
return false;
}

std::unique_ptr<cricket::MediaEngineInterface> media_engine =
worker_thread_->Invoke<std::unique_ptr<cricket::MediaEngineInterface>>(
RTC_FROM_HERE,
rtc::Bind(&PeerConnectionFactory::CreateMediaEngine_w, this));

channel_manager_.reset(new cricket::ChannelManager(
std::move(media_engine), worker_thread_, network_thread_));

channel_manager_->SetVideoRtxEnabled(true);
if (!channel_manager_->Init()) {
return false;
}

return true;
}

std::unique_ptr<cricket::MediaEngineInterface>
PeerConnectionFactory::CreateMediaEngine_w() {
RTC_DCHECK(worker_thread_ == rtc::Thread::Current());
return std::unique_ptr<cricket::MediaEngineInterface>(
cricket::WebRtcMediaEngineFactory::Create(
default_adm_.get(), audio_encoder_factory_,
audio_decoder_factory_,
video_encoder_factory_.get(), video_decoder_factory_.get(),
external_audio_mixer_));
}

WebRtcMediaEngine2继承自CompositeMediaEngine,CompositeMediaEngine父类MediaEngineInterface有WebRtcVoiceEngine voice与WebRtcVideoEngine2 video两个对象

WebRtcVideoEngine2

WebRtcVideoEngine2定义在media/engine/webrtcvideoengine2.h下,用于创建WebRtcVideoChannel2(定义在同一头文件),WebRtcVideoChannel2定义了WebRtcVideoSendStream与WebRtcVideoReceiveStream两个内部类.

WebRtc源码分析(1) PeerConnection

发表于 2017-07-11 | 分类于 webrtc

ChannelManager

pc/channelmanager.h

1
2
3
4
5
6
7
8
// ChannelManager allows the MediaEngine to run on a separate thread, and takes
// care of marshalling calls between threads. It also creates and keeps track of
// voice and video channels; by doing so, it can temporarily(暂时的) pause all the
// channels when a new audio or video device is chosen. The voice and video
// channels are stored in separate vectors, to easily allow operations on just
// voice or just video channels.
// ChannelManager also allows the application to discover what devices it has
// using device manager.

WebRtcSession构造中通过MediaControllerInterface初始化ChannelManager变量channel_manager_ , ChannelManager通过构造传入MediaEngineInterface.

WebRtcSession

pc/webrtcsession.h

1
2
3
4
5
6
7
// A WebRtcSession manages general session state. This includes negotiation
// of both the application-level and network-level protocols: the former
// defines what will be sent and the latter defines how it will be sent. Each
// network-level protocol is represented by a Transport object. Each Transport
// participates in the network-level negotiation. The individual streams of
// packets are represented by TransportChannels. The application-level protocol
// is represented by SessionDecription objects.

MediaControllerInterface

pc/mediacontroller.h

1
2
3
// The MediaController currently owns shared state between media channels.
// Abstract interface is defined here such that it can be faked/mocked for
// tests, but no other real reason.

实现类MediaController,管理ChannelManager,cricket::ChannelManager* const channel_manager_;,在PeerConnection的Initialize方法中,通过PeerConnectionFactory创建.PeerConnectionFactory中Initialize中真正创建ChannelManager,创建ChannelManager之前,先创建出MediaEngine,实际在PeerConnectionFactory::CreateMediaEngine_w中通过cricket::WebRtcMediaEngineFactory::Create创建.

7> Downloading src/resources/voice_engine/audio_tiny44.wav… 4> Downloading src/resources/voice_engine/audio_tiny48.wav… 2> Downloading src/resources/voice_engine/audio_tiny8.wav… Hook ‘download_from_google_storage –directory –recursive –num_threads=10 –no_auth –quiet –bucket chromium-webrtc-resources src/resources’ took 528.97 secs

WARNING: ‘src/testing/gmock’ has been moved from DEPS to a higher level checkout. The git folder containing all the local branches has been saved to /Users/shenjunwei/soft-source/webrtc/old_src_testing_gmock.git. If you don’t care about its state you can safely remove that folder to free up space.

WARNING: ‘src/testing/gtest’ has been moved from DEPS to a higher level checkout. The git folder containing all the local branches has been saved to /Users/shenjunwei/soft-source/webrtc/old_src_testing_gtest.git. If you don’t care about its state you can safely remove that folder to free up space.

关于内存对齐那些事

发表于 2017-07-09 | 分类于 Memory

Wrote by mutouyun.

1. 内存对齐(Data Structure Alignment)是什么

内存对齐,或者说字节对齐,是一个数据类型所能存放的内存地址的属性(Alignment is a property of a memory address)。 这个属性是一个无符号整数,并且这个整数必须是2的N次方(1、2、4、8、……、1024、……)。 当我们说,一个数据类型的内存对齐为8时,意思就是指这个数据类型所定义出来的所有变量,其内存地址都是8的倍数。

当一个基本数据类型(fundamental types)的对齐属性,和这个数据类型的大小相等时,这种对齐方式称作自然对齐(naturally aligned)。 比如,一个4字节大小的int型数据,默认情况下它的字节对齐也是4。

2. 为什么我们需要内存对齐

这是因为,并不是每一个硬件平台都能够随便访问任意位置的内存的。 微软的MSDN里有这样一段话:

Many CPUs, such as those based on Alpha, IA-64, MIPS, and SuperH architectures, refuse to read misaligned data. When a program requests that one of these CPUs access data that is not aligned, the CPU enters an exception state and notifies the software that it cannot continue. On ARM, MIPS, and SH device platforms, for example, the operating system default is to give the application an exception notification when a misaligned access is requested.

大意是说,有不少平台的CPU,比如Alpha、IA-64、MIPS还有SuperH架构,若读取的数据是未对齐的(比如一个4字节的int在一个奇数内存地址上),将拒绝访问,或抛出硬件异常。

另外,在维基百科里也记载着如下内容:

Data alignment means putting the data at a memory offset equal to some multiple of the word size, which increases the system’s performance due to the way the CPU handles memory.

意思是,考虑到CPU处理内存的方式(32位的x86 CPU,一个时钟周期可以读取4个连续的内存单元,即4字节),使用字节对齐将会提高系统的性能(也就是CPU读取内存数据的效率。比如你一个int放在奇数内存位置上,想把这4个字节读出来,32位CPU就需要两次。但对齐之后一次就可以了)。

3. 内存对齐带来的数据结构大小变化

因为有了内存对齐,因此数据在内存里的存放就不是紧挨着的,而是可能会出现一些空隙(Data Structure Padding,也就是用于填充的空白内容)。因此对基本数据类型来说可能还好说,对于一个内部有多个基本类型的结构体(struct)或类而言,sizeof的结果往往和想象中不大一样。

让我们来看一个例子:

1
2
3
4
5
6
7
8
struct MyStruct  
{
char a; // 1 byte
int b; // 4 bytes
short c; // 2 bytes
long long d; // 8 bytes
char e; // 1 byte
};

我们可以看到,MyStruct中有5个成员,如果直接相加的话大小应该是16,但在32位MSVC里它的sizeof结果是32。 之所以结果出现偏差,为了保证这个结构体里的每个成员都应该在它对齐了的内存位置上,而在某些位置插入了Padding。

下面我们尝试考虑内存对齐,来计算一下这个结构体的大小。首先,我们可以假设MyStruct的整体偏移从0x00开始,这样就可以暂时忽略MyStruct本身的对齐。这时,结构体的整体内存分布如下图所示: iamge 我们可以看到,char和int之间;short和long long之间,为了保证成员各自的对齐属性,分别插入了一些Padding。 因此整个结构体会被填充得看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12

struct MyStruct
{
char a; // 1 byte
char pad_0[3]; // Padding 3
int b; // 4 bytes
short c; // 2 bytes
char pad_1[6]; // Padding 6
long long d; // 8 bytes
char e; // 1 byte
char pad_2[7]; // Padding 7
};

注意到上面加了Padding的示意结构体里,e的后面还跟了7个字节的填充。这是因为结构体的整体大小必须可被对齐值整除,所以“char e”的后面还会被继续填充7个字节好让结构体的整体大小是8的倍数32。

我们可以在gcc + 32位linux中尝试计算sizeof(MyStruct),得到的结果是24。 这是因为gcc中的对齐规则和MSVC不一样,不同的平台下会使用不同的默认对齐值(The default alignment is fixed for a particular target ABI)。在gcc + 32位linux中,大小超过4字节的基本类型仍然按4字节对齐。因此MyStruct的内存布局这时看起来应该像这个样子: image

下面我们来确定这个结构体类型本身的内存对齐是多少。为了保证结构体内的每个成员都能够放在它自然对齐的位置上,对这个结构体本身来说最理想的内存对齐数值应该是结构体里内存对齐数值最大的成员的内存对齐数。 也就是说,对于上面的MyStruct,结构体类型本身的内存对齐应该是8。并且,当我们强制对齐方式小于8时,比如设置MyStruct对齐为2,那么其内部成员的对齐也将被强制不能超过2。

为什么?因为对于一个数据类型来说,其内部成员的位置应该是相对固定的。假如上面这个结构体整体按1或者2字节对齐,而成员却按照各自的方式自然对齐,就有可能出现成员的相对偏移量随内存位置而改变的问题。 比如说,我们可以画一下整个结构体按1字节对齐,并且结构体内的每个成员按自然位置对齐的内存布局:

image

上面的第一种情况,假设MyStruct的起始地址是0x01(因为结构体本身的偏移按1字节对齐),那么char和int之间将会被填充2个字节的Padding,以保证int的对齐还是4字节。 如果第二次分配MyStruct的内存时起始地址变为0x03,由于int还是4字节对齐,则char和int之间将不会填充Padding(填充了反而不对齐了)。 以此类推,若MyStruct按1字节对齐时不强制所有成员的对齐均不超过1的话,这个结构体里的相对偏移方式一共有4种。

因此对于结构体来说,默认的对齐将等于其中对齐最大的成员的对齐值。并且,当我们限定结构体的内存对齐时,同时也限定了结构体内所有成员的内存对齐不能超过结构体本身的内存对齐。

4. 指定内存对齐

在C++98/03里,对内存对齐的操作在不同的编译器里可能有不同的方法。

在MSVC中,一般使用#progma pack来指定内存对齐:

1
2
3
4
5
6
7
8
9
10
#pragma pack(1) // 指定后面的内容内存对齐为1  
struct MyStruct
{
char a; // 1 byte
int b; // 4 bytes
short c; // 2 bytes
long long d; // 8 bytes
char e; // 1 byte
};
#pragma pack() // 还原默认的内存对齐

这时,MyStruct由于按1字节对齐,其中的所有成员都将变为1字节对齐,因此sizeof(MyStruct)将等于16。 还有另外一个简单的方法:

1
2
3
4
5
6
7
8
__declspec(align(64)) struct MyStruct  
{
char a; // 1 byte
int b; // 4 bytes
short c; // 2 bytes
long long d; // 8 bytes
char e; // 1 byte
};

__declspec(align(64))将指定内存对齐为64。比较坑的是,这种方法不能指定内存对齐小于默认对齐,也就是说它只能调大不能调小(__declspec(align(#)) can only increase alignment restrictions)。因此下面这样写会忽略掉declspec:

1
2
__declspec(align(1)) struct MyStruct // ...  
// warning C4359: 'MyStruct': Alignment specifier is less than actual alignment (8), and will be ignored.

微软的__declspec(align(#)),其#的内容可以是预编译宏,但不能是编译期数值:

1
2
3
4
5
6
7
8
#define XX 32  
struct __declspec(align(XX)) MyStruct_1 {}; // OK

template <size_t YY>
struct __declspec(align(YY)) MyStruct_2 {}; // error C2059: syntax error: 'identifier'

static const unsigned ZZ = 32;
struct __declspec(align(ZZ)) MyStruct_3 {}; // error C2057: expected constant expression

在Visual C++ Compiler November 2013 CTP之后,微软终于支持编译期数值的写法了:

1
2
3

template <size_t YY>
struct __declspec(align(YY)) MyStruct_2 {}; // OK in 2013 CTP

__declspec(align(#))最大支持对齐为8192(Valid entries are integer powers of two from 1 to 8192)。

下面再来看gcc。gcc和MSVC一样,可以使用#pragma pack:

1
2
3
4
5
6
#pragma pack(1)  
struct MyStruct
{
// ...
};
#pragma pack()

另外,也可以使用__attribute__((__aligned__((#)))):

1
2
3
4
5
6
7
8
9
struct __attribute__((__aligned__((1)))) MyStruct_1  
{
// ...
};

struct MyStruct_2
{
// ...
} __attribute__((__aligned__((1))));

这东西写上面写下面都是可以的,但是不能写在struct前面。 和MSVC一样,__attribute__也只能把字节对齐改大,不能改小(The aligned attribute can only increase the alignment)。比较坑的是当你试图改小的时候,gcc没有任何编译提示信息。 gcc可以接受一个宏或编译期数值:

1
2
3
4
5
6
7
8
9
10
#define XX 1  
struct __attribute__((__aligned__((XX)))) MyStruct_1 {}; // OK

template <size_t YY>
struct __attribute__((__aligned__((YY)))) MyStruct_2 {}; // OK

static const unsigned ZZ = 1;
struct __attribute__((__aligned__((ZZ)))) MyStruct_3 {};
// ^
// error: requested alignment is not an integer constant

gcc的__attribute__((__aligned__((#))))支持的上限受限于链接器(Note that the effectiveness of aligned attributes may be limited by inherent limitations in your linker)。

5. 获得内存对齐

同样的,在C++98/03里,不同的编译器可能有不同的方法来获得一个类型的内存对齐。

MSVC使用__alignof操作符获得内存对齐大小:

1
2
3
MyStruct xx;  
std::cout << __alignof(xx) << std::endl;
std::cout << __alignof(MyStruct) << std::endl;

gcc则使用__alignof__:

1
2
3
MyStruct xx;  
std::cout << __alignof__(xx) << std::endl;
std::cout << __alignof__(MyStruct) << std::endl;

需要注意的是,不论是__alignof还是__alignof__,对于对齐的计算都发生在编译期。因此像下面这样写:

1
2
3
int a;  
char& c = reinterpret_cast<char&>(a);
std::cout << __alignof__(c) << std::endl;

得到的结果将是1。

如果需要在运行时动态计算一个变量的内存对齐,比如根据一个void*指针指向的内存地址来判断这个地址的内存对齐是多少,我们可以用下面这个简单的方法:

1
2
3
4
__declspec(align(128)) long a = 0;  
size_t x = reinterpret_cast<size_t>(&a);
x &= ~(x - 1); // 计算a的内存对齐大小
std::cout << x << std::endl;

用这种方式得到的内存对齐大小可能比实际的大,因为它是切实的获得这个内存地址到底能被多大的2^N整除。

6. 堆内存的内存对齐

我们在讨论内存对齐的时候很容易忽略掉堆内存。我们经常会使用malloc分配内存,却不理会这块内存的对齐方式,仿佛堆内存不需要考虑内存对齐一样。 实际上,malloc一般使用当前平台默认的最大内存对齐数对齐内存。比如MSVC在32位下一般是8字节对齐;64位下则是16字节(In Visual C++, the fundamental alignment is the alignment that’s required for a double, or 8 bytes. In code that targets 64-bit platforms, it’s 16 bytes)。这样对于常规的数据都是没有问题的。 但是如果我们自定义的内存对齐超出了这个范围,则是不能直接使用malloc来获取内存的。

当我们需要分配一块具有特定内存对齐的内存块时,在MSVC下应当使用_aligned_malloc;而在gcc下一般使用memalign等函数。

其实自己实现一个简易的aligned_malloc是很容易的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <assert.h>  

inline void* aligned_malloc(size_t size, size_t alignment)
{
// 检查alignment是否是2^N
assert(!(alignment & (alignment - 1)));
// 计算出一个最大的offset,sizeof(void*)是为了存储原始指针地址
size_t offset = sizeof(void*) + (--alignment);

// 分配一块带offset的内存
char* p = static_cast<char*>(malloc(offset + size));
if (!p) return nullptr;

// 通过“& (~alignment)”把多计算的offset减掉
void* r = reinterpret_cast<void*>(reinterpret_cast<size_t>(p + offset) & (~alignment));
// 将r当做一个指向void*的指针,在r当前地址前面放入原始地址
static_cast<void**>(r)[-1] = p;
// 返回经过对齐的内存地址
return r;
}

inline void aligned_free(void* p)
{
// 还原回原始地址,并free
free(static_cast<void**>(p)[-1]);
}

7. C++11中对内存对齐的操作

C++11标准里统一了内存对齐的相关操作。

指定内存对齐使用alignas说明符:

1
2
3
4
5
6
7
8
9
10
alignas(32) long long a = 0;  

#define XX 1
struct alignas(XX) MyStruct_1 {}; // OK

template <size_t YY = 1>
struct alignas(YY) MyStruct_2 {}; // OK

static const unsigned ZZ = 1;
struct alignas(ZZ) MyStruct_3 {}; // OK

注意到MyStruct_3编译是OK的。在C++11里,只要是一个编译期数值(包括static const)都支持alignas(the assignment-expression shall be an integral constant expression,参考ISO/IEC-14882:2011,7.6.2 Alignment specifier,第2款)。 但是需要小心的是,目前微软的编译器(Visual C++ Compiler November 2013 CTP)在MyStruct_3的情况下仍然会报error C2057。 另外,alignas同前面介绍的__declspec、__attribute__一样,只能改大不能改小(参考ISO/IEC-14882:2011,7.6.2 Alignment specifier,第5款)。如果需要改小,比如设置对齐为1的话,仍然需要使用#pragma pack。或者,可以使用C++11里#pragma的等价物_Pragma(微软暂不支持这个):

1
2
3
4
5
6
7
8
9
10
_Pragma("pack(1)")  
struct MyStruct
{
char a; // 1 byte
int b; // 4 bytes
short c; // 2 bytes
long long d; // 8 bytes
char e; // 1 byte
};
_Pragma("pack()")

除了这些之外,alignas比__declspec、这个char就按int的方式对齐了。 获取内存对齐使用alignof操作符:强大的地方在于它还可以这样用:

1
alignas(int) char c;

这个char就按int的方式对齐了。 获取内存对齐使用alignof操作符:

1
2
3
MyStruct xx;  
std::cout << alignof(xx) << std::endl;
std::cout << alignof(MyStruct) << std::endl;

相关注意点和前面介绍的__alignof、__alignof__并无二致。 除了alignas和alignof,C++11中还提供了几个有用的工具。

A. std::alignment_of

功能是编译期计算类型的内存对齐。 std里提供这个是为了补充alignof的功能。alignof只能返回一个size_t,而alignment_of则继承自std::integral_constant,因此拥有value_type、type、operator()等接口(或者说操作)。

B. std::aligned_storage

这是个好东西。我们知道,很多时候需要分配一块单纯的内存块,比如new char[32],之后再使用placement new在这块内存上构建对象:

1
2
char xx[32];  
::new (xx) MyStruct;

但是char[32]是1字节对齐的,xx很有可能并不在MyStruct指定的对齐位置上。这时调用placement new构造内存块,可能会引起效率问题或出错,这时我们应该使用std::aligned_storage来构造内存块:

1
2
std::aligned_storage<sizeof(MyStruct), alignof(MyStruct)>::type xx;  
::new (&xx) MyStruct;

需要注意的是,当使用堆内存的时候我们可能还是需要aligned_malloc。因为现在的编译器里new并不能在超出默认最大对齐后,还能保证内存的对齐是正确的。比如在MSVC 2013里,下面的代码:

1
2
3
4
5
6
7
8
9
10
11
struct alignas(32) MyStruct  
{
char a; // 1 byte
int b; // 4 bytes
short c; // 2 bytes
long long d; // 8 bytes
char e; // 1 byte
};

void* p = new MyStruct;
// warning C4316: 'MyStruct' : object allocated on the heap may not be aligned 32

将会得到一个编译警告。

C. std::max_align_t

返回当前平台的最大默认内存对齐类型。malloc返回的内存,其对齐和max_align_t类型的对齐大小应当是一致的。 我们可以通过下面这个方式获得当前平台的最大默认内存对齐数:

1
2

std::cout << alignof(std::max_align_t) << std::endl;

D. std::align

这货是一个函数,用来在一大块内存当中获取一个符合指定内存要求的地址。 看下面这个例子:

1
2
3
4
char buffer[] = "------------------------";  
void * pt = buffer;
std::size_t space = sizeof(buffer) - 1;
std::align(alignof(int), sizeof(char), pt, space);

意思是,在buffer这个大内存块中,指定内存对齐为alignof(int),找一块sizeof(char)大小的内存,并在找到这块内存后,将地址放入pt,将buffer从pt开始的长度放入space。

关于这个函数的更多信息,可以参考这里。

关于内存对齐,该说的就是这么多了。我们经常会看到内存对齐的应用,是在网络收发包中。一般用于发送的结构体,都是1字节对齐的,目的是统一收发双方(可能处于不同平台)之间的数据内存布局,以及减少不必要的流量消耗。

C++11中为我们提供了不少有用的工具,可以让我们方便的操作内存对齐。但是在堆内存方面,我们很可能还是需要自己想办法。不过在平时的应用中,因为很少会手动指定内存对齐到大于系统默认的对齐数,所以倒也不比每次new/delete的时候都提心吊胆。


参考文章:

  1. Data structure alignment
  2. About Data Alignment
  3. #pragma pack
  4. align (C++)
  5. __alignof Operator
  6. 6.57.8 Structure-Packing Pragmas
  7. 5.32 Specifying Attributes of Types
  8. C/C++ Data alignment 及 struct size深入分析
  9. C++ 内存对齐
  10. 结构/类对齐的声明方式
  11. 字节对齐(强制对齐以及自然对齐)
  12. malloc函数字节对齐很经典的问题
  13. C语言字节对齐
  14. 网络编程(9)内存对齐对跨平台通讯的影响
  15. Usage Issue of std::align
  16. std::align and std::aligned_storage for aligned allocation of memory blocks

http://www.cnblogs.com/fangkm/p/4370492.html

WebRTC的模块处理机制

发表于 2017-07-06 | 分类于 webrtc

对于实时音视频应用来讲,媒体数据从采集到渲染,在数据流水线上依次完成一系列处理。流水线由不同的功能模块组成,彼此分工协作:数据采集模块负责从摄像头/麦克风采集音视频数据,编解码模块负责对数据进行编解码,RTP模块负责数据打包和解包。数据流水线上的数据处理速度是影响应用实时性的最重要因素。与此同时,从服务质量保证角度讲,应用需要知道数据流水线的运行状态,如视频采集模块的实时帧率、当前网络的实时速率、接收端的数据丢包率,等等。各个功能模块可以基于这些运行状态信息作相应调整,从而在质量、速度等方面优化数据流水线的运行,实现更快、更好的用户体验。

WebRTC采用模块机制,把数据流水线上功能相对独立的处理点定义为模块,每个模块专注于自己的任务,模块之间基于数据流进行通信。与此同时,专有线程收集和处理模块内部的运行状态信息,并把这些信息反馈到目标模块,实现模块运行状态监控和服务质量保证。本文在深入分析WebRTC源代码基础上,学习研究其模块处理机制的实现细节,从另一个角度理解WebRTC的技术原理。

1 WebRTC数据流水线

我们可以把WebRTC看作是一个专注于实时音视频通信的SDK。其对外的API主要负责PeerConnection建立、MediaStream创建、NAT穿透、SDP协商等工作,对内则主要集中于音视频数据的处理,从数据采集到渲染的整个处理过程可以用一个数据流水线来描述,如图1所示。 图1 WebRTC音视频数据流水线

音视频数据首先从采集端进行采集,一般来说音频数据来自麦克风,视频数据来自摄像头。在某些应用场景下,音频数据来自扬声器,视频数据来自桌面共享。采集端的输出是音视频Raw Data。然后Raw Data到达编码模块,数据被编码器编码成符合语法规则的NAL单元,到达发送端缓冲区PacedSender处。接下来PacedSender把NAL单元发送到RTP模块打包为RTP数据包,最后经过网络模块发送到网络。

在接收端,RTP数据包经过网络模块接收后进行解包得到NAL单元,接下来NAL单元到达接收端缓冲区(JitterBuffer或者NetEQ)进行乱序重排和组帧。一帧完整的数据接收并组帧之后,调用解码模块进行解码,得到该帧数据的Raw Data。最后Raw Data交给渲染模块进行播放/显示。

在数据流水线上,还有一系列模块负责服务质量监控,如丢帧策略,丢包策略,编码器过度使用保护,码率估计,前向纠错,丢包重传,等等。

WebRTC数据流水线上的功能单元被定义为模块,每个模块从上游模块获取输入数据,在本模块进行加工后得到输出数据,交给下游模块进行下一步处理。WebRTC的模块处理机制包括模块和模块处理线程,前者把WebRTC数据流水线上的功能部件封装为模块,后者驱动模块内部状态更新和模块之间状态传递。模块一般挂载到模块处理线程上,由处理线程驱动模块的处理函数。下面分别描述之。

WebRTC模块

WebRTC模块虚基类Module定义在webrtc/modules/include/modue.h中,如图2所示。 图2 模块虚基类Module定义

Module虚基类对外提供三个函数作为API:TimeUntilNextProcess()用来计算距下次调用处理函数Process()的时间间隔;Process()是模块的处理函数,负责模块内部运行监控、状态更新和模块间通信;ProcessThreadAttached()用来把模块挂载到模块处理线程,或者从模块处理线程分离出来,实际实现中这个函数暂时没有被用到。

Module的派生类分布在WebRTC数据流水线上的不同部分,各自承担自己的数据处理和服务质量保证任务。

3 WebRTC模块处理线程

WebRTC模块处理线程是模块处理机制的驱动器,它的核心作用是对所有挂载在本线程下的模块,周期性调用其Process()处理函数处理模块内部事务,并处理异步任务。其虚基类ProcessThread和派生类ProcessThreadImpl如图3所示。

图3 模块处理线程虚基类ProcessThread及派生类ProcessThreadImpl

ProcessThread基类提供一系列API完成线程功能:Start()/Stop()函数用来启动和结束线程;WakeUp()函数用来唤醒挂载在本线程下的某个模块,使得该模块有机会马上执行其Process()处理函数;PostTask()函数用来邮递一个任务给本线程;RegisterModule()和DeRegisterModule()用来向线程注册/注销模块。

WebRTC基于ProcessThread线程实现派生类ProcessThreadImpl,如图3所示。在成员变量方面,wake_up_用来唤醒处于等待状态的线程;thread_是平台相关的线程实现如POSIX线程;modules_是注册在本线程下的模块集合;queue_是邮递给本线程的任务集合;thread_name_是线程名字。在成员函数方面,Process()完成ProcessThread的核心任务,其伪代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool ProcessThreadImpl::Process() {
for (ModuleCallback& m : modules_) {
if (m.next_callback == 0)
m.next_callback = GetNextCallbackTime(m.module, now);
if (m.next_callback <= now || m.next_callback == kCallProcessImmediately) {
m.module->Process();
m.next_callback = GetNextCallbackTime(m.module, rtc::TimeMillis(););
}
if (m.next_callback < next_checkpoint)
next_checkpoint = m.next_callback;
}
while (!queue_.empty()) {
ProcessTask* task = queue_.front();
queue_.pop();
task->Run();
delete task;
}
}
int64_t time_to_wait = next_checkpoint - rtc::TimeMillis();
if (time_to_wait > 0)
wake_up_->Wait(static_cast<unsigned long>(time_to_wait));
return true;
}

Process()函数首先处理挂载在本线程下的模块,这也是模块处理线程的核心任务:针对每个模块,计算其下次调用模块Process()处理函数的时刻(调用该模块的TimeUntilNextProcess()函数)。如果时刻是当前时刻,则调用模块的Process()处理函数,并更新下次调用时刻。需要注意,不同模块的执行频率不一样,线程在本轮调用末尾的等待时间和本线程下所有模块的最近下次调用时刻相关。

接下来线程Process()函数处理ProcessTask队列中的异步任务,针对每个任务调用Run()函数,然后任务出队列并销毁。等模块调用和任务都处理完后,则把本次时间片的剩余时间等待完毕,然后返回。如果在等待期间其他线程向本线程Wakeup模块或者邮递一个任务,则线程被立即唤醒并返回,进行下一轮时间片的执行。

至此,关于WebRTC的模块和模块处理线程的基本实现分析完毕,下一节将对WebRTC SDK内模块实例和模块处理线程实例进行详细分析。

4 WebRTC模块处理线程实例

WebRTC关于模块和处理线程的实现在webrtc/modules目录下,该目录汇集了所有派生类模块和模块处理线程的实现及实例分布。本节对这些内容进行总结。

WebRTC目前创建三个ProcessThreadImpl线程实例,分别是负责处理音频的VoiceProcessTread,负责处理视频和音视频同步的ModuleProcessThread,以及负责数据平滑发送的PacerThread。这三个线程和挂载在线程下的模块如图4所示。 图4 模块处理线程实例

VoiceProcessThread线程由Worker线程在创建VoiceEngine时创建,负责音频端模块的处理。挂载在该线程下的模块如图4所示,其中MonitorModule负责对音频数据混音处理过程中产生的警告和错误进行处理,AudioDeviceModuleImpl负责对音频设备采集和播放音频数据时产生的警告和错误进行处理,ModuleRtpRtcpImpl负责音频RTP数据包发送过程中的码率计算、RTT更新、RTCP报文发送等内容。

ModuleProcessThread线程由Worker线程在创建VideoChannel时创建,负责视频端模块的处理。挂载在该线程下的模块如图4所示,其中CallStats负责Call对象统计数据(如RTT)的更新,CongestionController负责拥塞控制[1][2],VideoSender负责视频发送端统计数据的更新,VideoReceiver负责视频接收端统计数据更新和处理状态反馈(如请求关键帧),ModuleRtpRtcpImpl负责视频RTP数据包发送过程中的码率计算、RTT更新、RTCP报文发送等内容,OveruseFrameDetector负责视频帧采集端过载监控,ReceiveStatisticsImpl负责由接收端统计数据触发的码率更新过程,ViESyncModule负责音视频同步。

PacerThread线程由Worker线程在创建VideoChannel时创建,负责数据平滑发送。挂载在该线程下的PacedSender负责发送端数据平滑发送;RemoteEstimatorProxy派生自RemoteBitrateEstimator,负责在启用发送端码率估计的情况下把接收端收集到的反馈信息发送回发送端。

由以上分析可知,WebRTC创建的模块处理线程实例基本上涵盖了音视频数据从采集到渲染过程中的大部分数据操作。但还有一些模块不依赖于模块线程工作,这部分模块是少数,本文不展开具体的描述。

参考文献

[1] WebRTC基于GCC的拥塞控制(上) – 算法分析 [2] WebRTC基于GCC的拥塞控制(下) - 实现分析

转至

Android应用性能优化

发表于 2017-07-03 | 分类于 Android

Android手机由于其本身的后台机制和硬件特点,性能上一直被诟病,所以软件开发者对软件本身的性能优化就显得尤为重要;本文将对Android开发过程中性能优化的各个方面做一个回顾与总结。

Cache优化

  • ListView缓存:

    • ListView中有一个回收器,Item滑出界面的时候View会回收到这里,需要显示新的Item的时候,就尽量重用回收器里面的View;每次在getView函数中inflate新的item之前会先判断回收器中是否有缓存的view,即判断convertView是否为null,是则inflate一个新的item View,否则重用回收器中的item。
    • 此外,ListView还使用静态的ViewHolder减少findViewById的次数
    • ListView中有getViewTypeCount()函数用于获取列表有几种布局类型,getItemViewType(int position)用于获取在position位置上的布局类型; 我们可以利用ViewType来给不同类型的item创建不同的View,这样可以利于ListView的回收
    • 对Item中图片进行适当压缩, 并进行异步加载;如果快速滑动,不加载图片;实现数据的分页加载
  • IO缓存:在文件和网络IO中使用具有缓存策略的输入流,BufferedInputStream替代InputStream,BufferedReader替代Reader,BufferedReader替代BufferedInputStream

  • data缓存(空间换时间):①缓存数据库的查询结果,比如缓存数据库表中的数据条数,这样就可以避免多次的select count查询 ②缓存磁盘文件中需要频繁访问的数据到内存中 ③缓存耗时计算的结果

Battery优化

  • cpu的使用率和使用频率将直接或间接的影响电量的分配和使用,cpu降频可以节约电量

  • service优化

    • service作为一个运行在主线程中的后台服务,应该尽量避免耗时动作,而应该尽量新开线程去处理耗时动作 监听系统广播看service是否存活,否则kill掉;降低service优先级使得系统内存吃紧时会被自动kill掉

    • 使用Android提供的IntentService代替service,因为IntentService会在运行完成之后自动停止,而service需要手动调用stopService()才能停止运行

    • 定时执行任务的Alarm机制:Android的定时任务有两种实现方式,Timer类和Alarm机制;Timer不适合长期后台运行的定时任务。因为每种手机都会有自己的休眠策略,Android手机就会在长时间不操作的情况下自动让CPU进入到睡眠状态,这就有可能导致Timer中的定时任务无法正常运行。而Alarm机制则不存在这种情况,它具有唤醒CPU的功能,即可以保证每次需要执行定时任务的时候CPU能正常工作。然而从Android4.4之后,Alarm任务的触发时间将会变得不准确,有可能会延迟一段时间后任务才能得到执行。这不是bug,而是系统在耗电性方面进行的优化。系统会自动检测目前有多少Alarm任务存在,然后将触发时间将近的几个任务放在一起执行,这就可以大幅度的减少CPU被唤醒的次数,从而有效延长电池的使用时间

渲染层优化

  • Android 界面卡顿的原因?

    • UI线程中做耗时操作,比如进行网络请求,磁盘读取,位图修改,更新UI等耗时操作,从而导致UI线程卡顿
    • 布局Layout过于复杂,无法在16ms内完成渲染,或者嵌套层次过深
    • View过度绘制或者频繁的触发measure、layout,同一时间动画执行的次数过多,导致CPU或GPU负载过重
    • 冗余资源及逻辑等导致加载和执行缓慢
  • Android 界面卡顿怎么处理?

    • xml布局优化:尽量使用include、merge、ViewStub标签,尽量不存在冗余嵌套及过于复杂布局(譬如10层就会直接异常),例如使用RelativeLayout代替LinearLayout可以减少布局层次和复杂性,View的嵌套层次不能过深,尽量使用GONE替换INVISIBLE,使用weight后尽量将width和heigh设置为0dp,减少运算,Item存在非常复杂的嵌套时考虑使用自定义Item View来取代,减少measure与layout次数等。
    • ListView及Adapter优化;尽量复用getView方法中的相关View,不重复获取实例导致卡顿,列表尽量在滑动过程中不进行UI元素刷新等。
    • 背景和图片等内存分配优化;尽量减少不必要的背景设置,图片尽量压缩处理显示,尽量避免频繁内存抖动等问题出现;尽可能为不同分辨率创建资源,以减少不必要的硬件缩放
    • 自定义View等绘图与布局优化;尽量避免在draw、measure、layout中做过于耗时及耗内存操作,尤其是draw方法中,尽量减少draw、measure、layout等执行次数,避免过度渲染和绘制;减少不必要的inflate,尽量使用全局变量缓存View
    • 避免ANR,不要在UI线程中做耗时操作,譬如多次数据库操作等
  • Layout常用的标签

    • include标签:该标签可以用于将布局文件中的公共部分提取出来给其它布局文件复用,从而使得布局模块化,代码轻量化; 注意点: ①如果标签已经定义了id,而嵌入布局文件的root布局文件也定义了id,标签的id会覆盖掉嵌入布局文件root的id,如果include标签没有定义id则会使用嵌入文件root的id ②如果想使用标签覆盖嵌入布局root布局属性,必须同时覆盖layout_height和layout_width属性,否则会直接报编译时语法错误

    • viewstub标签:该标签与include一样用于引入布局模块,只是引入的布局默认不会扩张,既不会占用显示也不会占用位置,从而在解析layout时节省cpu和内存,只有通过调用setVisibility函数或者Inflate函数才会将其要装载的目标布局给加载出来,从而达到延迟加载的效果;例如条目详情、进度条标识或者未读消息等,这些情况如果在一开始初始化,虽然设置可见性View.GONE,但是在Inflate的时候View仍然会被Inflate,仍然会创建对象。

    • merge标签:该标签在layout中会被自动忽略,从而减少一层布局嵌套,其主要用处是当一个布局作为子布局被其他布局include时,使用merge当作该布局的顶节点来代替layout顶节点就可以减少一层嵌套

    • hierarchy viewer:该工具可以方便的查看Activity的布局,各个View的属性、measure、layout、draw的时间,如果耗时较多会用红色标记,否则显示绿色

网络优化

  • 异步请求网络数据,避免频繁请求数据(例如如果某个页面内请求过多,可以考虑做一定的请求合并),尽可能的减少网络请求次数和减小网络请求时间间隔

  • 网络应用传输中使用高效率的数据格式,譬如使用JSON代替XML,使用WebP代替其他图片格式,并对数据进行Gzip压缩数据,比如post请求的body和header内容

  • 及时缓存数据到内存/文件/数据库

  • 执行某些操作前尽量先进行网络状态判断,比如wifi传输数据比蜂窝数据更省电,所以尽量在wifi下进行数据的预加载

  • httpClient和httpUrlConnection对比:

    • httpClient是apache的开源实现,API数量多,非常稳定
    • httpUrlConnection是java自带的模块: ①可以直接支持GZIP压缩,而HttpClient虽然也支持GZIP,但要自己写代码处理 ②httpUrlConnection直接在系统层面做了缓存策略处理,加快重复请求的速度 ③API简单,体积较小,而且直接支持系统级连接池,即打开的连接不会直接关闭,在一段时间内所有程序可共用
    • HttpURLConnection在Android2.2之前有个重大Bug,调用close()函数会影响连接池,导致连接复用失效,需要关闭keepAlive;因此在2.2之前http请求都是用httpClient,2.2之后则是使用HttpURLConnection
    • 但是!!!现在!!!Android不再推荐这两种方式!二是直接使用OKHttp这种成熟方案!支持Android 2.3及其以上版本

数据结构优化

  • ArrayList和LinkedList的选择:ArrayList根据index取值更快,LinkedList更占内存、随机插入删除更快速、扩容效率更高

  • ArrayList、HashMap、LinkedHashMap、HashSet的选择:hash系列数据结构查询速度更优,ArrayList存储有序元素,HashMap为键值对数据结构,LinkedHashMap可以记住加入次序的hashMap,HashSet不允许重复元素

  • HashMap、WeakHashMap选择:WeakHashMap中元素可在适当时候被系统垃圾回收器自动回收,所以适合在内存紧张时使用

  • Collections.synchronizedMap和ConcurrentHashMap的选择:ConcurrentHashMap为细分锁,锁粒度更小,并发性能更优;Collections.synchronizedMap为对象锁,自己添加函数进行锁控制更方便

  • Android中性能更优的数据类型:如SparseArray、SparseBooleanArray、SparseIntArray、Pair;Sparse系列的数据结构是为key为int情况的特殊处理,采用二分查找及简单的数组存储,加上不需要泛型转换的开销,相对Map来说性能更优

内存优化

  • Android应用内存溢出OOM
    • 内存溢出的主要导致原因有如下几类:①应用代码存在内存泄露,长时间积累无法释放导致OOM;②应用的某些逻辑操作疯狂的消耗掉大量内存(譬如加载一张不经过处理的超大超高清图片等)导致超过阈值OOM
    • 解决思路①在内存引用上做些处理,常用的有软引用、弱引用 ②在内存中加载图片时直接在内存中做处理,如:边界压缩 ③动态回收内存,手动recyle bitmap,回收对象 ④优化Dalvik虚拟机的堆内存分配 ⑤自定义堆内存大小

多媒体之术语

发表于 2017-06-27 | 分类于 media

一.编解码术语

1.GOP/码流/比特率/帧速率/分辨率

1.1GOP(Group of picture)

关键帧的周期,也就是两个IDR帧之间的距离,一个帧组的最大帧数,一般而言,每一秒视频至少需要使用 1 个关键帧。增加关键帧个数可改善质量,但是同时增加带宽和网络负载。

需要说明的是,通过提高GOP值来提高图像质量是有限度的,在遇到场景切换的情况时,H.264编码器会自动强制插入一个I帧,此时实际的GOP值被缩短了。另一方面,在一个GOP中,P、B帧是由I帧预测得到的,当I帧的图像质量比较差时,会影响到一个GOP中后续P、B帧的图像质量,直到下一个GOP开始才有可能得以恢复,所以GOP值也不宜设置过大。

同时,由于P、B帧的复杂度大于I帧,所以过多的P、B帧会影响编码效率,使编码效率降低。另外,过长的GOP还会影响Seek操作的响应速度,由于P、B帧是由前面的I或P帧预测得到的,所以Seek操作需要直接定位,解码某一个P或B帧时,需要先解码得到本GOP内的I帧及之前的N个预测帧才可以,GOP值越长,需要解码的预测帧就越多,seek响应的时间也越长。

1.12CABAC/CAVLC

H.264/AVC标准中两种熵编码方法,CABAC叫自适应二进制算数编码,CAVLC叫前后自适应可变长度编码,

  1. CABAC:是一种无损编码方式,画质好,X264就会舍弃一些较小的DCT系数,码率降低,可以将码率再降低10-15%(特别是在高码率情况下),会降低编码和解码的速速。
  2. CAVLC将占用更少的CPU资源,但会影响压缩性能。

其他:

  • 帧:当采样视频信号时,如果是通过逐行扫描,那么得到的信号就是一帧图像,通常帧频为25帧每秒(PAL制)、30帧每秒(NTSC制);
  • 场:当采样视频信号时,如果是通过隔行扫描(奇、偶数行),那么一帧图像就被分成了两场,通常场频为50Hz(PAL制)、60Hz(NTSC制);
  • 帧频、场频的由来:最早由于抗干扰和滤波技术的限制,电视图像的场频通常与电网频率(交流电)相一致,于是根据各地交流电频率不同就有了欧洲和中国等PAL制的50Hz和北美等NTSC制的60Hz,但是现在并没有这样的限制了,帧频可以和场频一样,或者场频可以更高。
  • 帧编码、场编码方式:逐行视频帧内邻近行空间相关性较强,因此当活动量非常小或者静止的图像比较适宜采用帧编码方式;而场内相邻行之间的时间相关性较强,对运动量较大的运动图像则适宜采用场编码方式。
1.1.3Deblocking

开启会减少块效应.

1.1.4FORCE_IDR

是否让每个I帧变成IDR帧,如果是IDR帧,支持随机访问。

1.1.5frame,tff,bff

–frame 将两场合并作为一帧进行编码,–tff Enable interlaced mode (开启隔行编码并设置上半场在前),–bff Enable interlaced mode。

PAFF 和MBAFF:当对隔行扫描图像进行编码时,每帧包括两个场,由于两个场之间存在较大的扫描间隔,这样,对运动图像来说,帧中相邻两行之间的空间相关性相对于逐行扫描时就会减小,因此这时对两个场分别进行编码会更节省码流。

对帧来说,存在三种可选的编码方式:将两场合并作为一帧进行编码(frame 方式)或将两场分别编码(field 方式)或将两场合并起来作为一帧,但不同的是将帧中垂直相邻的两个宏块合并为宏块对进行编码;前两种称为PAFF 编码,对运动区域进行编码时field 方式有效,对非运区域编码时,由于相邻两行有较大的相关性,因而frame 方式会更有效。当图像同时存在运动区域和非运动区域时,在MB 层次上,对运动区域采取field 方式,对非运动区域采取frame 方式会更加有效,这种方式就称为MBAFF,预测的单位是宏块对。

1.2码流/码率

码流(Data Rate)是指视频文件在单位时间内使用的数据流量,也叫码率或码流率,通俗一点的理解就是取样率,是视频编码中画面质量控制中最重要的部分,一般我们用的单位是kb/s或者Mb/s。一般来说同样分辨率下,视频文件的码流越大,压缩比就越小,画面质量就越高。码流越大,说明单位时间内取样率越大,数据流,精度就越高,处理出来的文件就越接近原始文件,图像质量越好,画质越清晰,要求播放设备的解码能力也越高。

当然,码流越大,文件体积也越大,其计算公式是文件体积=时间X码率/8。例如,网络上常见的一部90分钟1Mbps码流的720P RMVB文件,其体积就=5400秒×1Mb/8=675MB。

通常来说,一个视频文件包括了画面及声音,例如一个RMVB的视频文件,里面包含了视频信息和音频信息,音频及视频都有各自不同的采样方式和比特率,也就是说,同一个视频文件音频和视频的比特率并不是一样的。而我们所说的一个视频文件码流率大小,一般是指视频文件中音频及视频信息码流率的总和。

以以国内最流行,大家最熟悉的RMVB视频文件为例,RMVB中的VB,指的是VBR,即Variable Bit Rate的缩写,中文含义是可变比特率,它表示RMVB采用的是动态编码的方式,把较高的采样率用于复杂的动态画面(歌舞、飞车、战争、动作等),而把较低的采样率用于静态画面,合理利用资源,达到画质与体积可兼得的效果。

码率和取样率最根本的差别就是码率是针对源文件来讲的。

1.3采样率

采样率(也称为采样速度或者采样频率)定义了每秒从连续信号中提取并组成离散信号的采样个数,它用赫兹(Hz)来表示。采样率是指将模拟信号转换成数字信号时的采样频率,也就是单位时间内采样多少点。一个采样点数据有多少个比特。比特率是指每秒传送的比特(bit)数。单位为 bps(Bit Per Second),比特率越高,传送的数据越大,音质越好.比特率 =采样率 x 采用位数 x声道数.

采样率类似于动态影像的帧数,比如电影的采样率是24赫兹,PAL制式的采样率是25赫兹,NTSC制式的采样率是30赫兹。当我们把采样到的一个个静止画面再以采样率同样的速度回放时,看到的就是连续的画面。同样的道理,把以44.1kHZ采样率记录的CD以同样的速率播放时,就能听到连续的声音。显然,这个采样率越高,听到的声音和看到的图像就越连贯。当然,人的听觉和视觉器官能分辨的采样率是有限的,基本上高于44.1kHZ采样的声音,绝大部分人已经觉察不到其中的分别了。

而声音的位数就相当于画面的颜色数,表示每个取样的数据量,当然数据量越大,回放的声音越准确,不至于把开水壶的叫声和火车的鸣笛混淆。同样的道理,对于画面来说就是更清晰和准确,不至于把血和西红柿酱混淆。不过受人的器官的机能限制,16位的声音和24位的画面基本已经是普通人类的极限了,更高位数就只能靠仪器才能分辨出来了。比如电话就是3kHZ取样的7位声音,而CD是44.1kHZ取样的16位声音,所以CD就比电话更清楚。

当你理解了以上这两个概念,比特率就很容易理解了。以电话为例,每秒3000次取样,每个取样是7比特,那么电话的比特率是21000。 而CD是每秒 44100次取样,两个声道,每个取样是13位PCM编码,所以CD的比特率是44100213=1146600,也就是说CD每秒的数据量大约是 144KB,而一张CD的容量是74分等于4440秒,就是639360KB=640MB。

码率和取样率最根本的差别就是码率是针对源文件来讲的

1.4比特率

比特率是指每秒传送的比特(bit)数。单位为bps(Bit Per Second),比特率越高,传送的数据越大。在视频领域,比特率常翻译为码率 !!!

比特率表示经过编码(压缩)后的音、视频数据每秒钟需要用多少个比特来表示,而比特就是二进制里面最小的单位,要么是0,要么是1。比特率与音、视频压缩的关系,简单的说就是比特率越高,音、视频的质量就越好,但编码后的文件就越大;如果比特率越少则情况刚好相反。

比特率是指将数字声音、视频由模拟格式转化成数字格式的采样率,采样率越高,还原后的音质、画质就越好。

常见编码模式:

  • VBR(Variable Bitrate)动态比特率 也就是没有固定的比特率,压缩软件在压缩时根据音频数据即时确定使用什么比特率,这是以质量为前提兼顾文件大小的方式,推荐编码模式;
  • ABR(Average Bitrate)平均比特率 是VBR的一种插值参数。LAME针对CBR不佳的文件体积比和VBR生成文件大小不定的特点独创了这种编码模式。ABR在指定的文件大小内,以每50帧(30帧约1秒)为一段,低频和不敏感频率使用相对低的流量,高频和大动态表现时使用高流量,可以做为VBR和CBR的一种折衷选择。
  • CBR(Constant Bitrate),常数比特率 指文件从头到尾都是一种位速率。相对于VBR和ABR来讲,它压缩出来的文件体积很大,而且音质相对于VBR和ABR不会有明显的提高。

1.5帧速率

帧速率也称为FPS(Frames PerSecond)的缩写——帧/秒。是指每秒钟刷新的图片的帧数,也可以理解为图形处理器每秒钟能够刷新几次。越高的帧速率可以得到更流畅、更逼真的动画。每秒钟帧数(FPS)越多,所显示的动作就会越流畅。

1.6分辨率

就是帧大小每一帧就是一副图像。

640*480分辨率的视频,建议视频的码速率设置在700以上,音频采样率44100就行了

一个音频编码率为128Kbps,视频编码率为800Kbps的文件,其总编码率为928Kbps,意思是经过编码后的数据每秒钟需要用928K比特来表示。

视频分辨率是指视频成像产品所成图像的大小或尺寸。常见的视像分辨率有352×288,176×144,640×480,1024×768。在成像的两组数字中,前者为图片长度,后者为图片的宽度,两者相乘得出的是图片的像素,长宽比一般为4:3.  目前监控行业中主要使用Qcif(176×144)、CIF(352×288)、HALF D1(704×288)、D1(704×576)等几种分辨率。 D1是数字电视系统显示格式的标准,共分为以下5种规格:

  1. D1:480i格式(525i):720×480(水平480线,隔行扫描),和NTSC模拟电视清晰度相同,行频为15.25kHz,相当于我们所说的4CIF(720×576)
  2. D2:480P格式(525p):720×480(水平480线,逐行扫描),较D1隔行扫描要清晰不少,和逐行扫描DVD规格相同,行频为31.5kHz
  3. D3:1080i格式(1125i):1920×1080(水平1080线,隔行扫描),高清方式采用最多的一种分辨率,分辨率为1920×1080i/60Hz,行频为33.75kHz
  4. D4:720p格式(750p):1280×720(水平720线,逐行扫描),虽然分辨率较D3要低,但是因为逐行扫描,市面上更多人感觉相对于1080I(实际逐次540线)视觉效果更加清晰。不过个人感觉来说,在最大分辨率达到1920×1080的情况下,D3要比D4感觉更加清晰,尤其是文字表现力上,分辨率为1280×720p/60Hz,行频为45kHz
  5. D5:1080p格式(1125p):1920×1080(水平1080线,逐行扫描),目前民用高清视频的最高标准,分辨率为1920×1080P/60Hz,行频为67.5KHZ。

其中D1 和D2标准是我们一般模拟电视的最高标准,并不能称的上高清晰,D3的1080i标准是高清晰电视的基本标准,它可以兼容720p格式,而D5的1080P只是专业上的标准。

计算输出文件大小公式: (音频编码率(KBit为单位)/8 +视频编码率(KBit为单位)/8)×影片总长度(秒为单位)=文件大小(MB为单位)

2.高清视频

目前的720P以及1080P采用了很多种编码,例如主流的MPEG2,VC-1以及H.264,还有Divx以及Xvid,至于封装格式更多到令人发指,ts、mkv、wmv以及蓝光专用等等。

720和1080代表视频流的分辨率,前者1280720,后者19201080,不同的编码需要不同的系统资源,大概可以认为是H.264>VC-1>MPEG2。  

VC-1是最后被认可的高清编码格式,不过因为有微软的后台,所以这种编码格式不能小窥。相对于MPEG2,VC-1的压缩比更高,但相对于H.264而言,编码解码的计算则要稍小一些,目前来看,VC-1可能是一个比较好的平衡,辅以微软的支持,应该是一只不可忽视的力量。一般来说,VC-1多为 “.wmv”后缀,但这都不是绝对的,具体的编码格式还是要通过软件来查询。

总的来说,从压缩比上来看,H.264的压缩比率更高一些,也就是同样的视频,通过H.264编码算法压出来的视频容量要比VC-1的更小,但是VC-1 格式的视频在解码计算方面则更小一些,一般通过高性能的CPU就可以很流畅的观看高清视频。相信这也是目前NVIDIA Geforce 8系列显卡不能完全解码VC-1视频的主要原因。

PS&TS是两种视频或影片封装格式,常用于高清片。扩展名分别为VOB/EVO和TS等;其文件编码一般用MPEG2/VC-1/H.264

高清,英文为“High Definition”,即指“高分辨率”。 高清电视(HDTV),是由美国电影电视工程师协会确定的高清晰度电视标准格式。现在的大屏幕液晶电视机,一般都支持1080i和720P,而一些俗称的“全高清”(Full HD),则是指支持1080P输出的电视机。

目前的高清视频编码格式主要有H.264、VC-1、MPEG-2、MPEG-4、DivX、XviD、WMA-HD以及X264。事实上,现在网络上流传的高清视频主要以两类文件的方式存在:一类是经过MPEG-2标准压缩,以tp和ts为后缀的视频流文件;一类是经过WMV-HD(Windows Media Video HighDefinition)标准压缩过的wmv文件,还有少数文件后缀为avi或mpg,其性质与wmv是一样的。真正效果好的高清视频更多地以H.264与VC-1这两种主流的编码格式流传。

一般来说,H.264格式以“.avi”、“.mkv”以及“.ts”封装比较常见。

3.位率(定码率,变码率)

位率又称为“码率”。指单位时间内,单个录像通道所产生的数据量,其单位通常是bps、Kbps或Mbps。可以根据录像的时间与位率估算出一定时间内的录像文件大小。  位率是一个可调参数,不同的分辨率模式下和监控场景下,合适的位率大小是不同的。在设置时,要综合考虑三个因素:   

  1. 分辨率:分辨率是决定位率(码率)的主要因素,不同的分辨率要采用不同的位率。总体而言,录像的分辨率越高,所要求的位率(码率)也越大,但并不总是如此,图1说明了不同分辨率的合理的码率选择范围。所谓“合理的范围”指的是,如果低于这个范围,图像质量看起来会变得不可接受;如果高于这个范围,则显得没有必要,对于网络资源以及存储资源来说是一种浪费。   
  2. 场景:监控的场景是设置码率时要考虑的第二个因素。在视频监控中,图像的运动剧烈程度还与位率有一定的关系,运动越剧烈,编码所要求的码率就越高。反之则越低。因此在同样的图像分辨率条件下,监控人多的场景和人少的场景,所要求的位率也是不同的。   
  3. 存储空间:最后需要考量的因素是存储空间,这个因素主要是决定了录像系统的成本。位率设置得越高,画质相对会越好,但所要求的存储空间就越大。所以在工程实施中,设置合适的位率即可以保证良好的回放图像质量,又可以避免不必要的资源浪费。    QP(quantizer parameter) 介于0~31之间,值越小,量化越精细,图像质量就越高,而产生的码流也越长。

PSNR 允许计算峰值信噪比(PSNR,Peak signal-to-noise ratio),编码结束后在屏幕上显示PSNR计算结果。开启与否与输出的视频质量无关,关闭后会带来微小的速度提升。

profile level 分别是BP、EP、MP、HP:

  1. BP-Baseline Profile:基本画质。支持I/P 帧,只支持无交错(Progressive)和CAVLC;
  2. EP-Extended profile:进阶画质。支持I/P/B/SP/SI 帧,只支持无交错(Progressive)和CAVLC;
  3. MP-Main profile:主流画质。提供I/P/B 帧,支持无交错(Progressive)和交错(Interlaced),也支持CAVLC 和CABAC 的支持;
  4. HP-High profile:高级画质。在main Profile 的基础上增加了8x8内部预测、自定义量化、无损视频编码和更多的YUV 格式;

H.264规定了三种档次,每个档次支持一组特定的编码功能,并支持一类特定的应用。

  1. 基本档次:利用I片和P片支持帧内和帧间编码,支持利用基于上下文的自适应的变长编码进行的熵编码(CAVLC)。主要用于可视电话、会议电视、无线通信等实时视频通信;
  2. 主要档次:支持隔行视频,采用B片的帧间编码和采用加权预测的帧内编码;支持利用基于上下文的自适应的算术编码(CABAC)。主要用于数字广播电视与数字视频存储;
  3. 扩展档次:支持码流之间有效的切换(SP和SI片)、改进误码性能(数据分割),但不支持隔行视频和CABAC。主要用于网络的视频流,如视频点播。

Image Stride(内存图像行跨度)

发表于 2017-06-27 | 分类于 media

When a video image is stored in memory, the memory buffer might contain extra padding bytes after each row of pixels. The padding bytes affect how the image is store in memory, but do not affect how the image is displayed.

当视频图像存储在内存时,图像的每一行末尾也许包含一些扩展的内容,这些扩展的内容只影响图像如何存储在内存中,但是不影响图像如何显示出来;

The stride is the number of bytes from one row of pixels in memory to the next row of pixels in memory. Stride is also called pitch. If padding bytes are present, the stride is wider than the width of the image, as shown in the following illustration.

Stride 就是这些扩展内容的名称,Stride 也被称作 Pitch,如果图像的每一行像素末尾拥有扩展内容,Stride 的值一定大于图像的宽度值,就像下图所示: image

Two buffers that contain video frames with equal dimensions can have two different strides. If you process a video image, you must take into the stride into account.

两个缓冲区包含同样大小(宽度和高度)的视频帧,却不一定拥有同样的 Stride 值,如果你处理一个视频帧,你必须在计算的时候把 Stride 考虑进去;

In addition, there are two ways that an image can be arranged in memory. In a top-down image, the top row of pixels in the image appears first in memory. In a bottom-up image, the last row of pixels appears first in memory. The following illustration shows the difference between a top-down image and a bottom-up image.

另外,一张图像在内存中有两种不同的存储序列(arranged),对于一个从上而下存储(Top-Down) 的图像,最顶行的像素保存在内存中最开头的部分,对于一张从下而上存储(Bottom-Up)的图像,最后一行的像素保存在内存中最开头的部分,下面图示展示了这两种情况: image

A bottom-up image has a negative stride, because stride is defined as the number of bytes need to move down a row of pixels, relative to the displayed image. YUV images should always be top-down, and any image that is contained in a Direct3D surface must be top-down. RGB images in system memory are usually bottom-up.

一张从下而上的图像拥有一个负的 Stride 值,因为 Stride 被定义为[从一行像素移动到下一行像素时需要跨过多少个像素],仅相对于被显示出来的图像而言;而 YUV 图像永远都是从上而下表示的,以及任何包含在 Direct3D Surface 中的图像必须是从上而下,RGB 图像保存在系统内存时通常是从下而上;

Video transforms in particular need to handle buffers with mismatched strides, because the input buffer might not match the output buffer. For example, suppose that you want to convert a source image and write the result to a destination image. Assume that both images have the same width and height, but might not have the same pixel format or the same image stride.

尤其是视频变换,特别需要处理不同 Stride 值的图像,因为输入缓冲也许与输出缓冲不匹配,举个例子,假设你想要将源图像转换并且将结果写入到目标图像,假设两个图像拥有相同的宽度和高度,但是其像素格式与 Stride 值也许不同;

The following example code shows a generalized approach for writing this kind of function. This is not a complete working example, because it abstracts many of the specific details.

下面代码演示了一种通用方法来编写这种功能,这段代码并不完整,因为这只是一个抽象的算法,没有完全考虑到真实需求中的所有细节;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void ProcessVideoImage(
BYTE* pDestScanLine0,
LONG lDestStride,
const BYTE* pSrcScanLine0,
LONG lSrcStride,
DWORD dwWidthInPixels,
DWORD dwHeightInPixels
)
{
for (DWORD y = 0; y < dwHeightInPixels; y++)
{
SOURCE_PIXEL_TYPE *pSrcPixel = (SOURCE_PIXEL_TYPE*)pDestScanLine0;
DEST_PIXEL_TYPE *pDestPixel = (DEST_PIXEL_TYPE*)pSrcScanLine0;

for (DWORD x = 0; x < dwWidthInPixels; x +=2)
{
pDestPixel[x] = TransformPixelValue(pSrcPixel[x]);
}
pDestScanLine0 += lDestStride;
pSrcScanLine0 += lSrcStride;
}
}

This function takes six parameters:

  • A pointer to the start of scan line 0 in the destination image.
  • The stride of the destination image.
  • A pointer to the start of scan line 0 in the source image.
  • The stride of the source image.
  • The width of the image in pixels.
  • The height of the image in pixels.

这个函数需要六个参数:

  • 目标图像的起始扫描行的内存指针
  • 目标图像的 Stride 值
  • 源图像的起始扫描行的内存指针
  • 源图像的 Stride 值
  • 图像的宽度值(以像素为单位)
  • 图像的高度值(以像素为单位) The general idea is to process one row at a time, iterating over each pixel in the row. Assume that SOURCE_PIXEL_TYPE and DEST_PIXEL_TYPE are structures representing the pixel layout for the source and destination images, respectively. (For example, 32-bit RGB uses the RGBQUAD structure. Not every pixel format has a pre-defined structure.) Casting the array pointer to the structure type enables you to access the RGB or YUV components of each pixel. At the start of each row, the function stores a pointer to the row. At the end of the row, it increments the pointer by the width of the image stride, which advances the pointer to the next row.

这里的要点是如何一次处理一行像素,遍历一行里面的每一个像素,假设源像素类型与目标像素类型各自在像素的层面上已经结构化来表示一个源图像与目标图像的像素,(举个例子,32 位 RGB 像素使用 RGBQUAD 结构体,并不是每一种像素类型都有预定义结构体的)强制转换数组指针到这样的结构体指针,可以方便你直接读写每一个像素的 RGB 或者 YUV 值,在每一行的开头,这个函数保存了一个指向这行像素的指针,函数的最后一行,通过图像的 Stride 值直接将指针跳转到图像的下一行像素的起始点;

This example calls a hypothetical function named TransformPixelValue for each pixel. This could be any function that calculates a target pixel from a source pixel. Of course, the exact details will depend on the particular task. For example, if you have a planar YUV format, you must access the chroma planes independently from the luma plane; with interlaced video, you might need to process the fields separately; and so forth.

To give a more concrete example, the following code converts a 32-bit RGB image into an AYUV image. The RGB pixels are accessed using an RGBQUAD structure, and the AYUV pixels are accessed using aDXVA2_AYUVSample8 Structure structure.

引用: 如果你用的是 MSDN Library For Visual Studio 2008 SP1,那么你应该能够在下面地址中找到这篇文章的原文: ms-help://MS.MSDNQTR.v90.chs/medfound/html/13cd1106-48b3-4522-ac09-8efbaab5c31d.htm

http://blog.csdn.net/g0ose/article/details/52116453

1…151617…19
轻口味

轻口味

190 日志
27 分类
63 标签
RSS
GitHub 微博 豆瓣 知乎
友情链接
  • SRS
© 2015 - 2019 轻口味
京ICP备17018543号
本站访客数 人次 本站总访问量 次