Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14f722fa69 |
@@ -162,3 +162,8 @@ JSON 导出升级为格式 version 2,包含人物、关系和当前账号有
|
|||||||
### 并发与数据一致性
|
### 并发与数据一致性
|
||||||
|
|
||||||
设备对象的校验与保存串行执行;镜头数量限制不能被同时提交绕过。摄像头对象移动空间时,所有镜头归属在同一数据库事务更新。录像机地址或账号更新后,关联通道读取最新端点。无效对象编号、错误 Origin 和非法地址类型会返回明确错误。
|
设备对象的校验与保存串行执行;镜头数量限制不能被同时提交绕过。摄像头对象移动空间时,所有镜头归属在同一数据库事务更新。录像机地址或账号更新后,关联通道读取最新端点。无效对象编号、错误 Origin 和非法地址类型会返回明确错误。
|
||||||
|
|
||||||
|
|
||||||
|
### 实机验收完善(v0.1.20)
|
||||||
|
|
||||||
|
摄像机信息及参数改为紧凑布局;全屏监控隐藏管理参数入口。读取或保存时暂时锁定参数输入,避免正在编辑的值被返回结果覆盖。视频服务错误响应也会正确关闭,修复新 Python 版本提示的资源未释放问题。
|
||||||
|
|||||||
@@ -343,6 +343,7 @@ def media_json(path, playback=False):
|
|||||||
with urllib.request.urlopen(req, timeout=5) as r:
|
with urllib.request.urlopen(req, timeout=5) as r:
|
||||||
return json.load(r)
|
return json.load(r)
|
||||||
except urllib.error.HTTPError as exc:
|
except urllib.error.HTTPError as exc:
|
||||||
|
with exc:
|
||||||
if playback and path.startswith('/list?') and exc.code == 404:
|
if playback and path.startswith('/list?') and exc.code == 404:
|
||||||
try:
|
try:
|
||||||
detail = json.loads(exc.read(4096))
|
detail = json.loads(exc.read(4096))
|
||||||
|
|||||||
@@ -338,12 +338,14 @@ class WebTests(unittest.TestCase):
|
|||||||
io.BytesIO(b'{"error":"no recording segments found"}'))
|
io.BytesIO(b'{"error":"no recording segments found"}'))
|
||||||
with patch('app.urllib.request.urlopen', side_effect=error):
|
with patch('app.urllib.request.urlopen', side_effect=error):
|
||||||
self.assertEqual(app.media_json('/list?path=example', playback=True), [])
|
self.assertEqual(app.media_json('/list?path=example', playback=True), [])
|
||||||
|
self.assertTrue(error.fp.closed)
|
||||||
|
|
||||||
def test_real_media_service_error_is_preserved(self):
|
def test_real_media_service_error_is_preserved(self):
|
||||||
error = app.urllib.error.HTTPError('http://localhost/list', 500, 'Error', {}, io.BytesIO(b'{}'))
|
error = app.urllib.error.HTTPError('http://localhost/list', 500, 'Error', {}, io.BytesIO(b'{}'))
|
||||||
with patch('app.urllib.request.urlopen', side_effect=error):
|
with patch('app.urllib.request.urlopen', side_effect=error):
|
||||||
with self.assertRaises(app.Problem):
|
with self.assertRaises(app.Problem):
|
||||||
app.media_json('/list?path=example', playback=True)
|
app.media_json('/list?path=example', playback=True)
|
||||||
|
self.assertTrue(error.fp.closed)
|
||||||
|
|
||||||
def family_save(self, kind, data):
|
def family_save(self, kind, data):
|
||||||
return app.FAMILY.save(kind, dict(data, revision=app.setting('familyRevision', 0)), self.admin)
|
return app.FAMILY.save(kind, dict(data, revision=app.setting('familyRevision', 0)), self.admin)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ async function loadCameraSettings(){
|
|||||||
if(!cameraSettingsContext)return;const context=cameraSettingsContext,generation=++cameraSettingsGeneration;
|
if(!cameraSettingsContext)return;const context=cameraSettingsContext,generation=++cameraSettingsGeneration;
|
||||||
context.settings=null;context.quality=$('#camera-settings-quality').value;
|
context.settings=null;context.quality=$('#camera-settings-quality').value;
|
||||||
$('#camera-settings-status').textContent='正在读取设备实际参数…';
|
$('#camera-settings-status').textContent='正在读取设备实际参数…';
|
||||||
$$('#camera-settings-dialog form button[type=submit]').forEach(b=>b.disabled=true);$('#camera-settings-reload').disabled=true;
|
$$('#camera-settings-dialog form input,#camera-settings-dialog form select,#camera-settings-dialog form button[type=submit]').forEach(b=>b.disabled=true);$('#camera-settings-reload').disabled=true;
|
||||||
try{const result=await api('/api/camera/settings?'+new URLSearchParams({camera:context.id,quality:context.quality}));
|
try{const result=await api('/api/camera/settings?'+new URLSearchParams({camera:context.id,quality:context.quality}));
|
||||||
if(generation!==cameraSettingsGeneration||context!==cameraSettingsContext||!loggedIn)return;context.settings=result;renderCameraSettings(result);
|
if(generation!==cameraSettingsGeneration||context!==cameraSettingsContext||!loggedIn)return;context.settings=result;renderCameraSettings(result);
|
||||||
}catch(e){if(!e.stale&&generation===cameraSettingsGeneration)$('#camera-settings-status').textContent=e.message;}
|
}catch(e){if(!e.stale&&generation===cameraSettingsGeneration)$('#camera-settings-status').textContent=e.message;}
|
||||||
@@ -42,7 +42,7 @@ async function saveCameraSettings(form,section){
|
|||||||
const context=cameraSettingsContext;if(!context?.settings)return;
|
const context=cameraSettingsContext;if(!context?.settings)return;
|
||||||
const values=changedCameraValues(form,section,context.settings);if(!Object.keys(values).length){toast('参数没有变化');return;}
|
const values=changedCameraValues(form,section,context.settings);if(!Object.keys(values).length){toast('参数没有变化');return;}
|
||||||
const generation=++cameraSettingsGeneration;
|
const generation=++cameraSettingsGeneration;
|
||||||
$$('#camera-settings-dialog form button[type=submit]').forEach(b=>b.disabled=true);$('#camera-settings-reload').disabled=true;$('#camera-settings-quality').disabled=true;
|
$$('#camera-settings-dialog form input,#camera-settings-dialog form select,#camera-settings-dialog form button[type=submit]').forEach(b=>b.disabled=true);$('#camera-settings-reload').disabled=true;$('#camera-settings-quality').disabled=true;
|
||||||
$('#camera-settings-status').textContent='正在保存并读取设备返回值…';
|
$('#camera-settings-status').textContent='正在保存并读取设备返回值…';
|
||||||
try{const result=await api('/api/camera/settings',{camera:context.id,quality:context.quality,revision:context.settings.revision,section,values});
|
try{const result=await api('/api/camera/settings',{camera:context.id,quality:context.quality,revision:context.settings.revision,section,values});
|
||||||
if(generation!==cameraSettingsGeneration||context!==cameraSettingsContext||!loggedIn)return;
|
if(generation!==cameraSettingsGeneration||context!==cameraSettingsContext||!loggedIn)return;
|
||||||
|
|||||||
@@ -11,3 +11,14 @@ textarea{font:inherit;color:#e3f0ef;background:#111e24;border:1px solid #3a4e55;
|
|||||||
.brand-icon{width:44px;height:44px;object-fit:cover;border-radius:12px;flex-shrink:0}.wall-toolbar{display:flex;align-items:center;gap:15px;flex-wrap:wrap;margin:16px 0}.wall-toolbar select{padding:8px}.wall-paging{display:flex;align-items:center;gap:9px;color:var(--muted);font-size:12px}#wall-clock{margin-left:auto;color:var(--muted);font-size:12px}.live-grid[data-layout="1"]{grid-template-columns:1fr}.live-grid[data-layout="2"],.live-grid[data-layout="4"]{grid-template-columns:repeat(2,minmax(0,1fr))}.live-grid[data-layout="6"],.live-grid[data-layout="9"]{grid-template-columns:repeat(3,minmax(0,1fr))}.live-grid[data-layout="16"]{grid-template-columns:repeat(4,minmax(0,1fr))}.wall-reorder{display:none;padding:8px 12px;align-items:center;gap:8px}.arranging .wall-reorder{display:flex}.wall-reorder span{cursor:grab;margin-right:auto;font-size:12px;color:var(--muted)}.wall-reorder button{padding:4px 9px;font-size:11px}#page-live:fullscreen{background:#070f12;padding:12px;width:100vw;height:100dvh;display:flex;flex-direction:column;overflow:hidden}#page-live:fullscreen>.section-head,#page-live:fullscreen>#quality-note{display:none}#page-live:fullscreen .wall-toolbar{margin:0 0 10px;flex-shrink:0}#page-live:fullscreen .live-grid{flex:1;min-height:0;gap:8px;grid-template-rows:repeat(2,minmax(0,1fr))}#page-live:fullscreen .live-grid[data-layout="1"],#page-live:fullscreen .live-grid[data-layout="2"]{grid-template-rows:1fr}#page-live:fullscreen .live-grid[data-layout="9"]{grid-template-rows:repeat(3,minmax(0,1fr))}#page-live:fullscreen .live-grid[data-layout="16"]{grid-template-rows:repeat(4,minmax(0,1fr))}#page-live:fullscreen .camera-card{display:flex;flex-direction:column;min-height:0;position:relative}#page-live:fullscreen .video-panel{flex:1;min-height:0;aspect-ratio:auto}#page-live:fullscreen .camera-meta{padding:7px 10px;align-items:center}#page-live:fullscreen .camera-meta p,#page-live:fullscreen .camera-meta .badge{display:none}#page-live:fullscreen .camera-meta strong{font-size:12px}#page-live:fullscreen .camera-actions{position:absolute;bottom:34px;right:6px;padding:4px;background:#10191ddd;border-radius:6px;opacity:0}#page-live:fullscreen .camera-card:hover .camera-actions,#page-live:fullscreen .camera-card:focus-within .camera-actions{opacity:1}#page-live:fullscreen .camera-actions [data-edit-camera],#page-live:fullscreen .camera-actions [data-play-camera]{display:none}.ptz-panel{background:#1a3038;border:1px solid #41656c;border-radius:12px;padding:16px;display:flex;align-items:center;gap:22px;position:fixed;bottom:22px;right:22px;z-index:4;max-width:calc(100vw - 32px);box-shadow:0 12px 50px #0008}.ptz-panel p{font-size:12px;color:var(--muted);max-width:320px;margin:8px 0 0}.ptz-pad{display:grid;grid-template-columns:repeat(3,46px);grid-template-rows:repeat(3,40px);gap:4px}.ptz-pad button{padding:5px;font-size:17px}.ptz-pad [data-ptz-action="up"]{grid-column:2}.ptz-pad [data-ptz-action="left"]{grid-column:1;grid-row:2}.ptz-pad [data-ptz-action="stop"]{grid-column:2;grid-row:2;background:#f0bda6;font-size:12px}.ptz-pad [data-ptz-action="right"]{grid-column:3;grid-row:2}.ptz-pad [data-ptz-action="down"]{grid-column:2;grid-row:3}.family-date{border:1px solid var(--line);border-radius:8px;padding:14px;margin:0;min-width:0}.family-date legend{font-size:13px;color:#a5bec1}.family-date label{margin-bottom:10px}.family-date output{display:block;font-size:12px;line-height:1.7;color:var(--accent)}@media(max-width:760px){.wall-toolbar{gap:9px}#wall-clock{display:none}.camera-meta strong{font-size:12px}.camera-actions{flex-wrap:wrap;padding:0 8px 8px}.camera-meta{padding:9px}.ptz-panel{flex-wrap:wrap;gap:12px}.ptz-panel>div:first-child{flex:1;min-width:160px}.family-date{grid-column:1/-1}#page-live:fullscreen .camera-actions{opacity:1}.live-grid[data-layout="16"] .camera-meta strong{font-size:9px}}
|
.brand-icon{width:44px;height:44px;object-fit:cover;border-radius:12px;flex-shrink:0}.wall-toolbar{display:flex;align-items:center;gap:15px;flex-wrap:wrap;margin:16px 0}.wall-toolbar select{padding:8px}.wall-paging{display:flex;align-items:center;gap:9px;color:var(--muted);font-size:12px}#wall-clock{margin-left:auto;color:var(--muted);font-size:12px}.live-grid[data-layout="1"]{grid-template-columns:1fr}.live-grid[data-layout="2"],.live-grid[data-layout="4"]{grid-template-columns:repeat(2,minmax(0,1fr))}.live-grid[data-layout="6"],.live-grid[data-layout="9"]{grid-template-columns:repeat(3,minmax(0,1fr))}.live-grid[data-layout="16"]{grid-template-columns:repeat(4,minmax(0,1fr))}.wall-reorder{display:none;padding:8px 12px;align-items:center;gap:8px}.arranging .wall-reorder{display:flex}.wall-reorder span{cursor:grab;margin-right:auto;font-size:12px;color:var(--muted)}.wall-reorder button{padding:4px 9px;font-size:11px}#page-live:fullscreen{background:#070f12;padding:12px;width:100vw;height:100dvh;display:flex;flex-direction:column;overflow:hidden}#page-live:fullscreen>.section-head,#page-live:fullscreen>#quality-note{display:none}#page-live:fullscreen .wall-toolbar{margin:0 0 10px;flex-shrink:0}#page-live:fullscreen .live-grid{flex:1;min-height:0;gap:8px;grid-template-rows:repeat(2,minmax(0,1fr))}#page-live:fullscreen .live-grid[data-layout="1"],#page-live:fullscreen .live-grid[data-layout="2"]{grid-template-rows:1fr}#page-live:fullscreen .live-grid[data-layout="9"]{grid-template-rows:repeat(3,minmax(0,1fr))}#page-live:fullscreen .live-grid[data-layout="16"]{grid-template-rows:repeat(4,minmax(0,1fr))}#page-live:fullscreen .camera-card{display:flex;flex-direction:column;min-height:0;position:relative}#page-live:fullscreen .video-panel{flex:1;min-height:0;aspect-ratio:auto}#page-live:fullscreen .camera-meta{padding:7px 10px;align-items:center}#page-live:fullscreen .camera-meta p,#page-live:fullscreen .camera-meta .badge{display:none}#page-live:fullscreen .camera-meta strong{font-size:12px}#page-live:fullscreen .camera-actions{position:absolute;bottom:34px;right:6px;padding:4px;background:#10191ddd;border-radius:6px;opacity:0}#page-live:fullscreen .camera-card:hover .camera-actions,#page-live:fullscreen .camera-card:focus-within .camera-actions{opacity:1}#page-live:fullscreen .camera-actions [data-edit-camera],#page-live:fullscreen .camera-actions [data-play-camera]{display:none}.ptz-panel{background:#1a3038;border:1px solid #41656c;border-radius:12px;padding:16px;display:flex;align-items:center;gap:22px;position:fixed;bottom:22px;right:22px;z-index:4;max-width:calc(100vw - 32px);box-shadow:0 12px 50px #0008}.ptz-panel p{font-size:12px;color:var(--muted);max-width:320px;margin:8px 0 0}.ptz-pad{display:grid;grid-template-columns:repeat(3,46px);grid-template-rows:repeat(3,40px);gap:4px}.ptz-pad button{padding:5px;font-size:17px}.ptz-pad [data-ptz-action="up"]{grid-column:2}.ptz-pad [data-ptz-action="left"]{grid-column:1;grid-row:2}.ptz-pad [data-ptz-action="stop"]{grid-column:2;grid-row:2;background:#f0bda6;font-size:12px}.ptz-pad [data-ptz-action="right"]{grid-column:3;grid-row:2}.ptz-pad [data-ptz-action="down"]{grid-column:2;grid-row:3}.family-date{border:1px solid var(--line);border-radius:8px;padding:14px;margin:0;min-width:0}.family-date legend{font-size:13px;color:#a5bec1}.family-date label{margin-bottom:10px}.family-date output{display:block;font-size:12px;line-height:1.7;color:var(--accent)}@media(max-width:760px){.wall-toolbar{gap:9px}#wall-clock{display:none}.camera-meta strong{font-size:12px}.camera-actions{flex-wrap:wrap;padding:0 8px 8px}.camera-meta{padding:9px}.ptz-panel{flex-wrap:wrap;gap:12px}.ptz-panel>div:first-child{flex:1;min-width:160px}.family-date{grid-column:1/-1}#page-live:fullscreen .camera-actions{opacity:1}.live-grid[data-layout="16"] .camera-meta strong{font-size:9px}}
|
||||||
#page-live:fullscreen .live-grid[data-layout="auto"]{grid-template-columns:repeat(3,minmax(0,1fr))}
|
#page-live:fullscreen .live-grid[data-layout="auto"]{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||||
.wall-reorder [data-wall-drag]{touch-action:none;user-select:none;padding:5px 0}.camera-card.wall-drop-target{outline:2px solid var(--accent);outline-offset:-3px}
|
.wall-reorder [data-wall-drag]{touch-action:none;user-select:none;padding:5px 0}.camera-card.wall-drop-target{outline:2px solid var(--accent);outline-offset:-3px}
|
||||||
|
|
||||||
|
#camera-settings-dialog{width:min(1060px,94vw)}
|
||||||
|
#camera-settings-info dl{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin:18px 0 24px;padding:16px;border:1px solid var(--line);border-radius:10px}
|
||||||
|
#camera-settings-info dt{font-size:12px;color:var(--muted);margin-bottom:6px}
|
||||||
|
#camera-settings-info dd{font-size:13px;margin:0;overflow-wrap:anywhere}
|
||||||
|
#camera-image-fields,#camera-video-fields{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||||
|
#camera-settings-status:empty{display:none}
|
||||||
|
#camera-settings-dialog details{margin-top:20px;color:var(--muted);font-size:13px;line-height:1.8}
|
||||||
|
#page-live:fullscreen .camera-actions [data-camera-settings]{display:none}
|
||||||
|
@media(max-width:700px){#camera-settings-info dl,#camera-image-fields,#camera-video-fields{grid-template-columns:1fr 1fr}}
|
||||||
|
@media(max-width:420px){#camera-settings-info dl,#camera-image-fields,#camera-video-fields{grid-template-columns:1fr}}
|
||||||
|
|||||||
Reference in New Issue
Block a user