diff --git a/src/common/AmongUsState.ts b/src/common/AmongUsState.ts index 6c71e0050..adb2c9c44 100644 --- a/src/common/AmongUsState.ts +++ b/src/common/AmongUsState.ts @@ -85,5 +85,6 @@ export interface VoiceState { localIsAlive: boolean; muted: boolean; deafened: boolean; + effectivelyMuted: boolean; mod: ModsType; } diff --git a/src/main/index.ts b/src/main/index.ts index e1cec42f2..ea50763f5 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -18,9 +18,11 @@ import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-insta import { gameReader } from './hook'; import { GenerateHat } from './avatarGenerator'; const args = require('minimist')(process.argv); // eslint-disable-line +import * as http from 'http'; +import { readFileSync } from 'fs'; const isDevelopment = process.env.NODE_ENV !== 'production'; const devTools = (isDevelopment || args.dev === 1) && true; -const appVersion: string = isDevelopment? "DEV" : autoUpdater.currentVersion.version; +const appVersion: string = isDevelopment ? "DEV" : autoUpdater.currentVersion.version; declare global { namespace NodeJS { @@ -35,6 +37,117 @@ declare global { // global reference to mainWindow (necessary to prevent window from being garbage collected) global.mainWindow = null; global.overlay = null; + +let isMutedGlobal = false; +let isDeafenedGlobal = false; + +export function updateMuteStatus(muted: boolean, deafened: boolean) { + isMutedGlobal = muted; + isDeafenedGlobal = deafened; +} + +function startMicrophoneStatusServer() { + const server = http.createServer((req, res) => { + if (!req.url) return; + + if (req.url.startsWith('/status')) { + res.writeHead(200, { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + }); + res.end(JSON.stringify({ muted: isMutedGlobal, deafened: isDeafenedGlobal })); + return; + } + + if (req.url === '/Muted.png' || req.url === '/Unmuted.png' || req.url.startsWith('/icon')) { + const iconName = req.url.startsWith('/icon') ? (isMutedGlobal || isDeafenedGlobal ? 'Muted.png' : 'Unmuted.png') : req.url.slice(1).split('?')[0]; + + const possiblePaths = isDevelopment ? [ + joinPath(__dirname, '..', '..', 'static', iconName), + joinPath(process.cwd(), 'static', iconName), + joinPath(__dirname, 'static', iconName) + ] : [ + joinPath(__dirname, '..', 'renderer', 'static', iconName), + joinPath(app.getAppPath(), 'dist', 'renderer', 'static', iconName), + joinPath(process.resourcesPath, 'static', iconName), + joinPath(__dirname, iconName) + ]; + + let image = null; + for (const path of possiblePaths) { + try { + image = readFileSync(path); + if (image) break; + } catch (e) { } + } + + if (image) { + res.writeHead(200, { + 'Content-Type': 'image/png', + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + }); + res.end(image); + } else { + res.writeHead(404); + res.end(); + } + return; + } + + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(` + + + + Microphone Status Overlay + + + + + + + + `); + }); + + server.listen(4697, '0.0.0.0', () => { + console.log('Microphone Status Overlay server running on http://localhost:4697'); + }); +} + const store = new Store(); app.commandLine.appendSwitch('disable-pinch'); @@ -43,7 +156,7 @@ if (platform() === 'linux' || !store.get('hardware_acceleration', true)) { } -if(platform() === 'linux'){ +if (platform() === 'linux') { app.commandLine.appendSwitch('disable-gpu-sandbox'); } @@ -304,6 +417,8 @@ if (!gotTheLock) { callback(pathname); }); + startMicrophoneStatusServer(); + protocol.registerFileProtocol('generate', async (request, callback) => { const url = new URL(request.url.replace('generate:///', '')); const path = await GenerateHat(url, gameReader.playercolors, Number(url.searchParams.get('color')), ''); diff --git a/src/main/ipc-handlers.ts b/src/main/ipc-handlers.ts index f112a9bdf..995d33f04 100644 --- a/src/main/ipc-handlers.ts +++ b/src/main/ipc-handlers.ts @@ -14,6 +14,7 @@ import path from 'path'; import fs from 'fs'; import { IpcMessages, IpcOverlayMessages } from '../common/ipc-messages'; +import { updateMuteStatus } from './index'; // Listeners are fire and forget, they do not have "responses" or return values export const initializeIpcListeners = (): void => { @@ -50,8 +51,15 @@ export const initializeIpcListeners = (): void => { app.quit(); }); - ipcMain.on(IpcMessages.SEND_TO_OVERLAY, (_, event: IpcOverlayMessages, ...args: unknown[]) => { + ipcMain.on(IpcMessages.SEND_TO_OVERLAY, (_, event: IpcOverlayMessages, ...args: any[]) => { try { + if (event === IpcOverlayMessages.NOTIFY_VOICE_STATE_CHANGED) { + const state = args[0]; + if (state) { + const isMuted = state.effectivelyMuted !== undefined ? state.effectivelyMuted : (state.muted || false); + updateMuteStatus(isMuted, state.deafened || false); + } + } if (global.overlay) global.overlay.webContents.send(event, ...args); } catch (e) { /*empty*/ diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index e6a3adbbc..36c90971e 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -944,6 +944,7 @@ const Voice: React.FC = function ({ t, error: initialError }: VoiceP if (!connectionStuff.current.deafened && !connectionStuff.current.muted) { inStream.getAudioTracks()[0].enabled = connectionStuff.current.pushToTalkMode === pushToTalkOptions.PUSH_TO_TALK ? pressing : !pressing; + setTalking(pressing); } }); @@ -1305,9 +1306,7 @@ const Voice: React.FC = function ({ t, error: initialError }: VoiceP // Pass voice state to overlay useEffect(() => { - if (!settings.enableOverlay) { - return; - } + const isEffectivelyMuted = mutedState || deafenedState || (settings.pushToTalkMode !== pushToTalkOptions.VOICE && !talking); ipcRenderer.send(IpcMessages.SEND_TO_OVERLAY, IpcOverlayMessages.NOTIFY_VOICE_STATE_CHANGED, { otherTalking, playerSocketIds: playerSocketIdsRef.current, @@ -1319,6 +1318,7 @@ const Voice: React.FC = function ({ t, error: initialError }: VoiceP impostorRadioClientId: !myPlayer?.isImpostor ? -1 : impostorRadioClientId.current, muted: mutedState, deafened: deafenedState, + effectivelyMuted: isEffectivelyMuted, mod: gameState.mod, } as VoiceState); }, [ @@ -1330,6 +1330,8 @@ const Voice: React.FC = function ({ t, error: initialError }: VoiceP mutedState, deafenedState, impostorRadioClientId.current, + gameState.mod, + myPlayer?.isDead ]); return ( diff --git a/static/Muted.png b/static/Muted.png new file mode 100644 index 000000000..c9d226a2b Binary files /dev/null and b/static/Muted.png differ diff --git a/static/Unmuted.png b/static/Unmuted.png new file mode 100644 index 000000000..d2cf4b49f Binary files /dev/null and b/static/Unmuted.png differ