mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-03 06:07:19 +02:00
Add WebGPU renderer (#648)
This commit is contained in:
parent
39ecef976c
commit
fd665b6fcd
2
build.sh
2
build.sh
@ -5,8 +5,8 @@ build_all () {
|
|||||||
printf "\033c"
|
printf "\033c"
|
||||||
|
|
||||||
# Build all variants
|
# Build all variants
|
||||||
bun build.ts --version $1 --variant full --meta
|
|
||||||
bun build.ts --version $1 --variant full --pretty
|
bun build.ts --version $1 --variant full --pretty
|
||||||
|
bun build.ts --version $1 --variant full --meta
|
||||||
# bun build.ts --version $1 --variant lite
|
# bun build.ts --version $1 --variant lite
|
||||||
|
|
||||||
# Wait for key
|
# Wait for key
|
||||||
|
9
bun.lock
9
bun.lock
@ -3,10 +3,11 @@
|
|||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.1.14",
|
"@types/bun": "^1.2.0",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.10",
|
||||||
"@types/stylus": "^0.48.43",
|
"@types/stylus": "^0.48.43",
|
||||||
"eslint": "^9.17.0",
|
"@webgpu/types": "^0.1.53",
|
||||||
|
"eslint": "^9.19.0",
|
||||||
"eslint-plugin-compat": "^6.0.2",
|
"eslint-plugin-compat": "^6.0.2",
|
||||||
"stylus": "^0.64.0",
|
"stylus": "^0.64.0",
|
||||||
},
|
},
|
||||||
@ -60,6 +61,8 @@
|
|||||||
|
|
||||||
"@types/ws": ["@types/ws@8.5.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A=="],
|
"@types/ws": ["@types/ws@8.5.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A=="],
|
||||||
|
|
||||||
|
"@webgpu/types": ["@webgpu/types@0.1.53", "", {}, "sha512-x+BLw/opaz9LiVyrMsP75nO1Rg0QfrACUYIbVSfGwY/w0DiWIPYYrpte6us//KZXinxFAOJl0+C17L1Vi2vmDw=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
||||||
|
|
||||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||||
|
567
dist/better-xcloud.pretty.user.js
vendored
567
dist/better-xcloud.pretty.user.js
vendored
@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Better xCloud
|
// @name Better xCloud
|
||||||
// @namespace https://github.com/redphx
|
// @namespace https://github.com/redphx
|
||||||
// @version 6.3.2-beta
|
// @version 6.4.0-beta
|
||||||
// @description Improve Xbox Cloud Gaming (xCloud) experience
|
// @description Improve Xbox Cloud Gaming (xCloud) experience
|
||||||
// @author redphx
|
// @author redphx
|
||||||
// @license MIT
|
// @license MIT
|
||||||
@ -190,7 +190,7 @@ class UserAgent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var SCRIPT_VERSION = "6.3.2-beta", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface;
|
var SCRIPT_VERSION = "6.4.0-beta", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface;
|
||||||
UserAgent.init();
|
UserAgent.init();
|
||||||
var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = {
|
var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = {
|
||||||
supportedRegion: !0,
|
supportedRegion: !0,
|
||||||
@ -398,6 +398,7 @@ var SUPPORTED_LANGUAGES = {
|
|||||||
"zh-CN": "中文(简体)",
|
"zh-CN": "中文(简体)",
|
||||||
"zh-TW": "中文(繁體)"
|
"zh-TW": "中文(繁體)"
|
||||||
}, Texts = {
|
}, Texts = {
|
||||||
|
webgpu: "WebGPU",
|
||||||
achievements: "Achievements",
|
achievements: "Achievements",
|
||||||
activate: "Activate",
|
activate: "Activate",
|
||||||
activated: "Activated",
|
activated: "Activated",
|
||||||
@ -2033,6 +2034,266 @@ class ControllerShortcutsTable extends BasePresetsTable {
|
|||||||
BxLogger.info(this.LOG_TAG, "constructor()");
|
BxLogger.info(this.LOG_TAG, "constructor()");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var clarity_boost_default = `struct Params {
|
||||||
|
filterId: f32,
|
||||||
|
sharpness: f32,
|
||||||
|
brightness: f32,
|
||||||
|
contrast: f32,
|
||||||
|
saturation: f32,
|
||||||
|
};
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) position: vec4<f32>,
|
||||||
|
@location(0) uv: vec2<f32>,
|
||||||
|
};
|
||||||
|
@group(0) @binding(0) var ourSampler: sampler;
|
||||||
|
@group(0) @binding(1) var ourTexture: texture_external;
|
||||||
|
@group(0) @binding(2) var<uniform> ourParams: Params;
|
||||||
|
const FILTER_UNSHARP_MASKING: f32 = 1.0;
|
||||||
|
const CAS_CONTRAST_PEAK: f32 = 0.8 * -3.0 + 8.0;
|
||||||
|
const LUMINOSITY_FACTOR = vec3(0.299, 0.587, 0.114);
|
||||||
|
@vertex
|
||||||
|
fn vsMain(@location(0) pos: vec2<f32>) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
out.position = vec4(pos, 0.0, 1.0);
|
||||||
|
out.uv = (vec2(pos.x, 1.0 - (pos.y + 1.0)) + vec2(1.0, 1.0)) * 0.5;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
fn clarityBoost(coord: vec2<f32>, texSize: vec2<f32>, e: vec3<f32>) -> vec3<f32> {
|
||||||
|
let texelSize = 1.0 / texSize;
|
||||||
|
let a = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2(-1.0, 1.0)).rgb;
|
||||||
|
let b = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2( 0.0, 1.0)).rgb;
|
||||||
|
let c = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2( 1.0, 1.0)).rgb;
|
||||||
|
let d = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2(-1.0, 0.0)).rgb;
|
||||||
|
let f = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2( 1.0, 0.0)).rgb;
|
||||||
|
let g = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2(-1.0, -1.0)).rgb;
|
||||||
|
let h = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2( 0.0, -1.0)).rgb;
|
||||||
|
let i = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2( 1.0, -1.0)).rgb;
|
||||||
|
if ourParams.filterId == FILTER_UNSHARP_MASKING {
|
||||||
|
let gaussianBlur = (a + c + g + i) * 1.0 + (b + d + f + h) * 2.0 + e * 4.0;
|
||||||
|
let blurred = gaussianBlur / 16.0;
|
||||||
|
return e + (e - blurred) * (ourParams.sharpness / 3.0);
|
||||||
|
}
|
||||||
|
let minRgb = min(min(min(d, e), min(f, b)), h) + min(min(a, c), min(g, i));
|
||||||
|
let maxRgb = max(max(max(d, e), max(f, b)), h) + max(max(a, c), max(g, i));
|
||||||
|
let reciprocalMaxRgb = 1.0 / maxRgb;
|
||||||
|
var amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, vec3(0.0), vec3(1.0));
|
||||||
|
amplifyRgb = 1.0 / sqrt(amplifyRgb);
|
||||||
|
let weightRgb = -(1.0 / (amplifyRgb * CAS_CONTRAST_PEAK));
|
||||||
|
let reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);
|
||||||
|
let window = b + d + f + h;
|
||||||
|
let outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, vec3(0.0), vec3(1.0));
|
||||||
|
return mix(e, outColor, ourParams.sharpness / 2.0);
|
||||||
|
}
|
||||||
|
@fragment
|
||||||
|
fn fsMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let texSize = vec2<f32>(textureDimensions(ourTexture));
|
||||||
|
let center = textureSampleBaseClampToEdge(ourTexture, ourSampler, input.uv);
|
||||||
|
var adjustedRgb = clarityBoost(input.uv, texSize, center.rgb);
|
||||||
|
let gray = dot(adjustedRgb, LUMINOSITY_FACTOR);
|
||||||
|
adjustedRgb = mix(vec3(gray), adjustedRgb, ourParams.saturation);
|
||||||
|
adjustedRgb = (adjustedRgb - 0.5) * ourParams.contrast + 0.5;
|
||||||
|
adjustedRgb *= ourParams.brightness;
|
||||||
|
return vec4(adjustedRgb, 1.0);
|
||||||
|
}`;
|
||||||
|
class BaseStreamPlayer {
|
||||||
|
logTag;
|
||||||
|
playerType;
|
||||||
|
elementType;
|
||||||
|
$video;
|
||||||
|
options = {
|
||||||
|
processing: "usm",
|
||||||
|
sharpness: 0,
|
||||||
|
brightness: 1,
|
||||||
|
contrast: 1,
|
||||||
|
saturation: 1
|
||||||
|
};
|
||||||
|
isStopped = !1;
|
||||||
|
constructor(playerType, elementType, $video, logTag) {
|
||||||
|
this.playerType = playerType, this.elementType = elementType, this.$video = $video, this.logTag = logTag;
|
||||||
|
}
|
||||||
|
init() {
|
||||||
|
BxLogger.info(this.logTag, "Initialize");
|
||||||
|
}
|
||||||
|
updateOptions(newOptions, refresh = !1) {
|
||||||
|
this.options = Object.assign(this.options, newOptions), refresh && this.refreshPlayer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class BaseCanvasPlayer extends BaseStreamPlayer {
|
||||||
|
$canvas;
|
||||||
|
targetFps = 60;
|
||||||
|
frameInterval = 0;
|
||||||
|
lastFrameTime = 0;
|
||||||
|
animFrameId = null;
|
||||||
|
frameCallback;
|
||||||
|
boundDrawFrame;
|
||||||
|
constructor(playerType, $video, logTag) {
|
||||||
|
super(playerType, "canvas", $video, logTag);
|
||||||
|
let $canvas = document.createElement("canvas");
|
||||||
|
$canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, $video.insertAdjacentElement("afterend", this.$canvas);
|
||||||
|
let frameCallback;
|
||||||
|
if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
|
||||||
|
let $video2 = this.$video;
|
||||||
|
frameCallback = $video2.requestVideoFrameCallback.bind($video2);
|
||||||
|
} else frameCallback = requestAnimationFrame;
|
||||||
|
this.frameCallback = frameCallback, this.boundDrawFrame = this.drawFrame.bind(this);
|
||||||
|
}
|
||||||
|
async init() {
|
||||||
|
super.init(), await this.setupShaders(), this.setupRendering();
|
||||||
|
}
|
||||||
|
setTargetFps(target) {
|
||||||
|
this.targetFps = target, this.lastFrameTime = 0, this.frameInterval = target ? Math.floor(1000 / target) : 0;
|
||||||
|
}
|
||||||
|
getCanvas() {
|
||||||
|
return this.$canvas;
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
if (BxLogger.info(this.logTag, "Destroy"), this.isStopped = !0, this.animFrameId) {
|
||||||
|
if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.$video.cancelVideoFrameCallback(this.animFrameId);
|
||||||
|
else cancelAnimationFrame(this.animFrameId);
|
||||||
|
this.animFrameId = null;
|
||||||
|
}
|
||||||
|
if (this.$canvas.isConnected) this.$canvas.remove();
|
||||||
|
this.$canvas.width = 1, this.$canvas.height = 1;
|
||||||
|
}
|
||||||
|
toFilterId(processing) {
|
||||||
|
return processing === "cas" ? 2 : 1;
|
||||||
|
}
|
||||||
|
shouldDraw() {
|
||||||
|
if (this.targetFps >= 60) return !0;
|
||||||
|
else if (this.targetFps === 0) return !1;
|
||||||
|
let currentTime = performance.now();
|
||||||
|
if (currentTime - this.lastFrameTime < this.frameInterval) return !1;
|
||||||
|
return this.lastFrameTime = currentTime, !0;
|
||||||
|
}
|
||||||
|
drawFrame() {
|
||||||
|
if (this.isStopped) return;
|
||||||
|
if (this.animFrameId = this.frameCallback(this.boundDrawFrame), !this.shouldDraw()) return;
|
||||||
|
this.updateFrame();
|
||||||
|
}
|
||||||
|
setupRendering() {
|
||||||
|
this.animFrameId = this.frameCallback(this.boundDrawFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class WebGPUPlayer extends BaseCanvasPlayer {
|
||||||
|
static device;
|
||||||
|
context;
|
||||||
|
pipeline;
|
||||||
|
sampler;
|
||||||
|
bindGroup;
|
||||||
|
optionsUpdated = !1;
|
||||||
|
paramsBuffer;
|
||||||
|
vertexBuffer;
|
||||||
|
static async prepare() {
|
||||||
|
if (!navigator.gpu) {
|
||||||
|
BxEventBus.Script.emit("webgpu.ready", {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let adapter = await navigator.gpu.requestAdapter();
|
||||||
|
if (adapter) WebGPUPlayer.device = await adapter.requestDevice(), WebGPUPlayer.device?.addEventListener("uncapturederror", (e) => {
|
||||||
|
console.error(e.error.message);
|
||||||
|
});
|
||||||
|
} catch (ex) {
|
||||||
|
alert(ex);
|
||||||
|
}
|
||||||
|
BxEventBus.Script.emit("webgpu.ready", {});
|
||||||
|
}
|
||||||
|
constructor($video) {
|
||||||
|
super("webgpu", $video, "WebGPUPlayer");
|
||||||
|
}
|
||||||
|
setupShaders() {
|
||||||
|
if (this.context = this.$canvas.getContext("webgpu"), !this.context) {
|
||||||
|
alert("Can't initiate context");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let format = navigator.gpu.getPreferredCanvasFormat();
|
||||||
|
this.context.configure({
|
||||||
|
device: WebGPUPlayer.device,
|
||||||
|
format,
|
||||||
|
alphaMode: "opaque"
|
||||||
|
}), this.vertexBuffer = WebGPUPlayer.device.createBuffer({
|
||||||
|
label: "vertex buffer",
|
||||||
|
size: 24,
|
||||||
|
usage: GPUBufferUsage.VERTEX,
|
||||||
|
mappedAtCreation: !0
|
||||||
|
});
|
||||||
|
let mappedRange = this.vertexBuffer.getMappedRange();
|
||||||
|
new Float32Array(mappedRange).set([
|
||||||
|
-1,
|
||||||
|
3,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
3,
|
||||||
|
-1
|
||||||
|
]), this.vertexBuffer.unmap();
|
||||||
|
let shaderModule = WebGPUPlayer.device.createShaderModule({ code: clarity_boost_default });
|
||||||
|
this.pipeline = WebGPUPlayer.device.createRenderPipeline({
|
||||||
|
layout: "auto",
|
||||||
|
vertex: {
|
||||||
|
module: shaderModule,
|
||||||
|
entryPoint: "vsMain",
|
||||||
|
buffers: [{
|
||||||
|
arrayStride: 8,
|
||||||
|
attributes: [{
|
||||||
|
format: "float32x2",
|
||||||
|
offset: 0,
|
||||||
|
shaderLocation: 0
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
fragment: {
|
||||||
|
module: shaderModule,
|
||||||
|
entryPoint: "fsMain",
|
||||||
|
targets: [{ format }]
|
||||||
|
},
|
||||||
|
primitive: { topology: "triangle-list" }
|
||||||
|
}), this.sampler = WebGPUPlayer.device.createSampler({ magFilter: "linear", minFilter: "linear" }), this.updateCanvas();
|
||||||
|
}
|
||||||
|
prepareUniformBuffer(value, classType) {
|
||||||
|
let uniform = new classType(value), uniformBuffer = WebGPUPlayer.device.createBuffer({
|
||||||
|
size: uniform.byteLength,
|
||||||
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
||||||
|
});
|
||||||
|
return WebGPUPlayer.device.queue.writeBuffer(uniformBuffer, 0, uniform), uniformBuffer;
|
||||||
|
}
|
||||||
|
updateCanvas() {
|
||||||
|
let externalTexture = WebGPUPlayer.device.importExternalTexture({ source: this.$video });
|
||||||
|
if (!this.optionsUpdated) this.paramsBuffer = this.prepareUniformBuffer([
|
||||||
|
this.toFilterId(this.options.processing),
|
||||||
|
this.options.sharpness,
|
||||||
|
this.options.brightness / 100,
|
||||||
|
this.options.contrast / 100,
|
||||||
|
this.options.saturation / 100
|
||||||
|
], Float32Array), this.optionsUpdated = !0;
|
||||||
|
this.bindGroup = WebGPUPlayer.device.createBindGroup({
|
||||||
|
layout: this.pipeline.getBindGroupLayout(0),
|
||||||
|
entries: [
|
||||||
|
{ binding: 0, resource: this.sampler },
|
||||||
|
{ binding: 1, resource: externalTexture },
|
||||||
|
{ binding: 2, resource: { buffer: this.paramsBuffer } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateFrame() {
|
||||||
|
this.updateCanvas();
|
||||||
|
let commandEncoder = WebGPUPlayer.device.createCommandEncoder(), passEncoder = commandEncoder.beginRenderPass({
|
||||||
|
colorAttachments: [{
|
||||||
|
view: this.context.getCurrentTexture().createView(),
|
||||||
|
loadOp: "clear",
|
||||||
|
storeOp: "store",
|
||||||
|
clearValue: [0, 0, 0, 1]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
passEncoder.setPipeline(this.pipeline), passEncoder.setBindGroup(0, this.bindGroup), passEncoder.setVertexBuffer(0, this.vertexBuffer), passEncoder.draw(3), passEncoder.end(), WebGPUPlayer.device.queue.submit([commandEncoder.finish()]);
|
||||||
|
}
|
||||||
|
refreshPlayer() {
|
||||||
|
this.optionsUpdated = !1, this.updateCanvas();
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
if (super.destroy(), this.isStopped = !0, this.pipeline = null, this.bindGroup = null, this.sampler = null, this.paramsBuffer?.destroy(), this.paramsBuffer = null, this.vertexBuffer?.destroy(), this.vertexBuffer = null, this.context) this.context.unconfigure(), this.context = null;
|
||||||
|
console.log("WebGPU context successfully freed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
class StreamSettingsStorage extends BaseSettingsStorage {
|
class StreamSettingsStorage extends BaseSettingsStorage {
|
||||||
static DEFINITIONS = {
|
static DEFINITIONS = {
|
||||||
"deviceVibration.mode": {
|
"deviceVibration.mode": {
|
||||||
@ -2146,11 +2407,17 @@ class StreamSettingsStorage extends BaseSettingsStorage {
|
|||||||
default: "default",
|
default: "default",
|
||||||
options: {
|
options: {
|
||||||
default: t("default"),
|
default: t("default"),
|
||||||
webgl2: t("webgl2")
|
webgl2: t("webgl2"),
|
||||||
|
webgpu: `${t("webgpu")} (${t("experimental")})`
|
||||||
},
|
},
|
||||||
suggest: {
|
suggest: {
|
||||||
lowest: "default",
|
lowest: "default",
|
||||||
highest: "webgl2"
|
highest: "webgl2"
|
||||||
|
},
|
||||||
|
ready: (setting) => {
|
||||||
|
BxEventBus.Script.on("webgpu.ready", () => {
|
||||||
|
if (!navigator.gpu || !WebGPUPlayer.device) delete setting.options["webgpu"];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"video.processing": {
|
"video.processing": {
|
||||||
@ -4187,9 +4454,8 @@ class SettingsManager {
|
|||||||
},
|
},
|
||||||
"video.player.powerPreference": {
|
"video.player.powerPreference": {
|
||||||
onChange: () => {
|
onChange: () => {
|
||||||
let streamPlayer = STATES.currentStream.streamPlayer;
|
if (!STATES.currentStream.streamPlayerManager) return;
|
||||||
if (!streamPlayer) return;
|
updateVideoPlayer();
|
||||||
streamPlayer.reloadPlayer(), updateVideoPlayer();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"video.processing": {
|
"video.processing": {
|
||||||
@ -4347,17 +4613,17 @@ function onChangeVideoPlayerType() {
|
|||||||
let playerType = getStreamPref("video.player.type"), settingsManager = SettingsManager.getInstance();
|
let playerType = getStreamPref("video.player.type"), settingsManager = SettingsManager.getInstance();
|
||||||
if (!settingsManager.hasElement("video.processing")) return;
|
if (!settingsManager.hasElement("video.processing")) return;
|
||||||
let isDisabled = !1, $videoProcessing = settingsManager.getElement("video.processing"), $videoSharpness = settingsManager.getElement("video.processing.sharpness"), $videoPowerPreference = settingsManager.getElement("video.player.powerPreference"), $videoMaxFps = settingsManager.getElement("video.maxFps"), $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`);
|
let isDisabled = !1, $videoProcessing = settingsManager.getElement("video.processing"), $videoSharpness = settingsManager.getElement("video.processing.sharpness"), $videoPowerPreference = settingsManager.getElement("video.player.powerPreference"), $videoMaxFps = settingsManager.getElement("video.maxFps"), $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`);
|
||||||
if (playerType === "webgl2") $optCas && ($optCas.disabled = !1);
|
if (playerType === "default") {
|
||||||
else if ($videoProcessing.value = "usm", setStreamPref("video.processing", "usm", "direct"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0;
|
if ($videoProcessing.value = "usm", setStreamPref("video.processing", "usm", "direct"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0;
|
||||||
$videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2");
|
} else $optCas && ($optCas.disabled = !1);
|
||||||
|
$videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType === "default");
|
||||||
}
|
}
|
||||||
function limitVideoPlayerFps(targetFps) {
|
function limitVideoPlayerFps(targetFps) {
|
||||||
STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps);
|
STATES.currentStream.streamPlayerManager?.getCanvasPlayer()?.setTargetFps(targetFps);
|
||||||
}
|
}
|
||||||
function updateVideoPlayer() {
|
function updateVideoPlayer() {
|
||||||
let streamPlayer = STATES.currentStream.streamPlayer;
|
let streamPlayerManager = STATES.currentStream.streamPlayerManager;
|
||||||
if (!streamPlayer) return;
|
if (!streamPlayerManager) return;
|
||||||
limitVideoPlayerFps(getStreamPref("video.maxFps"));
|
|
||||||
let options = {
|
let options = {
|
||||||
processing: getStreamPref("video.processing"),
|
processing: getStreamPref("video.processing"),
|
||||||
sharpness: getStreamPref("video.processing.sharpness"),
|
sharpness: getStreamPref("video.processing.sharpness"),
|
||||||
@ -4365,9 +4631,12 @@ function updateVideoPlayer() {
|
|||||||
contrast: getStreamPref("video.contrast"),
|
contrast: getStreamPref("video.contrast"),
|
||||||
brightness: getStreamPref("video.brightness")
|
brightness: getStreamPref("video.brightness")
|
||||||
};
|
};
|
||||||
streamPlayer.setPlayerType(getStreamPref("video.player.type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer();
|
streamPlayerManager.switchPlayerType(getStreamPref("video.player.type")), limitVideoPlayerFps(getStreamPref("video.maxFps")), streamPlayerManager.updateOptions(options), streamPlayerManager.refreshPlayer();
|
||||||
}
|
}
|
||||||
window.addEventListener("resize", updateVideoPlayer);
|
function resizeVideoPlayer() {
|
||||||
|
STATES.currentStream.streamPlayerManager?.resizePlayer();
|
||||||
|
}
|
||||||
|
window.addEventListener("resize", resizeVideoPlayer);
|
||||||
class NavigationDialog {
|
class NavigationDialog {
|
||||||
dialogManager;
|
dialogManager;
|
||||||
onMountedCallbacks = [];
|
onMountedCallbacks = [];
|
||||||
@ -7680,17 +7949,18 @@ class ScreenshotManager {
|
|||||||
e.target.classList.remove("bx-taking-screenshot");
|
e.target.classList.remove("bx-taking-screenshot");
|
||||||
}
|
}
|
||||||
takeScreenshot(callback) {
|
takeScreenshot(callback) {
|
||||||
let currentStream = STATES.currentStream, streamPlayer = currentStream.streamPlayer, $canvas = this.$canvas;
|
let currentStream = STATES.currentStream, streamPlayerManager = currentStream.streamPlayerManager, $canvas = this.$canvas;
|
||||||
if (!streamPlayer || !$canvas) return;
|
if (!streamPlayerManager || !$canvas) return;
|
||||||
let $player;
|
let $player;
|
||||||
if (getGlobalPref("screenshot.applyFilters")) $player = streamPlayer.getPlayerElement();
|
if (getGlobalPref("screenshot.applyFilters")) $player = streamPlayerManager.getPlayerElement();
|
||||||
else $player = streamPlayer.getPlayerElement("default");
|
else $player = streamPlayerManager.getPlayerElement("video");
|
||||||
if (!$player || !$player.isConnected) return;
|
if (!$player || !$player.isConnected) return;
|
||||||
|
let canvasContext = this.canvasContext;
|
||||||
|
if ($player instanceof HTMLCanvasElement) streamPlayerManager.getCanvasPlayer()?.updateFrame();
|
||||||
|
canvasContext.drawImage($player, 0, 0);
|
||||||
let $gameStream = $player.closest("#game-stream");
|
let $gameStream = $player.closest("#game-stream");
|
||||||
if ($gameStream) $gameStream.addEventListener("animationend", this.onAnimationEnd, { once: !0 }), $gameStream.classList.add("bx-taking-screenshot");
|
if ($gameStream) $gameStream.addEventListener("animationend", this.onAnimationEnd, { once: !0 }), $gameStream.classList.add("bx-taking-screenshot");
|
||||||
let canvasContext = this.canvasContext;
|
if (AppInterface) {
|
||||||
if ($player instanceof HTMLCanvasElement) streamPlayer.getWebGL2Player().forceDrawFrame();
|
|
||||||
if (canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height), AppInterface) {
|
|
||||||
let data = $canvas.toDataURL("image/png").split(";base64,")[1];
|
let data = $canvas.toDataURL("image/png").split(";base64,")[1];
|
||||||
AppInterface.saveScreenshot(currentStream.titleSlug, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();
|
AppInterface.saveScreenshot(currentStream.titleSlug, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();
|
||||||
return;
|
return;
|
||||||
@ -7885,7 +8155,8 @@ var FeatureGates = {
|
|||||||
EnableWifiWarnings: !1,
|
EnableWifiWarnings: !1,
|
||||||
EnableUpdateRequiredPage: !1,
|
EnableUpdateRequiredPage: !1,
|
||||||
ShowForcedUpdateScreen: !1,
|
ShowForcedUpdateScreen: !1,
|
||||||
EnableTakControlResizing: !0
|
EnableTakControlResizing: !0,
|
||||||
|
EnableLazyLoadedHome: !1
|
||||||
}, nativeMkbMode = getGlobalPref("nativeMkb.mode");
|
}, nativeMkbMode = getGlobalPref("nativeMkb.mode");
|
||||||
if (nativeMkbMode !== "default") FeatureGates.EnableMouseAndKeyboard = nativeMkbMode === "on";
|
if (nativeMkbMode !== "default") FeatureGates.EnableMouseAndKeyboard = nativeMkbMode === "on";
|
||||||
var blockFeatures = getGlobalPref("block.features");
|
var blockFeatures = getGlobalPref("block.features");
|
||||||
@ -9128,18 +9399,18 @@ function patchSdpBitrate(sdp, video, audio) {
|
|||||||
return lines.join(`\r
|
return lines.join(`\r
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
var clarity_boost_default = `#version 300 es
|
var clarity_boost_default2 = `#version 300 es
|
||||||
in vec4 position;
|
in vec4 position;
|
||||||
void main() {
|
void main() {
|
||||||
gl_Position = position;
|
gl_Position = position;
|
||||||
}`;
|
}`;
|
||||||
var clarity_boost_default2 = `#version 300 es
|
var clarity_boost_default3 = `#version 300 es
|
||||||
precision mediump float;
|
precision mediump float;
|
||||||
uniform sampler2D data;
|
uniform sampler2D data;
|
||||||
uniform vec2 iResolution;
|
uniform vec2 iResolution;
|
||||||
const int FILTER_UNSHARP_MASKING = 1;
|
const int FILTER_UNSHARP_MASKING = 1;
|
||||||
const float CAS_CONTRAST_PEAK = 0.8 * -3.0 + 8.0;
|
const float CAS_CONTRAST_PEAK = 0.8 * -3.0 + 8.0;
|
||||||
const vec3 LUMINOSITY_FACTOR = vec3(0.2126, 0.7152, 0.0722);
|
const vec3 LUMINOSITY_FACTOR = vec3(0.299, 0.587, 0.114);
|
||||||
uniform int filterId;
|
uniform int filterId;
|
||||||
uniform float sharpenFactor;
|
uniform float sharpenFactor;
|
||||||
uniform float brightness;
|
uniform float brightness;
|
||||||
@ -9183,117 +9454,56 @@ color = contrast * (color - 0.5) + 0.5;
|
|||||||
color = brightness * color;
|
color = brightness * color;
|
||||||
fragColor = vec4(color, 1.0);
|
fragColor = vec4(color, 1.0);
|
||||||
}`;
|
}`;
|
||||||
class WebGL2Player {
|
class WebGL2Player extends BaseCanvasPlayer {
|
||||||
LOG_TAG = "WebGL2Player";
|
|
||||||
$video;
|
|
||||||
$canvas;
|
|
||||||
gl = null;
|
gl = null;
|
||||||
resources = [];
|
resources = [];
|
||||||
program = null;
|
program = null;
|
||||||
stopped = !1;
|
|
||||||
options = {
|
|
||||||
filterId: 1,
|
|
||||||
sharpenFactor: 0,
|
|
||||||
brightness: 0,
|
|
||||||
contrast: 0,
|
|
||||||
saturation: 0
|
|
||||||
};
|
|
||||||
targetFps = 60;
|
|
||||||
frameInterval = 0;
|
|
||||||
lastFrameTime = 0;
|
|
||||||
animFrameId = null;
|
|
||||||
constructor($video) {
|
constructor($video) {
|
||||||
BxLogger.info(this.LOG_TAG, "Initialize"), this.$video = $video;
|
super("webgl2", $video, "WebGL2Player");
|
||||||
let $canvas = document.createElement("canvas");
|
|
||||||
$canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, this.setupShaders(), this.setupRendering(), $video.insertAdjacentElement("afterend", $canvas);
|
|
||||||
}
|
|
||||||
setFilter(filterId, update = !0) {
|
|
||||||
this.options.filterId = filterId, update && this.updateCanvas();
|
|
||||||
}
|
|
||||||
setSharpness(sharpness, update = !0) {
|
|
||||||
this.options.sharpenFactor = sharpness, update && this.updateCanvas();
|
|
||||||
}
|
|
||||||
setBrightness(brightness, update = !0) {
|
|
||||||
this.options.brightness = 1 + (brightness - 100) / 100, update && this.updateCanvas();
|
|
||||||
}
|
|
||||||
setContrast(contrast, update = !0) {
|
|
||||||
this.options.contrast = 1 + (contrast - 100) / 100, update && this.updateCanvas();
|
|
||||||
}
|
|
||||||
setSaturation(saturation, update = !0) {
|
|
||||||
this.options.saturation = 1 + (saturation - 100) / 100, update && this.updateCanvas();
|
|
||||||
}
|
|
||||||
setTargetFps(target) {
|
|
||||||
this.targetFps = target, this.lastFrameTime = 0, this.frameInterval = target ? Math.floor(1000 / target) : 0;
|
|
||||||
}
|
|
||||||
getCanvas() {
|
|
||||||
return this.$canvas;
|
|
||||||
}
|
}
|
||||||
updateCanvas() {
|
updateCanvas() {
|
||||||
let gl = this.gl, program = this.program;
|
console.log("updateCanvas", this.options);
|
||||||
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);
|
let gl = this.gl, program = this.program, filterId = this.toFilterId(this.options.processing);
|
||||||
|
gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpness), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness / 100), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast / 100), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation / 100);
|
||||||
}
|
}
|
||||||
forceDrawFrame() {
|
updateFrame() {
|
||||||
let gl = this.gl;
|
let gl = this.gl;
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||||
}
|
}
|
||||||
setupRendering() {
|
async setupShaders() {
|
||||||
let frameCallback;
|
|
||||||
if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
|
|
||||||
let $video = this.$video;
|
|
||||||
frameCallback = $video.requestVideoFrameCallback.bind($video);
|
|
||||||
} else frameCallback = requestAnimationFrame;
|
|
||||||
let animate = () => {
|
|
||||||
if (this.stopped) return;
|
|
||||||
this.animFrameId = frameCallback(animate);
|
|
||||||
let draw = !0;
|
|
||||||
if (this.targetFps === 0) draw = !1;
|
|
||||||
else if (this.targetFps < 60) {
|
|
||||||
let currentTime = performance.now();
|
|
||||||
if (currentTime - this.lastFrameTime < this.frameInterval) draw = !1;
|
|
||||||
else this.lastFrameTime = currentTime;
|
|
||||||
}
|
|
||||||
if (draw) {
|
|
||||||
let gl = this.gl;
|
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.animFrameId = frameCallback(animate);
|
|
||||||
}
|
|
||||||
setupShaders() {
|
|
||||||
BxLogger.info(this.LOG_TAG, "Setting up", getStreamPref("video.player.powerPreference"));
|
|
||||||
let gl = this.$canvas.getContext("webgl2", {
|
let gl = this.$canvas.getContext("webgl2", {
|
||||||
isBx: !0,
|
isBx: !0,
|
||||||
antialias: !0,
|
antialias: !0,
|
||||||
alpha: !1,
|
alpha: !1,
|
||||||
|
depth: !1,
|
||||||
|
preserveDrawingBuffer: !1,
|
||||||
|
stencil: !1,
|
||||||
powerPreference: getStreamPref("video.player.powerPreference")
|
powerPreference: getStreamPref("video.player.powerPreference")
|
||||||
});
|
});
|
||||||
this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);
|
this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);
|
||||||
let vShader = gl.createShader(gl.VERTEX_SHADER);
|
let vShader = gl.createShader(gl.VERTEX_SHADER);
|
||||||
gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader);
|
gl.shaderSource(vShader, clarity_boost_default2), gl.compileShader(vShader);
|
||||||
let fShader = gl.createShader(gl.FRAGMENT_SHADER);
|
let fShader = gl.createShader(gl.FRAGMENT_SHADER);
|
||||||
gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader);
|
gl.shaderSource(fShader, clarity_boost_default3), gl.compileShader(fShader);
|
||||||
let program = gl.createProgram();
|
let 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();
|
||||||
let buffer = gl.createBuffer();
|
let buffer = gl.createBuffer();
|
||||||
this.resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0);
|
this.resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
3,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
3
|
||||||
|
]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0);
|
||||||
let texture = gl.createTexture();
|
let 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() {
|
|
||||||
this.stop(), this.stopped = !1, BxLogger.info(this.LOG_TAG, "Resume"), this.$canvas.classList.remove("bx-gone"), this.setupRendering();
|
|
||||||
}
|
|
||||||
stop() {
|
|
||||||
if (BxLogger.info(this.LOG_TAG, "Stop"), this.$canvas.classList.add("bx-gone"), this.stopped = !0, this.animFrameId) {
|
|
||||||
if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.$video.cancelVideoFrameCallback(this.animFrameId);
|
|
||||||
else cancelAnimationFrame(this.animFrameId);
|
|
||||||
this.animFrameId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
destroy() {
|
destroy() {
|
||||||
BxLogger.info(this.LOG_TAG, "Destroy"), this.stop();
|
super.destroy();
|
||||||
let gl = this.gl;
|
let gl = this.gl;
|
||||||
if (gl) {
|
if (!gl) return;
|
||||||
gl.getExtension("WEBGL_lose_context")?.loseContext(), gl.useProgram(null);
|
gl.getExtension("WEBGL_lose_context")?.loseContext(), gl.useProgram(null);
|
||||||
for (let resource of this.resources)
|
for (let resource of this.resources)
|
||||||
if (resource instanceof WebGLProgram) gl.deleteProgram(resource);
|
if (resource instanceof WebGLProgram) gl.deleteProgram(resource);
|
||||||
@ -9302,37 +9512,47 @@ class WebGL2Player {
|
|||||||
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);
|
refreshPlayer() {
|
||||||
this.$canvas.width = 1, this.$canvas.height = 1;
|
this.updateCanvas();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class StreamPlayer {
|
class VideoPlayer extends BaseStreamPlayer {
|
||||||
$video;
|
$videoCss;
|
||||||
playerType = "default";
|
$usmMatrix;
|
||||||
options = {};
|
constructor($video, logTag) {
|
||||||
webGL2Player = null;
|
super("default", "video", $video, logTag);
|
||||||
$videoCss = null;
|
|
||||||
$usmMatrix = null;
|
|
||||||
constructor($video, type, options) {
|
|
||||||
this.setupVideoElements(), this.$video = $video, this.options = options || {}, this.setPlayerType(type);
|
|
||||||
}
|
}
|
||||||
setupVideoElements() {
|
init() {
|
||||||
if (this.$videoCss = document.getElementById("bx-video-css"), this.$videoCss) return;
|
super.init();
|
||||||
let $fragment = document.createDocumentFragment();
|
let xmlns = "http://www.w3.org/2000/svg", $svg = CE("svg", {
|
||||||
this.$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.$videoCss);
|
|
||||||
let $svg = CE("svg", {
|
|
||||||
id: "bx-video-filters",
|
id: "bx-video-filters",
|
||||||
xmlns: "http://www.w3.org/2000/svg",
|
class: "bx-gone",
|
||||||
class: "bx-gone"
|
xmlns
|
||||||
}, 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
|
||||||
}, 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
|
||||||
}))));
|
}))));
|
||||||
$fragment.appendChild($svg), document.documentElement.appendChild($fragment);
|
this.$videoCss = CE("style", { id: "bx-video-css" });
|
||||||
|
let $fragment = document.createDocumentFragment();
|
||||||
|
$fragment.append(this.$videoCss, $svg), document.documentElement.appendChild($fragment);
|
||||||
|
}
|
||||||
|
setupRendering() {}
|
||||||
|
forceDrawFrame() {}
|
||||||
|
updateCanvas() {}
|
||||||
|
refreshPlayer() {
|
||||||
|
let filters = this.getVideoPlayerFilterStyle(), videoCss = "";
|
||||||
|
if (filters) videoCss += `filter: ${filters} !important;`;
|
||||||
|
if (getGlobalPref("screenshot.applyFilters")) ScreenshotManager.getInstance().updateCanvasFilters(filters);
|
||||||
|
let css = "";
|
||||||
|
if (videoCss) css = `#game-stream video { ${videoCss} }`;
|
||||||
|
this.$videoCss.textContent = css;
|
||||||
|
}
|
||||||
|
clearFilters() {
|
||||||
|
this.$videoCss.textContent = "";
|
||||||
}
|
}
|
||||||
getVideoPlayerFilterStyle() {
|
getVideoPlayerFilterStyle() {
|
||||||
let filters = [], sharpness = this.options.sharpness || 0;
|
let filters = [], sharpness = this.options.sharpness || 0;
|
||||||
@ -9348,10 +9568,20 @@ class StreamPlayer {
|
|||||||
if (brightness != 100) filters.push(`brightness(${brightness}%)`);
|
if (brightness != 100) filters.push(`brightness(${brightness}%)`);
|
||||||
return filters.join(" ");
|
return filters.join(" ");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
class StreamPlayerManager {
|
||||||
|
static instance;
|
||||||
|
static getInstance = () => StreamPlayerManager.instance ?? (StreamPlayerManager.instance = new StreamPlayerManager);
|
||||||
|
$video;
|
||||||
|
videoPlayer;
|
||||||
|
canvasPlayer;
|
||||||
|
playerType = "default";
|
||||||
|
constructor() {}
|
||||||
|
setVideoElement($video) {
|
||||||
|
this.$video = $video, this.videoPlayer = new VideoPlayer($video, "VideoPlayer"), this.videoPlayer.init();
|
||||||
|
}
|
||||||
resizePlayer() {
|
resizePlayer() {
|
||||||
let PREF_RATIO = getStreamPref("video.ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, $webGL2Canvas;
|
let PREF_RATIO = getStreamPref("video.ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, targetWidth, targetHeight, targetObjectFit;
|
||||||
if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas();
|
|
||||||
let targetWidth, targetHeight, targetObjectFit;
|
|
||||||
if (PREF_RATIO.includes(":")) {
|
if (PREF_RATIO.includes(":")) {
|
||||||
let tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]), width = 0, height = 0, parentRect = $video.parentElement.getBoundingClientRect();
|
let tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]), width = 0, height = 0, parentRect = $video.parentElement.getBoundingClientRect();
|
||||||
if (parentRect.width / parentRect.height > videoRatio) height = parentRect.height, width = height * videoRatio;
|
if (parentRect.width / parentRect.height > videoRatio) height = parentRect.height, width = height * videoRatio;
|
||||||
@ -9367,58 +9597,49 @@ class StreamPlayer {
|
|||||||
}
|
}
|
||||||
targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill";
|
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, $video.dispatchEvent(new Event("resize"));
|
if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, this.canvasPlayer) {
|
||||||
if (isNativeTouchGame && this.playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions();
|
let $canvas = this.canvasPlayer.getCanvas();
|
||||||
|
$canvas.style.width = targetWidth, $canvas.style.height = targetHeight, $canvas.style.objectFit = targetObjectFit, $video.dispatchEvent(new Event("resize"));
|
||||||
}
|
}
|
||||||
setPlayerType(type, refreshPlayer = !1) {
|
if (isNativeTouchGame && this.playerType !== "default") window.BX_EXPOSED.streamSession.updateDimensions();
|
||||||
|
}
|
||||||
|
switchPlayerType(type, refreshPlayer = !1) {
|
||||||
if (this.playerType !== type) {
|
if (this.playerType !== type) {
|
||||||
let videoClass = BX_FLAGS.DeviceInfo.deviceType === "android-tv" ? "bx-pixel" : "bx-gone";
|
let videoClass = BX_FLAGS.DeviceInfo.deviceType === "android-tv" ? "bx-pixel" : "bx-gone";
|
||||||
if (type === "webgl2") {
|
if (this.cleanUpCanvasPlayer(), type === "default") this.$video.classList.remove(videoClass);
|
||||||
if (!this.webGL2Player) this.webGL2Player = new WebGL2Player(this.$video);
|
else {
|
||||||
else this.webGL2Player.resume();
|
if (type === "webgpu") this.canvasPlayer = new WebGPUPlayer(this.$video);
|
||||||
this.$videoCss.textContent = "", this.$video.classList.add(videoClass);
|
else this.canvasPlayer = new WebGL2Player(this.$video);
|
||||||
} else this.webGL2Player?.stop(), this.$video.classList.remove(videoClass);
|
this.canvasPlayer.init(), this.videoPlayer.clearFilters(), this.$video.classList.add(videoClass);
|
||||||
}
|
}
|
||||||
this.playerType = type, refreshPlayer && this.refreshPlayer();
|
this.playerType = type;
|
||||||
}
|
}
|
||||||
setOptions(options, refreshPlayer = !1) {
|
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.canvasPlayer || this.videoPlayer).updateOptions(options, refreshPlayer);
|
||||||
}
|
}
|
||||||
getPlayerElement(playerType) {
|
getPlayerElement(elementType) {
|
||||||
if (typeof playerType === "undefined") playerType = this.playerType;
|
if (typeof elementType === "undefined") elementType = this.playerType === "default" ? "video" : "canvas";
|
||||||
if (playerType === "webgl2") return this.webGL2Player?.getCanvas();
|
if (elementType !== "video") return this.canvasPlayer?.getCanvas();
|
||||||
return this.$video;
|
return this.$video;
|
||||||
}
|
}
|
||||||
getWebGL2Player() {
|
getCanvasPlayer() {
|
||||||
return this.webGL2Player;
|
return this.canvasPlayer;
|
||||||
}
|
}
|
||||||
refreshPlayer() {
|
refreshPlayer() {
|
||||||
if (this.playerType === "webgl2") {
|
if (this.playerType === "default") this.videoPlayer.refreshPlayer();
|
||||||
let options = this.options, webGL2Player = this.webGL2Player;
|
else ScreenshotManager.getInstance().updateCanvasFilters("none"), this.canvasPlayer?.refreshPlayer();
|
||||||
if (options.processing === "usm") webGL2Player.setFilter(1);
|
|
||||||
else webGL2Player.setFilter(2);
|
|
||||||
ScreenshotManager.getInstance().updateCanvasFilters("none"), webGL2Player.setSharpness(options.sharpness || 0), webGL2Player.setSaturation(options.saturation || 100), webGL2Player.setContrast(options.contrast || 100), webGL2Player.setBrightness(options.brightness || 100);
|
|
||||||
} else {
|
|
||||||
let filters = this.getVideoPlayerFilterStyle(), videoCss = "";
|
|
||||||
if (filters) videoCss += `filter: ${filters} !important;`;
|
|
||||||
if (getGlobalPref("screenshot.applyFilters")) ScreenshotManager.getInstance().updateCanvasFilters(filters);
|
|
||||||
let css = "";
|
|
||||||
if (videoCss) css = `#game-stream video { ${videoCss} }`;
|
|
||||||
this.$videoCss.textContent = css;
|
|
||||||
}
|
|
||||||
this.resizePlayer();
|
this.resizePlayer();
|
||||||
}
|
}
|
||||||
reloadPlayer() {
|
getVideoPlayerFilterStyle() {
|
||||||
this.cleanUpWebGL2Player(), this.playerType = "default", this.setPlayerType("webgl2", !1);
|
throw new Error("Method not implemented.");
|
||||||
}
|
}
|
||||||
cleanUpWebGL2Player() {
|
cleanUpCanvasPlayer() {
|
||||||
this.webGL2Player?.destroy(), this.webGL2Player = null;
|
this.canvasPlayer?.destroy(), this.canvasPlayer = null;
|
||||||
}
|
}
|
||||||
destroy() {
|
destroy() {
|
||||||
this.cleanUpWebGL2Player();
|
this.cleanUpCanvasPlayer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function patchVideoApi() {
|
function patchVideoApi() {
|
||||||
@ -9430,8 +9651,8 @@ function patchVideoApi() {
|
|||||||
saturation: getStreamPref("video.saturation"),
|
saturation: getStreamPref("video.saturation"),
|
||||||
contrast: getStreamPref("video.contrast"),
|
contrast: getStreamPref("video.contrast"),
|
||||||
brightness: getStreamPref("video.brightness")
|
brightness: getStreamPref("video.brightness")
|
||||||
};
|
}, streamPlayerManager = StreamPlayerManager.getInstance();
|
||||||
STATES.currentStream.streamPlayer = new StreamPlayer(this, getStreamPref("video.player.type"), playerOptions), BxEventBus.Stream.emit("state.playing", {
|
streamPlayerManager.setVideoElement(this), streamPlayerManager.updateOptions(playerOptions, !1), streamPlayerManager.switchPlayerType(getStreamPref("video.player.type")), STATES.currentStream.streamPlayerManager = streamPlayerManager, BxEventBus.Stream.emit("state.playing", {
|
||||||
$video: this
|
$video: this
|
||||||
});
|
});
|
||||||
}, nativePlay = HTMLMediaElement.prototype.play;
|
}, nativePlay = HTMLMediaElement.prototype.play;
|
||||||
@ -10260,7 +10481,7 @@ BxEventBus.Stream.on("dataChannelCreated", (payload) => {
|
|||||||
});
|
});
|
||||||
function unload() {
|
function unload() {
|
||||||
if (!STATES.isPlaying) return;
|
if (!STATES.isPlaying) return;
|
||||||
KeyboardShortcutHandler.getInstance().stop(), EmulatedMkbHandler.getInstance()?.destroy(), NativeMkbHandler.getInstance()?.destroy(), DeviceVibrationManager.getInstance()?.reset(), STATES.currentStream.streamPlayer?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1, NavigationDialogManager.getInstance().hide(), StreamStats.getInstance().destroy(), StreamBadges.getInstance().destroy(), MouseCursorHider.getInstance()?.stop(), TouchController.reset(), GameBar.getInstance()?.disable(), BxEventBus.Stream.emit("xboxTitleId.changed", { id: -1 });
|
KeyboardShortcutHandler.getInstance().stop(), EmulatedMkbHandler.getInstance()?.destroy(), NativeMkbHandler.getInstance()?.destroy(), DeviceVibrationManager.getInstance()?.reset(), STATES.currentStream.streamPlayerManager?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1, NavigationDialogManager.getInstance().hide(), StreamStats.getInstance().destroy(), StreamBadges.getInstance().destroy(), MouseCursorHider.getInstance()?.stop(), TouchController.reset(), GameBar.getInstance()?.disable(), BxEventBus.Stream.emit("xboxTitleId.changed", { id: -1 });
|
||||||
}
|
}
|
||||||
BxEventBus.Stream.on("state.stopped", unload);
|
BxEventBus.Stream.on("state.stopped", unload);
|
||||||
window.addEventListener("pagehide", (e) => {
|
window.addEventListener("pagehide", (e) => {
|
||||||
@ -10275,7 +10496,7 @@ function main() {
|
|||||||
BX_FLAGS.ForceNativeMkbTitles.push(...customList);
|
BX_FLAGS.ForceNativeMkbTitles.push(...customList);
|
||||||
}
|
}
|
||||||
if (StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getGlobalPref("audio.volume.booster.enabled") && patchAudioContext(), getGlobalPref("block.tracking")) patchMeControl(), disableAdobeAudienceManager();
|
if (StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getGlobalPref("audio.volume.booster.enabled") && patchAudioContext(), getGlobalPref("block.tracking")) patchMeControl(), disableAdobeAudienceManager();
|
||||||
if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), DeviceVibrationManager.getInstance(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getGlobalPref("xhome.enabled")) RemotePlayManager.detect();
|
if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), WebGPUPlayer.prepare(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), DeviceVibrationManager.getInstance(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getGlobalPref("xhome.enabled")) RemotePlayManager.detect();
|
||||||
if (getGlobalPref("touchController.mode") === "all") TouchController.setup();
|
if (getGlobalPref("touchController.mode") === "all") TouchController.setup();
|
||||||
if (AppInterface && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on")) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString());
|
if (AppInterface && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on")) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString());
|
||||||
if (getGlobalPref("ui.gameCard.waitTime.show") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getGlobalPref("ui.controllerStatus.show")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad));
|
if (getGlobalPref("ui.gameCard.waitTime.show") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getGlobalPref("ui.controllerStatus.show")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad));
|
||||||
|
4
dist/better-xcloud.user.js
vendored
4
dist/better-xcloud.user.js
vendored
File diff suppressed because one or more lines are too long
@ -13,6 +13,7 @@
|
|||||||
"@types/bun": "^1.2.0",
|
"@types/bun": "^1.2.0",
|
||||||
"@types/node": "^22.10.10",
|
"@types/node": "^22.10.10",
|
||||||
"@types/stylus": "^0.48.43",
|
"@types/stylus": "^0.48.43",
|
||||||
|
"@webgpu/types": "^0.1.53",
|
||||||
"eslint": "^9.19.0",
|
"eslint": "^9.19.0",
|
||||||
"eslint-plugin-compat": "^6.0.2",
|
"eslint-plugin-compat": "^6.0.2",
|
||||||
"stylus": "^0.64.0"
|
"stylus": "^0.64.0"
|
||||||
|
@ -116,6 +116,7 @@ export const enum VideoPowerPreference {
|
|||||||
export const enum StreamPlayerType {
|
export const enum StreamPlayerType {
|
||||||
VIDEO = 'default',
|
VIDEO = 'default',
|
||||||
WEBGL2 = 'webgl2',
|
WEBGL2 = 'webgl2',
|
||||||
|
WEBGPU = 'webgpu',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum StreamVideoProcessing {
|
export const enum StreamVideoProcessing {
|
||||||
|
@ -47,6 +47,7 @@ import { BxEventBus } from "./utils/bx-event-bus";
|
|||||||
import { getGlobalPref, getStreamPref } from "./utils/pref-utils";
|
import { getGlobalPref, getStreamPref } from "./utils/pref-utils";
|
||||||
import { SettingsManager } from "./modules/settings-manager";
|
import { SettingsManager } from "./modules/settings-manager";
|
||||||
import { Toast } from "./utils/toast";
|
import { Toast } from "./utils/toast";
|
||||||
|
import { WebGPUPlayer } from "./modules/player/webgpu/webgpu-player";
|
||||||
|
|
||||||
SettingsManager.getInstance();
|
SettingsManager.getInstance();
|
||||||
|
|
||||||
@ -347,7 +348,7 @@ function unload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Destroy StreamPlayer
|
// Destroy StreamPlayer
|
||||||
STATES.currentStream.streamPlayer?.destroy();
|
STATES.currentStream.streamPlayerManager?.destroy();
|
||||||
|
|
||||||
STATES.isPlaying = false;
|
STATES.isPlaying = false;
|
||||||
STATES.currentStream = {};
|
STATES.currentStream = {};
|
||||||
@ -416,6 +417,8 @@ function main() {
|
|||||||
StreamStats.setupEvents();
|
StreamStats.setupEvents();
|
||||||
|
|
||||||
if (isFullVersion()) {
|
if (isFullVersion()) {
|
||||||
|
WebGPUPlayer.prepare();
|
||||||
|
|
||||||
STATES.userAgent.capabilities.touch && TouchController.updateCustomList();
|
STATES.userAgent.capabilities.touch && TouchController.updateCustomList();
|
||||||
|
|
||||||
DeviceVibrationManager.getInstance();
|
DeviceVibrationManager.getInstance();
|
||||||
|
119
src/modules/player/base-canvas-player.ts
Normal file
119
src/modules/player/base-canvas-player.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { BxLogger } from "@/utils/bx-logger";
|
||||||
|
import { BaseStreamPlayer, StreamPlayerElement, StreamPlayerFilter } from "./base-stream-player";
|
||||||
|
import { StreamVideoProcessing, type StreamPlayerType } from "@/enums/pref-values";
|
||||||
|
|
||||||
|
export abstract class BaseCanvasPlayer extends BaseStreamPlayer {
|
||||||
|
protected $canvas: HTMLCanvasElement;
|
||||||
|
|
||||||
|
protected targetFps = 60;
|
||||||
|
protected frameInterval = 0;
|
||||||
|
protected lastFrameTime = 0;
|
||||||
|
protected animFrameId: number | null = null;
|
||||||
|
protected frameCallback: any;
|
||||||
|
private boundDrawFrame: () => void;
|
||||||
|
|
||||||
|
constructor(playerType: StreamPlayerType, $video: HTMLVideoElement, logTag: string) {
|
||||||
|
super(playerType, StreamPlayerElement.CANVAS, $video, logTag);
|
||||||
|
|
||||||
|
const $canvas = document.createElement('canvas');
|
||||||
|
$canvas.width = $video.videoWidth;
|
||||||
|
$canvas.height = $video.videoHeight;
|
||||||
|
this.$canvas = $canvas;
|
||||||
|
|
||||||
|
$video.insertAdjacentElement('afterend', this.$canvas);
|
||||||
|
|
||||||
|
let frameCallback: any;
|
||||||
|
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
|
||||||
|
const $video = this.$video;
|
||||||
|
frameCallback = $video.requestVideoFrameCallback.bind($video);
|
||||||
|
} else {
|
||||||
|
frameCallback = requestAnimationFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.frameCallback = frameCallback;
|
||||||
|
this.boundDrawFrame = this.drawFrame.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
super.init();
|
||||||
|
|
||||||
|
await this.setupShaders();
|
||||||
|
this.setupRendering();
|
||||||
|
}
|
||||||
|
|
||||||
|
setTargetFps(target: number) {
|
||||||
|
this.targetFps = target;
|
||||||
|
this.lastFrameTime = 0;
|
||||||
|
this.frameInterval = target ? Math.floor(1000 / target) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCanvas() {
|
||||||
|
return this.$canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
BxLogger.info(this.logTag, 'Destroy');
|
||||||
|
|
||||||
|
this.isStopped = true;
|
||||||
|
if (this.animFrameId) {
|
||||||
|
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
|
||||||
|
this.$video.cancelVideoFrameCallback(this.animFrameId);
|
||||||
|
} else {
|
||||||
|
cancelAnimationFrame(this.animFrameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animFrameId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$canvas.isConnected) {
|
||||||
|
this.$canvas.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$canvas.width = 1;
|
||||||
|
this.$canvas.height = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
toFilterId(processing: StreamVideoProcessing) {
|
||||||
|
return processing === StreamVideoProcessing.CAS ? StreamPlayerFilter.CAS : StreamPlayerFilter.USM;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected shouldDraw() {
|
||||||
|
if (this.targetFps >= 60) {
|
||||||
|
// Always draw
|
||||||
|
return true;
|
||||||
|
} else if (this.targetFps === 0) {
|
||||||
|
// Don't draw when FPS is 0
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTime = performance.now();
|
||||||
|
const timeSinceLastFrame = currentTime - this.lastFrameTime;
|
||||||
|
if (timeSinceLastFrame < this.frameInterval) {
|
||||||
|
// Skip frame to limit FPS
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastFrameTime = currentTime;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawFrame() {
|
||||||
|
if (this.isStopped) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animFrameId = this.frameCallback(this.boundDrawFrame);
|
||||||
|
if (!this.shouldDraw()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setupRendering(): void {
|
||||||
|
this.animFrameId = this.frameCallback(this.boundDrawFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract setupShaders(): void;
|
||||||
|
abstract updateFrame(): void;
|
||||||
|
}
|
48
src/modules/player/base-stream-player.ts
Normal file
48
src/modules/player/base-stream-player.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { StreamVideoProcessing, type StreamPlayerType } from "@/enums/pref-values";
|
||||||
|
import type { StreamPlayerOptions } from "@/types/stream";
|
||||||
|
import { BxLogger } from "@/utils/bx-logger";
|
||||||
|
|
||||||
|
export const enum StreamPlayerElement {
|
||||||
|
VIDEO = 'video',
|
||||||
|
CANVAS = 'canvas',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum StreamPlayerFilter {
|
||||||
|
USM = 1,
|
||||||
|
CAS = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class BaseStreamPlayer {
|
||||||
|
protected logTag: string;
|
||||||
|
protected playerType: StreamPlayerType;
|
||||||
|
protected elementType: StreamPlayerElement;
|
||||||
|
protected $video: HTMLVideoElement;
|
||||||
|
|
||||||
|
protected options: StreamPlayerOptions = {
|
||||||
|
processing: StreamVideoProcessing.USM,
|
||||||
|
sharpness: 0,
|
||||||
|
brightness: 1.0,
|
||||||
|
contrast: 1.0,
|
||||||
|
saturation: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
protected isStopped = false;
|
||||||
|
|
||||||
|
constructor(playerType: StreamPlayerType, elementType: StreamPlayerElement, $video: HTMLVideoElement, logTag: string) {
|
||||||
|
this.playerType = playerType;
|
||||||
|
this.elementType = elementType;
|
||||||
|
this.$video = $video;
|
||||||
|
this.logTag = logTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
BxLogger.info(this.logTag, 'Initialize');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOptions(newOptions: Partial<StreamPlayerOptions>, refresh=false) {
|
||||||
|
this.options = Object.assign(this.options, newOptions);
|
||||||
|
refresh && this.refreshPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract refreshPlayer(): void;
|
||||||
|
}
|
102
src/modules/player/video/video-player.ts
Normal file
102
src/modules/player/video/video-player.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { CE } from "@/utils/html";
|
||||||
|
import { BaseStreamPlayer, StreamPlayerElement } from "../base-stream-player";
|
||||||
|
import { StreamPlayerType, StreamVideoProcessing } from "@/enums/pref-values";
|
||||||
|
import { GlobalPref } from "@/enums/pref-keys";
|
||||||
|
import { getGlobalPref } from "@/utils/pref-utils";
|
||||||
|
import { ScreenshotManager } from "@/utils/screenshot-manager";
|
||||||
|
|
||||||
|
export class VideoPlayer extends BaseStreamPlayer {
|
||||||
|
private $videoCss!: HTMLStyleElement;
|
||||||
|
private $usmMatrix!: SVGFEConvolveMatrixElement;
|
||||||
|
|
||||||
|
constructor($video: HTMLVideoElement, logTag: string) {
|
||||||
|
super(StreamPlayerType.VIDEO, StreamPlayerElement.VIDEO, $video, logTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
super.init();
|
||||||
|
|
||||||
|
// Setup SVG filters
|
||||||
|
const xmlns = 'http://www.w3.org/2000/svg';
|
||||||
|
const $svg = CE('svg', {
|
||||||
|
id: 'bx-video-filters',
|
||||||
|
class: 'bx-gone',
|
||||||
|
xmlns,
|
||||||
|
},
|
||||||
|
CE('defs', { xmlns: 'http://www.w3.org/2000/svg' },
|
||||||
|
CE('filter', {
|
||||||
|
id: 'bx-filter-usm',
|
||||||
|
xmlns,
|
||||||
|
}, this.$usmMatrix = CE('feConvolveMatrix', {
|
||||||
|
id: 'bx-filter-usm-matrix',
|
||||||
|
order: '3',
|
||||||
|
xmlns,
|
||||||
|
}) as unknown as SVGFEConvolveMatrixElement),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.$videoCss = CE('style', { id: 'bx-video-css' });
|
||||||
|
|
||||||
|
const $fragment = document.createDocumentFragment();
|
||||||
|
$fragment.append(this.$videoCss, $svg);
|
||||||
|
document.documentElement.appendChild($fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setupRendering(): void {}
|
||||||
|
forceDrawFrame(): void {}
|
||||||
|
updateCanvas(): void {}
|
||||||
|
|
||||||
|
refreshPlayer() {
|
||||||
|
let filters = this.getVideoPlayerFilterStyle();
|
||||||
|
let videoCss = '';
|
||||||
|
if (filters) {
|
||||||
|
videoCss += `filter: ${filters} !important;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply video filters to screenshots
|
||||||
|
if (getGlobalPref(GlobalPref.SCREENSHOT_APPLY_FILTERS)) {
|
||||||
|
ScreenshotManager.getInstance().updateCanvasFilters(filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
let css = '';
|
||||||
|
if (videoCss) {
|
||||||
|
css = `#game-stream video { ${videoCss} }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$videoCss.textContent = css;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearFilters() {
|
||||||
|
this.$videoCss.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getVideoPlayerFilterStyle() {
|
||||||
|
const filters = [];
|
||||||
|
|
||||||
|
const sharpness = this.options.sharpness || 0;
|
||||||
|
if (this.options.processing === StreamVideoProcessing.USM && sharpness != 0) {
|
||||||
|
const level = (7 - ((sharpness / 2) - 1) * 0.5).toFixed(1); // 5, 5.5, 6, 6.5, 7
|
||||||
|
const matrix = `0 -1 0 -1 ${level} -1 0 -1 0`;
|
||||||
|
this.$usmMatrix?.setAttributeNS(null, 'kernelMatrix', matrix);
|
||||||
|
|
||||||
|
filters.push(`url(#bx-filter-usm)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const saturation = this.options.saturation || 100;
|
||||||
|
if (saturation != 100) {
|
||||||
|
filters.push(`saturate(${saturation}%)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contrast = this.options.contrast || 100;
|
||||||
|
if (contrast != 100) {
|
||||||
|
filters.push(`contrast(${contrast}%)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const brightness = this.options.brightness || 100;
|
||||||
|
if (brightness != 100) {
|
||||||
|
filters.push(`brightness(${brightness}%)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters.join(' ');
|
||||||
|
}
|
||||||
|
}
|
@ -1,268 +0,0 @@
|
|||||||
import vertClarityBoost from "./shaders/clarity_boost.vert" with { type: "text" };
|
|
||||||
import fsClarityBoost from "./shaders/clarity_boost.fs" with { type: "text" };
|
|
||||||
import { BxLogger } from "@/utils/bx-logger";
|
|
||||||
import { StreamPref } from "@/enums/pref-keys";
|
|
||||||
import { getStreamPref } from "@/utils/pref-utils";
|
|
||||||
|
|
||||||
|
|
||||||
export class WebGL2Player {
|
|
||||||
private readonly LOG_TAG = 'WebGL2Player';
|
|
||||||
|
|
||||||
private $video: HTMLVideoElement;
|
|
||||||
private $canvas: HTMLCanvasElement;
|
|
||||||
|
|
||||||
private gl: WebGL2RenderingContext | null = null;
|
|
||||||
private resources: Array<any> = [];
|
|
||||||
private program: WebGLProgram | null = null;
|
|
||||||
|
|
||||||
private stopped: boolean = false;
|
|
||||||
|
|
||||||
private options = {
|
|
||||||
filterId: 1,
|
|
||||||
sharpenFactor: 0,
|
|
||||||
brightness: 0.0,
|
|
||||||
contrast: 0.0,
|
|
||||||
saturation: 0.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
private targetFps = 60;
|
|
||||||
private frameInterval = 0;
|
|
||||||
private lastFrameTime = 0;
|
|
||||||
|
|
||||||
private animFrameId: number | null = null;
|
|
||||||
|
|
||||||
constructor($video: HTMLVideoElement) {
|
|
||||||
BxLogger.info(this.LOG_TAG, 'Initialize');
|
|
||||||
this.$video = $video;
|
|
||||||
|
|
||||||
const $canvas = document.createElement('canvas');
|
|
||||||
$canvas.width = $video.videoWidth;
|
|
||||||
$canvas.height = $video.videoHeight;
|
|
||||||
this.$canvas = $canvas;
|
|
||||||
|
|
||||||
this.setupShaders();
|
|
||||||
this.setupRendering();
|
|
||||||
|
|
||||||
$video.insertAdjacentElement('afterend', $canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilter(filterId: number, update = true) {
|
|
||||||
this.options.filterId = filterId;
|
|
||||||
update && this.updateCanvas();
|
|
||||||
}
|
|
||||||
|
|
||||||
setSharpness(sharpness: number, update = true) {
|
|
||||||
this.options.sharpenFactor = sharpness;
|
|
||||||
update && this.updateCanvas();
|
|
||||||
}
|
|
||||||
|
|
||||||
setBrightness(brightness: number, update = true) {
|
|
||||||
this.options.brightness = 1 + (brightness - 100) / 100;
|
|
||||||
update && this.updateCanvas();
|
|
||||||
}
|
|
||||||
|
|
||||||
setContrast(contrast: number, update = true) {
|
|
||||||
this.options.contrast = 1 + (contrast - 100) / 100;
|
|
||||||
update && this.updateCanvas();
|
|
||||||
}
|
|
||||||
|
|
||||||
setSaturation(saturation: number, update = true) {
|
|
||||||
this.options.saturation = 1 + (saturation - 100) / 100;
|
|
||||||
update && this.updateCanvas();
|
|
||||||
}
|
|
||||||
|
|
||||||
setTargetFps(target: number) {
|
|
||||||
this.targetFps = target;
|
|
||||||
this.lastFrameTime = 0;
|
|
||||||
this.frameInterval = target ? Math.floor(1000 / target) : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCanvas() {
|
|
||||||
return this.$canvas;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCanvas() {
|
|
||||||
const gl = this.gl!;
|
|
||||||
const 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
forceDrawFrame() {
|
|
||||||
const gl = this.gl!;
|
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video);
|
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupRendering() {
|
|
||||||
let frameCallback: any;
|
|
||||||
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
|
|
||||||
const $video = this.$video;
|
|
||||||
frameCallback = $video.requestVideoFrameCallback.bind($video);
|
|
||||||
} else {
|
|
||||||
frameCallback = requestAnimationFrame;
|
|
||||||
}
|
|
||||||
|
|
||||||
let animate = () => {
|
|
||||||
if (this.stopped) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.animFrameId = frameCallback(animate);
|
|
||||||
|
|
||||||
let draw = true;
|
|
||||||
|
|
||||||
// Don't draw when FPS is 0
|
|
||||||
if (this.targetFps === 0) {
|
|
||||||
draw = false;
|
|
||||||
} else if (this.targetFps < 60) {
|
|
||||||
// Limit FPS
|
|
||||||
const currentTime = performance.now();
|
|
||||||
const timeSinceLastFrame = currentTime - this.lastFrameTime;
|
|
||||||
if (timeSinceLastFrame < this.frameInterval) {
|
|
||||||
draw = false;
|
|
||||||
} else {
|
|
||||||
this.lastFrameTime = currentTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (draw) {
|
|
||||||
const gl = this.gl!;
|
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video);
|
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.animFrameId = frameCallback(animate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupShaders() {
|
|
||||||
BxLogger.info(this.LOG_TAG, 'Setting up', getStreamPref(StreamPref.VIDEO_POWER_PREFERENCE));
|
|
||||||
|
|
||||||
const gl = this.$canvas.getContext('webgl2', {
|
|
||||||
isBx: true,
|
|
||||||
antialias: true,
|
|
||||||
alpha: false,
|
|
||||||
powerPreference: getStreamPref(StreamPref.VIDEO_POWER_PREFERENCE),
|
|
||||||
}) as WebGL2RenderingContext;
|
|
||||||
this.gl = gl;
|
|
||||||
|
|
||||||
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);
|
|
||||||
|
|
||||||
// Vertex shader: Identity map
|
|
||||||
const vShader = gl.createShader(gl.VERTEX_SHADER)!;
|
|
||||||
gl.shaderSource(vShader, vertClarityBoost);
|
|
||||||
gl.compileShader(vShader);
|
|
||||||
|
|
||||||
const fShader = gl.createShader(gl.FRAGMENT_SHADER)!;
|
|
||||||
gl.shaderSource(fShader, fsClarityBoost);
|
|
||||||
gl.compileShader(fShader);
|
|
||||||
|
|
||||||
// Create and link program
|
|
||||||
const program = gl.createProgram()!;
|
|
||||||
this.program = program;
|
|
||||||
|
|
||||||
gl.attachShader(program, vShader);
|
|
||||||
gl.attachShader(program, fShader);
|
|
||||||
gl.linkProgram(program);
|
|
||||||
gl.useProgram(program);
|
|
||||||
|
|
||||||
if (!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();
|
|
||||||
|
|
||||||
// Vertices: A screen-filling quad made from two triangles
|
|
||||||
const buffer = gl.createBuffer();
|
|
||||||
this.resources.push(buffer);
|
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);
|
|
||||||
|
|
||||||
gl.enableVertexAttribArray(0);
|
|
||||||
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
||||||
|
|
||||||
// Texture to contain the video data
|
|
||||||
const texture = gl.createTexture();
|
|
||||||
this.resources.push(texture);
|
|
||||||
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
||||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Bind texture to the "data" argument to the fragment shader
|
|
||||||
gl.uniform1i(gl.getUniformLocation(program, 'data'), 0);
|
|
||||||
|
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
|
||||||
// gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
resume() {
|
|
||||||
this.stop();
|
|
||||||
this.stopped = false;
|
|
||||||
BxLogger.info(this.LOG_TAG, 'Resume');
|
|
||||||
|
|
||||||
this.$canvas.classList.remove('bx-gone');
|
|
||||||
this.setupRendering();
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
BxLogger.info(this.LOG_TAG, 'Stop');
|
|
||||||
this.$canvas.classList.add('bx-gone');
|
|
||||||
|
|
||||||
this.stopped = true;
|
|
||||||
if (this.animFrameId) {
|
|
||||||
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
|
|
||||||
this.$video.cancelVideoFrameCallback(this.animFrameId);
|
|
||||||
} else {
|
|
||||||
cancelAnimationFrame(this.animFrameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.animFrameId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
BxLogger.info(this.LOG_TAG, 'Destroy');
|
|
||||||
this.stop();
|
|
||||||
|
|
||||||
const gl = this.gl;
|
|
||||||
if (gl) {
|
|
||||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
|
||||||
gl.useProgram(null);
|
|
||||||
|
|
||||||
for (const resource of this.resources) {
|
|
||||||
if (resource instanceof WebGLProgram) {
|
|
||||||
gl.deleteProgram(resource);
|
|
||||||
} else if (resource instanceof WebGLShader) {
|
|
||||||
gl.deleteShader(resource);
|
|
||||||
} else if (resource instanceof WebGLTexture) {
|
|
||||||
gl.deleteTexture(resource);
|
|
||||||
} else if (resource instanceof WebGLBuffer) {
|
|
||||||
gl.deleteBuffer(resource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.gl = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.$canvas.isConnected) {
|
|
||||||
this.$canvas.parentElement?.removeChild(this.$canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$canvas.width = 1;
|
|
||||||
this.$canvas.height = 1;
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,8 +10,8 @@ const int FILTER_UNSHARP_MASKING = 1;
|
|||||||
// constrast = 0.8
|
// constrast = 0.8
|
||||||
const float CAS_CONTRAST_PEAK = 0.8 * -3.0 + 8.0;
|
const float CAS_CONTRAST_PEAK = 0.8 * -3.0 + 8.0;
|
||||||
|
|
||||||
// Luminosity factor
|
// Luminosity factor: https://www.w3.org/TR/AERT/#color-contrast
|
||||||
const vec3 LUMINOSITY_FACTOR = vec3(0.2126, 0.7152, 0.0722);
|
const vec3 LUMINOSITY_FACTOR = vec3(0.299, 0.587, 0.114);
|
||||||
|
|
||||||
uniform int filterId;
|
uniform int filterId;
|
||||||
uniform float sharpenFactor;
|
uniform float sharpenFactor;
|
141
src/modules/player/webgl2/webgl2-player.ts
Executable file
141
src/modules/player/webgl2/webgl2-player.ts
Executable file
@ -0,0 +1,141 @@
|
|||||||
|
import vertClarityBoost from "./shaders/clarity-boost.vert" with { type: "text" };
|
||||||
|
import fsClarityBoost from "./shaders/clarity-boost.fs" with { type: "text" };
|
||||||
|
import { StreamPref } from "@/enums/pref-keys";
|
||||||
|
import { getStreamPref } from "@/utils/pref-utils";
|
||||||
|
import { BaseCanvasPlayer } from "../base-canvas-player";
|
||||||
|
import { StreamPlayerType } from "@/enums/pref-values";
|
||||||
|
|
||||||
|
|
||||||
|
export class WebGL2Player extends BaseCanvasPlayer {
|
||||||
|
private gl: WebGL2RenderingContext | null = null;
|
||||||
|
private resources: Array<WebGLBuffer | WebGLTexture | WebGLProgram | WebGLShader> = [];
|
||||||
|
private program: WebGLProgram | null = null;
|
||||||
|
|
||||||
|
constructor($video: HTMLVideoElement) {
|
||||||
|
super(StreamPlayerType.WEBGL2, $video, 'WebGL2Player');
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCanvas() {
|
||||||
|
console.log('updateCanvas', this.options);
|
||||||
|
|
||||||
|
const gl = this.gl!;
|
||||||
|
const program = this.program!;
|
||||||
|
const filterId = this.toFilterId(this.options.processing);
|
||||||
|
|
||||||
|
gl.uniform2f(gl.getUniformLocation(program, 'iResolution'), this.$canvas.width, this.$canvas.height);
|
||||||
|
|
||||||
|
gl.uniform1i(gl.getUniformLocation(program, 'filterId'), filterId);
|
||||||
|
gl.uniform1f(gl.getUniformLocation(program, 'sharpenFactor'), this.options.sharpness);
|
||||||
|
gl.uniform1f(gl.getUniformLocation(program, 'brightness'), this.options.brightness / 100);
|
||||||
|
gl.uniform1f(gl.getUniformLocation(program, 'contrast'), this.options.contrast / 100);
|
||||||
|
gl.uniform1f(gl.getUniformLocation(program, 'saturation'), this.options.saturation / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFrame() {
|
||||||
|
const gl = this.gl!;
|
||||||
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video);
|
||||||
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async setupShaders(): Promise<void> {
|
||||||
|
const gl = this.$canvas.getContext('webgl2', {
|
||||||
|
isBx: true,
|
||||||
|
antialias: true,
|
||||||
|
alpha: false,
|
||||||
|
depth: false,
|
||||||
|
preserveDrawingBuffer: false,
|
||||||
|
stencil: false,
|
||||||
|
powerPreference: getStreamPref(StreamPref.VIDEO_POWER_PREFERENCE),
|
||||||
|
} as WebGLContextAttributes) as WebGL2RenderingContext;
|
||||||
|
this.gl = gl;
|
||||||
|
|
||||||
|
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);
|
||||||
|
|
||||||
|
// Vertex shader: Identity map
|
||||||
|
const vShader = gl.createShader(gl.VERTEX_SHADER)!;
|
||||||
|
gl.shaderSource(vShader, vertClarityBoost);
|
||||||
|
gl.compileShader(vShader);
|
||||||
|
|
||||||
|
const fShader = gl.createShader(gl.FRAGMENT_SHADER)!;
|
||||||
|
gl.shaderSource(fShader, fsClarityBoost);
|
||||||
|
gl.compileShader(fShader);
|
||||||
|
|
||||||
|
// Create and link program
|
||||||
|
const program = gl.createProgram()!;
|
||||||
|
this.program = program;
|
||||||
|
|
||||||
|
gl.attachShader(program, vShader);
|
||||||
|
gl.attachShader(program, fShader);
|
||||||
|
gl.linkProgram(program);
|
||||||
|
gl.useProgram(program);
|
||||||
|
|
||||||
|
if (!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();
|
||||||
|
|
||||||
|
// Vertices: A screen-filling quad made from two triangles
|
||||||
|
const buffer = gl.createBuffer();
|
||||||
|
this.resources.push(buffer);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
||||||
|
-1.0, -1.0, // Bottom-left
|
||||||
|
3.0, -1.0, // Bottom-right
|
||||||
|
-1.0, 3.0, // Top-left
|
||||||
|
]), gl.STATIC_DRAW);
|
||||||
|
|
||||||
|
gl.enableVertexAttribArray(0);
|
||||||
|
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
||||||
|
|
||||||
|
// Texture to contain the video data
|
||||||
|
const texture = gl.createTexture();
|
||||||
|
this.resources.push(texture);
|
||||||
|
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||||
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Bind texture to the "data" argument to the fragment shader
|
||||||
|
gl.uniform1i(gl.getUniformLocation(program, 'data'), 0);
|
||||||
|
|
||||||
|
gl.activeTexture(gl.TEXTURE0);
|
||||||
|
// gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
|
|
||||||
|
const gl = this.gl;
|
||||||
|
if (!gl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||||
|
gl.useProgram(null);
|
||||||
|
|
||||||
|
for (const resource of this.resources) {
|
||||||
|
if (resource instanceof WebGLProgram) {
|
||||||
|
gl.deleteProgram(resource);
|
||||||
|
} else if (resource instanceof WebGLShader) {
|
||||||
|
gl.deleteShader(resource);
|
||||||
|
} else if (resource instanceof WebGLTexture) {
|
||||||
|
gl.deleteTexture(resource);
|
||||||
|
} else if (resource instanceof WebGLBuffer) {
|
||||||
|
gl.deleteBuffer(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshPlayer(): void {
|
||||||
|
this.updateCanvas();
|
||||||
|
}
|
||||||
|
}
|
93
src/modules/player/webgpu/shaders/clarity-boost.wgsl
Normal file
93
src/modules/player/webgpu/shaders/clarity-boost.wgsl
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
struct Params {
|
||||||
|
filterId: f32,
|
||||||
|
sharpness: f32,
|
||||||
|
brightness: f32,
|
||||||
|
contrast: f32,
|
||||||
|
saturation: f32,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) position: vec4<f32>,
|
||||||
|
@location(0) uv: vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@group(0) @binding(0) var ourSampler: sampler;
|
||||||
|
@group(0) @binding(1) var ourTexture: texture_external;
|
||||||
|
@group(0) @binding(2) var<uniform> ourParams: Params;
|
||||||
|
|
||||||
|
|
||||||
|
const FILTER_UNSHARP_MASKING: f32 = 1.0;
|
||||||
|
const CAS_CONTRAST_PEAK: f32 = 0.8 * -3.0 + 8.0;
|
||||||
|
// Luminosity factor: https://www.w3.org/TR/AERT/#color-contrast
|
||||||
|
const LUMINOSITY_FACTOR = vec3(0.299, 0.587, 0.114);
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vsMain(@location(0) pos: vec2<f32>) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
out.position = vec4(pos, 0.0, 1.0);
|
||||||
|
// Flip the Y-coordinate of UVs
|
||||||
|
out.uv = (vec2(pos.x, 1.0 - (pos.y + 1.0)) + vec2(1.0, 1.0)) * 0.5;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn clarityBoost(coord: vec2<f32>, texSize: vec2<f32>, e: vec3<f32>) -> vec3<f32> {
|
||||||
|
let texelSize = 1.0 / texSize;
|
||||||
|
|
||||||
|
// Load 3x3 neighborhood samples
|
||||||
|
let a = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2(-1.0, 1.0)).rgb;
|
||||||
|
let b = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2( 0.0, 1.0)).rgb;
|
||||||
|
let c = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2( 1.0, 1.0)).rgb;
|
||||||
|
|
||||||
|
let d = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2(-1.0, 0.0)).rgb;
|
||||||
|
let f = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2( 1.0, 0.0)).rgb;
|
||||||
|
|
||||||
|
let g = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2(-1.0, -1.0)).rgb;
|
||||||
|
let h = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2( 0.0, -1.0)).rgb;
|
||||||
|
let i = textureSampleBaseClampToEdge(ourTexture, ourSampler, coord + texelSize * vec2( 1.0, -1.0)).rgb;
|
||||||
|
|
||||||
|
// Unsharp Masking (USM)
|
||||||
|
if ourParams.filterId == FILTER_UNSHARP_MASKING {
|
||||||
|
let gaussianBlur = (a + c + g + i) * 1.0 + (b + d + f + h) * 2.0 + e * 4.0;
|
||||||
|
let blurred = gaussianBlur / 16.0;
|
||||||
|
return e + (e - blurred) * (ourParams.sharpness / 3.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contrast Adaptive Sharpening (CAS)
|
||||||
|
let minRgb = min(min(min(d, e), min(f, b)), h) + min(min(a, c), min(g, i));
|
||||||
|
let maxRgb = max(max(max(d, e), max(f, b)), h) + max(max(a, c), max(g, i));
|
||||||
|
|
||||||
|
let reciprocalMaxRgb = 1.0 / maxRgb;
|
||||||
|
var amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, vec3(0.0), vec3(1.0));
|
||||||
|
amplifyRgb = 1.0 / sqrt(amplifyRgb);
|
||||||
|
|
||||||
|
let weightRgb = -(1.0 / (amplifyRgb * CAS_CONTRAST_PEAK));
|
||||||
|
let reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);
|
||||||
|
|
||||||
|
let window = b + d + f + h;
|
||||||
|
let outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, vec3(0.0), vec3(1.0));
|
||||||
|
|
||||||
|
return mix(e, outColor, ourParams.sharpness / 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fsMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let texSize = vec2<f32>(textureDimensions(ourTexture));
|
||||||
|
let center = textureSampleBaseClampToEdge(ourTexture, ourSampler, input.uv);
|
||||||
|
var adjustedRgb = clarityBoost(input.uv, texSize, center.rgb);
|
||||||
|
|
||||||
|
// Compute grayscale intensity
|
||||||
|
let gray = dot(adjustedRgb, LUMINOSITY_FACTOR);
|
||||||
|
// Interpolate between grayscale and color
|
||||||
|
adjustedRgb = mix(vec3(gray), adjustedRgb, ourParams.saturation);
|
||||||
|
|
||||||
|
// Adjust contrast
|
||||||
|
adjustedRgb = (adjustedRgb - 0.5) * ourParams.contrast + 0.5;
|
||||||
|
|
||||||
|
// Adjust brightness
|
||||||
|
adjustedRgb *= ourParams.brightness;
|
||||||
|
return vec4(adjustedRgb, 1.0);
|
||||||
|
}
|
186
src/modules/player/webgpu/webgpu-player.ts
Normal file
186
src/modules/player/webgpu/webgpu-player.ts
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import wgslClarityBoost from "./shaders/clarity-boost.wgsl" with { type: "text" };
|
||||||
|
import { BaseCanvasPlayer } from "../base-canvas-player";
|
||||||
|
import { StreamPlayerType } from "@/enums/pref-values";
|
||||||
|
import { BxEventBus } from "@/utils/bx-event-bus";
|
||||||
|
|
||||||
|
export class WebGPUPlayer extends BaseCanvasPlayer {
|
||||||
|
static device: GPUDevice;
|
||||||
|
|
||||||
|
context!: GPUCanvasContext | null;
|
||||||
|
pipeline!: GPURenderPipeline | null;
|
||||||
|
sampler!: GPUSampler | null;
|
||||||
|
bindGroup!: GPUBindGroup | null;
|
||||||
|
optionsUpdated: boolean = false;
|
||||||
|
paramsBuffer!: GPUBuffer | null;
|
||||||
|
vertexBuffer!: GPUBuffer | null;
|
||||||
|
|
||||||
|
static async prepare(): Promise<void> {
|
||||||
|
if (!navigator.gpu) {
|
||||||
|
BxEventBus.Script.emit('webgpu.ready', {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const adapter = await navigator.gpu.requestAdapter();
|
||||||
|
|
||||||
|
if (adapter) {
|
||||||
|
WebGPUPlayer.device = await adapter.requestDevice();
|
||||||
|
WebGPUPlayer.device?.addEventListener('uncapturederror', e => {
|
||||||
|
console.error((e as GPUUncapturedErrorEvent).error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
alert(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
BxEventBus.Script.emit('webgpu.ready', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor($video: HTMLVideoElement) {
|
||||||
|
super(StreamPlayerType.WEBGPU, $video, 'WebGPUPlayer');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setupShaders(): void {
|
||||||
|
this.context = this.$canvas.getContext('webgpu')!;
|
||||||
|
if (!this.context) {
|
||||||
|
alert('Can\'t initiate context');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const format = navigator.gpu.getPreferredCanvasFormat();
|
||||||
|
this.context.configure({
|
||||||
|
device: WebGPUPlayer.device,
|
||||||
|
format,
|
||||||
|
alphaMode: 'opaque',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.vertexBuffer = WebGPUPlayer.device.createBuffer({
|
||||||
|
label: 'vertex buffer',
|
||||||
|
size: 6 * 4, // 6 floats (2 per vertex)
|
||||||
|
usage: GPUBufferUsage.VERTEX,
|
||||||
|
mappedAtCreation: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mappedRange = this.vertexBuffer.getMappedRange();
|
||||||
|
new Float32Array(mappedRange).set([
|
||||||
|
-1, 3, // Vertex 1
|
||||||
|
-1, -1, // Vertex 2
|
||||||
|
3, -1, // Vertex 3
|
||||||
|
]);
|
||||||
|
this.vertexBuffer.unmap();
|
||||||
|
|
||||||
|
const shaderModule = WebGPUPlayer.device.createShaderModule({ code: wgslClarityBoost });
|
||||||
|
this.pipeline = WebGPUPlayer.device.createRenderPipeline({
|
||||||
|
layout: 'auto',
|
||||||
|
vertex: {
|
||||||
|
module: shaderModule,
|
||||||
|
entryPoint: 'vsMain',
|
||||||
|
buffers: [{
|
||||||
|
arrayStride: 8,
|
||||||
|
attributes: [{
|
||||||
|
format: 'float32x2',
|
||||||
|
offset: 0,
|
||||||
|
shaderLocation: 0,
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
fragment: {
|
||||||
|
module: shaderModule,
|
||||||
|
entryPoint: 'fsMain',
|
||||||
|
targets: [{ format }],
|
||||||
|
},
|
||||||
|
primitive: { topology: 'triangle-list' },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sampler = WebGPUPlayer.device.createSampler({ magFilter: 'linear', minFilter: 'linear' });
|
||||||
|
this.updateCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
private prepareUniformBuffer(value: any, classType: any) {
|
||||||
|
const uniform = new classType(value);
|
||||||
|
const uniformBuffer = WebGPUPlayer.device.createBuffer({
|
||||||
|
size: uniform.byteLength,
|
||||||
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
||||||
|
});
|
||||||
|
|
||||||
|
WebGPUPlayer.device.queue.writeBuffer(uniformBuffer, 0, uniform);
|
||||||
|
return uniformBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCanvas() {
|
||||||
|
const externalTexture = WebGPUPlayer.device.importExternalTexture({ source: this.$video });
|
||||||
|
|
||||||
|
if (!this.optionsUpdated) {
|
||||||
|
this.paramsBuffer = this.prepareUniformBuffer([
|
||||||
|
this.toFilterId(this.options.processing),
|
||||||
|
this.options.sharpness,
|
||||||
|
this.options.brightness / 100,
|
||||||
|
this.options.contrast / 100,
|
||||||
|
this.options.saturation / 100,
|
||||||
|
], Float32Array);
|
||||||
|
|
||||||
|
this.optionsUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bindGroup = WebGPUPlayer.device.createBindGroup({
|
||||||
|
layout: this.pipeline!.getBindGroupLayout(0),
|
||||||
|
entries: [
|
||||||
|
{ binding: 0, resource: this.sampler },
|
||||||
|
{ binding: 1, resource: externalTexture as any },
|
||||||
|
{ binding: 2, resource: { buffer: this.paramsBuffer } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFrame(): void {
|
||||||
|
this.updateCanvas();
|
||||||
|
|
||||||
|
const commandEncoder = WebGPUPlayer.device.createCommandEncoder();
|
||||||
|
const passEncoder = commandEncoder.beginRenderPass({
|
||||||
|
colorAttachments: [{
|
||||||
|
view: this.context!.getCurrentTexture().createView(),
|
||||||
|
loadOp: 'clear',
|
||||||
|
storeOp: 'store',
|
||||||
|
clearValue: [0.0, 0.0, 0.0, 1.0],
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
passEncoder.setPipeline(this.pipeline!);
|
||||||
|
passEncoder.setBindGroup(0, this.bindGroup);
|
||||||
|
passEncoder.setVertexBuffer(0, this.vertexBuffer);
|
||||||
|
passEncoder.draw(3);
|
||||||
|
passEncoder.end();
|
||||||
|
|
||||||
|
WebGPUPlayer.device.queue.submit([commandEncoder.finish()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshPlayer(): void {
|
||||||
|
this.optionsUpdated = false;
|
||||||
|
this.updateCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
super.destroy();
|
||||||
|
|
||||||
|
this.isStopped = true;
|
||||||
|
|
||||||
|
// Unset GPU resources
|
||||||
|
this.pipeline = null;
|
||||||
|
this.bindGroup = null;
|
||||||
|
this.sampler = null;
|
||||||
|
|
||||||
|
this.paramsBuffer?.destroy();
|
||||||
|
this.paramsBuffer = null;
|
||||||
|
|
||||||
|
this.vertexBuffer?.destroy();
|
||||||
|
this.vertexBuffer = null;
|
||||||
|
|
||||||
|
// Reset the WebGPU context (force garbage collection)
|
||||||
|
if (this.context) {
|
||||||
|
this.context.unconfigure();
|
||||||
|
this.context = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('WebGPU context successfully freed.');
|
||||||
|
}
|
||||||
|
}
|
@ -72,12 +72,11 @@ export class SettingsManager {
|
|||||||
},
|
},
|
||||||
[StreamPref.VIDEO_POWER_PREFERENCE]: {
|
[StreamPref.VIDEO_POWER_PREFERENCE]: {
|
||||||
onChange: () => {
|
onChange: () => {
|
||||||
const streamPlayer = STATES.currentStream.streamPlayer;
|
const streamPlayer = STATES.currentStream.streamPlayerManager;
|
||||||
if (!streamPlayer) {
|
if (!streamPlayer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
streamPlayer.reloadPlayer();
|
|
||||||
updateVideoPlayer();
|
updateVideoPlayer();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
191
src/modules/stream-player-manager.ts
Executable file
191
src/modules/stream-player-manager.ts
Executable file
@ -0,0 +1,191 @@
|
|||||||
|
import { WebGL2Player } from "./player/webgl2/webgl2-player";
|
||||||
|
import { ScreenshotManager } from "@/utils/screenshot-manager";
|
||||||
|
import { STATES } from "@/utils/global";
|
||||||
|
import { StreamPref } from "@/enums/pref-keys";
|
||||||
|
import { BX_FLAGS } from "@/utils/bx-flags";
|
||||||
|
import { StreamPlayerType, VideoPosition } from "@/enums/pref-values";
|
||||||
|
import { getStreamPref } from "@/utils/pref-utils";
|
||||||
|
import type { BaseCanvasPlayer } from "./player/base-canvas-player";
|
||||||
|
import { VideoPlayer } from "./player/video/video-player";
|
||||||
|
import { StreamPlayerElement } from "./player/base-stream-player";
|
||||||
|
import { WebGPUPlayer } from "./player/webgpu/webgpu-player";
|
||||||
|
import type { StreamPlayerOptions } from "@/types/stream";
|
||||||
|
|
||||||
|
|
||||||
|
export class StreamPlayerManager {
|
||||||
|
private static instance: StreamPlayerManager;
|
||||||
|
public static getInstance = () => StreamPlayerManager.instance ?? (StreamPlayerManager.instance = new StreamPlayerManager());
|
||||||
|
|
||||||
|
private $video!: HTMLVideoElement;
|
||||||
|
private videoPlayer!: VideoPlayer;
|
||||||
|
private canvasPlayer: BaseCanvasPlayer | null | undefined;
|
||||||
|
private playerType: StreamPlayerType = StreamPlayerType.VIDEO;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
setVideoElement($video: HTMLVideoElement) {
|
||||||
|
this.$video = $video;
|
||||||
|
this.videoPlayer = new VideoPlayer($video, 'VideoPlayer');
|
||||||
|
this.videoPlayer.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
resizePlayer() {
|
||||||
|
const PREF_RATIO = getStreamPref(StreamPref.VIDEO_RATIO);
|
||||||
|
const $video = this.$video;
|
||||||
|
const isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport;
|
||||||
|
|
||||||
|
let targetWidth;
|
||||||
|
let targetHeight;
|
||||||
|
let targetObjectFit;
|
||||||
|
|
||||||
|
if (PREF_RATIO.includes(':')) {
|
||||||
|
const tmp = PREF_RATIO.split(':');
|
||||||
|
|
||||||
|
// Get preferred ratio
|
||||||
|
const videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]);
|
||||||
|
|
||||||
|
let width = 0;
|
||||||
|
let height = 0;
|
||||||
|
|
||||||
|
// Get parent's ratio
|
||||||
|
const parentRect = $video.parentElement!.getBoundingClientRect();
|
||||||
|
const parentRatio = parentRect.width / parentRect.height;
|
||||||
|
|
||||||
|
// Get target width & height
|
||||||
|
if (parentRatio > videoRatio) {
|
||||||
|
height = parentRect.height;
|
||||||
|
width = height * videoRatio;
|
||||||
|
} else {
|
||||||
|
width = parentRect.width;
|
||||||
|
height = width / videoRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid floating points
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Set position
|
||||||
|
const $parent = $video.parentElement!;
|
||||||
|
const position = getStreamPref(StreamPref.VIDEO_POSITION);
|
||||||
|
$parent.style.removeProperty('padding-top');
|
||||||
|
|
||||||
|
$parent.dataset.position = position;
|
||||||
|
if (position === VideoPosition.TOP_HALF || position === VideoPosition.BOTTOM_HALF) {
|
||||||
|
let padding = Math.floor((window.innerHeight - height) / 4);
|
||||||
|
if (padding > 0) {
|
||||||
|
if (position === VideoPosition.BOTTOM_HALF) {
|
||||||
|
padding *= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parent.style.paddingTop = padding + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update size
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
$video.style.width = targetWidth;
|
||||||
|
$video.style.height = targetHeight;
|
||||||
|
$video.style.objectFit = targetObjectFit;
|
||||||
|
|
||||||
|
if (this.canvasPlayer) {
|
||||||
|
const $canvas = this.canvasPlayer.getCanvas();
|
||||||
|
$canvas.style.width = targetWidth;
|
||||||
|
$canvas.style.height = targetHeight;
|
||||||
|
$canvas.style.objectFit = targetObjectFit;
|
||||||
|
|
||||||
|
$video.dispatchEvent(new Event('resize'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update video dimensions
|
||||||
|
if (isNativeTouchGame && this.playerType !== StreamPlayerType.VIDEO) {
|
||||||
|
window.BX_EXPOSED.streamSession.updateDimensions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switchPlayerType(type: StreamPlayerType, refreshPlayer: boolean = false) {
|
||||||
|
if (this.playerType !== type) {
|
||||||
|
const videoClass = BX_FLAGS.DeviceInfo.deviceType === 'android-tv' ? 'bx-pixel' : 'bx-gone';
|
||||||
|
|
||||||
|
// Destroy old player
|
||||||
|
this.cleanUpCanvasPlayer();
|
||||||
|
|
||||||
|
if (type === StreamPlayerType.VIDEO) {
|
||||||
|
// Switch from Canvas -> Video
|
||||||
|
this.$video.classList.remove(videoClass);
|
||||||
|
} else {
|
||||||
|
// Switch from Video -> Canvas
|
||||||
|
if (type === StreamPlayerType.WEBGPU) {
|
||||||
|
this.canvasPlayer = new WebGPUPlayer(this.$video);
|
||||||
|
} else {
|
||||||
|
this.canvasPlayer = new WebGL2Player(this.$video);
|
||||||
|
}
|
||||||
|
this.canvasPlayer.init();
|
||||||
|
|
||||||
|
this.videoPlayer.clearFilters();
|
||||||
|
this.$video.classList.add(videoClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playerType = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshPlayer && this.refreshPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOptions(options: StreamPlayerOptions, refreshPlayer: boolean = false) {
|
||||||
|
(this.canvasPlayer || this.videoPlayer).updateOptions(options, refreshPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlayerElement(elementType?: StreamPlayerElement) {
|
||||||
|
if (typeof elementType === 'undefined') {
|
||||||
|
elementType = this.playerType === StreamPlayerType.VIDEO ? StreamPlayerElement.VIDEO : StreamPlayerElement.CANVAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elementType !== StreamPlayerElement.VIDEO) {
|
||||||
|
return this.canvasPlayer?.getCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.$video;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCanvasPlayer() {
|
||||||
|
return this.canvasPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshPlayer() {
|
||||||
|
if (this.playerType === StreamPlayerType.VIDEO) {
|
||||||
|
this.videoPlayer.refreshPlayer();
|
||||||
|
} else {
|
||||||
|
ScreenshotManager.getInstance().updateCanvasFilters('none');
|
||||||
|
this.canvasPlayer?.refreshPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resizePlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
getVideoPlayerFilterStyle() {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanUpCanvasPlayer() {
|
||||||
|
this.canvasPlayer?.destroy();
|
||||||
|
this.canvasPlayer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.cleanUpCanvasPlayer();
|
||||||
|
}
|
||||||
|
}
|
@ -1,297 +0,0 @@
|
|||||||
import { isFullVersion } from "@macros/build" with { type: "macro" };
|
|
||||||
|
|
||||||
import { CE } from "@/utils/html";
|
|
||||||
import { WebGL2Player } from "./player/webgl2-player";
|
|
||||||
import { ScreenshotManager } from "@/utils/screenshot-manager";
|
|
||||||
import { STATES } from "@/utils/global";
|
|
||||||
import { GlobalPref, StreamPref } from "@/enums/pref-keys";
|
|
||||||
import { getGlobalPref } from "@/utils/pref-utils";
|
|
||||||
import { BX_FLAGS } from "@/utils/bx-flags";
|
|
||||||
import { StreamPlayerType, StreamVideoProcessing, VideoPosition } from "@/enums/pref-values";
|
|
||||||
import { getStreamPref } from "@/utils/pref-utils";
|
|
||||||
|
|
||||||
|
|
||||||
export class StreamPlayer {
|
|
||||||
private $video: HTMLVideoElement;
|
|
||||||
private playerType: StreamPlayerType = StreamPlayerType.VIDEO;
|
|
||||||
|
|
||||||
private options: StreamPlayerOptions = {};
|
|
||||||
|
|
||||||
private webGL2Player: WebGL2Player | null = null;
|
|
||||||
|
|
||||||
private $videoCss: HTMLStyleElement | null = null;
|
|
||||||
private $usmMatrix: SVGFEConvolveMatrixElement | null = null;
|
|
||||||
|
|
||||||
constructor($video: HTMLVideoElement, type: StreamPlayerType, options: StreamPlayerOptions) {
|
|
||||||
this.setupVideoElements();
|
|
||||||
|
|
||||||
this.$video = $video;
|
|
||||||
this.options = options || {};
|
|
||||||
this.setPlayerType(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupVideoElements() {
|
|
||||||
this.$videoCss = document.getElementById('bx-video-css') as HTMLStyleElement;
|
|
||||||
if (this.$videoCss) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const $fragment = document.createDocumentFragment();
|
|
||||||
|
|
||||||
this.$videoCss = CE('style', { id: 'bx-video-css' });
|
|
||||||
$fragment.appendChild(this.$videoCss);
|
|
||||||
|
|
||||||
// Setup SVG filters
|
|
||||||
const $svg = CE('svg', {
|
|
||||||
id: 'bx-video-filters',
|
|
||||||
xmlns: 'http://www.w3.org/2000/svg',
|
|
||||||
class: 'bx-gone',
|
|
||||||
}, CE('defs', { xmlns: 'http://www.w3.org/2000/svg' },
|
|
||||||
CE('filter', {
|
|
||||||
id: 'bx-filter-usm',
|
|
||||||
xmlns: 'http://www.w3.org/2000/svg',
|
|
||||||
}, this.$usmMatrix = CE('feConvolveMatrix', {
|
|
||||||
id: 'bx-filter-usm-matrix',
|
|
||||||
order: '3',
|
|
||||||
xmlns: 'http://www.w3.org/2000/svg',
|
|
||||||
}) as unknown as SVGFEConvolveMatrixElement),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
$fragment.appendChild($svg);
|
|
||||||
document.documentElement.appendChild($fragment);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getVideoPlayerFilterStyle() {
|
|
||||||
const filters = [];
|
|
||||||
|
|
||||||
const sharpness = this.options.sharpness || 0;
|
|
||||||
if (this.options.processing === StreamVideoProcessing.USM && sharpness != 0) {
|
|
||||||
const level = (7 - ((sharpness / 2) - 1) * 0.5).toFixed(1); // 5, 5.5, 6, 6.5, 7
|
|
||||||
const matrix = `0 -1 0 -1 ${level} -1 0 -1 0`;
|
|
||||||
this.$usmMatrix?.setAttributeNS(null, 'kernelMatrix', matrix);
|
|
||||||
|
|
||||||
filters.push(`url(#bx-filter-usm)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const saturation = this.options.saturation || 100;
|
|
||||||
if (saturation != 100) {
|
|
||||||
filters.push(`saturate(${saturation}%)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const contrast = this.options.contrast || 100;
|
|
||||||
if (contrast != 100) {
|
|
||||||
filters.push(`contrast(${contrast}%)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const brightness = this.options.brightness || 100;
|
|
||||||
if (brightness != 100) {
|
|
||||||
filters.push(`brightness(${brightness}%)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filters.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
private resizePlayer() {
|
|
||||||
const PREF_RATIO = getStreamPref(StreamPref.VIDEO_RATIO);
|
|
||||||
const $video = this.$video;
|
|
||||||
const isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport;
|
|
||||||
|
|
||||||
let $webGL2Canvas;
|
|
||||||
if (this.playerType == StreamPlayerType.WEBGL2) {
|
|
||||||
$webGL2Canvas = this.webGL2Player?.getCanvas()!;
|
|
||||||
}
|
|
||||||
|
|
||||||
let targetWidth;
|
|
||||||
let targetHeight;
|
|
||||||
let targetObjectFit;
|
|
||||||
|
|
||||||
if (PREF_RATIO.includes(':')) {
|
|
||||||
const tmp = PREF_RATIO.split(':');
|
|
||||||
|
|
||||||
// Get preferred ratio
|
|
||||||
const videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]);
|
|
||||||
|
|
||||||
let width = 0;
|
|
||||||
let height = 0;
|
|
||||||
|
|
||||||
// Get parent's ratio
|
|
||||||
const parentRect = $video.parentElement!.getBoundingClientRect();
|
|
||||||
const parentRatio = parentRect.width / parentRect.height;
|
|
||||||
|
|
||||||
// Get target width & height
|
|
||||||
if (parentRatio > videoRatio) {
|
|
||||||
height = parentRect.height;
|
|
||||||
width = height * videoRatio;
|
|
||||||
} else {
|
|
||||||
width = parentRect.width;
|
|
||||||
height = width / videoRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent floating points
|
|
||||||
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();
|
|
||||||
|
|
||||||
// Set position
|
|
||||||
const $parent = $video.parentElement!;
|
|
||||||
const position = getStreamPref(StreamPref.VIDEO_POSITION);
|
|
||||||
$parent.style.removeProperty('padding-top');
|
|
||||||
|
|
||||||
$parent.dataset.position = position;
|
|
||||||
if (position === VideoPosition.TOP_HALF || position === VideoPosition.BOTTOM_HALF) {
|
|
||||||
let padding = Math.floor((window.innerHeight - height) / 4);
|
|
||||||
if (padding > 0) {
|
|
||||||
if (position === VideoPosition.BOTTOM_HALF) {
|
|
||||||
padding *= 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
$parent.style.paddingTop = padding + 'px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update size
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
$video.style.width = targetWidth;
|
|
||||||
$video.style.height = targetHeight;
|
|
||||||
$video.style.objectFit = targetObjectFit;
|
|
||||||
|
|
||||||
// $video.style.padding = padding;
|
|
||||||
|
|
||||||
if ($webGL2Canvas) {
|
|
||||||
$webGL2Canvas.style.width = targetWidth;
|
|
||||||
$webGL2Canvas.style.height = targetHeight;
|
|
||||||
$webGL2Canvas.style.objectFit = targetObjectFit;
|
|
||||||
|
|
||||||
$video.dispatchEvent(new Event('resize'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update video dimensions
|
|
||||||
if (isNativeTouchGame && this.playerType == StreamPlayerType.WEBGL2) {
|
|
||||||
window.BX_EXPOSED.streamSession.updateDimensions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setPlayerType(type: StreamPlayerType, refreshPlayer: boolean = false) {
|
|
||||||
if (this.playerType !== type) {
|
|
||||||
const videoClass = BX_FLAGS.DeviceInfo.deviceType === 'android-tv' ? 'bx-pixel' : 'bx-gone';
|
|
||||||
|
|
||||||
// Switch from Video -> WebGL2
|
|
||||||
if (type === StreamPlayerType.WEBGL2) {
|
|
||||||
// Initialize WebGL2 player
|
|
||||||
if (!this.webGL2Player) {
|
|
||||||
this.webGL2Player = new WebGL2Player(this.$video);
|
|
||||||
} else {
|
|
||||||
this.webGL2Player.resume();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$videoCss!.textContent = '';
|
|
||||||
|
|
||||||
this.$video.classList.add(videoClass);
|
|
||||||
} else {
|
|
||||||
// Cleanup WebGL2 Player
|
|
||||||
this.webGL2Player?.stop();
|
|
||||||
|
|
||||||
this.$video.classList.remove(videoClass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.playerType = type;
|
|
||||||
refreshPlayer && this.refreshPlayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
setOptions(options: StreamPlayerOptions, refreshPlayer: boolean = false) {
|
|
||||||
this.options = options;
|
|
||||||
refreshPlayer && this.refreshPlayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateOptions(options: StreamPlayerOptions, refreshPlayer: boolean = false) {
|
|
||||||
this.options = Object.assign(this.options, options);
|
|
||||||
refreshPlayer && this.refreshPlayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
getPlayerElement(playerType?: StreamPlayerType) {
|
|
||||||
if (typeof playerType === 'undefined') {
|
|
||||||
playerType = this.playerType;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playerType === StreamPlayerType.WEBGL2) {
|
|
||||||
return this.webGL2Player?.getCanvas();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.$video;
|
|
||||||
}
|
|
||||||
|
|
||||||
getWebGL2Player() {
|
|
||||||
return this.webGL2Player;
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshPlayer() {
|
|
||||||
if (this.playerType === StreamPlayerType.WEBGL2) {
|
|
||||||
const options = this.options;
|
|
||||||
const webGL2Player = this.webGL2Player!;
|
|
||||||
|
|
||||||
if (options.processing === StreamVideoProcessing.USM) {
|
|
||||||
webGL2Player.setFilter(1);
|
|
||||||
} else {
|
|
||||||
webGL2Player.setFilter(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
isFullVersion() && ScreenshotManager.getInstance().updateCanvasFilters('none');
|
|
||||||
|
|
||||||
webGL2Player.setSharpness(options.sharpness || 0);
|
|
||||||
webGL2Player.setSaturation(options.saturation || 100);
|
|
||||||
webGL2Player.setContrast(options.contrast || 100);
|
|
||||||
webGL2Player.setBrightness(options.brightness || 100);
|
|
||||||
} else {
|
|
||||||
let filters = this.getVideoPlayerFilterStyle();
|
|
||||||
let videoCss = '';
|
|
||||||
if (filters) {
|
|
||||||
videoCss += `filter: ${filters} !important;`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply video filters to screenshots
|
|
||||||
if (isFullVersion() && getGlobalPref(GlobalPref.SCREENSHOT_APPLY_FILTERS)) {
|
|
||||||
ScreenshotManager.getInstance().updateCanvasFilters(filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
let css = '';
|
|
||||||
if (videoCss) {
|
|
||||||
css = `#game-stream video { ${videoCss} }`;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$videoCss!.textContent = css;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.resizePlayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
reloadPlayer() {
|
|
||||||
this.cleanUpWebGL2Player();
|
|
||||||
|
|
||||||
this.playerType = StreamPlayerType.VIDEO;
|
|
||||||
this.setPlayerType(StreamPlayerType.WEBGL2, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private cleanUpWebGL2Player() {
|
|
||||||
// Clean up WebGL2 Player
|
|
||||||
this.webGL2Player?.destroy();
|
|
||||||
this.webGL2Player = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.cleanUpWebGL2Player();
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,6 +4,7 @@ import { StreamPref } from "@/enums/pref-keys";
|
|||||||
import { StreamVideoProcessing, StreamPlayerType } from "@/enums/pref-values";
|
import { StreamVideoProcessing, StreamPlayerType } from "@/enums/pref-values";
|
||||||
import { getStreamPref, setStreamPref } from "@/utils/pref-utils";
|
import { getStreamPref, setStreamPref } from "@/utils/pref-utils";
|
||||||
import { SettingsManager } from "../settings-manager";
|
import { SettingsManager } from "../settings-manager";
|
||||||
|
import type { StreamPlayerOptions } from "@/types/stream";
|
||||||
|
|
||||||
export function onChangeVideoPlayerType() {
|
export function onChangeVideoPlayerType() {
|
||||||
const playerType = getStreamPref(StreamPref.VIDEO_PLAYER_TYPE);
|
const playerType = getStreamPref(StreamPref.VIDEO_PLAYER_TYPE);
|
||||||
@ -21,9 +22,7 @@ export function onChangeVideoPlayerType() {
|
|||||||
|
|
||||||
const $optCas = $videoProcessing.querySelector<HTMLOptionElement>(`option[value=${StreamVideoProcessing.CAS}]`);
|
const $optCas = $videoProcessing.querySelector<HTMLOptionElement>(`option[value=${StreamVideoProcessing.CAS}]`);
|
||||||
|
|
||||||
if (playerType === StreamPlayerType.WEBGL2) {
|
if (playerType === StreamPlayerType.VIDEO) {
|
||||||
$optCas && ($optCas.disabled = false);
|
|
||||||
} else {
|
|
||||||
// Only allow USM when player type is Video
|
// Only allow USM when player type is Video
|
||||||
$videoProcessing.value = StreamVideoProcessing.USM;
|
$videoProcessing.value = StreamVideoProcessing.USM;
|
||||||
setStreamPref(StreamPref.VIDEO_PROCESSING, StreamVideoProcessing.USM, 'direct');
|
setStreamPref(StreamPref.VIDEO_PROCESSING, StreamVideoProcessing.USM, 'direct');
|
||||||
@ -33,6 +32,8 @@ export function onChangeVideoPlayerType() {
|
|||||||
if (UserAgent.isSafari()) {
|
if (UserAgent.isSafari()) {
|
||||||
isDisabled = true;
|
isDisabled = true;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
$optCas && ($optCas.disabled = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
$videoProcessing.disabled = isDisabled;
|
$videoProcessing.disabled = isDisabled;
|
||||||
@ -40,24 +41,22 @@ 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);
|
$videoMaxFps.closest('.bx-settings-row')!.classList.toggle('bx-gone', playerType === StreamPlayerType.VIDEO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function limitVideoPlayerFps(targetFps: number) {
|
export function limitVideoPlayerFps(targetFps: number) {
|
||||||
const streamPlayer = STATES.currentStream.streamPlayer;
|
const streamPlayer = STATES.currentStream.streamPlayerManager;
|
||||||
streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps);
|
streamPlayer?.getCanvasPlayer()?.setTargetFps(targetFps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function updateVideoPlayer() {
|
export function updateVideoPlayer() {
|
||||||
const streamPlayer = STATES.currentStream.streamPlayer;
|
const streamPlayerManager = STATES.currentStream.streamPlayerManager;
|
||||||
if (!streamPlayer) {
|
if (!streamPlayerManager) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
limitVideoPlayerFps(getStreamPref(StreamPref.VIDEO_MAX_FPS));
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
processing: getStreamPref(StreamPref.VIDEO_PROCESSING),
|
processing: getStreamPref(StreamPref.VIDEO_PROCESSING),
|
||||||
sharpness: getStreamPref(StreamPref.VIDEO_SHARPNESS),
|
sharpness: getStreamPref(StreamPref.VIDEO_SHARPNESS),
|
||||||
@ -66,9 +65,15 @@ export function updateVideoPlayer() {
|
|||||||
brightness: getStreamPref(StreamPref.VIDEO_BRIGHTNESS),
|
brightness: getStreamPref(StreamPref.VIDEO_BRIGHTNESS),
|
||||||
} satisfies StreamPlayerOptions;
|
} satisfies StreamPlayerOptions;
|
||||||
|
|
||||||
streamPlayer.setPlayerType(getStreamPref(StreamPref.VIDEO_PLAYER_TYPE));
|
streamPlayerManager.switchPlayerType(getStreamPref(StreamPref.VIDEO_PLAYER_TYPE));
|
||||||
streamPlayer.updateOptions(options);
|
limitVideoPlayerFps(getStreamPref(StreamPref.VIDEO_MAX_FPS));
|
||||||
streamPlayer.refreshPlayer();
|
streamPlayerManager.updateOptions(options);
|
||||||
|
streamPlayerManager.refreshPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', updateVideoPlayer);
|
function resizeVideoPlayer() {
|
||||||
|
const streamPlayerManager = STATES.currentStream.streamPlayerManager;
|
||||||
|
streamPlayerManager?.resizePlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', resizeVideoPlayer);
|
||||||
|
60
src/types/index.d.ts
vendored
60
src/types/index.d.ts
vendored
@ -22,58 +22,6 @@ type ServerRegion = {
|
|||||||
contintent: ServerContinent;
|
contintent: ServerContinent;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BxStates = {
|
|
||||||
supportedRegion: boolean;
|
|
||||||
serverRegions: Record<string, ServerRegion>;
|
|
||||||
selectedRegion: any;
|
|
||||||
gsToken: string;
|
|
||||||
isSignedIn: boolean;
|
|
||||||
|
|
||||||
isPlaying: boolean;
|
|
||||||
|
|
||||||
browser: {
|
|
||||||
capabilities: {
|
|
||||||
touch: boolean;
|
|
||||||
batteryApi: boolean;
|
|
||||||
deviceVibration: boolean;
|
|
||||||
mkb: boolean;
|
|
||||||
emulatedNativeMkb: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
userAgent: {
|
|
||||||
isTv: boolean;
|
|
||||||
capabilities: {
|
|
||||||
touch: boolean;
|
|
||||||
mkb: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
currentStream: Partial<{
|
|
||||||
titleSlug: string;
|
|
||||||
titleInfo: XcloudTitleInfo;
|
|
||||||
xboxTitleId: number | null;
|
|
||||||
gameSpecificSettings: boolean;
|
|
||||||
|
|
||||||
streamPlayer: StreamPlayer | null;
|
|
||||||
|
|
||||||
peerConnection: RTCPeerConnection;
|
|
||||||
audioContext: AudioContext | null;
|
|
||||||
audioGainNode: GainNode | null;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
remotePlay: Partial<{
|
|
||||||
isPlaying: boolean;
|
|
||||||
server: string;
|
|
||||||
config: {
|
|
||||||
serverId: string;
|
|
||||||
};
|
|
||||||
titleId?: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
pointerServerPort: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type XcloudTitleInfo = {
|
type XcloudTitleInfo = {
|
||||||
titleId: string,
|
titleId: string,
|
||||||
|
|
||||||
@ -106,10 +54,12 @@ declare module '*.js' {
|
|||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '*.svg' {
|
declare module '*.svg' {
|
||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '*.styl' {
|
declare module '*.styl' {
|
||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
@ -119,11 +69,17 @@ declare module '*.fs' {
|
|||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '*.vert' {
|
declare module '*.vert' {
|
||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '*.wgsl' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
type MkbMouseMove = {
|
type MkbMouseMove = {
|
||||||
movementX: number;
|
movementX: number;
|
||||||
movementY: number;
|
movementY: number;
|
||||||
|
53
src/types/states.d.ts
vendored
Normal file
53
src/types/states.d.ts
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import type { StreamPlayerManager } from "@/modules/stream-player-manager";
|
||||||
|
|
||||||
|
type BxStates = {
|
||||||
|
supportedRegion: boolean;
|
||||||
|
serverRegions: Record<string, ServerRegion>;
|
||||||
|
selectedRegion: any;
|
||||||
|
gsToken: string;
|
||||||
|
isSignedIn: boolean;
|
||||||
|
|
||||||
|
isPlaying: boolean;
|
||||||
|
|
||||||
|
browser: {
|
||||||
|
capabilities: {
|
||||||
|
touch: boolean;
|
||||||
|
batteryApi: boolean;
|
||||||
|
deviceVibration: boolean;
|
||||||
|
mkb: boolean;
|
||||||
|
emulatedNativeMkb: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
userAgent: {
|
||||||
|
isTv: boolean;
|
||||||
|
capabilities: {
|
||||||
|
touch: boolean;
|
||||||
|
mkb: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
currentStream: Partial<{
|
||||||
|
titleSlug: string;
|
||||||
|
titleInfo: XcloudTitleInfo;
|
||||||
|
xboxTitleId: number | null;
|
||||||
|
gameSpecificSettings: boolean;
|
||||||
|
|
||||||
|
streamPlayerManager: StreamPlayerManager | null;
|
||||||
|
|
||||||
|
peerConnection: RTCPeerConnection;
|
||||||
|
audioContext: AudioContext | null;
|
||||||
|
audioGainNode: GainNode | null;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
remotePlay: Partial<{
|
||||||
|
isPlaying: boolean;
|
||||||
|
server: string;
|
||||||
|
config: {
|
||||||
|
serverId: string;
|
||||||
|
};
|
||||||
|
titleId?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
pointerServerPort: number;
|
||||||
|
}
|
8
src/types/stream.d.ts
vendored
8
src/types/stream.d.ts
vendored
@ -1,7 +1,9 @@
|
|||||||
type StreamPlayerOptions = Partial<{
|
import type { StreamVideoProcessing } from "@/enums/pref-values";
|
||||||
processing: string,
|
|
||||||
|
type StreamPlayerOptions = {
|
||||||
|
processing: StreamVideoProcessing,
|
||||||
sharpness: number,
|
sharpness: number,
|
||||||
saturation: number,
|
saturation: number,
|
||||||
contrast: number,
|
contrast: number,
|
||||||
brightness: number,
|
brightness: number,
|
||||||
}>;
|
};
|
||||||
|
@ -32,6 +32,8 @@ type ScriptEvents = {
|
|||||||
'list.localCoOp.updated': {
|
'list.localCoOp.updated': {
|
||||||
ids: Set<string>;
|
ids: Set<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
'webgpu.ready': {},
|
||||||
};
|
};
|
||||||
|
|
||||||
type StreamEvents = {
|
type StreamEvents = {
|
||||||
|
@ -9,6 +9,7 @@ export let FeatureGates: { [key: string]: boolean } = {
|
|||||||
EnableUpdateRequiredPage: false,
|
EnableUpdateRequiredPage: false,
|
||||||
ShowForcedUpdateScreen: false,
|
ShowForcedUpdateScreen: false,
|
||||||
EnableTakControlResizing: true, // Experimenting
|
EnableTakControlResizing: true, // Experimenting
|
||||||
|
EnableLazyLoadedHome: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enable Native Mouse & Keyboard
|
// Enable Native Mouse & Keyboard
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { BxStates } from "@/types/states";
|
||||||
import { UserAgent } from "./user-agent";
|
import { UserAgent } from "./user-agent";
|
||||||
|
|
||||||
export const SCRIPT_VERSION = Bun.env.SCRIPT_VERSION!;
|
export const SCRIPT_VERSION = Bun.env.SCRIPT_VERSION!;
|
||||||
|
@ -2,12 +2,13 @@ import { BxEvent } from "@utils/bx-event";
|
|||||||
import { STATES } from "@utils/global";
|
import { STATES } from "@utils/global";
|
||||||
import { BxLogger } from "@utils/bx-logger";
|
import { BxLogger } from "@utils/bx-logger";
|
||||||
import { patchSdpBitrate, setCodecPreferences } from "./sdp";
|
import { patchSdpBitrate, setCodecPreferences } from "./sdp";
|
||||||
import { StreamPlayer } from "@/modules/stream-player";
|
import { StreamPlayerManager } from "@/modules/stream-player-manager";
|
||||||
import { GlobalPref, StreamPref } from "@/enums/pref-keys";
|
import { GlobalPref, StreamPref } from "@/enums/pref-keys";
|
||||||
import { CodecProfile } from "@/enums/pref-values";
|
import { CodecProfile } from "@/enums/pref-values";
|
||||||
import type { SettingDefinition } from "@/types/setting-definition";
|
import type { SettingDefinition } from "@/types/setting-definition";
|
||||||
import { BxEventBus } from "./bx-event-bus";
|
import { BxEventBus } from "./bx-event-bus";
|
||||||
import { getGlobalPref, getGlobalPrefDefinition, getStreamPref } from "@/utils/pref-utils";
|
import { getGlobalPref, getGlobalPrefDefinition, getStreamPref } from "@/utils/pref-utils";
|
||||||
|
import type { StreamPlayerOptions } from "@/types/stream";
|
||||||
|
|
||||||
export function patchVideoApi() {
|
export function patchVideoApi() {
|
||||||
const PREF_SKIP_SPLASH_VIDEO = getGlobalPref(GlobalPref.UI_SKIP_SPLASH_VIDEO);
|
const PREF_SKIP_SPLASH_VIDEO = getGlobalPref(GlobalPref.UI_SKIP_SPLASH_VIDEO);
|
||||||
@ -26,7 +27,13 @@ export function patchVideoApi() {
|
|||||||
contrast: getStreamPref(StreamPref.VIDEO_CONTRAST),
|
contrast: getStreamPref(StreamPref.VIDEO_CONTRAST),
|
||||||
brightness: getStreamPref(StreamPref.VIDEO_BRIGHTNESS),
|
brightness: getStreamPref(StreamPref.VIDEO_BRIGHTNESS),
|
||||||
} satisfies StreamPlayerOptions;
|
} satisfies StreamPlayerOptions;
|
||||||
STATES.currentStream.streamPlayer = new StreamPlayer(this, getStreamPref(StreamPref.VIDEO_PLAYER_TYPE), playerOptions);
|
|
||||||
|
const streamPlayerManager= StreamPlayerManager.getInstance();
|
||||||
|
streamPlayerManager.setVideoElement(this);
|
||||||
|
streamPlayerManager.updateOptions(playerOptions, false);
|
||||||
|
streamPlayerManager.switchPlayerType(getStreamPref(StreamPref.VIDEO_PLAYER_TYPE));
|
||||||
|
|
||||||
|
STATES.currentStream.streamPlayerManager = streamPlayerManager;
|
||||||
|
|
||||||
BxEventBus.Stream.emit('state.playing', {
|
BxEventBus.Stream.emit('state.playing', {
|
||||||
$video: this,
|
$video: this,
|
||||||
@ -231,6 +238,7 @@ export function patchCanvasContext() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
return nativeGetContext.apply(this, [contextType, contextAttributes]);
|
return nativeGetContext.apply(this, [contextType, contextAttributes]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@ import { AppInterface, STATES } from "./global";
|
|||||||
import { CE } from "./html";
|
import { CE } from "./html";
|
||||||
import { GlobalPref } from "@/enums/pref-keys";
|
import { GlobalPref } from "@/enums/pref-keys";
|
||||||
import { BxLogger } from "./bx-logger";
|
import { BxLogger } from "./bx-logger";
|
||||||
import { StreamPlayerType } from "@/enums/pref-values";
|
|
||||||
import { getGlobalPref } from "@/utils/pref-utils";
|
import { getGlobalPref } from "@/utils/pref-utils";
|
||||||
|
import { StreamPlayerElement } from "@/modules/player/base-stream-player";
|
||||||
|
|
||||||
|
|
||||||
export class ScreenshotManager {
|
export class ScreenshotManager {
|
||||||
@ -42,36 +42,36 @@ export class ScreenshotManager {
|
|||||||
|
|
||||||
takeScreenshot(callback?: any) {
|
takeScreenshot(callback?: any) {
|
||||||
const currentStream = STATES.currentStream;
|
const currentStream = STATES.currentStream;
|
||||||
const streamPlayer = currentStream.streamPlayer;
|
const streamPlayerManager = currentStream.streamPlayerManager;
|
||||||
const $canvas = this.$canvas;
|
const $canvas = this.$canvas;
|
||||||
if (!streamPlayer || !$canvas) {
|
if (!streamPlayerManager || !$canvas) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let $player;
|
let $player;
|
||||||
if (getGlobalPref(GlobalPref.SCREENSHOT_APPLY_FILTERS)) {
|
if (getGlobalPref(GlobalPref.SCREENSHOT_APPLY_FILTERS)) {
|
||||||
$player = streamPlayer.getPlayerElement();
|
$player = streamPlayerManager.getPlayerElement();
|
||||||
} else {
|
} else {
|
||||||
$player = streamPlayer.getPlayerElement(StreamPlayerType.VIDEO);
|
$player = streamPlayerManager.getPlayerElement(StreamPlayerElement.VIDEO);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$player || !$player.isConnected) {
|
if (!$player || !$player.isConnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canvasContext = this.canvasContext;
|
||||||
|
if ($player instanceof HTMLCanvasElement) {
|
||||||
|
streamPlayerManager.getCanvasPlayer()?.updateFrame();
|
||||||
|
}
|
||||||
|
canvasContext.drawImage($player, 0, 0);
|
||||||
|
|
||||||
|
// Play animation
|
||||||
const $gameStream = $player.closest('#game-stream');
|
const $gameStream = $player.closest('#game-stream');
|
||||||
if ($gameStream) {
|
if ($gameStream) {
|
||||||
$gameStream.addEventListener('animationend', this.onAnimationEnd, { once: true });
|
$gameStream.addEventListener('animationend', this.onAnimationEnd, { once: true });
|
||||||
$gameStream.classList.add('bx-taking-screenshot');
|
$gameStream.classList.add('bx-taking-screenshot');
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvasContext = this.canvasContext;
|
|
||||||
|
|
||||||
if ($player instanceof HTMLCanvasElement) {
|
|
||||||
streamPlayer.getWebGL2Player().forceDrawFrame();
|
|
||||||
}
|
|
||||||
canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height);
|
|
||||||
|
|
||||||
// Get data URL and pass to parent app
|
// Get data URL and pass to parent app
|
||||||
if (AppInterface) {
|
if (AppInterface) {
|
||||||
const data = $canvas.toDataURL('image/png').split(';base64,')[1];
|
const data = $canvas.toDataURL('image/png').split(';base64,')[1];
|
||||||
|
@ -12,6 +12,8 @@ import { GameSettingsStorage } from "./game-settings-storage";
|
|||||||
import { BxLogger } from "../bx-logger";
|
import { BxLogger } from "../bx-logger";
|
||||||
import { ControllerCustomizationDefaultPresetId } from "../local-db/controller-customizations-table";
|
import { ControllerCustomizationDefaultPresetId } from "../local-db/controller-customizations-table";
|
||||||
import { ControllerShortcutDefaultId } from "../local-db/controller-shortcuts-table";
|
import { ControllerShortcutDefaultId } from "../local-db/controller-shortcuts-table";
|
||||||
|
import { BxEventBus } from "../bx-event-bus";
|
||||||
|
import { WebGPUPlayer } from "@/modules/player/webgpu/webgpu-player";
|
||||||
|
|
||||||
|
|
||||||
export class StreamSettingsStorage extends BaseSettingsStorage<StreamPref> {
|
export class StreamSettingsStorage extends BaseSettingsStorage<StreamPref> {
|
||||||
@ -150,11 +152,21 @@ export class StreamSettingsStorage extends BaseSettingsStorage<StreamPref> {
|
|||||||
options: {
|
options: {
|
||||||
[StreamPlayerType.VIDEO]: t('default'),
|
[StreamPlayerType.VIDEO]: t('default'),
|
||||||
[StreamPlayerType.WEBGL2]: t('webgl2'),
|
[StreamPlayerType.WEBGL2]: t('webgl2'),
|
||||||
|
[StreamPlayerType.WEBGPU]: `${t('webgpu')} (${t('experimental')})`,
|
||||||
},
|
},
|
||||||
suggest: {
|
suggest: {
|
||||||
lowest: StreamPlayerType.VIDEO,
|
lowest: StreamPlayerType.VIDEO,
|
||||||
highest: StreamPlayerType.WEBGL2,
|
highest: StreamPlayerType.WEBGL2,
|
||||||
},
|
},
|
||||||
|
ready: (setting: any) => {
|
||||||
|
BxEventBus.Script.on('webgpu.ready', () => {
|
||||||
|
if (!navigator.gpu || !WebGPUPlayer.device) {
|
||||||
|
// Remove WebGPU option on unsupported browsers
|
||||||
|
delete setting.options[StreamPlayerType.WEBGPU];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
[StreamPref.VIDEO_PROCESSING]: {
|
[StreamPref.VIDEO_PROCESSING]: {
|
||||||
label: t('clarity-boost'),
|
label: t('clarity-boost'),
|
||||||
|
@ -27,6 +27,7 @@ export const SUPPORTED_LANGUAGES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Texts = {
|
const Texts = {
|
||||||
|
"webgpu": "WebGPU",
|
||||||
"achievements": "Achievements",
|
"achievements": "Achievements",
|
||||||
"activate": "Activate",
|
"activate": "Activate",
|
||||||
"activated": "Activated",
|
"activated": "Activated",
|
||||||
|
@ -16,6 +16,9 @@
|
|||||||
"@modules/*": ["./modules/*"],
|
"@modules/*": ["./modules/*"],
|
||||||
"@utils/*": ["./utils/*"],
|
"@utils/*": ["./utils/*"],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"types": ["@types/bun", "@webgpu/types"],
|
||||||
|
|
||||||
// Enable latest features
|
// Enable latest features
|
||||||
"lib": ["ESNext", "DOM"],
|
"lib": ["ESNext", "DOM"],
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user