一、可实现方案

VCAM + Magisk + Lsposed
android_virtual_cam 基于 Xposed 的虚拟摄像头,采用 hook 方式去勾住系统 Camera 相关 API,然后读取本地指定视频文件进行替换
Magisk 用于 root 系统,刷入系统 boot.img,然后再安装 Riru 和 LSPosed 模块

二、效果图

三、实现过程和难点
难点一、Magisk 预装并烧写完成替换 boot
熟悉 Magisk 的伙计都知道,要想成功刷入修补后 boot.img ,需要将设备解锁才行。
以前我的认知都是解锁需要重启进入 BootLoader 模式配合音量键操作才能成功,这样如果
批量生产这种需要默认解锁的特殊设备岂不是要麻烦到家了。后来仔细阅读了 lk 源码,发现
MTK 已经考虑到这种情况了,提供两特殊宏 MTK_BUILD_DEFAULT_UNLOCK 和 MTK_SECURITY_SW_SUPPORT
将其打开后再次编译烧写设备已经是解锁状态,这样就省去了手动解锁麻烦。
配置路径 vendor/mediatek/proprietary/bootable/bootloader/lk/project/xxxx.mk
vendor\mediatek\proprietary\bootable\bootloader\lk\platform\common\boot\avb20\load_vfy_boot.c
int load_vfy_boot(uint32_t bootimg_type, uint32_t addr)
{
#ifdef MTK_SEC_FASTBOOT_UNLOCK_SUPPORT
#if defined(MTK_DM_VERITY_OFF) || defined(MTK_BUILD_DEFAULT_UNLOCK) || !defined(MTK_SECURITY_SW_SUPPORT)
/* This feature is for test purpose only.
* we unlock device without wiping userdata, which should not
* happen for MP product. We do this because after userdata
* has been wiped, Android will fail to mount userdata and reboot
* to recovery to format userdata, and then reboot. If we always
* wipe userdata in boot process, Android will never get a properly
* formatted userdata.
*/
#ifdef MTK_SECURITY_SW_SUPPORT
/* unlock device */
if (sec_set_device_lock(0) != 0) {
ret = ERR_AVB_UNLOCK_DEVICE_FAILED;
goto end;
}
#endif
解锁后开机时间明显变长了且主屏幕有警告文字提示,这很明显是需要我们避免的。
vendor/mediatek/proprietary/bootable/bootloader/lk/platform/common/boot/vboot_state.c
+ /*video_set_cursor(video_get_rows() / 2, 0);
video_printf("Orange State\n\n");
video_printf("Your device has been unlocked and can't be trusted\n");
video_printf("Your device will boot in 5 seconds\n");
mtk_wdt_restart();
+ mdelay(5000);*/
mtk_wdt_restart();
虽然这样处理系统后,烧写开机还是免不了要做一次修补提取 boot.img 操作,然后就可以再次直接刷入提取 boot
重启后 Magsik 可正常工作。对于最终要发放系统版本,发放人员可这样操作,先刷机后开机进行修补提取 boot.img,
将修补后 boot 替换原来编译后 boot, 整个刷机包就是最终烧写固件。
难点二、Magsik 安装依赖模块 Riru 和 LSPosed 需要再次重启
Magisk 成功安装后,底部会多出 4 按钮菜单

传统做法手动点击模块按钮,跳转安装页面,从手机 sdcard 下选择预制的 Riruxxx.zip LSPosedxxx.zip
模块的安装顺序必须先 riru 然后再 lsposed,这样操作起来又是更麻烦。好在 Magisk 是开源的,那我们就来自己定制一个
Magisk,实现自动安装模块。源码下载编译可参考之前写的 Magisk 最新版本 V24.1 源码编译踩坑集锦
编译成功后来修改代码吧,虽然是 kotlin 问题不大,盘它。修改思路如下
1、先判断是否成功安装 Magsik,没安装自动跳转然后直接安装
刷机完修补后的boot,开机后需要选推荐安装一次 Magisk 才能正常使用(底部工具栏)
2、安装成功后拷贝 assert 目录下的 riru.zip 和 LSPosed.zip 到 sdcard 根目录
3、跳转安装模块界面先安装 riru 模块,再安装 LSPosed 模块,安装成功重启
这部分的逻辑比较简单就不贴了,直接放个最终的 Magisk.apk
难点三、高版本预装 APK 可卸载
之前也写过参考这篇 android11.0® data分区节点加密控制分析
修改
system/core/init/util.cpp
system/core/rootdir/init.rc
新建 package/app/MagiskAPP 文件夹
Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := Magisk
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(LOCAL_MODULE).apk
LOCAL_MODULE_CLASS := APPS
LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
# LOCAL_PRIVILEGED_MODULE := true
LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
# LOCAL_CERTIFICATE := PRESIGNED
LOCAL_DEX_PREOPT := false
LOCAL_CERTIFICATE := platform
include $(BUILD_PREBUILT)
include $(CLEAR_VARS)
LOCAL_MODULE := VCAM
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(LOCAL_MODULE).apk
LOCAL_MODULE_CLASS := APPS
LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
# LOCAL_PRIVILEGED_MODULE := true
LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
LOCAL_CERTIFICATE := PRESIGNED
LOCAL_DEX_PREOPT := false
# LOCAL_CERTIFICATE := platform
include $(BUILD_PREBUILT)
经过测试 微信视频通话使用 Camera1
抖音直播使用 Camera2
四、准备替换视频
抖音替换路径 /sdcard/DCIM/Camera1/virtual.mp4
微信替换路径 /sdcard/Android/data/com.tencent.mm/files/Camera1/virtual.mp4
virtual.mp4 用系统相机直接随便录制一个即可,主要是看其具体分辨率。
2022-02-09 14:39:19.398 2179-3231/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】1位参数初始化相机,类:class com.mediatek.ims.internal.VTSource$DeviceHandler$1
2022-02-09 14:39:19.465 2179-3232/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】重建垃圾场
2022-02-09 14:39:19.472 2179-3232/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】打开相机C2
2022-02-09 14:39:19.663 2179-3231/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】surfaceInfo=Surface(name=android.graphics.SurfaceTexture@3131cdf)/@0x6b39b1f
2022-02-09 14:39:19.663 2179-3231/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】获取 c2_preview_Surfcae
2022-02-09 14:39:19.664 2179-3231/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】添加目标:Surface(name=android.graphics.SurfaceTexture@3131cdf)/@0x6b39b1f
2022-02-09 14:39:19.665 2179-3231/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】开始build请求
2022-02-09 14:39:19.672 2179-3231/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】c2_player.setDataSource
2022-02-09 14:39:19.734 2179-3231/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】处理过程完全执行
2022-02-09 14:39:21.105 2179-3231/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】surfaceInfo=Surface(name=android.graphics.SurfaceTexture@3131cdf)/@0xd964558
2022-02-09 14:39:21.106 2179-3231/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】获取 c2_preview_Surfcae_1
2022-02-09 14:39:21.106 2179-3231/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】添加目标:Surface(name=android.graphics.SurfaceTexture@3131cdf)/@0xd964558
2022-02-09 14:39:21.109 2179-3231/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】开始build请求
2022-02-09 14:39:21.191 2179-3231/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】c2_player.setDataSource
2022-02-09 14:39:21.249 2179-3231/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】c2_player_1.setDataSource
2022-02-09 14:39:21.276 2179-3231/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】[c2player1][ Surface(name=android.graphics.SurfaceTexture@3131cdf)/@0xd964558]java.lang.IllegalStateException
2022-02-09 14:39:21.276 2179-3231/com.mediatek.ims I/LSPosed-Bridge: 【VCAM】处理过程完全执行
2022-02-11 13:01:59.107 695-1207/? E/Camera3-OutputStream: configureConsumerQueueLocked: Unable to connect to native window for stream 0
2022-02-11 13:01:59.107 695-1207/? E/Camera3-Stream: finishConfiguration: Unable to configure stream 0 queue: Invalid argument (-22)
2022-02-11 13:01:59.107 695-1207/? E/Camera3-Device: Camera 1: configureStreamsLocked: Can’t finish configuring output stream 0: Invalid argument (-22)
2022-02-11 13:01:59.107 695-1207/? E/CameraDeviceClient: endConfigure: Camera 1: Unsupported set of inputs/outputs provided
2022-02-11 13:01:59.108 769-4340/? I/MtkCam/StreamingPipe/MDPWrapper: [MDPWrapper][p2s] out port count=2, mUseIONFD=0
2022-02-11 13:01:59.108 769-4340/? I/MtkCam/StreamingPipe/MDPWrapper: [MDPWrapper][p2s] out port count=2, mUseIONFD=0
2022-02-11 13:01:59.108 769-4339/? I/MtkCam/fdNodeImp: [config] is FD in secure mode : 0
2022-02-11 13:01:59.108 2200-4306/com.mediatek.ims W/CameraDevice-JV-1: Stream configuration failed due to: endConfigure:505: Camera 1: Unsupported set of inputs/outputs provided
2022-02-11 13:01:59.109 769-4340/? I/MtkCam/StreamingPipe/MDPWrapper: [MDPWrapper][p2s] out port count=2, mUseIONFD=0
2022-02-11 13:01:59.109 2200-4306/com.mediatek.ims E/CameraCaptureSession: Session 0: Failed to create capture session; configuration failed
2022-02-11 13:01:59.109 769-4340/? I/MtkCam/StreamingPipe/MDPWrapper: [MDPWrapper][fpipe.helper] out port count=2, mUseIONFD=0
Dialer: DialerCall.onCameraError - Camera session error: 8003, Call: [DialerCall_1, INCOMING, [Capabilities: rsp_v_txt mut], [Properties: m_volte], children:[], parent:null, conferenceable:[], videoState:Audio Tx Rx, mSessionModificationState:0, CameraDir:-1]
在看雪论坛发现有大佬直接修改 framework 层可实现此功能,后来实操了,发现只能 hook camera1 微信,抖音就不行了,也算是一种思路