音视频开发之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) {
// 创建失败
}

音视频开发之Android录制视频

FFmpeg对于Android平台的视频输入设备API有一定的支持,但是比较局限,只支持Android N以上的版本,低版本无法使用。

其原因是因为FFmpeg采用的是Android N推出的native层camera API来实现的(NDK中的libcamera2ndk.so),而并非采用JNI的方式来调用Java层camera API。

Android平台视频录制API

对于视频录制API,Android平台上有两套API实现,一套是老版的Camera1,另一套是Android L之后推出的Camera2。

Camera1使用起来较为简单,但是功能相对较少,不支持多纹理输出。Camera2的API功能上虽然更加强大,但是API设计的非常底层化,不利于理解,并且YUV_420_888的数据格式,国内各大产商在实现上留下的坑太多。

Jetpack组件中,Google推出了全新的CameraX组件,对Camera1和Camera2进行了统一的封装,使得API更加简单易用。

Camera1

获取Camera设备信息
  • Camera.getNumberOfCameras() : 获取Camera设备数量
  • index : 索引就是后续会使用的CameraID
  • Camera.CameraInfo : Camera设备信息
  • Camera.getCameraInfo(index, info) : 获取指定索引的Camera设备信息,存储到info对象中
  • Camera.CameraInfo.facing : 相机面对的方向,前置还是后置 CAMERA_FACING_BACK or CAMERA_FACING_FRONT.
  • Camera.CameraInfo.orientation : 相机图像的方向,获取到的图像需要顺时针旋转该角度才能正常显示。值为0、90、180、270。(因为我们拿手机一般是竖着拿,但是摄像头可能是向左横着或向右横着被安装的)
  • Camera.open : 获取指定索引位置的Camera设备实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 遍历所有的camera设备
for (int index = 0; index < android.hardware.Camera.getNumberOfCameras(); ++index) {
// 获取camera设备信息
android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(index, info);
String facing =
(info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT) ? "front" : "back";
final android.hardware.Camera camera;
try {
// 开启camera设备,获取camera实例
camera = android.hardware.Camera.open(cameraId);
} catch (RuntimeException e) {
callback.onFailure(FailureType.ERROR, e.getMessage());
return;
}
}

音视频开发之Android录制音频

由于FFmpeg一直都没有支持Android平台的音频输入设备API,所以无法使用FFmpeg在Android上录制音频。

Android平台音频录制API

Android上音频输入的API比较繁琐,有多套实现。有Java层的实现AudioRecord,也有native层实现OpenSLES,在 Android O之后又推出了新的native层实现AAudio,并且提供了Oboe库,对OpenSLES和AAudio进行了封装。

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

AudioRecord

AudioRecord是Android平台提供的录制音频的Java层API,使用起来非常简单。通过设置采集参数就可以创建一个AudioRecord对象,调用了start方法后,就可以开始读取数据了,通过read方法将数据读取到指定的缓冲区中,调用stop后就停止采集。

创建AudioRecord
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
// 一个音频帧的字节大小 = 声道数 *(位数 /8)
final int bytesPerFrame = channels * (BITS_PER_SAMPLE / 8);
// BUFFERS_PER_SECOND = 100,预测一秒钟回调100次
// 那么44100的采样率,每次回调应该给的数据帧个数为4410个
final int framesPerBuffer = sampleRate / BUFFERS_PER_SECOND;
// 一次回调给的总数据字节大小 = 一次给的数据帧个数 * 一个数据帧的字节大小
// 创建一个buffer用来接收回调数据,使用堆外内存,在通过JNI传递的时候避免内存拷贝
byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer);

emptyBytes = new byte[byteBuffer.capacity()];
// 避免每次读取数据都需要传递buffer到JNI层,这里提前将buffer的地址保存在JNI层
nativeCacheDirectBufferAddress(byteBuffer, nativeAudioRecord);

final int channelConfig = channelCountToConfiguration(channels);
// 根据音频参数(采样率,声道数,位数)得到系统最小需要的缓冲区大小,创建AudioRecord时设置的缓冲区大小必须大于这个值
int minBufferSize =
AudioRecord.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);

// BUFFER_SIZE_FACTOR = 2
// 缓冲区一般要大一点,所以下面比较了两倍的最小缓冲区和我们自己计算的缓冲区大小
// 也就是说设置的缓冲区大小最小也要是最小缓冲区的两倍。
int bufferSizeInBytes = Math.max(BUFFER_SIZE_FACTOR * minBufferSize, byteBuffer.capacity());
try {
// 根据音频参数创建AudioRecord,audioSource是指音频采集的来源
audioRecord = new AudioRecord(audioSource, sampleRate, channelConfig,
AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes);
} catch (IllegalArgumentException e) {
return -1;
}
if (audioRecord == null || audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
return -1;
}

音视频开发之FFmpeg录制视频

视频录制流程

前面讲解录制音频时讲过,FFmpeg对各大平台的输入和输出API进行了统一的封装,这里就不在阐述。

视频的录制相比音频的录制流程基本差不多,主要是去掉了FIFO队列的逻辑。另外对于视频帧来说,FFmpeg提供的SwsContext API处理较慢,一般不太使用,可以用libyuv库,或者使用OpenGL API用GPU来提速。

0c9c218a544d7c258f06aeded22f3197

打开视频输入

注册视频设备

所有的输入和输出设备默认都是没有注册的,如果需要使用,需要手动调用下面的注册函数。

1
avdevice_register_all();
创建输入上下文

FFmpeg将对视频文件的解封装和封装等操作都抽象到AVFormatContext中,所以视频文件的打开和输出都是要通道AVFormatContext来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Mac平台上依旧使用avfoundation API
AVInputFormat* format = av_find_input_format("avfoundation");
// 视频的采集必须设置采集的分辨率和帧率以及像素格式,如果设置的组合在设备上不支持,会出错
AVDictionary* options = nullptr;
av_dict_set(&options, "video_size", "1280x720", 0);
av_dict_set(&options, "framerate", "30", 0);
av_dict_set(&options, "pixel_format", "nv12", 0);
// 开启输入上下文
if ((error = avformat_open_input(&fmt_ctx_in_, "0", format, &options)) < 0) {
std::cout << "Could not open input device " << av_err2str(error)
<< std::endl;
fmt_ctx_in_ = nullptr;
return error;
}

音视频开发之FFmpeg录制音频

音频录制流程

FFmpeg对各大平台的音频输入和输出的API进行了统一的封装,将输入API封装成AVInputFormat,将输出API封装成AVOutputFormat,并且将音频采集设备封装成一个音频文件进行处理,所以我们在采集音频数据的时候,可以将音频设备当做一个无限大的音频文件,不断的从音频文件中读取数据。

FFmpeg录制音频

打开音频输入

注册音频设备

所有的输入和输出设备默认都是没有注册的,如果需要使用,需要手动调用下面的注册函数。

1
avdevice_register_all();
创建输入上下文

FFmpeg将对音频文件的解封装和封装等操作都抽象到AVFormatContext中,所以音频文件的打开和输出都是要通道AVFormatContext来完成。

1
2
3
4
5
6
7
8
9
10
11
// 设置音频设备API Mac上为avfoundation
AVInputFormat* format = av_find_input_format("avfoundation");
// :0 为音频第一个设备,开启音频设备,FFmpeg中将音频设备封装为一个音频文件
// video:audio 如果视频设备为空,则写成 :音频设备
// 获取到输入上下文
if ((error = avformat_open_input(&fmt_ctx_in_, ":0", format, nullptr)) < 0) {
std::cout << "Could not open input device " << av_err2str(error)
<< std::endl;
fmt_ctx_in_ = nullptr;
return error;
}
Your browser is out-of-date!

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

×