openvibers/openvibe.mediahub Commits
Video: show Edit in Studio only when Studio can edit video (#368)
Changes
@@@ -63,6 +63,8 @@ MEDIAHUB_MEDIA_APP=media-hubMEDIAHUB_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@@@ -241,8 +241,11 @@ pages read Live's public JSON API server-side, signed out (Live already appliesreviews, 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 isprivate 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'sown 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`.@@@ -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),@@@ -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>@@@ -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}&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); });@@@ -113,7 +113,7 @@ function vreq(base, path, { method = 'GET', cookie, form, json, body, same = truconst 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 () => {
@@ -63,6 +63,8 @@ MEDIAHUB_MEDIA_APP=media-hub
@@ -63,6 +63,8 @@ MEDIAHUB_MEDIA_APP=media-hub
MEDIAHUB_VIDEO_URL=https://openvibe.video
MEDIAHUB_VIDEO_URL=https://openvibe.video
# Where "Edit in Studio" goes for a finished upload (the hand-off route).
# Where "Edit in Studio" goes for a finished upload (the hand-off route).
MEDIAHUB_STUDIO_URL=https://openvibe.studio
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.
# 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=off
#VIDEO_CUTS_PER_DAY=5
#VIDEO_CUTS_PER_DAY=5
@@ -241,8 +241,11 @@ pages read Live's public JSON API server-side, signed out (Live already applies
@@ -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.
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
- 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.
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
- 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),
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`.
`GET …/media/:id/bytes` (Range reads for the editor preview) and `POST …/studio/publish`.
@@ -63,8 +63,11 @@ function load(env = process.env) {
@@ -63,8 +63,11 @@ function load(env = process.env) {
liveUrl: trim(env.MEDIAHUB_LIVE_URL || 'https://openvibe.live'),
liveUrl: trim(env.MEDIAHUB_LIVE_URL || 'https://openvibe.live'),
livePublicUrl: trim(env.MEDIAHUB_LIVE_PUBLIC_URL || 'https://openvibe.live'),
livePublicUrl: trim(env.MEDIAHUB_LIVE_PUBLIC_URL || 'https://openvibe.live'),
chatUrl: trim(env.MEDIAHUB_CHAT_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'),
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.
// Playlists (step C): the per-person caps the pages and the API enforce together.
playlistsMax: size(env.MEDIAHUB_VIDEO_PLAYLISTS_MAX, 100),
playlistsMax: size(env.MEDIAHUB_VIDEO_PLAYLISTS_MAX, 100),
playlistItemsMax: size(env.MEDIAHUB_VIDEO_PLAYLIST_ITEMS_MAX, 500),
playlistItemsMax: size(env.MEDIAHUB_VIDEO_PLAYLIST_ITEMS_MAX, 500),
@@ -40,7 +40,8 @@ function problem(res, err) {
@@ -40,7 +40,8 @@ function problem(res, err) {
function createUploadApi({ ctx, config }) {
function createUploadApi({ ctx, config }) {
const up = ctx.videoUploads;
const up = ctx.videoUploads;
const studioUrl = String((config.video && config.video.studioUrl) || 'https://openvibe.studio').replace(/\/+$/, '');
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();
const r = express.Router();
r.use((req, res, next) => { res.set('Cache-Control', cache.htmlHeaders({ private: true })); next(); });
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
const who = (req) => (req.viewer && req.viewer.kind === 'user' && req.viewer.subject
@@ -126,7 +127,8 @@ function createUploadApi({ ctx, config }) {
@@ -126,7 +127,8 @@ function createUploadApi({ ctx, config }) {
if (!row) return undefined;
if (!row) return undefined;
try {
try {
const out = await up.complete(row);
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); }
} catch (err) { return problem(res, err); }
});
});
@@ -175,7 +177,8 @@ function mountUploadPages(r, h) {
@@ -175,7 +177,8 @@ function mountUploadPages(r, h) {
: row.status === 'uploading' ? 'uploading' : row.status === 'processing' ? 'processing' : row.status === 'failed' ? 'failed' : 'ready');
: 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). */
/** 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 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) {
function myVideoRow(row) {
const state = uploadState(row);
const state = uploadState(row);
@@ -195,7 +198,7 @@ ${state === 'uploading' ? html`<div class="ovv-myprogress" data-ov-upload-progre
@@ -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>` : ''}
${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">
<div class="ovv-myactions">
${state === 'uploading' ? html`<a class="ovv-btn ovv-btn--small ovv-btn--primary" href="/upload">Resume upload</a>` : ''}
${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>` : ''}
${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>
<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>
</div>
@@ -442,7 +445,7 @@ ${what}
@@ -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>` : ''}
${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 class="ovv-byline-text">${channel ? html`<a class="ovv-channel-name" href="/@${channel}">@${channel}</a>` : ''}
</div>
</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>
</div>
${row.description ? html`<section class="ovv-panel ovv-description"><p>${row.description}</p></section>` : ''}
${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>
${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>
@@ -17,6 +17,7 @@ function areq(base, path, { method = 'GET', bearer, json, range, cookie } = {})
@@ -17,6 +17,7 @@ function areq(base, path, { method = 'GET', bearer, json, range, cookie } = {})
return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
const u = new URL(base + path);
const u = new URL(base + path);
const headers = { Host: new URL(VIDEO).host };
const headers = { Host: new URL(VIDEO).host };
+ if (method !== 'GET') headers['sec-fetch-site'] = 'same-origin';
if (bearer) headers.authorization = `Bearer ${bearer}`;
if (bearer) headers.authorization = `Bearer ${bearer}`;
if (cookie) headers.cookie = cookie;
if (cookie) headers.cookie = cookie;
if (range) headers.range = range;
if (range) headers.range = range;
@@ -37,7 +38,7 @@ function areq(base, path, { method = 'GET', bearer, json, range, cookie } = {})
@@ -37,7 +38,7 @@ function areq(base, path, { method = 'GET', bearer, json, range, cookie } = {})
}
}
(async () => {
(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 kim = t.network.addUser('kim');
const sam = t.network.addUser('sam');
const sam = t.network.addUser('sam');
const kimToken = t.network.userToken(kim);
const kimToken = t.network.userToken(kim);
@@ -123,11 +124,33 @@ function areq(base, path, { method = 'GET', bearer, json, range, cookie } = {})
@@ -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) });
const watch = await areq(t.base, `/watch?v=${priv.watch_id}`, { cookie: t.signIn(kim) });
assert.strictEqual(watch.status, 200);
assert.strictEqual(watch.status, 200);
assert.ok(watch.text.includes(`https://openvibe.studio/edit?media=${priv.id}&from=video`) || watch.text.includes(`https://openvibe.studio/edit?media=${priv.id}&from=video`), 'the finished recording links to Studio');
assert.ok(watch.text.includes(`https://openvibe.studio/edit?media=${priv.id}&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) });
const other = await areq(t.base, `/watch?v=${pub.watch_id}`, { cookie: t.signIn(sam) });
assert.strictEqual(other.status, 200);
assert.strictEqual(other.status, 200);
assert.ok(!other.text.includes('Edit in Studio'), 'only the owner gets the action');
assert.ok(!other.text.includes('Edit in Studio'), 'only the owner gets the action');
});
});
await t.close();
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();
done();
})().catch((err) => { console.error(err); process.exit(1); });
})().catch((err) => { console.error(err); process.exit(1); });
@@ -113,7 +113,7 @@ function vreq(base, path, { method = 'GET', cookie, form, json, body, same = tru
@@ -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 });
const done1 = await vreq(t.base, `/api/v1/videos/${id}/complete`, { method: 'POST', cookie: kimCookie });
assert.strictEqual(done1.status, 202);
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.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 () => {
await check('the watch page plays it from Media, and it is on the channel, the home page and the sitemap', async () => {