Add camera presets snapshots and continuous recording playback

This commit is contained in:
Codex
2026-10-04 08:33:00 +08:00
parent 14f722fa69
commit 68be32e269
15 changed files with 513 additions and 43 deletions
+64 -14
View File
@@ -70,23 +70,73 @@ document.addEventListener('click',e=>{
if(b.dataset.wallBefore||b.dataset.wallAfter){const ids=wallOrder.filter(id=>filtered().some(c=>c.id===id)),id=b.dataset.wallBefore||b.dataset.wallAfter;wallMove(id,ids[ids.indexOf(id)+(b.dataset.wallBefore?-1:1)]);}
if(b.dataset.ptzCamera)openPtz(b.dataset.ptzCamera);
});
let ptzPresets=null,ptzPresetGeneration=0,ptzBusy=false,ptzCommandGeneration=0,ptzReady=false,ptzConfirmation=null;
function setPtzBusy(busy){
ptzBusy=busy;
$$('[data-ptz-action]').forEach(b=>b.disabled=!ptzReady||(busy&&b.dataset.ptzAction!=='stop'));
$$('#ptz-presets button,#ptz-presets input').forEach(b=>b.disabled=busy);
if(!busy&&ptzPresets)$('#preset-form button').disabled=ptzPresets.items.length>=ptzPresets.maximum;
}
function clearPresetConfirmation(){ptzConfirmation=null;$('#preset-confirmation').hidden=true;}
function renderPresets(data){
ptzPresets=data;clearPresetConfirmation();
$('#preset-list').innerHTML=data.items.map(p=>`<div class="preset-row"><strong>${esc(p.name)}</strong><button class="quiet" data-preset-goto="${esc(p.token)}">转到</button>${isAdmin()?`<button class="quiet" data-preset-remove="${esc(p.token)}">删除</button>`:''}</div>`).join('')||'<p class="description">还没有预置位。把云台调到需要的位置后保存。</p>';
$('#preset-count').textContent=`${data.items.length} / ${data.maximum} 个位置`;
$('#preset-form').hidden=!isAdmin();$('#preset-form button').disabled=data.items.length>=data.maximum;
}
async function loadPresets(){
if(!ptzCamera||ptzBusy)return;const id=ptzCamera,generation=++ptzPresetGeneration;
ptzPresets=null;clearPresetConfirmation();$('#preset-list').replaceChildren();$('#preset-form').hidden=true;
$('#preset-status').textContent='正在读取设备预置位…';$('#preset-reload').disabled=true;
try{const data=await api('/api/ptz/presets?camera='+encodeURIComponent(id));if(generation!==ptzPresetGeneration||id!==ptzCamera||!loggedIn)return;renderPresets(data);$('#preset-status').textContent=data.note;}
catch(e){if(!e.stale&&generation===ptzPresetGeneration)$('#preset-status').textContent=e.message;}
finally{if(generation===ptzPresetGeneration)$('#preset-reload').disabled=false;}
}
async function openPtz(id){
const generation=++ptzGeneration;ptzCamera=id;$('#ptz-panel').hidden=false;
$('#ptz-title').textContent=(state.cameras.find(c=>c.id===id)?.name||'摄像头')+' · 云台';
$('#ptz-status').textContent='正在读取云台能力…';$$('[data-ptz-action]').forEach(b=>b.disabled=true);
try{const caps=await api('/api/ptz?camera='+id);if(generation!==ptzGeneration)return;$('#ptz-status').textContent=caps.note;$$('[data-ptz-action]').forEach(b=>b.disabled=false);}
catch(e){if(generation===ptzGeneration)$('#ptz-status').textContent=e.message;}
if(ptzCamera&&ptzCamera!==id)closePtz();
const generation=++ptzGeneration;ptzCamera=id;ptzReady=false;$('#ptz-panel').hidden=false;
$('#ptz-title').textContent=(state.cameras.find(c=>c.id===id)?.name||'摄像头')+' · 云台与预置位';
$('#ptz-status').textContent='正在读取云台能力…';setPtzBusy(false);loadPresets();
try{const caps=await api('/api/ptz?camera='+id);if(generation!==ptzGeneration||!loggedIn)return;ptzReady=true;$('#ptz-status').textContent=caps.note;setPtzBusy(ptzBusy);}
catch(e){if(!e.stale&&generation===ptzGeneration)$('#ptz-status').textContent=e.message;}
}
async function ptzCommand(action){
if(!ptzCamera)return;const id=ptzCamera,generation=ptzGeneration;
$$('[data-ptz-action]:not([data-ptz-action="stop"])').forEach(b=>b.disabled=true);
$('#ptz-status').textContent=action==='stop'?'正在停止…':'正在微调…';
try{await api('/api/ptz',{camera:id,action});if(generation===ptzGeneration)$('#ptz-status').textContent='停止指令已确认,可继续微调';}
catch(e){if(generation===ptzGeneration)$('#ptz-status').textContent=e.message;}
finally{if(generation===ptzGeneration)$$('[data-ptz-action]').forEach(b=>b.disabled=false);}
if(!ptzCamera||(!ptzReady&&action!=='stop')||(ptzBusy&&action!=='stop'))return;
const id=ptzCamera,generation=ptzGeneration,command=++ptzCommandGeneration;
setPtzBusy(true);$('#ptz-status').textContent=action==='stop'?'正在停止…':'正在微调…';
try{await api('/api/ptz',{camera:id,action});if(generation===ptzGeneration&&command===ptzCommandGeneration)$('#ptz-status').textContent='停止指令已确认,可继续微调';}
catch(e){if(!e.stale&&generation===ptzGeneration&&command===ptzCommandGeneration)$('#ptz-status').textContent=e.message;}
finally{if(generation===ptzGeneration&&command===ptzCommandGeneration)setPtzBusy(false);}
}
function confirmPreset(action,token){
if(!isAdmin()||ptzBusy||!ptzPresets)return;
const preset=ptzPresets.items.find(p=>p.token===token);if(!preset)return;
ptzConfirmation={action,token,name:preset.name};
$('#preset-confirm-text').textContent=`删除“${preset.name}”?设备中的这个预置位会被移除。`;
$('#preset-confirmation').hidden=false;
}
async function presetCommand(action,token,name){
if(ptzBusy||!ptzCamera||!ptzPresets)return;
const id=ptzCamera,generation=ptzGeneration,command=++ptzCommandGeneration,revision=ptzPresets.revision;
clearPresetConfirmation();setPtzBusy(true);$('#preset-status').textContent=action==='goto'?'正在转到预置位,可随时点击停止…':'正在更新设备预置位…';
try{const result=await api('/api/ptz/presets',{camera:id,action,token,name,revision});
if(generation!==ptzGeneration||command!==ptzCommandGeneration||!loggedIn)return;
if(result.presets){renderPresets(result.presets);$('#preset-form').reset();}
$('#preset-status').textContent=result.message;
}catch(e){if(!e.stale&&generation===ptzGeneration&&command===ptzCommandGeneration){ptzPresets=null;$('#preset-list').replaceChildren();$('#preset-form').hidden=true;$('#preset-status').textContent=e.message+'。请重新读取后核对。';}}
finally{if(generation===ptzGeneration&&command===ptzCommandGeneration)setPtzBusy(false);}
}
function closePtz(){
const id=ptzCamera;if(id&&ptzBusy&&loggedIn)api('/api/ptz',{camera:id,action:'stop'}).catch(()=>{});
ptzGeneration++;ptzPresetGeneration++;ptzCommandGeneration++;ptzCamera='';ptzPresets=null;ptzBusy=false;ptzReady=false;
$('#ptz-panel').hidden=true;$('#preset-list').replaceChildren();$('#preset-form').reset();clearPresetConfirmation();
}
function closePtz(){ptzGeneration++;ptzCamera='';$('#ptz-panel').hidden=true;}
$('#ptz-close').onclick=closePtz;
$$('[data-ptz-action]').forEach(b=>b.onclick=()=>ptzCommand(b.dataset.ptzAction));
document.addEventListener('visibilitychange',()=>{if(document.hidden&&ptzCamera)ptzCommand('stop');});
window.addEventListener('pagehide',()=>{if(ptzCamera)fetch('/api/ptz',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({camera:ptzCamera,action:'stop'})}).catch(()=>{});});
$('#preset-reload').onclick=loadPresets;
$('#preset-form').onsubmit=e=>{e.preventDefault();presetCommand('create',undefined,$('#preset-name').value);};
$('#preset-confirm-cancel').onclick=clearPresetConfirmation;
$('#preset-confirm-yes').onclick=()=>{if(ptzConfirmation){const p=ptzConfirmation;presetCommand(p.action,p.token,p.name);}};
$('#preset-list').onclick=e=>{const b=e.target.closest('button');if(!b)return;if(b.dataset.presetGoto)presetCommand('goto',b.dataset.presetGoto);if(b.dataset.presetRemove)confirmPreset('remove',b.dataset.presetRemove);};
document.addEventListener('visibilitychange',()=>{if(document.hidden&&ptzCamera&&ptzBusy)ptzCommand('stop');});
window.addEventListener('pagehide',()=>{if(ptzCamera&&ptzBusy)fetch('/api/ptz',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({camera:ptzCamera,action:'stop'})}).catch(()=>{});});