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

426 lines
14 KiB
JavaScript
Raw Normal View History

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';
if(Debug.debug)
console.log("Loading: PlayerData.js");
/* sprejme <video> tag (element) in seznam imen, ki se lahko pojavijo v razredih oz. id staršev.
// 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 {
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;
// 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);
this.invalid = true;
}
2019-09-22 02:07:04 +02:00
}
2019-09-22 02:07:04 +02:00
async sleep(timeout) {
return new Promise( (resolve, reject) => setTimeout(() => resolve(), timeout));
}
2019-09-22 02:07:04 +02:00
static isFullScreen(){
return ( window.innerHeight == window.screen.height && window.innerWidth == window.screen.width);
}
// 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();
}
}
start(){
this.startChangeDetection();
}
stop(){
this.halted = true;
this.stopChangeDetection();
}
destroy() {
this.stopChangeDetection();
this.destroyOverlay();
}
startChangeDetection(){
if (this.invalid) {
return;
}
2019-09-21 23:50:06 +02:00
2019-09-22 02:07:04 +02:00
try {
const ths = this;
this.observer = new MutationObserver((m,o) => this.onPlayerDimensionsChanged(m,o,ths));
const observerConf = {
attributes: true,
// attributeFilter: ['style', 'class'],
attributeOldValue: true,
};
2019-09-14 23:23:00 +02:00
this.observer.observe(this.element, observerConf);
2019-09-22 02:07:04 +02: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) {
await this.sleep(1000);
2019-09-22 02:07:04 +02:00
try {
if (this.checkPlayerSizeChange()) {
this.videoData.resizer.restore();
}
} 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
}
}
}
2019-09-21 23:50:06 +02:00
stopChangeDetection(){
this.observer.disconnect();
}
makeOverlay() {
if (!this.overlayNode) {
this.destroyOverlay();
}
var overlay = document.createElement('div');
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.position = 'absolute';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.zIndex = '1000000000';
overlay.style.pointerEvents = 'none';
this.overlayNode = overlay;
this.element.appendChild(overlay);
}
destroyOverlay() {
if(this.playerIdElement) {
this.playerIdElement.remove();
this.playerIdElement = undefined;
}
if (this.overlayNode) {
this.overlayNode.remove();
this.overlayNode = undefined;
}
}
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() {
this.logger.log('info', 'debug', "[PlayerData::unmarkPlayer] unmarking player!")
if (this.playerIdElement) {
this.playerIdElement.remove();
}
this.playerIdElement = undefined;
}
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;
}
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
};
}
getPlayer() {
const host = window.location.host;
let element = this.video.parentNode;
2019-08-31 18:21:49 +02:00
const videoWidth = this.video.offsetWidth, videoHeight = this.video.offsetHeight;
const elementQ = [];
let scorePenalty = 0;
let score;
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
}
if (this.settings.active.sites[host]
&& this.settings.active.sites[host].DOM
&& this.settings.active.sites[host].DOM.player
&& 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) {
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);
return element;
}
} else if (this.settings.active.sites[host].DOM.player.querySelectors) {
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,
});
}
element = element.parentNode;
}
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;
}
}
}
// try to find element the old fashioned way
2019-08-31 18:21:49 +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;
}
// 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;
// }
score -= scorePenalty++; // prefer elements closer to <video>
elementQ.push({
element: element,
score: score,
});
}
2019-08-31 18:21:49 +02:00
element = element.parentNode;
}
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;
this.updatePlayerDimensions(playerElement);
return playerElement;
}
// 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-08-31 18:21:49 +02:00
equalish(a,b, tolerance) {
return a > b - tolerance && a < b + tolerance;
}
forceRefreshPlayerElement() {
2019-11-04 23:53:08 +01:00
this.getPlayer();
}
checkPlayerSizeChange(){
// 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)
if (this.logger.canLog('debug')){
2019-09-14 23:23:00 +02:00
if (!this.dimensions) {
} else if (this.dimensions && this.dimensions.fullscreen){
if(! PlayerData.isFullScreen()){
this.logger.log('info', 'debug', "[PlayerDetect] player size changed. reason: exited fullscreen");
}
}
if(! this.element) {
2019-09-14 23:23:00 +02:00
this.logger.log('info', 'playerDetect', "[PlayerDetect] player element isn't defined");
}
2019-09-14 23:23:00 +02:00
if ( this.element && this.dimensions &&
( this.dimensions.width != this.element.offsetWidth ||
this.dimensions.height != this.element.offsetHeight )
) {
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);
}
}
2019-09-22 02:07:04 +02:00
2019-09-14 23:23:00 +02:00
// if size doesn't match, update & return true
if (!this.dimensions
|| this.dimensions.width != this.element.offsetWidth
|| this.dimensions.height != this.element.offsetHeight ){
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
};
}
return true;
}
return false;
}
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
};
return true;
}
return false;
}
this.logger.log('info', 'debug', "[PlayerData::checkFullscreenChange] this.dimensions is not defined. Assuming fs change happened and setting default values.")
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
};
return true;
}
}
export default PlayerData;