实现录制音视频也有两种方案,分别是MediaRecorder和MediaCodec
什么是MediaRecorder
MediaRecorder是安卓提供的一个用于音视频采集的类
MediaRecorder的优缺点
优点
可以实现直接录制视频 使用方便,得到就是编码和封装好的音视频文件,可以直接使用
缺点
无法获取原始数据,不能对每一帧数据进行处理,无法支持我们程序中自己需要的一些逻辑,比方需要录制灰度视频。
由于不满足我的需求,所以这里就不再对MediaRecorder讲解了,那接下来我们来说说MediaCodec
什么是MediaCodec
MediaCodec
类是Android平台提供的用于访问低层多媒体硬件编/解码器接口,它是Android低层多媒体架构的一部分,通常与MediaExtractor、MediaMuxer、AudioTrack结合使用,能够编解码诸如H.264、H.265、AAC、3gp等常见的音视频格式。一般来说H.264的AVC视频编码和AAC的音频编码是最常见的。
MediaCodec工作原理
MediaCodec的工作原理就是处理输入数据以产生输出数据。具体来说,MediaCodec在编解码的过程中使用了一组输入/输出缓存区来同步或异步处理数据:首先,客户端向获取到的编解码器输入缓存区写入要编解码的数据并将其提交给编解码器,待编解码器处理完毕后将其转存到编码器的输出缓存区,同时收回客户端对输入缓存区的所有权;然后,客户端从获取到编解码输出缓存区读取编码好的数据进行处理,待处理完毕后编解码器收回客户端对输出缓存区的所有权。不断重复整个过程,直至编码器停止工作或者异常退出。
MediaCodec生命周期中的状态
mediacodec分为三种状态,Stopped
, Executing
和Released
。一张图表示(这张图是从网上直接下载下来使用的):
Stopped状态包含三个子状态:Uninitialized
, Configured
和Error
,Executing同样包含三个状态:Flushed
, Running
和End-of-Stream
。
在mediacodec的使用过程中必须遵守图里标出的流程,否则会发生错误。
比方,没有调用start()方法,就开始sotp()会报错。
以解码器为例,讲解一下使用流程。当使用工厂方法创建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可以获取组件的名称。
- createDecoderByType/createEncoderByType:根据特定MIME类型
-
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参数配置
private void initVideoCodec(int width, int height) {
try {
// https://developer.android.google.cn/reference/android/media/MediaCodec mediacodec官方介绍
// 比方MediaCodec的几种状态
// avc即h264编码
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,width,height);
// 设置颜色格式
// 本地原始视频格式(native raw video format):这种格式通过COLOR_FormatSurface标记,并可以与输入或输出Surface一起使用
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
// 设置码率,通常码率越高,视频越清晰,但是对应的视频也越大
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE,width * height * 4);
// 设置帧率 三星s21手机camera预览时,支持的帧率为10-30
// 通常这个值越高,视频会显得越流畅,一般默认设置成30,你最低可以设置成24,不要低于这个值,低于24会明显卡顿,微信为28
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE,30);
// 设置 I 帧间隔的时间
// 通常的方案是设置为 1s,对于图片电影等等特殊情况,这里可以设置为 0,表示希望每一帧都是 KeyFrame
// IFRAME_INTERVAL是指的帧间隔,这是个很有意思的值,它指的是,关键帧的间隔时间。通常情况下,你设置成多少问题都不大。
// 比如你设置成10,那就是10秒一个关键帧。但是,如果你有需求要做视频的预览,那你最好设置成1
// 因为如果你设置成10,那你会发现,10秒内的预览都是一个截图
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,5);
// 创建编码器
// https://www.codercto.com/a/41316.html MediaCodec 退坑指南
mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
mMediaCodec.configure(mediaFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
// 相机的像素数据绘制到该 surface 上面
mSurface = mMediaCodec.createInputSurface();
videoEncoderThread = new VideoEncoderThread(videoRecorderReference);
} catch (Exception e) {
e.printStackTrace();
}
}
可以用微信录制一个短视频,然后看下参数:
比较有参照意思的参数:
//录制了4.917秒
Duration/String : 4 秒 917 毫秒
//每秒一帧
Format_Settings_RefFrames/String : 4 帧
//视频宽高
Width/String : 288 像素
Height/String : 640 像素
//1601*1024除以288*640=8.9,KEY_BIT_RATE和宽高比接近9
OverallBitRate/String : 1 601 kb/s
//格式avc,和MIMETYPE_VIDEO_AVC对应
Format/String : AVC
"FileExtension" : "mp4",
//avc也是mpeg-4
"Format" : "MPEG-4",
//帧率
FrameRate/String : 28.067 FPS
//yuv420,在camera预览时设定的nv21对应
ColorSpace : YUV
ChromaSubsampling/String : 4:2:0
//声音的格式,再补充
Format/String : AAC LC
Channel(s)/String : 1 声道
SamplingRate/String : 44.1 kHz
FrameRate/String : 43.066 FPS (1024 SPF)
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);
网友评论