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.myapplicationimport 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.ActivityCompatclass CameraController {private val handlerThread: HandlerThread = HandlerThread("cameraThread")private var mCameraHandler: Handler? = nullvar size: Array<Size>? = null// 这里只是通过接口去拿了相机的尺寸,我们默认选了一个相机Idfun initCamera(context: Context) {val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManagervar cameraIdList = cameraManager.cameraIdListval 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 CameraManagervar cameraIdList = cameraManager.cameraIdListhandlerThread.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就是最终相机数据会输出的Surfacefun 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.myapplicationimport android.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaFormat
import android.view.Surface
import java.nio.ByteBufferclass CodecEncodeController {var mMediaCodec: MediaCodec? = nullvar mEncodeDataCallback: EncodeDataCallback? = nullvar mState = 0 // 0为初始状态,1为start状态,2为stop状态fun initMediaCodec(width: Int, height: Int) {// 构造编码器mMediaCodec = MediaCodec.createEncoderByType("video/avc");// 配置MediaFormatval 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配置MediaCodecmMediaCodec?.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)}fun getInputSurface(): Surface? {return mMediaCodec?.createInputSurface()}// 启动编码器fun startEncode() {mMediaCodec?.start()mState = 1processEncodeOutput()}// 循环获取输出数据OutputBuffer,输出数据通过mEncodeDataCallback回调给业务层,然后释放OutputBufferfun 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 = 2mMediaCodec?.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.myapplicationimport android.media.MediaCodec
import android.media.MediaCodec.BufferInfo
import android.media.MediaCodecInfo
import android.media.MediaFormat
import android.view.Surface
import java.nio.ByteBufferclass CodecDecodeController {var mMediaCodec: MediaCodec? = nullvar mState = 0 // 0为初始状态,1为start状态,2为stop状态fun initMediaCodec(width: Int, height: Int, outputSurface: Surface) {mMediaCodec = MediaCodec.createDecoderByType("video/avc");// 配置MediaFormatval 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 = 1processOutputData()}fun stop() {mState = 2mMediaCodec?.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)}}// 处理outputBufferfun 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.myapplicationimport 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.ByteBufferclass 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就是当前SurfaceViewdecodeController.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.rhkb.cn/news/453003.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vScode---配置Pyqt5环境--记录

前提条件&#xff1a; python运行环境已正常安装 1、安装Pyqt5 第三方库下载地址记录&#xff1a; 清华大学&#xff1a;https://pypi.tuna.tsinghua.edu.cn/simple 豆瓣&#xff1a;https://pypi.douban.com/simple 阿里云&#xff1a;https://mirrors.aliyun.com/pypi/simpl…

一文读懂组态图和组态软件,最浅显的解读

一、什么是组态图 组态图是指在工业自动化领域中&#xff0c;用来描述和展示控制系统中各个组件之间关系和工作流程的图形化表示方法。它是一个系统的框架图&#xff0c;通过图形符号和连接线&#xff0c;将各个组件&#xff08;如传感器、执行器、控制器等&#xff09;以及它…

linux ps和kill指令

目录 ps 命令 kill指令&#xff1a; 示例&#xff1a; 补充&#xff1a;管道的概念 管道的概念 管道的用途 示例 在Linux系统中&#xff0c;ps 和 kill 是两个非常常用的命令&#xff0c;用于管理和终止进程。 ps 命令 ps 命令用于显示当前系统中的进程状态。它可以提供…

责任链模式下,解决开闭原则问题实践

前言 在现代软件工程中&#xff0c;设计模式是解决常见问题的有效工具之一。它们吸收了前人的经验&#xff0c;不仅帮助开发者编写更清晰、更可维护的代码&#xff0c;还能促进团队之间的沟通和协作。责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;作为一…

无人机+视频推流直播EasyCVR视频汇聚/EasyDSS平台在森林防护巡检中的解决方案

随着科技的飞速发展&#xff0c;无人机技术在各个领域的应用日益广泛&#xff0c;特别是在森林防护与巡检方面&#xff0c;无人机以其独特的优势&#xff0c;为传统林业管理带来了革命性的变化。本文将探讨无人机在森林防护巡检中的解决方案&#xff0c;分析其工作原理、优势及…

基于SSM+微信小程序的电子点餐管理系统(点餐1)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 基于SSM微信小程序的电子点餐管理系统实现了管理员及用户。管理员实现了首页、个人中心、餐品分类管理、特色餐品管理、订单信息管理、用户管理、特价餐品管理、活动订单管理、系统管理。…

【论文学习与撰写】使用endnote工具进行论文参考文献的引用与插入

目录 1、软件的安装 2、放入endnote格式文献 3、endnote里文献管理 4、论文里引用参考文献的插入 5、参考文献的格式转换&#xff0c;及格式的下载 1、软件的安装 关注软件管家&#xff0c;进行下载软件和安装软件 下载通道②百度网盘丨下载链接&#xff1a; https://pa…

js.矩阵置零

链接&#xff1a;73. 矩阵置零 - 力扣&#xff08;LeetCode&#xff09; 题目&#xff1a; 给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,1,1],…

Flutter 11 Android原生项目集成Flutter Module

本文主要讲解如何在已有的Android原生老项目中集成Flutter模块。 实现流程&#xff1a; 1、在Android原生项目根目录下&#xff0c;创建Flutter Module&#xff1b; 2、修改Android原生项目settings.gradle&#xff0c;绑定 Flutter Module&#xff1b; 3、修改Android原生…

15分钟学Go 第6天:变量与常量

第6天&#xff1a;变量与常量 在Go语言中&#xff0c;变量和常量是编程的基础概念。理解如何定义和使用它们不仅能帮助我们管理数据&#xff0c;还能增强代码的可读性和可维护性。在本章中&#xff0c;我们将详细探讨Go语言中的变量和常量&#xff0c;涵盖它们的定义、使用、作…

[Xshell] Xshell的下载安装使用及连接linux过程 详解(附下载链接)

前言 Xshell.zip 链接&#xff1a;https://pan.quark.cn/s/5d9d1836fafc 提取码&#xff1a;SPn7 安装 下载后解压得到文件 安装路径不要有中文 打开文件 注意&#xff01;360等软件会拦截创建注册表的行为&#xff0c;需要全部允许、同意。或者退出360以后再安装。 在“绿化…

spdlog学习记录

spdlog Loggers&#xff1a;是 Spdlog 最基本的组件&#xff0c;负责记录日志消息。在 Spdlog 中&#xff0c;一个 Logger 对象代表着一个日志记录器&#xff0c;应用程序可以使用 Logger 对象记录不同级别的日志消息Sinks&#xff1a;决定了日志消息的输出位置。在 Spdlog 中&…

程序员节的故事:在代码的海洋中遨游

#1024程序员节 | 征文# 一年一度的程序员节又来了&#xff0c;作为一名热爱编程的开发者&#xff0c;我总是期待着这个特殊的日子。10月24日&#xff0c;不仅是程序员们的节日&#xff0c;更是我们分享故事、交流技术的时刻。今年的1024征文活动让我感到无比兴奋&#xff0c;因…

Axure重要元件三——中继器修改数据

亲爱的小伙伴&#xff0c;在您浏览之前&#xff0c;烦请关注一下&#xff0c;在此深表感谢&#xff01; 课程主题&#xff1a;中继器修改数据 主要内容&#xff1a;显示编辑内容、表格赋值、修改数据 应用场景&#xff1a;更新行、表单数据行修改 案例展示&#xff1a; 正文…

STM32L031F6P6基于CubeMX的串口通信调试笔记

用CubeMX创建项目 本实例用的PA14、PA13两个引脚&#xff0c;LPUART1。 对串口参数进行设置&#xff1a; 开启串口中断&#xff1a; 时钟源设置成内部高频时钟&#xff1a; 对项目进行设置&#xff1a; 生成代码&#xff1a; 在串口初始化函数中加入 __HAL_UART_ENA…

C++11 thread,mutex,condition_variable,atomic,原子操作CAS,智能指针线程安全,单例模式最简单实现方式

1.thread 在c11中&#xff0c;c标准委员会开发出了thread库&#xff1b;接下来我们先来看看这个库的使用吧&#xff1b; 1.1 thread类接口介绍 1.1.1 thread类构造函数 我们thread库中的thread类的构造函数可以通过直接传递回调函数与函数的参数来构造线程&#xff1a; int…

THP4 SOP16 芯片 高速光耦芯片

光电耦合器输入端加电信号使发光源发光&#xff0c;光的强度取决于激励电流的大小&#xff0c;此光照射到封装在一起的受光器上后&#xff0c;因光电效应而产生了光电流&#xff0c;由受光器输出端引出&#xff0c;这样就实现了电一光一电的转换。 由于光耦合器输入输出间互相…

mysql主从复制及故障修复

一、主MySQL数据库的配置 分别在三台主机&#xff08;chen2/10.110、chen3/10.120、chen4/10.130)中安装mysql数据&#xff0c;其中chen2/10.110作为主MySQL服务器&#xff0c;其余两台作为从MySQL服务器。 1、在主机上部署mysql数据库 详细的请看上一篇&#xff1a;mysql数据…

2021年江西省职业院校技能大赛(高职组) “云计算应用”赛项样题

2021年江西省职业院校技能大赛&#xff08;高职组&#xff09; “云计算应用”赛项样题 【任务 1】基础运维任务[5 分]【题目 1】基础环境配置【题目 2】镜像挂载【题目 3】Yum 源配置【题目 4】时间同步配置【题目 5】计算节点分区 【任务 2】OpenStack 搭建任务[15 分]【题目…

现今 CSS3 最强二维布局系统 Grid 网格布局

深入学习 CSS3 目前最强大的布局系统 Grid 网格布局 Grid 网格布局的基本认识 Grid 网格布局: Grid 布局是一个基于网格的二位布局系统&#xff0c;是目前 CSS 最强的布局系统&#xff0c;它可以同时对列和行进行处理&#xff08;它将网页划分成一个个网格&#xff0c;可以任…