Skip to content

feat(mqtt): publish microphone state to {topicPrefix}/microphone#2497

Open
IsmaelMartinez wants to merge 6 commits into
mainfrom
feat/mqtt-microphone-state
Open

feat(mqtt): publish microphone state to {topicPrefix}/microphone#2497
IsmaelMartinez wants to merge 6 commits into
mainfrom
feat/mqtt-microphone-state

Conversation

@IsmaelMartinez
Copy link
Copy Markdown
Owner

Summary

Wires the speaking-indicator's three-state WebRTC detection into the existing microphone-state-changed IPC, so {topicPrefix}/microphone now publishes one of speaking | silent | muted | off (retained), instead of the placeholder "true" / "false" documented in the topics table.

The infrastructure has been in place since PR #2007 (Phase 1 of the extended-status work): MQTTMediaStatusService already handles microphone-state-changed and publishes to {topicPrefix}/microphone. What was missing was the renderer-side emission. This PR plugs that in by reusing the speaking-indicator's polling loop (which already runs whenever mqtt.enabled is true, per the #2358 fix), so MQTT users do not need to enable the visual overlay to get the topic.

Implementation matches docs-site/docs/development/research/mqtt-microphone-state-research.md exactly.

Scope

In: microphone state via WebRTC media-source.audioLevel. Closes the mic half of #1938 / #2107 / #2465.

Out (deferred): {topicPrefix}/camera (no equivalent videoLevel stat, needs a separate validation pass per the research doc); a bonus {topicPrefix}/speaking topic; anything in #2370.

Files changed

  • app/browser/tools/speakingIndicator.js — accepts ipcRenderer in init(), emits microphone-state-changed on state transitions and on call-end (both React and WebRTC paths). Dedupes consecutive identical emissions.
  • app/browser/preload.js — adds speakingIndicator to modulesRequiringIpc so it receives ipcRenderer.
  • app/mqtt/mediaStatusService.js — renames the handler parameter from enabled to state, drops the String() boolean coercion, updates the docblock.
  • docs-site/docs/configuration.md — updates the {topicPrefix}/microphone row from "true" / "false" to the four state strings, with descriptions and the activation rule (auto-on when mqtt.enabled).
  • tests/unit/speakingIndicator.test.js — three new tests covering IPC emission on state change, the 'off' emission on connection close, and a no-op when ipcRenderer is missing (overlay-only path).
  • docs-site/docs/development/ipc-api-generated.md — regenerated. Mostly line-number drift from main movement; the new microphone-state-changed description is unchanged.

Test plan

  • npm run lint — clean
  • node --test tests/unit/speakingIndicator.test.js — 12/12 pass (9 existing + 3 new)
  • Local repro with an isolated E2E_USER_DATA_DIR profile, MQTT enabled, real Teams call, mosquitto subscriber on {topicPrefix}/microphone to verify the four states
  • Verify {topicPrefix}/microphone is "off" after hanging up
  • Verify dedupe: rapid speaking/silent flips do not produce duplicate retained messages for the same state

closes #1938
closes #2107
closes #2465

🤖 Generated with Claude Code

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements detailed microphone state tracking ('speaking', 'silent', 'muted', 'off') and forwards these states to the main process via IPC for MQTT publishing. Key changes include updating the SpeakingIndicator tool to handle IPC communication, modifying the MQTTMediaStatusService to process string-based states, and updating the documentation and unit tests. Feedback focuses on ensuring the initial microphone state is correctly captured by adjusting the emission logic in speakingIndicator.js and removing redundant string coercion in mediaStatusService.js.

Comment thread app/browser/tools/speakingIndicator.js Outdated
Comment thread app/mqtt/mediaStatusService.js Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

📦 PR Snap Build Artifacts

Snap builds successful! Download artifacts:

🐧 Linux Snap Packages

x86_64 (110.69 MB)

arm64 (107.51 MB)

armv7l (101.54 MB)


📝 Note: Other package formats (.deb, .rpm, .AppImage, .dmg, .exe) are built in the main workflow

View workflow run

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

📦 PR Build Artifacts

Build successful! Download artifacts:

🐧 Linux

x86_64 (447.78 MB) - Contains: .deb, .rpm, .tar.gz, .AppImage

arm64 (438.03 MB) - Contains: .deb, .rpm, .tar.gz, .AppImage

armv7l (415.86 MB) - Contains: .deb, .rpm, .tar.gz, .AppImage

🍎 macOS

x86_64 (129.20 MB) - Contains: .dmg

🪟 Windows

x86_64 (109.58 MB) - Contains: .exe installer


📝 Note: Snap packages (.snap) are built in a separate workflow

View workflow run

🕐 Last updated: 2026-05-15 19:05 UTC

IsmaelMartinez added a commit that referenced this pull request May 5, 2026
…coercion

- speakingIndicator: move #emitMicrophoneState outside the state-change
  guard so the first detected state is published even when it matches
  the initial 'silent' value. Internal #lastEmittedState dedupe still
  prevents redundant IPC traffic.
- mediaStatusService: drop String(state) since state is already a string,
  matching the PR description and updated docblock.

