ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

android_virtual_cam(VCAM)实战指南:基于 Xposed 的安卓虚拟摄像头模块配置、控制文件与源码原理解析

android_virtual_cam(VCAM)实战指南:基于 Xposed 的安卓虚拟摄像头模块配置、控制文件与源码原理解析
  • 移动开发
  • 音视频

【免费下载链接】com.example.vcam

虚拟摄像头 virtual camera

项目地址:https://gitcode.com/gh_mirrors/co/com.example.vcam
点击查看免费下载

本文围绕开源仓库 com.example.vcam(android_virtual_cam,应用名 VCAM)的英文使用文档展开,系统讲解在 Android 5.0+ 设备上如何安装启用这个基于 Xposed 的虚拟摄像头模块:从 LSPosed 作用域选择、存储权限授权,到按 Toast 分辨率制作virtual.mp4与1000.bmp,再到 7 个控制文件的实时开关机制。同时结合 HookMain.java、VideoToFrames.java 与 MainActivity.java 源码,深入说明目录重定向、Camera1/Camera2 预览替换、拍照替换与硬解码链路的底层实现,帮助读者既会用、也看得懂原理。

文档语言说明:本仓库同时提供 简体中文、繁體中文 与 English 三份使用文档,本文以英文文档为主体,并依据源码对个别笔误进行了校正。

项目概述与支持平台

android_virtual_cam 是一款基于Xposed框架的安卓虚拟摄像头模块:安装并启用后,目标应用调用摄像头时,模块会拦截系统相机接口,把真实的相机预览替换为指定视频画面,把拍照结果替换为指定图片,从而实现"虚拟摄像头"效果。

  • 项目名称:android_virtual_cam / VCAM
  • 应用包名:com.example.vcam
  • 实现方式:Xposed 模块,入口为 xposed_init 中声明的com.example.vcam.HookMain
  • 支持平台:Android 5.0+(对应 app/build.gradle 中minSdk 21)
  • 当前仓库版本:versionName "4.4"、versionCode 28(见 app/build.gradle)
  • 法律声明:文档明确要求DO NOT USE FOR ANY ILLEGAL PURPOSE,使用者需自行承担全部责任与后果

从 AndroidManifest.xml 可以看到模块的三个关键元数据:xposedmodule=true、xposeddescription=安卓虚拟摄像头、xposedminversion=51,同时声明了READ_EXTERNAL_STORAGE与WRITE_EXTERNAL_STORAGE两个存储权限。

模块安装与在 Xposed 中启用

  1. 安装模块:安装本模块 APK(仓库 app/release/app-release.apk 已提供编译产物),并在 Xposed 框架中启用。
  2. 选择作用域:对于LSPosed等带有作用域列表的框架,需要勾选目标应用,而无需选择系统框架。这一点对替换效果至关重要——模块只对被勾选的应用生效。
  3. 授予存储权限:在系统设置中授予目标应用读取本地存储的权限,然后强制结束目标应用(确保权限与模块配置在下次启动时重新生效)。
  4. 若目标应用本身未申请存储权限,则无需授权,模块会自动改用应用私有目录(详见下文"目录重定向机制")。

从源码看,模块依赖 Xposed API 82,见 app/build.gradle:

dependencies { compileOnly 'de.robv.android.xposed:api:82' compileOnly 'de.robv.android.xposed:api:82:sources' }

认识 Camera1 目录与核心文件

模块的一切替换素材都围绕Camera1目录组织,该目录有两种形态:

目录形态路径适用范围
公共目录(默认)/[内部存储]/DCIM/Camera1/所有有存储权限的应用共用
应用私有目录/[内部存储]/Android/data/[应用包名]/files/Camera1/仅对该应用单独生效

对应的默认路径在源码中有明确声明(HookMain.java):

public static String video_path = "/storage/emulated/0/DCIM/Camera1/";

目录下需要放置两类替换素材:

  • virtual.mp4:替换相机预览的视频,分辨率必须与目标应用预览分辨率一致;
  • 1000.bmp:替换拍照结果的图片,分辨率必须与拍照分辨率一致(支持把其他格式的图片直接改后缀为.bmp)。

如果目录不存在,请手动创建。注意:只需要一级Camera1目录,不要在内部再嵌套一层Camera1(这是黑屏的常见原因之一,详见 FAQ)。

