Initial commit

This commit is contained in:
2026-05-14 19:22:48 +02:00
commit ef0d17680f
21 changed files with 362 additions and 0 deletions
+72
View File
@@ -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
Executable
+62
View File
@@ -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
+6
View File
@@ -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.
+53
View File
@@ -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 >/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 >/dev/null 2>&1 &
echo "[$(date)] provider restarted"
fi
fi
done
) &
echo "[$(date)] post-fs-data done"
Executable
+16
View File
@@ -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
+4
View File
@@ -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 };
Executable
+10
View File
@@ -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 >/dev/null 2>&1 &
fi
exit 0
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest version="2.0" type="device">
<hal format="aidl">
<name>android.hardware.camera.provider</name>
<version>1</version>
<fqname>ICameraProvider/virtual/0</fqname>
</hal>
</manifest>
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest version="2.0" type="device">
<hal format="aidl">
<name>android.hardware.camera.provider</name>
<version>1</version>
<fqname>ICameraProvider/virtual/0</fqname>
</hal>
</manifest>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+43
View File
@@ -0,0 +1,43 @@
<!DOCTYPE html><html lang="en"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Virtual Camera</title>
<link rel="stylesheet" href="style.css">
</head><body>
<div class="container">
<header><div class="title">Virtual Camera</div><div class="subtitle">Replace physical cameras</div></header>
<div class="card">
<div class="card-title">Status</div>
<div class="dashboard" id="status-dash"></div>
</div>
<div class="card">
<div class="card-title">Video Source</div>
<div class="source-selector">
<button class="src-btn active" data-src="rtsp">RTSP</button>
<button class="src-btn" data-src="mp4">MP4</button>
</div>
<div id="rtsp-config"><label>RTSP URL</label><input type="text" id="rtsp-url" placeholder="rtsp://192.168.1.100:8554/stream"></div>
<div id="mp4-config" class="hidden"><label>MP4 Path</label><input type="text" id="mp4-path" placeholder="/sdcard/Download/video.mp4"></div>
<div class="row">
<div><label>Width</label><input type="number" id="res-w" value="1920" class="num"></div>
<div><label>Height</label><input type="number" id="res-h" value="1080" class="num"></div>
<div><label>FPS</label><input type="number" id="fps" value="30" class="num"></div>
</div>
<button id="btn-save" class="primary">Save</button>
</div>
<div class="card">
<div class="card-title">Controls</div>
<div class="grid">
<button id="btn-start" class="btn">Start Provider</button>
<button id="btn-stop" class="btn danger">Stop</button>
<button id="btn-feed" class="btn">Start Feeder</button>
<button id="btn-unfeed" class="btn danger">Stop Feeder</button>
<button id="btn-refresh" class="btn">Refresh</button>
</div>
</div>
<div class="card">
<div class="card-title">Log</div>
<div id="console" class="console"></div>
</div>
</div>
<script src="script.js"></script>
</body></html>
+53
View File
@@ -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+=`<div class="${t||'log'}">[${d.toLocaleTimeString()}] ${m}</div>`;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=>`<div class="stat"><span class="l">${i[0]}</span><span class="v ${i[1]}">${i[2]}</span></div>`).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);
});
+27
View File
@@ -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}