openvibers/openvibe.mediahub Commits

Video: show Edit in Studio only when Studio can edit video (#368)

OP OpenVibers committed 3 days ago · f1e830a659 · 6 files, +44 −10

MEDIAHUB_STUDIO_VIDEO_EDIT (default off) gates the watch-page action, the Your videos link and edit_url in the record completion response. The permission-checked hand-off API stays available. Tests cover both the default and the flag.

Changes

ddiff --git a/.env.example b/.env.example
iindex 74c94ad..3ff288f 100644
---- a/.env.example
++++ b/.env.example
@@@ -63,6 +63,8 @@ MEDIAHUB_MEDIA_APP=media-hub
MEDIAHUB_VIDEO_URL=https://openvibe.video
# Where "Edit in Studio" goes for a finished upload (the hand-off route).
MEDIAHUB_STUDIO_URL=https://openvibe.studio
++# Off until Studio has real video tools: the hand-off API stays, the Edit action is not shown.
++# MEDIAHUB_STUDIO_VIDEO_EDIT=1
# The AI cut of an upload (server/video/cut.js): edited copies made here with ffmpeg, one at a time under nice.
#VIDEO_CUTS=off
#VIDEO_CUTS_PER_DAY=5
ddiff --git a/README.md b/README.md
iindex 6c96c4f..df6cc44 100644
---- a/README.md
++++ b/README.md
@@@ -241,8 +241,11 @@ pages read Live's public JSON API server-side, signed out (Live already applies
reviews, trims and chooses Use this. Nothing uploads until that choice.
- The returned `File` goes through the ordinary multipart upload form, with progress and retries. A recording is
private by default. The older `/api/v1/videos/recordings` chunk API remains available to its existing callers.
--- **Edit in Studio** (`server/video/studio.js`): the finished-recording watch page and each ready row on
-- `/me/videos` link to `https://openvibe.studio/edit?media=<vup id>&from=video` (`MEDIAHUB_STUDIO_URL`).
++- **Edit in Studio** (`server/video/studio.js`): with `MEDIAHUB_STUDIO_VIDEO_EDIT=1` (**off** by default), the
++ finished-recording watch page and each ready row on `/me/videos` link to
++ `https://openvibe.studio/edit?media=<vup id>&from=video` (`MEDIAHUB_STUDIO_URL`); the record completion response
++ carries the same `edit_url`. With the flag off, no page or response offers the action, but the hand-off API below
++ stays available.
- Studio is another origin and has no Video cookie, so it calls these routes server-to-server with the person's
own Network token as a Bearer: `GET /api/v1/video/studio/media/:id` (metadata and where it came from),
`GET …/media/:id/bytes` (Range reads for the editor preview) and `POST …/studio/publish`.
ddiff --git a/server/config.js b/server/config.js
iindex 6abc2dc..dd22c3d 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -63,8 +63,11 @@ function load(env = process.env) {
liveUrl: trim(env.MEDIAHUB_LIVE_URL || 'https://openvibe.live'),
livePublicUrl: trim(env.MEDIAHUB_LIVE_PUBLIC_URL || 'https://openvibe.live'),
chatUrl: trim(env.MEDIAHUB_CHAT_URL || 'https://openvibe.live'),
-- // Where "Edit in Studio" goes (the hand-off route; see server/video/studio.js).
++ // Where "Edit in Studio" goes (the hand-off route; see server/video/studio.js). The link is
++ // shown only when MEDIAHUB_STUDIO_VIDEO_EDIT=1: until Studio has real video tools, an
++ // "Edit" action that only previews a video is a promise the page does not keep.
studioUrl: trim(env.MEDIAHUB_STUDIO_URL || 'https://openvibe.studio'),
++ studioVideoEdit: env.MEDIAHUB_STUDIO_VIDEO_EDIT === '1',
// Playlists (step C): the per-person caps the pages and the API enforce together.
playlistsMax: size(env.MEDIAHUB_VIDEO_PLAYLISTS_MAX, 100),
playlistItemsMax: size(env.MEDIAHUB_VIDEO_PLAYLIST_ITEMS_MAX, 500),
ddiff --git a/server/video/upload-routes.js b/server/video/upload-routes.js
iindex 9aa26ec..2303bfc 100644
---- a/server/video/upload-routes.js
++++ b/server/video/upload-routes.js
@@@ -40,7 +40,8 @@ function problem(res, err) {
function createUploadApi({ ctx, config }) {
const up = ctx.videoUploads;
const studioUrl = String((config.video && config.video.studioUrl) || 'https://openvibe.studio').replace(/\/+$/, '');
-- const studioEditUrl = (row) => `${studioUrl}/edit?media=${encodeURIComponent(row.id)}&from=video`;
++ const studioEditUrl = (row) => (config.video && config.video.studioVideoEdit
++ ? `${studioUrl}/edit?media=${encodeURIComponent(row.id)}&from=video` : null);
const r = express.Router();
r.use((req, res, next) => { res.set('Cache-Control', cache.htmlHeaders({ private: true })); next(); });
const who = (req) => (req.viewer && req.viewer.kind === 'user' && req.viewer.subject
@@@ -126,7 +127,8 @@ function createUploadApi({ ctx, config }) {
if (!row) return undefined;
try {
const out = await up.complete(row);
-- return res.status(202).json({ video: { id: out.id, status: out.status, url: out.watch_id ? `/watch?v=${out.watch_id}` : `/u/${out.id}`, edit_url: studioEditUrl(out) } });
++ const editUrl = studioEditUrl(out);
++ return res.status(202).json({ video: { id: out.id, status: out.status, url: out.watch_id ? `/watch?v=${out.watch_id}` : `/u/${out.id}`, ...(editUrl ? { edit_url: editUrl } : {}) } });
} catch (err) { return problem(res, err); }
});
@@@ -175,7 +177,8 @@ function mountUploadPages(r, h) {
: row.status === 'uploading' ? 'uploading' : row.status === 'processing' ? 'processing' : row.status === 'failed' ? 'failed' : 'ready');
/** Where "Edit in Studio" goes (the hand-off; see server/video/studio.js). */
const studioUrl = String((ctx.config.video && ctx.config.video.studioUrl) || 'https://openvibe.studio').replace(/\/+$/, '');
-- const studioEditUrl = (row) => `${studioUrl}/edit?media=${encodeURIComponent(row.id)}&from=video`;
++ const studioVideoEdit = Boolean(ctx.config.video && ctx.config.video.studioVideoEdit);
++ const studioEditUrl = (row) => (studioVideoEdit ? `${studioUrl}/edit?media=${encodeURIComponent(row.id)}&from=video` : null);
function myVideoRow(row) {
const state = uploadState(row);
@@@ -195,7 +198,7 @@ ${state === 'uploading' ? html`<div class="ovv-myprogress" data-ov-upload-progre
${state === 'processing' ? html`<p class="ovv-myprocess"><span class="ovv-pulse" aria-hidden="true"></span> Making it playable. You can close this page.</p>` : ''}
<div class="ovv-myactions">
${state === 'uploading' ? html`<a class="ovv-btn ovv-btn--small ovv-btn--primary" href="/upload">Resume upload</a>` : ''}
--${state === 'ready' ? html`<a class="ovv-btn ovv-btn--small" href="${watch}">Watch</a> <a class="ovv-btn ovv-btn--small" href="${watch}#h-owner">Edit</a> <a class="ovv-btn ovv-btn--small" href="${studioEditUrl(row)}">Edit in Studio</a>` : ''}
++${state === 'ready' ? html`<a class="ovv-btn ovv-btn--small" href="${watch}">Watch</a> <a class="ovv-btn ovv-btn--small" href="${watch}#h-owner">Edit</a>${studioVideoEdit ? html` <a class="ovv-btn ovv-btn--small" href="${studioEditUrl(row)}">Edit in Studio</a>` : ''}` : ''}
${state === 'processing' || state === 'hidden' || state === 'failed' ? html`<a class="ovv-btn ovv-btn--small" href="${watch}">Open</a>` : ''}
<button class="ovv-btn ovv-btn--small ovv-btn--danger" type="button" data-delete-video="${row.id}" data-delete-title="${row.title}">Delete</button>
</div>
@@@ -442,7 +445,7 @@ ${what}
${channel ? html`<a class="ovv-avatar" href="/@${channel}" aria-hidden="true" tabindex="-1">${channel.slice(0, 1).toUpperCase()}${/^[A-Za-z0-9_]{1,32}$/.test(channel) ? html`<img src="${ctx.config.networkUrl}/avatar/${channel}" alt="" loading="lazy">` : ''}</a>` : ''}
<div class="ovv-byline-text">${channel ? html`<a class="ovv-channel-name" href="/@${channel}">@${channel}</a>` : ''}
</div>
--<div class="ovv-byline-actions">${saveForm}${ready && vod && fileOf(vod) ? html`<a class="ovv-btn" href="https://openvibe.party/new?media=${encodeURIComponent(fileOf(vod))}&source_url=${encodeURIComponent(`${String(ctx.config.video.url).replace(/\/+$/, '')}${watchPath}`)}&title=${encodeURIComponent(String(row.title || 'Watch party'))}"><i class="fa-solid fa-users" aria-hidden="true"></i> Watch together</a>` : ''}${own && ready && row.state === 'active' ? html`<a class="ovk-btn ovk-btn--quiet" href="${studioUrl}/edit?media=${encodeURIComponent(row.id)}&from=video">Edit in Studio</a>` : ''}${raw(shareButton({ cls: 'ovv-btn', url: `${ctx.config.video.url}${watchPath}`, title: row.title, text: about, image: thumb || '', timeFrom: ready && vod ? '.ovv-player' : null }))}${raw(askAgent({ text: `About the video “${row.title}” on OpenVibe.Video (${ctx.config.video.url}${watchPath}): ` }))}</div>
++<div class="ovv-byline-actions">${saveForm}${ready && vod && fileOf(vod) ? html`<a class="ovv-btn" href="https://openvibe.party/new?media=${encodeURIComponent(fileOf(vod))}&source_url=${encodeURIComponent(`${String(ctx.config.video.url).replace(/\/+$/, '')}${watchPath}`)}&title=${encodeURIComponent(String(row.title || 'Watch party'))}"><i class="fa-solid fa-users" aria-hidden="true"></i> Watch together</a>` : ''}${own && ready && row.state === 'active' && studioVideoEdit ? html`<a class="ovk-btn ovk-btn--quiet" href="${studioEditUrl(row)}">Edit in Studio</a>` : ''}${raw(shareButton({ cls: 'ovv-btn', url: `${ctx.config.video.url}${watchPath}`, title: row.title, text: about, image: thumb || '', timeFrom: ready && vod ? '.ovv-player' : null }))}${raw(askAgent({ text: `About the video “${row.title}” on OpenVibe.Video (${ctx.config.video.url}${watchPath}): ` }))}</div>
</div>
${row.description ? html`<section class="ovv-panel ovv-description"><p>${row.description}</p></section>` : ''}
${aiDone && ai.overview ? html`<section class="ovv-panel" aria-labelledby="h-ai"><h2 id="h-ai">About this video</h2><p>${ai.overview}</p><p class="ovv-fine">Written by AI from the video's sound and pictures (OpenVibe.AI, run on OpenVibe's own servers).</p>
ddiff --git a/test/video-studio.test.js b/test/video-studio.test.js
iindex 8a11432..738bee9 100644
---- a/test/video-studio.test.js
++++ b/test/video-studio.test.js
@@@ -17,6 +17,7 @@ function areq(base, path, { method = 'GET', bearer, json, range, cookie } = {})
return new Promise((resolve, reject) => {
const u = new URL(base + path);
const headers = { Host: new URL(VIDEO).host };
++ if (method !== 'GET') headers['sec-fetch-site'] = 'same-origin';
if (bearer) headers.authorization = `Bearer ${bearer}`;
if (cookie) headers.cookie = cookie;
if (range) headers.range = range;
@@@ -37,7 +38,7 @@ function areq(base, path, { method = 'GET', bearer, json, range, cookie } = {})
}
(async () => {
-- const t = await boot({ env: { MEDIAHUB_VIDEO_URL: VIDEO, MEDIAHUB_LIVE_URL: 'http://127.0.0.1:1', MEDIAHUB_LIVE_PUBLIC_URL: 'https://openvibe.live', MEDIAHUB_CHAT_URL: 'http://127.0.0.1:1', MEDIAHUB_STUDIO_URL: 'https://openvibe.studio' } });
++ const t = await boot({ env: { MEDIAHUB_VIDEO_URL: VIDEO, MEDIAHUB_LIVE_URL: 'http://127.0.0.1:1', MEDIAHUB_LIVE_PUBLIC_URL: 'https://openvibe.live', MEDIAHUB_CHAT_URL: 'http://127.0.0.1:1', MEDIAHUB_STUDIO_URL: 'https://openvibe.studio', MEDIAHUB_STUDIO_VIDEO_EDIT: '1' } });
const kim = t.network.addUser('kim');
const sam = t.network.addUser('sam');
const kimToken = t.network.userToken(kim);
@@@ -123,11 +124,33 @@ function areq(base, path, { method = 'GET', bearer, json, range, cookie } = {})
const watch = await areq(t.base, `/watch?v=${priv.watch_id}`, { cookie: t.signIn(kim) });
assert.strictEqual(watch.status, 200);
assert.ok(watch.text.includes(`https://openvibe.studio/edit?media=${priv.id}&amp;from=video`) || watch.text.includes(`https://openvibe.studio/edit?media=${priv.id}&from=video`), 'the finished recording links to Studio');
++ const complete = await areq(t.base, `/api/v1/videos/${priv.id}/complete`, { method: 'POST', cookie: t.signIn(kim) });
++ assert.strictEqual(complete.status, 202, complete.text);
++ assert.strictEqual(complete.json().video.edit_url, `https://openvibe.studio/edit?media=${priv.id}&from=video`, 'the record completion names Studio when the editor is on');
const other = await areq(t.base, `/watch?v=${pub.watch_id}`, { cookie: t.signIn(sam) });
assert.strictEqual(other.status, 200);
assert.ok(!other.text.includes('Edit in Studio'), 'only the owner gets the action');
});
await t.close();
++
++ // The default: the hand-off API stays, but no page offers an Edit action until the editor is real.
++ const off = await boot({ env: { MEDIAHUB_VIDEO_URL: VIDEO, MEDIAHUB_LIVE_URL: 'http://127.0.0.1:1', MEDIAHUB_LIVE_PUBLIC_URL: 'https://openvibe.live', MEDIAHUB_CHAT_URL: 'http://127.0.0.1:1', MEDIAHUB_STUDIO_URL: 'https://openvibe.studio' } });
++ try {
++ const kim2 = off.network.addUser('kim');
++ const row = await off.ctx.videoUploads.startRecording({ requester: `user:${kim2.subject}`, username: 'kim' }, { title: 'Default off', contentType: 'video/webm', visibility: 'public' });
++ await off.ctx.s.db.query("UPDATE video_uploads SET status = 'ready', duration_seconds = 2 WHERE id = $1", [row.id]);
++ const page = await areq(off.base, `/watch?v=${row.watch_id}`, { cookie: off.signIn(kim2) });
++ assert.strictEqual(page.status, 200);
++ assert.ok(!page.text.includes('Edit in Studio'), 'no Edit action by default');
++ const mine = await areq(off.base, '/me/videos', { cookie: off.signIn(kim2) });
++ assert.ok(!mine.text.includes('Edit in Studio'), 'no link on Your videos by default');
++ const complete = await areq(off.base, `/api/v1/videos/${row.id}/complete`, { method: 'POST', cookie: off.signIn(kim2) });
++ assert.strictEqual(complete.status, 202);
++ assert.ok(!('edit_url' in complete.json().video), 'the record completion omits the link by default');
++ const api = await areq(off.base, `/api/v1/video/studio/media/${row.id}`, { bearer: off.network.userToken(kim2) });
++ assert.strictEqual(api.status, 200, 'the hand-off API stays available');
++ } finally { await off.close(); }
++
done();
})().catch((err) => { console.error(err); process.exit(1); });
ddiff --git a/test/video-uploads.test.js b/test/video-uploads.test.js
iindex 8b14374..88992da 100644
---- a/test/video-uploads.test.js
++++ b/test/video-uploads.test.js
@@@ -113,7 +113,7 @@ function vreq(base, path, { method = 'GET', cookie, form, json, body, same = tru
const done1 = await vreq(t.base, `/api/v1/videos/${id}/complete`, { method: 'POST', cookie: kimCookie });
assert.strictEqual(done1.status, 202);
assert.strictEqual(done1.json().video.url, `/watch?v=${[...t.media.vods.values()][0].watch_id}`);
-- assert.strictEqual(done1.json().video.edit_url, `https://openvibe.studio/edit?media=${id}&from=video`, 'the finished recording names Studio');
++ assert.ok(!('edit_url' in done1.json().video), 'no Studio link until MEDIAHUB_STUDIO_VIDEO_EDIT is on');
});
await check('the watch page plays it from Media, and it is on the channel, the home page and the sitemap', async () => {