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

news2024/10/22 4:06:54

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的框架流程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2220516.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【wpf】08 xml文件的存取操作

在使用wpf编程过程中&#xff0c;会用到xml的配置文件&#xff0c;实现对其读取和存储的操作是必须的。 1 xml说明 可扩展标记语言 (Extensible Markup Language, XML) &#xff0c;标准通用标记语言的子集&#xff0c;可以用来标记数据、定义数据类型&#xff0c;是一种允许…

git clone报错fatal: pack has bad object at offset 186137397: inflate returned 1

逐步拷贝 https://stackoverflow.com/questions/27653116/git-fatal-pack-has-bad-object-at-offset-x-inflate-returned-5 https://www.cnblogs.com/Lenbrother/p/17726195.html https://cloud.tencent.com/developer/ask/sof/107092182 git clone --depth 1 <repository…

外包干了30年,人都快要废了。。。。。

先说一下自己的情况&#xff0c;本科生&#xff0c;19年通过校招进入南京某软件公司&#xff0c;干了接近2年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了2年的功能测试&…

如何做软件系统的维护成本估算?

一、人员成本 维护工程师 确定维护工程师的数量和技能级别。例如&#xff0c;可能需要 2 名中级维护工程师&#xff0c;月薪 10000 元左右。计算每月的人员成本为 2 10000 20000 元。 技术支持人员 技术支持人员负责解答用户的问题和处理紧急情况。假设需要 1 名技术支持人员…

django5入门【03】新建一个hello界面

注意 ⭐前提&#xff1a;将上节的项目导入到pycharm中操作步骤总结&#xff1a; 1、HelloDjango/HelloDjango目录下&#xff0c;新建一个views.py 2、HelloDjango/HelloDjango/urls.py 文件中&#xff0c;配置url路由信息 3、新建终端&#xff0c;执行运行命令python manag…

Noteexpress在已有作者名字时怎么只标注年份

如图 需要除掉重复的人名 达到如下只出现年份的效果 方法&#xff1a; 打开 编辑引文 然后&#xff0c;选中文献&#xff0c;并勾选“不显示作者名” 按确定即可。

js.杨辉三角和分发饼干

1&#xff0c;链接&#xff1a;118. 杨辉三角 - 力扣&#xff08;LeetCode&#xff09; 题目&#xff1a; 给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中&#xff0c;每个数是它左上方和右上方的数的和。 示例 1: 输入: numRows …

PostgreSQL的学习心得和知识总结(一百五十五)|[performance]优化期间将 WHERE 子句中的 IN VALUES 替换为 ANY

目录结构 注&#xff1a;提前言明 本文借鉴了以下博主、书籍或网站的内容&#xff0c;其列表如下&#xff1a; 1、参考书籍&#xff1a;《PostgreSQL数据库内核分析》 2、参考书籍&#xff1a;《数据库事务处理的艺术&#xff1a;事务管理与并发控制》 3、PostgreSQL数据库仓库…

二叉树遍历(前序、中序、后续)

目录 什么是二叉树二叉树遍历以递归创建树的角度看前、中、后序遍历前序遍历中序遍历后序遍历 栈来实现前、中、后序遍历栈的实现栈操作进行前序、中序遍历代码实现中序遍历和先序遍历栈操作进行后序遍历 什么是二叉树 树&#xff1a;树的根节点没有前驱&#xff0c;除根节点以…

Spring声明式事务管理:深入探索XML配置方式

前言 Spring的事务管理&#xff0c;无论是基于xml还是注解实现&#xff0c;本质上还是实现数据库的事务管理机制&#xff0c;因此要注意发送SQL的连接是否为同一个&#xff0c;这是实现声明式事务的关键。 以下案例和实现基于SSM整合框架完成&#xff0c;不知道如何整合SSM&…

CTFHUB技能树之文件上传——无验证

