commit ef0d17680fe653e8702107da46f047d03148bdd3 Author: JorySeverijnse Date: Thu May 14 19:22:48 2026 +0200 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..1de69e1 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# Virtual Camera HAL — KernelSU Module + +Replaces physical cameras on Pixel 9A (Android 16) with virtual cameras +backed by v4l2loopback, fed via RTSP stream or MP4 file. + +## Architecture + +``` +Boot → post-fs-data.sh → insmod v4l2loopback.ko → /dev/video0 + → copy binary + AIDL .so files → /data/local/tmp/ + → start virtual_camera_provider with LD_LIBRARY_PATH + → registers ICameraProvider/virtual/0 + → cameraserver discovers 2 virtual cameras +``` + +## Files + +| Path | Purpose | +|------|---------| +| `module.prop` | KernelSU metadata | +| `post-fs-data.sh` | Boot startup (insmod, copy, start) | +| `service.sh` | Watchdog (30s delay) | +| `sepolicy.rule` | SELinux permissions | +| `v4l2loopback.ko` | Kernel module (0x440 struct, ARM64) | +| `vendor/bin/virtual_camera_provider` | AOSP-built AIDL HAL3 binary | +| `vendor/lib64/*.so` | AIDL NDK shared libraries (V3) | +| `system/etc/vintf/manifest/*.xml` | VINTF manifest overlay | +| `vendor/etc/vintf/manifest/*.xml` | VINTF manifest overlay (vendor) | +| `control.sh` | WebUI backend | +| `rtsp_feeder.sh` | RTSP/MP4 → /dev/video0 bridge | +| `webui/` | KSU WebUI (index.html, style.css, script.js) | + +## Usage + +1. Install module via KernelSU app or `ksud module install` +2. Reboot +3. Open KernelSU → Modules → Virtual Camera → WebUI +4. Configure RTSP URL or MP4 file path +5. Tap "Start" (provides 2 virtual cameras at IDs 10 and 11) + +## Verification + +```bash +# Check provider registered +adb shell su -c 'service list | grep camera.provider' +# Expected: ICameraProvider/virtual/0 + +# Check virtual cameras +adb shell su -c 'dumpsys media.camera | grep "Number of camera"' +# Expected: Number of camera devices: 4 + +# Check camera characteristics +adb shell su -c 'dumpsys media.camera | grep -A25 "virtual/10"' +``` + +## Building + +```bash +cd /mnt/opslag/aosp-16-platform +export TARGET_RELEASE=bp2a +source build/envsetup.sh +lunch aosp_arm64-bp2a-userdebug +cd external/virtualcam && mma -j10 +``` + +## Key Fixes + +- **-3 registration error**: Overrode `createBinder()` to skip + `AIBinder_markVintfStability` which caused STATUS_BAD_VALUE +- **CANNOT LINK**: Bundled AIDL V3 .so files with module +- **AOSP build memory**: Use `-j10` for 10 cores max, incremental builds + are fast after initial Soong analysis diff --git a/control.sh b/control.sh new file mode 100755 index 0000000..fff2ecc --- /dev/null +++ b/control.sh @@ -0,0 +1,62 @@ +#!/system/bin/sh +CONFIG_FILE="/data/local/tmp/virtualcam_config.json" +MODDIR=$(dirname "$0") + +load_config() { + [ -f "$CONFIG_FILE" ] && . /data/adb/modules/alias/system/bin/shflags 2>/dev/null || true + SOURCE_TYPE=$(grep -o '"source_type":"[^"]*"' "$CONFIG_FILE" 2>/dev/null | cut -d'"' -f4) + RTSP_URL=$(grep -o '"rtsp_url":"[^"]*"' "$CONFIG_FILE" 2>/dev/null | cut -d'"' -f4) + MP4_PATH=$(grep -o '"mp4_path":"[^"]*"' "$CONFIG_FILE" 2>/dev/null | cut -d'"' -f4) + RES_W=$(grep -o '"res_w":[0-9]*' "$CONFIG_FILE" 2>/dev/null | cut -d: -f2) + RES_H=$(grep -o '"res_h":[0-9]*' "$CONFIG_FILE" 2>/dev/null | cut -d: -f2) + FPS=$(grep -o '"fps":[0-9]*' "$CONFIG_FILE" 2>/dev/null | cut -d: -f2) + : ${SOURCE_TYPE:=rtsp} ${RTSP_URL:=rtsp://192.168.1.100:8554/stream} + : ${MP4_PATH:=/sdcard/Download/video.mp4} ${RES_W:=1920} ${RES_H:=1080} ${FPS:=30} +} + +case "${1:-help}" in + status) + PROVIDER_PID=$(pgrep -f virtual_camera_provider | head -1) + FEEDER_PID=$(pgrep -f "ffmpeg.*video0\|gst-launch.*video0" | head -1) + V4L2=$(lsmod 2>/dev/null | grep -q v4l2loopback && echo 1 || echo 0) + SVC=$(service list 2>/dev/null | grep -q "virtual/0" && echo 1 || echo 0) + VIDEO=$(cat /sys/class/video4linux/video0/name 2>/dev/null || echo "not found") + echo "{\"provider_pid\":${PROVIDER_PID:-0},\"feeder_pid\":${FEEDER_PID:-0},\"v4l2_loaded\":$V4L2,\"service_registered\":$SVC,\"video0\":\"$VIDEO\"}" + ;; + start) + insmod "$MODDIR/v4l2loopback.ko" devices=1 video_nr=0 card_label="VirtualCam" exclusive_caps=1 2>/dev/null || true + pkill -9 -f virtual_camera_provider 2>/dev/null || true + "$MODDIR/vendor/bin/virtual_camera_provider" -d /dev/video0 -w 1920 -h 1080 -i virtual & + echo '{"status":"started"}' + ;; + stop) + pkill -9 -f virtual_camera_provider 2>/dev/null || true + echo '{"status":"stopped"}' + ;; + start_feeder) + load_config + pkill -9 -f "ffmpeg.*video0\|gst-launch.*video0" 2>/dev/null || true + if [ "$SOURCE_TYPE" = "rtsp" ]; then + ffmpeg -rtsp_transport tcp -i "$RTSP_URL" -f v4l2 -input_format nv21 \ + -video_size "${RES_W}x${RES_H}" -framerate "$FPS" -pix_fmt nv21 /dev/video0 & + elif [ "$SOURCE_TYPE" = "mp4" -a -f "$MP4_PATH" ]; then + ffmpeg -stream_loop -1 -re -i "$MP4_PATH" -f v4l2 -input_format nv21 \ + -video_size "${RES_W}x${RES_H}" -framerate "$FPS" -pix_fmt nv21 /dev/video0 & + fi + echo '{"status":"feeder_started"}' + ;; + stop_feeder) + pkill -9 -f "ffmpeg.*video0\|gst-launch.*video0" 2>/dev/null || true + echo '{"status":"feeder_stopped"}' + ;; + get_config) + load_config + echo "{\"source_type\":\"$SOURCE_TYPE\",\"rtsp_url\":\"$RTSP_URL\",\"mp4_path\":\"$MP4_PATH\",\"res_w\":$RES_W,\"res_h\":$RES_H,\"fps\":$FPS}" + ;; + set_config|save) + cat > "$CONFIG_FILE" << DATA +{"source_type":"$2","rtsp_url":"$3","mp4_path":"$4","res_w":$5,"res_h":$6,"fps":$7} +DATA + echo '{"status":"saved"}' + ;; +esac diff --git a/module.prop b/module.prop new file mode 100644 index 0000000..f9b86b9 --- /dev/null +++ b/module.prop @@ -0,0 +1,6 @@ +id=virtualcam +name=Virtual Camera (RTSP/MP4) +version=3.0 +versionCode=3 +author=sisyphus +description=Replaces Pixel 9A cameras with virtual cameras backed by v4l2loopback. Feeds RTSP stream or MP4 video. FULL hardware level with 25+ metadata entries, FPS ranges, AE/AWB/AF modes, stabilization. WebUI for configuration. AOSP-built AIDL HAL3 provider. diff --git a/post-fs-data.sh b/post-fs-data.sh new file mode 100755 index 0000000..b2a74f0 --- /dev/null +++ b/post-fs-data.sh @@ -0,0 +1,53 @@ +#!/system/bin/sh +MODDIR=${0%/*} +exec >> "$MODDIR/boot.log" 2>&1 +echo "[$(date)] post-fs-data starting" + +# Stop the stock Google camera HAL so it doesn't re-register its cameras. +# This is an APEX-managed service that auto-restarts, so we need to keep it down. +setprop ctl.stop vendor.camera-provider-2-7-google 2>/dev/null || true +kill -9 $(pgrep -f camera-provider-2-7-google) 2>/dev/null || true + +# Reload v4l2loopback (rmmod first to clear stale params like exclusive_caps) +rmmod v4l2loopback 2>/dev/null || true +sleep 1 +insmod "$MODDIR/v4l2loopback.ko" devices=1 video_nr=0 card_label="VirtualCam" 2>/dev/null || true + +# Copy provider binary and AIDL libs +PROVIDER_DST="/data/local/tmp/virtual_camera_provider" +LIB_DST="/data/local/tmp/virtualcam_libs" +cp "$MODDIR/vendor/bin/virtual_camera_provider" "$PROVIDER_DST" 2>/dev/null || true +chmod 755 "$PROVIDER_DST" 2>/dev/null || true +mkdir -p "$LIB_DST" 2>/dev/null || true +cp "$MODDIR/vendor/lib64"/*.so "$LIB_DST/" 2>/dev/null || true + +# Start our provider +if [ -x "$PROVIDER_DST" ]; then + nohup env LD_LIBRARY_PATH="$LIB_DST" "$PROVIDER_DST" \ + -d /dev/video0 -w 1920 -h 1080 -i virtual \ + /dev/null 2>&1 & + echo "[$(date)] provider started" +fi + +# Fork a background watchdog to keep the stock HAL dead +( + while sleep 30; do + # Re-kill stock HAL if it came back + if pgrep -f camera-provider-2-7-google >/dev/null 2>&1; then + setprop ctl.stop vendor.camera-provider-2-7-google 2>/dev/null || true + kill -9 $(pgrep -f camera-provider-2-7-google) 2>/dev/null || true + echo "[$(date)] re-killed stock HAL" + fi + # Restart our provider if it died + if ! pgrep -f virtual_camera_provider >/dev/null 2>&1; then + if [ -x "$PROVIDER_DST" ]; then + nohup env LD_LIBRARY_PATH="$LIB_DST" "$PROVIDER_DST" \ + -d /dev/video0 -w 1920 -h 1080 -i virtual \ + /dev/null 2>&1 & + echo "[$(date)] provider restarted" + fi + fi + done +) & + +echo "[$(date)] post-fs-data done" \ No newline at end of file diff --git a/rtsp_feeder.sh b/rtsp_feeder.sh new file mode 100755 index 0000000..2456a29 --- /dev/null +++ b/rtsp_feeder.sh @@ -0,0 +1,16 @@ +#!/system/bin/sh +# Launched from WebUI. Never runs at boot. +RTSP_URL="${RTSP_URL:-rtsp://192.168.1.100:8554/stream}" +V4L2_DEVICE="${V4L2_DEVICE:-/dev/video0}" +WIDTH=1920; HEIGHT=1080; FPS=30 +[ -e "$V4L2_DEVICE" ] || exit 1 + +if command -v ffmpeg >/dev/null 2>&1; then + exec ffmpeg -rtsp_transport tcp -i "$RTSP_URL" \ + -f v4l2 -input_format nv21 -video_size "${WIDTH}x${HEIGHT}" \ + -framerate "$FPS" -pix_fmt nv21 "$V4L2_DEVICE" +elif command -v gst-launch-1.0 >/dev/null 2>&1; then + exec gst-launch-1.0 rtspsrc location="$RTSP_URL" latency=0 ! \ + rtph264depay ! h264parse ! v4l2h264dec ! v4l2sink device="$V4L2_DEVICE" +fi +exit 1 diff --git a/sepolicy.rule b/sepolicy.rule new file mode 100644 index 0000000..7fbcd57 --- /dev/null +++ b/sepolicy.rule @@ -0,0 +1,4 @@ +# Only what's strictly needed. No broad "allow init ..." rules. +allow virtual_camera_provider video_device:chr_file { read write ioctl open }; +allow virtual_camera_provider servicemanager:binder { call transfer }; +allow cameraserver virtual_camera_provider:binder { call transfer }; diff --git a/service.sh b/service.sh new file mode 100755 index 0000000..b8c0c4d --- /dev/null +++ b/service.sh @@ -0,0 +1,10 @@ +#!/system/bin/sh +PROVIDER_DST="/data/local/tmp/virtual_camera_provider" +LIB_DST="/data/local/tmp/virtualcam_libs" +sleep 30 +if ! pgrep -f virtual_camera_provider >/dev/null 2>&1 && [ -x "$PROVIDER_DST" ]; then + nohup env LD_LIBRARY_PATH="$LIB_DST" "$PROVIDER_DST" \ + -d /dev/video0 -w 1920 -h 1080 -i virtual \ + /dev/null 2>&1 & +fi +exit 0 \ No newline at end of file diff --git a/system/etc/vintf/manifest/virtual_camera_manifest.xml b/system/etc/vintf/manifest/virtual_camera_manifest.xml new file mode 100644 index 0000000..0bd6615 --- /dev/null +++ b/system/etc/vintf/manifest/virtual_camera_manifest.xml @@ -0,0 +1,8 @@ + + + + android.hardware.camera.provider + 1 + ICameraProvider/virtual/0 + + diff --git a/v4l2loopback.ko b/v4l2loopback.ko new file mode 100755 index 0000000..539180c Binary files /dev/null and b/v4l2loopback.ko differ diff --git a/vendor/bin/virtual_camera_provider b/vendor/bin/virtual_camera_provider new file mode 100755 index 0000000..054041a Binary files /dev/null and b/vendor/bin/virtual_camera_provider differ diff --git a/vendor/etc/vintf/manifest/virtual_camera_manifest.xml b/vendor/etc/vintf/manifest/virtual_camera_manifest.xml new file mode 100644 index 0000000..0bd6615 --- /dev/null +++ b/vendor/etc/vintf/manifest/virtual_camera_manifest.xml @@ -0,0 +1,8 @@ + + + + android.hardware.camera.provider + 1 + ICameraProvider/virtual/0 + + diff --git a/vendor/lib64/android.hardware.camera.common-V1-ndk.so b/vendor/lib64/android.hardware.camera.common-V1-ndk.so new file mode 100755 index 0000000..f0d0c7e Binary files /dev/null and b/vendor/lib64/android.hardware.camera.common-V1-ndk.so differ diff --git a/vendor/lib64/android.hardware.camera.device-V3-ndk.so b/vendor/lib64/android.hardware.camera.device-V3-ndk.so new file mode 100755 index 0000000..17510ce Binary files /dev/null and b/vendor/lib64/android.hardware.camera.device-V3-ndk.so differ diff --git a/vendor/lib64/android.hardware.camera.metadata-V3-ndk.so b/vendor/lib64/android.hardware.camera.metadata-V3-ndk.so new file mode 100755 index 0000000..4b34e9d Binary files /dev/null and b/vendor/lib64/android.hardware.camera.metadata-V3-ndk.so differ diff --git a/vendor/lib64/android.hardware.camera.provider-V3-ndk.so b/vendor/lib64/android.hardware.camera.provider-V3-ndk.so new file mode 100755 index 0000000..31b9ec4 Binary files /dev/null and b/vendor/lib64/android.hardware.camera.provider-V3-ndk.so differ diff --git a/vendor/lib64/android.hardware.common-V2-ndk.so b/vendor/lib64/android.hardware.common-V2-ndk.so new file mode 100755 index 0000000..ffdeebd Binary files /dev/null and b/vendor/lib64/android.hardware.common-V2-ndk.so differ diff --git a/vendor/lib64/android.hardware.common.fmq-V1-ndk.so b/vendor/lib64/android.hardware.common.fmq-V1-ndk.so new file mode 100755 index 0000000..aaa176d Binary files /dev/null and b/vendor/lib64/android.hardware.common.fmq-V1-ndk.so differ diff --git a/vendor/lib64/android.hardware.graphics.common-V6-ndk.so b/vendor/lib64/android.hardware.graphics.common-V6-ndk.so new file mode 100755 index 0000000..74f33a7 Binary files /dev/null and b/vendor/lib64/android.hardware.graphics.common-V6-ndk.so differ diff --git a/webui/index.html b/webui/index.html new file mode 100644 index 0000000..297ecd6 --- /dev/null +++ b/webui/index.html @@ -0,0 +1,43 @@ + + +Virtual Camera + + +
+
Virtual Camera
Replace physical cameras
+
+
Status
+
+
+
+
Video Source
+
+ + +
+
+ +
+
+
+
+
+ +
+
+
Controls
+
+ + + + + +
+
+
+
Log
+
+
+
+ + diff --git a/webui/script.js b/webui/script.js new file mode 100644 index 0000000..3c475ec --- /dev/null +++ b/webui/script.js @@ -0,0 +1,53 @@ +const CMD='/data/adb/modules/virtualcam/control.sh'; +function log(m,t){const e=document.getElementById('console');const d=new Date(); + e.innerHTML+=`
[${d.toLocaleTimeString()}] ${m}
`;e.scrollTop=e.scrollHeight} +async function exec(c){try{return await KSU.exec(c)}catch(e){log('exec error','err');return null}} + +async function status(){ + const r=await exec(CMD+' status');if(!r)return; + try{ + const s=JSON.parse(r);const d=document.getElementById('status-dash'); + const items=[['Provider',s.provider_pid>0?'green':'red',s.provider_pid>0?'Running':'Stopped'], + ['Feeder',s.feeder_pid>0?'green':'red',s.feeder_pid>0?'Running':'Stopped'], + ['Service',s.service_registered==1?'green':'red',s.service_registered==1?'Registered':'Not found'], + ['v4l2',s.v4l2_loaded==1?'green':'red',s.v4l2_loaded==1?'Loaded':'Missing'], + ['Device','yellow',s.video0||'N/A']]; + d.innerHTML=items.map(i=>`
${i[0]}${i[2]}
`).join(''); + log('Status updated','ok'); + }catch(e){log('Parse error','err')} +} + +document.addEventListener('DOMContentLoaded',()=>{ + status(); + document.querySelectorAll('.src-btn').forEach(b=>{ + b.addEventListener('click',()=>{ + document.querySelectorAll('.src-btn').forEach(x=>x.classList.remove('active')); + b.classList.add('active'); + document.getElementById('rtsp-config').classList.toggle('hidden',b.dataset.src!=='rtsp'); + document.getElementById('mp4-config').classList.toggle('hidden',b.dataset.src!=='mp4'); + }); + }); + document.getElementById('btn-save').addEventListener('click',async()=>{ + const src=document.querySelector('.src-btn.active').dataset.src; + const rtsp=document.getElementById('rtsp-url').value; + const mp4=document.getElementById('mp4-path').value; + const w=document.getElementById('res-w').value||1920; + const h=document.getElementById('res-h').value||1080; + const f=document.getElementById('fps').value||30; + await exec(`echo '{"source_type":"${src}","rtsp_url":"${rtsp}","mp4_path":"${mp4}","res_w":${w},"res_h":${h},"fps":${f}}' > /data/local/tmp/virtualcam_config.json`); + log('Config saved','ok'); + }); + document.getElementById('btn-start').addEventListener('click',async()=>{ + log('Starting provider...','info');await exec(CMD+' start');log('Provider started','ok');setTimeout(status,2000); + }); + document.getElementById('btn-stop').addEventListener('click',async()=>{ + log('Stopping...','info');await exec(CMD+' stop');log('Stopped','ok');status(); + }); + document.getElementById('btn-feed').addEventListener('click',async()=>{ + log('Starting feeder...','info');await exec(CMD+' start_feeder');log('Feeder started','ok');setTimeout(status,2000); + }); + document.getElementById('btn-unfeed').addEventListener('click',async()=>{ + log('Stopping feeder...','info');await exec(CMD+' stop_feeder');log('Feeder stopped','ok');status(); + }); + document.getElementById('btn-refresh').addEventListener('click',status); +}); diff --git a/webui/style.css b/webui/style.css new file mode 100644 index 0000000..3ad6bdd --- /dev/null +++ b/webui/style.css @@ -0,0 +1,27 @@ +*{box-sizing:border-box;margin:0;padding:0} +body{font-family:-apple-system,sans-serif;background:#0d1117;color:#c9d1d9;padding:16px;max-width:480px;margin:0 auto} +.container{display:flex;flex-direction:column;gap:12px} +header{text-align:center;padding:12px 0} +.title{font-size:22px;font-weight:700;color:#58a6ff} +.subtitle{font-size:13px;color:#8b949e} +.card{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:14px} +.card-title{font-size:14px;font-weight:600;color:#58a6ff;margin-bottom:10px} +.dashboard{display:grid;grid-template-columns:1fr 1fr;gap:8px} +.stat{background:#0d1117;border-radius:6px;padding:8px;text-align:center} +.stat .l{font-size:11px;color:#8b949e;display:block} +.stat .v{font-size:15px;font-weight:600;display:block} +.green{color:#3fb950}.red{color:#f85149}.yellow{color:#d29922} +.source-selector{display:flex;gap:8px;margin-bottom:12px} +.src-btn{flex:1;padding:10px;border:1px solid #30363d;border-radius:6px;background:#0d1117;color:#8b949e;cursor:pointer} +.src-btn.active{background:#1f6feb33;border-color:#1f6feb;color:#58a6ff} +.hidden{display:none} +label{font-size:12px;color:#8b949e;display:block;margin-bottom:4px} +input{width:100%;padding:10px;border:1px solid #30363d;border-radius:6px;background:#0d1117;color:#c9d1d9;font-size:14px} +.row{display:flex;gap:8px;margin-bottom:12px}.row>div{flex:1} +.num{width:100%} +.primary{width:100%;padding:12px;border:none;border-radius:6px;background:#238636;color:#fff;font-size:14px;font-weight:600;cursor:pointer} +.grid{display:grid;grid-template-columns:1fr 1fr;gap:8px} +.btn{padding:12px;border:1px solid #30363d;border-radius:6px;background:#21262d;color:#c9d1d9;cursor:pointer;text-align:center} +.btn.danger{border-color:#f8514944;color:#f85149} +.console{background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:10px;height:150px;overflow-y:auto;font-family:monospace;font-size:12px;color:#8b949e} +.console .ok{color:#3fb950}.console .err{color:#f85149}.console .info{color:#58a6ff}