2018-12-31 01:03:07 +01:00
import Debug from '../../conf/Debug' ;
2021-02-08 23:04:54 +01:00
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' ;
2021-02-08 20:43:56 +01:00
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' ;
2022-01-07 00:50:58 +01:00
import UI from '../uwui/UI' ;
2023-01-07 18:57:47 +01:00
import { SiteSettings } from '../settings/SiteSettings' ;
2023-04-12 23:08:19 +02:00
import PageInfo from './PageInfo' ;
2024-05-07 19:05:10 +02:00
import { RunLevel } from '../../enum/run-level.enum' ;
2025-01-29 23:58:16 +01:00
import { ExtensionEnvironment } from '../../../common/interfaces/SettingsInterface' ;
2018-12-31 01:03:07 +01:00
2020-12-03 01:05:39 +01:00
if ( process . env . CHANNEL !== 'stable' ) {
console . info ( "Loading: PlayerData.js" ) ;
}
2018-05-13 13:49:25 +02:00
2021-10-26 00:30:38 +02:00
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 [ ] ;
2021-10-26 00:30:38 +02:00
/ * *
* 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 .
2021-10-26 00:30:38 +02:00
* /
2018-05-13 13:49:25 +02:00
class PlayerData {
2021-10-26 00:30:38 +02:00
private playerCssClass = 'uw-ultrawidify-player-css' ;
2021-02-08 22:45:51 +01:00
//#region helper objects
logger : Logger ;
videoData : VideoData ;
2023-04-12 23:08:19 +02:00
pageInfo : PageInfo ;
2023-01-07 18:57:47 +01:00
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 ;
2021-10-26 00:30:38 +02:00
enabled : boolean ;
2021-02-08 22:45:51 +01:00
invalid : boolean = false ;
private periodicallyRefreshPlayerElement : boolean = false ;
halted : boolean = true ;
2023-01-07 03:06:37 +01:00
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.
2024-12-30 23:02:55 +01:00
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 ;
2021-10-26 00:30:38 +02:00
dimensions : PlayerDimensions ;
2021-03-29 23:30:54 +02:00
private observer : ResizeObserver ;
2021-08-26 01:07:39 +02:00
2024-12-30 23:02:55 +01:00
private trackChangesTimeout : any ;
2025-01-01 22:15:22 +01:00
private markedElement : HTMLElement ;
2024-12-30 23:02:55 +01:00
2023-09-10 19:51:36 +02:00
private ui : UI ;
2022-06-09 01:28:46 +02:00
2025-01-06 03:05:18 +01:00
elementStack : ElementStack = [ ] as ElementStack ;
2021-02-08 22:45:51 +01:00
//#endregion
2022-06-14 00:26:06 +02:00
//#region event bus configuration
private eventBusCommands = {
'get-player-tree' : [ {
function : ( ) = > this . handlePlayerTreeRequest ( )
} ] ,
2024-12-26 14:58:14 +01:00
'get-player-dimensions' : [ {
function : ( ) = > {
2025-01-06 03:05:18 +01:00
this . eventBus . send ( 'uw-config-broadcast' , {
2024-12-26 14:58:14 +01:00
type : 'player-dimensions' ,
data : this.dimensions
} ) ;
}
} ] ,
2022-06-14 00:26:06 +02:00
'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 ( )
2023-01-07 03:06:37 +01:00
} ] ,
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
} ]
2022-06-14 00:26:06 +02:00
}
//#endregion
2023-09-10 19:51:36 +02:00
private dimensionChangeListener = {
that : this ,
handleEvent : function ( event : Event ) {
2025-01-29 23:58:16 +01:00
this . that . trackEnvironmentChanges ( event ) ;
2023-09-10 19:51:36 +02:00
this . that . trackDimensionChanges ( )
}
}
2021-04-10 04:08:09 +02:00
/ * *
* 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 {
2023-01-07 18:57:47 +01:00
if ( this . isFullscreen ) {
2021-04-12 19:01:28 +02:00
return window . innerWidth / window . innerHeight ;
}
2023-04-16 02:43:50 +02:00
if ( ! this . dimensions ) {
this . trackDimensionChanges ( ) ;
2025-01-29 23:58:16 +01:00
this . trackEnvironmentChanges ( ) ;
2023-04-16 02:43:50 +02:00
}
2021-04-10 04:08:09 +02:00
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 ;
}
2021-04-10 04:08:09 +02:00
}
2025-01-29 23:58:16 +01:00
/ * *
* 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
* — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
*
* /
2023-04-12 23:08:19 +02:00
//#region lifecycle
2019-09-03 22:42:38 +02:00
constructor ( videoData ) {
2019-09-22 02:07:04 +02:00
try {
2023-04-12 23:08:19 +02:00
// set all our helper objects
2019-09-22 02:07:04 +02:00
this . logger = videoData . logger ;
this . videoData = videoData ;
2023-04-12 23:08:19 +02:00
this . videoElement = videoData . video ;
this . pageInfo = videoData . pageInfo ;
2023-01-07 18:57:47 +01:00
this . siteSettings = videoData . siteSettings ;
2021-10-26 22:19:41 +02:00
this . eventBus = videoData . eventBus ;
2023-04-12 23:08:19 +02:00
// do the rest
2019-09-22 02:07:04 +02:00
this . invalid = false ;
2025-03-30 23:49:13 +02:00
this . updatePlayer ( ) ;
2024-12-30 23:02:55 +01:00
this . isTooSmall = ( this . element . clientWidth < 1208 || this . element . clientHeight < 720 ) ;
2022-06-14 00:26:06 +02:00
this . initEventBus ( ) ;
2019-09-22 02:07:04 +02:00
this . dimensions = undefined ;
2019-11-29 01:33:58 +01:00
this . periodicallyRefreshPlayerElement = false ;
try {
2023-01-07 18:57:47 +01:00
this . periodicallyRefreshPlayerElement = this . siteSettings . data . currentDOMConfig . periodicallyRefreshPlayerElement ;
2019-11-29 01:33:58 +01:00
} 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-04-16 02:43:50 +02:00
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 ) ;
2019-09-18 01:03:04 +02:00
this . invalid = true ;
}
2022-06-14 00:26:06 +02:00
}
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
* /
2022-06-14 00:26:06 +02:00
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
}
2019-09-18 01:03:04 +02:00
2024-05-07 19:05:10 +02:00
/ * *
* Completely stops everything the extension is doing
* /
2023-04-12 23:08:19 +02:00
destroy() {
2023-09-10 19:51:36 +02:00
document . removeEventListener ( 'fullscreenchange' , this . dimensionChangeListener ) ;
2023-04-12 23:08:19 +02:00
this . stopChangeDetection ( ) ;
2024-12-30 23:02:55 +01:00
this . ui ? . destroy ( ) ;
2023-04-12 23:08:19 +02:00
this . notificationService ? . destroy ( ) ;
}
//#endregion
2024-12-30 23:02:55 +01:00
deferredUiInitialization ( playerDimensions ) {
2025-01-27 02:59:30 +01:00
if ( this . ui || this . siteSettings . data . enableUI . fullscreen === ExtensionMode . Disabled ) {
2024-12-30 23:02:55 +01:00
return ;
}
2025-03-30 23:49:13 +02:00
2024-12-30 23:02:55 +01:00
if (
this . isFullscreen
|| (
2025-01-27 02:59:30 +01:00
this . siteSettings . data . enableUI . theater !== ExtensionMode . Disabled
&& playerDimensions . width > 1208
2024-12-30 23:02:55 +01:00
&& playerDimensions . height > 720
)
) {
2025-03-30 23:49:13 +02:00
2024-12-30 23:02:55 +01:00
this . ui = new UI (
'ultrawidifyUi' ,
{
parentElement : this.element ,
eventBus : this.eventBus ,
playerData : this ,
uiSettings : this.videoData.settings.active.ui
}
) ;
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 ) ;
}
}
}
2023-04-12 23:08:19 +02:00
/ * *
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 ) {
2024-12-30 23:02:55 +01:00
this . ui ? . disable ( ) ;
2024-05-07 19:05:10 +02:00
}
} else {
if ( runLevel >= RunLevel . UIOnly ) {
2024-12-30 23:02:55 +01:00
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 ;
}
2021-04-10 04:10:12 +02:00
/ * *
2023-01-07 03:06:37 +01:00
* Detects whether player element is in theater mode or not .
* If theater mode changed , emits event .
* @returns whether player is in theater mode
2021-04-10 04:10:12 +02:00
* /
2023-01-07 03:06:37 +01:00
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 ;
2018-05-13 13:49:25 +02:00
}
2025-01-29 23:58:16 +01:00
trackEnvironmentChanges() {
if ( this . environment !== this . lastEnvironment ) {
this . lastEnvironment = this . environment ;
this . eventBus . send ( 'uw-environment-change' , { newEnvironment : this.environment } ) ;
}
}
2021-10-26 00:30:38 +02:00
/ * *
*
* /
trackDimensionChanges() {
// get player dimensions _once_
let currentPlayerDimensions ;
2023-04-16 02:43:50 +02:00
this . isFullscreen = ! ! document . fullscreenElement ;
2021-10-26 00:30:38 +02:00
2023-01-07 03:06:37 +01:00
if ( this . isFullscreen ) {
2021-10-26 00:30:38 +02:00
currentPlayerDimensions = {
width : window.innerWidth ,
height : window.innerHeight ,
} ;
} else {
currentPlayerDimensions = {
width : this.element.offsetWidth ,
2023-01-07 03:06:37 +01:00
height : this.element.offsetHeight
} ;
this . detectTheaterMode ( ) ;
2021-10-26 00:30:38 +02:00
}
2024-12-30 23:02:55 +01:00
// defer creating UI
this . deferredUiInitialization ( currentPlayerDimensions ) ;
2021-10-26 00:30:38 +02:00
// if dimensions of the player box are the same as the last known
2024-12-30 23:02:55 +01:00
// 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.
2021-10-26 00:30:38 +02:00
if (
2023-03-29 22:07:50 +02:00
this . dimensions ? . width == currentPlayerDimensions . width
&& this . dimensions ? . height == currentPlayerDimensions . height
2021-10-26 00:30:38 +02:00
) {
2024-12-30 23:02:55 +01:00
this . eventBus . send ( 'restore-ar' , null ) ;
this . eventBus . send ( 'delayed-restore-ar' , { delay : 500 } ) ;
2021-10-26 00:30:38 +02:00
this . dimensions = currentPlayerDimensions ;
return ;
2019-08-25 21:19:56 +02:00
}
2021-10-26 00:30:38 +02:00
// in every other case, we need to check if the player is still
// big enough to warrant our extension running.
this . handleSizeConstraints ( currentPlayerDimensions ) ;
2024-12-30 23:02:55 +01:00
// this.handleDimensionChanges(currentPlayerDimensions, this.dimensions);
2021-10-26 00:30:38 +02:00
// Save current dimensions to avoid triggering this function pointlessly
this . dimensions = currentPlayerDimensions ;
2019-08-25 21:19:56 +02:00
}
2018-05-23 23:57:51 +02:00
2021-10-26 00:30:38 +02:00
/ * *
2023-04-16 02:43:50 +02:00
* Checks if extension is allowed to run in current environment .
2021-10-26 00:30:38 +02:00
* @param currentPlayerDimensions
* /
private handleSizeConstraints ( currentPlayerDimensions : PlayerDimensions ) {
2023-04-16 02:43:50 +02:00
// 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 ;
2023-04-16 02:43:50 +02:00
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 ) ;
2023-04-16 02:43:50 +02:00
}
2021-10-26 00:30:38 +02:00
}
private handleDimensionChanges ( newDimensions : PlayerDimensions , oldDimensions : PlayerDimensions ) {
2024-06-12 20:29:00 +02:00
if ( this . runLevel === RunLevel . Off ) {
2021-10-26 00:30:38 +02:00
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();
2024-12-26 14:58:14 +01:00
this . eventBus . send ( 'uw-config-broadcast' , {
type : 'player-dimensions' ,
data : newDimensions
} ) ;
2024-12-30 23:02:55 +01:00
this . isTooSmall = ! newDimensions . fullscreen && ( newDimensions . width < 1208 || newDimensions . height < 720 ) ;
2021-10-26 00:30:38 +02:00
}
}
onPlayerDimensionsChanged ( mutationList ? , observer ? ) {
this . trackDimensionChanges ( ) ;
2025-01-29 23:58:16 +01:00
this . trackEnvironmentChanges ( ) ;
2018-05-13 13:49:25 +02:00
}
2024-12-26 14:58:14 +01:00
2021-10-26 00:30:38 +02:00
//#region player element change detection
2024-05-07 19:05:10 +02:00
/ * *
* Starts change detection .
* @returns
* /
2018-05-23 23:57:51 +02:00
startChangeDetection ( ) {
2019-09-18 01:03:04 +02:00
if ( this . invalid ) {
2019-08-25 21:19:56 +02:00
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
}
)
) ;
2019-11-29 01:33:58 +01:00
const observerConf = {
attributes : true ,
// attributeFilter: ['style', 'class'],
attributeOldValue : true ,
} ;
2021-10-25 23:11:34 +02:00
2021-03-29 23:30:54 +02:00
this . observer . observe ( this . element ) ;
2019-11-29 01:33:58 +01:00
} 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 ) {
2021-02-08 20:43:56 +01:00
await sleep ( 1000 ) ;
2019-09-22 02:07:04 +02:00
try {
2021-10-26 00:30:38 +02:00
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
}
}
2018-05-23 23:57:51 +02:00
}
2019-09-21 23:50:06 +02:00
2020-01-28 01:27:30 +01:00
doPeriodicPlayerElementChangeCheck() {
if ( this . periodicallyRefreshPlayerElement ) {
2021-10-26 00:30:38 +02:00
this . forceRefreshPlayerElement ( ) ;
2020-01-28 01:27:30 +01:00
}
}
2018-05-23 23:57:51 +02:00
stopChangeDetection ( ) {
2019-08-25 21:19:56 +02:00
this . observer . disconnect ( ) ;
2018-05-23 23:57:51 +02:00
}
2021-10-26 00:30:38 +02:00
//#region helper functions
2019-06-10 23:45:15 +02:00
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 ) {
2019-06-10 23:45:15 +02:00
return true ;
}
}
return false ;
}
2022-06-10 00:22:06 +02:00
equalish ( a , b , tolerance ) {
return a > b - tolerance && a < b + tolerance ;
}
2021-10-26 00:30:38 +02:00
//#endregion
2019-11-04 23:53:28 +01:00
2025-01-06 03:05:18 +01:00
private getElementStack ( ) : ElementStack {
2022-06-09 01:28:46 +02:00
const elementStack : any [ ] = [ {
2023-04-12 23:08:19 +02:00
element : this.videoElement ,
2022-06-10 00:22:06 +02:00
type : 'video' ,
tagName : 'video' ,
2023-04-12 23:08:19 +02:00
classList : this.videoElement.classList ,
id : this.videoElement.id ,
2022-06-09 01:28:46 +02:00
} ] ;
2025-01-06 03:05:18 +01:00
let element = this . videoElement . parentNode as HTMLElement ;
2022-06-09 01:28:46 +02:00
// 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
2022-06-09 01:28:46 +02: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 ,
uiSettings : this.videoData.settings.active.ui
}
) ;
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 host = window . location . hostname ;
let element = this . videoElement . parentNode ;
const videoWidth = this . videoElement . offsetWidth ;
const videoHeight = this . videoElement . offsetHeight ;
let playerCandidate ;
const elementStack = this . getElementStack ( ) ;
2023-01-07 18:57:47 +01:00
const playerQs = this . siteSettings . getCustomDOMQuerySelector ( 'player' ) ;
const playerIndex = this . siteSettings . getPlayerIndex ( ) ;
2018-05-13 13:49:25 +02:00
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 ;
}
2022-06-09 01:28:46 +02:00
}
2020-01-28 01:27:30 +01:00
2023-01-07 18:57:47 +01:00
if ( playerCandidate ) {
if ( options ? . verbose ) {
this . getPlayerAuto ( elementStack , videoWidth , videoHeight ) ;
playerCandidate . heuristics [ 'activePlayer' ] = true ;
}
2022-06-10 00:22:06 +02:00
return playerCandidate . element ;
2022-06-09 01:28:46 +02:00
} else {
2022-06-10 00:22:06 +02:00
const playerCandidate = this . getPlayerAuto ( elementStack , videoWidth , videoHeight ) ;
playerCandidate . heuristics [ 'activePlayer' ] = true ;
return playerCandidate . element ;
2022-06-09 01:28:46 +02:00
}
}
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
}
}
2023-01-07 18:57:47 +01:00
/ * *
* Gets player based on some assumptions , without us defining shit .
* @param elementStack
* @param videoWidth
* @param videoHeight
* @returns
* /
2022-06-09 01:28:46 +02:00
private getPlayerAuto ( elementStack : any [ ] , videoWidth , videoHeight ) {
let penaltyMultiplier = 1 ;
const sizePenaltyMultiplier = 0.1 ;
const perLevelScorePenalty = 10 ;
2025-03-31 23:26:03 +02:00
let sameSizeBonus = 0 ;
2019-09-01 01:40:39 +02:00
2025-01-06 03:05:18 +01:00
for ( const [ index , element ] of elementStack . entries ( ) ) {
element . index = index ;
2019-09-01 01:40:39 +02:00
2022-06-09 01:28:46 +02:00
// ignore weird elements, those would break our stuff
if ( element . width == 0 || element . height == 0 ) {
element . heuristics [ 'invalidSize' ] = true ;
continue ;
}
2020-01-28 01:27:30 +01:00
2022-06-09 01:28:46 +02:00
// 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 ;
2019-06-10 23:45:15 +02:00
}
2022-06-09 01:28:46 +02:00
score -= playerSizePenalty ;
// we prefer elements closer to the video, so the score of each potential
// candidate gets dinked a bit
2025-03-31 23:26:03 +02:00
// score -= perLevelScorePenalty * penaltyMultiplier;
if ( element . width === elementStack [ index - 1 ] . width && element . height === elementStack [ index - 1 ] . height ) {
score += ++ sameSizeBonus ;
} else {
sameSizeBonus = 0 ;
}
2022-06-09 01:28:46 +02:00
element . autoScore = score ;
element . heuristics [ 'autoScoreDetails' ] = {
playerSizePenalty ,
diffX ,
diffY ,
penaltyMultiplier
}
// ensure next valid candidate is gonna have a harder job winning out
penaltyMultiplier ++ ;
}
}
2025-03-31 23:26:03 +02:00
2022-06-09 01:28:46 +02:00
let bestCandidate : any = { autoScore : - 99999999 , initialValue : true } ;
for ( const element of elementStack ) {
if ( element . autoScore > bestCandidate . autoScore ) {
bestCandidate = element ;
2019-06-10 23:45:15 +02:00
}
2022-06-09 01:28:46 +02:00
}
2025-03-31 23:26:03 +02:00
2022-06-09 01:28:46 +02:00
if ( bestCandidate . initialValue ) {
bestCandidate = null ;
} else {
2022-06-10 00:22:06 +02:00
bestCandidate . heuristics [ 'autoMatch' ] = true ;
2025-01-06 03:05:18 +01:00
if ( this . siteSettings . data . playerAutoConfig ? . initialIndex !== bestCandidate . index ) {
2025-01-28 00:47:45 +01:00
this . siteSettings . set ( 'playerAutoConfig.initialIndex' , bestCandidate . index , { reload : false , scripted : true } ) ;
2025-01-06 03:05:18 +01:00
}
2022-06-09 01:28:46 +02:00
}
2019-06-10 23:45:15 +02:00
2025-03-31 23:26:03 +02: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 } ) ;
}
2022-06-09 01:28:46 +02:00
return bestCandidate ;
}
2019-08-31 18:21:49 +02:00
2023-01-07 18:57:47 +01: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 ) {
2022-06-09 01:28:46 +02:00
const perLevelScorePenalty = 10 ;
let penaltyMultiplier = 0 ;
2021-10-25 23:11:34 +02:00
2023-01-07 18:57:47 +01:00
const allSelectors = document . querySelectorAll ( queryString ) ;
2022-06-09 01:28:46 +02:00
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
2021-04-12 19:03:18 +02:00
if (
2022-06-09 01:28:46 +02:00
( element . width >= videoWidth && this . equalish ( element . height , videoHeight , 2 ) )
|| ( element . height >= videoHeight && this . equalish ( element . width , videoWidth , 2 ) )
2021-04-12 19:03:18 +02:00
) {
2022-06-09 01:28:46 +02:00
score += 75 ;
2019-06-10 23:45:15 +02:00
}
2021-10-25 23:11:34 +02:00
2022-06-09 01:28:46 +02:00
score -= perLevelScorePenalty * penaltyMultiplier ;
element . heuristics [ 'qsScore' ] = score ;
2019-06-10 23:45:15 +02:00
2022-06-09 01:28:46 +02:00
penaltyMultiplier ++ ;
2019-09-18 01:03:04 +02:00
}
2022-06-09 01:28:46 +02:00
}
2019-06-12 23:55:15 +02:00
2022-06-09 01:28:46 +02:00
let bestCandidate : any = { qsScore : - 99999999 , initialValue : true } ;
for ( const element of elementStack ) {
if ( element . qsScore > bestCandidate . qsScore ) {
bestCandidate = element ;
}
2019-09-18 01:03:04 +02:00
}
2022-06-09 01:28:46 +02:00
if ( bestCandidate . initialValue ) {
bestCandidate = null ;
} else {
2022-06-10 00:22:06 +02:00
bestCandidate . heuristics [ 'qsMatch' ] = true ;
2022-06-09 01:28:46 +02:00
}
return bestCandidate ;
}
2023-01-07 18:57:47 +01:00
/ * *
* Lists elements between video and DOM root for display in player selector ( UI )
* /
2022-06-10 00:22:06 +02:00
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 ) ) ) ;
2024-12-30 23:02:55 +01:00
console . log ( '————————————————————— handling player tree request!' )
2022-06-10 00:22:06 +02:00
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
}
2019-06-10 23:45:15 +02:00
2019-08-31 22:10:51 +02:00
forceRefreshPlayerElement() {
2025-03-30 23:49:13 +02:00
this . updatePlayer ( ) ;
2018-11-02 21:19:34 +01:00
}
2018-05-13 13:49:25 +02:00
}
2020-12-03 01:05:39 +01:00
if ( process . env . CHANNEL !== 'stable' ) {
console . info ( "PlayerData loaded" ) ;
}
2018-12-31 01:03:07 +01:00
export default PlayerData ;