diff --git a/control.sh b/control.sh index 49c2727..2668f70 100755 --- a/control.sh +++ b/control.sh @@ -3,6 +3,13 @@ CONFIG_FILE="/data/local/tmp/virtualcam_config.json" MODDIR=$(dirname "$0") LIB_DST="/data/local/tmp/virtualcam_libs" FFMPEG_BIN="/data/local/tmp/ffmpeg" +MODULE_PROP="$MODDIR/module.prop" +BOOT_LOG="$MODDIR/boot.log" +PROVIDER_DST="/data/local/tmp/virtual_camera_provider" + +log() { + echo "[$(date)] $*" >> "$BOOT_LOG" +} load_config() { [ -f "$CONFIG_FILE" ] || return @@ -12,73 +19,168 @@ load_config() { 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) + ROTATION=$(grep -o '"rotation":[0-9]*' "$CONFIG_FILE" 2>/dev/null | cut -d: -f2) : ${SOURCE_TYPE:=mp4} ${RTSP_URL:=rtsp://192.168.1.100:8554/stream} - : ${MP4_PATH:=/sdcard/Movies/2.mp4} ${RES_W:=1920} ${RES_H:=1080} ${FPS:=30} + : ${MP4_PATH:=/sdcard/Movies/2.mp4} ${RES_W:=1920} ${RES_H:=1080} ${FPS:=30} ${ROTATION:=0} +} + +update_description() { + local status="$1" + local desc + case "$status" in + active) desc="[ACTIVE] Virtual Camera - Physical cameras replaced" ;; + idle) desc="[IDLE] Virtual Camera - Ready to start" ;; + error) desc="[ERROR] Virtual Camera - Check config and logs" ;; + partial) desc="[PARTIAL] Virtual Camera - Starting up..." ;; + *) desc="Virtual Camera HAL3 - Replaces Pixel 9A cameras" ;; + esac + if [ -f "$MODULE_PROP" ]; then + sed -i "s/^description=.*/description=$desc/" "$MODULE_PROP" + fi +} + +get_status_json() { + PROVIDER_PID=$(pgrep -f "/virtual_camera_provider" | head -1) + FEEDER_PID=$(pgrep -f "ffmpeg.*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") + CHECKS="provider_$([ -n "$PROVIDER_PID" ] && echo ok || echo missing),service_$([ "$SVC" = "1" ] && echo ok || echo missing),v4l2_$([ "$V4L2" = "1" ] && echo ok || echo missing),feeder_$([ -n "$FEEDER_PID" ] && echo ok || echo stopped)" + if [ "${PROVIDER_PID:-0}" -gt 0 ] && [ "${SVC:-0}" = "1" ]; then + STATUS="active" + elif [ "${PROVIDER_PID:-0}" -gt 0 ] || [ "${SVC:-0}" = "1" ]; then + STATUS="partial" + else + STATUS="idle" + fi + echo "{\"provider_pid\":${PROVIDER_PID:-0},\"feeder_pid\":${FEEDER_PID:-0},\"v4l2_loaded\":$V4L2,\"service_registered\":$SVC,\"video0\":\"$VIDEO\",\"status\":\"$STATUS\",\"camera2_level\":\"FULL\",\"checks\":\"$CHECKS\"}" +} + +get_vf_rotation() { + local rot="${ROTATION:-0}" + case "$rot" in + 90) echo "transpose=1," ;; + 180) echo "transpose=1,transpose=1," ;; + 270) echo "transpose=2," ;; + *) echo "" ;; + esac } case "${1:-help}" in status) - PROVIDER_PID=$(pgrep -f "virtual_camera_provider$" | head -1) - FEEDER_PID=$(pgrep -f "ffmpeg.*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\"}" + get_status_json ;; start) - rmmod v4l2loopback 2>/dev/null; sleep 1 - insmod "$MODDIR/v4l2loopback.ko" devices=1 video_nr=0 card_label="VirtualCam" 2>/dev/null || true - pkill -9 -f "virtual_camera_provider$" 2>/dev/null || true + log "start: killing old provider" + 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 - nohup env LD_LIBRARY_PATH="$LIB_DST" "$MODDIR/vendor/bin/virtual_camera_provider" \ - -d /dev/video0 -w 1920 -h 1080 -i virtual > /dev/null 2>&1 & - echo '{"status":"started"}' - ;; - stop) - pkill -9 -f "virtual_camera_provider$" 2>/dev/null || true - pkill -9 cameraserver 2>/dev/null || true - echo '{"status":"stopped"}' + log "start: launching provider" + env LD_LIBRARY_PATH="$LIB_DST" "$MODDIR/vendor/bin/virtual_camera_provider" \ + -d /dev/video0 -w 1920 -h 1080 -i virtual & + sleep 2 + get_status_json + log "start: done" ;; restart) - pkill -9 -f "virtual_camera_provider$" 2>/dev/null || true - pkill -9 cameraserver 2>/dev/null || true + log "restart: killing provider" + pkill -9 -f "/virtual_camera_provider" 2>/dev/null || true sleep 2 mkdir -p "$LIB_DST" 2>/dev/null cp "$MODDIR/vendor/lib64"/*.so "$LIB_DST/" 2>/dev/null + log "restart: launching provider" nohup env LD_LIBRARY_PATH="$LIB_DST" "$MODDIR/vendor/bin/virtual_camera_provider" \ -d /dev/video0 -w 1920 -h 1080 -i virtual > /dev/null 2>&1 & - echo '{"status":"restarted"}' + sleep 2 + 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: loading config" load_config pkill -9 -f "ffmpeg.*video0" 2>/dev/null || true + sleep 1 if command -v "$FFMPEG_BIN" >/dev/null 2>&1; then FF="$FFMPEG_BIN" elif command -v ffmpeg >/dev/null 2>&1; then FF="ffmpeg" else + log "start_feeder: ERROR ffmpeg not found" echo '{"status":"error","message":"ffmpeg not found"}' exit 1 fi + VFILTER=$(get_vf_rotation) + log "start_feeder: source=$SOURCE_TYPE, ff=$FF, rot=$ROTATION, ${RES_W}x${RES_H}" if [ "$SOURCE_TYPE" = "rtsp" ]; then nohup "$FF" -rtsp_transport tcp -i "$RTSP_URL" \ + -vf "${VFILTER}scale=${RES_W}:${RES_H},fps=${FPS:-30}" \ -f v4l2 -pix_fmt yuv420p /dev/video0 > /dev/null 2>&1 & elif [ "$SOURCE_TYPE" = "mp4" ] && [ -f "$MP4_PATH" ]; then nohup "$FF" -stream_loop -1 -re -i "$MP4_PATH" \ + -vf "${VFILTER}scale=${RES_W}:${RES_H},fps=${FPS:-30}" \ -f v4l2 -pix_fmt yuv420p /dev/video0 > /dev/null 2>&1 & + else + log "start_feeder: ERROR source=$SOURCE_TYPE path=$MP4_PATH not found" + echo '{"status":"error","message":"source not found or invalid"}' + exit 1 fi + sleep 1 + log "start_feeder: done" echo '{"status":"feeder_started"}' ;; stop_feeder) + log "stop_feeder: killing ffmpeg" pkill -9 -f "ffmpeg.*video0" 2>/dev/null || true + log "stop_feeder: done" 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:-1920},\"res_h\":${RES_H:-1080},\"fps\":${FPS:-30}}" + echo "{\"source_type\":\"$SOURCE_TYPE\",\"rtsp_url\":\"$RTSP_URL\",\"mp4_path\":\"$MP4_PATH\",\"res_w\":${RES_W:-1920},\"res_h\":${RES_H:-1080},\"fps\":${FPS:-30},\"rotation\":${ROTATION:-0}}" + ;; + validate) + PROVIDER_PID=$(pgrep -f "/virtual_camera_provider" | head -1) + FEEDER_PID=$(pgrep -f "ffmpeg.*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) + VSTATE=$(cat /sys/devices/virtual/video4linux/video0/state 2>/dev/null || echo "unknown") + VFORMAT=$(cat /sys/devices/virtual/video4linux/video0/format 2>/dev/null || echo "unknown") + CAM_DUMP=$(dumpsys media.camera 2>/dev/null | grep -c "virtual/0" || true) + log "validate: provider=$PROVIDER_PID feeder=$FEEDER_PID v4l2=$V4L2 svc=$SVC state=$VSTATE fmt=$VFORMAT cam_count=$CAM_DUMP" + echo "{" + echo " \"provider_pid\":${PROVIDER_PID:-0}," + echo " \"feeder_pid\":${FEEDER_PID:-0}," + echo " \"v4l2_loaded\":$V4L2," + echo " \"service_registered\":$SVC," + echo " \"v4l2_state\":\"$VSTATE\"," + echo " \"v4l2_format\":\"$VFORMAT\"," + echo " \"camera2_devices\":$CAM_DUMP," + echo " \"camera2_level\":\"FULL\"," + echo " \"all_checks\":\"$( \ + [ -n "$PROVIDER_PID" ] && echo -n provider_ok || echo -n provider_missing; \ + [ "$SVC" = "1" ] && echo -n ,service_ok || echo -n ,service_missing; \ + [ "$V4L2" = "1" ] && echo -n ,v4l2_ok || echo -n ,v4l2_missing; \ + [ -n "$FEEDER_PID" ] && echo -n ,feeder_ok || echo -n ,feeder_stopped; \ + )\"" + 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}" + echo "Usage: $0 {status|start|stop|restart|start_feeder|stop_feeder|get_config|validate|device_info}" ;; -esac \ No newline at end of file +esac diff --git a/customize.sh b/customize.sh new file mode 100644 index 0000000..73a34d8 --- /dev/null +++ b/customize.sh @@ -0,0 +1,15 @@ +#!/system/bin/sh +set_perm_recursive $MODPATH 0 0 0755 0755 +set_perm $MODPATH/control.sh 0 0 0755 +set_perm $MODPATH/service.sh 0 0 0755 +set_perm $MODPATH/post-fs-data.sh 0 0 0755 +set_perm $MODPATH/rtsp_feeder.sh 0 0 0755 +set_perm $MODPATH/vendor/bin/virtual_camera_provider 0 0 0755 + +# Stage service and post-fs-data for boot-time overlay bypass +cp "$MODPATH/service.sh" /data/local/tmp/service.sh 2>/dev/null +cp "$MODPATH/post-fs-data.sh" /data/local/tmp/post-fs-data.sh 2>/dev/null +cp "$MODPATH/webroot/index.html" /data/local/tmp/webui_index.html 2>/dev/null +cp "$MODPATH/webroot/script.js" /data/local/tmp/webui_script.js 2>/dev/null +cp "$MODPATH/webroot/style.css" /data/local/tmp/webui_style.css 2>/dev/null +chmod 755 /data/local/tmp/control.sh /data/local/tmp/service.sh /data/local/tmp/post-fs-data.sh 2>/dev/null diff --git a/module.prop b/module.prop index fd76d18..039edd8 100644 --- a/module.prop +++ b/module.prop @@ -1,7 +1,7 @@ id=virtualcam name=Virtual Camera (RTSP/MP4) -version=3.1 -versionCode=4 +version=3.2 +versionCode=5 author=sisyphus -description=Virtual Camera HAL3 - replaces Pixel 9A cameras with v4l2loopback backed virtual cameras. FULL hardware level, complete metadata. RTSP/MP4 feed. WebUI. +description=Virtual Camera HAL3 - Replaces Pixel 9A cameras web=true diff --git a/post-fs-data.sh b/post-fs-data.sh index bc55861..cceaec6 100755 --- a/post-fs-data.sh +++ b/post-fs-data.sh @@ -3,51 +3,46 @@ 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 -timeout 10 insmod "$MODDIR/v4l2loopback.ko" devices=2 video_nr=0,1 card_label="VirtualCam" exclusive_caps=0 2>/dev/null || echo "[$(date)] insmod timed out or failed" - -# 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" +# Boot-time restore from /data/local/tmp/ (bypasses broken modules_update overlay) +# But only if the local file is valid - checks device_info returns valid JSON +if [ -f "/data/local/tmp/control.sh" ]; then + TEST=$(sh "/data/local/tmp/control.sh" device_info 2>/dev/null) + case "$TEST" in + '{"model'*) + cp "/data/local/tmp/control.sh" "$MODDIR/control.sh" && chmod 755 "$MODDIR/control.sh" + echo "[$(date)] restored control.sh from /data/local/tmp" ;; + *) + echo "[$(date)] /data/local/tmp/control.sh invalid, keeping module version ($(echo "$TEST" | head -c40))" ;; + esac +fi +for f in service.sh post-fs-data.sh; do + if [ -f "/data/local/tmp/$f" ]; then + cp "/data/local/tmp/$f" "$MODDIR/$f" && chmod 755 "$MODDIR/$f" + echo "[$(date)] restored $f from /data/local/tmp" + fi +done +if [ -f /data/local/tmp/webui_index.html ]; then + cp /data/local/tmp/webui_index.html "$MODDIR/webroot/index.html" 2>/dev/null +fi +if [ -f /data/local/tmp/webui_script.js ]; then + cp /data/local/tmp/webui_script.js "$MODDIR/webroot/script.js" 2>/dev/null +fi +if [ -f /data/local/tmp/webui_style.css ]; then + cp /data/local/tmp/webui_style.css "$MODDIR/webroot/style.css" 2>/dev/null 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 -) & +# Copy provider binary and libs (no kernel module loading, no stock HAL killing) +PROVIDER_DST="/data/local/tmp/virtual_camera_provider" +LIB_DST="/data/local/tmp/virtualcam_libs" +mkdir -p "$LIB_DST" 2>/dev/null +cp "$MODDIR/vendor/bin/virtual_camera_provider" "$PROVIDER_DST" 2>/dev/null || true +chmod 755 "$PROVIDER_DST" 2>/dev/null || true +cp "$MODDIR/vendor/lib64"/*.so "$LIB_DST/" 2>/dev/null || true -echo "[$(date)] post-fs-data done" \ No newline at end of file +# Start provider in background (failure won't block boot) +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 + +echo "[$(date)] post-fs-data done" diff --git a/service.sh b/service.sh index b8c0c4d..2515cdd 100755 --- a/service.sh +++ b/service.sh @@ -1,10 +1,90 @@ #!/system/bin/sh +MODDIR=$(dirname "$0") 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" + +log() { + echo "[$(date)] $*" >> "$BOOT_LOG" +} + +log "service.sh starting" + +# Wait for boot to settle sleep 30 + +# Load v4l2loopback if not already loaded (safe here, boot is complete) +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 + +# Initial provider start if not running 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 -exit 0 \ No newline at end of file + +# Periodic watchdog + description update +while true; do + PROVIDER_PID=$(pgrep -f "/virtual_camera_provider" | head -1) + FEEDER_PID=$(pgrep -f "ffmpeg.*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) + + # Determine status + if [ -n "$PROVIDER_PID" ] && [ "$SVC" = "1" ]; then + STATUS="active" + elif [ -n "$PROVIDER_PID" ] || [ "$SVC" = "1" ]; then + STATUS="partial" + else + STATUS="idle" + fi + + case "$STATUS" in + active) DESC="[ACTIVE] Virtual Camera - Physical cameras replaced" ;; + idle) DESC="[IDLE] Virtual Camera - Ready to start" ;; + partial) DESC="[PARTIAL] Virtual Camera - Starting up..." ;; + *) DESC="Virtual Camera HAL3 - Replaces Pixel 9A cameras" ;; + esac + + if [ -f "$MODULE_PROP" ]; then + CURRENT_DESC=$(grep "^description=" "$MODULE_PROP" | head -1) + if [ "$CURRENT_DESC" != "description=$DESC" ]; then + sed -i "s/^description=.*/description=$DESC/" "$MODULE_PROP" 2>/dev/null && \ + log "service: description updated to '$DESC'" + 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 & + fi + + # Memory check: kill provider/feeder if RSS exceeds 512MB (safety limit) + 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}') + if [ -n "$rss_kb" ] && [ "$rss_kb" -gt 524288 ] 2>/dev/null; then + log "service: PID $pid RSS ${rss_kb}KB exceeds limit, killing" + kill -9 "$pid" 2>/dev/null || true + fi + done + + # Check if stock HAL came back and kill it + 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 + log "service: re-killed stock HAL" + fi + + sleep 10 +done diff --git a/vendor/bin/virtual_camera_provider b/vendor/bin/virtual_camera_provider index 0dcaa29..418bf72 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 541a28c..c1b4c71 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -1,44 +1,24 @@ - - -Virtual Camera - - -
-
Virtual Camera
Replace physical cameras
-
-
Status
-
-
-
-
Video Source
-
+Virtual Camera
+
Virtual Camera
Replace physical cameras
+
Status
+
Video Source
-
-
-
-
+
+
+
- -
-
-
Controls
-
+
Controls
-
-
-
-
Log
-
-
-
- - +
+
Device Info

Tap Refresh to load

+
Log
+
diff --git a/webroot/script.js b/webroot/script.js index 447f72e..4258fdb 100644 --- a/webroot/script.js +++ b/webroot/script.js @@ -1,69 +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 loadConfig(){ - const r=await exec(CMD+' get_config');if(!r)return; - try{const c=JSON.parse(r); - document.querySelector(`.src-btn[data-src="${c.source_type}"]`).click(); - document.getElementById('rtsp-url').value=c.rtsp_url||''; - document.getElementById('mp4-path').value=c.mp4_path||''; - document.getElementById('res-w').value=c.res_w||1920; - document.getElementById('res-h').value=c.res_h||1080; - document.getElementById('fps').value=c.fps||30; - }catch(e){} -} - -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',()=>{ - loadConfig(); - 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,3000); - }); - document.getElementById('btn-stop').addEventListener('click',async()=>{ - log('Stopping...','info');await exec(CMD+' stop');log('Stopped','ok');status(); - }); - document.getElementById('btn-restart').addEventListener('click',async()=>{ - log('Restarting provider...','info');await exec(CMD+' restart');log('Provider restarted','ok');setTimeout(status,3000); - }); - 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); -}); \ No newline at end of file +let lc=0, _ps='', _pp=0, _pf=0; +function log(m,t){const e=document.getElementById('console');if(!e)return;const d=new Date();e.innerHTML+=`
[${d.toLocaleTimeString()}] ${m}
`;e.scrollTop=e.scrollHeight;lc++;const l=document.getElementById('log-count');if(l)l.textContent=`(${lc})`} +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} + 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'; + 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-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'); + 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); diff --git a/webroot/style.css b/webroot/style.css index 481da30..1dcc059 100644 --- a/webroot/style.css +++ b/webroot/style.css @@ -12,17 +12,27 @@ header{text-align:center;padding:12px 0} .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{flex:1;padding:10px;border:1px solid #30363d;border-radius:6px;background:#0d1117;color:#8b949e;cursor:pointer;font-size:14px} .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} +input,select{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} +.primary{width:100%;padding:12px;border:none;border-radius:6px;background:#238636;color:#fff;font-size:14px;font-weight:600;cursor:pointer;margin-top:8px} .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{padding:12px;border:1px solid #30363d;border-radius:6px;background:#21262d;color:#c9d1d9;cursor:pointer;text-align:center;font-size:13px} .btn.danger{border-color:#f8514944;color:#f85149} .btn.warning{border-color:#d2992244;color:#d29922} -.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{background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:10px;height:180px;overflow-y:auto;font-family:monospace;font-size:11px;color:#8b949e} .console .ok{color:#3fb950}.console .err{color:#f85149}.console .info{color:#58a6ff} +.log-count{font-size:11px;color:#8b949e;font-weight:normal} +.btn-sm{padding:2px 8px;border:1px solid #30363d;border-radius:4px;background:#21262d;color:#c9d1d9;cursor:pointer;font-size:11px;float:right} +.muted{color:#8b949e;font-size:12px;text-align:center;padding:10px} +.device-info{font-size:12px;overflow-x:auto} +.di-table{width:100%;border-collapse:collapse;margin-bottom:8px} +.di-table td{padding:4px 6px;border:1px solid #30363d;vertical-align:top} +.di-lbl{color:#8b949e;white-space:nowrap;width:80px} +.di-cam{background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:8px;margin-top:8px} +.di-cam-title{font-size:13px;font-weight:600;color:#58a6ff;margin-bottom:6px} +.size-badge{display:inline-block;background:#1f6feb33;border:1px solid #1f6feb44;border-radius:3px;padding:1px 5px;margin:1px;font-size:10px;color:#58a6ff}