feat(meeting): say why the mic or camera is blocked, and how to allow it - #614
Conversation
A blocked device was indistinguishable from a muted one: the pill looked the same, the picker showed a single flat line, and turning a blocked mic OFF succeeded silently because mute() never attempts capture, so the block did not surface until a later click had to re-acquire the device. - guidance panel (webrtc/skeleton/media-blocked): browser steps plus the settings path for the platform the user is actually on, shown in the device picker in place of the device list, with Retry - apps-warning badge on the top-right corner of the mic / camera toggle, the toggle gated via _setService, and the pill tinted amber so "blocked" no longer reads as "muted" - failures classified where they happen: changeLocalAudio had no catch at all (onUiEvent does not await it, so a blocked mic rejected into nothing) and changeLocalVideo's catch only reverted the button - toggles ask the browser before choosing a branch, so the FIRST click lands on the blocked state whichever way it was pointing - media alerts close whichever device picker is open first (mediaAlert) - denial overlay carries the same guidance, drops its corner X in favour of the topbar Leave split (now interactive while denied), and offers Retry, which re-runs the startup after resetStartupState clears the stale connection + _startupMediaFailed that made a re-join hang - restores the camera picker and background-effects controller and their styles, dropped from room/index.js and command.scss by the merge/preview-to-test merge (37bd5ef) while their skeletons survived - window-info: flat header, 300px min width, spacer-6 padding, no corner X, smaller action buttons - desk sidebar nav scrolls without painting a scrollbar Locale keys added to all six files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6345709e95
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // Brand logo only. The X was removed: every caller's action row already | ||
| // carries a dismissing button (Close / Cancel / Got it), so the corner cross | ||
| // was a second way out that only cost the header its padding. |
There was a problem hiding this comment.
Restore the close affordance for header-only info windows
Not every caller has an action row: the invited-workspace and billing loaders in modules/desk/index.js use mode: "hb", which omits the footer, and explicitly rely on the header X for manual dismissal. Removing it leaves users unable to close these overlays until their 30-second timeout or the underlying operation completes.
Useful? React with 👍 / 👎.
| case "video-device-select": | ||
| this.selectedVideoDevice = cmd.$el.data("deviceid"); | ||
| // Remember the pick so reopening re-highlights it and toggling the | ||
| // camera off/on re-acquires this device (see changeLocalVideo). | ||
| this.preferredVideoInputDevice = this.selectedVideoDevice; |
There was a problem hiding this comment.
Defer the camera preference until Save
When the camera is off, selecting a different row updates preferredVideoInputDevice immediately, while close-camera-select only clears selectedVideoDevice. Consequently, clicking Cancel still changes the camera used by the next changeLocalVideo call, so the supposedly cancelled selection is applied invisibly; retain the pending choice separately and only commit the preference in confirmCameraSelection.
Useful? React with 👍 / 👎.
| const id = `bg-${(this.backgrounds || []).length + 1}-${file.size}`; | ||
| this.backgrounds = this.backgrounds || []; | ||
| this.backgrounds.push({ id, url, img }); |
There was a problem hiding this comment.
Generate collision-free background IDs
The ID is derived from the current list length and file size, so removing an earlier upload and then adding another file of the same size can duplicate an existing ID. Since selection and removal resolve backgrounds solely by this ID, clicking the new thumbnail can apply or remove the older image instead; use a monotonic or random identifier rather than the mutable array length.
Useful? React with 👍 / 👎.
| input.addEventListener("change", () => { | ||
| const file = input.files && input.files[0]; | ||
| if (!file) return; | ||
| const url = URL.createObjectURL(file); |
There was a problem hiding this comment.
Revoke uploaded background URLs during teardown
Each uploaded image creates an object URL, but URLs are revoked only when the user explicitly removes an individual background; onBeforeDestroy never releases the remaining entries. In this SPA, closing and reopening calls after uploading large images therefore retains each Blob for the lifetime of the page, causing memory usage to accumulate across meetings.
Useful? React with 👍 / 👎.
| if (!(await this.ensureMediaPermission(_a.video))) { | ||
| this._setMediaDeniedUi(_a.video, true); | ||
| // Stamp the wrapper so the skin can widen it for the guidance text; a | ||
| // structural selector would be at the mercy of the panel's markup. | ||
| p.el.dataset.blocked = 1; | ||
| p.feed(require("../skeleton/media-blocked")(this, _a.video)); |
There was a problem hiding this comment.
Distinguish capture failures from denied camera permission
On a first camera attempt with no device, or in a browser that cannot query camera permission, ensureMediaPermission returns false for every getUserMedia rejection, including NotFoundError and NotReadableError. This branch consequently labels a missing or busy camera as browser/OS-blocked, disables the toggle, and makes the otherwise defined NO_CAMERA_DEVICE view unreachable for that case; preserve the rejection reason and only apply the denied UI for actual permission errors.
Useful? React with 👍 / 👎.
| if (/iphone|ipad|ipod|android/.test(probe)) return null; | ||
| if (/mac/.test(probe)) return "mac"; |
There was a problem hiding this comment.
Detect desktop-mode iPads before classifying macOS
Modern iPads commonly expose navigator.platform as MacIntel and a Macintosh desktop user agent, without an ipad token. This test therefore returns "mac" and tells those users to open macOS System Settings, which does not exist on iPadOS; include the standard Mac-platform-plus-touch-points check before the macOS branch.
Useful? React with 👍 / 👎.
| current: this.bgEffect || { type: "none" }, | ||
| backgrounds: (this.backgrounds || []).map((b) => ({ id: b.id, url: b.url })), | ||
| }); | ||
| p.feed(view); |
There was a problem hiding this comment.
Clear the effects wrapper before refreshing its panel
feed() appends to this wrapper, and this method is called again after every successful upload and removal. The first uploaded image therefore appends a second complete effects panel beneath the original one, leaving stale duplicate controls and thumbnails on screen; clear the wrapper before feeding the refreshed view.
Useful? React with 👍 / 👎.
| // The new track has no effect attached — re-apply the background effect. | ||
| if (this.bgEffect && this.bgEffect.type && this.bgEffect.type !== "none") { | ||
| await this.applyBackgroundEffect(); |
There was a problem hiding this comment.
Reapply effects after microphone-triggered track rebuilds
An active camera is also recreated by recreateLocalTrackOnDeviceChange whenever the microphone is changed or a device-list event fires, but only this camera-specific recreation path reapplies bgEffect. As a result, changing the microphone or plugging in a headset replaces the processed camera track with a raw one while the UI still shows blur or the uploaded background as active; run the same reapply step after every rebuild that includes video.
Useful? React with 👍 / 👎.
| await track.setEffect(this._bgFx); | ||
| } else { | ||
| await track.setEffect(undefined); |
There was a problem hiding this comment.
Serialize background effect switches
The bundled JitsiLocalTrack.setEffect rejects a second request while _setEffectInProgress is true, but the other effect tiles remain clickable during this await. Selecting blur and then another image or None during renegotiation updates bgEffect and the highlighted UI to the second choice while its setEffect call fails, leaving the first effect on the transmitted camera; queue switches or disable all effect choices until the current call settles.
Useful? React with 👍 / 👎.
| const BackgroundEffect = require("./effects/background-effect"); | ||
| this._bgFx = new BackgroundEffect({ | ||
| type: spec.type, | ||
| blurValue: spec.level === "strong" ? 18 : 8, | ||
| image: spec.image, | ||
| onStatus: (s) => this._onBgStatus(s), | ||
| }); |
There was a problem hiding this comment.
Cancel pending segmenter initialization when replacing effects
If an effect is disabled or replaced while MediaPipe's asynchronous initialize() is still pending, BackgroundEffect.stopEffect() runs before _segmenter has been assigned and therefore cannot close it. The pending initialization later stores the segmenter even though _running is already false, leaking its worker/WASM resources on each quick toggle; the newly activated effect path needs cancellation or a post-await running check that closes the local segmenter.
Useful? React with 👍 / 👎.




A blocked device was indistinguishable from a muted one: the pill looked the same, the picker showed a single flat line, and turning a blocked mic OFF succeeded silently because mute() never attempts capture, so the block did not surface until a later click had to re-acquire the device.
Locale keys added to all six files.