这是一个不太常见的需求,因为博主本人所在公司是做教育相关产品的,故而有此需求,通过录制学生端pad屏幕,进行屏幕广播,本文主要介绍其中需要注意的一些关键点,详细代码可以在文末的 Github 仓库中查看。
1. 权限申请
不同于普通的动态权限申请,屏幕录制的权限在每次使用 App 时都需要重新申请一次。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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) } }
|
在 onActivityResult
回调中保存 resultCode
与 data
,这两个参数将会在后续用于实例化 MediaProjection
对象
1 2 3 4 5 6 7 8 9 10
| override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == Utils.REQUEST_MEDIA_PROJECTION) { if (resultCode == Activity.RESULT_OK) { GlobalConfig.intent = data!! } } }
|
1 2 3 4 5 6 7
| 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()
|
3. 创建虚拟显示器 VirtualDisplay
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| GlobalConfig.intent?.let { (this.getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager).getMediaProjection( AppCompatActivity.RESULT_OK, it ).apply { 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 }
|
其中createVirtualDisplay
参数有如下几种:
1 2 3 4 5
| 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. 开始录屏编码
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
| 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") val sps = ByteArray(byteBuffer?.capacity()!!) byteBuffer.get(sps) byteBuffer = format.getByteBuffer("csd-1") val pps = ByteArray(byteBuffer?.capacity()!!) byteBuffer?.get(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) mMediaCodecEncoder.releaseOutputBuffer(outputBufferIndex, false) LogUtils.d("拿到录屏流数据:${chunk.size}") if (chunk.isNotEmpty()) { if ((chunk[4] and 0x1f).toInt() == 5) { LogUtils.d("关键帧数据处理") lifecycleScope.launch { h264SpsPpsData?.let { data -> sH264DataFlow.emit(data) sOnReceiveH264DataCallback?.onReceiveH264Data(data) } } } lifecycleScope.launch { sH264DataFlow.emit(chunk) } sOnReceiveH264DataCallback?.onReceiveH264Data(chunk) } } } if (mBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { LogUtils.d("视频结束") break } } } }
|
大致流程如下:
- 从编码器的缓冲队列获取缓冲数据索引
outputBufferIndex
- 索引 > 0 时,从编码器获取指定索引的缓冲
outputBuffer
- 根据 BufferInfo,从缓冲的中获取帧画面数据
- 用 Flow 或者 回调发送数据
写在最后
Demo代码仓库地址: junerver/TestCaptureAndRecord