本章仅对部分代码进行讲解,以帮助读者更好的理解章节内容。
本系列文章涉及的项目HardwareVideoCodec已经开源到Github,支持软编和硬编。使用它你可以很容易的实现任何分辨率的视频编码,无需关心摄像头预览大小。一切都如此简单。目前已迭代多个稳定版本,欢迎查阅学习和使用,如有BUG或建议,欢迎Issue。
说到Android的视频硬编码,很多新人首先会想到MediaRecorder,这可以说是Android早期版本视频硬编码的唯一选择。这个类的使用很简单,只需要给定一个Surface(输入)和一个File(输出),它就给你生成一个标准的mp4文件。
但越是简单的东西便意味着越难以控制,MediaRecorder的缺点很明显。相信很多人在接触到断点视频录制
这个需求的时候,首先会想到使用MediaRecorder,很遗憾,这个东西并不能给你很多期待,就像一开始的我一样。
首先,MediaRecorder并没有断点录制的API,当然你可以使用一些“小技巧”,每次录制的时候,都把MediaRecorder stop掉,然后再次初始化,这样就会生成一系列的视频,最后把它们拼接起来。然而问题在于,每次初始化MediaRecorder都需要消耗很长时间,这意味着,当用户快速点击录制按钮的时候可能会出现问题。对于这个问题,你可以等到MediaRecorder初始化完成才让用户点击开始录制,但是这样往往会因为等待时间过长,导致用户体验极差。
这种情况下,一个可控的视频编码器是必须的。虽然在Android 4.4以前我们没得选择,但是在Android 4.4之后,我们有了MediaCodec
,一个完全可控的视频编码器,虽然无法直接输出mp4(需要配合MediaMuxer来对音视频进行混合,最终输出mp4,或者其它封装格式)。如今的Android生态,大部分手机都已经是Android 5.0系统,完全可以使用MediaCodec来进行音视频编码的开发,而MediaRecorder则降级作为一个提高兼容性的备选方案。
废话不多说,我们直接步入正题。要想正确的使用MediaCodec,我们首先得先了解它的工作流程,关于这个,强烈大家去看一下Android文档。呃呃,相信在这个快速开发为王道的环境,没几个人会去看,所以还是在这里简单介绍一下。
- 首先,通过MediaCodec的工厂方法
createEncoderByType
或createByCodecName
创建实例,这时候MediaCodec处于Uninitialized
状态。- 接下来,调用configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)设置编码器参数,这时候MediaCodec处于
Configured
状态。- 正确设置各种参数之后,调用start方法,让MediaCodec开始编码,这时候MediaCodec处于
Running
状态。- 最后顺序调用
signalEndOfInputStream
、stop
和release
来结束编码。
流程很简单,相信大家都能看懂。难点在于running状态,也就是上图右侧绿色部分的流程。
当MediaCodec处于Running状态时,内部会持有两个缓冲区队列,一个输入缓冲区,一个输出缓冲区。当我们向输入缓冲区输入数据后,MediaCodec会从中取出数据,送到硬件进行编码,编码结束后送到缓冲区,这是一个异步过程,这时候我们可以从输出缓冲区取出编码后的数据。这个过程在更高版本有更好的API,新版MediaCodec可以通过回调返回编码后的数据。由于我们可以控制什么时候给编码器输入数据,所以可以随时暂停或者开始编码。
理论讲的差不多了,接下来我们看一下具体实现。
//初始化一个编码器配置MediaFormat
fun createVideoFormat(parameter: Parameter, ignoreDevice: Boolean = false): MediaFormat? {
val codecInfo = getCodecInfo(parameter.video.mime, true)
if (!ignoreDevice && null == codecInfo) {//Unsupport codec type
return null
}
val mediaFormat = MediaFormat()
//使用H264编码
mediaFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC)
//设置视频宽度
mediaFormat.setInteger(MediaFormat.KEY_WIDTH, width)
//设置视频高度
mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, height)
//设置视频输入颜色格式,这里选择使用Surface作为输入,可以忽略颜色格式的问题,并且不需要直接操作输入缓冲区。
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
//设置视频码率,这里计算公式选择一个中等码率,把3改为更大的值可以开启更高码率,通常不建议超过5
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * fps * 3)
//设置视频fps
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, fps)
//设置视频关键帧间隔,这里设置两秒一个关键帧
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
/**
* 可选配置,设置码率模式
* BITRATE_MODE_VBR:恒定质量
* BITRATE_MODE_VBR:可变码率
* BITRATE_MODE_CBR:恒定码率
*/
mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR)
/**
* 可选配置,设置H264 Profile
* 需要做兼容性检查
*/
mediaFormat.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh)
/**
* 可选配置,设置H264 Level
* 需要做兼容性检查
*/
mediaFormat.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel31)
}
return mediaFormat
}
//初始化并配置编码器
private fun initCodec() {
val format = CodecHelper.createVideoFormat(parameter)
debug_v("create codec: ${format.getString(MediaFormat.KEY_MIME)}")
try {
codec = MediaCodec.createEncoderByType(format.getString(MediaFormat.KEY_MIME))
/**
* 配置编码器
*/
codec!!.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
/**
* 由于我们使用Surface作为输入,所以不需要直接操作输入缓冲区,只需要把MediaCodec生成的Surface绑定到OpenGL即可,所以这里使用了一个纹理封装CodecTextureWrapper,请参考前几章的CameraTextureWrapper和ScreenTextureWrapper,或者直接查看文章末尾给出的源码。
*/
codecWrapper = CodecTextureWrapper(codec!!.createInputSurface(), textureId, eglContext)
codecWrapper?.egl?.makeCurrent()
codec!!.start()
} catch (e: Exception) {
debug_e("Can not create codec")
} finally {
if (null == codec)
debug_e("Can not create codec")
}
}
/**
* 从编码器循环取出编码数据,通过OpenGL来控制数据输入,省去了直接控制输入缓冲区的步骤,所以这里直接操控输出缓冲区即可
*/
private fun dequeue(): Boolean {
try {
/**
* 从输出缓冲区取出一个Buffer,返回一个状态
* 这是一个同步操作,所以我们需要给定最大等待时间WAIT_TIME,一般设置为10000us
*/
val flag = codec!!.dequeueOutputBuffer(mBufferInfo, WAIT_TIME)
when (flag) {
MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {//输出缓冲区改变,通常忽略
debug_v("INFO_OUTPUT_BUFFERS_CHANGED")
}
MediaCodec.INFO_TRY_AGAIN_LATER -> {//等待超时,需要再次等待,通常忽略
// debug_v("INFO_TRY_AGAIN_LATER")
return false
}
/**
* 输出格式改变,很重要
* 这里必须把outputFormat设置给MediaMuxer,而不能不能用inputFormat代替,它们时不一样的,不然无法正确生成mp4文件
*/
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
debug_v("INFO_OUTPUT_FORMAT_CHANGED")
//这里通过回调把outputFormat送出去
onSampleListener?.onFormatChanged(codec!!.outputFormat)
}
else -> {
if (flag < 0) return@dequeue false//如果小于零,则跳过
val data = codec!!.outputBuffers[flag]//否则代表便阿门成功,可以从输出缓冲区队列取出数据
if (null != data) {
val endOfStream = mBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM
if (endOfStream == 0) {//如果没有收到BUFFER_FLAG_END_OF_STREAM信号,则代表输出数据时有效的
mBufferInfo.presentationTimeUs = pTimer.presentationTimeUs
//这里把编码后的数据通过回调送出去
onSampleListener?.onSample(mBufferInfo, data)
}
//缓冲区使用完后必须把它还给MediaCodec,以便再次使用,至此一个流程结束,再次循环
codec!!.releaseOutputBuffer(flag, false)
// if (endOfStream == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
// return true
// }
return true
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return false
}
//编码结束后,停止编码器
private fun stop(){
while (dequeue()) {//取出编码器输出缓冲区中剩余的帧数据
}
debug_e("Video encoder stop")
//编码结束,发送结束信号,让surface不在提供数据
codec!!.signalEndOfInputStream()
codec!!.stop()
codec!!.release()
}
以上就是本章关于MediaCodec的全部学习内容,如果有疑问或者错误,欢迎在评论区留言。
本章知识点:
- MediaCodec的工作流程。
- MediaCodec的使用。
本章相关源码·HardwareVideoCodec项目:
- VideoEncoderImpl
- CodecHelper