相机之使用OpenGL预览
相机之使用OpenGL拍照
相机之使用OpenGL录像
使用 MediaCodec 生成一个 Surface,在这个 Surface 上的画面会自动编码。然后仿照 GLSurfaceView 的方法,配置EGL环境,在一个单独线程中在Surface上绘制,然后将编码后的视频数据封装为 MP4
MediaCodec
MediaCodec 是 Android 提供的用于访问低层多媒体编/解码器接口,通常与MediaExtractor、MediaMuxer、AudioTrack 一起使用,能够编解码诸如 H.264、H.265、AAC、3gp 等常见的音视频格式
MediaCodec 的工作原理就是接收输入数据,在 MediaCodec 内部进行编码,产生编码后的数据,MediaCodec 在编解码的过程中使用了一组输入/输出缓存区来同步或异步处理数据:首先,客户端向获取到的编解码器输入缓存区写入要编解码的数据并将其提交给编解码器,待编解码器处理完毕后将其转存到编码器的输出缓存区,同时收回客户端对输入缓存区的所有权;然后,客户端从获取到编解码输出缓存区读取编码好的数据进行处理,待处理完毕后编解码器收回客户端对输出缓存区的所有权,如下所示
mediacodec.png
MediaCodec 还有一个 createInputSurface() 方法,会返回一个Surface,而在这个Surface上绘制的内容,就不用客户端再手动向 MediaCodec 输入数据,而可以直接处理编码后的输出数据
创建 MediaCodec
fun start() {
//创建视频格式
var videoFormat =
MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height)
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, calculateBitRate())
//CQ 完全不控制码率,尽最大可能保证图像质量
//CBR 编码器会尽量把输出码率控制为设定值
//VBR 编码器会根据图像内容的复杂度(实际上是帧间变化量的大小)来动态调整输出码率,图像复杂则码率高,图像简单则码率低
videoFormat.setInteger(
MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR
)
//设置帧率
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE)
//设置I帧的间隔
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL)
//颜色格式是GraphicBuffer元数据
videoFormat.setInteger(
MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
)
//创建视频编码器mediaCodec
mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
//配置视频编码器mediaCodec的格式
mediaCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
//通过编码器创建一个Surface,之后的图像数据绘制到这上面,再进行保存
var inputSurface = mediaCodec.createInputSurface()
//为了不阻塞,录制在单开的一个线程中进行
var recordThread = HandlerThread("videoThread").apply { start() }
recordHandler = Handler(recordThread.looper)
recordHandler.post {
eglBase = EglBase(context, eglContext, inputSurface, width, height)
mediaCodec.start()
}
isStart = true
}
Egl 环境
class EglBase constructor(
context: Context,
eglContext: EGLContext,
surface: Surface,
private val width: Int,
private val height: Int
) {
private val TAG = "EglBase"
private var eglSurface: EGLSurface? = null
private var screenFilter: ScreenFilter? = null
private var mEglDisplay: EGLDisplay? = null
private var eglConfig: EGLConfig? = null
private var mEglContext: EGLContext? = null
init {
// 先创建EGL环境
createEgl(eglContext)
val attrib_list = intArrayOf(
EGL14.EGL_NONE
)
// 创建EGL绘制eglSurface,这是真正绘制的地方
eglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, eglConfig, surface, attrib_list, 0)
if (!EGL14.eglMakeCurrent(mEglDisplay, eglSurface, eglSurface, mEglContext)) {
Log.d(TAG, "EglBase: eglMakeCurrent error")
}
// eglMakeCurrent把context绑定到当前的渲染线程以及draw和read指定的Surface,
// 当在线程T上调用GL指令,OpenGL会查询T线程绑定是哪个Context,进而查询是哪个Surface绑定到了这个Context上
if (!EGL14.eglMakeCurrent(mEglDisplay, eglSurface, eglSurface, mEglContext)) {
Log.d(TAG, "EglBase: eglMakeCurrent error")
}
GLES20.glViewport(0, 0, width, height)
// 绘制的时候使用,其作用和将FBO数据显示到屏幕上是一样的
screenFilter = ScreenFilter(context, width, height)
}
/**
* 绘制
* @param textureId Int 纹理id
* @param timestamp Long 时间戳
*/
fun draw(textureId: Int, timestamp: Long) {
// 将textureId指向的数据,绘制到当前线程的eglSurface上
screenFilter?.onDrawFrame(textureId)
// 设置当前帧的时间戳
EGLExt.eglPresentationTimeANDROID(mEglDisplay, eglSurface, timestamp)
EGL14.eglSwapBuffers(mEglDisplay, eglSurface)
}
/**
* 创建EGL环境
* @param eglContext EGLContext
*/
private fun createEgl(eglContext: EGLContext) {
//创建 虚拟显示器
mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
if (mEglDisplay === EGL14.EGL_NO_DISPLAY) {
throw RuntimeException("eglGetDisplay failed")
}
//初始化显示器
val version = IntArray(2)
// 12.1020203
//major:主版本 记录在 version[0]
//minor : 子版本 记录在 version[1]
if (!EGL14.eglInitialize(mEglDisplay, version, 0, version, 1)) {
throw RuntimeException("eglInitialize failed")
}
// egl 根据我们配置的属性 选择一个配置
val attrib_list = intArrayOf(
EGL14.EGL_RED_SIZE, 8, // 缓冲区中 红分量 位数
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, //egl版本 2
EGL14.EGL_NONE
)
val configs =
arrayOfNulls(1)
val num_config = IntArray(1)
// attrib_list:属性列表+属性列表的第几个开始
// configs:获取的配置 (输出参数)
// num_config: 长度和 configs 一样就行了
EGL14.eglChooseConfig(
mEglDisplay, attrib_list, 0,
configs, 0, configs.size, num_config, 0
)
eglConfig = configs[0]
//EGL_CONTEXT_CLIENT_VERSION egl版本 2
val ctx_attrib_list = intArrayOf(
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
EGL14.EGL_NONE
)
//创建EGL上下文
// 3 share_context: 共享上下文 传绘制线程(GLThread)中的EGL上下文 达到共享资源的目的 发生关系
mEglContext = EGL14.eglCreateContext(
mEglDisplay, eglConfig, eglContext, ctx_attrib_list, 0
)
// 创建失败
if (mEglContext === EGL14.EGL_NO_CONTEXT) {
throw RuntimeException("EGL Context Error.")
}
}
/**
* 释放各种资源
*/
fun release() {
EGL14.eglDestroySurface(mEglDisplay, eglSurface)
EGL14.eglMakeCurrent(
mEglDisplay,
EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT
)
EGL14.eglDestroyContext(mEglDisplay, mEglContext)
EGL14.eglReleaseThread()
EGL14.eglTerminate(mEglDisplay)
}
}
在渲染器的绘制方法中进行编码
在 onDrawFrame 方法中对每一帧进行编码
/**
* 编码这一帧数据
*/
fun encodeFrame(textureId: Int, timestamp: Long) {
//没有开始录制,直接返回
if (!isStart) {
return
}
recordHandler.post {
if (isStart) {
Log.e(TAG, "encodeFrame: timestamp=$timestamp isStart=$isStart")
//在eglBase中绘制出这一帧内容
eglBase.draw(textureId, timestamp)
//将这一帧封装进Muxer
writeToMuxer(false)
}
}
}
/**
* 将编码后的数据写入Muxer,生成MP4文件
*/
open fun writeToMuxer(endOfStream: Boolean) {
if (endOfStream) {
mediaCodec.signalEndOfInputStream()
}
var bufferInfo = MediaCodec.BufferInfo()
loop@ while (true) {
//得到当前编码器的状态
var status = mediaCodec.dequeueOutputBuffer(bufferInfo, 10_000)
Log.d(TAG, "writeToMuxer: status=$status")
when (status) {
//稍后再试,直接退出循环,直到下次调用writeToMuxer
MediaCodec.INFO_TRY_AGAIN_LATER -> {
if (!endOfStream) {
break@loop
}
}
//格式变化,为mediaMuxer添加轨道,一共两个轨道,一个音频,一个视频,如果都添加了,就可以开始封装为MP4了
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
var outputFormat = mediaCodec.outputFormat
//添加格式轨道
trackIndex = mediaMuxer.addTrack(outputFormat)
mediaMuxer.start()
}
MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
Log.d(TAG, "getCodec: INFO_OUTPUT_BUFFERS_CHANGED")
}
else -> {
//得到编码好的数据
var outputBuffer = mediaCodec.getOutputBuffer(status)
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
bufferInfo.size = 0
}
//数据大小不等于0
if (bufferInfo.size != 0) {
//设置数据显示的时间戳
bufferInfo.presentationTimeUs = getPtsUs()
outputBuffer.position(bufferInfo.offset)
outputBuffer.limit(bufferInfo.offset + bufferInfo.size)
//将编码后的数据写入相应轨道
mediaMuxer.writeSampleData(trackIndex, outputBuffer, bufferInfo)
}
//释放Buffer,以便复用
mediaCodec.releaseOutputBuffer(status, false)
//此次编码完成,退出
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
Log.d(TAG, "writeToMuxer: BUFFER_FLAG_END_OF_STREAM break@loop")
break@loop
}
}
}
}
}
停止录像
/**
* 停止录制
*/
fun stop() {
isStart = false
recordHandler.post {
//写入标志位,停止录制
writeToMuxer(true)
//释放各种资源
eglBase.release()
mediaMuxer.stop()
mediaMuxer.release()
recordHandler.looper.quitSafely()
}
}