Android视频编解码 MediaCodec使用(2)

Android视频编解码 MediaCodec使用

简述

Android系统提供给上层应用来编解码的接口是MediaCodec相关的接口,MediaCodec.java是提供给java层的接口,它通过jni调用到C++层,通过一个JMediaCodec来控制真正的C++层MediaCodec,Android其实还在NDK层也提供了C++的MedianCodec接口,是AMediaCodec,最终的实现也是通过一个C++层的MediaCodec,只不过分别为了暴露接口给java层使用和C++层使用做了一下封装而已,本节我们会写一个简单demo来演示MediaCodec的用法。

接口简述

无论是编码还是解码,MediaCodec提供接口的思路都是App层通过接口去请求一个inputBuffer,然后填充数据,然后提交InputBuffer,编解码器内部处理编解码,然后app再去请求outputBuffer,获取处理完后的数据,使用完成后releaseOutputBuffer。

但是编码的Input数据和解码的output数据都可以使用Surface来输入/接收,这里这个Surface虽然在java层和窗口那一章节说到的Surface一样,但是在C++层是有所区别的,这里的Surface并不是通过BLASTBufferQueue产生的,而是通过编解码器hal层生成的,但是本质来说这个Surface也就是一个BufferQueue的生产者/消费者。

使用到的接口
  • MediaCodec.createDecoderByType(@NonNull String type)
    用于构造一个解码的MediaCodec实例,type表示编解码类型,"video/avc"就是H264
  • MediaCodec.createEncoderByType(@NonNull String type)
    同上,区别就是这里构造的事编码器
  • MediaFormat.createVideoFormat(@NonNull String mime, int width, int height)
    构造一个MediaFormat,MediaFormat用于存储编解码器参数,通过键值对的方式存储参数。
  • configure(@Nullable MediaFormat format,@Nullable Surface surface, @Nullable MediaCrypto crypto, @ConfigureFlag int flags)
    配置编解码器,可以配置format,surface等。
  • MediaCodec.start/stop/release
    状态控制,开始/停止/释放
  • MediaCodec.dequeueInputBuffer(long timeoutUs)
    获取一个inputBuffer的索引,参数是等待时间,如果传入-1则一直等待
  • MediaCodec.getInputBuffer(int index)
    根据索引获取InputBuffer
  • queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags)
    提交InputBuffer
  • dequeueOutputBuffer(@NonNull BufferInfo info, long timeoutUs)
    获取OutputBuffer的索引,BufferInfo里会有一些当前帧的信息,例如是否是关键帧之类的
  • getOutputBuffer(int index)
    同getInputBuffer,只是这里是获取输出的数据
  • releaseOutputBuffer(int index, boolean render)
    释放OutputBuffer,render表示如果Surface不为空,是否需要将buffer渲染到Surface上。

从这个接口的设计我们大概可以猜出来,无论是编码还是解码,应该都有一个Buffer数组在循环利用,建议大家可以去看一下这里jni的实现,代码不复杂,但是还是挺有参考意义的,我们类似的场景写jni的时候可以参照这种方式减少数据的拷贝以及jni reference的构建。

demo

我们要做的demo是将通过Camera来的数据编码再解码,最终显示到我们的SurfaceView上去。

PS:Camera的数据完全可以直接显示到SurfaceView上,我们这么做只是为了演示MediaCodec到使用,正常业务场景编码之后应该通过网络或者其他信道传输到另一个设备再解码,我们这节的重心是编解码,就省了传输了。

我们一共就4个类:

  • MainActivity.java UI和业务逻辑(演示demo就把逻辑全放Acitivity了)
  • CodecDecodeController.java 管理解码
  • CodecEncodeController.java 管理编码
  • CameraController.java 管理相机

CameraController.java

由于Camera的接口不是我们的重点,这里我们就随便写一下可以用就行了,很多地方写的并不标准。

就是提供了一个openCamera接口来打开相机,而相机数据会输出到入参Surface上,所以后面我们需要将编码器的InputSurface传进来。

package com.example.myapplication

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.ImageFormat
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraDevice.StateCallback
import android.hardware.camera2.CameraManager
import android.hardware.camera2.params.StreamConfigurationMap
import android.os.Handler
import android.os.HandlerThread
import android.util.Size
import android.view.Surface
import androidx.core.app.ActivityCompat


class CameraController {
    private val handlerThread: HandlerThread = HandlerThread("cameraThread")
    private var mCameraHandler: Handler? = null
    var size: Array<Size>? = null

    // 这里只是通过接口去拿了相机的尺寸,我们默认选了一个相机Id
    fun initCamera(context: Context) {
        val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
        var cameraIdList = cameraManager.cameraIdList
        val character: CameraCharacteristics =
            cameraManager.getCameraCharacteristics(cameraIdList[0])

        val streamConfigurationMap: StreamConfigurationMap? =
            character.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
        size = streamConfigurationMap?.getOutputSizes(ImageFormat.JPEG)
    }

