Android MediaCodec 简明教程(四):使用 MediaCodec 将视频解码到 Surface,并使用 SurfaceView 播放视频

系列文章目录

  1. Android MediaCodec 简明教程(一):使用 MediaCodecList 查询 Codec 信息,并创建 MediaCodec 编解码器
  2. Android MediaCodec 简明教程(二):使用 MediaCodecInfo.CodecCapabilities 查询 Codec 支持的宽高,颜色空间等能力
  3. Android MediaCodec 简明教程(三):详解如何在同步与异步模式下,使用MediaCodec将视频解码到ByteBuffers,并在ImageView上展示

文章目录

  • 系列文章目录
  • 前言
  • 一、Surface 是什么?
  • 二、MediaCodec 解码到 Surface
    • 2.1 Surface 从哪来?
    • 2. 2 MediaCodec 与 Surface 共享内存,实现零拷贝
    • 2.3 数据流动
    • 2.4 ReleaseOutputBuffer,控制水流速度
    • 2.5 Show me the code
  • 参考


前言

在上一个教程 Android MediaCodec 简明教程(三) 中,我们学会了使用 MediaCodec 解码到 ByteBuffers 上,包括同步模式和异步模式。本章将讨论 MediaCodec 解码到 Surface 的相关知识点。Google 推荐使用 Surface 进行编解码操作,这样效率更高。

一、Surface 是什么?

Android Surface 是一个复杂的概念,它涉及到 Android 图形系统,网络已经有很多相关的博文,例如:

  • 从整体上看Android图像显示系统
  • Android图形系统综述(干货篇)
  • 浅谈Android Surface机制
  • 深入Android系统(十二)Android图形显示系统-1-显示原理与Surface

Surface 是 Android 图形架构的一部分,它与 SurfaceFlinger、SurfaceView、SurfaceTexture、SurfaceHolder 等组件一起,构成了 Android 的图形系统。
Surface 的主要作用是存储图形数据。当你在一个 Surface 上绘制图像时,这些图像会被存储在 Surface 的缓冲区中。然后,这个 Surface 可以被提交给 SurfaceFlinger,SurfaceFlinger 会将这个 Surface 的内容合成到屏幕上。
Surface 通常与其他组件一起使用。例如,你可以使用 Canvas 在一个 Surface 上绘制2D图像,或者使用 OpenGL ES 在一个 Surface 上绘制3D图像。你也可以使用 MediaCodec 或者 Camera 将视频帧输出到一个 Surface 上。
Surface 还有一些其他的特性。例如,它可以被多个进程共享,这使得跨进程的图形数据传输成为可能。它还支持 vsync 信号,这可以帮助你实现平滑的动画效果。

这部分对于我来说过于复杂了,并且这也不是目前要关心的内容,我们抓一个重点即可:Surface 中有一个 BufferQueue,里头存放着图像数据,生产者将数据送入 Queue 中,而消费者从 Queue 获取数据。在 Android 中,Surface 可以被看作是一个画布,你可以在上面绘制图像,然后这些图像会被显示到屏幕上。这个画布并不是一个实体,而是一个虚拟的概念,它代表了一个可以被绘制的区域。

你可以把 Surface 想象成一个电子屏幕上的窗户。你可以在这个窗户上画画,然后这些画就会显示在电子屏幕上。这个窗户就像是你和电子屏幕之间的一个桥梁,你通过这个窗户,将你的画送到电子屏幕上。在这个比喻中,你的画就是图像数据,电子屏幕就是 Android 设备的显示屏,而窗户就是 Surface。你将图像数据(即你的画)送入 Surface(即窗户),然后这些数据就会被渲染到显示屏(即电子屏幕)上。

在 Android Surface的理解和应用 中对 Surface 中 生产者 - 消费者 架构做了较为详细的说明,不再赘述。

二、MediaCodec 解码到 Surface

像上一个教程一样,我们将使用 MediaExtractor 和 MediaCodec 解码视频,不同的是,解码后的视频帧画面将使用 SurfaceView 显示。

