openvibers/openvibe.live Commits
Broadcast: read the screen-share mic/camera the streamer actually picked
Changes
@@@ -2073,8 +2073,9 @@ async function _wsRequestMicPermission() {_wsPopulateInlineDevices('bc-ws-screen-mic-select', mics);_wsShowEl('bc-ws-screen-mic-device', true);_wsShowEl('bc-ws-screen-mic-perm', false);-- // Also sync to the hidden form selects used by createNewStream-- _wsPopulateInlineDevices('bc-screen-audio', mics);++ // (There is no hidden bc-screen-audio mirror any more: createNewStream() reads++ // bc-ws-screen-mic-select directly. Writing into a non-existent element was what++ // hid the id mismatch that discarded the streamer's microphone choice.)} catch (err) {console.warn('[Workspace] Mic permission denied:', err.message);_wsShowEl('bc-ws-screen-mic-perm', true);@@@ -2091,8 +2092,8 @@ async function _wsRequestCamPermission() {_wsPopulateInlineDevices('bc-ws-screen-cam-select', cams);_wsShowEl('bc-ws-screen-cam-device', true);_wsShowEl('bc-ws-screen-cam-perm', false);-- // Also sync to the hidden form selects used by createNewStream-- _wsPopulateInlineDevices('bc-screen-camera', cams);++ // See the note in _wsRequestMicPermission: createNewStream() reads++ // bc-ws-screen-cam-select directly, so there is nothing to mirror into.} catch (err) {console.warn('[Workspace] Camera permission denied:', err.message);_wsShowEl('bc-ws-screen-cam-perm', true);@@@ -1474,11 +1474,31 @@ async function createNewStream() {_syncCameraSelectionUI(createCamera);_syncAudioSelectionUI(createAudio);-- // Read screen share device selections-- const screenMicEnabled = document.getElementById('bc-screen-mic-enabled')?.checked ?? true;-- const screenCamEnabled = document.getElementById('bc-screen-cam-enabled')?.checked ?? false;-- const screenAudio = document.getElementById('bc-screen-audio')?.value || 'default';-- const screenCamera = document.getElementById('bc-screen-camera')?.value || 'default';++ // Read screen share device selections.++ //++ // These four IDs never existed. The screen-share panel is rendered by++ // broadcast-workspace.js as bc-ws-screen-mic / -mic-select / -cam / -cam-select, and the++ // old bc-screen-* names were left behind here when that panel was rewritten. Every lookup++ // therefore returned null, and the `?? true` / `|| 'default'` defaults quietly papered++ // over it:++ // - the chosen microphone was discarded and capture always fell back to the system++ // default device, which is why viewers heard nothing whenever the OS default was not++ // the mic the streamer picked, and why the UI kept reporting "Default";++ // - screenCamEnabled defaulted to FALSE, so the camera overlay never ran at all.++ // The workspace's own sync helpers wrote into the same dead ids (_wsPopulateInlineDevices++ // ('bc-screen-audio', ...)), so nothing surfaced the breakage.++ //++ // Read the real controls, keeping the legacy ids as a fallback so any other host page++ // that still renders them keeps working.++ const _el = (...ids) => ids.map(id => document.getElementById(id)).find(Boolean) || null;++ const micToggleEl = _el('bc-ws-screen-mic', 'bc-screen-mic-enabled');++ const camToggleEl = _el('bc-ws-screen-cam', 'bc-screen-cam-enabled');++ const screenMicEnabled = micToggleEl ? !!micToggleEl.checked : true;++ const screenCamEnabled = camToggleEl ? !!camToggleEl.checked : false;++ const screenAudio = _el('bc-ws-screen-mic-select', 'bc-screen-audio')?.value || 'default';++ const screenCamera = _el('bc-ws-screen-cam-select', 'bc-screen-camera')?.value || 'default';++ console.log('[Broadcast] Screen-share devices — mic:', screenMicEnabled ? screenAudio : 'off',++ '| camera:', screenCamEnabled ? screenCamera : 'off');let _diagStreamId = null; // captured in catch for diagnostic loggingtry {nnew file mode 100644@@@ -0,0 +1,88 @@++/**++ * Regression: the screen-share capture path must read the controls the UI actually renders.++ *++ * broadcast-workspace.js renders the screen-share panel with bc-ws-screen-mic /++ * -mic-select / -cam / -cam-select. createNewStream() in broadcast.js read a different,++ * older set of ids (bc-screen-mic-enabled / bc-screen-audio / bc-screen-cam-enabled /++ * bc-screen-camera) which survive only as hidden stubs carrying hardcoded values:++ *++ * <input type="checkbox" id="bc-screen-mic-enabled" checked style="display:none">++ * <input type="checkbox" id="bc-screen-cam-enabled" style="display:none">++ * <select id="bc-screen-audio" style="display:none"><option value="default">…++ * <select id="bc-screen-camera" style="display:none"><option value="default">…++ *++ * Nothing ever copied the streamer's SELECTION into those stubs — the workspace mirrored++ * the option list and not the chosen value — so every screen share captured with++ * audioId 'default' and cameraId 'default', and screenCamEnabled read as false:++ *++ * - the microphone the streamer picked was discarded and capture silently fell back to++ * the OS default input. When that default was not the intended mic, viewers heard++ * nothing at all, and the UI kept reporting "Default".++ * - the camera overlay never ran, because its enable flag came from a stub that is++ * never checked.++ *++ * These assertions pin the contract in both directions: the ids the UI renders, and the++ * ids the capture path reads.++ */++const assert = require('assert');++const fs = require('fs');++const path = require('path');++++const root = path.join(__dirname, '..');++const broadcast = fs.readFileSync(path.join(root, 'public/js/broadcast.js'), 'utf8');++const workspace = fs.readFileSync(path.join(root, 'public/js/broadcast-workspace.js'), 'utf8');++++// ── A: the workspace still renders the controls we claim to read ─────────────────────++for (const id of ['bc-ws-screen-mic', 'bc-ws-screen-mic-select', 'bc-ws-screen-cam', 'bc-ws-screen-cam-select']) {++ assert.ok(workspace.includes(`id="${id}"`),++ `broadcast-workspace.js must render #${id} (the screen-share capture path reads it)`);++}++console.log('OK A: workspace renders the four screen-share device controls');++++// ── B: the capture path reads those ids, not only the hidden stubs ───────────────────++const capture = broadcast.slice(broadcast.indexOf('async function createNewStream()'),++ broadcast.indexOf('await startMediaCapture(streamData.id, captureOpts)'));++assert.ok(capture.length > 0, 'could not locate createNewStream() capture section');++++for (const id of ['bc-ws-screen-mic', 'bc-ws-screen-mic-select', 'bc-ws-screen-cam', 'bc-ws-screen-cam-select']) {++ assert.ok(capture.includes(`'${id}'`),++ `createNewStream() must read #${id} — reading only the hidden stub discards the streamer's choice`);++}++console.log('OK B: createNewStream() reads the real controls');++++// ── C: the stubs must never be the ONLY source for these four values ─────────────────++// If a legacy id appears without its bc-ws- counterpart on the same lookup, the value is++// coming from a hardcoded hidden input again.++const legacyPairs = [++ ['bc-screen-mic-enabled', 'bc-ws-screen-mic'],++ ['bc-screen-cam-enabled', 'bc-ws-screen-cam'],++ ['bc-screen-audio', 'bc-ws-screen-mic-select'],++ ['bc-screen-camera', 'bc-ws-screen-cam-select'],++];++for (const [legacy, real] of legacyPairs) {++ for (const line of capture.split('\n')) {++ if (line.includes(`'${legacy}'`) && !line.includes('//')) {++ assert.ok(line.includes(`'${real}'`),++ `capture path reads #${legacy} without #${real} on the same lookup — the stub is hardcoded, so this silently ignores the streamer's selection`);++ }++ }++}++console.log('OK C: every legacy-id lookup is paired with the real control as the preferred source');++++// ── D: the hidden stubs really are hardcoded (so C is not paranoia) ──────────────────++assert.ok(/id="bc-screen-mic-enabled"[^>]*checked/.test(workspace),++ 'bc-screen-mic-enabled stub is hardcoded checked — reading it can never reflect the toggle');++assert.ok(/id="bc-screen-cam-enabled"(?![^>]*checked)/.test(workspace),++ 'bc-screen-cam-enabled stub is never checked — reading it always disables the camera overlay');++console.log('OK D: confirmed the stubs carry fixed values and cannot reflect user intent');++++// ── E: the camera overlay flag is derived from the toggle, before capture starts ──────++assert.ok(/ss\._cameraOverlayEnabled\s*=\s*screenCamEnabled/.test(broadcast),++ 'the camera overlay flag must come from the screen-share camera toggle');++const flagAt = broadcast.indexOf('ss._cameraOverlayEnabled = screenCamEnabled');++const captureAt = broadcast.indexOf('await startMediaCapture(streamData.id, captureOpts)');++assert.ok(flagAt > 0 && flagAt < captureAt,++ 'the overlay flag must be set before startMediaCapture() reads it');++console.log('OK E: camera overlay flag set from the toggle, before capture');++++console.log('✅ broadcast screen-share device wiring test passed');
@@ -2073,8 +2073,9 @@ async function _wsRequestMicPermission() {
@@ -2073,8 +2073,9 @@ async function _wsRequestMicPermission() {
_wsPopulateInlineDevices('bc-ws-screen-mic-select', mics);
_wsPopulateInlineDevices('bc-ws-screen-mic-select', mics);
_wsShowEl('bc-ws-screen-mic-device', true);
_wsShowEl('bc-ws-screen-mic-device', true);
_wsShowEl('bc-ws-screen-mic-perm', false);
_wsShowEl('bc-ws-screen-mic-perm', false);
- // Also sync to the hidden form selects used by createNewStream
- _wsPopulateInlineDevices('bc-screen-audio', mics);
+ // (There is no hidden bc-screen-audio mirror any more: createNewStream() reads
+ // bc-ws-screen-mic-select directly. Writing into a non-existent element was what
+ // hid the id mismatch that discarded the streamer's microphone choice.)
} catch (err) {
} catch (err) {
console.warn('[Workspace] Mic permission denied:', err.message);
console.warn('[Workspace] Mic permission denied:', err.message);
_wsShowEl('bc-ws-screen-mic-perm', true);
_wsShowEl('bc-ws-screen-mic-perm', true);
@@ -2091,8 +2092,8 @@ async function _wsRequestCamPermission() {
@@ -2091,8 +2092,8 @@ async function _wsRequestCamPermission() {
_wsPopulateInlineDevices('bc-ws-screen-cam-select', cams);
_wsPopulateInlineDevices('bc-ws-screen-cam-select', cams);
_wsShowEl('bc-ws-screen-cam-device', true);
_wsShowEl('bc-ws-screen-cam-device', true);
_wsShowEl('bc-ws-screen-cam-perm', false);
_wsShowEl('bc-ws-screen-cam-perm', false);
- // Also sync to the hidden form selects used by createNewStream
- _wsPopulateInlineDevices('bc-screen-camera', cams);
+ // See the note in _wsRequestMicPermission: createNewStream() reads
+ // bc-ws-screen-cam-select directly, so there is nothing to mirror into.
} catch (err) {
} catch (err) {
console.warn('[Workspace] Camera permission denied:', err.message);
console.warn('[Workspace] Camera permission denied:', err.message);
_wsShowEl('bc-ws-screen-cam-perm', true);
_wsShowEl('bc-ws-screen-cam-perm', true);
@@ -1474,11 +1474,31 @@ async function createNewStream() {
@@ -1474,11 +1474,31 @@ async function createNewStream() {
_syncCameraSelectionUI(createCamera);
_syncCameraSelectionUI(createCamera);
_syncAudioSelectionUI(createAudio);
_syncAudioSelectionUI(createAudio);
- // Read screen share device selections
- const screenMicEnabled = document.getElementById('bc-screen-mic-enabled')?.checked ?? true;
- const screenCamEnabled = document.getElementById('bc-screen-cam-enabled')?.checked ?? false;
- const screenAudio = document.getElementById('bc-screen-audio')?.value || 'default';
- const screenCamera = document.getElementById('bc-screen-camera')?.value || 'default';
+ // Read screen share device selections.
+ //
+ // These four IDs never existed. The screen-share panel is rendered by
+ // broadcast-workspace.js as bc-ws-screen-mic / -mic-select / -cam / -cam-select, and the
+ // old bc-screen-* names were left behind here when that panel was rewritten. Every lookup
+ // therefore returned null, and the `?? true` / `|| 'default'` defaults quietly papered
+ // over it:
+ // - the chosen microphone was discarded and capture always fell back to the system
+ // default device, which is why viewers heard nothing whenever the OS default was not
+ // the mic the streamer picked, and why the UI kept reporting "Default";
+ // - screenCamEnabled defaulted to FALSE, so the camera overlay never ran at all.
+ // The workspace's own sync helpers wrote into the same dead ids (_wsPopulateInlineDevices
+ // ('bc-screen-audio', ...)), so nothing surfaced the breakage.
+ //
+ // Read the real controls, keeping the legacy ids as a fallback so any other host page
+ // that still renders them keeps working.
+ const _el = (...ids) => ids.map(id => document.getElementById(id)).find(Boolean) || null;
+ const micToggleEl = _el('bc-ws-screen-mic', 'bc-screen-mic-enabled');
+ const camToggleEl = _el('bc-ws-screen-cam', 'bc-screen-cam-enabled');
+ const screenMicEnabled = micToggleEl ? !!micToggleEl.checked : true;
+ const screenCamEnabled = camToggleEl ? !!camToggleEl.checked : false;
+ const screenAudio = _el('bc-ws-screen-mic-select', 'bc-screen-audio')?.value || 'default';
+ const screenCamera = _el('bc-ws-screen-cam-select', 'bc-screen-camera')?.value || 'default';
+ console.log('[Broadcast] Screen-share devices — mic:', screenMicEnabled ? screenAudio : 'off',
+ '| camera:', screenCamEnabled ? screenCamera : 'off');
let _diagStreamId = null; // captured in catch for diagnostic logging
let _diagStreamId = null; // captured in catch for diagnostic logging
try {
try {
new file mode 100644
new file mode 100644
@@ -0,0 +1,88 @@
@@ -0,0 +1,88 @@
+/**
+ * Regression: the screen-share capture path must read the controls the UI actually renders.
+ *
+ * broadcast-workspace.js renders the screen-share panel with bc-ws-screen-mic /
+ * -mic-select / -cam / -cam-select. createNewStream() in broadcast.js read a different,
+ * older set of ids (bc-screen-mic-enabled / bc-screen-audio / bc-screen-cam-enabled /
+ * bc-screen-camera) which survive only as hidden stubs carrying hardcoded values:
+ *
+ * <input type="checkbox" id="bc-screen-mic-enabled" checked style="display:none">
+ * <input type="checkbox" id="bc-screen-cam-enabled" style="display:none">
+ * <select id="bc-screen-audio" style="display:none"><option value="default">…
+ * <select id="bc-screen-camera" style="display:none"><option value="default">…
+ *
+ * Nothing ever copied the streamer's SELECTION into those stubs — the workspace mirrored
+ * the option list and not the chosen value — so every screen share captured with
+ * audioId 'default' and cameraId 'default', and screenCamEnabled read as false:
+ *
+ * - the microphone the streamer picked was discarded and capture silently fell back to
+ * the OS default input. When that default was not the intended mic, viewers heard
+ * nothing at all, and the UI kept reporting "Default".
+ * - the camera overlay never ran, because its enable flag came from a stub that is
+ * never checked.
+ *
+ * These assertions pin the contract in both directions: the ids the UI renders, and the
+ * ids the capture path reads.
+ */
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const root = path.join(__dirname, '..');
+const broadcast = fs.readFileSync(path.join(root, 'public/js/broadcast.js'), 'utf8');
+const workspace = fs.readFileSync(path.join(root, 'public/js/broadcast-workspace.js'), 'utf8');
+
+// ── A: the workspace still renders the controls we claim to read ─────────────────────
+for (const id of ['bc-ws-screen-mic', 'bc-ws-screen-mic-select', 'bc-ws-screen-cam', 'bc-ws-screen-cam-select']) {
+ assert.ok(workspace.includes(`id="${id}"`),
+ `broadcast-workspace.js must render #${id} (the screen-share capture path reads it)`);
+}
+console.log('OK A: workspace renders the four screen-share device controls');
+
+// ── B: the capture path reads those ids, not only the hidden stubs ───────────────────
+const capture = broadcast.slice(broadcast.indexOf('async function createNewStream()'),
+ broadcast.indexOf('await startMediaCapture(streamData.id, captureOpts)'));
+assert.ok(capture.length > 0, 'could not locate createNewStream() capture section');
+
+for (const id of ['bc-ws-screen-mic', 'bc-ws-screen-mic-select', 'bc-ws-screen-cam', 'bc-ws-screen-cam-select']) {
+ assert.ok(capture.includes(`'${id}'`),
+ `createNewStream() must read #${id} — reading only the hidden stub discards the streamer's choice`);
+}
+console.log('OK B: createNewStream() reads the real controls');
+
+// ── C: the stubs must never be the ONLY source for these four values ─────────────────
+// If a legacy id appears without its bc-ws- counterpart on the same lookup, the value is
+// coming from a hardcoded hidden input again.
+const legacyPairs = [
+ ['bc-screen-mic-enabled', 'bc-ws-screen-mic'],
+ ['bc-screen-cam-enabled', 'bc-ws-screen-cam'],
+ ['bc-screen-audio', 'bc-ws-screen-mic-select'],
+ ['bc-screen-camera', 'bc-ws-screen-cam-select'],
+];
+for (const [legacy, real] of legacyPairs) {
+ for (const line of capture.split('\n')) {
+ if (line.includes(`'${legacy}'`) && !line.includes('//')) {
+ assert.ok(line.includes(`'${real}'`),
+ `capture path reads #${legacy} without #${real} on the same lookup — the stub is hardcoded, so this silently ignores the streamer's selection`);
+ }
+ }
+}
+console.log('OK C: every legacy-id lookup is paired with the real control as the preferred source');
+
+// ── D: the hidden stubs really are hardcoded (so C is not paranoia) ──────────────────
+assert.ok(/id="bc-screen-mic-enabled"[^>]*checked/.test(workspace),
+ 'bc-screen-mic-enabled stub is hardcoded checked — reading it can never reflect the toggle');
+assert.ok(/id="bc-screen-cam-enabled"(?![^>]*checked)/.test(workspace),
+ 'bc-screen-cam-enabled stub is never checked — reading it always disables the camera overlay');
+console.log('OK D: confirmed the stubs carry fixed values and cannot reflect user intent');
+
+// ── E: the camera overlay flag is derived from the toggle, before capture starts ──────
+assert.ok(/ss\._cameraOverlayEnabled\s*=\s*screenCamEnabled/.test(broadcast),
+ 'the camera overlay flag must come from the screen-share camera toggle');
+const flagAt = broadcast.indexOf('ss._cameraOverlayEnabled = screenCamEnabled');
+const captureAt = broadcast.indexOf('await startMediaCapture(streamData.id, captureOpts)');
+assert.ok(flagAt > 0 && flagAt < captureAt,
+ 'the overlay flag must be set before startMediaCapture() reads it');
+console.log('OK E: camera overlay flag set from the toggle, before capture');
+
+console.log('✅ broadcast screen-share device wiring test passed');