import Debug from '../../conf/Debug'; import Scaler, { CropStrategy, VideoDimensions } from './Scaler'; import Stretcher from './Stretcher'; import Zoom from './Zoom'; import PlayerData from '../video-data/PlayerData'; import ExtensionMode from '../../../common/enums/ExtensionMode.enum'; import StretchType from '../../../common/enums/StretchType.enum'; import VideoAlignmentType from '../../../common/enums/VideoAlignmentType.enum'; import AspectRatioType from '../../../common/enums/AspectRatioType.enum'; import CropModePersistance from '../../../common/enums/CropModePersistence.enum'; import { sleep } from '../Util'; import Logger from '../Logger'; import Settings from '../Settings'; import VideoData from '../video-data/VideoData'; import EventBus from '../EventBus'; if(Debug.debug) { console.log("Loading: Resizer.js"); } class Resizer { //#region flags canPan: boolean = false; destroyed: boolean = false; manualZoom: boolean = false; //#endregion //#region helper objects logger: Logger; settings: Settings; scaler: Scaler; stretcher: Stretcher; zoom: Zoom; conf: VideoData; eventBus: EventBus; //#endregion //#region HTML elements video: any; //#endregion //#region data correctedVideoDimensions: any; currentCss: any; currentStyleString: string; currentPlayerStyleString: any; currentCssValidFor: any; currentVideoSettings: any; lastAr: {type: any, ratio?: number} = {type: AspectRatioType.Initial}; resizerId: any; videoAlignment: {x: VideoAlignmentType, y: VideoAlignmentType}; userCss: string; userCssClassName: any; pan: any = null; //#endregion //#region event bus configuration private eventBusCommands = { 'set-ar': [{ function: (config: any) => { this.manualZoom = false; // this only gets called from UI or keyboard shortcuts, making this action safe. this.setAr(config); } }], 'set-alignment': [{ function: (config: any) => { this.setVideoAlignment(config.videoAlignmentX, config.videoAlignmentY); } }], 'set-stretch': [{ function: (config: any) => { this.manualZoom = false; // we also need to unset manual aspect ratio when doing this this.setStretchMode(config.stretchMode, config.fixedAspectRatio) } }], 'set-zoom': [{ function: (config: any) => this.setZoom(config.zoom, config.axis) }], 'change-zoom': [{ function: (config: any) => this.zoomStep(config.step) }], } //#endregion constructor(videoData) { this.resizerId = (Math.random()*100).toFixed(0); this.conf = videoData; this.logger = videoData.logger; this.video = videoData.video; this.settings = videoData.settings; this.eventBus = videoData.eventBus; this.initEventBus(); this.scaler = new Scaler(this.conf); this.stretcher = new Stretcher(this.conf); this.zoom = new Zoom(this.conf); this.videoAlignment = { x: this.settings.getDefaultVideoAlignment(window.location.hostname), y: VideoAlignmentType.Center }; // this is initial video alignment this.destroyed = false; if (this.settings.active.pan) { this.canPan = this.settings.active.miscSettings.mousePan.enabled; } else { this.canPan = false; } this.userCssClassName = videoData.userCssClassName; } initEventBus() { for (const action in this.eventBusCommands) { for (const command of this.eventBusCommands[action]) { this.eventBus.subscribe(action, command); } } } 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] received destroy command.`); this.destroyed = true; } calculateRatioForLegacyOptions(ar){ // also present as modeToAr in Scaler.js if (ar.type !== AspectRatioType.FitWidth && ar.type !== AspectRatioType.FitHeight && ar.ratio) { return ar; } // handles "legacy" options, such as 'fit to widht', 'fit to height' and AspectRatioType.Reset. No zoom tho let ratioOut; if (!this.conf.video) { this.logger.log('info', 'debug', "[Scaler.js::modeToAr] No video??",this.conf.video, "killing videoData"); this.conf.destroy(); return null; } if (! this.conf.player.dimensions) { ratioOut = screen.width / screen.height; } else { this.logger.log('info', 'debug', `[Resizer::calculateRatioForLegacyOptions] 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) ratioOut = this.conf.player.dimensions.width / this.conf.player.dimensions.height; } // 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). let fileAr = this.conf.video.videoWidth / this.conf.video.videoHeight; if (ar.type === AspectRatioType.FitWidth){ ar.ratio = ratioOut > fileAr ? ratioOut : fileAr; } else if(ar.type === AspectRatioType.FitHeight){ ar.ratio = ratioOut < fileAr ? ratioOut : fileAr; } else if(ar.type === AspectRatioType.Reset){ this.logger.log('info', 'debug', "[Scaler.js::modeToAr] Using original aspect ratio -", fileAr); ar.ratio = fileAr; } else { return null; } return ar; } updateAr(ar) { if (!ar) { return; } // Some options require a bit more testing re: whether they make sense // if they don't, we refuse to update aspect ratio until they do if (ar.type === AspectRatioType.Automatic || ar.type === AspectRatioType.Fixed) { if (!ar.ratio || isNaN(ar.ratio)) { return; } } // Only update aspect ratio if there's a difference between the old and the new state if (!this.lastAr || ar.type !== this.lastAr.type || ar.ratio !== this.lastAr.ratio) { this.setAr(ar); } } async setAr(ar: {type: any, ratio?: number}, lastAr?: {type: any, ratio?: number}) { if (this.destroyed) { return; } if (ar.type !== AspectRatioType.Automatic) { this.manualZoom = false; } if (!this.video.videoWidth || !this.video.videoHeight) { this.logger.log('warning', 'debug', '[Resizer::setAr] Video has no width or no height. This is not allowed. Aspect ratio will not be set, and videoData will be uninitialized.'); this.conf.videoUnloaded(); } this.logger.log('info', 'debug', '[Resizer::setAr] trying to set ar. New ar:', ar); if (ar == null) { return; } const siteSettings = this.settings.active.sites[window.location.hostname]; let stretchFactors: {xFactor: number, yFactor: number, arCorrectionFactor?: number, ratio?: number} | any; // reset zoom, but only on aspect ratio switch. We also know that aspect ratio gets converted to // AspectRatioType.Fixed when zooming, so let's keep that in mind if ( (ar.type !== AspectRatioType.Fixed && ar.type !== AspectRatioType.Manual) // anything not these two _always_ changes AR || ar.type !== this.lastAr.type // this also means aspect ratio has changed || ar.ratio !== this.lastAr.ratio // this also means aspect ratio has changed ) { this.zoom.reset(); this.resetPan(); } // most everything that could go wrong went wrong by this stage, and returns can happen afterwards // this means here's the optimal place to set or forget aspect ratio. Saving of current crop ratio // is handled in pageInfo.updateCurrentCrop(), which also makes sure to persist aspect ratio if ar // is set to persist between videos / through current session / until manual reset. if (ar.type === AspectRatioType.Automatic || ar.type === AspectRatioType.Reset || ar.type === AspectRatioType.Initial ) { // reset/undo default this.conf.pageInfo.updateCurrentCrop(undefined); } else { this.conf.pageInfo.updateCurrentCrop(ar); } if (lastAr) { this.lastAr = this.calculateRatioForLegacyOptions(lastAr); ar = this.calculateRatioForLegacyOptions(ar); } else { // 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.`); return; } this.lastAr = {type: ar.type, ratio: ar.ratio}; } if (! this.video) { this.conf.destroy(); } // pause AR on: // * ar.type NOT automatic // * ar.type is auto, but stretch is set to basic basic stretch // // unpause when using other modes if (ar.type !== AspectRatioType.Automatic || this.stretcher.mode === StretchType.Basic) { this.conf?.arDetector?.pause(); } else { if (this.lastAr.type === AspectRatioType.Automatic) { this.conf?.arDetector?.unpause(); } } // do stretch thingy if (this.stretcher.mode === StretchType.NoStretch || this.stretcher.mode === StretchType.Conditional || this.stretcher.mode === StretchType.FixedSource){ stretchFactors = this.scaler.calculateCrop(ar); if(! stretchFactors || stretchFactors.error){ this.logger.log('error', 'debug', `[Resizer::setAr] failed to set AR due to problem with calculating crop. Error:`, stretchFactors?.error); if (stretchFactors?.error === 'no_video'){ this.conf.destroy(); return; } // we could have issued calculate crop too early. Let's tell VideoData that there's something wrong // and exit this function. When