From 46647dbffd863e90c54f4075c6269513a3b1d3cc Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Thu, 1 Feb 2024 09:20:17 +0700 Subject: [PATCH] Refactor + bug fixes (#245) * Create DATA_CHANNEL_CREATED event * Add BxEvent.dispatch() * Dispatch STREAM_WEBRTC_CONNECTED event * Fix not being able to hide touch control in Remote Play * Fix touch bar again * Listen to STREAM_LOADING & STREAM_STARTING events * Add setupEvents() functions * Show/hide touch controller using CSS instead * Fix exception in LoadingScreen class * Fix Remote Play stopped working --- better-xcloud.user.js | 494 +++++++++++++++++++++++++----------------- 1 file changed, 294 insertions(+), 200 deletions(-) diff --git a/better-xcloud.user.js b/better-xcloud.user.js index d690ab3..ccaad84 100644 --- a/better-xcloud.user.js +++ b/better-xcloud.user.js @@ -44,11 +44,30 @@ const BxEvent = { JUMP_BACK_IN_READY: 'bx-jump-back-in-ready', POPSTATE: 'bx-popstate', + STREAM_LOADING: 'bx-stream-loading', STREAM_STARTING: 'bx-stream-starting', STREAM_STARTED: 'bx-stream-started', + STREAM_PLAYING: 'bx-stream-playing', STREAM_STOPPED: 'bx-stream-stopped', + STREAM_WEBRTC_CONNECTED: 'bx-stream-webrtc-connected', + STREAM_WEBRTC_DISCONNECTED: 'bx-stream-webrtc-disconnected', + CUSTOM_TOUCH_LAYOUTS_LOADED: 'bx-custom-touch-layouts-loaded', + + DATA_CHANNEL_CREATED: 'bx-data-channel-created', + + dispatch: (target, eventName, data) => { + const event = new Event(eventName); + + if (data) { + for (const key in data) { + event[key] = data[key]; + } + } + + target.dispatchEvent(event); + }, }; // Quickly create a tree of elements without having to use innerHTML @@ -3068,7 +3087,29 @@ class RemotePlay { return; } - const GSSV_TOKEN = JSON.parse(localStorage.getItem('xboxcom_xbl_user_info')).tokens['http://gssv.xboxlive.com/'].token; + let GSSV_TOKEN; + try { + GSSV_TOKEN = JSON.parse(localStorage.getItem('xboxcom_xbl_user_info')).tokens['http://gssv.xboxlive.com/'].token; + } catch (e) { + for (let i = 0; i < localStorage.length; i++){ + const key = localStorage.key(i); + if (!key.startsWith('Auth.User.')) { + continue; + } + + const json = JSON.parse(localStorage.getItem(key)); + for (const token of json.tokens) { + if (!token.relyingParty.includes('gssv.xboxlive.com')) { + continue; + } + + GSSV_TOKEN = token.tokenData.token; + break; + } + + break; + } + } fetch('https://xhome.gssv-play-prod.xboxlive.com/v2/login/user', { method: 'POST', @@ -3317,20 +3358,24 @@ class LoadingScreen { LoadingScreen.#orgWebTitle && (document.title = LoadingScreen.#orgWebTitle); LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add('bx-gone'); - const $rocketBg = document.querySelector('#game-stream rect[width="800"]'); - $rocketBg && $rocketBg.addEventListener('transitionend', e => { - LoadingScreen.#$bgStyle.textContent += ` + if (LoadingScreen.#$bgStyle) { + const $rocketBg = document.querySelector('#game-stream rect[width="800"]'); + $rocketBg && $rocketBg.addEventListener('transitionend', e => { + LoadingScreen.#$bgStyle.textContent += ` #game-stream { background: #000 !important; } `; - }); + }); - LoadingScreen.#$bgStyle.textContent += ` + LoadingScreen.#$bgStyle.textContent += ` #game-stream rect[width="800"] { opacity: 1 !important; } `; + } + + LoadingScreen.reset(); } static reset() { @@ -3386,12 +3431,14 @@ class TouchController { } static #show() { - TouchController.loadCustomLayout(GAME_XBOX_TITLE_ID, TouchController.#currentLayoutId, 0); + document.querySelector('#BabylonCanvasContainer-main').parentElement.classList.remove('bx-gone'); + // TouchController.loadCustomLayout(GAME_XBOX_TITLE_ID, TouchController.#currentLayoutId, 0); TouchController.#showing = true; } static #hide() { - TouchController.#dispatchMessage(TouchController.#EVENT_HIDE_CONTROLLER); + document.querySelector('#BabylonCanvasContainer-main').parentElement.classList.add('bx-gone'); + // TouchController.#dispatchMessage(TouchController.#EVENT_HIDE_CONTROLLER); TouchController.#showing = false; } @@ -3403,8 +3450,8 @@ class TouchController { TouchController.#showing ? TouchController.#hide() : TouchController.#show(); } - static enableBar() { - TouchController.#$bar && TouchController.#$bar.setAttribute('data-showing', true); + static #toggleBar(value) { + TouchController.#$bar && TouchController.#$bar.setAttribute('data-showing', value); } static reset() { @@ -3424,9 +3471,9 @@ class TouchController { static getCustomLayouts(xboxTitleId) { const dispatchLayouts = data => { - const event = new Event(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); - event.data = data; - window.dispatchEvent(event); + BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, { + data: data, + }); }; xboxTitleId = '' + xboxTitleId; @@ -3503,11 +3550,14 @@ class TouchController { } static setup() { + const $fragment = document.createDocumentFragment(); const $style = document.createElement('style'); - document.documentElement.appendChild($style); + $fragment.appendChild($style); const $bar = createElement('div', {'id': 'bx-touch-controller-bar'}); - document.documentElement.appendChild($bar); + $fragment.appendChild($bar); + + document.documentElement.appendChild($fragment); // Setup double-tap event let clickTimeout; @@ -3531,11 +3581,10 @@ class TouchController { const PREF_STYLE_STANDARD = getPref(Preferences.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD); const PREF_STYLE_CUSTOM = getPref(Preferences.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM); - const nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel; - RTCPeerConnection.prototype.createDataChannel = function() { - const dataChannel = nativeCreateDataChannel.apply(this, arguments); - if (dataChannel.label !== 'message') { - return dataChannel; + window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => { + const dataChannel = e.dataChannel; + if (!dataChannel || dataChannel.label !== 'message') { + return; } // Apply touch controller's style @@ -3552,6 +3601,8 @@ class TouchController { if (filter) { $style.textContent = `#babylon-canvas { filter: ${filter} !important; }`; + } else { + $style.textContent = ''; } TouchController.#dataChannel = dataChannel; @@ -3576,14 +3627,19 @@ class TouchController { try { if (msg.data.includes('/titleinfo')) { const json = JSON.parse(JSON.parse(msg.data).content); - const xboxTitleId = parseInt(json.titleid, 16); - GAME_XBOX_TITLE_ID = xboxTitleId; - } - } catch (e) { console.log(e) } - }); + TouchController.#toggleBar(json.focused); - return dataChannel; - }; + if (!json.focused) { + TouchController.#show(); + } + + GAME_XBOX_TITLE_ID = parseInt(json.titleid, 16); + } + } catch (e) { + console.log(e); + } + }); + }); } } @@ -4765,9 +4821,9 @@ class MkbHandler { virtualGamepad.connected = true; virtualGamepad.timestamp = performance.now(); - const event = new Event('gamepadconnected'); - event.gamepad = virtualGamepad; - window.dispatchEvent(event); + BxEvent.dispatch(window, 'gamepadconnected', { + gamepad: virtualGamepad, + }); } stop = () => { @@ -4777,9 +4833,9 @@ class MkbHandler { virtualGamepad.connected = false; virtualGamepad.timestamp = performance.now(); - const event = new Event('gamepaddisconnected'); - event.gamepad = virtualGamepad; - window.dispatchEvent(event); + BxEvent.dispatch(window, 'gamepaddisconnected', { + gamepad: virtualGamepad, + }); window.navigator.getGamepads = this.#nativeGetGamepads; @@ -4793,6 +4849,16 @@ class MkbHandler { window.removeEventListener('wheel', this.#onWheelEvent); window.removeEventListener('contextmenu', this.#disableContextMenu); } + + static setupEvents() { + window.addEventListener(BxEvent.STREAM_PLAYING, e => { + // Enable MKB + if (getPref(Preferences.MKB_ENABLED) && (!ENABLE_NATIVE_MKB_BETA || !window.NATIVE_MKB_TITLES.includes(GAME_PRODUCT_ID))) { + console.log('Emulate MKB'); + MkbHandler.INSTANCE.init(); + } + }); + } } @@ -5518,11 +5584,10 @@ class VibrationManager { VibrationManager.updateGlobalVars(); - const orgCreateDataChannel = RTCPeerConnection.prototype.createDataChannel; - RTCPeerConnection.prototype.createDataChannel = function() { - const dataChannel = orgCreateDataChannel.apply(this, arguments); - if (dataChannel.label !== 'input') { - return dataChannel; + window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => { + const dataChannel = e.dataChannel; + if (!dataChannel || dataChannel.label !== 'input') { + return; } const VIBRATION_DATA_MAP = { @@ -5581,9 +5646,7 @@ class VibrationManager { VibrationManager.#playDeviceVibration(data); }); - - return dataChannel; - }; + }); } } @@ -5828,6 +5891,22 @@ class StreamBadges { return $wrapper; } + + static setupEvents() { + window.addEventListener(BxEvent.STREAM_PLAYING, e => { + const $video = e.$video; + + StreamBadges.resolution = {width: $video.videoWidth, height: $video.videoHeight}; + StreamBadges.startTimestamp = +new Date; + + // Get battery level + try { + navigator.getBattery && navigator.getBattery().then(bm => { + StreamBadges.startBatteryLevel = Math.round(bm.level * 100); + }); + } catch(e) {} + }); + } } @@ -6034,6 +6113,92 @@ class StreamStats { StreamStats.refreshStyles(); } + + static getServerStats() { + STREAM_WEBRTC && STREAM_WEBRTC.getStats().then(stats => { + const allVideoCodecs = {}; + let videoCodecId; + + const allAudioCodecs = {}; + let audioCodecId; + + const allCandidates = {}; + let candidateId; + + stats.forEach(stat => { + if (stat.type == 'codec') { + const mimeType = stat.mimeType.split('/'); + if (mimeType[0] === 'video') { + // Store all video stats + allVideoCodecs[stat.id] = stat; + } else if (mimeType[0] === 'audio') { + // Store all audio stats + allAudioCodecs[stat.id] = stat; + } + } else if (stat.type === 'inbound-rtp' && stat.packetsReceived > 0) { + // Get the codecId of the video/audio track currently being used + if (stat.kind === 'video') { + videoCodecId = stat.codecId; + } else if (stat.kind === 'audio') { + audioCodecId = stat.codecId; + } + } else if (stat.type === 'candidate-pair' && stat.packetsReceived > 0 && stat.state === 'succeeded') { + candidateId = stat.remoteCandidateId; + } else if (stat.type === 'remote-candidate') { + allCandidates[stat.id] = stat.address; + } + }); + + // Get video codec from codecId + if (videoCodecId) { + const videoStat = allVideoCodecs[videoCodecId]; + const video = { + codec: videoStat.mimeType.substring(6), + }; + + if (video.codec === 'H264') { + const match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine); + video.profile = match ? match[1] : null; + } + + StreamBadges.video = video; + } + + // Get audio codec from codecId + if (audioCodecId) { + const audioStat = allAudioCodecs[audioCodecId]; + StreamBadges.audio = { + codec: audioStat.mimeType.substring(6), + bitrate: audioStat.clockRate, + } + } + + // Get server type + if (candidateId) { + console.log('candidate', candidateId, allCandidates); + StreamBadges.ipv6 = allCandidates[candidateId].includes(':'); + } + + if (getPref(Preferences.STATS_SHOW_WHEN_PLAYING)) { + StreamStats.start(); + } + }); + } + + static setupEvents() { + window.addEventListener(BxEvent.STREAM_PLAYING, e => { + const PREF_STATS_QUICK_GLANCE = getPref(Preferences.STATS_QUICK_GLANCE); + const PREF_STATS_SHOW_WHEN_PLAYING = getPref(Preferences.STATS_SHOW_WHEN_PLAYING); + + StreamStats.getServerStats(); + // Setup Stat's Quick Glance mode + if (PREF_STATS_QUICK_GLANCE) { + StreamStats.quickGlanceSetup(); + // Show stats bar + !PREF_STATS_SHOW_WHEN_PLAYING && StreamStats.start(true); + } + }); + } } class UserAgent { @@ -8785,8 +8950,11 @@ function interceptHttpRequests() { let url = (typeof request === 'string') ? request : request.url; if (url.endsWith('/play')) { - // Setup UI - setupBxUi(); + BxEvent.dispatch(window, BxEvent.STREAM_LOADING); + } + + if (url.endsWith('/configuration')) { + BxEvent.dispatch(window, BxEvent.STREAM_STARTING); } if (IS_REMOTE_PLAYING && (url.includes('/sessions/home') || url.includes('inputconfigs'))) { @@ -8855,9 +9023,9 @@ function interceptHttpRequests() { if (obj[0].supportedTabs.length > 0) { TouchController.disable(); - const event = new Event(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); - event.data = null; - window.dispatchEvent(event); + BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, { + data: null, + }); } else { TouchController.enable(); @@ -8966,9 +9134,6 @@ function interceptHttpRequests() { // Get region if (url.endsWith('/sessions/cloud/play')) { - // Setup loading screen - PREF_UI_LOADING_SCREEN_GAME_ART && LoadingScreen.setup(); - // Start hiding cursor if (!getPref(Preferences.MKB_ENABLED) && getPref(Preferences.MKB_HIDE_IDLE_CURSOR)) { MouseCursorHider.start(); @@ -9023,8 +9188,6 @@ function interceptHttpRequests() { } if (url.endsWith('/configuration') && url.includes('/sessions/cloud/') && request.method === 'GET') { - PREF_UI_LOADING_SCREEN_GAME_ART && LoadingScreen.hide(); - const promise = NATIVE_FETCH(...arg); // Touch controller for all games @@ -9438,8 +9601,9 @@ function getVideoPlayerFilterStyle() { function updateVideoPlayerCss() { let $elm = document.getElementById('bx-video-css'); if (!$elm) { + const $fragment = document.createDocumentFragment(); $elm = CE('style', {id: 'bx-video-css'}); - document.documentElement.appendChild($elm); + $fragment.appendChild($elm); // Setup SVG filters const $svg = CE('svg', { @@ -9451,7 +9615,8 @@ function updateVideoPlayerCss() { CE('feConvolveMatrix', {'id': 'bx-filter-clarity-matrix', 'order': '3', 'xmlns': 'http://www.w3.org/2000/svg'})) ) ); - document.documentElement.appendChild($svg); + $fragment.appendChild($svg); + document.documentElement.appendChild($fragment); } let filters = getVideoPlayerFilterStyle(); @@ -9795,13 +9960,13 @@ function patchVideoApi() { return; } - onStreamStarted(this); + BxEvent.dispatch(window, BxEvent.STREAM_PLAYING, { + $video: this, + }); } const nativePlay = HTMLMediaElement.prototype.play; HTMLMediaElement.prototype.play = function() { - LoadingScreen.reset(); - if (this.className && this.className.startsWith('XboxSplashVideo')) { if (PREF_SKIP_SPLASH_VIDEO) { this.volume = 0; @@ -10282,10 +10447,9 @@ function patchHistoryMethod(type) { const orig = window.history[type]; return function(...args) { - const event = new Event(BxEvent.POPSTATE); - event.arguments = args; - window.dispatchEvent(event); - + BxEvent.dispatch(window, BxEvent.POPSTATE, { + arguments: args, + }); return orig.apply(this, arguments); }; }; @@ -10332,143 +10496,6 @@ function onHistoryChanged(e) { } -function onStreamStarted($video) { - IS_PLAYING = true; - - // Get title ID for screenshot's name - if (window.location.pathname.includes('/launch/')) { - const matches = /\/launch\/(?[^\/]+)\/(?\w+)/.exec(window.location.pathname); - GAME_TITLE_ID = matches.groups.title_id; - GAME_PRODUCT_ID = matches.groups.product_id; - } else { - GAME_TITLE_ID = 'remote-play'; - GAME_PRODUCT_ID = null; - } - - // Enable MKB - if (getPref(Preferences.MKB_ENABLED) && (!ENABLE_NATIVE_MKB_BETA || !window.NATIVE_MKB_TITLES.includes(GAME_PRODUCT_ID))) { - console.log('Emulate MKB'); - MkbHandler.INSTANCE.init(); - } - - if (TouchController.isEnabled()) { - TouchController.enableBar(); - } - - /* - if (getPref(Preferences.CONTROLLER_ENABLE_SHORTCUTS)) { - GamepadHandler.startPolling(); - } - */ - - const PREF_SCREENSHOT_BUTTON_POSITION = getPref(Preferences.SCREENSHOT_BUTTON_POSITION); - const PREF_STATS_QUICK_GLANCE = getPref(Preferences.STATS_QUICK_GLANCE); - const PREF_STATS_SHOW_WHEN_PLAYING = getPref(Preferences.STATS_SHOW_WHEN_PLAYING); - - // Setup Stat's Quick Glance mode - if (PREF_STATS_QUICK_GLANCE) { - StreamStats.quickGlanceSetup(); - // Show stats bar - !PREF_STATS_SHOW_WHEN_PLAYING && StreamStats.start(true); - } - - $STREAM_VIDEO = $video; - $SCREENSHOT_CANVAS.width = $video.videoWidth; - $SCREENSHOT_CANVAS.height = $video.videoHeight; - - StreamBadges.resolution = {width: $video.videoWidth, height: $video.videoHeight}; - StreamBadges.startTimestamp = +new Date; - - // Get battery level - try { - navigator.getBattery && navigator.getBattery().then(bm => { - StreamBadges.startBatteryLevel = Math.round(bm.level * 100); - }); - } catch(e) {} - - STREAM_WEBRTC.getStats().then(stats => { - const allVideoCodecs = {}; - let videoCodecId; - - const allAudioCodecs = {}; - let audioCodecId; - - const allCandidates = {}; - let candidateId; - - stats.forEach(stat => { - if (stat.type == 'codec') { - const mimeType = stat.mimeType.split('/'); - if (mimeType[0] === 'video') { - // Store all video stats - allVideoCodecs[stat.id] = stat; - } else if (mimeType[0] === 'audio') { - // Store all audio stats - allAudioCodecs[stat.id] = stat; - } - } else if (stat.type === 'inbound-rtp' && stat.packetsReceived > 0) { - // Get the codecId of the video/audio track currently being used - if (stat.kind === 'video') { - videoCodecId = stat.codecId; - } else if (stat.kind === 'audio') { - audioCodecId = stat.codecId; - } - } else if (stat.type === 'candidate-pair' && stat.packetsReceived > 0 && stat.state === 'succeeded') { - candidateId = stat.remoteCandidateId; - } else if (stat.type === 'remote-candidate') { - allCandidates[stat.id] = stat.address; - } - }); - - // Get video codec from codecId - if (videoCodecId) { - const videoStat = allVideoCodecs[videoCodecId]; - const video = { - codec: videoStat.mimeType.substring(6), - }; - - if (video.codec === 'H264') { - const match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine); - video.profile = match ? match[1] : null; - } - - StreamBadges.video = video; - } - - // Get audio codec from codecId - if (audioCodecId) { - const audioStat = allAudioCodecs[audioCodecId]; - StreamBadges.audio = { - codec: audioStat.mimeType.substring(6), - bitrate: audioStat.clockRate, - } - } - - // Get server type - if (candidateId) { - console.log(candidateId, allCandidates); - StreamBadges.ipv6 = allCandidates[candidateId].includes(':'); - } - - if (PREF_STATS_SHOW_WHEN_PLAYING) { - StreamStats.start(); - } - }); - - // Setup screenshot button - if (PREF_SCREENSHOT_BUTTON_POSITION !== 'none') { - const $btn = document.querySelector('.bx-screenshot-button'); - $btn.style.display = 'block'; - - if (PREF_SCREENSHOT_BUTTON_POSITION === 'bottom-right') { - $btn.style.right = '0'; - } else { - $btn.style.left = '0'; - } - } -} - - function disablePwa() { const userAgent = (window.navigator.orgUserAgent || window.navigator.userAgent || '').toLowerCase(); if (!userAgent) { @@ -10508,6 +10535,59 @@ window.addEventListener('popstate', onHistoryChanged); window.history.pushState = patchHistoryMethod('pushState'); window.history.replaceState = patchHistoryMethod('replaceState'); +window.addEventListener(BxEvent.STREAM_LOADING, e => { + // Get title ID for screenshot's name + if (window.location.pathname.includes('/launch/')) { + const matches = /\/launch\/(?[^\/]+)\/(?\w+)/.exec(window.location.pathname); + GAME_TITLE_ID = matches.groups.title_id; + GAME_PRODUCT_ID = matches.groups.product_id; + } else { + GAME_TITLE_ID = 'remote-play'; + GAME_PRODUCT_ID = null; + } + + // Setup UI + setupBxUi(); + + // Setup loading screen + getPref(Preferences.UI_LOADING_SCREEN_GAME_ART) && LoadingScreen.setup(); +}); + +window.addEventListener(BxEvent.STREAM_STARTING, e => { + // Hide loading screen + getPref(Preferences.UI_LOADING_SCREEN_GAME_ART) && LoadingScreen.hide(); +}); + +window.addEventListener(BxEvent.STREAM_PLAYING, e => { + const $video = e.$video; + $STREAM_VIDEO = $video; + + IS_PLAYING = true; + + /* + if (getPref(Preferences.CONTROLLER_ENABLE_SHORTCUTS)) { + GamepadHandler.startPolling(); + } + */ + + const PREF_SCREENSHOT_BUTTON_POSITION = getPref(Preferences.SCREENSHOT_BUTTON_POSITION); + $SCREENSHOT_CANVAS.width = $video.videoWidth; + $SCREENSHOT_CANVAS.height = $video.videoHeight; + + // Setup screenshot button + if (PREF_SCREENSHOT_BUTTON_POSITION !== 'none') { + const $btn = document.querySelector('.bx-screenshot-button'); + $btn.style.display = 'block'; + + if (PREF_SCREENSHOT_BUTTON_POSITION === 'bottom-right') { + $btn.style.right = '0'; + } else { + $btn.style.left = '0'; + } + } +}); + + PreloadedState.override(); // Check for Update @@ -10562,11 +10642,21 @@ if (getPref(Preferences.STREAM_TOUCH_CONTROLLER) === 'all') { VibrationManager.initialSetup(); +const nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel; +RTCPeerConnection.prototype.createDataChannel = function() { + const dataChannel = nativeCreateDataChannel.apply(this, arguments); + + BxEvent.dispatch(window, BxEvent.DATA_CHANNEL_CREATED, { + dataChannel: dataChannel, + }); + + return dataChannel; +} + const OrgRTCPeerConnection = window.RTCPeerConnection; window.RTCPeerConnection = function() { - const peer = new OrgRTCPeerConnection(); - STREAM_WEBRTC = peer; - return peer; + STREAM_WEBRTC = new OrgRTCPeerConnection(); + return STREAM_WEBRTC; } patchRtcCodecs(); @@ -10589,3 +10679,7 @@ if (getPref(Preferences.CONTROLLER_ENABLE_SHORTCUTS)) { Patcher.initialize(); RemotePlay.detect(); + +StreamBadges.setupEvents(); +StreamStats.setupEvents(); +MkbHandler.setupEvents();