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的框架流程。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |