import { SiteSettings } from './../settings/SiteSettings'; 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 siteSettings from '../Settings'; import VideoData from '../video-data/VideoData'; import EventBus from '../EventBus'; import { _cp } from '../../../common/js/utils'; import Settings from '../Settings'; import { Ar } from '../../../common/interfaces/ArInterface'; import { RunLevel } from '../../enum/run-level.enum'; import * as _ from 'lodash'; if(Debug.debug) { console.log("Loading: Resizer.js"); } /** * Resizer is the top class and is responsible for figuring out which component needs to crop, which * component needs to zoom, and which component needs to stretch. * * It also kinda does lots of the work that should prolly be moved to Scaler. * */ class Resizer { //#region flags canPan: boolean = false; destroyed: boolean = false; manualZoom: boolean = false; //#endregion //#region helper objects logger: Logger; settings: Settings; siteSettings: SiteSettings; scaler: Scaler; stretcher: Stretcher; zoom: Zoom; videoData: 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: Ar = {type: AspectRatioType.Initial}; set lastAr(x: Ar) { this._lastAr = x; // emit updates for UI when setting lastAr this.eventBus.send('uw-config-broadcast', {type: 'ar', config: x}) } get lastAr() { return this._lastAr; } resizerId: any; videoAlignment: {x: VideoAlignmentType, y: VideoAlignmentType}; userCss: string; userCssClassName: any; pan: any = null; //#endregion cycleableAspectRatios: Ar[]; nextCycleOptionIndex = 0; //#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. if (config.type !== AspectRatioType.Cycle) { this.setAr(config); } else { // if we manually switched to a different aspect ratio, cycle from that ratio forward const lastArIndex = this.cycleableAspectRatios.findIndex(x => x.type === this.lastAr.type && x.ratio === this.lastAr.ratio); if (lastArIndex !== -1) { this.nextCycleOptionIndex = (lastArIndex + 1) % this.cycleableAspectRatios.length; } this.setAr(this.cycleableAspectRatios[this.nextCycleOptionIndex]); this.nextCycleOptionIndex = (this.nextCycleOptionIndex + 1) % this.cycleableAspectRatios.length; } } }], 'set-alignment': [{ function: (config: any) => { this.setVideoAlignment(config.x, config.y); } }], 'set-stretch': [{ function: (config: any) => { this.manualZoom = false; // we also need to unset manual aspect ratio when doing this this.setStretchMode(config.type, config.ratio) } }], 'set-zoom': [{ function: (config: any) => this.setZoom(config.zoom, config.axis, config.noAnnounce) }], 'change-zoom': [{ function: (config: any) => this.zoomStep(config.step) }], 'get-ar': [{ function: () => this.eventBus.send('uw-config-broadcast', {type: 'ar', config: this.lastAr}) }], 'get-resizer-config': [{ function: () => this.eventBus.send( 'uw-resizer-config-broadcast', { ar: this.lastAr, stretchMode: this.stretcher.mode, videoAlignment: this.videoAlignment } ) }], 'restore-ar': [{ function: () => this.restore() }], 'delayed-restore-ar': [{ function: () => { _.debounce( this.restore, 500, { leading: true, trailing: true } ) } }] } //#endregion constructor(videoData) { this.resizerId = (Math.random()*100).toFixed(0); this.videoData = videoData; this.logger = videoData.logger; this.video = videoData.video; this.settings = videoData.settings; this.siteSettings = videoData.siteSettings; this.eventBus = videoData.eventBus; this.initEventBus(); this.scaler = new Scaler(this.videoData); this.stretcher = new Stretcher(this.videoData); this.zoom = new Zoom(this.videoData); const defaultCrop = this.siteSettings.getDefaultOption('crop') as {type: AspectRatioType, ratio?: number }; if (defaultCrop.type !== AspectRatioType.Reset) { this.lastAr = defaultCrop; } this.videoAlignment = this.siteSettings.getDefaultOption('alignment') as {x: VideoAlignmentType, y: VideoAlignmentType} // this is initial video alignment this.destroyed = false; // if (this.siteSettings.active.pan) { // this.canPan = this.siteSettings.active.miscSettings.mousePan.enabled; // } else { // this.canPan = false; // } this.cycleableAspectRatios = (this.settings?.active?.commands?.crop ?? []) .filter(x => [AspectRatioType.FitHeight, AspectRatioType.FitWidth, AspectRatioType.Fixed, AspectRatioType.Reset].includes(x?.arguments?.type)) .map(x => x.arguments) as Ar[]; this.nextCycleOptionIndex = 0; this.userCssClassName = videoData.userCssClassName; } initEventBus() { for (const action in this.eventBusCommands) { for (const command of this.eventBusCommands[action]) { this.eventBus.subscribe(action, command); } } } prepareCss(css) { return `.${this.userCssClassName} {${css}}`; } destroy(){ this.logger.log('info', ['debug', 'init'], `[Resizer::destroy] received destroy command.`); this.destroyed = true; } getFileAr() { return this.videoData.video.videoWidth / this.videoData.video.videoHeight; } 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.videoData.video) { this.logger.log('info', 'debug', "[Scaler.js::modeToAr] No video??",this.videoData.video, "killing videoData"); this.videoData.destroy(); return null; } if (! this.videoData.player.dimensions) { ratioOut = screen.width / screen.height; } else { this.logger.log('info', 'debug', `[Resizer::calculateRatioForLegacyOptions] Player dimensions:`, this.videoData.player.dimensions.width ,'x', this.videoData.player.dimensions.height,'aspect ratio:', this.videoData.player.dimensions.width / this.videoData.player.dimensions.height) ratioOut = this.videoData.player.dimensions.width / this.videoData.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.getFileAr(); 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.AutomaticUpdate || 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: Ar, lastAr?: Ar) { if (this.destroyed) { return; } if ([AspectRatioType.Reset, AspectRatioType.Initial].includes(ar.type)) { console.log('run level is UI only because aspect ratio type is', ar.type) this.eventBus.send('set-run-level', RunLevel.UIOnly); } else { this.eventBus.send('set-run-level', RunLevel.CustomCSSActive); } // handle autodetection stuff if (ar.type === AspectRatioType.Automatic) { this.videoData.arDetector?.start(); return; } else if (ar.type !== AspectRatioType.AutomaticUpdate) { this.videoData.arDetector?.stop(); } if (ar.type !== AspectRatioType.AutomaticUpdate) { 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.videoData.videoUnloaded(); } this.logger.log('info', 'debug', '%c[Resizer::setAr] trying to set ar. New ar:', 'background-color: #4c3a2f, color: #ffa349', ar); if (ar == null) { return; } 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.Reset || // ar.type === AspectRatioType.Initial // ) { // // reset/undo default // this.videoData.pageInfo.updateCurrentCrop(undefined); // } else { this.videoData.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.videoData.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 && ar.type !== AspectRatioType.AutomaticUpdate) || this.stretcher.mode === StretchType.Basic) { this.videoData?.arDetector?.pause(); } else { if (ar.type !== AspectRatioType.AutomaticUpdate) { if (this.lastAr.type === AspectRatioType.Automatic || this.lastAr.type === AspectRatioType.AutomaticUpdate) { this.videoData?.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.videoData.destroy(); return; } // we could have issued calculate crop too early. Let's tell VideoData that there's something wrong // and exit this function. When