Skip to content

Conversation

@kixelated
Copy link
Collaborator

@kixelated kixelated commented Aug 31, 2025

Summary by CodeRabbit

  • Bug Fixes

    • More reliable microphone/camera detection and selection with smarter defaults and graceful handling when devices are unavailable.
    • Improved permission handling for media devices, reducing setup failures.
    • Device pickers no longer show when audio or video is disabled.
  • Style

    • “Audio Only” and “Video Only” status indicators now use a green dot for clarity.
  • Documentation

    • Publish page guidance updated: captions are not automatic and must be explicitly enabled via the captions attribute.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 31, 2025

Walkthrough

Refactors device selection to use requested/active device IDs, adds permission and active state tracking, consolidates device enumeration and permission handling, updates camera/microphone sources to the new API, gates device UI by broadcast.enabled flags, tweaks status indicators, and changes demo captions guidance to require explicit enablement.

Changes

Cohort / File(s) Summary
Demo markup and captions guidance
js/hang-demo/src/publish.html
Removed automatic captions claim from example and updated copy; demo now shows captions must be explicitly enabled via the captions attribute.
Publish element gating and UI
js/hang/src/publish/element.ts
Hide mic/camera selectors when broadcast.enabled (audio/video) is false; switch dropdown value/source from selectedrequested; adjust dropdown handlers and device-availability guards; change single-source status badge color to green.
Device API overhaul
js/hang/src/publish/source/device.ts
Replace MediaDeviceInfo-based default with string device IDs; add active (deviceId), permission, and requested signals; remove selected; rename request()requestPermission(); unify run path, perform permission-aware enumeration, derive default deviceId, and update devicechange handling and logs.
Camera source updates
js/hang/src/publish/source/camera.ts
Use device.requested (string) for constraints, build deviceId as { exact: device } when present, set device.permission on success, read track settings and set device.active to the actual deviceId, and keep existing stream/cleanup flow.
Microphone source updates
js/hang/src/publish/source/microphone.ts
Mirror microphone changes: use device.requested, construct constraints from raw id, set device.permission after getUserMedia, update device.active from track settings, and store track in stream.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI as Publish Element
  participant Dev as Device (audio/video)
  participant Nav as Navigator.mediaDevices
  participant Src as Source (Mic/Camera)

  Note over UI: Initialization / render
  UI->>Dev: start device run (permission-aware)
  Dev->>Nav: getUserMedia(kind) to request permission
  Nav-->>Dev: permission granted (or denied)
  Dev->>Nav: enumerateDevices()
  Nav-->>Dev: device list
  Dev->>Dev: derive default deviceId, set devices, set permission
  Dev-->>UI: available device IDs / requested/default

  alt broadcast.enabled == false
    UI-->>UI: skip rendering selector
  else broadcast.enabled == true
    UI->>UI: render selector with value = Dev.requested
    UI->>Dev: onChange -> set Dev.requested
  end

  Note over Src: Start capture flow
  UI->>Src: start()
  Src->>Dev: read Dev.requested (deviceId string)
  Src->>Nav: getUserMedia({ deviceId: { exact: id } }) or no deviceId
  Nav-->>Src: MediaStream
  Src->>Dev: device.permission.set(true)
  Src->>Dev: device.active.set(track.getSettings().deviceId)
  Src-->>UI: stream ready / active track set
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I twitch my whiskers, hop and spin,
Requested IDs hop—then stream comes in.
Green dots blink where choices land,
Permissions set by gentle hand.
Captions now a chosen song—hop on! 🐇🎧

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch device-manager

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
js/hang-demo/src/publish.html (1)

50-59: Clarify captions opt-in, remove transcribe references
Replace the “automatic transcription” language and transcribe attribute mention in js/hang-demo/src/publish.html (lines 57–58) with copy that reflects the new opt-in captions attribute:

@@ js/hang-demo/src/publish.html:57-58
- They run in a WebWorker to avoid blocking the main thread so unless battery is an issue, keep the automatic transcription enabled!
- But if you want to disable it, the `transcribe` attribute controls whether transcription is enabled.
+ They run in a WebWorker to avoid blocking the main thread. Captions are opt-in; enable them only if you want local transcription.
+ Toggle captions by adding/removing the `captions` attribute (or via JS: <code>publish.captions = true/false</code>).
🧹 Nitpick comments (5)
js/hang/src/publish/source/camera.ts (1)

56-64: Set permission, propagate active device — consider consistent stream cleanup

Logic is good. Minor consistency: microphone clears its stream on effect cleanup via default restore, while camera explicitly restores to undefined. Pick one behavior for both to avoid transient stale streams.

If you prefer explicit clearing, mirror this in microphone:

- effect.set(this.stream, track);
+ effect.set(this.stream, track, undefined);
js/hang/src/publish/source/microphone.ts (1)

