Android媒体录制框架

Android媒体录制框架

我在此文中只写部分内容,详情参考网上的文章。

在android中录制音频有两种方式,MediaRecorder和AudioRecord。两者的区别如下:

  1. MediaRecorder
    简单方便,不需要理会中间录制过程,结束录制后可以直接得到音频文件进行播放;录制的音频文件是经过压缩的,需要设置编码器;录制的音频文件可以用系统自带的播放器播放。
  2. audiorecorder
    在声音录制过程中,可以处理采集的声音数据,如降噪、合成等。过程为一段一段进行录制然后得到数据分别进行处理。录制的是pcm格式的音频文件,需要用audiotrack来播放,audiotrack更接近底层。

MediaRecorder

Android MediaRecorder架构详解

Android MediaRecorder框架简洁梳理

Android MediaRecorder整体架构源码浅析

MediaRecorder概览图:

MediaRecorder整体框架图:

概要总结:

如上在运行时,整个MediaRecorder大致上可以分为Client和Server两个部分,分别在两个进程中运行,它们之间使用Binder机制实现IPC通信。

MediaPlayerService是多媒体框架中非常重要的一个服务,从框架图中可以看出,MediaRecorder是客户端,MediaPlayerService和MediaRecorderClient是服务器端,MediaPlayerService实现了IMediaPlayerService(接口类)定义的业务逻辑。MediaRecorderClient实现了IMediaRecorder(接口类)定义的业务逻辑,其主要功能包括prepare、start、pause、resume、stop、reset、release等。

C++/JNI层回调事件(JNI层:mr->setListener(listener):将回调接口类MediaRecorderListener设置给MediaRecorder)通知JAVA层是使用JNIMediaRecorderListener::notify(int msg, int ext1, int ext2),该方法通过调用MediaRecoder.java类中private static void postEventFromNative(Object mediarecorder_ref, int what, int arg1, int arg2, Object obj)方法在JNI中的方法句柄,把Native事件回调到Java层,然后使用EventHandler.java类发送事件回到主线层。

主要根据一个基本的录制音视频的调用流程开始分析:

  1. 创建:new MediaRecorder();
  2. 设置Camera:mRecorder.setCamera();
  3. 设置音频源(采集方式):mRecorder.setAudioSource();
  4. 设置视频源(采集方式):mRecorder.setVideoSource();
  5. 设置文件的输出格式:mRecorder.setOutputFormat();
  6. 设置Audio的编码格式(生成对应的编码器):mRecorder.setAudioEncoder();
  7. 设置Video的编码格式(生成对应的编码器):mRecorder.setVideoEncoder();
  8. 设置录制的视频编码比特率(每秒编码多少位bit):mRecorder.setVideoEncodingBitRate();
  9. 设置录制的视频帧率:mRecorder.setVideoFrameRate();
  10. 设置要捕获的视频的宽度和高度:mRecorder.setVideoSize();
  11. 设置记录会话的最大持续时间(毫秒):mRecorder.setMaxDuration();
  12. 设置一个Surface进行预览显示:mRecorder.setPreviewDisplay();
  13. 设置输出文件路径:mRecorder.setOutputFile();
  14. 准备录制:mRecorder.prepare();
  15. 开始录制:mRecorder.start();
  16. 暂停或恢复录制:mRecorder.pause()/resume();
  17. 停止录制:mRecorder.stop();
  18. 重置Recorder:mRecorder.reset();
  19. 释放Recorder资源:mRecorder.release();

StagefrightRecorder

如图所示,MediaRecorder在运行时,可以分成Client和Server两个部分,它们分别在两个进程中运行,它们之间使用Binder机制实现IPC通讯。

  1. 手机启动时会启动进程/system/bin/mediaserver。该进程会把media相关服务注册到ServiceManager中,如MediaPlayerService。

  2. 应用层创建MediaRecorder实例:mMediaRecorder = new MediaRecorder();调用SDK 中MediaRecorder.java,通过JNI方式调用到framework层android_media_MediaRecorder.cpp。

  3. 继而调用mediarecorder.cpp的构造函数,它首先会从ServiceManager中获得MediaPlayerService服务,然后通过服务来创建recorder。这个recorder就是录音的真实实例。

  4. 通过getMediaPlayerService得到的service其实是BpMediaPlayerService,它和mediaserver进程中的BnMediaPlayerService是相对应的,共同负责进程间binder通信。BpMediaPlayerService中的createMediaRecorder其实是通过binder机制将CREATE_MEDIA_RECORDER消息发送出去。

  5. 在BnMediaPlayerService中,通过onTransact()来处理接收到的消息,并返回结果。当接收消息中的code为CREATE_MEDIA_RECORDER时,调用MediaPlayerService中的createMediaRecorder函数。在该函数中创建了一个MediaRecorderClient的实例,也就是说MediaPlayerService会为每个client应用进程创建一个相应的MediaRecorderClient的实例,来提供服务。

  6. 如此MediaRecorder.cpp就得到了一个recorder的实例,对它来说这个实例和本地的其他类的实例没什么用法上的区别,但其实这个实例是运行在另外一个进程中。实现这种假象的就是binder机制。在MediaRecorderClient的构造函数中,才会真正的创建StagefrightRecorder的具体实例,即真正的录制对象,使用的StageFright多媒体框架。在android 4.0以后只有StagefrightRecorder一个录制框架。在2.2、2.3中还存在另外一个录制对象PVMediaRecorder,使用的是OpenCore框架实现录音或录像。

  • 音视频处理过程图:
  • 音视频Puller处理过程图:

录像流程

这里直接是网上的资料(内容竟然还是蛮多的):

Android基础总结: Camera2详解之一 API学习

android基础总结:Camera2详解之二调用流程图解

极客笔记 Android Camera2 API(里面有系列讲解)

使用Camera2 替代过时的Camera API(里面有谷歌文档链接)

一篇文章带你了解Android 最新Camera框架

android Camera2 API使用详解(不用看,和下一篇类似)

Android:Camera2开发详解(上):实现预览、拍照、保存照片等功能(不用看)

androidCamera2使用(和上一篇类似,更详细)

android-camera2预览拍照录制(接着上一篇)

马小藤的简书:

Android Camera简单整理(一)-Camera Android架构(基于Q)

Android Camera简单整理(二)-Qcom HAL3 Camx架构学习

Android Camera简单整理(三)-Mtk Camera MtkCam3架构学习

Android Camera简单整理(四)-Android Camera性能Debug经验

这是从谷歌官方文档翻译而来:

android.hardware.camera2包为连接到Android设备的各个相机设备提供了一个接口。它取代了不推荐使用的Camera类。

此包将相机设备建模为管道,它接收用于捕获单个帧的输入请求,根据请求捕获单个图像,然后输出一个捕获结果元数据包,以及一组用于请求的输出图像缓冲区。请求按顺序处理,多个请求可以同时进行。由于摄像头设备是具有多个阶段的管道,因此需要在运行中处理多个请求才能在大多数Android设备上保持全帧率。

要枚举、查询和打开可用的摄像头设备,请获取一个CameraManager实例。

一个CameraDevices提供一组静态属性信息,描述硬件设备以及设备的可用设置和输出参数。此信息通过CameraCharacteristics对象提供,并可通过getCameraCharacteristics(String)提供。

要从相机设备捕获或流式传输图像,应用程序必须首先创建一个带有一组输出Surfaces的camera capture session以使用相机设备,这用createCaptureSession(SessionConfiguration)来完成。每个Surface都必须预先配置appropriate size and format(如果适用)以匹配相机设备可用的尺寸和格式。目标Surface(包括SurfaceView、SurfaceTexture)可以从各种类中获得,例如:Surface(SurfaceTexture)、MediaCodec、MediaRecorder、Allocation和ImageReader。