Addresses Gemini review on PR #2497.
IsmaelMartinez added a commit that referenced this pull request May 6, 2026
…ebar (#2503)

Audit of the research folder against the current codebase state. Per the
lifecycle policy in the research README, three docs whose work has fully
shipped are removed; four are refreshed to match what is now live; two
docs that were never indexed in the Docusaurus sidebar are added.

Removed:
- electron-40-migration-research.md (premise obsolete; repo skipped 40,
  on Electron 41.5.0 since v2.8.0 via PR #2347)
- notification-sound-overhaul-research.md (Phase 1 shipped in v2.7.10
  via PR #2306; Phase 2/3 speculative without a driving issue)
- cross-distro-ci-smoke-test-design.md (workflow shipped, plan and
  ADR-016 supersede)

Updated status sections:
- mqtt-extended-status-investigation.md (Phase 2 microphone now in PR #2497)
- custom-notification-system-research.md (footer version stamp + note on
  in-flight PR #2414 / issue #2411)
- system-performance-research.md (item 5 shipped, 1/2/3/4/8 remaining)
- graph-api-integration-research.md (IPC table now reflects the 7 channels
  actually shipped, including search-people and send-chat-message)

Sidebar additions:
- system-performance-research and join-meeting-window-takeover-research
  existed in the folder but were not in sidebars.ts, so were reachable
  only by direct URL. Now indexed alongside the other research docs.

Roadmap and plan files updated to remove links to the deleted research,
so Docusaurus build does not break.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
IsmaelMartinez added a commit that referenced this pull request May 7, 2026
…coercion

- speakingIndicator: move #emitMicrophoneState outside the state-change
  guard so the first detected state is published even when it matches
  the initial 'silent' value. Internal #lastEmittedState dedupe still
  prevents redundant IPC traffic.
- mediaStatusService: drop String(state) since state is already a string,
  matching the PR description and updated docblock.

Addresses Gemini review on PR #2497.
@IsmaelMartinez IsmaelMartinez force-pushed the feat/mqtt-microphone-state branch from 38e1645 to 297b75b Compare May 7, 2026 16:19
IsmaelMartinez added a commit that referenced this pull request May 11, 2026
…coercion

- speakingIndicator: move #emitMicrophoneState outside the state-change
  guard so the first detected state is published even when it matches
  the initial 'silent' value. Internal #lastEmittedState dedupe still
  prevents redundant IPC traffic.
- mediaStatusService: drop String(state) since state is already a string,
  matching the PR description and updated docblock.

Addresses Gemini review on PR #2497.
@IsmaelMartinez IsmaelMartinez force-pushed the feat/mqtt-microphone-state branch from 6731633 to 9d00390 Compare May 11, 2026 06:37
IsmaelMartinez and others added 6 commits May 15, 2026 19:56
Wires the speaking-indicator's three-state WebRTC detection
(speaking/silent/muted, plus 'off' on call end) into the existing
microphone-state-changed IPC, which mediaStatusService already publishes
to MQTT with retain. Closes the microphone half of #1938 / #2107 / #2465.
Camera state remains future work since it needs a separate detection
path with no equivalent of media-source.audioLevel.

Refs: docs-site/docs/development/research/mqtt-microphone-state-research.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…coercion

- speakingIndicator: move #emitMicrophoneState outside the state-change
  guard so the first detected state is published even when it matches
  the initial 'silent' value. Internal #lastEmittedState dedupe still
  prevents redundant IPC traffic.
- mediaStatusService: drop String(state) since state is already a string,
  matching the PR description and updated docblock.

Addresses Gemini review on PR #2497.
Local repro showed the silent/muted flip @MiguelAngelLV reported is the
quiet-mic noise floor oscillating across the 0.0001 MUTED_LEVEL
threshold. Captured values on my session: actual muted reads 0.00009,
silent (unmuted, quiet) reads 0.001-0.009, but on a quieter setup the
silent floor sits much closer to 0.0001 and crosses it per tick.

Require 3 consecutive below-threshold ticks before transitioning to
muted; reset the counter on any above-threshold tick. At 150ms polling
this is ~450ms entry delay, acceptable for an MQTT topic. Exit from
muted stays immediate on the first above-threshold tick.

Drops the in-file claim that Teams zeroes audioLevel to exactly 0.0
when muted; local repro showed 0.00009, not 0.

Two new unit tests cover the oscillation (no muted emission) and the
sustained-below (muted emitted after hysteresis) cases.
…te signal

audioLevel alone was unreliable for detecting mute on quiet mics, where the
unmuted level can read zero and look indistinguishable from a hardware mute
(issue #2465). The hysteresis tick threshold could not solve this without
introducing equal false negatives on rapid mute/unmute.

Switching to RTCRtpSender.track.enabled gives a direct signal: Teams sets the
local audio track to enabled = false when the user clicks mute. Combined with
audioLevel for the speaking vs silent split, the three-state machine becomes
unambiguous: track disabled → muted, audioLevel above SPEAKING_THRESHOLD →
speaking, otherwise silent. Removes MUTED_LEVEL, MUTED_TICKS_REQUIRED, the
mutedTicks counter, and the hasSeenAudio pre-join guard, all of which the new
signal makes unnecessary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@IsmaelMartinez IsmaelMartinez force-pushed the feat/mqtt-microphone-state branch from 54be6ce to 67acc85 Compare May 15, 2026 18:58
@sonarqubecloud
Copy link
Copy Markdown

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.

[Feat]: Mute, Video, Sharing and others status on mqtt [Enhancement]: Publish screen sharing status to MQTT [Feat]: Extended MQTT status fields

1 participant