fix: restore recording playback and default to 30-day retention

This commit is contained in:
Codex
2026-10-06 22:20:00 +08:00
parent 91915b9d90
commit 59bd83403f
17 changed files with 160 additions and 20 deletions
+1
View File
@@ -107,6 +107,7 @@ function render() {
if (activePage === 'recorders') renderRecorders();
if (activePage === 'playback') {
setOptions($('#play-camera'), filtered().map(c=>`<option value="${c.id}">${esc(siteName(c.siteId)+' / '+c.name)}</option>`).join(''));
renderRecordingStatus();
drawTimeline();
}
if (activePage === 'storage') renderStorage();
+7 -1
View File
@@ -1,9 +1,15 @@
'use strict';
// Pure range logic shared by the UI and regression tests. All ranges are [start,end).
const RecordingRanges=(()=>{
// Date truncates fractional milliseconds. Round the query start inward so
// MediaMTX never receives a time before this segment's microsecond timestamp.
function firstMillisecond(value){
const time=+new Date(value),fraction=typeof value==='string'&&value.match(/\.(\d+)(?:Z|[+-]\d{2}:\d{2})$/);
return time+(fraction&&/[1-9]/.test(fraction[1].slice(3))?1:0);
}
function normalize(items,start,end){
const lo=+start,hi=+end;if(!Number.isFinite(lo)||!Number.isFinite(hi)||hi<=lo)return [];
const ranges=(Array.isArray(items)?items:[]).filter(s=>s&&Number.isFinite(+new Date(s.start))&&Number.isFinite(Number(s.duration))&&Number(s.duration)>0).map(s=>({start:Math.max(lo,+new Date(s.start)),end:Math.min(hi,+new Date(s.start)+Number(s.duration)*1000)}))
const ranges=(Array.isArray(items)?items:[]).filter(s=>s&&Number.isFinite(+new Date(s.start))&&Number.isFinite(Number(s.duration))&&Number(s.duration)>0).map(s=>({start:Math.max(lo,firstMillisecond(s.start)),end:Math.min(hi,Math.floor(+new Date(s.start)+Number(s.duration)*1000))}))
.filter(s=>Number.isFinite(s.start)&&Number.isFinite(s.end)&&s.end>s.start).sort((a,b)=>a.start-b.start);
const result=[];for(const r of ranges){const last=result.at(-1);if(last&&r.start<=last.end)last.end=Math.max(last.end,r.end);else result.push({...r});}return result;
}
+13 -2
View File
@@ -1,6 +1,17 @@
'use strict';
let playbackRange=null;
function playbackSourceHint(){if($('#archive-source').value==='recorder')$('#play-info').textContent='录像机原有录像待接入;查看本系统保存的录像请选择“小电脑本机录像”。';}
function playbackSourceHint(){renderRecordingStatus();if($('#archive-source').value==='recorder')$('#play-info').textContent='录像机原有录像待接入;查看本系统保存的录像请选择“小电脑本机录像”。';}
function renderRecordingStatus(){
const camera=state.cameras.find(c=>c.id===$('#play-camera').value),box=$('#recording-health');
box.hidden=!camera||$('#archive-source').value==='recorder';if(box.hidden)return;
const messages={recording:'正在录像 · 主码流持续保存',off:'未开启本机录像 · 实时画面接通不代表已保存录像',disabled:'录像已暂停 · 设备连接已停用',paused:'录像已暂停 · 已触发容量或磁盘保护',unavailable:'录像服务未连接 · 请联系管理员检查服务',offline:'等待摄像头接通 · 当前没有新录像写入',waiting:'已开启录像 · 等待文件写入,请稍候;若持续等待请检查存储'};
$('#recording-health-text').textContent=messages[camera.recordingState]||'正在读取录像状态';
const policy=state.storage||{};
$('#recording-policy-note').textContent=policy.retentionDays?`最长保留 ${policy.retentionDays} 天,过期自动清理;全平台录像上限 ${policy.maxGB} GB,磁盘预留 ${policy.reserveGB} GB。容量不足会提前暂停,实际可留时长取决于码率和路数。`:'录像由服务器按保留策略自动管理。';
$('#recording-start-note').textContent='本机只保存开启录像期间的画面。新录像按 5 分钟分段保存,回放查询可能稍有延迟。';
const button=$('#recording-configure');button.hidden=!isAdmin();button.dataset.editCamera=camera.id;
}
function localDateTime(value){const d=new Date(value);return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}T${d.toTimeString().slice(0,8)}`;}
function resetPlayback(){
@@ -51,7 +62,7 @@ function makeClip(){
}catch(e){$('#clip-status').textContent=e.message;}
}
$('#query-recordings').onclick=queryRecordings;
$('#play-camera').onchange=()=>{resetPlayback();const c=state.cameras.find(c=>c.id===$('#play-camera').value);$('#archive-source').value=c?.archiveSource||'local';};$('#archive-source').onchange=()=>{resetPlayback();playbackSourceHint();};$('#play-date').onchange=resetPlayback;
$('#play-camera').onchange=()=>{resetPlayback();const c=state.cameras.find(c=>c.id===$('#play-camera').value);$('#archive-source').value=c?.archiveSource||'local';playbackSourceHint();};$('#archive-source').onchange=()=>{resetPlayback();playbackSourceHint();};$('#play-date').onchange=resetPlayback;
$('#seek-button').onclick=()=>{if(!queryCamera){toast('请先查询录像');return;}playAt(new Date($('#play-date').value+'T'+$('#seek-time').value));};
$('#timeline').onclick=e=>{if(!queryStart)return;const r=e.currentTarget.getBoundingClientRect();playAt(new Date(+queryStart+(queryEnd-queryStart)*(e.clientX-r.left)/r.width));};
$('#play-video').onerror=()=>{if($('#play-video').getAttribute('src'))$('#play-info').textContent='回放失败:请稍后重试,或下载原始片段;服务器最多同时转换两个回放。';};
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="theme-color" content="#244d43"><link rel="icon" href="/zhao-icon.png"><link rel="apple-touch-icon" href="/zhao-icon.png"><title>赵府 · 智能数字管家</title> <script type="module" crossorigin src="/ui/assets/index-DuGUtT0D.js"></script>
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="theme-color" content="#244d43"><link rel="icon" href="/zhao-icon.png"><link rel="apple-touch-icon" href="/zhao-icon.png"><title>赵府 · 智能数字管家</title> <script type="module" crossorigin src="/ui/assets/index-DKr_acag.js"></script>
<link rel="stylesheet" crossorigin href="/ui/assets/index-XTjkr4dE.css">
</head><body><div id="app"></div></body></html>