57-66: Align stream cleanup behavior with camera to avoid stale track on rerun

Set a cleanup value so the signal returns to undefined when the effect reruns/closes.

-			effect.set(this.stream, track);
+			effect.set(this.stream, track, undefined);
js/hang/src/publish/element.ts (1)

341-341: Drop debug logging or guard it

console.log("rendering devices", ...) will spam in production.

-			console.log("rendering devices", devices);
+			// console.debug("rendering devices", devices);
js/hang/src/publish/source/device.ts (2)

56-66: No-permission path clears caches; consider also clearing requested

#runRequested will recompute on #devices change, but explicitly clearing #requested here would make the state immediately consistent.

 			this.#devices.set(undefined);
 			this.#default.set(undefined);
+			this.#requested.set(undefined);
 			return;

124-135: Method naming: enumerate() actually requests permission

Optional: rename to requestPermission() or add JSDoc clarifying that it prompts GUM to unlock labels/deviceIds, not just enumerates.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 15adbad and 39a9c69.

📒 Files selected for processing (5)
  • js/hang-demo/src/publish.html (2 hunks)
  • js/hang/src/publish/element.ts (5 hunks)
  • js/hang/src/publish/source/camera.ts (2 hunks)
  • js/hang/src/publish/source/device.ts (3 hunks)
  • js/hang/src/publish/source/microphone.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
{rs,js}/hang/**

📄 CodeRabbit inference engine (CLAUDE.md)

All media logic belongs in the hang layer (rs/hang and js/hang)

Files:

  • js/hang/src/publish/element.ts
  • js/hang/src/publish/source/camera.ts
  • js/hang/src/publish/source/microphone.ts
  • js/hang/src/publish/source/device.ts
js/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place JS/TS packages under the js/ directory

Files:

  • js/hang/src/publish/element.ts
  • js/hang-demo/src/publish.html
  • js/hang/src/publish/source/camera.ts
  • js/hang/src/publish/source/microphone.ts
  • js/hang/src/publish/source/device.ts
🧬 Code graph analysis (4)
js/hang/src/publish/element.ts (3)
js/hang/src/publish/source/camera.ts (1)
  • effect (28-66)
js/hang/src/publish/source/device.ts (2)
  • effect (45-111)
  • effect (113-122)
js/hang/src/publish/source/microphone.ts (1)
  • effect (30-68)
js/hang/src/publish/source/camera.ts (2)
js/hang/src/publish/source/device.ts (2)
  • effect (45-111)
  • effect (113-122)
js/hang/src/publish/source/microphone.ts (1)
  • effect (30-68)
js/hang/src/publish/source/microphone.ts (3)
js/hang/src/publish/source/camera.ts (1)
  • effect (28-66)
js/hang/src/publish/source/device.ts (2)
  • effect (45-111)
  • effect (113-122)
js/hang/src/publish/audio/index.ts (5)
  • effect (104-147)
  • effect (149-162)
  • effect (164-246)
  • effect (248-270)
  • AudioStreamTrack (26-30)
js/hang/src/publish/source/device.ts (3)
js/signals/src/index.ts (3)
  • Signal (19-83)
  • Getter (10-13)
  • Effect (88-447)
js/hang/src/publish/source/camera.ts (1)
  • effect (28-66)
js/hang/src/publish/source/microphone.ts (1)
  • effect (30-68)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check
🔇 Additional comments (11)
js/hang-demo/src/publish.html (1)

25-25: Opt-in captions via attribute looks good

Defaulting to no captions and requiring <hang-publish captions> is clear and predictable.

js/hang/src/publish/source/camera.ts (1)

32-39: Switch to requested deviceId and constraints merge is correct

Reading this.device.requested and using { exact: device } while letting explicit constraints override is sound.

js/hang/src/publish/source/microphone.ts (1)

34-40: Requested device and deviceId constraint build look good

The constraint shape matches MediaTrackConstraints and aligns with the new Device API.

js/hang/src/publish/element.ts (5)

337-343: Gating device UI by audio.enabled is a good UX guard

Prevents device queries/select UI until audio is actually in use.


353-355: Select handlers now use preferred/requested correctly

Updating preferred on change and reflecting requested into select.value is correct.

Also applies to: 362-365


425-427: Video device UI gated by video.enabled is consistent with audio

Good consistency across sources.


440-442: Camera select wired to preferred/requested correctly

Looks good.

Also applies to: 449-452


534-537: Status color change: confirm intended UX

Switching “Video Only”/“Audio Only” to green implies “healthy/ready.” If green equates to “streaming,” this is fine; otherwise consider keeping yellow for partial.

js/hang/src/publish/source/device.ts (3)

15-17: Public API change: default/requested now strings

default: Getter<string|undefined> and requested: Getter<string|undefined> are breaking if external consumers expected MediaDeviceInfo. If Device is a public export, document this and note migration (use .available to map id→info).

I can draft a short migration note if this is a published API.

Also applies to: 27-30


37-40: Device enumeration loop and devicechange handling look solid

Using effect.spawn with cancellation and devicechangeeffect.reload() is robust.


101-111: Reasonable default selection heuristics

Front/external/USB for video and default/communications for audio are practical; falling back to first item is acceptable.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
js/hang/src/publish/source/device.ts (4)

37-40: Guard against SSR/test environments without navigator.

This prevents ReferenceError in Node/SSR and no-op attaches when mediaDevices is missing.

Apply:

 this.signals.effect((effect) => {
-  effect.spawn(this.#run.bind(this, effect));
-  effect.event(navigator.mediaDevices, "devicechange", () => effect.reload());
+  if (typeof navigator === "undefined" || !navigator.mediaDevices) return;
+  effect.spawn(this.#run.bind(this, effect));
+  effect.event(navigator.mediaDevices, "devicechange", () => effect.reload());
 });

67-70: Determinism and debug noise.

  • Consider sorting devices by label for stable UX when heuristics don’t match.
  • console.debug is fine; if you’re shipping minified builds, consider gating behind DEV.

Apply:

-    let defaultDevice: MediaDeviceInfo | undefined;
+    let defaultDevice: MediaDeviceInfo | undefined;
+    // Optional: stable order for UI
+    devices.sort((a, b) => a.label.localeCompare(b.label));

Also applies to: 101-111


113-121: Prefer public Getter over private field for readability.

Use available instead of #devices to avoid poking private state for reads.

Apply:

-    const preferred = effect.get(this.preferred);
-    if (preferred && effect.get(this.#devices)?.some((d) => d.deviceId === preferred)) {
+    const preferred = effect.get(this.preferred);
+    const list = effect.get(this.available) ?? [];
+    if (preferred && list.some((d) => d.deviceId === preferred)) {
       // Use the preferred device if it's in our devices list.
       this.#requested.set(preferred);
     } else {
       // Otherwise use the default device.
       this.#requested.set(effect.get(this.default));
     }

124-135: Propagate failure in requestPermission and return a Promise.

Set permission=false on failure and expose a Promise so callers can await UI updates if needed.

Apply:

-  requestPermission() {
-    navigator.mediaDevices
-      .getUserMedia({ [this.kind]: true })
-      .then((stream) => {
-        this.permission.set(true);
-        stream.getTracks().forEach((track) => {
-          track.stop();
-        });
-      })
-      .catch(() => undefined);
-  }
+  async requestPermission(): Promise<void> {
+    try {
+      const stream = await navigator.mediaDevices.getUserMedia({ [this.kind]: true });
+      this.permission.set(true);
+      stream.getTracks().forEach((track) => track.stop());
+    } catch {
+      this.permission.set(false);
+    }
+  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 39a9c69 and 44ec620.

📒 Files selected for processing (2)
  • js/hang/src/publish/element.ts (5 hunks)
  • js/hang/src/publish/source/device.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • js/hang/src/publish/element.ts
🧰 Additional context used
📓 Path-based instructions (2)
{rs,js}/hang/**

📄 CodeRabbit inference engine (CLAUDE.md)

All media logic belongs in the hang layer (rs/hang and js/hang)

Files:

  • js/hang/src/publish/source/device.ts
js/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place JS/TS packages under the js/ directory

Files:

  • js/hang/src/publish/source/device.ts
🧬 Code graph analysis (1)
js/hang/src/publish/source/device.ts (3)
js/signals/src/index.ts (3)
  • Signal (19-83)
  • Getter (10-13)
  • Effect (88-447)
js/hang/src/publish/source/camera.ts (1)
  • effect (28-66)
js/hang/src/publish/source/microphone.ts (1)
  • effect (30-68)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check
🔇 Additional comments (5)
js/hang/src/publish/source/device.ts (5)

15-16: Switching default from object to deviceId is sensible.

Reduces churn and simplifies consumers; looks consistent with downstream usage.


21-26: Active/permission signals are a good addition.

These enable clean wiring from producers; no issues spotted.


27-30: Requested signal aligns with new selection flow.

Clear separation between preferred/default/requested; good.


45-53: Spawn/run pattern is solid and cancel-aware.

Using Promise.race(cancel) avoids stale updates; nice.


31-43: Lifecycle: ensure signals.close() is called.

Callers should invoke close() when tearing down to remove devicechange listener and cancel effects.

Would you like a small helper to auto-close via AbortSignal or a DOM disconnect hook?

Comment on lines +56 to +66
// An empty deviceId means no permissions, or at the very least, no useful information.
if (devices.some((d) => d.deviceId === "")) {
console.warn(`no ${this.kind} permission`);
this.#devices.set(undefined);
this.#default.set(undefined);
return;
}

// Assume we have permission now.
this.permission.set(true);

Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Permission detection is likely wrong; check label, not deviceId, and reset permission=false.

Browsers typically hide labels (empty string) pre-permission while deviceId may still be non-empty. Also, ensure permission is explicitly set to false on this branch to avoid stale true.

Apply:

-    // An empty deviceId means no permissions, or at the very least, no useful information.
-    if (devices.some((d) => d.deviceId === "")) {
-      console.warn(`no ${this.kind} permission`);
-      this.#devices.set(undefined);
-      this.#default.set(undefined);
-      return;
-    }
+    // No permission typically => empty labels (deviceIds may still be non-empty).
+    if (devices.length && devices.every((d) => d.label === "")) {
+      console.warn(`no ${this.kind} permission`);
+      this.permission.set(false);
+      this.#devices.set(undefined);
+      this.#default.set(undefined);
+      return;
+    }
 
-    // Assume we have permission now.
-    this.permission.set(true);
+    // Assume we have permission now.
+    this.permission.set(true);

To confirm cross-browser behavior:


🌐 Web query:

What does enumerateDevices() return before permission is granted across Chrome, Firefox, and Safari in 2025? Specifically, are deviceId or label empty?

💡 Result:

Short answer

  • label: empty on all major browsers (Chrome, Firefox, Safari) unless the page has an active MediaStream or persistent permission. [1][2]
  • deviceId: in current browsers it must not expose stable identifiers to origins that lack permission — Chrome and Firefox return opaque/empty IDs before permission; Safari/WebKit has had bugs and regressions that in some cases expose non‑empty deviceId/label when permissions are partially denied (so behavior may be inconsistent on some Safari versions). See citations. [1][3][4]

Sources

  1. MDN — MediaDeviceInfo / label (labels blank unless active stream or persistent permission). [1]
  2. MDN — MediaDeviceInfo (deviceId description + privacy note). [1]
  3. Discussion / bug & community reports showing deviceId is empty before getUserMedia (fingerprinting/privacy changes). [3]
  4. WebKit bug report showing Safari sometimes returns deviceId/label unexpectedly when permissions are mixed/changed. [4]

If you want, I can run a quick live test snippet for your current browser (or provide code) and report the exact values returned on your machine.


Use labels for permission detection and explicitly reset permission=false
Replace the deviceId check with a label-based check and ensure permission is cleared on the “no permission” branch. For example, in js/hang/src/publish/source/device.ts (lines 56-66):

-    // An empty deviceId means no permissions, or at the very least, no useful information.
-    if (devices.some((d) => d.deviceId === "")) {
+    // No permission typically → all labels are blank (deviceIds may still be opaque or non-empty in some browsers).
+    if (devices.length && devices.every((d) => d.label === "")) {
       console.warn(`no ${this.kind} permission`);
-      this.#devices.set(undefined);
-      this.#default.set(undefined);
+      this.permission.set(false);
+      this.#devices.set(undefined);
+      this.#default.set(undefined);
       return;
     }

-    // Assume we have permission now.
-    this.permission.set(true);
+    // Assume we have permission now.
+    this.permission.set(true);

This covers Chrome/Firefox behavior (labels are blank pre-grant) and avoids stale true values.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// An empty deviceId means no permissions, or at the very least, no useful information.
if (devices.some((d) => d.deviceId === "")) {
console.warn(`no ${this.kind} permission`);
this.#devices.set(undefined);
this.#default.set(undefined);
return;
}
// Assume we have permission now.
this.permission.set(true);
// No permission typically → all labels are blank (deviceIds may still be opaque or non-empty in some browsers).
if (devices.length && devices.every((d) => d.label === "")) {
console.warn(`no ${this.kind} permission`);
this.permission.set(false);
this.#devices.set(undefined);
this.#default.set(undefined);
return;
}
// Assume we have permission now.
this.permission.set(true);
🤖 Prompt for AI Agents
In js/hang/src/publish/source/device.ts around lines 56 to 66, replace the
current deviceId-based permission detection with a label-based check (use
devices.some(d => d.label === "")) because labels are blank pre-grant in some
browsers, and in the "no permission" branch explicitly clear the permission flag
by calling this.permission.set(false) (alongside this.#devices.set(undefined)
and this.#default.set(undefined)); only set this.permission.set(true) after the
label check confirms permission granted (i.e., in the else branch).

@kixelated kixelated merged commit e52f601 into main Aug 31, 2025
4 checks passed
@kixelated kixelated deleted the device-manager branch August 31, 2025 21:45
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.

2 participants