通常,相机预览图像被发送到SurfaceView或TextureView(通过其SurfaceTexture)。可以使用JPEG和RAW_SENSOR格式的ImageReader为DngCreator捕获JPEG图像或RAW缓冲区。在RenderScript、OpenGL ES中或直接在托管代码或本机代码中,应用程序驱动的相机数据处理最好分别通过YUV类型、SurfaceTexture和ImageReader的YUV_420_888格式的分配来完成。

然后,应用程序需要构建一个CaptureRequest,它定义了相机设备捕获单个图像所需的所有捕获参数。该请求还列出了哪些配置的输出Surfaces应该作为此捕获的目标。CameraDevice有一个factory method用于为给定用例创建一个request builder,它针对运行应用程序的Android设备进行了优化。

设置请求后,可以将其交给活动捕获会话以供一次性使用capture或无休止地repeating使用。这两种方法还有一个变体,它接受请求列表以用作突发捕获/重复突发。重复请求的优先级低于当前捕获,因此在有重复请求时通过capture()提交的请求将在当前重复(突发)捕获的任何新实例开始捕获之前被捕获。

处理请求后,相机设备将生成一个TotalCaptureResult对象,其中包含有关相机设备在捕获时的状态以及使用的最终设置的信息。如果需要舍去或解决矛盾的参数,这些可能与请求有所不同。相机设备还将向请求中包含的每个输出Surfaces发送一帧图像数据。这些是相对于输出CaptureResult异步生成的,有时会晚得多。

这是谷歌关于相机的文档AOSP开发相机

录音流程

这是AudioRecorder的架构和使用流程图:

也是先放网上的资料:

AudioRecord API Reference Document

Android录音AudioRecord,AudioTrack学习

这是从谷歌官方文档翻译而来:

AudioRecord类管理Java应用程序的音频资源,以记录来自平台音频输入硬件的音频。这是通过从AudioRecord对象“拉”(读取)数据来实现的。应用程序负责使用以下三种方法之一read(byte[], int, int),read(short[], int, int)或read(java.nio.ByteBuffer, int)及时轮询AudioRecord对象。选择使用哪种方法将基于AudioRecord用户最方便的音频数据存储格式。

在创建时,AudioRecord对象初始化其关联的音频缓冲区,它将用新的音频数据填充。这个缓冲区的大小,在构造过程中指定,决定了AudioRecord在“溢出”尚未读取的数据之前可以记录多长时间。应该从音频硬件中以小于总记录缓冲区大小的块读取数据。

创建AudioRecord实例的应用程序需要Manifest.permission.RECORD_AUDIO权限。否则Builder将在build()中抛出UnsupportedOperationException,并且构造函数将返回一个STATE_UNINITIALIZED状态的实例 。

利用AudioRecord实现Android录音的流程为:

  • 构造一个AudioRecord对象,其中需要的最小录音缓存buffer大小可以通过。getMinBufferSize方法得到。如果buffer容量过小,将导致对象构造的失败。
  • 初始化一个buffer,该buffer大于等于AudioRecord对象用于写声音数据的buffer大小。
  • 调用startRecording函数,开始录音。
  • 创建一个数据流,一边从AudioRecord中读取声音数据到初始化的buffer,一边将buffer中数据导入数据流,生成PCM格式文件
  • 录音结束时,关闭数据流, 停止录音。

代码实现如下:

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.text.TextUtils;
import android.util.Log;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
* 实现录音
*
* @author chenmy0709
* @version V001R001C01B001
*/
public class AudioRecorder {
//音频输入-麦克风
private final static int AUDIO_INPUT = MediaRecorder.AudioSource.MIC;
//采用频率
//44100是目前的标准,但是某些设备仍然支持22050,16000,11025
//采样频率一般共分为22.05KHz、44.1KHz、48KHz三个等级
private final static int AUDIO_SAMPLE_RATE = 16000;
//声道 单声道
private final static int AUDIO_CHANNEL = AudioFormat.CHANNEL_IN_MONO;
//编码
private final static int AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT;
// 缓冲区字节大小
private int bufferSizeInBytes = 0;

//录音对象
private AudioRecord audioRecord;

//录音状态
private Status status = Status.STATUS_NO_READY;

//文件名
private String fileName;

//录音文件
private List<String> filesName = new ArrayList<>();


/**
* 类级的内部类,也就是静态类的成员式内部类,该内部类的实例与外部类的实例
* 没有绑定关系,而且只有被调用时才会装载,从而实现了延迟加载
*/
private static class AudioRecorderHolder {
/**
* 静态初始化器,由JVM来保证线程安全
*/
private static AudioRecorder instance = new AudioRecorder();
}

private AudioRecorder() {
}

public static AudioRecorder getInstance() {
return AudioRecorderHolder.instance;
}

/**
* 创建录音对象
*/
public void createAudio(String fileName, int audioSource, int sampleRateInHz, int channelConfig, int audioFormat) {
// 获得缓冲区字节大小
bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz,
channelConfig, channelConfig);
audioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes);
this.fileName = fileName;
}

