Optimize Clarity Boost shader

This commit is contained in:
redphx 2024-10-10 17:28:19 +07:00
parent 97fb7a114f
commit baa22dbefc
7 changed files with 344 additions and 347 deletions

View File

@ -76,6 +76,12 @@ 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, '');
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'));

View File

@ -1580,7 +1580,7 @@ class GlobalSettingsStorage extends BaseSettingsStore {
}, },
suggest: { suggest: {
lowest: 0, lowest: 0,
highest: 4 highest: 2
} }
}, },
video_ratio: { video_ratio: {
@ -5033,86 +5033,86 @@ function patchSdpBitrate(sdp, video, audio) {
} }
return lines.join("\r\n"); return lines.join("\r\n");
} }
var clarity_boost_default = "attribute vec2 position;\n\nvoid main() {\n gl_Position = vec4(position, 0, 1);\n}\n"; var clarity_boost_default = "attribute vec4 position;\n\nvoid main() {\ngl_Position = position;\n}\n";
var clarity_boost_default2 = "const int FILTER_UNSHARP_MASKING = 1;\nconst int FILTER_CAS = 2;\n\nprecision highp float;\nuniform sampler2D data;\nuniform vec2 iResolution;\n\nuniform int filterId;\nuniform float sharpenFactor;\nuniform float brightness;\nuniform float contrast;\nuniform float saturation;\n\nvec3 textureAt(sampler2D tex, vec2 coord) {\n return texture2D(tex, coord / iResolution.xy).rgb;\n}\n\nvec3 clarityBoost(sampler2D tex, vec2 coord)\n{\n // Load a collection of samples in a 3x3 neighorhood, where e is the current pixel.\n // a b c\n // d e f\n // g h i\n vec3 a = textureAt(tex, coord + vec2(-1, 1));\n vec3 b = textureAt(tex, coord + vec2(0, 1));\n vec3 c = textureAt(tex, coord + vec2(1, 1));\n\n vec3 d = textureAt(tex, coord + vec2(-1, 0));\n vec3 e = textureAt(tex, coord);\n vec3 f = textureAt(tex, coord + vec2(1, 0));\n\n vec3 g = textureAt(tex, coord + vec2(-1, -1));\n vec3 h = textureAt(tex, coord + vec2(0, -1));\n vec3 i = textureAt(tex, coord + vec2(1, -1));\n\n if (filterId == FILTER_CAS) {\n // Soft min and max.\n // a b c b\n // d e f * 0.5 + d e f * 0.5\n // g h i h\n // These are 2.0x bigger (factored out the extra multiply).\n vec3 minRgb = min(min(min(d, e), min(f, b)), h);\n vec3 minRgb2 = min(min(a, c), min(g, i));\n minRgb += min(minRgb, minRgb2);\n\n vec3 maxRgb = max(max(max(d, e), max(f, b)), h);\n vec3 maxRgb2 = max(max(a, c), max(g, i));\n maxRgb += max(maxRgb, maxRgb2);\n\n // Smooth minimum distance to signal limit divided by smooth max.\n vec3 reciprocalMaxRgb = 1.0 / maxRgb;\n vec3 amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, 0.0, 1.0);\n\n // Shaping amount of sharpening.\n amplifyRgb = inversesqrt(amplifyRgb);\n\n float contrast = 0.8;\n float peak = -3.0 * contrast + 8.0;\n vec3 weightRgb = -(1.0 / (amplifyRgb * peak));\n\n vec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);\n\n // 0 w 0\n // Filter shape: w 1 w\n // 0 w 0\n vec3 window = (b + d) + (f + h);\n vec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0);\n\n outColor = mix(e, outColor, sharpenFactor / 2.0);\n\n return outColor;\n } else if (filterId == FILTER_UNSHARP_MASKING) {\n vec3 gaussianBlur = (a * 1.0 + b * 2.0 + c * 1.0 +\n d * 2.0 + e * 4.0 + f * 2.0 +\n g * 1.0 + h * 2.0 + i * 1.0) / 16.0;\n\n // Return edge detection\n return e + (e - gaussianBlur) * sharpenFactor / 3.0;\n }\n\n return e;\n}\n\nvec3 adjustBrightness(vec3 color) {\n return (1.0 + brightness) * color;\n}\n\nvec3 adjustContrast(vec3 color) {\n return 0.5 + (1.0 + contrast) * (color - 0.5);\n}\n\nvec3 adjustSaturation(vec3 color) {\n const vec3 luminosityFactor = vec3(0.2126, 0.7152, 0.0722);\n vec3 grayscale = vec3(dot(color, luminosityFactor));\n\n return mix(grayscale, color, 1.0 + saturation);\n}\n\nvoid main() {\n vec3 color;\n\n if (sharpenFactor > 0.0) {\n color = clarityBoost(data, gl_FragCoord.xy);\n } else {\n color = textureAt(data, gl_FragCoord.xy);\n }\n\n if (saturation != 0.0) {\n color = adjustSaturation(color);\n }\n\n if (contrast != 0.0) {\n color = adjustContrast(color);\n }\n\n if (brightness != 0.0) {\n color = adjustBrightness(color);\n }\n\n gl_FragColor = vec4(color, 1.0);\n}\n"; var clarity_boost_default2 = "precision mediump float;\nuniform sampler2D data;\nuniform vec2 iResolution;\n\nconst int FILTER_UNSHARP_MASKING = 1;\nconst int FILTER_CAS = 2;\n\nconst float CAS_CONTRAST_PEAK = (-3.0 * 0.8 + 8.0);\n\nconst vec3 LUMINOSITY_FACTOR = vec3(0.2126, 0.7152, 0.0722);\n\nuniform int filterId;\nuniform float sharpenFactor;\nuniform float brightness;\nuniform float contrast;\nuniform float saturation;\n\nvec3 clarityBoost(sampler2D tex, vec2 coord) {\nvec2 texelSize = 1.0 / iResolution.xy;\n\nvec3 a = texture2D(tex, coord + texelSize * vec2(-1, 1)).rgb;\nvec3 b = texture2D(tex, coord + texelSize * vec2(0, 1)).rgb;\nvec3 c = texture2D(tex, coord + texelSize * vec2(1, 1)).rgb;\n\nvec3 d = texture2D(tex, coord + texelSize * vec2(-1, 0)).rgb;\nvec3 e = texture2D(tex, coord).rgb;\nvec3 f = texture2D(tex, coord + texelSize * vec2(1, 0)).rgb;\n\nvec3 g = texture2D(tex, coord + texelSize * vec2(-1, -1)).rgb;\nvec3 h = texture2D(tex, coord + texelSize * vec2(0, -1)).rgb;\nvec3 i = texture2D(tex, coord + texelSize * vec2(1, -1)).rgb;\n\nif (filterId == FILTER_CAS) {\nvec3 minRgb = min(min(min(d, e), min(f, b)), h);\nvec3 minRgb2 = min(min(a, c), min(g, i));\nminRgb += min(minRgb, minRgb2);\n\nvec3 maxRgb = max(max(max(d, e), max(f, b)), h);\nvec3 maxRgb2 = max(max(a, c), max(g, i));\nmaxRgb += max(maxRgb, maxRgb2);\n\nvec3 reciprocalMaxRgb = 1.0 / maxRgb;\nvec3 amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, 0.0, 1.0);\n\namplifyRgb = inversesqrt(amplifyRgb);\n\nvec3 weightRgb = -(1.0 / (amplifyRgb * CAS_CONTRAST_PEAK));\nvec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);\n\nvec3 window = (b + d) + (f + h);\nvec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0);\n\noutColor = mix(e, outColor, sharpenFactor / 2.0);\n\nreturn outColor;\n} else if (filterId == FILTER_UNSHARP_MASKING) {\nvec3 gaussianBlur = (a + c + g + i) * 1.0 +\n(b + d + f + h) * 2.0 +\ne * 4.0;\ngaussianBlur /= 16.0;\n\nreturn e + (e - gaussianBlur) * sharpenFactor / 3.0;\n}\n\nreturn e;\n}\n\nvoid main() {\nvec3 color;\nvec2 uv = gl_FragCoord.xy / iResolution.xy;\n\nif (sharpenFactor > 0.0) {\ncolor = clarityBoost(data, uv);\n} else {\ncolor = texture2D(data, uv).rgb;\n}\n\nif (saturation != 1.0) {\nvec3 grayscale = vec3(dot(color, LUMINOSITY_FACTOR));\ncolor = mix(grayscale, color, saturation);\n}\n\nif (contrast != 1.0) {\ncolor = 0.5 + contrast * (color - 0.5);\n}\n\nif (brightness != 1.0) {\ncolor = brightness * color;\n}\n\ngl_FragColor = vec4(color, 1.0);\n}\n";
var LOG_TAG3 = "WebGL2Player"; var LOG_TAG3 = "WebGL2Player";
class WebGL2Player { class WebGL2Player {
#$video; $video;
#$canvas; $canvas;
#gl = null; gl = null;
#resources = []; resources = [];
#program = null; program = null;
#stopped = !1; stopped = !1;
#options = { options = {
filterId: 1, filterId: 1,
sharpenFactor: 0, sharpenFactor: 0,
brightness: 0, brightness: 0,
contrast: 0, contrast: 0,
saturation: 0 saturation: 0
}; };
#animFrameId = null; animFrameId = null;
constructor($video) { constructor($video) {
BxLogger.info(LOG_TAG3, "Initialize"), this.#$video = $video; BxLogger.info(LOG_TAG3, "Initialize"), this.$video = $video;
const $canvas = document.createElement("canvas"); const $canvas = document.createElement("canvas");
$canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.#$canvas = $canvas, this.#setupShaders(), this.#setupRendering(), $video.insertAdjacentElement("afterend", $canvas); $canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, this.setupShaders(), this.setupRendering(), $video.insertAdjacentElement("afterend", $canvas);
} }
setFilter(filterId, update = !0) { setFilter(filterId, update = !0) {
this.#options.filterId = filterId, update && this.updateCanvas(); this.options.filterId = filterId, update && this.updateCanvas();
} }
setSharpness(sharpness, update = !0) { setSharpness(sharpness, update = !0) {
this.#options.sharpenFactor = sharpness, update && this.updateCanvas(); this.options.sharpenFactor = sharpness, update && this.updateCanvas();
} }
setBrightness(brightness, update = !0) { setBrightness(brightness, update = !0) {
this.#options.brightness = (brightness - 100) / 100, update && this.updateCanvas(); this.options.brightness = 1 + (brightness - 100) / 100, update && this.updateCanvas();
} }
setContrast(contrast, update = !0) { setContrast(contrast, update = !0) {
this.#options.contrast = (contrast - 100) / 100, update && this.updateCanvas(); this.options.contrast = 1 + (contrast - 100) / 100, update && this.updateCanvas();
} }
setSaturation(saturation, update = !0) { setSaturation(saturation, update = !0) {
this.#options.saturation = (saturation - 100) / 100, update && this.updateCanvas(); this.options.saturation = 1 + (saturation - 100) / 100, update && this.updateCanvas();
} }
getCanvas() { getCanvas() {
return this.#$canvas; return this.$canvas;
} }
updateCanvas() { updateCanvas() {
const gl = this.#gl, program = this.#program; const gl = this.gl, program = this.program;
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.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);
} }
drawFrame() { drawFrame() {
const gl = this.#gl, $video = this.#$video; const gl = this.gl, $video = this.$video;
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, $video), gl.drawArrays(gl.TRIANGLES, 0, 6); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, $video), gl.drawArrays(gl.TRIANGLES, 0, 6);
} }
#setupRendering() { setupRendering() {
let animate; let animate;
if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) { if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
const $video = this.#$video; const $video = this.$video;
animate = () => { animate = () => {
if (this.#stopped) return; if (this.stopped) return;
this.drawFrame(), this.#animFrameId = $video.requestVideoFrameCallback(animate); this.drawFrame(), this.animFrameId = $video.requestVideoFrameCallback(animate);
}, this.#animFrameId = $video.requestVideoFrameCallback(animate); }, this.animFrameId = $video.requestVideoFrameCallback(animate);
} else animate = () => { } else animate = () => {
if (this.#stopped) return; if (this.stopped) return;
this.drawFrame(), this.#animFrameId = requestAnimationFrame(animate); this.drawFrame(), this.animFrameId = requestAnimationFrame(animate);
}, this.#animFrameId = requestAnimationFrame(animate); }, this.animFrameId = requestAnimationFrame(animate);
} }
#setupShaders() { setupShaders() {
BxLogger.info(LOG_TAG3, "Setting up", getPref("video_power_preference")); BxLogger.info(LOG_TAG3, "Setting up", getPref("video_power_preference"));
const gl = this.#$canvas.getContext("webgl", { const gl = this.$canvas.getContext("webgl", {
isBx: !0, isBx: !0,
antialias: !0, antialias: !0,
alpha: !1, alpha: !1,
powerPreference: getPref("video_power_preference") powerPreference: getPref("video_power_preference")
}); });
this.#gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth); this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);
const vShader = gl.createShader(gl.VERTEX_SHADER); const vShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader); gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader);
const fShader = gl.createShader(gl.FRAGMENT_SHADER); const fShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader); gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader);
const program = gl.createProgram(); const program = gl.createProgram();
if (this.#program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`); if (this.program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`);
this.updateCanvas(); this.updateCanvas();
const buffer = gl.createBuffer(); const buffer = gl.createBuffer();
this.#resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ this.resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1,
-1, -1,
1, 1,
@ -5127,51 +5127,51 @@ class WebGL2Player {
1 1
]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0); ]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0);
const texture = gl.createTexture(); const texture = gl.createTexture();
this.#resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0); this.resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0);
} }
resume() { resume() {
this.stop(), this.#stopped = !1, BxLogger.info(LOG_TAG3, "Resume"), this.#$canvas.classList.remove("bx-gone"), this.#setupRendering(); this.stop(), this.stopped = !1, BxLogger.info(LOG_TAG3, "Resume"), this.$canvas.classList.remove("bx-gone"), this.setupRendering();
} }
stop() { stop() {
if (BxLogger.info(LOG_TAG3, "Stop"), this.#$canvas.classList.add("bx-gone"), this.#stopped = !0, this.#animFrameId) { if (BxLogger.info(LOG_TAG3, "Stop"), this.$canvas.classList.add("bx-gone"), this.stopped = !0, this.animFrameId) {
if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.#$video.cancelVideoFrameCallback(this.#animFrameId); if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.$video.cancelVideoFrameCallback(this.animFrameId);
else cancelAnimationFrame(this.#animFrameId); else cancelAnimationFrame(this.animFrameId);
this.#animFrameId = null; this.animFrameId = null;
} }
} }
destroy() { destroy() {
BxLogger.info(LOG_TAG3, "Destroy"), this.stop(); BxLogger.info(LOG_TAG3, "Destroy"), 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 (let resource of this.#resources) for (let resource of this.resources)
if (resource instanceof WebGLProgram) gl.useProgram(null), gl.deleteProgram(resource); if (resource instanceof WebGLProgram) gl.useProgram(null), gl.deleteProgram(resource);
else if (resource instanceof WebGLShader) gl.deleteShader(resource); else if (resource instanceof WebGLShader) gl.deleteShader(resource);
else if (resource instanceof WebGLTexture) gl.deleteTexture(resource); else if (resource instanceof WebGLTexture) gl.deleteTexture(resource);
else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource); else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource);
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;
} }
} }
class StreamPlayer { class StreamPlayer {
#$video; $video;
#playerType = "default"; playerType = "default";
#options = {}; options = {};
#webGL2Player = null; webGL2Player = null;
#$videoCss = null; $videoCss = null;
#$usmMatrix = null; $usmMatrix = null;
constructor($video, type, options) { constructor($video, type, options) {
this.#setupVideoElements(), this.#$video = $video, this.#options = options || {}, this.setPlayerType(type); this.setupVideoElements(), this.$video = $video, this.options = options || {}, this.setPlayerType(type);
} }
#setupVideoElements() { setupVideoElements() {
if (this.#$videoCss = document.getElementById("bx-video-css"), this.#$videoCss) { if (this.$videoCss = document.getElementById("bx-video-css"), this.$videoCss) {
this.#$usmMatrix = this.#$videoCss.querySelector("#bx-filter-usm-matrix"); this.$usmMatrix = this.$videoCss.querySelector("#bx-filter-usm-matrix");
return; return;
} }
const $fragment = document.createDocumentFragment(); const $fragment = document.createDocumentFragment();
this.#$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.#$videoCss); this.$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.$videoCss);
const $svg = CE("svg", { const $svg = CE("svg", {
id: "bx-video-filters", id: "bx-video-filters",
xmlns: "http://www.w3.org/2000/svg", xmlns: "http://www.w3.org/2000/svg",
@ -5179,31 +5179,31 @@ class StreamPlayer {
}, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, CE("filter", { }, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, 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"
})))); }))));
$fragment.appendChild($svg), document.documentElement.appendChild($fragment); $fragment.appendChild($svg), document.documentElement.appendChild($fragment);
} }
#getVideoPlayerFilterStyle() { getVideoPlayerFilterStyle() {
const filters = [], sharpness = this.#options.sharpness || 0; const filters = [], sharpness = this.options.sharpness || 0;
if (this.#options.processing === "usm" && sharpness != 0) { if (this.options.processing === "usm" && sharpness != 0) {
const matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`; const matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`;
this.#$usmMatrix?.setAttributeNS(null, "kernelMatrix", matrix), filters.push("url(#bx-filter-usm)"); 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}%)`); 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}%)`); 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}%)`); if (brightness != 100) filters.push(`brightness(${brightness}%)`);
return filters.join(" "); return filters.join(" ");
} }
#resizePlayer() { resizePlayer() {
const PREF_RATIO = getPref("video_ratio"), $video = this.#$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport; const PREF_RATIO = getPref("video_ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport;
let $webGL2Canvas; let $webGL2Canvas;
if (this.#playerType == "webgl2") $webGL2Canvas = this.#webGL2Player?.getCanvas(); if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas();
let targetWidth, targetHeight, targetObjectFit; let targetWidth, targetHeight, targetObjectFit;
if (PREF_RATIO.includes(":")) { if (PREF_RATIO.includes(":")) {
const tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]); const tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]);
@ -5214,53 +5214,53 @@ class StreamPlayer {
width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString(), targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill"; width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString(), targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill";
} else targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString(); } else targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString();
if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, $webGL2Canvas) $webGL2Canvas.style.width = targetWidth, $webGL2Canvas.style.height = targetHeight, $webGL2Canvas.style.objectFit = targetObjectFit; if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, $webGL2Canvas) $webGL2Canvas.style.width = targetWidth, $webGL2Canvas.style.height = targetHeight, $webGL2Canvas.style.objectFit = targetObjectFit;
if (isNativeTouchGame && this.#playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions(); if (isNativeTouchGame && this.playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions();
} }
setPlayerType(type, refreshPlayer = !1) { setPlayerType(type, refreshPlayer = !1) {
if (this.#playerType !== type) if (type === "webgl2") { if (this.playerType !== type) if (type === "webgl2") {
if (!this.#webGL2Player) this.#webGL2Player = new WebGL2Player(this.#$video); if (!this.webGL2Player) this.webGL2Player = new WebGL2Player(this.$video);
else this.#webGL2Player.resume(); else this.webGL2Player.resume();
this.#$videoCss.textContent = "", this.#$video.classList.add("bx-pixel"); this.$videoCss.textContent = "", this.$video.classList.add("bx-pixel");
} else this.#webGL2Player?.stop(), this.#$video.classList.remove("bx-pixel"); } else this.webGL2Player?.stop(), this.$video.classList.remove("bx-pixel");
this.#playerType = type, refreshPlayer && this.refreshPlayer(); this.playerType = type, refreshPlayer && this.refreshPlayer();
} }
setOptions(options, refreshPlayer = !1) { setOptions(options, refreshPlayer = !1) {
this.#options = options, refreshPlayer && this.refreshPlayer(); this.options = options, refreshPlayer && this.refreshPlayer();
} }
updateOptions(options, refreshPlayer = !1) { updateOptions(options, refreshPlayer = !1) {
this.#options = Object.assign(this.#options, options), refreshPlayer && this.refreshPlayer(); this.options = Object.assign(this.options, options), refreshPlayer && this.refreshPlayer();
} }
getPlayerElement(playerType) { getPlayerElement(playerType) {
if (typeof playerType === "undefined") playerType = this.#playerType; if (typeof playerType === "undefined") playerType = this.playerType;
if (playerType === "webgl2") return this.#webGL2Player?.getCanvas(); if (playerType === "webgl2") return this.webGL2Player?.getCanvas();
return this.#$video; return this.$video;
} }
getWebGL2Player() { getWebGL2Player() {
return this.#webGL2Player; return this.webGL2Player;
} }
refreshPlayer() { refreshPlayer() {
if (this.#playerType === "webgl2") { if (this.playerType === "webgl2") {
const options = this.#options, webGL2Player = this.#webGL2Player; const options = this.options, webGL2Player = this.webGL2Player;
if (options.processing === "usm") webGL2Player.setFilter(1); if (options.processing === "usm") webGL2Player.setFilter(1);
else webGL2Player.setFilter(2); else webGL2Player.setFilter(2);
webGL2Player.setSharpness(options.sharpness || 0), webGL2Player.setSaturation(options.saturation || 100), webGL2Player.setContrast(options.contrast || 100), webGL2Player.setBrightness(options.brightness || 100); webGL2Player.setSharpness(options.sharpness || 0), webGL2Player.setSaturation(options.saturation || 100), webGL2Player.setContrast(options.contrast || 100), webGL2Player.setBrightness(options.brightness || 100);
} else { } else {
let filters = this.#getVideoPlayerFilterStyle(), videoCss = ""; let filters = this.getVideoPlayerFilterStyle(), videoCss = "";
if (filters) videoCss += `filter: ${filters} !important;`; if (filters) videoCss += `filter: ${filters} !important;`;
let css = ""; let css = "";
if (videoCss) css = `#game-stream video { ${videoCss} }`; if (videoCss) css = `#game-stream video { ${videoCss} }`;
this.#$videoCss.textContent = css; this.$videoCss.textContent = css;
} }
this.#resizePlayer(); this.resizePlayer();
} }
reloadPlayer() { reloadPlayer() {
this.#cleanUpWebGL2Player(), this.#playerType = "default", this.setPlayerType("webgl2", !1); this.cleanUpWebGL2Player(), this.playerType = "default", this.setPlayerType("webgl2", !1);
} }
#cleanUpWebGL2Player() { cleanUpWebGL2Player() {
this.#webGL2Player?.destroy(), this.#webGL2Player = null; this.webGL2Player?.destroy(), this.webGL2Player = null;
} }
destroy() { destroy() {
this.#cleanUpWebGL2Player(); this.cleanUpWebGL2Player();
} }
} }
function patchVideoApi() { function patchVideoApi() {

View File

@ -1607,7 +1607,7 @@ class GlobalSettingsStorage extends BaseSettingsStore {
}, },
suggest: { suggest: {
lowest: 0, lowest: 0,
highest: 4 highest: 2
} }
}, },
video_ratio: { video_ratio: {
@ -3903,13 +3903,13 @@ class VibrationManager {
}); });
} }
} }
var controller_shortcuts_default = "if (window.BX_EXPOSED.disableGamepadPolling) {\n this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(50) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, 50);\n return;\n}\n\nconst currentGamepad = ${gamepadVar};\n\n// Share button on XS controller\nif (currentGamepad.buttons[17] && currentGamepad.buttons[17].pressed) {\n window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT));\n}\n\nconst btnHome = currentGamepad.buttons[16];\nif (btnHome) {\n if (!this.bxHomeStates) {\n this.bxHomeStates = {};\n }\n\n let intervalMs = 0;\n let hijack = false;\n\n if (btnHome.pressed) {\n hijack = true;\n intervalMs = 16;\n this.gamepadIsIdle.set(currentGamepad.index, false);\n\n if (this.bxHomeStates[currentGamepad.index]) {\n const lastTimestamp = this.bxHomeStates[currentGamepad.index].timestamp;\n\n if (currentGamepad.timestamp !== lastTimestamp) {\n this.bxHomeStates[currentGamepad.index].timestamp = currentGamepad.timestamp;\n\n const handled = window.BX_EXPOSED.handleControllerShortcut(currentGamepad);\n if (handled) {\n this.bxHomeStates[currentGamepad.index].shortcutPressed += 1;\n }\n }\n } else {\n // First time pressing > save current timestamp\n window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index);\n this.bxHomeStates[currentGamepad.index] = {\n shortcutPressed: 0,\n timestamp: currentGamepad.timestamp,\n };\n }\n } else if (this.bxHomeStates[currentGamepad.index]) {\n hijack = true;\n const info = structuredClone(this.bxHomeStates[currentGamepad.index]);\n\n // Home button released\n this.bxHomeStates[currentGamepad.index] = null;\n\n if (info.shortcutPressed === 0) {\n const fakeGamepadMappings = [{\n GamepadIndex: currentGamepad.index,\n A: 0,\n B: 0,\n X: 0,\n Y: 0,\n LeftShoulder: 0,\n RightShoulder: 0,\n LeftTrigger: 0,\n RightTrigger: 0,\n View: 0,\n Menu: 0,\n LeftThumb: 0,\n RightThumb: 0,\n DPadUp: 0,\n DPadDown: 0,\n DPadLeft: 0,\n DPadRight: 0,\n Nexus: 1,\n LeftThumbXAxis: 0,\n LeftThumbYAxis: 0,\n RightThumbXAxis: 0,\n RightThumbYAxis: 0,\n PhysicalPhysicality: 0,\n VirtualPhysicality: 0,\n Dirty: true,\n Virtual: false,\n }];\n\n const isLongPress = (currentGamepad.timestamp - info.timestamp) >= 500;\n intervalMs = isLongPress ? 500 : 100;\n\n this.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings);\n } else {\n intervalMs = 4;\n }\n }\n\n if (hijack && intervalMs) {\n // Listen to next button press\n this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs);\n\n // Hijack this button\n return;\n }\n}\n"; var controller_shortcuts_default = "if (window.BX_EXPOSED.disableGamepadPolling) {\nthis.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(50) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, 50);\nreturn;\n}\n\nconst currentGamepad = ${gamepadVar};\n\nif (currentGamepad.buttons[17] && currentGamepad.buttons[17].pressed) {\nwindow.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT));\n}\n\nconst btnHome = currentGamepad.buttons[16];\nif (btnHome) {\nif (!this.bxHomeStates) {\nthis.bxHomeStates = {};\n}\n\nlet intervalMs = 0;\nlet hijack = false;\n\nif (btnHome.pressed) {\nhijack = true;\nintervalMs = 16;\nthis.gamepadIsIdle.set(currentGamepad.index, false);\n\nif (this.bxHomeStates[currentGamepad.index]) {\nconst lastTimestamp = this.bxHomeStates[currentGamepad.index].timestamp;\n\nif (currentGamepad.timestamp !== lastTimestamp) {\nthis.bxHomeStates[currentGamepad.index].timestamp = currentGamepad.timestamp;\n\nconst handled = window.BX_EXPOSED.handleControllerShortcut(currentGamepad);\nif (handled) {\nthis.bxHomeStates[currentGamepad.index].shortcutPressed += 1;\n}\n}\n} else {\nwindow.BX_EXPOSED.resetControllerShortcut(currentGamepad.index);\nthis.bxHomeStates[currentGamepad.index] = {\nshortcutPressed: 0,\ntimestamp: currentGamepad.timestamp,\n};\n}\n} else if (this.bxHomeStates[currentGamepad.index]) {\nhijack = true;\nconst info = structuredClone(this.bxHomeStates[currentGamepad.index]);\n\nthis.bxHomeStates[currentGamepad.index] = null;\n\nif (info.shortcutPressed === 0) {\nconst fakeGamepadMappings = [{\nGamepadIndex: currentGamepad.index,\nA: 0,\nB: 0,\nX: 0,\nY: 0,\nLeftShoulder: 0,\nRightShoulder: 0,\nLeftTrigger: 0,\nRightTrigger: 0,\nView: 0,\nMenu: 0,\nLeftThumb: 0,\nRightThumb: 0,\nDPadUp: 0,\nDPadDown: 0,\nDPadLeft: 0,\nDPadRight: 0,\nNexus: 1,\nLeftThumbXAxis: 0,\nLeftThumbYAxis: 0,\nRightThumbXAxis: 0,\nRightThumbYAxis: 0,\nPhysicalPhysicality: 0,\nVirtualPhysicality: 0,\nDirty: true,\nVirtual: false,\n}];\n\nconst isLongPress = (currentGamepad.timestamp - info.timestamp) >= 500;\nintervalMs = isLongPress ? 500 : 100;\n\nthis.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings);\n} else {\nintervalMs = 4;\n}\n}\n\nif (hijack && intervalMs) {\nthis.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs);\n\nreturn;\n}\n}\n";
var expose_stream_session_default = "window.BX_EXPOSED.streamSession = this;\n\nconst orgSetMicrophoneState = this.setMicrophoneState.bind(this);\nthis.setMicrophoneState = state => {\n orgSetMicrophoneState(state);\n\n const evt = new Event(BxEvent.MICROPHONE_STATE_CHANGED);\n evt.microphoneState = state;\n\n window.dispatchEvent(evt);\n};\n\nwindow.dispatchEvent(new Event(BxEvent.STREAM_SESSION_READY));\n\n// Patch updateDimensions() to make native touch work correctly with WebGL2\nlet updateDimensionsStr = this.updateDimensions.toString();\n\nif (updateDimensionsStr.startsWith('function ')) {\n updateDimensionsStr = updateDimensionsStr.substring(9);\n}\n\n// if(r){\nconst renderTargetVar = updateDimensionsStr.match(/if\\((\\w+)\\){/)[1];\n\nupdateDimensionsStr = updateDimensionsStr.replaceAll(renderTargetVar + '.scroll', 'scroll');\n\nupdateDimensionsStr = updateDimensionsStr.replace(`if(${renderTargetVar}){`, `\nif (${renderTargetVar}) {\n const scrollWidth = ${renderTargetVar}.dataset.width ? parseInt(${renderTargetVar}.dataset.width) : ${renderTargetVar}.scrollWidth;\n const scrollHeight = ${renderTargetVar}.dataset.height ? parseInt(${renderTargetVar}.dataset.height) : ${renderTargetVar}.scrollHeight;\n`);\n\neval(`this.updateDimensions = function ${updateDimensionsStr}`);\n"; var expose_stream_session_default = "window.BX_EXPOSED.streamSession = this;\n\nconst orgSetMicrophoneState = this.setMicrophoneState.bind(this);\nthis.setMicrophoneState = state => {\norgSetMicrophoneState(state);\n\nconst evt = new Event(BxEvent.MICROPHONE_STATE_CHANGED);\nevt.microphoneState = state;\n\nwindow.dispatchEvent(evt);\n};\n\nwindow.dispatchEvent(new Event(BxEvent.STREAM_SESSION_READY));\n\nlet updateDimensionsStr = this.updateDimensions.toString();\n\nif (updateDimensionsStr.startsWith('function ')) {\nupdateDimensionsStr = updateDimensionsStr.substring(9);\n}\n\nconst renderTargetVar = updateDimensionsStr.match(/if\\((\\w+)\\){/)[1];\n\nupdateDimensionsStr = updateDimensionsStr.replaceAll(renderTargetVar + '.scroll', 'scroll');\n\nupdateDimensionsStr = updateDimensionsStr.replace(`if(${renderTargetVar}){`, `\nif (${renderTargetVar}) {\nconst scrollWidth = ${renderTargetVar}.dataset.width ? parseInt(${renderTargetVar}.dataset.width) : ${renderTargetVar}.scrollWidth;\nconst scrollHeight = ${renderTargetVar}.dataset.height ? parseInt(${renderTargetVar}.dataset.height) : ${renderTargetVar}.scrollHeight;\n`);\n\neval(`this.updateDimensions = function ${updateDimensionsStr}`);\n";
var local_co_op_enable_default = "let match;\nlet onGamepadChangedStr = this.onGamepadChanged.toString();\n\nif (onGamepadChangedStr.startsWith('function ')) {\n onGamepadChangedStr = onGamepadChangedStr.substring(9);\n}\n\nonGamepadChangedStr = onGamepadChangedStr.replaceAll('0', 'arguments[1]');\neval(`this.onGamepadChanged = function ${onGamepadChangedStr}`);\n\nlet onGamepadInputStr = this.onGamepadInput.toString();\n\nmatch = onGamepadInputStr.match(/(\\w+\\.GamepadIndex)/);\nif (match) {\n const gamepadIndexVar = match[0];\n onGamepadInputStr = onGamepadInputStr.replace('this.gamepadStates.get(', `this.gamepadStates.get(${gamepadIndexVar},`);\n eval(`this.onGamepadInput = function ${onGamepadInputStr}`);\n BxLogger.info('supportLocalCoOp', '✅ Successfully patched local co-op support');\n} else {\n BxLogger.error('supportLocalCoOp', '❌ Unable to patch local co-op support');\n}\n"; var local_co_op_enable_default = "let match;\nlet onGamepadChangedStr = this.onGamepadChanged.toString();\n\nif (onGamepadChangedStr.startsWith('function ')) {\nonGamepadChangedStr = onGamepadChangedStr.substring(9);\n}\n\nonGamepadChangedStr = onGamepadChangedStr.replaceAll('0', 'arguments[1]');\neval(`this.onGamepadChanged = function ${onGamepadChangedStr}`);\n\nlet onGamepadInputStr = this.onGamepadInput.toString();\n\nmatch = onGamepadInputStr.match(/(\\w+\\.GamepadIndex)/);\nif (match) {\nconst gamepadIndexVar = match[0];\nonGamepadInputStr = onGamepadInputStr.replace('this.gamepadStates.get(', `this.gamepadStates.get(${gamepadIndexVar},`);\neval(`this.onGamepadInput = function ${onGamepadInputStr}`);\nBxLogger.info('supportLocalCoOp', '✅ Successfully patched local co-op support');\n} else {\nBxLogger.error('supportLocalCoOp', '❌ Unable to patch local co-op support');\n}\n";
var set_currently_focused_interactable_default = "e && BxEvent.dispatch(window, BxEvent.NAVIGATION_FOCUS_CHANGED, {element: e});\n"; var set_currently_focused_interactable_default = "e && BxEvent.dispatch(window, BxEvent.NAVIGATION_FOCUS_CHANGED, {element: e});\n";
var remote_play_enable_default = "connectMode: window.BX_REMOTE_PLAY_CONFIG ? \"xhome-connect\" : \"cloud-connect\",\nremotePlayServerId: (window.BX_REMOTE_PLAY_CONFIG && window.BX_REMOTE_PLAY_CONFIG.serverId) || '',\n"; var remote_play_enable_default = "connectMode: window.BX_REMOTE_PLAY_CONFIG ? \"xhome-connect\" : \"cloud-connect\",\nremotePlayServerId: (window.BX_REMOTE_PLAY_CONFIG && window.BX_REMOTE_PLAY_CONFIG.serverId) || '',\n";
var remote_play_keep_alive_default = "const msg = JSON.parse(e);\nif (msg.reason === 'WarningForBeingIdle' && !window.location.pathname.includes('/launch/')) {\n try {\n this.sendKeepAlive();\n return;\n } catch (ex) { console.log(ex); }\n}\n"; var remote_play_keep_alive_default = "const msg = JSON.parse(e);\nif (msg.reason === 'WarningForBeingIdle' && !window.location.pathname.includes('/launch/')) {\ntry {\nthis.sendKeepAlive();\nreturn;\n} catch (ex) { console.log(ex); }\n}\n";
var vibration_adjust_default = "if (!window.BX_ENABLE_CONTROLLER_VIBRATION) {\n return void(0);\n}\n\nconst intensity = window.BX_VIBRATION_INTENSITY;\nif (intensity === 0) {\n return void(0);\n}\n\nif (intensity < 1) {\n e.leftMotorPercent *= intensity;\n e.rightMotorPercent *= intensity;\n e.leftTriggerMotorPercent *= intensity;\n e.rightTriggerMotorPercent *= intensity;\n}\n"; var vibration_adjust_default = "if (!window.BX_ENABLE_CONTROLLER_VIBRATION) {\nreturn void(0);\n}\n\nconst intensity = window.BX_VIBRATION_INTENSITY;\nif (intensity === 0) {\nreturn void(0);\n}\n\nif (intensity < 1) {\ne.leftMotorPercent *= intensity;\ne.rightMotorPercent *= intensity;\ne.leftTriggerMotorPercent *= intensity;\ne.rightTriggerMotorPercent *= intensity;\n}\n";
var FeatureGates = { var FeatureGates = {
PwaPrompt: !1, PwaPrompt: !1,
EnableWifiWarnings: !1, EnableWifiWarnings: !1,
@ -6942,86 +6942,86 @@ function patchSdpBitrate(sdp, video, audio) {
} }
return lines.join("\r\n"); return lines.join("\r\n");
} }
var clarity_boost_default = "attribute vec2 position;\n\nvoid main() {\n gl_Position = vec4(position, 0, 1);\n}\n"; var clarity_boost_default = "attribute vec4 position;\n\nvoid main() {\ngl_Position = position;\n}\n";
var clarity_boost_default2 = "const int FILTER_UNSHARP_MASKING = 1;\nconst int FILTER_CAS = 2;\n\nprecision highp float;\nuniform sampler2D data;\nuniform vec2 iResolution;\n\nuniform int filterId;\nuniform float sharpenFactor;\nuniform float brightness;\nuniform float contrast;\nuniform float saturation;\n\nvec3 textureAt(sampler2D tex, vec2 coord) {\n return texture2D(tex, coord / iResolution.xy).rgb;\n}\n\nvec3 clarityBoost(sampler2D tex, vec2 coord)\n{\n // Load a collection of samples in a 3x3 neighorhood, where e is the current pixel.\n // a b c\n // d e f\n // g h i\n vec3 a = textureAt(tex, coord + vec2(-1, 1));\n vec3 b = textureAt(tex, coord + vec2(0, 1));\n vec3 c = textureAt(tex, coord + vec2(1, 1));\n\n vec3 d = textureAt(tex, coord + vec2(-1, 0));\n vec3 e = textureAt(tex, coord);\n vec3 f = textureAt(tex, coord + vec2(1, 0));\n\n vec3 g = textureAt(tex, coord + vec2(-1, -1));\n vec3 h = textureAt(tex, coord + vec2(0, -1));\n vec3 i = textureAt(tex, coord + vec2(1, -1));\n\n if (filterId == FILTER_CAS) {\n // Soft min and max.\n // a b c b\n // d e f * 0.5 + d e f * 0.5\n // g h i h\n // These are 2.0x bigger (factored out the extra multiply).\n vec3 minRgb = min(min(min(d, e), min(f, b)), h);\n vec3 minRgb2 = min(min(a, c), min(g, i));\n minRgb += min(minRgb, minRgb2);\n\n vec3 maxRgb = max(max(max(d, e), max(f, b)), h);\n vec3 maxRgb2 = max(max(a, c), max(g, i));\n maxRgb += max(maxRgb, maxRgb2);\n\n // Smooth minimum distance to signal limit divided by smooth max.\n vec3 reciprocalMaxRgb = 1.0 / maxRgb;\n vec3 amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, 0.0, 1.0);\n\n // Shaping amount of sharpening.\n amplifyRgb = inversesqrt(amplifyRgb);\n\n float contrast = 0.8;\n float peak = -3.0 * contrast + 8.0;\n vec3 weightRgb = -(1.0 / (amplifyRgb * peak));\n\n vec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);\n\n // 0 w 0\n // Filter shape: w 1 w\n // 0 w 0\n vec3 window = (b + d) + (f + h);\n vec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0);\n\n outColor = mix(e, outColor, sharpenFactor / 2.0);\n\n return outColor;\n } else if (filterId == FILTER_UNSHARP_MASKING) {\n vec3 gaussianBlur = (a * 1.0 + b * 2.0 + c * 1.0 +\n d * 2.0 + e * 4.0 + f * 2.0 +\n g * 1.0 + h * 2.0 + i * 1.0) / 16.0;\n\n // Return edge detection\n return e + (e - gaussianBlur) * sharpenFactor / 3.0;\n }\n\n return e;\n}\n\nvec3 adjustBrightness(vec3 color) {\n return (1.0 + brightness) * color;\n}\n\nvec3 adjustContrast(vec3 color) {\n return 0.5 + (1.0 + contrast) * (color - 0.5);\n}\n\nvec3 adjustSaturation(vec3 color) {\n const vec3 luminosityFactor = vec3(0.2126, 0.7152, 0.0722);\n vec3 grayscale = vec3(dot(color, luminosityFactor));\n\n return mix(grayscale, color, 1.0 + saturation);\n}\n\nvoid main() {\n vec3 color;\n\n if (sharpenFactor > 0.0) {\n color = clarityBoost(data, gl_FragCoord.xy);\n } else {\n color = textureAt(data, gl_FragCoord.xy);\n }\n\n if (saturation != 0.0) {\n color = adjustSaturation(color);\n }\n\n if (contrast != 0.0) {\n color = adjustContrast(color);\n }\n\n if (brightness != 0.0) {\n color = adjustBrightness(color);\n }\n\n gl_FragColor = vec4(color, 1.0);\n}\n"; var clarity_boost_default2 = "precision mediump float;\nuniform sampler2D data;\nuniform vec2 iResolution;\n\nconst int FILTER_UNSHARP_MASKING = 1;\nconst int FILTER_CAS = 2;\n\nconst float CAS_CONTRAST_PEAK = (-3.0 * 0.8 + 8.0);\n\nconst vec3 LUMINOSITY_FACTOR = vec3(0.2126, 0.7152, 0.0722);\n\nuniform int filterId;\nuniform float sharpenFactor;\nuniform float brightness;\nuniform float contrast;\nuniform float saturation;\n\nvec3 clarityBoost(sampler2D tex, vec2 coord) {\nvec2 texelSize = 1.0 / iResolution.xy;\n\nvec3 a = texture2D(tex, coord + texelSize * vec2(-1, 1)).rgb;\nvec3 b = texture2D(tex, coord + texelSize * vec2(0, 1)).rgb;\nvec3 c = texture2D(tex, coord + texelSize * vec2(1, 1)).rgb;\n\nvec3 d = texture2D(tex, coord + texelSize * vec2(-1, 0)).rgb;\nvec3 e = texture2D(tex, coord).rgb;\nvec3 f = texture2D(tex, coord + texelSize * vec2(1, 0)).rgb;\n\nvec3 g = texture2D(tex, coord + texelSize * vec2(-1, -1)).rgb;\nvec3 h = texture2D(tex, coord + texelSize * vec2(0, -1)).rgb;\nvec3 i = texture2D(tex, coord + texelSize * vec2(1, -1)).rgb;\n\nif (filterId == FILTER_CAS) {\nvec3 minRgb = min(min(min(d, e), min(f, b)), h);\nvec3 minRgb2 = min(min(a, c), min(g, i));\nminRgb += min(minRgb, minRgb2);\n\nvec3 maxRgb = max(max(max(d, e), max(f, b)), h);\nvec3 maxRgb2 = max(max(a, c), max(g, i));\nmaxRgb += max(maxRgb, maxRgb2);\n\nvec3 reciprocalMaxRgb = 1.0 / maxRgb;\nvec3 amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, 0.0, 1.0);\n\namplifyRgb = inversesqrt(amplifyRgb);\n\nvec3 weightRgb = -(1.0 / (amplifyRgb * CAS_CONTRAST_PEAK));\nvec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);\n\nvec3 window = (b + d) + (f + h);\nvec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0);\n\noutColor = mix(e, outColor, sharpenFactor / 2.0);\n\nreturn outColor;\n} else if (filterId == FILTER_UNSHARP_MASKING) {\nvec3 gaussianBlur = (a + c + g + i) * 1.0 +\n(b + d + f + h) * 2.0 +\ne * 4.0;\ngaussianBlur /= 16.0;\n\nreturn e + (e - gaussianBlur) * sharpenFactor / 3.0;\n}\n\nreturn e;\n}\n\nvoid main() {\nvec3 color;\nvec2 uv = gl_FragCoord.xy / iResolution.xy;\n\nif (sharpenFactor > 0.0) {\ncolor = clarityBoost(data, uv);\n} else {\ncolor = texture2D(data, uv).rgb;\n}\n\nif (saturation != 1.0) {\nvec3 grayscale = vec3(dot(color, LUMINOSITY_FACTOR));\ncolor = mix(grayscale, color, saturation);\n}\n\nif (contrast != 1.0) {\ncolor = 0.5 + contrast * (color - 0.5);\n}\n\nif (brightness != 1.0) {\ncolor = brightness * color;\n}\n\ngl_FragColor = vec4(color, 1.0);\n}\n";
var LOG_TAG7 = "WebGL2Player"; var LOG_TAG7 = "WebGL2Player";
class WebGL2Player { class WebGL2Player {
#$video; $video;
#$canvas; $canvas;
#gl = null; gl = null;
#resources = []; resources = [];
#program = null; program = null;
#stopped = !1; stopped = !1;
#options = { options = {
filterId: 1, filterId: 1,
sharpenFactor: 0, sharpenFactor: 0,
brightness: 0, brightness: 0,
contrast: 0, contrast: 0,
saturation: 0 saturation: 0
}; };
#animFrameId = null; animFrameId = null;
constructor($video) { constructor($video) {
BxLogger.info(LOG_TAG7, "Initialize"), this.#$video = $video; BxLogger.info(LOG_TAG7, "Initialize"), this.$video = $video;
const $canvas = document.createElement("canvas"); const $canvas = document.createElement("canvas");
$canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.#$canvas = $canvas, this.#setupShaders(), this.#setupRendering(), $video.insertAdjacentElement("afterend", $canvas); $canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, this.setupShaders(), this.setupRendering(), $video.insertAdjacentElement("afterend", $canvas);
} }
setFilter(filterId, update = !0) { setFilter(filterId, update = !0) {
this.#options.filterId = filterId, update && this.updateCanvas(); this.options.filterId = filterId, update && this.updateCanvas();
} }
setSharpness(sharpness, update = !0) { setSharpness(sharpness, update = !0) {
this.#options.sharpenFactor = sharpness, update && this.updateCanvas(); this.options.sharpenFactor = sharpness, update && this.updateCanvas();
} }
setBrightness(brightness, update = !0) { setBrightness(brightness, update = !0) {
this.#options.brightness = (brightness - 100) / 100, update && this.updateCanvas(); this.options.brightness = 1 + (brightness - 100) / 100, update && this.updateCanvas();
} }
setContrast(contrast, update = !0) { setContrast(contrast, update = !0) {
this.#options.contrast = (contrast - 100) / 100, update && this.updateCanvas(); this.options.contrast = 1 + (contrast - 100) / 100, update && this.updateCanvas();
} }
setSaturation(saturation, update = !0) { setSaturation(saturation, update = !0) {
this.#options.saturation = (saturation - 100) / 100, update && this.updateCanvas(); this.options.saturation = 1 + (saturation - 100) / 100, update && this.updateCanvas();
} }
getCanvas() { getCanvas() {
return this.#$canvas; return this.$canvas;
} }
updateCanvas() { updateCanvas() {
const gl = this.#gl, program = this.#program; const gl = this.gl, program = this.program;
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.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);
} }
drawFrame() { drawFrame() {
const gl = this.#gl, $video = this.#$video; const gl = this.gl, $video = this.$video;
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, $video), gl.drawArrays(gl.TRIANGLES, 0, 6); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, $video), gl.drawArrays(gl.TRIANGLES, 0, 6);
} }
#setupRendering() { setupRendering() {
let animate; let animate;
if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) { if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
const $video = this.#$video; const $video = this.$video;
animate = () => { animate = () => {
if (this.#stopped) return; if (this.stopped) return;
this.drawFrame(), this.#animFrameId = $video.requestVideoFrameCallback(animate); this.drawFrame(), this.animFrameId = $video.requestVideoFrameCallback(animate);
}, this.#animFrameId = $video.requestVideoFrameCallback(animate); }, this.animFrameId = $video.requestVideoFrameCallback(animate);
} else animate = () => { } else animate = () => {
if (this.#stopped) return; if (this.stopped) return;
this.drawFrame(), this.#animFrameId = requestAnimationFrame(animate); this.drawFrame(), this.animFrameId = requestAnimationFrame(animate);
}, this.#animFrameId = requestAnimationFrame(animate); }, this.animFrameId = requestAnimationFrame(animate);
} }
#setupShaders() { setupShaders() {
BxLogger.info(LOG_TAG7, "Setting up", getPref("video_power_preference")); BxLogger.info(LOG_TAG7, "Setting up", getPref("video_power_preference"));
const gl = this.#$canvas.getContext("webgl", { const gl = this.$canvas.getContext("webgl", {
isBx: !0, isBx: !0,
antialias: !0, antialias: !0,
alpha: !1, alpha: !1,
powerPreference: getPref("video_power_preference") powerPreference: getPref("video_power_preference")
}); });
this.#gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth); this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);
const vShader = gl.createShader(gl.VERTEX_SHADER); const vShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader); gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader);
const fShader = gl.createShader(gl.FRAGMENT_SHADER); const fShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader); gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader);
const program = gl.createProgram(); const program = gl.createProgram();
if (this.#program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`); if (this.program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`);
this.updateCanvas(); this.updateCanvas();
const buffer = gl.createBuffer(); const buffer = gl.createBuffer();
this.#resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ this.resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1,
-1, -1,
1, 1,
@ -7036,51 +7036,51 @@ class WebGL2Player {
1 1
]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0); ]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0);
const texture = gl.createTexture(); const texture = gl.createTexture();
this.#resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0); this.resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0);
} }
resume() { resume() {
this.stop(), this.#stopped = !1, BxLogger.info(LOG_TAG7, "Resume"), this.#$canvas.classList.remove("bx-gone"), this.#setupRendering(); this.stop(), this.stopped = !1, BxLogger.info(LOG_TAG7, "Resume"), this.$canvas.classList.remove("bx-gone"), this.setupRendering();
} }
stop() { stop() {
if (BxLogger.info(LOG_TAG7, "Stop"), this.#$canvas.classList.add("bx-gone"), this.#stopped = !0, this.#animFrameId) { if (BxLogger.info(LOG_TAG7, "Stop"), this.$canvas.classList.add("bx-gone"), this.stopped = !0, this.animFrameId) {
if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.#$video.cancelVideoFrameCallback(this.#animFrameId); if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.$video.cancelVideoFrameCallback(this.animFrameId);
else cancelAnimationFrame(this.#animFrameId); else cancelAnimationFrame(this.animFrameId);
this.#animFrameId = null; this.animFrameId = null;
} }
} }
destroy() { destroy() {
BxLogger.info(LOG_TAG7, "Destroy"), this.stop(); BxLogger.info(LOG_TAG7, "Destroy"), 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 (let resource of this.#resources) for (let resource of this.resources)
if (resource instanceof WebGLProgram) gl.useProgram(null), gl.deleteProgram(resource); if (resource instanceof WebGLProgram) gl.useProgram(null), gl.deleteProgram(resource);
else if (resource instanceof WebGLShader) gl.deleteShader(resource); else if (resource instanceof WebGLShader) gl.deleteShader(resource);
else if (resource instanceof WebGLTexture) gl.deleteTexture(resource); else if (resource instanceof WebGLTexture) gl.deleteTexture(resource);
else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource); else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource);
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;
} }
} }
class StreamPlayer { class StreamPlayer {
#$video; $video;
#playerType = "default"; playerType = "default";
#options = {}; options = {};
#webGL2Player = null; webGL2Player = null;
#$videoCss = null; $videoCss = null;
#$usmMatrix = null; $usmMatrix = null;
constructor($video, type, options) { constructor($video, type, options) {
this.#setupVideoElements(), this.#$video = $video, this.#options = options || {}, this.setPlayerType(type); this.setupVideoElements(), this.$video = $video, this.options = options || {}, this.setPlayerType(type);
} }
#setupVideoElements() { setupVideoElements() {
if (this.#$videoCss = document.getElementById("bx-video-css"), this.#$videoCss) { if (this.$videoCss = document.getElementById("bx-video-css"), this.$videoCss) {
this.#$usmMatrix = this.#$videoCss.querySelector("#bx-filter-usm-matrix"); this.$usmMatrix = this.$videoCss.querySelector("#bx-filter-usm-matrix");
return; return;
} }
const $fragment = document.createDocumentFragment(); const $fragment = document.createDocumentFragment();
this.#$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.#$videoCss); this.$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.$videoCss);
const $svg = CE("svg", { const $svg = CE("svg", {
id: "bx-video-filters", id: "bx-video-filters",
xmlns: "http://www.w3.org/2000/svg", xmlns: "http://www.w3.org/2000/svg",
@ -7088,31 +7088,31 @@ class StreamPlayer {
}, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, CE("filter", { }, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, 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"
})))); }))));
$fragment.appendChild($svg), document.documentElement.appendChild($fragment); $fragment.appendChild($svg), document.documentElement.appendChild($fragment);
} }
#getVideoPlayerFilterStyle() { getVideoPlayerFilterStyle() {
const filters = [], sharpness = this.#options.sharpness || 0; const filters = [], sharpness = this.options.sharpness || 0;
if (this.#options.processing === "usm" && sharpness != 0) { if (this.options.processing === "usm" && sharpness != 0) {
const matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`; const matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`;
this.#$usmMatrix?.setAttributeNS(null, "kernelMatrix", matrix), filters.push("url(#bx-filter-usm)"); 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}%)`); 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}%)`); 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}%)`); if (brightness != 100) filters.push(`brightness(${brightness}%)`);
return filters.join(" "); return filters.join(" ");
} }
#resizePlayer() { resizePlayer() {
const PREF_RATIO = getPref("video_ratio"), $video = this.#$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport; const PREF_RATIO = getPref("video_ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport;
let $webGL2Canvas; let $webGL2Canvas;
if (this.#playerType == "webgl2") $webGL2Canvas = this.#webGL2Player?.getCanvas(); if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas();
let targetWidth, targetHeight, targetObjectFit; let targetWidth, targetHeight, targetObjectFit;
if (PREF_RATIO.includes(":")) { if (PREF_RATIO.includes(":")) {
const tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]); const tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]);
@ -7123,54 +7123,54 @@ class StreamPlayer {
width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString(), targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill"; width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString(), targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill";
} else targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString(); } else targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString();
if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, $webGL2Canvas) $webGL2Canvas.style.width = targetWidth, $webGL2Canvas.style.height = targetHeight, $webGL2Canvas.style.objectFit = targetObjectFit; if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, $webGL2Canvas) $webGL2Canvas.style.width = targetWidth, $webGL2Canvas.style.height = targetHeight, $webGL2Canvas.style.objectFit = targetObjectFit;
if (isNativeTouchGame && this.#playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions(); if (isNativeTouchGame && this.playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions();
} }
setPlayerType(type, refreshPlayer = !1) { setPlayerType(type, refreshPlayer = !1) {
if (this.#playerType !== type) if (type === "webgl2") { if (this.playerType !== type) if (type === "webgl2") {
if (!this.#webGL2Player) this.#webGL2Player = new WebGL2Player(this.#$video); if (!this.webGL2Player) this.webGL2Player = new WebGL2Player(this.$video);
else this.#webGL2Player.resume(); else this.webGL2Player.resume();
this.#$videoCss.textContent = "", this.#$video.classList.add("bx-pixel"); this.$videoCss.textContent = "", this.$video.classList.add("bx-pixel");
} else this.#webGL2Player?.stop(), this.#$video.classList.remove("bx-pixel"); } else this.webGL2Player?.stop(), this.$video.classList.remove("bx-pixel");
this.#playerType = type, refreshPlayer && this.refreshPlayer(); this.playerType = type, refreshPlayer && this.refreshPlayer();
} }
setOptions(options, refreshPlayer = !1) { setOptions(options, refreshPlayer = !1) {
this.#options = options, refreshPlayer && this.refreshPlayer(); this.options = options, refreshPlayer && this.refreshPlayer();
} }
updateOptions(options, refreshPlayer = !1) { updateOptions(options, refreshPlayer = !1) {
this.#options = Object.assign(this.#options, options), refreshPlayer && this.refreshPlayer(); this.options = Object.assign(this.options, options), refreshPlayer && this.refreshPlayer();
} }
getPlayerElement(playerType) { getPlayerElement(playerType) {
if (typeof playerType === "undefined") playerType = this.#playerType; if (typeof playerType === "undefined") playerType = this.playerType;
if (playerType === "webgl2") return this.#webGL2Player?.getCanvas(); if (playerType === "webgl2") return this.webGL2Player?.getCanvas();
return this.#$video; return this.$video;
} }
getWebGL2Player() { getWebGL2Player() {
return this.#webGL2Player; return this.webGL2Player;
} }
refreshPlayer() { refreshPlayer() {
if (this.#playerType === "webgl2") { if (this.playerType === "webgl2") {
const options = this.#options, webGL2Player = this.#webGL2Player; const options = this.options, webGL2Player = this.webGL2Player;
if (options.processing === "usm") webGL2Player.setFilter(1); if (options.processing === "usm") webGL2Player.setFilter(1);
else webGL2Player.setFilter(2); else webGL2Player.setFilter(2);
Screenshot.updateCanvasFilters("none"), webGL2Player.setSharpness(options.sharpness || 0), webGL2Player.setSaturation(options.saturation || 100), webGL2Player.setContrast(options.contrast || 100), webGL2Player.setBrightness(options.brightness || 100); Screenshot.updateCanvasFilters("none"), webGL2Player.setSharpness(options.sharpness || 0), webGL2Player.setSaturation(options.saturation || 100), webGL2Player.setContrast(options.contrast || 100), webGL2Player.setBrightness(options.brightness || 100);
} else { } else {
let filters = this.#getVideoPlayerFilterStyle(), videoCss = ""; let filters = this.getVideoPlayerFilterStyle(), videoCss = "";
if (filters) videoCss += `filter: ${filters} !important;`; if (filters) videoCss += `filter: ${filters} !important;`;
if (getPref("screenshot_apply_filters")) Screenshot.updateCanvasFilters(filters); if (getPref("screenshot_apply_filters")) Screenshot.updateCanvasFilters(filters);
let css = ""; let css = "";
if (videoCss) css = `#game-stream video { ${videoCss} }`; if (videoCss) css = `#game-stream video { ${videoCss} }`;
this.#$videoCss.textContent = css; this.$videoCss.textContent = css;
} }
this.#resizePlayer(); this.resizePlayer();
} }
reloadPlayer() { reloadPlayer() {
this.#cleanUpWebGL2Player(), this.#playerType = "default", this.setPlayerType("webgl2", !1); this.cleanUpWebGL2Player(), this.playerType = "default", this.setPlayerType("webgl2", !1);
} }
#cleanUpWebGL2Player() { cleanUpWebGL2Player() {
this.#webGL2Player?.destroy(), this.#webGL2Player = null; this.webGL2Player?.destroy(), this.webGL2Player = null;
} }
destroy() { destroy() {
this.#cleanUpWebGL2Player(); this.cleanUpWebGL2Player();
} }
} }
function patchVideoApi() { function patchVideoApi() {

View File

@ -1,9 +1,15 @@
precision mediump float;
uniform sampler2D data;
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;
precision highp float; // constrast = 0.8
uniform sampler2D data; const float CAS_CONTRAST_PEAK = (-3.0 * 0.8 + 8.0);
uniform vec2 iResolution;
// 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;
@ -11,27 +17,24 @@ uniform float brightness;
uniform float contrast; uniform float contrast;
uniform float saturation; uniform float saturation;
vec3 textureAt(sampler2D tex, vec2 coord) { vec3 clarityBoost(sampler2D tex, vec2 coord) {
return texture2D(tex, coord / iResolution.xy).rgb; 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 = texture2D(tex, coord + texelSize * vec2(-1, 1)).rgb;
vec3 b = textureAt(tex, coord + vec2(0, 1)); vec3 b = texture2D(tex, coord + texelSize * vec2(0, 1)).rgb;
vec3 c = textureAt(tex, coord + vec2(1, 1)); vec3 c = texture2D(tex, coord + texelSize * vec2(1, 1)).rgb;
vec3 d = textureAt(tex, coord + vec2(-1, 0)); vec3 d = texture2D(tex, coord + texelSize * vec2(-1, 0)).rgb;
vec3 e = textureAt(tex, coord); vec3 e = texture2D(tex, coord).rgb;
vec3 f = textureAt(tex, coord + vec2(1, 0)); vec3 f = texture2D(tex, coord + texelSize * vec2(1, 0)).rgb;
vec3 g = textureAt(tex, coord + vec2(-1, -1)); vec3 g = texture2D(tex, coord + texelSize * vec2(-1, -1)).rgb;
vec3 h = textureAt(tex, coord + vec2(0, -1)); vec3 h = texture2D(tex, coord + texelSize * vec2(0, -1)).rgb;
vec3 i = textureAt(tex, coord + vec2(1, -1)); vec3 i = texture2D(tex, coord + texelSize * vec2(1, -1)).rgb;
if (filterId == FILTER_CAS) { if (filterId == FILTER_CAS) {
// Soft min and max. // Soft min and max.
@ -54,10 +57,7 @@ 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
@ -70,9 +70,10 @@ vec3 clarityBoost(sampler2D tex, vec2 coord)
return outColor; return outColor;
} else if (filterId == FILTER_UNSHARP_MASKING) { } else if (filterId == FILTER_UNSHARP_MASKING) {
vec3 gaussianBlur = (a * 1.0 + b * 2.0 + c * 1.0 + vec3 gaussianBlur = (a + c + g + i) * 1.0 +
d * 2.0 + e * 4.0 + f * 2.0 + (b + d + f + h) * 2.0 +
g * 1.0 + h * 2.0 + i * 1.0) / 16.0; e * 4.0;
gaussianBlur /= 16.0;
// Return edge detection // Return edge detection
return e + (e - gaussianBlur) * sharpenFactor / 3.0; return e + (e - gaussianBlur) * sharpenFactor / 3.0;
@ -81,40 +82,30 @@ vec3 clarityBoost(sampler2D tex, vec2 coord)
return e; 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; vec3 color;
vec2 uv = gl_FragCoord.xy / iResolution.xy;
if (sharpenFactor > 0.0) { if (sharpenFactor > 0.0) {
color = clarityBoost(data, gl_FragCoord.xy); color = clarityBoost(data, uv);
} else { } else {
color = textureAt(data, gl_FragCoord.xy); color = texture2D(data, uv).rgb;
} }
if (saturation != 0.0) { // Saturation
color = adjustSaturation(color); if (saturation != 1.0) {
vec3 grayscale = vec3(dot(color, LUMINOSITY_FACTOR));
color = mix(grayscale, color, saturation);
} }
if (contrast != 0.0) { // Contrast
color = adjustContrast(color); if (contrast != 1.0) {
color = 0.5 + contrast * (color - 0.5);
} }
if (brightness != 0.0) { // Brightness
color = adjustBrightness(color); if (brightness != 1.0) {
color = brightness * color;
} }
gl_FragColor = vec4(color, 1.0); gl_FragColor = vec4(color, 1.0);

View File

@ -1,5 +1,5 @@
attribute vec2 position; attribute vec4 position;
void main() { void main() {
gl_Position = vec4(position, 0, 1); gl_Position = position;
} }

View File

@ -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,112 @@ export class WebGL2Player {
saturation: 0.0, saturation: 0.0,
}; };
#animFrameId: number | null = null; 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();
} }
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!; const gl = this.gl!;
const $video = this.#$video; 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('webgl', {
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 +145,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 +162,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 +179,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 +197,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 +224,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 +241,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;
} }
} }

View File

@ -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();
} }
} }