Android 悬浮窗、屏幕推流与录屏截屏

项目需求声明

  1. 打开应用后直接显示桌面悬浮窗
  2. 点击悬浮按钮开始屏幕推流

1. 打开应用后直接显示桌面悬浮窗

针对此项目需求,需要考虑的是,我打开app后应启动Launcher Activity,然后将应用栈放到后台。需要注意的是,如果时平常的Activity,那么会闪一下屏幕,这样的效果真的让人不能接受。以下是编码思路:

1). 解决Launcher Activity闪屏问题

  1. 不给Launcher Activity绑定布局
  2. 给Launcher Activity设置Theme为透明背景:
android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen"
  1. 启动Activity后将栈移至后台运行
moveTaskToBack(true)

2). 添加悬浮窗

  1. 添加权限

AndroidManifest中添加权限:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

在Activity中检查悬浮窗权限

	/**
	 *	检查权限
	 */
    public static boolean hasPermissionOnActivityResult(Context context) {
        if (Build.VERSION.SDK_INT == 26) {
            return hasPermissionForO(context);
        } else {
            return Build.VERSION.SDK_INT >= 23 ? Settings.canDrawOverlays(context) : hasPermissionBelowMarshmallow(context);
        }
    }

    private static boolean hasPermissionBelowMarshmallow(Context context) {
        try {
            AppOpsManager manager = (AppOpsManager) context.getSystemService("appops");
            Method dispatchMethod = AppOpsManager.class.getMethod("checkOp", Integer.TYPE, Integer.TYPE, String.class);
            return 0 == (Integer) dispatchMethod.invoke(manager, 24, Binder.getCallingUid(), context.getApplicationContext().getPackageName());
        } catch (Exception var3) {
            return false;
        }
    }
    
    @RequiresApi(api = 23)
    private static boolean hasPermissionForO(Context context) {
        try {
            WindowManager mgr = (WindowManager) context.getSystemService("window");
            if (mgr == null) {
                return false;
            } else {
                View viewToAdd = new View(context);
                WindowManager.LayoutParams params = new WindowManager.LayoutParams(0, 0, Build.VERSION.SDK_INT >= 26 ? 2038 : 2003, 24, -2);
                viewToAdd.setLayoutParams(params);
                mgr.addView(viewToAdd, params);
                mgr.removeView(viewToAdd);
                return true;
            }
        } catch (Exception var4) {
            Log.e("FHApplication", "hasPermissionForO e:" + var4.toString());
            return false;
        }
    }

如果没有权限,则要申请权限(跳转到悬浮窗权限设置页):

    if (VERSION.SDK_INT >= 23) {
        this.requestAlertWindowPermission();
    }

    @RequiresApi(
        api = 23
    )
    private void requestAlertWindowPermission() {
        Intent intent = new Intent("android.settings.action.MANAGE_OVERLAY_PERMISSION");
        intent.setData(Uri.parse("package:" + this.getPackageName()));
        this.startActivityForResult(intent, requestCode);
    }

返回到Activity时,在onActivityResult方法中添加如下代码:

        if (requestCode == requestCode) {
            if (PermissionUtil.hasPermissionOnActivityResult(this)) {
            	//成功申请到权限
                mPermissionListener.onSuccess();
            } else {
            	//没有申请到权限
                mPermissionListener.onFail();
            }
        }
  1. 添加悬浮窗
    我使用了开源库,随便找一个就行。已经有的轮子,如非必要,不建议费时费力重新造(但请保持一颗好奇心,可以不造,但总得把实现方法搞清楚得差不多)。

2. 屏幕推流

Android 5.0开始Android开放了录屏的方法,可以直接使用。

	mMediaProjectionManager = 
		getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
	val captureIntent = mMediaProjectionManager!!.createScreenCaptureIntent()
	startActivityForResult(captureIntent, REQUEST_CODE_A)

回调

	override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
	    super.onActivityResult(requestCode, resultCode, data)
	    try {
	        val mediaProjection =
	            mMediaProjectionManager!!.getMediaProjection(resultCode, data!!)
	        if (mediaProjection == null) {
	            Toast.makeText(this, "程序发生错误:MediaProjection@1", Toast.LENGTH_SHORT).show()
	            return
	        }
	        mScreenRecord = ScreenRecord(this, mediaProjection)
	        mScreenRecord.start()
	    } catch (e: Exception) {
	        e.printStackTrace()
	    }
	}

ScreenRecord是个线程,在里面继续处理:

    mVirtualDisplay = mMediaProjection.createVirtualDisplay(
        "ScreenRecord",
        width, height, dpi,
        DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
        //编码器创建的Surface对象,API>=18时支持:codec.createInputSurface()
        getVideoEncoder()!!.inputSurface,	
        null,
        null
    )

推流过程不是重点,略过。。。

3. 补充

mMediaProjection.createVirtualDisplay的调用在Android 8.0及以上必须要在前台线程中进行,否则会报异常。需声明

        <service
            android:name=".view.PushStreamService"
            android:priority="1000"
            android:exported="false"
            android:enabled="true"
            android:foregroundServiceType="mediaProjection">
        </service>

重点时最后一行。

关于录屏和截屏

1). 录屏

Android 提供了MediaRecorder来帮助录屏(不仅仅支持录屏,还可以录制摄像头视频),可以同时录制音视频。网上的教程很多,不再赘述。

2). 截屏

Android提供了ImageReader可以使用。

        ImageReader imageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 2);
        imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable (ImageReader reader){
                Image image = reader.acquireNextImage();
				//这里做一些其他处理
				...

				//最后必须close掉拿到的image
                image.close();
            }
        },new Handler(Looper.getMainLooper()));

以上可以定义一个ImageReader来接收数据,但还需将ImageReader的Surface交给VirtualDisplay:

    mVirtualDisplay = mMediaProjection.createVirtualDisplay(
        "ScreenRecord",
        width, height, dpi,
        DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
        imageReader.getSurface(),		//这里是重点
        null,
        null
    )

备注:

  1. 我在为ImageReader设置监听器时,Handler传入了主线程的Handler,这样可以正常接收到数据,但我如果传入了子线程的Handler,就无法接收到任何数据了。原因不详,如有朋友知道原因,请不吝赐教,下方留言👇,蟹蟹~
  2. 我在公司的Android 5.1测试机上测试,ImageReader创建时的第三个参数format传入ImageFormat.JPEG后不会接收到任何数据。然后尝试使用ImageFormat.YUV_420_888后直接报异常,异常的大致意思是i420(ImageFormat.YUV_420_888)的Format值与0x1不匹配,于是第三个参数改为0x1,可以拿到RGBA的byte数组。由此可以使用该数组还原一张完整的图像。至于为什么JPEG不回调、YUV_420_888也不回调、只有0x1才回调但是该值又不在官方给出的format的范围内,如有朋友知道原因,也请不吝赐教👇!!谢谢!!
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值