Improve embedded frame detection

This commit is contained in:
Tamius Han 2025-06-17 20:12:28 +02:00
parent f7f046ea53
commit 178bb65b4a
6 changed files with 153 additions and 56 deletions

View File

@ -86,6 +86,7 @@
:settings="settings"
:eventBus="eventBus"
:siteSettings="siteSettings"
:frames="activeFrames"
></PopupVideoSettings>
<BaseExtensionSettings
v-if="selectedTab === 'extensionSettings'"
@ -171,6 +172,7 @@ export default {
},
mounted() {
this.tabs.find(x => x.id === 'changelog').highlight = !this.settings.active?.whatsNewChecked;
this.requestSite();
},
async created() {
try {

View File

@ -18,7 +18,7 @@
:class="{'active': tab === 'embeddedSites'}"
@click="setTab(tab = 'embeddedSites')"
>
Embedded content
Embedded content ({{frames?.length}} {{frames?.length === 1 ? 'site' : 'sites'}})
</div>
<div
class="tab"

View File

@ -27,7 +27,9 @@
<b>NOTE:</b> Sites not on this list use default extension settings.
</div>
</div>
<b>Other sites:</b>
<div class="w-full text-center" style="margin-bottom: -1.25rem">
<b>Other sites</b>
</div>
<div style="margin: 1rem 0rem" class="w-full">
<div class="flex flex-row items-baseline">
<div style="margin-right: 1rem">Search for site:</div>

View File

@ -5,7 +5,7 @@
Extension is disabled for a given site when it's disabled in full screen, since
current settings do not allow the extension to only be disabled while in full screen
-->
<template v-if="siteSettings.isEnabledForEnvironment(false, true) === ExtensionMode.Disabled">
<template v-if="siteSettings.isEnabledForEnvironment(false, true) === ExtensionMode.Disabled && !enabledFrames?.length">
<div class="h-full flex flex-col items-center justify-center">
<div class="info">
Extension is not enabled for this site.
@ -16,6 +16,17 @@
</div>
</template>
<template v-else>
<div
v-if="siteSettings.isEnabledForEnvironment(false, true) === ExtensionMode.Disabled"
class="warning-compact"
>
<b>Extension is disabled for this site.</b><br />
<small>Controls will only work on content embedded from the following sites:</small><br/>
<div class="w-full flex flex-row justify-center">
<span v-for="frameSite of enabledFrames" :key="frameSite.host" class="website-name">{{frameSite.host}}</span>
</div>
</div>
<div class="flex flex-row">
<mdicon name="crop" :size="16" />&nbsp;&nbsp;
<span>CROP</span>
@ -90,6 +101,7 @@ import StretchOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoS
import ZoomOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoSettings/ZoomOptionsPanel.vue';
import ExtensionMode from '@src/common/enums/ExtensionMode.enum.ts';
import AlignmentOptionsControlComponent from '@csui/src/PlayerUiPanels/AlignmentOptionsControlComponent.vue';
import { SiteSettings } from '../../../../ext/lib/settings/SiteSettings';
export default {
components: {
@ -102,16 +114,24 @@ export default {
],
props: [
'site',
'settings',
'siteSettings',
'eventBus',
'frames'
],
data() {
return {
exec: null,
ExtensionMode: ExtensionMode,
enabledFrames: [],
};
},
watch: {
frames(val) {
this.filterActiveSites(val);
}
},
created() {
this.eventBus.subscribe(
'uw-config-broadcast',
@ -128,8 +148,34 @@ export default {
this.eventBus.unsubscribeAll(this);
},
methods: {
filterActiveSites(val) {
this.enabledFrames = [];
for (const site of val) {
const siteSettings = new SiteSettings(this.settings, site.host);
if (siteSettings.isEnabledForEnvironment(false, true) === ExtensionMode.Enabled) {
this.enabledFrames.push(site);
}
}
}
}
}
</script>
<style lang="scss" scoped>
.warning-compact {
background-color: #d6ba4a;
color: #000;
padding: 0.5rem 1rem;
margin-top: -0.5rem;
margin-bottom: 0.5rem;
.website-name {
font-size: 0.85rem;
&:not(:last-of-type)::after {
content: ','
}
}
}
</style>

View File

@ -69,6 +69,7 @@ class PageInfo {
keyboardHandler: any;
fsStatus = {fullscreen: true}; // fsStatus needs to be passed to VideoData, so fullScreen property is shared between videoData instances
isIframe: boolean = false;
//#endregion
fsEventListener = {
@ -78,7 +79,9 @@ class PageInfo {
}
};
constructor(eventBus: EventBus, siteSettings: SiteSettings, settings: Settings, logAggregator: LogAggregator, readOnly = false){
constructor(eventBus: EventBus, siteSettings: SiteSettings, settings: Settings, logAggregator: LogAggregator, readOnly = false) {
this.isIframe = window.self !== window.top;
this.logAggregator = logAggregator;
this.logger = new ComponentLogger(logAggregator, 'PageInfo', {});
this.settings = settings;
@ -112,10 +115,11 @@ class PageInfo {
this.eventBus.subscribeMulti({
'probe-video': {
function: () => {
console.warn('[uw] probe-video event received..');
console.log(`[${window.location}] probe-video received.`)
this.rescan();
}
}
})
});
}
destroy() {
@ -186,106 +190,62 @@ class PageInfo {
}
}
getVideos(): HTMLVideoElement[] {
/**
* 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> ?? []);
} else{
} else {
videos = Array.from(document.getElementsByTagName('video') ?? []);
}
// filter out videos that aren't big enough
videos = videos.filter(
(v: HTMLVideoElement) => v.clientHeight > 720 && v.clientWidth > 1208
);
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
return videos.filter(
(v: HTMLVideoElement) => v.clientHeight >= minY[minSize] && v.clientWidth >= minX[minSize]
);
}
/**
* 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;
}
/**
* 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.
* @returns
*/
rescan(rescanReason?: RescanReason){
// is there any video data objects that had their HTML elements removed but not yet
// destroyed? We clean that up here.
const orphans = this.videos.filter(x => !document.body.contains(x.element));
for (const orphan of orphans) {
orphan.videoData.destroy();
}
// remove all destroyed videos.
this.videos = this.videos.filter(x => !x.videoData.destroyed);
// add new videos
try{
let vids = this.getVideos();
if(!vids || vids.length == 0){
this.hasVideos = false;
if(rescanReason == RescanReason.PERIODIC){
this.logger.info({src: 'rescan', origin: 'videoRescan'}, "Scheduling normal rescan.")
this.scheduleRescan(RescanReason.PERIODIC);
}
return;
}
// add new videos
this.hasVideos = false;
let videoExists = false;
for (const videoElement of vids) {
// do not re-add videos that we already track:
if (this.videos.find(x => x.element.isEqualNode(videoElement))) {
continue;
}
// 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
if(!videoElement.offsetWidth || !videoElement.offsetHeight) {
continue;
}
// at this point, we're certain that we found new videos. Let's update some properties:
this.hasVideos = true;
// if PageInfo is marked as "readOnly", we actually aren't adding any videos to anything because
// that's super haram. We're only interested in whether
if (this.readOnly) {
// in lite mode, we're done. This is all the info we want, but we want to actually start doing
// things that interfere with the website. We still want to be running a rescan, tho.
if(rescanReason == RescanReason.PERIODIC){
this.scheduleRescan(RescanReason.PERIODIC);
}
return;
}
this.logger.info({src: 'rescan', origin: 'videoRescan'}, "found new video candidate:", videoElement, "NOTE:: Video initialization starts here:\n--------------------------------\n")
try {
const newVideo = new VideoData(videoElement, this.settings, this.siteSettings, this);
this.videos.push({videoData: newVideo, element: videoElement});
} catch (e) {
this.logger.error('rescan', "rescan error: failed to initialize videoData. Skipping this video.",e);
}
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")
}
this.removeDestroyed();
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) {
@ -303,19 +263,105 @@ class PageInfo {
// things in the less-than-optimal. more-than-retarded way.
//
// no but honestly fuck Chrome.
// if (this.videos.length != oldVideoCount) {
// }
if (this.videos.length > 0) {
// this.comms.registerVideo({host: window.location.hostname, location: window.location});
if (videosDetected || this.hasVideo()) {
this.eventBus.send('has-video', null);
} else {
// this.comms.unregisterVideo({host: window.location.hostname, location: window.location});
this.eventBus.send('noVideo', null);
}
}
}
/**
* 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.
* @returns
*/
rescan(rescanReason?: RescanReason){
let videosDetected = false;
// is there any video data objects that had their HTML elements removed but not yet
// destroyed? We clean that up here.
const orphans = this.videos.filter(x => !document.body.contains(x.element));
for (const orphan of orphans) {
orphan.videoData.destroy();
}
// remove all destroyed videos.
this.videos = this.videos.filter(x => !x.videoData.destroyed);
// add new videos
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');
if(!vids || vids.length == 0){
this.hasVideos = false;
if(rescanReason == RescanReason.PERIODIC){
this.logger.info({src: 'rescan', origin: 'videoRescan'}, "Scheduling normal rescan.")
this.scheduleRescan(RescanReason.PERIODIC);
}
this.emitVideoStatus(videosDetected);
return;
}
// add new videos
this.hasVideos = false;
for (const videoElement of vids) {
// do not re-add videos that we already track:
if (this.videos.find(x => x.element.isEqualNode(videoElement))) {
continue;
}
// 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
if(!videoElement.offsetWidth || !videoElement.offsetHeight) {
continue;
}
// at this point, we're certain that we found new videos. Let's update some properties:
this.hasVideos = true;
videosDetected ||= true;
// if PageInfo is marked as "readOnly", we actually aren't adding any videos to anything because
// that's super haram. We're only interested in whether
if (this.readOnly) {
// in lite mode, we're done. This is all the info we want, but we want to actually start doing
// things that interfere with the website. We still want to be running a rescan, tho.
if(rescanReason == RescanReason.PERIODIC){
this.scheduleRescan(RescanReason.PERIODIC);
}
this.emitVideoStatus(videosDetected);
return;
}
this.logger.info({src: 'rescan', origin: 'videoRescan'}, "found new video candidate:", videoElement, "NOTE:: Video initialization starts here:\n--------------------------------\n")
try {
const newVideo = new VideoData(videoElement, this.settings, this.siteSettings, this);
this.videos.push({videoData: newVideo, element: videoElement});
} catch (e) {
this.logger.error('rescan', "rescan error: failed to initialize videoData. Skipping this video.",e);
}
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")
}
this.removeDestroyed();
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

View File

@ -5,17 +5,18 @@
import UWContent from './UWContent';
if(process.env.CHANNEL !== 'stable'){
console.warn("\n\n\n\n\n\n ——— Sᴛλʀᴛɪɴɢ Uʟᴛʀᴀɪɪʏ ———\n << ʟᴏᴀᴅɪɴɢ ᴍᴀɪɴ ꜰɪʟᴇ >>\n\n\n\n");
let isIframe;
try {
if(window.self !== window.top){
console.info("%cWe aren't in an iframe.", "color: #afc, background: #174");
}
else{
console.info("%cWe are in an iframe!", "color: #fea, background: #d31", window.self, window.top);
}
isIframe = window.self !== window.top;
} catch (e) {
console.info("%cWe are in an iframe!", "color: #fea, background: #d31");
isIframe = true;
}
console.warn(
"\n\n\n\n\n\n ——— Sᴛλʀᴛɪɴɢ Uʟᴛʀᴀɪɪʏ ———\n << ʟᴏᴀᴅɪɴɢ ᴍᴀɪɴ ꜰɪʟᴇ >>\n\n\n\n",
"\n - are we in iframe?", isIframe
);
}
const main = new UWContent();