Android 屏幕采集并编码为H.264

news2025/1/9 2:06:54

前言

我们前面基于摄像机的图像采集以及编解码已经完成了,那么接下来计划后面的三篇博文分别实现Android屏幕采集实现并进行H.264编解码、MIC音频采集并编码为AAC以及AAC解码播放,希冀可以通过这六篇博文能够对Android上面的音视频编解码有一个初步的学习和了解,由于博主也是近期刚从0开始学习这部分的知识,因此博文中有不恰当的描述,希望大家能够指正,对于有想法进行Android音视频开发的同学,希望这6篇博文能够帮助您启蒙。

那么本篇,我们就先来看看Android屏幕采集实现并进行H.264编解码。

屏幕采集简介

在Android 5.0及以上版本中,可以使用系统提供的MediaProjection API进行屏幕采集,而无需root权限 。MediaProjection 允许应用程序捕获屏幕内容并进行处理。

在实际实现过程还需要用到下面两个类:
MediaProjectionManager:是一个系统服务,看名字可以理解为对MediaProjection进行管理,所以在使用时需要通过MediaProjectionManager获取MediaProjection。

VirtualDisplay:大家可以理解为安卓上面的虚拟显示器,而最终的屏幕显示采集就是通过这个虚拟显示器实现的,可以理解为在录屏时安卓系统会将主屏画面拷贝一份到这个虚拟显示,而虚拟显示器会将图像数据最终输出到Surface,Surface还是之前说的大家理解为队列或者缓冲区都可以。

具体的屏幕采集实现流程如下:

屏幕采集实现

还是与之前一样我们需要对屏幕采集完整的流程进行封装,感觉有点封装上瘾了,哈哈。新建一个ScreenCapture类,并添加如下代码。

class ScreenCapture {
    private val REQUEST_CODE: Int = 1000
    internal val act: Activity?
    internal var videoEncFormat:VideoEncFormat = VideoEncFormat()
    internal var dpi:Int = 320
    internal val callback:((ByteArray,Int)->Unit)?
    internal var data: Intent? = null
    internal var resultCode:Int = 0
    internal val mediaProjectionManager: MediaProjectionManager?

    private constructor(builder:Builder){
        act = builder.act
        videoEncFormat = builder.videoEncFormat
        dpi = builder.dpi
        callback = builder.callback

        mediaProjectionManager = act?.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
    }

乍一看,是不是感觉全局变量有点多,不要怕,实际的比这个还要多一些,跟编码相关的一部分已经封装到了videoEncFormat中,这个就是我们上一篇中优化后的编码参数类。

ScreenCapture因为参数比较多,所以我使用了构造者模式,ScreenCapture不能直接创建必须通过构造器来创建。

然后屏幕录制时需要用到Activity,这里注意下是Activity,不是Context,因为屏幕录制权限申请需要通过startActivityForResult函数进行请求,权限申请结果会在onActivityResult中返回,所以这里的REQUEST_CODE就是Activity的请求码,data,resultCode则是onActivityResult中返回的Intent和结果码。

这里需要传入一个dpi,后面会设置到VirtualDisplay,可以认为就是虚拟显示器的dpi,我们任何屏幕都会有dpi,虚拟显示器也不例外。

这里的callback则是编码数据返回的回调接口。

全局变量作用介绍完了,那么下来我们看下这个构造器长什么样子。

companion object{
    fun newBuilder():Builder{
        return Builder()
    }
}

class Builder{
        var act: Activity? = null
        var videoEncFormat:VideoEncFormat = VideoEncFormat()
        var dpi:Int = 320
        var callback:((ByteArray,Int)->Unit)? = null

        fun with(act: Activity):Builder{
            this.act = act
            return this
        }

        fun resolution(width:Int,height:Int):Builder{
            videoEncFormat.setWidth(width)
            videoEncFormat.setHeight(height)
            return this
        }

        fun fps(fps:Int):Builder{
            videoEncFormat.setFrameRate(fps)
            return this
        }

        fun bitRate(bitRate:Int):Builder{
            videoEncFormat.setBitRate(bitRate)
            return this
        }

        fun keyInterval(keyInterval:Float):Builder{
            videoEncFormat.setKeyInterval(keyInterval)
            return this
        }

        fun rotation(rotation:Int):Builder{
            videoEncFormat.setRotation(rotation)
            return this
        }

