controller UI: visual overlay editor + snapshot preview fallback
Поверх preview (image от /snapshot endpoint) добавлен editor layer с
drag-and-drop созданием / перемещением / resize / удалением overlay'ев
через HTTP API (POST/PATCH/DELETE /overlay/{instance}[/{id}]).
Toolbar: draw mode toggle, type (rect-border / rect-fill / dim / text),
color picker, opacity slider. Drag по preview → создание нового overlay
с правильными нормализованными координатами. Клик на overlay → выбор +
drag перемещение, нижний-правый handle = resize, красный × delete.
Cell-bound overlays (Frigate motion borders, dynamic_overlays grafana
panel) не отрисовываются в editor — они уже физически в видео output
и заполняли весь editor area, делая cursor:not-allowed везде. Только
absolute (cell=null) overlays редактируемы.
Preview: HLS отключён.
mediamtx в LL-HLS режиме отдаёт пустые audio fragments
(passthrough-remuxer: "No samples for initPTS at playlist time",
"Duration parsed from mp4 should be greater than zero"). hls.js застревает
с readyState=1, buffered=0. lowLatencyMode:false на клиенте не помог —
mediamtx сам делит на video1_stream.m3u8 + audio2_stream.m3u8 на server
side. Diagnosed через Playwright + hls.js debug logs.
Заменено на <img> с polling /snapshot/{instance} (~700ms request, де-факто
~5s из-за времени ffmpeg RTSP+PNG, но достаточно как reference для
overlay positioning).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,41 @@ label { font-size:11px; color:var(--muted); display:block; margin-bottom:2px; }
|
|||||||
pre { background:#0f0f12; padding:8px; border-radius:4px; font-size:11px; overflow-x:auto; max-height:180px; margin:0; }
|
pre { background:#0f0f12; padding:8px; border-radius:4px; font-size:11px; overflow-x:auto; max-height:180px; margin:0; }
|
||||||
.kv { display:grid; grid-template-columns:auto 1fr; gap:4px 10px; font-size:12px; }
|
.kv { display:grid; grid-template-columns:auto 1fr; gap:4px 10px; font-size:12px; }
|
||||||
.kv .k { color:var(--muted); }
|
.kv .k { color:var(--muted); }
|
||||||
|
/* ── Visual overlay editor ─────────────────────────── */
|
||||||
|
.editor-layer {
|
||||||
|
position:absolute; pointer-events:auto; user-select:none;
|
||||||
|
cursor:crosshair; z-index:15;
|
||||||
|
}
|
||||||
|
.editor-layer.dragging { cursor:grabbing; }
|
||||||
|
.editor-layer.disabled { display:none; }
|
||||||
|
.ov-box {
|
||||||
|
position:absolute; box-sizing:border-box;
|
||||||
|
border:2px solid var(--accent); background:rgba(68,170,255,0.15);
|
||||||
|
font-size:11px; color:#fff; text-shadow:0 0 3px #000;
|
||||||
|
pointer-events:auto; cursor:grab;
|
||||||
|
min-width:8px; min-height:8px;
|
||||||
|
}
|
||||||
|
.ov-box.cell-bound { border-style:dashed; border-color:#888; background:rgba(128,128,128,0.08); cursor:not-allowed; }
|
||||||
|
.ov-box.selected { border-color:var(--warn); background:rgba(255,170,68,0.18); z-index:10; }
|
||||||
|
.ov-box .ov-label { position:absolute; top:-18px; left:0; padding:1px 4px; background:rgba(0,0,0,0.7); border-radius:2px; white-space:nowrap; }
|
||||||
|
.ov-box .ov-del {
|
||||||
|
position:absolute; top:-10px; right:-10px; width:20px; height:20px;
|
||||||
|
background:var(--err); color:#fff; border:1px solid #000; border-radius:50%;
|
||||||
|
font-size:14px; line-height:18px; text-align:center; cursor:pointer;
|
||||||
|
pointer-events:auto;
|
||||||
|
}
|
||||||
|
.ov-box .ov-handle {
|
||||||
|
position:absolute; right:-5px; bottom:-5px; width:12px; height:12px;
|
||||||
|
background:var(--accent); border:1px solid #000; cursor:nwse-resize;
|
||||||
|
}
|
||||||
|
.ov-draft {
|
||||||
|
position:absolute; border:2px dashed var(--good); background:rgba(68,255,136,0.15);
|
||||||
|
pointer-events:none; box-sizing:border-box;
|
||||||
|
}
|
||||||
|
.video-pane { position:relative; }
|
||||||
|
.editor-toolbar { position:absolute; top:8px; left:8px; z-index:20; background:rgba(0,0,0,0.7); border-radius:4px; padding:6px 10px; font-size:12px; display:flex; gap:8px; align-items:center; }
|
||||||
|
.editor-toolbar label { margin:0; color:#fff; display:flex; gap:4px; align-items:center; }
|
||||||
|
.editor-toolbar select, .editor-toolbar input { width:auto; padding:2px 6px; font-size:11px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -45,7 +80,24 @@ pre { background:#0f0f12; padding:8px; border-radius:4px; font-size:11px; overfl
|
|||||||
|
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<div class="video-pane">
|
<div class="video-pane">
|
||||||
<video id="video" controls autoplay muted playsinline></video>
|
<video id="video" autoplay muted playsinline style="pointer-events:none; display:none"></video>
|
||||||
|
<img id="snap-preview" alt="live preview" style="width:100%; height:100%; object-fit:contain; display:block; background:#000; pointer-events:none">
|
||||||
|
<div id="snap-stats" style="position:absolute; top:8px; right:8px; z-index:20; background:rgba(0,0,0,0.6); color:#fff; font-size:11px; padding:2px 6px; border-radius:3px">snapshot —</div>
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<label><input id="edit-enable" type="checkbox" checked> draw mode</label>
|
||||||
|
<label>type:
|
||||||
|
<select id="edit-type">
|
||||||
|
<option value="rect-border">rect — border</option>
|
||||||
|
<option value="rect-fill">rect — fill</option>
|
||||||
|
<option value="dim">dim (privacy)</option>
|
||||||
|
<option value="text">text</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>color: <input id="edit-color" type="color" value="#ff8800"></label>
|
||||||
|
<label>opacity: <input id="edit-opacity" type="range" min="0.1" max="1" step="0.05" value="0.4" style="width:80px"></label>
|
||||||
|
<span id="edit-hint" style="color:var(--muted)">drag на видео → создать; клик на overlay → выбрать</span>
|
||||||
|
</div>
|
||||||
|
<div class="editor-layer" id="editor"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
@@ -132,20 +184,45 @@ pre { background:#0f0f12; padding:8px; border-radius:4px; font-size:11px; overfl
|
|||||||
const INSTANCE = 'tv_grid';
|
const INSTANCE = 'tv_grid';
|
||||||
const HLS_URL = `http://${location.hostname}:8888/live/index.m3u8`;
|
const HLS_URL = `http://${location.hostname}:8888/live/index.m3u8`;
|
||||||
|
|
||||||
// ── Video ─────────────────────────────────────────────
|
// ── Video preview — snapshot-loop (~1.5 fps) ───────────
|
||||||
|
// HLS отключен: mediamtx LL-HLS отдаёт пустые audio fragments
|
||||||
|
// (passthrough-remuxer: "No samples for initPTS, Duration=0") → playback stuck.
|
||||||
|
// Для overlay editor использовать /snapshot/{instance} polling — latency
|
||||||
|
// ~700 ms но достаточно для drag/resize reference.
|
||||||
function initVideo() {
|
function initVideo() {
|
||||||
const v = document.getElementById('video');
|
const img = document.getElementById('snap-preview');
|
||||||
if (Hls.isSupported()) {
|
const stat = document.getElementById('snap-stats');
|
||||||
const hls = new Hls({ liveDurationInfinity:true, lowLatencyMode:true, maxBufferLength:6 });
|
let busy = false, count = 0, lastT = Date.now(), nativeW = 0, nativeH = 0;
|
||||||
hls.loadSource(HLS_URL);
|
async function tick() {
|
||||||
hls.attachMedia(v);
|
if (busy) return;
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, () => v.play().catch(()=>{}));
|
busy = true;
|
||||||
hls.on(Hls.Events.ERROR, (_,d) => { if (d.fatal) setStatus('HLS error: '+d.type, true); });
|
try {
|
||||||
} else if (v.canPlayType('application/vnd.apple.mpegurl')) {
|
const t0 = performance.now();
|
||||||
v.src = HLS_URL; v.play().catch(()=>{});
|
const r = await fetch(`/snapshot/${INSTANCE}`, {method:'POST', cache:'no-store'});
|
||||||
} else {
|
if (!r.ok) { setStatus('snapshot fail '+r.status, true); busy=false; return; }
|
||||||
setStatus('HLS не supported in this browser', true);
|
const blob = await r.blob();
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const prev = img.src;
|
||||||
|
img.onload = () => {
|
||||||
|
nativeW = img.naturalWidth; nativeH = img.naturalHeight;
|
||||||
|
syncEditorBounds();
|
||||||
|
if (prev && prev.startsWith('blob:')) URL.revokeObjectURL(prev);
|
||||||
|
};
|
||||||
|
img.src = url;
|
||||||
|
count++;
|
||||||
|
const dt = performance.now() - t0;
|
||||||
|
if (Date.now() - lastT > 2000) {
|
||||||
|
stat.textContent = `snapshot ${count} · ${dt.toFixed(0)}ms`;
|
||||||
|
lastT = Date.now();
|
||||||
|
}
|
||||||
|
} catch (e) { setStatus('snap '+e.message, true); }
|
||||||
|
busy = false;
|
||||||
}
|
}
|
||||||
|
tick();
|
||||||
|
setInterval(tick, 700);
|
||||||
|
// expose для syncEditorBounds
|
||||||
|
window.__previewEl = img;
|
||||||
|
window.__previewSize = () => ({w: nativeW, h: nativeH});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── API helpers ───────────────────────────────────────
|
// ── API helpers ───────────────────────────────────────
|
||||||
@@ -326,6 +403,228 @@ async function refreshState() {
|
|||||||
} catch (e) { setStatus('refresh fail: '+e.message, true); }
|
} catch (e) { setStatus('refresh fail: '+e.message, true); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Visual editor: drag/draw/delete overlays поверх video ─
|
||||||
|
const editor = document.getElementById('editor');
|
||||||
|
const videoEl = document.getElementById('video');
|
||||||
|
let currentOverlays = []; // последний snapshot от /overlay/{INSTANCE}
|
||||||
|
let selectedId = null;
|
||||||
|
let draftEl = null; // div для рисуемого rect
|
||||||
|
let dragState = null; // {mode:'draw|move|resize', id, startX, startY, orig}
|
||||||
|
|
||||||
|
// Размещаем editor layer ровно поверх видимой area preview element'a (с учётом
|
||||||
|
// object-fit:contain — есть letterbox bars сверху/снизу или по бокам).
|
||||||
|
// Preview source — snapshot image (HLS disabled из-за mediamtx LL-HLS bug).
|
||||||
|
function syncEditorBounds() {
|
||||||
|
const el = window.__previewEl || videoEl;
|
||||||
|
const sz = window.__previewSize ? window.__previewSize() : {w: videoEl.videoWidth, h: videoEl.videoHeight};
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
const pane = el.parentElement.getBoundingClientRect();
|
||||||
|
const vw = sz.w || 1920, vh = sz.h || 1080;
|
||||||
|
const containerRatio = r.width / r.height;
|
||||||
|
const videoRatio = vw / vh;
|
||||||
|
let w, h;
|
||||||
|
if (containerRatio > videoRatio) { // letterbox по бокам
|
||||||
|
h = r.height; w = h * videoRatio;
|
||||||
|
} else { // letterbox сверху/снизу
|
||||||
|
w = r.width; h = w / videoRatio;
|
||||||
|
}
|
||||||
|
const left = (r.left - pane.left) + (r.width - w) / 2;
|
||||||
|
const top = (r.top - pane.top) + (r.height - h) / 2;
|
||||||
|
editor.style.left = left + 'px';
|
||||||
|
editor.style.top = top + 'px';
|
||||||
|
editor.style.width = w + 'px';
|
||||||
|
editor.style.height = h + 'px';
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', syncEditorBounds);
|
||||||
|
videoEl.addEventListener('loadedmetadata', syncEditorBounds);
|
||||||
|
setInterval(syncEditorBounds, 1000); // защита от async layout перестроек
|
||||||
|
|
||||||
|
function renderOverlays() {
|
||||||
|
const bounds = editor.getBoundingClientRect();
|
||||||
|
if (bounds.width < 10) return; // editor ещё не layout'нулся
|
||||||
|
// remove существующие .ov-box (но не draft)
|
||||||
|
editor.querySelectorAll('.ov-box').forEach(e => e.remove());
|
||||||
|
currentOverlays.forEach(ov => {
|
||||||
|
const hasGeo = ov.x !== undefined && ov.y !== undefined;
|
||||||
|
if (!hasGeo) return;
|
||||||
|
// Cell-bound overlays (Frigate borders, motion indicators, cell-anchored icons)
|
||||||
|
// уже физически нарисованы фильтром на видео и не редактируются из UI —
|
||||||
|
// не отрисовываем дубликат в HTML (иначе они покрывают всю площадь cursor=not-allowed).
|
||||||
|
if (ov.cell !== null && ov.cell !== undefined) return;
|
||||||
|
const w = ov.w ?? 0.05, h = ov.h ?? 0.05;
|
||||||
|
const box = document.createElement('div');
|
||||||
|
box.className = 'ov-box' + (ov.id === selectedId ? ' selected' : '');
|
||||||
|
box.dataset.id = ov.id;
|
||||||
|
box.style.left = (ov.x * 100) + '%';
|
||||||
|
box.style.top = (ov.y * 100) + '%';
|
||||||
|
box.style.width = (w * 100) + '%';
|
||||||
|
box.style.height = (h * 100) + '%';
|
||||||
|
if (ov.color) box.style.borderColor = ov.color;
|
||||||
|
const lab = document.createElement('div');
|
||||||
|
lab.className = 'ov-label'; lab.textContent = `${ov.type}: ${ov.id}`;
|
||||||
|
box.appendChild(lab);
|
||||||
|
const del = document.createElement('div');
|
||||||
|
del.className = 'ov-del'; del.textContent = '×';
|
||||||
|
del.title = 'Удалить overlay';
|
||||||
|
del.addEventListener('mousedown', (e) => e.stopPropagation());
|
||||||
|
del.addEventListener('click', (e) => { e.stopPropagation(); deleteOverlay(ov.id); });
|
||||||
|
box.appendChild(del);
|
||||||
|
const handle = document.createElement('div');
|
||||||
|
handle.className = 'ov-handle';
|
||||||
|
handle.addEventListener('mousedown', (e) => startResize(e, ov));
|
||||||
|
box.appendChild(handle);
|
||||||
|
box.addEventListener('mousedown', (e) => startMove(e, ov));
|
||||||
|
editor.appendChild(box);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtCoord(v) { return Math.max(0, Math.min(1, v)).toFixed(4); }
|
||||||
|
function pickColor(ov) { return document.getElementById('edit-color').value; }
|
||||||
|
|
||||||
|
function startDraw(e) {
|
||||||
|
if (e.target !== editor) return; // если клик на существующий — другой handler
|
||||||
|
if (!document.getElementById('edit-enable').checked) return;
|
||||||
|
const bounds = editor.getBoundingClientRect();
|
||||||
|
dragState = {
|
||||||
|
mode: 'draw',
|
||||||
|
startX: e.clientX - bounds.left,
|
||||||
|
startY: e.clientY - bounds.top,
|
||||||
|
};
|
||||||
|
draftEl = document.createElement('div');
|
||||||
|
draftEl.className = 'ov-draft';
|
||||||
|
draftEl.style.left = dragState.startX + 'px';
|
||||||
|
draftEl.style.top = dragState.startY + 'px';
|
||||||
|
draftEl.style.width = '0'; draftEl.style.height = '0';
|
||||||
|
editor.appendChild(draftEl);
|
||||||
|
editor.classList.add('dragging');
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
editor.addEventListener('mousedown', startDraw);
|
||||||
|
|
||||||
|
function startMove(e, ov) {
|
||||||
|
e.stopPropagation();
|
||||||
|
selectedId = ov.id; renderOverlays();
|
||||||
|
const bounds = editor.getBoundingClientRect();
|
||||||
|
dragState = {
|
||||||
|
mode: 'move', id: ov.id,
|
||||||
|
startX: e.clientX, startY: e.clientY,
|
||||||
|
origX: ov.x, origY: ov.y, orig: ov,
|
||||||
|
};
|
||||||
|
editor.classList.add('dragging');
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
function startResize(e, ov) {
|
||||||
|
e.stopPropagation();
|
||||||
|
selectedId = ov.id; renderOverlays();
|
||||||
|
const bounds = editor.getBoundingClientRect();
|
||||||
|
dragState = {
|
||||||
|
mode: 'resize', id: ov.id,
|
||||||
|
startX: e.clientX, startY: e.clientY,
|
||||||
|
origW: ov.w ?? 0.05, origH: ov.h ?? 0.05, orig: ov,
|
||||||
|
};
|
||||||
|
editor.classList.add('dragging');
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', (e) => {
|
||||||
|
if (!dragState) return;
|
||||||
|
const bounds = editor.getBoundingClientRect();
|
||||||
|
if (dragState.mode === 'draw') {
|
||||||
|
const cx = e.clientX - bounds.left, cy = e.clientY - bounds.top;
|
||||||
|
const x = Math.min(cx, dragState.startX);
|
||||||
|
const y = Math.min(cy, dragState.startY);
|
||||||
|
draftEl.style.left = x + 'px';
|
||||||
|
draftEl.style.top = y + 'px';
|
||||||
|
draftEl.style.width = Math.abs(cx - dragState.startX) + 'px';
|
||||||
|
draftEl.style.height = Math.abs(cy - dragState.startY) + 'px';
|
||||||
|
} else if (dragState.mode === 'move') {
|
||||||
|
const dx = (e.clientX - dragState.startX) / bounds.width;
|
||||||
|
const dy = (e.clientY - dragState.startY) / bounds.height;
|
||||||
|
const ov = dragState.orig;
|
||||||
|
ov.x = Math.max(0, Math.min(1 - (ov.w ?? 0.05), dragState.origX + dx));
|
||||||
|
ov.y = Math.max(0, Math.min(1 - (ov.h ?? 0.05), dragState.origY + dy));
|
||||||
|
renderOverlays();
|
||||||
|
} else if (dragState.mode === 'resize') {
|
||||||
|
const dw = (e.clientX - dragState.startX) / bounds.width;
|
||||||
|
const dh = (e.clientY - dragState.startY) / bounds.height;
|
||||||
|
const ov = dragState.orig;
|
||||||
|
ov.w = Math.max(0.02, Math.min(1 - ov.x, dragState.origW + dw));
|
||||||
|
ov.h = Math.max(0.02, Math.min(1 - ov.y, dragState.origH + dh));
|
||||||
|
renderOverlays();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', async (e) => {
|
||||||
|
if (!dragState) return;
|
||||||
|
editor.classList.remove('dragging');
|
||||||
|
const st = dragState; dragState = null;
|
||||||
|
if (st.mode === 'draw' && draftEl) {
|
||||||
|
const bounds = editor.getBoundingClientRect();
|
||||||
|
const cx = e.clientX - bounds.left, cy = e.clientY - bounds.top;
|
||||||
|
const x0 = Math.min(cx, st.startX), y0 = Math.min(cy, st.startY);
|
||||||
|
const w = Math.abs(cx - st.startX), h = Math.abs(cy - st.startY);
|
||||||
|
draftEl.remove(); draftEl = null;
|
||||||
|
if (w < 8 || h < 8) return; // click без drag
|
||||||
|
const tSel = document.getElementById('edit-type').value;
|
||||||
|
const opacity = +document.getElementById('edit-opacity').value;
|
||||||
|
const color = document.getElementById('edit-color').value;
|
||||||
|
const body = {
|
||||||
|
id: rndId(),
|
||||||
|
x: +fmtCoord(x0 / bounds.width),
|
||||||
|
y: +fmtCoord(y0 / bounds.height),
|
||||||
|
opacity: opacity,
|
||||||
|
};
|
||||||
|
if (tSel === 'rect-border' || tSel === 'rect-fill') {
|
||||||
|
body.type = 'rect';
|
||||||
|
body.w = +fmtCoord(w / bounds.width);
|
||||||
|
body.h = +fmtCoord(h / bounds.height);
|
||||||
|
body.color = color;
|
||||||
|
body.border_only = (tSel === 'rect-border');
|
||||||
|
body.border_width = 3;
|
||||||
|
} else if (tSel === 'dim') {
|
||||||
|
body.type = 'dim';
|
||||||
|
body.w = +fmtCoord(w / bounds.width);
|
||||||
|
body.h = +fmtCoord(h / bounds.height);
|
||||||
|
body.color = '#000000';
|
||||||
|
body.dim_factor = opacity;
|
||||||
|
} else if (tSel === 'text') {
|
||||||
|
const txt = prompt('Текст:', 'Hello');
|
||||||
|
if (!txt) return;
|
||||||
|
body.type = 'text';
|
||||||
|
body.text = txt;
|
||||||
|
body.font_size = Math.max(12, Math.round(h * (videoEl.videoHeight || 1080) * 0.8));
|
||||||
|
body.color = color;
|
||||||
|
}
|
||||||
|
await api('POST', `/overlay/${INSTANCE}/add`, body, '+ '+body.type+' '+body.id);
|
||||||
|
refreshOverlays();
|
||||||
|
} else if (st.mode === 'move' || st.mode === 'resize') {
|
||||||
|
const ov = st.orig;
|
||||||
|
// PATCH с новыми координатами
|
||||||
|
await api('PATCH', `/overlay/${INSTANCE}/${ov.id}`, ov, ov.id+' moved');
|
||||||
|
refreshOverlays();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function deleteOverlay(id) {
|
||||||
|
if (!confirm('Удалить overlay ' + id + '?')) return;
|
||||||
|
await api('DELETE', `/overlay/${INSTANCE}/${id}`, null, 'deleted '+id);
|
||||||
|
if (selectedId === id) selectedId = null;
|
||||||
|
refreshOverlays();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshOverlays() {
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/overlay/${INSTANCE}`);
|
||||||
|
if (!r.ok) return;
|
||||||
|
const d = await r.json();
|
||||||
|
if (!dragState) { // не overwrite пока user тащит
|
||||||
|
currentOverlays = d.overlays;
|
||||||
|
renderOverlays();
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
setInterval(refreshOverlays, 1500);
|
||||||
|
|
||||||
// ── Init ──────────────────────────────────────────────
|
// ── Init ──────────────────────────────────────────────
|
||||||
// Defensive init — async (non-video) FIRST so кнопки работают независимо от HLS.
|
// Defensive init — async (non-video) FIRST so кнопки работают независимо от HLS.
|
||||||
try { ovTypeChanged(); } catch(e) { console.error('ovTypeChanged', e); }
|
try { ovTypeChanged(); } catch(e) { console.error('ovTypeChanged', e); }
|
||||||
@@ -335,6 +634,7 @@ try { loadAudio(); } catch(e) { console.error('loadAudio', e); }
|
|||||||
try { loadHistory(); } catch(e) { console.error('loadHistory', e); }
|
try { loadHistory(); } catch(e) { console.error('loadHistory', e); }
|
||||||
try { refreshState(); setInterval(refreshState, 2000); } catch(e) { console.error('refreshState', e); }
|
try { refreshState(); setInterval(refreshState, 2000); } catch(e) { console.error('refreshState', e); }
|
||||||
try { initVideo(); } catch(e) { console.error('initVideo', e); setStatus('video init failed (HLS?)', true); }
|
try { initVideo(); } catch(e) { console.error('initVideo', e); setStatus('video init failed (HLS?)', true); }
|
||||||
|
try { syncEditorBounds(); refreshOverlays(); } catch(e) { console.error('editor init', e); }
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user