openvibers/openvibe.live Commits
The studio preview shows what viewers get; mirroring your preview is a choice (More → Mirror my preview) (#492)
Changes
@@@ -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())));nnew file mode 100644@@@ -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');
@@ -27,6 +27,14 @@
@@ -27,6 +27,14 @@
};
};
const $ = (id) => document.getElementById(id);
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) {
function el(tag, attrs, ...kids) {
const n = document.createElement(tag);
const n = document.createElement(tag);
for (const [k, v] of Object.entries(attrs || {})) {
for (const [k, v] of Object.entries(attrs || {})) {
@@ -595,13 +603,14 @@
@@ -595,13 +603,14 @@
if (st.camOff) flags.push(['fa-video-slash', 'Camera off: viewers see black']);
if (st.camOff) flags.push(['fa-video-slash', 'Camera off: viewers see black']);
const v = $('bc-video-preview');
const v = $('bc-video-preview');
if (v && v.style.display === 'none') flags.push(['fa-eye-slash', 'Preview hidden, still live']);
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 box = $('bcs-flags');
const sig = JSON.stringify(flags);
const sig = JSON.stringify(flags);
if (box.dataset.sig !== sig) {
if (box.dataset.sig !== sig) {
box.dataset.sig = sig;
box.dataset.sig = sig;
box.replaceChildren(...flags.map(([i, t]) => el('span', { class: 'bcs-flag' }, icon(i), t)));
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 ──
// ── Desktop row: the same Mute / Camera off / Flip ──
@@ -725,6 +734,7 @@
@@ -725,6 +734,7 @@
tile('fa-film', 'Media requests', after(() => toggleMediaPip())),
tile('fa-film', 'Media requests', after(() => toggleMediaPip())),
tile('fa-scissors', 'Clip last minute', after(() => quickClip(60))),
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(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-sliders', 'All settings', after(() => leave('#bc-live-section .bc-settings-group'))),
tile('fa-up-right-and-down-left-from-center', 'Full page', after(() => leave())));
tile('fa-up-right-and-down-left-from-center', 'Full page', after(() => leave())));
new file mode 100644
new file mode 100644
@@ -0,0 +1,40 @@
@@ -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');