视频通话调研

WebRTC Android编译及音视频相关记录。

WebRTC Android编译

参照官网

1、硬件要求

Linux (with Android): 16 GB (of which ~8 GB is Android SDK+NDK images)

Mac (with iOS support): 5.6GB,Mac系统不支持编译Android,脚本中有lsb_release

2、Webrtc的源码使用了chromium内核的代码管理工具:depot_tools,下载depot_tools

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
```
3、设置depot_tools环境变量
添加export PATH=“$PATH:/home/webrtc/depot_tools”
4、下载源码
``` bash
mkdir webrtc && cd webrtc
// 若有报错,检查环境配置是否正确
fetch --nohooks webrtc_android
// 若有 You have unstaged changes.Please commit, stash, or reset.错误,加 --force
gclient sync

5、编译源码

1
2
3
4
5
6
7
8
9
10
11
12
13
cd src
// SDK NDK环境配置到PATH中
. build/android/envsetup.sh
// 这里不加,编译会有问题
./build/install-build-deps.sh
./build/install-build-deps-android.sh
// 编译生成Debug版
gn gen out/Debug --args='target_os="android" target_cpu="arm"'
ninja -C out/Debug
// 生成release版ninja项目文件
gn gen out/Release --args='target_os="android" target_cpu="arm" is_debug=false'
ninja -C out/Release

成功后可以在out/Debug下找到编译好的apk demo文件及so,jar包

6、apk demo运行

1
2
3
4
adb install -r out/Default/apks/AppRTCMobile.apk
// 在chrome浏览器中输入:https://appr.tc 并输入房间号点击JOIN创建一个房间。打开装好的AppRTC,并输入相同的房间号,可进行WebRTC通话
//WebRTCDemo只是局域网内的点对点,知道对方的ip和端口号就可以对打,不需要服务器,AppRTCDemo是需要服务器的,可以局域网,或者广域网

7、使用Android Studio开发

1
2
3
4
5
6
7
ninja -C out/Debug AppRTCMobile
build/android/gradle/generate_gradle.py --output-directory $PWD/out/Debug
--target "//examples:AppRTCMobile" --use-gradle-process-resources
--split-projects --canary
// 会在out/Debug/gradle里生成android项目,Android studio import后,可以按照每个module里build文件里源码路径把源码手动添加进来

低延时

延时的产生:

  • T1:设备端上的延时

音视频数据在设备端上产生延时还可以细分。设备端上的延时主要与硬件性能、采用的编解码算法、音视频数据量相关,设备端上的延时可达到 30~200ms,甚至更高。如上表所示,音频与视频分别在采集端或播放端产生延时的过程基本相同,但产生延时的原因不同。

1、音频在设备端上的延时:

音频采集延时:采集后的音频首先会经过声卡进行信号转换,声卡本身会产生延时,比如 M-Audio 声卡设备延迟 1ms,艾肯声卡设备延迟约为 37ms;

编解码延时:随后音频进入前处理、编码的阶段,如果采用 OPUS 标准编码,最低算法延时大约需要 2.5~60ms;

音频播放延时:这部分延时与播放端硬件性能相关。

音频处理延时:前后处理,包括 AEC,ANS,AGC 等前后处理算法都会带来算法延时,通常这里的延时就是滤波器阶数。在 10ms 以内。

端网络延时:这部分延时主要出现在解码之前的 jitter buffer 内,如果在抗丢包处理中,增加了重传算法和前向纠错算法,这里的延时一般在 20ms 到 200ms 左右。但是受到 jitter buffer 影响,可能会更高。

2、视频在设备端上的延时:

采集延时:采集时会遇到成像延迟,主要由 CCD 相关硬件产生,市面上较好的 CCD 一秒可达 50 帧,成像延时约为 20ms,如果是一秒 20~25 帧的 CCD,会产生 40~50ms 的延时;

编解码延时:以 H.264 为例,它包含 I、P、B 三种帧(下文会详细分析),如果是每秒 30 帧相连帧,且不包括 B 帧(由于 B 帧的解码依赖前后视频帧会增加延迟),采集的一帧数据可能直接进入编码器,没有 B 帧时,编码的帧延时可以忽略不计,但如果有 B 帧,会带来算法延时。