完整使用步骤(按文档 10 步走通全流程)

以下步骤完整继承自 README_en.md,并补充了源码依据。

步骤 1:安装并启用模块在 Xposed 中启用本模块,LSPosed 等带作用域的框架需选择目标应用而非系统框架。

步骤 2:授权并强制停止目标应用在系统设置中授予目标应用读取本地存储的权限,并强制结束目标应用;若应用未申请此权限,直接跳到步骤 3。

步骤 3:确认 Camera1 目录位置打开目标应用,若应用未获得读取存储的权限,会弹出 Toast 提示:Camera1目录已被重定向到应用私有目录/[内部存储]/Android/data/[应用包名]/files/Camera1/。若没有该提示,则默认目录为/[内部存储]/DCIM/Camera1/,目录不存在时请手动创建。

注意:私有目录下的Camera1仅对该应用单独生效,每个应用需分别放置自己的素材。

步骤 4:制作替换视频virtual.mp4在目标应用中打开相机预览,模块会弹出 Toast 提示分辨率("宽:…… 高:……")。根据该分辨率制作替换视频,命名为virtual.mp4,放入Camera1目录。若打开相机没有提示消息,则无需调整视频分辨率。

源码印证:该 Toast 由 HookMain.java 中的预览回调钩子触发,同时还会给出预览帧率:发现预览\n宽:…\n高:…\n需要视频分辨率与其完全相同。

步骤 5:制作替换照片1000.bmp在目标应用中拍照,若显示真实图片并弹出 Toast("发现拍照")及分辨率,则按此分辨率准备一张照片,命名为1000.bmp,放入Camera1目录(支持其它格式改后缀为.bmp)。若拍照时无 Toast 提示,则1000.bmp不参与替换,模块不会拦截该应用的拍照。

源码印证:拍照替换逻辑见 HookMain.java,通过钩住Camera.takePicture的PictureCallback,在回调中把1000.bmp解码后转成 JPEG 或 YUV_420_888 数据替换原始字节。

步骤 6:播放视频声音在/[内部存储]/DCIM/Camera1/目录下创建no-silent.jpg文件,即可让替换视频在预览时带声音播放(全局实时生效)。

勘误说明:英文 README 步骤 6 误写为"创建./DCIM/Camera1/Camera1/virtual.mp4",实际控制文件名以源码为准——HookMain.java 中MediaPlayer.setVolume(0, 0)的静音逻辑正是由no-silent.jpg是否存在决定,中文 README.md 亦为no-silent.jpg。

步骤 7:临时停用视频替换在/[内部存储]/DCIM/Camera1/目录下创建disable.jpg文件,模块即临时恢复真实相机(全局实时生效)。源码中每个钩子入口都会先检查disable.jpg是否存在,存在则直接放行原始调用(如 HookMain.java)。

勘误说明:英文 README 步骤 7 同样误写为创建virtual.mp4,正确控制文件为disable.jpg,见 HookMain.java 与 MainActivity.java。

步骤 8:关闭 Toast 提示若觉得各步骤的 Toast 消息烦人,可在/[内部存储]/DCIM/Camera1/目录下创建no_toast.jpg文件(全局实时生效)。源码中所有 Toast 输出前都会先检查该文件,例如need_to_show_toast = !toast_control.exists()(HookMain.java)。

步骤 9:强制再次显示目录重定向提示目录重定向提示默认只显示一次。如果错过了该 Toast,可在/[内部存储]/DCIM/Camera1/目录下创建force_show.jpg文件来覆盖默认设定(全局实时生效)。源码中通过私有目录下的has_shown标记文件记录"已显示",force_show.jpg存在时即使已有标记也会再次弹出提示(HookMain.java)。

步骤 10:为每个应用分配独立视频如果需要为每一个应用分配各自的视频,可在/[内部存储]/DCIM/Camera1/目录下创建private_dir.jpg文件,强制所有应用改用各自的私有目录(全局实时生效),这样每个应用的Camera1素材互不干扰。

便捷配置:步骤 6~10 的开关都可以在应用内快捷配置——MainActivity.java 提供了 5 个开关,分别对应force_show.jpg(强制显示权限提示)、disable.jpg(临时停用)、no-silent.jpg(播放视频声音)、private_dir.jpg(强制私有目录)与no_toast.jpg(关闭 Toast),开关状态与目录中的文件实时双向同步(sync_statue_with_files(),见 MainActivity.java)。

