• 如何在 Android 中录制屏幕内容,并以H.264数据流形式发送(屏幕广播)


    如何在 Android 中录制屏幕内容,并以H.264数据流形式发送(屏幕广播)

    这是一个不太常见的需求,因为博主本人所在公司是做教育相关产品的,故而有此需求,通过录制学生端pad屏幕,进行屏幕广播,本文主要介绍其中需要注意的一些关键点,详细代码可以在文末的 Github 仓库中查看。

    1. 权限申请

    不同于普通的动态权限申请,屏幕录制的权限在每次使用 App 时都需要重新申请一次。

    object Utils {
        const val REQUEST_MEDIA_PROJECTION = 1
    
        /**
         * 申请录屏权限
         */
        fun createPermission(activity: Activity) {
            val mediaProjectionManager =
                activity.application.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
            val intent = mediaProjectionManager.createScreenCaptureIntent()
            activity.startActivityForResult(intent, REQUEST_MEDIA_PROJECTION)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    onActivityResult 回调中保存 resultCodedata,这两个参数将会在后续用于实例化 MediaProjection 对象

        override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
            super.onActivityResult(requestCode, resultCode, data)
            //授权成功,保存intent,在后续需要使用该intent申请相关屏幕录制的对象
            if (requestCode == Utils.REQUEST_MEDIA_PROJECTION) {
                if (resultCode == Activity.RESULT_OK) {
                    //保存intent
                    GlobalConfig.intent = data!!
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2. 创建 MediaCodec 编码器

    mMediaCodecEncoder = MediaCodec.createEncoderByType("video/avc") // H.264编码格式
    //配置编码器
    val mediaFormat = Utils.getMediaFormat()
    mMediaCodecEncoder.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    //该surface用于下一步中创建VirtualDisplay
    surface = mMediaCodecEncoder.createInputSurface()
    mMediaCodecEncoder.start()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3. 创建虚拟显示器 VirtualDisplay

    GlobalConfig.intent?.let {
                (this.getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager).getMediaProjection(
                    AppCompatActivity.RESULT_OK,
                    it
                ).apply {
                    //使用MediaProjection创建VirtualDisplay
                    val dpi = resources.displayMetrics.densityDpi
                    LogUtils.d("dpi:$dpi")
                    val virtualDisplay = this.createVirtualDisplay(
                        "MainScreen", 720, 1280, dpi,
                        DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null
                    )
                    LogUtils.d("创建成功: ${virtualDisplay?.display?.width} x ${virtualDisplay?.display?.height}")
                }
            } ?: run {
                LogUtils.e("RecordService intent is null")
                return
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    其中createVirtualDisplay参数有如下几种:

    VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR:当没有内容显示时,允许将内容镜像到专用显示器上。
    VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY:仅显示此屏幕的内容,不镜像显示其他屏幕的内容。
    VIRTUAL_DISPLAY_FLAG_PRESENTATION:创建演示文稿的屏幕。
    VIRTUAL_DISPLAY_FLAG_PUBLIC:创建公开的屏幕。
    VIRTUAL_DISPLAY_FLAG_SECURE:创建一个安全的屏幕
    
    • 1
    • 2
    • 3
    • 4
    • 5

    一般来说用 VIRTUAL_DISPLAY_FLAG_PUBLIC 即可。

    4. 开始录屏编码

        private fun startRecord() {
            isRun = true
            //防断流黑屏方法1:正常发送I帧 P帧,但是每隔1秒强制请求一次关键帧 I帧,
    //        setInterval(1000,1000) {
    //            if (isRun) {
    //                val params = Bundle()
    //                params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0)
    //                mMediaCodecEncoder.setParameters(params)
    //            }
    //        }
            GlobalThreadPools.instance?.execute {
                val mBufferInfo = MediaCodec.BufferInfo()
                while (isRun) {
                    //输出缓冲区出列,返回缓冲的索引
                    val outputBufferIndex = mMediaCodecEncoder.dequeueOutputBuffer(
                        mBufferInfo,
                        -1 //超时时间,负数表示无限等待
                    )
                    if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                        LogUtils.d("输出格式变化")
                        val format: MediaFormat = mMediaCodecEncoder.outputFormat
                        var byteBuffer = format.getByteBuffer("csd-0")
                        //根据缓冲区的容量创建一个字节数组,用于存储视频编码器的sps数据
                        val sps = ByteArray(byteBuffer?.capacity()!!)
                        byteBuffer.get(sps)
                        byteBuffer = format.getByteBuffer("csd-1")
                        //根据缓冲区的容量创建一个字节数组,用于存储视频编码器的pps数据
                        val pps = ByteArray(byteBuffer?.capacity()!!)
                        byteBuffer?.get(pps)
                        //拼接sps和pps
                        val spsPps = ByteArray(sps.size + pps.size)
                        System.arraycopy(sps, 0, spsPps, 0, sps.size)
                        System.arraycopy(pps, 0, spsPps, sps.size, pps.size)
                        h264SpsPpsData = spsPps
                    }
                    //索引为正数,表示缓冲区存在,可以获取缓冲区数据
                    if (outputBufferIndex >= 0) {
                        //传入索引值,获取缓冲区对象
                        val outputBuffer = mMediaCodecEncoder.getOutputBuffer(outputBufferIndex)
                        outputBuffer?.apply {
                            //确定该帧的起止位置
                            position(mBufferInfo.offset)
                            limit(mBufferInfo.offset + mBufferInfo.size)
                            //根据该帧的大小创建字节数组,并从缓冲区获取数据
                            val chunk = ByteArray(mBufferInfo.size)
                            get(chunk)
                            //获取帧画面数据完毕,调用编码器函数释放缓冲区,因为我们是录制屏幕,不需要渲染到surface,所以参数2传递false
                            mMediaCodecEncoder.releaseOutputBuffer(outputBufferIndex, false)
                            LogUtils.d("拿到录屏流数据:${chunk.size}")
                            //将流数据发送
                            if (chunk.isNotEmpty()) {
                                //防断流黑屏方法2:融合sps和pps,配合format中的每隔1秒请求一次关键帧 I帧
                                if ((chunk[4] and 0x1f).toInt() == 5) {
                                    LogUtils.d("关键帧数据处理")
                                    lifecycleScope.launch {
                                        //发送sps和pps数据,这样可以避免掉线重连时因为没有sps和pps数据而导致黑屏
                                        h264SpsPpsData?.let { data ->
                                            sH264DataFlow.emit(data)
                                            sOnReceiveH264DataCallback?.onReceiveH264Data(data)
                                        }
                                    }
                                }
                                //flow 与 回调各给一份 用kotlin的就用flow拿数据,用java就从回调拿数据
                                lifecycleScope.launch {
                                    sH264DataFlow.emit(chunk)
                                }
                                sOnReceiveH264DataCallback?.onReceiveH264Data(chunk)
                            }
                        }
                    }
                    if (mBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
                        LogUtils.d("视频结束")
                        break
                    }
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    大致流程如下:

    1. 从编码器的缓冲队列获取缓冲数据索引 outputBufferIndex
    2. 索引 > 0 时,从编码器获取指定索引的缓冲 outputBuffer
    3. 根据 BufferInfo,从缓冲的中获取帧画面数据
    4. 用 Flow 或者 回调发送数据

    写在最后

    Demo代码仓库地址: junerver/TestCaptureAndRecord

  • 相关阅读:
    pix2pix-论文阅读笔记
    微信小程序-云开发 起步
    MySQL知识笔记——初级基础(实施工程师和DBA工作笔记)
    CSS学习笔记
    代码随想录算法训练营19期总结
    使用python制作epub
    C++---map和set的使用
    【hisi】——hisi开发相关文档
    I.MX RT1176笔记(9)-- 程序异常追踪(CmBacktrace 和 segger rtt)
    html5 主体标签
  • 原文地址:https://blog.csdn.net/u011133887/article/details/125555304