openvibers/openvibe.live Commits
Live: devicechange only re-enumerates, never asks for camera or mic (#776)
Changes
@@@ -4823,7 +4823,9 @@ function _refreshVerticalPreview() {navigator.mediaDevices._openvibeDevicechangeListening = true;navigator.mediaDevices.addEventListener('devicechange', async () => {console.log('[Broadcast] Device change detected — re-enumerating devices');-- await populateDeviceLists().catch(() => {});++ // Enumerate-only: a hardware event is not a press, so it must never ask for the camera or mic here.++ // Permission still comes only from the "Turn on camera / microphone" press (requestMediaPermissions).++ await refreshDeviceLists().catch(() => {});// The Go Live card follows too (a camera or headset plugged in or out), so it needs no refresh button.if (document.getElementById('bc-create-camera')) {await navigator.mediaDevices.enumerateDevices().then(_populateCreateDeviceDropdowns).catch(() => {});nnew file mode 100644@@@ -0,0 +1,33 @@++/**++ * No capture until a press: a devicechange (plug/unplug) is a hardware event, not++ * a person press, so it must only re-enumerate. It must never reach getUserMedia++ * through populateDeviceLists / bcMediaSetup.request / startMediaCapture.++ * Permission still comes only from the "Turn on camera / microphone" press.++ */++const assert = require('assert');++const fs = require('fs');++const path = require('path');++++const root = path.join(__dirname, '..');++const read = (p) => fs.readFileSync(path.join(root, p), 'utf8');++const broadcast = read('public/js/broadcast.js');++const devices = read('public/js/broadcast-devices.js');++++// The devicechange handler must be enumerate-only.++const at = broadcast.indexOf("addEventListener('devicechange'");++assert.ok(at !== -1, 'broadcast.js must listen for devicechange');++const handler = broadcast.slice(at, broadcast.indexOf('});', broadcast.indexOf("addEventListener('devicechange'")) + 3);++assert.ok(/refreshDeviceLists\(\)/.test(handler), 'devicechange re-enumerates via refreshDeviceLists (enumerate-only)');++for (const bad of ['populateDeviceLists', 'bcMediaSetup.request', 'getUserMedia(', 'startMediaCapture']) {++ assert.ok(!handler.includes(bad), `devicechange must not reach ${bad}`);++}++console.log('OK capture-on-load: devicechange only re-enumerates');++++// The permission flow itself is unchanged: it still asks through the explainer.++const populate = devices.slice(devices.indexOf('async function populateDeviceLists()'),++ devices.indexOf('function _getPreferredCameraId()'));++assert.ok(populate.includes("bcMediaSetup.request('mic')") && populate.includes("bcMediaSetup.request('camera')"),++ 'populateDeviceLists must still ask per-device through the explainer');++console.log('OK capture-on-load: permission flow unchanged (explainer, on press)');++++console.log('✅ capture on load test passed');
@@ -4823,7 +4823,9 @@ function _refreshVerticalPreview() {
@@ -4823,7 +4823,9 @@ function _refreshVerticalPreview() {
navigator.mediaDevices._openvibeDevicechangeListening = true;
navigator.mediaDevices._openvibeDevicechangeListening = true;
navigator.mediaDevices.addEventListener('devicechange', async () => {
navigator.mediaDevices.addEventListener('devicechange', async () => {
console.log('[Broadcast] Device change detected — re-enumerating devices');
console.log('[Broadcast] Device change detected — re-enumerating devices');
- await populateDeviceLists().catch(() => {});
+ // Enumerate-only: a hardware event is not a press, so it must never ask for the camera or mic here.
+ // Permission still comes only from the "Turn on camera / microphone" press (requestMediaPermissions).
+ await refreshDeviceLists().catch(() => {});
// The Go Live card follows too (a camera or headset plugged in or out), so it needs no refresh button.
// The Go Live card follows too (a camera or headset plugged in or out), so it needs no refresh button.
if (document.getElementById('bc-create-camera')) {
if (document.getElementById('bc-create-camera')) {
await navigator.mediaDevices.enumerateDevices().then(_populateCreateDeviceDropdowns).catch(() => {});
await navigator.mediaDevices.enumerateDevices().then(_populateCreateDeviceDropdowns).catch(() => {});
new file mode 100644
new file mode 100644
@@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
+/**
+ * No capture until a press: a devicechange (plug/unplug) is a hardware event, not
+ * a person press, so it must only re-enumerate. It must never reach getUserMedia
+ * through populateDeviceLists / bcMediaSetup.request / startMediaCapture.
+ * Permission still comes only from the "Turn on camera / microphone" press.
+ */
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const root = path.join(__dirname, '..');
+const read = (p) => fs.readFileSync(path.join(root, p), 'utf8');
+const broadcast = read('public/js/broadcast.js');
+const devices = read('public/js/broadcast-devices.js');
+
+// The devicechange handler must be enumerate-only.
+const at = broadcast.indexOf("addEventListener('devicechange'");
+assert.ok(at !== -1, 'broadcast.js must listen for devicechange');
+const handler = broadcast.slice(at, broadcast.indexOf('});', broadcast.indexOf("addEventListener('devicechange'")) + 3);
+assert.ok(/refreshDeviceLists\(\)/.test(handler), 'devicechange re-enumerates via refreshDeviceLists (enumerate-only)');
+for (const bad of ['populateDeviceLists', 'bcMediaSetup.request', 'getUserMedia(', 'startMediaCapture']) {
+ assert.ok(!handler.includes(bad), `devicechange must not reach ${bad}`);
+}
+console.log('OK capture-on-load: devicechange only re-enumerates');
+
+// The permission flow itself is unchanged: it still asks through the explainer.
+const populate = devices.slice(devices.indexOf('async function populateDeviceLists()'),
+ devices.indexOf('function _getPreferredCameraId()'));
+assert.ok(populate.includes("bcMediaSetup.request('mic')") && populate.includes("bcMediaSetup.request('camera')"),
+ 'populateDeviceLists must still ask per-device through the explainer');
+console.log('OK capture-on-load: permission flow unchanged (explainer, on press)');
+
+console.log('✅ capture on load test passed');