WebRTC源码初探之Android平台视频采集

WebRTC的采集模块中提供了多个平台的音视频采集实现,这里我们针对Android平台来分析一下WebRTC的采集模块。

在讲解之前有几个概念需要明白

  • Source: 在WebRTC中将一切数据的来源都抽象成一个Source
  • Track: 在WebRTC中一个Track代表一路音频或视频,一个Track需要通过一个Source来创建
  • Sink: 在WebRTC中将数据的消费都抽象成一个Sink,将一个Sink添加到一个Track上,就可以收到对应Source的数据

VideoCapturer

VideoCapturer是设备采集模块对外提供的控制接口,通过CameraEnumerator来创建。

通过CameraEnumerator可以检测当前设备上Camera设备的支持情况,然后通过createCapturer函数可以开启指定Camera设备进行采集,VideoCapturer对采集提供一些逻辑控制。

1
2
3
4
5
6
private static VideoCapturer createVideoCapturer(Context context) {
CameraEnumerator enumerator = Camera2Enumerator.isSupported(context)
? new Camera2Enumerator(context)
: new Camera1Enumerator();
return enumerator.createCapturer(enumerator.getDeviceNames()[0], null /* eventsHandler */);
}

CameraEnumerator

CameraEnumerator负责提供对Camera设备信息的获取、以及负责创建CameraCapturer,具体实现有Camera1Enumerator和Camera2Enumerator。

CameraCapturer

CameraCapturer是对采集操作的封装,对外提供了开始采集、结束采集、设置采集参数等接口,负责创建CameraSession,对CameraSession的功能进行接口封装。CameraCapturer的具体实现有Camera1Capturer和Camera2Capturer。

CameraSession

CameraSession是对AndroidCameraApi的包装,具体实现有Camera1Session和Camera2Session。

数据类型

CameraApi支持两种类型的数据回调,一种是字节流,另一种是纹理。这两种类型适合不同的场景,字节流类型对于FFmpeg等软编码比较方便,但是如果想要使用OpenGL-ES来对数据进行处理,那么就需要将字节流转换成纹理才行;纹理类型对OpenGL-ES处理比较方便,但是只有MediaCodec才支持纹理的编码,如果想要使用FFmpeg软编码,需要将纹理数据提取成字节流。

Camera1Session两种类型的数据都支持,通过参数captureToTexture来控制返回的数据类型。

1
2
3
public Camera1Enumerator(boolean captureToTexture) {
this.captureToTexture = captureToTexture;
}

Camera2Session只支持纹理类型的数据返回,并没有实现Camera2的字节流数据返回。(Camera2可以通过ImageReader获取字节流,但是硬件兼容性太差,不同厂商实现不一致)

音视频开发之H.264码流结构与编码原理

目前主流的H.264和H.265编码格式是由ITU和MPEG两个组织合力制定的。

在早期,ITU和MPEG两家组织都是各搞各的,ITU组织推行了H.261、H.262、H.263编码格式,而MPEG组织则推行了MPEG-1、MPEG-2、MPEG-3标准族群。后来两家组织准备合力制作新一代的视频编码标准,对于ITU组织来说,将这个新一代的编码标准命名为H.264,而对于MPEG来说,这个新一代的压缩标准只是其MPEG-4标准的第10部分,其第10部分叫做高级视频编码AVC(Advanced Video Coding)。

所以可以简单的认为H.264就是MPEG-4 AVC。

码流结构

NAL单元组成

H.264原始码流是由一个一个的NALU组成的,而NALU是由一个字节的Header和RBSP组成,而RBSP是由SODB和对齐字节数据组成(因为SODB是原始数据比特流, 长度不一定是8的倍数,需补足对齐),SODB就是真实的编码数据。

对于SODB中的数据,根据NAL单元的类型不同,存储了不同的数据,比如说对于Slice类型,其内部存储的就是Slice数据
,分为Slice Header和Slice Data。Slice Data中是由一个一个的MB(宏块)数据组成,MB又可以分为一个个的子MB,在子MB中存储了mb_type(宏块类型)、mb_pred(宏块预测类型)、coded residual(残差值)。