/**
* 创建默认的录音对象
*
* @param fileName 文件名
*/
public void createDefaultAudio(String fileName) {
// 获得缓冲区字节大小
bufferSizeInBytes = AudioRecord.getMinBufferSize(AUDIO_SAMPLE_RATE,
AUDIO_CHANNEL, AUDIO_ENCODING);
audioRecord = new AudioRecord(AUDIO_INPUT, AUDIO_SAMPLE_RATE, AUDIO_CHANNEL, AUDIO_ENCODING, bufferSizeInBytes);
this.fileName = fileName;
status = Status.STATUS_READY;
}


/**
* 开始录音
*
* @param listener 音频流的监听
*/
public void startRecord(final RecordStreamListener listener) {

if (status == Status.STATUS_NO_READY || TextUtils.isEmpty(fileName)) {
throw new IllegalStateException("录音尚未初始化,请检查是否禁止了录音权限~");
}
if (status == Status.STATUS_START) {
throw new IllegalStateException("正在录音");
}
Log.d("AudioRecorder", "===startRecord===" + audioRecord.getState());
audioRecord.startRecording();

new Thread(new Runnable() {
@Override
public void run() {
writeDataTOFile(listener);
}
}).start();
}

/**
* 暂停录音
*/
public void pauseRecord() {
Log.d("AudioRecorder", "===pauseRecord===");
if (status != Status.STATUS_START) {
throw new IllegalStateException("没有在录音");
} else {
audioRecord.stop();
status = Status.STATUS_PAUSE;
}
}

/**
* 停止录音
*/
public void stopRecord() {
Log.d("AudioRecorder", "===stopRecord===");
if (status == Status.STATUS_NO_READY || status == Status.STATUS_READY) {
throw new IllegalStateException("录音尚未开始");
} else {
audioRecord.stop();
status = Status.STATUS_STOP;
release();
}
}

/**
* 释放资源
*/
public void release() {
Log.d("AudioRecorder", "===release===");
//假如有暂停录音
try {
if (filesName.size() > 0) {
List<String> filePaths = new ArrayList<>();
for (String fileName : filesName) {
filePaths.add(FileUtil.getPcmFileAbsolutePath(fileName));
}
//清除
filesName.clear();
//将多个pcm文件转化为wav文件
mergePCMFilesToWAVFile(filePaths);

} else {
//这里由于只要录音过filesName.size都会大于0,没录音时fileName为null
//会报空指针 NullPointerException
// 将单个pcm文件转化为wav文件
//Log.d("AudioRecorder", "=====makePCMFileToWAVFile======");
//makePCMFileToWAVFile();
}
} catch (IllegalStateException e) {
throw new IllegalStateException(e.getMessage());
}

if (audioRecord != null) {
audioRecord.release();
audioRecord = null;
}

status = Status.STATUS_NO_READY;
}

/**
* 取消录音
*/
public void canel() {
filesName.clear();
fileName = null;
if (audioRecord != null) {
audioRecord.release();
audioRecord = null;
}

status = Status.STATUS_NO_READY;
}


