Compare commits

...

9 Commits

Author SHA1 Message Date
d1b99705e6 Bump version to 4.1.2 2024-05-05 21:32:41 +07:00
52896c94ae Update dist 2024-05-05 18:29:14 +07:00
cadc7987b7 Update "disableIndexDbLogging" patch 2024-05-05 18:29:04 +07:00
8fb1787222 Disable telemetry flags in meversion.js 2024-05-05 18:04:20 +07:00
4231d7e9c6 Update logs 2024-05-05 11:54:59 +07:00
ba05eab47b Update dist 2024-05-05 09:37:06 +07:00
e852b246d3 Rewrite volume control feature 2024-05-05 09:36:53 +07:00
23fb50cb6f Update dist 2024-05-04 17:32:17 +07:00
443bf93c9a Fix the refresh button not focusable using gamepad 2024-05-04 17:32:08 +07:00
8 changed files with 219 additions and 85 deletions

View File

@ -1,5 +1,5 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 4.1.1
// @version 4.1.2
// ==/UserScript==

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 4.1.1
// @version 4.1.2
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -14,7 +14,7 @@
// ==/UserScript==
'use strict';
// src/utils/global.ts
var SCRIPT_VERSION = "4.1.1";
var SCRIPT_VERSION = "4.1.2";
var SCRIPT_HOME = "https://github.com/redphx/better-xcloud";
var AppInterface = window.AppInterface;
var STATES = {
@ -3862,7 +3862,7 @@ class BxLogger {
BxLogger.#log(TextColor.ERROR, tag, ...args);
}
static #log(color, tag, ...args) {
console.log("%c" + BxLogger.#PREFIX, "color:" + color + ";font-weight:bold;", tag, "-", ...args);
console.log(`%c${BxLogger.#PREFIX}`, `color:${color};font-weight:bold;`, tag, "//", ...args);
}
}
window.BxLogger = BxLogger;
@ -4814,6 +4814,24 @@ var BxExposed = {
STATES.currentStream.titleInfo = titleInfo;
BxEvent.dispatch(window, BxEvent.TITLE_INFO_READY);
return titleInfo;
},
setupGainNode: ($media, audioStream) => {
if ($media instanceof HTMLAudioElement) {
$media.muted = true;
$media.addEventListener("playing", (e) => {
$media.muted = true;
$media.pause();
});
} else {
$media.muted = true;
$media.addEventListener("playing", (e) => {
$media.muted = true;
});
}
const audioCtx = STATES.currentStream.audioContext;
const source = audioCtx.createMediaStreamSource(audioStream);
const gainNode = audioCtx.createGain();
source.connect(gainNode).connect(audioCtx.destination);
}
};
@ -8527,6 +8545,9 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
.bx-stream-refresh-button {
top: calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important;
}
body[data-media-type=default] .bx-stream-refresh-button {
left: calc(env(safe-area-inset-left, 0px) + 11px) !important;
}
body[data-media-type=tv] .bx-stream-refresh-button {
top: calc(var(--gds-focus-borderSize) + 80px) !important;
}
@ -9206,11 +9227,12 @@ var PATCHES = {
return str2.replace(text, newCode + ";" + text);
},
disableIndexDbLogging(str2) {
const text = "async addLog(e,t=1e4){";
const text = ",this.logsDb=new";
if (!str2.includes(text)) {
return false;
}
return str2.replace(text, text + "return;");
let newCode = ",this.log=()=>{}";
return str2.replace(text, newCode + text);
},
websiteLayout(str2) {
const text = '?"tv":"default"';
@ -9515,6 +9537,24 @@ BxLogger.info('patchRemotePlayMkb', ${configsVar});
`;
str2 = str2.substring(0, backetIndex + 1) + newCode + str2.substring(backetIndex + 1);
return str2;
},
patchAudioMediaStream(str2) {
const text = ".srcObject=this.audioMediaStream,";
if (!str2.includes(text)) {
return false;
}
const newCode = `window.BX_EXPOSED.setupGainNode(arguments[1], this.audioMediaStream),`;
str2 = str2.replace(text, text + newCode);
return str2;
},
patchCombinedAudioVideoMediaStream(str2) {
const text = ".srcObject=this.combinedAudioVideoStream";
if (!str2.includes(text)) {
return false;
}
const newCode = `,window.BX_EXPOSED.setupGainNode(arguments[0], this.combinedAudioVideoStream)`;
str2 = str2.replace(text, text + newCode);
return str2;
}
};
var PATCH_ORDERS = [
@ -9548,6 +9588,8 @@ var PLAYING_PATCH_ORDERS = [
"disableGamepadDisconnectedScreen",
"patchStreamHud",
"playVibration",
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && !getPref(PrefKey.STREAM_COMBINE_SOURCES) && "patchAudioMediaStream",
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && getPref(PrefKey.STREAM_COMBINE_SOURCES) && "patchCombinedAudioVideoMediaStream",
STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "all" && "exposeTouchLayoutManager",
STATES.hasTouchSupport && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "off" || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) && "disableTakRenderer",
BX_FLAGS.EnableXcloudLogging && "enableConsoleLogging",
@ -9619,7 +9661,7 @@ class Patcher {
}
modified = true;
str = patchedStr;
BxLogger.info(LOG_TAG4, `Applied "${patchName}" patch`);
BxLogger.info(LOG_TAG4, `${patchName}`);
appliedPatches.push(patchName);
patchesToCheck.splice(patchIndex, 1);
patchIndex--;
@ -10157,48 +10199,64 @@ function patchRtcPeerConnection() {
const conn = new OrgRTCPeerConnection;
STATES.currentStream.peerConnection = conn;
conn.addEventListener("connectionstatechange", (e) => {
if (conn.connectionState === "connecting") {
STATES.currentStream.audioGainNode = null;
}
BxLogger.info("connectionstatechange", conn.connectionState);
});
return conn;
};
}
function patchAudioContext() {
if (UserAgent.isSafari(true)) {
const nativeCreateGain = window.AudioContext.prototype.createGain;
window.AudioContext.prototype.createGain = function() {
const OrgAudioContext = window.AudioContext;
const nativeCreateGain = OrgAudioContext.prototype.createGain;
window.AudioContext = function(options) {
const ctx = new OrgAudioContext(options);
BxLogger.info("patchAudioContext", ctx, options);
ctx.createGain = function() {
const gainNode = nativeCreateGain.apply(this);
gainNode.gain.value = getPref(PrefKey.AUDIO_VOLUME) / 100;
STATES.currentStream.audioGainNode = gainNode;
return gainNode;
};
}
const OrgAudioContext = window.AudioContext;
window.AudioContext = function() {
const ctx = new OrgAudioContext;
STATES.currentStream.audioContext = ctx;
STATES.currentStream.audioGainNode = null;
return ctx;
};
const nativePlay = HTMLAudioElement.prototype.play;
HTMLAudioElement.prototype.play = function() {
this.muted = true;
const promise = nativePlay.apply(this);
if (STATES.currentStream.audioGainNode) {
return promise;
}
this.addEventListener("playing", (e) => e.target.pause());
const audioCtx = STATES.currentStream.audioContext;
const audioStream = audioCtx.createMediaStreamSource(this.srcObject);
const gainNode = audioCtx.createGain();
audioStream.connect(gainNode);
gainNode.connect(audioCtx.destination);
gainNode.gain.value = getPref(PrefKey.AUDIO_VOLUME) / 100;
STATES.currentStream.audioGainNode = gainNode;
return promise;
}
function patchMeControl() {
const overrideConfigs = {
enableAADTelemetry: false,
enableTelemetry: false,
telEvs: "",
oneDSUrl: ""
};
const MSA = {
MeControl: {}
};
const MeControl = {};
const MsaHandler = {
get(target, prop, receiver) {
return target[prop];
},
set(obj, prop, value) {
if (prop === "MeControl" && value.Config) {
value.Config = Object.assign(value.Config, overrideConfigs);
}
obj[prop] = value;
return true;
}
};
const MeControlHandler = {
get(target, prop, receiver) {
return target[prop];
},
set(obj, prop, value) {
if (prop === "Config") {
value = Object.assign(value, overrideConfigs);
}
obj[prop] = value;
return true;
}
};
window.MSA = new Proxy(MSA, MsaHandler);
window.MeControl = new Proxy(MeControl, MeControlHandler);
}
// src/index.ts
@ -10207,9 +10265,8 @@ var main = function() {
patchRtcCodecs();
interceptHttpRequests();
patchVideoApi();
if (getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL)) {
patchAudioContext();
}
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && patchAudioContext();
getPref(PrefKey.BLOCK_TRACKING) && patchMeControl();
PreloadedState.override();
VibrationManager.initialSetup();
BX_FLAGS.CheckForUpdate && checkForUpdate();

View File

@ -12,6 +12,10 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
top: calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important;
}
body[data-media-type=default] .bx-stream-refresh-button {
left: calc(env(safe-area-inset-left, 0px) + 11px) !important;
}
body[data-media-type=tv] .bx-stream-refresh-button {
top: calc(var(--gds-focus-borderSize) + 80px) !important;
}

View File

@ -23,7 +23,7 @@ import { RemotePlay } from "@modules/remote-play";
import { onHistoryChanged, patchHistoryMethod } from "@utils/history";
import { VibrationManager } from "@modules/vibration-manager";
import { PreloadedState } from "@utils/titles-info";
import { patchAudioContext, patchRtcCodecs, patchRtcPeerConnection, patchVideoApi } from "@utils/monkey-patches";
import { patchAudioContext, patchMeControl, patchRtcCodecs, patchRtcPeerConnection, patchVideoApi } from "@utils/monkey-patches";
import { STATES } from "@utils/global";
import { injectStreamMenuButtons } from "@modules/stream/stream-ui";
import { BxLogger } from "@utils/bx-logger";
@ -216,9 +216,8 @@ function main() {
interceptHttpRequests();
patchVideoApi();
if (getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL)) {
patchAudioContext();
}
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && patchAudioContext();
getPref(PrefKey.BLOCK_TRACKING) && patchMeControl();
PreloadedState.override();

View File

@ -59,12 +59,14 @@ const PATCHES = {
// Disable IndexDB logging
disableIndexDbLogging(str: string) {
const text = 'async addLog(e,t=1e4){';
const text = ',this.logsDb=new';
if (!str.includes(text)) {
return false;
}
return str.replace(text, text + 'return;');
// Replace log() with an empty function
let newCode = ',this.log=()=>{}';
return str.replace(text, newCode + text);
},
// Set custom website layout
@ -459,6 +461,29 @@ BxLogger.info('patchRemotePlayMkb', ${configsVar});
return str;
},
patchAudioMediaStream(str: string) {
const text = '.srcObject=this.audioMediaStream,';
if (!str.includes(text)) {
return false;
}
const newCode = `window.BX_EXPOSED.setupGainNode(arguments[1], this.audioMediaStream),`;
str = str.replace(text, text + newCode);
return str;
},
patchCombinedAudioVideoMediaStream(str: string) {
const text = '.srcObject=this.combinedAudioVideoStream';
if (!str.includes(text)) {
return false;
}
const newCode = `,window.BX_EXPOSED.setupGainNode(arguments[0], this.combinedAudioVideoStream)`;
str = str.replace(text, text + newCode);
return str;
},
};
let PATCH_ORDERS: PatchArray = [
@ -501,6 +526,12 @@ let PLAYING_PATCH_ORDERS: PatchArray = [
'patchStreamHud',
'playVibration',
// 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
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchCombinedAudioVideoMediaStream',
STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all' && 'exposeTouchLayoutManager',
STATES.hasTouchSupport && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'off' || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) && 'disableTakRenderer',
@ -564,7 +595,7 @@ export class Patcher {
let patchesToCheck: PatchArray;
let appliedPatches: PatchArray;
const patchesMap: { [key: string]: PatchArray } = {};
const patchesMap: Record<string, PatchArray> = {};
for (let id in item[1]) {
appliedPatches = [];
@ -608,7 +639,7 @@ export class Patcher {
modified = true;
str = patchedStr;
BxLogger.info(LOG_TAG, `Applied "${patchName}" patch`);
BxLogger.info(LOG_TAG, `${patchName}`);
appliedPatches.push(patchName);
// Remove patch
@ -698,7 +729,7 @@ export class PatcherCache {
return PatcherCache.#CACHE[id];
}
static saveToCache(subCache: { [key: string]: PatchArray }) {
static saveToCache(subCache: Record<string, PatchArray>) {
for (const id in subCache) {
const patchNames = subCache[id];

View File

@ -91,5 +91,26 @@ export const BxExposed = {
BxEvent.dispatch(window, BxEvent.TITLE_INFO_READY);
return titleInfo;
},
setupGainNode: ($media: HTMLMediaElement, audioStream: MediaStream) => {
if ($media instanceof HTMLAudioElement) {
$media.muted = true;
$media.addEventListener('playing', e => {
$media.muted = true;
$media.pause();
});
} else {
$media.muted = true;
$media.addEventListener('playing', e => {
$media.muted = true;
});
}
const audioCtx = STATES.currentStream.audioContext!;
const source = audioCtx.createMediaStreamSource(audioStream);
const gainNode = audioCtx.createGain(); // call monkey-patched createGain() in BxAudioContext
source.connect(gainNode).connect(audioCtx.destination);
}
};

View File

@ -20,7 +20,7 @@ export class BxLogger {
}
static #log(color: TextColor, tag: string, ...args: any) {
console.log('%c' + BxLogger.#PREFIX, 'color:' + color + ';font-weight:bold;', tag, '-', ...args);
console.log(`%c${BxLogger.#PREFIX}`, `color:${color};font-weight:bold;`, tag, '//', ...args);
}
}

