MeetPods e uma aplicacao de menu bar para macOS que permite usar o gesto de mute dos AirPods (pressionar e segurar a haste) para mutar/desmutar o microfone no Google Meet. Tambem intercepta teclas de midia (play/pause) para o mesmo proposito.
┌────────────────────────────────────────────────────────────────────────┐
│ macOS / Hardware │
│ AirPods stem press ──► audioaccessoryd ──► Darwin Notification │
│ AirPods single press ──► Bluetooth AVRCP ──► NX_SYSDEFINED │
└───────────────────────────────┬────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Camada 1: Native Addon (C++/ObjC++) │
│ src/native/media_key_tap.cc │
│ │
│ ┌─────────────┐ ┌───────────────┐ ┌──────────────────────────┐ │
│ │ CGEventTap │ │ NSEvent │ │ AVAudioApplication │ │
│ │ (media keys) │ │ globalMonitor │ │ inputMuteStateChange │ │
│ └──────┬───────┘ └──────┬────────┘ │ Handler (macOS 14+) │ │
│ │ │ └────────────┬─────────────┘ │
│ │ │ │ │
│ ┌──────┴─────────────────┴───┐ ┌────────────────┴───────────────┐ │
│ │ Dedup: 200ms window │ │ Darwin Notification Observer │ │
│ │ (lastTapEventTimeMs) │ │ com.apple.audioaccessoryd │ │
│ └────────────┬───────────────┘ │ .MuteState │ │
│ │ └────────────┬───────────────────┘ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ Dedup: 500ms bidirectional│ │ │
│ │ │ (lastAirpodsMuteTimeMs) │ │ │
│ │ └──────────────────────────┘ │ │
│ │ │ │
│ ┌────────────┴───────────────────────────────┴───────────────────┐ │
│ │ AUHAL Audio Unit │ │
│ │ kAudioUnitSubType_HALOutput (input habilitado, output off) │ │
│ │ Render callback descarta audio — satisfaz "active I/O" │ │
│ │ Ligado/desligado dinamicamente conforme estado da chamada │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ThreadSafeFunction → callback JavaScript │
└───────────────────────────────┬────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Camada 2: Electron Main Process (TypeScript) │
│ src/main/ │
│ │
│ ┌──────────────┐ ┌────────────────┐ ┌──────────────────┐ │
│ │ index.ts │ │ media-key.ts │ │ native-msg.ts │ │
│ │ Orquestrador │ │ Wrapper NAPI │ │ WebSocket Server │ │
│ │ principal │ │ do addon nativo│ │ (porta 18432) │ │
│ └──────┬────────┘ └────────────────┘ └────────┬─────────┘ │
│ │ │ │
│ ┌──────┴───────┐ │ │
│ │ tray.ts │ │ │
│ │ Icone menu │ │ │
│ │ bar + menu │ │ │
│ └──────────────┘ │ │
└───────────────────────────────────────────────────┬────────────────────┘
│
WebSocket 127.0.0.1:18432
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Camada 3: Chrome Extension (Manifest V3) │
│ src/extension/ │
│ │
│ ┌───────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
│ │ background.ts │ │ content.ts │ │ popup.html/popup.ts │ │
│ │ Service Worker │ │ Injetado no │ │ UI de status │ │
│ │ WebSocket ↔ │ │ Google Meet │ │ │ │
│ │ Electron │ │ DOM mute │ │ │ │
│ └───────┬────────┘ └──────┬──────┘ └──────────────────────┘ │
│ │ │ │
│ └──────────────────┘ │
│ chrome.runtime.sendMessage │
└────────────────────────────────────────────────────────────────────────┘
1. AirPods stem press (toque simples)
2. Bluetooth AVRCP → macOS gera NX_SYSDEFINED (subtype 8, keyCode 16)
3. CGEventTap captura o evento
└─ NSEvent globalMonitor serve como fallback (dedup 200ms)
4. ThreadSafeFunction envia para JS: fireMediaKeyCallback(16, true)
5. MediaKeyManager emite evento 'media-key'
6. index.ts:handleMediaKey() recebe
7. ExtensionBridge envia {type: 'toggle_mute'} via WebSocket
8. background.js recebe e repassa via chrome.tabs.sendMessage
9. content.js encontra o botao de mute e faz .click()
10. content.js responde com {success: true, muted: <novo estado>}
11. Resposta volta pela cadeia ate index.ts atualizar tray
1. AirPods stem press-and-hold (gesto de mute)
2. Bluetooth proprietary → audioaccessoryd daemon
3. DOIS caminhos paralelos:
a) Darwin Notification: com.apple.audioaccessoryd.MuteState
b) AVAudioApplication.inputMuteStateChangeHandler (se AUHAL ativo)
4. Dedup bidirecional (500ms) — o primeiro que chegar ganha
5. fireMediaKeyCallback(16, true) — mesmo caminho do media key
6. ... restante identico ao fluxo acima
1. Usuario entra em chamada Meet → status active=true
2. syncAudioInput() → setupAUHAL()
3. AUHAL comeca a puxar audio do microfone (e descartar)
4. macOS reconhece MeetPods como app com audio I/O ativo
5. Usuario faz gesto de mute nos AirPods
6. AVAudioApplication handler dispara (porque ha I/O ativo)
7. Handler retorna YES → notificacao SUPRIMIDA
8. Usuario sai da chamada → status active=false
9. syncAudioInput() → teardownAUHAL()
10. Indicador de microfone desaparece
| Funcao | Responsabilidade |
|---|---|
eventTapCallback() |
Intercepta NX_SYSDEFINED via CGEventTap |
darwinMuteNotificationCallback() |
Recebe Darwin notification do audioaccessoryd |
auhalInputCallback() |
Render callback — chama AudioUnitRender e descarta |
setupAUHAL() / teardownAUHAL() |
Gerencia ciclo de vida do audio unit |
Start() |
Registra CGEventTap, NSEvent monitor, AVAudioApplication handler, Darwin observer |
Stop() |
Limpa todos os recursos |
SetConsume() |
Controla se eventos sao consumidos ou passados adiante |
StartAudioInput() / StopAudioInput() |
N-API exports para controle do AUHAL |
| Funcao | Responsabilidade |
|---|---|
shouldConsume() |
Decide se media keys devem ser consumidos |
syncConsume() |
Atualiza o estado de consumo no addon nativo |
syncAudioInput() |
Liga/desliga AUHAL conforme estado da chamada |
updateTrayState() |
Atualiza icone do tray e sincroniza tudo |
handleMediaKey() |
Logica principal: query + toggle mute |
Wrapper TypeScript sobre o addon nativo. Herda de EventEmitter e emite:
media-key: quando uma tecla de midia e pressionadastatus: quando o listener inicia/paraerror: em caso de falha
Servidor WebSocket na porta 18432 (127.0.0.1 apenas). Gerencia:
- Conexoes de clientes (extensao Chrome)
- Broadcast de mensagens
- Request/response com timeout de 2s para
queryMeetStatusetoggleMute
Gerencia icone no menu bar com 3 estados visuais:
idle: microfone cinza (sem chamada)in-call: microfone com ondas (na chamada, mic ligado)muted: microfone com risco (na chamada, mutado)
Service Worker que:
- Mantem WebSocket com Electron
- Rastreia tabs do Google Meet (Map por tabId)
- Roteia mensagens entre Electron ↔ content script
- Reconecta automaticamente a cada 5s se desconectar
Injetado em paginas do Google Meet:
- Busca botao de mute via seletores CSS (suporte multi-idioma)
- MutationObserver para detectar mudancas de estado
- Faz
.click()no botao de mute quando solicitado - Poll de seguranca a cada 5s
UI da extensao mostrando status em tempo real:
- Electron App: Connected / Offline
- Google Meet: In call / No call
- Microphone: Mic ON / Muted