一、问题概要
假如你有一个录屏应用,开启录屏后,安卓原生状态栏会显示一个红色的计时按钮,点击这个按钮弹弹出一个窗口可选择关闭或者停止共享,停止共享后录屏实际上已经停止输出数据了,你的录屏应用需要监听到这个事件进行自己的逻辑处理,除了这个按钮,在下拉状态栏的tile里面也有一个投放开关,点击关闭投放后也会结束录屏。
二、原因分析
录屏应用开启时,会先创建一个虚拟显示,然后利用录像机MediaRecorder开始录制,这里的虚拟显示场景需要用到两个关键的“人物”,一个是MediaProjection,它提供了权限管理和屏幕内容捕获的功能,一个是VirtualDisplay ,它一个虚拟的显示设备,可以将屏幕内容输出到指定的 Surface.一个简单的虚拟显示创建方法如下:
// 创建 MediaProjection 实例
MediaProjection mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);// 创建 VirtualDisplay
virtualDisplay = mediaProjection.createVirtualDisplay("ScreenCapture",screenWidth,screenHeight,screenDensity,DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,surface, // Surface 对象null,null
);
虚拟显示创建了之后,就需要一个“录像机”去记录数据,MediaRecorder的使用一般如下
MediaRecorder mediaRecorder = new MediaRecorder();
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); // 使用 Surface 作为视频源
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); // 设置输出格式
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); // 设置视频编码器
mediaRecorder.setVideoSize(screenWidth, screenHeight); // 设置视频分辨率
mediaRecorder.setVideoFrameRate(30); // 设置帧率
mediaRecorder.setOutputFile(outputFilePath); // 设置输出文件路径
mediaRecorder.prepare(); // 准备录制
mediaRecorder.start(); // 开始录制
这个录像机需要一个输入源,所以MediaRecorder 使用 Surface 作为输入源,VirtualDisplay 的输出会被绑定到这个 Surface 上,
// 创建 Surface 并绑定到 MediaRecorder
Surface surface = mediaRecorder.getSurface();// 将 Surface 传递给 VirtualDisplay
virtualDisplay = mediaProjection.createVirtualDisplay("ScreenCapture",screenWidth,screenHeight,screenDensity,DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,surface,null,null
);
停止录制时需要调用 stop() 方法,并释放资源
mediaRecorder.stop();
mediaRecorder.release();
virtualDisplay.release();
mediaProjection.stop();
在了解了这个录制基本原理之后,就可以分析原因了,分步骤来讲大概有如下几点原因
1、Surface 的依赖关系
MediaRecorder 使用 Surface 作为输入源(通过 setVideoSource(MediaRecorder.VideoSource.SURFACE) 配置)。
VirtualDisplay 的输出被绑定到这个 Surface 上。
当 VirtualDisplay 被释放或停止时,它不再向 Surface 输出数据,导致 MediaRecorder 没有新的帧数据可以录制。
关键点:
MediaRecorder 不会主动从其他地方获取数据,它完全依赖于绑定的 Surface 提供的帧数据。
如果 Surface 停止接收数据,MediaRecorder 就会“无米下锅”
2. VirtualDisplay 的生命周期
VirtualDisplay 的生命周期与 MediaProjection 紧密相关。
当调用 virtualDisplay.release() 或 mediaProjection.stop() 时,VirtualDisplay 会被销毁,停止向 Surface 输出数据。
此时,即使 MediaRecorder 仍在运行,也无法继续录制内容。
3. MediaRecorder 的行为
MediaRecorder 在录制过程中,会持续从绑定的 Surface 中拉取帧数据。
如果 Surface 没有新数据提供,MediaRecorder 会等待一段时间后抛出异常或直接停止录制。
这是因为 MediaRecorder 内部没有缓存机制来处理长时间无数据的情况。
知道原因之后就大概知道怎么改了,最简单的办法就是在停止共享后发送广播通知应用结束录屏。
三、修改源码
1、录屏应用本身添加虚拟显示回调
这是第一种最简单的方法就是在创建虚拟显示的时候监听回调,在onStop处理自己的逻辑
mVirtualDisplay = mediaProjection.createVirtualDisplay(TAG, width, height, mScreenDensity,DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mediaRecorder.getSurface(), new VirtualDisplay.Callback() {@Overridepublic void onResumed() {//监听虚拟显示完成后再开启录像机,避免录屏前几帧黑屏super.onResumed();Log.i(TAG, "VirtualDisplay onResumed");mIsVirtualDisplayReady = true;startMediaRecorder();}@Overridepublic void onStopped() {//在这里实现自己的逻辑,比如释放资源保存录屏文件等等super.onStopped();Log.i(TAG, "VirtualDisplay onStopped");mIsVirtualDisplayReady = false;}}, null);
这种方式可以不改安卓framework,好处是实现简单,缺点是如果在多个地方使用了这个虚拟显示,会有意想不到的情况。我这里使用的是第二种方式
2、修改framework在点击停止共享时发送广播通知录屏结束
1、下拉状态栏tile的投屏开关修改如下
在stopCasting方法里面发送广播,源码路径:
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java
@Overridepublic void stopCasting(CastDevice device) {final boolean isProjection = device.getTag() instanceof MediaProjectionInfo;mLogger.logStopCasting(isProjection);if (isProjection) {final MediaProjectionInfo projection = (MediaProjectionInfo) device.getTag();if (Objects.equals(mProjectionManager.getActiveProjectionInfo(), projection)) {if("你的应用包名".equals(mProjection.getPackageName())){Log.d(TAG, "send broadcast to stop screenrecorder");//send broadcastString RECEIVING_PACKAGE = "包名";Intent intent = new Intent("广播action");intent.setPackage(RECEIVING_PACKAGE);mContext.sendBroadcast(intent);} mProjectionManager.stopActiveProjection();} else {mLogger.logStopCastingNoProjection(projection);}} else {mLogger.logStopCastingMediaRouter();mMediaRouter.getFallbackRoute().select();}}//别忘了导入
import android.content.Intent;
import android.util.Log;
2、左上角红色的计时按钮结束录屏修改
源码路径:
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
这个代码时kt代码,跟踪源码时需要懂一点它的设计模式,这里给出具体修改如下
/** Stops the currently active projection. */private fun stopProjectingFromDialog() {logger.log(TAG, LogLevel.INFO, {}, { "Stop sharing requested from dialog" })chipTransitionHelper.onActivityStoppedFromDialog()mediaProjectionChipInteractor.stopProjecting()sendStopScreenRecordingBroadcast()}private fun sendStopScreenRecordingBroadcast() {val receivingPackage = "包名"val intent = Intent("广播action").apply {setPackage(receivingPackage)}context.sendBroadcast(intent)Log.d(TAG, "send broadcast to stop screenrecorder")}
追踪源码首先是根据弹窗的按钮文件查找到最终在这里引用
warpStopAction的实现如下
里面的stopAction是从model那边模块化注入的