所以简单来看一个NAL单元是由一个字节的Header加上Slice数据组成,而Slice数据是由一个一个的宏块数据组成。

对于H.264来说,编码器会将每一帧图像都拆分成一个或多个Slice,每个Slice又分割成多个宏块,每个宏块是一个nxm大小的像素区域,每个宏块又可以切分成更小的子宏块。每个Slice编码完成后,会将当前Slice的编码数据打包成NAL单元下发出去。

H264码流结构

Annex B格式

这种格式的H.264码流用于实时流的传播中,其特点是码流中每个NALU(单元块)之间通过起始码来分割,起始码分为两种,一帧开始则用四个字节的1来表示,不是一帧开始就用三个字节的1来表示。

NAL单元类型

NAL单元的Header由一个字节的数据组成,由三个数据组成

  • forbidden_zero_bit : 1个比特,在H.264规范中规定了这一位必须为0。
  • nal_ref_idc : 2个比特,取00~11,指示这个NALU的重要性,取值越大,表示当前NAL越重要,需要优先受到保护。
  • nal_unit_type : 5个比特,表示NALU单元的类型。

音视频开发之ffplay渲染线程分析

视频渲染线程

视频渲染线程实际就是main线程。

初始化

初始化SDL
1
2
3
4
5
6
7
8
// SDL初始化
flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER;
if (SDL_Init(flags)) {
av_log(NULL, AV_LOG_FATAL, "Could not initialize SDL - %s\n",
SDL_GetError());
av_log(NULL, AV_LOG_FATAL, "(Did you set the DISPLAY variable?)\n");
exit(1);
}
创建Window和Renderer
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
if (borderless)
flags |= SDL_WINDOW_BORDERLESS; // 去掉窗口状态栏
else
flags |= SDL_WINDOW_RESIZABLE; // 窗口是否可缩放
// 创建SDL窗口
window = SDL_CreateWindow(program_name, SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED, default_width,
default_height, flags);
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
if (window) {
// 创建SDL渲染器,SDL_RENDERER_ACCELERATED 使用硬件加速
renderer = SDL_CreateRenderer(
window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!renderer) {
av_log(NULL, AV_LOG_WARNING,
"Failed to initialize a hardware accelerated renderer: %s\n",
SDL_GetError());
// 如果创建SDL渲染器失败,则去掉标记再重试,可能是当前设备不支持该标记
renderer = SDL_CreateRenderer(window, -1, 0);
}
if (renderer) {
// 输出渲染器信息
if (!SDL_GetRendererInfo(renderer, &renderer_info))
av_log(NULL, AV_LOG_VERBOSE, "Initialized %s renderer.\n",
renderer_info.name);
}
}
if (!window || !renderer || !renderer_info.num_texture_formats) {
// 如果窗口或者渲染器创建失败,或者渲染器中可用的纹理格式为0,则退出
av_log(NULL, AV_LOG_FATAL, "Failed to create window or renderer: %s",
SDL_GetError());
do_exit(NULL);
}

轮询

主线程在开启解复用线程后,就会开始轮询处理SDL事件

1
2
3
4
5
6
7
8
9
10
11
static void event_loop(VideoState *cur_stream) {
SDL_Event event;
double incr, pos, frac;
// 开始轮询SDL消息
for (;;) {
double x;
// 获取事件,如果有,则执行下面的switch,如果没有,则会尝试刷新视频渲染
refresh_loop_wait_event(cur_stream, &event);
...
}
}

音视频开发之ffplay解码线程分析

视频解码线程

轮询

不断从Packet队列中取出一帧数据进行解码,然后将解码数据放入到Frame队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for (;;) {
// 获取一帧解码数据
ret = get_video_frame(is, frame);
if (ret < 0) goto the_end;
if (!ret) continue;
...

// 根据帧率计算每帧的显示时长
duration = (frame_rate.num && frame_rate.den
? av_q2d((AVRational){frame_rate.den, frame_rate.num})
: 0);
// 将帧的PTS转换为秒
pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
// 将解码后的帧数据放入到视频原始数据队列中
ret = queue_picture(is, frame, pts, duration, frame->pkt_pos,
is->viddec.pkt_serial);
av_frame_unref(frame);
...

if (ret < 0) goto the_end;
}