        fun setCaptureCallback(cbk:(data:ByteArray,flag:Int)-> Unit):Builder{
            this.callback = cbk
            return this
        }

        fun dpi(dpi:Int){
            this.dpi = dpi
        }

        fun build():ScreenCapture{
            return ScreenCapture(this)
        }
    }

这个类很简单,就不再赘述,有疑问可以留言,我看到后会答复。
这里还添加了一个快捷函数,Java写习惯了,Kotlin貌似不需要,哈哈,留着吧。

接着我们来看下,如何启动屏幕采集:

    fun start(){
        act?.startActivityForResult(mediaProjectionManager?.createScreenCaptureIntent(), REQUEST_CODE)
    }
    
    fun onActivityResult(requestCode:Int, resultCode:Int, data: Intent) {
        if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            this.resultCode = resultCode
            this.data = data
           ScreenEncoderService.start(this)
        }
    }

start()这里通过Activity的startActivityForResult启动录屏权限请求,第一个参数是Intent,但不是我们自建的,而是通过mediaProjectionManager.createScreenCaptureIntent()直接获取的,第二个就是咱们上面定义的请求码了。

不管用户确认还是取消,最后权限结果都会通过onActivityResult返回,如果权限校验是OK的,那么先记录下返回的结果码和data(Intent),这两个数据后面录屏启动流程中还需要用到,之后是调用了ScreenEncoderService.start(this),这里是启动了名为ScreenEncoderService的Service,后续所有的录屏流程都在ScreenEncoderService中实现了。

这里之所以将后续录屏流程放到了Service中实现,是因为在targetSdkVersion大于等于29(Android 10)时,系统加强了对屏幕采集的限制,必须先启动相应的前台Service,才能正常调用getMediaProjection方法,否则会抛出异常。

我们再来看下ScreenCapture中的stop()。

    fun stop(){
        ScreenEncoderService.stop()
    }
    

stop()中的ScreenEncoderService.stop()与start()中的类似,不过这里是停止ScreenEncoderService。

下来,我们来看录屏的核心服务ScreenEncoderService。

class ScreenEncoderService :ForegroundService(),VideoEncoder.EncoderCallback{

    private var mediaProjection: MediaProjection? = null
    private var virtualDisplay: VirtualDisplay? = null

    private var videoEncoder: VideoEncoder? = null
    
        
    override fun onCallback(data: ByteArray, frameFlags: Int) {
         capture?.callback?.invoke(data,frameFlags)
    }

ScreenEncoderService继承了ForegroundService类,ForegroundService是一个封装的前台服务类,这个类大家可以在ForegroundService看到,这里就不过多扩充前台服务的知识了,有需要的可以自行查找了解。

ScreenEncoderService也实现了VideoEncoder编码器的EncoderCallback接口,通过这个接口间接的将编码数据回调给监听者。

这个三个全局变量,我就不介绍了,mediaProjection和virtualDisplay上面已经介绍过了,接下来只需要关注他们的怎么实例化即可,videoEncoder这个是之前我们封装的视频编码器,有需要了解的可以通过Android Camera2采集并编码为H.264文章进行了解。

  companion object{
        private var capture:ScreenCapture? = null
        fun start(capture:ScreenCapture){
            this.capture = capture

            var intent:Intent  = Intent(capture.act,ScreenEncoderService::class.java)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                capture.act?.startForegroundService(intent)
                return
            }
            capture.act?.startService(intent)
        }

        fun stop(){
            capture?.act?.stopService(Intent(capture?.act,ScreenEncoderService::class.java))
        }
    }

start()函数中保存了我们外面调用的ScreenCapture,接着通过capture中保存的Activity对象启动了自己,启动的时候进行了版本校验,如果版本大于等于26就会启动为前台服务,否则就启动为后台服务。

    override fun onCreate() {
        super.onCreate()
        startScreenCapture()
    }

    private fun startScreenCapture() {
        var inputSurface = startEncoder()

        capture?.let {
            mediaProjection = it.mediaProjectionManager?.getMediaProjection(it.resultCode, it.data!!)

            var dpi = it.dpi
            var width = it.videoEncFormat.getWidth()
            var height = it.videoEncFormat.getHeight()

            virtualDisplay = mediaProjection?.createVirtualDisplay("ScreenCapture",
                width, height, dpi,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                inputSurface, null, null)
        }
    }
    

在Service生命周期的onCreate中调用了startScreenCapture(),这个startScreenCapture()就是我们最后的屏幕采集实现了。

