openvibers/openvibe.live Commits

Clip UX: processing indicator on the clip page; dedupe clip announces

OP OpenVibers committed 17 Aug 2026 · eed59df798 · 3 files, +31 −2

- /clip/:id on a still-processing clip showed a black player — now shows a
  progress note and polls every 3s until the cut is ready.
- Clip announce sweeper: in-flight guard so stacked sweep ticks can't announce
  the same clip repeatedly when a Media lookup is slow (observed 6x spam).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Xw6Sf5zzSALw8m93XNu5JE

Changes

ddiff --git a/public/index.html b/public/index.html
iindex 1c475cba..5edfc2f4 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -4904,7 +4904,7 @@ https://www.101soundboards.com/sounds/12345 150p 2</code></pre>
<!-- Scripts -->
<script src="/js/theme-engine.js?v=4"></script>
<script src="/live-notify.js?v=2" defer></script>
--<script src="/js/app.js?v=119" defer></script>
++<script src="/js/app.js?v=120" defer></script>
<script src="/js/stream-player.js?v=17" defer></script>
<script src="/js/chat-shared.js?v=3" defer></script>
<script src="/js/emotes.js?v=8" defer></script>
ddiff --git a/public/js/app.js b/public/js/app.js
iindex 2469f671..ff3d536c 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -6760,6 +6760,27 @@ async function loadClipPlayer(clipId) {
}
const video = document.getElementById('clp-video');
++ clearTimeout(window._clpProcessingPoll);
++ { const _n = document.getElementById('clp-processing-note'); if (_n) _n.remove(); }
++ if (!cl.file_path && (cl.status || 'processing') !== 'ready') {
++ // Server is still cutting this clip — show progress instead of a black
++ // player, and poll until it's ready.
++ video.style.display = 'none';
++ const container = document.getElementById('clp-container');
++ if (container) {
++ const note = document.createElement('div');
++ note.id = 'clp-processing-note';
++ note.style.cssText = 'display:flex;flex-direction:column;align-items:center;justify-content:center;height:300px;gap:12px;color:var(--text-muted,#999)';
++ note.innerHTML = `
++ <i class="fa-solid fa-scissors fa-bounce" style="font-size:2.5rem;color:var(--accent)"></i>
++ <p style="font-size:1.05rem;font-weight:600">Clip is processing…</p>
++ <p class="muted" style="font-size:0.85rem">The server is cutting your clip — this page will update automatically.</p>`;
++ container.appendChild(note);
++ }
++ window._clpProcessingPoll = setTimeout(() => {
++ if (window._clpClipId === cl.id) loadClipPlayer(cl.id);
++ }, 3000);
++ }
if (cl.file_path) {
const filename = cl.file_path.split('/').pop();
// Handle video load errors (corrupt files, codec issues)
ddiff --git a/server/media-proxy/clip-notify.js b/server/media-proxy/clip-notify.js
iindex 3462a2bf..0f2febb4 100644
---- a/server/media-proxy/clip-notify.js
++++ b/server/media-proxy/clip-notify.js
@@@ -86,12 +86,20 @@ async function _sendOne(clipId) {
}
let _sweepTimer = null;
++// Clips whose announce is currently being sent. A slow Media getClip() once let
++// six 15s sweep ticks stack up on the same clip and announce it six times when
++// Media finally answered — a clip stays claimed here until its send resolves.
++const _inFlight = new Set();
function startClipNotifySweeper() {
if (_sweepTimer) return;
_sweepTimer = setInterval(() => {
try {
const due = db.getDueClipNotifies(20) || [];
-- for (const row of due) _sendOne(row.clip_id).catch(() => {});
++ for (const row of due) {
++ if (_inFlight.has(row.clip_id)) continue;
++ _inFlight.add(row.clip_id);
++ _sendOne(row.clip_id).catch(() => {}).finally(() => _inFlight.delete(row.clip_id));
++ }
} catch (e) { console.warn('[ClipNotify] sweep error:', e.message); }
}, SWEEP_INTERVAL_MS);
if (_sweepTimer.unref) _sweepTimer.unref();