ultrawidify/src/ext/lib/video-data/PageInfo.ts

462 lines
14 KiB
TypeScript
Raw Normal View History

import VideoData from './VideoData';
import RescanReason from './enums/RescanReason.enum';
2025-07-15 19:41:00 +02:00
import Settings from '../settings/Settings';
import CommsClient from '../comms/CommsClient';
2022-05-06 00:23:01 +02:00
import EventBus from '../EventBus';
import { SiteSettings } from '../settings/SiteSettings';
2023-03-29 22:07:50 +02:00
import IframeManager from './IframeManager';
2025-05-01 00:55:22 +02:00
import { LogAggregator } from '../logging/LogAggregator';
import { ComponentLogger } from '../logging/ComponentLogger';
2020-04-13 15:20:29 +02:00
if (process.env.CHANNEL !== 'stable'){
2020-12-03 01:16:57 +01:00
console.info("Loading PageInfo");
2020-04-13 15:20:29 +02:00
}
2019-02-19 21:10:49 +01:00
/**
*
* The classes kinda go like this:
*
* PageInfo one per page/frame.
* |
* + VideoData child of PageInfo. There may be more than one of those
* | + PlayerData VideoData has exactly ONE (1) PlayerData
* | + AspectRatioDetector VideoData has 0-1 AARD things
* | + Resizer
* | + Scaler
* | + Stretcher
* | + Zoom
* + VideoData
* | + PlayerData
* | :
* :
*
* There is as many VideoData objects as there are videos.
*/
class PageInfo {
//#region flags
readOnly: boolean = false;
hasVideos: boolean = false;
siteDisabled: boolean = false;
isFullscreen: boolean = false;
//#endregion
//#region timers and timeouts
rescanTimer: any;
urlCheckTimer: any;
announceZoomTimeout: any;
//#endregion
//#region helper objects
2025-05-01 00:55:22 +02:00
logAggregator: LogAggregator;
logger: ComponentLogger;
settings: Settings;
siteSettings: SiteSettings;
comms: CommsClient;
2022-05-06 00:23:01 +02:00
eventBus: EventBus;
2021-09-14 23:21:53 +02:00
videos: {videoData: VideoData, element: HTMLVideoElement}[] = [];
2023-03-29 22:07:50 +02:00
iframeManager: IframeManager;
//#endregion
//#region misc stuff
lastUrl: string;
defaultCrop: any;
currentCrop: any;
2022-09-28 01:18:58 +02:00
keyboardHandlerInitQueue: any[] = [];
currentZoomScale: number = 1;
2021-10-25 23:11:34 +02:00
keyboardHandler: any;
2023-07-10 18:27:06 +02:00
fsStatus = {fullscreen: true}; // fsStatus needs to be passed to VideoData, so fullScreen property is shared between videoData instances
2025-06-17 20:12:28 +02:00
isIframe: boolean = false;
//#endregion
2023-09-10 19:51:36 +02:00
fsEventListener = {
that: this,
handleEvent: function(event: Event) {
this.that.fullscreenHandler();
}
};
2025-06-17 20:12:28 +02:00
constructor(eventBus: EventBus, siteSettings: SiteSettings, settings: Settings, logAggregator: LogAggregator, readOnly = false) {
this.isIframe = window.self !== window.top;
2025-05-01 00:55:22 +02:00
this.logAggregator = logAggregator;
this.logger = new ComponentLogger(logAggregator, 'PageInfo', {});
this.settings = settings;
this.siteSettings = siteSettings;
this.lastUrl = window.location.href;
2019-07-03 21:03:12 +02:00
this.readOnly = readOnly;
this.isFullscreen = !!document.fullscreenElement;
2023-03-29 22:07:50 +02:00
this.iframeManager = new IframeManager({eventBus});
2022-07-31 01:12:54 +02:00
if (eventBus){
this.eventBus = eventBus;
}
try {
// request inject css immediately
const playerStyleString = this.siteSettings.data.currentDOMConfig?.customCss?.replace('\\n', '');
this.eventBus.send('inject-css', {cssString: playerStyleString});
} catch (e) {
// do nothing. It's ok if there's no special settings for the player element or crop persistence
}
this.currentCrop = this.defaultCrop;
this.rescan(RescanReason.PERIODIC);
this.scheduleUrlCheck();
2023-09-10 19:51:36 +02:00
document.addEventListener('fullscreenchange', this.fsEventListener);
2025-05-01 00:55:22 +02:00
this.eventBus.subscribeMulti({
'probe-video': {
function: () => {
2025-06-17 20:12:28 +02:00
console.log(`[${window.location}] probe-video received.`)
this.rescan();
2025-05-01 00:55:22 +02:00
}
}
2025-06-17 20:12:28 +02:00
});
}
destroy() {
2025-05-01 00:55:22 +02:00
// this.logger.debug('destroy', 'destroying all videos!")
if(this.rescanTimer){
clearTimeout(this.rescanTimer);
}
2021-02-18 22:38:32 +01:00
for (let video of this.videos) {
2019-08-25 01:52:04 +02:00
try {
this.eventBus.send('noVideo', undefined);
2021-09-14 23:21:53 +02:00
video.videoData.destroy();
2019-08-25 01:52:04 +02:00
} catch (e) {
2025-05-01 00:55:22 +02:00
this.logger.error('destroy', 'unable to destroy video! Error:', e);
2019-08-25 01:52:04 +02:00
}
}
try {
const playerStyleString = this.siteSettings.data.currentDOMConfig?.customCss?.replace('\\n', '');
this.eventBus.send('eject-css', {cssString: playerStyleString});
} catch (e) {
// do nothing. It's ok if there's no special settings for the player element
}
}
2018-06-15 00:33:10 +02:00
/**
* Runs when browser enters full screen.
*/
enterFullscreen() {
2023-07-10 18:27:06 +02:00
this.fsStatus.fullscreen = true;
this.eventBus.send('page-fs-enter', {});
}
/**
* Runs when browser exits full screen
*/
exitFullscreen() {
2023-07-10 18:27:06 +02:00
this.fsStatus.fullscreen = false;
this.eventBus.send('page-fs-exit', {});
}
2023-03-29 22:07:50 +02:00
/**
* Handler for fullscreenchanged event.
*/
2023-09-10 19:51:36 +02:00
fullscreenHandler() {
this.isFullscreen = !!document.fullscreenElement;
2023-03-29 22:07:50 +02:00
2023-09-10 19:51:36 +02:00
if (this.isFullscreen) {
this.enterFullscreen();
} else {
this.exitFullscreen();
2023-03-29 22:07:50 +02:00
}
2023-09-10 19:51:36 +02:00
}
2023-03-29 22:07:50 +02:00
reset() {
2021-02-18 22:38:32 +01:00
for(let video of this.videos) {
2021-09-14 23:21:53 +02:00
video.videoData.destroy();
video.videoData = null;
2018-06-15 00:33:10 +02:00
}
2021-09-14 23:21:53 +02:00
this.videos = [];
2018-06-15 00:33:10 +02:00
this.rescan(RescanReason.MANUAL);
}
initMouseActionHandler(videoData) {
if (this.keyboardHandler) {
this.keyboardHandler.registerHandleMouse(videoData);
} else {
this.keyboardHandlerInitQueue.push(videoData);
}
}
2025-06-17 20:12:28 +02:00
/**
* Returns all videos on the page.
*
* If minSize is provided, it only returns <video> elements that are
* equal or bigger than desired size:
*
* * sm: 320 x 180
* * md: 720 x 400
* * lg: 1280 x 720
*
* If minSize is omitted, it returns all <video> elements.
* @param minSize
* @returns
*/
getAllVideos(minSize?: 'sm' | 'md' | 'lg') {
const videoQs = this.siteSettings.getCustomDOMQuerySelector('video');
let videos: HTMLVideoElement[] = [];
if (videoQs){
videos = Array.from(document.querySelectorAll(videoQs) as NodeListOf<HTMLVideoElement> ?? []);
2025-06-17 20:12:28 +02:00
} else {
videos = Array.from(document.getElementsByTagName('video') ?? []);
}
2025-06-17 20:12:28 +02:00
if (!minSize) {
return videos;
}
return this.filterVideos(videos, minSize);
}
filterVideos(videos: HTMLVideoElement[], minSize: 'sm' | 'md' | 'lg') {
// minimums are determined by vibes and shit.
// 'sm' is based on "slightly smaller than embeds on old.reddit"
const minX = { sm: 320, md: 720, lg: 1280 };
const minY = { sm: 180, md: 400, lg: 720 };
// filter out videos that aren't big enough
2025-06-17 20:12:28 +02:00
return videos.filter(
(v: HTMLVideoElement) => v.clientHeight >= minY[minSize] && v.clientWidth >= minX[minSize]
);
2025-06-17 20:12:28 +02:00
}
2025-06-17 20:12:28 +02:00
/**
* Gets videos on the page that are big enough for extension to trigger
* @returns
*/
getVideos(): HTMLVideoElement[] {
return this.getAllVideos('lg');
}
hasVideo() {
return this.readOnly ? this.hasVideos : this.videos.length;
}
2025-06-17 20:12:28 +02:00
private emitVideoStatus(videosDetected?: boolean) {
// if we're left without videos on the current page, we unregister the page.
// if we have videos, we call register.
if (this.eventBus) {
// We used to send "register video" requests only on the first load, or if the number of
// videos on the page has changed. However, since Chrome Web Store started to require every
// extension requiring "broad permissions" to undergo manual review
// ... and since Chrome Web Store is known for taking their sweet ass time reviewing extensions,
// with review times north of an entire fucking month
// ... and since the legacy way of checking whether our frames-with-videos cache in background
// script contains any frames that no longer exist required us to use webNavigation.getFrame()/
// webNavigation.getAllFrames(), which requires a permission that triggers a review.
//
// While the extension uses some other permissions that trigger manual review, it's said that
// less is better / has a positive effect on your manual review times ... So I guess we'll do
// things in the less-than-optimal. more-than-retarded way.
//
// no but honestly fuck Chrome.
if (videosDetected || this.hasVideo()) {
this.eventBus.send('has-video', null);
} else {
this.eventBus.send('noVideo', null);
}
}
}
2021-09-14 23:21:53 +02:00
/**
* Re-scans the page for videos. Removes any videos that no longer exist from our list
* of videos. Destroys all videoData objects for all the videos that don't have their
* own <video> html element on the page.
* @param rescanReason Why was the rescan triggered. Mostly used for logging.
2021-10-25 23:11:34 +02:00
* @returns
2021-09-14 23:21:53 +02:00
*/
rescan(rescanReason?: RescanReason){
2025-06-17 20:12:28 +02:00
let videosDetected = false;
2021-10-25 23:11:34 +02:00
// is there any video data objects that had their HTML elements removed but not yet
2021-09-14 23:21:53 +02:00
// destroyed? We clean that up here.
const orphans = this.videos.filter(x => !document.body.contains(x.element));
2021-09-14 23:21:53 +02:00
for (const orphan of orphans) {
orphan.videoData.destroy();
}
// remove all destroyed videos.
this.videos = this.videos.filter(x => !x.videoData.destroyed);
2025-06-17 20:12:28 +02:00
2021-09-14 23:21:53 +02:00
// add new videos
2025-06-17 20:12:28 +02:00
try {
// in iframes, emit registerIframe even if video is smaller than required
let vids = this.getAllVideos('sm');
if (this.isIframe && this.eventBus) {
videosDetected ||= vids?.length > 0;
};
// for normal operations, use standard size limits
vids = this.filterVideos(vids, 'lg');
2019-09-03 23:49:22 +02:00
if(!vids || vids.length == 0){
this.hasVideos = false;
2021-10-25 23:11:34 +02:00
2019-09-03 23:49:22 +02:00
if(rescanReason == RescanReason.PERIODIC){
2025-05-01 00:55:22 +02:00
this.logger.info({src: 'rescan', origin: 'videoRescan'}, "Scheduling normal rescan.")
2019-09-03 23:49:22 +02:00
this.scheduleRescan(RescanReason.PERIODIC);
}
2025-06-17 20:12:28 +02:00
this.emitVideoStatus(videosDetected);
2019-09-03 23:49:22 +02:00
return;
}
2019-09-03 23:49:22 +02:00
// add new videos
this.hasVideos = false;
2021-09-14 23:21:53 +02:00
for (const videoElement of vids) {
// do not re-add videos that we already track:
if (this.videos.find(x => x.element.isEqualNode(videoElement))) {
continue;
}
2019-09-03 23:49:22 +02:00
// if we find even a single video with width and height, that means the page has valid videos
// if video lacks either of the two properties, we skip all further checks cos pointless
2021-09-14 23:21:53 +02:00
if(!videoElement.offsetWidth || !videoElement.offsetHeight) {
2019-09-03 23:49:22 +02:00
continue;
}
2021-10-25 23:11:34 +02:00
2021-09-14 23:21:53 +02:00
// at this point, we're certain that we found new videos. Let's update some properties:
this.hasVideos = true;
2025-06-17 20:12:28 +02:00
videosDetected ||= true;
2021-09-14 23:21:53 +02:00
// if PageInfo is marked as "readOnly", we actually aren't adding any videos to anything because
2021-10-25 23:11:34 +02:00
// that's super haram. We're only interested in whether
2021-09-14 23:21:53 +02:00
if (this.readOnly) {
2021-10-25 23:11:34 +02:00
// in lite mode, we're done. This is all the info we want, but we want to actually start doing
2021-09-14 23:21:53 +02:00
// things that interfere with the website. We still want to be running a rescan, tho.
2021-09-14 23:21:53 +02:00
if(rescanReason == RescanReason.PERIODIC){
this.scheduleRescan(RescanReason.PERIODIC);
2019-09-03 23:49:22 +02:00
}
2025-06-17 20:12:28 +02:00
this.emitVideoStatus(videosDetected);
2021-09-14 23:21:53 +02:00
return;
}
2025-05-01 00:55:22 +02:00
this.logger.info({src: 'rescan', origin: 'videoRescan'}, "found new video candidate:", videoElement, "NOTE:: Video initialization starts here:\n--------------------------------\n")
2021-09-14 23:21:53 +02:00
try {
2023-03-29 22:07:50 +02:00
const newVideo = new VideoData(videoElement, this.settings, this.siteSettings, this);
2021-10-25 23:11:34 +02:00
this.videos.push({videoData: newVideo, element: videoElement});
2021-09-14 23:21:53 +02:00
} catch (e) {
2025-05-01 00:55:22 +02:00
this.logger.error('rescan', "rescan error: failed to initialize videoData. Skipping this video.",e);
2019-09-03 23:49:22 +02:00
}
2021-10-25 23:11:34 +02:00
2025-05-01 00:55:22 +02:00
this.logger.info({src: 'rescan', origin: 'videoRescan'}, "END VIDEO INITIALIZATION\n\n\n-------------------------------------\nvideos[] is now this:", this.videos,"\n\n\n\n\n\n\n\n")
}
2019-09-03 23:49:22 +02:00
this.removeDestroyed();
2025-06-17 20:12:28 +02:00
this.emitVideoStatus(videosDetected);
} catch(e) {
// if we encounter a fuckup, we can assume that no videos were found on the page. We destroy all videoData
// objects to prevent multiple initialization (which happened, but I don't know why). No biggie if we destroyed
// videoData objects in error — they'll be back in the next rescan
2025-05-01 00:55:22 +02:00
this.logger.error('rescan', "rescan error: — destroying all videoData objects",e);
for (const v of this.videos) {
2021-09-14 23:21:53 +02:00
v.videoData.destroy();
}
2021-09-14 23:21:53 +02:00
this.videos = [];
return;
}
if(rescanReason == RescanReason.PERIODIC){
this.scheduleRescan(RescanReason.PERIODIC);
}
}
removeDestroyed(){
2021-09-14 23:21:53 +02:00
this.videos = this.videos.filter( vid => vid.videoData.destroyed === false);
}
scheduleRescan(rescanReason){
if(rescanReason != RescanReason.PERIODIC){
this.rescan(rescanReason);
return;
}
try{
if(this.rescanTimer){
clearTimeout(this.rescanTimer);
}
2021-02-18 22:38:32 +01:00
let ths = this;
2021-10-25 23:11:34 +02:00
2019-06-14 21:53:48 +02:00
this.rescanTimer = setTimeout(function(rescanReason){
ths.rescanTimer = null;
2019-06-14 21:53:48 +02:00
ths.rescan(rescanReason);
ths = null;
2019-06-14 21:53:48 +02:00
}, this.settings.active.pageInfo.timeouts.rescan, RescanReason.PERIODIC)
} catch(e) {
2025-05-01 00:55:22 +02:00
this.logger.error('scheduleRescan', "scheduling rescan failed. Here's why:",e)
}
}
scheduleUrlCheck() {
try{
if(this.urlCheckTimer){
clearTimeout(this.urlCheckTimer);
}
2021-02-18 22:38:32 +01:00
let ths = this;
2021-10-25 23:11:34 +02:00
2019-09-03 23:49:22 +02:00
this.urlCheckTimer = setTimeout(function(){
ths.urlCheckTimer = null;
ths.ghettoUrlCheck();
ths = null;
}, this.settings.active.pageInfo.timeouts.urlCheck)
2019-06-14 21:53:48 +02:00
} catch(e){
2025-05-01 00:55:22 +02:00
this.logger.log('scheduleUrlCheck', "scheduling URL check failed. Here's why:",e)
}
}
ghettoUrlCheck() {
if (this.lastUrl != window.location.href){
2025-05-01 00:55:22 +02:00
this.logger.warn('ghettoUrlCheck', "URL has changed. Triggering a rescan!");
2021-10-25 23:11:34 +02:00
this.rescan(RescanReason.URL_CHANGE);
this.lastUrl = window.location.href;
}
2018-07-12 23:27:10 +02:00
this.scheduleUrlCheck();
}
/**
* Updates current crop configuration.
*
* If crop persistence is set to; then
* disabled do nothing
* until tab reload set this.defaultCrop
* current session set current AR to session storage
* forever save settings
* @param ar
* @returns
*/
updateCurrentCrop(ar) {
this.currentCrop = ar;
2021-10-25 23:11:34 +02:00
this.siteSettings.updatePersistentOption('crop', ar);
}
2023-03-29 22:07:50 +02:00
}
2020-12-03 01:16:57 +01:00
if (process.env.CHANNEL !== 'stable'){
console.info("PageInfo loaded!");
}
export default PageInfo;