An Xposed module that replaces the device camera feed with a local video file. Target apps see a virtual camera stream (preview and still capture) instead of the real camera.
Warning: For testing and legitimate use only. You are responsible for your own use.
- Preview replacement: Replaces live camera preview with a looping video (
virtual.mp4). - Still capture: Replaces taken photos with a static image (
1000.bmp) when using the legacy Camera API. - Dual API support: Works with both Camera (legacy) and Camera2.
- Per-app control: Enable/disable and configure via files in
DCIM/Camera1/or app-private directory. - Optional audio: Can play or mute the replacement video’s sound.
- Rooted device with Xposed Framework (LSPosed / EdXposed / similar), min Xposed API 51.
- Android 5.0 (API 21) or higher (minSdk 21; target 26).
- Storage permission for the VCAM app (to read/write config and media under
DCIM/Camera1/or private dir).
- Install the VCAM APK and grant storage permission.
- In Xposed Manager, enable the VCAM module and reboot.
- Put your replacement media in the correct folder (see below).
- Optionally use the VCAM app switches or control files to disable toasts, mute sound, force private dir, etc.
- Default path:
/storage/emulated/0/DCIM/Camera1/ - If “Force every app use private directory” is on, or the target app has no storage permission:
each target app uses its own private dir, e.g.
Android/data/<target.package>/files/Camera1/
Required files:
| File | Purpose |
|---|---|
virtual.mp4 |
Video shown as camera preview (loop). Resolution/aspect should match app’s preview when possible. |
1000.bmp |
Image used for still capture (legacy Camera API only). Used to generate JPEG and YUV data. |
Control files (all optional):
| File | Effect when present |
|---|---|
disable.jpg |
Disable the module (no replacement). |
no_toast.jpg |
Suppress toast messages from the module. |
no-silent.jpg |
Play video sound; if absent, video is muted. |
force_show.jpg |
Force showing “permission / path” toasts again. |
private_dir.jpg |
Force using app-private directory for video/image. |
The VCAM app UI toggles create/remove these .jpg flag files under DCIM/Camera1/.
The module hooks into the Android camera stack so that:
- Preview uses your video (or a virtual surface fed by it) instead of the real camera.
- Capture (legacy API) returns frames derived from
1000.bmpinstead of the real sensor.
Behavior differs slightly for Camera vs Camera2.
Camera.setPreviewTexture
Replaces the app’sSurfaceTexturewith a fake one. The real preview is not used; the module feeds preview fromvirtual.mp4(viaMediaPlayeror decoded frames).Camera.setPreviewDisplay
Intercepted so the app binds to a fake surface; again, video is played onto the surface the app thinks is the camera.Camera.startPreview
Starts playback ofvirtual.mp4(one or moreMediaPlayerinstances) onto the appropriate surface(s).- Preview callbacks (
setPreviewCallback,setPreviewCallbackWithBuffer,setOneShotPreviewCallback)
Hooked soonPreviewFramereceives NV21 data from a VideoToFrames decoder that decodesvirtual.mp4in a loop, instead of real camera frames. addCallbackBuffer
Replaced with empty buffers so the pipeline does not use real camera buffers.takePicture
JPEG and YUV callbacks are hooked; the module supplies JPEG from1000.bmp(compressed to JPEG) and YUV from the same bitmap (RGB→YUV conversion) instead of real capture data.
-
CameraManager.openCamera
When the app opens a camera, the module does not block it but later redirects capture session outputs to a virtual surface. -
CameraDevice.StateCallback.onOpened
The module creates a virtualSurface(backed by aSurfaceTexture) and hooks:createCaptureSession(List<Surface>, ...)createCaptureSessionByOutputConfigurations(API 24+)createConstrainedHighSpeedCaptureSessioncreateReprocessableCaptureSession(API 23+)createReprocessableCaptureSessionByConfigurations(API 24+)createCaptureSession(SessionConfiguration)(API 28+)
In each case, the output list is replaced with a list containing only the virtual surface, so the real camera does not feed the app’s surfaces.
-
CaptureRequest.Builder.addTarget
Replaces the app’s target surface with the virtual surface; the module records which surfaces were “preview” vs “reader” for its own use. -
CaptureRequest.Builder.build
Triggers the module’s playback logic: it starts VideoToFrames decoders for reader surfaces (feeding NV21/JPEG into the pipeline) and MediaPlayer for preview surfaces, both playingvirtual.mp4.
So for Camera2:
- Preview: App draws from the virtual surface; the module renders
virtual.mp4onto it (via MediaPlayer or decoder). - Capture/ImageReader: Frames come from the module’s video decoder output (or equivalent), not from the real camera.
- MediaExtractor + MediaCodec decode
virtual.mp4in a loop. - Output format is YUV (NV21) or JPEG depending on what the app’s
ImageReader/ pipeline expects (e.g. format 256 → JPEG). - Decoded frames are either:
- Rendered to a
Surface(e.g. for preview), or - Written into a shared buffer (
HookMain.data_buffer) that is then copied into the app’s preview/capture callbacks or surfaces.
- Rendered to a
- On
Instrumentation.callApplicationOnCreate(each app process start), the module gets the app’sApplicationcontext and:- If the app has no storage permission (or “force private dir” is set), it sets
video_pathto that app’s private directory (getExternalFilesDir(null)/Camera1/). - Otherwise it uses public
DCIM/Camera1/.
- If the app has no storage permission (or “force private dir” is set), it sets
- Toasts (e.g. “no video”, “path”, “recording not supported”) are shown in the target app’s context when
need_to_show_toastis true andno_toast.jpgis not present.
- Recording (MediaRecorder) is not intercepted. If the app starts recording, the module only shows a toast; the real camera is used for recording.
- Some Camera2 session types or high-speed/reprocess flows may not be fully covered on all devices.
- Preview/capture resolution and frame rate depend on the app; for best results,
virtual.mp4resolution/aspect should match what the app requests.
| Path | Role |
|---|---|
app/src/main/java/.../HookMain.java |
Xposed entry; hooks Camera/Camera2 and drives preview/capture replacement. |
app/src/main/java/.../VideoToFrames.java |
Decodes virtual.mp4 to frames (MediaCodec), outputs to Surface or byte buffer (NV21/JPEG). |
app/src/main/java/.../MainActivity.java |
UI for storage permission and toggles (disable, toasts, sound, private dir, force show). |
app/src/main/java/.../Logger.java |
Logging wrapper (e.g. android.util.Log with tag VCAM). |
app/src/main/assets/xposed_init |
Declares com.android.vcam.HookMain as the Xposed module entry class. |
- JDK 17, Android Gradle Plugin, compileSdk 34.
- Xposed API is compileOnly (
de.robv.android.xposed:api:82); no need to ship it in the APK.
./gradlew assembleReleaseOutput: app/build/outputs/apk/release/app-release.apk.
See LICENSE in the repository.