2.1 Surface 从哪来?

获取 Surface 最简单的方式是:使用 SurfaceView。例如:

val surfaceView = findViewById<SurfaceView>(R.id.surface_view)
val surface = surfaceView.holder.surface

2. 2 MediaCodec 与 Surface 共享内存,实现零拷贝

MediaCodec 使用 Surface 进行解码时,效率更高的原因是因为 Android 使用了共享内存技术。具体来说,MediaCodec 解码后的数据会直接传递给 Surface 的 BufferQueue,而不是通过拷贝的方式传递给 Surface。这样就避免了从 MediaCodec 到 Surface 之间的数据拷贝,提高了解码效率。

为了实现这种共享内存的方式,我们在调用 configure 方法时需要传入一个 Surface 对象,这样才能让 MediaCodec 和 Surface 共享同一个 BufferQueue。这种优化方式可以有效地减少数据拷贝的次数,提高解码效率,从而更好地满足视频播放等应用的需求。

codec.configure(videoFormat, surface, null, 0)

2.3 数据流动

在解码的过程涉及到三个对象: MediaExtractor、MediaCodec 和 Surface,数据就像流水线一样从一个地方流向另一个地方,并最终被消费。以 SurfaceView 为例,最终的消费者是 SurfaceFlinger,流水线如下图所示:
在这里插入图片描述

2.4 ReleaseOutputBuffer,控制水流速度

MediaCodec 的 releaseOutputBuffer 方法就像一个水龙头,你可以通过它来控制解码的速度。这个方法有两个版本:

  1. releaseOutputBuffer(int index, boolean render):这个版本的方法中,如果 render 参数为 true,那么解码后的数据(Buffer)会立即被送到 Surface 进行渲染(播放),播放完后,这个 Buffer 就会被标记为可用,返回给 MediaCodec。如果 render 参数为 false,那么这个 Buffer 不会被送到 Surface,而是直接被释放,然后被标记为可用,返回给 MediaCodec。

  2. releaseOutputBuffer(int index, long renderTimestampNs):这个版本的方法中,你可以传入一个时间戳 renderTimestampNs。如果这个时间戳小于当前的系统时间,那么 Buffer 的内容会立即被渲染。如果这个时间戳大于当前的系统时间,那么系统会等待,直到系统时间达到这个时间戳,然后再进行渲染。这个特性非常有用,特别是当你需要精确控制音频或视频的播放时间,或者需要同步多个音频或视频流的播放时。

总的来说,releaseOutputBuffer 方法就像一个控制解码速度的调节器,你可以通过它来精确控制音视频的播放。

2.5 Show me the code

所有代码你可以在 DecodeUsingSurfaceActivity 中找到,下面代码中,给出了同步和异步两种实现。

