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

597 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情

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

虽然本文大部分篇幅都是代码,但几乎每行代码我都附上了详细的介绍,请务必仔细阅读代码,如有谬误,还请斧正。

1. 权限申请

不同于普通的动态权限申请,屏幕录制的权限在每次使用 App 时都需要重新申请一次。直接附上工具类,方便大家使用

object Utils {
    const val REQUEST_MEDIA_PROJECTION = 9898

    /**
     * 申请录屏权限
     */
    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)
    }
}

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!!
            }
        }
    }

2. 创建 MediaCodec 编码器

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

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
                val virtualDisplay = this.createVirtualDisplay(
                    "MainScreen", 720, 1280, dpi,
                    DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null
                )
            }
        } ?: run {
            LogUtils.e("RecordService intent is null")
            return
        }

其中createVirtualDisplay参数有如下几种:

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

一般来说用 VIRTUAL_DISPLAY_FLAG_PUBLIC 即可。

4. 开始录屏编码

    private fun startRecord() {
        isRun = true
        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. 从编码器的缓冲队列获取缓冲数据索引 outputBufferIndex
  2. 索引 > 0 时,从编码器获取指定索引的缓冲 outputBuffer
  3. 根据 BufferInfo,从缓冲的中获取帧画面数据
  4. 用 Flow 或者回调函数发送数据
  5. 根据具体业务决定如何处理h264的流数据

写在最后

Demo代码仓库地址: junerver/TestCaptureAndRecord