View File

@ -1,7 +1,6 @@
import { BxEvent } from "@utils/bx-event";
import { getPref, PrefKey } from "@utils/preferences";
import { STATES } from "@utils/global";
import { UserAgent } from "@utils/user-agent";
import { BxLogger } from "@utils/bx-logger";
export function patchVideoApi() {
@ -104,10 +103,6 @@ export function patchRtcPeerConnection() {
STATES.currentStream.peerConnection = conn;
conn.addEventListener('connectionstatechange', e => {
if (conn.connectionState === 'connecting') {
STATES.currentStream.audioGainNode = null;
}
BxLogger.info('connectionstatechange', conn.connectionState);
});
return conn;
@ -115,46 +110,73 @@ export function patchRtcPeerConnection() {
}
export function patchAudioContext() {
if (UserAgent.isSafari(true)) {
const nativeCreateGain = window.AudioContext.prototype.createGain;
window.AudioContext.prototype.createGain = function() {
const OrgAudioContext = window.AudioContext;
const nativeCreateGain = OrgAudioContext.prototype.createGain;
// @ts-ignore
window.AudioContext = function(options?: AudioContextOptions | undefined): AudioContext {
const ctx = new OrgAudioContext(options);
BxLogger.info('patchAudioContext', ctx, options);
ctx.createGain = function() {
const gainNode = nativeCreateGain.apply(this);
gainNode.gain.value = getPref(PrefKey.AUDIO_VOLUME) / 100;
STATES.currentStream.audioGainNode = gainNode;
return gainNode;
}
}
const OrgAudioContext = window.AudioContext;
// @ts-ignore
window.AudioContext = function() {
const ctx = new OrgAudioContext();
STATES.currentStream.audioContext = ctx;
STATES.currentStream.audioGainNode = null;
return ctx;
}
const nativePlay = HTMLAudioElement.prototype.play;
HTMLAudioElement.prototype.play = function() {
this.muted = true;
const promise = nativePlay.apply(this);
if (STATES.currentStream.audioGainNode) {
return promise;
}
this.addEventListener('playing', e => (e.target as HTMLAudioElement).pause());
const audioCtx = STATES.currentStream.audioContext!;
// TOOD: check srcObject
const audioStream = audioCtx.createMediaStreamSource(this.srcObject as any);
const gainNode = audioCtx.createGain();
audioStream.connect(gainNode);
gainNode.connect(audioCtx.destination);
gainNode.gain.value = getPref(PrefKey.AUDIO_VOLUME) / 100;
STATES.currentStream.audioGainNode = gainNode;
return promise;
}
}
/**
* Disable telemetry flags in meversion.js
*/
export function patchMeControl() {
const overrideConfigs = {
enableAADTelemetry: false,
enableTelemetry: false,
telEvs: '',
oneDSUrl: '',
};
const MSA = {
MeControl: {},
};
const MeControl = {};
const MsaHandler: ProxyHandler<any> = {
get(target, prop, receiver) {
return target[prop];
},
set(obj, prop, value) {
if (prop === 'MeControl' && value.Config) {
value.Config = Object.assign(value.Config, overrideConfigs);
}
obj[prop] = value;
return true;
},
};
const MeControlHandler: ProxyHandler<any> = {
get(target, prop, receiver) {
return target[prop];
},
set(obj, prop, value) {
if (prop === 'Config') {
value = Object.assign(value, overrideConfigs);
}
obj[prop] = value;
return true;
},
};
(window as any).MSA = new Proxy(MSA, MsaHandler);
(window as any).MeControl = new Proxy(MeControl, MeControlHandler);
}