private fun decodeToSurface(surface: Surface){// create and configure media extractorval mediaExtractor = MediaExtractor()resources.openRawResourceFd(R.raw.h264_720p).use {mediaExtractor.setDataSource(it)}val videoTrackIndex = 0mediaExtractor.selectTrack(videoTrackIndex)val videoFormat = mediaExtractor.getTrackFormat(videoTrackIndex)// create and configure media codecval codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)val codecName = codecList.findDecoderForFormat(videoFormat)val codec = MediaCodec.createByCodecName(codecName)// configure with surfacecodec.configure(videoFormat, surface, null, 0)// start decodingval maxInputSize = videoFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)val inputBuffer = ByteBuffer.allocate(maxInputSize)val bufferInfo = MediaCodec.BufferInfo()val timeoutUs = 10000L // 10msvar inputEnd = falsevar outputEnd = falsecodec.start()while (!outputEnd && !stopDecoding) {val isExtractorReadEnd =getInputBufferFromExtractor(mediaExtractor, inputBuffer, bufferInfo)if (isExtractorReadEnd) {inputEnd = true}// get codec input buffer and fill it with data from extractor// timeoutUs is -1L means wait foreverval inputBufferId = codec.dequeueInputBuffer(-1L)if (inputBufferId >= 0) {if (inputEnd) {codec.queueInputBuffer(inputBufferId, 0, 0, 0,MediaCodec.BUFFER_FLAG_END_OF_STREAM)} else {val codecInputBuffer = codec.getInputBuffer(inputBufferId)codecInputBuffer!!.put(inputBuffer)codec.queueInputBuffer(inputBufferId,0,bufferInfo.size,bufferInfo.presentationTimeUs,0)}}// get output buffer from codec and render it to image view// NOTE! dequeueOutputBuffer with -1L is will stuck here,  so wait 10ms hereval outputBufferId = codec.dequeueOutputBuffer(bufferInfo, timeoutUs)if (outputBufferId >= 0) {if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {outputEnd = true}if (bufferInfo.size > 0) {val pts = bufferInfo.presentationTimeUs * 1000L + startTimecodec.releaseOutputBuffer(outputBufferId, pts)}}mediaExtractor.advance()}mediaExtractor.release()codec.stop()codec.release()}private fun decodeToSurfaceAsync(surface: Surface) {// create and configure media extractorval mediaExtractor = MediaExtractor()resources.openRawResourceFd(R.raw.h264_720p).use {mediaExtractor.setDataSource(it)}val videoTrackIndex = 0mediaExtractor.selectTrack(videoTrackIndex)val videoFormat = mediaExtractor.getTrackFormat(videoTrackIndex)// create and configure media codecval codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)val codecName = codecList.findDecoderForFormat(videoFormat)val codec = MediaCodec.createByCodecName(codecName)val maxInputSize = videoFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)val inputBuffer = ByteBuffer.allocate(maxInputSize)val bufferInfo = MediaCodec.BufferInfo()val inputEnd = AtomicBoolean(false)val outputEnd = AtomicBoolean(false)// set codec callback in async modecodec.setCallback(object : MediaCodec.Callback() {override fun onInputBufferAvailable(codec: MediaCodec, inputBufferId: Int) {val isExtractorReadEnd =getInputBufferFromExtractor(mediaExtractor, inputBuffer, bufferInfo)if (isExtractorReadEnd) {inputEnd.set(true)codec.queueInputBuffer(inputBufferId, 0, 0, 0,MediaCodec.BUFFER_FLAG_END_OF_STREAM)} else {val codecInputBuffer = codec.getInputBuffer(inputBufferId)codecInputBuffer!!.put(inputBuffer)codec.queueInputBuffer(inputBufferId,0,bufferInfo.size,bufferInfo.presentationTimeUs,bufferInfo.flags)mediaExtractor.advance()}}override fun onOutputBufferAvailable(codec: MediaCodec,outputBufferId: Int,info: MediaCodec.BufferInfo) {if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {outputEnd.set(true)}if(info.size > 0){// render the decoded frameval pts = info.presentationTimeUs * 1000L + startTimecodec.releaseOutputBuffer(outputBufferId, pts)}}override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {e.printStackTrace()}override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {// do nothing}})// configure with surfacecodec.configure(videoFormat, surface, null, 0)// start decodingcodec.start()// wait for processing to completewhile (!outputEnd.get() && !stopDecoding) {Thread.sleep(10)}mediaExtractor.release()codec.stop()codec.release()}

参考

  • Android Surface的理解和应用
  • 从整体上看Android图像显示系统
  • Android图形系统综述(干货篇)
  • 浅谈Android Surface机制
  • 深入Android系统(十二)Android图形显示系统-1-显示原理与Surface

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

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

相关文章

【大数据】Flink SQL 语法篇(二):WITH、SELECT WHERE、SELECT DISTINCT

Flink SQL 语法篇&#xff08;二&#xff09; 1.WITH 子句2.SELECT & WHERE 子句3.SELECT DISTINCT 子句 1.WITH 子句 应用场景&#xff08;支持 Batch / Streaming&#xff09;&#xff1a;With 语句和离线 Hive SQL With 语句一样的&#xff0c;语法糖 1&#xff0c;使用…

inode生命周期

1.添加inode到inode cache链表 当inode的引用计数器i_count为0后&#xff0c;会调用iput_final去释放 static void iput_final(struct inode *inode) {struct super_block *sb inode->i_sb;const struct super_operations *op inode->i_sb->s_op;unsigned long sta…

PaddleNLP的简单使用

1 介绍 PaddleNLP是一个基于PaddlePaddle深度学习平台的自然语言处理&#xff08;NLP&#xff09;工具库。 它提供了一系列用于文本处理、文本分类、情感分析、文本生成等任务的预训练模型、模型组件和工具函数。 PaddleNLP有统一的应用范式&#xff1a;通过 paddlenlp.Task…

数据结构-数组(详细讲解)

文章目录 数组数组的概述数组的图示一维数组二维数组 数组的定义一维数组的定义二维数组的定义 数组的取值赋值一维数组二维数组 数组的操作一维数组的操作索引实现指针实现 二位数组的操作矩阵转三元组矩阵的乘法 数组 数组的概述 概述&#xff1a;数组是一种线性数据结构&a…

C# 使用WMI监听进程的启动和关闭

写在前面 Windows Management Instrumentation&#xff08;WMI&#xff09;是用于管理基于 Windows 操作系统的数据和操作的基础结构。具体的API可以查看 WMI编程手册。 WMIC 是WMI的命令行管理工具&#xff0c;使用 WMIC&#xff0c;不但可以管理本地计算机&#xff0c;还可…

粒子群算法求解港口泊位调度问题(MATLAB代码)

粒子群算法&#xff08;Particle Swarm Optimization&#xff0c;PSO&#xff09;是一种基于群体智能的优化算法&#xff0c;它通过模拟鸟群或鱼群的行为来寻找最优解。在泊位调度问题中&#xff0c;目标是最小化所有船只在港时间的总和&#xff0c;而PSO算法可以帮助我们找到一…

Java把列表数据导出为PDF文件,同时加上PDF水印

一、实现效果 二、遇到的问题 实现导出PDF主体代码参考&#xff1a;Java纯代码实现导出PDF功能&#xff0c;下图是原作者实现的效果 导出报错Font STSong-Light with UniGB-UCS2-H is not recognized.。参考&#xff1a;itext 生成 PDF(五) 使用外部字体 网上都是说jar包的版本…

FastBee开源物联网平台2.0开源版发布啦!!!

一、项目介绍 物美智能(wumei-smart)更名为蜂信物联(FastBee)。 FastBee开源物联网平台&#xff0c;简单易用&#xff0c;更适合中小企业和个人学习使用。适用于智能家居、智慧办公、智慧社区、农业监测、水利监测、工业控制等。 系统后端采用Spring boot&#xff1b;前端采用…

成功解决AttributeError: ‘str‘ object has no attribute ‘decode‘

成功解决AttributeError: ‘str’ object has no attribute ‘decode’. &#x1f335;文章目录&#x1f335; &#x1f333;引言&#x1f333;&#x1f333;报错分析及解决方案&#x1f333;&#x1f333;参考文章&#x1f333;&#x1f333;结尾&#x1f333; &#x1f333;引…

Git安装,Git镜像,Git已安装但无法使用解决经验

git下载地址&#xff1a; Git - 下载 (git-scm.com) <-git官方资源 Git for Windows (github.com) <-github资源 CNPM Binaries Mirror (npmmirror.com) <-阿里镜像&#xff08;推荐&#xff0c;镜…

算法沉淀——前缀和(leetcode真题剖析)

算法沉淀——前缀和 01.一维前缀和02.二维前缀和03.寻找数组的中心下标04.除自身以外数组的乘积05.和为 K 的子数组06.和可被 K 整除的子数组07.连续数组08.矩阵区域和 前缀和算法是一种用于高效计算数组或序列中某个范围内元素之和的技巧。它通过预先计算数组的前缀和&#xf…

Redhat 8.4 一键安装 Oracle 11GR2 单机版

Oracle 一键安装脚本&#xff0c;演示 Redhat 8.4 一键安装 Oracle 11GR2 单机版过程&#xff08;全程无需人工干预&#xff09;&#xff1a;&#xff08;脚本包括 ORALCE PSU/OJVM 等补丁自动安装&#xff09; ⭐️ 脚本下载地址&#xff1a;Shell脚本安装Oracle数据库 脚本…

宝塔控制面板配置SSL证书实现网站HTTPS

宝塔安装SSL证书提前申请好SSL证书&#xff0c;如果还没有&#xff0c;先去Gworg里面申请&#xff0c;一般几分钟就可以下来&#xff0c;申请地址&#xff1a;首页-Gworg官方店-淘宝网 一、登录邮箱下载&#xff1a;Gworg证书文件目录 &#xff0c;都会有以下五个文件夹。宝塔…

ROS学习笔记11——ROS中的重名问题

一、ros功能包重名——ros工作空间覆盖 功能包重名时&#xff0c;会按照 ROS_PACKAGE_PATH 查找&#xff0c;在前的会优先执行。ROS 会解析 .bashrc 文件&#xff0c;并生成 ROS_PACKAGE_PATH ROS包路径&#xff0c;即调用功能包的顺序&#xff0c;该变量中按照 .bashrc 中配置…

Blender教程(基础)-内插面、分离、环切、倒角-08

一、内插面 菜单位置如下图位置。 单击需要处理的面&#xff0c;出现一个黄色的圈。 1、菜单选中内插 鼠标悬停在黄色圈内单击左键可以来回实现内插&#xff0c;但是发现并不好操作。 2、快捷键内插 在选中需要操作的面之后&#xff0c;鼠标移动到外面&#xff0c;键盘在英…

【linux】复制cp和硬连接、软连接的区别? innode 关系?

1.命令&#xff1a; cp -r [源文件或目录] [目的目录] #复制 ln -s [被链接的文件] [链接的目录/名称] #软连接 ln [被链接的文件] [链接的目录/名称] #硬连接 注&#xff1a;cp -r 会把所有source当作普通文件&#xff08;regular文件&#xff09;&#x…

【CMU-自主导航与规划】M-TARE planner 配置与运行

M-TARE docker M-TARE 源码 一、依赖 Docker, Docker Compose, NVIDIA Container Toolkit, Nvidia GPU Driver&#xff08;需要至少2个&#xff0c;带Nvidia GPU&#xff09; 1.1 Docker docker -v #查询版本1.2 Docker Compose docker compose version1.3 …

Python 数据分析实战——社交游戏的用户流失?酒卷隆治_案例2

# 什么样的顾客会选择离开 # 数据集 DAU : 每天至少来访问一次的用户数据 数据内容 数据类型 字段名 访问时间 string&#xff08;字符串&#xff09; log_data 应用名称 string&#xff08;字符串&#xff09; app_name 用户 ID int&#xff08;数值&#xff09; user_id…

时隔3年 | 微软 | Windows Server 2025 重磅发布

最新功能 以下是微软产品团队正在努力的方向&#xff1a; Windows Server 2025 为所有人提供的热补丁下一代 AD 活动目录和 SMB数据与存储Hyper-V 和人工智能还有更多… Ignite 发布视频 Windows Server 2025 Ignite Video 介绍 Windows Server 2022 正式发布日期是2021年…

Linux部署lomp环境,安装typecho、WordPress博客

部署lomp环境&#xff0c;安装typecho、WordPress博客 一、环境要求1.1.版本信息1.2.准备阿里云服务器【新用户免费使用三个月】1.3.准备远程工具【FinalShell】 二、Linux下安装openresty三、Linux下安装Mysql四、安装Apache【此步骤可省略】4.1.安装Apache服务及其扩展包4.2.…