feat(mqtt): publish microphone state to {topicPrefix}/microphone#2497
feat(mqtt): publish microphone state to {topicPrefix}/microphone#2497IsmaelMartinez wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
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.
📦 PR Snap Build Artifacts✅ Snap builds successful! Download artifacts: 🐧 Linux Snap Packagesx86_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 |
📦 PR Build Artifacts✅ Build successful! Download artifacts: 🐧 Linuxx86_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 🍎 macOSx86_64 (129.20 MB) - Contains: .dmg 🪟 Windowsx86_64 (109.58 MB) - Contains: .exe installer 📝 Note: Snap packages (.snap) are built in a separate workflow 🕐 Last updated: 2026-05-15 19:05 UTC |
…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.
…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>
…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.
38e1645 to
297b75b
Compare
…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.
6731633 to
9d00390
Compare
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>
54be6ce to
67acc85
Compare
|



Summary
Wires the speaking-indicator's three-state WebRTC detection into the existing
microphone-state-changedIPC, so{topicPrefix}/microphonenow publishes one ofspeaking | 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):
MQTTMediaStatusServicealready handlesmicrophone-state-changedand 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 whenevermqtt.enabledis 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.mdexactly.Scope
In: microphone state via WebRTC
media-source.audioLevel. Closes the mic half of #1938 / #2107 / #2465.Out (deferred):
{topicPrefix}/camera(no equivalentvideoLevelstat, needs a separate validation pass per the research doc); a bonus{topicPrefix}/speakingtopic; anything in #2370.Files changed
app/browser/tools/speakingIndicator.js— acceptsipcRendererininit(), emitsmicrophone-state-changedon state transitions and on call-end (both React and WebRTC paths). Dedupes consecutive identical emissions.app/browser/preload.js— addsspeakingIndicatortomodulesRequiringIpcso it receivesipcRenderer.app/mqtt/mediaStatusService.js— renames the handler parameter fromenabledtostate, drops theString()boolean coercion, updates the docblock.docs-site/docs/configuration.md— updates the{topicPrefix}/microphonerow from"true" / "false"to the four state strings, with descriptions and the activation rule (auto-on whenmqtt.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 whenipcRendereris missing (overlay-only path).docs-site/docs/development/ipc-api-generated.md— regenerated. Mostly line-number drift from main movement; the newmicrophone-state-changeddescription is unchanged.Test plan
npm run lint— cleannode --test tests/unit/speakingIndicator.test.js— 12/12 pass (9 existing + 3 new)E2E_USER_DATA_DIRprofile, MQTT enabled, real Teams call, mosquitto subscriber on{topicPrefix}/microphoneto verify the four states{topicPrefix}/microphoneis"off"after hanging upcloses #1938
closes #2107
closes #2465
🤖 Generated with Claude Code