2018-12-31 01:03:07 +01:00
import VideoData from './VideoData' ;
2021-02-09 00:37:54 +01:00
import RescanReason from './enums/RescanReason.enum' ;
import Settings from '../Settings' ;
import CommsClient from '../comms/CommsClient' ;
2022-05-06 00:23:01 +02:00
import EventBus from '../EventBus' ;
2023-01-07 03:06:37 +01:00
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' ;
2018-12-31 01:03:07 +01:00
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
2021-10-26 00:30:38 +02: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 .
* /
2018-05-13 13:49:25 +02:00
class PageInfo {
2021-02-09 00:37:54 +01:00
//#region flags
readOnly : boolean = false ;
hasVideos : boolean = false ;
siteDisabled : boolean = false ;
2023-01-07 03:06:37 +01:00
isFullscreen : boolean = false ;
2021-02-09 00:37:54 +01:00
//#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 ;
2021-02-09 00:37:54 +01:00
settings : Settings ;
2023-01-07 03:06:37 +01:00
siteSettings : SiteSettings ;
2021-02-09 00:37:54 +01:00
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 ;
2021-02-09 00:37:54 +01:00
//#endregion
//#region misc stuff
lastUrl : string ;
defaultCrop : any ;
currentCrop : any ;
2022-09-28 01:18:58 +02:00
keyboardHandlerInitQueue : any [ ] = [ ] ;
2021-02-09 00:37:54 +01:00
currentZoomScale : number = 1 ;
2021-10-25 23:11:34 +02:00
2022-09-28 00:38:36 +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 ;
2021-02-09 00:37:54 +01:00
//#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' , { } ) ;
2018-08-05 23:48:56 +02:00
this . settings = settings ;
2023-01-07 18:57:47 +01:00
this . siteSettings = siteSettings ;
2023-01-07 03:06:37 +01:00
2018-05-21 22:43:56 +02:00
this . lastUrl = window . location . href ;
2019-07-03 21:03:12 +02:00
this . readOnly = readOnly ;
2019-07-18 21:25:58 +02:00
2023-01-07 03:06:37 +01:00
this . isFullscreen = ! ! document . fullscreenElement ;
2023-03-29 22:07:50 +02:00
this . iframeManager = new IframeManager ( { eventBus } ) ;
2023-01-07 03:06:37 +01:00
2022-07-31 01:12:54 +02:00
if ( eventBus ) {
this . eventBus = eventBus ;
2018-08-30 00:56:15 +02:00
}
2019-07-07 21:13:28 +02:00
try {
2019-10-24 00:45:11 +02:00
// request inject css immediately
2023-01-07 03:06:37 +01:00
const playerStyleString = this . siteSettings . data . currentDOMConfig ? . customCss ? . replace ( '\\n' , '' ) ;
2022-07-31 00:15:28 +02:00
this . eventBus . send ( 'inject-css' , { cssString : playerStyleString } ) ;
2019-10-24 23:27:43 +02:00
} catch ( e ) {
// do nothing. It's ok if there's no special settings for the player element or crop persistence
}
2019-10-24 00:45:11 +02:00
2019-10-24 23:27:43 +02:00
this . currentCrop = this . defaultCrop ;
2019-07-07 21:13:28 +02:00
2018-11-07 00:03:06 +01:00
this . rescan ( RescanReason . PERIODIC ) ;
this . scheduleUrlCheck ( ) ;
2023-01-07 03:06:37 +01:00
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
} ) ;
2018-05-13 21:05:11 +02:00
}
2018-08-30 00:56:15 +02:00
destroy() {
2025-05-01 00:55:22 +02:00
// this.logger.debug('destroy', 'destroying all videos!")
2018-08-30 23:03:47 +02:00
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 {
2022-07-31 00:15:28 +02:00
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
}
2018-08-30 00:56:15 +02:00
}
2019-06-14 02:15:24 +02:00
try {
2023-01-07 03:06:37 +01:00
const playerStyleString = this . siteSettings . data . currentDOMConfig ? . customCss ? . replace ( '\\n' , '' ) ;
2022-07-31 00:15:28 +02:00
this . eventBus . send ( 'eject-css' , { cssString : playerStyleString } ) ;
2019-06-14 02:15:24 +02:00
} catch ( e ) {
// do nothing. It's ok if there's no special settings for the player element
}
2018-08-30 00:56:15 +02:00
}
2018-06-15 00:33:10 +02:00
2023-01-07 03:06:37 +01:00
/ * *
* Runs when browser enters full screen .
* /
enterFullscreen() {
2023-07-10 18:27:06 +02:00
this . fsStatus . fullscreen = true ;
2023-01-07 03:06:37 +01:00
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 ;
2023-01-07 03:06:37 +01:00
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
2018-08-22 23:46:59 +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 ) ;
}
2018-12-02 23:51:34 +01:00
initMouseActionHandler ( videoData ) {
2022-09-28 00:38:36 +02:00
if ( this . keyboardHandler ) {
this . keyboardHandler . registerHandleMouse ( videoData ) ;
2018-12-02 23:51:34 +01:00
} else {
2022-09-28 00:38:36 +02:00
this . keyboardHandlerInitQueue . push ( videoData ) ;
2018-12-02 23:51:34 +01:00
}
}
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' ) {
2023-01-07 03:06:37 +01:00
const videoQs = this . siteSettings . getCustomDOMQuerySelector ( 'video' ) ;
2024-12-30 23:02:55 +01:00
let videos : HTMLVideoElement [ ] = [ ] ;
2019-06-10 23:45:15 +02:00
2024-12-30 23:02:55 +01:00
if ( videoQs ) {
videos = Array . from ( document . querySelectorAll ( videoQs ) as NodeListOf < HTMLVideoElement > ? ? [ ] ) ;
2025-06-17 20:12:28 +02:00
} else {
2024-12-30 23:02:55 +01:00
videos = Array . from ( document . getElementsByTagName ( 'video' ) ? ? [ ] ) ;
2019-06-10 23:45:15 +02:00
}
2024-12-30 23:02:55 +01:00
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 } ;
2024-12-30 23:02:55 +01:00
// 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 ]
2024-12-30 23:02:55 +01:00
) ;
2025-06-17 20:12:28 +02:00
}
2024-12-30 23:02:55 +01: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' ) ;
2019-06-10 23:45:15 +02:00
}
2019-07-03 21:55:08 +02:00
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 ) ) ;
2024-12-30 23:02:55 +01:00
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 ) ;
2018-11-07 00:03:06 +01:00
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' ) ;
2018-05-13 13:49:25 +02:00
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 ;
2018-05-21 22:43:56 +02:00
}
2018-05-13 13:49:25 +02:00
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 ;
2019-07-03 21:55:08 +02:00
}
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 ;
2018-05-20 23:17:09 +02:00
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.
2018-05-20 23:17:09 +02:00
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 ;
2018-05-21 22:43:56 +02:00
}
2018-05-20 23:17:09 +02:00
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" )
2018-05-22 00:19:50 +02:00
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" )
2018-05-21 22:43:56 +02:00
}
2018-05-16 23:26:47 +02:00
2019-09-03 23:49:22 +02:00
this . removeDestroyed ( ) ;
2025-06-17 20:12:28 +02:00
this . emitVideoStatus ( videosDetected ) ;
2019-05-07 23:40:13 +02:00
} catch ( e ) {
// if we encounter a fuckup, we can assume that no videos were found on the page. We destroy all videoData
2020-05-16 20:52:37 +02:00
// objects to prevent multiple initialization (which happened, but I don't know why). No biggie if we destroyed
2019-05-07 23:40:13 +02:00
// 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 ) ;
2019-05-07 23:40:13 +02:00
for ( const v of this . videos ) {
2021-09-14 23:21:53 +02:00
v . videoData . destroy ( ) ;
2019-05-07 23:40:13 +02:00
}
2021-09-14 23:21:53 +02:00
this . videos = [ ] ;
2019-05-07 23:40:13 +02:00
return ;
2018-05-13 13:49:25 +02:00
}
2018-05-21 22:43:56 +02:00
if ( rescanReason == RescanReason . PERIODIC ) {
this . scheduleRescan ( RescanReason . PERIODIC ) ;
}
2018-05-16 23:26:47 +02:00
}
2018-05-21 22:43:56 +02:00
removeDestroyed ( ) {
2021-09-14 23:21:53 +02:00
this . videos = this . videos . filter ( vid = > vid . videoData . destroyed === false ) ;
2018-05-21 22:43:56 +02:00
}
scheduleRescan ( rescanReason ) {
if ( rescanReason != RescanReason . PERIODIC ) {
this . rescan ( rescanReason ) ;
return ;
}
2018-05-16 23:26:47 +02:00
try {
2018-05-21 22:43:56 +02:00
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 ) {
2018-05-21 22:43:56 +02:00
ths . rescanTimer = null ;
2019-06-14 21:53:48 +02:00
ths . rescan ( rescanReason ) ;
2018-05-21 22:43:56 +02:00
ths = null ;
2019-06-14 21:53:48 +02:00
} , this . settings . active . pageInfo . timeouts . rescan , RescanReason . PERIODIC )
2018-05-21 22:43:56 +02:00
} catch ( e ) {
2025-05-01 00:55:22 +02:00
this . logger . error ( 'scheduleRescan' , "scheduling rescan failed. Here's why:" , e )
2018-05-21 22:43:56 +02:00
}
}
scheduleUrlCheck() {
try {
if ( this . urlCheckTimer ) {
clearTimeout ( this . urlCheckTimer ) ;
2018-05-16 23:26:47 +02:00
}
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 ;
2018-05-21 22:43:56 +02:00
ths . ghettoUrlCheck ( ) ;
2018-05-16 23:26:47 +02:00
ths = null ;
2018-08-30 00:56:15 +02:00
} , 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 )
2018-05-21 22:43:56 +02:00
}
}
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
2018-05-21 22:43:56 +02:00
this . rescan ( RescanReason . URL_CHANGE ) ;
this . lastUrl = window . location . href ;
}
2018-07-12 23:27:10 +02:00
this . scheduleUrlCheck ( ) ;
2018-05-13 13:49:25 +02:00
}
2023-01-07 03:06:37 +01:00
/ * *
* 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
* /
2019-10-26 02:38:47 +02:00
updateCurrentCrop ( ar ) {
2019-10-24 23:27:43 +02:00
this . currentCrop = ar ;
2021-10-25 23:11:34 +02:00
2023-01-07 03:06:37 +01:00
this . siteSettings . updatePersistentOption ( 'crop' , ar ) ;
2019-10-24 00:45:11 +02:00
}
2023-03-29 22:07:50 +02:00
2018-05-13 21:05:11 +02:00
}
2018-05-21 22:43:56 +02:00
2020-12-03 01:16:57 +01:00
if ( process . env . CHANNEL !== 'stable' ) {
console . info ( "PageInfo loaded!" ) ;
}
2018-12-31 01:03:07 +01:00
export default PageInfo ;