startScreenCapture() 中先调用了startEncoder()返回了一个Surface,这个实际上就是VideoEncoder的输入Surface。startEncoder()等下再看,我们先将startScreenCapture()看完

通过mediaProjectionManager以及前面onActivityResult中返回过来的data和resultCode获取MediaProjection实例mediaProjection,接着通过mediaProjection又创建了VirtualDisplay的实例virtualDisplay,

createVirtualDisplay()中的参数比较多,我单独列了个表格,其作用如下:

参数名称作用
name虚拟显示的名称,必须非空。这是用于标识虚拟显示的一个字符串。
width虚拟显示的宽度(以像素为单位),必须大于0。这指定了虚拟显示的像素宽度。
height虚拟显示的高度(以像素为单位),必须大于0。这指定了虚拟显示的像素高度。
densityDpi虚拟显示的密度(以dpi为单位),必须大于0。这指定了虚拟显示的屏幕密度。
surface虚拟显示的内容应该被渲染到的 Surface,如果没有则为 null。这个 Surface 是应用提供的,用于渲染虚拟显示的内容。
flags虚拟显示标志的组合,可以是以下几种:
VIRTUAL_DISPLAY_FLAG_PUBLIC:创建公共显示。
VIRTUAL_DISPLAY_FLAG_PRESENTATION:创建用于展示的显示。
VIRTUAL_DISPLAY_FLAG_SECURE:创建安全的显示,内容不会被截屏或录屏。
VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY:只显示应用自己的内容。
VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR:自动镜像主屏幕的内容。
callback当虚拟显示的状态改变时调用的回调,如果没有则为 null。这个回调用于监听虚拟显示的状态变化。
handler应该在哪个 Handler 上调用回调,如果没有则为 null,这意味着回调将在调用线程的主 Looper 上被调用。

虚拟显示器创建成功后,就标志着已经开始进行屏幕数据采集了,这些都是系统内部自行实现的。

那么让我们回过头来看看startEncoder()。

    private fun startEncoder():Surface? {
        if(videoEncoder == null){           
            videoEncoder = VideoEncoder(capture!!.videoEncFormat).apply {
                 setEncoderCallback(this@ScreenEncoderService)
                 start()
            }
        }
        var inputSurface = videoEncoder?.getInputSurface()
        return inputSurface
    }

这块VideoEncoder创建就显得简单很多了,创建videoEncoder时将capture中传入的参数及编码VideoEncFormat直接传给了VideoEncoder,接着设置了编码后的数据回调并启动了编码器。

在之后获取了编码器的输入Surface并返回。

至此屏幕采集和编码部分的核心代码就已经编码完成,接着让我们再继续添加如下代码:

    private fun stopScreenCapture(){
        videoEncoder?.stop()
        mediaProjection?.stop()
        virtualDisplay?.release()
    }

    override fun onDestroy() {
        super.onDestroy()
        stopScreenCapture()
    }

在Service销毁的时候同步停止了屏幕采集,停止屏幕采集的时候一并销毁了虚拟显示器,这一点大家一定要注意,这两个要同步销毁。

现在ScreenEncoderService已经编写完成,那么还有一个小点不要忘记了,将它添加到AndroidManifest中。

    <service android:name="com.zlgspace.andcodec.codec.ScreenEncoderService"
        android:foregroundServiceType="mediaProjection"
        />

foregroundServiceType中的mediaProjection应该是固定写法,没有深究,这个大家记着这么写即可。

至此我们屏幕采集编码的封装就已经全部完成,接下来让我们看看如何应用。

使用ScreenCapture

lateinit var screenCapture:ScreenCapture

screenCapture = ScreenCapture.newBuilder()
    .with(this)
    .fps(30)
    .resolution(1920, 1080)
    .setCaptureCallback{data,flag->
        //对编码后的数据进行处理
    }
    .build()
    
 screenCapture.start()
 
 screenCapture.stop()
 
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    screenCapture.onActivityResult(requestCode, resultCode, data)
}

是不是还算比较简单,通过构造器实例化ScreenCapture后就可以对屏幕采集进行开始或者停止操作,不过还需要转发下Activity的onActivityResult到ScreenCapture的onActivityResult,这块略微有点繁琐,但是目前没有其他好的办法,只能这样。

写到最后

