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

news2024/11/17 5:52:28

系列文章目录

  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.coloradmin.cn/o/1419851.html

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

相关文章

白嫖!平替ChatGPT,高效阅读文档,支持pdf上传!

大家好&#xff0c;我是阿潘&#xff0c;现在技术更新的太快了&#xff0c;每天arxiv上面更新的论文太多了看不过来&#xff0c;同时还有一大堆公众号、知识星球、知乎等等&#xff0c;太多需要关注的信息了&#xff0c;力不从心啊。但是又怕漏掉一些有用的信息 因此今天跟大家…

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

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

怎么把word文档转换成pdf?几种高效转换方法了解一下

怎么把word文档转换成pdf&#xff1f;在当今这个时代&#xff0c;PDF已经成为一种通用的文件格式&#xff0c;广泛应用于各种场景。将Word文档转换为PDF&#xff0c;可以确保文档的格式、字体、图片等元素在各种设备和软件上保持一致。那么&#xff0c;如何将Word文档转换为PDF…

Liunx基础-----------------------第十六章网站服务

一、概念 UI的转变&#xff1a;B/S框架 HYML&#xff1a;超文本标记语言 网页&#xff1a;使用HTML&#xff0c;PHP&#xff0c;JAVA语言格式书写的文件 主页&#xff1a;网页中呈现用户的第一个页面 网站&#xff1a;多个网页组合而成的一台网站服务器 URL&#xff1a;统…

vue3封装el-pagination分页组件

1、效果如图&#xff1a; 2、分页组件代码&#xff1a; <template><div class"paging"><el-config-provider :locale"zhCn"><el-paginationv-model:current-page"page.currentPage"v-model:page-size"page.pageSize…

【BUG】golang gorm导入数据库报错 “unexpected type clause.Expr“

帮同事排查一个gorm导入数据报错的问题 事发现场 ck sql CREATE TABLE ods_api.t_sms_jg_msg_callback_dis (app_key String DEFAULT COMMENT 应用标识,callback_type Int32 DEFAULT 0 COMMENT 0送达&#xff0c;1回执,channel Int32 DEFAULT 0 COMMENT uid下发的渠道,mode…

element ui组件 el-date-picker设置default-time的默认时间

default-time &#xff1a;选择日期后的默认时间值。 如未指定则默认时间值为 00:00:00 默认值修改 <el-form-item label"计划开始时间" style"width: 100%;" prop"planStartTime"><el-date-picker v-model"formData.planStart…

安装elasticsearch、kibana、IK分词器

1.部署单点es 1.1.创建网络 因为我们还需要部署kibana容器&#xff0c;因此需要让es和kibana容器互联。这里先创建一个网络&#xff1a; docker network create es-net 1.2.加载镜像 这里我们采用elasticsearch的7.12.1版本的镜像&#xff0c;这个镜像体积非常大&#xff0…

flutter module打包成framework引入原生工程

Flutter - 将 Flutter 集成到现有项目&#xff08;iOS - Framework篇&#xff09; 本篇文章大幅参考了 caijinglong 大佬的总结文章&#xff1a; 把flutter作为framework添加到已存在的iOS中[1] 用 Flutter 来开发&#xff0c;从来都不可能是新开的一个纯 Flutter 项目&#xf…

vite+vue3 使用svg icon(包括element-plus icon)

1、安装依赖 npm i element-plus/icons-vue -S npm i vite-plugin-svg-icons -D2、在vite.config.ts文件中 import path from path; import { createSvgIconsPlugin } from vite-plugin-svg-icons; // 版本不同引入方式不同 export default defineConfig({...plugins: [...cr…

文件夹隐藏了怎么找出来?如何取消文件夹隐藏属性

在我们的日常工作中&#xff0c;经常会遇到文件夹被隐藏的情况&#xff0c;这可能会让我们在寻找需要的文件时感到困惑。那么&#xff0c;如何找回这些隐藏的文件夹呢&#xff1f;本文将为你提供一些实用的方法&#xff0c;帮助你解决这个问题。 图片来源于网络&#xff0c;如有…

温酒读Qt:QObject中篇2 ——欲遮还羞的 QObjectPrivate

《妙法莲华经》曰&#xff1a;“佛道长远&#xff0c;久受勤苦&#xff0c;乃可得成。” 世事修炼&#xff0c;莫不如是&#xff0c;日拱一卒无有尽&#xff0c;功不唐捐终入海。 传送门: 《温酒读Qt&#xff1a;QObject 序篇》 《温酒读Qt&#xff1a;QObject中篇1—— Q_OBJ…

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

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

红外热成像仪定制_热成像仪/红外夜视仪开发方案

红外热成像技术是一种利用红外热成像仪将物体发出的不可见红外辐射能量转换成可见的温度场图像的技术&#xff0c;通过不同颜色来表示不同温度。这项技术的应用领域非常广泛&#xff0c;从电路维修到暖通检测再到汽车故障排查等各个领域都有着重要的作用。 红外热成像仪的解决方…

数字人解决方案VividTalk——音频驱动单张照片实现人物头像说话的效果

前言 VividTalk是一项由南京大学、阿里巴巴、字节跳动和南开大学共同开发的创新项目。该项目通过结合单张人物静态照片和一段语音录音&#xff0c;能够制作出一个看起来仿佛实际说话的人物视频。项目的特点包括自然的面部表情和头部动作&#xff0c;口型能够同步&#xff0c;同…

搞明白手机卡的合约期和优惠期才能避免很多坑!

很多朋友注销流量卡时才发现自己的套餐有合约期无法注销&#xff0c;尤其是联通和移动&#xff0c;那么什么是合约期呢&#xff1f;合约期和优惠期又有什么不一样呢&#xff1f;下来答案来了。 其实&#xff0c;目前很多在网上办理的大流量卡都是有合约期的&#xff0c;尤其是移…

05. 交换机的基本配置

文章目录 一. 初识交换机1.1. 交换机的概述1.2. Ethernet_ll格式1.3. MAC分类1.4. 冲突域1.5. 广播域1.6. 交换机的原理1.7. 交换机的3种转发行为 二. 初识ARP2.1. ARP概述2.2. ARP报文格式2.3. ARP的分类2.4. 免费ARP的作用 三. 实验专题3.1. 实验1&#xff1a;交换机的基本原…

JSP仓储管理系统myeclipse定制开发SQLServer数据库网页模式java编程jdbc

一、源码特点 JSP仓储管理系统系统是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库 &#xff0c;系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为SQLServer2008&#x…

小白水平理解面试经典题目LeetCode 455 Assign Cookies【Java实现】

455 分配cookies 小白渣翻译&#xff1a; 假设你是一位很棒的父母&#xff0c;想给你的孩子一些饼干。但是&#xff0c;你最多应该给每个孩子一块饼干。 每个孩子 i 都有一个贪婪因子 g[i] &#xff0c;这是孩子满意的 cookie 的最小大小&#xff1b;每个 cookie j 都有一个…

非内积级联学习

1.首页推荐非内积召回现状 非内积召回源是目前首页推荐最重要的召回源之一。同时非内积相比于向量化召回最终仅将user和item匹配程度表征为embeding内积&#xff0c;非内积召回仅保留item embedding&#xff0c;不构造user显式表征&#xff0c;而是通过一个打分网络计算用户-商…