openvibers/openvibe.live Commits
Clips: stop telling viewers a failed clip is still being cut
Changes
@@@ -4970,7 +4970,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig<!-- Scripts --><script src="/js/theme-engine.js?v=4"></script><script src="/live-notify.js?v=3" defer></script>--<script src="/js/app.js?v=124" defer></script>++<script src="/js/app.js?v=125" defer></script><script src="/js/stream-player.js?v=20" defer></script><script src="/js/chat-shared.js?v=3" defer></script><script src="/js/emotes.js?v=8" defer></script>@@@ -6788,6 +6788,20 @@ function _startClipCooldownUI(btn) {}/* ── Clip Player ──────────────────────────────────────────────── */++/** Ask the server to re-render a clip whose cut failed, then resume polling. */++async function recutClip(clipId) {++ const note = document.getElementById('clp-processing-note');++ if (note) note.innerHTML = '<i class="fa-solid fa-spinner fa-spin" style="font-size:2rem;color:var(--accent)"></i><p class="muted">Re-cutting…</p>';++ try {++ await api(`/clips/${clipId}/recut`, { method: 'POST' });++ toast('Re-cutting the clip — this page will update when it is ready', 'info');++ setTimeout(() => { if (window._clpClipId === clipId) loadClipPlayer(clipId); }, 3000);++ } catch (e) {++ toast(e.message, 'error');++ if (window._clpClipId === clipId) loadClipPlayer(clipId);++ }++}++async function loadClipPlayer(clipId) {try {// Clean up chat replay@@@ -6880,23 +6894,34 @@ async function loadClipPlayer(clipId) {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.++ // A clip with no file is either still being cut or the cut FAILED. These used++ // to render identically — a failed clip showed "the server is cutting your++ // clip" and polled forever, which is why hours-old broken clips still claimed++ // to be in progress. Tell the truth, and offer a retry.++ const failed = String(cl.status || '') === 'failed';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 = `++ note.innerHTML = failed ? `++ <i class="fa-solid fa-triangle-exclamation" style="font-size:2.5rem;color:var(--danger,#e74c3c)"></i>++ <p style="font-size:1.05rem;font-weight:600">This clip failed to render</p>++ <p class="muted" style="font-size:0.85rem">The server couldn't cut it from the recording.</p>++ <button class="btn btn-sm btn-outline" onclick="recutClip(${cl.id})"><i class="fa-solid fa-rotate-right"></i> Try again</button>` : `<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);++ // Only poll while it is genuinely in progress; polling a failed clip forever++ // was pure noise.++ if (!failed) {++ window._clpProcessingPoll = setTimeout(() => {++ if (window._clpClipId === cl.id) loadClipPlayer(cl.id);++ }, 3000);++ }}if (cl.file_path) {const filename = cl.file_path.split('/').pop();@@@ -191,6 +191,11 @@ function deleteClip(clipId, opts = {}) {return request('DELETE', `/clips/${clipId}`, opts);}++/** Ask Media to re-cut a clip whose cut failed. */++function recutClip(clipId, opts = {}) {++ return request('POST', `/clips/${clipId}/recut`, opts);++}++// ── Pastes ───────────────────────────────────────────────────────────────────/**@@@ -330,7 +335,7 @@ module.exports = {uploadVodChunk, completeVodChunks, finalizeVod,getVod, listVods, updateVod, deleteVod,// clips-- createClip, getClip, listClips, updateClip, deleteClip,++ createClip, getClip, listClips, updateClip, deleteClip, recutClip,// pastescreatePaste, getPaste, listPastes, deletePaste,// files + thumbnails@@@ -317,4 +317,22 @@ router.delete('/:id', requireAuth, async (req, res) => {}});++/**++ * Retry a failed clip cut. Same permission rule as deleting one: whoever may remove a++ * clip may also ask for it to be re-rendered. Nothing here is destructive — the row and++ * its timestamps are untouched, only the media is rebuilt.++ */++router.post('/:id/recut', requireAuth, async (req, res) => {++ try {++ let clip;++ try { clip = await media.getClip(req.params.id); } catch (err) { return mediaErr(res, err, 'Clip not found'); }++ if (!clip) return res.status(404).json({ error: 'Clip not found' });++ if (!(await canActorDeleteClip(req.user, clip))) return res.status(403).json({ error: 'Not authorized to re-cut this clip' });++ const out = await media.recutClip(clip.id);++ res.json({ message: 'Re-cutting clip', status: out?.status || 'processing' });++ } catch (err) {++ mediaErr(res, err, 'Failed to re-cut clip');++ }++});++module.exports = router;
@@ -4970,7 +4970,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
@@ -4970,7 +4970,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<!-- Scripts -->
<!-- Scripts -->
<script src="/js/theme-engine.js?v=4"></script>
<script src="/js/theme-engine.js?v=4"></script>
<script src="/live-notify.js?v=3" defer></script>
<script src="/live-notify.js?v=3" defer></script>
-<script src="/js/app.js?v=124" defer></script>
+<script src="/js/app.js?v=125" defer></script>
<script src="/js/stream-player.js?v=20" defer></script>
<script src="/js/stream-player.js?v=20" defer></script>
<script src="/js/chat-shared.js?v=3" defer></script>
<script src="/js/chat-shared.js?v=3" defer></script>
<script src="/js/emotes.js?v=8" defer></script>
<script src="/js/emotes.js?v=8" defer></script>
@@ -6788,6 +6788,20 @@ function _startClipCooldownUI(btn) {
@@ -6788,6 +6788,20 @@ function _startClipCooldownUI(btn) {
}
}
/* ── Clip Player ──────────────────────────────────────────────── */
/* ── Clip Player ──────────────────────────────────────────────── */
+/** Ask the server to re-render a clip whose cut failed, then resume polling. */
+async function recutClip(clipId) {
+ const note = document.getElementById('clp-processing-note');
+ if (note) note.innerHTML = '<i class="fa-solid fa-spinner fa-spin" style="font-size:2rem;color:var(--accent)"></i><p class="muted">Re-cutting…</p>';
+ try {
+ await api(`/clips/${clipId}/recut`, { method: 'POST' });
+ toast('Re-cutting the clip — this page will update when it is ready', 'info');
+ setTimeout(() => { if (window._clpClipId === clipId) loadClipPlayer(clipId); }, 3000);
+ } catch (e) {
+ toast(e.message, 'error');
+ if (window._clpClipId === clipId) loadClipPlayer(clipId);
+ }
+}
+
async function loadClipPlayer(clipId) {
async function loadClipPlayer(clipId) {
try {
try {
// Clean up chat replay
// Clean up chat replay
@@ -6880,23 +6894,34 @@ async function loadClipPlayer(clipId) {
@@ -6880,23 +6894,34 @@ async function loadClipPlayer(clipId) {
clearTimeout(window._clpProcessingPoll);
clearTimeout(window._clpProcessingPoll);
{ const _n = document.getElementById('clp-processing-note'); if (_n) _n.remove(); }
{ const _n = document.getElementById('clp-processing-note'); if (_n) _n.remove(); }
if (!cl.file_path && (cl.status || 'processing') !== 'ready') {
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.
+ // A clip with no file is either still being cut or the cut FAILED. These used
+ // to render identically — a failed clip showed "the server is cutting your
+ // clip" and polled forever, which is why hours-old broken clips still claimed
+ // to be in progress. Tell the truth, and offer a retry.
+ const failed = String(cl.status || '') === 'failed';
video.style.display = 'none';
video.style.display = 'none';
const container = document.getElementById('clp-container');
const container = document.getElementById('clp-container');
if (container) {
if (container) {
const note = document.createElement('div');
const note = document.createElement('div');
note.id = 'clp-processing-note';
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.style.cssText = 'display:flex;flex-direction:column;align-items:center;justify-content:center;height:300px;gap:12px;color:var(--text-muted,#999)';
- note.innerHTML = `
+ note.innerHTML = failed ? `
+ <i class="fa-solid fa-triangle-exclamation" style="font-size:2.5rem;color:var(--danger,#e74c3c)"></i>
+ <p style="font-size:1.05rem;font-weight:600">This clip failed to render</p>
+ <p class="muted" style="font-size:0.85rem">The server couldn't cut it from the recording.</p>
+ <button class="btn btn-sm btn-outline" onclick="recutClip(${cl.id})"><i class="fa-solid fa-rotate-right"></i> Try again</button>` : `
<i class="fa-solid fa-scissors fa-bounce" style="font-size:2.5rem;color:var(--accent)"></i>
<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 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>`;
<p class="muted" style="font-size:0.85rem">The server is cutting your clip — this page will update automatically.</p>`;
container.appendChild(note);
container.appendChild(note);
}
}
- window._clpProcessingPoll = setTimeout(() => {
- if (window._clpClipId === cl.id) loadClipPlayer(cl.id);
- }, 3000);
+ // Only poll while it is genuinely in progress; polling a failed clip forever
+ // was pure noise.
+ if (!failed) {
+ window._clpProcessingPoll = setTimeout(() => {
+ if (window._clpClipId === cl.id) loadClipPlayer(cl.id);
+ }, 3000);
+ }
}
}
if (cl.file_path) {
if (cl.file_path) {
const filename = cl.file_path.split('/').pop();
const filename = cl.file_path.split('/').pop();
@@ -191,6 +191,11 @@ function deleteClip(clipId, opts = {}) {
@@ -191,6 +191,11 @@ function deleteClip(clipId, opts = {}) {
return request('DELETE', `/clips/${clipId}`, opts);
return request('DELETE', `/clips/${clipId}`, opts);
}
}
+/** Ask Media to re-cut a clip whose cut failed. */
+function recutClip(clipId, opts = {}) {
+ return request('POST', `/clips/${clipId}/recut`, opts);
+}
+
// ── Pastes ───────────────────────────────────────────────────────────────────
// ── Pastes ───────────────────────────────────────────────────────────────────
/**
/**
@@ -330,7 +335,7 @@ module.exports = {
@@ -330,7 +335,7 @@ module.exports = {
uploadVodChunk, completeVodChunks, finalizeVod,
uploadVodChunk, completeVodChunks, finalizeVod,
getVod, listVods, updateVod, deleteVod,
getVod, listVods, updateVod, deleteVod,
// clips
// clips
- createClip, getClip, listClips, updateClip, deleteClip,
+ createClip, getClip, listClips, updateClip, deleteClip, recutClip,
// pastes
// pastes
createPaste, getPaste, listPastes, deletePaste,
createPaste, getPaste, listPastes, deletePaste,
// files + thumbnails
// files + thumbnails
@@ -317,4 +317,22 @@ router.delete('/:id', requireAuth, async (req, res) => {
@@ -317,4 +317,22 @@ router.delete('/:id', requireAuth, async (req, res) => {
}
}
});
});
+/**
+ * Retry a failed clip cut. Same permission rule as deleting one: whoever may remove a
+ * clip may also ask for it to be re-rendered. Nothing here is destructive — the row and
+ * its timestamps are untouched, only the media is rebuilt.
+ */
+router.post('/:id/recut', requireAuth, async (req, res) => {
+ try {
+ let clip;
+ try { clip = await media.getClip(req.params.id); } catch (err) { return mediaErr(res, err, 'Clip not found'); }
+ if (!clip) return res.status(404).json({ error: 'Clip not found' });
+ if (!(await canActorDeleteClip(req.user, clip))) return res.status(403).json({ error: 'Not authorized to re-cut this clip' });
+ const out = await media.recutClip(clip.id);
+ res.json({ message: 'Re-cutting clip', status: out?.status || 'processing' });
+ } catch (err) {
+ mediaErr(res, err, 'Failed to re-cut clip');
+ }
+});
+
module.exports = router;
module.exports = router;