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
+34 -8
View File
@@ -73,6 +73,7 @@ SCAN = {'running': False, 'items': [], 'error': '', 'network': '', 'at': None}
ATTEMPTS = {}
API_PORT, HLS_PORT, PLAYBACK_PORT = 19997, 18888, 19996
MEDIA_AUTH = None
DEFAULT_STORAGE = {'retentionDays': 30, 'maxGB': 40, 'reserveGB': 8}
class Problem(Exception):
@@ -329,7 +330,7 @@ def source(c, quality):
def write_media_config():
with LOCK:
paths = {}
policy = setting('storage', {'retentionDays': 7, 'maxGB': 40, 'reserveGB': 8})
policy = setting('storage', DEFAULT_STORAGE)
for c in objects('cameras'):
for quality in ('main', 'sub'):
paths[stream_name(c, quality)] = {
@@ -381,16 +382,24 @@ def media_json(path, playback=False):
return json.load(r)
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 in (400, 404):
try:
detail = json.loads(exc.read(4096))
if detail.get('error') == 'no recording segments found':
message = detail.get('error')
if exc.code == 404 and message == 'no recording segments found':
return []
# MediaMTX 1.21 returns 400 before this camera's first recording.
# Match only the requested camera directory, never permissions,
# another path, a corrupt file, or an unavailable service.
name = url.parse_qs(url.urlsplit(path).query).get('path', [''])[0]
if (exc.code == 400 and re.fullmatch(r'cam_[a-f0-9]{16}_main', name)
and message == 'lstat ' + str(RECORDINGS / name) + ': no such file or directory'):
return []
except (ValueError, AttributeError):
pass
raise Problem('视频服务暂不可用,或该时段尚无可回放录像', 503)
raise Problem('录像服务暂不可用,请稍后重试或联系管理员检查服务', 503)
except Exception:
raise Problem('视频服务暂不可用,或该时段尚无可回放录像', 503)
raise Problem('录像服务暂不可用,请稍后重试或联系管理员检查服务', 503)
def media_config_watch():
@@ -417,6 +426,22 @@ def media_config_watch():
time.sleep(5)
def recording_state(camera, ready, media_online):
if not camera.get('record'):
return 'off'
if not camera.get('enabled'):
return 'disabled'
if STORAGE['paused']:
return 'paused'
if not media_online:
return 'unavailable'
if not ready:
return 'offline'
if time.time() - RECENT_RECORDS.get(stream_name(camera), 0) < 45:
return 'recording'
return 'waiting'
def status():
try:
data = media_json('/v3/paths/list?itemsPerPage=200')
@@ -432,10 +457,10 @@ def status():
if endpoint:
c = dict(c, host=endpoint['host'], port=endpoint['port'])
main = ready.get(stream_name(c), {})
recording = recording_state(c, main.get('ready', False), media)
cameras.append({**public_camera(c), 'ready': main.get('ready', False), 'tracks': main.get('tracks', []),
'recordingRequested': c['enabled'] and c['record'] and not STORAGE['paused'],
'recordingActive': bool(c['enabled'] and c['record'] and not STORAGE['paused'] and main.get('ready', False)
and time.time() - RECENT_RECORDS.get(stream_name(c), 0) < 45)})
'recordingState': recording, 'recordingActive': recording == 'recording'})
with LOCK:
storage = dict(STORAGE)
scan = dict(SCAN)
@@ -867,6 +892,7 @@ class Handler(BaseHTTPRequestHandler):
self.connection.settimeout(15)
self.send_response(200)
self.send_header('Content-Type', 'video/mp4')
self.send_header('X-Accel-Buffering', 'no')
self.send_header('Cache-Control', 'no-store')
self.send_header('Accept-Ranges', 'none')
self.send_header('X-Content-Type-Options', 'nosniff')
@@ -1107,7 +1133,7 @@ def initialize():
HERITAGE.initialize()
JOURNAL.initialize()
if not setting('storage'):
set_setting('storage', {'retentionDays': 7, 'maxGB': 40, 'reserveGB': 8})
set_setting('storage', DEFAULT_STORAGE)
if not setting('mediaSecret'):
set_setting('mediaSecret', secrets.token_hex(32))
MEDIA_AUTH = 'Basic ' + base64.b64encode(('vision:' + setting('mediaSecret')).encode()).decode()