mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-09 09:07:20 +02:00
304 lines
9.9 KiB
TypeScript
Executable File
304 lines
9.9 KiB
TypeScript
Executable File
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 { PrefKey } from "@/enums/pref-keys";
|
|
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
|
import { BX_FLAGS } from "@/utils/bx-flags";
|
|
import { StreamPlayerType, StreamVideoProcessing, VideoPosition, VideoRatio } from "@/enums/pref-values";
|
|
|
|
export type StreamPlayerOptions = Partial<{
|
|
processing: string,
|
|
sharpness: number,
|
|
saturation: number,
|
|
contrast: number,
|
|
brightness: number,
|
|
}>;
|
|
|
|
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<HTMLStyleElement>('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',
|
|
})),
|
|
),
|
|
);
|
|
$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 = getPref<VideoRatio>(PrefKey.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 = getPref<VideoPosition>(PrefKey.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() && getPref(PrefKey.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();
|
|
}
|
|
}
|