这是一个不太常见的需求,因为博主本人所在公司是做教育相关产品的,故而有此需求,通过录制学生端pad屏幕,进行屏幕广播,本文主要介绍其中需要注意的一些关键点,详细代码可以在文末的 Github 仓库中查看。
不同于普通的动态权限申请,屏幕录制的权限在每次使用 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)
}
}
在 onActivityResult 回调中保存 resultCode 与 data,这两个参数将会在后续用于实例化 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!!
}
}
}
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()
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
}
其中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 即可。
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
}
}
}
}
大致流程如下:
outputBufferIndexoutputBufferDemo代码仓库地址: junerver/TestCaptureAndRecord