openvibers/openvibe.mediahub Commits

A recording in progress on openvibe.video plays Media's DVR playlist over the growing file (MP4, not private), not the seekable copy (#123)

OP OpenVibers committed 20 days ago · 08b7ae9bfe · 2 files, +30 −2

Changes

ddiff --git a/server/video/pages.js b/server/video/pages.js
iindex 37a828e..16d8fa4 100644
---- a/server/video/pages.js
++++ b/server/video/pages.js
@@@ -139,14 +139,24 @@ function factualDescription(kind, item, firstWords = '') {
return kind === 'vod' ? `${head} Watch with AI captions and chapters on OpenVibe.Video.` : `${head} Watch it on OpenVibe.Video.`;
}
--/** The player's HLS source: Media's timeline playlist, for a finished item that has one, on the playback file's origin. */
++/**
++ * The player's HLS source, on the playback file's origin: Media's timeline playlist for a finished item that has one; for
++ * an MP4 recording still being written, its DVR playlist over the growing file (Media /v/<id>/dvr.m3u8, Media#168), so
++ * the page plays the recording without the seekable copy Media rewrote each pass. A private or WebM recording: the file.
++ */
function hlsAttrs(item) {
-- const hls = !item.is_recording && httpsUrl(item.hls_url);
const file = fileOf(item);
++ const hls = item.is_recording ? dvrPlaylist(item, file) : httpsUrl(item.hls_url);
if (!hls || !file || new URL(hls).origin !== new URL(file).origin) return '';
return html` data-ovv-hls="${hls}" data-ovv-hls-lib="${asset('vendor/hls.light.min.js')}"`;
}
++function dvrPlaylist(item, file) {
++ const open = (item.visibility || (item.is_public ? 'public' : 'private')) !== 'private';
++ if (!file || !open || !/\.mp4$/i.test(String(item.file_path || '')) || !Number.isSafeInteger(Number(item.id))) return null;
++ return `${new URL(file).origin}/v/${Number(item.id)}/dvr.m3u8`;
++}
++
function durationLabel(s) {
const n = Math.max(0, Math.floor(Number(s) || 0));
const h = Math.floor(n / 3600);
ddiff --git a/test/video-pages.test.js b/test/video-pages.test.js
iindex e522f4f..c3dec75 100644
---- a/test/video-pages.test.js
++++ b/test/video-pages.test.js
@@@ -36,6 +36,13 @@ function startLive() {
transcript_status: 'done', stream_category: 'Just Chatting', stream_id: 1001, stream_started_at: '2026-10-12 09:30:00', stream_ended_at: '2026-10-12 09:30:45',
stream_peak_viewers: 12, comment_count: 3, view_count: 100, is_recording: false,
};
++ // An MP4 recording still being written (RTMP): its player plays Media's DVR playlist over the growing file.
++ const recordingVod = {
++ id: 107, title: 'Still live', description: '', duration_seconds: 600, created_at: '2026-10-24T16:00:00.000Z',
++ thumbnail_url: null, playback_url: 'https://openvibe.media/v/107', hls_url: null, visibility: 'public',
++ file_path: '/data/vods/vod-live-107.mp4', username: 'streamer', display_name: 'Streamer', view_count: 1,
++ is_recording: true, ai_overview: '', ai_transcript_json: null,
++ };
const unlistedVod = {
id: 102, title: 'An unlisted recording', description: '', duration_seconds: 20,
created_at: '2026-10-11T10:00:00.000Z', thumbnail_url: null,
@@@ -171,6 +178,8 @@ function startLive() {
if (vodDetail[1] === '104') return json(200, { vod: secondVod, clips: [] });
if (vodDetail[1] === '106') return json(200, { vod: thinVod, clips: [] });
if (vodDetail[1] === '105') return json(200, { vod: hlsFirstVod, clips: [] });
++ if (vodDetail[1] === '107') return json(200, { vod: recordingVod, clips: [] });
++ if (vodDetail[1] === '108') return json(200, { vod: { ...recordingVod, id: 108, file_path: '/data/vods/vod-live-108.webm', playback_url: 'https://openvibe.media/v/108' }, clips: [] });
return json(404, { error: 'not found' });
}
const clipDetail = /^\/api\/clips\/(\d+)$/.exec(p);
@@@ -352,6 +361,15 @@ function countOccurrences(haystack, needle) {
assert.ok(!r.text.includes('Manage this clip'), 'owner tools only for its maker or channel (video-owner.test.js)');
});
++ await check('a recording in progress plays Media\'s DVR playlist when it is an MP4, the file when it is a WebM', async () => {
++ let r = await getVideo('/v/107');
++ assert.strictEqual(r.status, 200);
++ assert.ok(r.text.includes('This recording is still in progress'));
++ assert.ok(r.text.includes('data-ovv-hls="https://openvibe.media/v/107/dvr.m3u8"'), 'the playlist over the growing file');
++ r = await getVideo('/v/108');
++ assert.ok(!r.text.includes('data-ovv-hls='), 'a WebM recording plays its file');
++ });
++
await check('an unlisted VOD is reachable but noindex', async () => {
const r = await getVideo('/v/102');
assert.strictEqual(r.status, 200);