openvibers/openvibe.live Commits

The studio preview shows what viewers get; mirroring your preview is a choice (More → Mirror my preview) (#492)

OP OpenVibers committed 12 days ago · 4b483aab40 · 2 files, +51 −1

Changes

ddiff --git a/public/js/broadcast-studio.js b/public/js/broadcast-studio.js
iindex 163fe31e..65aee885 100644
---- a/public/js/broadcast-studio.js
++++ b/public/js/broadcast-studio.js
@@@ -27,6 +27,14 @@
};
const $ = (id) => document.getElementById(id);
++ // The preview shows what viewers get (the owner, 2026-11-01: "it shows everything flipped, like mirrored… when
++ // you watch the VOD, it's correct"). Viewers and the recording always get the camera's true picture; a mirrored
++ // self-view, like a selfie camera, is the person's choice for their own preview only, remembered on this device.
++ const MIRROR_KEY = 'ov_bc_mirror_preview';
++ function mirrorWanted() { try { return localStorage.getItem(MIRROR_KEY) === '1'; } catch { return false; } }
++ function toggleMirror() { try { localStorage.setItem(MIRROR_KEY, mirrorWanted() ? '0' : '1'); } catch { /* a private window keeps the default */ } refresh(); }
++ /** Is the preview mirrored right now: asked for, a front camera, and not a shared screen. */
++ function mirrored() { return mirrorWanted() && facing() === 'user' && !screenSharing(); }
function el(tag, attrs, ...kids) {
const n = document.createElement(tag);
for (const [k, v] of Object.entries(attrs || {})) {
@@@ -595,13 +603,14 @@
if (st.camOff) flags.push(['fa-video-slash', 'Camera off: viewers see black']);
const v = $('bc-video-preview');
if (v && v.style.display === 'none') flags.push(['fa-eye-slash', 'Preview hidden, still live']);
++ else if (mirrored()) flags.push(['fa-left-right', 'Preview mirrored: viewers see it the right way']);
const box = $('bcs-flags');
const sig = JSON.stringify(flags);
if (box.dataset.sig !== sig) {
box.dataset.sig = sig;
box.replaceChildren(...flags.map(([i, t]) => el('span', { class: 'bcs-flag' }, icon(i), t)));
}
-- if (v) v.classList.toggle('bcs-mirror', facing() === 'user' && !screenSharing());
++ if (v) v.classList.toggle('bcs-mirror', mirrored());
}
// ── Desktop row: the same Mute / Camera off / Flip ──
@@@ -725,6 +734,7 @@
tile('fa-film', 'Media requests', after(() => toggleMediaPip())),
tile('fa-scissors', 'Clip last minute', after(() => quickClip(60))),
tile(previewHidden ? 'fa-eye' : 'fa-eye-slash', previewHidden ? 'Show my preview' : 'Hide my preview', after(() => toggleBroadcastPreview()), previewHidden ? null : 'Saves battery'),
++ tile('fa-left-right', mirrorWanted() ? 'Show it as viewers see it' : 'Mirror my preview', after(() => toggleMirror()), mirrorWanted() ? 'Your preview is mirrored now' : 'Like a selfie: only you see it mirrored'),
tile('fa-sliders', 'All settings', after(() => leave('#bc-live-section .bc-settings-group'))),
tile('fa-up-right-and-down-left-from-center', 'Full page', after(() => leave())));
ddiff --git a/test/broadcast-mirror.test.js b/test/broadcast-mirror.test.js
nnew file mode 100644
iindex 00000000..63f821ca
---- /dev/null
++++ b/test/broadcast-mirror.test.js
@@@ -0,0 +1,40 @@
++'use strict';
++/**
++ * The preview shows what viewers get (the owner, 2026-11-01: "when the streamer is live… it shows everything flipped,
++ * like mirrored. However, when you watch the VOD, it's correct… it looks proper, both on the viewer's end, in the
++ * preview and the actual final thing"). The phone studio used to mirror a front camera's preview by itself; now the
++ * preview is the camera's true picture unless the person asks for a mirrored self-view, which stays theirs: viewers,
++ * the recording, calls and every other page never mirror a video.
++ */
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++
++const read = (f) => fs.readFileSync(path.join(__dirname, '..', f), 'utf8');
++const studio = read('public/js/broadcast-studio.js');
++const css = read('public/css/features/broadcast.css');
++
++// The class that flips the preview is set from mirrored() only, and mirrored() needs the person's own choice.
++assert.ok(/v\.classList\.toggle\('bcs-mirror', mirrored\(\)\)/.test(studio), 'the preview flips only when mirrored() says so');
++assert.ok(!/classList\.toggle\('bcs-mirror', facing\(\)/.test(studio), 'a front camera alone no longer mirrors the preview');
++assert.ok(/function mirrored\(\) \{ return mirrorWanted\(\) && facing\(\) === 'user' && !screenSharing\(\); \}/.test(studio), 'asked for, a front camera, not a shared screen');
++assert.ok(/function mirrorWanted\(\) \{ try \{ return localStorage\.getItem\(MIRROR_KEY\) === '1'; \}/.test(studio), 'off unless the person turned it on');
++assert.ok(/tile\('fa-left-right', mirrorWanted\(\) \? 'Show it as viewers see it' : 'Mirror my preview'/.test(studio), 'the switch is in the More sheet, in plain words');
++assert.ok(/'Preview mirrored: viewers see it the right way'/.test(studio), 'while mirrored, the picture says viewers see it right');
++
++// Nothing else mirrors a video anywhere in Live's pages: the one rule is the studio preview's, behind its class.
++const flips = [];
++const walk = (dir) => fs.readdirSync(path.join(__dirname, '..', dir), { withFileTypes: true }).forEach((d) => {
++ const rel = path.join(dir, d.name);
++ if (d.isDirectory()) return walk(rel);
++ if (!/\.(css|js|html)$/.test(d.name) || /\.min\./.test(d.name)) return;
++ const text = read(rel);
++ const re = /scaleX\(\s*-1\s*\)|scale\(\s*-1\s*,\s*1\s*\)|rotateY\(\s*180deg\s*\)|setTransform\(\s*-1/g;
++ let m;
++ while ((m = re.exec(text))) flips.push(`${rel}:${text.slice(0, m.index).split('\n').length}`);
++});
++walk('public');
++assert.deepStrictEqual(flips, ['public/css/features/broadcast.css:' + (css.slice(0, css.indexOf('#bc-video-preview.bcs-mirror')).split('\n').length)],
++ `only the studio preview's own rule may mirror a video: ${flips.join(', ')}`);
++
++console.log('broadcast-mirror: the preview is the true picture unless the person mirrors it; nothing else flips a video');