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);

音视频开发之SDL2播放音频

使用SDL来播放音频非常简单,只需要根据指定的参数开启音频设备,然后设置音频回调函数,在音频回调函数中,将数据写入到指定的buffer中即可。
对于音频播放来讲,是音频设备主动向我们要数据,而并非我们主动写入数据到音频设备,音频设备维护了一个数据缓冲区,会将数据存放在缓冲区中进行逐一播放,当缓冲区中有空位时,就会通过回调函数向我们要数据,当缓冲区中数据已经填满时,则不会回调。

开启音频设备

SDL_AudioSpec是对音频参数的组合,开启音频设备的时候需要传入一组期望的参数组合,开启成功后会返回一个真正开启的参数组合。

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
SDL_AudioSpec wanted_spec, spec;

// 设置SDL音频播放参数

// 设置 采样率、声道数
wanted_nb_channels = av_get_channel_layout_nb_channels(wanted_channel_layout);
wanted_spec.channels = wanted_nb_channels;
wanted_spec.freq = wanted_sample_rate;


// 采样位数
wanted_spec.format = AUDIO_S16SYS;
// 静音
wanted_spec.silence = 0;
// 缓冲区样本数量(单声道样本数量)
wanted_spec.samples =
FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE,
2 << av_log2(wanted_spec.freq / SDL_AUDIO_MAX_CALLBACKS_PER_SEC));
// 回调函数
wanted_spec.callback = sdl_audio_callback;
// 回调函数参数
wanted_spec.userdata = opaque;
// 开启音频播放设备,如果开启失败,则更换参数不断重试
while (
!(audio_dev = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &spec,
SDL_AUDIO_ALLOW_FREQUENCY_CHANGE |
SDL_AUDIO_ALLOW_CHANNELS_CHANGE))) {
// 更换参数组合,重新尝试
}

回调函数

回调函数是在一个单独的线程中,第一个参数就是开启设备时设置的回调函数上下文。第二个参数就是需要写入的缓冲区,第三个参数是需要的数据字节大小。

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
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len) {
VideoState *is = opaque;
int audio_size, len1;

while (len > 0) {
// 计算还未读取的字节大小
len1 = is->audio_buf_size - is->audio_buf_index;
// 如果为读取的字节大小大于音频设备想要的大小,则使用音频设备想要的大小
if (len1 > len) len1 = len;
// 将audio_buf中的数据copy到stream中
if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)
memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
else {
memset(stream, 0, len1);
if (!is->muted && is->audio_buf)
SDL_MixAudioFormat(stream,
(uint8_t *)is->audio_buf + is->audio_buf_index,
AUDIO_S16SYS, len1, is->audio_volume);
}
// 减去已经传递给音频设备的buffer大小,如果剩下的还有,则再次读取传递
len -= len1;
// stream加上偏移量
stream += len1;
// 更新audio_buf的偏移量
is->audio_buf_index += len1;
}
}

音视频开发之Android播放视频

图像的显示最终都是由显示器完成的,显示器通过接收到的颜色矩阵来进行对应的显示。而颜色矩阵的产生一般有两种,一种是通过GPU来进行渲染生成,另一种是通过CPU来进行渲染生成。其中GPU比较适合来处理这件事情,所以其效率高。(硬件加速也就是指使用GPU来进行渲染加速)

在Android平台上,GPU渲染的API有两套,一套就是OpenGL-ES,另一套就是7.0后推出的Vulkan。目前使用最多的还是OpenGL-ES。

整个渲染流程中主要节点如下:
SurfaceFlinger <- SurfaceView <- Surface <- EGLSurface <- EGLContext <- OpenGL-ES

接入原生渲染体系

Android原生平台上封装了一整套View体系用来进行图像的渲染和显示,所以任何的渲染都必须基于这个体系才能正确的显示出来。

