made some good changes

This commit is contained in:
2026-05-16 09:53:25 +02:00
parent 6bd51ff38b
commit 5176c9fc41
9 changed files with 344 additions and 178 deletions
+12 -32
View File
@@ -1,44 +1,24 @@
<!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">
<!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" id="subtitle">Replace physical cameras</div></header>
<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>
<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 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 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>
<button id="btn-save" class="primary">Save Config</button>
</div>
<button id="btn-save" class="primary">Save</button>
</div>
<div class="card">
<div class="card-title">Controls</div>
<div class="grid">
<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-restart" class="btn warning">Restart</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>
</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-title">Log <span id="log-count" class="log-count"></span></div><div id="console" class="console"></div></div>
</div><script src="script.js"></script></body></html>
+52 -68
View File
@@ -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+=`<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 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=>`<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',()=>{
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);
});
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+=`<div class="${t||'log'}">[${d.toLocaleTimeString()}] ${m}</div>`;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='<div class="stat" style="grid-column:1/3;color:#f85149">No data</div>';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=>`<div class="stat"><span class="l">${i[0]}</span><span class="v ${i[1]}">${i[2]}</span></div>`).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?'<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,'&lt;').replace(/>/g,'&gt;')+'</pre>':'<p class="muted">No data</p>';
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);
+15 -5
View File
@@ -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}