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

前言

我们前面基于摄像机的图像采集以及编解码已经完成了,那么接下来计划后面的三篇博文分别实现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类,并添加如下代码。

kotlin 复制代码
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则是编码数据返回的回调接口。

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

kotlin 复制代码
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貌似不需要,哈哈,留着吧。

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

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()。

kotlin 复制代码
    fun stop(){
        ScreenEncoderService.stop()
    }
    

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

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

kotlin 复制代码
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文章进行了解。

kotlin 复制代码
  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就会启动为前台服务,否则就启动为后台服务。

kotlin 复制代码
    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()。

kotlin 复制代码
    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并返回。

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

kotlin 复制代码
    private fun stopScreenCapture(){
        videoEncoder?.stop()
        mediaProjection?.stop()
        virtualDisplay?.release()
    }

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

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

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

xml 复制代码
    <service android:name="com.zlgspace.andcodec.codec.ScreenEncoderService"
        android:foregroundServiceType="mediaProjection"
        />

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

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

使用ScreenCapture

kotlin 复制代码
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从应用到系统,多少都懂一些,对于安卓后续还会持续更新更多更高质量的博文,对自己的技能加强的的同时,也希望能够帮到有需要的同学。

相关推荐
openinstall全渠道统计18 分钟前
免填邀请码工具:赋能六大核心场景,重构App增长新模型
android·ios·harmonyos
双鱼大猫40 分钟前
一句话说透Android里面的ServiceManager的注册服务
android
双鱼大猫1 小时前
一句话说透Android里面的View的绘制流程和实现原理
android
双鱼大猫2 小时前
一句话说透Android里面的Window的内部机制
android
双鱼大猫2 小时前
一句话说透Android里面的为什么要设计Window?
android
双鱼大猫2 小时前
一句话说透Android里面的主线程创建时机,frameworks层面分析
android
苏金标3 小时前
android 快速定位当前页面
android
雾里看山6 小时前
【MySQL】内置函数
android·数据库·mysql
风浅月明6 小时前
[Android]页面间传递model列表
android
法迪6 小时前
Android自带的省电模式主要做什么呢?
android·功耗