made some good changes
This commit is contained in:
+123
-21
@@ -3,6 +3,13 @@ CONFIG_FILE="/data/local/tmp/virtualcam_config.json"
|
|||||||
MODDIR=$(dirname "$0")
|
MODDIR=$(dirname "$0")
|
||||||
LIB_DST="/data/local/tmp/virtualcam_libs"
|
LIB_DST="/data/local/tmp/virtualcam_libs"
|
||||||
FFMPEG_BIN="/data/local/tmp/ffmpeg"
|
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() {
|
load_config() {
|
||||||
[ -f "$CONFIG_FILE" ] || return
|
[ -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_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)
|
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)
|
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}
|
: ${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}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "${1:-help}" in
|
update_description() {
|
||||||
status)
|
local status="$1"
|
||||||
PROVIDER_PID=$(pgrep -f "virtual_camera_provider$" | head -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)
|
FEEDER_PID=$(pgrep -f "ffmpeg.*video0" | head -1)
|
||||||
V4L2=$(lsmod 2>/dev/null | grep -q v4l2loopback && echo 1 || echo 0)
|
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)
|
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")
|
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\"}"
|
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)
|
||||||
|
get_status_json
|
||||||
;;
|
;;
|
||||||
start)
|
start)
|
||||||
rmmod v4l2loopback 2>/dev/null; sleep 1
|
log "start: killing old provider"
|
||||||
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
|
||||||
pkill -9 -f "virtual_camera_provider$" 2>/dev/null || true
|
|
||||||
mkdir -p "$LIB_DST" 2>/dev/null
|
mkdir -p "$LIB_DST" 2>/dev/null
|
||||||
cp "$MODDIR/vendor/lib64"/*.so "$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" \
|
log "start: launching provider"
|
||||||
-d /dev/video0 -w 1920 -h 1080 -i virtual > /dev/null 2>&1 &
|
env LD_LIBRARY_PATH="$LIB_DST" "$MODDIR/vendor/bin/virtual_camera_provider" \
|
||||||
echo '{"status":"started"}'
|
-d /dev/video0 -w 1920 -h 1080 -i virtual &
|
||||||
;;
|
sleep 2
|
||||||
stop)
|
get_status_json
|
||||||
pkill -9 -f "virtual_camera_provider$" 2>/dev/null || true
|
log "start: done"
|
||||||
pkill -9 cameraserver 2>/dev/null || true
|
|
||||||
echo '{"status":"stopped"}'
|
|
||||||
;;
|
;;
|
||||||
restart)
|
restart)
|
||||||
pkill -9 -f "virtual_camera_provider$" 2>/dev/null || true
|
log "restart: killing provider"
|
||||||
pkill -9 cameraserver 2>/dev/null || true
|
pkill -9 -f "/virtual_camera_provider" 2>/dev/null || true
|
||||||
sleep 2
|
sleep 2
|
||||||
mkdir -p "$LIB_DST" 2>/dev/null
|
mkdir -p "$LIB_DST" 2>/dev/null
|
||||||
cp "$MODDIR/vendor/lib64"/*.so "$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" \
|
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 &
|
-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)
|
start_feeder)
|
||||||
|
log "start_feeder: loading config"
|
||||||
load_config
|
load_config
|
||||||
pkill -9 -f "ffmpeg.*video0" 2>/dev/null || true
|
pkill -9 -f "ffmpeg.*video0" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
if command -v "$FFMPEG_BIN" >/dev/null 2>&1; then
|
if command -v "$FFMPEG_BIN" >/dev/null 2>&1; then
|
||||||
FF="$FFMPEG_BIN"
|
FF="$FFMPEG_BIN"
|
||||||
elif command -v ffmpeg >/dev/null 2>&1; then
|
elif command -v ffmpeg >/dev/null 2>&1; then
|
||||||
FF="ffmpeg"
|
FF="ffmpeg"
|
||||||
else
|
else
|
||||||
|
log "start_feeder: ERROR ffmpeg not found"
|
||||||
echo '{"status":"error","message":"ffmpeg not found"}'
|
echo '{"status":"error","message":"ffmpeg not found"}'
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
if [ "$SOURCE_TYPE" = "rtsp" ]; then
|
||||||
nohup "$FF" -rtsp_transport tcp -i "$RTSP_URL" \
|
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 &
|
-f v4l2 -pix_fmt yuv420p /dev/video0 > /dev/null 2>&1 &
|
||||||
elif [ "$SOURCE_TYPE" = "mp4" ] && [ -f "$MP4_PATH" ]; then
|
elif [ "$SOURCE_TYPE" = "mp4" ] && [ -f "$MP4_PATH" ]; then
|
||||||
nohup "$FF" -stream_loop -1 -re -i "$MP4_PATH" \
|
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 &
|
-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
|
fi
|
||||||
|
sleep 1
|
||||||
|
log "start_feeder: done"
|
||||||
echo '{"status":"feeder_started"}'
|
echo '{"status":"feeder_started"}'
|
||||||
;;
|
;;
|
||||||
stop_feeder)
|
stop_feeder)
|
||||||
|
log "stop_feeder: killing ffmpeg"
|
||||||
pkill -9 -f "ffmpeg.*video0" 2>/dev/null || true
|
pkill -9 -f "ffmpeg.*video0" 2>/dev/null || true
|
||||||
|
log "stop_feeder: done"
|
||||||
echo '{"status":"feeder_stopped"}'
|
echo '{"status":"feeder_stopped"}'
|
||||||
;;
|
;;
|
||||||
get_config)
|
get_config)
|
||||||
load_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
|
esac
|
||||||
@@ -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
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
id=virtualcam
|
id=virtualcam
|
||||||
name=Virtual Camera (RTSP/MP4)
|
name=Virtual Camera (RTSP/MP4)
|
||||||
version=3.1
|
version=3.2
|
||||||
versionCode=4
|
versionCode=5
|
||||||
author=sisyphus
|
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
|
web=true
|
||||||
|
|||||||
+31
-36
@@ -3,51 +3,46 @@ MODDIR=${0%/*}
|
|||||||
exec >> "$MODDIR/boot.log" 2>&1
|
exec >> "$MODDIR/boot.log" 2>&1
|
||||||
echo "[$(date)] post-fs-data starting"
|
echo "[$(date)] post-fs-data starting"
|
||||||
|
|
||||||
# Stop the stock Google camera HAL so it doesn't re-register its cameras.
|
# Boot-time restore from /data/local/tmp/ (bypasses broken modules_update overlay)
|
||||||
# This is an APEX-managed service that auto-restarts, so we need to keep it down.
|
# But only if the local file is valid - checks device_info returns valid JSON
|
||||||
setprop ctl.stop vendor.camera-provider-2-7-google 2>/dev/null || true
|
if [ -f "/data/local/tmp/control.sh" ]; then
|
||||||
kill -9 $(pgrep -f camera-provider-2-7-google) 2>/dev/null || true
|
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
|
||||||
|
|
||||||
# Reload v4l2loopback (rmmod first to clear stale params like exclusive_caps)
|
# Copy provider binary and libs (no kernel module loading, no stock HAL killing)
|
||||||
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"
|
PROVIDER_DST="/data/local/tmp/virtual_camera_provider"
|
||||||
LIB_DST="/data/local/tmp/virtualcam_libs"
|
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
|
cp "$MODDIR/vendor/bin/virtual_camera_provider" "$PROVIDER_DST" 2>/dev/null || true
|
||||||
chmod 755 "$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
|
cp "$MODDIR/vendor/lib64"/*.so "$LIB_DST/" 2>/dev/null || true
|
||||||
|
|
||||||
# Start our provider
|
# Start provider in background (failure won't block boot)
|
||||||
if [ -x "$PROVIDER_DST" ]; then
|
if [ -x "$PROVIDER_DST" ]; then
|
||||||
nohup env LD_LIBRARY_PATH="$LIB_DST" "$PROVIDER_DST" \
|
nohup env LD_LIBRARY_PATH="$LIB_DST" "$PROVIDER_DST" -d /dev/video0 -w 1920 -h 1080 -i virtual </dev/null >/dev/null 2>&1 &
|
||||||
-d /dev/video0 -w 1920 -h 1080 -i virtual \
|
|
||||||
</dev/null >/dev/null 2>&1 &
|
|
||||||
echo "[$(date)] provider started"
|
echo "[$(date)] provider started"
|
||||||
fi
|
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"
|
echo "[$(date)] post-fs-data done"
|
||||||
+81
-1
@@ -1,10 +1,90 @@
|
|||||||
#!/system/bin/sh
|
#!/system/bin/sh
|
||||||
|
MODDIR=$(dirname "$0")
|
||||||
PROVIDER_DST="/data/local/tmp/virtual_camera_provider"
|
PROVIDER_DST="/data/local/tmp/virtual_camera_provider"
|
||||||
LIB_DST="/data/local/tmp/virtualcam_libs"
|
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
|
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
|
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" \
|
nohup env LD_LIBRARY_PATH="$LIB_DST" "$PROVIDER_DST" \
|
||||||
-d /dev/video0 -w 1920 -h 1080 -i virtual \
|
-d /dev/video0 -w 1920 -h 1080 -i virtual \
|
||||||
</dev/null >/dev/null 2>&1 &
|
</dev/null >/dev/null 2>&1 &
|
||||||
|
log "service: provider launched"
|
||||||
fi
|
fi
|
||||||
exit 0
|
|
||||||
|
# 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 >/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
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+12
-32
@@ -1,44 +1,24 @@
|
|||||||
<!DOCTYPE html><html lang="en"><head>
|
<!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">
|
||||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<header><div class="title">Virtual Camera</div><div class="subtitle" id="subtitle">Replace physical cameras</div></header>
|
||||||
<title>Virtual Camera</title>
|
<div class="card"><div class="card-title">Status</div><div id="status-msg" style="font-size:11px;color:#f85149;margin-bottom:6px"></div><div class="dashboard" id="status-dash"></div></div>
|
||||||
<link rel="stylesheet" href="style.css">
|
<div class="card"><div class="card-title">Video Source</div><div class="source-selector">
|
||||||
</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 active" data-src="rtsp">RTSP</button>
|
||||||
<button class="src-btn" data-src="mp4">MP4</button>
|
<button class="src-btn" data-src="mp4">MP4</button>
|
||||||
</div>
|
</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="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 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 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>
|
||||||
<div><label>Width</label><input type="number" id="res-w" value="1920" class="num"></div>
|
<div class="row"><div><label>Rotation</label><select id="rotation" class="num"><option value="0">0</option><option value="90">90</option><option value="180">180</option><option value="270">270</option></select></div></div>
|
||||||
<div><label>Height</label><input type="number" id="res-h" value="1080" class="num"></div>
|
<button id="btn-save" class="primary">Save Config</button>
|
||||||
<div><label>FPS</label><input type="number" id="fps" value="30" class="num"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<button id="btn-save" class="primary">Save</button>
|
<div class="card"><div class="card-title">Controls</div><div class="grid">
|
||||||
</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-start" class="btn">Start Provider</button>
|
||||||
<button id="btn-stop" class="btn danger">Stop</button>
|
<button id="btn-stop" class="btn danger">Stop</button>
|
||||||
<button id="btn-restart" class="btn warning">Restart</button>
|
<button id="btn-restart" class="btn warning">Restart</button>
|
||||||
<button id="btn-feed" class="btn">Start Feeder</button>
|
<button id="btn-feed" class="btn">Start Feeder</button>
|
||||||
<button id="btn-unfeed" class="btn danger">Stop Feeder</button>
|
<button id="btn-unfeed" class="btn danger">Stop Feeder</button>
|
||||||
<button id="btn-refresh" class="btn">Refresh</button>
|
<button id="btn-refresh" class="btn">Refresh</button>
|
||||||
</div>
|
</div></div>
|
||||||
</div>
|
<div class="card"><div class="card-title">Device Info <button id="btn-deviceinfo" class="btn-sm">Refresh</button></div><div id="device-info" class="device-info"><p class="muted">Tap Refresh to load</p></div></div>
|
||||||
<div class="card">
|
<div class="card"><div class="card-title">Log <span id="log-count" class="log-count"></span></div><div id="console" class="console"></div></div>
|
||||||
<div class="card-title">Log</div>
|
</div><script src="script.js"></script></body></html>
|
||||||
<div id="console" class="console"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="script.js"></script>
|
|
||||||
</body></html>
|
|
||||||
|
|||||||
+48
-64
@@ -1,69 +1,53 @@
|
|||||||
const CMD='/data/adb/modules/virtualcam/control.sh';
|
const CMD='/data/adb/modules/virtualcam/control.sh';
|
||||||
function log(m,t){const e=document.getElementById('console');const d=new Date();
|
let lc=0, _ps='', _pp=0, _pf=0;
|
||||||
e.innerHTML+=`<div class="${t||'log'}">[${d.toLocaleTimeString()}] ${m}</div>`;e.scrollTop=e.scrollHeight}
|
function log(m,t){const e=document.getElementById('console');if(!e)return;const d=new Date();e.innerHTML+=`<div class="${t||'log'}">[${d.toLocaleTimeString()}] ${m}</div>`;e.scrollTop=e.scrollHeight;lc++;const l=document.getElementById('log-count');if(l)l.textContent=`(${lc})`}
|
||||||
async function exec(c){try{return await KSU.exec(c)}catch(e){log('exec error','err');return null}}
|
function run(a){try{ksu.exec(CMD+' '+a);log(a+' OK','ok')}catch(e){log(a+' error: '+e.message,'err')}updateDash()}
|
||||||
|
function updateDash(){try{
|
||||||
async function loadConfig(){
|
const r=typeof ksu!=='undefined'?ksu.exec(CMD+' status'):null;
|
||||||
const r=await exec(CMD+' get_config');if(!r)return;
|
if(!r||typeof r!=='string'){document.getElementById('status-dash').innerHTML='<div class="stat" style="grid-column:1/3;color:#f85149">No data</div>';return}
|
||||||
try{const c=JSON.parse(r);
|
const s=JSON.parse(r);
|
||||||
document.querySelector(`.src-btn[data-src="${c.source_type}"]`).click();
|
const d=document.getElementById('status-dash');if(!d)return;
|
||||||
document.getElementById('rtsp-url').value=c.rtsp_url||'';
|
const sc=s.status==='active'?'green':(s.status==='partial'?'yellow':'red');
|
||||||
document.getElementById('mp4-path').value=c.mp4_path||'';
|
const st=s.status==='active'?'ACTIVE':(s.status==='partial'?'PARTIAL':'STOPPED');
|
||||||
document.getElementById('res-w').value=c.res_w||1920;
|
let v=[];
|
||||||
document.getElementById('res-h').value=c.res_h||1080;
|
if(s.camera2_level) v=[['camera2','green',s.camera2_level],['Checks',s.checks&&s.checks.includes('provider_ok')?'green':'red',s.checks||'?']];
|
||||||
document.getElementById('fps').value=c.fps||30;
|
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=>`<div class="stat"><span class="l">${i[0]}</span><span class="v ${i[1]}">${i[2]}</span></div>`).join('');
|
||||||
}catch(e){}
|
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')}}
|
||||||
async function status(){
|
function loadDeviceInfo(){try{
|
||||||
const r=await exec(CMD+' status');if(!r)return;
|
const r=ksu.exec(CMD+' device_info');
|
||||||
try{
|
document.getElementById('device-info').innerHTML=r?'<pre style="background:#0d1117;border:1px solid #30363d;border-radius:4px;padding:8px;font-size:10px;overflow-x:auto;white-space:pre-wrap">'+r.replace(/</g,'<').replace(/>/g,'>')+'</pre>':'<p class="muted">No data</p>';
|
||||||
const s=JSON.parse(r);const d=document.getElementById('status-dash');
|
log('Device info loaded','ok');
|
||||||
const items=[['Provider',s.provider_pid>0?'green':'red',s.provider_pid>0?'Running':'Stopped'],
|
}catch(e){log('DevInfo error: '+e.message,'err')}}
|
||||||
['Feeder',s.feeder_pid>0?'green':'red',s.feeder_pid>0?'Running':'Stopped'],
|
document.getElementById('btn-start')?.addEventListener('click',()=>run('start'));
|
||||||
['Service',s.service_registered==1?'green':'red',s.service_registered==1?'Registered':'Not found'],
|
document.getElementById('btn-stop')?.addEventListener('click',()=>run('stop'));
|
||||||
['v4l2',s.v4l2_loaded==1?'green':'red',s.v4l2_loaded==1?'Loaded':'Missing'],
|
document.getElementById('btn-restart')?.addEventListener('click',()=>run('restart'));
|
||||||
['Device','yellow',s.video0||'N/A']];
|
document.getElementById('btn-feed')?.addEventListener('click',()=>run('start_feeder'));
|
||||||
d.innerHTML=items.map(i=>`<div class="stat"><span class="l">${i[0]}</span><span class="v ${i[1]}">${i[2]}</span></div>`).join('');
|
document.getElementById('btn-unfeed')?.addEventListener('click',()=>run('stop_feeder'));
|
||||||
log('Status updated','ok');
|
document.getElementById('btn-save')?.addEventListener('click',()=>{
|
||||||
}catch(e){log('Parse error','err')}
|
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");
|
||||||
document.addEventListener('DOMContentLoaded',()=>{
|
log('Config saved','ok');
|
||||||
loadConfig();
|
});
|
||||||
status();
|
document.getElementById('btn-refresh')?.addEventListener('click',updateDash);
|
||||||
document.querySelectorAll('.src-btn').forEach(b=>{
|
document.getElementById('btn-deviceinfo')?.addEventListener('click',loadDeviceInfo);
|
||||||
b.addEventListener('click',()=>{
|
document.querySelectorAll('.src-btn').forEach(b=>b.addEventListener('click',()=>{
|
||||||
document.querySelectorAll('.src-btn').forEach(x=>x.classList.remove('active'));
|
document.querySelectorAll('.src-btn').forEach(x=>x.classList.remove('active'));
|
||||||
b.classList.add('active');
|
b.classList.add('active');
|
||||||
document.getElementById('rtsp-config').classList.toggle('hidden',b.dataset.src!=='rtsp');
|
document.getElementById('rtsp-config').classList.toggle('hidden',b.dataset.src!=='rtsp');
|
||||||
document.getElementById('mp4-config').classList.toggle('hidden',b.dataset.src!=='mp4');
|
document.getElementById('mp4-config').classList.toggle('hidden',b.dataset.src!=='mp4');
|
||||||
});
|
}));
|
||||||
});
|
updateDash();
|
||||||
document.getElementById('btn-save').addEventListener('click',async()=>{
|
setInterval(updateDash,5000);
|
||||||
const src=document.querySelector('.src-btn.active').dataset.src;
|
document.getElementById('btn-refresh')?.addEventListener('click',updateDash);
|
||||||
const rtsp=document.getElementById('rtsp-url').value;
|
document.getElementById('btn-deviceinfo')?.addEventListener('click',loadDeviceInfo);
|
||||||
const mp4=document.getElementById('mp4-path').value;
|
document.querySelectorAll('.src-btn').forEach(b=>b.addEventListener('click',()=>{
|
||||||
const w=document.getElementById('res-w').value||1920;
|
document.querySelectorAll('.src-btn').forEach(x=>x.classList.remove('active'));
|
||||||
const h=document.getElementById('res-h').value||1080;
|
b.classList.add('active');
|
||||||
const f=document.getElementById('fps').value||30;
|
document.getElementById('rtsp-config').classList.toggle('hidden',b.dataset.src!=='rtsp');
|
||||||
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`);
|
document.getElementById('mp4-config').classList.toggle('hidden',b.dataset.src!=='mp4');
|
||||||
log('Config saved','ok');
|
}));
|
||||||
});
|
updateDash();
|
||||||
document.getElementById('btn-start').addEventListener('click',async()=>{
|
setInterval(updateDash,5000);
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|||||||
+15
-5
@@ -12,17 +12,27 @@ header{text-align:center;padding:12px 0}
|
|||||||
.stat .v{font-size:15px;font-weight:600;display:block}
|
.stat .v{font-size:15px;font-weight:600;display:block}
|
||||||
.green{color:#3fb950}.red{color:#f85149}.yellow{color:#d29922}
|
.green{color:#3fb950}.red{color:#f85149}.yellow{color:#d29922}
|
||||||
.source-selector{display:flex;gap:8px;margin-bottom:12px}
|
.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}
|
.src-btn.active{background:#1f6feb33;border-color:#1f6feb;color:#58a6ff}
|
||||||
.hidden{display:none}
|
.hidden{display:none}
|
||||||
label{font-size:12px;color:#8b949e;display:block;margin-bottom:4px}
|
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}
|
.row{display:flex;gap:8px;margin-bottom:12px}.row>div{flex:1}
|
||||||
.num{width:100%}
|
.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}
|
.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.danger{border-color:#f8514944;color:#f85149}
|
||||||
.btn.warning{border-color:#d2992244;color:#d29922}
|
.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}
|
.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}
|
||||||
|
|||||||
Reference in New Issue
Block a user