视频渲染延时:一般情况下渲染延时非常小,但是它也会受到系统性能、音画同步的影响而增大。

端网络延时:与音频一样,视频也会遇到端网络延时。

另外,在设备端,CPU、缓存通常会同时处理来自多个应用、外接设备的请求,如果某个问题设备的请求占用了 CPU,会导致音视频的处理请求出现延时。以音频为例,当出现该状况时,CPU 可能无法及时填充音频缓冲区,音频会出现卡顿。所以设备整体的性能,也会影响音视频采集、编解码与播放的延时。

T2:端与服务器间的延时

影响采集端与服务器、服务器与播放端的延时的有以下主几个因素:客户端同服务间的物理距离、客户端和服务器的网络运营商、终端网络的网速、负载和网络类型等。如果服务器就近部署在服务区域、服务器与客户端的网络运营商一致时,影响上下行网络延时的主要因素就是终端网络的负载和网络类型。一般来说,无线网络环境下的传输延时波动较大,传输延时通常在 10~100ms 不定。而有线宽带网络下,同城的传输延时能较稳定的低至 5ms~10ms。但是在国内有很多中小运营商,以及一些交叉的网络环境、跨国传输,那么延时会更高。

T3:服务器间的延时
在此我们要要考虑两种情况,第一种,两端都连接着同一个边缘节点,那么作为最优路径,数据直接通过边缘节点进行转发至播放端;第二种,采集端与播放端并不在同一个边缘节点覆盖范围内,那么数据会经由“靠近”采集端的边缘节点传输至主干网络,然后再发送至“靠近”播放端的边缘节点,但这时服务器之间的传输、排队还会产生延时。仅以骨干网络来讲,数据传输从黑龙江到广州大约需要 30ms,从上海到洛杉矶大约需要 110ms~130ms。

在实际情况下,我们为了解决网络不佳、网络抖动,会在采集设备端、服务器、播放端增设缓冲策略。一旦触发缓冲策略就会产生延时。如果卡顿情况多,延时会慢慢积累。要解决卡顿、积累延时,就需要优化整个网络状况。

综上所述,由于音视频在采集与播放端上的延时取决于硬件性能、编解码内核的优化,不同设备,表现不同。所以通常市面上常见的“端到端延时”指的是 T2+T3。

  • 丢包

没按时到的包为丢包。一个包应该在某个时间点到,但它晚到了,即使来了但是晚了,也叫丢包。因为播放的这段时间已经过去了,即使放了,体验也不好。从整个网络上看,丢包一定有时限,否则,都通过反复重传方法,一定能送达一个包。

音频采样率、码率、延时(影响实时音频通讯质量的因素)

音频信息其实就是一段以时间为横轴的正弦波,它是一段连续的信号。

  • 采样率:是每秒从连续信号中提取并组成离散信号的采样个数。采样率越高,音频听起来越接近真实声音。

  • 码率:它描述了单位时间长度的媒体内容需要空间。码率越高,意味着每个采样的信息量就越大,对这个采样的描述就越精确,音质越好。

码率、帧率、分辨率、延时(影响实时视频质量的因素)

在视频压缩的过程中,I 帧是帧内图像数据压缩,是独立帧,视频序列中的第一个帧始终都是 I 帧。

在视频会议系统中,并不是每次都把完整的一幅幅图片发送到远端,而只是发送后一幅画面在前一幅画面基础上发生变化的部分。如果在网络状况不好的情况下,终端的接收远端或者发送给远程的画面就会有丢包而出现图像花屏、图像卡顿的现象。

在视频画面播放过程中,若I帧丢失了,则后面的P帧也就随着解不出来,就会出现视频画面黑屏的现象;若P帧丢失了,则视频画面会出现花屏、马赛克等现象。

在视频压缩的过程中,I帧是帧内图像数据压缩,是独立帧。而P帧则是参考I帧进行帧间图像数据压缩,不是独立帧。在压缩后的视频中绝大多数都是P帧,故视频质量主要由P帧表现出来。由于P帧不是独立帧,而只是保存了与邻近的I帧的差值,故实际上并不存在分辨率的概念,应该看成一个二进制差值序列。而该二进制序列在使用熵编码压缩技术时会使用量化参数进行有损压缩,视频的质量直接由量化参数决定,而量化参数会直接影响到压缩比和码率。

