Initial commit
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html><html lang="en"><head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Virtual Camera</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head><body>
|
||||
<div class="container">
|
||||
<header><div class="title">Virtual Camera</div><div class="subtitle">Replace physical cameras</div></header>
|
||||
<div class="card">
|
||||
<div class="card-title">Status</div>
|
||||
<div class="dashboard" id="status-dash"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Video Source</div>
|
||||
<div class="source-selector">
|
||||
<button class="src-btn active" data-src="rtsp">RTSP</button>
|
||||
<button class="src-btn" data-src="mp4">MP4</button>
|
||||
</div>
|
||||
<div id="rtsp-config"><label>RTSP URL</label><input type="text" id="rtsp-url" placeholder="rtsp://192.168.1.100:8554/stream"></div>
|
||||
<div id="mp4-config" class="hidden"><label>MP4 Path</label><input type="text" id="mp4-path" placeholder="/sdcard/Download/video.mp4"></div>
|
||||
<div class="row">
|
||||
<div><label>Width</label><input type="number" id="res-w" value="1920" class="num"></div>
|
||||
<div><label>Height</label><input type="number" id="res-h" value="1080" class="num"></div>
|
||||
<div><label>FPS</label><input type="number" id="fps" value="30" class="num"></div>
|
||||
</div>
|
||||
<button id="btn-save" class="primary">Save</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Controls</div>
|
||||
<div class="grid">
|
||||
<button id="btn-start" class="btn">Start Provider</button>
|
||||
<button id="btn-stop" class="btn danger">Stop</button>
|
||||
<button id="btn-feed" class="btn">Start Feeder</button>
|
||||
<button id="btn-unfeed" class="btn danger">Stop Feeder</button>
|
||||
<button id="btn-refresh" class="btn">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Log</div>
|
||||
<div id="console" class="console"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="script.js"></script>
|
||||
</body></html>
|
||||
@@ -0,0 +1,53 @@
|
||||
const CMD='/data/adb/modules/virtualcam/control.sh';
|
||||
function log(m,t){const e=document.getElementById('console');const d=new Date();
|
||||
e.innerHTML+=`<div class="${t||'log'}">[${d.toLocaleTimeString()}] ${m}</div>`;e.scrollTop=e.scrollHeight}
|
||||
async function exec(c){try{return await KSU.exec(c)}catch(e){log('exec error','err');return null}}
|
||||
|
||||
async function status(){
|
||||
const r=await exec(CMD+' status');if(!r)return;
|
||||
try{
|
||||
const s=JSON.parse(r);const d=document.getElementById('status-dash');
|
||||
const items=[['Provider',s.provider_pid>0?'green':'red',s.provider_pid>0?'Running':'Stopped'],
|
||||
['Feeder',s.feeder_pid>0?'green':'red',s.feeder_pid>0?'Running':'Stopped'],
|
||||
['Service',s.service_registered==1?'green':'red',s.service_registered==1?'Registered':'Not found'],
|
||||
['v4l2',s.v4l2_loaded==1?'green':'red',s.v4l2_loaded==1?'Loaded':'Missing'],
|
||||
['Device','yellow',s.video0||'N/A']];
|
||||
d.innerHTML=items.map(i=>`<div class="stat"><span class="l">${i[0]}</span><span class="v ${i[1]}">${i[2]}</span></div>`).join('');
|
||||
log('Status updated','ok');
|
||||
}catch(e){log('Parse error','err')}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded',()=>{
|
||||
status();
|
||||
document.querySelectorAll('.src-btn').forEach(b=>{
|
||||
b.addEventListener('click',()=>{
|
||||
document.querySelectorAll('.src-btn').forEach(x=>x.classList.remove('active'));
|
||||
b.classList.add('active');
|
||||
document.getElementById('rtsp-config').classList.toggle('hidden',b.dataset.src!=='rtsp');
|
||||
document.getElementById('mp4-config').classList.toggle('hidden',b.dataset.src!=='mp4');
|
||||
});
|
||||
});
|
||||
document.getElementById('btn-save').addEventListener('click',async()=>{
|
||||
const src=document.querySelector('.src-btn.active').dataset.src;
|
||||
const rtsp=document.getElementById('rtsp-url').value;
|
||||
const mp4=document.getElementById('mp4-path').value;
|
||||
const w=document.getElementById('res-w').value||1920;
|
||||
const h=document.getElementById('res-h').value||1080;
|
||||
const f=document.getElementById('fps').value||30;
|
||||
await exec(`echo '{"source_type":"${src}","rtsp_url":"${rtsp}","mp4_path":"${mp4}","res_w":${w},"res_h":${h},"fps":${f}}' > /data/local/tmp/virtualcam_config.json`);
|
||||
log('Config saved','ok');
|
||||
});
|
||||
document.getElementById('btn-start').addEventListener('click',async()=>{
|
||||
log('Starting provider...','info');await exec(CMD+' start');log('Provider started','ok');setTimeout(status,2000);
|
||||
});
|
||||
document.getElementById('btn-stop').addEventListener('click',async()=>{
|
||||
log('Stopping...','info');await exec(CMD+' stop');log('Stopped','ok');status();
|
||||
});
|
||||
document.getElementById('btn-feed').addEventListener('click',async()=>{
|
||||
log('Starting feeder...','info');await exec(CMD+' start_feeder');log('Feeder started','ok');setTimeout(status,2000);
|
||||
});
|
||||
document.getElementById('btn-unfeed').addEventListener('click',async()=>{
|
||||
log('Stopping feeder...','info');await exec(CMD+' stop_feeder');log('Feeder stopped','ok');status();
|
||||
});
|
||||
document.getElementById('btn-refresh').addEventListener('click',status);
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:-apple-system,sans-serif;background:#0d1117;color:#c9d1d9;padding:16px;max-width:480px;margin:0 auto}
|
||||
.container{display:flex;flex-direction:column;gap:12px}
|
||||
header{text-align:center;padding:12px 0}
|
||||
.title{font-size:22px;font-weight:700;color:#58a6ff}
|
||||
.subtitle{font-size:13px;color:#8b949e}
|
||||
.card{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:14px}
|
||||
.card-title{font-size:14px;font-weight:600;color:#58a6ff;margin-bottom:10px}
|
||||
.dashboard{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
||||
.stat{background:#0d1117;border-radius:6px;padding:8px;text-align:center}
|
||||
.stat .l{font-size:11px;color:#8b949e;display:block}
|
||||
.stat .v{font-size:15px;font-weight:600;display:block}
|
||||
.green{color:#3fb950}.red{color:#f85149}.yellow{color:#d29922}
|
||||
.source-selector{display:flex;gap:8px;margin-bottom:12px}
|
||||
.src-btn{flex:1;padding:10px;border:1px solid #30363d;border-radius:6px;background:#0d1117;color:#8b949e;cursor:pointer}
|
||||
.src-btn.active{background:#1f6feb33;border-color:#1f6feb;color:#58a6ff}
|
||||
.hidden{display:none}
|
||||
label{font-size:12px;color:#8b949e;display:block;margin-bottom:4px}
|
||||
input{width:100%;padding:10px;border:1px solid #30363d;border-radius:6px;background:#0d1117;color:#c9d1d9;font-size:14px}
|
||||
.row{display:flex;gap:8px;margin-bottom:12px}.row>div{flex:1}
|
||||
.num{width:100%}
|
||||
.primary{width:100%;padding:12px;border:none;border-radius:6px;background:#238636;color:#fff;font-size:14px;font-weight:600;cursor:pointer}
|
||||
.grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
||||
.btn{padding:12px;border:1px solid #30363d;border-radius:6px;background:#21262d;color:#c9d1d9;cursor:pointer;text-align:center}
|
||||
.btn.danger{border-color:#f8514944;color:#f85149}
|
||||
.console{background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:10px;height:150px;overflow-y:auto;font-family:monospace;font-size:12px;color:#8b949e}
|
||||
.console .ok{color:#3fb950}.console .err{color:#f85149}.console .info{color:#58a6ff}
|
||||
Reference in New Issue
Block a user