    // 打开相机,
    fun openCamera(context: Context, surface: Surface) {
        val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
        var cameraIdList = cameraManager.cameraIdList
        handlerThread.start()
        mCameraHandler = Handler(handlerThread.looper)
        if (ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.CAMERA
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            // 本来应该加动态权限请求,这里不是我们的重点就不写了
            return
        }
        cameraManager.openCamera(cameraIdList[0], object : StateCallback() {
            override fun onOpened(camera: CameraDevice) {
                startPreview(camera, surface)
            }

            override fun onDisconnected(camera: CameraDevice) {

            }

            override fun onError(camera: CameraDevice, error: Int) {

            }

        }, mCameraHandler)

    }

    // 开始浏览,这里surface就是最终相机数据会输出的Surface
    fun startPreview(camera: CameraDevice, surface: Surface) {
        camera.createCaptureSession(mutableListOf(surface),
            object : CameraCaptureSession.StateCallback() {
                override fun onConfigured(session: CameraCaptureSession) {
                    val captureRequest = camera.createCaptureRequest(
                        CameraDevice.TEMPLATE_PREVIEW
                    ).apply { addTarget(surface) }

                    captureRequest?.let { session.setRepeatingRequest(it.build(), null, mCameraHandler) }
                }

                override fun onConfigureFailed(session: CameraCaptureSession) {

                }

            }, mCameraHandler)
    }
}

CodecEncodeController.java

initMediaCodec构造MediaCodec编码器,里面会配置一些基础的参数,processEncodeOutput会启动一个线程循环获取输出数据,获取到的输出数据通过mEncodeDataCallback回调给业务层。

package com.example.myapplication

import android.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaFormat
import android.view.Surface
import java.nio.ByteBuffer

class CodecEncodeController {
    var mMediaCodec: MediaCodec? = null
    var mEncodeDataCallback: EncodeDataCallback? = null
    var mState = 0 // 0为初始状态,1为start状态,2为stop状态
    fun initMediaCodec(width: Int, height: Int) {
        // 构造编码器
        mMediaCodec = MediaCodec.createEncoderByType("video/avc");
        // 配置MediaFormat
        val format = MediaFormat.createVideoFormat("video/avc", width, height)
        // 颜色空间,COLOR_FormatSurface表示数据来自Surface,这里还可以是YUV420,RGBA之类的
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
        // 码率
        format.setInteger(MediaFormat.KEY_BIT_RATE, 96000)
        // 帧率
        format.setInteger(MediaFormat.KEY_FRAME_RATE, 60)
        // 关键帧间隔,1s一个
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
        // 调用configure配置MediaCodec
        mMediaCodec?.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    }

    fun getInputSurface(): Surface? {
        return mMediaCodec?.createInputSurface()
    }

    // 启动编码器
    fun startEncode() {
        mMediaCodec?.start()
        mState = 1
        processEncodeOutput()
    }

    // 循环获取输出数据OutputBuffer,输出数据通过mEncodeDataCallback回调给业务层,然后释放OutputBuffer
    fun processEncodeOutput() {
        Thread {
            while (mState == 1) {
                val bufferInfo = MediaCodec.BufferInfo()
                val bufferIndex = mMediaCodec?.dequeueOutputBuffer(bufferInfo, -1)
                if (bufferIndex!! >= 0) {
                    val byteData = mMediaCodec?.getOutputBuffer(bufferIndex)
                    byteData?.let { mEncodeDataCallback?.onDataReady(it) }
                    mMediaCodec?.releaseOutputBuffer(bufferIndex, true)
                } else {
                    mEncodeDataCallback?.onError()
                }
            }
        }.start()
    }

    fun stop() {
        mState = 2
        mMediaCodec?.stop()
        mMediaCodec?.release()
        mMediaCodec = null
    }

    fun setCallback(callback: EncodeDataCallback) {
        mEncodeDataCallback = callback
    }

    interface EncodeDataCallback {
        fun onDataReady(byteData: ByteBuffer)
        fun onError()
    }
}

CodecDecodeController.java

和CodecEncodeController非常类似,initMediaCodec构造解码器,这里提供一个inputData方法,业务层通过该方法将编码后的数据传入。然后也会启动一个线程不断的dequeueOutputBuffer,这里dequeueOutputBuffer之后直接releaseOutputBuffer了,releaseOutputBuffer的后一个render参数为true,则会把数据渲染到Surface上。

package com.example.myapplication

import android.media.MediaCodec
import android.media.MediaCodec.BufferInfo
import android.media.MediaCodecInfo
import android.media.MediaFormat
import android.view.Surface
import java.nio.ByteBuffer

