diff --git a/src/index.ts b/src/index.ts index bbcd20c..735581c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -213,6 +213,7 @@ function main() { // Setup UI addCss(); Toast.setup(); + getPref(PrefKey.GAME_BAR_ENABLED) && GameBar.getInstance(); BX_FLAGS.PreloadUi && setupStreamUi(); StreamBadges.setupEvents(); diff --git a/src/modules/game-bar/action-microphone.ts b/src/modules/game-bar/action-microphone.ts new file mode 100644 index 0000000..01d1031 --- /dev/null +++ b/src/modules/game-bar/action-microphone.ts @@ -0,0 +1,81 @@ +import { BxEvent, XcloudEvent } from "@utils/bx-event"; +import { BxIcon } from "@utils/bx-icon"; +import { createButton, ButtonStyle, CE } from "@utils/html"; +import { t } from "@utils/translation"; +import { BaseGameBarAction } from "./action-base"; + +enum MicrophoneState { + REQUESTED = 'Requested', + ENABLED = 'Enabled', + MUTED = 'Muted', + NOT_ALLOWED = 'NotAllowed', + NOT_FOUND = 'NotFound', +} + +export class MicrophoneAction extends BaseGameBarAction { + $content: HTMLElement; + + visible: boolean = false; + + constructor() { + super(); + + const onClick = (e: Event) => { + BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED); + const state = this.$content.getAttribute('data-enabled'); + const enableMic = state === 'true' ? false : true; + + try { + window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic); + this.$content.setAttribute('data-enabled', enableMic.toString()); + } catch (e) { + console.log(e); + } + }; + + const $btnDefault = createButton({ + style: ButtonStyle.GHOST, + icon: BxIcon.MICROPHONE, + title: t('show-touch-controller'), + onClick: onClick, + classes: ['bx-activated'], + }); + + const $btnMuted = createButton({ + style: ButtonStyle.GHOST, + icon: BxIcon.MICROPHONE_MUTED, + title: t('hide-touch-controller'), + onClick: onClick, + }); + + this.$content = CE('div', {}, + $btnDefault, + $btnMuted, + ); + + this.reset(); + + window.addEventListener(BxEvent.STREAM_EVENT_TARGET_READY, e => { + const eventTarget = window.BX_EXPOSED.eventTarget as EventTarget; + eventTarget.addEventListener(XcloudEvent.MICROPHONE_STATE_CHANGED, e => { + const state = window.BX_EXPOSED.streamSession.microphoneState as MicrophoneState; + const enabled = state === MicrophoneState.ENABLED; + + this.$content.setAttribute('data-enabled', enabled.toString()); + + // Show the button in Game Bar if the mic is enabled + this.$content.classList.remove('bx-gone'); + }); + }); + } + + render(): HTMLElement { + return this.$content; + } + + reset(): void { + this.visible = false; + this.$content.classList.add('bx-gone'); + this.$content.setAttribute('data-enabled', 'false'); + } +} diff --git a/src/modules/game-bar/game-bar.ts b/src/modules/game-bar/game-bar.ts index cd4006c..a43b990 100644 --- a/src/modules/game-bar/game-bar.ts +++ b/src/modules/game-bar/game-bar.ts @@ -6,6 +6,7 @@ import { BxIcon } from "@utils/bx-icon"; import type { BaseGameBarAction } from "./action-base"; import { STATES } from "@utils/global"; import { PrefKey, getPref } from "@utils/preferences"; +import { MicrophoneAction } from "./action-microphone"; export class GameBar { @@ -38,6 +39,7 @@ export class GameBar { this.actions = [ new ScreenshotAction(), ...(STATES.hasTouchSupport && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== 'off') ? [new TouchControlAction()] : []), + new MicrophoneAction(), ]; // Render actions diff --git a/src/modules/patcher.ts b/src/modules/patcher.ts index d8bd850..296ca51 100644 --- a/src/modules/patcher.ts +++ b/src/modules/patcher.ts @@ -4,6 +4,7 @@ import { getPref, PrefKey } from "@utils/preferences"; import { VibrationManager } from "@modules/vibration-manager"; import { BxLogger } from "@utils/bx-logger"; import { hashCode } from "@utils/utils"; +import { BxEvent } from "@/utils/bx-event"; type PatchArray = (keyof typeof PATCHES)[]; @@ -496,6 +497,45 @@ BxLogger.info('patchRemotePlayMkb', ${configsVar}); str = str.replace(text, newCode); return str; }, + + exposeEventTarget(str: string) { + const text ='this._eventTarget=new EventTarget'; + if (!str.includes(text)) { + return false; + } + + const newCode = ` +window.BX_EXPOSED.eventTarget = ${text}, +window.dispatchEvent(new Event('${BxEvent.STREAM_EVENT_TARGET_READY}')) +`; + + str = str.replace(text, newCode); + return str; + }, + + exposeStreamSession(str: string) { + const text =',this._connectionType='; + if (!str.includes(text)) { + return false; + } + + const newCode = `; + +window.BX_EXPOSED.streamSession = this; + +const orgSetMicrophoneState = this.setMicrophoneState.bind(this); +this.setMicrophoneState = (e) => { + console.log(e); + orgSetMicrophoneState(e); +}; + +window.dispatchEvent(new Event('${BxEvent.STREAM_SESSION_READY}')) + +true` + text; + + str = str.replace(text, newCode); + return str; + }, }; let PATCH_ORDERS: PatchArray = [ @@ -503,6 +543,8 @@ let PATCH_ORDERS: PatchArray = [ 'overrideSettings', 'broadcastPollingMode', + 'exposeStreamSession', + getPref(PrefKey.UI_LAYOUT) !== 'default' && 'websiteLayout', getPref(PrefKey.LOCAL_CO_OP_ENABLED) && 'supportLocalCoOp', getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && 'forceFortniteConsole', @@ -538,6 +580,8 @@ let PLAYING_PATCH_ORDERS: PatchArray = [ 'patchStreamHud', 'playVibration', + 'exposeEventTarget', + // Patch volume control for normal stream getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && !getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchAudioMediaStream', // Patch volume control for combined audio+video stream diff --git a/src/modules/stream/stream-ui.ts b/src/modules/stream/stream-ui.ts index bf94531..6ded232 100644 --- a/src/modules/stream/stream-ui.ts +++ b/src/modules/stream/stream-ui.ts @@ -120,8 +120,13 @@ export function injectStreamMenuButtons() { let $elm: HTMLElement | null = $node as HTMLElement; + // Ignore SVG elements + if ($elm instanceof SVGSVGElement) { + return; + } + // Error Page: .PureErrorPage.ErrorScreen - if ($elm.className.includes('PureErrorPage')) { + if ($elm.className?.includes('PureErrorPage')) { BxEvent.dispatch(window, BxEvent.STREAM_ERROR_PAGE); return; } @@ -133,7 +138,7 @@ export function injectStreamMenuButtons() { } // Render badges - if ($elm.className.startsWith('StreamMenu-module__container')) { + if ($elm.className?.startsWith('StreamMenu-module__container')) { BxEvent.dispatch(window, BxEvent.STREAM_MENU_SHOWN); const $btnCloseHud = document.querySelector('button[class*=StreamMenu-module__backButton]'); @@ -175,7 +180,7 @@ export function injectStreamMenuButtons() { return; } - if ($elm.className.startsWith('Overlay-module_') || $elm.className.startsWith('InProgressScreen')) { + if ($elm.className?.startsWith('Overlay-module_') || $elm.className?.startsWith('InProgressScreen')) { $elm = $elm.querySelector('#StreamHud'); } diff --git a/src/modules/ui/ui.ts b/src/modules/ui/ui.ts index 3a2529f..2469950 100644 --- a/src/modules/ui/ui.ts +++ b/src/modules/ui/ui.ts @@ -9,7 +9,6 @@ import { StreamStats } from "@modules/stream/stream-stats"; import { TouchController } from "@modules/touch-controller"; import { t } from "@utils/translation"; import { VibrationManager } from "@modules/vibration-manager"; -import { GameBar } from "../game-bar/game-bar"; import { Screenshot } from "@/utils/screenshot"; @@ -477,7 +476,6 @@ export function setupStreamUi() { StreamStats.render(); Screenshot.setup(); - getPref(PrefKey.GAME_BAR_ENABLED) && GameBar.getInstance(); } updateVideoPlayerCss(); diff --git a/src/utils/bx-event.ts b/src/utils/bx-event.ts index 023c34f..d2868cf 100644 --- a/src/utils/bx-event.ts +++ b/src/utils/bx-event.ts @@ -19,6 +19,9 @@ export enum BxEvent { STREAM_WEBRTC_CONNECTED = 'bx-stream-webrtc-connected', STREAM_WEBRTC_DISCONNECTED = 'bx-stream-webrtc-disconnected', + STREAM_EVENT_TARGET_READY = 'bx-stream-event-target-ready', + STREAM_SESSION_READY = 'bx-stream-session-ready', + CUSTOM_TOUCH_LAYOUTS_LOADED = 'bx-custom-touch-layouts-loaded', REMOTE_PLAY_READY = 'bx-remote-play-ready', @@ -31,6 +34,10 @@ export enum BxEvent { GAME_BAR_ACTION_ACTIVATED = 'bx-game-bar-action-activated', } +export enum XcloudEvent { + MICROPHONE_STATE_CHANGED = 'microphoneStateChanged', +} + export namespace BxEvent { export function dispatch(target: HTMLElement | Window, eventName: string, data?: any) { if (!eventName) {