到这里,我们屏幕采集并编码就已经全部完成,整个实现还是有点粗糙,但是相信我们一定会将这些打磨的更加优秀,至此感谢大家观看,如果觉得对你有帮助希望能点下关注,博主是多年Android开发者,关于Android从应用到系统,多少都懂一些,对于安卓后续还会持续更新更多更高质量的博文,对自己的技能加强的的同时,也希望能够帮到有需要的同学。

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

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

相关文章

深入探索 Compose 渲染流程:从 UI 树到 Skia 绘制的实现解析

文章目录 前言Compose 渲染流程概述1. Compose 解析1.1 Compose 声明性 UI1.2 Compose 编译1.2.1 Compose 编译概述1.2.2 代码示例1.2.3 编译过程细节 1.3 组合与重组合1.3.1 组合&#xff08;Composition&#xff09;1.3.2 重组合1.3.3 组合与重组合的区别1.3.4 组合与重组合的…

PySpark3.4.4_基于StreamingContext实现网络字节流统计分析

网络字节流与嵌套字节流的区别 概念解释 网络嵌套字节流&#xff1a; 在网络编程的情境下&#xff0c;网络嵌套字节流通常是指将字节流&#xff08;字节序列&#xff09;以一种分层或者包含的方式进行组织&#xff0c;用于在网络传输过程中更好地处理数据。例如&#xff0c;在一…

【Homework】【8】Learning resources for DQ Robotics in MATLAB

作业任务 创建一个名为“VS050RobotDH”的类&#xff0c;该类代表Denso VS050机器人&#xff0c;其DH参数如下表所示&#xff0c;并且完全由旋转关节组成。&#xff08;请记住第6课的内容&#xff09; θ \theta θ d d d a a a α \alpha α − π -\pi −π0.3450 π 2 \fra…

如何防御ARP欺骗 保护IP安全

在数字化浪潮席卷全球的今天&#xff0c;网络安全威胁如同暗流涌动&#xff0c;时刻考验着我们的防范能力。其中&#xff0c;ARP欺骗攻击作为一种隐蔽性强、成本低廉且危害严重的网络攻击手段&#xff0c;成为众多网络安全事件中的一颗“毒瘤”。那么我们究竟是如何防御ARP欺骗…

低代码场景案例配置——复杂数据模型下表单与表格关联字段的保存

主子表的场景是每个业务系统都绕不过的功能点&#xff0c;低代码能不能在业务上用的起来&#xff0c;这个是必须过的门槛。那么什么主子表有哪些场景的应用&#xff0c;如何配置呢&#xff0c;接下来我们就举个例详细说明 订单管理系统&#xff0c;场景描述&#xff1a; 在电…

方案拆解 | 打击矩阵新规频出!2025矩阵营销该怎么玩?

社媒平台的矩阵营销又要“变天”了&#xff1f;&#xff01; 11月18日&#xff0c;小红书官方发表了被安全薯 称为“小红书史上最严打击黑灰产专项”新规&#xff0c;其中就包括黑灰产矩阵号的公告。 ▲ 图源&#xff1a;小红书 实际上&#xff0c;不包括这次&#xff0c;今年…

C51小车项目-笔记11-SU-03T语音控制模块

一、网页配置 网站&#xff1a;智能公元/AI产品零代码平台 配置步骤&#xff1a; 发布版本&#xff0c;输入版本名字 等待SDK生成成功 成功之后下载SDK&#xff0c;完成之后将压缩包放到一个没有中文的文件目录中解压 二、接线 三、操作步骤 解压&#xff0c;以管理员身份打…

Springboot3介绍

一、Springboot3简介: https://docs.spring.io/spring-boot/docs/current/reference/html/getting-started.html?spmwolai.workspace.0.0.68b62306Q6jtTw#getting-started.introducing-spring-boot 无论使用XML、注解、Java配置类还是他们的混合用法&#xff0c;配置文件过于…

Mac上基于pyenv管理Python多版本的最佳实践

首先声明&#xff0c;你可以选择使用 Homebrew 来安装pyenv。我这里主要是想和我 Linux 设备上一致&#xff0c;所以选择使用脚本来安装pyenv。 准备安装脚本 这个安装的脚本来源于官方的的github仓库。 关于安装脚本的解读请看《pyenv 安装脚本解读》。 pyenv-installer.sh …

生成:安卓证书uniapp

