Compare commits

..

2 Commits

Author SHA1 Message Date
Codex a887a76095 fix: preserve authenticated HLS session redirects 2026-10-03 16:45:58 +08:00
Codex 7962a7e50a fix: reconcile media paths after batched configuration updates 2026-10-03 16:26:28 +08:00
3 changed files with 43 additions and 2 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.1
0.1.3
+36
View File
@@ -302,6 +302,30 @@ def media_json(path, playback=False):
raise Problem('视频服务暂不可用,或该时段尚无可回放录像', 503)
def media_config_watch():
# Reconcile API configuration after atomic file replacements. Multiple rapid
# writes can be coalesced by a filesystem watcher; the file remains authority.
applied = None
while True:
try:
raw = CONFIG.read_bytes()
digest = hashlib.sha256(raw).digest()
if digest != applied:
desired = json.loads(raw)['paths']
existing = {p['name'] for p in media_json('/v3/config/paths/list?itemsPerPage=200').get('items', [])}
for name, body in desired.items():
action, method = ('patch', 'PATCH') if name in existing else ('add', 'POST')
req = urllib.request.Request('http://127.0.0.1:' + str(API_PORT) + '/v3/config/paths/' + action + '/' + name,
data=json.dumps(body).encode(), method=method,
headers={'Authorization': MEDIA_AUTH, 'Content-Type': 'application/json'})
with urllib.request.urlopen(req, timeout=5) as response:
response.read()
applied = digest
except Exception:
pass # Retry on the next pass, including after a media process restart.
time.sleep(5)
def status():
try:
data = media_json('/v3/paths/list?itemsPerPage=200')
@@ -571,7 +595,18 @@ class Handler(BaseHTTPRequestHandler):
r = conn.getresponse()
if r.status >= 400:
raise Problem('暂时没有可播放画面,请确认设备网络、账号、码流路径和录像时间', 503)
location = None
if 300 <= r.status < 400:
# MediaMTX starts an HLS session with a relative redirect. The
# browser must keep that session query on subsequent playlists.
target = url.urlsplit(url.urljoin(path, r.getheader('Location', '')))
if (port != HLS_PORT or target.scheme or target.netloc
or not re.fullmatch(r'/cam_[a-f0-9]{16}_(main|sub)/[a-zA-Z0-9_.-]+', target.path)):
raise Problem('视频服务返回了无效的播放跳转', 502)
location = '/media/live' + target.path + ('?' + target.query if target.query else '')
self.send_response(r.status)
if location:
self.send_header('Location', location)
for k in ('Content-Type', 'Content-Length', 'Content-Range', 'Accept-Ranges'):
if r.getheader(k):
self.send_header(k, r.getheader(k))
@@ -682,6 +717,7 @@ def initialize():
if __name__ == '__main__':
initialize()
threading.Thread(target=storage_watch, daemon=True).start()
threading.Thread(target=media_config_watch, daemon=True).start()
bindings = [h.strip() for h in os.environ.get('VISION_BIND', '127.0.0.1').split(',') if h.strip()]
port = int(os.environ.get('VISION_PORT', '8790'))
servers = []
+6 -1
View File
@@ -65,7 +65,12 @@ function renderLive() {
video.onerror=()=>error('浏览器无法解码此码流。可尝试切换清晰度,或将摄像头设为 H.264。');
if (window.Hls?.isSupported()) {
const hls = new Hls({maxBufferLength:10,backBufferLength:5,liveSyncDurationCount:3,manifestLoadingMaxRetry:2,levelLoadingMaxRetry:2});
hls.on(Hls.Events.ERROR,(_,data)=>{if(data.fatal){error('画面暂未接通,请检查设备账号、路径、网络或编码。点击刷新状态可重试。');hls.destroy();}});
hls.on(Hls.Events.ERROR,(_,data)=>{if(data.fatal){
const codecError = /codec|bufferAddCodec|manifestIncompatible/i.test(data.details || '');
error(codecError ? '浏览器不支持当前视频编码,需要服务器提供兼容码流。' : '画面加载失败('+(data.details || data.type)+')。点击刷新状态可重试。');
console.warn('Video playback failed', c.id, data.type, data.details);
hls.destroy();
}});
hls.on(Hls.Events.MANIFEST_PARSED,()=>video.play().catch(()=>{}));
hls.loadSource(src);hls.attachMedia(video);players.push(hls);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {video.src=src;video.play().catch(()=>{});}