前言
前面Android实现录屏直播+远程控制(一)和Android实现录屏直播+远程控制(二)两篇文章说到了实现Android录屏的方法
接下来就讲讲录制音视频的幕后黑手,这里实现录制音视频也有两种方案,分别是MediaRecorder和MediaCodec
什么是MediaRecorder
MediaRecorder是安卓提供的一个用于音视频采集的类
MediaRecorder的优缺点
优点
可以实现直接录制视频 使用方便,得到就是编码和封装好的音视频文件,可以直接使用
缺点
无法获取原始数据,不能对每一帧数据进行处理,无法支持我们程序中自己需要的一些逻辑
由于不满足我的需求,所以这里就不再对MediaRecorder讲解了,那接下来我们来说说MediaCodec
什么是MediaCodec
MediaCodec类是Android平台提供的用于访问低层多媒体硬件编/解码器接口,它是Android低层多媒体架构的一部分,通常与MediaExtractor、MediaMuxer、AudioTrack结合使用,能够编解码诸如H.264、H.265、AAC、3gp等常见的音视频格式。
MediaCodec工作原理
MediaCodec的工作原理就是处理输入数据以产生输出数据。具体来说,MediaCodec在编解码的过程中使用了一组输入/输出缓存区来同步或异步处理数据:首先,客户端向获取到的编解码器输入缓存区写入要编解码的数据并将其提交给编解码器,待编解码器处理完毕后将其转存到编码器的输出缓存区,同时收回客户端对输入缓存区的所有权;然后,客户端从获取到编解码输出缓存区读取编码好的数据进行处理,待处理完毕后编解码器收回客户端对输出缓存区的所有权。不断重复整个过程,直至编码器停止工作或者异常退出。
MediaCodec生命周期中的状态
mediacodec分为三种状态,Stopped, Executing和Released。一张图表示(这张图是从网上直接下载下来使用的):
Stopped状态包含三个子状态:Uninitialized, Configured和Error,Executing同样包含三个状态:Flushed, Running 和End-of-Stream。
在mediacodec的使用过程中必须遵守图里标出的流程,否则会发生错误。
以解码器为例,讲解一下使用流程。当使用工厂方法创建mediacodec并且指定为解码后,进入Uninitialized状态,调用configure方法后,进入Configured状态,然后调用start方法进入Executing状态。
进入Executing状态后,首先到达Flush状态,此时mediacodec会持有所有的数据,当第一个inputbufffer从队列中取出时,立即进入Running状态,这个时间很短。然后就可以调用dequeueInputBuffer和getInputBuffer来获取用户可用的缓冲区,用户填满数据后调用queueinputbuffer方法返回给解码器,解码器大部分时间都会工作在Running状态。当想inputbufferqueue中输入一帧标记EndOfStream的时候,进入End-of-Stream状态,在这种状态下,解码器不再接受任何新的数据输入,缓冲区中的数据和标记EndOfStream最终会执行完毕。在任何时候都可以调用flush方法回到Flush状态。
调用stop方法会使mediacode进入Uninitialized状态,这时候可以执行configure方法来进入下一循环。当mediacodec使用完毕后必须调用release方法来释放所有的资源。
在某些情况下,例如取出缓冲区索引时,mediacodec会发生错误进入Error状态,此时调用reset方法来是mediacodec重新处于Uninitialized状态,或者调用release来结束解码。
MediaCodec API 说明
MediaCodec 主要的API做一个介绍:
- MediaCodec创建:
- createDecoderByType/createEncoderByType:根据特定MIME类型(如"video/avc")创建codec。
- createByCodecName:知道组件的确切名称(如OMX.google.mp3.decoder)的时候,根据组件名创建codec。使用MediaCodecList可以获取组件的名称。
- configure:配置解码器或者编码器。
- start:成功配置组件后调用start。
-
buffer处理的接口:
- dequeueInputBuffer:从输入流队列中取数据进行编码操作。
- queueInputBuffer:输入流入队列。
- dequeueOutputBuffer:从输出队列中取出编码操作之后的数据。
- releaseOutputBuffer:处理完成,释放ByteBuffer数据。
- getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组。
- getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组。
- flush:清空的输入和输出端口。
- stop:终止decode/encode会话
- release:释放编解码器实例使用的资源。
MediaCodec创建编/解码器
MediaCodec主要提供了createEncoderByType(String type)、createDecoderByType(String type)两个方法来创建编解码器,它们均需要传入一个MIME类型多媒体格式。常见的MIME类型多媒体格式如下:
● “video/x-vnd.on2.vp8” - VP8 video (i.e. video in .webm)
● “video/x-vnd.on2.vp9” - VP9 video (i.e. video in .webm)
● “video/avc” - H.264/AVC video
● “video/mp4v-es” - MPEG4 video
● “video/3gpp” - H.263 video
● “audio/3gpp” - AMR narrowband audio
● “audio/amr-wb” - AMR wideband audio
● “audio/mpeg” - MPEG1/2 audio layer III
● “audio/mp4a-latm” - AAC audio (note, this is raw AAC packets, not packaged in LATM!)
● “audio/vorbis” - vorbis audio
● “audio/g711-alaw” - G.711 alaw audio
● “audio/g711-mlaw” - G.711 ulaw audio
MediaCodec参数配置
public static MediaCodec getVideoMediaCodec() {
MediaFormat format = MediaFormat.createVideoFormat("video/avc", 720, 1280);
//设置颜色格式
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
//设置比特率(设置码率,通常码率越高,视频越清晰)
format.setInteger(MediaFormat.KEY_BIT_RATE, 1000 * 1024);
//设置帧率
format.setInteger(MediaFormat.KEY_FRAME_RATE, 10);
//关键帧间隔时间,通常情况下,你设置成多少问题都不大。
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10);
// 当画面静止时,重复最后一帧,不影响界面显示
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 1000000 / 45);
format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
//设置复用模式
format.setInteger(MediaFormat.KEY_COMPLEXITY, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
MediaCodec mediaCodec = null;
try {
mediaCodec = MediaCodec.createEncoderByType("video/avc");
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (Exception e) {
e.printStackTrace();
if (mediaCodec != null) {
mediaCodec.reset();
mediaCodec.stop();
mediaCodec.release();
mediaCodec = null;
}
}
return mediaCodec;
}
configure
public void configure(
MediaFormat format,
Surface surface, MediaCrypto crypto, int flags);
-
MediaFormat format
:输入数据的格式(解码器)或输出数据的所需格式(编码器)。传null等同于传递MediaFormat#MediaFormat作为空的MediaFormat。 -
Surface surface
:指定Surface,用于解码器输出的渲染。如果编解码器不生成原始视频输出(例如,不是视频解码器)和/或想配置解码器输出ByteBuffer,则传null。 -
MediaCrypto crypto
:指定一个crypto对象,用于对媒体数据进行安全解密。对于非安全的编解码器,传null。 -
int flags
:当组件是编码器时,flags指定为常量CONFIGURE_FLAG_ENCODE。
MediaFormat:封装描述媒体数据格式的信息(包括音频或视频),以及可选的特性元数据。
- 媒体数据的格式指定为key/value对。key是字符串。值可以integer、long、float、String或ByteBuffer。
- 特性元数据被指定为string/boolean对。
开始录制
我们通过对MediaCodec参数进行配置,然后得到一个MediaCodec
mMediaCodec.start();
结束录制
这里需要注意一下释放的顺序,一定得是按照下面的顺序进行资源释放的
mMediaCodec.signalEndOfInputStream();
mMediaCodec.stop();
mMediaCodec.release();
● 下面看一段源码:当编解码器start后,会进入一个for(;;)循环,该循环是一个死循环,以实现不断地去从编解码器的输入缓存池中获取包含数据的一个缓存区,然后再从输出缓存池中获取编解码好的输出数据。
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
MediaFormat outputFormat = codec.getOutputFormat(); // option B
codec.start();
for (;;) {
int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(…);
// fill inputBuffer with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is identical to outputFormat
// outputBuffer is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
outputFormat = codec.getOutputFormat(); // option B
}
}
codec.stop();
codec.release();
上层数据获取
do {
if (mMediaCodec != null) {
int outBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, -1);
if (outBufferIndex >= 0) {
ByteBuffer bb = mMediaCodec.getOutputBuffer(outBufferIndex);
//这里获取到原始数据,然后根据相关需求可对数据进行处理
}
if (outBufferIndex >= 0) {
mMediaCodec.releaseOutputBuffer(outBufferIndex, false);
}
}
}
while (isStarted);