音视频开发之ffplay解复用线程分析

由主线程创建,负责媒体文件的解复用和读取,读取的数据根据流类型放入到对应的编码数据队列中

查询流信息

创建解复用上下文
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

// 创建解复用上下文
ic = avformat_alloc_context();
if (!ic) {
...
}
// 设置中断回调
ic->interrupt_callback.callback = decode_interrupt_cb;
// 回调函数的参数
ic->interrupt_callback.opaque = is;
...
// 打开输入
err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
...

is->ic = ic;

if (genpts) ic->flags |= AVFMT_FLAG_GENPTS;

av_format_inject_global_side_data(ic);

if (find_stream_info) {
AVDictionary **opts = setup_find_stream_info_opts(ic, codec_opts);
int orig_nb_streams = ic->nb_streams;
// 读取文件头,获取文件的流详细信息
err = avformat_find_stream_info(ic, opts);

...
}
查找流索引

查找音频和视频流的索引

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
for (i = 0; i < ic->nb_streams; i++) {
AVStream *st = ic->streams[i];
enum AVMediaType type = st->codecpar->codec_type;
st->discard = AVDISCARD_ALL;
if (type >= 0 && wanted_stream_spec[type] && st_index[type] == -1)
if (avformat_match_stream_specifier(ic, st, wanted_stream_spec[type]) > 0)
st_index[type] = i;
}
for (i = 0; i < AVMEDIA_TYPE_NB; i++) {
if (wanted_stream_spec[i] && st_index[i] == -1) {
av_log(NULL, AV_LOG_ERROR,
"Stream specifier %s does not match any %s stream\n",
wanted_stream_spec[i], av_get_media_type_string(i));
st_index[i] = INT_MAX;
}
}
// 查找流索引
if (!video_disable)
st_index[AVMEDIA_TYPE_VIDEO] = av_find_best_stream(
ic, AVMEDIA_TYPE_VIDEO, st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);
if (!audio_disable)
st_index[AVMEDIA_TYPE_AUDIO] = av_find_best_stream(
ic, AVMEDIA_TYPE_AUDIO, st_index[AVMEDIA_TYPE_AUDIO],
st_index[AVMEDIA_TYPE_VIDEO], NULL, 0);
if (!video_disable && !subtitle_disable)
st_index[AVMEDIA_TYPE_SUBTITLE] = av_find_best_stream(
ic, AVMEDIA_TYPE_SUBTITLE, st_index[AVMEDIA_TYPE_SUBTITLE],
(st_index[AVMEDIA_TYPE_AUDIO] >= 0 ? st_index[AVMEDIA_TYPE_AUDIO]
: st_index[AVMEDIA_TYPE_VIDEO]),
NULL, 0);

音视频开发之ffplay队列分析

ffplay的整体结构是由5个线程和4个队列组成、运转的(不分析字幕)。

整体结构

音视频开发之ffplay队列分析

5个线程

  • 解复用线程 : read_thread,由主线程创建,负责媒体文件的解复用和读取,读取的数据根据流类型放入到对应的编码数据队列中。
  • 音频解码线程 : audio_thread,由read_thread创建,负责将编码数据队列中的数据解码,放入到原始数据队列中。
  • 视频解码线程 : video_thread,由read_thread创建,负责将编码数据队列中的数据解码,放入到原始数据队列中。
  • 音频渲染线程 : 由SDL创建,负责将音频原始数据队列中的数据发送给音频播放设备。
  • 视频渲染线程 : main线程,负责用视频原始数据队列中的数据不断的更新纹理内容,并刷新显示器进行显示。

