ultrawidify/src/ext/lib/video-transform/Resizer.js

526 lines
19 KiB
JavaScript
Raw Normal View History

import Debug from '../../conf/Debug';
import Scaler from './Scaler';
import Stretcher from './Stretcher';
import Zoom from './Zoom';
import PlayerData from '../video-data/PlayerData';
2019-01-20 23:01:45 +01:00
import ExtensionMode from '../../../common/enums/extension-mode.enum';
2019-02-19 21:10:49 +01:00
import Stretch from '../../../common/enums/stretch.enum';
import VideoAlignment from '../../../common/enums/video-alignment.enum';
2019-03-10 23:27:50 +01:00
import AspectRatio from '../../../common/enums/aspect-ratio.enum';
if(Debug.debug) {
console.log("Loading: Resizer.js");
}
class Resizer {
constructor(videoData, logger) {
2018-05-12 02:51:58 +02:00
this.conf = videoData;
this.video = videoData.video;
this.settings = videoData.settings;
this.extensionMode = videoData.extensionMode;
2018-05-12 02:51:58 +02:00
this.logger = logger;
this.scaler = new Scaler(this.conf, logger);
this.stretcher = new Stretcher(this.conf, logger);
this.zoom = new Zoom(this.conf, logger);
2018-05-12 02:51:58 +02:00
// load up default values
this.correctedVideoDimensions = {};
this.currentCss = {};
this.currentStyleString = "";
this.currentPlayerStyleString = "";
this.currentCssValidFor = {};
2018-05-12 02:51:58 +02:00
2019-03-10 23:27:50 +01:00
this.lastAr = {type: AspectRatio.Initial};
2019-02-19 21:10:49 +01:00
this.videoAlignment = this.settings.getDefaultVideoAlignment(window.location.hostname); // this is initial video alignment
this.destroyed = false;
this.resizerId = (Math.random(99)*100).toFixed(0);
2018-09-13 23:47:20 +02:00
if (this.settings.active.pan) {
// console.log("can pan:", this.settings.active.miscSettings.mousePan.enabled, "(default:", this.settings.active.miscSettings.mousePan.enabled, ")")
this.canPan = this.settings.active.miscSettings.mousePan.enabled;
2018-09-13 23:47:20 +02:00
} else {
this.canPan = false;
}
this.userCss = '';
this.userCssClassName = videoData.userCssClassName;
}
injectCss(css) {
this.conf.pageInfo.injectCss(css);
}
ejectCss(css) {
this.conf.pageInfo.ejectCss(css);
}
replaceCss(oldCss, newCss) {
this.conf.pageInfo.replaceCss(oldCss, newCss);
}
prepareCss(css) {
return `.${this.userCssClassName} {${css}}`;
}
destroy(){
this.logger.log('info', ['debug', 'init'], `[Resizer::destroy] <rid:${this.resizerId}> received destroy command.`);
this.destroyed = true;
}
2019-03-10 23:27:50 +01:00
calculateRatioForLegacyOptions(ar){
// also present as modeToAr in Scaler.js
if (ar.type !== AspectRatio.FitWidth && ar.type !== AspectRatio.FitHeight && ar.ratio) {
2019-04-25 22:02:10 +02:00
return ar;
2019-03-10 23:27:50 +01:00
}
// Skrbi za "stare" možnosti, kot na primer "na širino zaslona", "na višino zaslona" in "ponastavi".
// Približevanje opuščeno.
// handles "legacy" options, such as 'fit to widht', 'fit to height' and AspectRatio.Reset. No zoom tho
var ratioOut;
if (!this.conf.video) {
this.logger.log('info', 'debug', "[Scaler.js::modeToAr] No video??",this.conf.video, "killing videoData");
2019-03-10 23:27:50 +01:00
this.conf.destroy();
return null;
}
2019-04-25 22:02:10 +02:00
if (! this.conf.player.dimensions) {
2019-03-10 23:27:50 +01:00
ratioOut = screen.width / screen.height;
2019-04-25 22:02:10 +02:00
} else {
this.logger.log('info', 'debug', `[Resizer::calculateRatioForLegacyOptions] <rid:${this.resizerId}> Player dimensions:`, this.conf.player.dimensions.width ,'x', this.conf.player.dimensions.height,'aspect ratio:', this.conf.player.dimensions.width / this.conf.player.dimensions.height)
2019-03-10 23:27:50 +01:00
ratioOut = this.conf.player.dimensions.width / this.conf.player.dimensions.height;
}
// POMEMBNO: lastAr je potrebno nastaviti šele po tem, ko kličemo _res_setAr(). _res_setAr() predvideva,
// da želimo nastaviti statično (type: 'static') razmerje stranic — tudi, če funkcijo kličemo tu oz. v ArDetect.
//
// IMPORTANT NOTE: lastAr needs to be set after _res_setAr() is called, as _res_setAr() assumes we're
// setting a static aspect ratio (even if the function is called from here or ArDetect).
var fileAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
if (ar.type === AspectRatio.FitWidth){
2019-04-25 22:02:10 +02:00
ar.ratio = ratioOut > fileAr ? ratioOut : fileAr;
2019-03-10 23:27:50 +01:00
}
else if(ar.type === AspectRatio.FitHeight){
2019-04-25 22:02:10 +02:00
ar.ratio = ratioOut < fileAr ? ratioOut : fileAr;
2019-03-10 23:27:50 +01:00
}
else if(ar.type === AspectRatio.Reset){
this.logger.log('info', 'debug', "[Scaler.js::modeToAr] Using original aspect ratio -", fileAr);
2019-04-25 22:02:10 +02:00
ar.ratio = fileAr;
} else {
return null;
2019-03-10 23:27:50 +01:00
}
2019-04-25 22:02:10 +02:00
return ar;
2019-03-10 23:27:50 +01:00
}
2018-05-12 02:51:58 +02:00
setAr(ar, lastAr){
if (this.destroyed) {
return;
}
this.logger.log('info', 'debug', '[Resizer::setAr] <rid:'+this.resizerId+'> trying to set ar. New ar:', ar)
2019-03-10 23:27:50 +01:00
if (ar == null) {
return;
}
2019-05-26 02:53:29 +02:00
if (ar.type === AspectRatio.Automatic ||
ar.type === AspectRatio.Reset && this.lastAr.type === AspectRatio.Initial) {
// some sites do things that interfere with our site (and aspect ratio setting in general)
// first, we check whether video contains anything we don't like
const siteSettings = this.settings.active.sites[window.location.host];
if (siteSettings && siteSettings.autoarPreventConditions) {
if (siteSettings.autoarPreventConditions.videoStyleString) {
const styleString = (this.video.getAttribute('style') || '').split(';');
2019-05-26 02:53:29 +02:00
if (siteSettings.autoarPreventConditions.videoStyleString.containsProperty) {
const bannedProperties = siteSettings.autoarPreventConditions.videoStyleString.containsProperty;
for (const prop in bannedProperties) {
for (const s of styleString) {
if (s.trim().startsWith(prop)) {
// check if css property has a list of allowed values:
if (bannedProperties[prop].allowedValues) {
const styleValue = s.split(':')[1].trim();
// check if property value is on the list of allowed values
// if it's not, we aren't allowed to start aard
if (bannedProperties[prop].allowedValues.indexOf(styleValue) === -1) {
this.logger.log('error', 'debug', "%c[Resizer::setAr] video style contains forbidden css property/value combo: ", "color: #900, background: #100", prop, " — we aren't allowed to start autoar.")
2019-05-26 02:53:29 +02:00
return;
}
} else {
// no allowed values, no problem. We have forbidden property
// and this means aard can't start.
this.logger.log('info', 'debug', "%c[Resizer::setAr] video style contains forbidden css property: ", "color: #900, background: #100", prop, " — we aren't allowed to start autoar.")
2019-05-26 02:53:29 +02:00
return;
}
}
}
}
}
}
}
}
2019-04-25 22:02:10 +02:00
if (lastAr) {
this.lastAr = this.calculateRatioForLegacyOptions(lastAr);
ar = this.calculateRatioForLegacyOptions(ar);
2018-05-12 02:51:58 +02:00
} else {
2019-04-25 22:02:10 +02:00
// NOTE: "fitw" "fith" and "reset" should ignore ar.ratio bit, but
// I'm not sure whether they do. Check that.
ar = this.calculateRatioForLegacyOptions(ar);
if (! ar) {
this.logger.log('info', 'resizer', `[Resizer::setAr] <${this.resizerId}> Something wrong with ar or the player. Doing nothing.`);
2019-04-25 22:02:10 +02:00
return;
}
2019-04-25 22:02:10 +02:00
this.lastAr = {type: ar.type, ratio: ar.ratio}
2018-05-12 02:51:58 +02:00
}
2019-03-10 23:27:50 +01:00
if (this.extensionMode === ExtensionMode.Basic && !PlayerData.isFullScreen() && ar.type !== AspectRatio.Reset) {
// don't actually apply or calculate css when using basic mode if not in fullscreen
// ... unless we're resetting the aspect ratio to original
return;
}
if (! this.video) {
// console.log("No video detected.")
this.conf.destroy();
}
// // pause AR on basic stretch, unpause when using other mdoes
// fir sine reason unpause doesn't unpause. investigate that later
try {
2019-02-19 21:10:49 +01:00
if (this.stretcher.mode === Stretch.Basic) {
this.conf.arDetector.pause();
} else {
2019-03-10 23:27:50 +01:00
if (this.lastAr.type === AspectRatio.Automatic) {
this.conf.arDetector.unpause();
}
}
} catch (e) { // resizer starts before arDetector. this will do nothing but fail if arDetector isn't setup
}
// do stretch thingy
2019-02-19 21:10:49 +01:00
if (this.stretcher.mode === Stretch.NoStretch || this.stretcher.mode === Stretch.Conditional){
var stretchFactors = this.scaler.calculateCrop(ar);
if(! stretchFactors || stretchFactors.error){
this.logger.log('error', 'debug', "[Resizer::setAr] <rid:"+this.resizerId+"> failed to set AR due to problem with calculating crop. Error:", (stretchFactors ? stretchFactors.error : stretchFactors));
if (stretchFactors.error === 'no_video'){
this.conf.destroy();
}
if (stretchFactors.error === 'illegal_video_dimensions') {
if(Debug.debug){
console.log("[Resizer::setAr] <rid:"+this.resizerId+"> Illegal video dimensions found. We will pause everything.");
}
}
return;
}
2019-02-19 21:10:49 +01:00
if(this.stretcher.mode === Stretch.Conditional){
2019-03-10 23:27:50 +01:00
this.stretcher.applyConditionalStretch(stretchFactors, ar.ratio);
}
this.logger.log('info', 'debug', "[Resizer::setAr] Processed stretch factors for ", this.stretcher.mode === Stretch.NoStretch ? 'stretch-free crop.' : 'crop with conditional stretch.', 'Stretch factors are:', stretchFactors);
2019-02-19 21:10:49 +01:00
} else if (this.stretcher.mode === Stretch.Hybrid) {
2019-03-10 23:27:50 +01:00
var stretchFactors = this.stretcher.calculateStretch(ar.ratio);
this.logger.log('info', 'debug', '[Resizer::setAr] Processed stretch factors for hybrid stretch/crop. Stretch factors are:', stretchFactors);
2019-02-19 21:10:49 +01:00
} else if (this.stretcher.mode === Stretch.Basic) {
2018-06-15 00:33:10 +02:00
var stretchFactors = this.stretcher.calculateBasicStretch();
this.logger.log('info', 'debug', '[Resizer::setAr] Processed stretch factors for basic stretch. Stretch factors are:', stretchFactors);
} else {
var stretchFactors = {xFactor: 1, yFactor: 1}
this.logger.log('error', 'debug', '[Resizer::setAr] Okay wtf happened? If you see this, something has gone wrong', stretchFactors,"\n------[ i n f o d u m p ]------\nstretcher:", this.stretcher);
}
this.zoom.applyZoom(stretchFactors);
2018-05-02 17:52:25 +02:00
//TODO: correct these two
var translate = this.computeOffsets(stretchFactors);
this.applyCss(stretchFactors, translate);
2018-07-10 20:36:12 +02:00
}
2018-06-15 00:33:10 +02:00
2018-07-10 20:36:12 +02:00
resetLastAr() {
2019-03-10 23:27:50 +01:00
this.lastAr = {type: AspectRatio.Initial};
2018-05-12 02:51:58 +02:00
}
2018-05-02 17:52:25 +02:00
setLastAr(override){
this.lastAr = override;
}
getLastAr(){
return this.lastAr;
}
setStretchMode(stretchMode){
2019-02-19 21:10:49 +01:00
this.stretcher.setStretchMode(stretchMode);
2018-05-27 21:41:08 +02:00
this.restore();
}
2018-05-02 17:52:25 +02:00
panHandler(event, forcePan) {
2018-09-13 23:47:20 +02:00
// console.log("this.conf.canPan:", this.conf.canPan)
if (this.canPan || forcePan) {
2018-09-13 23:47:20 +02:00
if(!this.conf.player || !this.conf.player.element) {
return;
}
// dont allow weird floats
2019-02-19 21:10:49 +01:00
this.videoAlignment = VideoAlignment.Center;
2018-09-13 23:47:20 +02:00
const player = this.conf.player.element;
const relativeX = (event.pageX - player.offsetLeft) / player.offsetWidth;
const relativeY = (event.pageY - player.offsetTop) / player.offsetHeight;
this.logger.log('info', 'mousemove', "[Resizer::panHandler] mousemove.pageX, pageY:", event.pageX, event.pageY, "\nrelativeX/Y:", relativeX, relativeY)
2018-09-13 23:47:20 +02:00
this.setPan(relativeX, relativeY);
}
}
setPan(relativeMousePosX, relativeMousePosY){
// relativeMousePos[X|Y] - on scale from 0 to 1, how close is the mouse to player edges.
// use these values: top, left: 0, bottom, right: 1
if(! this.pan){
this.pan = {};
}
2018-02-04 17:39:26 +01:00
if (this.settings.active.miscSettings.mousePanReverseMouse) {
this.pan.relativeOffsetX = (relativeMousePosX * 1.1) - 0.55;
this.pan.relativeOffsetY = (relativeMousePosY * 1.1) - 0.55;
} else {
this.pan.relativeOffsetX = -(relativeMousePosX * 1.1) + 0.55;
this.pan.relativeOffsetY = -(relativeMousePosY * 1.1) + 0.55;
}
2018-09-13 23:47:20 +02:00
this.restore();
}
2018-05-02 17:52:25 +02:00
setvideoAlignment(videoAlignment) {
this.videoAlignment = videoAlignment;
this.restore();
}
2018-06-15 00:33:10 +02:00
restore() {
this.logger.log('info', 'debug', "[Resizer::restore] <rid:"+this.resizerId+"> attempting to restore aspect ratio. this & settings:", {'a_lastAr': this.lastAr, 'this': this, "settings": this.settings} );
// this is true until we verify that css has actually been applied
2019-03-10 23:27:50 +01:00
if(this.lastAr.type === AspectRatio.Initial){
this.setAr({type: AspectRatio.Reset});
}
else {
2019-03-10 23:27:50 +01:00
if (this.lastAr && this.lastAr.ratio === null) {
throw "Last ar is null!"
}
2019-03-10 23:27:50 +01:00
this.setAr(this.lastAr, this.lastAr)
}
}
2018-05-02 17:52:25 +02:00
reset(){
this.setStretchMode(this.settings.active.sites[window.location.hostname] ? this.settings.active.sites[window.location.hostname].stretch : this.settings.active.sites['@global'].stretch);
this.zoom.setZoom(1);
this.resetPan();
2019-03-10 23:27:50 +01:00
this.setAr({type: AspectRatio.Reset});
}
2018-09-13 23:47:20 +02:00
setPanMode(mode) {
if (mode === 'enable') {
this.canPan = true;
} else if (mode === 'disable') {
this.canPan = false;
} else if (mode === 'toggle') {
this.canPan = !this.canPan;
}
}
resetPan(){
this.pan = undefined;
}
2018-09-21 00:26:08 +02:00
setZoom(zoomLevel, no_announce) {
this.zoom.setZoom(zoomLevel, no_announce);
}
zoomStep(step){
this.zoom.zoomStep(step);
}
resetZoom(){
this.zoom.setZoom(1);
this.restore();
}
resetCrop(){
2019-03-10 23:27:50 +01:00
this.setAr({type: AspectRatio.Reset});
}
resetStretch(){
2019-02-19 21:10:49 +01:00
this.stretcher.setStretchMode(Stretch.NoStretch);
this.restore();
}
// mostly internal stuff
computeOffsets(stretchFactors){
this.logger.log('info', 'debug', "[Resizer::computeOffsets] <rid:"+this.resizerId+"> video will be aligned to ", this.settings.active.sites['@global'].videoAlignment);
2018-09-13 23:47:20 +02:00
const wdiff = this.conf.player.dimensions.width - this.conf.video.offsetWidth;
const hdiff = this.conf.player.dimensions.height - this.conf.video.offsetHeight;
const wdiffAfterZoom = this.conf.video.offsetWidth * stretchFactors.xFactor - this.conf.player.dimensions.width;
const hdiffAfterZoom = this.conf.video.offsetHeight * stretchFactors.yFactor - this.conf.player.dimensions.height;
2018-11-16 23:02:56 +01:00
var translate = {
x: wdiff * 0.5,
y: hdiff * 0.5,
};
if (this.pan) {
2018-09-13 23:47:20 +02:00
// don't offset when video is smaller than player
if(wdiffAfterZoom < 0 && hdiffAfterZoom < 0) {
2018-09-13 23:47:20 +02:00
return translate;
}
translate.x += wdiffAfterZoom * this.pan.relativeOffsetX * this.zoom.scale;
translate.y += hdiffAfterZoom * this.pan.relativeOffsetY * this.zoom.scale;
} else {
if (this.videoAlignment == VideoAlignment.Left) {
translate.x += wdiffAfterZoom * 0.5;
}
else if (this.videoAlignment == VideoAlignment.Right) {
translate.x -= wdiffAfterZoom * 0.5;
}
}
this.logger.log('info', 'debug', "[Resizer::_res_computeOffsets] <rid:"+this.resizerId+"> calculated offsets:\n\n",
'---- data in ----\n',
'player dimensions:', {w: this.conf.player.dimensions.width, h: this.conf.player.dimensions.height},
'video dimensions: ', {w: this.conf.video.offsetWidth, h: this.conf.video.offsetHeight},
'stretch factors: ', stretchFactors,
'pan & zoom: ', this.pan, this.zoom,
'\n\n---- data out ----\n',
'translate:', translate);
2018-09-13 23:47:20 +02:00
return translate;
}
buildStyleArray(existingStyleString, extraStyleString) {
if (existingStyleString) {
const styleArray = existingStyleString.split(";");
if (extraStyleString) {
const extraCss = extraStyleString.split(';');
let dup = false;
for (const ecss of extraCss) {
for (let i in styleArray) {
if (ecss.split(':')[0].trim() === styleArray[i].split(':')[0].trim()) {
dup = true;
styleArray[i] = ecss;
}
if (dup) {
dup = false;
continue;
}
styleArray.push(ecss);
}
}
}
for (var i in styleArray) {
styleArray[i] = styleArray[i].trim();
// some sites do 'top: 50%; left: 50%; transform: <transform>' to center videos.
// we dont wanna, because we already center videos on our own
if (styleArray[i].startsWith("transform:") ||
styleArray[i].startsWith("top:") ||
styleArray[i].startsWith("left:") ||
styleArray[i].startsWith("right:") ||
2019-02-16 01:19:29 +01:00
styleArray[i].startsWith("bottom:") ){
delete styleArray[i];
}
}
return styleArray;
}
return [];
}
buildStyleString(styleArray) {
let styleString = '';
for(var i in styleArray) {
if(styleArray[i]) {
styleString += styleArray[i] + " !important; ";
}
}
return styleString;
}
applyCss(stretchFactors, translate){
// apply extra CSS here. In case of duplicated properties, extraCss overrides
// default styleString
if (! this.video) {
this.logger.log('warn', 'debug', "[Resizer::applyCss] <rid:"+this.resizerId+"> Video went missing, doing nothing.");
this.conf.destroy();
return;
}
this.logger.log('info', 'resizer', "[Resizer::applyCss] <rid:"+this.resizerId+"> will apply css.", {stretchFactors, translate});
// save stuff for quick tests (before we turn numbers into css values):
this.currentVideoSettings = {
validFor: this.conf.player.dimensions,
// videoWidth: dimensions.width,
// videoHeight: dimensions.height
}
let extraStyleString;
try {
extraStyleString = this.settings.active.sites[window.location.host].DOM.video.additionalCss;
} catch (e) {
// do nothing. It's ok if no special settings are defined for this site, we'll just do defaults
}
const styleArray = this.buildStyleArray('', extraStyleString)
// add remaining elements
if (stretchFactors) {
2018-11-16 23:02:56 +01:00
styleArray.push(`transform: translate(${translate.x}px, ${translate.y}px) scale(${stretchFactors.xFactor}, ${stretchFactors.yFactor})`);
styleArray.push("top: 0px !important; left: 0px !important; bottom: 0px !important; right: 0px");
}
const styleString = `${this.buildStyleString(styleArray)}${extraStyleString || ''}`; // string returned by buildStyleString() should end with ; anyway
// build style string back
this.setStyleString(styleString);
}
setStyleString (styleString) {
this.currentCssValidFor = this.conf.player.dimensions;
const newCssString = this.prepareCss(styleString);
// inject new CSS or replace existing one
if (!this.userCss) {
this.injectCss(newCssString);
this.userCss = newCssString;
2019-08-24 17:04:53 +02:00
} else if (newCssString !== this.userCss) {
// we only replace css if it
this.replaceCss(this.userCss, newCssString);
this.userCss = newCssString;
2018-02-05 22:46:38 +01:00
}
}
}
export default Resizer;