图片名称

上图所示为 H.264 标准下的视频帧。它以 I 帧、P 帧、B 帧组成的 GOP 分组来表示图像画面(如下图):I 帧是关键帧,带有图像全部信息;P 帧是预测编码帧,表示与当前与前一帧(I 或 P 帧)之间的差别;B 帧是双向预测编码帧,记录本帧与前后帧的差别。

  • 显卡帧率(FPS):每秒显示的图片数。影响画面流畅度。帧率越大,画面越流畅;帧率越小,画面越有跳动感。

由于人类眼睛的特殊生理结构,如果所看画面帧率高于24fps的时候,就会认为是连贯的,此现象称之为视觉暂留。高的帧率可以得到更流畅、更逼真的动画。一般来说30fps就是可以接受的,提升至60fps则可以明显提升交互感和逼真感,但是一般来说超过75fps一般就不容易察觉到有明显的流畅度提升了。

另一个区分的概念是显示器刷新率,人眼可以直接感知的画面是来自显示器,因此所谓的画面是否流畅,是从显示器观察而来。显卡的帧率是否能让我们感知到(换句话说就是画面是否流畅)是受到显示器的刷新率的制约的。现在的显卡通常120FPS,显示器通常60HZ刷新率。

FPS与分辨率、显卡处理能力的关系:
处理能力=分辨率×刷新率
这也就是为什么在玩游戏时,分辨率设置得越大,画面就越不流畅的原因

  • 分辨率:就是帧大小每一帧就是一副图像。

720P和1080P代表视频流的分辨率,前者1280*720,后者1920*1080

清晰是指画面细腻,没有马赛克,并不是分辨率越高图像就越清晰。

在码率一定的情况下,分辨率与清晰度成反比关系:分辨率越高,图像越不清晰;分辨率越低,图像越清晰。在分辨率一定的情况下,码率与清晰度成正比关系,码率越高,图像越清晰;码率越低,图像越不清晰。

分辨率的变化又称为重新采样。由高分辨率变成低分辨率称为下采样,由于采样前数据充足,只需要尽量保留更多的信息量,一般可以获得相对较好的结果。而由低分辨率变成高分辨率称为上采样,由于需要插值等方法来补充(猜测)缺少的像素点,故必然会带有失真,这就是一种视频质量(清晰度)的损失。

  • 码率:数据传输时单位时间传送的数据位数,一般我们用的单位是kbps即千位每秒(kb/s或Mb/s)。

通俗一点的理解就是取样率或者比特率。一般来说同样分辨率下,视频文件的码流(码率)越大,压缩比就越小,画面质量就越高。码流越大,说明单位时间内取样率越大,数据流,精度就越高,处理出来的文件就越接近原始文件,图像质量越好,画质越清晰,要求播放设备的解码能力也越高。但是文件体积与取样率是成正比的,所以几乎所有的编码格式重视的都是如何用最低的码率达到最少的失真。视频的码率与音频码率相似,码率越大,画面细节信息越丰富,视频文件体积越大。

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

用mediainfo查看

参数
长度 3分 47秒
码率 1411.2 Kbps
声道 2声道
采样率 44.1 KHz
位深度 16位
大小 38.3 MiB (100%)

1、码率计算公式:

码率=采样率 x 位深度 x 声道

文件的码率= 44.1Khz x 16位 x 2声道 = 1411.2 Kbps

2、文件大小 = 码率 x 时长 = 1411.2 Kbps x (3 x 60 + 47 )s = 1411.2Kbps x 227s

=38102.4 Kb

38102.4 Kb / 1024 Kb/M = 37.2M

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

直播特效的实现原理

直播的具体流程,包括:采集、前处理、编码、传输、解码、后处理、播放。

  • 采集

视频的采集源主要有三种:摄像头采集、屏幕录制和从视频文件推流。直播中常见的是通过摄像头采集的图像。以Android为例,由于需要进行图像的二次处理(滤镜、特效),所以使用 SurfaceTexture来处理图像流,给采集到的图像增添特效、滤镜等。SurfaceTexture 是一个纹理,可以想象成一个 View 的中间件。Camera 把视频采集的内容交给 SurfaceTexture,SurfaceTexture 进行美颜处理,然后把内容交给 SurfaceView,渲染出来。

  • 前处理