地址&#xff1a; https://ask.dcloud.net.cn/article/35777 // 使用keytool -genkey命令生成证书&#xff1a; 官网&#xff1a; keytool -genkey -alias testalias -keyalg RSA -keysize 2048 -validity 36500 -keystore test.keystore ----------------------------------…

SpringBoot基于Redis+WebSocket 实现账号单设备登录.

引言 在现代应用中&#xff0c;一个账号在多个设备上的同时登录可能带来安全隐患。为了解决这个问题&#xff0c;许多应用实现了单设备登录&#xff0c;确保同一个用户只能在一个设备上登录。当用户在新的设备上登录时&#xff0c;旧设备会被强制下线。 本文将介绍如何使用 Spr…

【MySQL 进阶之路】事务并发情况分析

MySQL事务并发控制分析笔记 在数据库系统中&#xff0c;事务并发控制至关重要&#xff0c;能够确保多个事务并发执行时的数据一致性、隔离性和正确性。MySQL通过不同的锁机制控制并发操作&#xff0c;以确保事务的隔离性。以下是对事务A和事务B并发行为的详细分析&#xff0c;…

如何在小米平板5上运行 deepin 23 ?

deepin 23 加入了 ARM64 支持&#xff0c;这里尝试将 deepin 系统刷入平板中&#xff0c;平常使用中&#xff0c;带个笔记本电脑有时候也会嫌比较麻烦&#xff0c;把 Linux 系统刷入平板中既满足了使用需要&#xff0c;又满足了轻便的需求。为什么不使用 Termux &#xff1f;虽…

华为HarmonyOS 快速构建各种文本识别应用 -- 通用文字识别

适用场景 通用文字识别&#xff0c;是通过拍照、扫描等光学输入方式&#xff0c;将各种票据、卡证、表格、报刊、书籍等印刷品文字转化为图像信息&#xff0c;再利用文字识别技术将图像信息转化为计算机等设备可以使用的字符信息的技术。 可以对文档翻拍、街景翻拍等图片进行…

【系统架构核心服务设计】使用 Redis ZSET 实现排行榜服务

目录 一、排行榜的应用场景 二、排行榜技术的特点 三、使用Redis ZSET实现排行榜 3.1 引入依赖 3.2 配置Redis连接 3.3 创建实体类&#xff08;可选&#xff09; 3.4 编写 Redis 操作服务层 3.5 编写控制器层 3.6 测试 3.6.1 测试 addMovieScore 接口 3.6.2 测试 g…

【Docker】如何在Docker中配置防火墙规则?

Docker本身并不直接管理防火墙规则&#xff1b;它依赖于主机系统的防火墙设置。不过&#xff0c;Docker在启动容器时会自动配置一些iptables规则来管理容器网络流量。如果你需要更细粒度地控制进出容器的流量&#xff0c;你需要在主机系统上配置防火墙规则。以下是如何在Linux主…

java+ssm+mysql美妆论坛

项目介绍&#xff1a; 使用javassmmysql开发的美妆论坛&#xff0c;系统包含超级管理员&#xff0c;系统管理员、用户角色&#xff0c;功能如下&#xff1a; 用户&#xff1a;主要是前台功能使用&#xff0c;包括注册、登录&#xff1b;查看论坛板块和板块下帖子&#xff1b;…

【MFC】vs2019中使用sqlite3完成学生管理系统

目录 效果图list Contral 控件的简单使用使用sqlite3 效果图 使用sqlite3完成简单的数据库操作。 list Contral 控件的简单使用 本章只介绍基本应用 添加表头&#xff1a;语法&#xff1a; int InsertColumn(int nCol, LPCTSTR lpszColumnHeading, int nFormat LVCFMT_LEFT…

Java设计模式 —— 【创建型模式】建造者模式详解

文章目录 一、建造者模式二、案例实现三、优缺点四、模式拓展五、对比1、工厂方法模式VS建造者模式2、抽象工厂模式VS建造者模式 一、建造者模式 建造者模式&#xff08;Builder Pattern&#xff09; 又叫生成器模式&#xff0c;是一种对象构建模式。它可以将复杂对象的建造过…

单链表(C语言版本)

前提 不探讨头结点空链表可以插入和查找&#xff0c;不可删除一般不选择phead移动&#xff0c;定义一个新结点把phead赋给他&#xff0c;移动新结点即可单链表不适合在前面和后面插入或删除&#xff0c;适合在后面插入删除 头插 void SLPushFront(SLTNode** pphead, SLTDataTy…