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)
Changes
@@@ -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);@@@ -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);
@@ -139,14 +139,24 @@ function factualDescription(kind, item, firstWords = '') {
@@ -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.`;
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) {
function hlsAttrs(item) {
- const hls = !item.is_recording && httpsUrl(item.hls_url);
const file = fileOf(item);
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 '';
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')}"`;
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) {
function durationLabel(s) {
const n = Math.max(0, Math.floor(Number(s) || 0));
const n = Math.max(0, Math.floor(Number(s) || 0));
const h = Math.floor(n / 3600);
const h = Math.floor(n / 3600);
@@ -36,6 +36,13 @@ function startLive() {
@@ -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',
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,
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 = {
const unlistedVod = {
id: 102, title: 'An unlisted recording', description: '', duration_seconds: 20,
id: 102, title: 'An unlisted recording', description: '', duration_seconds: 20,
created_at: '2026-10-11T10:00:00.000Z', thumbnail_url: null,
created_at: '2026-10-11T10:00:00.000Z', thumbnail_url: null,
@@ -171,6 +178,8 @@ function startLive() {
@@ -171,6 +178,8 @@ function startLive() {
if (vodDetail[1] === '104') return json(200, { vod: secondVod, clips: [] });
if (vodDetail[1] === '104') return json(200, { vod: secondVod, clips: [] });
if (vodDetail[1] === '106') return json(200, { vod: thinVod, clips: [] });
if (vodDetail[1] === '106') return json(200, { vod: thinVod, clips: [] });
if (vodDetail[1] === '105') return json(200, { vod: hlsFirstVod, 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' });
return json(404, { error: 'not found' });
}
}
const clipDetail = /^\/api\/clips\/(\d+)$/.exec(p);
const clipDetail = /^\/api\/clips\/(\d+)$/.exec(p);
@@ -352,6 +361,15 @@ function countOccurrences(haystack, needle) {
@@ -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)');
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 () => {
await check('an unlisted VOD is reachable but noindex', async () => {
const r = await getVideo('/v/102');
const r = await getVideo('/v/102');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);