对采集到的图像进行处理:比如通过均值模糊、高斯模糊和中值滤波等去噪算法,给原始视频进行“磨皮”;增加滤镜;为视频添加实时的 AR 特效;回声消除、噪声抑制等。

  • 编码

在完成图像的处理后,按照合适码率、格式进行编码。主流的视频编码器分为3个系列:VPx(VP8,VP9),H.26x(H.264,H.265),AVS(AVS1.0,AVS2.0)。VPx系列是由Google开源的视频编解码标准。在保证相同质量情况下,VP9相比VP8码率减少约50%。H.26x系列在硬件支持上比较广泛,H.265的编码效率能比上一代提高了30-50%,但是复杂度和功耗会比上一代大很多,所以纯软件编码实现的话有一定瓶颈,现有的技术下,还是需要依靠硬件编解码为主。AVS是我国具备自主知识产权的第二代信源编码标准,目前已经发展到第二代。

封装格式(专业上讲叫容器,通俗的叫文件格式)

1、MPEG : 编码采用的容器,具有流的特性。里面又分为 PS,TS 等,PS 主要用于 DVD 存储,TS 主要用于 HDTV.

2、MPEG Audio Layer 3 :大名鼎鼎的 MP3,已经成为网络音频的主流格式,能在 128kbps 的码率接近 CD 音质

3、MPEG-4(Mp4) : 编码采用的容器,基于 QuickTime MOV 开发,具有许多先进特性;实际上是对Apple公司开发的MOV格式(也称Quicktime格式)的一种改进。

4、MKV: 它能把 Windows Media Video,RealVideo,MPEG-4 等视频音频融为一个文件,而且支持多音轨,支持章节字幕等;开源的容器格式。

5、3GP : 3GPP视频采用的格式, 主要用于流媒体传送;3GP其实是MP4格式的一种简化版本,是手机视频格式的绝对主流。

6、MOV : QuickTime 的容器,恐怕也是现今最强大的容器,甚至支持虚拟现实技术,Java等,它的变种 MP4,3GP都没有这么厉害;广泛应用于Mac OS操作系统,在Windows操作系统上也可兼容,但是远比不上AVI格式流行

7、AVI : 最常见的音频视频容器,音频视频交错(Audio Video Interleaved)允许视频和音频交错在一起同步播放。

8、WAV : 一种音频容器,大家常说的 WAV 就是没有压缩的 PCM 编码,其实 WAV 里面还可以包括 MP3 等其他 ACM 压缩编码。

最后,推流到 CDN。

流媒体

边下载边播的流式传输方式

1、RTP RTCP RTSP

RTP :(Real-time Transport Protocol)是用于Internet上针对多媒体数据流的一种传输层协议。RTP协议和RTP控制协议RTCP一起使用,而且它是建立在UDP协议上的。

RTCP:Real-time Transport Control Protocol或RTP Control Protocol或简写RTCP,实时传输控制协议,是实时传输协议(RTP)的一个姐妹协议。RTP协议和RTP控制协议RTCP一起使用,而且它是建立在UDP协议上的。

RTSP:(Real Time Streaming Protocol)是用来控制声音或影像的多媒体串流协议,RTSP提供了一个可扩展框架,使实时数据,如音频与视频的受控、点播成为可能。

RTP不像http和ftp可完整的下载整个影视文件,它是以固定的数据率在网络上发送数据,客户端也是按照这种速度观看影视文件,当影视画面播放过后,就不可以再重复播放,除非重新向服务器端要求数据。

RTSP与RTP最大的区别在于:RTSP是一种双向实时数据传输协议,它允许客户端向服务器端发送请求,如回放、快进、倒退等操作。当然,RTSP可基于RTP来传送数据,还可以选择TCP、UDP、组播UDP等通道来发送数据,具有很好的扩展性。它时一种类似与http协议的网络应用层协议

2、RTMP