控制文件速查表

控制文件作用存放位置生效方式
virtual.mp4替换相机预览的视频画面Camera1 目录打开相机预览时
1000.bmp替换拍照结果的照片Camera1 目录拍照回调时
no-silent.jpg播放替换视频的声音/[内部存储]/DCIM/Camera1/全局实时生效
disable.jpg临时停用视频替换(恢复真实相机)/[内部存储]/DCIM/Camera1/全局实时生效
no_toast.jpg关闭全部 Toast 提示/[内部存储]/DCIM/Camera1/全局实时生效
force_show.jpg强制再次显示目录重定向提示/[内部存储]/DCIM/Camera1/全局实时生效
private_dir.jpg强制所有应用使用私有目录/[内部存储]/DCIM/Camera1/全局实时生效

除virtual.mp4与1000.bmp需要按分辨率制作外,其余 5 个控制文件只需"存在/不存在"即可切换状态,这也是模块设计中最灵活的部分——无需重启应用,实时生效。

目录重定向机制(源码级解析)

模块在目标应用的Application创建时(钩住android.app.Instrumentation.callApplicationOnCreate,见 HookMain.java)完成video_path的最终决策:

  1. 权限判定:读取READ_EXTERNAL_STORAGE与(Android 11+ 的)MANAGE_EXTERNAL_STORAGE权限状态,累加得到auth_statue。
  2. 决策逻辑:若auth_statue < 1(即应用无存储权限)或存在private_dir.jpg,则video_path指向应用私有目录getExternalFilesDir(null)/Camera1/;否则使用公共目录/storage/emulated/0/DCIM/Camera1/。
  3. 重定向提示:无权限时弹出 Toast 说明重定向路径,并通过私有目录下的has_shown文件保证提示默认只出现一次,force_show.jpg可强制重复显示。
  4. 公共目录不存在时,模块会自动尝试创建(uni_Camera1_path.mkdir())。

这一机制解释了步骤 3 中"有没有重定向提示"的两种情形,也解释了为什么同一台设备上不同应用可能看到不同的Camera1路径。

替换链路源码解析

Camera1(android.hardware.Camera)预览替换

针对旧版 Camera API,模块通过多个钩子协同完成替换(HookMain.java):

  • Camera.setPreviewTexture(L97-L146):用新建的fake_SurfaceTexture(纹理 ID 10)替换原始SurfaceTexture,让真实相机的预览帧流向一个"垃圾场",同时记录原始相机对象与纹理,供后续播放器输出。
  • Camera.setPreviewDisplay/Camera.startPreview(L359-L461):创建MediaPlayer并setSurface(...)到应用预览用的 Surface 上,循环播放video_path + "virtual.mp4",实现"预览即视频";声音是否静音由no-silent.jpg决定。
  • setPreviewCallbackWithBuffer/setPreviewCallback/setOneShotPreviewCallback/addCallbackBuffer(L220-L254):拦截预览帧回调,通过VideoToFrames解码出的帧数据(data_buffer)覆盖原始onPreviewFrame的字节数组,供依赖帧数据的应用使用。
  • MediaRecorder.setCamera:检测到应用触发录像时提示"触发了录像,但目前无法拦截"(L270-L285),说明录像场景目前不在替换范围内。

Camera2 API 替换

针对新版 Camera2 API,模块钩住CameraManager.openCamera(分别处理 Handler 与 Executor 两个重载,L148-L217),并在相机打开回调(onOpened)中钩住createCaptureSession系列方法(普通、ByOutputConfigurations、高速、可重处理等多个重载,L813-L1011),把应用的输出配置替换为模块创建的"虚拟 Surface"(create_virtual_surface(),纹理 ID 15),同时:

  • 钩住CaptureRequest.Builder.addTarget/removeTarget/build(L507-L637),按Surface名称区分预览 Surface 与ImageReader读取 Surface,分别交给MediaPlayer播放与硬解码器输出。
  • 钩住ImageReader.newInstance(L661-L678),弹出"应用创建了渲染器:宽/高/格式"的 Toast,提醒"一般只需要宽高比与视频相同"。
  • 最终在process_camera2_play()(L691-L788)中,为每个 Surface 分配MediaPlayer或VideoToFrames解码器,按ImageReader的格式(256即 JPEG,否则 NV21)决定输出帧类型。

