最近晚上和周末基本都在排队练车,累成狗,好久没写文章了~
抽空整理了一下音视频采集的方式,最终生成mp4。
一、音频采集,得到PCM数据
音频采集比较简单,通过 AudioRecord
录音,然后在子线程不断去读PCM数据
记得声明录音权限 <uses-permission android:name="android.permission.RECORD_AUDIO" />
开始录音
//默认参数
private static final int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
private static final int SAMPLE_RATE = 44100;
private static final int CHANNEL_CONFIGS = AudioFormat.CHANNEL_IN_STEREO;
private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
private int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIGS, AUDIO_FORMAT);
private AudioRecord audioRecord;
public void start() {
start(AUDIO_SOURCE, SAMPLE_RATE, CHANNEL_CONFIGS, AUDIO_FORMAT);
}
public void start(int audioSource, int sampleRate, int channels, int audioFormat) {
if (isStartRecord) {
Log.d(TAG, "音频录制已经开启");
return;
}
bufferSize = AudioRecord.getMinBufferSize(sampleRate, channels, audioFormat);
if (bufferSize == AudioRecord.ERROR_BAD_VALUE) {
Log.d(TAG, "无效参数");
return;
}
audioRecord = new AudioRecord(audioSource, sampleRate, channels, audioFormat, bufferSize);
audioRecord.startRecording();
isStopRecord = false;
threadCapture = new Thread(new CaptureRunnable());
threadCapture.start();
}
主要是创建AudioRecord的一些参数,然后调用audioRecord.startRecording();
开始录制,然后启动一个子线程,去读取录制的PCM格式的数据
读取PCM
/**
* 子线程读取采集到的PCM数据
*/
private class CaptureRunnable implements Runnable {
@Override
public void run() {
while (!isStopRecord) {
byte[] buffer = new byte[bufferSize];
int readRecord = audioRecord.read(buffer, 0, bufferSize);
if (readRecord > 0) {
if (captureListener != null)
captureListener.onCaptureListener(buffer,readRecord);
Log.d(TAG, "音频采集数据源 -- ".concat(String.valueOf(readRecord)).concat(" -- bytes"));
} else {
Log.d(TAG, "录音采集异常");
}
//延迟写入 SystemClock -- Android专用
SystemClock.sleep(10);
}
}
}
读取PCM 比较简单,就是通过audioRecord.read(buffer, 0, bufferSize)
,最终PCM格式的数据会读到这个buffer里
拿到录制的每一帧PCM数据之后,可以用AudioTrack播放,这里就不播放了,回调出去,后面合成mp4要用到。
上面两个步骤,可以封装一个录音的管理类。
【传送门】(待补充)
现在获取的音频是PCM格式,我们要将它编码成aac,然后跟视频数据合成mp4,这里要用到 MediaCodec
和 MediaMuxer
MediaCodec 使用
MediaCodec 是一个音视频编解码器,本篇主要用于:
- 将PCM格式的音频数据编码成aac格式,
- 将NV21格式的相机预览数据编码成avc格式。
API 简介
getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组
queueInputBuffer:输入流入队列
dequeueInputBuffer:从输入流队列中取数据进行编码操作
getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组
dequeueOutputBuffer:从输出队列中取出编码操作之后的数据
releaseOutputBuffer:处理完成,释放ByteBuffer数据
初始化音频编解码器
private MediaCodec mAudioCodec;
String audioType = MediaFormat.MIMETYPE_AUDIO_AAC; //编码成aac格式
int sampleRate = 44100;
int channels = 2;//单声道 channelCount=1 , 双声道 channelCount=2
private void initAudioCodec(String audioType, int sampleRate, int channels) {
try {
mAudioCodec = MediaCodec.createEncoderByType(audioType);
MediaFormat audioFormat = MediaFormat.createAudioFormat(audioType, sampleRate, channels);
int BIT_RATE = 96000;
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE,
MediaCodecInfo.CodecProfileLevel.AACObjectLC);
int MAX_INOUT_SIZE = 8192;
audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, MAX_INOUT_SIZE);
mAudioCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (IOException e) {
Log.e(TAG, "initAudioCodec: 音频类型无效");
}
}
视频编解码器的初始化同理
String videoType = MediaFormat.MIMETYPE_VIDEO_AVC;
private void initVideoCodec(String videoType, int width, int height) {
try {
mVideoCodec = MediaCodec.createEncoderByType(videoType);
MediaFormat videoFormat = MediaFormat.createVideoFormat(videoType, width, height);
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
//MediaFormat.KEY_FRAME_RATE -- 可通过Camera#Parameters#getSupportedPreviewFpsRange获取
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
//mSurfaceWidth*mSurfaceHeight*N N标识码率低、中、高,类似可设置成1,3,5,码率越高视频越大,也越清晰
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 4);
//每秒关键帧数
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
videoFormat.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
videoFormat.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel31);
}
mVideoCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
//注意这里,获取视频解码器的surface,之后要将opengl输出到这个surface中
mSurface = mVideoCodec.createInputSurface();
} catch (IOException e) {
Log.e(TAG, "initVideoCodec: 视频类型无效");
}
}
这里要注意的是,音频数据从AudioRecord 直接读出来,但是视频数据处理有些不同,视频数据不直接读相机预览,而是通过相机纹理id,利用OpenGL直接渲染到视频编解码器的surface上,可以直接被编码,效率比较高,所以这里获取了MediaCodec的surface,mSurface = mVideoCodec.createInputSurface();
,后面通过EGL创建NatieWindow的时候要用到,这里大概先了解一下就行,关于GLSurfaceView原理,EGL的使用,后面再整理一篇文章吧。
两个编解码器创建好了,接下来要用到混合器, MediaMuxer
MediaMuxer 使用详解
MediaMuxer 是一个音视频混合器,我们录制音频和视频数据,经过MediaCodec编码,然后再将编码后的音视频数据混合在一起,最终生成mp4。
MediaMuxer主要方法:
1.int addTrack(@NonNull MediaFormat format)
一个视频文件是包含一个或多个音视频轨道的,而这个方法就是用于添加一个音频频或视频轨道,并返回对应的ID。之后我们可以通过这个ID向相应的轨道写入数据。用于新建音视频轨道的MediaFormat是需要从MediaCodec.getOutputFormat()获取的,而不是自己简单构造的MediaFormat。
2.start()
当我们添加完所有音视频轨道之后,需要调用这个方法告诉Muxer,我要开始写入数据了。需要注意的是,调用了这个方法之后,我们是无法再次addTrack了的。
3.void writeSampleData(int trackIndex, @NonNull ByteBuffer byteBuf,
@NonNull BufferInfo bufferInfo)
用于向Muxer写入编码后的音视频数据。trackIndex是我们addTrack的时候返回的ID,byteBuf便是要写入的数据,而bufferInfo是跟这一帧byteBuf相关的信息,包括时间戳、数据长度和数据在ByteBuffer中的位移
4.void stop()
与start()相对应,用于停止写入数据,并生成文件。
5.void release()
释放Muxer资源。
MediaMuxer 实战
我们先来构造一个 MediaMuxer ,需要两个参数,第一个是音视频文件的保存路径,第二个是音视频封装文件的格式,可以选择mp4或3gp,我们使用mp4就好
int mediaFormat = MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4;
private void initMediaMuxer(String filePath, int mediaFormat) {
try {
mMediaMuxer = new MediaMuxer(filePath, mediaFormat);
} catch (IOException e) {
Log.e(TAG, "initMediaMuxer: 文件打开失败,path=" + filePath);
}
}
添加音频轨道
添加音频轨道,在音频编码线程 AudioCodecThread 处理
public void run() {
super.run();
mIsStop = false;
audioCodec.start();
while (true) {
if (mMediaEncodeManager == null) {
Log.e(TAG, "run: mediaEncodeManagerWeakReference == null");
return;
}
if (mIsStop) {
mMediaEncodeManager.audioStop();
return;
}
//获取一帧解码完成的数据到bufferInfo,没有数据就阻塞
int outputBufferIndex = audioCodec.dequeueOutputBuffer(bufferInfo, 0);
//第一次会返回-2,在这时候添加音轨
if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
mAudioTrackIndex = mediaMuxer.addTrack(audioCodec.getOutputFormat());
mMediaEncodeManager.mAudioTrackReady = true;
Log.d(TAG, "run: 添加音轨 mAudioTrackIndex= " + mAudioTrackIndex);
mMediaEncodeManager.startMediaMuxer();
} else {
while (outputBufferIndex != 0) {
if (!mMediaEncodeManager.mEncodeStart) {
Log.d(TAG, "run: 混合器还没开始,线程延迟");
SystemClock.sleep(10);
continue;
}
ByteBuffer outputBuffer = audioCodec.getOutputBuffers()[outputBufferIndex];
outputBuffer.position(bufferInfo.offset);
outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
if (mPresentationTimeUs == 0) {
mPresentationTimeUs = bufferInfo.presentationTimeUs;
}
bufferInfo.presentationTimeUs = bufferInfo.presentationTimeUs - mPresentationTimeUs;
mediaMuxer.writeSampleData(mAudioTrackIndex, outputBuffer, bufferInfo);
audioCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = audioCodec.dequeueOutputBuffer(bufferInfo, 0);
}
}
}
}
添加视频轨道
添加视频轨道,在视频编码线程 VideoCodecThread 处理
public void run() {
mIsStop = false;
videoCodec.start();
while (true) {
if (mMediaEncodeManager == null) {
Log.e(TAG, "run: mMediaEncodeManager == null");
return;
}
if (mIsStop) {
mMediaEncodeManager.videoStop();
return;
}
int outputBufferIndex = videoCodec.dequeueOutputBuffer(bufferInfo, 0);
//第一次返回 -2,在这个时候添加音轨
if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
mVideoTrackIndex = mediaMuxer.addTrack(videoCodec.getOutputFormat());
Log.d(TAG, "添加视频轨道,mVideoTrackIndex = " + mVideoTrackIndex);
mMediaEncodeManager.mVideoTrackReady = true;
mMediaEncodeManager.startMediaMuxer();
} else {
while (outputBufferIndex >= 0) {
if (!mMediaEncodeManager.mEncodeStart) {
Log.d(TAG, "run: 混合器还没开始,线程延迟");
SystemClock.sleep(10);
continue;
}
ByteBuffer outputBuffer = videoCodec.getOutputBuffers()[outputBufferIndex];
outputBuffer.position(bufferInfo.offset);
outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
if (mPresentationTimeUs == 0) {
mPresentationTimeUs = bufferInfo.presentationTimeUs;
}
bufferInfo.presentationTimeUs = bufferInfo.presentationTimeUs - mPresentationTimeUs;
mediaMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, bufferInfo);
if (bufferInfo != null) {
mMediaEncodeManager.onRecordTimeCallBack((int) (bufferInfo.presentationTimeUs / 1000000));
}
videoCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = videoCodec.dequeueOutputBuffer(bufferInfo, 0);
}
}
}
}
添加音频轨道和视频轨道后,就可以启动混合器,然后不断从编解码器MediaCodec中读取已经编码成功的数据,然后调用mediaMuxer.writeSampleData(mAudioTrackIndex, outputBuffer, bufferInfo);
将编码后的音/视频数据写到混合器里,停止的时候要调用
mMediaMuxer.stop();
mMediaMuxer.release();
如果不出意外的话,会在指定目录下生成mp4文件。
然后在PCM回调那里,将PCM数据扔到 MediaCodec 里面去,这样AudioCodecThread 里面就能读到已经编码的aac格式数据。
public void setPcmSource(byte[] pcmBuffer, int buffSize) {
try {
int buffIndex = mAudioCodec.dequeueInputBuffer(0);
if (buffIndex < 0) {
return;
}
ByteBuffer byteBuffer;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
byteBuffer = mAudioCodec.getInputBuffer(buffIndex);
} else {
byteBuffer = mAudioCodec.getInputBuffers()[buffIndex];
}
byteBuffer.clear();
byteBuffer.put(pcmBuffer);
//mPresentationTimeUs = 1000000L * (buffSize / 2) / mSampleRate
//一帧音频帧大小 int size = 采样率 x 位宽 x 采样时间 x 通道数
// 1s时间戳计算公式 mPresentationTimeUs = 1000000L * (totalBytes / mSampleRate/ mAudioFormat / mChannelCount / 8 )
//totalBytes : 传入编码器的总大小
//1000 000L : 单位为 微秒,换算后 = 1s,
//除以8 : pcm原始单位是bit, 1 byte = 8 bit, 1 short = 16 bit, 用 Byte[]、Short[] 承载则需要进行换算
mPresentationTimeUs += (long) (1.0 * buffSize / (mSampleRate * mChannelCount * (mAudioFormat / 8)) * 1000000.0);
Log.d(TAG, "pcm一帧时间戳 = " + mPresentationTimeUs / 1000000.0f);
mAudioCodec.queueInputBuffer(buffIndex, 0, buffSize, mPresentationTimeUs, 0);
} catch (IllegalStateException e) {
//mAudioCodec 线程对象已释放MediaCodec对象
Log.d(TAG, "setPcmSource: " + "MediaCodec对象已释放");
}
}
视频数据通过OpenGL渲染到视频编解码器的surface中,只要打开相机,视频编码器就能获取到编码后的视频数据,然后写到混合器里,跟音频处理基本差不多。当然,这里涉及到自定义GLSurfaceView,参照GLSurfaceView中对EGL的处理,自己写EglHelper,这里不是本文重点,后面有时间再说下GLSurfaceView源码。
接下来再简单看一下如何通过Camera1采集视频数据
视频数据采集
相机功能封装在 CameraManager中,使用的是Camera1,需要注意的是设置预览数据格式,还有一个是SurfaceTexture,在外部创建(OpenGL创建纹理的时候),然后再启动相机,把纹理传过去,简单贴下启动相机代码
private void startCamera(int cameraId) {
try {
camera = Camera.open(cameraId);
camera.setPreviewTexture(surfaceTexture);
Camera.Parameters parameters = camera.getParameters();
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
parameters.setPreviewFormat(ImageFormat.NV21);
//设置对焦模式,后置摄像头开启时打开,切换到前置时关闭(三星、华为不能设置前置对焦,魅族、小米部分机型可行)
if (cameraId == 0) {
//小米、魅族手机存在对焦无效情况,需要针对设备适配,想要无感知对焦完全适配最好是监听加速度传感器
camera.cancelAutoFocus();
//这种设置方式存在屏幕闪烁一下问题,包括Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
}
Camera.Size size = getCameraSize(parameters.getSupportedPreviewSizes(), screenWidth,
screenHeight, 0.1f);
parameters.setPreviewSize(size.width, size.height);
//水平方向未旋转,所以宽就是竖直方向的高,对应旋转操作
Log.d(TAG, "startCamera: 预览宽:" + size.width + " -- " + "预览高:" + size.height);
previewWidth = size.width;
previewHeight = size.height;
size = getCameraSize(parameters.getSupportedPictureSizes(), screenWidth, screenHeight, 0.1f);
parameters.setPictureSize(size.width, size.height);
//水平方向未旋转,所以宽就是竖直方向的高
Log.d(TAG, "startCamera: 图片宽:" + size.width + " -- " + "图片高:" + size.height);
camera.setParameters(parameters);
camera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
}
相机启动之后预览数据会输出到 surfaceTexture,这个surfaceTexture 关联一个纹理id,就是通过OpenGL创建并绑定的纹理id
/**
* 创建摄像头预览扩展纹理
*/
private void createCameraTexture() {
int[] textureIds = new int[1];
GLES20.glGenTextures(1, textureIds, 0);
cameraTextureId = textureIds[0];
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTextureId);
//环绕(超出纹理坐标范围) (s==x t==y GL_REPEAT 重复)
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
//过滤(纹理像素映射到坐标点) (缩小、放大:GL_LINEAR线性)
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
surfaceTexture = new SurfaceTexture(cameraTextureId);
surfaceTexture.setOnFrameAvailableListener(this);
if (onSurfaceListener != null) {
//回调给CameraManager获取surfaceTexture:通过camera.setPreviewTexture(surfaceTexture);
onSurfaceListener.onSurfaceCreate(surfaceTexture, fboTextureId);
}
// 解绑扩展纹理
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
}
总结一下相机数据采集过程
- OpenGL创建纹理,绑定一个纹理id
- 启动相机,传入前面创建的纹理,这样相机预览数据就会输出到OpenGL绑定的纹理。
- 纹理并不能直接拿来编码,需要参考GLSurfaceView的显示原理,创建EGL,通过OpenGL不断将纹理渲染到MediaCodec的surface上,然后在一个子线程不断获取MediaCodec 中编码成功的数据,后面就跟音频处理一样,添加到混合器里,最终合成mp4文件。
对OpenGL不熟悉的话没关系,有时间的话可以去学一下,不需要太深,也可以在我的简书主页查看OpenGL的入门系列文章
https://www.jianshu.com/u/282785a6b12f
这篇文章内容属于音视频开发的基础部分了,后面要整理相机推流,会涉及到音视频采集,也就是本章内容。
在后面章节完成之后会把源码提交到github,
想让自己变优秀,就要少看头条,少刷抖音,坚持学习,写文章,不然的话可能如下图:
网友评论