首页 > 其他分享 >Android 屏幕采集并编码为H.264

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

时间:2024-12-07 16:29:05浏览次数:4  
标签:编码 H.264 Int var fun Android data 屏幕 ScreenCapture

前言

我们前面基于摄像机的图像采集以及编解码已经完成了,那么接下来计划后面的三篇博文分别实现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从应用到系统,多少都懂一些,对于安卓后续还会持续更新更多更高质量的博文,对自己的技能加强的的同时,也希望能够帮到有需要的同学。

标签:编码,H.264,Int,var,fun,Android,data,屏幕,ScreenCapture
From: https://blog.csdn.net/ZLGSPACE/article/details/144299437

相关文章

  • Android——Click事件实现方式
    Android观察者模式(ObserverDesignPattern):在对象之间定义⼀个⼀对多的依赖,当⼀个对象状态改变的时候,所有依赖的对象都会得到通知并⾃动更新。说⼈话:也叫发布订阅模式,能够很好的解耦一个对象改变,⾃动改变另⼀个对象这种情况。①、Subject被观察者定义被观察者必......
  • Android 移动应用开发---乡村民宿(2)Banner 轮播图,并实现跳转对应界面
    一,添加插件1,去东软职业技能在线下载插件2,找到需要的插件,并导入到libs里,只需要在build.gradle里面刷新一下就行了刷新就是把第三个步骤先注释点击syncNow,再取消注释再点一下syncNow这样就刷新成功了,我们就可以正常使用Banner插件了一般常用的插件有这些如果没有步骤三的......
  • Android 流畅度评估及卡顿定位、优化
    原文见:在路上的博客:Android流畅度评估及卡顿优化导言:本文主要是关于Android流畅度和卡顿优化的全方位介绍,算是对2020部分工作的总结。全文主要包括:1、渲染原理和流畅概念2、卡顿的标准3、卡顿评估和判断4、卡顿定位工具和高效定位方法5、卡顿优化建议1、渲染和流......
  • 【花雕学编程】Arduino动手做(229)---带编码器350w机器人轮毂马达6.5 英寸电动轮毂伺服
    37款传感器与执行器的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的,这里准备逐一动手尝试系列实验,不管成功(程序走通)与否,都会记录下来——小小的......
  • 基于方块编码的图像压缩matlab仿真,带GUI界面
    1.算法运行效果图预览(完整程序运行后无水印) 下图是随着方块大小的变化,图像的压缩率以及对应的图像质量指标PSNR的变化趋势曲线。 2.算法运行软件版本matlab2022a 3.部分核心程序(完整版代码包含详细中文注释和操作步骤视频)figure;subplot(121);plot(sets,tr......
  • 基于Huffman编码的GPS定位数据无损压缩算法
    目录一、引言二、霍夫曼编码三、经典Huffman编码四、适应性Huffman编码五、GPS定位数据压缩提示:文末附定位数据压缩工具和源码一、引言        车载监控系统中,车载终端需要获取GPS信号(经度、纬度、速度、方向等)实时上传至监控中心,监控中心按通信协议将收......
  • 将 C++程序移植到 Android 平台
    将C++程序移植到Android平台 将C++程序移植到Android平台需要多个步骤,涉及AndroidNDK(NativeDevelopmentKit)和AndroidStudio。下面是详细的步骤指导:1.环境准备安装AndroidStudio:确保你的开发环境中安装了AndroidStudio,这是开发Android应用的主流IDE。......
  • UNICODE编码特殊符号
    平时编程中会用到各种各样的符号,有时不想去找图片,一些简单的符号,可以直接通过Unicode字符来获取: RoundButton{text:"\u2713"//UnicodeCharacter'CHECKMARK'onClicked:textArea.readOnly=true}以下是常用的Unicode特殊符号,建议收藏:⇠  箭头......
  • 基于 C# 编写的 Visual Studio 文件编码显示与修改扩展插件
    前言在软件开发过程中,尤其是在处理跨平台或来自不同来源的项目时,文件的编码格式往往会成为一个不可忽视的问题。不同的操作系统、编程语言和编辑器可能对文件编码有不同的支持和默认设置,这可能导致在打开一个文件时出现乱码、编译错误或运行时问题。今天大姚给大家分享一款基于C......
  • Android studio出现uplicate class kotlin.time.jdk8.DurationConversionsJDK8Kt foun
    Android编译KotlinSDK依赖包类重复冲突问题1、问题问题:gradle同步可以成功,但是编译运行时,出现以下异常。2、分析取以上内容中的一条进行分析可以看到在模块org.jetbrains.kotlin:kotlin-stdlib:1.8.20和org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21中存在重复的......