class CodecDecodeController {
    var mMediaCodec: MediaCodec? = null
    var mState = 0 // 0为初始状态,1为start状态,2为stop状态
    fun initMediaCodec(width: Int, height: Int, outputSurface: Surface) {
        mMediaCodec = MediaCodec.createDecoderByType("video/avc");
        // 配置MediaFormat
        val format = MediaFormat.createVideoFormat("video/avc", width, height)
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)
        format.setInteger(MediaFormat.KEY_BIT_RATE, 96000)
        format.setInteger(MediaFormat.KEY_FRAME_RATE, 60)
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
        // 配置解码器
        mMediaCodec?.configure(format, outputSurface, null, 0);
        // 启动解码器
        mMediaCodec?.start()
        mState = 1
        processOutputData()
    }

    fun stop() {
        mState = 2
        mMediaCodec?.stop()
        mMediaCodec?.release()
        mMediaCodec = null
    }

    // 业务层通过该方法数据编码后的数据
    fun inputData(inputData: ByteBuffer) {
        val bufferIndex = mMediaCodec?.dequeueInputBuffer(-1)
        if (bufferIndex!! >= 0) {
            val byteData = mMediaCodec?.getInputBuffer(bufferIndex)
            byteData?.put(inputData)
            mMediaCodec?.queueInputBuffer(bufferIndex, 0, inputData.limit(), System.currentTimeMillis(),0)
        }
    }

    // 处理outputBuffer
    fun processOutputData() {
        Thread {
            while (mState == 1) {
                val bufferInfo = BufferInfo()
                val bufferIndex = mMediaCodec?.dequeueOutputBuffer(bufferInfo, -1)
                if (bufferIndex!! >= 0) {
                    // 释放OutputBuffer并且将数据直接渲染到outputSurface上。
                    mMediaCodec?.releaseOutputBuffer(bufferIndex, true)
                }
            }
        }.start()
    }
}

MainActivity.java

布局文件里就一个SurfaceView我们就不看了,在surfaceCreated的时候来初始化编解码器以及相机控制器。

package com.example.myapplication

import android.os.Bundle
import android.view.SurfaceHolder
import android.view.SurfaceView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import java.nio.ByteBuffer

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val surfaceView = findViewById<SurfaceView>(R.id.sf_view)
        surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
            override fun surfaceCreated(holder: SurfaceHolder) {
                CameraController().let {
                    // 先获取相机尺寸
                    it.initCamera(baseContext)
                    it.size?.get(0)?.let{ size ->
                        // 有了尺寸以后构造编码器
                        CodecEncodeController().let {  encodeController->
                            // 初始化编码器
                            encodeController.initMediaCodec(
                                size.width, size.height
                            )
                            // 构造解码器
                            CodecDecodeController().let { decodeController->
                                // 初始化解码器,解码器的outputSurface就是当前SurfaceView
                                decodeController.initMediaCodec(size.width, size.height, holder.surface)
                                encodeController.setCallback(object :
                                    CodecEncodeController.EncodeDataCallback {
                                    override fun onDataReady(byteData: ByteBuffer) {
                                        // 编码器数据ready后传给解码器
                                        // 正常业务使用这里中间可能还有网络传输
                                        decodeController.inputData(byteData)
                                    }

                                    override fun onError() {
                                    }
                                })
                            }
                            // 使用编码器的InputSurface作为相机的outputSurface来打开相机
                            encodeController.getInputSurface()
                                ?.let { surface -> it.openCamera(baseContext, surface) }
                            encodeController.startEncode()
                        }
                    }
                }


            }

            override fun surfaceChanged(
                holder: SurfaceHolder,
                format: Int,
                width: Int,
                height: Int
            ) {

            }

            override fun surfaceDestroyed(holder: SurfaceHolder) {

            }

        })
    }
}

由于没有写动态权限申请,需要手动到设置里打开相机权限才能用。

小结

本节主要是演示一下MediaCodec提供的java接口使用,demo主要是演示接口用法,所以写的比较随意,后续我们会进一步介绍MediaCodec的框架流程。

相关推荐
Patience to do11 分钟前
Android Studio项目(算法计算器)
android·算法·android studio
我又来搬代码了3 小时前
【Android】使用TextView实现按钮开关代替Switch开关
android
江-月*夜6 小时前
uniapp vuex 搭建
android·javascript·uni-app
大风起兮云飞扬丶7 小时前
Android——显式/隐式Intent
android
大风起兮云飞扬丶7 小时前
Android——metaData
android
看山还是山,看水还是。7 小时前
Nginx 的 Http 模块介绍(中)
android·运维·网络·nginx·http
SouthBay49310 小时前
PHP内存马:不死马
android·开发语言·php
安於宿命10 小时前
【Linux内核揭秘】深入理解命令行参数和环境变量
android·linux·服务器
Gerry_Liang11 小时前
Android Studio 无法查看Kotlin源码的解决办法
android·kotlin·android studio
IT生活课堂12 小时前
唤醒车机时娱乐屏出现黑屏,卡顿的案例分享
android·智能手机·汽车