mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-08-07 05:38:27 +02:00
Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2eea9ce8f5 | ||
![]() |
27abab8473 | ||
![]() |
0c34173815 | ||
![]() |
0164423e45 | ||
![]() |
71dcaf4b07 | ||
![]() |
8f49c48e74 | ||
![]() |
6fa1f73702 | ||
![]() |
728abced45 | ||
![]() |
411e43ceb0 | ||
![]() |
baa22dbefc | ||
![]() |
97fb7a114f | ||
![]() |
39b2f814b6 |
13
build.ts
13
build.ts
@@ -55,7 +55,7 @@ const postProcess = (str: string): string => {
|
|||||||
|
|
||||||
// Minify SVG import code
|
// Minify SVG import code
|
||||||
const svgMap = {}
|
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
|
// Remove new lines in SVG
|
||||||
p2 = p2.replaceAll(/\\n*\s*/g, '');
|
p2 = p2.replaceAll(/\\n*\s*/g, '');
|
||||||
|
|
||||||
@@ -76,6 +76,17 @@ const postProcess = (str: string): string => {
|
|||||||
// Remove blank lines
|
// Remove blank lines
|
||||||
str = str.replaceAll(/\n([\s]*)\n/g, "\n");
|
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('/* ADDITIONAL CODE */'));
|
||||||
assert(str.includes('window.BX_EXPOSED = BxExposed'));
|
assert(str.includes('window.BX_EXPOSED = BxExposed'));
|
||||||
assert(str.includes('window.BxEvent = BxEvent'));
|
assert(str.includes('window.BxEvent = BxEvent'));
|
||||||
|
284
dist/better-xcloud.lite.user.js
vendored
284
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.1
|
// @version 5.8.3
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
296
dist/better-xcloud.user.js
vendored
296
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -91,6 +91,7 @@ div[class^=StreamMenu-module__container] .bx-badges {
|
|||||||
&[data-stats*="[batt]"] > .bx-stat-batt,
|
&[data-stats*="[batt]"] > .bx-stat-batt,
|
||||||
&[data-stats*="[fps]"] > .bx-stat-fps,
|
&[data-stats*="[fps]"] > .bx-stat-fps,
|
||||||
&[data-stats*="[ping]"] > .bx-stat-ping,
|
&[data-stats*="[ping]"] > .bx-stat-ping,
|
||||||
|
&[data-stats*="[jit]"] > .bx-stat-jit,
|
||||||
&[data-stats*="[btr]"] > .bx-stat-btr,
|
&[data-stats*="[btr]"] > .bx-stat-btr,
|
||||||
&[data-stats*="[dt]"] > .bx-stat-dt,
|
&[data-stats*="[dt]"] > .bx-stat-dt,
|
||||||
&[data-stats*="[pl]"] > .bx-stat-pl,
|
&[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$="[batt]"] > .bx-stat-batt,
|
||||||
&[data-stats$="[fps]"] > .bx-stat-fps,
|
&[data-stats$="[fps]"] > .bx-stat-fps,
|
||||||
&[data-stats$="[ping]"] > .bx-stat-ping,
|
&[data-stats$="[ping]"] > .bx-stat-ping,
|
||||||
|
&[data-stats$="[jit]"] > .bx-stat-jit,
|
||||||
&[data-stats$="[btr]"] > .bx-stat-btr,
|
&[data-stats$="[btr]"] > .bx-stat-btr,
|
||||||
&[data-stats$="[dt]"] > .bx-stat-dt,
|
&[data-stats$="[dt]"] > .bx-stat-dt,
|
||||||
&[data-stats$="[pl]"] > .bx-stat-pl,
|
&[data-stats$="[pl]"] > .bx-stat-pl,
|
||||||
|
@@ -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',
|
||||||
|
@@ -211,7 +211,8 @@ const PATCHES = {
|
|||||||
|
|
||||||
// Block gamepad stats collecting
|
// Block gamepad stats collecting
|
||||||
if (getPref(PrefKey.BLOCK_TRACKING)) {
|
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
|
// Map the Share button on Xbox Series controller with the capturing screenshot feature
|
||||||
|
@@ -1,51 +1,64 @@
|
|||||||
const int FILTER_UNSHARP_MASKING = 1;
|
#version 300 es
|
||||||
const int FILTER_CAS = 2;
|
|
||||||
|
|
||||||
precision highp 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_CAS = 2;
|
||||||
|
|
||||||
|
// constrast = 0.8
|
||||||
|
const float CAS_CONTRAST_PEAK = 0.8 * -3.0 + 8.0;
|
||||||
|
|
||||||
|
// Luminosity factor
|
||||||
|
const vec3 LUMINOSITY_FACTOR = vec3(0.2126, 0.7152, 0.0722);
|
||||||
|
|
||||||
uniform int filterId;
|
uniform int filterId;
|
||||||
uniform float sharpenFactor;
|
uniform float sharpenFactor;
|
||||||
uniform float brightness;
|
uniform float brightness;
|
||||||
uniform float contrast;
|
uniform float contrast;
|
||||||
uniform float saturation;
|
uniform float saturation;
|
||||||
|
|
||||||
vec3 textureAt(sampler2D tex, vec2 coord) {
|
out vec4 fragColor;
|
||||||
return texture2D(tex, coord / iResolution.xy).rgb;
|
|
||||||
}
|
vec3 clarityBoost(sampler2D tex, vec2 coord, vec3 e) {
|
||||||
|
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.
|
// 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 = textureAt(tex, coord + vec2(-1, 1));
|
vec3 a = texture(tex, coord + texelSize * vec2(-1, 1)).rgb;
|
||||||
vec3 b = textureAt(tex, coord + vec2(0, 1));
|
vec3 b = texture(tex, coord + texelSize * vec2(0, 1)).rgb;
|
||||||
vec3 c = textureAt(tex, coord + vec2(1, 1));
|
vec3 c = texture(tex, coord + texelSize * vec2(1, 1)).rgb;
|
||||||
|
|
||||||
vec3 d = textureAt(tex, coord + vec2(-1, 0));
|
vec3 d = texture(tex, coord + texelSize * vec2(-1, 0)).rgb;
|
||||||
vec3 e = textureAt(tex, coord);
|
vec3 f = texture(tex, coord + texelSize * vec2(1, 0)).rgb;
|
||||||
vec3 f = textureAt(tex, coord + vec2(1, 0));
|
|
||||||
|
|
||||||
vec3 g = textureAt(tex, coord + vec2(-1, -1));
|
vec3 g = texture(tex, coord + texelSize * vec2(-1, -1)).rgb;
|
||||||
vec3 h = textureAt(tex, coord + vec2(0, -1));
|
vec3 h = texture(tex, coord + texelSize * vec2(0, -1)).rgb;
|
||||||
vec3 i = textureAt(tex, coord + vec2(1, -1));
|
vec3 i = texture(tex, coord + texelSize * vec2(1, -1)).rgb;
|
||||||
|
|
||||||
if (filterId == FILTER_CAS) {
|
// USM
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Return edge detection
|
||||||
|
return e + (e - gaussianBlur) * sharpenFactor / 3.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CAS
|
||||||
// Soft min and max.
|
// Soft min and max.
|
||||||
// a b c b
|
// a b c b
|
||||||
// d e f * 0.5 + d e f * 0.5
|
// d e f * 0.5 + d e f * 0.5
|
||||||
// g h i h
|
// g h i h
|
||||||
// These are 2.0x bigger (factored out the extra multiply).
|
// These are 2.0x bigger (factored out the extra multiply).
|
||||||
vec3 minRgb = min(min(min(d, e), min(f, b)), h);
|
vec3 minRgb = min(min(min(d, e), min(f, b)), h);
|
||||||
vec3 minRgb2 = min(min(a, c), min(g, i));
|
minRgb += min(min(a, c), min(g, i));
|
||||||
minRgb += min(minRgb, minRgb2);
|
|
||||||
|
|
||||||
vec3 maxRgb = max(max(max(d, e), max(f, b)), h);
|
vec3 maxRgb = max(max(max(d, e), max(f, b)), h);
|
||||||
vec3 maxRgb2 = max(max(a, c), max(g, i));
|
maxRgb += max(max(a, c), max(g, i));
|
||||||
maxRgb += max(maxRgb, maxRgb2);
|
|
||||||
|
|
||||||
// Smooth minimum distance to signal limit divided by smooth max.
|
// Smooth minimum distance to signal limit divided by smooth max.
|
||||||
vec3 reciprocalMaxRgb = 1.0 / maxRgb;
|
vec3 reciprocalMaxRgb = 1.0 / maxRgb;
|
||||||
@@ -54,68 +67,34 @@ vec3 clarityBoost(sampler2D tex, vec2 coord)
|
|||||||
// Shaping amount of sharpening.
|
// Shaping amount of sharpening.
|
||||||
amplifyRgb = inversesqrt(amplifyRgb);
|
amplifyRgb = inversesqrt(amplifyRgb);
|
||||||
|
|
||||||
float contrast = 0.8;
|
vec3 weightRgb = -(1.0 / (amplifyRgb * CAS_CONTRAST_PEAK));
|
||||||
float peak = -3.0 * contrast + 8.0;
|
|
||||||
vec3 weightRgb = -(1.0 / (amplifyRgb * peak));
|
|
||||||
|
|
||||||
vec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);
|
vec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);
|
||||||
|
|
||||||
// 0 w 0
|
// 0 w 0
|
||||||
// Filter shape: w 1 w
|
// Filter shape: w 1 w
|
||||||
// 0 w 0
|
// 0 w 0
|
||||||
vec3 window = (b + d) + (f + h);
|
vec3 window = b + d + f + h;
|
||||||
vec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0);
|
vec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0);
|
||||||
|
|
||||||
outColor = mix(e, outColor, sharpenFactor / 2.0);
|
return mix(e, outColor, sharpenFactor / 2.0);
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Return edge detection
|
|
||||||
return e + (e - gaussianBlur) * sharpenFactor / 3.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
void main() {
|
||||||
vec3 color;
|
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, gl_FragCoord.xy);
|
color = sharpenFactor > 0.0 ? clarityBoost(data, uv, color) : color;
|
||||||
} else {
|
|
||||||
color = textureAt(data, gl_FragCoord.xy);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (saturation != 0.0) {
|
// Saturation
|
||||||
color = adjustSaturation(color);
|
color = saturation != 1.0 ? mix(vec3(dot(color, LUMINOSITY_FACTOR)), color, saturation) : color;
|
||||||
}
|
|
||||||
|
|
||||||
if (contrast != 0.0) {
|
// Contrast
|
||||||
color = adjustContrast(color);
|
color = contrast * (color - 0.5) + 0.5;
|
||||||
}
|
|
||||||
|
|
||||||
if (brightness != 0.0) {
|
// Brightness
|
||||||
color = adjustBrightness(color);
|
color = brightness * color;
|
||||||
}
|
|
||||||
|
|
||||||
gl_FragColor = vec4(color, 1.0);
|
fragColor = vec4(color, 1.0);
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
attribute vec2 position;
|
#version 300 es
|
||||||
|
|
||||||
|
in vec4 position;
|
||||||
|
|
||||||
void main() {
|
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';
|
const LOG_TAG = 'WebGL2Player';
|
||||||
|
|
||||||
export class WebGL2Player {
|
export class WebGL2Player {
|
||||||
#$video: HTMLVideoElement;
|
private $video: HTMLVideoElement;
|
||||||
#$canvas: HTMLCanvasElement;
|
private $canvas: HTMLCanvasElement;
|
||||||
|
|
||||||
#gl: WebGL2RenderingContext | null = null;
|
private gl: WebGL2RenderingContext | null = null;
|
||||||
#resources: Array<any> = [];
|
private resources: Array<any> = [];
|
||||||
#program: WebGLProgram | null = null;
|
private program: WebGLProgram | null = null;
|
||||||
|
|
||||||
#stopped: boolean = false;
|
private stopped: boolean = false;
|
||||||
|
|
||||||
#options = {
|
private options = {
|
||||||
filterId: 1,
|
filterId: 1,
|
||||||
sharpenFactor: 0,
|
sharpenFactor: 0,
|
||||||
brightness: 0.0,
|
brightness: 0.0,
|
||||||
@@ -25,112 +25,131 @@ export class WebGL2Player {
|
|||||||
saturation: 0.0,
|
saturation: 0.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
#animFrameId: number | null = null;
|
private targetFps = 60;
|
||||||
|
private frameInterval = Math.ceil(1000 / this.targetFps);
|
||||||
|
private lastFrameTime = 0;
|
||||||
|
|
||||||
|
private animFrameId: number | null = null;
|
||||||
|
|
||||||
constructor($video: HTMLVideoElement) {
|
constructor($video: HTMLVideoElement) {
|
||||||
BxLogger.info(LOG_TAG, 'Initialize');
|
BxLogger.info(LOG_TAG, 'Initialize');
|
||||||
this.#$video = $video;
|
this.$video = $video;
|
||||||
|
|
||||||
const $canvas = document.createElement('canvas');
|
const $canvas = document.createElement('canvas');
|
||||||
$canvas.width = $video.videoWidth;
|
$canvas.width = $video.videoWidth;
|
||||||
$canvas.height = $video.videoHeight;
|
$canvas.height = $video.videoHeight;
|
||||||
this.#$canvas = $canvas;
|
this.$canvas = $canvas;
|
||||||
|
|
||||||
this.#setupShaders();
|
this.setupShaders();
|
||||||
this.#setupRendering();
|
this.setupRendering();
|
||||||
|
|
||||||
$video.insertAdjacentElement('afterend', $canvas);
|
$video.insertAdjacentElement('afterend', $canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
setFilter(filterId: number, update = true) {
|
setFilter(filterId: number, update = true) {
|
||||||
this.#options.filterId = filterId;
|
this.options.filterId = filterId;
|
||||||
update && this.updateCanvas();
|
update && this.updateCanvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
setSharpness(sharpness: number, update = true) {
|
setSharpness(sharpness: number, update = true) {
|
||||||
this.#options.sharpenFactor = sharpness;
|
this.options.sharpenFactor = sharpness;
|
||||||
update && this.updateCanvas();
|
update && this.updateCanvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
setBrightness(brightness: number, update = true) {
|
setBrightness(brightness: number, update = true) {
|
||||||
this.#options.brightness = (brightness - 100) / 100;
|
this.options.brightness = 1 + (brightness - 100) / 100;
|
||||||
update && this.updateCanvas();
|
update && this.updateCanvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
setContrast(contrast: number, update = true) {
|
setContrast(contrast: number, update = true) {
|
||||||
this.#options.contrast = (contrast - 100) / 100;
|
this.options.contrast = 1 + (contrast - 100) / 100;
|
||||||
update && this.updateCanvas();
|
update && this.updateCanvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
setSaturation(saturation: number, update = true) {
|
setSaturation(saturation: number, update = true) {
|
||||||
this.#options.saturation = (saturation - 100) / 100;
|
this.options.saturation = 1 + (saturation - 100) / 100;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCanvas() {
|
updateCanvas() {
|
||||||
const gl = this.#gl!;
|
const gl = this.gl!;
|
||||||
const program = this.#program!;
|
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.uniform1i(gl.getUniformLocation(program, 'filterId'), this.options.filterId);
|
||||||
gl.uniform1f(gl.getUniformLocation(program, 'sharpenFactor'), this.#options.sharpenFactor);
|
gl.uniform1f(gl.getUniformLocation(program, 'sharpenFactor'), this.options.sharpenFactor);
|
||||||
gl.uniform1f(gl.getUniformLocation(program, 'brightness'), this.#options.brightness);
|
gl.uniform1f(gl.getUniformLocation(program, 'brightness'), this.options.brightness);
|
||||||
gl.uniform1f(gl.getUniformLocation(program, 'contrast'), this.#options.contrast);
|
gl.uniform1f(gl.getUniformLocation(program, 'contrast'), this.options.contrast);
|
||||||
gl.uniform1f(gl.getUniformLocation(program, 'saturation'), this.#options.saturation);
|
gl.uniform1f(gl.getUniformLocation(program, 'saturation'), this.options.saturation);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawFrame() {
|
drawFrame() {
|
||||||
const gl = this.#gl!;
|
// Limit FPS
|
||||||
const $video = this.#$video;
|
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 $video = this.$video;
|
||||||
|
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, $video);
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, $video);
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
#setupRendering() {
|
private setupRendering() {
|
||||||
let animate: any;
|
let animate: any;
|
||||||
|
|
||||||
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
|
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
|
||||||
const $video = this.#$video;
|
const $video = this.$video;
|
||||||
animate = () => {
|
animate = () => {
|
||||||
if (this.#stopped) {
|
if (this.stopped) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.drawFrame();
|
this.drawFrame();
|
||||||
this.#animFrameId = $video.requestVideoFrameCallback(animate);
|
this.animFrameId = $video.requestVideoFrameCallback(animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#animFrameId = $video.requestVideoFrameCallback(animate);
|
this.animFrameId = $video.requestVideoFrameCallback(animate);
|
||||||
} else {
|
} else {
|
||||||
animate = () => {
|
animate = () => {
|
||||||
if (this.#stopped) {
|
if (this.stopped) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.drawFrame();
|
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));
|
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,
|
||||||
powerPreference: getPref(PrefKey.VIDEO_POWER_PREFERENCE),
|
powerPreference: getPref(PrefKey.VIDEO_POWER_PREFERENCE),
|
||||||
}) as WebGL2RenderingContext;
|
}) as WebGL2RenderingContext;
|
||||||
this.#gl = gl;
|
this.gl = gl;
|
||||||
|
|
||||||
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);
|
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);
|
||||||
|
|
||||||
@@ -145,7 +164,7 @@ export class WebGL2Player {
|
|||||||
|
|
||||||
// Create and link program
|
// Create and link program
|
||||||
const program = gl.createProgram()!;
|
const program = gl.createProgram()!;
|
||||||
this.#program = program;
|
this.program = program;
|
||||||
|
|
||||||
gl.attachShader(program, vShader);
|
gl.attachShader(program, vShader);
|
||||||
gl.attachShader(program, fShader);
|
gl.attachShader(program, fShader);
|
||||||
@@ -162,7 +181,7 @@ export class WebGL2Player {
|
|||||||
|
|
||||||
// Vertices: A screen-filling quad made from two triangles
|
// Vertices: A screen-filling quad made from two triangles
|
||||||
const buffer = gl.createBuffer();
|
const buffer = gl.createBuffer();
|
||||||
this.#resources.push(buffer);
|
this.resources.push(buffer);
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
||||||
@@ -179,7 +198,7 @@ export class WebGL2Player {
|
|||||||
|
|
||||||
// Texture to contain the video data
|
// Texture to contain the video data
|
||||||
const texture = gl.createTexture();
|
const texture = gl.createTexture();
|
||||||
this.#resources.push(texture);
|
this.resources.push(texture);
|
||||||
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
||||||
@@ -197,26 +216,26 @@ export class WebGL2Player {
|
|||||||
|
|
||||||
resume() {
|
resume() {
|
||||||
this.stop();
|
this.stop();
|
||||||
this.#stopped = false;
|
this.stopped = false;
|
||||||
BxLogger.info(LOG_TAG, 'Resume');
|
BxLogger.info(LOG_TAG, 'Resume');
|
||||||
|
|
||||||
this.#$canvas.classList.remove('bx-gone');
|
this.$canvas.classList.remove('bx-gone');
|
||||||
this.#setupRendering();
|
this.setupRendering();
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
BxLogger.info(LOG_TAG, 'Stop');
|
BxLogger.info(LOG_TAG, 'Stop');
|
||||||
this.#$canvas.classList.add('bx-gone');
|
this.$canvas.classList.add('bx-gone');
|
||||||
|
|
||||||
this.#stopped = true;
|
this.stopped = true;
|
||||||
if (this.#animFrameId) {
|
if (this.animFrameId) {
|
||||||
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
|
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
|
||||||
this.#$video.cancelVideoFrameCallback(this.#animFrameId);
|
this.$video.cancelVideoFrameCallback(this.animFrameId);
|
||||||
} else {
|
} else {
|
||||||
cancelAnimationFrame(this.#animFrameId);
|
cancelAnimationFrame(this.animFrameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#animFrameId = null;
|
this.animFrameId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,11 +243,11 @@ export class WebGL2Player {
|
|||||||
BxLogger.info(LOG_TAG, 'Destroy');
|
BxLogger.info(LOG_TAG, 'Destroy');
|
||||||
this.stop();
|
this.stop();
|
||||||
|
|
||||||
const gl = this.#gl;
|
const gl = this.gl;
|
||||||
if (gl) {
|
if (gl) {
|
||||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||||
|
|
||||||
for (const resource of this.#resources) {
|
for (const resource of this.resources) {
|
||||||
if (resource instanceof WebGLProgram) {
|
if (resource instanceof WebGLProgram) {
|
||||||
gl.useProgram(null);
|
gl.useProgram(null);
|
||||||
gl.deleteProgram(resource);
|
gl.deleteProgram(resource);
|
||||||
@@ -241,14 +260,14 @@ export class WebGL2Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#gl = null;
|
this.gl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.#$canvas.isConnected) {
|
if (this.$canvas.isConnected) {
|
||||||
this.#$canvas.parentElement?.removeChild(this.#$canvas);
|
this.$canvas.parentElement?.removeChild(this.$canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#$canvas.width = 1;
|
this.$canvas.width = 1;
|
||||||
this.#$canvas.height = 1;
|
this.$canvas.height = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,35 +17,35 @@ export type StreamPlayerOptions = Partial<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
export class StreamPlayer {
|
export class StreamPlayer {
|
||||||
#$video: HTMLVideoElement;
|
private $video: HTMLVideoElement;
|
||||||
#playerType: StreamPlayerType = StreamPlayerType.VIDEO;
|
private playerType: StreamPlayerType = StreamPlayerType.VIDEO;
|
||||||
|
|
||||||
#options: StreamPlayerOptions = {};
|
private options: StreamPlayerOptions = {};
|
||||||
|
|
||||||
#webGL2Player: WebGL2Player | null = null;
|
private webGL2Player: WebGL2Player | null = null;
|
||||||
|
|
||||||
#$videoCss: HTMLStyleElement | null = null;
|
private $videoCss: HTMLStyleElement | null = null;
|
||||||
#$usmMatrix: SVGFEConvolveMatrixElement | null = null;
|
private $usmMatrix: SVGFEConvolveMatrixElement | null = null;
|
||||||
|
|
||||||
constructor($video: HTMLVideoElement, type: StreamPlayerType, options: StreamPlayerOptions) {
|
constructor($video: HTMLVideoElement, type: StreamPlayerType, options: StreamPlayerOptions) {
|
||||||
this.#setupVideoElements();
|
this.setupVideoElements();
|
||||||
|
|
||||||
this.#$video = $video;
|
this.$video = $video;
|
||||||
this.#options = options || {};
|
this.options = options || {};
|
||||||
this.setPlayerType(type);
|
this.setPlayerType(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
#setupVideoElements() {
|
private setupVideoElements() {
|
||||||
this.#$videoCss = document.getElementById('bx-video-css') as HTMLStyleElement;
|
this.$videoCss = document.getElementById('bx-video-css') as HTMLStyleElement;
|
||||||
if (this.#$videoCss) {
|
if (this.$videoCss) {
|
||||||
this.#$usmMatrix = this.#$videoCss.querySelector('#bx-filter-usm-matrix') as any;
|
this.$usmMatrix = this.$videoCss.querySelector('#bx-filter-usm-matrix') as any;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const $fragment = document.createDocumentFragment();
|
const $fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
this.#$videoCss = CE<HTMLStyleElement>('style', {id: 'bx-video-css'});
|
this.$videoCss = CE<HTMLStyleElement>('style', {id: 'bx-video-css'});
|
||||||
$fragment.appendChild(this.#$videoCss);
|
$fragment.appendChild(this.$videoCss);
|
||||||
|
|
||||||
// Setup SVG filters
|
// Setup SVG filters
|
||||||
const $svg = CE('svg', {
|
const $svg = CE('svg', {
|
||||||
@@ -56,7 +56,7 @@ export class StreamPlayer {
|
|||||||
CE('filter', {
|
CE('filter', {
|
||||||
id: 'bx-filter-usm',
|
id: 'bx-filter-usm',
|
||||||
xmlns: 'http://www.w3.org/2000/svg',
|
xmlns: 'http://www.w3.org/2000/svg',
|
||||||
}, this.#$usmMatrix = CE('feConvolveMatrix', {
|
}, this.$usmMatrix = CE('feConvolveMatrix', {
|
||||||
id: 'bx-filter-usm-matrix',
|
id: 'bx-filter-usm-matrix',
|
||||||
order: '3',
|
order: '3',
|
||||||
xmlns: 'http://www.w3.org/2000/svg',
|
xmlns: 'http://www.w3.org/2000/svg',
|
||||||
@@ -67,29 +67,29 @@ export class StreamPlayer {
|
|||||||
document.documentElement.appendChild($fragment);
|
document.documentElement.appendChild($fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
#getVideoPlayerFilterStyle() {
|
private getVideoPlayerFilterStyle() {
|
||||||
const filters = [];
|
const filters = [];
|
||||||
|
|
||||||
const sharpness = this.#options.sharpness || 0;
|
const sharpness = this.options.sharpness || 0;
|
||||||
if (this.#options.processing === StreamVideoProcessing.USM && 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 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`;
|
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)`);
|
filters.push(`url(#bx-filter-usm)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const saturation = this.#options.saturation || 100;
|
const saturation = this.options.saturation || 100;
|
||||||
if (saturation != 100) {
|
if (saturation != 100) {
|
||||||
filters.push(`saturate(${saturation}%)`);
|
filters.push(`saturate(${saturation}%)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const contrast = this.#options.contrast || 100;
|
const contrast = this.options.contrast || 100;
|
||||||
if (contrast != 100) {
|
if (contrast != 100) {
|
||||||
filters.push(`contrast(${contrast}%)`);
|
filters.push(`contrast(${contrast}%)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const brightness = this.#options.brightness || 100;
|
const brightness = this.options.brightness || 100;
|
||||||
if (brightness != 100) {
|
if (brightness != 100) {
|
||||||
filters.push(`brightness(${brightness}%)`);
|
filters.push(`brightness(${brightness}%)`);
|
||||||
}
|
}
|
||||||
@@ -97,14 +97,14 @@ export class StreamPlayer {
|
|||||||
return filters.join(' ');
|
return filters.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
#resizePlayer() {
|
private resizePlayer() {
|
||||||
const PREF_RATIO = getPref(PrefKey.VIDEO_RATIO);
|
const PREF_RATIO = getPref(PrefKey.VIDEO_RATIO);
|
||||||
const $video = this.#$video;
|
const $video = this.$video;
|
||||||
const isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport;
|
const isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport;
|
||||||
|
|
||||||
let $webGL2Canvas;
|
let $webGL2Canvas;
|
||||||
if (this.#playerType == StreamPlayerType.WEBGL2) {
|
if (this.playerType == StreamPlayerType.WEBGL2) {
|
||||||
$webGL2Canvas = this.#webGL2Player?.getCanvas()!;
|
$webGL2Canvas = this.webGL2Player?.getCanvas()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetWidth;
|
let targetWidth;
|
||||||
@@ -166,67 +166,67 @@ export class StreamPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update video dimensions
|
// Update video dimensions
|
||||||
if (isNativeTouchGame && this.#playerType == StreamPlayerType.WEBGL2) {
|
if (isNativeTouchGame && this.playerType == StreamPlayerType.WEBGL2) {
|
||||||
window.BX_EXPOSED.streamSession.updateDimensions();
|
window.BX_EXPOSED.streamSession.updateDimensions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setPlayerType(type: StreamPlayerType, refreshPlayer: boolean = false) {
|
setPlayerType(type: StreamPlayerType, refreshPlayer: boolean = false) {
|
||||||
if (this.#playerType !== type) {
|
if (this.playerType !== type) {
|
||||||
// Switch from Video -> WebGL2
|
// Switch from Video -> WebGL2
|
||||||
if (type === StreamPlayerType.WEBGL2) {
|
if (type === StreamPlayerType.WEBGL2) {
|
||||||
// Initialize WebGL2 player
|
// Initialize WebGL2 player
|
||||||
if (!this.#webGL2Player) {
|
if (!this.webGL2Player) {
|
||||||
this.#webGL2Player = new WebGL2Player(this.#$video);
|
this.webGL2Player = new WebGL2Player(this.$video);
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
// Cleanup WebGL2 Player
|
// 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();
|
refreshPlayer && this.refreshPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
setOptions(options: StreamPlayerOptions, refreshPlayer: boolean = false) {
|
setOptions(options: StreamPlayerOptions, refreshPlayer: boolean = false) {
|
||||||
this.#options = options;
|
this.options = options;
|
||||||
refreshPlayer && this.refreshPlayer();
|
refreshPlayer && this.refreshPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOptions(options: StreamPlayerOptions, refreshPlayer: boolean = false) {
|
updateOptions(options: StreamPlayerOptions, refreshPlayer: boolean = false) {
|
||||||
this.#options = Object.assign(this.#options, options);
|
this.options = Object.assign(this.options, options);
|
||||||
refreshPlayer && this.refreshPlayer();
|
refreshPlayer && this.refreshPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlayerElement(playerType?: StreamPlayerType) {
|
getPlayerElement(playerType?: StreamPlayerType) {
|
||||||
if (typeof playerType === 'undefined') {
|
if (typeof playerType === 'undefined') {
|
||||||
playerType = this.#playerType;
|
playerType = this.playerType;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playerType === StreamPlayerType.WEBGL2) {
|
if (playerType === StreamPlayerType.WEBGL2) {
|
||||||
return this.#webGL2Player?.getCanvas();
|
return this.webGL2Player?.getCanvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.#$video;
|
return this.$video;
|
||||||
}
|
}
|
||||||
|
|
||||||
getWebGL2Player() {
|
getWebGL2Player() {
|
||||||
return this.#webGL2Player;
|
return this.webGL2Player;
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshPlayer() {
|
refreshPlayer() {
|
||||||
if (this.#playerType === StreamPlayerType.WEBGL2) {
|
if (this.playerType === StreamPlayerType.WEBGL2) {
|
||||||
const options = this.#options;
|
const options = this.options;
|
||||||
const webGL2Player = this.#webGL2Player!;
|
const webGL2Player = this.webGL2Player!;
|
||||||
|
|
||||||
if (options.processing === StreamVideoProcessing.USM) {
|
if (options.processing === StreamVideoProcessing.USM) {
|
||||||
webGL2Player.setFilter(1);
|
webGL2Player.setFilter(1);
|
||||||
@@ -241,7 +241,7 @@ export class StreamPlayer {
|
|||||||
webGL2Player.setContrast(options.contrast || 100);
|
webGL2Player.setContrast(options.contrast || 100);
|
||||||
webGL2Player.setBrightness(options.brightness || 100);
|
webGL2Player.setBrightness(options.brightness || 100);
|
||||||
} else {
|
} else {
|
||||||
let filters = this.#getVideoPlayerFilterStyle();
|
let filters = this.getVideoPlayerFilterStyle();
|
||||||
let videoCss = '';
|
let videoCss = '';
|
||||||
if (filters) {
|
if (filters) {
|
||||||
videoCss += `filter: ${filters} !important;`;
|
videoCss += `filter: ${filters} !important;`;
|
||||||
@@ -257,26 +257,26 @@ export class StreamPlayer {
|
|||||||
css = `#game-stream video { ${videoCss} }`;
|
css = `#game-stream video { ${videoCss} }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#$videoCss!.textContent = css;
|
this.$videoCss!.textContent = css;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#resizePlayer();
|
this.resizePlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadPlayer() {
|
reloadPlayer() {
|
||||||
this.#cleanUpWebGL2Player();
|
this.cleanUpWebGL2Player();
|
||||||
|
|
||||||
this.#playerType = StreamPlayerType.VIDEO;
|
this.playerType = StreamPlayerType.VIDEO;
|
||||||
this.setPlayerType(StreamPlayerType.WEBGL2, false);
|
this.setPlayerType(StreamPlayerType.WEBGL2, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
#cleanUpWebGL2Player() {
|
private cleanUpWebGL2Player() {
|
||||||
// Clean up WebGL2 Player
|
// Clean up WebGL2 Player
|
||||||
this.#webGL2Player?.destroy();
|
this.webGL2Player?.destroy();
|
||||||
this.#webGL2Player = null;
|
this.webGL2Player = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.#cleanUpWebGL2Player();
|
this.cleanUpWebGL2Player();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -345,7 +345,7 @@ export class StreamBadges {
|
|||||||
text += server.region;
|
text += server.region;
|
||||||
}
|
}
|
||||||
|
|
||||||
text += '@' + (server ? 'IPv6' : 'IPv4');
|
text += '@' + (server.ipv6 ? 'IPv6' : 'IPv4');
|
||||||
this.badges.server.$element = this.renderBadge(StreamBadge.SERVER, text);
|
this.badges.server.$element = this.renderBadge(StreamBadge.SERVER, text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
|
@@ -37,6 +37,10 @@ export class StreamStats {
|
|||||||
name: t('stat-ping'),
|
name: t('stat-ping'),
|
||||||
$element: CE('span'),
|
$element: CE('span'),
|
||||||
},
|
},
|
||||||
|
[StreamStat.JITTER]: {
|
||||||
|
name: t('jitter'),
|
||||||
|
$element: CE('span'),
|
||||||
|
},
|
||||||
[StreamStat.FPS]: {
|
[StreamStat.FPS]: {
|
||||||
name: t('stat-fps'),
|
name: t('stat-fps'),
|
||||||
$element: CE('span'),
|
$element: CE('span'),
|
||||||
@@ -179,10 +183,8 @@ export class StreamStats {
|
|||||||
$element.textContent = value.toString();
|
$element.textContent = value.toString();
|
||||||
|
|
||||||
// Get stat's grade
|
// Get stat's grade
|
||||||
if (PREF_STATS_CONDITIONAL_FORMATTING) {
|
if (PREF_STATS_CONDITIONAL_FORMATTING && 'grades' in value) {
|
||||||
if (statKey === StreamStat.PING || statKey === StreamStat.DECODE_TIME) {
|
grade = statsCollector.calculateGrade(value.current, value.grades);
|
||||||
grade = (value as any).calculateGrade();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($element.dataset.grade !== grade) {
|
if ($element.dataset.grade !== grade) {
|
||||||
|
@@ -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,
|
||||||
@@ -631,7 +646,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
|||||||
},
|
},
|
||||||
suggest: {
|
suggest: {
|
||||||
lowest: 0,
|
lowest: 0,
|
||||||
highest: 4,
|
highest: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[PrefKey.VIDEO_RATIO]: {
|
[PrefKey.VIDEO_RATIO]: {
|
||||||
@@ -714,6 +729,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
|||||||
[StreamStat.PLAYTIME]: `${StreamStat.PLAYTIME.toUpperCase()}: ${t('playtime')}`,
|
[StreamStat.PLAYTIME]: `${StreamStat.PLAYTIME.toUpperCase()}: ${t('playtime')}`,
|
||||||
[StreamStat.BATTERY]: `${StreamStat.BATTERY.toUpperCase()}: ${t('battery')}`,
|
[StreamStat.BATTERY]: `${StreamStat.BATTERY.toUpperCase()}: ${t('battery')}`,
|
||||||
[StreamStat.PING]: `${StreamStat.PING.toUpperCase()}: ${t('stat-ping')}`,
|
[StreamStat.PING]: `${StreamStat.PING.toUpperCase()}: ${t('stat-ping')}`,
|
||||||
|
[StreamStat.JITTER]: `${StreamStat.JITTER.toUpperCase()}: ${t('jitter')}`,
|
||||||
[StreamStat.FPS]: `${StreamStat.FPS.toUpperCase()}: ${t('stat-fps')}`,
|
[StreamStat.FPS]: `${StreamStat.FPS.toUpperCase()}: ${t('stat-fps')}`,
|
||||||
[StreamStat.BITRATE]: `${StreamStat.BITRATE.toUpperCase()}: ${t('stat-bitrate')}`,
|
[StreamStat.BITRATE]: `${StreamStat.BITRATE.toUpperCase()}: ${t('stat-bitrate')}`,
|
||||||
[StreamStat.DECODE_TIME]: `${StreamStat.DECODE_TIME.toUpperCase()}: ${t('stat-decode-time')}`,
|
[StreamStat.DECODE_TIME]: `${StreamStat.DECODE_TIME.toUpperCase()}: ${t('stat-decode-time')}`,
|
||||||
|
@@ -4,6 +4,7 @@ import { humanFileSize, secondsToHm } from "./html";
|
|||||||
|
|
||||||
export enum StreamStat {
|
export enum StreamStat {
|
||||||
PING = 'ping',
|
PING = 'ping',
|
||||||
|
JITTER = 'jit',
|
||||||
FPS = 'fps',
|
FPS = 'fps',
|
||||||
BITRATE = 'btr',
|
BITRATE = 'btr',
|
||||||
DECODE_TIME = 'dt',
|
DECODE_TIME = 'dt',
|
||||||
@@ -21,7 +22,13 @@ export type StreamStatGrade = '' | 'bad' | 'ok' | 'good';
|
|||||||
type CurrentStats = {
|
type CurrentStats = {
|
||||||
[StreamStat.PING]: {
|
[StreamStat.PING]: {
|
||||||
current: number;
|
current: number;
|
||||||
calculateGrade: () => StreamStatGrade;
|
grades: [number, number, number];
|
||||||
|
toString: () => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
[StreamStat.JITTER]: {
|
||||||
|
current: number;
|
||||||
|
grades: [number, number, number];
|
||||||
toString: () => string;
|
toString: () => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,7 +57,7 @@ type CurrentStats = {
|
|||||||
[StreamStat.DECODE_TIME]: {
|
[StreamStat.DECODE_TIME]: {
|
||||||
current: number;
|
current: number;
|
||||||
total: number;
|
total: number;
|
||||||
calculateGrade: () => StreamStatGrade;
|
grades: [number, number, number];
|
||||||
toString: () => string;
|
toString: () => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,17 +103,27 @@ export class StreamStatsCollector {
|
|||||||
// Collect in background - 60 seconds
|
// Collect in background - 60 seconds
|
||||||
static readonly INTERVAL_BACKGROUND = 60 * 1000;
|
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 = {
|
private currentStats: CurrentStats = {
|
||||||
[StreamStat.PING]: {
|
[StreamStat.PING]: {
|
||||||
current: -1,
|
current: -1,
|
||||||
calculateGrade() {
|
grades: [40, 75, 100],
|
||||||
return (this.current >= 100) ? 'bad' : (this.current > 75) ? 'ok' : (this.current > 40) ? 'good' : '';
|
|
||||||
},
|
|
||||||
toString() {
|
toString() {
|
||||||
return this.current === -1 ? '???' : this.current.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]: {
|
[StreamStat.FPS]: {
|
||||||
current: 0,
|
current: 0,
|
||||||
toString() {
|
toString() {
|
||||||
@@ -142,9 +159,7 @@ export class StreamStatsCollector {
|
|||||||
[StreamStat.DECODE_TIME]: {
|
[StreamStat.DECODE_TIME]: {
|
||||||
current: 0,
|
current: 0,
|
||||||
total: 0,
|
total: 0,
|
||||||
calculateGrade() {
|
grades: [6, 9, 12],
|
||||||
return (this.current > 12) ? 'bad' : (this.current > 9) ? 'ok' : (this.current > 6) ? 'good' : '';
|
|
||||||
},
|
|
||||||
toString() {
|
toString() {
|
||||||
return isNaN(this.current) ? '??ms' : `${this.current.toFixed(2)}ms`;
|
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() {
|
async collect() {
|
||||||
const stats = await STATES.currentStream.peerConnection?.getStats();
|
const stats = await STATES.currentStream.peerConnection?.getStats();
|
||||||
|
if (!stats) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
stats?.forEach(stat => {
|
stats.forEach(stat => {
|
||||||
if (stat.type === 'inbound-rtp' && stat.kind === 'video') {
|
if (stat.type === 'inbound-rtp' && stat.kind === 'video') {
|
||||||
// FPS
|
// FPS
|
||||||
const fps = this.currentStats[StreamStat.FPS];
|
const fps = this.currentStats[StreamStat.FPS];
|
||||||
@@ -229,15 +247,23 @@ export class StreamStatsCollector {
|
|||||||
|
|
||||||
const lastStat = this.lastVideoStat;
|
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
|
// Bitrate
|
||||||
const btr = this.currentStats[StreamStat.BITRATE];
|
const btr = this.currentStats[StreamStat.BITRATE];
|
||||||
const timeDiff = stat.timestamp - lastStat.timestamp;
|
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
|
// Decode time
|
||||||
const dt = this.currentStats[StreamStat.DECODE_TIME];
|
const dt = this.currentStats[StreamStat.DECODE_TIME];
|
||||||
dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime;
|
dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime!;
|
||||||
const framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded;
|
const framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded!;
|
||||||
dt.current = dt.total / framesDecodedDiff * 1000;
|
dt.current = dt.total / framesDecodedDiff * 1000;
|
||||||
|
|
||||||
this.lastVideoStat = stat;
|
this.lastVideoStat = stat;
|
||||||
|
@@ -131,6 +131,7 @@ const Texts = {
|
|||||||
"increase": "Increase",
|
"increase": "Increase",
|
||||||
"install-android": "Better xCloud app for Android",
|
"install-android": "Better xCloud app for Android",
|
||||||
"japan": "Japan",
|
"japan": "Japan",
|
||||||
|
"jitter": "Jitter",
|
||||||
"keyboard-shortcuts": "Keyboard shortcuts",
|
"keyboard-shortcuts": "Keyboard shortcuts",
|
||||||
"korea": "Korea",
|
"korea": "Korea",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
@@ -142,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