2018-12-31 01:03:07 +01:00
import Debug from '../../conf/Debug' ;
2019-01-20 23:01:45 +01:00
import ExtensionMode from '../../../common/enums/extension-mode.enum'
2019-03-10 23:27:50 +01:00
import AspectRatio from '../../../common/enums/aspect-ratio.enum' ;
2018-12-31 01:03:07 +01:00
2018-05-13 13:49:25 +02:00
if ( Debug . debug )
2018-05-13 15:22:28 +02:00
console . log ( "Loading: PlayerData.js" ) ;
2018-05-13 13:49:25 +02:00
/ * s p r e j m e < v i d e o > t a g ( e l e m e n t ) i n s e z n a m i m e n , k i s e l a h k o p o j a v i j o v r a z r e d i h o z . i d s t a r š e v .
// vrne dimenzije predvajalnika (širina, višina)
//
// Na youtube v theater mode je razširitev rahlo pokvarjena. Video tag ostane večji od predvajalnika, ko se zapusti
// celozaslonski način. Ta funkcija skuša to težavo rešiti tako, da poišče element predvajalnika, ki je zavit okoli videa.
//
// Funkcija izkorišča lastnost, da bi načeloma moral biti vsak zunanji element večji od notranjega. Najmanjši element od
// <video> značke pa do korena drevesa bi tako moral biti predvajalnik.
//
// Če je podan seznam imen, potem funkcija vrne dimenzije prvega elementa, ki v id oz. razredu vsebuje katerokoli ime iz seznama
//
// | EN |
//
// 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.
* /
class PlayerData {
2019-09-03 22:42:38 +02:00
constructor ( videoData ) {
2019-09-22 02:07:04 +02:00
try {
this . logger = videoData . logger ;
this . videoData = videoData ;
this . video = videoData . video ;
this . settings = videoData . settings ;
this . extensionMode = videoData . extensionMode ;
this . invalid = false ;
this . element = this . getPlayer ( ) ;
this . dimensions = undefined ;
this . overlayNode = undefined ;
2019-11-29 01:33:58 +01:00
this . periodicallyRefreshPlayerElement = false ;
try {
this . periodicallyRefreshPlayerElement = this . settings . active . sites [ window . location . host ] . DOM . player . 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 ;
}
if ( this . extensionMode === ExtensionMode . Enabled ) {
this . checkPlayerSizeChange ( ) ;
}
this . startChangeDetection ( ) ;
} 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 ;
}
2019-09-22 02:07:04 +02:00
}
2019-09-18 01:03:04 +02:00
2019-09-22 02:07:04 +02:00
async sleep ( timeout ) {
return new Promise ( ( resolve , reject ) => setTimeout ( ( ) => resolve ( ) , timeout ) ) ;
2018-05-13 13:49:25 +02:00
}
2019-09-22 02:07:04 +02:00
2018-05-13 13:49:25 +02:00
static isFullScreen ( ) {
return ( window . innerHeight == window . screen . height && window . innerWidth == window . screen . width ) ;
}
2019-08-25 21:19:56 +02:00
// player size observer may not be strictly necessary here
2019-09-14 23:23:00 +02:00
onPlayerDimensionsChanged ( mutationList , observer , context ) {
if ( context . checkPlayerSizeChange ( ) ) {
context . videoData . resizer . restore ( ) ;
2019-08-25 21:19:56 +02:00
}
}
2018-05-23 23:57:51 +02:00
start ( ) {
this . startChangeDetection ( ) ;
}
stop ( ) {
this . halted = true ;
this . stopChangeDetection ( ) ;
2018-05-13 13:49:25 +02:00
}
2018-05-22 00:19:50 +02:00
destroy ( ) {
this . stopChangeDetection ( ) ;
2018-12-09 01:37:19 +01:00
this . destroyOverlay ( ) ;
2018-05-22 00:19:50 +02:00
}
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 {
2019-11-29 01:33:58 +01:00
const ths = this ;
this . observer = new MutationObserver ( ( m , o ) => this . onPlayerDimensionsChanged ( m , o , ths ) ) ;
const observerConf = {
attributes : true ,
// attributeFilter: ['style', 'class'],
attributeOldValue : true ,
} ;
this . observer . observe ( this . element , observerConf ) ;
} 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 this . sleep ( 1000 ) ;
2019-09-22 02:07:04 +02:00
try {
2020-01-28 01:27:30 +01:00
this . doPeriodicPlayerElementChangeCheck ( ) ;
2019-09-22 02:07:04 +02:00
} catch ( e ) {
console . error ( '[playerdata::legacycd] this message is pretty high on the list of messages you shouldnt 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 ) {
if ( this . forceDetectPlayerElementChange ( ) ) {
this . videoData . resizer . restore ( ) ;
}
}
}
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
}
2018-12-09 01:37:19 +01:00
makeOverlay ( ) {
if ( ! this . overlayNode ) {
this . destroyOverlay ( ) ;
}
2018-12-06 23:55:54 +01:00
var overlay = document . createElement ( 'div' ) ;
overlay . style . width = '100%' ;
overlay . style . height = '100%' ;
2018-12-07 00:17:49 +01:00
overlay . style . position = 'absolute' ;
overlay . style . top = '0' ;
overlay . style . left = '0' ;
overlay . style . zIndex = '1000000000' ;
2018-12-07 00:22:27 +01:00
overlay . style . pointerEvents = 'none' ;
2018-12-06 23:55:54 +01:00
this . overlayNode = overlay ;
this . element . appendChild ( overlay ) ;
}
2018-12-09 01:37:19 +01:00
destroyOverlay ( ) {
if ( this . playerIdElement ) {
this . playerIdElement . remove ( ) ;
this . playerIdElement = undefined ;
}
2018-12-06 23:55:54 +01:00
if ( this . overlayNode ) {
this . overlayNode . remove ( ) ;
2018-12-09 01:37:19 +01:00
this . overlayNode = undefined ;
2018-12-06 23:55:54 +01:00
}
}
2018-12-09 01:37:19 +01:00
markPlayer ( name , color ) {
if ( ! this . overlayNode ) {
this . makeOverlay ( ) ;
}
if ( this . playerIdElement ) {
this . playerIdElement . remove ( ) ;
}
this . playerIdElement = document . createElement ( 'div' ) ;
this . playerIdElement . innerHTML = ` <div style="background-color: ${ color } ; color: #fff; position: absolute; top: 0; left: 0"> ${ name } </div> ` ;
this . overlayNode . appendChild ( this . playerIdElement ) ;
}
unmarkPlayer ( ) {
2019-07-18 21:25:58 +02:00
this . logger . log ( 'info' , 'debug' , "[PlayerData::unmarkPlayer] unmarking player!" )
2018-12-09 01:37:19 +01:00
if ( this . playerIdElement ) {
this . playerIdElement . remove ( ) ;
}
this . playerIdElement = undefined ;
}
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 ;
}
2018-09-13 23:47:20 +02:00
2019-11-04 23:53:28 +01:00
updatePlayerDimensions ( element ) {
const isFullScreen = PlayerData . isFullScreen ( ) ;
this . dimensions = {
width : element . offsetWidth ,
height : element . offsetHeight ,
fullscreen : isFullScreen
} ;
}
2019-09-18 01:03:04 +02:00
getPlayer ( ) {
2019-06-10 23:45:15 +02:00
const host = window . location . host ;
let element = this . video . parentNode ;
2020-01-27 23:42:55 +01:00
const videoWidth = this . video . offsetWidth ;
const videoHeight = this . video . offsetHeight ;
2019-09-01 01:40:39 +02:00
const elementQ = [ ] ;
let scorePenalty = 0 ;
let score ;
2018-05-13 13:49:25 +02:00
2019-09-18 01:03:04 +02:00
try {
if ( ! element ) {
this . logger . log ( 'info' , 'debug' , "[PlayerDetect::_pd_getPlayer] element is not valid, doing nothing." , element )
if ( this . element ) {
const ths = this ;
}
this . element = undefined ;
this . dimensions = undefined ;
return ;
2018-09-13 23:47:20 +02:00
}
2018-05-13 13:49:25 +02:00
2020-01-28 01:27:30 +01:00
// log the entire hierarchy from <video> to root
if ( this . logger . canLog ( 'playerDetect' ) ) {
const logObj = [ ] ;
logObj . push ( ` window size: ${ window . innerWidth } x ${ window . innerHeight } ` ) ;
let e = element ;
while ( e ) {
logObj . push ( { offsetSize : { width : e . offsetWidth , height : e . offsetHeight } , clientSize : { width : e . clientWidth , height : e . clientHeight } , element : e } ) ;
e = e . parentNode ;
}
this . logger . log ( 'info' , 'playerDetect' , "\n\n[PlayerDetect::getPlayer()] element hierarchy (video->root)" , logObj ) ;
}
2020-01-28 23:34:36 +01:00
if ( this . settings . active . sites [ host ] ? . DOM ? . player ? . manual ) {
if ( this . settings . active . sites [ host ] ? . DOM ? . player ? . useRelativeAncestor
&& this . settings . active . sites [ host ] ? . DOM ? . player ? . videoAncestor ) {
2019-06-10 23:45:15 +02:00
2019-09-18 01:03:04 +02:00
let parentsLeft = this . settings . active . sites [ host ] . DOM . player . videoAncestor - 1 ;
while ( parentsLeft -- > 0 ) {
element = element . parentNode ;
}
if ( element ) {
2019-11-04 23:53:28 +01:00
this . updatePlayerDimensions ( element ) ;
2019-09-18 01:03:04 +02:00
return element ;
}
2020-01-28 23:34:36 +01:00
} else if ( this . settings . active . sites [ host ] ? . DOM ? . player ? . querySelectors ) {
2019-09-18 01:03:04 +02:00
const allSelectors = document . querySelectorAll ( this . settings . active . sites [ host ] . DOM . player . querySelectors ) ;
// actually we'll also score this branch in a similar way we score the regular, auto branch
while ( element ) {
// Let's see how this works
if ( this . collectionHas ( allSelectors , element ) ) {
score = 100 ; // every matching element gets a baseline 100 points
// elements that match the size get a hefty bonus
if ( ( element . offsetWidth >= videoWidth && this . equalish ( element . offsetHeight , videoHeight , 2 ) )
|| ( element . offsetHeight >= videoHeight && this . equalish ( element . offsetWidth , videoHeight , 2 ) ) ) {
score += 75 ;
}
// elements farther away from the video get a penalty
score -= ( scorePenalty ++ ) * 20 ;
// push the element on the queue/stack:
elementQ . push ( {
score : score ,
element : element ,
} ) ;
2019-09-01 01:40:39 +02:00
}
2019-09-18 01:03:04 +02:00
element = element . parentNode ;
2019-09-01 01:40:39 +02:00
}
2020-01-28 01:27:30 +01:00
// log player candidates
this . logger . log ( 'info' , 'playerDetect' , 'player detect via query selector: element queue and final element:' , { queue : elementQ , bestCandidate : elementQ . length ? elementQ . sort ( ( a , b ) => b . score - a . score ) [ 0 ] . element : 'n/a' } ) ;
2019-09-18 01:03:04 +02:00
if ( elementQ . length ) {
// return element with biggest score
// if video player has not been found, proceed to automatic detection
2019-11-04 23:53:28 +01:00
const playerElement = elementQ . sort ( ( a , b ) => b . score - a . score ) [ 0 ] . element ;
this . updatePlayerDimensions ( playerElement ) ;
return playerElement ;
2019-09-18 01:03:04 +02:00
}
2019-06-10 23:45:15 +02:00
}
}
2019-09-18 01:03:04 +02:00
// try to find element the old fashioned way
2019-08-31 18:21:49 +02:00
2019-09-18 01:03:04 +02:00
while ( element ) {
// odstranimo čudne elemente, ti bi pokvarili zadeve
// remove weird elements, those would break our stuff
if ( element . offsetWidth == 0 || element . offsetHeight == 0 ) {
element = element . parentNode ;
continue ;
2018-05-13 13:49:25 +02:00
}
2019-09-18 01:03:04 +02:00
// element je player, če je ena stranica enako velika kot video, druga pa večja ali enaka.
// za enakost dovolimo mala odstopanja
// element is player, if one of the sides is as long as the video and the other bigger (or same)
// we allow for tiny variations when checking for equality
if ( ( element . offsetWidth >= videoWidth && this . equalish ( element . offsetHeight , videoHeight , 2 ) )
|| ( element . offsetHeight >= videoHeight && this . equalish ( element . offsetWidth , videoHeight , 2 ) ) ) {
// todo — in case the match is only equalish and not exact, take difference into account when
// calculating score
score = 100 ;
2019-11-04 23:53:08 +01:00
// This entire section is disabled because of some bullshit on vk and some shady CIS streaming sites.
// Possibly removal of this criteria is not necessary, because there was also a bug with force player
//
// if (element.id.indexOf('player') !== -1) { // prefer elements with 'player' in id
// score += 75;
// }
2019-09-22 02:07:04 +02:00
// this has only been observed on steam
2019-11-04 23:53:08 +01:00
// if (element.id.indexOf('movie') !== -1) {
// score += 75;
// }
// if (element.classList.toString().indexOf('player') !== -1) { // prefer elements with 'player' in classlist, but a bit less than id
// score += 50;
// }
2019-09-18 01:03:04 +02:00
score -= scorePenalty ++ ; // prefer elements closer to <video>
elementQ . push ( {
element : element ,
score : score ,
} ) ;
2019-06-10 23:45:15 +02:00
}
2019-08-31 18:21:49 +02:00
2019-09-18 01:03:04 +02:00
element = element . parentNode ;
2018-05-13 13:49:25 +02:00
}
2019-06-10 23:45:15 +02:00
2020-01-28 01:27:30 +01:00
// log player candidates
this . logger . log ( 'info' , 'playerDetect' , 'player detect, auto/fallback: element queue and final element:' , { queue : elementQ , bestCandidate : elementQ . length ? elementQ . sort ( ( a , b ) => b . score - a . score ) [ 0 ] . element : 'n/a' } ) ;
2019-09-18 01:03:04 +02:00
if ( elementQ . length ) {
// return element with biggest score
2019-11-04 23:53:28 +01:00
const playerElement = elementQ . sort ( ( a , b ) => b . score - a . score ) [ 0 ] . element ;
2020-01-28 01:27:30 +01:00
2019-11-04 23:53:28 +01:00
this . updatePlayerDimensions ( playerElement ) ;
return playerElement ;
2019-09-18 01:03:04 +02:00
}
2019-06-12 23:55:15 +02:00
2019-09-18 01:03:04 +02:00
// if no candidates were found, something is obviously very, _very_ wrong.
// we return nothing. Player will be marked as invalid and setup will stop.
// VideoData should check for that before starting anything.
this . logger . log ( 'warn' , 'debug' , '[PlayerData::getPlayer] no matching player was found for video' , this . video , 'Extension cannot work on this site.' ) ;
return ;
} catch ( e ) {
this . logger . log ( 'crit' , 'debug' , '[PlayerData::getPlayer] something went wrong while detecting player:' , e , 'Shutting down extension for this page' ) ;
}
2019-06-10 23:45:15 +02:00
}
2019-08-31 18:21:49 +02:00
equalish ( a , b , tolerance ) {
return a > b - tolerance && a < b + tolerance ;
}
2019-06-10 23:45:15 +02:00
2020-01-27 23:42:55 +01:00
forceDetectPlayerElementChange ( ) {
// save current dimensions before refreshing the player object
const oldDimensions = this . dimensions ;
this . getPlayer ( ) ;
// compare new player object dimensions with the old dimensions
// don't fucking trigger changes if nothing changed
if ( this . dimensions . width === this . dimensions . width && this . dimensions . height === this . dimensions . height ) {
return false ;
} else {
return true ;
}
}
2019-08-31 22:10:51 +02:00
forceRefreshPlayerElement ( ) {
2019-11-04 23:53:08 +01:00
this . getPlayer ( ) ;
2019-08-31 22:10:51 +02:00
}
2018-05-13 13:49:25 +02:00
checkPlayerSizeChange ( ) {
2019-08-25 21:19:56 +02:00
// 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)
2019-07-18 21:25:58 +02:00
if ( this . logger . canLog ( 'debug' ) ) {
2020-01-28 23:34:36 +01:00
if ( this . dimensions ? . fullscreen ) {
2018-05-26 23:08:49 +02:00
if ( ! PlayerData . isFullScreen ( ) ) {
2019-07-18 21:25:58 +02:00
this . logger . log ( 'info' , 'debug' , "[PlayerDetect] player size changed. reason: exited fullscreen" ) ;
2018-05-13 13:49:25 +02:00
}
}
2019-07-18 21:25:58 +02:00
if ( ! this . element ) {
2019-09-14 23:23:00 +02:00
this . logger . log ( 'info' , 'playerDetect' , "[PlayerDetect] player element isn't defined" ) ;
2019-07-04 22:46:18 +02:00
}
2020-01-28 23:34:36 +01:00
if ( this . element &&
( + this . dimensions ? . width != + this . element ? . offsetWidth ||
+ this . dimensions ? . height != + this . element ? . offsetHeight )
2019-06-12 23:55:15 +02:00
) {
2020-01-28 23:34:36 +01:00
this . logger . log ( 'info' , 'debug' , "[PlayerDetect] player size changed. reason: dimension change. Old dimensions?" , this . dimensions ? . width , this . dimensions ? . height , "new dimensions:" , this . element ? . offsetWidth , this . element ? . offsetHeight ) ;
2018-05-13 13:49:25 +02:00
}
}
2019-09-22 02:07:04 +02:00
2019-09-14 23:23:00 +02:00
// if size doesn't match, update & return true
2020-01-28 23:34:36 +01:00
if ( this . dimensions ? . width != this . element . offsetWidth
|| this . dimensions ? . height != this . element . offsetHeight ) {
2019-09-14 23:23:00 +02:00
const isFullScreen = PlayerData . isFullScreen ( ) ;
if ( isFullScreen ) {
this . dimensions = {
width : window . innerWidth ,
height : window . innerHeight ,
fullscreen : true
}
} else {
this . dimensions = {
width : this . element . offsetWidth ,
height : this . element . offsetHeight ,
fullscreen : isFullScreen
} ;
}
2018-05-13 13:49:25 +02:00
return true ;
}
return false ;
}
2018-11-02 21:19:34 +01:00
checkFullscreenChange ( ) {
const isFs = PlayerData . isFullScreen ( ) ;
if ( this . dimensions ) {
if ( this . dimensions . fullscreen != isFs ) {
this . dimensions = {
fullscreen : isFs ,
2018-11-18 20:40:57 +01:00
width : isFs ? screen . width : this . video . offsetWidth ,
height : isFs ? screen . height : this . video . offsetHeight
2018-11-02 21:19:34 +01:00
} ;
return true ;
}
return false ;
}
2019-07-18 21:25:58 +02:00
this . logger . log ( 'info' , 'debug' , "[PlayerData::checkFullscreenChange] this.dimensions is not defined. Assuming fs change happened and setting default values." )
2018-11-02 21:19:34 +01:00
this . dimensions = {
fullscreen : isFs ,
2018-11-18 20:40:57 +01:00
width : isFs ? screen . width : this . video . offsetWidth ,
height : isFs ? screen . height : this . video . offsetHeight
2018-11-02 21:19:34 +01:00
} ;
return true ;
}
2018-05-13 13:49:25 +02:00
}
2018-12-31 01:03:07 +01:00
export default PlayerData ;