diff --git a/control.sh b/control.sh old mode 100755 new mode 100644 index 2668f70..cfcb347 --- a/control.sh +++ b/control.sh @@ -70,8 +70,18 @@ case "${1:-help}" in status) get_status_json ;; + stop) + log "stop: killing provider and feeder" + pkill -9 -f "/virtual_camera_provider" 2>/dev/null || true + pkill -9 -f "ffmpeg.*video0" 2>/dev/null || true + echo 1 > /data/local/tmp/virtualcam_disabled 2>/dev/null + log "stop: disabled flag set" + get_status_json + log "stop: done" + ;; start) - log "start: killing old provider" + log "start: removing disable flag" + rm -f /data/local/tmp/virtualcam_disabled 2>/dev/null pkill -9 -f "/virtual_camera_provider" 2>/dev/null || true mkdir -p "$LIB_DST" 2>/dev/null cp "$MODDIR/vendor/lib64"/*.so "$LIB_DST/" 2>/dev/null @@ -85,6 +95,7 @@ case "${1:-help}" in restart) log "restart: killing provider" pkill -9 -f "/virtual_camera_provider" 2>/dev/null || true + rm -f /data/local/tmp/virtualcam_disabled 2>/dev/null sleep 2 mkdir -p "$LIB_DST" 2>/dev/null cp "$MODDIR/vendor/lib64"/*.so "$LIB_DST/" 2>/dev/null @@ -95,13 +106,9 @@ case "${1:-help}" in get_status_json log "restart: done" ;; - stop) - log "stop: killing provider" - pkill -9 -f "/virtual_camera_provider" 2>/dev/null || true - log "stop: done" - echo '{"status":"stopped"}' - ;; start_feeder) + log "start_feeder: removing feeder disable flag" + rm -f /data/local/tmp/virtualcam_feeder_disabled 2>/dev/null log "start_feeder: loading config" load_config pkill -9 -f "ffmpeg.*video0" 2>/dev/null || true @@ -135,10 +142,11 @@ case "${1:-help}" in echo '{"status":"feeder_started"}' ;; stop_feeder) - log "stop_feeder: killing ffmpeg" + log "stop_feeder: killing ffmpeg and setting feeder disable flag" pkill -9 -f "ffmpeg.*video0" 2>/dev/null || true + echo 1 > /data/local/tmp/virtualcam_feeder_disabled 2>/dev/null log "stop_feeder: done" - echo '{"status":"feeder_stopped"}' + get_status_json ;; get_config) load_config @@ -170,17 +178,7 @@ case "${1:-help}" in )\"" echo "}" ;; - device_info) - dumpsys media.camera 2>/dev/null > /data/local/tmp/cam_dump.txt 2>/dev/null - F0=$(dumpsys media.camera 2>/dev/null | grep -A10 "virtual/0 (" | grep Facing | head -1) - F1=$(dumpsys media.camera 2>/dev/null | grep -A10 "virtual/1 (" | grep Facing | head -1) - L0=$(dumpsys media.camera 2>/dev/null | grep -A10 "virtual/0 (" | grep "FULL\|LEVEL" | head -1) - L1=$(dumpsys media.camera 2>/dev/null | grep -A10 "virtual/1 (" | grep "FULL\|LEVEL" | head -1) - echo "Camera0: Facing=$F0 Level=$L0" - echo "Camera1: Facing=$F1 Level=$L1" - echo "Full dump: /data/local/tmp/cam_dump.txt" - ;; *) - echo "Usage: $0 {status|start|stop|restart|start_feeder|stop_feeder|get_config|validate|device_info}" + echo "Usage: $0 {status|start|stop|restart|start_feeder|stop_feeder|get_config|validate}" ;; esac diff --git a/post-fs-data.sh b/post-fs-data.sh index cceaec6..417f130 100755 --- a/post-fs-data.sh +++ b/post-fs-data.sh @@ -39,7 +39,7 @@ cp "$MODDIR/vendor/bin/virtual_camera_provider" "$PROVIDER_DST" 2>/dev/null || t chmod 755 "$PROVIDER_DST" 2>/dev/null || true cp "$MODDIR/vendor/lib64"/*.so "$LIB_DST/" 2>/dev/null || true -# Start provider in background (failure won't block boot) +# Start provider in background (always at boot, disable flag is for runtime only) 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" diff --git a/service.sh b/service.sh index d030244..322ae0b 100755 --- a/service.sh +++ b/service.sh @@ -4,6 +4,7 @@ PROVIDER_DST="/data/local/tmp/virtual_camera_provider" LIB_DST="/data/local/tmp/virtualcam_libs" BOOT_LOG="$MODDIR/boot.log" MODULE_PROP="$MODDIR/module.prop" +DISABLE_FLAG="/data/local/tmp/virtualcam_disabled" log() { echo "[$(date)] $*" >> "$BOOT_LOG" @@ -11,40 +12,44 @@ log() { log "service.sh starting" +# Clear any stale disable flag from a previous session +# This ensures the module always starts fresh after boot +rm -f /data/local/tmp/virtualcam_disabled + # Wait for boot to settle sleep 30 -# Load v4l2loopback if not already loaded (safe here, boot is complete) +# Load v4l2loopback if not already loaded if ! lsmod 2>/dev/null | grep -q v4l2loopback; then log "service: loading v4l2loopback" insmod "$MODDIR/v4l2loopback.ko" devices=2 video_nr=0,1 card_label="VirtualCam" exclusive_caps=0 2>/dev/null && \ log "service: v4l2loopback loaded" || log "service: v4l2loopback load failed" fi -# Configure v4l2loopback for seamless loop transitions: -# sustain_framerate=1 repeats the last written frame when the writer pauses -# timeout=0 prevents kernel from injecting black timeout images -sleep 2 # Wait for device nodes to appear +# Configure v4l2loopback +sleep 2 if [ -c /dev/video0 ]; then v4l2-ctl -d /dev/video0 -c sustain_framerate=1 >/dev/null 2>&1 || true v4l2-ctl -d /dev/video0 -c timeout=0 >/dev/null 2>&1 || true - log "service: v4l2loopback configured (sustain_framerate=1, timeout=0)" + log "service: v4l2loopback configured" fi -# Start feeder FIRST so /dev/video0 has a format set before provider starts -# This prevents mmap EACCES in V4L2 reader -if [ -f "/data/local/tmp/virtualcam_config.json" ]; then - log "service: starting feeder before provider" - /system/bin/sh "$MODDIR/control.sh" start_feeder >/dev/null 2>&1 -fi +# Start feeder then provider at boot (unless disabled flag exists) +if [ ! -f "$DISABLE_FLAG" ]; then + if [ -f "/data/local/tmp/virtualcam_config.json" ]; then + log "service: starting feeder before provider" + /system/bin/sh "$MODDIR/control.sh" start_feeder >/dev/null 2>&1 + fi -# Now start provider (feeder already writing, so mmap will succeed) -if ! pgrep -f virtual_camera_provider >/dev/null 2>&1 && [ -x "$PROVIDER_DST" ]; then - log "service: starting provider" - nohup env LD_LIBRARY_PATH="$LIB_DST" "$PROVIDER_DST" \ - -d /dev/video0 -w 1920 -h 1080 -i virtual \ - /dev/null 2>&1 & - log "service: provider launched" + if ! pgrep -f virtual_camera_provider >/dev/null 2>&1 && [ -x "$PROVIDER_DST" ]; then + log "service: starting provider" + nohup env LD_LIBRARY_PATH="$LIB_DST" "$PROVIDER_DST" \ + -d /dev/video0 -w 1920 -h 1080 -i virtual \ + /dev/null 2>&1 & + log "service: provider launched" + fi +else + log "service: disabled flag exists, skipping auto-start" fi # Periodic watchdog + description update @@ -54,7 +59,6 @@ while true; do 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) - # Determine status if [ -n "$PROVIDER_PID" ] && [ "$SVC" = "1" ]; then STATUS="active" elif [ -n "$PROVIDER_PID" ] || [ "$SVC" = "1" ]; then @@ -78,21 +82,21 @@ while true; do fi fi - # Restart provider if dead - if [ -z "$PROVIDER_PID" ] && [ -x "$PROVIDER_DST" ]; then - log "service: provider died, restarting" - nohup env LD_LIBRARY_PATH="$LIB_DST" "$PROVIDER_DST" \ - -d /dev/video0 -w 1920 -h 1080 -i virtual \ - /dev/null 2>&1 & + # Only auto-restart if not disabled + if [ ! -f "$DISABLE_FLAG" ] && [ ! -f "/data/local/tmp/virtualcam_feeder_disabled" ]; then + if [ -z "$FEEDER_PID" ] && [ -f "/data/local/tmp/virtualcam_config.json" ]; then + log "service: auto-starting feeder" + /system/bin/sh "$MODDIR/control.sh" start_feeder >/dev/null 2>&1 + fi fi - # Auto-start feeder if not running and config exists - if [ -z "$FEEDER_PID" ] && [ -f "/data/local/tmp/virtualcam_config.json" ]; then - log "service: auto-starting feeder" - /system/bin/sh "$MODDIR/control.sh" start_feeder >/dev/null 2>&1 + if [ -z "$FEEDER_PID" ] && [ -f "/data/local/tmp/virtualcam_config.json" ]; then + log "service: auto-starting feeder" + /system/bin/sh "$MODDIR/control.sh" start_feeder >/dev/null 2>&1 + fi fi - # Memory check: kill provider/feeder if RSS exceeds 512MB (safety limit) + # Memory check for pid in $PROVIDER_PID $FEEDER_PID; do [ -z "$pid" ] && continue rss_kb=$(cat /proc/$pid/status 2>/dev/null | grep VmRSS | awk '{print $2}') @@ -102,14 +106,13 @@ while true; do fi done - # Check if stock HAL came back and kill it + # Kill stock HAL if it reappears STOCK_PIDS=$(ps -A | grep -i "camera.provider" | grep -v "virtual_camera_provider" | awk '{print $1}') if [ -n "$STOCK_PIDS" ]; then for pid in $STOCK_PIDS; do log "service: killing stock HAL PID $pid" kill -9 "$pid" 2>/dev/null || true done - # Try to stop the service via property if possible setprop ctl.stop vendor.camera-provider-2-7-google 2>/dev/null || true setprop ctl.stop vendor.camera-provider-pixel 2>/dev/null || true fi diff --git a/vendor/bin/virtual_camera_provider b/vendor/bin/virtual_camera_provider index fc9e0f7..4b60150 100755 Binary files a/vendor/bin/virtual_camera_provider and b/vendor/bin/virtual_camera_provider differ diff --git a/webroot/index.html b/webroot/index.html index c1b4c71..b87cb59 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -19,6 +19,5 @@ -
Device Info

Tap Refresh to load

Log
diff --git a/webroot/script.js b/webroot/script.js index 4258fdb..480e034 100644 --- a/webroot/script.js +++ b/webroot/script.js @@ -4,45 +4,34 @@ function log(m,t){const e=document.getElementById('console');if(!e)return;const function run(a){try{ksu.exec(CMD+' '+a);log(a+' OK','ok')}catch(e){log(a+' error: '+e.message,'err')}updateDash()} function updateDash(){try{ const r=typeof ksu!=='undefined'?ksu.exec(CMD+' status'):null; - if(!r||typeof r!=='string'){document.getElementById('status-dash').innerHTML='
No data
';return} + if(!r||typeof r!=='string'){document.getElementById('status-dash').innerHTML='
No data
';return} const s=JSON.parse(r); const d=document.getElementById('status-dash');if(!d)return; - const sc=s.status==='active'?'green':(s.status==='partial'?'yellow':'red'); - const st=s.status==='active'?'ACTIVE':(s.status==='partial'?'PARTIAL':'STOPPED'); - let v=[]; - if(s.camera2_level) v=[['camera2','green',s.camera2_level],['Checks',s.checks&&s.checks.includes('provider_ok')?'green':'red',s.checks||'?']]; - d.innerHTML=[['Status',sc,st],['Provider',s.provider_pid>0?'green':'red',s.provider_pid>0?'PID '+s.provider_pid:'Stopped'],['Feeder',s.feeder_pid>0?'green':'red',s.feeder_pid>0?'PID '+s.feeder_pid:'Stopped'],['Service',s.service_registered==1?'green':'red',s.service_registered==1?'Registered':'Missing'],['v4l2',s.v4l2_loaded==1?'green':'red',s.v4l2_loaded==1?'Loaded':'Missing'],['Device','yellow',s.video0||'N/A'],...v].map(i=>`
${i[0]}${i[2]}
`).join(''); - document.getElementById('subtitle').textContent=s.status==='active'?'Feeding video to /dev/video0':'Physical cameras NOT replaced'; + const colorMap={active:'green',partial:'yellow',stopped:'red',idle:'red'}; + const sc=colorMap[s.status]||'yellow'; + const st=s.status.toUpperCase(); + d.innerHTML=[['Status',sc,st],['Provider',s.provider_pid>0?'green':'red',s.provider_pid>0?'PID '+s.provider_pid:'Stopped'],['Feeder',s.feeder_pid>0?'green':'red',s.feeder_pid>0?'PID '+s.feeder_pid:'Stopped'],['Service',s.service_registered==1?'green':'red',s.service_registered==1?'Registered':'Missing'],['v4l2',s.v4l2_loaded==1?'green':'red',s.v4l2_loaded==1?'Loaded':'Missing'],['Device','yellow',s.video0||'N/A']].map(i=>`
${i[0]}${i[2]}
`).join(''); + document.getElementById('subtitle').textContent=s.status==='active'?'Physical cameras replaced':s.status==='partial'?'Starting up...':'Not running'; + // Toggle buttons based on state + document.getElementById('btn-start').style.display=s.provider_pid>0?'none':'block'; + document.getElementById('btn-stop').style.display=s.provider_pid>0?'block':'none'; + document.getElementById('btn-restart').style.display=s.provider_pid>0?'block':'none'; + document.getElementById('btn-feed').style.display=s.feeder_pid>0?'none':'block'; + document.getElementById('btn-unfeed').style.display=s.feeder_pid>0?'block':'none'; if(s.status!==_ps||s.provider_pid!==_pp||s.feeder_pid!==_pf){_ps=s.status;_pp=s.provider_pid;_pf=s.feeder_pid;log('Status: '+st+' pid='+s.provider_pid+' feeder='+s.feeder_pid,'ok')} }catch(e){log('Dash error: '+e.message,'err')}} -function loadDeviceInfo(){try{ - const r=ksu.exec(CMD+' device_info'); - document.getElementById('device-info').innerHTML=r?'
'+r.replace(//g,'>')+'
':'

No data

'; - log('Device info loaded','ok'); -}catch(e){log('DevInfo error: '+e.message,'err')}} document.getElementById('btn-start')?.addEventListener('click',()=>run('start')); document.getElementById('btn-stop')?.addEventListener('click',()=>run('stop')); document.getElementById('btn-restart')?.addEventListener('click',()=>run('restart')); document.getElementById('btn-feed')?.addEventListener('click',()=>run('start_feeder')); document.getElementById('btn-unfeed')?.addEventListener('click',()=>run('stop_feeder')); +document.getElementById('btn-refresh')?.addEventListener('click',updateDash); document.getElementById('btn-save')?.addEventListener('click',()=>{ const src=document.querySelector('.src-btn.active')?.dataset.src||'mp4'; const json=JSON.stringify({source_type:src,rtsp_url:document.getElementById('rtsp-url').value,mp4_path:document.getElementById('mp4-path').value,res_w:parseInt(document.getElementById('res-w').value)||1920,res_h:parseInt(document.getElementById('res-h').value)||1080,fps:parseInt(document.getElementById('fps').value)||30,rotation:parseInt(document.getElementById('rotation').value)||0}); ksu.exec("echo '"+json+"' > /data/local/tmp/virtualcam_config.json"); log('Config saved','ok'); }); -document.getElementById('btn-refresh')?.addEventListener('click',updateDash); -document.getElementById('btn-deviceinfo')?.addEventListener('click',loadDeviceInfo); -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'); -})); -updateDash(); -setInterval(updateDash,5000); -document.getElementById('btn-refresh')?.addEventListener('click',updateDash); -document.getElementById('btn-deviceinfo')?.addEventListener('click',loadDeviceInfo); document.querySelectorAll('.src-btn').forEach(b=>b.addEventListener('click',()=>{ document.querySelectorAll('.src-btn').forEach(x=>x.classList.remove('active')); b.classList.add('active');