RTMP(Real Time Messaging Protocol(实时消息传送协议是Adobe Systems公司为Flash播放器和服务器之间音频、视频和数据传输开发的开放协议。

3、HLS

HTTP Live Streaming(HLS)是苹果公司实现的基于HTTP的流媒体传输协议,可实现流媒体的直播和点播,主要应用在iOS系统,为iOS设备(如iPhone、iPad)提供音视频直播和点播方案。HLS点播,基本上就是常见的分段HTTP点播,不同在于,它的分段非常小。相对于常见的流媒体直播协议,例如RTMP协议、RTSP协议、MMS协议等,HLS直播最大的不同在于,直播客户端获取到的,并不是一个完整的数据流。HLS协议在服务器端将直播数据流存储为连续的、很短时长的媒体文件(MPEG-TS格式),而客户端则不断的下载并播放这些小文件。因为服务器端总是会将最新的直播数据生成新的小文件,这样客户端只要不停的按顺序播放从服务器获取到的文件,就实现了直播。

MediaCodec

  • MediaCodec编码器包含两个缓冲区,一个输入缓冲区,一个输出缓冲区。
  • 客户端先从 MediaCodec 获取一个可用的输入缓冲区,然后将待编码的数据填充到缓冲区,然后交给 MediaCodec 去处理。
  • 客户端从输出缓冲区获取已经处理好的数据,客户端得到数据后并处理后,释放空间,最后将缓冲区还给 MediaCodec。

  • 要用 MediaCodec,首先需要创建,根据我们想要的编码格式创建。创建后就是Uninitialized 状态
  • 创建完成之后还不能直接用,我们需要进行配置,进入 Configured。这时候就准备就绪了
  • Configured 后,就可以 start。进行运行阶段了。
  • 运行阶段又分3个子状态。start() 后就进入 Flushed 状态。
  • 当客户端获取一个有效的输入缓冲区后,就进入了 Running,而MediaCodec 大部分时间在这个状态。
  • 如果客户端将得到的输入缓冲区入队时带有末尾标记时,编码器就进入 End of Stream 状态,这时候就不再接受后面缓冲区的输入。
  • stop 之后就会重新进入 Uninitialized 状态。
  • 如果出现错误就会进入 Error 状态。

MediaCodec编码

使用MediaCodec流程

1、创建MediaCodec

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
private void initMediaCodec() {
int bitrate = 2 * WIDTH * HEIGHT * FRAME_RATE / 20;
try {
// 获取系统的编码器并查找是否有需要的编码器并返回其信息
MediaCodecInfo mediaCodecInfo = selectCodec(VCODEC_MIME);
if (mediaCodecInfo == null) {
Toast.makeText(this, "mMediaCodec null", Toast.LENGTH_LONG).show();
throw new RuntimeException("mediaCodecInfo is Empty");
}
LogUtils.w("MediaCodecInfo " + mediaCodecInfo.getName());
// 得到信息后创建 MediaCodec
mMediaCodec = MediaCodec.createByCodecName(mediaCodecInfo.getName());
// 配置编码信息
// 码率KEY_BIT_RATE、编码像素格式KEY_COLOR_FORMAT、帧率KEY_FRAME_RATE、像素格式KEY_COLOR_FORMAT(不同的设备可能支持的格式不同)
MediaFormat mediaFormat = MediaFormat.createVideoFormat(VCODEC_MIME, WIDTH, HEIGHT);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
// 编码信息配置给编码器
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mMediaCodec.start();
} catch (IOException e) {
e.printStackTrace();
}
}

2、编码

从Camera.PreviewCallback的onPreviewFrame(final byte[] data, Camera camera)回调方法中获取数据。data就是采集到的原始YUV数据,这里把第几帧和编码时间以及采集时间打印出来,这里的YUV数据和Camera的参数设置有关params.setPreviewFormat(ImageFormat.YV12),系统默认使用的N21。

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
public class StreamIt implements Camera.PreviewCallback {
@Override
public void onPreviewFrame(final byte[] data, Camera camera) {
long endTime = System.currentTimeMillis();
executor.execute(new Runnable() {
@Override
public void run() {
encodeTime = System.currentTimeMillis();
flvPackage(data);
LogUtils.w("编码第:" + (encodeCount++) + "帧,耗时:" + (System.currentTimeMillis() - encodeTime));
}
});
LogUtils.d("采集第:" + (++count) + "帧,距上一帧间隔时间:"
+ (endTime - previewTime) + " " + Thread.currentThread().getName());
previewTime = endTime;
}
private void flvPackage(byte[] buf) {
final int LENGTH = HEIGHT * WIDTH;
//YV12数据转化成COLOR_FormatYUV420Planar,因为编码器支持的输入是COLOR_FormatYUV420Planar,而采集到的是YV12。两者的区别就是 U、V 分量颠倒了个位置。
LogUtils.d(LENGTH + " " + (buf.length - LENGTH));
for (int i = LENGTH; i < (LENGTH + LENGTH / 4); i++) {
byte temp = buf[i];
buf[i] = buf[i + LENGTH / 4];
buf[i + LENGTH / 4] = temp;
// char x = 128;
// buf[i] = (byte) x;
}
//MediaCodec 进行 H.264编码
//获取编码器的输入和输出缓冲区
ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();
try {
//获取一个可用的输入缓冲区索引,用来填充有效数据
int bufferIndex = mMediaCodec.dequeueInputBuffer(-1);
//如果返回>0说明有效。然后获取到对应的ByteBuffer
if (bufferIndex >= 0) {
//将图像数据填充到inputBuffer中
ByteBuffer inputBuffer = inputBuffers[bufferIndex];
inputBuffer.clear();
inputBuffer.put(buf, 0, buf.length);
//把数据传给编码器并进行编码
mMediaCodec.queueInputBuffer(bufferIndex, 0,
inputBuffers[bufferIndex].position(),
System.nanoTime() / 1000, 0);
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
//获取编码后的数据,先得到输出缓冲区索引,返回成功的buffer索引。
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
//得到对应的 ByteBuffer,也就是编码后的数据,进行flv封装
mFlvPacker.onVideoData(outputBuffer, bufferInfo);
mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
}
} else {
LogUtils.w("No buffer available !");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

3、flv封装

将原始的 H.264 数据封装成 flv 格式的数据

  • flv 文件封装视频数据前先写入 flv 头,metadata 数据
  • MediaCodec 进行编码后的第一个数据是 sps、pps数据,也是 flv 中的第一个 video tag
  • 后面收到的 MediaCodec 编码后的数据就是正常的视频 H.264 数据,封装到 flv 中
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
@Override
public void onVideoData(ByteBuffer bb, MediaCodec.BufferInfo bi) {
mAnnexbHelper.analyseVideoData(bb, bi);
}
/**
* 将硬编得到的视频数据进行处理生成每一帧视频数据,然后传给flv打包器
* @param bb 硬编后的数据buffer
* @param bi 硬编的BufferInfo
*/
public void analyseVideoData(ByteBuffer bb, MediaCodec.BufferInfo bi) {
bb.position(bi.offset);
bb.limit(bi.offset + bi.size);
ArrayList<byte[]> frames = new ArrayList<>();
boolean isKeyFrame = false;
while(bb.position() < bi.offset + bi.size) {
byte[] frame = annexbDemux(bb, bi);
if(frame == null) {
LogUtils.e("annexb not match.");
break;
}
// ignore the nalu type aud(9)
if (isAccessUnitDelimiter(frame)) {
continue;
}
// for pps
if(isPps(frame)) {
mPps = frame;
continue;
}
// for sps
if(isSps(frame)) {
mSps = frame;
continue;
}
// for IDR frame
if(isKeyFrame(frame)) {
isKeyFrame = true;
} else {
isKeyFrame = false;
}
byte[] naluHeader = buildNaluHeader(frame.length);
frames.add(naluHeader);
frames.add(frame);
}
if (mPps != null && mSps != null && mListener != null && mUploadPpsSps) {
if(mListener != null) {
mListener.onSpsPps(mSps, mPps);
}
mUploadPpsSps = false;
}
if(frames.size() == 0 || mListener == null) {
return;
}
int size = 0;
for (int i = 0; i < frames.size(); i++) {
byte[] frame = frames.get(i);
size += frame.length;
}
byte[] data = new byte[size];
int currentSize = 0;
for (int i = 0; i < frames.size(); i++) {
byte[] frame = frames.get(i);
System.arraycopy(frame, 0, data, currentSize, frame.length);
currentSize += frame.length;
}
if(mListener != null) {
mListener.onVideo(data, isKeyFrame);
}
}

参考

[1] 帧率、显示器刷新率与垂直同步

[2] 谈码率、帧率、分辨率和清晰度的关系

[3] WebRTC 的 Android 2 Android 实现

[4] 声网论坛