/**
* 将音频信息写入文件
*
* @param listener 音频流的监听
*/
private void writeDataTOFile(RecordStreamListener listener) {
// new一个byte数组用来存一些字节数据,大小为缓冲区大小
byte[] audiodata = new byte[bufferSizeInBytes];

FileOutputStream fos = null;
int readsize = 0;
try {
String currentFileName = fileName;
if (status == Status.STATUS_PAUSE) {
//假如是暂停录音 将文件名后面加个数字,防止重名文件内容被覆盖
currentFileName += filesName.size();

}
filesName.add(currentFileName);
File file = new File(FileUtil.getPcmFileAbsolutePath(currentFileName));
if (file.exists()) {
file.delete();
}
fos = new FileOutputStream(file);// 建立一个可存取字节的文件
} catch (IllegalStateException e) {
Log.e("AudioRecorder", e.getMessage());
throw new IllegalStateException(e.getMessage());
} catch (FileNotFoundException e) {
Log.e("AudioRecorder", e.getMessage());

}
//将录音状态设置成正在录音状态
status = Status.STATUS_START;
while (status == Status.STATUS_START) {
readsize = audioRecord.read(audiodata, 0, bufferSizeInBytes);
if (AudioRecord.ERROR_INVALID_OPERATION != readsize && fos != null) {
try {
fos.write(audiodata);
if (listener != null) {
//用于拓展业务
listener.recordOfByte(audiodata, 0, audiodata.length);
}
} catch (IOException e) {
Log.e("AudioRecorder", e.getMessage());
}
}
}
try {
if (fos != null) {
fos.close();// 关闭写入流
}
} catch (IOException e) {
Log.e("AudioRecorder", e.getMessage());
}
}

/**
* 将pcm合并成wav
*
* @param filePaths
*/
private void mergePCMFilesToWAVFile(final List<String> filePaths) {
new Thread(new Runnable() {
@Override
public void run() {
if (PcmToWav.mergePCMFilesToWAVFile(filePaths, FileUtil.getWavFileAbsolutePath(fileName))) {
//操作成功
} else {
//操作失败
Log.e("AudioRecorder", "mergePCMFilesToWAVFile fail");
throw new IllegalStateException("mergePCMFilesToWAVFile fail");
}
fileName = null;
}
}).start();
}

/**
* 将单个pcm文件转化为wav文件
*/
private void makePCMFileToWAVFile() {
new Thread(new Runnable() {
@Override
public void run() {
if (PcmToWav.makePCMFileToWAVFile(FileUtil.getPcmFileAbsolutePath(fileName), FileUtil.getWavFileAbsolutePath(fileName), true)) {
//操作成功
} else {
//操作失败
Log.e("AudioRecorder", "makePCMFileToWAVFile fail");
throw new IllegalStateException("makePCMFileToWAVFile fail");
}
fileName = null;
}
}).start();
}

/**
* 获取录音对象的状态
*
* @return
*/
public Status getStatus() {
return status;
}

/**
* 获取本次录音文件的个数
*
* @return
*/
public int getPcmFilesCount() {
return filesName.size();
}

/**
* 录音对象的状态
*/
public enum Status {
//未开始
STATUS_NO_READY,
//预备
STATUS_READY,
//录音
STATUS_START,
//暂停
STATUS_PAUSE,
//停止
STATUS_STOP
}

}

AudioBitrate客制化

各种各样的图

  • PersistentSurface及GraphicBufferSource实现的BufferQueue框架:
  • 写文件的过程及重要类:
  • MediaRecorder init:
  • MediaRecorder prepare:
  • MediaCodecSource 即对应videoEncoder和audioEncoder的初始化:
  • MediaRecorder start:
  • MediaRecorder stop:
  • MediaRecorder init flow:
  • PersistentSurface init and setInputSurface flow:
  • PersistentSurface prepare flow:
  • MediaRecorder start flow:
  • MediaRecorder Buffer Callback from OMX to MediaRecorder:
  • MediaRecorder stop flow:

Android媒体录制框架
https://blog.siantao.top/技术/计算机/软件/Android/多媒体/媒体录制/Android媒体录制框架/
作者
玉水仙楊
发布于
2022年6月1日
许可协议