View系统中提供了两个View可以用来进行自定义渲染,一个是SurfaceView,另一个是TextureView。(关于SurfaceView和TextureView可以看我之前写的Android图形架构总览

只要将图形数据写入到SurfaceView或TextureView的Surface中,那么最终SurfaceFlinger服务就会将图形内容显示到显卡上。

所以对于播放视频来说,就是要将视频每一帧的图像数据写入到SurfaceView或TextureView的Surface中即可。

SurfaceView可以通过addCallback来接收来自原生渲染的生命周期回调,通过getSurface来获取内部的Surface对象。

1
2
this.getHolder().addCallback();
this.getHolder().getSurface()

TextureView可以通过setSurfaceTextureListener来接收生命周期回调,通过getSurfaceTexture可以获取到SurfaceTexture。

1
2
setSurfaceTextureListener();
Surface surface = new Surface(getSurfaceTexture());

Surface与EGLSurface

一个Surface对象,可以关联一个EGLSurface对象(window_surface),window_surface可以通过swap方法将其内部图像缓冲数据传入到Surface中(其它类型的EGLSurface是不可以的)。

1
2
3
4
5
6
7
// 创建一个提供给opengl-es绘制的surface(display,配置,原生window,指定属性)
if (!(eglSurface = eglCreateWindowSurface(display, config, aNativeWindow, 0))) {
return;
}

// android中创建NativeWindow
ANativeWindow *pNativeWindow = ANativeWindow_fromSurface(jenv, surface);

EGLSurface与EGLContext

EGLContext是与线程相关的,一个线程中只能激活一个EGLContext,激活的EGLContext关联一个EGLSurface,激活后,在当前线程调用OpenGL-ES的API都将作用到EGLSurface的缓冲区中。

1
2
3
4
5
// 创建context,在context中保存了opengl-es的状态信息 (display,配置,共享context的handle 一般设为null,属性)
// 一个display可以创建多个context
if (!(context = eglCreateContext(display, config, 0, context_attribs))) {
return;
}

音视频开发之Android播放音频

SDL是一个跨平台的音视频渲染库,是支持Android平台的,所以可以直接使用SDL库进行音频的播放。

但是SDL库在Android平台上的实现只有一种,就是通过JNI来调用Java层的AudioTrack来进行播放。所以如果你有其它的需求不想使用SDL库,那么可以直接使用Android平台原生API来播放音频。

Android平台音频播放API

前面录制的时候讲过,Android上音频输出的API比较繁琐,有多套实现。有Java层的实现AudioTrack,也有native层实现OpenSLES,在 Android O上又推出了新的native层实现AAudio,并且提供了Oboe库,对OpenSLES和AAudio进行了封装。

下面来详细的介绍下每套API的使用

AudioTrack

AudioTrack是Android平台提供的播放音频的Java层API,使用起来非常简单。通过设置待播放音频参数就可以创建一个AudioTrack对象,调用了play方法后,就可以开始写入数据了,通过write方法将数据写入,调用stop后就停止播放。

需要注意的是,在创建AudioTrack的时候,对于待播放的音频数据格式都已经设定好了,也就是说这个创建的AudioTrack只能播放这种格式的音频数据。所以一般在获取到音频流解码后,都需要将PCM数据进行重采样,重采样成和AudioTrack一样的数据格式,才能交给AudioTrack播放。

创建AudioTrack

AudioTrack的构造函数有两套,新的一套是在Android L推出的,老的一套已经被标记为deprecated。

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
// 根据指定采样率、声道数、位数获取最小需要的缓冲区大小,后续再创建AudioTrack时设置的缓冲区大小必须大于这个值
// 如果想要将缓冲区设大一点,比如 bufferSizeFactor = 1.5
final int minBufferSizeInBytes = (int) (AudioTrack.getMinBufferSize(sampleRate, channelConfig,
AudioFormat.ENCODING_PCM_16BIT) * bufferSizeFactor);

// 一个音频帧的字节大小 = 声道数 *(位数 /8)
final int bytesPerFrame = channels * (BITS_PER_SAMPLE / 8);

// 创建buffer用来存放每一次要写入的数据,使用堆外内存,避免JNI内存拷贝
// BUFFERS_PER_SECOND = 100,预测一秒钟回调100次
// 那么44100的采样率,每次回调应该给的数据帧个数为4410个
byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * (sampleRate / BUFFERS_PER_SECOND));

AudioTrack audioTrack;
if (Build.VERSION.SDK_INT >= 21) {
// AudioFormat是对音频的格式进行封装,包括采样率、声道数、位数等。
// AudioAttributes是对播放内容的描述
audioTrack = new AudioTrack(
new AudioAttributes.Builder()
// 音频的用途
.setUsage(usageAttribute)
// 音频内容的类型
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
new AudioFormat.Builder()
// 位数
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
// 采样率
.setSampleRate(sampleRateInHz)
// 声道数
.setChannelMask(channelConfig)
.build(),
bufferSizeInBytes,// 缓冲区大小
AudioTrack.MODE_STREAM,// 模式
// 生成新的会话ID
AudioManager.AUDIO_SESSION_ID_GENERATE );


} else {
// 构造参数:
// 音频流类型
// 采样率
// 声道数
// 位数
// 缓冲区大小
// 模式 : MODE_STATIC 预先将需要播放的音频数据读取到内存中,然后才开始播放。MODE_STREAM 边读边播,不会将数据直接加载到内存
audioTrack = new AudioTrack(AudioManager.STREAM_VOICE_CALL, sampleRateInHz, channelConfig,
AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes, AudioTrack.MODE_STREAM);

}

if (audioTrack == null || audioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
// 创建失败
}
Your browser is out-of-date!

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

×