资讯动态

把手机摄像头换成一段视频:安卓虚拟摄像头 Xposed 实现全拆解

发布时间:2026/8/18 18:00:56 来源:尧图企业网站定制
把手机摄像头换成一段视频安卓虚拟摄像头 Xposed 实现全拆解【免费下载链接】android_virtual_camxposed安卓虚拟摄像头 android virtual camera on xposed hook项目地址: https://gitcode.com/gh_mirrors/an/android_virtual_camandroid_virtual_cam 是一款基于 Xposed 的安卓虚拟摄像头模块支持安卓 5.0 及以上系统。它能在完全不改动目标应用代码的前提下把摄像头预览画面和拍照结果悄悄替换成指定视频与图片对应用开发者、测试工程师和安全研究人员都很有价值。本文会从落地步骤讲起再逐层拆开 Hook 原理、MediaCodec 硬解码和避坑要点帮你把原理吃透。一、先回答一个问题虚拟摄像头到底有什么用做应用测试的人大概都遇到过这样的场景要验证扫码、人脸识别、直播推流功能但手边没有稳定的测试环境画面忽明忽暗、背景杂乱根本没法复现问题。更麻烦的是有些功能必须在摄像头真实出画面的前提下才能跑通流程。虚拟摄像头就是为这类场景准备的。它像一扇假窗户应用以为自己在看真实的摄像头实际上看到的是一段循环播放的视频或一张静态图片。典型用途包括功能测试用固定画面反复验证相机相关逻辑保证每次测试条件一致隐私审计监控哪些应用在后台偷偷调起摄像头、怎么处理画面数据演示教学给摄像头功能做 Demo 时不用真人出镜画面可控又整洁android_virtual_cam 这个项目选择的实现路线很硬核用 Xposed 在系统层拦截相机 API。这意味着它对目标应用透明应用自身完全感知不到异样。二、模块很小五脏俱全整个项目的核心代码只有三个 Java 文件结构非常清爽文件职责app/src/main/java/com/example/vcam/HookMain.java所有 Hook 点的注册与替换逻辑是偷天换日的总指挥app/src/main/java/com/example/vcam/VideoToFrames.java基于 MediaCodec 的硬解码器把视频拆成一帧帧画面app/src/main/java/com/example/vcam/MainActivity.java配置界面提供几个开关控制模块行为app/src/main/assets/xposed_init声明模块入口类Xposed 框架靠它找到 HookMain入口声明只有一行写的是com.example.vcam.HookMain。Xposed 框架在目标应用进程启动时会加载这个类并调用handleLoadPackage()所有 Hook 都注册在这个方法里。想自己拉源码研究可以执行git clone https://gitcode.com/gh_mirrors/an/android_virtual_cam三、动手十分钟让目标应用看到你的视频先跑通再讲原理这样后面读代码会更有感觉。准备一台已安装 Xposed 框架如 LSPosed的安卓 5.0 设备按下面四步走第 1 步安装并启用模块。安装这个 App 后在 Xposed 管理器里勾选启用。如果用的是 LSPosed记得在作用域里选上目标应用不需要勾选系统框架。第 2 步准备视频文件。在手机存储的DCIM/Camera1/目录下放入一段视频命名为virtual.mp4。目录不存在就手动创建。第 3 步读分辨率提示。打开目标应用的相机界面会弹出一行气泡提示宽xx 高xx。这个数值就是应用实际请求的预览分辨率替换视频必须与它完全一致否则会花屏。第 4 步处理权限问题。如果目标应用没有申请存储权限模块会把Camera1目录自动重定向到该应用的私有目录/内部存储/Android/data/应用包名/files/Camera1/重定向发生时会有气泡提示别错过。私有目录下的配置只对这一个应用生效。拍照替换同理根据拍照时的提示分辨率准备一张图片命名为1000.bmp放进同一目录即可。其他图片格式改成.bmp后缀也能用。四、核心机制Hook 是怎么做到偷天换日的可以把 Xposed 的 Hook 理解成在系统门口安插的检查员。应用每次调用某个系统 API都得先经过这位检查员过目检查员可以放行、改参数甚至直接把结果换掉。android_virtual_cam 一共安插了十几位检查员分别守着不同岗位。岗位一守预览出口Camera1 预览替换老相机 APIandroid.hardware.Camera里应用通常用setPreviewTexture()把相机画面连到一块SurfaceTexture上。Hook 抓住这个方法把应用传进来的纹理偷偷换成自己创建的假纹理XposedHelpers.findAndHookMethod( android.hardware.Camera, // 要拦截的类老相机 API lpparam.classLoader, // 目标应用的类加载器 setPreviewTexture, // 方法名 SurfaceTexture.class, // 方法签名参数类型 new XC_MethodHook() { Override protected void beforeHookedMethod(MethodHookParam param) { // param.args[0] 是应用原本要传入的纹理对象 // 这里偷梁换柱替换成我们自己的假纹理 fake_SurfaceTexture new SurfaceTexture(10); param.args[0] fake_SurfaceTexture; } });替换之后应用拿到的是一块空转的纹理真正的画面哪来的答案是 MediaPlayer。模块把视频文件交给 MediaPlayer让它把画面渲染到这块纹理对应的 Surface 上于是应用看到的就是视频画面而且是无缝循环播放。如果还想要声音创建一个no-silent.jpg开关文件MediaPlayer 就会取消静音。岗位二守帧数据通道预览帧回调替换有些应用不走纹理而是用setPreviewCallback()这类回调接口直接拿预览帧的字节数组通常是 NV21 格式做分析比如人脸检测、扫码。这个岗位更刁钻Hook 拦截回调注册然后在onPreviewFrame()被调用前把解码好的视频帧数据填进字节数组// 拦截 onPreviewFrame 回调把真实相机数据换成视频帧 beforeHookedMethod(paramd) { // data_buffer 是解码线程持续写入的最新视频帧 System.arraycopy(data_buffer, 0, paramd.args[0], 0, Math.min(data_buffer.length, ((byte[]) paramd.args[0]).length)); }data_buffer里装的就是 VideoToFrames 解码出的 NV21 帧。这招的精妙之处在于回调的时序、缓冲区大小都保持原样应用完全看不出数据是假的。三个回调注册方法setPreviewCallback、setPreviewCallbackWithBuffer、setOneShotPreviewCallback都被守住了。岗位三守拍照入口照片替换拍照走的是takePicture()它接受三个回调快门回调、YUV 回调、JPEG 回调。Hook 在afterHookedMethod里分别拦截把1000.bmp读出来对 JPEG 回调Bitmap 压缩成 JPEG 字节替换回调参数对 YUV 回调先把 Bitmap 的像素按公式转成 YCbCr420 字节代码里实现了完整的 RGB 转 YUV 换算再替换回调参数// 读取预设图片压缩成 JPEG 字节流替换拍照结果 Bitmap pict getBMP(video_path 1000.bmp); ByteArrayOutputStream temp new ByteArrayOutputStream(); pict.compress(Bitmap.CompressFormat.JPEG, 100, temp); paramd.args[0] temp.toByteArray(); // 偷换成假照片注意一个细节模块会先弹出发现拍照 分辨率的提示。如果拍照时没有这个提示说明1000.bmp不生效——因为应用走的可能不是常规拍照路径。五、MediaCodec 硬解码视频是怎么变成一帧帧画面的前面提到的解码线程就是 VideoToFrames 类它是整个项目的血管。它的工作分三步第一步用 MediaExtractor 选轨道。从视频文件里挑出mime以video/开头的视频轨道拿到宽、高、编码格式等信息。第二步创建并配置 MediaCodec 解码器。按 mime 类型创建解码器把解码输出的颜色格式尽量设为YUV420Flexible硬件解码器普遍支持的一种灵活格式并指定输出 Surface。第三步循环解码。这是最核心的一段逻辑是标准的输入-输出双循环while (!sawOutputEOS !stopDecode) { // 1) 取空闲输入缓冲区把视频数据喂给解码器 int inId decoder.dequeueInputBuffer(TIMEOUT_US); if (inId 0) { int size extractor.readSampleData(decoder.getInputBuffer(inId), 0); if (size 0) { // 视频读完了标记输入结束 decoder.queueInputBuffer(inId, 0, 0, 0, BUFFER_FLAG_END_OF_STREAM); sawInputEOS true; } else { decoder.queueInputBuffer(inId, 0, size, extractor.getSampleTime(), 0); extractor.advance(); // 推进到下一帧 } } // 2) 取解码完成的输出帧渲染或取数据 int outId decoder.dequeueOutputBuffer(info, TIMEOUT_US); if (outId 0) { // 按帧时间戳控制节奏让播放速度与视频本身一致 long sleep info.presentationTimeUs / 1000 - elapsed; if (sleep 0) Thread.sleep(sleep); decoder.releaseOutputBuffer(outId, true); } }解码出的帧有两个去向渲染到指定 Surface对应纹理预览路径或取出 YUV 数据写进data_buffer对应帧回调路径。取数据时还会处理 YUV 平面到 NV21/I420 的排列转换把三个 plane 按行间距重新排布成标准字节流——这一步不处理干净画面就会出现色偏和花屏。解码完成后extractor.seekTo(0, 0)回到起点形成无缝循环正好对应虚拟摄像头一直有画面的需求。六、Camera2 新接口换一套打法安卓 5.0 之后新增了 Camera2 API架构完全不同但思路一致把应用要用的输出 Surface 全部换成假的。模块在CameraManager.openCamera()处拦截接管相机状态回调在CaptureRequest.Builder.addTarget()里做替换// 应用每次把目标 Surface 加进拍照/预览请求 // 我们都悄悄替换成虚拟 Surface视频画面 XposedHelpers.findAndHookMethod( android.hardware.camera2.CaptureRequest.Builder, lpparam.classLoader, addTarget, Surface.class, new XC_MethodHook() { Override protected void beforeHookedMethod(MethodHookParam param) { param.args[0] c2_virtual_surface; // 换 } });同时还要盯住createCaptureSession系列方法把应用传给会话的 Surface 列表整体替换。另外模块会拦截ImageReader.newInstance()——应用创建渲染器时宽、高、格式这些参数会暴露出来正好用来提示开发者视频应该做成多大。这里有个识别技巧addTarget传入的 Surface 名字里如果带Surface(namenull)一般是读取数据的 Surface带名字的通常是预览 Surface。模块会分别记录为每个 Surface 都准备一套解码器或播放器所以 Camera2 路径下最多会同时跑两个解码器加两个 MediaPlayer。七、藏在文件名里的开关这个项目最巧的设计之一配置靠文件存在与否来控制。在Camera1目录下创建特定名字的文件就相当于拨动开关而且全局实时生效无需重启应用文件名作用virtual.mp4替换预览画面的视频核心素材1000.bmp替换拍照结果的图片disable.jpg临时停用视频替换恢复真实摄像头no_toast.jpg关闭所有气泡提示no-silent.jpg播放视频的声音默认静音force_show.jpg强制重新显示权限提示默认只提示一次private_dir.jpg强制所有应用走私有目录MainActivity 界面上的五个开关本质就是帮你创建/删除这些文件。手动建文件也能达到同样效果两条路都通。八、避坑清单黑屏、花屏、方向错乱怎么办跑起来之后常见的坑基本集中在下面几类对照排查即可画面黑屏或相机启动失败。优先检查三点一是替换视频是否真的在正确目录最常见的翻车点是建了两级目录比如DCIM/Camera1/Camera1/virtual.mp4只要一级就够二是目标应用是否属于无法替换的类型——系统相机应用基本替换不了某些自研相机的应用也不行三是视频路径是否因为权限问题被重定向到了私有目录。画面花屏。九成是分辨率不匹配。用气泡提示的宽高数值用剪辑软件把视频裁成完全一致的尺寸。画面扭曲变形。视频比例和屏幕比例不一致导致去剪辑软件里重新裁剪画面匹配即可。前置摄像头方向不对。大多数情况下替换前置摄像头的视频需要水平翻转并右旋 90 度处理后的分辨率也要和提示一致。但大多数不等于全部具体以实际效果为准。disable.jpg不生效。注意版本差异模块版本 4.0 及以下有存储权限的应用看DCIM/Camera1下的文件无权限应用要在私有目录下建4.1 及以上版本统一在DCIM/Camera1下创建即可。九、性能与边界哪些做得到哪些做不到先说做不到的录像无法替换。模块拦截了MediaRecorder.setCamera()但它只能弹出触发了录像目前无法拦截的提示无法真正把录像内容换成视频。这是当前实现的明确边界。性能方面有几个值得注意的点资源要及时释放。解码器、MediaPlayer、SurfaceTexture 用完都要release()项目里每次重建前都会先释放旧对象避免内存泄漏解码线程有节流。按帧时间戳Thread.sleep控制节奏避免解码器空转烧 CPU多路并行有代价。Camera2 路径下双解码器 双播放器同时运行对低端机的内存和功耗都是考验硬解码依赖编解码器支持。H.264 是兼容性最好的选择H.265 在高版本系统上才普遍支持兼容性上项目覆盖安卓 5.0对老 API 的多个版本P 及以上的 Executor 版本 openCamera、N 及以上的 createCaptureSessionByOutputConfigurations 等都做了分支处理看得出在适配层面下了不少功夫。十、除了换画面这套机制还能玩出什么理解了原理之后这个项目的价值会超出换视频本身测试提效相机类功能测试从此有了确定性的输入回归测试不再受环境干扰隐私研究通过它观察应用对摄像头数据的处理行为评估权限滥用风险教学演示摄像头原理、帧回调机制、MediaCodec 解码流程都能拿它当活教材从演进方向看这个项目未来可以做的还有很多支持更多视频格式、增加实时滤镜、优化多路解码的资源占用、把配置界面做得更图形化。如果你有兴趣完全可以在现有 Hook 骨架上叠加自己的功能——毕竟拦截系统 API这个能力本身就是一套通用的舞台。写在最后android_virtual_cam 用一个不大的代码量把 Xposed Hook、MediaCodec 硬解码、Surface 与帧数据替换这几块硬骨头啃了下来是一份很适合入门的系统级开发教材。读源码时建议按这个顺序先看handleLoadPackage里注册了哪些 Hook再看VideoToFrames的解码循环最后回到process_camera2_play看各路画面是怎么汇合的。动手跑通一个 demo再回头读代码你会发现自己对安卓相机架构的理解深了一层。记得把技术用在合法合规的场景里测试、研究、学习都是它最好的归宿。【免费下载链接】android_virtual_camxposed安卓虚拟摄像头 android virtual camera on xposed hook项目地址: https://gitcode.com/gh_mirrors/an/android_virtual_cam创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价