4个队列

  • FrameQueue pictq : 视频原始数据队列
  • FrameQueue sampq : 音频原始数据队列
  • PacketQueue audioq : 音频编码数据队列
  • PacketQueue videoq : 视频编码数据队列
PacketQueue

Packet队列是基于链表实现的普通队列,由于编码数据帧比较小,所以这是个无限队列。

1
2
3
4
5
6
7
8
9
10
11
typedef struct PacketQueue {
// Packet队列采用的是链表结构
MyAVPacketList *first_pkt, *last_pkt; // 第一个节点和最后一个节点
int nb_packets; // 队列中节点个数
int size; // 队列中所有节点的字节总数
int64_t duration; // 队列中所有节点的总时长
int abort_request; // 是否退出标记 1 退出,0 不退出
int serial; // 序号
SDL_mutex *mutex; // 锁
SDL_cond *cond; // 互斥量
} PacketQueue;

音视频开发之SDL2播放视频

如果熟悉OpenGL,那么SDL来渲染视频就很简单了,Window对象时用来显示的窗口,Renderer是具体渲染的渲染器,Texture可以认为就是一张图像,不断的更新Texture的内容,将其显示在窗口上,这就完成了视频的播放。

创建Window

对于渲染视频来说,首先需要创建一个Window对象,这是与Native渲染体系相关联的一个窗口。

1
2
3
4
5
6
7
8
9
if (borderless)
flags |= SDL_WINDOW_BORDERLESS; // 去掉窗口状态栏
else
flags |= SDL_WINDOW_RESIZABLE; // 窗口是否可缩放
// 创建SDL窗口
// 参数(标题,x, y, w, h, 标记)
window = SDL_CreateWindow(program_name, SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED, default_width,
default_height, flags);

创建Renderer

Renderer负责具体的渲染逻辑

1
2
3
// 创建SDL渲染器,SDL_RENDERER_ACCELERATED 使用硬件加速
// 参数(渲染的Window,渲染驱动索引,标记)
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);

创建纹理

像素格式:
YUV、RGB
纹理访问格式:
SDL_TEXTUREACCESS_STATIC : 纹理不经常变更
SDL_TEXTUREACCESS_STREAMING : 纹理经常变更
SDL_TEXTUREACCESS_TARGET : 纹理可作为渲染的target

1
2
3
// 创建纹理 
// 参数(对应的渲染器,像素格式,纹理访问格式设置,w, h)
*texture = SDL_CreateTexture(renderer, new_format,SDL_TEXTUREACCESS_STREAMING, new_width,new_height)

什么是纹理

纹理是显卡中一段连续的内存,可以用来存储图片数据,也可以用来存储计算过程中的中间结果等任意数据。
而视频图像数据则是内存中的像素数组。

狭义地讲,纹理是存在于显存的,可以存放任意数据;是视频图像数据存在于内存的,内容是像素数组。纹理存放的不是真正的像素数据,而是存放的图像的描述信息。

所以可以清楚的明白,为什么要使用纹理,将CPU中的像素数据拷贝到GPU中进行存储,然后在GPU上进行处理后交由显示屏进行显示。

更新纹理

更新YUV格式的纹理数据,就是将YUV三个分量的数据和长度一次传入。

1
2
3
ret = SDL_UpdateYUVTexture(
*tex, NULL, frame->data[0], frame->linesize[0], frame->data[1],
frame->linesize[1], frame->data[2], frame->linesize[2]);

更新RGB格式的纹理数据

1
ret = SDL_UpdateTexture(*tex, NULL, frame->data[0], frame->linesize[0]);

显示纹理

Renderer有一个默认的渲染目标,可以通过SDL_SetRenderTarget来更改。要想将纹理的内容显示在屏幕上,需要先将纹理的内容拷贝到Renderer的默认渲染目标上,然后再通过SDL_RenderPresent来刷新屏幕。

1
2
3
4
// 将纹理上的内容拷贝到渲染器的默认渲染目标上
SDL_RenderCopy(renderer, texture, nullptr, &rect);
// 将渲染目标的内容刷新到屏幕上
SDL_RenderPresent(renderer);

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×