【安卓15】分析录屏应用与原生投放和屏幕共享的联系,停止共享后录屏结束

一、问题概要

假如你有一个录屏应用,开启录屏后,安卓原生状态栏会显示一个红色的计时按钮,点击这个按钮弹弹出一个窗口可选择关闭或者停止共享,停止共享后录屏实际上已经停止输出数据了,你的录屏应用需要监听到这个事件进行自己的逻辑处理,除了这个按钮,在下拉状态栏的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();

在了解了这个录制基本原理之后,就可以分析原因了,分步骤来讲大概有如下几点原因

1Surface 的依赖关系
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() {
                    @Override
                    public void onResumed() {
                        //监听虚拟显示完成后再开启录像机,避免录屏前几帧黑屏
                        super.onResumed();
                        Log.i(TAG, "VirtualDisplay onResumed");
                        mIsVirtualDisplayReady = true;
                        startMediaRecorder();
                    }

                    @Override
                    public 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

    @Override
    public 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 broadcast
                    String 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那边模块化注入的
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值