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 extractor
        val mediaExtractor = MediaExtractor()
        resources.openRawResourceFd(R.raw.h264_720p).use {
            mediaExtractor.setDataSource(it)
        }
        val videoTrackIndex = 0
        mediaExtractor.selectTrack(videoTrackIndex)
        val videoFormat = mediaExtractor.getTrackFormat(videoTrackIndex)

        // create and configure media codec
        val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
        val codecName = codecList.findDecoderForFormat(videoFormat)
        val codec = MediaCodec.createByCodecName(codecName)
        // configure with surface
        codec.configure(videoFormat, surface, null, 0)

        // start decoding
        val maxInputSize = videoFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
        val inputBuffer = ByteBuffer.allocate(maxInputSize)
        val bufferInfo = MediaCodec.BufferInfo()
        val timeoutUs = 10000L // 10ms
        var inputEnd = false
        var outputEnd = false

        codec.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 forever
            val 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 here
            val 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 + startTime
                    codec.releaseOutputBuffer(outputBufferId, pts)
                }
            }

            mediaExtractor.advance()
        }


        mediaExtractor.release()
        codec.stop()
        codec.release()
    }

    private fun decodeToSurfaceAsync(surface: Surface) {
        // create and configure media extractor
        val mediaExtractor = MediaExtractor()
        resources.openRawResourceFd(R.raw.h264_720p).use {
            mediaExtractor.setDataSource(it)
        }
        val videoTrackIndex = 0
        mediaExtractor.selectTrack(videoTrackIndex)
        val videoFormat = mediaExtractor.getTrackFormat(videoTrackIndex)

        // create and configure media codec
        val 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 mode
        codec.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 frame
                    val pts = info.presentationTimeUs * 1000L + startTime
                    codec.releaseOutputBuffer(outputBufferId, pts)
                }
            }

            override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
                e.printStackTrace()
            }

            override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
                // do nothing
            }
        })

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

        // start decoding
        codec.start()

        // wait for processing to complete
        while (!outputEnd.get() && !stopDecoding) {
            Thread.sleep(10)
        }

        mediaExtractor.release()
        codec.stop()
        codec.release()
    }

参考

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

微信小程序(二十五)条件判断语句与结构隐藏

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.条件判断语句的演示 2.隐藏结构的演示 源码&#xff1a; index.wxml <view><!-- wx:if和wx:else为条件判断语句 --><text wx:if"{{isLogin}}">已登入的用户</text><tex…

Flutter 应用服务:主题、暗黑、国际化、本地化-app_service库

Flutter应用服务 主题、暗黑、国际化、本地化-app_service库 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/article/det…

(一)PySpark3:安装教程及RDD编程(非常详细)

目录 一、pyspark介绍 二、PySpark安装 三、RDD编程 1、创建RDD 2、常用Action操作 ①collect ②take ③takeSample ④first ⑤count ⑥reduce ⑦foreach ⑧countByKey ⑨saveAsTextFile 3、常用Transformation操作 ①map ②filter ③flatMap ④sample ⑤d…

SQL报错注入

君衍. 一、sqllabs第五关报错注入updatexml报错注入原理及思路 二、常见的报错函数三、floor报错注入原理1、概念2、函数回顾2.1 rand函数2.2 floor(rand(0)*2)函数2.3 group by函数2.4 count(*)函数2.5 函数综合报错 3、报错分析4、总结 一、sqllabs第五关报错注入 之前我在这…

Linux系列之查看cpu、内存、磁盘使用情况

查看磁盘空间 df命令用于显示磁盘分区上的可使用的磁盘空间。默认显示单位为KB。可以利用该命令来获取硬盘被占用了多少空间&#xff0c;目前还剩下多少空间等信息。使用df -h命令&#xff0c;加个-h参数是为了显示GB MB KB单位&#xff0c;这样更容易查看 Filesystem …

使用Hugging Face下载ImageNet

1. 创建Hugging Face账号&#xff0c;在个人中心的setting部分&#xff0c;生成Access Token 2. 使用在python命令行中运行&#xff1a; pip install datasets 此时需要输入token 3. 新建一个python文件 from datasets import load_dataset dset load_dataset(imagenet-1k…

http-server开启一个服务器

前言 在写前端页面中&#xff0c;经常会在浏览器运行HTML页面&#xff0c;从本地文件夹中直接打开的一般都是file协议&#xff0c;当代码中存在http或https的链接时&#xff0c;HTML页面就无法正常打开&#xff0c;为了解决这种情况&#xff0c;需要在在本地开启一个本地的服务…

vuex store,mutations,getters,actions

文章目录 1.vuex概述2.构建vuex【多组件数据共享】环境Son1.vueSon2.vueApp.vue 3.创建一个空仓库4.如何提供&访问vuex的数据①核心概念 - state状态1.通过store直接访问2.通过辅助函数简化代码 ②核心概念 - mutations&#xff08;粗略&#xff09; 5.核心概念 - mutation…

