Refactor player detection + have player detection log some data for use in settings window
This commit is contained in:
parent
9a7e38d897
commit
296f146835
@ -65,6 +65,8 @@ class PlayerData {
|
|||||||
private observer: ResizeObserver;
|
private observer: ResizeObserver;
|
||||||
|
|
||||||
private ui: any;
|
private ui: any;
|
||||||
|
|
||||||
|
elementStack: any[] = [];
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -140,7 +142,6 @@ class PlayerData {
|
|||||||
return ( ihdiff < 5 && iwdiff < 5 );
|
return ( ihdiff < 5 && iwdiff < 5 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@ -414,162 +415,188 @@ class PlayerData {
|
|||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
getPlayer() {
|
/**
|
||||||
|
* Finds and returns HTML element of the player
|
||||||
|
*/
|
||||||
|
getPlayer(options?: {verbose?: boolean}) {
|
||||||
const host = window.location.hostname;
|
const host = window.location.hostname;
|
||||||
let element = this.video.parentNode;
|
let element = this.video.parentNode;
|
||||||
const videoWidth = this.video.offsetWidth;
|
const videoWidth = this.video.offsetWidth;
|
||||||
const videoHeight = this.video.offsetHeight;
|
const videoHeight = this.video.offsetHeight;
|
||||||
const elementQ = [];
|
let playerCandidate;
|
||||||
const scorePenalty = 10;
|
|
||||||
const sizePenaltyMultiplier = 0.1;
|
|
||||||
let penaltyMultiplier = 0;
|
|
||||||
let score;
|
|
||||||
|
|
||||||
try {
|
const elementStack: any[] = [{
|
||||||
if(! element ){
|
element: this.video,
|
||||||
this.logger.log('info', 'debug', "[PlayerDetect::_pd_getPlayer] element is not valid, doing nothing.", element)
|
type: 'video'
|
||||||
if(this.element) {
|
}];
|
||||||
const ths = this;
|
|
||||||
}
|
|
||||||
this.element = undefined;
|
|
||||||
this.dimensions = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// log the entire hierarchy from <video> to root
|
// first pass to generate the element stack and translate it into array
|
||||||
if (this.logger.canLog('playerDetect')) {
|
while (element) {
|
||||||
const logObj = [];
|
elementStack.push({
|
||||||
logObj.push(`window size: ${window.innerWidth} x ${window.innerHeight}`);
|
element,
|
||||||
let e = element;
|
tagName: element.tagName,
|
||||||
while (e) {
|
classList: element.classList,
|
||||||
logObj.push({offsetSize: {width: e.offsetWidth, height: e.offsetHeight}, clientSize: {width: e.clientWidth, height: e.clientHeight}, element: e});
|
id: element.id,
|
||||||
e = e.parentNode;
|
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
|
||||||
this.logger.log('info', 'playerDetect', "\n\n[PlayerDetect::getPlayer()] element hierarchy (video->root)", logObj);
|
heuristics: {},
|
||||||
}
|
});
|
||||||
|
element = element.parentElement;
|
||||||
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) {
|
|
||||||
|
|
||||||
let parentsLeft = this.settings.active.sites[host].DOM.player.videoAncestor - 1;
|
|
||||||
while (parentsLeft --> 0) {
|
|
||||||
element = element.parentNode;
|
|
||||||
}
|
|
||||||
if (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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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'});
|
|
||||||
|
|
||||||
if (elementQ.length) {
|
|
||||||
// return element with biggest score
|
|
||||||
// if video player has not been found, proceed to automatic detection
|
|
||||||
const playerElement = elementQ.sort( (a,b) => b.score - a.score)[0].element;
|
|
||||||
return playerElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to find element the old fashioned way
|
|
||||||
|
|
||||||
while (element){
|
|
||||||
// remove weird elements, those would break our stuff
|
|
||||||
if ( element.offsetWidth == 0 || element.offsetHeight == 0){
|
|
||||||
element = element.parentNode;
|
|
||||||
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.offsetHeight, videoHeight, 5)
|
|
||||||
|| this.equalish(element.offsetWidth, videoWidth, 5)
|
|
||||||
) {
|
|
||||||
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.
|
|
||||||
|
|
||||||
// prefer elements closer to <video>
|
|
||||||
score -= scorePenalty * penaltyMultiplier++;
|
|
||||||
|
|
||||||
// the bigger the size difference between the video and the player,
|
|
||||||
// the more penalty we'll incur. Since we did some grace ith
|
|
||||||
let playerSizePenalty = 1;
|
|
||||||
if ( element.offsetHeight > (videoHeight + 5)) {
|
|
||||||
playerSizePenalty = (element.offsetWidth - videoHeight) * sizePenaltyMultiplier;
|
|
||||||
}
|
|
||||||
if ( element.offsetWidth > (videoWidth + 5)) {
|
|
||||||
playerSizePenalty *= (element.offsetWidth - videoWidth) * sizePenaltyMultiplier
|
|
||||||
}
|
|
||||||
|
|
||||||
score -= playerSizePenalty;
|
|
||||||
|
|
||||||
elementQ.push({
|
|
||||||
element: element,
|
|
||||||
score: score,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
element = element.parentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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'});
|
|
||||||
|
|
||||||
if (elementQ.length) {
|
|
||||||
// return element with biggest score
|
|
||||||
const playerElement = elementQ.sort( (a,b) => b.score - a.score)[0].element;
|
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
this.elementStack = elementStack;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
playerCandidate = this.getPlayerParentIndex(elementStack);
|
||||||
|
} else if (this.settings.active.sites[host]?.DOM?.player?.querySelectors) {
|
||||||
|
playerCandidate = this.getPlayerQs(elementStack, videoWidth, videoHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if 'verbose' option is passed, we also populate the elementStack
|
||||||
|
// with heuristics data for auto player detection.
|
||||||
|
if (playerCandidate && !options?.verbose) {
|
||||||
|
return playerCandidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.verbose && playerCandidate) {
|
||||||
|
// remember — we're only populating elementStack. If we found a player
|
||||||
|
// element using manual methods, we will still return that element.
|
||||||
|
this.getPlayerAuto(elementStack, videoWidth, videoHeight);
|
||||||
|
return playerCandidate;
|
||||||
|
} else {
|
||||||
|
return this.getPlayerAuto(elementStack, videoWidth, videoHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPlayerAuto(elementStack: any[], videoWidth, videoHeight) {
|
||||||
|
let penaltyMultiplier = 1;
|
||||||
|
const sizePenaltyMultiplier = 0.1;
|
||||||
|
const perLevelScorePenalty = 10;
|
||||||
|
|
||||||
|
for (const element of elementStack) {
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
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 = bestCandidate.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestCandidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPlayerQs(elementStack: any[], videoWidth, videoHeight) {
|
||||||
|
const host = window.location.hostname;
|
||||||
|
const perLevelScorePenalty = 10;
|
||||||
|
let penaltyMultiplier = 0;
|
||||||
|
|
||||||
|
const allSelectors = document.querySelectorAll(this.settings.active.sites[host].DOM.player.querySelectors);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = bestCandidate.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestCandidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPlayerParentIndex(elementStack: any[]) {
|
||||||
|
const host = window.location.hostname;
|
||||||
|
elementStack[this.settings.active.sites[host].DOM.player.videoAncestor].heuristics['manualElementByParentIndex'] = true;
|
||||||
|
return elementStack[this.settings.active.sites[host].DOM.player.videoAncestor].element;
|
||||||
}
|
}
|
||||||
|
|
||||||
equalish(a,b, tolerance) {
|
equalish(a,b, tolerance) {
|
||||||
|
Loading…
Reference in New Issue
Block a user