ultrawidify/src/ext/lib/video-data/PlayerData.ts

938 lines
30 KiB
TypeScript
Raw Normal View History

import Debug from '../../conf/Debug';
import ExtensionMode from '../../../common/enums/ExtensionMode.enum'
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
2020-12-03 01:35:48 +01:00
import PlayerNotificationUi from '../uwui/PlayerNotificationUI';
2020-12-22 03:21:14 +01:00
import BrowserDetect from '../../conf/BrowserDetect';
2021-02-08 22:45:51 +01:00
import * as _ from 'lodash';
import { sleep } from '../../../common/js/utils';
2021-02-08 22:45:51 +01:00
import VideoData from './VideoData';
import Settings from '../Settings';
import Logger from '../Logger';
2021-10-26 22:19:41 +02:00
import EventBus from '../EventBus';
import UI from '../uwui/UI';
import { SiteSettings } from '../settings/SiteSettings';
import PageInfo from './PageInfo';
2024-05-07 19:05:10 +02:00
import { RunLevel } from '../../enum/run-level.enum';
import { ExtensionEnvironment } from '../../../common/interfaces/SettingsInterface';
2020-12-03 01:05:39 +01:00
if (process.env.CHANNEL !== 'stable'){
console.info("Loading: PlayerData.js");
}
interface PlayerDimensions {
width?: number;
height?: number;
fullscreen?: boolean;
}
2025-01-06 03:05:18 +01:00
interface ElementData {
element: HTMLElement,
type: string,
tagName?: string,
classList?: any,
id?: string
}
type ElementStack = ElementData[];
/**
* accepts <video> tag (element) and list of names that can appear in id or class
* returns player dimensions (width, height)
* Theater mode is mildly broken on youtube. <video> tag remains bigger than the player after leaving the fullscreen mode, and
* there's nothing we can do about that. This function aims to solve the problem by finding the player element that's wrapped around
* the <video> tag.
* In general, an outer tag should be bigger than the inner tag. Therefore the smallest element between <video> tag and the document
* root should be the player.
* If list of names is provided, the function returns dimensions of the first element that contains any name from the list in either
* id or class.
2024-05-07 19:05:10 +02:00
*
*
* RUN LEVELS
* Run are there to ensure only the necessary functions run.
*
* * Off:
* * Extension is effectively disabled. However, even in this quasi-disabled state,
* certain functions of the class should still be active.
* 1. Player size monitoring
* (Run level could be set to 'off' due to player being too small)
* 2. Event bus
* (Actions from popup may cause RunLevel to increase)
*
* * UiOnly:
* * Extension should show in-player UI, but it should not inject any
* unnecessary CSS.
*/
class PlayerData {
private playerCssClass = 'uw-ultrawidify-player-css';
2021-02-08 22:45:51 +01:00
//#region helper objects
logger: Logger;
videoData: VideoData;
pageInfo: PageInfo;
siteSettings: SiteSettings;
2021-02-08 22:45:51 +01:00
notificationService: PlayerNotificationUi;
2021-10-26 22:19:41 +02:00
eventBus: EventBus;
2021-02-08 22:45:51 +01:00
//#endregion
//#region HTML objects
2025-01-06 03:05:18 +01:00
videoElement: HTMLVideoElement;
element: HTMLElement;
2021-02-08 22:45:51 +01:00
//#endregion
//#region flags
2024-05-07 21:01:45 +02:00
runLevel: RunLevel = RunLevel.Off;
enabled: boolean;
2021-02-08 22:45:51 +01:00
invalid: boolean = false;
private periodicallyRefreshPlayerElement: boolean = false;
halted: boolean = true;
isFullscreen: boolean = !!document.fullscreenElement;
isTheaterMode: boolean = false; // note: fullscreen mode will count as theaterMode if player was in theater mode before fs switch. This is desired, so far.
isTooSmall: boolean = true;
2021-02-08 22:45:51 +01:00
2025-03-30 23:49:13 +02:00
//#endregion
2021-02-08 22:45:51 +01:00
//#region misc stuff
extensionMode: any;
dimensions: PlayerDimensions;
private observer: ResizeObserver;
2021-08-26 01:07:39 +02:00
private trackChangesTimeout: any;
2025-01-01 22:15:22 +01:00
private markedElement: HTMLElement;
2023-09-10 19:51:36 +02:00
private ui: UI;
private _isTrackDimensionChangesActive: boolean = false;
2025-01-06 03:05:18 +01:00
elementStack: ElementStack = [] as ElementStack;
2021-02-08 22:45:51 +01:00
//#endregion
//#region event bus configuration
private eventBusCommands = {
'get-player-tree': [{
function: () => this.handlePlayerTreeRequest()
}],
'get-player-dimensions': [{
function: () => {
2025-01-06 03:05:18 +01:00
this.eventBus.send('uw-config-broadcast', {
type: 'player-dimensions',
data: this.dimensions
});
}
}],
'set-mark-element': [{ // NOTE: is this still used?
function: (data) => this.markElement(data)
}],
'update-player': [{
2025-03-30 23:49:13 +02:00
function: () => this.updatePlayer()
}],
2024-05-07 19:05:10 +02:00
'set-run-level': [{
function: (runLevel) => this.setRunLevel(runLevel)
2025-01-06 03:05:18 +01:00
}],
'change-player-element': [{
function: () => {
this.nextPlayerElement();
2025-01-10 19:06:20 +01:00
}
2024-05-07 19:05:10 +02:00
}]
}
//#endregion
2023-09-10 19:51:36 +02:00
private dimensionChangeListener = {
that: this,
handleEvent: function(event: Event) {
this.that.trackEnvironmentChanges(event);
2023-09-10 19:51:36 +02:00
this.that.trackDimensionChanges()
}
}
/**
* Gets player aspect ratio. If in full screen, it returns screen aspect ratio unless settings say otherwise.
*/
get aspectRatio() {
2021-04-12 19:01:28 +02:00
try {
if (this.isFullscreen) {
2021-04-12 19:01:28 +02:00
return window.innerWidth / window.innerHeight;
}
if (!this.dimensions) {
this.trackDimensionChanges();
this.trackEnvironmentChanges();
}
2021-04-12 19:01:28 +02:00
return this.dimensions.width / this.dimensions.height;
} catch (e) {
console.error('cannot determine aspect ratio!', e);
return 1;
}
}
/**
* Gets current environment (needed when determining whether extension runs in fulls screen, theater, or normal)
*/
private lastEnvironment: ExtensionEnvironment;
get environment(): ExtensionEnvironment {
if (this.isFullscreen) {
return ExtensionEnvironment.Fullscreen;
}
if (this.isTheaterMode) {
return ExtensionEnvironment.Theater;
}
return ExtensionEnvironment.Normal;
}
2025-03-30 23:49:13 +02:00
/**
*
* END OF PROPERTIES
*
*
*/
//#region lifecycle
constructor(videoData) {
2019-09-22 02:07:04 +02:00
try {
// set all our helper objects
2019-09-22 02:07:04 +02:00
this.logger = videoData.logger;
this.videoData = videoData;
this.videoElement = videoData.video;
this.pageInfo = videoData.pageInfo;
this.siteSettings = videoData.siteSettings;
2021-10-26 22:19:41 +02:00
this.eventBus = videoData.eventBus;
// do the rest
2019-09-22 02:07:04 +02:00
this.invalid = false;
2025-03-30 23:49:13 +02:00
this.updatePlayer();
this.isTooSmall = (this.element.clientWidth < 1208 || this.element.clientHeight < 720);
this.initEventBus();
2019-09-22 02:07:04 +02:00
this.dimensions = undefined;
this.periodicallyRefreshPlayerElement = false;
try {
this.periodicallyRefreshPlayerElement = this.siteSettings.data.currentDOMConfig.periodicallyRefreshPlayerElement;
} catch (e) {
// no biggie — that means we don't have any special settings for this site.
}
2019-09-22 02:07:04 +02:00
// this happens when we don't find a matching player element
if (!this.element) {
this.invalid = true;
return;
}
this.startChangeDetection();
2023-09-10 19:51:36 +02:00
document.addEventListener('fullscreenchange', this.dimensionChangeListener);
2023-07-15 04:03:32 +02:00
// we want to reload on storage changes
2023-07-15 04:17:38 +02:00
this.siteSettings.subscribeToStorageChange('PlayerData', (siteConfUpdate) => this.reloadPlayerDataConfig(siteConfUpdate));
2019-09-22 02:07:04 +02:00
} catch (e) {
console.error('[Ultrawidify::PlayerData::ctor] There was an error setting up player data. You should be never seeing this message. Error:', e);
this.invalid = true;
}
}
2021-08-26 01:07:39 +02:00
2023-07-15 04:17:38 +02:00
private reloadPlayerDataConfig(siteConfUpdate) {
// this.siteSettings = siteConfUpdate;
2025-03-30 23:49:13 +02:00
this.updatePlayer();
2023-07-15 04:03:32 +02:00
this.periodicallyRefreshPlayerElement = false;
try {
this.periodicallyRefreshPlayerElement = this.siteSettings.data.currentDOMConfig.periodicallyRefreshPlayerElement;
} catch (e) {
// no biggie — that means we don't have any special settings for this site.
}
// because this is often caused by the UI
this.handlePlayerTreeRequest();
}
2024-05-07 19:05:10 +02:00
/**
* Initializes event bus
*/
private initEventBus() {
for (const action in this.eventBusCommands) {
for (const command of this.eventBusCommands[action]) {
this.eventBus.subscribe(action, command);
}
}
2019-09-22 02:07:04 +02:00
}
2024-05-07 19:05:10 +02:00
/**
* Completely stops everything the extension is doing
*/
destroy() {
2023-09-10 19:51:36 +02:00
document.removeEventListener('fullscreenchange', this.dimensionChangeListener);
this.stopChangeDetection();
this.ui?.destroy();
this.notificationService?.destroy();
}
//#endregion
deferredUiInitialization(playerDimensions) {
if (this.ui || this.siteSettings.data.enableUI.fullscreen === ExtensionMode.Disabled) {
return;
}
2025-03-30 23:49:13 +02:00
if (
this.isFullscreen
|| (
this.siteSettings.data.enableUI.theater !== ExtensionMode.Disabled
&& playerDimensions.width > 1208
&& playerDimensions.height > 720
)
) {
2025-03-30 23:49:13 +02:00
this.ui = new UI(
'ultrawidifyUi',
{
parentElement: this.element,
eventBus: this.eventBus,
playerData: this,
2025-04-22 02:37:36 +02:00
uiSettings: this.videoData.settings.active.ui,
siteSettings: this.siteSettings,
}
);
if (this.runLevel < RunLevel.UIOnly) {
this.ui?.disable();
}
if (this.runLevel >= RunLevel.UIOnly) {
this.ui?.enable();
this.startChangeDetection();
}
if (this.runLevel >= RunLevel.CustomCSSActive) {
this.element.classList.add(this.playerCssClass);
}
}
}
/**
2024-05-07 21:01:45 +02:00
* Sets extension runLevel and sets or unsets appropriate css classes as necessary
2024-05-07 19:05:10 +02:00
* @param runLevel
* @returns
*/
setRunLevel(runLevel: RunLevel) {
if (this.runLevel === runLevel) {
return;
}
// increasing runLevel works differently than decreasing
if (this.runLevel > runLevel) {
if (runLevel < RunLevel.CustomCSSActive) {
this.element.classList.remove(this.playerCssClass);
}
if (runLevel < RunLevel.UIOnly) {
this.ui?.disable();
2024-05-07 19:05:10 +02:00
}
} else {
if (runLevel >= RunLevel.UIOnly) {
this.ui?.enable();
2024-05-07 19:05:10 +02:00
this.startChangeDetection();
}
if (runLevel >= RunLevel.CustomCSSActive) {
this.element.classList.add(this.playerCssClass);
}
}
this.runLevel = runLevel;
}
/**
* Detects whether player element is in theater mode or not.
* If theater mode changed, emits event.
* @returns whether player is in theater mode
*/
private detectTheaterMode() {
const oldTheaterMode = this.isTheaterMode;
const newTheaterMode = this.equalish(window.innerWidth, this.element.offsetWidth, 32);
this.isTheaterMode = newTheaterMode;
// theater mode changed
if (oldTheaterMode !== newTheaterMode) {
if (newTheaterMode) {
this.eventBus.send('player-theater-enter', {});
} else {
this.eventBus.send('player-theater-exit', {});
}
}
return newTheaterMode;
}
trackEnvironmentChanges() {
if (this.environment !== this.lastEnvironment) {
this.lastEnvironment = this.environment;
this.eventBus.send('uw-environment-change', {newEnvironment: this.environment});
}
}
/**
*
*/
trackDimensionChanges() {
if (this._isTrackDimensionChangesActive) {
// this shouldn't really get called, _ever_ ... but sometimes it happens
console.warn('[PlayerData::trackDimensionChanges] trackDimensionChanges is already active!');
return;
}
this._isTrackDimensionChangesActive = true;
try {
// get player dimensions _once_
let currentPlayerDimensions;
this.isFullscreen = !!document.fullscreenElement;
if (this.isFullscreen) {
currentPlayerDimensions = {
width: window.innerWidth,
height: window.innerHeight,
};
} else {
currentPlayerDimensions = {
width: this.element.offsetWidth,
height: this.element.offsetHeight
};
this.detectTheaterMode();
}
// defer creating UI
this.deferredUiInitialization(currentPlayerDimensions);
// if dimensions of the player box are the same as the last known
// dimensions, we don't have to do anything ... in theory. In practice,
// sometimes restore-ar doesn't appear to register the first time, and
// this function doesn't really run often enough to warrant finding a
// real, optimized fix.
if (
this.dimensions?.width == currentPlayerDimensions.width
&& this.dimensions?.height == currentPlayerDimensions.height
) {
this.eventBus.send('restore-ar', null);
this.eventBus.send('delayed-restore-ar', {delay: 500});
this.dimensions = currentPlayerDimensions;
this._isTrackDimensionChangesActive = false;
return;
}
// in every other case, we need to check if the player is still
// big enough to warrant our extension running.
this.handleSizeConstraints(currentPlayerDimensions);
// this.handleDimensionChanges(currentPlayerDimensions, this.dimensions);
// Save current dimensions to avoid triggering this function pointlessly
this.dimensions = currentPlayerDimensions;
} catch (e) {
}
this._isTrackDimensionChangesActive = false;
}
/**
* Checks if extension is allowed to run in current environment.
* @param currentPlayerDimensions
*/
private handleSizeConstraints(currentPlayerDimensions: PlayerDimensions) {
// Check if extension is allowed to run in current combination of theater + full screen
2023-07-11 00:48:34 +02:00
const canEnable = this.siteSettings.isEnabledForEnvironment(this.isFullscreen, this.isTheaterMode) === ExtensionMode.Enabled;
2024-05-07 21:01:45 +02:00
if (this.runLevel === RunLevel.Off && canEnable) {
this.eventBus.send('restore-ar', null);
// must be called after
this.handleDimensionChanges(currentPlayerDimensions, this.dimensions);
} else if (!canEnable && this.runLevel !== RunLevel.Off) {
// must be called before
this.handleDimensionChanges(currentPlayerDimensions, this.dimensions);
this.setRunLevel(RunLevel.Off);
}
}
private handleDimensionChanges(newDimensions: PlayerDimensions, oldDimensions: PlayerDimensions) {
2024-06-12 20:29:00 +02:00
if (this.runLevel === RunLevel.Off ) {
this.logger.log('info', 'debug', "[PlayerDetect] player size changed, but PlayerDetect is in disabled state. The player element is probably too small.");
return;
}
// this 'if' is just here for debugging — real code starts later. It's safe to collapse and
// ignore the contents of this if (unless we need to change how logging works)
this.logger.log('info', 'debug', "[PlayerDetect] player size potentially changed.\n\nold dimensions:", oldDimensions, '\nnew dimensions:', newDimensions);
// if size doesn't match, trigger onPlayerDimensionChange
if (
newDimensions?.width != oldDimensions?.width
|| newDimensions?.height != oldDimensions?.height
|| newDimensions?.fullscreen != oldDimensions?.fullscreen
){
// If player size changes, we restore aspect ratio
2024-05-07 19:05:10 +02:00
this.eventBus.send('restore-ar', null);
2024-06-12 20:29:00 +02:00
this.eventBus.send('delayed-restore-ar', {delay: 500});
2024-05-07 19:05:10 +02:00
// this.videoData.resizer?.restore();
this.eventBus.send('uw-config-broadcast', {
type: 'player-dimensions',
data: newDimensions
});
this.isTooSmall = !newDimensions.fullscreen && (newDimensions.width < 1208 || newDimensions.height < 720);
}
}
onPlayerDimensionsChanged(mutationList?, observer?) {
this.trackDimensionChanges();
this.trackEnvironmentChanges();
}
//#region player element change detection
2024-05-07 19:05:10 +02:00
/**
* Starts change detection.
* @returns
*/
startChangeDetection(){
if (this.invalid) {
return;
}
2019-09-21 23:50:06 +02:00
2019-09-22 02:07:04 +02:00
try {
2023-07-11 00:48:34 +02:00
this.observer = new ResizeObserver(
_.debounce( // don't do this too much:
(m,o) => {
this.onPlayerDimensionsChanged(m,o)
},
250, // do it once per this many ms
{
leading: true, // do it when we call this fallback first
trailing: true // do it after the timeout if we call this callback few more times
}
)
);
const observerConf = {
attributes: true,
// attributeFilter: ['style', 'class'],
attributeOldValue: true,
};
2021-10-25 23:11:34 +02:00
this.observer.observe(this.element);
} catch (e) {
console.error("failed to set observer",e )
}
2019-09-21 23:50:06 +02:00
// legacy mode still exists, but acts as a fallback for observers and is triggered less
// frequently in order to avoid too many pointless checks
this.legacyChangeDetection();
}
async legacyChangeDetection() {
while (!this.halted) {
await sleep(1000);
2019-09-22 02:07:04 +02:00
try {
this.forceRefreshPlayerElement();
2019-09-22 02:07:04 +02:00
} catch (e) {
2021-04-04 15:48:01 +02:00
console.error('[PlayerData::legacycd] this message is pretty high on the list of messages you shouldn\'t see', e);
2019-09-21 23:50:06 +02:00
}
}
}
2019-09-21 23:50:06 +02:00
doPeriodicPlayerElementChangeCheck() {
if (this.periodicallyRefreshPlayerElement) {
this.forceRefreshPlayerElement();
}
}
stopChangeDetection(){
this.observer.disconnect();
}
//#region helper functions
collectionHas(collection, element) {
2019-06-14 21:53:48 +02:00
for (let i = 0, len = collection.length; i < len; i++) {
if (collection[i] == element) {
return true;
}
}
return false;
}
equalish(a,b, tolerance) {
return a > b - tolerance && a < b + tolerance;
}
//#endregion
2019-11-04 23:53:28 +01:00
2025-01-06 03:05:18 +01:00
private getElementStack(): ElementStack {
const elementStack: any[] = [{
element: this.videoElement,
type: 'video',
tagName: 'video',
classList: this.videoElement.classList,
id: this.videoElement.id,
}];
2025-01-06 03:05:18 +01:00
let element = this.videoElement.parentNode as HTMLElement;
// first pass to generate the element stack and translate it into array
while (element) {
elementStack.push({
element,
tagName: element.tagName,
classList: element.classList,
id: element.id,
width: element.offsetWidth, // say no to reflows, don't do element.offset[width/height]
height: element.offsetHeight, // repeatedly ... let's just do it once at this spot
heuristics: {},
});
element = element.parentElement;
}
2025-01-06 03:05:18 +01:00
this.elementStack = elementStack;
2025-01-06 03:05:18 +01:00
return this.elementStack;
}
2025-03-30 23:49:13 +02:00
updatePlayer(options?: {verbose?: boolean, newElement?: HTMLElement}) {
const newPlayer = options?.newElement ?? this.getPlayer(options);
if (newPlayer === this.element) {
return;
}
// clean up and re-initialize UI
this.ui?.destroy();
delete this.ui;
this.element = newPlayer;
this.ui = new UI(
'ultrawidifyUi',
{
parentElement: this.element,
eventBus: this.eventBus,
playerData: this,
2025-04-22 02:37:36 +02:00
uiSettings: this.videoData.settings.active.ui,
siteSettings: this.siteSettings,
2025-03-30 23:49:13 +02:00
}
);
this.trackDimensionChanges();
this.trackEnvironmentChanges();
}
2025-01-06 03:05:18 +01:00
/**
* Finds and returns HTML element of the player
*/
2025-03-30 23:49:13 +02:00
private getPlayer(options?: {verbose?: boolean}): HTMLElement {
2025-01-06 03:05:18 +01:00
const videoWidth = this.videoElement.offsetWidth;
const videoHeight = this.videoElement.offsetHeight;
let playerCandidate;
const elementStack = this.getElementStack();
const playerQs = this.siteSettings.getCustomDOMQuerySelector('player');
const playerIndex = this.siteSettings.getPlayerIndex();
2023-07-15 04:03:32 +02:00
// on verbose, get both qs and index player
2023-07-15 04:17:38 +02:00
if (options?.verbose) {
2023-07-15 04:03:32 +02:00
if (playerIndex) {
playerCandidate = elementStack[playerIndex];
playerCandidate.heuristics['manualElementByParentIndex'] = true;
}
if (playerQs) {
playerCandidate = this.getPlayerQs(playerQs, elementStack, videoWidth, videoHeight);
}
}
2023-07-15 04:17:38 +02:00
2023-07-15 04:03:32 +02:00
// if mode is given, we follow the preference
2023-07-15 04:17:38 +02:00
if (this.siteSettings.data.currentDOMConfig?.elements?.player?.manual && this.siteSettings.data.currentDOMConfig?.elements?.player?.mode) {
2023-07-15 04:03:32 +02:00
if (this.siteSettings.data.currentDOMConfig?.elements?.player?.mode === 'qs') {
playerCandidate = this.getPlayerQs(playerQs, elementStack, videoWidth, videoHeight);
} else {
playerCandidate = elementStack[playerIndex];
playerCandidate.heuristics['manualElementByParentIndex'] = true;
}
} else {
// try to figure it out based on what we have, with playerQs taking priority
if (playerQs) {
playerCandidate = this.getPlayerQs(playerQs, elementStack, videoWidth, videoHeight);
} else if (playerIndex) { // btw 0 is not a valid index for player
playerCandidate = elementStack[playerIndex];
playerCandidate.heuristics['manualElementByParentIndex'] = true;
}
}
if (playerCandidate) {
if (options?.verbose) {
this.getPlayerAuto(elementStack, videoWidth, videoHeight);
playerCandidate.heuristics['activePlayer'] = true;
}
return playerCandidate.element;
} else {
const playerCandidate = this.getPlayerAuto(elementStack, videoWidth, videoHeight);
playerCandidate.heuristics['activePlayer'] = true;
return playerCandidate.element;
}
}
2025-01-06 03:05:18 +01:00
private nextPlayerElement() {
const currentIndex = this.siteSettings.data.playerAutoConfig?.currentIndex ?? this.siteSettings.data.playerAutoConfig?.initialIndex;
if (!currentIndex) {
// console.warn('Player Element not valid.');
return;
}
const nextIndex = currentIndex + 1;
const elementStack = this.getElementStack();
if (
this.equalish(elementStack[currentIndex].element.offsetWidth, elementStack[nextIndex].element.offsetWidth, 2)
&& this.equalish(elementStack[currentIndex].element.offsetHeight, elementStack[nextIndex].element.offsetHeight, 2)
) {
this.siteSettings.set('playerAutoConfig.initialIndex', this.siteSettings.data.playerAutoConfig.initialIndex + 1, {noSave: true});
this.siteSettings.set('playerAutoConfig.modified', true);
console.log('updated site settings:', this.siteSettings.data.playerAutoConfig);
this.videoData.settings.saveWithoutReload();
2025-03-30 23:49:13 +02:00
this.updatePlayer({newElement: elementStack[nextIndex].element});
2025-01-06 03:05:18 +01:00
}
}
/**
* Gets player based on some assumptions, without us defining shit.
* @param elementStack
* @param videoWidth
* @param videoHeight
* @returns
*/
private getPlayerAuto(elementStack: any[], videoWidth, videoHeight) {
let penaltyMultiplier = 1;
const sizePenaltyMultiplier = 0.1;
const perLevelScorePenalty = 10;
let sameSizeBonus = 0;
2025-01-06 03:05:18 +01:00
for (const [index, element] of elementStack.entries()) {
element.index = index;
// ignore weird elements, those would break our stuff
if (element.width == 0 || element.height == 0) {
element.heuristics['invalidSize'] = true;
continue;
}
// element is player, if at least one of the sides is as long as the video
// note that we can't make any additional assumptions with regards to player
// size, since there are both cases where the other side is bigger _and_ cases
// where other side is smaller than the video.
//
// Don't bother thinking about this too much, as any "thinking" was quickly
// corrected by bugs caused by various edge cases.
if (
this.equalish(element.height, videoHeight, 5)
|| this.equalish(element.width, videoWidth, 5)
) {
let score = 1000;
// -------------------
// PENALTIES
// -------------------
//
// Our ideal player will be as close to the video element, and it will als
// be as close to the size of the video.
const diffX = (element.width - videoWidth);
const diffY = (element.height - videoHeight);
// we have a minimal amount of grace before we start dinking scores for
// mismatched dimensions. The size of the dimension mismatch dink is
// proportional to area rather than circumference, meaning we multiply
// x and y dinks instead of adding them up.
let playerSizePenalty = 1;
if (diffY > 5) {
playerSizePenalty *= diffY * sizePenaltyMultiplier;
}
if (diffX > 5) {
playerSizePenalty *= diffX * sizePenaltyMultiplier;
}
score -= playerSizePenalty;
// we prefer elements closer to the video, so the score of each potential
// candidate gets dinked a bit
// score -= perLevelScorePenalty * penaltyMultiplier;
if (element.width === elementStack[index - 1].width && element.height === elementStack[index - 1].height) {
score += ++sameSizeBonus;
} else {
sameSizeBonus = 0;
}
element.autoScore = score;
element.heuristics['autoScoreDetails'] = {
playerSizePenalty,
diffX,
diffY,
penaltyMultiplier
}
// ensure next valid candidate is gonna have a harder job winning out
penaltyMultiplier++;
}
}
let bestCandidate: any = {autoScore: -99999999, initialValue: true};
for (const element of elementStack) {
if (element.autoScore > bestCandidate.autoScore) {
bestCandidate = element;
}
}
if (bestCandidate.initialValue) {
bestCandidate = null;
} else {
bestCandidate.heuristics['autoMatch'] = true;
2025-01-06 03:05:18 +01:00
if (this.siteSettings.data.playerAutoConfig?.initialIndex !== bestCandidate.index) {
this.siteSettings.set('playerAutoConfig.initialIndex', bestCandidate.index, {reload: false, scripted: true});
2025-01-06 03:05:18 +01:00
}
}
// BUT WAIT! THERE'S MORE
// Some sites (youtube) can re-parent elements, causing current player element to vanish from DOM
if (bestCandidate) {
const observer = new MutationObserver( (mutations) => {
mutations.forEach((mutation) => {
mutation.removedNodes.forEach((node) => {
if (node === bestCandidate.element) {
observer.disconnect();
this.updatePlayer();
}
})
});
});
observer.observe(bestCandidate.element.parentNode, {childList: true});
}
return bestCandidate;
}
2019-08-31 18:21:49 +02:00
/**
* Gets player element based on a query string.
*
* Since query string does not necessarily uniquely identify an element, this function also
* tries to evaluate which candidate of element that match the query selector is the most
* likely the one element we're looking for.
*
* Function prefers elements that are:
* 1. closer to the video
* 2. about the same size as the video
* 3. they must appear between video and root of the DOM hierarchy
*
* @param queryString query string for player element
* @param elementStack branch of DOM hierarchy that ends with a video
* @param videoWidth width of the video
* @param videoHeight height of the video
* @returns best candidate or null, if nothing in elementStack matches our query selector
*/
private getPlayerQs(queryString: string, elementStack: any[], videoWidth, videoHeight) {
const perLevelScorePenalty = 10;
let penaltyMultiplier = 0;
2021-10-25 23:11:34 +02:00
const allSelectors = document.querySelectorAll(queryString);
for (const element of elementStack) {
if (this.collectionHas(allSelectors, element.element)) {
let score = 100;
// we award points to elements which match video size in one
// dimension and exceed it in the other
if (
(element.width >= videoWidth && this.equalish(element.height, videoHeight, 2))
|| (element.height >= videoHeight && this.equalish(element.width, videoWidth, 2))
) {
score += 75;
}
2021-10-25 23:11:34 +02:00
score -= perLevelScorePenalty * penaltyMultiplier;
element.heuristics['qsScore'] = score;
penaltyMultiplier++;
}
}
let bestCandidate: any = {qsScore: -99999999, initialValue: true};
for (const element of elementStack) {
if (element.qsScore > bestCandidate.qsScore) {
bestCandidate = element;
}
}
if (bestCandidate.initialValue) {
bestCandidate = null;
} else {
bestCandidate.heuristics['qsMatch'] = true;
}
return bestCandidate;
}
/**
* Lists elements between video and DOM root for display in player selector (UI)
*/
private handlePlayerTreeRequest() {
// this populates this.elementStack fully
2025-03-30 23:49:13 +02:00
this.updatePlayer({verbose: true});
2023-07-15 04:03:32 +02:00
console.log('tree:', JSON.parse(JSON.stringify(this.elementStack)));
console.log('————————————————————— handling player tree request!')
this.eventBus.send('uw-config-broadcast', {type: 'player-tree', config: JSON.parse(JSON.stringify(this.elementStack))});
}
private markElement(data: {parentIndex: number, enable: boolean}) {
2025-01-01 22:15:22 +01:00
if (data.enable === false) {
this.markedElement.remove();
return;
}
if (this.markedElement) {
this.markedElement.remove();
}
const elementBB = this.elementStack[data.parentIndex].element.getBoundingClientRect();
// console.log('element bounding box:', elementBB);
const div = document.createElement('div');
div.style.position = 'fixed';
div.style.top = `${elementBB.top}px`;
div.style.left = `${elementBB.left}px`;
div.style.width = `${elementBB.width}px`;
div.style.height = `${elementBB.height}px`;
div.style.zIndex = '100';
div.style.border = '5px dashed #fa6';
div.style.pointerEvents = 'none';
div.style.boxSizing = 'border-box';
div.style.backgroundColor = 'rgba(255, 128, 64, 0.25)';
document.body.insertBefore(div, document.body.firstChild);
this.markedElement = div;
// this.elementStack[data.parentIndex].element.style.outline = data.enable ? '5px dashed #fa6' : null;
// this.elementStack[data.parentIndex].element.style.filter = data.enable ? 'sepia(1) brightness(2) contrast(0.5)' : null;
2019-08-31 18:21:49 +02:00
}
forceRefreshPlayerElement() {
2025-03-30 23:49:13 +02:00
this.updatePlayer();
}
}
2020-12-03 01:05:39 +01:00
if (process.env.CHANNEL !== 'stable'){
console.info("PlayerData loaded");
}
export default PlayerData;