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

511 lines
14 KiB
JavaScript
Raw Normal View History

import Debug from '../../conf/Debug';
import PlayerData from './PlayerData';
import Resizer from '../video-transform/Resizer';
import ArDetector from '../ar-detect/ArDetector';
import AspectRatio from '../../../common/enums/aspect-ratio.enum';
2018-05-09 00:03:22 +02:00
class VideoData {
constructor(video, settings, pageInfo){
2019-09-03 23:49:22 +02:00
this.vdid = (Math.random()*100).toFixed();
2019-09-03 23:01:23 +02:00
this.logger = pageInfo.logger;
this.arSetupComplete = false;
2018-05-09 00:03:22 +02:00
this.video = video;
this.destroyed = false;
this.settings = settings;
2018-11-02 02:52:01 +01:00
this.pageInfo = pageInfo;
this.extensionMode = pageInfo.extensionMode;
this.videoStatusOk = false;
this.userCssClassName = `uw-fuck-you-and-do-what-i-tell-you_${this.vdid}`;
this.videoLoaded = false;
this.videoDimensionsLoaded = true;
this.dimensions = {
width: this.video.offsetWidth,
height: this.video.offsetHeight,
};
// this is in case extension loads before the video
video.addEventListener('loadeddata', () => {
this.logger.log('info', 'init', '[VideoData::ctor->video.onloadeddata] Video fired event "loaded data!"');
this.onVideoLoaded();
});
// this one is in case extension loads after the video is loaded
video.addEventListener('timeupdate', () => {
this.onVideoLoaded();
});
}
async onVideoLoaded() {
if (!this.videoLoaded) {
this.logger.log('info', 'init', '%c[VideoData::onVideoLoaded] ——————————— Initiating phase two of videoData setup ———————————', 'color: #0f9');
this.videoLoaded = true;
this.videoDimensionsLoaded = true;
try {
await this.setupStageTwo();
this.logger.log('info', 'init', '%c[VideoData::onVideoLoaded] ——————————— videoData setup stage two complete ———————————', 'color: #0f9');
} catch (e) {
this.logger.log('error', 'init', '%c[VideoData::onVideoLoaded] ——————————— Setup stage two failed. ———————————\n', 'color: #f00', e);
}
} else if (!this.videoDimensionsLoaded) {
this.restoreCrop();
this.videoDimensionsLoaded = true;
}
}
async setupStageTwo() {
2020-06-02 00:52:23 +02:00
// POZOR: VRSTNI RED JE POMEMBEN (arDetect mora bit zadnji)
// NOTE: ORDERING OF OBJ INITIALIZATIONS IS IMPORTANT (arDetect needs to go last)
// NOTE: We only init observers once player is confirmed valid
const observerConf = {
attributes: true,
// attributeFilter: ['style', 'class'],
attributeOldValue: true,
};
this.player = new PlayerData(this);
if (this.player.invalid) {
this.invalid = true;
return;
}
this.resizer = new Resizer(this);
2020-06-02 00:52:23 +02:00
// INIT OBSERVERS
this.observer = new MutationObserver( (m, o) => {
this.logger.log('info', 'debug', `[VideoData::setupStageTwo->mutationObserver] Mutation observer detected a mutation:`, {m, o});
this.onVideoDimensionsChanged(m, o, this)
});
this.observer.observe(this.video, observerConf);
2020-06-02 00:52:23 +02:00
// INIT AARD
this.arDetector = new ArDetector(this); // this starts Ar detection. needs optional parameter that prevets ardetdctor from starting
2018-05-12 02:51:58 +02:00
// player dimensions need to be in:
// this.player.dimensions
// apply default align and stretch
this.logger.log('info', 'debug', "%c[VideoData::ctor] Initial resizer reset!", {background: '#afd', color: '#132'});
this.resizer.reset();
this.logger.log('info', ['debug', 'init'], '[VideoData::ctor] Created videoData with vdid', this.vdid, '\nextension mode:', this.extensionMode)
this.pageInfo.initMouseActionHandler(this);
this.video.classList.add(this.userCssClassName); // this also needs to be applied BEFORE we initialize resizer!
// start fallback video/player size detection
this.fallbackChangeDetection();
// force reload last aspect ratio (if default crop ratio exists), but only after the video is
if (this.pageInfo.defaultCrop) {
this.resizer.setAr(this.pageInfo.defaultCrop);
}
try {
if (!this.pageInfo.defaultCrop) {
if (!this.invalid) {
this.initArDetection();
} else {
this.logger.log('error', 'debug', '[VideoData::secondStageSetup] Video is invalid. Aard not started.', this.video);
}
} else {
this.logger.log('info', 'debug', '[VideoData::secondStageSetup] Default crop is specified for this site. Not starting aard.');
}
} catch (e) {
this.logger.log('error', 'init', `[VideoData::secondStageSetup] Error with aard initialization (or error with default aspect ratio application)`, e)
}
}
restoreCrop() {
this.logger.log('info', 'debug', "%c[VideoData::restoreCrop] Recovering from illegal video dimensions. Resetting aspect ratio.", {background: '#afd', color: '#132'});
// if we have default crop set for this page, apply this.
// otherwise, reset crop
if (this.pageInfo.defaultCrop) {
this.resizer.setAr(this.pageInfo.defaultCrop);
} else {
this.resizer.reset();
try {
this.startArDetection();
} catch (e) {
this.logger.log('warn', 'debug', '[VideoData::restoreCrop] Autodetection not resumed. Reason:', e);
}
}
}
async fallbackChangeDetection() {
while (!this.destroyed && !this.invalid) {
2019-09-21 23:50:06 +02:00
await this.sleep(500);
this.doPeriodicFallbackChangeDetectionCheck();
}
}
doPeriodicFallbackChangeDetectionCheck() {
this.validateVideoOffsets();
}
async sleep(timeout) {
2020-06-01 23:54:24 +02:00
return new Promise( (resolve) => setTimeout(() => resolve(), timeout));
}
2019-09-14 23:23:00 +02:00
onVideoDimensionsChanged(mutationList, observer, context) {
if (!mutationList || context.video === undefined) { // something's wrong
if (observer && context.video) {
2019-08-25 01:52:04 +02:00
observer.disconnect();
}
return;
}
let confirmAspectRatioRestore = false;
for (let mutation of mutationList) {
if (mutation.type === 'attributes') {
if (mutation.attributeName === 'class') {
if(!context.video.classList.contains(this.userCssClassName) ) {
// force the page to include our class in classlist, if the classlist has been removed
// while classList.add() doesn't duplicate classes (does nothing if class is already added),
// we still only need to make sure we're only adding our class to classlist if it has been
// removed. classList.add() will _still_ trigger mutation (even if classlist wouldn't change).
// This is a problem because INFINITE RECURSION TIME, and we _really_ don't want that.
context.video.classList.add(this.userCssClassName);
}
// always trigger refresh on class changes, since change of classname might trigger change
// of the player size as well.
confirmAspectRatioRestore = true;
}
if (mutation.attributeName === 'style') {
confirmAspectRatioRestore = true;
}
}
}
if (!confirmAspectRatioRestore) {
return;
}
// adding player observer taught us that if element size gets triggered by a class, then
// the 'style' attributes don't necessarily trigger. This means we also need to trigger
// restoreAr here, in case video size was changed this way
context.player.forceRefreshPlayerElement();
context.restoreAr();
// sometimes something fucky wucky happens and mutations aren't detected correctly, so we
// try to get around that
setTimeout( () => {
context.validateVideoOffsets();
}, 100);
}
validateVideoOffsets() {
2019-09-22 02:07:04 +02:00
// validate if current video still exists. If not, we destroy current object
try {
if (! document.body.contains(this.video)) {
this.destroy();
return;
}
} catch (e) {
}
// THIS BREAKS PANNING
const cs = window.getComputedStyle(this.video);
const pcs = window.getComputedStyle(this.player.element);
try {
const transformMatrix = cs.transform.split(')')[0].split(',');
const translateX = +transformMatrix[4];
const translateY = +transformMatrix[5];
const vh = +(cs.height.split('px')[0]);
const vw = +(cs.width.split('px')[0]);
const ph = +(pcs.height.split('px')[0]);
const pw = +(pcs.width.split('px')[0]);
// TODO: check & account for panning and alignment
if (transformMatrix[0] !== 'none'
&& this.isWithin(vh, (ph - (translateY * 2)), 2)
&& this.isWithin(vw, (pw - (translateX * 2)), 2)) {
} else {
this.player.forceDetectPlayerElementChange();
}
} catch(e) {
// do nothing on fail
}
2018-05-12 02:51:58 +02:00
}
isWithin(a, b, diff) {
return a < b + diff && a > b - diff
}
firstTimeArdInit(){
if(this.destroyed || this.invalid) {
2019-08-25 01:52:04 +02:00
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
return;
}
if(! this.arSetupComplete){
this.arDetector = new ArDetector(this);
}
}
initArDetection() {
if(this.destroyed || this.invalid) {
2019-08-25 01:52:04 +02:00
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
return;
}
if (this.arDetector){
this.arDetector.init();
}
else{
this.arDetector = new ArDetector(this);
this.arDetector.init();
}
2018-05-12 02:51:58 +02:00
}
startArDetection() {
this.logger.log('info', 'debug', "[VideoData::startArDetection] starting AR detection")
if(this.destroyed || this.invalid) {
2019-08-25 01:52:04 +02:00
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
return;
}
if (!this.arDetector) {
this.initArDetection();
2018-08-31 00:35:52 +02:00
}
2018-05-12 02:51:58 +02:00
this.arDetector.start();
2018-05-09 00:03:22 +02:00
}
rebootArDetection() {
if(this.destroyed || this.invalid) {
2019-08-25 01:52:04 +02:00
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
return;
}
this.arDetector.init();
}
stopArDetection() {
2018-08-31 00:35:52 +02:00
if (this.arDetector) {
this.arDetector.stop();
}
}
2018-05-09 00:03:22 +02:00
destroy() {
this.logger.log('info', ['debug', 'init'], `[VideoData::destroy] <vdid:${this.vdid}> received destroy command`);
2019-09-22 02:07:04 +02:00
if (this.video) {
this.video.classList.remove(this.userCssClassName);
}
2019-08-31 18:21:49 +02:00
this.pause();
this.destroyed = true;
2019-09-22 02:07:04 +02:00
try {
this.arDetector.stop();
this.arDetector.destroy();
} catch (e) {}
2019-08-25 01:52:04 +02:00
this.arDetector = undefined;
2019-09-22 02:07:04 +02:00
try {
this.resizer.destroy();
} catch (e) {}
2019-08-25 01:52:04 +02:00
this.resizer = undefined;
2019-09-22 02:07:04 +02:00
try {
this.player.destroy();
} catch (e) {}
try {
this.observer.disconnect();
} catch (e) {}
2019-08-25 01:52:04 +02:00
this.player = undefined;
this.video = undefined;
2018-05-09 00:03:22 +02:00
}
2018-05-23 23:58:34 +02:00
pause(){
this.paused = true;
if(this.arDetector){
this.arDetector.stop();
}
if(this.player){
this.player.stop();
}
}
resume(){
if(this.destroyed || this.invalid) {
2019-08-25 01:52:04 +02:00
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
return;
}
2018-05-23 23:58:34 +02:00
this.paused = false;
try {
this.resizer.start();
2018-11-02 02:52:01 +01:00
if (this.player) {
this.player.start();
}
2018-05-23 23:58:34 +02:00
} catch (e) {
this.logger.log('error', 'debug', "[VideoData.js::resume] cannot resume for reasons. Will destroy videoData. Error here:", e);
2018-05-23 23:58:34 +02:00
this.destroy();
}
}
resumeAutoAr(){
if(this.arDetector){
this.startArDetection();
}
}
2019-02-16 01:19:29 +01:00
setManualTick(manualTick) {
if(this.arDetector){
this.arDetector.setManualTick(manualTick);
}
}
2019-02-16 01:19:29 +01:00
tick() {
if(this.arDetector){
this.arDetector.tick();
}
}
setLastAr(lastAr){
if (this.invalid) {
return;
}
this.resizer.setLastAr(lastAr);
}
setAr(ar, lastAr){
if (this.invalid) {
return;
}
if (ar.type === AspectRatio.Fixed || ar.type === AspectRatio.FitHeight || ar.type === AspectRatio.FitHeight) {
this.player.forceRefreshPlayerElement();
}
this.resizer.setAr(ar, lastAr);
}
2018-09-14 00:10:57 +02:00
resetAr() {
if (this.invalid) {
return;
}
2018-09-14 00:10:57 +02:00
this.resizer.reset();
}
resetLastAr() {
if (this.invalid) {
return;
}
this.resizer.setLastAr('original');
}
panHandler(event, forcePan) {
if (this.invalid) {
return;
}
if(this.destroyed) {
2019-08-25 01:52:04 +02:00
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
return;
}
if(!this.resizer) {
this.destroy();
return;
}
this.resizer.panHandler(event, forcePan);
2018-09-13 23:47:20 +02:00
}
setPanMode(mode) {
if (this.invalid) {
return;
}
2018-09-13 23:47:20 +02:00
this.resizer.setPanMode(mode);
}
2019-09-03 23:49:22 +02:00
setVideoAlignment(videoAlignment) {
if (this.invalid) {
return;
}
2019-09-03 23:49:22 +02:00
this.resizer.setVideoAlignment(videoAlignment);
}
restoreAr(){
if (this.invalid) {
return;
}
this.resizer.restore();
}
setStretchMode(stretchMode, fixedStretchRatio){
if (this.invalid) {
return;
}
this.resizer.setStretchMode(stretchMode, fixedStretchRatio);
}
2018-09-21 00:26:08 +02:00
setZoom(zoomLevel, no_announce){
if (this.invalid) {
return;
}
2018-09-21 00:26:08 +02:00
this.resizer.setZoom(zoomLevel, no_announce);
}
zoomStep(step){
if (this.invalid) {
return;
}
2018-09-13 23:47:20 +02:00
this.resizer.zoomStep(step);
}
2018-09-21 00:26:08 +02:00
announceZoom(scale){
if (this.invalid) {
return;
}
2018-09-21 00:26:08 +02:00
this.pageInfo.announceZoom(scale);
}
markPlayer(name, color) {
if (this.invalid) {
return;
}
if (this.player) {
this.player.markPlayer(name, color)
}
}
unmarkPlayer() {
this.player.unmarkPlayer();
}
isPlaying() {
return this.video && this.video.currentTime > 0 && !this.video.paused && !this.video.ended;
}
2019-09-21 23:50:06 +02:00
checkVideoSizeChange(){
const videoWidth = this.video.offsetWidth;
const videoHeight = this.video.offsetHeight;
// 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')){
if(! this.video) {
this.logger.log('info', 'videoDetect', "[VideoDetect] player element isn't defined");
}
2020-01-28 23:34:36 +01:00
if ( this.video &&
( this.dimensions?.width != videoWidth ||
this.dimensions?.height != videoHeight )
2019-09-21 23:50:06 +02:00
) {
this.logger.log('info', 'debug', "[VideoDetect] player size changed. reason: dimension change. Old dimensions?", this.dimensions.width, this.dimensions.height, "new dimensions:", this.video.offsetWidth, this.video.offsetHeight);
}
}
// if size doesn't match, update & return true
2020-01-28 23:34:36 +01:00
if (this.dimensions?.width != videoWidth
|| this.dimensions?.height != videoHeight ){
2019-09-21 23:50:06 +02:00
this.dimensions = {
width: videoWidth,
height: videoHeight,
};
return true;
}
return false;
}
}
export default VideoData;