视频帧硬解码(VideoToFrames)

VideoToFrames.java 承担把virtual.mp4解码为逐帧数据的任务,核心链路是MediaExtractor + MediaCodec 硬件解码:

  • selectTrack()选取mime以video/开头的视频轨道(L236-L249);
  • 解码循环中把输出帧写入目标Surface,若目标 Surface 为空则取出Image转换为NV21字节存入HookMain.data_buffer(L201-L216),供 Camera1 预览帧回调使用;
  • 解码结束后extractor.seekTo(0, 0)循环播放,实现视频无限循环(L122-L126);
  • 帧节奏通过presentationTimeUs与真实时间的差值Thread.sleep控制,尽量贴近原视频帧率。

该实现由 README_en.md 致谢部分提到的 Android-VideoToImages 项目修改而来(H.264 硬件解码思路)。

拍照替换(1000.bmp)

钩住Camera.takePicture(L256-L268)后,模块分别处理两种回调:

  • JPEG 回调(process_a_shot_jpeg,L1013-L1054):读取video_path + "1000.bmp"解码为 Bitmap,再以Bitmap.CompressFormat.JPEG, 100压缩成 JPEG 字节,覆盖onPictureTaken的原始数据;
  • YUV 回调(process_a_shot_YUV,L1056-L1091):通过rgb2YCbCr420()将 Bitmap 像素按标准公式转换为 YUV 4:2:0 字节(源码注释标明该转换思路来自致谢部分的 JPEG→YUV 技术文章),覆盖原始 YUV 数据。

两个回调都会先弹出"发现拍照\n宽:…\n高:…\n格式:JPEG / YUV_420_888"的 Toast,并受disable.jpg与no_toast.jpg控制。

常见问题(FAQ)

Q1:前置摄像头方向问题?大多数情况下,替换前置摄像头的视频需要水平翻转并右旋 90 度,并且视频处理后的分辨率应与 Toast 提示的分辨率相同。但有时并不需要调整,具体请根据实际情况判断。

Q2:画面黑屏、相机启动失败?目前有些应用并不能成功替换,特别是系统相机。另一种常见原因是视频路径不对——检查是否错误地创建了两级Camera1目录(如./DCIM/Camera1/Camera1/virtual.mp4),实际上只需要一级目录。

Q3:画面花屏?视频分辨率不对,请按 Toast 提示的分辨率重新制作virtual.mp4。

Q4:画面扭曲、变形?请使用剪辑软件修改原视频,使其宽高比与目标应用屏幕/预览匹配。

Q5:创建disable.jpg无效?这与模块版本有关:

  • 应用版本<= 4.0:[内部存储]/DCIM/Camera1目录下的控制文件只对具有存储权限的应用生效;其余无权限应用应在私有目录下创建控制文件。
  • 应用版本>= 4.1:无论在目标应用是否有权限,控制文件都应创建在[内部存储]/DCIM/Camera1下。

当前仓库版本为4.4(见 app/build.gradle),因此遵循 4.1+ 规则即可。

问题反馈

请直接在项目 issues 中反馈。如果属于 BUG 反馈,请附带 Xposed 模块日志信息(而非应用日志),以便定位 hook 过程中的异常——源码中大量XposedBridge.log("【VCAM】...")输出(如"开始解码""创建预览""打开相机C2""onCaptureFailed 原因"等)即为排查线索。

致谢与依赖

模块的实现借鉴了以下开源成果(详见 README_en.md 致谢部分):

  • CameraHook:提供了相机相关 hook 方法的思路;
  • Android-VideoToImages:H.264 硬件解码方案(VideoToFrames.java 即由其修改而来);
  • JPEG → YUV 转换技术文章:用于拍照替换时的像素格式转换(HookMain.java 的实现来源)。

需要再次强调:本模块仅可用于合法、合规的测试与个人用途,请勿用于任何非法用途,使用者需自行承担全部责任与后果。

  • 移动开发
  • 音视频

【免费下载链接】com.example.vcam

虚拟摄像头 virtual camera

项目地址:https://gitcode.com/gh_mirrors/co/com.example.vcam
点击查看免费下载

相关推荐

上一篇:猫抓 cat-catch 完整教程:网页视频一键下载,M3U8 流还能自动合并
下一篇:Magisk终极指南:Android系统Root与模块化管理的完整解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进