openvibers/openvibe.live Commits

Live: VOD and clip pages link the same item on OpenVibe.Video (#104)

OP OpenVibers committed 12 Oct 2026 · 48dc991ed7 · 3 files, +15 −0

OpenVibe.Video is live at openvibe.video (MediaHub#28): public watch pages for Live's recordings and clips, with AI chapters and WebVTT captions. Plan T10 has Live link to it.

- The VOD page and the clip page get an "OpenVibe.Video" link in their meta row (`https://openvibe.video/v/<id>` or `/c/<id>`).
- It shows only for public or unlisted items that have finished recording; private items and recordings in progress keep Live's page alone.
- The href is set from a numeric id through DOM properties, with no innerHTML.

Frontend only: deploys without a restart. Tests: clip-attribution, features-registry, player-a11y, vod-hls-source.

Changes

ddiff --git a/public/fragments/clip-player.html b/public/fragments/clip-player.html
iindex 84163922..cfdafa4b 100644
---- a/public/fragments/clip-player.html
++++ b/public/fragments/clip-player.html
@@@ -51,6 +51,7 @@
<span class="muted"><i class="fa-solid fa-clock"></i> <span id="clp-duration">0:00</span></span>
<span class="muted"><i class="fa-solid fa-eye"></i> <span id="clp-views">0</span></span>
<span id="clp-protocol"></span>
++ <a id="clp-video-link" class="muted vod-video-link" href="#" style="display:none" title="This clip on OpenVibe.Video"><i class="fa-solid fa-film"></i> OpenVibe.Video</a>
</div>
<!-- Who made it, right under the title: "Clipped by <clipper>", or "AI clip · from <streamer>'s stream". -->
<div id="clp-clipped-by" class="muted" style="margin-top:4px"></div>
ddiff --git a/public/fragments/vod-player.html b/public/fragments/vod-player.html
iindex aef5b7b3..9e5af082 100644
---- a/public/fragments/vod-player.html
++++ b/public/fragments/vod-player.html
@@@ -60,6 +60,7 @@
<span class="muted"><i class="fa-solid fa-clock"></i> <span id="vp-duration">0:00</span></span>
<span class="muted"><i class="fa-solid fa-eye"></i> <span id="vp-views">0</span></span>
<span id="vp-protocol"></span>
++ <a id="vp-video-link" class="muted vod-video-link" href="#" style="display:none" title="The same recording on OpenVibe.Video, with AI chapters and captions"><i class="fa-solid fa-film"></i> OpenVibe.Video</a>
</div>
<div id="vp-extra-details" class="vod-extra-details" style="display:none"></div>
<p id="vp-description" class="vod-player-description"></p>
ddiff --git a/public/js/app-media.js b/public/js/app-media.js
iindex cbac7411..7752dd21 100644
---- a/public/js/app-media.js
++++ b/public/js/app-media.js
@@@ -104,6 +104,17 @@ function downloadMediaTranscript(btn) {
} // streamer's user id — the stable chat room key
// The site-wide VOD and clip lists are the Content feed now (public/js/content-feed.js).
// AI "memory" timeline on the VOD player — clickable timestamps that seek the video.
++// OpenVibe.Video (openvibe.video) serves public watch pages for recordings and clips that are public or unlisted
++// and finished; the link appears only for those. Private items and recordings in progress keep Live's page alone.
++function _showVideoLink(elId, kind, item) {
++ const a = document.getElementById(elId);
++ if (!a) return;
++ const vis = item.visibility || (item.is_public ? 'public' : 'private');
++ const ok = (vis === 'public' || vis === 'unlisted') && !item.is_recording && Number.isSafeInteger(Number(item.id));
++ a.style.display = ok ? '' : 'none';
++ if (ok) a.href = `https://openvibe.video/${kind === 'clip' ? 'c' : 'v'}/${Number(item.id)}`;
++}
++
async function loadVodAiTimeline(vodId) {
const el = document.getElementById('vp-ai-timeline');
if (!el) return;
@@@ -288,6 +299,7 @@ async function loadVodPlayer(vodId, seekTo) {
document.getElementById('vp-date').textContent = formatDateTime(v.created_at);
document.getElementById('vp-duration').textContent = formatDuration(v.duration_seconds || v.duration);
document.getElementById('vp-views').textContent = `${v.view_count || 0} views${v.unique_views != null ? ` · ${v.unique_views} unique` : ''}`;
++ _showVideoLink('vp-video-link', 'vod', v);
document.getElementById('vp-description').textContent = v.description || '';
_renderMediaAiOverview('vp-description', v.ai_overview);
_renderMediaTranscript('vp-description', 'vod', v);
@@@ -1032,6 +1044,7 @@ async function loadClipPlayer(clipId) {
// View count
const clpViews = document.getElementById('clp-views');
if (clpViews) clpViews.textContent = `${cl.view_count || 0} views${cl.unique_views != null ? ` · ${cl.unique_views} unique` : ''}`;
++ _showVideoLink('clp-video-link', 'clip', cl);
// Protocol badge
const clpProto = document.getElementById('clp-protocol');