开启靶场&#xff0c;打开链接&#xff1a; 直接上传一句话木马&#xff1a; <?php eval($_POST[pass]);?> 成功提交并显示了上传的文件的路径 访问一下该文件触发一句话木马&#xff1a; 看到一片空白是正常的&#xff0c;因为没有写什么函数&#xff0c;比如&#x…

FineReport 计算同比增长

1、数据库查询 SELECTt1.年,t1.月,t1.总金额 AS 同期金额,t1.仓库名称,t2.总金额 AS 上期金额 FROMtest t1LEFT JOIN test t2 ON ( t1.年 t2.年 1 ) AND t1.月 t2.月 AND t1.仓库名称 t2.仓库名称2、配置字段 月份字段加后缀 月 数据列加后缀 计算同比增长率 if(LEN(B3)0 …

移动零---双指针法

目录 一&#xff1a;题目 二:算法原理讲解 三&#xff1a;代码编写 一&#xff1a;题目 题目链接&#xff1a;https://leetcode.cn/problems/move-zeroes/description/ 二:算法原理讲解 三&#xff1a;代码编写 void moveZeroes2(vector<int>& nums) {for (int d…

数据轻松上云——Mbox边缘计算网关

随着工业4.0时代的到来&#xff0c;工厂数字化转型已成为提升生产效率、优化资源配置、增强企业竞争力的关键。我们凭借其先进的边缘计算网关与云平台技术&#xff0c;为工厂提供了高效、稳定的数据采集与上云解决方案。本文将为您介绍Mbox边缘计算网关如何配合明达云平台&…

基于Java语言的培训平台+学习平台+在线学习培训系统+教育平台+教育学习系统+课程学习平台

简述 企业培训平台企业考试系统培训平台考试系统企业大学企业视频网站视频学习平台 介绍 企业培训平台支持企业培训考试全流程&#xff0c;在线学习、在线考试&#xff0c;成熟的企业培训考试解决方案&#xff0c;充分满足企业培训需求。 独立部署&#xff0c;仅内部员工登录…

JAVA高级--常用类(九)

JAVA高级–常用类 观看b站尚硅谷视频做的笔记 1、字符串相关的类 1.1 String 的使用 String 的特性&#xff1a; String 类&#xff1a;代表字符串。Java 程序中的所有字符串字面值&#xff08;如 “abc”&#xff09;都作为此类的实例实现。 String 是一个 final 类&#…

ESP8266 模块介绍—AT指令学习 笔记

零、简介 感谢百文网韦东山 老师对ESP8266模块的讲解 笔记在CSDN也有文章备份 大家可以在我的gitee仓库 中下载笔记源文件、ESP8266资料等 笔记源文件可以在Notion中导入 一、ESP8266-01S模块详细介绍 1. 名字的由来 ESP8266 是方形的主控芯片旁边的长方形是一个Flash-0…

IO编程——消息队列

题目&#xff1a; 代码实现&#xff1a; #include <myhead.h> //正文大小 #define MSGSZ (sizeof(struct msgbuf)-sizeof(long)) //定义要发送的消息类型 struct msgbuf{long msgtype; //消息类型char mtext[1024]; //消息正文 };int main(int argc, const char *ar…

全面升级:亚马逊测评环境方案的最新趋势与实践

在亚马逊测评领域深耕多年&#xff0c;见证了无数环境方案的更迭与演变&#xff0c;每一次变化都体现了国人不畏艰难、勇于创新的精神。面对平台的政策调整&#xff0c;总能找到相应的对策。那么&#xff0c;当前是否存在一套相对稳定且高效的技术方案呢&#xff1f;答案是肯定…

【计网】从零开始理解TCP协议 --- 拥塞控制机制,延迟应答机制,捎带应答,面向字节流

时间就是性命。 无端的空耗别人的时间&#xff0c; 其实是无异于谋财害命的。 --- 鲁迅 --- 从零开始理解TCP协议 1 拥塞控制2 延迟应答3 捎带应答4 面向字节流5 TCP异常情况TCP小结 1 拥塞控制 尽管TCP拥有滑动窗口这一高效的数据传输机制&#xff0c;能够确保在对方接收…