mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-07-03 21:01:43 +02:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
8f49c48e74 | |||
6fa1f73702 | |||
728abced45 | |||
411e43ceb0 | |||
baa22dbefc | |||
97fb7a114f | |||
39b2f814b6 | |||
3d34bb3edf | |||
ab1c93eb3a | |||
739adfce41 | |||
2e77f19006 | |||
8a40d361d9 | |||
98fa273b48 | |||
1e6527413c | |||
b9134bc141 | |||
336a965653 |
15
build.ts
15
build.ts
@ -55,7 +55,7 @@ const postProcess = (str: string): string => {
|
||||
|
||||
// Minify SVG import code
|
||||
const svgMap = {}
|
||||
str = str.replaceAll(/var ([\w_]+) = ("<svg.*?");\n\n/g, function(match, p1, p2) {
|
||||
str = str.replaceAll(/var ([\w_]+) = ("<svg.*?");\n\n/g, (match, p1, p2) => {
|
||||
// Remove new lines in SVG
|
||||
p2 = p2.replaceAll(/\\n*\s*/g, '');
|
||||
|
||||
@ -76,6 +76,17 @@ const postProcess = (str: string): string => {
|
||||
// Remove blank lines
|
||||
str = str.replaceAll(/\n([\s]*)\n/g, "\n");
|
||||
|
||||
// Minify WebGL shaders & JS strings
|
||||
// Replace "\n " with "\n"
|
||||
str = str.replaceAll(/\\n+\s*/g, '\\n');
|
||||
// Remove comment line
|
||||
str = str.replaceAll(/\\n\/\/.*?(?=\\n)/g, '');
|
||||
|
||||
// Replace ${"time".toUpperCase()} with "TIME"
|
||||
str = str.replaceAll(/\$\{"([^"]+)"\.toUpperCase\(\)\}/g, (match, p1) => {
|
||||
return p1.toUpperCase();
|
||||
});
|
||||
|
||||
assert(str.includes('/* ADDITIONAL CODE */'));
|
||||
assert(str.includes('window.BX_EXPOSED = BxExposed'));
|
||||
assert(str.includes('window.BxEvent = BxEvent'));
|
||||
@ -139,7 +150,7 @@ const build = async (target: BuildTarget, version: string, variant: BuildVariant
|
||||
await Bun.write(path, scriptHeader + result);
|
||||
|
||||
// Create meta file (don't build if it's beta version)
|
||||
if (!version.includes('beta')) {
|
||||
if (!version.includes('beta') && variant === 'full') {
|
||||
await Bun.write(outDir + '/' + outputMetaName, txtMetaHeader.replace('[[VERSION]]', version));
|
||||
}
|
||||
|
||||
|
5
dist/better-xcloud.lite.meta.js
vendored
5
dist/better-xcloud.lite.meta.js
vendored
@ -1,5 +0,0 @@
|
||||
// ==UserScript==
|
||||
// @name Better xCloud
|
||||
// @namespace https://github.com/redphx
|
||||
// @version 5.8.0
|
||||
// ==/UserScript==
|
301
dist/better-xcloud.lite.user.js
vendored
301
dist/better-xcloud.lite.user.js
vendored
File diff suppressed because one or more lines are too long
2
dist/better-xcloud.meta.js
vendored
2
dist/better-xcloud.meta.js
vendored
@ -1,5 +1,5 @@
|
||||
// ==UserScript==
|
||||
// @name Better xCloud
|
||||
// @namespace https://github.com/redphx
|
||||
// @version 5.8.0
|
||||
// @version 5.8.2
|
||||
// ==/UserScript==
|
||||
|
311
dist/better-xcloud.user.js
vendored
311
dist/better-xcloud.user.js
vendored
File diff suppressed because one or more lines are too long
@ -11,7 +11,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.10",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/node": "^22.7.5",
|
||||
"@types/stylus": "^0.48.43",
|
||||
"eslint": "^9.12.0",
|
||||
"eslint-plugin-compat": "^6.0.1",
|
||||
|
@ -91,6 +91,7 @@ div[class^=StreamMenu-module__container] .bx-badges {
|
||||
&[data-stats*="[batt]"] > .bx-stat-batt,
|
||||
&[data-stats*="[fps]"] > .bx-stat-fps,
|
||||
&[data-stats*="[ping]"] > .bx-stat-ping,
|
||||
&[data-stats*="[jit]"] > .bx-stat-jit,
|
||||
&[data-stats*="[btr]"] > .bx-stat-btr,
|
||||
&[data-stats*="[dt]"] > .bx-stat-dt,
|
||||
&[data-stats*="[pl]"] > .bx-stat-pl,
|
||||
@ -106,6 +107,7 @@ div[class^=StreamMenu-module__container] .bx-badges {
|
||||
&[data-stats$="[batt]"] > .bx-stat-batt,
|
||||
&[data-stats$="[fps]"] > .bx-stat-fps,
|
||||
&[data-stats$="[ping]"] > .bx-stat-ping,
|
||||
&[data-stats$="[jit]"] > .bx-stat-jit,
|
||||
&[data-stats$="[btr]"] > .bx-stat-btr,
|
||||
&[data-stats$="[dt]"] > .bx-stat-dt,
|
||||
&[data-stats$="[pl]"] > .bx-stat-pl,
|
||||
|
@ -98,4 +98,5 @@ export enum PrefKey {
|
||||
REMOTE_PLAY_RESOLUTION = 'xhome_resolution',
|
||||
|
||||
GAME_FORTNITE_FORCE_CONSOLE = 'game_fortnite_force_console',
|
||||
GAME_MSFS2020_FORCE_NATIVE_MKB = 'game_msfs2020_force_native_mkb',
|
||||
}
|
||||
|
@ -378,6 +378,10 @@ function waitForRootDialog() {
|
||||
|
||||
|
||||
function main() {
|
||||
if (getPref(PrefKey.GAME_MSFS2020_FORCE_NATIVE_MKB)) {
|
||||
BX_FLAGS.ForceNativeMkbTitles.push('9PMQDM08SNK9');
|
||||
}
|
||||
|
||||
// Monkey patches
|
||||
patchRtcPeerConnection();
|
||||
patchRtcCodecs();
|
||||
|
@ -211,7 +211,8 @@ const PATCHES = {
|
||||
|
||||
// Block gamepad stats collecting
|
||||
if (getPref(PrefKey.BLOCK_TRACKING)) {
|
||||
codeBlock = codeBlock.replaceAll('this.inputPollingIntervalStats.addValue', '');
|
||||
codeBlock = codeBlock.replace('this.inputPollingIntervalStats.addValue', '');
|
||||
codeBlock = codeBlock.replace('this.inputPollingDurationStats.addValue', '');
|
||||
}
|
||||
|
||||
// Map the Share button on Xbox Series controller with the capturing screenshot feature
|
||||
@ -219,8 +220,8 @@ const PATCHES = {
|
||||
if (match) {
|
||||
const gamepadVar = match[1];
|
||||
const newCode = renderString(codeControllerShortcuts, {
|
||||
gamepadVar,
|
||||
});
|
||||
gamepadVar,
|
||||
});
|
||||
|
||||
codeBlock = codeBlock.replace('this.gamepadTimestamps.set', newCode + 'this.gamepadTimestamps.set');
|
||||
}
|
||||
|
@ -1,9 +1,15 @@
|
||||
precision mediump float;
|
||||
uniform sampler2D data;
|
||||
uniform vec2 iResolution;
|
||||
|
||||
const int FILTER_UNSHARP_MASKING = 1;
|
||||
const int FILTER_CAS = 2;
|
||||
|
||||
precision highp float;
|
||||
uniform sampler2D data;
|
||||
uniform vec2 iResolution;
|
||||
// constrast = 0.8
|
||||
const float CAS_CONTRAST_PEAK = (-3.0 * 0.8 + 8.0);
|
||||
|
||||
// Luminosity factor
|
||||
const vec3 LUMINOSITY_FACTOR = vec3(0.2126, 0.7152, 0.0722);
|
||||
|
||||
uniform int filterId;
|
||||
uniform float sharpenFactor;
|
||||
@ -11,27 +17,24 @@ uniform float brightness;
|
||||
uniform float contrast;
|
||||
uniform float saturation;
|
||||
|
||||
vec3 textureAt(sampler2D tex, vec2 coord) {
|
||||
return texture2D(tex, coord / iResolution.xy).rgb;
|
||||
}
|
||||
vec3 clarityBoost(sampler2D tex, vec2 coord) {
|
||||
vec2 texelSize = 1.0 / iResolution.xy;
|
||||
|
||||
vec3 clarityBoost(sampler2D tex, vec2 coord)
|
||||
{
|
||||
// Load a collection of samples in a 3x3 neighorhood, where e is the current pixel.
|
||||
// a b c
|
||||
// d e f
|
||||
// g h i
|
||||
vec3 a = textureAt(tex, coord + vec2(-1, 1));
|
||||
vec3 b = textureAt(tex, coord + vec2(0, 1));
|
||||
vec3 c = textureAt(tex, coord + vec2(1, 1));
|
||||
vec3 a = texture2D(tex, coord + texelSize * vec2(-1, 1)).rgb;
|
||||
vec3 b = texture2D(tex, coord + texelSize * vec2(0, 1)).rgb;
|
||||
vec3 c = texture2D(tex, coord + texelSize * vec2(1, 1)).rgb;
|
||||
|
||||
vec3 d = textureAt(tex, coord + vec2(-1, 0));
|
||||
vec3 e = textureAt(tex, coord);
|
||||
vec3 f = textureAt(tex, coord + vec2(1, 0));
|
||||
vec3 d = texture2D(tex, coord + texelSize * vec2(-1, 0)).rgb;
|
||||
vec3 e = texture2D(tex, coord).rgb;
|
||||
vec3 f = texture2D(tex, coord + texelSize * vec2(1, 0)).rgb;
|
||||
|
||||
vec3 g = textureAt(tex, coord + vec2(-1, -1));
|
||||
vec3 h = textureAt(tex, coord + vec2(0, -1));
|
||||
vec3 i = textureAt(tex, coord + vec2(1, -1));
|
||||
vec3 g = texture2D(tex, coord + texelSize * vec2(-1, -1)).rgb;
|
||||
vec3 h = texture2D(tex, coord + texelSize * vec2(0, -1)).rgb;
|
||||
vec3 i = texture2D(tex, coord + texelSize * vec2(1, -1)).rgb;
|
||||
|
||||
if (filterId == FILTER_CAS) {
|
||||
// Soft min and max.
|
||||
@ -54,10 +57,7 @@ vec3 clarityBoost(sampler2D tex, vec2 coord)
|
||||
// Shaping amount of sharpening.
|
||||
amplifyRgb = inversesqrt(amplifyRgb);
|
||||
|
||||
float contrast = 0.8;
|
||||
float peak = -3.0 * contrast + 8.0;
|
||||
vec3 weightRgb = -(1.0 / (amplifyRgb * peak));
|
||||
|
||||
vec3 weightRgb = -(1.0 / (amplifyRgb * CAS_CONTRAST_PEAK));
|
||||
vec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);
|
||||
|
||||
// 0 w 0
|
||||
@ -70,9 +70,10 @@ vec3 clarityBoost(sampler2D tex, vec2 coord)
|
||||
|
||||
return outColor;
|
||||
} else if (filterId == FILTER_UNSHARP_MASKING) {
|
||||
vec3 gaussianBlur = (a * 1.0 + b * 2.0 + c * 1.0 +
|
||||
d * 2.0 + e * 4.0 + f * 2.0 +
|
||||
g * 1.0 + h * 2.0 + i * 1.0) / 16.0;
|
||||
vec3 gaussianBlur = (a + c + g + i) * 1.0 +
|
||||
(b + d + f + h) * 2.0 +
|
||||
e * 4.0;
|
||||
gaussianBlur /= 16.0;
|
||||
|
||||
// Return edge detection
|
||||
return e + (e - gaussianBlur) * sharpenFactor / 3.0;
|
||||
@ -81,40 +82,30 @@ vec3 clarityBoost(sampler2D tex, vec2 coord)
|
||||
return e;
|
||||
}
|
||||
|
||||
vec3 adjustBrightness(vec3 color) {
|
||||
return (1.0 + brightness) * color;
|
||||
}
|
||||
|
||||
vec3 adjustContrast(vec3 color) {
|
||||
return 0.5 + (1.0 + contrast) * (color - 0.5);
|
||||
}
|
||||
|
||||
vec3 adjustSaturation(vec3 color) {
|
||||
const vec3 luminosityFactor = vec3(0.2126, 0.7152, 0.0722);
|
||||
vec3 grayscale = vec3(dot(color, luminosityFactor));
|
||||
|
||||
return mix(grayscale, color, 1.0 + saturation);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 color;
|
||||
vec2 uv = gl_FragCoord.xy / iResolution.xy;
|
||||
|
||||
if (sharpenFactor > 0.0) {
|
||||
color = clarityBoost(data, gl_FragCoord.xy);
|
||||
color = clarityBoost(data, uv);
|
||||
} else {
|
||||
color = textureAt(data, gl_FragCoord.xy);
|
||||
color = texture2D(data, uv).rgb;
|
||||
}
|
||||
|
||||
if (saturation != 0.0) {
|
||||
color = adjustSaturation(color);
|
||||
// Saturation
|
||||
if (saturation != 1.0) {
|
||||
vec3 grayscale = vec3(dot(color, LUMINOSITY_FACTOR));
|
||||
color = mix(grayscale, color, saturation);
|
||||
}
|
||||
|
||||
if (contrast != 0.0) {
|
||||
color = adjustContrast(color);
|
||||
// Contrast
|
||||
if (contrast != 1.0) {
|
||||
color = 0.5 + contrast * (color - 0.5);
|
||||
}
|
||||
|
||||
if (brightness != 0.0) {
|
||||
color = adjustBrightness(color);
|
||||
// Brightness
|
||||
if (brightness != 1.0) {
|
||||
color = brightness * color;
|
||||
}
|
||||
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
|
@ -1,5 +1,5 @@
|
||||
attribute vec2 position;
|
||||
attribute vec4 position;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(position, 0, 1);
|
||||
gl_Position = position;
|
||||
}
|
||||
|
@ -8,16 +8,16 @@ import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
const LOG_TAG = 'WebGL2Player';
|
||||
|
||||
export class WebGL2Player {
|
||||
#$video: HTMLVideoElement;
|
||||
#$canvas: HTMLCanvasElement;
|
||||
private $video: HTMLVideoElement;
|
||||
private $canvas: HTMLCanvasElement;
|
||||
|
||||
#gl: WebGL2RenderingContext | null = null;
|
||||
#resources: Array<any> = [];
|
||||
#program: WebGLProgram | null = null;
|
||||
private gl: WebGL2RenderingContext | null = null;
|
||||
private resources: Array<any> = [];
|
||||
private program: WebGLProgram | null = null;
|
||||
|
||||
#stopped: boolean = false;
|
||||
private stopped: boolean = false;
|
||||
|
||||
#options = {
|
||||
private options = {
|
||||
filterId: 1,
|
||||
sharpenFactor: 0,
|
||||
brightness: 0.0,
|
||||
@ -25,112 +25,112 @@ export class WebGL2Player {
|
||||
saturation: 0.0,
|
||||
};
|
||||
|
||||
#animFrameId: number | null = null;
|
||||
private animFrameId: number | null = null;
|
||||
|
||||
constructor($video: HTMLVideoElement) {
|
||||
BxLogger.info(LOG_TAG, 'Initialize');
|
||||
this.#$video = $video;
|
||||
this.$video = $video;
|
||||
|
||||
const $canvas = document.createElement('canvas');
|
||||
$canvas.width = $video.videoWidth;
|
||||
$canvas.height = $video.videoHeight;
|
||||
this.#$canvas = $canvas;
|
||||
this.$canvas = $canvas;
|
||||
|
||||
this.#setupShaders();
|
||||
this.#setupRendering();
|
||||
this.setupShaders();
|
||||
this.setupRendering();
|
||||
|
||||
$video.insertAdjacentElement('afterend', $canvas);
|
||||
}
|
||||
|
||||
setFilter(filterId: number, update = true) {
|
||||
this.#options.filterId = filterId;
|
||||
this.options.filterId = filterId;
|
||||
update && this.updateCanvas();
|
||||
}
|
||||
|
||||
setSharpness(sharpness: number, update = true) {
|
||||
this.#options.sharpenFactor = sharpness;
|
||||
this.options.sharpenFactor = sharpness;
|
||||
update && this.updateCanvas();
|
||||
}
|
||||
|
||||
setBrightness(brightness: number, update = true) {
|
||||
this.#options.brightness = (brightness - 100) / 100;
|
||||
this.options.brightness = 1 + (brightness - 100) / 100;
|
||||
update && this.updateCanvas();
|
||||
}
|
||||
|
||||
setContrast(contrast: number, update = true) {
|
||||
this.#options.contrast = (contrast - 100) / 100;
|
||||
this.options.contrast = 1 + (contrast - 100) / 100;
|
||||
update && this.updateCanvas();
|
||||
}
|
||||
|
||||
setSaturation(saturation: number, update = true) {
|
||||
this.#options.saturation = (saturation - 100) / 100;
|
||||
this.options.saturation = 1 + (saturation - 100) / 100;
|
||||
update && this.updateCanvas();
|
||||
}
|
||||
|
||||
getCanvas() {
|
||||
return this.#$canvas;
|
||||
return this.$canvas;
|
||||
}
|
||||
|
||||
updateCanvas() {
|
||||
const gl = this.#gl!;
|
||||
const program = this.#program!;
|
||||
const gl = this.gl!;
|
||||
const program = this.program!;
|
||||
|
||||
gl.uniform2f(gl.getUniformLocation(program, 'iResolution'), this.#$canvas.width, this.#$canvas.height);
|
||||
gl.uniform2f(gl.getUniformLocation(program, 'iResolution'), this.$canvas.width, this.$canvas.height);
|
||||
|
||||
gl.uniform1i(gl.getUniformLocation(program, 'filterId'), this.#options.filterId);
|
||||
gl.uniform1f(gl.getUniformLocation(program, 'sharpenFactor'), this.#options.sharpenFactor);
|
||||
gl.uniform1f(gl.getUniformLocation(program, 'brightness'), this.#options.brightness);
|
||||
gl.uniform1f(gl.getUniformLocation(program, 'contrast'), this.#options.contrast);
|
||||
gl.uniform1f(gl.getUniformLocation(program, 'saturation'), this.#options.saturation);
|
||||
gl.uniform1i(gl.getUniformLocation(program, 'filterId'), this.options.filterId);
|
||||
gl.uniform1f(gl.getUniformLocation(program, 'sharpenFactor'), this.options.sharpenFactor);
|
||||
gl.uniform1f(gl.getUniformLocation(program, 'brightness'), this.options.brightness);
|
||||
gl.uniform1f(gl.getUniformLocation(program, 'contrast'), this.options.contrast);
|
||||
gl.uniform1f(gl.getUniformLocation(program, 'saturation'), this.options.saturation);
|
||||
}
|
||||
|
||||
drawFrame() {
|
||||
const gl = this.#gl!;
|
||||
const $video = this.#$video;
|
||||
const gl = this.gl!;
|
||||
const $video = this.$video;
|
||||
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, $video);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
#setupRendering() {
|
||||
private setupRendering() {
|
||||
let animate: any;
|
||||
|
||||
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
|
||||
const $video = this.#$video;
|
||||
const $video = this.$video;
|
||||
animate = () => {
|
||||
if (this.#stopped) {
|
||||
if (this.stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.drawFrame();
|
||||
this.#animFrameId = $video.requestVideoFrameCallback(animate);
|
||||
this.animFrameId = $video.requestVideoFrameCallback(animate);
|
||||
}
|
||||
|
||||
this.#animFrameId = $video.requestVideoFrameCallback(animate);
|
||||
this.animFrameId = $video.requestVideoFrameCallback(animate);
|
||||
} else {
|
||||
animate = () => {
|
||||
if (this.#stopped) {
|
||||
if (this.stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.drawFrame();
|
||||
this.#animFrameId = requestAnimationFrame(animate);
|
||||
this.animFrameId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
this.#animFrameId = requestAnimationFrame(animate);
|
||||
this.animFrameId = requestAnimationFrame(animate);
|
||||
}
|
||||
}
|
||||
|
||||
#setupShaders() {
|
||||
private setupShaders() {
|
||||
BxLogger.info(LOG_TAG, 'Setting up', getPref(PrefKey.VIDEO_POWER_PREFERENCE));
|
||||
|
||||
const gl = this.#$canvas.getContext('webgl', {
|
||||
const gl = this.$canvas.getContext('webgl', {
|
||||
isBx: true,
|
||||
antialias: true,
|
||||
alpha: false,
|
||||
powerPreference: getPref(PrefKey.VIDEO_POWER_PREFERENCE),
|
||||
}) as WebGL2RenderingContext;
|
||||
this.#gl = gl;
|
||||
this.gl = gl;
|
||||
|
||||
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);
|
||||
|
||||
@ -145,7 +145,7 @@ export class WebGL2Player {
|
||||
|
||||
// Create and link program
|
||||
const program = gl.createProgram()!;
|
||||
this.#program = program;
|
||||
this.program = program;
|
||||
|
||||
gl.attachShader(program, vShader);
|
||||
gl.attachShader(program, fShader);
|
||||
@ -162,7 +162,7 @@ export class WebGL2Player {
|
||||
|
||||
// Vertices: A screen-filling quad made from two triangles
|
||||
const buffer = gl.createBuffer();
|
||||
this.#resources.push(buffer);
|
||||
this.resources.push(buffer);
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
||||
@ -179,7 +179,7 @@ export class WebGL2Player {
|
||||
|
||||
// Texture to contain the video data
|
||||
const texture = gl.createTexture();
|
||||
this.#resources.push(texture);
|
||||
this.resources.push(texture);
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
||||
@ -197,26 +197,26 @@ export class WebGL2Player {
|
||||
|
||||
resume() {
|
||||
this.stop();
|
||||
this.#stopped = false;
|
||||
this.stopped = false;
|
||||
BxLogger.info(LOG_TAG, 'Resume');
|
||||
|
||||
this.#$canvas.classList.remove('bx-gone');
|
||||
this.#setupRendering();
|
||||
this.$canvas.classList.remove('bx-gone');
|
||||
this.setupRendering();
|
||||
}
|
||||
|
||||
stop() {
|
||||
BxLogger.info(LOG_TAG, 'Stop');
|
||||
this.#$canvas.classList.add('bx-gone');
|
||||
this.$canvas.classList.add('bx-gone');
|
||||
|
||||
this.#stopped = true;
|
||||
if (this.#animFrameId) {
|
||||
this.stopped = true;
|
||||
if (this.animFrameId) {
|
||||
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
|
||||
this.#$video.cancelVideoFrameCallback(this.#animFrameId);
|
||||
this.$video.cancelVideoFrameCallback(this.animFrameId);
|
||||
} else {
|
||||
cancelAnimationFrame(this.#animFrameId);
|
||||
cancelAnimationFrame(this.animFrameId);
|
||||
}
|
||||
|
||||
this.#animFrameId = null;
|
||||
this.animFrameId = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -224,11 +224,11 @@ export class WebGL2Player {
|
||||
BxLogger.info(LOG_TAG, 'Destroy');
|
||||
this.stop();
|
||||
|
||||
const gl = this.#gl;
|
||||
const gl = this.gl;
|
||||
if (gl) {
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
|
||||
for (const resource of this.#resources) {
|
||||
for (const resource of this.resources) {
|
||||
if (resource instanceof WebGLProgram) {
|
||||
gl.useProgram(null);
|
||||
gl.deleteProgram(resource);
|
||||
@ -241,14 +241,14 @@ export class WebGL2Player {
|
||||
}
|
||||
}
|
||||
|
||||
this.#gl = null;
|
||||
this.gl = null;
|
||||
}
|
||||
|
||||
if (this.#$canvas.isConnected) {
|
||||
this.#$canvas.parentElement?.removeChild(this.#$canvas);
|
||||
if (this.$canvas.isConnected) {
|
||||
this.$canvas.parentElement?.removeChild(this.$canvas);
|
||||
}
|
||||
|
||||
this.#$canvas.width = 1;
|
||||
this.#$canvas.height = 1;
|
||||
this.$canvas.width = 1;
|
||||
this.$canvas.height = 1;
|
||||
}
|
||||
}
|
||||
|
@ -17,35 +17,35 @@ export type StreamPlayerOptions = Partial<{
|
||||
}>;
|
||||
|
||||
export class StreamPlayer {
|
||||
#$video: HTMLVideoElement;
|
||||
#playerType: StreamPlayerType = StreamPlayerType.VIDEO;
|
||||
private $video: HTMLVideoElement;
|
||||
private playerType: StreamPlayerType = StreamPlayerType.VIDEO;
|
||||
|
||||
#options: StreamPlayerOptions = {};
|
||||
private options: StreamPlayerOptions = {};
|
||||
|
||||
#webGL2Player: WebGL2Player | null = null;
|
||||
private webGL2Player: WebGL2Player | null = null;
|
||||
|
||||
#$videoCss: HTMLStyleElement | null = null;
|
||||
#$usmMatrix: SVGFEConvolveMatrixElement | null = null;
|
||||
private $videoCss: HTMLStyleElement | null = null;
|
||||
private $usmMatrix: SVGFEConvolveMatrixElement | null = null;
|
||||
|
||||
constructor($video: HTMLVideoElement, type: StreamPlayerType, options: StreamPlayerOptions) {
|
||||
this.#setupVideoElements();
|
||||
this.setupVideoElements();
|
||||
|
||||
this.#$video = $video;
|
||||
this.#options = options || {};
|
||||
this.$video = $video;
|
||||
this.options = options || {};
|
||||
this.setPlayerType(type);
|
||||
}
|
||||
|
||||
#setupVideoElements() {
|
||||
this.#$videoCss = document.getElementById('bx-video-css') as HTMLStyleElement;
|
||||
if (this.#$videoCss) {
|
||||
this.#$usmMatrix = this.#$videoCss.querySelector('#bx-filter-usm-matrix') as any;
|
||||
private setupVideoElements() {
|
||||
this.$videoCss = document.getElementById('bx-video-css') as HTMLStyleElement;
|
||||
if (this.$videoCss) {
|
||||
this.$usmMatrix = this.$videoCss.querySelector('#bx-filter-usm-matrix') as any;
|
||||
return;
|
||||
}
|
||||
|
||||
const $fragment = document.createDocumentFragment();
|
||||
|
||||
this.#$videoCss = CE<HTMLStyleElement>('style', {id: 'bx-video-css'});
|
||||
$fragment.appendChild(this.#$videoCss);
|
||||
this.$videoCss = CE<HTMLStyleElement>('style', {id: 'bx-video-css'});
|
||||
$fragment.appendChild(this.$videoCss);
|
||||
|
||||
// Setup SVG filters
|
||||
const $svg = CE('svg', {
|
||||
@ -56,7 +56,7 @@ export class StreamPlayer {
|
||||
CE('filter', {
|
||||
id: 'bx-filter-usm',
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
}, this.#$usmMatrix = CE('feConvolveMatrix', {
|
||||
}, this.$usmMatrix = CE('feConvolveMatrix', {
|
||||
id: 'bx-filter-usm-matrix',
|
||||
order: '3',
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
@ -67,29 +67,29 @@ export class StreamPlayer {
|
||||
document.documentElement.appendChild($fragment);
|
||||
}
|
||||
|
||||
#getVideoPlayerFilterStyle() {
|
||||
private getVideoPlayerFilterStyle() {
|
||||
const filters = [];
|
||||
|
||||
const sharpness = this.#options.sharpness || 0;
|
||||
if (this.#options.processing === StreamVideoProcessing.USM && sharpness != 0) {
|
||||
const sharpness = this.options.sharpness || 0;
|
||||
if (this.options.processing === StreamVideoProcessing.USM && sharpness != 0) {
|
||||
const level = (7 - ((sharpness / 2) - 1) * 0.5).toFixed(1); // 5, 5.5, 6, 6.5, 7
|
||||
const matrix = `0 -1 0 -1 ${level} -1 0 -1 0`;
|
||||
this.#$usmMatrix?.setAttributeNS(null, 'kernelMatrix', matrix);
|
||||
this.$usmMatrix?.setAttributeNS(null, 'kernelMatrix', matrix);
|
||||
|
||||
filters.push(`url(#bx-filter-usm)`);
|
||||
}
|
||||
|
||||
const saturation = this.#options.saturation || 100;
|
||||
const saturation = this.options.saturation || 100;
|
||||
if (saturation != 100) {
|
||||
filters.push(`saturate(${saturation}%)`);
|
||||
}
|
||||
|
||||
const contrast = this.#options.contrast || 100;
|
||||
const contrast = this.options.contrast || 100;
|
||||
if (contrast != 100) {
|
||||
filters.push(`contrast(${contrast}%)`);
|
||||
}
|
||||
|
||||
const brightness = this.#options.brightness || 100;
|
||||
const brightness = this.options.brightness || 100;
|
||||
if (brightness != 100) {
|
||||
filters.push(`brightness(${brightness}%)`);
|
||||
}
|
||||
@ -97,14 +97,14 @@ export class StreamPlayer {
|
||||
return filters.join(' ');
|
||||
}
|
||||
|
||||
#resizePlayer() {
|
||||
private resizePlayer() {
|
||||
const PREF_RATIO = getPref(PrefKey.VIDEO_RATIO);
|
||||
const $video = this.#$video;
|
||||
const $video = this.$video;
|
||||
const isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport;
|
||||
|
||||
let $webGL2Canvas;
|
||||
if (this.#playerType == StreamPlayerType.WEBGL2) {
|
||||
$webGL2Canvas = this.#webGL2Player?.getCanvas()!;
|
||||
if (this.playerType == StreamPlayerType.WEBGL2) {
|
||||
$webGL2Canvas = this.webGL2Player?.getCanvas()!;
|
||||
}
|
||||
|
||||
let targetWidth;
|
||||
@ -166,67 +166,67 @@ export class StreamPlayer {
|
||||
}
|
||||
|
||||
// Update video dimensions
|
||||
if (isNativeTouchGame && this.#playerType == StreamPlayerType.WEBGL2) {
|
||||
if (isNativeTouchGame && this.playerType == StreamPlayerType.WEBGL2) {
|
||||
window.BX_EXPOSED.streamSession.updateDimensions();
|
||||
}
|
||||
}
|
||||
|
||||
setPlayerType(type: StreamPlayerType, refreshPlayer: boolean = false) {
|
||||
if (this.#playerType !== type) {
|
||||
if (this.playerType !== type) {
|
||||
// Switch from Video -> WebGL2
|
||||
if (type === StreamPlayerType.WEBGL2) {
|
||||
// Initialize WebGL2 player
|
||||
if (!this.#webGL2Player) {
|
||||
this.#webGL2Player = new WebGL2Player(this.#$video);
|
||||
if (!this.webGL2Player) {
|
||||
this.webGL2Player = new WebGL2Player(this.$video);
|
||||
} else {
|
||||
this.#webGL2Player.resume();
|
||||
this.webGL2Player.resume();
|
||||
}
|
||||
|
||||
this.#$videoCss!.textContent = '';
|
||||
this.$videoCss!.textContent = '';
|
||||
|
||||
this.#$video.classList.add('bx-pixel');
|
||||
this.$video.classList.add('bx-pixel');
|
||||
} else {
|
||||
// Cleanup WebGL2 Player
|
||||
this.#webGL2Player?.stop();
|
||||
this.webGL2Player?.stop();
|
||||
|
||||
this.#$video.classList.remove('bx-pixel');
|
||||
this.$video.classList.remove('bx-pixel');
|
||||
}
|
||||
}
|
||||
|
||||
this.#playerType = type;
|
||||
this.playerType = type;
|
||||
refreshPlayer && this.refreshPlayer();
|
||||
}
|
||||
|
||||
setOptions(options: StreamPlayerOptions, refreshPlayer: boolean = false) {
|
||||
this.#options = options;
|
||||
this.options = options;
|
||||
refreshPlayer && this.refreshPlayer();
|
||||
}
|
||||
|
||||
updateOptions(options: StreamPlayerOptions, refreshPlayer: boolean = false) {
|
||||
this.#options = Object.assign(this.#options, options);
|
||||
this.options = Object.assign(this.options, options);
|
||||
refreshPlayer && this.refreshPlayer();
|
||||
}
|
||||
|
||||
getPlayerElement(playerType?: StreamPlayerType) {
|
||||
if (typeof playerType === 'undefined') {
|
||||
playerType = this.#playerType;
|
||||
playerType = this.playerType;
|
||||
}
|
||||
|
||||
if (playerType === StreamPlayerType.WEBGL2) {
|
||||
return this.#webGL2Player?.getCanvas();
|
||||
return this.webGL2Player?.getCanvas();
|
||||
}
|
||||
|
||||
return this.#$video;
|
||||
return this.$video;
|
||||
}
|
||||
|
||||
getWebGL2Player() {
|
||||
return this.#webGL2Player;
|
||||
return this.webGL2Player;
|
||||
}
|
||||
|
||||
refreshPlayer() {
|
||||
if (this.#playerType === StreamPlayerType.WEBGL2) {
|
||||
const options = this.#options;
|
||||
const webGL2Player = this.#webGL2Player!;
|
||||
if (this.playerType === StreamPlayerType.WEBGL2) {
|
||||
const options = this.options;
|
||||
const webGL2Player = this.webGL2Player!;
|
||||
|
||||
if (options.processing === StreamVideoProcessing.USM) {
|
||||
webGL2Player.setFilter(1);
|
||||
@ -241,7 +241,7 @@ export class StreamPlayer {
|
||||
webGL2Player.setContrast(options.contrast || 100);
|
||||
webGL2Player.setBrightness(options.brightness || 100);
|
||||
} else {
|
||||
let filters = this.#getVideoPlayerFilterStyle();
|
||||
let filters = this.getVideoPlayerFilterStyle();
|
||||
let videoCss = '';
|
||||
if (filters) {
|
||||
videoCss += `filter: ${filters} !important;`;
|
||||
@ -257,26 +257,26 @@ export class StreamPlayer {
|
||||
css = `#game-stream video { ${videoCss} }`;
|
||||
}
|
||||
|
||||
this.#$videoCss!.textContent = css;
|
||||
this.$videoCss!.textContent = css;
|
||||
}
|
||||
|
||||
this.#resizePlayer();
|
||||
this.resizePlayer();
|
||||
}
|
||||
|
||||
reloadPlayer() {
|
||||
this.#cleanUpWebGL2Player();
|
||||
this.cleanUpWebGL2Player();
|
||||
|
||||
this.#playerType = StreamPlayerType.VIDEO;
|
||||
this.playerType = StreamPlayerType.VIDEO;
|
||||
this.setPlayerType(StreamPlayerType.WEBGL2, false);
|
||||
}
|
||||
|
||||
#cleanUpWebGL2Player() {
|
||||
private cleanUpWebGL2Player() {
|
||||
// Clean up WebGL2 Player
|
||||
this.#webGL2Player?.destroy();
|
||||
this.#webGL2Player = null;
|
||||
this.webGL2Player?.destroy();
|
||||
this.webGL2Player = null;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#cleanUpWebGL2Player();
|
||||
this.cleanUpWebGL2Player();
|
||||
}
|
||||
}
|
||||
|
@ -345,7 +345,7 @@ export class StreamBadges {
|
||||
text += server.region;
|
||||
}
|
||||
|
||||
text += '@' + (server ? 'IPv6' : 'IPv4');
|
||||
text += '@' + (server.ipv6 ? 'IPv6' : 'IPv4');
|
||||
this.badges.server.$element = this.renderBadge(StreamBadge.SERVER, text);
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,10 @@ export class StreamStats {
|
||||
name: t('stat-ping'),
|
||||
$element: CE('span'),
|
||||
},
|
||||
[StreamStat.JITTER]: {
|
||||
name: t('jitter'),
|
||||
$element: CE('span'),
|
||||
},
|
||||
[StreamStat.FPS]: {
|
||||
name: t('stat-fps'),
|
||||
$element: CE('span'),
|
||||
@ -58,11 +62,11 @@ export class StreamStats {
|
||||
$element: CE('span'),
|
||||
},
|
||||
[StreamStat.DOWNLOAD]: {
|
||||
name: t('download'),
|
||||
name: t('downloaded'),
|
||||
$element: CE('span'),
|
||||
},
|
||||
[StreamStat.UPLOAD]: {
|
||||
name: t('upload'),
|
||||
name: t('uploaded'),
|
||||
$element: CE('span'),
|
||||
},
|
||||
};
|
||||
@ -179,10 +183,8 @@ export class StreamStats {
|
||||
$element.textContent = value.toString();
|
||||
|
||||
// Get stat's grade
|
||||
if (PREF_STATS_CONDITIONAL_FORMATTING) {
|
||||
if (statKey === StreamStat.PING || statKey === StreamStat.DECODE_TIME) {
|
||||
grade = (value as any).calculateGrade();
|
||||
}
|
||||
if (PREF_STATS_CONDITIONAL_FORMATTING && 'grades' in value) {
|
||||
grade = statsCollector.calculateGrade(value.current, value.grades);
|
||||
}
|
||||
|
||||
if ($element.dataset.grade !== grade) {
|
||||
|
@ -37,6 +37,7 @@ type SettingTabContentItem = Partial<{
|
||||
content: HTMLElement | (() => HTMLElement);
|
||||
options: {[key: string]: string};
|
||||
unsupported: boolean;
|
||||
unsupportedNote: string;
|
||||
onChange: (e: any, value: number) => void;
|
||||
onCreated: (setting: SettingTabContentItem, $control: any) => void;
|
||||
params: any;
|
||||
@ -46,8 +47,8 @@ type SettingTabContentItem = Partial<{
|
||||
type SettingTabContent = {
|
||||
group: 'general' | 'server' | 'stream' | 'game-bar' | 'co-op' | 'mkb' | 'touch-control' | 'loading-screen' | 'ui' | 'other' | 'advanced' | 'footer' | 'audio' | 'video' | 'controller' | 'native-mkb' | 'stats' | 'controller-shortcuts';
|
||||
label?: string;
|
||||
note?: string | Text | null;
|
||||
unsupported?: boolean;
|
||||
unsupportedNote?: string | Text | null;
|
||||
helpUrl?: string;
|
||||
content?: any;
|
||||
items?: Array<SettingTabContentItem | PrefKey | (($parent: HTMLElement) => void) | false>;
|
||||
@ -220,8 +221,14 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
||||
requiredVariants: 'full',
|
||||
group: 'mkb',
|
||||
label: t('mouse-and-keyboard'),
|
||||
unsupportedNote: !STATES.userAgent.capabilities.mkb ? CE('a', {
|
||||
href: 'https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657',
|
||||
target: '_blank',
|
||||
}, '⚠️ ' + t('browser-unsupported-feature')) : null,
|
||||
unsupported: !STATES.userAgent.capabilities.mkb,
|
||||
items: [
|
||||
PrefKey.NATIVE_MKB_ENABLED,
|
||||
PrefKey.GAME_MSFS2020_FORCE_NATIVE_MKB,
|
||||
PrefKey.MKB_ENABLED,
|
||||
PrefKey.MKB_HIDE_IDLE_CURSOR,
|
||||
],
|
||||
@ -229,8 +236,8 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
||||
requiredVariants: 'full',
|
||||
group: 'touch-control',
|
||||
label: t('touch-controller'),
|
||||
note: !STATES.userAgent.capabilities.touch ? '⚠️ ' + t('device-unsupported-touch') : null,
|
||||
unsupported: !STATES.userAgent.capabilities.touch,
|
||||
unsupportedNote: !STATES.userAgent.capabilities.touch ? '⚠️ ' + t('device-unsupported-touch') : null,
|
||||
items: [
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF,
|
||||
@ -516,17 +523,17 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
||||
requiredVariants: 'full',
|
||||
group: 'native-mkb',
|
||||
label: t('native-mkb'),
|
||||
items: [isFullVersion() && {
|
||||
items: isFullVersion() ? [{
|
||||
pref: PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY,
|
||||
onChange: (e: any, value: number) => {
|
||||
NativeMkbHandler.getInstance().setVerticalScrollMultiplier(value / 100);
|
||||
},
|
||||
}, isFullVersion() && {
|
||||
}, {
|
||||
pref: PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY,
|
||||
onChange: (e: any, value: number) => {
|
||||
NativeMkbHandler.getInstance().setHorizontalScrollMultiplier(value / 100);
|
||||
},
|
||||
}],
|
||||
}] : [],
|
||||
}];
|
||||
|
||||
private readonly TAB_SHORTCUTS_ITEMS: Array<SettingTabContent | false> = [{
|
||||
@ -1132,6 +1139,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
||||
|
||||
let label = prefDefinition?.label || setting.label;
|
||||
let note = prefDefinition?.note || setting.note;
|
||||
let unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote;
|
||||
const experimental = prefDefinition?.experimental || setting.experimental;
|
||||
|
||||
if (settingTabContent.label && setting.pref) {
|
||||
@ -1151,6 +1159,13 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
||||
}
|
||||
}
|
||||
|
||||
let $note;
|
||||
if (unsupportedNote) {
|
||||
$note = CE('div', {class: 'bx-settings-dialog-note'}, unsupportedNote);
|
||||
} else if (note) {
|
||||
$note = CE('div', {class: 'bx-settings-dialog-note'}, note);
|
||||
}
|
||||
|
||||
let $label;
|
||||
|
||||
const $row = CE('label', {
|
||||
@ -1163,7 +1178,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
||||
},
|
||||
$label = CE('span', {class: 'bx-settings-label'},
|
||||
label,
|
||||
note ? CE('div', {class: 'bx-settings-dialog-note'}, note) : prefDefinition?.unsupported && CE('div', {class: 'bx-settings-dialog-note'}, t('browser-unsupported-feature')),
|
||||
$note,
|
||||
),
|
||||
!prefDefinition?.unsupported && $control,
|
||||
);
|
||||
@ -1334,13 +1349,8 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
||||
}
|
||||
|
||||
// Add note
|
||||
if (settingTabContent.note) {
|
||||
let $note;
|
||||
if (typeof settingTabContent.note === 'string') {
|
||||
$note = CE('b', {class: 'bx-note-unsupported'}, settingTabContent.note);
|
||||
} else {
|
||||
$note = settingTabContent.note;
|
||||
}
|
||||
if (settingTabContent.unsupportedNote) {
|
||||
const $note = CE('b', {class: 'bx-note-unsupported'}, settingTabContent.unsupportedNote);
|
||||
|
||||
$tabContent.appendChild($note);
|
||||
}
|
||||
|
1
src/types/index.d.ts
vendored
1
src/types/index.d.ts
vendored
@ -46,6 +46,7 @@ type BxStates = {
|
||||
isTv: boolean;
|
||||
capabilities: {
|
||||
touch: boolean;
|
||||
mkb: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
|
3
src/types/preferences.d.ts
vendored
3
src/types/preferences.d.ts
vendored
@ -3,7 +3,8 @@ export type PreferenceSetting = {
|
||||
optionsGroup?: string;
|
||||
options?: {[index: string]: string};
|
||||
multipleOptions?: {[index: string]: string};
|
||||
unsupported?: string | boolean;
|
||||
unsupported?: boolean;
|
||||
unsupported_note?: string | HTMLElement;
|
||||
note?: string | HTMLElement;
|
||||
type?: SettingElementType;
|
||||
ready?: (setting: PreferenceSetting) => void;
|
||||
|
3
src/types/setting-definition.d.ts
vendored
3
src/types/setting-definition.d.ts
vendored
@ -20,7 +20,8 @@ export type SettingDefinition = {
|
||||
label: string;
|
||||
note: string | HTMLElement;
|
||||
experimental: boolean;
|
||||
unsupported: string | boolean;
|
||||
unsupported: boolean;
|
||||
unsupportedNote: string | HTMLElement;
|
||||
suggest: PartialRecord<SuggestedSettingCategory, any>,
|
||||
ready: (setting: SettingDefinition) => void;
|
||||
type: SettingElementType,
|
||||
|
@ -13,6 +13,7 @@ const isTv = userAgent.includes('smart-tv') || userAgent.includes('smarttv') ||
|
||||
const isVr = window.navigator.userAgent.includes('VR') && window.navigator.userAgent.includes('OculusBrowser');
|
||||
const browserHasTouchSupport = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
const userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport;
|
||||
const supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/);
|
||||
|
||||
export const STATES: BxStates = {
|
||||
supportedRegion: true,
|
||||
@ -35,6 +36,7 @@ export const STATES: BxStates = {
|
||||
isTv: isTv,
|
||||
capabilities: {
|
||||
touch: userAgentHasTouchSupport,
|
||||
mkb: supportMkb,
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -182,7 +182,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
|
||||
if (keys.length <= 1) { // Unsupported
|
||||
setting.unsupported = true;
|
||||
setting.note = '⚠️ ' + t('browser-unsupported-feature');
|
||||
setting.unsupportedNote = '⚠️ ' + t('browser-unsupported-feature');
|
||||
}
|
||||
|
||||
setting.suggest = {
|
||||
@ -393,10 +393,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
requiredVariants: 'full',
|
||||
label: t('enable-mkb'),
|
||||
default: false,
|
||||
unsupported: ((): string | boolean => {
|
||||
const userAgent = ((window.navigator as any).orgUserAgent || window.navigator.userAgent || '').toLowerCase();
|
||||
return !AppInterface && userAgent.match(/(android|iphone|ipad)/) ? t('browser-unsupported-feature') : false;
|
||||
})(),
|
||||
unsupported: !STATES.userAgent.capabilities.mkb,
|
||||
ready: (setting: SettingDefinition) => {
|
||||
let note;
|
||||
let url;
|
||||
@ -408,7 +405,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
url = 'https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer';
|
||||
}
|
||||
|
||||
setting.note = CE('a', {
|
||||
setting.unsupportedNote = CE('a', {
|
||||
href: url,
|
||||
target: '_blank',
|
||||
}, '⚠️ ' + note);
|
||||
@ -634,7 +631,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
},
|
||||
suggest: {
|
||||
lowest: 0,
|
||||
highest: 4,
|
||||
highest: 2,
|
||||
},
|
||||
},
|
||||
[PrefKey.VIDEO_RATIO]: {
|
||||
@ -717,13 +714,14 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
[StreamStat.PLAYTIME]: `${StreamStat.PLAYTIME.toUpperCase()}: ${t('playtime')}`,
|
||||
[StreamStat.BATTERY]: `${StreamStat.BATTERY.toUpperCase()}: ${t('battery')}`,
|
||||
[StreamStat.PING]: `${StreamStat.PING.toUpperCase()}: ${t('stat-ping')}`,
|
||||
[StreamStat.JITTER]: `${StreamStat.JITTER.toUpperCase()}: ${t('jitter')}`,
|
||||
[StreamStat.FPS]: `${StreamStat.FPS.toUpperCase()}: ${t('stat-fps')}`,
|
||||
[StreamStat.BITRATE]: `${StreamStat.BITRATE.toUpperCase()}: ${t('stat-bitrate')}`,
|
||||
[StreamStat.DECODE_TIME]: `${StreamStat.DECODE_TIME.toUpperCase()}: ${t('stat-decode-time')}`,
|
||||
[StreamStat.PACKETS_LOST]: `${StreamStat.PACKETS_LOST.toUpperCase()}: ${t('stat-packets-lost')}`,
|
||||
[StreamStat.FRAMES_LOST]: `${StreamStat.FRAMES_LOST.toUpperCase()}: ${t('stat-frames-lost')}`,
|
||||
[StreamStat.DOWNLOAD]: `${StreamStat.DOWNLOAD.toUpperCase()}: ${t('download')}`,
|
||||
[StreamStat.UPLOAD]: `${StreamStat.UPLOAD.toUpperCase()}: ${t('upload')}`,
|
||||
[StreamStat.DOWNLOAD]: `${StreamStat.DOWNLOAD.toUpperCase()}: ${t('downloaded')}`,
|
||||
[StreamStat.UPLOAD]: `${StreamStat.UPLOAD.toUpperCase()}: ${t('uploaded')}`,
|
||||
},
|
||||
params: {
|
||||
size: 6,
|
||||
@ -804,6 +802,13 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
default: false,
|
||||
note: t('fortnite-allow-stw-mode'),
|
||||
},
|
||||
|
||||
[PrefKey.GAME_MSFS2020_FORCE_NATIVE_MKB]: {
|
||||
requiredVariants: 'full',
|
||||
label: '✈️ ' + t('msfs2020-force-native-mkb'),
|
||||
default: false,
|
||||
note: t('may-not-work-properly'),
|
||||
},
|
||||
} satisfies SettingDefinitions;
|
||||
|
||||
constructor() {
|
||||
|
@ -4,6 +4,7 @@ import { humanFileSize, secondsToHm } from "./html";
|
||||
|
||||
export enum StreamStat {
|
||||
PING = 'ping',
|
||||
JITTER = 'jit',
|
||||
FPS = 'fps',
|
||||
BITRATE = 'btr',
|
||||
DECODE_TIME = 'dt',
|
||||
@ -21,7 +22,13 @@ export type StreamStatGrade = '' | 'bad' | 'ok' | 'good';
|
||||
type CurrentStats = {
|
||||
[StreamStat.PING]: {
|
||||
current: number;
|
||||
calculateGrade: () => StreamStatGrade;
|
||||
grades: [number, number, number];
|
||||
toString: () => string;
|
||||
};
|
||||
|
||||
[StreamStat.JITTER]: {
|
||||
current: number;
|
||||
grades: [number, number, number];
|
||||
toString: () => string;
|
||||
};
|
||||
|
||||
@ -50,7 +57,7 @@ type CurrentStats = {
|
||||
[StreamStat.DECODE_TIME]: {
|
||||
current: number;
|
||||
total: number;
|
||||
calculateGrade: () => StreamStatGrade;
|
||||
grades: [number, number, number];
|
||||
toString: () => string;
|
||||
};
|
||||
|
||||
@ -96,17 +103,27 @@ export class StreamStatsCollector {
|
||||
// Collect in background - 60 seconds
|
||||
static readonly INTERVAL_BACKGROUND = 60 * 1000;
|
||||
|
||||
public calculateGrade(value: number, grades: [number, number, number]): StreamStatGrade {
|
||||
return (value > grades[2]) ? 'bad' : (value > grades[1]) ? 'ok' : (value > grades[0]) ? 'good' : '';
|
||||
}
|
||||
|
||||
private currentStats: CurrentStats = {
|
||||
[StreamStat.PING]: {
|
||||
current: -1,
|
||||
calculateGrade() {
|
||||
return (this.current >= 100) ? 'bad' : (this.current > 75) ? 'ok' : (this.current > 40) ? 'good' : '';
|
||||
},
|
||||
grades: [40, 75, 100],
|
||||
toString() {
|
||||
return this.current === -1 ? '???' : this.current.toString();
|
||||
},
|
||||
},
|
||||
|
||||
[StreamStat.JITTER]: {
|
||||
current: 0,
|
||||
grades: [30, 40, 60],
|
||||
toString() {
|
||||
return `${this.current.toFixed(2)}ms`;
|
||||
},
|
||||
},
|
||||
|
||||
[StreamStat.FPS]: {
|
||||
current: 0,
|
||||
toString() {
|
||||
@ -142,9 +159,7 @@ export class StreamStatsCollector {
|
||||
[StreamStat.DECODE_TIME]: {
|
||||
current: 0,
|
||||
total: 0,
|
||||
calculateGrade() {
|
||||
return (this.current > 12) ? 'bad' : (this.current > 9) ? 'ok' : (this.current > 6) ? 'good' : '';
|
||||
},
|
||||
grades: [6, 9, 12],
|
||||
toString() {
|
||||
return isNaN(this.current) ? '??ms' : `${this.current.toFixed(2)}ms`;
|
||||
},
|
||||
@ -200,12 +215,15 @@ export class StreamStatsCollector {
|
||||
},
|
||||
};
|
||||
|
||||
private lastVideoStat?: RTCBasicStat | null;
|
||||
private lastVideoStat?: RTCInboundRtpStreamStats | null;
|
||||
|
||||
async collect() {
|
||||
const stats = await STATES.currentStream.peerConnection?.getStats();
|
||||
if (!stats) {
|
||||
return;
|
||||
}
|
||||
|
||||
stats?.forEach(stat => {
|
||||
stats.forEach(stat => {
|
||||
if (stat.type === 'inbound-rtp' && stat.kind === 'video') {
|
||||
// FPS
|
||||
const fps = this.currentStats[StreamStat.FPS];
|
||||
@ -229,15 +247,23 @@ export class StreamStatsCollector {
|
||||
|
||||
const lastStat = this.lastVideoStat;
|
||||
|
||||
// Jitter
|
||||
const jit = this.currentStats[StreamStat.JITTER];
|
||||
const bufferDelayDiff = (stat as RTCInboundRtpStreamStats).jitterBufferDelay! - lastStat.jitterBufferDelay!;
|
||||
const emittedCountDiff = (stat as RTCInboundRtpStreamStats).jitterBufferEmittedCount! - lastStat.jitterBufferEmittedCount!;
|
||||
if (emittedCountDiff > 0) {
|
||||
jit.current = bufferDelayDiff / emittedCountDiff * 1000;
|
||||
}
|
||||
|
||||
// Bitrate
|
||||
const btr = this.currentStats[StreamStat.BITRATE];
|
||||
const timeDiff = stat.timestamp - lastStat.timestamp;
|
||||
btr.current = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000;
|
||||
btr.current = 8 * (stat.bytesReceived - lastStat.bytesReceived!) / timeDiff / 1000;
|
||||
|
||||
// Decode time
|
||||
const dt = this.currentStats[StreamStat.DECODE_TIME];
|
||||
dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime;
|
||||
const framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded;
|
||||
dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime!;
|
||||
const framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded!;
|
||||
dt.current = dt.total / framesDecodedDiff * 1000;
|
||||
|
||||
this.lastVideoStat = stat;
|
||||
|
@ -93,10 +93,11 @@ const Texts = {
|
||||
"disabled": "Disabled",
|
||||
"disconnected": "Disconnected",
|
||||
"download": "Download",
|
||||
"downloaded": "Downloaded",
|
||||
"edit": "Edit",
|
||||
"enable-controller-shortcuts": "Enable controller shortcuts",
|
||||
"enable-local-co-op-support": "Enable local co-op support",
|
||||
"enable-local-co-op-support-note": "Only works if the game doesn't require a different profile",
|
||||
"enable-local-co-op-support-note": "Only works with some games",
|
||||
"enable-mic-on-startup": "Enable microphone on game launch",
|
||||
"enable-mkb": "Emulate controller with Mouse & Keyboard",
|
||||
"enable-quick-glance-mode": "Enable \"Quick Glance\" mode",
|
||||
@ -106,7 +107,7 @@ const Texts = {
|
||||
"experimental": "Experimental",
|
||||
"export": "Export",
|
||||
"fast": "Fast",
|
||||
"fortnite-allow-stw-mode": "Allows playing STW mode on mobile",
|
||||
"fortnite-allow-stw-mode": "Allows playing \"Save the World\" mode on mobile",
|
||||
"fortnite-force-console-version": "Fortnite: force console version",
|
||||
"game-bar": "Game Bar",
|
||||
"getting-consoles-list": "Getting the list of consoles...",
|
||||
@ -130,6 +131,7 @@ const Texts = {
|
||||
"increase": "Increase",
|
||||
"install-android": "Better xCloud app for Android",
|
||||
"japan": "Japan",
|
||||
"jitter": "Jitter",
|
||||
"keyboard-shortcuts": "Keyboard shortcuts",
|
||||
"korea": "Korea",
|
||||
"language": "Language",
|
||||
@ -149,6 +151,7 @@ const Texts = {
|
||||
"mkb-disclaimer": "Using this feature when playing online could be viewed as cheating",
|
||||
"mouse-and-keyboard": "Mouse & Keyboard",
|
||||
"mouse-wheel": "Mouse wheel",
|
||||
"msfs2020-force-native-mkb": "MSFS2020: force native M&KB support",
|
||||
"muted": "Muted",
|
||||
"name": "Name",
|
||||
"native-mkb": "Native Mouse & Keyboard",
|
||||
@ -348,6 +351,7 @@ const Texts = {
|
||||
"unmuted": "Unmuted",
|
||||
"unsharp-masking": "Unsharp masking",
|
||||
"upload": "Upload",
|
||||
"uploaded": "Uploaded",
|
||||
"use-mouse-absolute-position": "Use mouse's absolute position",
|
||||
"use-this-at-your-own-risk": "Use this at your own risk",
|
||||
"user-agent-profile": "User-Agent profile",
|
||||
|
Reference in New Issue
Block a user