Skip to content

feat(meeting): say why the mic or camera is blocked, and how to allow it - #614

Merged
tranh0anghuan merged 1 commit into
testfrom
feat/meeting-blocked-media-guidance
Sep 20, 2026
Merged

tranh0anghuan merged 1 commit into
testfrom
feat/meeting-blocked-media-guidance

Conversation

@tranh0anghuan

Copy link
Copy Markdown
Collaborator

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.

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>
@tranh0anghuan
tranh0anghuan merged commit d62f387 into test Sep 20, 2026
8 checks passed
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +3 to +5
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +1237 to +1241
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +855 to +857
const id = `bg-${(this.backgrounds || []).length + 1}-${file.size}`;
this.backgrounds = this.backgrounds || [];
this.backgrounds.push({ id, url, img });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +638 to +643
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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +31 to +32
if (/iphone|ipad|ipod|android/.test(probe)) return null;
if (/mac/.test(probe)) return "mac";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +979 to +981
// 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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +806 to +808
await track.setEffect(this._bgFx);
} else {
await track.setEffect(undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +799 to +805
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),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@tranh0anghuan
tranh0anghuan deleted the feat/meeting-blocked-media-guidance branch September 21, 2026 03:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant