mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-06 07:37:19 +02:00
263 lines
8.1 KiB
TypeScript
Executable File
263 lines
8.1 KiB
TypeScript
Executable File
import { CE } from "@utils/html"
|
|
import { t } from "@utils/translation"
|
|
import { STATES } from "@utils/global"
|
|
import { PrefKey } from "@/enums/pref-keys"
|
|
import { getPref } from "@/utils/settings-storages/global-settings-storage"
|
|
import { StreamStatsCollector, type StreamStatGrade } from "@/utils/stream-stats-collector"
|
|
import { BxLogger } from "@/utils/bx-logger"
|
|
import { StreamStat } from "@/enums/pref-values"
|
|
import { BxEventBus } from "@/utils/bx-event-bus"
|
|
|
|
|
|
export class StreamStats {
|
|
private static instance: StreamStats;
|
|
public static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats());
|
|
private readonly LOG_TAG = 'StreamStats';
|
|
|
|
private intervalId?: number | null;
|
|
private readonly REFRESH_INTERVAL = 1 * 1000;
|
|
|
|
private stats = {
|
|
[StreamStat.CLOCK]: {
|
|
name: t('clock'),
|
|
$element: CE('span'),
|
|
},
|
|
[StreamStat.PLAYTIME]: {
|
|
name: t('playtime'),
|
|
$element: CE('span'),
|
|
},
|
|
[StreamStat.BATTERY]: {
|
|
name: t('battery'),
|
|
$element: CE('span'),
|
|
},
|
|
[StreamStat.PING]: {
|
|
name: t('stat-ping'),
|
|
$element: CE('span'),
|
|
},
|
|
[StreamStat.JITTER]: {
|
|
name: t('jitter'),
|
|
$element: CE('span'),
|
|
},
|
|
[StreamStat.FPS]: {
|
|
name: t('stat-fps'),
|
|
$element: CE('span'),
|
|
},
|
|
[StreamStat.BITRATE]: {
|
|
name: t('stat-bitrate'),
|
|
$element: CE('span'),
|
|
},
|
|
[StreamStat.DECODE_TIME]: {
|
|
name: t('stat-decode-time'),
|
|
$element: CE('span'),
|
|
},
|
|
[StreamStat.PACKETS_LOST]: {
|
|
name: t('stat-packets-lost'),
|
|
$element: CE('span'),
|
|
},
|
|
[StreamStat.FRAMES_LOST]: {
|
|
name: t('stat-frames-lost'),
|
|
$element: CE('span'),
|
|
},
|
|
[StreamStat.DOWNLOAD]: {
|
|
name: t('downloaded'),
|
|
$element: CE('span'),
|
|
},
|
|
[StreamStat.UPLOAD]: {
|
|
name: t('uploaded'),
|
|
$element: CE('span'),
|
|
},
|
|
};
|
|
|
|
private $container!: HTMLElement;
|
|
|
|
quickGlanceObserver?: MutationObserver | null;
|
|
|
|
private constructor() {
|
|
BxLogger.info(this.LOG_TAG, 'constructor()');
|
|
this.render();
|
|
}
|
|
|
|
async start(glancing=false) {
|
|
if (!this.isHidden() || (glancing && this.isGlancing())) {
|
|
return;
|
|
}
|
|
|
|
this.intervalId && clearInterval(this.intervalId);
|
|
await this.update(true);
|
|
|
|
this.$container.classList.remove('bx-gone');
|
|
this.$container.dataset.display = glancing ? 'glancing' : 'fixed';
|
|
|
|
this.intervalId = window.setInterval(this.update, this.REFRESH_INTERVAL);
|
|
}
|
|
|
|
async stop(glancing=false) {
|
|
if (glancing && !this.isGlancing()) {
|
|
return;
|
|
}
|
|
|
|
this.intervalId && clearInterval(this.intervalId);
|
|
this.intervalId = null;
|
|
|
|
this.$container.removeAttribute('data-display');
|
|
this.$container.classList.add('bx-gone');
|
|
}
|
|
|
|
async toggle() {
|
|
if (this.isGlancing()) {
|
|
this.$container && (this.$container.dataset.display = 'fixed');
|
|
} else {
|
|
this.isHidden() ? await this.start() : await this.stop();
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
this.stop();
|
|
this.quickGlanceStop();
|
|
this.hideSettingsUi();
|
|
}
|
|
|
|
isHidden = () => this.$container.classList.contains('bx-gone');
|
|
isGlancing = () => this.$container.dataset.display === 'glancing';
|
|
|
|
quickGlanceSetup() {
|
|
if (!STATES.isPlaying || this.quickGlanceObserver) {
|
|
return;
|
|
}
|
|
|
|
const $uiContainer = document.querySelector('div[data-testid=ui-container]')!;
|
|
if (!$uiContainer) {
|
|
return;
|
|
}
|
|
|
|
this.quickGlanceObserver = new MutationObserver((mutationList, observer) => {
|
|
for (const record of mutationList) {
|
|
const $target = record.target as HTMLElement;
|
|
if (!$target.className || !$target.className.startsWith('GripHandle')) {
|
|
continue;
|
|
}
|
|
|
|
const expanded = (record.target as HTMLElement).ariaExpanded;
|
|
if (expanded === 'true') {
|
|
this.isHidden() && this.start(true);
|
|
} else {
|
|
this.stop(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
this.quickGlanceObserver.observe($uiContainer, {
|
|
attributes: true,
|
|
attributeFilter: ['aria-expanded'],
|
|
subtree: true,
|
|
});
|
|
}
|
|
|
|
quickGlanceStop() {
|
|
this.quickGlanceObserver && this.quickGlanceObserver.disconnect();
|
|
this.quickGlanceObserver = null;
|
|
}
|
|
|
|
private update = async (forceUpdate=false) => {
|
|
if ((!forceUpdate && this.isHidden()) || !STATES.currentStream.peerConnection) {
|
|
this.destroy();
|
|
return;
|
|
}
|
|
|
|
const PREF_STATS_CONDITIONAL_FORMATTING = getPref(PrefKey.STATS_CONDITIONAL_FORMATTING);
|
|
let grade: StreamStatGrade = '';
|
|
|
|
// Collect stats
|
|
const statsCollector = StreamStatsCollector.getInstance();
|
|
await statsCollector.collect();
|
|
|
|
let statKey: keyof typeof this.stats;
|
|
for (statKey in this.stats) {
|
|
grade = '';
|
|
|
|
const stat = this.stats[statKey];
|
|
const value = statsCollector.getStat(statKey);
|
|
const $element = stat.$element;
|
|
$element.textContent = value.toString();
|
|
|
|
// Get stat's grade
|
|
if (PREF_STATS_CONDITIONAL_FORMATTING && 'grades' in value) {
|
|
grade = statsCollector.calculateGrade(value.current, value.grades);
|
|
}
|
|
|
|
if ($element.dataset.grade !== grade) {
|
|
$element.dataset.grade = grade;
|
|
}
|
|
}
|
|
}
|
|
|
|
refreshStyles() {
|
|
const PREF_ITEMS = getPref<StreamStat[]>(PrefKey.STATS_ITEMS);
|
|
const PREF_OPACITY_BG = getPref<number>(PrefKey.STATS_OPACITY_BACKGROUND);
|
|
|
|
const $container = this.$container;
|
|
$container.dataset.stats = '[' + PREF_ITEMS.join('][') + ']';
|
|
$container.dataset.position = getPref(PrefKey.STATS_POSITION);
|
|
|
|
if (PREF_OPACITY_BG === 0) {
|
|
$container.style.removeProperty('background-color');
|
|
$container.dataset.shadow = 'true';
|
|
} else {
|
|
delete $container.dataset.shadow;
|
|
$container.style.backgroundColor = `rgba(0, 0, 0, ${PREF_OPACITY_BG}%)`;
|
|
}
|
|
|
|
$container.style.opacity = getPref(PrefKey.STATS_OPACITY_ALL) + '%';
|
|
$container.style.fontSize = getPref(PrefKey.STATS_TEXT_SIZE);
|
|
}
|
|
|
|
hideSettingsUi() {
|
|
if (this.isGlancing() && !getPref(PrefKey.STATS_QUICK_GLANCE_ENABLED)) {
|
|
this.stop();
|
|
}
|
|
}
|
|
|
|
private async render() {
|
|
this.$container = CE('div', { class: 'bx-stats-bar bx-gone' });
|
|
|
|
let statKey: keyof typeof this.stats;
|
|
for (statKey in this.stats) {
|
|
const stat = this.stats[statKey];
|
|
const $div = CE('div', {
|
|
class: `bx-stat-${statKey}`,
|
|
title: stat.name,
|
|
},
|
|
CE('label', {}, statKey.toUpperCase()),
|
|
stat.$element,
|
|
);
|
|
|
|
this.$container.appendChild($div);
|
|
}
|
|
|
|
this.refreshStyles();
|
|
document.documentElement.appendChild(this.$container);
|
|
}
|
|
|
|
static setupEvents() {
|
|
BxEventBus.Stream.on('state.playing', () => {
|
|
const PREF_STATS_QUICK_GLANCE = getPref(PrefKey.STATS_QUICK_GLANCE_ENABLED);
|
|
const PREF_STATS_SHOW_WHEN_PLAYING = getPref(PrefKey.STATS_SHOW_WHEN_PLAYING);
|
|
|
|
const streamStats = StreamStats.getInstance();
|
|
|
|
// Setup Stat's Quick Glance mode
|
|
if (PREF_STATS_SHOW_WHEN_PLAYING) {
|
|
streamStats.start();
|
|
} else if (PREF_STATS_QUICK_GLANCE) {
|
|
streamStats.quickGlanceSetup();
|
|
// Show stats bar
|
|
!PREF_STATS_SHOW_WHEN_PLAYING && streamStats.start(true);
|
|
}
|
|
});
|
|
}
|
|
|
|
static refreshStyles() {
|
|
StreamStats.getInstance().refreshStyles();
|
|
}
|
|
}
|