better-xcloud/src/modules/player/webgl2-player.ts

267 lines
8.5 KiB
TypeScript

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 { PrefKey } from "@/enums/pref-keys";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
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 = Math.ceil(1000 / this.targetFps);
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);
}
drawFrame() {
// Don't draw when FPS is 0
if (this.targetFps === 0) {
return;
}
// Limit FPS
if (this.targetFps < 60) {
const currentTime = performance.now();
const timeSinceLastFrame = currentTime - this.lastFrameTime;
if (timeSinceLastFrame < this.frameInterval) {
return;
}
this.lastFrameTime = currentTime;
}
const gl = this.gl!;
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
private setupRendering() {
let animate: any;
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
const $video = this.$video;
animate = () => {
if (!this.stopped) {
this.drawFrame();
this.animFrameId = $video.requestVideoFrameCallback(animate);
}
}
this.animFrameId = $video.requestVideoFrameCallback(animate);
} else {
animate = () => {
if (!this.stopped) {
this.drawFrame();
this.animFrameId = requestAnimationFrame(animate);
}
}
this.animFrameId = requestAnimationFrame(animate);
}
}
private setupShaders() {
BxLogger.info(this.LOG_TAG, 'Setting up', getPref(PrefKey.VIDEO_POWER_PREFERENCE));
const gl = this.$canvas.getContext('webgl2', {
isBx: true,
antialias: true,
alpha: false,
powerPreference: getPref(PrefKey.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;
}
}