openvibers/openrestream Commits

A service acting for a stream's owner switching mirror_to_live on links it to their Live channel like the person (Bot: a desk on my Live channel, a robot's Live switch); a stream naming its slot is never linked again (#117)

OP OpenVibers committed 17 days ago · 9bf0997254 · 3 files, +35 −4

Changes

ddiff --git a/README.md b/README.md
iindex 0b16581..281e6c9 100644
---- a/README.md
++++ b/README.md
@@@ -209,7 +209,9 @@ sudo deploy/scripts/deploy.sh status # workers.sh
A stream someone creates on openre.stream also goes live on their OpenVibe Live channel unless they untick "Show it
live on my OpenVibe Live channel too" (the create form, ticked by default; the API's `show_on_live: false` for a
--person's own stream; services such as Live and Bot name their own refs and are never linked). `server/live-link.js`
++person's own stream; a service's new stream, such as Live's and Bot's, names its own refs and is not linked; switching
++`mirror_to_live` on later links it, by the person or by a service acting for them with X-OV-Subject, such as Bot's
++"Show this desk on my Live channel", unless the stream already names its Live slot). `server/live-link.js`
asks Live for a slot on the owner's channel bound to the stream (`POST /internal/openre/slots` on Live, capability
`live.openre.slot.bind`, contracts 0.126.0, with this service's Network token for audience `openvibe.live`). The slot
becomes the stream's `live:managed_stream` ref (its label is the channel URL) and `mirror_to_live` turns on, so Live's
ddiff --git a/server/api/v1.js b/server/api/v1.js
iindex dca6eed..27b49d6 100644
---- a/server/api/v1.js
++++ b/server/api/v1.js
@@@ -181,9 +181,12 @@ function createV1Router({ rt, auth, liveLink = null }) {
if (!d) return;
const b = req.body || {};
// A person switching mirror_to_live on gets their Live slot first (it stays off, with the reason, when Live refuses).
-- // A record-only stream is refused instead (openre.record_only): it never goes live.
++ // So does a first-party service acting for the stream's owner (X-OV-Subject), at that person's request: Bot's
++ // "Show this desk on my Live channel", a robot's Live switch. A stream that already names its Live slot (Live's
++ // own) is never linked again. A record-only stream is refused instead (openre.record_only): it never goes live.
const recordOnly = b.record_to !== undefined ? b.record_to === 'videos' : d.record_to === 'videos';
-- const linkNow = liveLink && req.caller.kind === 'user' && b.mirror_to_live === true && !liveLink.slotOf(d) && !recordOnly;
++ const forOwner = req.caller.kind === 'user' || (req.caller.kind === 'service' && !!req.caller.subject);
++ const linkNow = liveLink && forOwner && b.mirror_to_live === true && !liveLink.slotOf(d) && !recordOnly;
let updated = await store.definitions.update(d.id, {
title: b.title, description: b.description, protocols: b.protocols, recording_mode: b.recording_mode,
recording_visibility: b.recording_visibility, playback_visibility: b.playback_visibility, mirror_to_live: linkNow ? undefined : b.mirror_to_live, state: b.state,
ddiff --git a/test/live-link.test.js b/test/live-link.test.js
iindex 237390a..b479af5 100644
---- a/test/live-link.test.js
++++ b/test/live-link.test.js
@@@ -3,7 +3,8 @@
* OpenVibe Live on by default (owner, 2026-10-09; contracts 0.126.0). A person's new stream asks Live for a slot on
* their channel (POST /internal/openre/slots with OpenRestream's service token) unless they untick it; the slot becomes
* the stream's live:managed_stream ref and mirror_to_live turns on. A refusal keeps the stream with Live off and says
-- * why. Services (Live's own slots, Bot's robots) are never linked here. Switching it on later links once.
++ * why. A service's new stream (Live's own slots, Bot's robots) is never linked here. Switching it on later links once,
++ * by the person or by a service acting for them (X-OV-Subject), unless the stream already names its Live slot.
*/
const assert = require('assert');
const { bootApi, request, userToken, serviceToken, suite, OWNER, OTHER } = require('./helpers');
@@@ -122,6 +123,31 @@ t('the API links a person\'s stream by default, never a service\'s', async () =>
assert.strictEqual(theirs.body.stream.mirror_to_live, false);
});
++t('a service acting for the owner switching it on links once (Bot: a desk on my Live channel); never a stream that names its slot', async () => {
++ const bot = serviceToken('bot', ['openre.stream.write', 'openre.stream.read']);
++ const made = await request(api.base, 'POST', '/api/v1/streams', { token: bot, headers: { 'X-OV-Subject': OWNER }, body: { title: 'desk-pc desk', mirror_to_live: false, playback_visibility: 'public', external_refs: [{ service: 'bot', type: 'machine_live', id: 'dev_01M5YGM39VC2RRGDQPRD6D4SHD' }] } });
++ assert.strictEqual(made.status, 201, made.text);
++ assert.strictEqual(made.body.live, undefined, 'never at create');
++ const id = made.body.stream.id;
++ let before = liveCalls.length;
++ const on = await request(api.base, 'PATCH', `/api/v1/streams/${id}`, { token: bot, headers: { 'X-OV-Subject': OWNER }, body: { mirror_to_live: true } });
++ assert.strictEqual(on.status, 200, on.text);
++ assert.deepStrictEqual(on.body.live, { linked: true, channel_url: 'https://openvibe.live/@owner' });
++ assert.strictEqual(on.body.stream.mirror_to_live, true);
++ assert.strictEqual(liveCalls.length, before + 1);
++ assert.strictEqual(liveCalls[liveCalls.length - 1].subject, OWNER, 'the owner\'s channel');
++ await request(api.base, 'PATCH', `/api/v1/streams/${id}`, { token: bot, headers: { 'X-OV-Subject': OWNER }, body: { mirror_to_live: true } });
++ assert.strictEqual(liveCalls.length, before + 1, 'already linked');
++ // Live's own slot stream names its slot: switching mirror_to_live on never asks Live for another one.
++ const live = serviceToken('live', ['openre.stream.write', 'openre.stream.read']);
++ const slot = await request(api.base, 'POST', '/api/v1/streams', { token: live, headers: { 'X-OV-Subject': OWNER }, body: { title: 'A Live slot', external_refs: [{ service: 'live', type: 'managed_stream', id: '9002' }] } });
++ before = liveCalls.length;
++ const relink = await request(api.base, 'PATCH', `/api/v1/streams/${slot.body.stream.id}`, { token: live, headers: { 'X-OV-Subject': OWNER }, body: { mirror_to_live: true } });
++ assert.strictEqual(relink.status, 200, relink.text);
++ assert.strictEqual(relink.body.live, undefined);
++ assert.strictEqual(liveCalls.length, before);
++});
++
t('teardown', async () => { await api.close(); });
t.run();