如何在 VM 虚拟机中安装 Kail Linux 2023.4 操作系统保姆级教程(附链接)

一、VMware Workstation 虚拟机 先得安装 VM 虚拟机&#xff0c;没有的可以参考这篇文章安装 VM 虚拟机 如何在 VM 虚拟机中安装 Win10 操作系统保姆级教程&#xff08;附链接&#xff09;https://eclecticism.blog.csdn.net/article/details/135713915 二、Kail 镜像 进入…

树控件、下拉框、文本框常用测试用例

01 控件的测试外观操作 1&#xff09;项目中的所有树是否风格一致 2&#xff09;树结构的默认状态是怎样的。比如默认树是否是展开&#xff0c;是展开几级&#xff1f; 是否有默认的焦点&#xff1f;默认值是什么&#xff1f;展开的节点图标和颜色&#xff1f; 3&#xff09…

关于如何将Win幻兽帕鲁服务端存档转化为单人本地存档的一种方法(无损转移)

本文转自博主的个人博客&#xff1a;https://blog.zhumengmeng.work,欢迎大家前往查看。 原文链接&#xff1a;点我访问 **起因&#xff1a;**最近大火的开放世界缝合体游戏幻兽帕鲁的大火也是引起了博主的注意&#xff0c;然后博主和周边小伙伴纷纷入手&#xff0c;博主也是利…

谷歌人工智能视频生成器-LUMIERE(未开源)

Google重磅发布视频生成模型Lumiere 据说后续会开源 亮点1.支持文本到视频与图像到视频 亮点2.画风迁移 亮点3.运动蒙版 亮点4.视频编辑 亮点5.视频修复 谷歌视频模型可以生成80帧的片段&#xff01;不仅画质好、质量高&#xff0c;而且时长更长。 视频局部编辑 这项功能可以…

07.领域驱动设计:了解3种常见微服务架构模型的对比和分析

目录 1、概述 2、整洁架构 3、六边形架构 4、三种微服务架构模型的对比和分析 5、从三种架构模型看中台和微服务设计 5.1 中台建设要聚焦领域模型 5.2 微服务要有合理的架构分层 5.2.1 项目级微服务 5.2.2 企业级中台微服务 5.3 应用和资源的解耦与适配 6、总结 1、概…

C语言-动态内存申请

一、动态分配内存的概述 在数组一章中&#xff0c;介绍过数组的长度是预先定义好的&#xff0c;在整个程序中固定不变&#xff0c;但是在实际的编程中&#xff0c;往往会发生这种情况&#xff0c;即所需的内存空间取决于实际输入的数据&#xff0c;而无法预先确定 。为了解决…

一、创建Vue3项目

1. 下载 node.js 下载地址&#xff1a;https://nodejs.org/zh-cn 优先选择 16 版本; node -v || node -version 可以检查本地 node.js 版本 2. 设置淘宝镜像源 npm config set registry https://registry.npmmirror.com/ 设置淘宝镜像源 npm config get registry 查看当前镜像…

常见的网络安全威胁和防护方法

随着数字化转型和新兴技术在各行业广泛应用&#xff0c;网络安全威胁对现代企业的业务运营和生产活动也产生了日益深远的影响。常见的网络安全威胁通常有以下几种&#xff1a; 1. 钓鱼攻击 攻击者伪装成合法的实体&#xff08;如银行、电子邮件提供商、社交媒体平台等&#xf…

解析PDF二维码:数字时代文件管理的创新之道

随着数字时代的来临&#xff0c;文件管理方式正经历着翻天覆地的变革。在这个变革的浪潮中&#xff0c;PDF二维码作为一种创新的技术手段&#xff0c;正逐渐引起人们的关注。本文将深入探讨PDF二维码的概念、应用领域以及在文件管理中的前景。 一、PDF二维码的概念 PDF二维码…

mcu专用看门狗复位芯片(如MAX706)

mcu专用看门狗复位芯片&#xff08;如MAX706&#xff09; 为什么要使用电压复位芯片RESET引脚WDO引脚MR引脚WDI引脚 国产替代型号应用电路1 推荐电路&#xff08;用一个跳线帽使能/关闭看门狗功能&#xff0c;调试MCU时防止看门狗芯片随便触发复位功能&#xff09;&#xff0c;…

Linux操作系统概述

操作系统&#xff08;Operating System&#xff09;的定义 操作系统&#xff0c;是指直接管理系统硬件和资源&#xff08;如 CPU、内存和存储空间&#xff09;的软件。 操作系统的基本功能 ①统一管理计算机资源&#xff1a;处理器资源&#xff0c;IO设备资源&#xff0c;存储…

Flutter的安装与环境配置

一、下载安装Futter&#xff1a; 1、Flutter中文文档&#xff1a; 安装和环境配置 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter 2、下载 Futter SDK&#xff1a; Flutter中文文档 里面有&#xff0c;下载完成之后找个文件夹解压出来&#xff0c;最好不要将 Flu…