多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

安卓虚拟摄像头实战指南:Xposed Hook如何接管相机,MediaCodec硬解码为何必不可少

安卓虚拟摄像头实战指南:Xposed Hook如何接管相机,MediaCodec硬解码为何必不可少 安卓虚拟摄像头实战指南Xposed Hook如何接管相机MediaCodec硬解码为何必不可少【免费下载链接】android_virtual_camxposed安卓虚拟摄像头 android virtual camera on xposed hook项目地址: https://gitcode.com/gh_mirrors/an/android_virtual_cam你手机屏幕上那个正在预览中的画面一定来自真实摄像头传感器吗未必。android_virtual_cam 这个开源项目用 Xposed Hook 机制对系统相机接口做了一次漂亮的偷梁换柱再借助 MediaCodec 硬解码把视频帧伪装成相机输出造出了一台并不存在的安卓虚拟摄像头。本文不打算逐行贴源码而是沿着一条主线——App 打开相机时系统背后到底发生了什么——带你依次看懂两个核心技术Hook 如何截胡相机调用、硬解码如何源源不断供帧最后奉上一份十分钟上手的部署清单和排雷笔记。一、先回答一个灵魂拷问为什么我们需要假相机做过相机类 App 的开发者大多经历过这样的尴尬本地跑得好好的扫码、美颜、人脸识别一到别人的真机上就翻车。原因很现实——我们没法在测试环境里控制摄像头的输入内容自动化测试想模拟对着固定画面拍照但真机画面不可控隐私测试想验证某个 App 是否在后台偷偷调用相机缺少可观测的替身直播、视频会议类应用想验证弱光、多分辨率下的表现却没有稳定的画面源。一台虚拟摄像头的价值正在于此它把相机输入从真实世界替换成可控素材让开发者获得了一个完全可复现的测试环境。android_virtual_cam 提供的正是这样一套方案——用视频文件充当预览流、用静态图片充当拍照结果还兼容 Camera1 与 Camera2 两代相机接口。二、第一步截胡Xposed 如何在 App 开口要相机之前动手脚先说人话Xposed 是一个运行在 Android 系统层面的外挂框架它会在系统核心进程 Zygote 启动早期把我们的模块代码注入进去。之后App 里每一次对系统 API 的调用都会先经过我们注册的检查点——我们可以在方法真正执行之前改写它的参数也可以在方法执行之后改写它的返回值。android_virtual_cam 的 Hook 主逻辑集中在app/src/main/java/com/example/vcam/HookMain.java它的思路不是包揽一切而是按相机 API 的两代演进分别布点被 Hook 的系统 API所属时代拦截目的Camera.setPreviewTexture/setPreviewDisplayCamera1把 App 指定的画面输出目标换成假纹理Camera.startPreviewCamera1用 MediaPlayer 把 virtual.mp4 播到预览 Surface 上Camera.setPreviewCallback(WithBuffer)Camera1在帧回调里灌入解码好的 NV21 数据Camera.takePictureCamera1拍照回调中把结果替换成 1000.bmpCameraManager.openCameraCamera2拿到 StateCallback为后续替换做准备CaptureRequest.Builder.addTarget/buildCamera2把输出目标 Surface 换成虚拟 SurfaceCameraDevice.createCaptureSession系列Camera2用假的输出配置构建捕获会话以 Camera1 为例最经典的一处 Hook 长这样注释已按项目思路重新整理XposedHelpers.findAndHookMethod( android.hardware.Camera, // 拦截目标类老一代相机对象 lpparam.classLoader, // 被 Hook 应用的类加载器 setPreviewTexture, // 方法名App 在这里指定画面画到哪里 SurfaceTexture.class, // 参数类型App 传入的纹理 new XC_MethodHook() { Override protected void beforeHookedMethod(MethodHookParam param) { // 方法尚未真正执行此刻替换参数最安全 param.args[0] new SurfaceTexture(10); // 换成假纹理 } });一句话小结Hook 的核心不是阻止而是调包——在系统方法真正执行前把输入换成我们准备好的东西。三、第二步供帧MediaCodec 硬解码如何源源不断喂画面参数换好了但假纹理上得有画面流才骗得过 App 的眼睛。这里就轮到第二个主角登场MediaCodec 硬解码。为什么非要用硬解码因为软解一帧 1080P 画面动辄几十毫秒还要烧 CPU根本扛不住相机 30fps 的预览节奏。而 MediaCodec 直接调用芯片自带的硬件解码单元又快又省电。解码链路放在app/src/main/java/com/example/vcam/VideoToFrames.java中核心流程可以画成一张图virtual.mp4 │ MediaExtractor 拆封装 ▼ H.264 压缩帧 ──► MediaCodec 硬解码 ──► YUV_420 原始帧 │ ┌─────────────────────────┤ ▼ ▼ MediaPlayer 直接渲染到 Surface getOutputImage 转 NV21 │ │ ▼ ▼ 预览画面两代 API 通用 onPreviewFrame 帧回调细看会发现项目其实走了双通道预览画面这种给人看的流直接用 MediaPlayer 把视频渲染到 Surface而onPreviewFrame这种给代码分析的帧回调则必须拿到原始的 YUV 字节数组。后者正是 VideoToFrames 的职责精简后的骨架如下MediaExtractor extractor new MediaExtractor(); extractor.setDataSource(videoPath); int track selectVideoTrack(extractor); // 找到视频轨道 MediaFormat format extractor.getTrackFormat(track); MediaCodec decoder MediaCodec.createDecoderByType( format.getString(MediaFormat.KEY_MIME)); // 按编码类型建解码器 decoder.configure(format, null, null, 0); // 输出到内存而非屏幕 decoder.start(); while (!sawEOS) { // 输入端塞入一帧压缩数据 int inIdx decoder.dequeueInputBuffer(10000); // ……写入 sample 并 queueInputBuffer…… // 输出端取回一帧原始图像 int outIdx decoder.dequeueOutputBuffer(info, 10000); Image image decoder.getOutputImage(outIdx); byte[] nv21 toNV21(image); // 转成相机回调要求的 NV21 格式 decoder.releaseOutputBuffer(outIdx, false); }这里还藏着一个细节解码器吐出的通常是 YUV_420_888 柔性格式而 Camera1 的预览回调要的是 NV21。项目里专门实现了多平面拷贝逻辑处理 rowStride、pixelStride 这些行对齐问题——这也是很多自研方案容易踩坑的地方。一句话小结硬解码解决画面从哪来的性能问题YUV 转换解决格式对不对的兼容问题两者缺一不可。四、十分钟上手把虚拟摄像头跑起来的部署清单理解了原理落地其实很轻量。整个项目只有三个 Java 文件模块入口声明在app/src/main/assets/xposed_init中。按下面几步走克隆源码并编译安装模块git clone https://gitcode.com/gh_mirrors/an/android_virtual_cam在 LSPosed 中勾选目标应用不需要勾选系统框架在系统设置里给目标应用授予存储权限并强制结束它的进程打开目标应用触发一次相机预览根据气泡提示的分辨率制作视频命名为virtual.mp4放到DCIM/Camera1/目录若要替换拍照按提示的分辨率准备一张图片并命名为1000.bmp放进同一目录之后想开关功能直接增删标记文件即可全局实时生效。这个项目一个颇具巧思的设计是文件即开关没有任何数据库或配置文件一切状态都由目录下的占位文件决定。标记文件作用virtual.mp4替换摄像头预览的视频1000.bmp替换拍照结果的图片disable.jpg临时停用全部替换no-silent.jpg播放视频原声默认静音no_toast.jpg关闭提示气泡private_dir.jpg强制使用应用私有目录force_show.jpg强制再次弹出目录重定向提示关于目录还有一个贴心逻辑若目标应用没有存储权限项目会把Camera1自动重定向到该应用的私有目录Android/data/[包名]/files/Camera1/保证无权限应用也能用。这套权限探测与目录重定向是在 HookInstrumentation.callApplicationOnCreate时完成的属于 Hook 主逻辑之外一个容易被忽略的细节。五、进阶排雷从能用到好用的四条经验实战中最常遇到的是下面四类问题提前知道能省大量排查时间前置摄像头画面方向不对多数机型下前置的视频需要水平翻转并右旋 90 度且处理后的分辨率要与气泡提示一致画面花屏几乎都是视频分辨率与 App 请求的预览分辨率不匹配用剪辑软件重新导出即可黑屏或相机启动失败先检查路径是否建了两级Camera1目录另外系统自带相机通常难以替换属正常现象录像功能拦不住项目会监测MediaRecorder.setCamera并弹出提示——目前录制流程尚无法拦截属于已知边界。一句话小结虚拟摄像头的兼容性天花板不在原理而在目标 App 用多花哨的方式拿相机理解边界比硬磕更高效。六、写在最后能力越大责任越重回看整条链路安卓虚拟摄像头的本质并不神秘Xposed 负责在调用链路上调包MediaCodec 负责把视频素材变成相机接口认得的格式两者拼起来就构成了一台以假乱真的虚拟相机。对开发者而言它是兼容性测试、隐私审计的趁手工具但也必须清醒地认识到这类能力一旦被滥用就可能沦为诈骗或作恶的工具。请务必在合法合规的范围内使用把技术用在测试与研究上——这也是项目 README 里反复强调的底线。如果你正在为相机功能的测试发愁不妨亲手编译一个模块在真机上跑通一次假预览当你看到画面被替换的那一刻对 Android 相机体系的理解会上一个台阶。【免费下载链接】android_virtual_camxposed安卓虚拟摄像头 android virtual camera on xposed hook项目地址: https://gitcode.com/gh_mirrors/an/android_virtual_cam创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表