mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-07-08 23:31:43 +02:00
Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
2eea9ce8f5 | |||
27abab8473 | |||
0c34173815 | |||
0164423e45 | |||
71dcaf4b07 |
50
dist/better-xcloud.lite.user.js
vendored
50
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==
|
// ==UserScript==
|
||||||
// @name Better xCloud
|
// @name Better xCloud
|
||||||
// @namespace https://github.com/redphx
|
// @namespace https://github.com/redphx
|
||||||
// @version 5.8.2
|
// @version 5.8.3
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
50
dist/better-xcloud.user.js
vendored
50
dist/better-xcloud.user.js
vendored
File diff suppressed because one or more lines are too long
@ -5,7 +5,7 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
font-family: var(--bx-monospaced-font);
|
font-family: var(--bx-monospaced-font);
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +75,7 @@ export enum PrefKey {
|
|||||||
VIDEO_PLAYER_TYPE = 'video_player_type',
|
VIDEO_PLAYER_TYPE = 'video_player_type',
|
||||||
VIDEO_PROCESSING = 'video_processing',
|
VIDEO_PROCESSING = 'video_processing',
|
||||||
VIDEO_POWER_PREFERENCE = 'video_power_preference',
|
VIDEO_POWER_PREFERENCE = 'video_power_preference',
|
||||||
|
VIDEO_MAX_FPS = 'video_max_fps',
|
||||||
VIDEO_SHARPNESS = 'video_sharpness',
|
VIDEO_SHARPNESS = 'video_sharpness',
|
||||||
VIDEO_RATIO = 'video_ratio',
|
VIDEO_RATIO = 'video_ratio',
|
||||||
VIDEO_BRIGHTNESS = 'video_brightness',
|
VIDEO_BRIGHTNESS = 'video_brightness',
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
|
#version 300 es
|
||||||
|
|
||||||
precision mediump float;
|
precision mediump float;
|
||||||
uniform sampler2D data;
|
uniform sampler2D data;
|
||||||
uniform vec2 iResolution;
|
uniform vec2 iResolution;
|
||||||
|
|
||||||
const int FILTER_UNSHARP_MASKING = 1;
|
const int FILTER_UNSHARP_MASKING = 1;
|
||||||
const int FILTER_CAS = 2;
|
// const int FILTER_CAS = 2;
|
||||||
|
|
||||||
// constrast = 0.8
|
// constrast = 0.8
|
||||||
const float CAS_CONTRAST_PEAK = (-3.0 * 0.8 + 8.0);
|
const float CAS_CONTRAST_PEAK = 0.8 * -3.0 + 8.0;
|
||||||
|
|
||||||
// Luminosity factor
|
// Luminosity factor
|
||||||
const vec3 LUMINOSITY_FACTOR = vec3(0.2126, 0.7152, 0.0722);
|
const vec3 LUMINOSITY_FACTOR = vec3(0.2126, 0.7152, 0.0722);
|
||||||
@ -17,96 +19,82 @@ uniform float brightness;
|
|||||||
uniform float contrast;
|
uniform float contrast;
|
||||||
uniform float saturation;
|
uniform float saturation;
|
||||||
|
|
||||||
vec3 clarityBoost(sampler2D tex, vec2 coord) {
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
vec3 clarityBoost(sampler2D tex, vec2 coord, vec3 e) {
|
||||||
vec2 texelSize = 1.0 / iResolution.xy;
|
vec2 texelSize = 1.0 / iResolution.xy;
|
||||||
|
|
||||||
// Load a collection of samples in a 3x3 neighorhood, where e is the current pixel.
|
// Load a collection of samples in a 3x3 neighorhood, where e is the current pixel.
|
||||||
// a b c
|
// a b c
|
||||||
// d e f
|
// d e f
|
||||||
// g h i
|
// g h i
|
||||||
vec3 a = texture2D(tex, coord + texelSize * vec2(-1, 1)).rgb;
|
vec3 a = texture(tex, coord + texelSize * vec2(-1, 1)).rgb;
|
||||||
vec3 b = texture2D(tex, coord + texelSize * vec2(0, 1)).rgb;
|
vec3 b = texture(tex, coord + texelSize * vec2(0, 1)).rgb;
|
||||||
vec3 c = texture2D(tex, coord + texelSize * vec2(1, 1)).rgb;
|
vec3 c = texture(tex, coord + texelSize * vec2(1, 1)).rgb;
|
||||||
|
|
||||||
vec3 d = texture2D(tex, coord + texelSize * vec2(-1, 0)).rgb;
|
vec3 d = texture(tex, coord + texelSize * vec2(-1, 0)).rgb;
|
||||||
vec3 e = texture2D(tex, coord).rgb;
|
vec3 f = texture(tex, coord + texelSize * vec2(1, 0)).rgb;
|
||||||
vec3 f = texture2D(tex, coord + texelSize * vec2(1, 0)).rgb;
|
|
||||||
|
|
||||||
vec3 g = texture2D(tex, coord + texelSize * vec2(-1, -1)).rgb;
|
vec3 g = texture(tex, coord + texelSize * vec2(-1, -1)).rgb;
|
||||||
vec3 h = texture2D(tex, coord + texelSize * vec2(0, -1)).rgb;
|
vec3 h = texture(tex, coord + texelSize * vec2(0, -1)).rgb;
|
||||||
vec3 i = texture2D(tex, coord + texelSize * vec2(1, -1)).rgb;
|
vec3 i = texture(tex, coord + texelSize * vec2(1, -1)).rgb;
|
||||||
|
|
||||||
if (filterId == FILTER_CAS) {
|
// USM
|
||||||
// Soft min and max.
|
if (filterId == FILTER_UNSHARP_MASKING) {
|
||||||
// a b c b
|
vec3 gaussianBlur = (a + c + g + i) * 1.0 + (b + d + f + h) * 2.0 + e * 4.0;
|
||||||
// d e f * 0.5 + d e f * 0.5
|
|
||||||
// g h i h
|
|
||||||
// These are 2.0x bigger (factored out the extra multiply).
|
|
||||||
vec3 minRgb = min(min(min(d, e), min(f, b)), h);
|
|
||||||
vec3 minRgb2 = min(min(a, c), min(g, i));
|
|
||||||
minRgb += min(minRgb, minRgb2);
|
|
||||||
|
|
||||||
vec3 maxRgb = max(max(max(d, e), max(f, b)), h);
|
|
||||||
vec3 maxRgb2 = max(max(a, c), max(g, i));
|
|
||||||
maxRgb += max(maxRgb, maxRgb2);
|
|
||||||
|
|
||||||
// Smooth minimum distance to signal limit divided by smooth max.
|
|
||||||
vec3 reciprocalMaxRgb = 1.0 / maxRgb;
|
|
||||||
vec3 amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, 0.0, 1.0);
|
|
||||||
|
|
||||||
// Shaping amount of sharpening.
|
|
||||||
amplifyRgb = inversesqrt(amplifyRgb);
|
|
||||||
|
|
||||||
vec3 weightRgb = -(1.0 / (amplifyRgb * CAS_CONTRAST_PEAK));
|
|
||||||
vec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);
|
|
||||||
|
|
||||||
// 0 w 0
|
|
||||||
// Filter shape: w 1 w
|
|
||||||
// 0 w 0
|
|
||||||
vec3 window = (b + d) + (f + h);
|
|
||||||
vec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0);
|
|
||||||
|
|
||||||
outColor = mix(e, outColor, sharpenFactor / 2.0);
|
|
||||||
|
|
||||||
return outColor;
|
|
||||||
} else if (filterId == FILTER_UNSHARP_MASKING) {
|
|
||||||
vec3 gaussianBlur = (a + c + g + i) * 1.0 +
|
|
||||||
(b + d + f + h) * 2.0 +
|
|
||||||
e * 4.0;
|
|
||||||
gaussianBlur /= 16.0;
|
gaussianBlur /= 16.0;
|
||||||
|
|
||||||
// Return edge detection
|
// Return edge detection
|
||||||
return e + (e - gaussianBlur) * sharpenFactor / 3.0;
|
return e + (e - gaussianBlur) * sharpenFactor / 3.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return e;
|
// CAS
|
||||||
|
// Soft min and max.
|
||||||
|
// a b c b
|
||||||
|
// d e f * 0.5 + d e f * 0.5
|
||||||
|
// g h i h
|
||||||
|
// These are 2.0x bigger (factored out the extra multiply).
|
||||||
|
vec3 minRgb = min(min(min(d, e), min(f, b)), h);
|
||||||
|
minRgb += min(min(a, c), min(g, i));
|
||||||
|
|
||||||
|
vec3 maxRgb = max(max(max(d, e), max(f, b)), h);
|
||||||
|
maxRgb += max(max(a, c), max(g, i));
|
||||||
|
|
||||||
|
// Smooth minimum distance to signal limit divided by smooth max.
|
||||||
|
vec3 reciprocalMaxRgb = 1.0 / maxRgb;
|
||||||
|
vec3 amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, 0.0, 1.0);
|
||||||
|
|
||||||
|
// Shaping amount of sharpening.
|
||||||
|
amplifyRgb = inversesqrt(amplifyRgb);
|
||||||
|
|
||||||
|
vec3 weightRgb = -(1.0 / (amplifyRgb * CAS_CONTRAST_PEAK));
|
||||||
|
vec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);
|
||||||
|
|
||||||
|
// 0 w 0
|
||||||
|
// Filter shape: w 1 w
|
||||||
|
// 0 w 0
|
||||||
|
vec3 window = b + d + f + h;
|
||||||
|
vec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0);
|
||||||
|
|
||||||
|
return mix(e, outColor, sharpenFactor / 2.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec3 color;
|
|
||||||
vec2 uv = gl_FragCoord.xy / iResolution.xy;
|
vec2 uv = gl_FragCoord.xy / iResolution.xy;
|
||||||
|
// Get current pixel
|
||||||
|
vec3 color = texture(data, uv).rgb;
|
||||||
|
|
||||||
if (sharpenFactor > 0.0) {
|
// Clarity boost
|
||||||
color = clarityBoost(data, uv);
|
color = sharpenFactor > 0.0 ? clarityBoost(data, uv, color) : color;
|
||||||
} else {
|
|
||||||
color = texture2D(data, uv).rgb;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Saturation
|
// Saturation
|
||||||
if (saturation != 1.0) {
|
color = saturation != 1.0 ? mix(vec3(dot(color, LUMINOSITY_FACTOR)), color, saturation) : color;
|
||||||
vec3 grayscale = vec3(dot(color, LUMINOSITY_FACTOR));
|
|
||||||
color = mix(grayscale, color, saturation);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contrast
|
// Contrast
|
||||||
if (contrast != 1.0) {
|
color = contrast * (color - 0.5) + 0.5;
|
||||||
color = 0.5 + contrast * (color - 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Brightness
|
// Brightness
|
||||||
if (brightness != 1.0) {
|
color = brightness * color;
|
||||||
color = brightness * color;
|
|
||||||
}
|
|
||||||
|
|
||||||
gl_FragColor = vec4(color, 1.0);
|
fragColor = vec4(color, 1.0);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
attribute vec4 position;
|
#version 300 es
|
||||||
|
|
||||||
|
in vec4 position;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
gl_Position = position;
|
gl_Position = position;
|
||||||
|
@ -25,6 +25,10 @@ export class WebGL2Player {
|
|||||||
saturation: 0.0,
|
saturation: 0.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private targetFps = 60;
|
||||||
|
private frameInterval = Math.ceil(1000 / this.targetFps);
|
||||||
|
private lastFrameTime = 0;
|
||||||
|
|
||||||
private animFrameId: number | null = null;
|
private animFrameId: number | null = null;
|
||||||
|
|
||||||
constructor($video: HTMLVideoElement) {
|
constructor($video: HTMLVideoElement) {
|
||||||
@ -67,6 +71,11 @@ export class WebGL2Player {
|
|||||||
update && this.updateCanvas();
|
update && this.updateCanvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTargetFps(target: number) {
|
||||||
|
this.targetFps = target;
|
||||||
|
this.frameInterval = Math.ceil(1000 / target);
|
||||||
|
}
|
||||||
|
|
||||||
getCanvas() {
|
getCanvas() {
|
||||||
return this.$canvas;
|
return this.$canvas;
|
||||||
}
|
}
|
||||||
@ -85,6 +94,16 @@ export class WebGL2Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
drawFrame() {
|
drawFrame() {
|
||||||
|
// Limit FPS
|
||||||
|
if (this.targetFps < 60) {
|
||||||
|
const currentTime = performance.now();
|
||||||
|
const timeSinceLastFrame = currentTime - this.lastFrameTime;
|
||||||
|
if (timeSinceLastFrame < this.frameInterval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastFrameTime = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
const gl = this.gl!;
|
const gl = this.gl!;
|
||||||
const $video = this.$video;
|
const $video = this.$video;
|
||||||
|
|
||||||
@ -124,7 +143,7 @@ export class WebGL2Player {
|
|||||||
private setupShaders() {
|
private setupShaders() {
|
||||||
BxLogger.info(LOG_TAG, 'Setting up', getPref(PrefKey.VIDEO_POWER_PREFERENCE));
|
BxLogger.info(LOG_TAG, 'Setting up', getPref(PrefKey.VIDEO_POWER_PREFERENCE));
|
||||||
|
|
||||||
const gl = this.$canvas.getContext('webgl', {
|
const gl = this.$canvas.getContext('webgl2', {
|
||||||
isBx: true,
|
isBx: true,
|
||||||
antialias: true,
|
antialias: true,
|
||||||
alpha: false,
|
alpha: false,
|
||||||
|
@ -7,9 +7,10 @@ import { getPref, setPref } from "@/utils/settings-storages/global-settings-stor
|
|||||||
|
|
||||||
export function onChangeVideoPlayerType() {
|
export function onChangeVideoPlayerType() {
|
||||||
const playerType = getPref(PrefKey.VIDEO_PLAYER_TYPE);
|
const playerType = getPref(PrefKey.VIDEO_PLAYER_TYPE);
|
||||||
const $videoProcessing = document.getElementById('bx_setting_video_processing') as HTMLSelectElement;
|
const $videoProcessing = document.getElementById(`bx_setting_${PrefKey.VIDEO_PROCESSING}`) as HTMLSelectElement;
|
||||||
const $videoSharpness = document.getElementById('bx_setting_video_sharpness') as HTMLElement;
|
const $videoSharpness = document.getElementById(`bx_setting_${PrefKey.VIDEO_SHARPNESS}`) as HTMLElement;
|
||||||
const $videoPowerPreference = document.getElementById('bx_setting_video_power_preference') as HTMLElement;
|
const $videoPowerPreference = document.getElementById(`bx_setting_${PrefKey.VIDEO_POWER_PREFERENCE}`) as HTMLElement;
|
||||||
|
const $videoMaxFps = document.getElementById(`bx_setting_${PrefKey.VIDEO_MAX_FPS}`) as HTMLElement;
|
||||||
|
|
||||||
if (!$videoProcessing) {
|
if (!$videoProcessing) {
|
||||||
return;
|
return;
|
||||||
@ -38,17 +39,27 @@ export function onChangeVideoPlayerType() {
|
|||||||
|
|
||||||
// Hide Power Preference setting if renderer isn't WebGL2
|
// Hide Power Preference setting if renderer isn't WebGL2
|
||||||
$videoPowerPreference.closest('.bx-settings-row')!.classList.toggle('bx-gone', playerType !== StreamPlayerType.WEBGL2);
|
$videoPowerPreference.closest('.bx-settings-row')!.classList.toggle('bx-gone', playerType !== StreamPlayerType.WEBGL2);
|
||||||
|
$videoMaxFps.closest('.bx-settings-row')!.classList.toggle('bx-gone', playerType !== StreamPlayerType.WEBGL2);
|
||||||
|
|
||||||
updateVideoPlayer();
|
updateVideoPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function limitVideoPlayerFps() {
|
||||||
|
const targetFps = getPref(PrefKey.VIDEO_MAX_FPS);
|
||||||
|
const streamPlayer = STATES.currentStream.streamPlayer;
|
||||||
|
streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function updateVideoPlayer() {
|
export function updateVideoPlayer() {
|
||||||
const streamPlayer = STATES.currentStream.streamPlayer;
|
const streamPlayer = STATES.currentStream.streamPlayer;
|
||||||
if (!streamPlayer) {
|
if (!streamPlayer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
limitVideoPlayerFps();
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
processing: getPref(PrefKey.VIDEO_PROCESSING),
|
processing: getPref(PrefKey.VIDEO_PROCESSING),
|
||||||
sharpness: getPref(PrefKey.VIDEO_SHARPNESS),
|
sharpness: getPref(PrefKey.VIDEO_SHARPNESS),
|
||||||
@ -60,6 +71,7 @@ export function updateVideoPlayer() {
|
|||||||
streamPlayer.setPlayerType(getPref(PrefKey.VIDEO_PLAYER_TYPE));
|
streamPlayer.setPlayerType(getPref(PrefKey.VIDEO_PLAYER_TYPE));
|
||||||
streamPlayer.updateOptions(options);
|
streamPlayer.updateOptions(options);
|
||||||
streamPlayer.refreshPlayer();
|
streamPlayer.refreshPlayer();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', updateVideoPlayer);
|
window.addEventListener('resize', updateVideoPlayer);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { isFullVersion } from "@macros/build" with {type: "macro"};
|
import { isFullVersion } from "@macros/build" with {type: "macro"};
|
||||||
|
|
||||||
import { onChangeVideoPlayerType, updateVideoPlayer } from "@/modules/stream/stream-settings-utils";
|
import { limitVideoPlayerFps, onChangeVideoPlayerType, updateVideoPlayer } from "@/modules/stream/stream-settings-utils";
|
||||||
import { ButtonStyle, CE, createButton, createSvgIcon, removeChildElements, type BxButton } from "@/utils/html";
|
import { ButtonStyle, CE, createButton, createSvgIcon, removeChildElements, type BxButton } from "@/utils/html";
|
||||||
import { NavigationDialog, NavigationDirection } from "./navigation-dialog";
|
import { NavigationDialog, NavigationDirection } from "./navigation-dialog";
|
||||||
import { ControllerShortcut } from "@/modules/controller-shortcut";
|
import { ControllerShortcut } from "@/modules/controller-shortcut";
|
||||||
@ -407,6 +407,9 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
items: [{
|
items: [{
|
||||||
pref: PrefKey.VIDEO_PLAYER_TYPE,
|
pref: PrefKey.VIDEO_PLAYER_TYPE,
|
||||||
onChange: onChangeVideoPlayerType,
|
onChange: onChangeVideoPlayerType,
|
||||||
|
}, {
|
||||||
|
pref: PrefKey.VIDEO_MAX_FPS,
|
||||||
|
onChange: limitVideoPlayerFps,
|
||||||
}, {
|
}, {
|
||||||
pref: PrefKey.VIDEO_POWER_PREFERENCE,
|
pref: PrefKey.VIDEO_POWER_PREFERENCE,
|
||||||
onChange: () => {
|
onChange: () => {
|
||||||
|
@ -616,6 +616,21 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
|||||||
highest: 'low-power',
|
highest: 'low-power',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[PrefKey.VIDEO_MAX_FPS]: {
|
||||||
|
label: t('max-fps'),
|
||||||
|
type: SettingElementType.NUMBER_STEPPER,
|
||||||
|
default: 60,
|
||||||
|
min: 10,
|
||||||
|
max: 60,
|
||||||
|
steps: 10,
|
||||||
|
params: {
|
||||||
|
exactTicks: 10,
|
||||||
|
customTextValue: (value: any) => {
|
||||||
|
value = parseInt(value);
|
||||||
|
return value === 60 ? t('unlimited') : value + 'fps';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
[PrefKey.VIDEO_SHARPNESS]: {
|
[PrefKey.VIDEO_SHARPNESS]: {
|
||||||
label: t('sharpness'),
|
label: t('sharpness'),
|
||||||
type: SettingElementType.NUMBER_STEPPER,
|
type: SettingElementType.NUMBER_STEPPER,
|
||||||
|
@ -143,6 +143,7 @@ const Texts = {
|
|||||||
"local-co-op": "Local co-op",
|
"local-co-op": "Local co-op",
|
||||||
"lowest-quality": "Lowest quality",
|
"lowest-quality": "Lowest quality",
|
||||||
"map-mouse-to": "Map mouse to",
|
"map-mouse-to": "Map mouse to",
|
||||||
|
"max-fps": "Max FPS",
|
||||||
"may-not-work-properly": "May not work properly!",
|
"may-not-work-properly": "May not work properly!",
|
||||||
"menu": "Menu",
|
"menu": "Menu",
|
||||||
"microphone": "Microphone",
|
"microphone": "Microphone",
|
||||||
|
Reference in New Issue
Block a user