Merge branch 'master' into stable

This commit is contained in:
Tamius Han 2021-06-13 02:19:55 +02:00
commit 79fa726b19
89 changed files with 29213 additions and 3137 deletions

View File

@ -6,9 +6,9 @@
["@babel/preset-env", {
"useBuiltIns": false,
"targets": {
"esmodules": true,
},
}],
"esmodules": true
}
}]
]
}
// {

8
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"recommendations": [
"hollowtree.vue-pack",
"msjsdiag.debugger-for-chrome",
"firefox-devtools.vscode-firefox-debug",
"msjsdiag.debugger-for-edge"
]
}

View File

@ -17,6 +17,8 @@
"decycle",
"disneyplus",
"equalish",
"fith",
"fitw",
"fuckup",
"gfycat",
"gmail",
@ -28,6 +30,7 @@
"letterboxed",
"manjaro",
"minification",
"mitigations",
"nogrow",
"noshrink",
"outro",

View File

@ -8,14 +8,59 @@
* Settings page looks ugly af right now. Maybe fix it some time later
* other bug fixes
## v5.0 (planned major)
## v7.0 (planned major)
* WebGL autodetection
## v6.0 (next major)
* in-player GUI
* Fix UI logger
## v5.x (current major)
### v5.0.4
* Attempt to fix disney+ again, courtesy of [@jwannebo](https://github.com/tamius-han/ultrawidify/issues/84#issuecomment-846334005) on github.
### v5.0.3
* Fixed the issue where the videos were sometimes offset up and left. Again.
* Fix the issue where correcting source stretch was squished incorrectly ([#153](https://github.com/tamius-han/ultrawidify/issues/153))
### v5.0.2
* When in full screen, the extension will assume player element dimensions are the same as the screen resolution. This should help with sites where ultrawidify doesn't correctly identify the player, as cropping generally doesn't work if player element is not identified. Old behaviour can be restored in advanced extension settings by toggling the "use player aspect ratio in fullscreen" checkbox under 'player detection settings'.
* Extension should now respect 'disable extension' option for real.
* Fixed the issue where player wouldn't get detected if video was wider than the player.
### v5.0.1
* Added an option for users to turn off (and/or configure) Chrome/Edge's zoom limiter.
### v5.0.0
There's been some big-ish changes under the hood:
* Migrate main scripts to typescript (vue is currently not included).
* webextension-polyfill is now used everywhere (if only because typescript throws a hissy fit with `browser` and `chrome` otherwise) ([#114](https://github.com/tamius-han/ultrawidify/issues/114))
* Fix some bugs that I didn't even know I had, but typescript kinda shone some light on them
* Manual zoom (Z/U unless sites override the two) should now work again (without automatic AR constantly overriding it). Same goes for panning. ([#135](https://github.com/tamius-han/ultrawidify/issues/135) & [#138](https://github.com/tamius-han/ultrawidify/issues/138))
* Fix issue when video would be scaled incorrectly if video element uses `height:auto`.
* **[5.0.0.1]** Fixed the issue where settings were reset on page load.
* **[5.0.0.1]** Fixed the issue where settings page wouldn't load.
## v4.x (current major)
### v4.5.3
* Provides workaround for the fullscreen stretching bug Chrome 88 (or a recent Windows 10 update) introduced for nVidia users using hardware acceleration on Windows 10. In order to mitigate this bug, Ultrawidify needs to keep a 5-10 px wide black border while watching videos in full screen. This bug is also present in Edge.
* **[4.5.3.1]** Fixed letterbox misalignment binding in settings (#134)
* **[4.5.3.2]** Fixed false 'autodetection not supported' notifications.
### v4.5.2
* Fixed the issue where videos would sometimes get misaligned while using hybrid stretch, except for real this time. ([#125](https://github.com/tamius-han/ultrawidify/issues/125))
* Improved DRM detection (the 'autodetection cannot work on this site' popup should now no longer show up on the sites where autodetection _can_ work)
### v4.5.1
* Fixed the misalignment issue on netflix ... hopefully.
@ -28,6 +73,7 @@
* **[4.5.1.3]** Added fix for disney plus
* **[4.5.1.3]** Microsoft Edge has fixed the bugs that prevented the extension from working properly. Popup should no longer be shown.
### v4.5.0 (Current)
* Under the hood: migrated from vue2 to vue3, because optional chaining in templates is too OP.

View File

@ -1,10 +1,8 @@
# Ultrawidify — aspect ratio fixer for youtube and netflix
# Ultrawidify — aspect ratio fixer for youtube and netflix
## Super TL;DR: I'm just looking for the install links, thanks
[Firefox](https://addons.mozilla.org/en/firefox/addon/ultrawidify/), [Chrome](https://chrome.google.com/webstore/detail/ultrawidify/dndehlekllfkaijdlokmmicgnlanfjbi).
**Microsoft Edge is not supported at this time, as Edge features some bugs that make it impossible for extension to work.** [Read more](https://github.com/tamius-han/ultrawidify/issues/117#issuecomment-747109695).
[Firefox](https://addons.mozilla.org/en/firefox/addon/ultrawidify/), [Chrome](https://chrome.google.com/webstore/detail/ultrawidify/dndehlekllfkaijdlokmmicgnlanfjbi), [Edge](https://microsoftedge.microsoft.com/addons/detail/ultrawidify/lmpgpgechmkkkehkihpiddbcbgibokbi).
There's also [nightly "builds"](https://stuff.lionsarch.tamius.net/ultrawidify/nightly/).
@ -51,13 +49,13 @@ If extension doesn't work for a site I'm not testing on out of the box, follow [
### Installing this extension
You can download this extension from Firefox' and Chrome's extension stores:
You can download this extension from the relevant extension stores:
* [Firefox](https://addons.mozilla.org/en/firefox/addon/ultrawidify/)
* [Chrome, Opera](https://chrome.google.com/webstore/detail/ultrawidify/dndehlekllfkaijdlokmmicgnlanfjbi)
* [Chromium Edge](https://microsoftedge.microsoft.com/addons/detail/lmpgpgechmkkkehkihpiddbcbgibokbi)
* [Chrome](https://chrome.google.com/webstore/detail/ultrawidify/dndehlekllfkaijdlokmmicgnlanfjbi)
* [Edge](https://microsoftedge.microsoft.com/addons/detail/ultrawidify/lmpgpgechmkkkehkihpiddbcbgibokbi)
Users of beta and developer branches of Opera can find Ultrawidify on Opera store as well, but given Opera's review process is unacceptably slow (awaiting moderator review since 2020-03-15) not only am I not going to maintain Opera store presence, Opera users are advised to use Chrome version of the extension in order to avoid waiting years for new features to be approved by Opera moderators.
Other browsers are not officially supported. If you're using a different Chromium-based browser, you can try installing the addon from the Chrome Web Store — but if things don't work, you're on your own.
### Nightly builds

26492
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
{
"name": "ultrawidify",
"version": "4.5.1",
"version": "5.0.4",
"description": "Aspect ratio fixer for youtube and other sites, with automatic aspect ratio detection. Supports ultrawide and other ratios.",
"author": "Tamius Han <tamius.han@gmail.com>",
"scripts": {
"build": "npm run pre-build; cross-env NODE_ENV=production BROWSER=firefox CHANNEL=stable webpack --hide-modules",
"build-all": "mkdir -p ./build/old; npm run pre-build; rm ./dist-zip/uw-amo-source.zip; mv -f ./dist-zip/*.zip ./build/old; npm run build; node scripts/build-zip.js ff; npm run build-chrome; node scripts/build-zip.js chrome; npm run build-edge; node scripts/build-zip.js edge; ./scripts/prepare-amo-source.sh",
"build-all": "bash ./scripts/build-all.sh",
"build-chrome": "cross-env NODE_ENV=production BROWSER=chrome CHANNEL=stable webpack --hide-modules",
"build-chrome:dev": "cross-env NODE_ENV=development BROWSER=chrome webpack --hide-modules",
"build-edge": "cross-env NODE_ENV=production BROWSER=edge CHANNEL=stable webpack --hide-modules",
@ -15,15 +15,19 @@
"build-testing-chrome": "cross-env NODE_ENV=development BROWSER=chrome CHANNEL=testing webpack --hide-modules",
"build-zip": "node scripts/build-zip.js",
"build:dev": "webpack --hide-modules",
"dev": "cross-env NODE_ENV=development CHANNEL=dev concurrently \"cross-env BROWSER=firefox npm run build:dev -- --watch\" \"cross-env BROWSER=chrome npm run build:dev -- --watch\"",
"dev": "cross-env NODE_ENV=development CHANNEL=dev concurrently \"cross-env BROWSER=firefox npm run build:dev -- --watch\" \"cross-env BROWSER=chrome npm run build:dev -- --watch\" \"cross-env BROWSER=edge npm run build:dev -- --watch\"",
"pre-build": "rm -rf ./dist-ff; rm -rf ./dist-chrome; rm -rf ./node_modules; npm ci",
"start": "npm run dev"
},
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@types/chrome": "0.0.129",
"@types/core-js": "^2.5.3",
"@types/es6-promise": "^3.3.0",
"@types/firefox": "0.0.30",
"@types/resize-observer-browser": "^0.1.5",
"@vue/cli": "^4.5.9",
"@vue/cli-plugin-typescript": "^4.5.11",
"bootstrap": "^4.5.3",
"bootstrap-icons": "^1.1.0",
"bootstrap-icons-vue": "^0.3.0",
@ -31,17 +35,23 @@
"concurrently": "^5.2.0",
"fs-extra": "^7.0.1",
"json-cyclic": "0.0.3",
"lodash": "^4.17.21",
"typescript": "^4.2.3",
"vue": "^3.0.0-beta.1",
"vuex": "^4.0.0-alpha.1",
"vuex-webextensions": "^1.3.0"
"vuex-webextensions": "^1.3.0",
"webextension-polyfill-ts": "^0.24.0"
},
"devDependencies": {
"@babel/core": "^7.10.5",
"@babel/core": "^7.12.13",
"@babel/plugin-proposal-optional-chaining": "^7.10.4",
"@babel/preset-env": "^7.10.4",
"@babel/preset-env": "^7.12.13",
"@types/lodash": "^4.14.168",
"@types/node": "^14.14.25",
"@vue/compiler-sfc": "^3.0.3",
"archiver": "^3.0.0",
"babel-loader": "^8.1.0",
"babel-loader": "^8.2.2",
"babel-preset-es2020": "^1.0.2",
"copy-webpack-plugin": "^4.5.3",
"cross-env": "^5.2.0",
"css-loader": "^0.28.11",
@ -50,9 +60,10 @@
"mini-css-extract-plugin": "^0.4.4",
"node-sass": "^4.14.1",
"sass-loader": "^7.1.0",
"ts-loader": "^8.0.16",
"vue-cli-plugin-vue-next": "~0.1.4",
"vue-loader": "^16.0.0",
"web-ext-types": "^2.1.0",
"web-ext-types": "^2.3.0",
"webextension-polyfill": "^0.6.0",
"webpack": "^4.44.0",
"webpack-chrome-extension-reloader": "^0.8.3",

42
scripts/build-all.sh Normal file
View File

@ -0,0 +1,42 @@
#!/bin/bash
# NOTE: this script needs to be run with the npm run build-all
# command from the root directory of the project. Running it in
# any other way probably isn't going to work.
# pre-build steps:
mkdir -p ./build/old
npm run pre-build
rm ./dist-zip/uw-amo-source.zip
mv -f ./dist-zip/*.zip ./build/old
# lets force raise ram limit, but the improper way
# export NODE_OPTIONS=--max_old_space_size=4096
# build the version for each browser and create a zip afterwards
# step 1: define build functions
#function buildFF {
npm run build
node scripts/build-zip.js ff
#}
#function buildChrome {
npm run build-chrome
node scripts/build-zip.js chrome
#}
#function buildEdge {
npm run build-edge
node scripts/build-zip.js edge
#}
# step 2: execute them all at once
# buildFF &
# buildChrome &
# buildEdge &
# wait < <(jobs -p)
# prepare AMO source
# source code needs to be prepared AFTER
# the code has been built, to ensure that
# package-lock.json remains unchanged
./scripts/prepare-amo-source.sh

View File

@ -37,6 +37,9 @@ if [ ! -z "$GIT_COMMIT" ] ; then
fi
fi
# let's raise RAM limit for npm command globally
NODE_OPTIONS=--max_old_space_size=4096
npm ci
rm -rf ./dist-zip || true # no big deal if ./dist-zip doesn't exist
@ -46,7 +49,7 @@ mkdir dist-zip # create it back
# build firefox
#
npm run "${BUILD_SCRIPT}"
node scripts/build-zip.js ff nightly
node --max-old-space-size=2048 scripts/build-zip.js ff nightly
# if [ ! -z "${AMO_API_KEY}" ] ; then
# if [ ! -z "${AMO_API_SECRET}" ] ; then
# web-ext sign --source-dir ./dist --api-key "${AMO_API_KEY}" --api-secret "${AMO_API_SECRET}"
@ -57,12 +60,18 @@ node scripts/build-zip.js ff nightly
# build chrome
#
npm run "${BUILD_SCRIPT}-chrome"
node scripts/build-zip.js chrome nightly
node --max-old-space-size=2048 scripts/build-zip.js chrome nightly
#
#./scripts/build-crx.sh
#
#
# build edge
#
npm run "${BUILD_SCRIPT}-edge"
node --max-old-space-size=2048 scripts/build-zip.js chrome nightly
######################################
# UPLOAD TO WEB SERVER
######################################
@ -76,7 +85,6 @@ echo "Uploading to server ..."
scp -i ~/.ssh/id_rsa -r ./dist-zip/* "ultrawidify-uploader@${RELEASE_SERVER}:${RELEASE_DIRECTORY}${BUILD_CHANNEL_DIRECTORY}"
######################################
# Build finished message
######################################

View File

@ -5,7 +5,7 @@
</div>
<div class="flex action-name">
<span v-if="action.cmd && action.cmd.length > 1 || action.cmd[0].action === 'set-ar' && action.userAdded || (action.cmd[0].arg === AspectRatio.Fixed)" class="icon red" @click="removeAction()">🗙</span>
<span v-if="action.cmd && action.cmd.length > 1 || action.cmd[0].action === 'set-ar' && action.userAdded || (action.cmd[0].arg === AspectRatioType.Fixed)" class="icon red" @click="removeAction()">🗙</span>
<span v-else class="icon transparent">🗙</span> &nbsp; &nbsp;
<span class="icon" @click="editAction()">🖉</span> &nbsp; &nbsp;
{{action.name}}
@ -108,16 +108,16 @@
</template>
<script>
import Stretch from '../enums/stretch.enum';
import AspectRatio from '../enums/aspect-ratio.enum';
import StretchType from '../enums/StretchType.enum';
import AspectRatioType from '../enums/AspectRatioType.enum';
import KeyboardShortcutParser from '../js/KeyboardShortcutParser';
export default {
data () {
return {
Stretch: Stretch,
AspectRatio: AspectRatio,
StretchType: StretchType,
AspectRatioType: AspectRatioType,
}
},
created () {

View File

@ -46,13 +46,13 @@
</template>
<script>
import Stretch from '../enums/stretch.enum';
import StretchType from '../enums/StretchType.enum';
import KeyboardShortcutParser from '../js/KeyboardShortcutParser'
export default {
data () {
return {
Stretch: Stretch
StretchType: StretchType
}
},
created () {

View File

@ -6,7 +6,7 @@ let Notifications = Object.freeze({
},
'AARD_DRM': {
icon: 'exclamation-triangle',
text: '<b>Autodetection cannot run on this video.</b> This usually happens when sites use DRM. You will have to set aspect ratio manually.',
text: '<b>Autodetection may not be able to run on this video.</b> On sites that utilize DRM you will have to set aspect ratio manually.',
timeout: 5000,
}
});

View File

@ -0,0 +1,7 @@
enum AntiGradientMode {
Disabled = 0,
Lax = 1,
Strict = 2
}
export default AntiGradientMode;

View File

@ -0,0 +1,11 @@
enum AspectRatioType {
Initial = -1, // page default
Reset = 0, // reset to initial
Automatic = 1, // set by Aard
FitWidth = 2, // legacy/dynamic = fit to width
FitHeight = 3, // legacy/dynamic = fit to height
Fixed = 4, // pre-determined aspect ratio
Manual = 5, // ratio achieved by zooming in/zooming out
}
export default AspectRatioType;

View File

@ -0,0 +1,9 @@
enum CropModePersistence {
Default = -1,
Disabled = 0,
UntilPageReload = 1,
CurrentSession = 2,
Forever = 3,
}
export default CropModePersistence;

View File

@ -0,0 +1,10 @@
enum ExtensionMode {
AutoDisabled = -2,
Disabled = -1,
Default = 0,
Whitelist = 1,
Basic = 2,
Enabled = 3,
};
export default ExtensionMode;

View File

@ -0,0 +1,11 @@
enum StretchType {
NoStretch = 0,
Basic = 1,
Hybrid = 2,
Conditional = 3,
Fixed = 4,
FixedSource = 5,
Default = -1
};
export default StretchType;

View File

@ -0,0 +1,8 @@
enum VideoAlignmentType {
Left = 0,
Center = 1,
Right = 2,
Default = -1
};
export default VideoAlignmentType;

View File

@ -1,7 +0,0 @@
var AntiGradientMode = Object.freeze({
Disabled: 0,
Lax: 1,
Strict: 2
});
export default AntiGradientMode;

View File

@ -1,11 +0,0 @@
var AspectRatio = Object.freeze({
Initial: -1, // page default
Reset: 0, // reset to initial
Automatic: 1, // set by Aard
FitWidth: 2, // legacy/dynamic: fit to width
FitHeight: 3, // legacy/dynamic: fit to height
Fixed: 4, // pre-determined aspect ratio
Manual: 5, // ratio achieved by zooming in/zooming out
});
export default AspectRatio;

View File

@ -1,9 +0,0 @@
var CropModePersistence = Object.freeze({
Default: -1,
Disabled: 0,
UntilPageReload: 1,
CurrentSession: 2,
Forever: 3,
});
export default CropModePersistence;

View File

@ -1,19 +0,0 @@
if (process.env.CHANNEL !== 'stable') {
console.info('Loading ExtensionMode');
}
var ExtensionMode = Object.freeze({
AutoDisabled: -2,
Disabled: -1,
Default: 0,
Whitelist: 1,
Basic: 2,
Enabled: 3,
});
if (process.env.CHANNEL !== 'stable') {
console.info('Loaded ExtensionMode');
}
export default ExtensionMode;

View File

@ -1,11 +0,0 @@
var Stretch = Object.freeze({
NoStretch: 0,
Basic: 1,
Hybrid: 2,
Conditional: 3,
Fixed: 4,
FixedSource: 5,
Default: -1
});
export default Stretch;

View File

@ -1,8 +0,0 @@
var VideoAlignment = Object.freeze({
Left: 0,
Center: 1,
Right: 2,
Default: -1
});
export default VideoAlignment;

View File

@ -0,0 +1,293 @@
import { Action } from '../../../node_modules/vuex/types/index'
import AntiGradientMode from '../enums/AntiGradientMode.enum'
import AspectRatioType from '../enums/AspectRatioType.enum'
import CropModePersistence from '../enums/CropModePersistence.enum'
import ExtensionMode from '../enums/ExtensionMode.enum'
import StretchType from '../enums/StretchType.enum'
import VideoAlignmentType from '../enums/VideoAlignmentType.enum'
interface ActionScopeInterface {
show: boolean,
label?: string, // example override, takes precedence over default label
shortcut?: {
key?: string,
code?: string,
ctrlKey?: boolean,
metaKey?: boolean,
altKey?: boolean,
shiftKey?: boolean,
onKeyUp?: boolean,
onKeyDown?: boolean,
onMouseMove?: boolean,
}[],
}
interface SettingsInterface {
arDetect: {
disabledReason: string, // if automatic aspect ratio has been disabled, show reason
allowedMisaligned: number, // top and bottom letterbox thickness can differ by this much.
// Any more and we don't adjust ar.
allowedArVariance: number, // amount by which old ar can differ from the new (1 = 100%)
timers: { // autodetection frequency
playing: number, // while playing
paused: number, // while paused
error: number, // after error
minimumTimeout: number,
tickrate: number, // 1 tick every this many milliseconds
},
autoDisable: { // settings for automatically disabling the extension
maxExecutionTime: number, // if execution time of main autodetect loop exceeds this many milliseconds,
// we disable it.
consecutiveTimeoutCount: number, // we only do it if it happens this many consecutive times
// FOR FUTURE USE
consecutiveArResets: number // if aspect ratio reverts immediately after AR change is applied, we disable everything
},
canvasDimensions: {
blackframeCanvas: { // smaller than sample canvas, blackframe canvas is used to recon for black frames
// it's not used to detect aspect ratio by itself, so it can be tiny af
width: number,
height: number,
},
sampleCanvas: { // size of image sample for detecting aspect ratio. Bigger size means more accurate results,
// at the expense of performance
width: number,
height: number,
},
},
// samplingInterval: 10, // we sample at columns at (width/this) * [ 1 .. this - 1]
blackframe: {
sufficientColorVariance: number, // calculate difference between average intensity and pixel, for every pixel for every color
// component. Average intensity is normalized to where 0 is black and 1 is biggest value for
// that component. If sum of differences between normalized average intensity and normalized
// component varies more than this % between color components, we can afford to use less strict
// cumulative threshold.
cumulativeThresholdLax: number,
cumulativeThresholdStrict: number,// if we add values of all pixels together and get more than this, the frame is bright enough.
// (note: blackframe is 16x9 px -> 144px total. cumulative threshold can be reached fast)
blackPixelsCondition: number, // How much pixels must be black (1 all, 0 none) before we consider frame as black. Takes
// precedence over cumulative threshold: if blackPixelsCondition is met, the frame is dark
// regardless of whether cumulative threshold has been reached.
},
blackbar: {
blackLevel: number, // everything darker than 10/255 across all RGB components is considered black by
// default. blackLevel can decrease if we detect darker black.
threshold: number, // if pixel is darker than the sum of black level and this value, we count it as black
// on 0-255. Needs to be fairly high (8 might not cut it) due to compression
// artifacts in the video itself
frameThreshold: number, // threshold, but when doing blackframe test
imageThreshold: number, // in order to detect pixel as "not black", the pixel must be brighter than
// the sum of black level, threshold and this value.
gradientThreshold: number, // When trying to determine thickness of the black bars, we take 2 values: position of
// the last pixel that's darker than our threshold, and position of the first pixel that's
// brighter than our image threshold. If positions are more than this many pixels apart,
// we assume we aren't looking at letterbox and thus don't correct the aspect ratio.
gradientSampleSize: number, // How far do we look to find the gradient
maxGradient: number, // if two neighboring pixels in gradientSampleSize differ by more than this, then we aren't
// looking at a gradient
gradientNegativeTreshold: number,
gradientMaxSD: number, // reserved for future use
antiGradientMode: AntiGradientMode
},
variableBlackbarThresholdOptions: { // In case of poor bitrate videos, jpeg artifacts may cause us issues
// FOR FUTURE USE
enabled: boolean, // allow increasing blackbar threshold
disableArDetectOnMax: boolean, // disable autodetection when threshold goes over max blackbar threshold
maxBlackbarThreshold: number, // max threshold (don't increase past this)
thresholdStep: number, // when failing to set aspect ratio, increase threshold by this much
increaseAfterConsecutiveResets: number // increase if AR resets this many times in a row
},
sampling: {
staticCols: number, // we take a column at [0-n]/n-th parts along the width and sample it
randomCols: number, // we add this many randomly selected columns to the static columns
staticRows: number, // forms grid with staticSampleCols. Determined in the same way. For black frame checks
},
guardLine: { // all pixels on the guardline need to be black, or else we trigger AR recalculation
// (if AR fails to be recalculated, we reset AR)
enabled: boolean,
ignoreEdgeMargin: number, // we ignore anything that pokes over the black line this close to the edge
// (relative to width of the sample)
imageTestThreshold: number, // when testing for image, this much pixels must be over blackbarThreshold
edgeTolerancePx: number, // black edge violation is performed this far from reported 'last black pixel'
edgeTolerancePercent: null // unused. same as above, except use % of canvas height instead of pixels
},
fallbackMode: {
enabled: boolean,
safetyBorderPx: number, // determines the thickness of safety border in fallback mode
noTriggerZonePx: number // if we detect edge less than this many pixels thick, we don't correct.
},
arSwitchLimiter: { // to be implemented
switches: number, // we can switch this many times
period: number // per this period
},
edgeDetection: {
sampleWidth: number, // we take a sample this wide for edge detection
detectionThreshold: number, // sample needs to have this many non-black pixels to be a valid edge
confirmationThreshold: number, //
singleSideConfirmationThreshold: number, // we need this much edges (out of all samples, not just edges) in order
// to confirm an edge in case there's no edges on top or bottom (other
// than logo, of course)
logoThreshold: number, // if edge candidate sits with count greater than this*all_samples, it can't be logo
// or watermark.
edgeTolerancePx?: number, // we check for black edge violation this far from detection point
edgeTolerancePercent?: number, // we check for black edge detection this % of height from detection point. unused
middleIgnoredArea: number, // we ignore this % of canvas height towards edges while detecting aspect ratios
minColsForSearch: number, // if we hit the edge of blackbars for all but this many columns (%-wise), we don't
// continue with search. It's pointless, because black edge is higher/lower than we
// are now. (NOTE: keep this less than 1 in case we implement logo detection)
},
pillarTest: {
ignoreThinPillarsPx: number, // ignore pillars that are less than this many pixels thick.
allowMisaligned: number // left and right edge can vary this much (%)
},
textLineTest: {
nonTextPulse: number, // if a single continuous pulse has this many non-black pixels, we aren't dealing
// with text. This value is relative to canvas width (%)
pulsesToConfirm: number, // this is a threshold to confirm we're seeing text.
pulsesToConfirmIfHalfBlack: number, // this is the threshold to confirm we're seeing text if longest black pulse
// is over 50% of the canvas width
testRowOffset: number // we test this % of height from detected edge
}
},
zoom: {
minLogZoom: number,
maxLogZoom: number,
announceDebounce: number // we wait this long before announcing new zoom
},
miscSettings: {
mousePan: {
enabled: boolean
},
mousePanReverseMouse: boolean,
defaultAr?: any
},
stretch: {
conditionalDifferencePercent: number // black bars less than this wide will trigger stretch
// if mode is set to '1'. 1.0=100%
},
resizer: {
setStyleString: {
maxRetries: number,
retryTimeout: number
}
},
pageInfo: {
timeouts: {
urlCheck: number,
rescan: number
}
},
pan?: any,
version?: string,
preventReload?: boolean,
// -----------------------------------------
// ::: MITIGATIONS :::
// -----------------------------------------
// Settings for browser bug workarounds.
mitigations?: {
zoomLimit?: {
enabled?: boolean,
fullscreenOnly?: boolean,
limit?: number,
}
}
// -----------------------------------------
// ::: ACTIONS :::
// -----------------------------------------
// Nastavitve za ukaze. Zamenja stare nastavitve za bližnične tipke.
//
// Polje 'shortcut' je tabela, če se slučajno lotimo kdaj delati choordov.
actions: {
name?: string, // name displayed in settings
label?: string, // name displayed in ui (can be overridden in scope/playerUi)
cmd?: {
action: string,
arg: any,
customArg?: any,
persistent?: boolean, // optional, false by default. If true, change doesn't take effect immediately.
// Instead, this action saves stuff to settings
}[],
scopes?: {
global?: ActionScopeInterface,
site?: ActionScopeInterface,
page?: ActionScopeInterface
},
playerUi?: {
show: boolean,
path?: string,
},
userAdded?: boolean,
}[],
whatsNewChecked: boolean,
// -----------------------------------------
// ::: SITE CONFIGURATION :::
// -----------------------------------------
// Nastavitve za posamezno stran
// Config for a given page:
//
// <hostname> : {
// status: <option> // should extension work on this site?
// arStatus: <option> // should we do autodetection on this site?
//
// defaultAr?: <ratio> // automatically apply this aspect ratio on this side. Use extension defaults if undefined.
// stretch? <stretch mode> // automatically stretch video on this site in this manner
// videoAlignment? <left|center|right>
//
// type: <official|community|user> // 'official' — blessed by Tam.
// // 'community' — blessed by reddit.
// // 'user' — user-defined (not here)
// override: <true|false> // override user settings for this site on update
// }
//
// Veljavne vrednosti za možnosti
// Valid values for options:
//
// status, arStatus, statusEmbedded:
//
// * enabled — always allow, full
// * basic — allow, but only the basic version without playerData
// * default — allow if default is to allow, block if default is to block
// * disabled — never allow
//
sites: {
[x: string]: {
mode?: ExtensionMode,
autoar?: ExtensionMode,
autoarFallback?: ExtensionMode,
stretch?: StretchType,
videoAlignment?: VideoAlignmentType,
keyboardShortcutsEnabled?: ExtensionMode,
type?: string,
override?: boolean,
arPersistence?: boolean,
actions?: any;
cropModePersistence?: CropModePersistence;
DOM?: {
player?: {
manual?: boolean,
querySelectors?: string,
additionalCss?: string,
useRelativeAncestor?: boolean,
videoAncestor?: any,
playerNodeCss?: string,
periodicallyRefreshPlayerElement?: boolean
},
video?: {
manual?: boolean,
querySelectors?: string,
additionalCss?: string,
useRelativeAncestor?: boolean,
playerNodeCss?: string
}
},
css?: string;
usePlayerArInFullscreen?: boolean;
}
}
}
export default SettingsInterface;

3
src/common/js/utils.ts Normal file
View File

@ -0,0 +1,3 @@
export async function sleep(timeout) {
return new Promise<void>( (resolve, reject) => setTimeout(() => resolve(), timeout));
}

View File

@ -10,7 +10,7 @@ export default {
}) || [];
},
extensionActions: function(){
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-extension-mode') || [];
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-ExtensionMode') || [];
},
aardActions: function(){
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-autoar-mode') || [];

View File

@ -1,300 +1,177 @@
<template>
<div v-if="uiVisible" class="uw-hover uv-hover-trigger-region">
<div class="flex flex-column flex-center">
<div class="text panel">
<h1>Your browser is incompatible with this extension on this site</h1>
<!-- <p><sup>2020-12-22</sup></p> -->
<p>
In October 2020, Microsoft Edge received an update that breaks video playback on sites that utilize DRM in certain cases.
As a result of this bug, cropped videos will be displayed incorrectly. This issue cannot be fixed by any extension, and
<b>using this extension (and its alternatives) on this site may make things worse</b> even if you only set aspect ratio manually.
</p>
<p>
I have attempted all possible workarounds and none of them work.
</p>
<p>
I would like to ask a couple of favours:
</p>
<p>
<ul>
<li>
<b>Report this issue to Microsoft.</b>
I have already done so, however such issues get noticed quicker if more people report them.
<div class="vspc"></div>
I have prepared a description of the problem you can find it below (you may need to scroll).
</li>
<li>
<b>Please notify me when Microsoft fixes this issue.</b>
I use windows only sporadically and cannot check whether Edge developers have fixed the issue or not.
You can contact me by opening an issue on <a href="https://github.com/tamius-han/ultrawidify/issues" target="_blank">github</a> or
sending me <a href="mailto:tamius.han@gmail.com" target="_blank">an email</a>. You can also <a href="https://www.reddit.com/message/compose?to=xternal7" target="_blank">PM me on reddit</a>.
<div class="vspc"></div>
Please include the following text with your message:
<div class="vspc"></div>
<i>{{userAgent}}</i>
<div class="vspc"></div>
You can determine whether the issue is fixed by attempting to watch 21:9 movie on netflix and compare what you see to the screenshots I included at the bottom.
</li>
<li>
<small>
<b>Avoid leaving one-star reviews on Chrome Web Store.</b> I don't usually whine about one star
reviews, but Chrome Web Store is for Google Chrome users. As such, I cannot optimize the extension
for Edge in my Chrome Web Store submission. Furthermore, people reading reviews in Chrome
Web Store are interested in knowing how extension behaves in Google Chrome. The experience you
get in Microsoft Edge is irrelevant to them.
<div class="vspc"></div>
If you wish the best experience, you should install this addon from Edge Addons store (once I make
it available on that store again).
<div class="vspc"></div>
Thanks for being understanding.
</small>
</li>
</ul>
</p>
<p>
<b>Thanks for your help in advance. It's much appreciated.</b>
</p>
<p>&nbsp;</p>
<p>
If you're interested in more details about why this happens, you can find more details in <a href="https://stuff.tamius.net/sacred-texts/2020/12/22/ultrawidify-and-edge-2020-edition/" target="_blank">this blogpost</a>.
</p>
<p>
Finished reading? <b><a @click="uiVisible=false">Hide this popup</a></b>.
</p>
<p>
In order to disable this popup forever, open the ultrawidify popup, click on 'site settings' and disable automatic aspect ratio detection for this site.
You should probably even disable the extension for this site altogether for the time being.
</p>
<p>
<br/>
<br/>
</p>
<h3>Help by reporting this issue to Microsoft</h3>
<p><b>Go to the settings menu</b> <small>(upper right corner of the window, three dots)</small> <b> Help and feedback</b> <small>(second option from the bottom)</small> <b> Send feedback.</b> (Alternatively, press Alt + Shift + I)</p>
<p>Enter this in the first box:</p>
<p>
<br/>
<i>
Videos on sites that utilize DRM protection schemes are not being scaled correctly. If a part of a DRM-protected video is being displayed outside the boundaries of the browser window,
Edge will scale the video to only fit the portion of the video tag that is currently being displayed on the screen, rather than filling the entire video tag. This causes videos appear
differently than website developers intended at best, and breaking certain websites at worst.
</i>
<br/>
</p>
<p>Or something along these lines. Click 'send' when you're done.</p>
<p>It's the squeaky wheel that gets the grease: developers tend to prioritize issues that affect more people. The more people report this issue, the more likely it is for developers to notice it.</p>
<div class="uw-hover uv-hover-trigger-region">
TEST CONTENT
</div>
<div class="image-examples panel">
<h3>How can one tell when the Edge bug is fixed?</h3>
<p>
When 21:9 movies on netflix look like this:
</p>
<p>
<img :src="getUrl('res/img/git-ignore/edge-demo-working.jpg')" />
</p>
<p>
And not like this:
</p>
<p>
<img :src="getUrl('res/img/git-ignore/edge-demo.jpg')" />
</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<div class="popup-panel">
<div class="tab-row flex flex-row">
<div class="tab">
todo: icon<br/>
Video options
</div>
</div>
<div>sudpo
<!-- Panel section -->
<template v-if="settingsInitialized">
<VideoSettings
:settings="settings"
></VideoSettings>
<ResizerDebugPanel :debugData="debugData">
</ResizerDebugPanel>
</template>
</div>
</div>
</template>
<script>
import VideoSettings from './PlayerUiPanels/VideoSettings.vue'
import { mapState } from 'vuex';
import Icon from '../common/components/Icon';
import ResizerDebugPanel from './PlayerUiPanels/ResizerDebugPanelComponent';
import BrowserDetect from '../ext/conf/BrowserDetect';
import ExecAction from './ui-libs/ExecAction';
import Logger from '../ext/lib/Logger';
import Settings from '../ext/lib/Settings';
import FontLoader from '../ext/lib/uwui/FontLoader';
export default {
components: {
Icon,
ResizerDebugPanel, VideoSettings
},
data() {
return {
// component properties
settings: {},
settingsInitialized: false,
execAction: new ExecAction(),
logger: null,
uiVisible: true,
userAgent: window.navigator.userAgent
debugData: {
resizer: {},
player: {},
},
debugDataPrettified: ''
};
},
created() {
FontLoader.loadFonts();
},
computed: {
...mapState([
'showUi',
'resizerDebugData',
'playerDebugData'
]),
windowWidth: () => {
return window.innerWidth;
},
windowHeight: () => {
return window.innerHeight;
},
},
watch: {
showUi(visible) {
if (visible !== undefined) {
this.uiVisible = visible;
}
},
resizerDebugData(newData) {
this.debugData.resizer = newData;
this.debugDataPrettified = JSON.stringify(this.debugData, null, 2);
},
playerDebugData(newData) {
this.debugData.player = newData;
this.debugDataPrettified = JSON.stringify(this.debugData, null, 2);
}
},
async created() {
try {
this.logger = new Logger();
await this.logger.init({
allowLogging: true,
});
this.settings = new Settings({afterSettingsSaved: this.updateConfig, logger: this.logger});
await this.settings.init();
this.settingsInitialized = true;
this.execAction.setSettings(this.settings);
} catch (e) {
console.error('Failed to initiate ultrawidify player ui.', e);
}
},
methods: {
getUrl(url) {
return BrowserDetect.getURL(url);
},
async hidePopupForever() {
const settings = new Settings();
await settings.init();
if (!settings.active.mutedNotifications) {
settings.active.mutedNotifications = {};
}
if (!settings.active.mutedNotifications?.browserSpecific) {
settings.active.mutedNotifications.browserSpecific = {
edge: {
brokenDrm: {
}
}
};
}
settings.active.mutedNotifications.browserSpecific.edge.brokenDrm[window.location.hostname] = true;
await settings.saveWithoutReload();
this.uiVisible = false;
}
}
}
</script>
<style lang="scss" src="../res/css/uwui-base.scss"></style>
<style lang="scss" src="../res/css/uwui-base.scss" scoped></style>
<style lang="scss" scoped>
@import '../res/css/uwui-base.scss';
@import '../res/css/colors.scss';
// @import '../res/css/font/overpass.css';
// @import '../res/css/font/overpass-mono.css';
@import '../res/css/font/overpass.css';
@import '../res/css/font/overpass-mono.css';
@import '../res/css/common.scss';
.uw-ultrawidify-container-root {
* {
font-family: 'Overpass', 'Segoe UI';
pointer-events: all;
}
pointer-events: auto;
.vspc {
height: 0.5em;
display: block;
}
// .relative-wrapper {
// position: relative;
// width: 100%;
// height: 100%;
// }
.uw-hover {
position: absolute;
top: 5%;
left: 5%;
width: 90%;
height: 90%;
color: #ddd;
background-color: rgba(84, 8, 8, 0.786);
pointer-events: auto;
overflow-y: scroll;
p, h1, h2, h3 {
margin: 0.75em;
display: block;
// display: inline-block;
}
h1 {
top: 10%;
left: 10%;
width: 100px;
height: 100px;
color: #fff;
text-align: center;
background-color: #000;
z-index: 999999999999999999;
}
.uw-hover:hover {
background-color: #f00;
}
h2, h3 {
color: #fa6;
text-align: center;
}
.popup-panel {
position: absolute;
i {
display: block !important;
padding: 4px 8px;
font-family: 'Overpass Mono', monospace;
background-color: rgba(11,11,11,0.75);
font-size: 0.8em;
}
top: 10%;
left: 10%;
b {
z-index: 999999999999999999;
width: 2500px;
height: 1200px;
max-width: 80%;
max-height: 80%;
pointer-events: all !important;
background-color: rgba(0,0,0,0.69);
color: #fff;
}
a {
cursor: pointer;
}
p {
font-size: 16px;
}
h1 {
font-size: 36px;
}
sup {
vertical-align: super;
font-size: smaller;
}
.text {
flex-grow: 1;
flex-shrink: 0;
max-width: 960px;
min-width: 420px;
}
.image-examples {
flex-grow: auto;
flex-shrink: 1;
min-width: 720px;
img {
width: 100%;
}
}
ul {
list-style-type: disc;
list-style-position: inside;
}
ol {
list-style-type: decimal;
list-style-position: inside;
}
ul ul, ol ul {
list-style-type: circle;
list-style-position: inside;
margin-left: 15px;
}
ol ol, ul ol {
list-style-type: lower-latin;
list-style-position: inside;
margin-left: 15px;
}
li {
margin-top: 16px;
line-height: 16px;
}
}
.pad {
padding: 16px;
height: 100%;
width: 100%;
// padding-bottom: 10%;
overflow-y: auto;
.tab {
display: block;
height: 42px;
font-size: 2.5rem;
background: rgb(87, 54, 26);
}
.flex-row {
justify-content: space-around;
.tab:hover {
background-color: #f00;
}
}
pre {
white-space: pre-wrap;
}
}
</style>

143
src/ext/UWContent.ts Normal file
View File

@ -0,0 +1,143 @@
import Debug from './conf/Debug';
import ExtensionMode from '../common/enums/ExtensionMode.enum';
import Settings from './lib/Settings';
import ActionHandler from './lib/ActionHandler';
import Comms from './lib/comms/Comms';
import CommsClient from './lib/comms/CommsClient';
import PageInfo from './lib/video-data/PageInfo';
import Logger, { baseLoggingOptions } from './lib/Logger';
export default class UWContent {
pageInfo: PageInfo;
comms: CommsClient;
settings: Settings;
actionHandler: ActionHandler;
logger: Logger;
commsHandlers: {
[x: string]: ((a: any, b?: any) => void | Promise<void>)[]
} = {
'get-current-zoom': [() => this.pageInfo.requestCurrentZoom()],
'set-ar': [(message) => this.pageInfo.setAr({type: message.arg, ratio: message.customArg}, message.playing)],
'set-alignment': [(message) => {
this.pageInfo.setVideoAlignment(message.arg, message.playing);
this.pageInfo.restoreAr();
}],
'set-stretch': [(message) => this.pageInfo.setStretchMode(message.arg, message.playing, message.customArg)],
'set-keyboard': [(message) => this.pageInfo.setKeyboardShortcutsEnabled(message.arg)],
'autoar-start': [(message) => {
if (message.enabled !== false) {
this.pageInfo.initArDetection(message.playing);
this.pageInfo.startArDetection(message.playing);
} else {
this.pageInfo.stopArDetection(message.playing);
}
}],
'pause-processing': [(message) => this.pageInfo.pauseProcessing(message.playing)],
'resume-processing': [(message) => this.pageInfo.resumeProcessing(message.autoArStatus, message.playing)],
'set-zoom': [(message) => this.pageInfo.setZoom(message.arg, true, message.playing)],
'change-zoom': [(message) => this.pageInfo.zoomStep(message.arg, message.playing)],
'mark-player': [(message) => this.pageInfo.markPlayer(message.name, message.color)],
'unmark-player': [() => this.pageInfo.unmarkPlayer()],
'autoar-set-manual-tick': [(message) => this.pageInfo.setManualTick(message.arg)],
'autoar-tick': [() => this.pageInfo.tick()],
'set-ar-persistence': [(message) => this.pageInfo.setArPersistence(message.arg)],
}
constructor(){
}
reloadSettings() {
try {
this.logger.log('info', 'debug', 'Things happened in the popup. Will reload extension settings.');
this.init();
} catch (e) {
console.warn('Ultrawidify: settings reload failed. This probably shouldn\'t outright kill the extension, but page reload is recommended.');
}
}
async init(){
try {
if (Debug.debug) {
console.log("[uw::main] loading configuration ...");
}
// logger init is the first thing that needs to run
try {
if (!this.logger) {
this.logger = new Logger();
await this.logger.init(baseLoggingOptions);
// show popup if logging to file is enabled
if (this.logger.isLoggingAllowed() && this.logger.isLoggingToFile()) {
console.info("[uw::init] Logging is allowed! Initalizing vue and UI!");
// CommsClient is not initiated yet, so we use static comms to send the command
Comms.sendMessage({cmd: 'show-logger'});
}
}
} catch (e) {
console.error("logger init failed!", e)
}
// init() is re-run any time settings change
if (this.comms) {
this.comms.destroy();
}
if (!this.settings) {
this.settings = new Settings({
onSettingsChanged: () => this.reloadSettings(),
logger: this.logger
});
await this.settings.init();
}
this.comms = new CommsClient('content-main-port', this.logger, this.commsHandlers);
// če smo razširitev onemogočili v nastavitvah, ne naredimo ničesar
// If extension is soft-disabled, don't do shit
var extensionMode = this.settings.getExtensionMode();
this.logger.log('info', 'debug', "[uw::init] Extension mode:" + (extensionMode < 0 ? "disabled" : extensionMode == '1' ? 'basic' : 'full'));
const isSiteDisabled = extensionMode === ExtensionMode.Disabled
if (isSiteDisabled) {
if (this.settings.getExtensionMode('@global') === ExtensionMode.Disabled) {
this.logger.log('info', 'debug', "[uw::init] EXTENSION DISABLED, THEREFORE WONT BE STARTED")
return;
}
}
try {
if (this.pageInfo) {
this.logger.log('info', 'debug', '[uw.js::setup] An instance of pageInfo already exists and will be destroyed.');
this.pageInfo.destroy();
}
this.pageInfo = new PageInfo(this.comms, this.settings, this.logger, extensionMode, isSiteDisabled);
this.logger.log('info', 'debug', "[uw.js::setup] pageInfo initialized.");
this.logger.log('info', 'debug', "[uw.js::setup] will try to initate ActionHandler.");
// start action handler only if extension is enabled for this site
if (!isSiteDisabled) {
if (this.actionHandler) {
this.actionHandler.destroy();
}
this.actionHandler = new ActionHandler(this.pageInfo);
this.actionHandler.init();
this.logger.log('info', 'debug', "[uw.js::setup] ActionHandler initiated.");
}
} catch (e) {
console.error('Ultrawidify: failed to start extension. Error:', e)
this.logger.log('error', 'debug', "[uw::init] FAILED TO START EXTENSION. Error:", e);
}
} catch (e) {
console.error('Ultrawidify initalization failed for some reason:', e);
}
}
}

315
src/ext/UWServer.ts Normal file
View File

@ -0,0 +1,315 @@
import Debug from './conf/Debug.js';
import BrowserDetect from './conf/BrowserDetect';
import CommsServer from './lib/comms/CommsServer';
import Settings from './lib/Settings';
import Logger, { baseLoggingOptions } from './lib/Logger';
import { sleep } from '../common/js/utils';
import { browser } from 'webextension-polyfill-ts';
export default class UWServer {
settings: Settings;
logger: Logger;
comms: CommsServer;
ports: any[] = [];
hasVideos: boolean;
currentSite: string = '';
videoTabs: any = {};
currentTabId: number = 0;
selectedSubitem: any = {
'siteSettings': undefined,
'videoSettings': undefined,
}
private gcTimeout: any;
uiLoggerInitialized: boolean = false;
constructor() {
this.setup();
}
async setup() {
try {
// logger is the first thing that goes up
const loggingOptions = {
isBackgroundScript: true,
allowLogging: false,
useConfFromStorage: true,
logAll: true,
fileOptions: {
enabled: false,
},
consoleOptions: {
enabled: false
}
};
this.logger = new Logger();
await this.logger.init(loggingOptions);
this.settings = new Settings({logger: this.logger});
await this.settings.init();
this.comms = new CommsServer(this);
this.comms.subscribe('show-logger', async () => await this.initUiAndShowLogger());
this.comms.subscribe('init-vue', async () => await this.initUi());
this.comms.subscribe('uwui-vue-initialized', () => this.uiLoggerInitialized = true);
this.comms.subscribe('emit-logs', () => {}); // we don't need to do anything, this gets forwarded to UI content script as is
browser.tabs.onActivated.addListener((m) => {this.onTabSwitched(m)});
} catch (e) {
console.error(`Ultrawidify [server]: failed to start. Reason:`, e);
}
}
async _promisifyTabsGet(browserObj, tabId){
return new Promise( (resolve, reject) => {
browserObj.tabs.get(tabId, (tab) => resolve(tab));
});
}
async injectCss(css, sender) {
try {
if (BrowserDetect.firefox || BrowserDetect.edge) {
browser.tabs.insertCSS(sender.tab.id, {code: css, cssOrigin: 'user', frameId: sender.frameId});
} else if (BrowserDetect.anyChromium) {
chrome.tabs.insertCSS(sender.tab.id, {code: css, cssOrigin: 'user', frameId: sender.frameId});
}
} catch (e) {
this.logger.log('error','debug', '[UwServer::injectCss] Error while injecting css:', {error: e, css, sender});
}
}
async removeCss(css, sender) {
try {
browser.tabs.removeCSS(sender.tab.id, {code: css, cssOrigin: 'user', frameId: sender.frameId});
} catch (e) {
this.logger.log('error','debug', '[UwServer::injectCss] Error while removing css:', {error: e, css, sender});
}
}
async replaceCss(oldCss, newCss, sender) {
if (oldCss !== newCss) {
this.injectCss(newCss, sender);
this.removeCss(oldCss, sender);
}
}
extractHostname(url){
var hostname;
if (!url) {
return "<no url>";
}
// extract hostname
if (url.indexOf("://") > -1) { //find & remove protocol (http, ftp, etc.) and get hostname
hostname = url.split('/')[2];
}
else {
hostname = url.split('/')[0];
}
hostname = hostname.split(':')[0]; //find & remove port number
hostname = hostname.split('?')[0]; //find & remove "?"
return hostname;
}
async onTabSwitched(activeInfo){
this.hasVideos = false;
try {
this.currentTabId = activeInfo.tabId; // just for readability
let tab;
if (BrowserDetect.firefox) {
tab = await browser.tabs.get(this.currentTabId);
} else if (BrowserDetect.anyChromium) {
tab = await this._promisifyTabsGet(chrome, this.currentTabId);
}
this.currentSite = this.extractHostname(tab.url);
this.logger.log('info', 'debug', '[UwServer::onTabSwitched] user switched tab. New site:', this.currentSite);
} catch(e) {
this.logger.log('error', 'debug', '[UwServer::onTabSwitched] there was a problem getting currnet site:', e)
}
this.selectedSubitem = {
'siteSettings': undefined,
'videoSettings': undefined,
}
//TODO: change extension icon based on whether there's any videos on current page
}
registerVideo(sender) {
this.logger.log('info', 'comms', '[UWServer::registerVideo] Registering video.\nsender:', sender);
const tabHostname = this.extractHostname(sender.tab.url);
const frameHostname = this.extractHostname(sender.url);
// preveri za osirotele/zastarele vrednosti ter jih po potrebi izbriši
// check for orphaned/outdated values and remove them if neccessary
if (this.videoTabs[sender.tab.id]?.host != tabHostname) {
delete this.videoTabs[sender.tab.id]
} else if(this.videoTabs[sender.tab.id]?.frames[sender.frameId]?.host != frameHostname) {
delete this.videoTabs[sender.tab.id].frames[sender.frameId];
}
if (this.videoTabs[sender.tab.id]) {
this.videoTabs[sender.tab.id].frames[sender.frameId] = {
id: sender.frameId,
host: frameHostname,
url: sender.url,
registerTime: Date.now(),
}
} else {
this.videoTabs[sender.tab.id] = {
id: sender.tab.id,
host: tabHostname,
url: sender.tab.url,
frames: {}
};
this.videoTabs[sender.tab.id].frames[sender.frameId] = {
id: sender.frameId,
host: frameHostname,
url: sender.url,
registerTime: Date.now(),
}
}
this.logger.log('info', 'comms', '[UWServer::registerVideo] Video registered. current videoTabs:', this.videoTabs);
}
unregisterVideo(sender) {
this.logger.log('info', 'comms', '[UwServer::unregisterVideo] Unregistering video.\nsender:', sender);
if (this.videoTabs[sender.tab.id]) {
if ( Object.keys(this.videoTabs[sender.tab.id].frames).length <= 1) {
delete this.videoTabs[sender.tab.id]
} else {
if(this.videoTabs[sender.tab.id].frames[sender.frameId]) {
delete this.videoTabs[sender.tab.id].frames[sender.frameId];
}
}
}
this.logger.log('info', 'comms', '[UwServer::unregisterVideo] Video has been unregistered. Current videoTabs:', this.videoTabs);
}
setSelectedTab(menu, subitem) {
this.logger.log('info', 'comms', '[UwServer::setSelectedTab] saving selected tab for', menu, ':', subitem);
this.selectedSubitem[menu] = subitem;
}
async initUi() {
try {
if (BrowserDetect.firefox) {
await browser.tabs.executeScript({
file: '/ext/uw-ui.js',
allFrames: true,
});
} else if (BrowserDetect.anyChromium) {
await new Promise<void>( resolve =>
chrome.tabs.executeScript({
file: '/ext/uw-ui.js',
allFrames: true,
}, () => resolve())
);
}
} catch (e) {
console.warn('Ultrawidify [server]: UI setup failed. While problematic, this problem shouldn\'t completely crash the extension.');
this.logger.log('ERROR', 'uwbg', 'UI initialization failed. Reason:', e);
}
}
async initUiAndShowLogger() {
try {
// this implementation is less than optimal and very hacky, but it should work
// just fine for our use case.
this.uiLoggerInitialized = false;
await this.initUi();
await new Promise<void>( async (resolve, reject) => {
// if content script doesn't give us a response within 5 seconds, something is
// obviously wrong and we stop waiting,
// oh and btw, resolve/reject do not break the loops, so we need to do that
// ourselves:
// https://stackoverflow.com/questions/55207256/will-resolve-in-promise-loop-break-loop-iteration
let isRejected = false;
setTimeout( async () => {isRejected = true; reject()}, 5000);
// check whether UI has been initiated on the FE. If it was, we resolve the
// promise and off we go
while (!isRejected) {
if (this.uiLoggerInitialized) {
resolve();
return; // remember the bit about resolve() not breaking the loop?
}
await sleep(100);
}
});
} catch (e) {
console.warn('Ultrawidify [server]: failed to set up logger UI. While problematic, this problem shouldn\'t completely crash the extension.');
}
}
async getCurrentTab() {
return (await browser.tabs.query({active: true, currentWindow: true}))[0];
}
async getVideoTab() {
// friendly reminder: if current tab doesn't have a video,
// there won't be anything in this.videoTabs[this.currentTabId]
const ctab = await this.getCurrentTab();
if (!ctab || !ctab.id) {
return {
host: 'INVALID SITE',
frames: [],
}
}
if (this.videoTabs[ctab.id]) {
// if video is older than PageInfo's video rescan period (+ 4000ms of grace),
// we clean it up from videoTabs[tabId].frames array.
const ageLimit = Date.now() - this.settings.active.pageInfo.timeouts.rescan - 4000;
try {
for (const key in this.videoTabs[ctab.id].frames) {
if (this.videoTabs[ctab.id].frames[key].registerTime < ageLimit) {
delete this.videoTabs[ctab.id].frames[key];
}
}
} catch (e) {
// something went wrong. There's prolly no frames.
return {
host: this.extractHostname(ctab.url),
frames: [],
selected: this.selectedSubitem
}
}
return {
...this.videoTabs[ctab.id],
host: this.extractHostname(ctab.url),
selected: this.selectedSubitem
};
}
// return something more or less empty if this tab doesn't have
// a video registered for it
return {
host: this.extractHostname(ctab.url),
frames: [],
selected: this.selectedSubitem
}
}
// chrome shitiness mitigation
sendUnmarkPlayer(message) {
this.comms.sendUnmarkPlayer(message);
}
}

View File

@ -1,27 +1,27 @@
import VideoAlignment from '../../common/enums/video-alignment.enum';
import Stretch from '../../common/enums/stretch.enum';
import ExtensionMode from '../../common/enums/extension-mode.enum';
import AspectRatio from '../../common/enums/aspect-ratio.enum';
import CropModePersistence from '../../common/enums/crop-mode-persistence.enum';
import VideoAlignmentType from '../../common/enums/VideoAlignmentType.enum';
import StretchType from '../../common/enums/StretchType.enum';
import ExtensionMode from '../../common/enums/ExtensionMode.enum';
import AspectRatioType from '../../common/enums/AspectRatioType.enum';
import CropModePersistence from '../../common/enums/CropModePersistence.enum';
var ActionList = {
'set-ar': {
name: 'Set aspect ratio',
args: [{
name: 'Automatic',
arg: AspectRatio.Automatic,
arg: AspectRatioType.Automatic,
},{
name: 'Fit width',
arg: AspectRatio.FitWidth,
arg: AspectRatioType.FitWidth,
},{
name: 'Fit height',
arg: AspectRatio.FitHeight,
arg: AspectRatioType.FitHeight,
},{
name: 'Reset',
arg: AspectRatio.Reset,
arg: AspectRatioType.Reset,
},{
name: 'Manually specify ratio',
arg: AspectRatio.Fixed,
arg: AspectRatioType.Fixed,
customArg: true,
customSetter: (value) => {
const [width, height] = value.split(':');
@ -70,33 +70,33 @@ var ActionList = {
name: 'Set stretch',
args: [{
name: 'Normal',
arg: Stretch.NoStretch
arg: StretchType.NoStretch
},{
name: 'Basic',
arg: Stretch.Basic,
arg: StretchType.Basic,
},{
name: 'Hybrid',
arg: Stretch.Hybrid,
arg: StretchType.Hybrid,
},{
name: 'Thin borders',
arg: Stretch.Conditional,
arg: StretchType.Conditional,
},{
name: 'Fixed (source)',
arg: Stretch.FixedSource,
arg: StretchType.FixedSource,
customArg: true,
scopes: {
page: true,
}
},{
name: 'Fixed (displayed)',
arg: Stretch.Fixed,
arg: StretchType.Fixed,
customArg: true,
scopes: {
page: true,
}
},{
name: 'Default',
arg: Stretch.Default,
arg: StretchType.Default,
scopes: {
site: true
}
@ -111,16 +111,16 @@ var ActionList = {
name: 'Set video alignment',
args: [{
name: 'Left',
arg: VideoAlignment.Left,
arg: VideoAlignmentType.Left,
},{
name: 'Center',
arg: VideoAlignment.Center,
arg: VideoAlignmentType.Center,
},{
name: 'Right',
arg: VideoAlignment.Right
arg: VideoAlignmentType.Right
},{
name: 'Default',
arg: VideoAlignment.Default,
arg: VideoAlignmentType.Default,
scopes: {
site: true,
}
@ -179,7 +179,7 @@ var ActionList = {
page: true,
}
},
'set-extension-mode': {
'set-ExtensionMode': {
name: 'Set extension mode',
args: [{
name: 'Enable',

View File

@ -1,8 +1,9 @@
// How to use:
// version: {ExtensionConf object, but only properties that get overwritten}
import Stretch from '../../common/enums/stretch.enum';
import ExtensionMode from '../../common/enums/extension-mode.enum';
import VideoAlignment from '../../common/enums/video-alignment.enum';
import StretchType from '../../common/enums/StretchType.enum';
import ExtensionMode from '../../common/enums/ExtensionMode.enum';
import VideoAlignmentType from '../../common/enums/VideoAlignmentType.enum';
import BrowserDetect from './BrowserDetect';
const ExtensionConfPatch = [
{
@ -267,7 +268,7 @@ const ExtensionConfPatch = [
label: '4:3 stretch (src)',
cmd: [{
action: 'set-stretch',
arg: Stretch.FixedSource,
arg: StretchType.FixedSource,
customArg: 1.33,
}],
scopes: {
@ -284,7 +285,7 @@ const ExtensionConfPatch = [
label: '16:9 stretch (src)',
cmd: [{
action: 'set-stretch',
arg: Stretch.FixedSource,
arg: StretchType.FixedSource,
customArg: 1.77,
}],
scopes: {
@ -309,8 +310,8 @@ const ExtensionConfPatch = [
autoar: ExtensionMode.Enabled,
autoarFallback: ExtensionMode.Enabled,
override: true, // ignore value localStorage in favour of this
stretch: Stretch.Default,
videoAlignment: VideoAlignment.Default,
stretch: StretchType.Default,
videoAlignment: VideoAlignmentType.Default,
keyboardShortcutsEnabled: ExtensionMode.Default,
DOM: {
player: {
@ -339,8 +340,8 @@ const ExtensionConfPatch = [
autoar: ExtensionMode.Enabled,
override: false,
type: 'community',
stretch: Stretch.Default,
videoAlignment: VideoAlignment.Default,
stretch: StretchType.Default,
videoAlignment: VideoAlignmentType.Default,
keyboardShortcutsEnabled: ExtensionMode.Default,
arPersistence: true, // persist aspect ratio between different videos
autoarPreventConditions: { // prevents autoar on following conditions
@ -445,6 +446,71 @@ const ExtensionConfPatch = [
// do nothing if disney+ is missing
}
}
}, {
forVersion: '5.0.1',
updateFn: (userOptions, defaultOptions) => {
try {
userOptions.mitigations = {
zoomLimit: {
enabled: BrowserDetect.edge || BrowserDetect.isEdgeUA,
limit: 0.997,
fullscreenOnly: true
}
}
} catch (e) {
// do nothing
}
}
}, {
forVersion: '5.0.1.1',
updateFn: (userOptions, defaultOptions) => {
try {
userOptions.mitigations = {
zoomLimit: {
enabled: true,
limit: 0.997,
fullscreenOnly: true
}
}
} catch (e) {
// do nothing
}
}
}, {
forVersion: '5.0.2',
updateFn: (userOptions, defaultOptions) => {
try {
if (! userOptions.mitigations) {
userOptions.mitigations = {
zoomLimit: {
enabled: true,
limit: 0.997,
fullscreenOnly: true
}
}
} else if (BrowserDetect.chrome) {
userOptions.mitigations = {
zoomLimit: {
enabled: true,
limit: 0.997,
fullscreenOnly: true
}
}
}
} catch (e) {
// do nothing
}
}
}, {
forVersion: '5.0.4',
updateFn: (userOptions, defaultOptions) => {
userOptions.sites['www.disneyplus.com'].DOM.player = {
... userOptions.sites['www.disneyplus.com'].DOM.player,
querySelectors: ".btm-media-client-element",
useRelativeAncestor: true,
videoAncestor: 1
}
}
}
];

View File

@ -1,16 +1,18 @@
import Debug from './Debug';
import currentBrowser from './BrowserDetect';
import VideoAlignment from '../../common/enums/video-alignment.enum';
import Stretch from '../../common/enums/stretch.enum';
import ExtensionMode from '../../common/enums/extension-mode.enum';
import AntiGradientMode from '../../common/enums/anti-gradient-mode.enum';
import AspectRatio from '../../common/enums/aspect-ratio.enum';
import CropModePersistence from '../../common/enums/crop-mode-persistence.enum';
import VideoAlignmentType from '../../common/enums/VideoAlignmentType.enum';
import StretchType from '../../common/enums/StretchType.enum';
import ExtensionMode from '../../common/enums/ExtensionMode.enum';
import AntiGradientMode from '../../common/enums/AntiGradientMode.enum';
import AspectRatioType from '../../common/enums/AspectRatioType.enum';
import CropModePersistence from '../../common/enums/CropModePersistence.enum';
import SettingsInterface from '../../common/interfaces/SettingsInterface';
import BrowserDetect from './BrowserDetect';
if(Debug.debug)
console.log("Loading: ExtensionConf.js");
var ExtensionConf = {
const ExtensionConf: SettingsInterface = {
arDetect: {
disabledReason: "", // if automatic aspect ratio has been disabled, show reason
allowedMisaligned: 0.05, // top and bottom letterbox thickness can differ by this much.
@ -176,7 +178,7 @@ var ExtensionConf = {
label: 'Automatic', // name displayed in ui (can be overridden in scope/playerUi)
cmd: [{
action: 'set-ar',
arg: AspectRatio.Automatic,
arg: AspectRatioType.Automatic,
persistent: false, // optional, false by default. If true, change doesn't take effect immediately.
// Instead, this action saves stuff to settings
}],
@ -211,7 +213,7 @@ var ExtensionConf = {
label: 'Reset',
cmd: [{
action: 'set-ar',
arg: AspectRatio.Reset,
arg: AspectRatioType.Reset,
}],
scopes: {
page: {
@ -237,7 +239,7 @@ var ExtensionConf = {
label: 'Fit width',
cmd: [{
action: 'set-ar',
arg: AspectRatio.FitWidth,
arg: AspectRatioType.FitWidth,
}],
scopes: {
page: {
@ -263,7 +265,7 @@ var ExtensionConf = {
label: 'Fit height',
cmd: [{
action: 'set-ar',
arg: AspectRatio.FitHeight
arg: AspectRatioType.FitHeight
}],
scopes: {
page: {
@ -290,7 +292,7 @@ var ExtensionConf = {
label: '16:9',
cmd: [{
action: 'set-ar',
arg: AspectRatio.Fixed,
arg: AspectRatioType.Fixed,
customArg: 1.78,
}],
scopes: {
@ -318,7 +320,7 @@ var ExtensionConf = {
label: '21:9',
cmd: [{
action: 'set-ar',
arg: AspectRatio.Fixed,
arg: AspectRatioType.Fixed,
customArg: 2.39
}],
scopes: {
@ -346,7 +348,7 @@ var ExtensionConf = {
label: '18:9',
cmd: [{
action: 'set-ar',
arg: AspectRatio.Fixed,
arg: AspectRatioType.Fixed,
customArg: 2.0,
}],
scopes: {
@ -373,7 +375,7 @@ var ExtensionConf = {
label: 'Never persist',
cmd: [{
action: 'set-ar-persistence',
arg: CropModePersistence.Never,
arg: CropModePersistence.Disabled,
}],
scopes: {
site: {
@ -547,7 +549,7 @@ var ExtensionConf = {
label: 'Don\'t stretch',
cmd: [{
action: 'set-stretch',
arg: Stretch.NoStretch,
arg: StretchType.NoStretch,
}],
scopes: {
global: {
@ -572,7 +574,7 @@ var ExtensionConf = {
label: 'Basic stretch',
cmd: [{
action: 'set-stretch',
arg: Stretch.Basic,
arg: StretchType.Basic,
}],
scopes: {
global: {
@ -597,7 +599,7 @@ var ExtensionConf = {
label: 'Hybrid stretch',
cmd: [{
action: 'set-stretch',
arg: Stretch.Hybrid,
arg: StretchType.Hybrid,
}],
scopes: {
global: {
@ -622,7 +624,7 @@ var ExtensionConf = {
label: 'Thin borders only',
cmd: [{
action: 'set-stretch',
arg: Stretch.Conditional,
arg: StretchType.Conditional,
}],
scopes: {
global: {
@ -647,7 +649,7 @@ var ExtensionConf = {
label: 'Default',
cmd: [{
action: 'set-stretch',
arg: Stretch.Default,
arg: StretchType.Default,
}],
scopes: {
site: {
@ -660,7 +662,7 @@ var ExtensionConf = {
label: '4:3 stretch (src)',
cmd: [{
action: 'set-stretch',
arg: Stretch.FixedSource,
arg: StretchType.FixedSource,
customArg: 1.33,
}],
scopes: {
@ -677,7 +679,7 @@ var ExtensionConf = {
label: '16:9 stretch (src)',
cmd: [{
action: 'set-stretch',
arg: Stretch.FixedSource,
arg: StretchType.FixedSource,
customArg: 1.77,
}],
scopes: {
@ -698,7 +700,7 @@ var ExtensionConf = {
label: 'Left',
cmd: [{
action: 'set-alignment',
arg: VideoAlignment.Left,
arg: VideoAlignmentType.Left,
}],
scopes: {
global: {
@ -720,7 +722,7 @@ var ExtensionConf = {
label: 'Center',
cmd: [{
action: 'set-alignment',
arg: VideoAlignment.Center,
arg: VideoAlignmentType.Center,
}],
scopes: {
global: {
@ -742,7 +744,7 @@ var ExtensionConf = {
label: 'Right',
cmd: [{
action: 'set-alignment',
arg: VideoAlignment.Right
arg: VideoAlignmentType.Right
}],
scopes: {
global: {
@ -764,7 +766,7 @@ var ExtensionConf = {
label: 'Default',
cmd: [{
action: 'set-alignment',
arg: VideoAlignment.Default
arg: VideoAlignmentType.Default
}],
scopes: {
site: {
@ -780,7 +782,7 @@ var ExtensionConf = {
name: 'Enable extension',
label: 'Enable',
cmd: [{
action: 'set-extension-mode',
action: 'set-ExtensionMode',
arg: ExtensionMode.Enabled,
persistent: true,
}],
@ -796,7 +798,7 @@ var ExtensionConf = {
name: 'Enable extension on whitelisted sites only',
label: 'On whitelist only',
cmd: [{
action: 'set-extension-mode',
action: 'set-ExtensionMode',
arg: ExtensionMode.Whitelist,
persistent: true,
}],
@ -809,7 +811,7 @@ var ExtensionConf = {
name: 'Extension mode: use default settings',
label: 'Default',
cmd: [{
action: 'set-extension-mode',
action: 'set-ExtensionMode',
arg: ExtensionMode.Default,
persistent: true,
}],
@ -822,7 +824,7 @@ var ExtensionConf = {
name: 'Disable extension',
label: 'Disable',
cmd: [{
action: 'set-extension-mode',
action: 'set-ExtensionMode',
arg: ExtensionMode.Disabled,
persistent: true,
}],
@ -959,6 +961,13 @@ var ExtensionConf = {
}
},
],
mitigations: {
zoomLimit: {
enabled: true,
limit: 0.997,
fullscreenOnly: true
}
},
whatsNewChecked: true,
// -----------------------------------------
// ::: SITE CONFIGURATION :::
@ -1006,8 +1015,8 @@ var ExtensionConf = {
autoarFallback: currentBrowser.firefox ? // if autoAr fails, try fallback mode?
ExtensionMode.Enabled : // Options same as in autoar.
ExtensionMode.Disabled, // if autoar is disabled, this setting is irrelevant
stretch: Stretch.NoStretch, // Default stretch mode.
videoAlignment: VideoAlignment.Center, // Video alignment
stretch: StretchType.NoStretch, // Default stretch mode.
videoAlignment: VideoAlignmentType.Center, // Video alignment
keyboardShortcutsEnabled: ExtensionMode.Enabled,
},
"www.youtube.com" : {
@ -1017,8 +1026,8 @@ var ExtensionConf = {
override: false, // ignore value localStorage in favour of this
type: 'official', // is officially supported? (Alternatives are 'community' and 'user-defined')
actions: null, // overrides global keyboard shortcuts and button configs. Is array, is optional.
stretch: Stretch.Default,
videoAlignment: VideoAlignment.Default,
stretch: StretchType.Default,
videoAlignment: VideoAlignmentType.Default,
keyboardShortcutsEnabled: ExtensionMode.Default,
DOM: {
player: {
@ -1035,8 +1044,8 @@ var ExtensionConf = {
autoar: ExtensionMode.Enabled,
override: false,
type: 'official',
stretch: Stretch.Default,
videoAlignment: VideoAlignment.Default,
stretch: StretchType.Default,
videoAlignment: VideoAlignmentType.Default,
keyboardShortcutsEnabled: ExtensionMode.Default,
arPersistence: true, // persist aspect ratio between different videos
"DOM": {
@ -1054,16 +1063,17 @@ var ExtensionConf = {
autoar: ExtensionMode.Enabled,
override: false,
type: 'community',
stretch: Stretch.Default,
videoAlignment: VideoAlignment.Default,
stretch: StretchType.Default,
videoAlignment: VideoAlignmentType.Default,
keyboardShortcutsEnabled: ExtensionMode.Default,
arPersistence: true, // persist aspect ratio between different videos
DOM: {
"player": {
"manual": true,
"querySelectors": ".btn-media-clients",
"querySelectors": ".btm-media-client-element",
"additionalCss": "",
"useRelativeAncestor": false,
"videoAncestor": 1,
"playerNodeCss": ""
}
},
@ -1074,8 +1084,8 @@ var ExtensionConf = {
autoar: ExtensionMode.Enabled,
override: true,
type: 'official',
stretch: Stretch.Default,
videoAlignment: VideoAlignment.Default,
stretch: StretchType.Default,
videoAlignment: VideoAlignmentType.Default,
keyboardShortcutsEnabled: ExtensionMode.Default,
DOM: {
player: {
@ -1117,8 +1127,8 @@ var ExtensionConf = {
autoar:ExtensionMode.Enabled,
override: false,
type: 'testing',
stretch: Stretch.Default,
videoAlignment: VideoAlignment.Default,
stretch: StretchType.Default,
videoAlignment: VideoAlignmentType.Default,
keyboardShortcutsEnabled: ExtensionMode.Default,
DOM: {
player: {
@ -1134,8 +1144,8 @@ var ExtensionConf = {
autoar: ExtensionMode.Enabled,
override: false,
type: 'testing',
stretch: Stretch.Default,
videoAlignment: VideoAlignment.Default,
stretch: StretchType.Default,
videoAlignment: VideoAlignmentType.Default,
keyboardShortcutsEnabled: ExtensionMode.Default,
DOM: {
player: {

View File

@ -1,20 +1,37 @@
import Debug from '../conf/Debug';
import PlayerData from './video-data/PlayerData';
import ExtensionMode from '../../common/enums/extension-mode.enum';
import ExtensionMode from '../../common/enums/ExtensionMode.enum';
import Logger from './Logger';
import PageInfo from './video-data/PageInfo';
import Settings from './Settings';
import VideoData from './video-data/VideoData';
if(process.env.CHANNEL !== 'stable'){
console.info("Loading ActionHandler");
}
class ActionHandler {
logger: Logger;
pageInfo: PageInfo;
settings: Settings;
inputs: string[] = ['input', 'select', 'button', 'textarea'];
keyboardLocalDisabled: boolean = false;
keyUpActions: any[] = [];
keyDownActions: any[] = [];
mouseMoveActions: any[] = [];
mouseScrollUpActions: any[] = [];
mouseScrollDownActions: any[] = [];
mouseEnterActions: any[] = [];
mouseLeaveActions: any[] = [];
constructor(pageInfo) {
this.logger = pageInfo.logger;
this.pageInfo = pageInfo;
this.settings = pageInfo.settings;
this.inputs = ['input', 'select', 'button', 'textarea'];
this.keyboardLocalDisabled = false;
}
init() {
@ -105,7 +122,7 @@ class ActionHandler {
}
// events should be handled in handleEvent function. We need to do things this
// way, otherwise we can't remove event listenerđ
// way, otherwise we can't remove event listener
// https://stackoverflow.com/a/19507086
document.addEventListener('keydown', this );
document.addEventListener('keyup', this );
@ -168,7 +185,6 @@ class ActionHandler {
this.logger.resume(); // undisable
this.logger.log('info', 'keyboard', "[ActionHandler::preventAction] Testing whether we're in a textbox or something. Detailed rundown of conditions:\n" +
"is full screen? (yes->allow):", PlayerData.isFullScreen(),
"\nis tag one of defined inputs? (yes->prevent):", this.inputs.indexOf(activeElement.tagName.toLocaleLowerCase()) !== -1,
"\nis role = textbox? (yes -> prevent):", activeElement.getAttribute("role") === "textbox",
"\nis type === 'text'? (yes -> prevent):", activeElement.getAttribute("type") === "text",
@ -186,12 +202,6 @@ class ActionHandler {
);
}
// lately youtube has allowed you to read and write comments while watching video in
// fullscreen mode. We can no longer do this.
// if (PlayerData.isFullScreen()) {
// return false;
// }
if (this.keyboardLocalDisabled) {
return true;
}
@ -240,7 +250,7 @@ class ActionHandler {
this.isActionMatchStandard(shortcut, event) || this.isActionMatchKeyCode(shortcut, event);
}
execAction(actions, event, videoData) {
execAction(actions, event, videoData?: VideoData) {
this.logger.log('info', 'keyboard', "%c[ActionHandler::execAction] Trying to find and execute action for event. Actions/event: ", "color: #ff0", actions, event);
const isLatin = event.key ? this.isLatin(event.key) : true;
@ -276,9 +286,9 @@ class ActionHandler {
} else if (cmd.action === "set-alignment") {
this.settings.active.sites[site].videoAlignment = cmd.arg;
} else if (cmd.action === "set-extension-mode") {
this.settings.active.sites[site].status = cmd.arg;
this.settings.active.sites[site].mode = cmd.arg;
} else if (cmd.action === "set-autoar-mode") {
this.settings.active.sites[site].arStatus = cmd.arg;
this.settings.active.sites[site].autoar = cmd.arg;
} else if (cmd.action === 'set-keyboard') {
this.settings.active.sites[site].keyboardShortcutsEnabled = cmd.arg;
} else if (cmd.action === 'set-ar-persistence') {
@ -323,9 +333,9 @@ class ActionHandler {
this.execAction(this.keyDownActions, event);
}
handleMouseMove(event, videoData) {
handleMouseMove(event, videoData?: VideoData) {
this.logger.log('info', 'keyboard', "[ActionHandler::handleMouseMove] mouse move is being handled.\nevent:", event, "\nvideo data:", videoData);
videoData.panHandler(event);
videoData?.panHandler(event);
this.execAction(this.mouseMoveActions, event, videoData)
}

View File

@ -1,42 +1,112 @@
import currentBrowser from '../conf/BrowserDetect';
import { decycle } from 'json-cyclic';
import Comms from './comms/Comms';
import BrowserDetect from '../conf/BrowserDetect';
import { browser } from 'webextension-polyfill-ts';
if (process.env.CHANNEL !== 'stable'){
console.info('Loading Logger');
}
class Logger {
constructor(options) {
this.onLogEndCallbacks = [];
this.history = [];
this.globalHistory = {};
this.isContentScript = false;
this.isBackgroundScript = true;
export const baseLoggingOptions: LoggerConfig = {
isContentScript: true,
allowLogging: false,
useConfFromStorage: true,
fileOptions: {
enabled: false
},
consoleOptions: {
"enabled": false,
"debug": true,
"init": true,
"settings": true,
"keyboard": true,
"mousemove": false,
"actionHandler": true,
"comms": true,
"playerDetect": true,
"resizer": true,
"scaler": true,
"stretcher": true,
// "videoRescan": true,
// "playerRescan": true,
"arDetect": true,
"arDetect_verbose": true
},
allowBlacklistedOrigins: {
'periodicPlayerCheck': false,
'periodicVideoStyleChangeCheck': false,
'handleMouseMove': false
}
};
export interface LoggingOptions {
enabled?: boolean;
debug?: boolean;
init?: boolean;
settings?: boolean;
keyboard?: boolean;
mousemove?: boolean;
actionHandler?: boolean;
comms?: boolean;
playerDetect?: boolean;
resizer?: boolean;
scaler?: boolean;
stretcher?: boolean;
videoRescan?: boolean;
playerRescan?: boolean;
arDetect?: boolean;
arDetect_verbose?: boolean;
}
export interface LoggerBlacklistedOrigins {
periodicPlayerCheck?: boolean;
periodicVideoStyleChangeCheck?: boolean;
handleMouseMove?: boolean;
}
export interface LoggerConfig {
isContentScript?: boolean;
isBackgroundScript?: boolean;
allowLogging?: boolean;
useConfFromStorage?: boolean;
fileOptions?: LoggingOptions;
consoleOptions?: LoggingOptions;
allowBlacklistedOrigins?: LoggerBlacklistedOrigins;
}
class Logger {
temp_disable: boolean = false;
onLogEndCallbacks: any[] = [];
history: any[] = [];
globalHistory: any = {};
isContentScript: boolean = false;
isBackgroundScript: boolean = true;
vuexStore: any;
uwInstance: any;
conf: any;
startTime: number;
stopTime: number;
constructor(options?: {vuexStore?: any, uwInstance?: any}) {
this.vuexStore = options?.vuexStore;
this.uwInstance = options?.uwInstance;
}
static saveConfig(conf) {
static saveConfig(conf: LoggerConfig) {
if (process.env.CHANNEL === 'dev') {
console.info('Saving logger conf:', conf)
}
if (currentBrowser.firefox || currentBrowser.edge) {
return browser.storage.local.set( {'uwLogger': JSON.stringify(conf)});
} else if (currentBrowser.chrome) {
return chrome.storage.local.set( {'uwLogger': JSON.stringify(conf)});
}
browser.storage.local.set( {'uwLogger': JSON.stringify(conf)});
}
static syncConfig(callback) {
const br = currentBrowser.firefox ? browser : chrome;
br.storage.onChanged.addListener( (changes, area) => {
static syncConfig(callback: (x) => void) {
browser.storage.onChanged.addListener( (changes, area) => {
if (changes.uwLogger) {
const newLoggerConf = JSON.parse(changes.uwLogger.newValue)
if (process.env.CHANNEL === 'dev') {
console.info('Logger settings reloaded. New conf:', conf);
console.info('Logger settings reloaded. New conf:', newLoggerConf);
}
callback(newLoggerConf);
}
@ -46,17 +116,13 @@ class Logger {
static async getConfig() {
let ret;
if (currentBrowser.firefox) {
// if (BrowserDetect.firefox) {
ret = await browser.storage.local.get('uwLogger');
} else if (currentBrowser.chrome) {
ret = await new Promise( (resolve, reject) => {
chrome.storage.local.get('uwLogger', (res) => resolve(res));
});
} else if (currentBrowser.edge) {
ret = await new Promise( (resolve, reject) => {
browser.storage.local.get('uwLogger', (res) => resolve(res));
});
}
// } else if (BrowserDetect.anyChromium) {
// ret = await new Promise( (resolve, reject) => {
// browser.storage.local.get('uwLogger', (res) => resolve(res));
// });
// }
if (process.env.CHANNEL === 'dev') {
try {
@ -73,7 +139,7 @@ class Logger {
}
}
async init(conf) {
async init(conf: LoggerConfig) {
// this is the only property that always gets passed via conf
// and doesn't get ignored even if the rest of the conf gets
// loaded from browser storage
@ -101,9 +167,7 @@ class Logger {
this.temp_disable = false;
this.stopTime = this.conf.timeout ? performance.now() + (this.conf.timeout * 1000) : undefined;
const br = currentBrowser.firefox ? browser : chrome;
br.storage.onChanged.addListener( (changes, area) => {
browser.storage.onChanged.addListener( (changes, area) => {
if (process.env.CHANNEL === 'dev') {
if (!changes.uwLogger) {
// console.info('[Logger::<storage/on change> No new logger settings!');
@ -131,7 +195,7 @@ class Logger {
}
clear() {
this.log = [];
this.history = [];
this.startTime = performance.now();
this.stopTime = this.conf.timeout ? performance.now() + (this.conf.timeout * 1000) : undefined;
}
@ -142,9 +206,9 @@ class Logger {
Logger.saveConfig(conf);
}
async getSaved() {
return Logger.getSaved();
}
// async getSaved() {
// return Logger.getSaved();
// }
// allow syncing of start times between bg and page scripts.
@ -174,7 +238,7 @@ class Logger {
getFileLogJSONString() {
return {
site: window && window.location,
log: JSON.toString(this.history),
log: JSON.stringify(this.history),
}
}
@ -213,7 +277,7 @@ class Logger {
parseStack() {
const trace = (new Error()).stack;
const stackInfo = {};
const stackInfo: any = {};
// we turn our stack into array and remove the "file::line" part of the trace,
// since that is useless because minification/webpack
stackInfo['stack'] = {trace: trace.split('\n').map(a => a.split('@')[0])};
@ -336,7 +400,7 @@ class Logger {
}
}
canLogConsole(component, stackInfo) {
canLogConsole(component, stackInfo?) {
if (!this.conf.consoleOptions?.enabled || this.temp_disable) {
return false;
}
@ -437,7 +501,7 @@ class Logger {
let ts = performance.now();
let secondMark = ts - 1000;
let halfSecondMark = ts - 500;
let i = this.history.length();
let i = this.history.length;
// correct ts _after_ secondMark and halfSecondMark were determined
if (ts <= this.history[this.history.length - 1]) {

View File

@ -4,7 +4,7 @@ class ObjectCopy {
static addNew(current, newValues){
// clone target
var out = JSON.parse(JSON.stringify(newValues));
let out = JSON.parse(JSON.stringify(newValues));
if(! current) {
if(Debug.debug) {
@ -14,7 +14,7 @@ class ObjectCopy {
return out;
}
for(var k in out) {
for(let k in out) {
// if current key exist, replace it with existing value. Take no action otherwise.
if(current[k]) {
@ -37,7 +37,7 @@ class ObjectCopy {
// add the values that would otherwise be deleted back to our object. (We need that so user-defined
// sites don't get forgotten)
for(var k in current) {
for(let k in current) {
if (! out[k]) {
out[k] = current[k];
}
@ -47,7 +47,7 @@ class ObjectCopy {
}
static overwrite(current, newValues){
for(var k in newValues) {
for(let k in newValues) {
// if current key exist, replace it with existing value. Take no action otherwise.
if (current[k] !== undefined) {
// Types and constructors of objects must match. If they don't, we always use the new value.

View File

@ -1,19 +1,41 @@
import Debug from '../conf/Debug';
import currentBrowser from '../conf/BrowserDetect';
import ExtensionConf from '../conf/ExtensionConf';
import ExtensionMode from '../../common/enums/extension-mode.enum';
import ObjectCopy from '../lib/ObjectCopy';
import Stretch from '../../common/enums/stretch.enum';
import VideoAlignment from '../../common/enums/video-alignment.enum';
import ExtensionMode from '../../common/enums/ExtensionMode.enum';
import ObjectCopy from './ObjectCopy';
import StretchType from '../../common/enums/StretchType.enum';
import VideoAlignmentType from '../../common/enums/VideoAlignmentType.enum';
import ExtensionConfPatch from '../conf/ExtConfPatches';
import CropModePersistence from '../../common/enums/crop-mode-persistence.enum';
import CropModePersistence from '../../common/enums/CropModePersistence.enum';
import BrowserDetect from '../conf/BrowserDetect';
import Logger from './Logger';
import SettingsInterface from '../../common/interfaces/SettingsInterface';
import { browser } from 'webextension-polyfill-ts';
if(process.env.CHANNEL !== 'stable'){
console.info("Loading Settings");
}
class Settings {
//#region flags
useSync: boolean = false;
version: string;
//#endregion
//#region helper classes
logger: Logger;
//#endregion
//#region data
default: SettingsInterface; // default settings
active: SettingsInterface; // currently active settings
//#endregion
//#region callbacks
onSettingsChanged: any;
afterSettingsSaved: any;
//#endregion
constructor(options) {
// Options: activeSettings, updateCallback, logger
@ -23,14 +45,8 @@ class Settings {
this.active = options?.activeSettings ?? undefined;
this.default = ExtensionConf;
this.default['version'] = this.getExtensionVersion();
this.useSync = false;
this.version = undefined;
if (currentBrowser.firefox) {
browser.storage.onChanged.addListener((changes, area) => {this.storageChangeListener(changes, area)});
} else if (currentBrowser.chrome) {
chrome.storage.onChanged.addListener((changes, area) => {this.storageChangeListener(changes, area)});
}
}
storageChangeListener(changes, area) {
@ -60,14 +76,10 @@ class Settings {
}
}
static getExtensionVersion() {
if (currentBrowser.firefox) {
static getExtensionVersion(): string {
return browser.runtime.getManifest().version;
} else if (currentBrowser.chrome) {
return chrome.runtime.getManifest().version;
}
}
getExtensionVersion() {
getExtensionVersion(): string {
return Settings.getExtensionVersion();
}
@ -96,7 +108,10 @@ class Settings {
// also, the fourth digit can start with a letter.
// versions that start with a letter are ranked lower than
// versions x.x.x.0
if (isNaN(+aa[3]) ^ isNaN(+bb[3])) {
if (
(isNaN(+aa[3]) && !isNaN(+bb[3]))
|| (!isNaN(+aa[3]) && isNaN(+bb[3]))
) {
return isNaN(+aa[3]) ? -1 : 1;
}
@ -140,8 +155,6 @@ class Settings {
const sorted = this.sortConfPatches(extconfPatches);
return sorted.findIndex(x => this.compareExtensionVersions(x.forVersion, version) > 0);
}
applySettingsPatches(oldVersion, patches) {
let index = this.findFirstNecessaryPatch(oldVersion, patches);
if (index === -1) {
@ -216,16 +229,6 @@ class Settings {
// if there's settings, set saved object as active settings
this.active = settings;
// if last saved settings was for version prior to 4.x, we reset settings to default
// it's not like people will notice cos that version didn't preserve settings at all
if (this.active.version && !settings.version.startsWith('4')) {
this.active = this.getDefaultSettings();
this.active.version = this.version;
await this.save();
return this.active;
}
// if version number is undefined, we make it defined
// this should only happen on first extension initialization
if (!this.active.version) {
@ -268,17 +271,7 @@ class Settings {
async get() {
let ret;
if (currentBrowser.firefox) {
ret = await browser.storage.local.get('uwSettings');
} else if (currentBrowser.chrome) {
ret = await new Promise( (resolve, reject) => {
chrome.storage.local.get('uwSettings', (res) => resolve(res));
});
} else if (currentBrowser.edge) {
ret = await new Promise( (resolve, reject) => {
browser.storage.local.get('uwSettings', (res) => resolve(res));
});
}
this.logger?.log('info', 'settings', 'Got settings:', ret && ret.uwSettings && JSON.parse(ret.uwSettings));
@ -301,10 +294,10 @@ class Settings {
sites[site].autoar = ExtensionMode.Default;
}
if (sites[site].stretch === undefined) {
sites[site].stretch = Stretch.Default;
sites[site].stretch = StretchType.Default;
}
if (sites[site].videoAlignment === undefined) {
sites[site].videoAlignment = VideoAlignment.Default;
sites[site].videoAlignment = VideoAlignmentType.Default;
}
if (sites[site].keyboardShortcutsEnabled === undefined) {
sites[site].keyboardShortcutsEnabled = ExtensionMode.Default;
@ -312,7 +305,7 @@ class Settings {
}
}
async set(extensionConf, options) {
async set(extensionConf, options?) {
if (!options || !options.forcePreserveVersion) {
extensionConf.version = this.version;
}
@ -321,11 +314,7 @@ class Settings {
this.logger?.log('info', 'settings', "[Settings::set] setting new settings:", extensionConf)
if (BrowserDetect.firefox) {
return browser.storage.local.set( {'uwSettings': JSON.stringify(extensionConf)});
} else {
return chrome.storage.local.set( {'uwSettings': JSON.stringify(extensionConf)});
}
}
async setActive(activeSettings) {
@ -336,7 +325,7 @@ class Settings {
this.active[prop] = value;
}
async save(options) {
async save(options?) {
if (Debug.debug && Debug.storage) {
console.log("[Settings::save] Saving active settings:", this.active);
}
@ -389,7 +378,15 @@ class Settings {
return this.active.actions;
}
getExtensionMode(site) {
getSettingsForSite(site?) {
if (!site) {
site = window.location.hostname;
}
return this.active.sites[site];
}
getExtensionMode(site?: string) {
if (!site) {
site = window.location.hostname;
@ -487,14 +484,14 @@ class Settings {
}
extensionEnabled(){
return this.active.sites['@global'] !== ExtensionMode.Disabled
return this.active.sites['@global'].mode !== ExtensionMode.Disabled
}
extensionEnabledForSite(site) {
return this.canStartExtension(site);
}
canStartAutoAr(site) {
canStartAutoAr(site?: string) {
// 'site' argument is only ever used when calling this function recursively for debugging
if (!site) {
site = window.location.hostname;
@ -539,13 +536,13 @@ class Settings {
}
}
getDefaultOption(option) {
getDefaultOption(option?) {
const allDefault = {
mode: ExtensionMode.Default,
autoar: ExtensionMode.Default,
autoarFallback: ExtensionMode.Default,
stretch: Stretch.Default,
videoAlignment: VideoAlignment.Default,
stretch: StretchType.Default,
videoAlignment: VideoAlignmentType.Default,
};
if (!option || allDefault[option] === undefined) {
@ -565,7 +562,7 @@ class Settings {
}
getDefaultStretchMode(site) {
if (site && (this.active.sites[site]?.stretch ?? Stretch.Default) !== Stretch.Default) {
if (site && (this.active.sites[site]?.stretch ?? StretchType.Default) !== StretchType.Default) {
return this.active.sites[site].stretch;
}
@ -573,7 +570,7 @@ class Settings {
}
getDefaultCropPersistenceMode(site) {
if (site && (this.active.sites[site]?.cropModePersistence ?? Stretch.Default) !== Stretch.Default) {
if (site && (this.active.sites[site]?.cropModePersistence ?? StretchType.Default) !== StretchType.Default) {
return this.active.sites[site].cropModePersistence;
}
@ -582,7 +579,7 @@ class Settings {
}
getDefaultVideoAlignment(site) {
if ( (this.active.sites[site]?.videoAlignment ?? VideoAlignment.Default) !== VideoAlignment.Default) {
if ( (this.active.sites[site]?.videoAlignment ?? VideoAlignmentType.Default) !== VideoAlignmentType.Default) {
return this.active.sites[site].videoAlignment;
}

View File

@ -1,3 +0,0 @@
export async function sleep(timeout) {
return new Promise( (resolve, reject) => setTimeout(() => resolve(), timeout));
}

View File

@ -1,3 +1,3 @@
export async function sleep(timeout) {
return new Promise( (resolve, reject) => setTimeout(() => resolve(), timeout));
return new Promise( (resolve, reject) => setTimeout(() => resolve(null), timeout));
}

View File

@ -6,12 +6,56 @@ import EdgeDetectPrimaryDirection from './edge-detect/enums/EdgeDetectPrimaryDir
import EdgeDetectQuality from './edge-detect/enums/EdgeDetectQualityEnum';
import GuardLine from './GuardLine';
// import DebugCanvas from './DebugCanvas';
import VideoAlignment from '../../../common/enums/video-alignment.enum';
import AspectRatio from '../../../common/enums/aspect-ratio.enum';
import {sleep} from '../../lib/Util';
import VideoAlignmentType from '../../../common/enums/VideoAlignmentType.enum';
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import {sleep} from '../Util';
import BrowserDetect from '../../conf/BrowserDetect';
import Logger from '../Logger';
import VideoData from '../video-data/VideoData';
import Settings from '../Settings';
class ArDetector {
logger: Logger;
conf: VideoData;
video: HTMLVideoElement;
settings: Settings;
guardLine: GuardLine;
edgeDetector: EdgeDetect;
setupTimer: any;
sampleCols: any[];
sampleLines
canFallback: boolean = true;
fallbackMode: boolean = false;
blackLevel: number;
arid: string;
// ar detector starts in this state. running main() sets both to false
_paused: boolean;
_halted: boolean = true;
_exited: boolean = true;
private manualTickEnabled: boolean;
_nextTick: boolean;
canDoFallbackMode: boolean = false;
// helper objects
private attachedCanvas: HTMLCanvasElement;
canvas: HTMLCanvasElement;
private blackframeCanvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private blackframeContext: CanvasRenderingContext2D;
private canvasScaleFactor: number;
private detectionTimeoutEventCount: number;
canvasImageDataRowLength: number;
private noLetterboxCanvasReset: boolean;
private detectedAr: any;
private canvasDrawWindowHOffset: number;
private sampleCols_current: number;
constructor(videoData){
this.logger = videoData.logger;
@ -19,34 +63,18 @@ class ArDetector {
this.video = videoData.video;
this.settings = videoData.settings;
this.setupTimer = null;
this.sampleCols = [];
this.canFallback = true;
this.fallbackMode = false;
this.blackLevel = this.settings.active.arDetect.blackbar.blackLevel;
this.arid = (Math.random()*100).toFixed();
// ar detector starts in this state. running main() sets both to false
this._halted = true;
this._exited = true;
// we can tick manually, for debugging
this._manualTicks = false;
this._nextTick = false;
this.canDoFallbackMode = false;
this.drmNotificationShown = false;
this.logger.log('info', 'init', `[ArDetector::ctor] creating new ArDetector. arid: ${this.arid}`);
}
setManualTick(manualTick) {
this._manualTicks = manualTick;
this.manualTickEnabled = manualTick;
}
tick() {
@ -68,12 +96,12 @@ class ArDetector {
}
destroy(){
this.logger.log('info', 'init', `%c[ArDetect::destroy] <@${this.arid}> Destroying aard.`, _ard_console_stop, e);
this.logger.log('info', 'init', `%c[ArDetect::destroy] <@${this.arid}> Destroying aard.`, _ard_console_stop);
// this.debugCanvas.destroy();
this.stop();
}
setup(cwidth, cheight){
setup(cwidth?: number, cheight?: number){
this.logger.log('info', 'init', `[ArDetect::setup] <@${this.arid}> Starting autodetection setup.`);
//
// [-1] check for zero-width and zero-height videos. If we detect this, we kick the proverbial
@ -133,16 +161,16 @@ class ArDetector {
// [2] determine places we'll use to sample our main frame
//
var ncol = this.settings.active.arDetect.sampling.staticCols;
var nrow = this.settings.active.arDetect.sampling.staticRows;
let ncol = this.settings.active.arDetect.sampling.staticCols;
let nrow = this.settings.active.arDetect.sampling.staticRows;
var colSpacing = this.canvas.width / ncol;
var rowSpacing = (this.canvas.height << 2) / nrow;
let colSpacing = this.canvas.width / ncol;
let rowSpacing = (this.canvas.height << 2) / nrow;
this.sampleLines = [];
this.sampleCols = [];
for(var i = 0; i < ncol; i++){
for(let i = 0; i < ncol; i++){
if(i < ncol - 1)
this.sampleCols.push(Math.round(colSpacing * i));
else{
@ -150,7 +178,7 @@ class ArDetector {
}
}
for(var i = 0; i < nrow; i++){
for(let i = 0; i < nrow; i++){
if(i < ncol - 5)
this.sampleLines.push(Math.round(rowSpacing * i));
else{
@ -173,7 +201,7 @@ class ArDetector {
//
try {
this.blackframeContext.drawWindow(window,0, 0, this.blackframeCanvas.width, this.blackframeCanvas.height, "rgba(0,0,128,1)");
(this.blackframeContext as any).drawWindow(window,0, 0, this.blackframeCanvas.width, this.blackframeCanvas.height, "rgba(0,0,128,1)");
this.canDoFallbackMode = true;
} catch (e) {
this.canDoFallbackMode = false;
@ -187,7 +215,7 @@ class ArDetector {
this.resetBlackLevel();
// if we're restarting ArDetect, we need to do this in order to force-recalculate aspect ratio
this.conf.resizer.setLastAr({type: AspectRatio.Automatic, ratio: this.getDefaultAr()});
this.conf.resizer.setLastAr({type: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
this.canvasImageDataRowLength = cwidth << 2;
this.noLetterboxCanvasReset = false;
@ -212,9 +240,9 @@ class ArDetector {
return;
}
if (this.conf.resizer.lastAr.type === AspectRatio.Automatic) {
if (this.conf.resizer.lastAr.type === AspectRatioType.Automatic) {
// ensure first autodetection will run in any case
this.conf.resizer.setLastAr({type: AspectRatio.Automatic, ratio: this.getDefaultAr()});
this.conf.resizer.setLastAr({type: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
}
@ -292,7 +320,7 @@ class ArDetector {
// state from 'paused' to 'playing', we don't need to wait for the rest of the longer
// paused state timeout to finish.
if ( (!this._manualTicks && this.canTriggerFrameCheck(lastFrameCheckStartTime)) || this._nextTick) {
if ( (!this.manualTickEnabled && this.canTriggerFrameCheck(lastFrameCheckStartTime)) || this._nextTick) {
this._nextTick = false;
lastFrameCheckStartTime = Date.now();
@ -344,7 +372,7 @@ class ArDetector {
}
scheduleInitRestart(timeout, force_reset){
scheduleInitRestart(timeout?: number, force_reset?: boolean){
if(! timeout){
timeout = 100;
}
@ -353,7 +381,7 @@ class ArDetector {
clearTimeout(this.setupTimer);
}
var ths = this;
let ths = this;
this.setupTimer = setTimeout(function(){
ths.setupTimer = null;
try{
@ -391,7 +419,7 @@ class ArDetector {
}
getTimeout(baseTimeout, startTime){
var execTime = (performance.now() - startTime);
let execTime = (performance.now() - startTime);
return baseTimeout;
}
@ -447,12 +475,12 @@ class ArDetector {
// letterbox also needs to be corrected:
// letterbox += [video.zoomedHeight] - [video.unzoomedHeight]
var vbr = this.video.getBoundingClientRect();
let vbr = this.video.getBoundingClientRect();
zoomFactor = vbr.height / this.video.clientHeight;
letterbox += vbr.height - this.video.clientHeight;
var trueHeight = this.canvas.height * zoomFactor - letterbox;
let trueHeight = this.canvas.height * zoomFactor - letterbox;
if(edges.top > 1 && edges.top <= this.settings.active.arDetect.fallbackMode.noTriggerZonePx ){
this.logger.log('info', 'arDetect', `%c[ArDetect::calculateArFromEdges] <@${this.arid}> Edge is in the no-trigger zone. Aspect ratio change is not triggered.`)
@ -474,23 +502,23 @@ class ArDetector {
// poglejmo, če se je razmerje stranic spremenilo
// check if aspect ratio is changed:
var lastAr = this.conf.resizer.getLastAr();
if (lastAr.type === AspectRatio.Automatic && lastAr.ratio !== null && lastAr.ratio !== undefined){
let lastAr = this.conf.resizer.getLastAr();
if (lastAr.type === AspectRatioType.Automatic && lastAr.ratio !== null && lastAr.ratio !== undefined){
// spremembo lahko zavrnemo samo, če uporabljamo avtomatski način delovanja in če smo razmerje stranic
// že nastavili.
//
// we can only deny aspect ratio changes if we use automatic mode and if aspect ratio was set from here.
var arDiff = trueAr - lastAr.ar;
let arDiff = trueAr - lastAr.ratio;
if (arDiff < 0)
arDiff = -arDiff;
var arDiff_percent = arDiff / trueAr;
const arDiff_percent = arDiff / trueAr;
// ali je sprememba v mejah dovoljenega? Če da -> fertik
// is ar variance within acceptable levels? If yes -> we done
this.logger.log('info', 'arDetect', `%c[ArDetect::processAr] <@${this.arid}> New aspect ratio varies from the old one by this much:\n`,"color: #aaf","old Ar", lastAr.ar, "current ar", trueAr, "arDiff (absolute):",arDiff,"ar diff (relative to new ar)", arDiff_percent);
this.logger.log('info', 'arDetect', `%c[ArDetect::processAr] <@${this.arid}> New aspect ratio varies from the old one by this much:\n`,"color: #aaf","old Ar", lastAr.ratio, "current ar", trueAr, "arDiff (absolute):",arDiff,"ar diff (relative to new ar)", arDiff_percent);
if (arDiff < trueAr * this.settings.active.arDetect.allowedArVariance){
this.logger.log('info', 'arDetect', `%c[ArDetect::processAr] <@${this.arid}> Aspect ratio change denied — diff %: ${arDiff_percent}`, "background: #740; color: #fa2");
@ -500,35 +528,16 @@ class ArDetector {
}
this.logger.log('info', 'debug', `%c[ArDetect::processAr] <@${this.arid}> Triggering aspect ratio change. New aspect ratio: ${trueAr}`, _ard_console_change);
this.conf.resizer.updateAr({type: AspectRatio.Automatic, ratio: trueAr}, {type: AspectRatio.Automatic, ratio: trueAr});
this.conf.resizer.updateAr({type: AspectRatioType.Automatic, ratio: trueAr});
}
clearImageData(id) {
if (ArrayBuffer.transfer) {
ArrayBuffer.transfer(id, 0);
if ((ArrayBuffer as any).transfer) {
(ArrayBuffer as any).transfer(id, 0);
}
id = undefined;
}
/**
* Checks whether video we're trying to play is protected by DRM.
*
* When trying to get an image frame of a DRM-protected video in
* firefox, the method canvas.drawImage(video) will throw an exception.
*
* This doesn't happen in Chrome. As opposed to Firefox, chrome will
* simply draw a transparent black image and not tell anyone that
* anything is amiss. However, since the image is (according to my testing
* on netflix) completely transparent, this means we can determine whether
* the video is DRM-protected by looking at the alpha byte of the image.
*
* (Videos don't tend to have an alpha channel, so they're always
* completely opaque (i.e. have value of 255))
*/
hasDRM() {
return this.blackframeContext.getImageData(0,0,1,1).data[3] === 0;
}
frameCheck(){
if(! this.video){
this.logger.log('error', 'debug', `%c[ArDetect::frameCheck] <@${this.arid}> Video went missing. Destroying current instance of videoData.`);
@ -540,7 +549,7 @@ class ArDetector {
this.init();
}
var startTime = performance.now();
let startTime = performance.now();
let sampleCols = this.sampleCols.slice(0);
//
@ -550,31 +559,29 @@ class ArDetector {
this.blackframeContext.drawImage(this.video, 0, 0, this.blackframeCanvas.width, this.blackframeCanvas.height);
// special browsers require special tests
if (this.hasDRM()) {
this.fallbackMode = false;
throw 'Video is protected by DRM. Autodetection cannot run here.';
}
// if (this.hasDRM()) {
// this.fallbackMode = false;
// throw 'VIDEO_DRM_PROTECTED';
// }
this.fallbackMode = false;
} catch (e) {
this.logger.log('error', 'arDetect', `%c[ArDetect::frameCheck] <@${this.arid}> %c[ArDetect::frameCheck] can't draw image on canvas. ${this.canDoFallbackMode ? 'Trying canvas.drawWindow instead' : 'Doing nothing as browser doesn\'t support fallback mode.'}`, "color:#000; backgroud:#f51;", e);
// nothing to see here, really, if fallback mode isn't supported by browser
if (!this.drmNotificationShown) {
this.drmNotificationShown = true;
// if we detect Edge, we'll throw the aggressive popup
this.conf.player.showEdgeNotification();
this.conf.player.showNotification('AARD_DRM');
this.conf.resizer.setAr({type: AspectRatio.Reset});
return;
}
// in case of DRM errors, we need to prevent the execution to reach the aspec
// ratio setting part of this function
if (e === 'Video is protected by DRM. Autodetection cannot run here.') {
return;
}
// ratio setting part of this function. For the time being, we're only stopping
// in case we encounter DRM error in Chrome. Firefox has fallback mode and generates
// different error, so that goes.
// if (e === 'VIDEO_DRM_PROTECTED') {
// // nothing to see here, really, if fallback mode isn't supported by browser
// if (!this.drmNotificationShown) {
// this.drmNotificationShown = true;
// this.conf.player.showNotification('AARD_DRM');
// this.conf.resizer.setAr({type: AspectRatio.Reset});
// }
// return;
// }
if (! this.canvasReadyForDrawWindow()) {
// this means canvas needs to be resized, so we'll just re-run setup with all those new parameters
@ -583,9 +590,9 @@ class ArDetector {
let newCanvasWidth = window.innerHeight * (this.video.videoWidth / this.video.videoHeight);
let newCanvasHeight = window.innerHeight;
if (this.conf.resizer.videoAlignment === VideoAlignment.Center) {
if (this.conf.resizer.videoAlignment === VideoAlignmentType.Center) {
this.canvasDrawWindowHOffset = Math.round((window.innerWidth - newCanvasWidth) * 0.5);
} else if (this.conf.resizer.videoAlignment === VideoAlignment.Left) {
} else if (this.conf.resizer.videoAlignment === VideoAlignmentType.Left) {
this.canvasDrawWindowHOffset = 0;
} else {
this.canvasDrawWindowHOffset = window.innerWidth - newCanvasWidth;
@ -600,7 +607,7 @@ class ArDetector {
this.fallbackMode = true;
try {
this.context.drawWindow(window, this.canvasDrawWindowHOffset, 0, this.canvas.width, this.canvas.height, "rgba(0,0,128,1)");
(this.context as any).drawWindow(window, this.canvasDrawWindowHOffset, 0, this.canvas.width, this.canvas.height, "rgba(0,0,128,1)");
} catch (e) {
this.logger.log('error', 'arDetect', `%c[ArDetect::frameCheck] can't draw image on canvas with fallback mode either. This error is prolly only temporary.`, "color:#000; backgroud:#f51;", e);
return; // it's prolly just a fluke, so we do nothing special here
@ -632,7 +639,7 @@ class ArDetector {
// da je letterbox izginil.
// If we don't detect letterbox, we reset aspect ratio to aspect ratio of the video file. The aspect ratio could
// have been corrected manually. It's also possible that letterbox (that was there before) disappeared.
this.conf.resizer.updateAr({type: AspectRatio.Automatic, ratio: this.getDefaultAr()});
this.conf.resizer.updateAr({type: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
this.guardLine.reset();
this.noLetterboxCanvasReset = true;
@ -665,14 +672,16 @@ class ArDetector {
// otherwise we continue. We add blackbar violations to the list of the cols
// we'll sample and sort them
if (guardLineOut.blackbarFail) {
sampleCols.concat(guardLineOut.offenders).sort((a, b) => a > b);
sampleCols.concat(guardLineOut.offenders).sort(
(a: number, b: number) => a - b
);
}
// if we're in fallback mode and blackbar test failed, we restore CSS and quit
// (since the new letterbox edge isn't present in our sample due to technical
// limitations)
if (this.fallbackMode && guardLineOut.blackbarFail) {
this.conf.resizer.setAr({type: AspectRatio.Automatic, ratio: this.getDefaultAr()});
this.conf.resizer.setAr({type: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
this.guardLine.reset();
this.noLetterboxCanvasReset = true;
@ -692,17 +701,15 @@ class ArDetector {
// that we will cut too much, we rather avoid doing anything at all. There's gonna be a next chance.
try{
if(guardLineOut.blackbarFail || guardLineOut.imageFail){
if(this.edgeDetector.findBars(imageData, null, EdgeDetectPrimaryDirection.HORIZONTAL).status === 'ar_known'){
if(this.edgeDetector.findBars(imageData, null, EdgeDetectPrimaryDirection.Horizontal).status === 'ar_known'){
if(guardLineOut.blackbarFail){
this.logger.log('info', 'arDetect', `[ArDetect::frameCheck] Detected blackbar violation and pillarbox. Resetting to default aspect ratio.`);
this.conf.resizer.setAr({type: AspectRatio.Automatic, ratio: this.getDefaultAr()});
this.conf.resizer.setAr({type: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
this.guardLine.reset();
}
triggerTimeout = this.getTimeout(baseTimeout, startTime);
this.scheduleFrameCheck(triggerTimeout);
this.clearImageData(imageData);
return;
}
@ -717,20 +724,20 @@ class ArDetector {
// blackSamples -> {res_top, res_bottom}
var edgePost = this.edgeDetector.findBars(imageData, sampleCols, EdgeDetectPrimaryDirection.VERTICAL, EdgeDetectQuality.IMPROVED, guardLineOut, bfanalysis);
let edgePost = this.edgeDetector.findBars(imageData, sampleCols, EdgeDetectPrimaryDirection.Vertical, EdgeDetectQuality.Improved, guardLineOut, bfanalysis);
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] edgeDetector returned this\n`, "color: #aaf", edgePost);
if (edgePost.status !== EdgeStatus.AR_KNOWN){
if (edgePost.status !== EdgeStatus.ARKnown){
// rob ni bil zaznan, zato ne naredimo ničesar.
// no edge was detected. Let's leave things as they were
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] Edge wasn't detected with findBars`, "color: #fa3", edgePost, "EdgeStatus.AR_KNOWN:", EdgeStatus.AR_KNOWN);
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] Edge wasn't detected with findBars`, "color: #fa3", edgePost, "EdgeStatus.AR_KNOWN:", EdgeStatus.ARKnown);
this.clearImageData(imageData);
return;
}
var newAr = this.calculateArFromEdges(edgePost);
let newAr = this.calculateArFromEdges(edgePost);
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] Triggering aspect ration change! new ar: ${newAr}`, "color: #aaf");
@ -762,14 +769,14 @@ class ArDetector {
this.logger.log('error', 'arDetect', `%c[ArDetect::frameCheck] There was a problem setting blackbar. Doing nothing. Error:`, e);
try {
this.guardline.reset();
this.guardLine.reset();
} catch (e) {
// no guardline, no bigge
}
// WE DO NOT RESET ASPECT RATIO HERE IN CASE OF PROBLEMS, CAUSES UNWARRANTED RESETS:
// (eg. here: https://www.youtube.com/watch?v=nw5Z93Yt-UQ&t=410)
//
// this.conf.resizer.setAr({type: AspectRatio.Automatic, ratio: this.getDefaultAr()});
// this.conf.resizer.setAr({type: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
}
this.clearImageData(imageData);
@ -801,7 +808,7 @@ class ArDetector {
let cumulativeValue = 0;
let blackPixelCount = 0;
const bfImageData = this.blackframeContext.getImageData(0, 0, cols, rows).data;
const blackTreshold = this.blackLevel + this.settings.active.arDetect.blackbar.frameTreshold;
const blackTreshold = this.blackLevel + this.settings.active.arDetect.blackbar.frameThreshold;
// we do some recon for letterbox and pillarbox. While this can't determine whether letterbox/pillarbox exists
@ -809,7 +816,7 @@ class ArDetector {
let rowMax = new Array(rows).fill(0);
let colMax = new Array(cols).fill(0);
let r, c;
let r: number, c: number;
for (let i = 0; i < bfImageData.length; i+= 4) {
@ -923,7 +930,7 @@ class ArDetector {
// detect black level. if currentMax comes above blackbar + blackbar threshold, we know we aren't letterboxed
for (var i = 0; i < sampleCols.length; ++i){
for (let i = 0; i < sampleCols.length; ++i){
colOffset_r = sampleCols[i] << 2;
colOffset_g = colOffset_r + 1;
colOffset_b = colOffset_r + 2;
@ -959,8 +966,8 @@ class ArDetector {
}
var _ard_console_stop = "background: #000; color: #f41";
var _ard_console_start = "background: #000; color: #00c399";
var _ard_console_change = "background: #000; color: #ff8";
let _ard_console_stop = "background: #000; color: #f41";
let _ard_console_start = "background: #000; color: #00c399";
let _ard_console_change = "background: #000; color: #ff8";
export default ArDetector;

View File

@ -0,0 +1,12 @@
/**
* Checks whether video we're trying to play is protected by DRM.
* @param {*} video video we're trying to check
*/
export function hasDrm(video) {
// if video is not playing, we cannot know whether autodetection will work or not
if (!video || !(video.currentTime > 0 && !video.paused && !video.ended && video.readyState > 2)) {
return undefined;
}
return video.mediaKeys instanceof MediaKeys;
}

View File

@ -1,13 +1,33 @@
import Debug from '../../conf/Debug';
import Settings from '../Settings';
import ArDetector from './ArDetector';
export type GuardLineBar = {
top?: number;
bottom?: number;
}
export type ImageCheckResult = {
success: boolean;
}
class GuardLine {
blackbar: GuardLineBar;
imageBar: GuardLineBar;
aard: ArDetector;
settings: Settings;
blackbarThreshold: number;
imageThreshold: number;
// ardConf — reference to ArDetector that has current GuardLine instance
constructor (ardConf){
this.blackbar = {top: undefined, bottom: undefined};
this.imageBar = {top: undefined, bottom: undefined};
this.conf = ardConf;
this.aard = ardConf;
this.settings = ardConf.settings;
}
@ -28,12 +48,12 @@ class GuardLine {
}
setBlackbar(bbconf){
var bbTop = bbconf.top - this.settings.active.arDetect.guardLine.edgeTolerancePx;
var bbBottom = bbconf.bottom + this.settings.active.arDetect.guardLine.edgeTolerancePx;
let bbTop = bbconf.top - this.settings.active.arDetect.guardLine.edgeTolerancePx;
let bbBottom = bbconf.bottom + this.settings.active.arDetect.guardLine.edgeTolerancePx;
// to odstrani vse neveljavne nastavitve in vse možnosti, ki niso smiselne
// this removes any configs with invalid values or values that dont make sense
if (bbTop < 0 || bbBottom >= this.conf.canvas.height ){
if (bbTop < 0 || bbBottom >= this.aard.canvas.height ){
throw {error: "INVALID_SETTINGS_IN_GUARDLINE", bbTop, bbBottom}
}
@ -51,12 +71,12 @@ class GuardLine {
check(image, fallbackMode){
// izračunaj enkrat in shrani na objekt
// calculate once and save object-instance-wide
this.blackbarThreshold = this.conf.blackLevel + this.settings.active.arDetect.blackbar.threshold;
this.blackbarThreshold = this.aard.blackLevel + this.settings.active.arDetect.blackbar.threshold;
this.imageThreshold = this.blackbarThreshold + this.settings.active.arDetect.blackbar.imageThreshold;
// dejansko testiranje
// actual checks
var guardLineResult = this.guardLineCheck(image, fallbackMode);
let guardLineResult = this.guardLineCheck(image, fallbackMode);
// Zaznali smo kršitev črnega dela, zato nam ni treba preveriti, ali je slika
// prisotna. Vemo namreč, da se je razmerje stranic zmanjšalo.
@ -71,7 +91,7 @@ class GuardLine {
}
}
var imageCheckResult = this.imageCheck(image, fallbackMode);
let imageCheckResult = this.imageCheck(image, fallbackMode);
return {
blackbarFail: false,
@ -95,45 +115,46 @@ class GuardLine {
return { success: true };
}
var offset = parseInt(this.conf.canvas.width * this.settings.active.arDetect.guardLine.ignoreEdgeMargin) << 2;
let offset = (this.aard.canvas.width * this.settings.active.arDetect.guardLine.ignoreEdgeMargin) << 2;
var offenders = [];
var offenderCount = -1; // doing it this way means first offender has offenderCount==0. Ez index.
let offenders = [];
let offenderCount = -1; // doing it this way means first offender has offenderCount==0. Ez index.
// TODO: implement logo check.
// preglejmo obe vrstici
// check both rows
let edge_lower, edge_upper;
if(! fallbackMode){
var edge_upper = this.blackbar.top;
var edge_lower = this.blackbar.bottom;
edge_upper = this.blackbar.top;
edge_lower = this.blackbar.bottom;
}
else {
// fallback mode is a bit different
edge_upper = 0;
edge_lower = this.conf.canvas.height - 1;
edge_lower = this.aard.canvas.height - 1;
}
var rowStart, rowEnd;
let rowStart, rowEnd;
// <<<=======| checking upper row |========>>>
rowStart = ((edge_upper * this.conf.canvas.width) << 2) + offset;
rowEnd = rowStart + ( this.conf.canvas.width << 2 ) - (offset * 2);
rowStart = ((edge_upper * this.aard.canvas.width) << 2) + offset;
rowEnd = rowStart + ( this.aard.canvas.width << 2 ) - (offset * 2);
if (Debug.debugCanvas.enabled && Debug.debugCanvas.guardLine) {
offenderCount = this._gl_debugRowCheck(image, rowStart, rowEnd, offenders, offenderCount);
// offenderCount = this._gl_debugRowCheck(image, rowStart, rowEnd, offenders, offenderCount);
} else {
offenderCount = this._gl_rowCheck(image, rowStart, rowEnd, offenders, offenderCount);
}
// <<<=======| checking lower row |========>>>
rowStart = ((edge_lower * this.conf.canvas.width) << 2) + offset;
rowEnd = rowStart + ( this.conf.canvas.width << 2 ) - (offset * 2);
rowStart = ((edge_lower * this.aard.canvas.width) << 2) + offset;
rowEnd = rowStart + ( this.aard.canvas.width << 2 ) - (offset * 2);
if (Debug.debugCanvas.enabled && Debug.debugCanvas.guardLine) {
offenderCount = this._gl_debugRowCheck(image, rowStart, rowEnd, offenders, offenderCount);
// offenderCount = this._gl_debugRowCheck(image, rowStart, rowEnd, offenders, offenderCount);
} else {
offenderCount = this._gl_rowCheck(image, rowStart, rowEnd, offenders, offenderCount);
}
@ -148,19 +169,19 @@ class GuardLine {
return {success: true};
}
var ret = new Array(offenders.length);
for(var o in offenders){
let ret = new Array(offenders.length);
for(let o in offenders){
ret[o] = offenders[o].x + (offenders[o].width * 0.25);
}
return {success: false, offenders: ret};
}
imageCheck(image){
imageCheck(image, fallbackMode?: boolean): ImageCheckResult {
if(!this.imageBar.top || !this.imageBar.bottom)
return { success: false };
var offset = (this.conf.canvas.width * this.settings.active.arDetect.guardLine.ignoreEdgeMargin) << 2;
let offset = (this.aard.canvas.width * this.settings.active.arDetect.guardLine.ignoreEdgeMargin) << 2;
// TODO: implement logo check.
@ -169,47 +190,47 @@ class GuardLine {
// check both rows - by the rules and definitions, we shouldn't go out of bounds here. no need to check, then
// if(fallbackMode){
// var edge_upper = this.settings.active.arDetect.fallbackMode.noTriggerZonePx;
// var edge_lower = this.conf.canvas.height - this.settings.active.arDetect.fallbackMode.noTriggerZonePx - 1;
// let edge_upper = this.settings.active.arDetect.fallbackMode.noTriggerZonePx;
// let edge_lower = this.conf.canvas.height - this.settings.active.arDetect.fallbackMode.noTriggerZonePx - 1;
// }
// else{
var edge_upper = this.imageBar.top;
var edge_lower = this.imageBar.bottom;
let edge_upper = this.imageBar.top;
let edge_lower = this.imageBar.bottom;
// }
// koliko pikslov rabimo zaznati, da je ta funkcija uspe. Tu dovoljujemo tudi, da so vsi piksli na enem
// robu (eden izmed robov je lahko v celoti črn)
// how many non-black pixels we need to consider this check a success. We only need to detect enough pixels
// on one edge (one of the edges can be black as long as both aren't)
var successThreshold = (this.conf.canvas.width * this.settings.active.arDetect.guardLine.imageTestThreshold);
var rowStart, rowEnd;
let successThreshold = (this.aard.canvas.width * this.settings.active.arDetect.guardLine.imageTestThreshold);
let rowStart, rowEnd;
// <<<=======| checking upper row |========>>>
rowStart = ((edge_upper * this.conf.canvas.width) << 2) + offset;
rowEnd = rowStart + ( this.conf.canvas.width << 2 ) - (offset * 2);
rowStart = ((edge_upper * this.aard.canvas.width) << 2) + offset;
rowEnd = rowStart + ( this.aard.canvas.width << 2 ) - (offset * 2);
var res = false;
let res = false;
if(Debug.debugCanvas.enabled && Debug.debugCanvas.guardLine){
res = this._ti_debugCheckRow(image, rowStart, rowEnd, successThreshold);
// res = this._ti_debugCheckRow(image, rowStart, rowEnd, successThreshold);
} else {
res = this._ti_checkRow(image, rowStart, rowEnd,successThreshold);
}
if(res)
if (res) {
return {success: true};
}
// <<<=======| checking lower row |========>>>
rowStart = ((edge_lower * this.conf.canvas.width) << 2) + offset;
rowStart = ((edge_lower * this.aard.canvas.width) << 2) + offset;
// rowEnd = rowStart + ( this.conf.canvas.width << 2 ) - (offset * 2);
if(Debug.debugCanvas.enabled && Debug.debugCanvas.guardLine){
res = this._ti_debugCheckRow(image, rowStart, rowEnd, successThreshold);
// res = this._ti_debugCheckRow(image, rowStart, rowEnd, successThreshold);
} else {
res = this._ti_checkRow(image, rowStart, rowEnd,successThreshold);
}
@ -222,8 +243,8 @@ class GuardLine {
_gl_rowCheck(image, rowStart, rowEnd, offenders, offenderCount){
var firstOffender = -1;
for(var i = rowStart; i < rowEnd; i+=4){
let firstOffender = -1;
for(let i = rowStart; i < rowEnd; i+=4){
// we track sections that go over what's supposed to be a black line, so we can suggest more
// columns to sample
@ -246,36 +267,36 @@ class GuardLine {
return offenderCount;
}
_gl_debugRowCheck(image, rowStart, rowEnd, offenders, offenderCount){
var firstOffender = -1;
for(var i = rowStart; i < rowEnd; i+=4){
// _gl_debugRowCheck(image, rowStart, rowEnd, offenders, offenderCount){
// let firstOffender = -1;
// for(let i = rowStart; i < rowEnd; i+=4){
// we track sections that go over what's supposed to be a black line, so we can suggest more
// columns to sample
if(image[i] > this.blackbarThreshold || image[i+1] > this.blackbarThreshold || image[i+2] > this.blackbarThreshold){
this.conf.debugCanvas.trace(i, DebugCanvasClasses.VIOLATION);
if(firstOffender < 0){
firstOffender = (i - rowStart) >> 2;
offenderCount++;
offenders.push({x: firstOffender, width: 1});
}
else{
offenders[offenderCount].width++
}
}
else{
this.conf.debugCanvas.trace(i, DebugCanvasClasses.GUARDLINE_BLACKBAR);
// is that a black pixel again? Let's reset the 'first offender'
firstOffender = -1;
}
// // we track sections that go over what's supposed to be a black line, so we can suggest more
// // columns to sample
// if(image[i] > this.blackbarThreshold || image[i+1] > this.blackbarThreshold || image[i+2] > this.blackbarThreshold){
// this.aard.debugCanvas.trace(i, DebugCanvasClasses.VIOLATION);
// if(firstOffender < 0){
// firstOffender = (i - rowStart) >> 2;
// offenderCount++;
// offenders.push({x: firstOffender, width: 1});
// }
// else{
// offenders[offenderCount].width++
// }
// }
// else{
// this.aard.debugCanvas.trace(i, DebugCanvasClasses.GUARDLINE_BLACKBAR);
// // is that a black pixel again? Let's reset the 'first offender'
// firstOffender = -1;
// }
}
// }
return offenderCount;
}
// return offenderCount;
// }
_ti_checkRow(image, rowStart, rowEnd, successThreshold) {
for(var i = rowStart; i < rowEnd; i+=4){
_ti_checkRow(image, rowStart, rowEnd, successThreshold): boolean {
for(let i = rowStart; i < rowEnd; i+=4){
if(image[i] > this.imageThreshold || image[i+1] > this.imageThreshold || image[i+2] > this.imageThreshold){
if(successThreshold --<= 0){
return true;
@ -286,20 +307,20 @@ class GuardLine {
return false;
}
_ti_debugCheckRow(image, rowStart, rowEnd, successThreshold) {
for(var i = rowStart; i < rowEnd; i+=4){
if(image[i] > this.imageThreshold || image[i+1] > this.imageThreshold || image[i+2] > this.imageThreshold){
this.conf.debugCanvas.trace(i, DebugCanvasClasses.GUARDLINE_IMAGE);
if(successThreshold --<= 0){
return true;
}
} else {
this.conf.debugCanvas.trace(i, DebugCanvasClasses.WARN);
}
}
// _ti_debugCheckRow(image, rowStart, rowEnd, successThreshold) {
// for(let i = rowStart; i < rowEnd; i+=4){
// if(image[i] > this.imageThreshold || image[i+1] > this.imageThreshold || image[i+2] > this.imageThreshold){
// this.aard.debugCanvas.trace(i, DebugCanvasClasses.GUARDLINE_IMAGE);
// if(successThreshold --<= 0){
// return true;
// }
// } else {
// this.aard.debugCanvas.trace(i, DebugCanvasClasses.WARN);
// }
// }
return false;
}
// return false;
// }
}
export default GuardLine;

View File

@ -2,13 +2,28 @@ import Debug from '../../../conf/Debug';
import EdgeStatus from './enums/EdgeStatusEnum';
import EdgeDetectQuality from './enums/EdgeDetectQualityEnum';
import EdgeDetectPrimaryDirection from './enums/EdgeDetectPrimaryDirectionEnum';
import AntiGradientMode from '../../../../common/enums/anti-gradient-mode.enum';
import AntiGradientMode from '../../../../common/enums/AntiGradientMode.enum';
import ArDetector from '../ArDetector';
import Logger from '../../Logger';
import Settings from '../../Settings';
if (Debug.debug) {
console.log("Loading EdgeDetect.js");
}
class EdgeDetect{
conf: ArDetector;
logger: Logger;
settings: Settings;
// helper stuff
private sampleWidthBase: number;
private halfSample: number;
private detectionThreshold: number;
private colsThreshold: number;
private blackbarThreshold: number;
private imageThreshold: number;
constructor(ardConf){
this.conf = ardConf;
@ -28,27 +43,27 @@ class EdgeDetect{
}
findBars(image, sampleCols, direction = EdgeDetectPrimaryDirection.VERTICAL, quality = EdgeDetectQuality.IMPROVED, guardLineOut, blackFrameAnalysis){
findBars(image, sampleCols, direction = EdgeDetectPrimaryDirection.Vertical, quality = EdgeDetectQuality.Improved, guardLineOut?, blackFrameAnalysis?){
let fastCandidates, edgeCandidates, bars;
if (direction == EdgeDetectPrimaryDirection.VERTICAL) {
if (direction == EdgeDetectPrimaryDirection.Vertical) {
try {
fastCandidates = this.findCandidates(image, sampleCols, guardLineOut);
if (! this.isValidSample(fastCandidates)) {
return {status: EdgeStatus.AR_UNKNOWN};
return {status: EdgeStatus.ARUnknown};
}
// if(quality == EdgeDetectQuality.FAST){
// edges = fastCandidates; // todo: processing
// } else {
edgeCandidates = this.edgeDetect(image, fastCandidates);
bars = this.edgePostprocess(edgeCandidates, this.conf.canvas.height);
bars = this.edgePostprocess(edgeCandidates);
// }
} catch (e) {
this.logger.log('error', 'arDetect', '%c[EdgeDetect::findBars] find bars failed.', 'background: #f00, color: #000', e);
return {status: EdgeStatus.AR_UNKNOWN}
return {status: EdgeStatus.ARUnknown}
}
} else {
bars = this.pillarTest(image) ? {status: EdgeStatus.AR_KNOWN} : {status: EdgeStatus.AR_UNKNOWN};
bars = this.pillarTest(image) ? {status: EdgeStatus.ARKnown} : {status: EdgeStatus.ARUnknown};
}
return bars;
@ -113,10 +128,10 @@ class EdgeDetect{
this.logger.log('info', 'arDetect', '[EdgeDetect::findCandidates] searching for candidates on ranges', upper_top, '<->', upper_bottom, ';', lower_top, '<->', lower_bottom);
var upper_top_corrected = upper_top * this.conf.canvasImageDataRowLength;
var upper_bottom_corrected = upper_bottom * this.conf.canvasImageDataRowLength;
var lower_top_corrected = lower_top * this.conf.canvasImageDataRowLength;
var lower_bottom_corrected = lower_bottom * this.conf.canvasImageDataRowLength;
let upper_top_corrected = upper_top * this.conf.canvasImageDataRowLength;
let upper_bottom_corrected = upper_bottom * this.conf.canvasImageDataRowLength;
let lower_top_corrected = lower_top * this.conf.canvasImageDataRowLength;
let lower_bottom_corrected = lower_bottom * this.conf.canvasImageDataRowLength;
// if(Debug.debugCanvas.enabled){
// this._columnTest_dbgc(image, upper_top_corrected, upper_bottom_corrected, cols_a, res_top, false);
@ -221,20 +236,20 @@ class EdgeDetect{
}
edgeDetect(image, samples){
var edgeCandidatesTop = {count: 0};
var edgeCandidatesBottom = {count: 0};
let edgeCandidatesTop = {count: 0};
let edgeCandidatesBottom = {count: 0};
var detections;
var canvasWidth = this.conf.canvas.width;
var canvasHeight = this.conf.canvas.height;
let detections;
let canvasWidth = this.conf.canvas.width;
let canvasHeight = this.conf.canvas.height;
var sampleStart, sampleEnd, loopEnd;
var sampleRow_black, sampleRow_color;
let sampleStart, sampleEnd, loopEnd;
let sampleRow_black, sampleRow_color;
var blackEdgeViolation = false;
let blackEdgeViolation = false;
var topEdgeCount = 0;
var bottomEdgeCount = 0;
let topEdgeCount = 0;
let bottomEdgeCount = 0;
try {
for (const sample of samples.res_top){
@ -259,7 +274,7 @@ class EdgeDetect{
loopEnd = sampleRow_black + sampleEnd;
if (Debug.debugCanvas.enabled){
blackEdgeViolation = this._blackbarTest_dbg(image, sampleRow_black + sampleStart, loopEnd);
// blackEdgeViolation = this._blackbarTest_dbg(image, sampleRow_black + sampleStart, loopEnd);
} else {
blackEdgeViolation = this._blackbarTest(image, sampleRow_black + sampleStart, loopEnd);
}
@ -274,7 +289,7 @@ class EdgeDetect{
loopEnd = sampleRow_color + sampleEnd;
if (Debug.debugCanvas.enabled) {
this._imageTest_dbg(image, sampleRow_color + sampleStart, loopEnd, sample.black, edgeCandidatesTop)
// this._imageTest_dbg(image, sampleRow_color + sampleStart, loopEnd, sample.black, edgeCandidatesTop)
} else {
this._imageTest(image, sampleRow_color + sampleStart, loopEnd, sample.black, edgeCandidatesTop);
}
@ -302,7 +317,7 @@ class EdgeDetect{
loopEnd = sampleRow_black + sampleEnd;
if(Debug.debugCanvas.enabled){
blackEdgeViolation = this._blackbarTest_dbg(image, sampleRow_black + sampleStart, loopEnd);
// blackEdgeViolation = this._blackbarTest_dbg(image, sampleRow_black + sampleStart, loopEnd);
} else {
blackEdgeViolation = this._blackbarTest(image, sampleRow_black + sampleStart, loopEnd);
}
@ -317,7 +332,7 @@ class EdgeDetect{
loopEnd = sampleRow_color + sampleEnd;
if(Debug.debugCanvas.enabled) {
this._imageTest_dbg(image, sampleRow_color + sampleStart, loopEnd, sample.black, edgeCandidatesBottom);
// this._imageTest_dbg(image, sampleRow_color + sampleStart, loopEnd, sample.black, edgeCandidatesBottom);
} else {
this._imageTest(image, sampleRow_color + sampleStart, loopEnd, sample.black, edgeCandidatesBottom);
}
@ -335,11 +350,11 @@ class EdgeDetect{
}
edgePostprocess(edges){
var edgesTop = [];
var edgesBottom = [];
var alignMargin = this.conf.canvas.height * this.settings.active.arDetect.allowedMisaligned;
let edgesTop = [];
let edgesBottom = [];
let alignMargin = this.conf.canvas.height * this.settings.active.arDetect.allowedMisaligned;
var missingEdge = edges.edgeCandidatesTopCount == 0 || edges.edgeCandidatesBottomCount == 0;
let missingEdge = edges.edgeCandidatesTopCount == 0 || edges.edgeCandidatesBottomCount == 0;
// pretvorimo objekt v tabelo
// convert objects to array
@ -348,8 +363,8 @@ class EdgeDetect{
delete(edges.edgeCandidatesBottom.count);
if( edges.edgeCandidatesTopCount > 0){
for(var e in edges.edgeCandidatesTop){
var edge = edges.edgeCandidatesTop[e];
for(let e in edges.edgeCandidatesTop){
let edge = edges.edgeCandidatesTop[e];
edgesTop.push({
distance: edge.offset,
absolute: edge.offset,
@ -359,8 +374,8 @@ class EdgeDetect{
}
if( edges.edgeCandidatesBottomCount > 0){
for(var e in edges.edgeCandidatesBottom){
var edge = edges.edgeCandidatesBottom[e];
for(let e in edges.edgeCandidatesBottom){
let edge = edges.edgeCandidatesBottom[e];
edgesBottom.push({
distance: this.conf.canvas.height - edge.offset,
absolute: edge.offset,
@ -388,13 +403,13 @@ class EdgeDetect{
if( edgesTop[0].distance >= edgesBottom[0].distance - alignMargin &&
edgesTop[0].distance <= edgesBottom[0].distance + alignMargin ){
var blackbarWidth = edgesTop[0].distance > edgesBottom[0].distance ?
let blackbarWidth = edgesTop[0].distance > edgesBottom[0].distance ?
edgesTop[0].distance : edgesBottom[0].distance;
if (edgesTop[0].count + edgesBottom[0].count > this.settings.active.arDetect.edgeDetection.singleSideConfirmationThreshold
|| ( edgesTop[0].count > this.settings.active.arDetect.edgeDetection.confirmationThreshold && edgesBottom[0].count > this.settings.active.arDetect.edgeDetection.confirmationThreshold) ){
return {
status: EdgeStatus.AR_KNOWN,
status: EdgeStatus.ARKnown,
blackbarWidth: blackbarWidth,
guardLineTop: edgesTop[0].distance,
guardLineBottom: edgesBottom[0].absolute,
@ -415,20 +430,20 @@ class EdgeDetect{
// manj vzorcev kot navaden rob.
if (edgesTop[0].length > 1){
var lowMargin = edgesBottom[0].distance - alignMargin;
var highMargin = edgesBottom[0].distance + alignMargin;
let lowMargin = edgesBottom[0].distance - alignMargin;
let highMargin = edgesBottom[0].distance + alignMargin;
for (var i = 1; i < edgesTop.length; i++){
for (let i = 1; i < edgesTop.length; i++){
if(edgesTop[i].distance >= lowMargin && edgesTop[i].distance <= highMargin){
// dobili smo dejanski rob. vrnimo ga
// we found the actual edge. let's return that.
var blackbarWidth = edgesTop[i].distance > edgesBottom[0].distance ?
let blackbarWidth = edgesTop[i].distance > edgesBottom[0].distance ?
edgesTop[i].distance : edgesBottom[0].distance;
if (edgesTop[i].count + edgesBottom[0].count > this.settings.active.arDetect.edgeDetection.singleSideConfirmationThreshold
|| (edgesTop[i].count > this.settings.active.arDetect.edgeDetection.singleSideConfirmationThreshold && edgesBottom[0].count > this.settings.active.arDetect.edgeDetection.confirmationThreshold) ) {
return {
status: EdgeStatus.AR_KNOWN,
status: EdgeStatus.ARKnown,
blackbarWidth: blackbarWidth,
guardLineTop: edgesTop[i].distance,
guardLineBottom: edgesBottom[0].absolute,
@ -446,20 +461,20 @@ class EdgeDetect{
edgesBottom[0].count < this.conf.sampleCols.length * this.settings.active.arDetect.edgeDetection.logoThreshold){
if(edgesBottom[0].length > 1){
var lowMargin = edgesTop[0].distance - alignMargin;
var highMargin = edgesTop[0].distance + alignMargin;
let lowMargin = edgesTop[0].distance - alignMargin;
let highMargin = edgesTop[0].distance + alignMargin;
for(var i = 1; i < edgesBottom.length; i++){
for(let i = 1; i < edgesBottom.length; i++){
if (edgesBottom[i].distance >= lowMargin && edgesTop[i].distance <= highMargin) {
// dobili smo dejanski rob. vrnimo ga
// we found the actual edge. let's return that.
var blackbarWidth = edgesBottom[i].distance > edgesTop[0].distance ?
let blackbarWidth = edgesBottom[i].distance > edgesTop[0].distance ?
edgesBottom[i].distance : edgesTop[0].distance;
if (edgesTop[0].count + edgesBottom[i].count > this.settings.active.arDetect.edgeDetection.singleSideConfirmationThreshold
|| (edgesTop[0].count > this.settings.active.arDetect.edgeDetection.singleSideConfirmationThreshold && edgesBottom[i].count > this.settings.active.arDetect.edgeDetection.confirmationThreshold)) {
return {
status: EdgeStatus.AR_KNOWN,
status: EdgeStatus.ARKnown,
blackbarWidth: blackbarWidth,
guardLineTop: edgesTop[0].distance,
guardLineBottom: edgesBottom[i].absolute,
@ -482,10 +497,10 @@ class EdgeDetect{
const edgeDetectionThreshold = this.settings.active.arDetect.edgeDetection.singleSideConfirmationThreshold;
if (edges.edgeCandidatesTopCount == 0 && edges.edgeCandidatesBottomCount != 0){
for(var edge of edgesBottom){
for(let edge of edgesBottom){
if(edge.count >= edgeDetectionThreshold)
return {
status: EdgeStatus.AR_KNOWN,
status: EdgeStatus.ARKnown,
blackbarWidth: edge.distance,
guardLineTop: null,
guardLineBottom: edge.bottom,
@ -496,10 +511,10 @@ class EdgeDetect{
}
}
if (edges.edgeCandidatesTopCount != 0 && edges.edgeCandidatesBottomCount == 0){
for(var edge of edgesTop){
for(let edge of edgesTop){
if(edge.count >= edgeDetectionThreshold)
return {
status: EdgeStatus.AR_KNOWN,
status: EdgeStatus.ARKnown,
blackbarWidth: edge.distance,
guardLineTop: edge.top,
guardLineBottom: null,
@ -512,7 +527,7 @@ class EdgeDetect{
}
// če pridemo do sem, nam ni uspelo nič. Razmerje stranic ni znano
// if we reach this bit, we have failed in determining aspect ratio. It remains unknown.
return {status: EdgeStatus.AR_UNKNOWN}
return {status: EdgeStatus.ARUnknown}
}
pillarTest(image){
@ -523,22 +538,22 @@ class EdgeDetect{
// roughly centered, we return true. Otherwise we return false.
// we also return true if we detect too much black
var blackbarThreshold, upper, lower;
let blackbarThreshold, upper, lower;
blackbarThreshold = this.conf.blackLevel + this.settings.active.arDetect.blackbar.threshold;
var middleRowStart = (this.conf.canvas.height >> 1) * this.conf.canvas.width;
var middleRowEnd = middleRowStart + this.conf.canvas.width - 1;
let middleRowStart = (this.conf.canvas.height >> 1) * this.conf.canvas.width;
let middleRowEnd = middleRowStart + this.conf.canvas.width - 1;
var rowStart = middleRowStart << 2;
var midpoint = (middleRowStart + (this.conf.canvas.width >> 1)) << 2
var rowEnd = middleRowEnd << 2;
let rowStart = middleRowStart << 2;
let midpoint = (middleRowStart + (this.conf.canvas.width >> 1)) << 2
let rowEnd = middleRowEnd << 2;
var edge_left = -1, edge_right = -1;
let edge_left = -1, edge_right = -1;
// preverimo na levi strani
// let's check for edge on the left side
for(var i = rowStart; i < midpoint; i+=4){
for(let i = rowStart; i < midpoint; i+=4){
if(image[i] > blackbarThreshold || image[i+1] > blackbarThreshold || image[i+2] > blackbarThreshold){
edge_left = (i - rowStart) >> 2;
break;
@ -547,7 +562,7 @@ class EdgeDetect{
// preverimo na desni strani
// check on the right
for(var i = rowEnd; i > midpoint; i-= 4){
for(let i = rowEnd; i > midpoint; i-= 4){
if(image[i] > blackbarThreshold || image[i+1] > blackbarThreshold || image[i+2] > blackbarThreshold){
edge_right = this.conf.canvas.width - ((i - rowStart) >> 2);
break;
@ -566,9 +581,9 @@ class EdgeDetect{
return false;
}
var edgeError = this.settings.active.arDetect.pillarTest.allowMisaligned;
var error_low = 1 - edgeError;
var error_hi = 1 + edgeError;
let edgeError = this.settings.active.arDetect.pillarTest.allowMisaligned;
let error_low = 1 - edgeError;
let error_hi = 1 + edgeError;
// če sta 'edge_left' in 'edge_right' podobna/v mejah merske napake, potem vrnemo true — lahko da smo našli logo na sredini zaslona
// if 'edge_left' and 'edge_right' are similar enough to each other, we return true. If we found a logo in a black frame, we could
@ -848,13 +863,13 @@ class EdgeDetect{
let tmpI;
let lastTmpI = 0;
let edgeDetectCount = 0;
for(const c in colsOut) {
for(const c of colsOut) {
c.diffs = [];
}
if (reverseSearchDirection) {
if (this.settings.active.arDetect.blackbar.antiGradientMode === AntiGradientMode.Disabled) {
// todo: remove gradient detection code from this branch
for(var i = bottom - this.conf.canvasImageDataRowLength; i >= top; i-= this.conf.canvasImageDataRowLength){
for(let i = bottom - this.conf.canvasImageDataRowLength; i >= top; i-= this.conf.canvasImageDataRowLength){
for(let c = 0; c < colsIn.length; c++){
if (colsIn[c].blackFound && colsIn[c].imageFound) {
// če smo našli obe točki, potem ne pregledujemo več.
@ -908,7 +923,7 @@ class EdgeDetect{
}
} else {
// anti-gradient detection
for(var i = bottom - this.conf.canvasImageDataRowLength; i >= top; i-= this.conf.canvasImageDataRowLength){
for(let i = bottom - this.conf.canvasImageDataRowLength; i >= top; i-= this.conf.canvasImageDataRowLength){
for(let c = 0; c < colsIn.length; c++){
if (colsIn[c].blackFound && colsIn[c].imageFound) {
// če smo našli obe točki, potem ne pregledujemo več.
@ -995,7 +1010,7 @@ class EdgeDetect{
}
}
} else {
for(var i = top; i < bottom; i+= this.conf.canvasImageDataRowLength){
for(let i = top; i < bottom; i+= this.conf.canvasImageDataRowLength){
for(let c = 0; c < colsIn.length; c++){
if (colsIn[c].blackFound && colsIn[c].imageFound) {
// če smo našli obe točki, potem ne pregledujemo več.
@ -1051,18 +1066,18 @@ class EdgeDetect{
}
_columnTest(image, top, bottom, colsIn, colsOut, reverseSearchDirection){
var tmpI;
_columnTest(image, top: number, bottom: number, colsIn, colsOut, reverseSearchDirection){
let tmpI;
if(reverseSearchDirection){
for(var i = bottom - this.conf.canvasImageDataRowLength; i >= top; i-= this.conf.canvasImageDataRowLength){
for(var col of colsIn){
for(let i = bottom - this.conf.canvasImageDataRowLength; i >= top; i-= this.conf.canvasImageDataRowLength){
for(const col of colsIn){
tmpI = i + (col << 2);
if( image[tmpI] > this.blackbarThreshold ||
image[tmpI + 1] > this.blackbarThreshold ||
image[tmpI + 2] > this.blackbarThreshold ){
var bottom = (i / this.conf.canvasImageDataRowLength) + 1;
const bottom = (i / this.conf.canvasImageDataRowLength) + 1;
colsOut.push({
col: col,
bottom: bottom
@ -1074,8 +1089,8 @@ class EdgeDetect{
break;
}
} else {
for(var i = top; i < bottom; i+= this.conf.canvasImageDataRowLength){
for(var col of colsIn){
for(let i = top; i < bottom; i+= this.conf.canvasImageDataRowLength){
for(const col of colsIn){
tmpI = i + (col << 2);
if( image[tmpI] > this.blackbarThreshold ||
@ -1095,65 +1110,65 @@ class EdgeDetect{
}
}
_columnTest_dbgc(image, top, bottom, colsIn, colsOut, reverseSearchDirection){
var tmpI;
if(reverseSearchDirection){
for(var i = bottom - this.conf.canvasImageDataRowLength; i >= top; i-= this.conf.canvasImageDataRowLength){
for(var col of colsIn){
tmpI = i + (col << 2);
// _columnTest_dbgc(image, top, bottom, colsIn, colsOut, reverseSearchDirection){
// let tmpI;
// if(reverseSearchDirection){
// for(let i = bottom - this.conf.canvasImageDataRowLength; i >= top; i-= this.conf.canvasImageDataRowLength){
// for(let col of colsIn){
// tmpI = i + (col << 2);
if( image[tmpI] > this.blackbarThreshold ||
image[tmpI + 1] > this.blackbarThreshold ||
image[tmpI + 2] > this.blackbarThreshold ){
// if( image[tmpI] > this.blackbarThreshold ||
// image[tmpI + 1] > this.blackbarThreshold ||
// image[tmpI + 2] > this.blackbarThreshold ){
var bottom = (i / this.conf.canvasImageDataRowLength) + 1;
colsOut.push({
col: col,
bottom: bottom
});
colsIn.splice(colsIn.indexOf(col), 1);
this.conf.debugCanvas.trace(tmpI,DebugCanvasClasses.EDGEDETECT_CANDIDATE);
}
else{
this.conf.debugCanvas.trace(tmpI, DebugCanvasClasses.EDGEDETECT_ONBLACK);
}
}
if(colsIn.length < this.colsThreshold)
break;
}
} else {
for(var i = top; i < bottom; i+= this.conf.canvasImageDataRowLength){
for(var col of colsIn){
tmpI = i + (col << 2);
// let bottom = (i / this.conf.canvasImageDataRowLength) + 1;
// colsOut.push({
// col: col,
// bottom: bottom
// });
// colsIn.splice(colsIn.indexOf(col), 1);
// this.conf.debugCanvas.trace(tmpI,DebugCanvasClasses.EDGEDETECT_CANDIDATE);
// }
// else{
// this.conf.debugCanvas.trace(tmpI, DebugCanvasClasses.EDGEDETECT_ONBLACK);
// }
// }
// if(colsIn.length < this.colsThreshold)
// break;
// }
// } else {
// for(let i = top; i < bottom; i+= this.conf.canvasImageDataRowLength){
// for(let col of colsIn){
// tmpI = i + (col << 2);
if( image[tmpI] > this.blackbarThreshold ||
image[tmpI + 1] > this.blackbarThreshold ||
image[tmpI + 2] > this.blackbarThreshold ){
// if( image[tmpI] > this.blackbarThreshold ||
// image[tmpI + 1] > this.blackbarThreshold ||
// image[tmpI + 2] > this.blackbarThreshold ){
colsOut.push({
col: col,
top: (i / this.conf.canvasImageDataRowLength) - 1
});
colsIn.splice(colsIn.indexOf(col), 1);
this.conf.debugCanvas.trace(tmpI, DebugCanvasClasses.EDGEDETECT_CANDIDATE);
if(tmpI-1 > 0){
this.conf.debugCanvas.trace(tmpI - 1, DebugCanvasClasses.EDGEDETECT_CANDIDATE_SECONDARY);
}
if(tmpI+1 < image.length){
this.conf.debugCanvas.trace(tmpI + 1, DebugCanvasClasses.EDGEDETECT_CANDIDATE_SECONDARY);
}
} else {
this.conf.debugCanvas.trace(tmpI, DebugCanvasClasses.EDGEDETECT_ONBLACK);
}
}
if(colsIn.length < this.colsThreshold)
break;
}
}
}
// colsOut.push({
// col: col,
// top: (i / this.conf.canvasImageDataRowLength) - 1
// });
// colsIn.splice(colsIn.indexOf(col), 1);
// this.conf.debugCanvas.trace(tmpI, DebugCanvasClasses.EDGEDETECT_CANDIDATE);
// if(tmpI-1 > 0){
// this.conf.debugCanvas.trace(tmpI - 1, DebugCanvasClasses.EDGEDETECT_CANDIDATE_SECONDARY);
// }
// if(tmpI+1 < image.length){
// this.conf.debugCanvas.trace(tmpI + 1, DebugCanvasClasses.EDGEDETECT_CANDIDATE_SECONDARY);
// }
// } else {
// this.conf.debugCanvas.trace(tmpI, DebugCanvasClasses.EDGEDETECT_ONBLACK);
// }
// }
// if(colsIn.length < this.colsThreshold)
// break;
// }
// }
// }
_blackbarTest(image, start, end){
for(var i = start; i < end; i += 4){
for(let i = start; i < end; i += 4){
if( image[i ] > this.blackbarThreshold ||
image[i+1] > this.blackbarThreshold ||
image[i+2] > this.blackbarThreshold ){
@ -1163,25 +1178,25 @@ class EdgeDetect{
return false; // no violation
}
_blackbarTest_dbg(image, start, end){
for(var i = start; i < end; i += 4){
if( image[i ] > this.blackbarThreshold ||
image[i+1] > this.blackbarThreshold ||
image[i+2] > this.blackbarThreshold ){
// _blackbarTest_dbg(image, start, end){
// for(let i = start; i < end; i += 4){
// if( image[i ] > this.blackbarThreshold ||
// image[i+1] > this.blackbarThreshold ||
// image[i+2] > this.blackbarThreshold ){
this.conf.debugCanvas.trace(i, DebugCanvasClasses.VIOLATION)
return true;
} else {
this.conf.debugCanvas.trace(i, DebugCanvasClasses.EDGEDETECT_BLACKBAR)
}
}
return false; // no violation
}
// this.conf.debugCanvas.trace(i, DebugCanvasClasses.VIOLATION)
// return true;
// } else {
// this.conf.debugCanvas.trace(i, DebugCanvasClasses.EDGEDETECT_BLACKBAR)
// }
// }
// return false; // no violation
// }
_imageTest(image, start, end, sampleOffset, edgeCandidates){
var detections = 0;
let detections = 0;
for (var i = start; i < end; i += 4){
for (let i = start; i < end; i += 4){
if (image[i ] > this.blackbarThreshold ||
image[i+1] > this.blackbarThreshold ||
image[i+2] > this.blackbarThreshold ){
@ -1198,28 +1213,28 @@ class EdgeDetect{
}
}
_imageTest_dbg(image, start, end, sampleOffset, edgeCandidates){
var detections = 0;
// _imageTest_dbg(image, start, end, sampleOffset, edgeCandidates){
// let detections = 0;
for(var i = start; i < end; i += 4){
if( image[i ] > this.blackbarThreshold ||
image[i+1] > this.blackbarThreshold ||
image[i+2] > this.blackbarThreshold ){
++detections;
this.conf.debugCanvas.trace(i, DebugCanvasClasses.EDGEDETECT_IMAGE);
} else {
this.conf.debugCanvas.trace(i, DebugCanvasClasses.WARN);
}
}
if(detections >= this.detectionThreshold){
if(edgeCandidates[sampleOffset] != undefined)
edgeCandidates[sampleOffset].count++;
else{
edgeCandidates[sampleOffset] = {offset: sampleOffset, count: 1};
edgeCandidates.count++;
}
}
}
// for(let i = start; i < end; i += 4){
// if( image[i ] > this.blackbarThreshold ||
// image[i+1] > this.blackbarThreshold ||
// image[i+2] > this.blackbarThreshold ){
// ++detections;
// this.conf.debugCanvas.trace(i, DebugCanvasClasses.EDGEDETECT_IMAGE);
// } else {
// this.conf.debugCanvas.trace(i, DebugCanvasClasses.WARN);
// }
// }
// if(detections >= this.detectionThreshold){
// if(edgeCandidates[sampleOffset] != undefined)
// edgeCandidates[sampleOffset].count++;
// else{
// edgeCandidates[sampleOffset] = {offset: sampleOffset, count: 1};
// edgeCandidates.count++;
// }
// }
// }
}

View File

@ -1,6 +0,0 @@
var EdgeDetectPrimaryDirection = Object.freeze({
VERTICAL: 0,
HORIZONTAL: 1
});
export default EdgeDetectPrimaryDirection;

View File

@ -0,0 +1,6 @@
enum EdgeDetectPrimaryDirection {
Vertical = 0,
Horizontal = 1
};
export default EdgeDetectPrimaryDirection;

View File

@ -1,6 +0,0 @@
var EdgeDetectQuality = Object.freeze({
FAST: 0,
IMPROVED: 1
});
export default EdgeDetectQuality;

View File

@ -0,0 +1,6 @@
enum EdgeDetectQuality {
Fast = 0,
Improved = 1
};
export default EdgeDetectQuality;

View File

@ -1,6 +0,0 @@
var EdgeStatus = Object.freeze({
AR_UNKNOWN: 0,
AR_KNOWN: 1,
});
export default EdgeStatus;

View File

@ -0,0 +1,6 @@
enum EdgeStatus {
ARUnknown = 0,
ARKnown = 1,
};
export default EdgeStatus;

View File

@ -1,43 +0,0 @@
import Debug from '../../conf/Debug';
import BrowserDetect from '../../conf/BrowserDetect';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading Comms");
}
class Comms {
static async sendMessage(message){
if(BrowserDetect.firefox){
return browser.runtime.sendMessage(message);
} else {
return new Promise((resolve, reject) => {
try{
if(BrowserDetect.edge){
browser.runtime.sendMessage(message, function(response){
var r = response;
resolve(r);
});
} else {
chrome.runtime.sendMessage(message, function(response){
// Chrome/js shittiness mitigation — remove this line and an empty array will be returned
var r = response;
resolve(r);
return true;
});
}
}
catch(e){
reject(e);
}
});
}
}
}
if (process.env.CHANNEL !== 'stable'){
console.info("Comms loaded");
}
export default Comms;

View File

@ -0,0 +1,19 @@
import Debug from '../../conf/Debug';
import BrowserDetect from '../../conf/BrowserDetect';
import { browser } from 'webextension-polyfill-ts';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading Comms");
}
class Comms {
static async sendMessage(message){
browser.runtime.sendMessage(message);
}
}
if (process.env.CHANNEL !== 'stable'){
console.info("Comms loaded");
}
export default Comms;

View File

@ -1,19 +1,33 @@
import Debug from '../../conf/Debug';
import BrowserDetect from '../../conf/BrowserDetect';
import Logger from '../Logger';
import { browser } from 'webextension-polyfill-ts';
import Settings from '../Settings';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading CommsClient");
}
class CommsClient {
commsId: string;
logger: Logger;
settings: any; // sus?
commands: {[x: string]: any[]};
_listener: (m: any) => void;
port: any;
constructor(name, logger, commands) {
try {
this.logger = logger;
if (BrowserDetect.firefox) {
this.port = browser.runtime.connect({name: name});
} else if (BrowserDetect.anyChromium) {
this.port = chrome.runtime.connect({name: name});
}
this.port = browser.runtime.connect(null, {name: name});
this.logger.onLogEnd(
(history) => {
@ -32,6 +46,9 @@ class CommsClient {
this.commsId = (Math.random() * 20).toFixed(0);
this.commands = commands;
} catch (e) {
console.error("CONSTRUCOTR FAILED:", e)
}
}
destroy() {
@ -60,34 +77,11 @@ class CommsClient {
async sendMessage_nonpersistent(message){
message = JSON.parse(JSON.stringify(message)); // vue quirk. We should really use vue store instead
if(BrowserDetect.firefox){
return browser.runtime.sendMessage(message)
} else {
return new Promise((resolve, reject) => {
try{
if(BrowserDetect.edge){
browser.runtime.sendMessage(message, function(response){
var r = response;
resolve(r);
});
} else {
chrome.runtime.sendMessage(message, function(response){
// Chrome/js shittiness mitigation — remove this line and an empty array will be returned
var r = response;
resolve(r);
return true;
});
}
}
catch(e){
reject(e);
}
return true;
});
}
return browser.runtime.sendMessage(null, message, null);
}
// TODO: sus function — does it get any use?
async requestSettings(){
this.logger.log('info', 'comms', "%c[CommsClient::requestSettings] sending request for congif!", "background: #11D; color: #aad");
@ -99,12 +93,12 @@ class CommsClient {
return Promise.resolve(false);
}
this.settings.active = JSON.parse(response.extensionConf);
this.settings = {active: JSON.parse(response.extensionConf)};
return Promise.resolve(true);
}
async sendMessage(message) {
await this.sendMessage_nonpersistent(message);
return this.sendMessage_nonpersistent(message);
}
registerVideo(){

View File

@ -1,339 +0,0 @@
import Debug from '../../conf/Debug';
import BrowserDetect from '../../conf/BrowserDetect';
class CommsServer {
constructor(server) {
this.server = server;
this.logger = server.logger;
this.settings = server.settings;
this.ports = [];
this.popupPort = null;
if (BrowserDetect.firefox) {
browser.runtime.onConnect.addListener(p => this.onConnect(p));
browser.runtime.onMessage.addListener((m, sender) => this.processReceivedMessage_nonpersistent(m, sender));
} else {
chrome.runtime.onConnect.addListener(p => this.onConnect(p));
chrome.runtime.onMessage.addListener((m, sender, callback) => this.processReceivedMessage_nonpersistent(m, sender, callback));
}
// commands — functions that handle incoming messages
// functions can have the following arguments, which are,
// in this order:
// message — the message we received
// port|sender — on persistent channels, second argument is port on which the server
// listens. If the message was sent in non-persistent way, this is the
// sender script/frame/whatever of the message
// sendResponse — callback function on messages received via non-persistent channel
this.commands = {
'announce-zoom': [
(message) => {
try {
// forward message to the popup
this.popupPort.postMessage({cmd: 'set-current-zoom', zoom: message.zoom});
} catch (e) {
// if popup is closed, this will/may fail. This is okay, so we just ignore this error
}
},
],
'get-current-zoom': [
(message) => this.sendToActive(message),
],
'get-current-site': [
async (message, port) => {
port.postMessage({
cmd: 'set-current-site',
site: await this.server.getVideoTab(),
tabHostname: await this.getCurrentTabHostname()
});
},
],
'popup-set-selected-tab': [
(message) => this.server.setSelectedTab(message.selectedMenu, message.selectedSubitem),
],
'get-config': [
(message, port) => {
this.logger.log('info', 'comms', "CommsServer: received get-config. Active settings?", this.settings.active, "\n(settings:", this.settings, ")");
port.postMessage(
{cmd: "set-config", conf: this.settings.active, site: this.server.currentSite}
);
},
],
'has-video': [
(message, port) => this.server.registerVideo(port.sender),
],
'noVideo': [
(message, port) => this.server.unregisterVideo(port.sender),
],
'inject-css': [
(message, sender) => this.server.injectCss(message.cssString, sender),
],
'eject-css': [
(message, sender) => this.server.removeCss(message.cssString, sender),
],
'replace-css': [
(message, sender) => this.server.replaceCss(message.oldCssString, message.newCssString, sender),
],
'get-config': [
(message, sender, sendResponse) => {
if (BrowserDetect.firefox) {
var ret = {extensionConf: JSON.stringify(this.settings.active)};
this.logger.log('info', 'comms', "%c[CommsServer.js::processMessage_nonpersistent] Returning this:", "background-color: #11D; color: #aad", ret);
Promise.resolve(ret);
} else {
sendResponse({extensionConf: JSON.stringify(this.settings.active)});
return true;
}
}
],
'autoar-enable': [
() => {
this.settings.active.sites['@global'].autoar = "blacklist";
this.settings.save();
this.logger.log('info', 'comms', "[uw-bg] autoar set to enabled (blacklist). evidenz:", this.settings.active);
}
],
'autoar-disable': [
(message) => {
this.settings.active.sites['@global'].autoar = "disabled";
if (message.reason){
this.settings.active.arDetect.disabledReason = message.reason;
} else {
this.settings.active.arDetect.disabledReason = 'User disabled';
}
this.settings.save();
this.logger.log('info', 'comms', "[uw-bg] autoar set to disabled. evidenz:", this.settings.active);
}
],
'autoar-set-interval': [
(message) => {
this.logger.log('info', 'comms', `[uw-bg] trying to set new interval for autoAr. New interval is, ${message.timeout} ms`);
// set fairly liberal limit
var timeout = message.timeout < 4 ? 4 : message.timeout;
this.settings.active.arDetect.timer_playing = timeout;
this.settings.save();
}
],
'logging-stop-and-save': [ // TODO: possibly never used/superseded — check
(message, sender) => {
this.logger.log('info', 'comms', "Received command to stop logging and export the received input");
this.logger.addToGlobalHistory(`${message.host}::${sender?.tab?.id ?? '×'}-${sender.frameId ?? '×'}`, JSON.parse(message.history));
this.logger.finish();
}
],
'logging-save': [
(message, sender) => {
this.logger.log('info', 'comms', `Received command to save log for site ${message.host} (tabId ${sender.tab.id}, frameId ${sender.frameId}`);
this.logger.addToGlobalHistory(`${message?.host}::${sender?.tab?.id ?? '×'}-${sender?.frameId ?? '×'}`, JSON.parse(message.history));
}
]
}
}
subscribe(command, callback) {
if (!this.commands[command]) {
this.commands[command] = [callback];
} else {
this.commands[command].push(callback);
}
}
async getCurrentTabHostname() {
const activeTab = await this._getActiveTab();
if (!activeTab || activeTab.length < 1) {
this.logger.log('warn', 'comms', 'There is no active tab for some reason. activeTab:', activeTab);
}
const url = activeTab[0].url;
var hostname;
if (url.indexOf("://") > -1) { //find & remove protocol (http, ftp, etc.) and get hostname
hostname = url.split('/')[2];
}
else {
hostname = url.split('/')[0];
}
hostname = hostname.split(':')[0]; //find & remove port number
hostname = hostname.split('?')[0]; //find & remove "?"
return hostname;
}
sendToAll(message){
for(const tab of this.ports){
for(const frame in tab){
for (const port in tab[frame]) {
tab[frame][port].postMessage(message);
}
}
}
}
async _getActiveTab() {
if (BrowserDetect.firefox) {
return await browser.tabs.query({currentWindow: true, active: true});
} else {
return await new Promise( (resolve, reject) => {
chrome.tabs.query({lastFocusedWindow: true, active: true}, function (res) {
resolve(res);
return true;
});
});
}
}
// if port is NOT defined, send to all content scripts of a given frame
// if port is defined, send just to that particular script of a given frame
async sendToFrameContentScripts(message, tab, frame, port) {
if (port !== undefined) {
// note: 'port' is _not_ shadowed here.
this.ports[tab][frame][port].postMessage(message);
return;
}
for (const port in this.ports[tab][frame]) {
// note: 'port' is shadowed here!
this.ports[tab][frame][port].postMessage(message);
}
}
async sendToFrame(message, tab, frame, port) {
this.logger.log('info', 'comms', `%c[CommsServer::sendToFrame] attempting to send message to tab ${tab}, frame ${frame}`, "background: #dda; color: #11D", message);
if (isNaN(tab)) {
if (frame === '__playing') {
message['playing'] = true;
this.sendToAll(message);
return;
} else if (frame === '__all') {
this.sendToAll(message);
return;
}
[tab, frame] = frame.split('-');
}
this.logger.log('info', 'comms', `%c[CommsServer::sendToFrame] attempting to send message to tab ${tab}, frame ${frame}`, "background: #dda; color: #11D", message);
try {
this.sendToFrameContentScripts(message, tab, frame, port);
} catch (e) {
this.logger.log('error', 'comms', `%c[CommsServer::sendToFrame] Sending message failed. Reason:`, "background: #dda; color: #11D", e);
}
}
async sendToAllFrames(message, tab, port) {
for (const frame in this.ports[tab]) {
this.sendToFrameContentScripts(message, tab, frame, port);
}
}
async sendToActive(message) {
this.logger.log('info', 'comms', "%c[CommsServer::sendToActive] trying to send a message to active tab. Message:", "background: #dda; color: #11D", message);
const tabs = await this._getActiveTab();
this.logger.log('info', 'comms', "[CommsServer::_sendToActive] currently active tab(s)?", tabs);
for (const frame in this.ports[tabs[0].id]) {
this.logger.log('info', 'comms', "key?", frame, this.ports[tabs[0].id]);
}
for (const frame in this.ports[tabs[0].id]) {
this.sendToFrameContentScripts(message, tabs[0].id, frame);
}
}
onConnect(port){
// poseben primer | special case
if (port.name === 'popup-port') {
this.popupPort = port;
this.popupPort.onMessage.addListener( (m,p) => this.processReceivedMessage(m,p));
return;
}
var tabId = port.sender.tab.id;
var frameId = port.sender.frameId;
if (! this.ports[tabId]){
this.ports[tabId] = {};
}
if (! this.ports[tabId][frameId]) {
this.ports[tabId][frameId] = {};
}
this.ports[tabId][frameId][port.name] = port;
this.ports[tabId][frameId][port.name].onMessage.addListener( (m,p) => this.processReceivedMessage(m, p));
this.ports[tabId][frameId][port.name].onDisconnect.addListener( (p) => {
try {
delete this.ports[p.sender.tab.id][p.sender.frameId][port.name];
} catch (e) {
// no biggie if the thing above doesn't exist.
}
if (Object.keys(this.ports[tabId][frameId].length === 0)) {
delete this.ports[tabId][frameId];
if(Object.keys(this.ports[p.sender.tab.id]).length === 0) {
delete this.ports[tabId];
}
}
});
}
async execCmd(message, portOrSender, sendResponse) {
this.logger.log(
'info', 'comms', '[CommsServer.js::execCmd] Received message', message,
". Port/sender:", portOrSender, "sendResponse:", sendResponse, "\nThere is ", this.commands[message.cmd]?.length ?? 0,
" command(s) for action", message.cmd
);
if (this.commands[message.cmd]) {
for (const c of this.commands[message.cmd]) {
try {
await c(message, portOrSender, sendResponse);
} catch (e) {
this.logger.log('error', 'debug', "[CommsServer.js::execCmd] failed to execute command.", e)
}
}
}
}
async handleMessage(message, portOrSender, sendResponse) {
await this.execCmd(message, portOrSender, sendResponse);
if (message.forwardToSameFramePort) {
this.sendToFrameContentScripts(message, portOrSender.tab.id, portOrSender.frameId, message.port)
}
if (message.forwardToContentScript) {
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Message has 'forward to content script' flag set. Forwarding message as is. Message:", message);
this.sendToFrame(message, message.targetTab, message.targetFrame);
}
if (message.forwardToAll) {
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Message has 'forward to all' flag set. Forwarding message as is. Message:", message);
this.sendToAll(message);
}
if (message.forwardToActive) {
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Message has 'forward to active' flag set. Forwarding message as is. Message:", message);
this.sendToActive(message);
}
}
async processReceivedMessage(message, port){
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Received message from popup/content script!", message, "port", port);
this.handleMessage(message, port)
}
processReceivedMessage_nonpersistent(message, sender, sendResponse){
this.logger.log('info', 'comms', "%c[CommsServer.js::processMessage_nonpersistent] Received message from background script!", "background-color: #11D; color: #aad", message, sender);
this.handleMessage(message, sender, sendResponse);
}
// chrome shitiness mitigation
sendUnmarkPlayer(message) {
this.logger.log('info', 'comms', '[CommsServer.js::sendUnmarkPlayer] Chrome is a shit browser that doesn\'t do port.postMessage() in unload events, so we have to resort to inelegant hacks. If you see this, then the workaround method works.');
this.processReceivedMessage(message, this.popupPort);
}
}
export default CommsServer;

View File

@ -0,0 +1,347 @@
import Debug from '../../conf/Debug';
import BrowserDetect from '../../conf/BrowserDetect';
import Logger from '../Logger';
import Settings from '../Settings';
import { browser } from 'webextension-polyfill-ts';
import ExtensionMode from '../../../common/enums/ExtensionMode.enum';
class CommsServer {
server: any;
logger: Logger;
settings: Settings;
ports: {
[frame: string] : {
[port: string]: any
}
}[] = [];
popupPort: any;
/**
* commands functions that handle incoming messages
* functions can have the following arguments, which are,
* in this order:
* message the message we received
* port|sender on persistent channels, second argument is port on which the server
* listens. If the message was sent in non-persistent way, this is the
* sender script/frame/whatever of the message
* sendResponse callback function on messages received via non-persistent channel
*/
commands: {[x: string]: ((a: any, b: any) => void | Promise<void>)[]} = {
'announce-zoom': [
(message) => {
try {
// forward message to the popup
this.popupPort.postMessage({cmd: 'set-current-zoom', zoom: message.zoom});
} catch (e) {
// if popup is closed, this will/may fail. This is okay, so we just ignore this error
}
},
],
'get-current-zoom': [
(message) => this.sendToActive(message),
],
'get-current-site': [
async (message, port) => {
port.postMessage({
cmd: 'set-current-site',
site: await this.server.getVideoTab(),
tabHostname: await this.getCurrentTabHostname()
});
},
],
'popup-set-selected-tab': [
(message) => this.server.setSelectedTab(message.selectedMenu, message.selectedSubitem),
],
'has-video': [
(message, port) => this.server.registerVideo(port.sender),
],
'noVideo': [
(message, port) => this.server.unregisterVideo(port.sender),
],
'inject-css': [
(message, sender) => this.server.injectCss(message.cssString, sender),
],
'eject-css': [
(message, sender) => this.server.removeCss(message.cssString, sender),
],
'replace-css': [
(message, sender) => this.server.replaceCss(message.oldCssString, message.newCssString, sender),
],
// 'get-config': [
// (message, port) => {
// this.logger.log('info', 'comms', "CommsServer: received get-config. Active settings?", this.settings.active, "\n(settings:", this.settings, ")");
// port.postMessage(
// {cmd: "set-config", conf: this.settings.active, site: this.server.currentSite}
// );
// },
// ],
'get-config': [
(message, sender) => {
var ret = {extensionConf: JSON.stringify(this.settings.active)};
this.logger.log('info', 'comms', "%c[CommsServer.js::processMessage_nonpersistent] Returning this:", "background-color: #11D; color: #aad", ret);
Promise.resolve(ret);
}
],
'autoar-enable': [
() => {
this.settings.active.sites['@global'].autoar = ExtensionMode.Enabled;
this.settings.save();
this.logger.log('info', 'comms', "[uw-bg] autoar set to enabled (blacklist). evidenz:", this.settings.active);
}
],
'autoar-disable': [
(message) => {
this.settings.active.sites['@global'].autoar = ExtensionMode.Disabled;
if (message.reason){
this.settings.active.arDetect.disabledReason = message.reason;
} else {
this.settings.active.arDetect.disabledReason = 'User disabled';
}
this.settings.save();
this.logger.log('info', 'comms', "[uw-bg] autoar set to disabled. evidenz:", this.settings.active);
}
],
'autoar-set-interval': [
(message) => {
this.logger.log('info', 'comms', `[uw-bg] trying to set new interval for autoAr. New interval is, ${message.timeout} ms`);
// set fairly liberal limit
var timeout = message.timeout < 4 ? 4 : message.timeout;
this.settings.active.arDetect.timers.playing = timeout;
this.settings.save();
}
],
'logging-stop-and-save': [ // TODO: possibly never used/superseded — check
(message, sender) => {
this.logger.log('info', 'comms', "Received command to stop logging and export the received input");
this.logger.addToGlobalHistory(`${message.host}::${sender?.tab?.id ?? '×'}-${sender.frameId ?? '×'}`, JSON.parse(message.history));
this.logger.finish();
}
],
'logging-save': [
(message, sender) => {
this.logger.log('info', 'comms', `Received command to save log for site ${message.host} (tabId ${sender.tab.id}, frameId ${sender.frameId}`);
this.logger.addToGlobalHistory(`${message?.host}::${sender?.tab?.id ?? '×'}-${sender?.frameId ?? '×'}`, JSON.parse(message.history));
}
]
}
//#region getters
get activeTab() {
return browser.tabs.query({currentWindow: true, active: true});
}
//#endregion
constructor(server) {
this.server = server;
this.logger = server.logger;
this.settings = server.settings;
browser.runtime.onConnect.addListener(p => this.onConnect(p));
browser.runtime.onMessage.addListener((m, sender) => this.processReceivedMessage_nonpersistent(m, sender));
}
subscribe(command, callback) {
if (!this.commands[command]) {
this.commands[command] = [callback];
} else {
this.commands[command].push(callback);
}
}
async getCurrentTabHostname() {
const activeTab = await this.activeTab;
if (!activeTab || activeTab.length < 1) {
this.logger.log('warn', 'comms', 'There is no active tab for some reason. activeTab:', activeTab);
}
const url = activeTab[0].url;
var hostname;
if (url.indexOf("://") > -1) { //find & remove protocol (http, ftp, etc.) and get hostname
hostname = url.split('/')[2];
}
else {
hostname = url.split('/')[0];
}
hostname = hostname.split(':')[0]; //find & remove port number
hostname = hostname.split('?')[0]; //find & remove "?"
return hostname;
}
sendToAll(message){
for(const tab of this.ports){
for(const frame in tab){
for (const port in tab[frame]) {
tab[frame][port].postMessage(message);
}
}
}
}
/**
* Sends a message to addon content scripts.
* @param message message
* @param tab the tab we want to send the message to
* @param frame the frame within that tab that we want to send the message to
* @param port if defined, message will only be sent to that specific script, otherwise it gets sent to all scripts of a given frame
*/
async sendToFrameContentScripts(message, tab, frame, port?) {
if (port !== undefined) {
this.ports[tab][frame][port].postMessage(message);
return;
}
for (const framePort in this.ports[tab][frame]) {
this.ports[tab][frame][framePort].postMessage(message);
}
}
async sendToFrame(message, tab, frame, port?) {
this.logger.log('info', 'comms', `%c[CommsServer::sendToFrame] attempting to send message to tab ${tab}, frame ${frame}`, "background: #dda; color: #11D", message);
if (isNaN(tab)) {
if (frame === '__playing') {
message['playing'] = true;
this.sendToAll(message);
return;
} else if (frame === '__all') {
this.sendToAll(message);
return;
}
[tab, frame] = frame.split('-');
}
this.logger.log('info', 'comms', `%c[CommsServer::sendToFrame] attempting to send message to tab ${tab}, frame ${frame}`, "background: #dda; color: #11D", message);
try {
this.sendToFrameContentScripts(message, tab, frame, port);
} catch (e) {
this.logger.log('error', 'comms', `%c[CommsServer::sendToFrame] Sending message failed. Reason:`, "background: #dda; color: #11D", e);
}
}
async sendToAllFrames(message, tab, port) {
for (const frame in this.ports[tab]) {
this.sendToFrameContentScripts(message, tab, frame, port);
}
}
async sendToActive(message) {
this.logger.log('info', 'comms', "%c[CommsServer::sendToActive] trying to send a message to active tab. Message:", "background: #dda; color: #11D", message);
const tabs = await this.activeTab;
this.logger.log('info', 'comms', "[CommsServer::_sendToActive] currently active tab(s)?", tabs);
for (const frame in this.ports[tabs[0].id]) {
this.logger.log('info', 'comms', "key?", frame, this.ports[tabs[0].id]);
}
for (const frame in this.ports[tabs[0].id]) {
this.sendToFrameContentScripts(message, tabs[0].id, frame);
}
}
onConnect(port){
// poseben primer | special case
if (port.name === 'popup-port') {
this.popupPort = port;
this.popupPort.onMessage.addListener( (m,p) => this.processReceivedMessage(m,p));
return;
}
var tabId = port.sender.tab.id;
var frameId = port.sender.frameId;
if (! this.ports[tabId]){
this.ports[tabId] = {};
}
if (! this.ports[tabId][frameId]) {
this.ports[tabId][frameId] = {};
}
this.ports[tabId][frameId][port.name] = port;
this.ports[tabId][frameId][port.name].onMessage.addListener( (m,p) => this.processReceivedMessage(m, p));
this.ports[tabId][frameId][port.name].onDisconnect.addListener( (p) => {
try {
delete this.ports[p.sender.tab.id][p.sender.frameId][port.name];
} catch (e) {
// no biggie if the thing above doesn't exist.
}
if (Object.keys(this.ports[tabId][frameId].length === 0)) {
delete this.ports[tabId][frameId];
if(Object.keys(this.ports[p.sender.tab.id]).length === 0) {
delete this.ports[tabId];
}
}
});
}
// TODO: sendResponse seems redundant — it used to be a callback for
// chrome-based browsers, but browser polyfill doesn't do callback. Just
// awaits.
async execCmd(message, portOrSender, sendResponse?) {
this.logger.log(
'info', 'comms', '[CommsServer.js::execCmd] Received message', message,
". Port/sender:", portOrSender, "sendResponse:", sendResponse, "\nThere is ", this.commands[message.cmd]?.length ?? 0,
" command(s) for action", message.cmd
);
if (this.commands[message.cmd]) {
for (const c of this.commands[message.cmd]) {
try {
await c(message, portOrSender);
} catch (e) {
this.logger.log('error', 'debug', "[CommsServer.js::execCmd] failed to execute command.", e)
}
}
}
}
async handleMessage(message, portOrSender) {
await this.execCmd(message, portOrSender);
if (message.forwardToSameFramePort) {
this.sendToFrameContentScripts(message, portOrSender.tab.id, portOrSender.frameId, message.port);
}
if (message.forwardToContentScript) {
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Message has 'forward to content script' flag set. Forwarding message as is. Message:", message);
this.sendToFrame(message, message.targetTab, message.targetFrame);
}
if (message.forwardToAll) {
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Message has 'forward to all' flag set. Forwarding message as is. Message:", message);
this.sendToAll(message);
}
if (message.forwardToActive) {
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Message has 'forward to active' flag set. Forwarding message as is. Message:", message);
this.sendToActive(message);
}
}
async processReceivedMessage(message, port){
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Received message from popup/content script!", message, "port", port);
this.handleMessage(message, port)
}
processReceivedMessage_nonpersistent(message, sender){
this.logger.log('info', 'comms', "%c[CommsServer.js::processMessage_nonpersistent] Received message from background script!", "background-color: #11D; color: #aad", message, sender);
this.handleMessage(message, sender);
}
// chrome shitiness mitigation
sendUnmarkPlayer(message) {
this.logger.log('info', 'comms', '[CommsServer.js::sendUnmarkPlayer] Chrome is a shit browser that doesn\'t do port.postMessage() in unload events, so we have to resort to inelegant hacks. If you see this, then the workaround method works.');
this.processReceivedMessage(message, this.popupPort);
}
}
export default CommsServer;

View File

@ -86,7 +86,11 @@ class PlayerUi extends UI {
//#region lifecycle
replace(playerElement) {
try {
super.replace(this.getUiConfig(playerElement));
} catch (e) {
this.logger.log('error', 'Couldn\'t replace player element for ui. Error:', e);
}
}
//#endregion
}

View File

@ -15,7 +15,7 @@ class UI {
) {
this.interfaceId = interfaceId;
this.commsConfig = commsConfig;
this.storeConfig = storeConfig,
this.storeConfig = storeConfig;
this.uiConfig = uiConfig;
this.init();

View File

@ -1,27 +1,55 @@
import Debug from '../../conf/Debug';
import VideoData from './VideoData';
import RescanReason from './enums/RescanReason';
import AspectRatio from '../../../common/enums/aspect-ratio.enum';
import CropModePersistence from '../../../common/enums/crop-mode-persistence.enum';
import RescanReason from './enums/RescanReason.enum';
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import CropModePersistence from '../../../common/enums/CropModePersistence.enum';
import Logger from '../Logger';
import Settings from '../Settings';
import ExtensionMode from '../../../common/enums/ExtensionMode.enum';
import CommsClient from '../comms/CommsClient';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading PageInfo");
}
class PageInfo {
//#region flags
readOnly: boolean = false;
hasVideos: boolean = false;
siteDisabled: boolean = false;
//#endregion
//#region timers and timeouts
rescanTimer: any;
urlCheckTimer: any;
announceZoomTimeout: any;
//#endregion
//#region helper objects
logger: Logger;
settings: Settings;
comms: CommsClient;
videos: VideoData[] = [];
//#endregion
//#region misc stuff
lastUrl: string;
extensionMode: ExtensionMode;
defaultCrop: any;
currentCrop: any;
actionHandlerInitQueue: any[] = [];
currentZoomScale: number = 1;
actionHandler: any;
//#endregion
constructor(comms, settings, logger, extensionMode, readOnly = false){
this.logger = logger;
this.hasVideos = false;
this.siteDisabled = false;
this.videos = [];
this.settings = settings;
this.actionHandlerInitQueue = [];
this.lastUrl = window.location.href;
this.extensionMode = extensionMode;
this.readOnly = readOnly;
this.defaultCrop = undefined;
this.currentCrop = undefined;
if (comms){
this.comms = comms;
@ -54,8 +82,6 @@ class PageInfo {
this.rescan(RescanReason.PERIODIC);
this.scheduleUrlCheck();
this.currentZoomScale = 1;
}
async injectCss(cssString) {
@ -85,9 +111,9 @@ class PageInfo {
if(this.rescanTimer){
clearTimeout(this.rescanTimer);
}
for (var video of this.videos) {
for (let video of this.videos) {
try {
this.comms.unregisterVideo(video.id)
(this.comms.unregisterVideo as any)(video.vdid)
video.destroy();
} catch (e) {
this.logger.log('error', ['debug', 'init'], '[PageInfo::destroy] unable to destroy video! Error:', e);
@ -95,7 +121,7 @@ class PageInfo {
}
try {
playerStyleString = this.settings.active.sites[window.location.hostname].css;
const playerStyleString = this.settings.active.sites[window.location.hostname].css;
if (playerStyleString) {
this.comms.sendMessage({
cmd: 'eject-css',
@ -108,7 +134,7 @@ class PageInfo {
}
reset() {
for(var video of this.videos) {
for(let video of this.videos) {
video.destroy();
}
this.rescan(RescanReason.MANUAL);
@ -124,7 +150,7 @@ class PageInfo {
setActionHandler(actionHandler) {
this.actionHandler = actionHandler;
for (var item of this.actionHandlerInitQueue) {
for (let item of this.actionHandlerInitQueue) {
this.actionHandler.registerHandleMouse(item);
}
this.actionHandlerInitQueue = [];
@ -132,8 +158,8 @@ class PageInfo {
getVideos(host) {
if (this.settings.active.sites[host]?.DOM?.video?.manual
&& this.settings.active.sites[host]?.DOM?.video?.querySelector){
const videos = document.querySelectorAll(this.settings.active.sites[host].DOM.video.querySelector);
&& this.settings.active.sites[host]?.DOM?.video?.querySelectors){
const videos = document.querySelectorAll(this.settings.active.sites[host].DOM.video.querySelectors);
if (videos.length) {
return videos;
@ -150,7 +176,7 @@ class PageInfo {
const oldVideoCount = this.videos.length;
try{
var vids = this.getVideos(window.location.hostname);
let vids = this.getVideos(window.location.hostname);
if(!vids || vids.length == 0){
this.hasVideos = false;
@ -164,8 +190,8 @@ class PageInfo {
// add new videos
this.hasVideos = false;
var videoExists = false;
var video, v;
let videoExists = false;
let video, v;
for (video of vids) {
// če najdemo samo en video z višino in širino, to pomeni, da imamo na strani veljavne videe
@ -245,9 +271,11 @@ class PageInfo {
// }
if (this.videos.length > 0) {
this.comms.registerVideo({host: window.location.hostname, location: window.location});
// this.comms.registerVideo({host: window.location.hostname, location: window.location});
this.comms.registerVideo();
} else {
this.comms.unregisterVideo({host: window.location.hostname, location: window.location});
// this.comms.unregisterVideo({host: window.location.hostname, location: window.location});
this.comms.unregisterVideo();
}
}
@ -286,7 +314,7 @@ class PageInfo {
clearTimeout(this.rescanTimer);
}
var ths = this;
let ths = this;
this.rescanTimer = setTimeout(function(rescanReason){
ths.rescanTimer = null;
@ -304,7 +332,7 @@ class PageInfo {
clearTimeout(this.urlCheckTimer);
}
var ths = this;
let ths = this;
this.urlCheckTimer = setTimeout(function(){
ths.urlCheckTimer = null;
@ -329,14 +357,14 @@ class PageInfo {
initArDetection(playingOnly){
if (playingOnly) {
for(var vd of this.videos){
for(let vd of this.videos){
if(vd.isPlaying()) {
vd.initArDetection();
}
}
return;
} else {
for(var vd of this.videos){
for(let vd of this.videos){
vd.initArDetection();
}
}
@ -347,13 +375,13 @@ class PageInfo {
// these need to be called on tab switch
pauseProcessing(playingOnly){
if (playingOnly) {
for(var vd of this.videos){
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.pause();
}
}
} else {
for(var vd of this.videos){
for(let vd of this.videos){
vd.pause();
}
}
@ -361,7 +389,7 @@ class PageInfo {
resumeProcessing(resumeAutoar = false, playingOnly = false){
if (playingOnly) {
for(var vd of this.videos){
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.resume();
if(resumeAutoar){
@ -370,7 +398,7 @@ class PageInfo {
}
}
} else {
for(var vd of this.videos){
for(let vd of this.videos){
vd.resume();
if(resumeAutoar){
vd.resumeAutoAr();
@ -385,13 +413,13 @@ class PageInfo {
this.logger.log('info', 'debug', '[PageInfo::startArDetection()] starting automatic ar detection!')
}
if (playingOnly) {
for(var vd of this.videos){
if (video.isPlaying()) {
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.startArDetection();
}
}
} else {
for(var vd of this.videos){
for(let vd of this.videos){
vd.startArDetection();
}
}
@ -399,28 +427,28 @@ class PageInfo {
stopArDetection(playingOnly){
if (playingOnly) {
for(var vd of this.videos){
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.stopArDetection();
}
}
} else {
for(var vd of this.videos){
for(let vd of this.videos){
vd.stopArDetection();
}
}
}
setAr(ar, playingOnly){
setAr(ar, playingOnly?: boolean){
this.logger.log('info', 'debug', '[PageInfo::setAr] aspect ratio:', ar, "playing only?", playingOnly)
if (ar.type !== AspectRatio.Automatic) {
if (ar.type !== AspectRatioType.Automatic) {
this.stopArDetection(playingOnly);
} else {
this.logger.log('info', 'debug', '[PageInfo::setAr] aspect ratio is auto');
try {
for (var vd of this.videos) {
for (let vd of this.videos) {
if (!playingOnly || vd.isPlaying()) {
vd.resetLastAr();
}
@ -434,14 +462,14 @@ class PageInfo {
}
// TODO: find a way to only change aspect ratio for one video
if (ar === AspectRatio.Reset) {
for (var vd of this.videos) {
if (ar === AspectRatioType.Reset) {
for (let vd of this.videos) {
if (!playingOnly || vd.isPlaying()) {
vd.resetAr();
}
}
} else {
for (var vd of this.videos) {
for (let vd of this.videos) {
if (!playingOnly || vd.isPlaying()) {
vd.setAr(ar)
}
@ -451,78 +479,78 @@ class PageInfo {
setVideoAlignment(videoAlignment, playingOnly) {
if (playingOnly) {
for(var vd of this.videos) {
for(let vd of this.videos) {
if (vd.isPlaying()) {
vd.setVideoAlignment(videoAlignment)
}
}
} else {
for(var vd of this.videos) {
for(let vd of this.videos) {
vd.setVideoAlignment(videoAlignment)
}
}
}
setPanMode(mode, playingOnly) {
setPanMode(mode, playingOnly?: boolean) {
if (playingOnly) {
for(var vd of this.videos) {
for(let vd of this.videos) {
if (vd.isPlaying()) {
vd.setPanMode(mode);
}
}
} else {
for(var vd of this.videos) {
for(let vd of this.videos) {
vd.setPanMode(mode);
}
}
}
restoreAr(playingOnly) {
restoreAr(playingOnly?: boolean) {
if (playingOnly) {
for(var vd of this.videos){
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.restoreAr();
}
}
} else {
for(var vd of this.videos){
for(let vd of this.videos){
vd.restoreAr();
}
}
}
setStretchMode(stretchMode, playingOnly, fixedStretchRatio){
setStretchMode(stretchMode, playingOnly?: boolean, fixedStretchRatio?: boolean){
// TODO: find a way to only change aspect ratio for one video
if (playingOnly) {
for(var vd of this.videos){
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.setStretchMode(stretchMode, fixedStretchRatio)
}
}
} else {
for(var vd of this.videos){
for(let vd of this.videos){
vd.setStretchMode(stretchMode, fixedStretchRatio)
}
}
}
setZoom(zoomLevel, no_announce, playingOnly) {
setZoom(zoomLevel, no_announce?: boolean, playingOnly?: boolean) {
if (playingOnly) {
for(var vd of this.videos) {
for(let vd of this.videos) {
if (vd.isPlaying()) {
vd.setZoom(zoomLevel, no_announce);
}
}
} else {
for(var vd of this.videos) {
for(let vd of this.videos) {
vd.setZoom(zoomLevel, no_announce);
}
}
}
zoomStep(step, playingOnly) {
for(var vd of this.videos){
zoomStep(step, playingOnly?: boolean) {
for(let vd of this.videos){
if (!playingOnly || vd.isPlaying()) {
vd.zoomStep(step);
}
@ -530,19 +558,19 @@ class PageInfo {
}
markPlayer(name, color) {
for (var vd of this.videos) {
for (let vd of this.videos) {
vd.markPlayer(name,color);
}
}
unmarkPlayer() {
for (var vd of this.videos) {
for (let vd of this.videos) {
vd.unmarkPlayer();
}
}
announceZoom(scale) {
if (this.announceZoomTimeout) {
clearTimeout(this.announceZoom);
clearTimeout(this.announceZoomTimeout);
}
this.currentZoomScale = scale;
const ths = this;
@ -550,13 +578,13 @@ class PageInfo {
}
setManualTick(manualTick) {
for(var vd of this.videos) {
vd.setManualTick();
for(let vd of this.videos) {
vd.setManualTick(manualTick);
}
}
tick() {
for(var vd of this.videos) {
for(let vd of this.videos) {
vd.tick();
}
}

View File

@ -1,9 +1,14 @@
import Debug from '../../conf/Debug';
import ExtensionMode from '../../../common/enums/extension-mode.enum'
import AspectRatio from '../../../common/enums/aspect-ratio.enum';
import ExtensionMode from '../../../common/enums/ExtensionMode.enum'
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import PlayerNotificationUi from '../uwui/PlayerNotificationUI';
import PlayerUi from '../uwui/PlayerUI';
import BrowserDetect from '../../conf/BrowserDetect';
import * as _ from 'lodash';
import { sleep } from '../../../common/js/utils';
import VideoData from './VideoData';
import Settings from '../Settings';
import Logger from '../Logger';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading: PlayerData.js");
@ -37,6 +42,48 @@ if (process.env.CHANNEL !== 'stable'){
*/
class PlayerData {
//#region helper objects
logger: Logger;
videoData: VideoData;
settings: Settings;
notificationService: PlayerNotificationUi;
//#endregion
//#region HTML objects
video: any;
element: any;
overlayNode: any;
//#endregion
//#region flags
invalid: boolean = false;
private periodicallyRefreshPlayerElement: boolean = false;
halted: boolean = true;
//#region misc stuff
extensionMode: any;
dimensions: {width?: number, height?: number, fullscreen?: boolean};
private playerIdElement: any;
private observer: ResizeObserver;
//#endregion
/**
* Gets player aspect ratio. If in full screen, it returns screen aspect ratio unless settings say otherwise.
*/
get aspectRatio() {
try {
if (this.dimensions?.fullscreen && !this.settings.getSettingsForSite()?.usePlayerArInFullscreen) {
return window.innerWidth / window.innerHeight;
}
return this.dimensions.width / this.dimensions.height;
} catch (e) {
console.error('cannot determine aspect ratio!', e);
return 1;
}
}
constructor(videoData) {
try {
this.logger = videoData.logger;
@ -76,18 +123,24 @@ class PlayerData {
}
}
async sleep(timeout) {
return new Promise( (resolve, reject) => setTimeout(() => resolve(), timeout));
}
/**
* Returns whether we're in fullscreen mode or not.
*/
static isFullScreen(){
return ( window.innerHeight == window.screen.height && window.innerWidth == window.screen.width);
const ihdiff = Math.abs(window.screen.height - window.innerHeight);
const iwdiff = Math.abs(window.screen.width - window.innerWidth);
// Chrome on linux on X on mixed PPI displays may return ever so slightly different values
// for innerHeight vs screen.height abd innerWidth vs. screen.width, probably courtesy of
// fractional scaling or something. This means we'll give ourself a few px of margin — the
// window elements visible in not-fullscreen are usually double digit px tall
return ( ihdiff < 5 && iwdiff < 5 );
}
// player size observer may not be strictly necessary here
onPlayerDimensionsChanged(mutationList, observer, context) {
if (context.checkPlayerSizeChange()) {
context.videoData.resizer.restore();
onPlayerDimensionsChanged(mutationList?, observer?) {
if (this?.checkPlayerSizeChange()) {
this.videoData.resizer.restore();
}
}
@ -113,8 +166,31 @@ class PlayerData {
}
try {
const ths = this;
this.observer = new MutationObserver((m,o) => this.onPlayerDimensionsChanged(m,o,ths));
if (BrowserDetect.firefox) {
this.observer = new ResizeObserver(
_.debounce( // don't do this too much:
this.onPlayerDimensionsChanged,
250, // do it once per this many ms
{
leading: true, // do it when we call this fallback first
trailing: true // do it after the timeout if we call this callback few more times
}
)
);
} else {
// Chrome for some reason insists that this.onPlayerDimensionsChanged is not a function
// when it's not wrapped into an anonymous function
this.observer = new ResizeObserver(
_.debounce( // don't do this too much:
(m,o) => this.onPlayerDimensionsChanged(m,o),
250, // do it once per this many ms
{
leading: true, // do it when we call this fallback first
trailing: true // do it after the timeout if we call this callback few more times
}
)
);
}
const observerConf = {
attributes: true,
@ -122,7 +198,7 @@ class PlayerData {
attributeOldValue: true,
};
this.observer.observe(this.element, observerConf);
this.observer.observe(this.element);
} catch (e) {
console.error("failed to set observer",e )
}
@ -133,11 +209,11 @@ class PlayerData {
async legacyChangeDetection() {
while (!this.halted) {
await this.sleep(1000);
await sleep(1000);
try {
this.doPeriodicPlayerElementChangeCheck();
} catch (e) {
console.error('[PlayerData::legacycd] this message is pretty high on the list of messages you shouldnt see', e);
console.error('[PlayerData::legacycd] this message is pretty high on the list of messages you shouldn\'t see', e);
}
}
}
@ -157,7 +233,7 @@ class PlayerData {
this.destroyOverlay();
}
var overlay = document.createElement('div');
let overlay = document.createElement('div');
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.position = 'absolute';
@ -243,7 +319,9 @@ class PlayerData {
const videoWidth = this.video.offsetWidth;
const videoHeight = this.video.offsetHeight;
const elementQ = [];
let scorePenalty = 0;
const scorePenalty = 10;
const sizePenaltyMultiplier = 0.1;
let penaltyMultiplier = 0;
let score;
try {
@ -297,7 +375,7 @@ class PlayerData {
}
// elements farther away from the video get a penalty
score -= (scorePenalty++) * 20;
score -= (scorePenalty) * 20;
// push the element on the queue/stack:
elementQ.push({
@ -332,33 +410,40 @@ class PlayerData {
continue;
}
// element je player, če je ena stranica enako velika kot video, druga pa večja ali enaka.
// za enakost dovolimo mala odstopanja
// element is player, if one of the sides is as long as the video and the other bigger (or same)
// we allow for tiny variations when checking for equality
if ( (element.offsetWidth >= videoWidth && this.equalish(element.offsetHeight, videoHeight, 2))
|| (element.offsetHeight >= videoHeight && this.equalish(element.offsetWidth, videoHeight, 2))) {
// todo — in case the match is only equalish and not exact, take difference into account when
// calculating score
score = 100;
// This entire section is disabled because of some bullshit on vk and some shady CIS streaming sites.
// Possibly removal of this criteria is not necessary, because there was also a bug with force player
// element is player, if at least one of the sides is as long as the video
// note that we can't make any additional assumptions with regards to player
// size, since there are both cases where the other side is bigger _and_ cases
// where other side is smaller than the video.
//
// Don't bother thinking about this too much, as any "thinking" was quickly
// corrected by bugs caused by various edge cases.
if (
this.equalish(element.offsetHeight, videoHeight, 5)
|| this.equalish(element.offsetWidth, videoWidth, 5)
) {
score = 1000;
// if (element.id.indexOf('player') !== -1) { // prefer elements with 'player' in id
// score += 75;
// }
// this has only been observed on steam
// if (element.id.indexOf('movie') !== -1) {
// score += 75;
// }
// if (element.classList.toString().indexOf('player') !== -1) { // prefer elements with 'player' in classlist, but a bit less than id
// score += 50;
// }
score -= scorePenalty++; // prefer elements closer to <video>
// -------------------
// PENALTIES
// -------------------
//
// Our ideal player will be as close to the video element, and it will als
// be as close to the size of the video.
// prefer elements closer to <video>
score -= scorePenalty * penaltyMultiplier++;
// the bigger the size difference between the video and the player,
// the more penalty we'll incur. Since we did some grace ith
let playerSizePenalty = 1;
if ( element.offsetHeight > (videoHeight + 5)) {
playerSizePenalty = (element.offsetWidth - videoHeight) * sizePenaltyMultiplier;
}
if ( element.offsetWidth > (videoWidth + 5)) {
playerSizePenalty *= (element.offsetWidth - videoWidth) * sizePenaltyMultiplier
}
score -= playerSizePenalty;
elementQ.push({
element: element,

View File

@ -2,10 +2,60 @@ 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';
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import * as _ from 'lodash';
import BrowserDetect from '../../conf/BrowserDetect';
import Logger from '../Logger';
import Settings from '../Settings';
import PageInfo from './PageInfo';
import { sleep } from '../../../common/js/utils';
import { hasDrm } from '../ar-detect/DrmDetecor';
class VideoData {
//#region flags
arSetupComplete: boolean = false;
destroyed: boolean = false;
invalid: boolean = false;
videoStatusOk: boolean = false;
videoLoaded: boolean = false;
videoDimensionsLoaded: boolean = false;
paused: boolean = false;
//#endregion
//#region misc stuff
vdid: string;
video: any;
observer: ResizeObserver;
mutationObserver: MutationObserver;
mutationObserverConf: MutationObserverInit = {
attributes: true,
attributeFilter: ['class', 'style'],
attributeOldValue: true,
};
extensionMode: any;
userCssClassName: string;
validationId: number;
dimensions: any;
//#endregion
//#region helper objects
logger: Logger;
settings: Settings;
pageInfo: PageInfo;
player: PlayerData;
resizer: Resizer;
arDetector: ArDetector;
//#endregion
get aspectRatio() {
try {
return this.video.videoWidth / this.video.videoHeight;
} catch (e) {
console.error('cannot determine stream aspect ratio!', e);
return 1;
}
}
constructor(video, settings, pageInfo){
this.vdid = (Math.random()*100).toFixed();
@ -23,6 +73,8 @@ class VideoData {
this.videoLoaded = false;
this.videoDimensionsLoaded = true;
this.validationId = null;
this.dimensions = {
width: this.video.offsetWidth,
height: this.video.offsetHeight,
@ -33,6 +85,18 @@ class VideoData {
async onVideoLoaded() {
if (!this.videoLoaded) {
/**
* video.readyState 101:
* 0 no info. Can't play.
* 1 we have metadata but nothing else
* 2 we have data for current playback position, but not future <--- meaning current frame, meaning Aard can work here or higher
* 3 we have a lil bit for the future
* 4 we'll survive to the end
*/
if (!this.video?.videoWidth || !this.video?.videoHeight || this.video.readyState < 2) {
return; // onVideoLoaded is a lie in this case
}
this.logger.log('info', 'init', '%c[VideoData::onVideoLoaded] ——————————— Initiating phase two of videoData setup ———————————', 'color: #0f9');
this.videoLoaded = true;
@ -48,18 +112,26 @@ class VideoData {
this.restoreCrop();
this.videoDimensionsLoaded = true;
}
}
videoUnloaded() {
this.videoLoaded = false;
}
async injectBaseCss() {
try {
if (!this.mutationObserver) {
this.setupMutationObserver();
}
await this.pageInfo.injectCss(`
.uw-ultrawidify-base-wide-screen {
margin: 0px 0px 0px 0px !important;
width: initial !important;
align-self: start !important;
justify-self: start !important;
max-height: initial !important;
max-width: initial !important;
}
`);
} catch (e) {
@ -68,39 +140,51 @@ class VideoData {
}
unsetBaseClass() {
this.mutationObserver.disconnect();
this.mutationObserver = undefined;
this.video.classList.remove('uw-ultrawidify-base-wide-screen');
}
//#region <video> event handlers
onLoadedData() {
this.logger.log('info', 'init', '[VideoData::ctor->video.onloadeddata] Video fired event "loaded data!"');
this.onVideoLoaded();
}
onLoadedMetadata() {
this.logger.log('info', 'init', '[VideoData::ctor->video.onloadedmetadata] Video fired event "loaded metadata!"');
this.onVideoLoaded();
}
onTimeUpdate() {
this.onVideoLoaded();
}
//#endregion
//#region lifecycle-ish
/**
* Injects base CSS and sets up handlers for <video> tag events
*/
async setupStageOne() {
this.logger.log('info', 'init', '%c[VideoData::setupStageOne] ——————————— Starting setup stage one! ———————————', 'color: #0f9');
// ensure base css is loaded before doing anything
this.injectBaseCss();
// this is in case extension loads before the video
this.video.addEventListener('loadeddata', () => {
this.logger.log('info', 'init', '[VideoData::ctor->video.onloadeddata] Video fired event "loaded data!"');
this.onVideoLoaded();
});
this.video.addEventListener('loadeddata', this.onLoadedData.bind(this));
this.video.addEventListener('loadedmetadata', this.onLoadedMetadata.bind(this));
// this one is in case extension loads after the video is loaded
this.video.addEventListener('timeupdate', () => {
this.onVideoLoaded();
});
this.video.addEventListener('timeupdate', this.onTimeUpdate.bind(this));
this.logger.log('info', 'init', '%c[VideoData::setupStageOne] ——————————— Setup stage one complete! ———————————', 'color: #0f9');
}
/**
* Launches the extension for a given video (after the video element is defined well enough
* for our standards)
*/
async setupStageTwo() {
// 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;
@ -110,11 +194,38 @@ class VideoData {
this.resizer = new Resizer(this);
// 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);
try {
if (BrowserDetect.firefox) {
this.observer = new ResizeObserver(
_.debounce(
this.onVideoDimensionsChanged,
250,
{
leading: true,
trailing: true
}
)
);
} else {
// Chrome for some reason insists that this.onPlayerDimensionsChanged is not a function
// when it's not wrapped into an anonymous function
this.observer = new ResizeObserver(
_.debounce(
(m, o) => {
this.onVideoDimensionsChanged(m, o)
},
250,
{
leading: true,
trailing: true
}
)
);
}
} catch (e) {
console.error('[VideoData] Observer setup failed:', e);
}
this.observer.observe(this.video);
// INIT AARD
this.arDetector = new ArDetector(this); // this starts Ar detection. needs optional parameter that prevets ardetdctor from starting
@ -158,6 +269,87 @@ class VideoData {
}
}
setupMutationObserver() {
try {
if (BrowserDetect.firefox) {
this.mutationObserver = new MutationObserver(
_.debounce(
this.onVideoMutation,
250,
{
leading: true,
trailing: true
}
)
)
} else {
// Chrome for some reason insists that this.onPlayerDimensionsChanged is not a function
// when it's not wrapped into an anonymous function
this.mutationObserver = new MutationObserver(
_.debounce(
(m, o) => {
this.onVideoMutation(m, o)
},
250,
{
leading: true,
trailing: true
}
)
)
}
} catch (e) {
console.error('[VideoData] Observer setup failed:', e);
}
this.mutationObserver.observe(this.video, this.mutationObserverConf);
}
/**
* cleans up handlers and stuff when the show is over
*/
destroy() {
this.logger.log('info', ['debug', 'init'], `[VideoData::destroy] <vdid:${this.vdid}> received destroy command`);
if (this.video) {
this.video.classList.remove(this.userCssClassName);
this.video.classList.remove('uw-ultrawidify-base-wide-screen');
this.video.removeEventListener('onloadeddata', this.onLoadedData);
this.video.removeEventListener('onloadedmetadata', this.onLoadedMetadata);
this.video.removeEventListener('ontimeupdate', this.onTimeUpdate);
}
this.pause();
this.destroyed = true;
try {
this.arDetector.stop();
this.arDetector.destroy();
} catch (e) {}
this.arDetector = undefined;
try {
this.resizer.destroy();
} catch (e) {}
this.resizer = undefined;
try {
this.player.destroy();
} catch (e) {}
try {
this.observer.disconnect();
} catch (e) {}
this.player = undefined;
this.video = undefined;
}
//#endregion
//#region video status
isVideoPlaying() {
return this.video && !!(this.video.currentTime > 0 && !this.video.paused && !this.video.ended && this.video.readyState > 2);
}
hasVideoStartedPlaying() {
return this.video && this.video.currentTime > 0;
}
//#endregion
restoreCrop() {
this.logger.log('info', 'debug', '[VideoData::restoreCrop] Attempting to reset aspect ratio.')
@ -177,9 +369,15 @@ class VideoData {
}
}
/**
* Starts fallback change detection (validates whether currently applied settings are correct)
*/
async fallbackChangeDetection() {
while (!this.destroyed && !this.invalid) {
await this.sleep(500);
const validationId = Date.now();
this.validationId = validationId;
while (!this.destroyed && !this.invalid && this.validationId === validationId) {
await sleep(500);
this.doPeriodicFallbackChangeDetectionCheck();
}
}
@ -188,59 +386,84 @@ class VideoData {
this.validateVideoOffsets();
}
async sleep(timeout) {
return new Promise( (resolve) => setTimeout(() => resolve(), timeout));
}
onVideoDimensionsChanged(mutationList, observer, context) {
if (!mutationList || context.video === undefined) { // something's wrong
if (observer && context.video) {
observer.disconnect();
}
return;
}
onVideoMutation(mutationList?: MutationRecord[], observer?) {
// verify that mutation didn't remove our class. Some pages like to do that.
let confirmAspectRatioRestore = false;
for (let mutation of mutationList) {
if (!this.video) {
this.logger.log('error', 'debug', '[VideoData::onVideoMutation] mutation was triggered, but video element is missing. Something is fishy. Terminating this uw instance.');
this.destroy();
return;
}
for(const mutation of mutationList) {
if (mutation.type === 'attributes') {
if (mutation.attributeName === 'class') {
if(!context.video.classList.contains(this.userCssClassName) ) {
if( mutation.attributeName === 'class'
&& mutation.oldValue.indexOf('uw-ultrawidify-base-wide-screen') !== -1
&& !this.video.classList.contains('uw-ultrawidify-base-wide-screen')
) {
// 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);
context.video.classList.add('uw-ultrawidify-base-wide-screen');
}
// 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') {
this.video.classList.add(this.userCssClassName);
this.video.classList.add('uw-ultrawidify-base-wide-screen');
} else if (mutation.attributeName === 'style') {
confirmAspectRatioRestore = true;
}
}
}
if (!confirmAspectRatioRestore) {
this.processDimensionsChanged();
}
onVideoDimensionsChanged(mutationList, observer) {
if (!mutationList || this.video === undefined) { // something's wrong
if (observer && this.video) {
observer.disconnect();
}
return;
}
this.processDimensionsChanged();
}
/**
* Forces Ultrawidify to resotre aspect ratio. You should never call this method directly,
* instead you should be calling processDimensionChanged() wrapper function.
*/
private _processDimensionsChanged() {
// 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();
this.player.forceRefreshPlayerElement();
this.restoreAr();
// sometimes something fucky wucky happens and mutations aren't detected correctly, so we
// try to get around that
setTimeout( () => {
context.validateVideoOffsets();
this.validateVideoOffsets();
}, 100);
}
/**
* Restores aspect ratio and validates video offsets after the restore. Execution uses
* debounce to limit how often the function executes.
*/
private processDimensionsChanged() {
_.debounce(
this._processDimensionsChanged,
250,
{
leading: true,
trailing: true
}
);
}
validateVideoOffsets() {
// validate if current video still exists. If not, we destroy current object
try {
@ -251,17 +474,17 @@ class VideoData {
} catch (e) {
}
// THIS BREAKS PANNING
const cs = window.getComputedStyle(this.video);
const pcs = window.getComputedStyle(this.player.element);
const videoComputedStyle = window.getComputedStyle(this.video);
const playerComputedStyle = window.getComputedStyle(this.player.element);
try {
const transformMatrix = cs.transform.split(')')[0].split(',');
const transformMatrix = videoComputedStyle.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]);
const vh = +(videoComputedStyle.height.split('px')[0]);
const vw = +(videoComputedStyle.width.split('px')[0]);
const ph = +(playerComputedStyle.height.split('px')[0]);
const pw = +(playerComputedStyle.width.split('px')[0]);
// TODO: check & account for panning and alignment
if (transformMatrix[0] !== 'none'
@ -269,10 +492,11 @@ class VideoData {
&& this.isWithin(vw, (pw - (translateX * 2)), 2)) {
} else {
this.player.forceDetectPlayerElementChange();
this.restoreAr();
}
} catch(e) {
// do nothing on fail
console.error('Validating video offsets failed:', e)
}
}
@ -284,7 +508,7 @@ class VideoData {
* Gets the contents of the style attribute of the video element
* in a form of an object.
*/
getVideoStyle() {
getVideoStyle(): any {
// This will _always_ give us an array. Empty string gives an array
// that contains one element. That element is an empty string.
const styleArray = (this.video.getAttribute('style') || '').split(';');
@ -326,8 +550,6 @@ class VideoData {
}
return heightCompensationFactor;
}
firstTimeArdInit(){
if(this.destroyed || this.invalid) {
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
@ -358,6 +580,11 @@ class VideoData {
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
return;
}
if (hasDrm(this.video)) {
this.player.showNotification('AARD_DRM');
}
if (!this.arDetector) {
this.initArDetection();
}
@ -378,35 +605,6 @@ class VideoData {
}
}
destroy() {
this.logger.log('info', ['debug', 'init'], `[VideoData::destroy] <vdid:${this.vdid}> received destroy command`);
if (this.video) {
this.video.classList.remove(this.userCssClassName);
this.video.classList.remove('uw-ultrawidify-base-wide-screen');
}
this.pause();
this.destroyed = true;
try {
this.arDetector.stop();
this.arDetector.destroy();
} catch (e) {}
this.arDetector = undefined;
try {
this.resizer.destroy();
} catch (e) {}
this.resizer = undefined;
try {
this.player.destroy();
} catch (e) {}
try {
this.observer.disconnect();
} catch (e) {}
this.player = undefined;
this.video = undefined;
}
pause(){
this.paused = true;
if(this.arDetector){
@ -424,7 +622,7 @@ class VideoData {
}
this.paused = false;
try {
this.resizer.start();
// this.resizer.start();
if (this.player) {
this.player.start();
}
@ -459,12 +657,12 @@ class VideoData {
this.resizer.setLastAr(lastAr);
}
setAr(ar, lastAr){
setAr(ar, lastAr?){
if (this.invalid) {
return;
}
if (ar.type === AspectRatio.Fixed || ar.type === AspectRatio.FitHeight || ar.type === AspectRatio.FitHeight) {
if (ar.type === AspectRatioType.Fixed || ar.type === AspectRatioType.FitHeight || ar.type === AspectRatioType.FitHeight) {
this.player.forceRefreshPlayerElement();
}
@ -485,7 +683,7 @@ class VideoData {
this.resizer.setLastAr('original');
}
panHandler(event, forcePan) {
panHandler(event, forcePan?: boolean) {
if (this.invalid) {
return;
}

View File

@ -0,0 +1,7 @@
enum RescanReason {
PERIODIC = 0,
URL_CHANGE = 1,
MANUAL = 2
};
export default RescanReason;

View File

@ -1,7 +0,0 @@
var RescanReason = Object.freeze({
PERIODIC: 0,
URL_CHANGE: 1,
MANUAL: 2
});
export default RescanReason;

View File

@ -24,7 +24,7 @@ class CssHandler {
}
}
for (var i in styleArray) {
for (let i in styleArray) {
styleArray[i] = styleArray[i].trim();
// some sites do 'top: 50%; left: 50%; transform: <transform>' to center videos.
// we dont wanna, because we already center videos on our own
@ -45,7 +45,7 @@ class CssHandler {
static buildStyleString(styleArray) {
let styleString = '';
for(var i in styleArray) {
for(let i in styleArray) {
if(styleArray[i]) {
styleString += styleArray[i] + "; ";
}

View File

@ -1,42 +1,67 @@
import Debug from '../../conf/Debug';
import Scaler from './Scaler';
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/extension-mode.enum';
import Stretch from '../../../common/enums/stretch.enum';
import VideoAlignment from '../../../common/enums/video-alignment.enum';
import AspectRatio from '../../../common/enums/aspect-ratio.enum';
import CropModePersistance from '../../../common/enums/crop-mode-persistence.enum';
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 Settings from '../Settings';
import VideoData from '../video-data/VideoData';
if(Debug.debug) {
console.log("Loading: Resizer.js");
}
class Resizer {
//#region flags
canPan: boolean = false;
destroyed: boolean = false;
//#endregion
//#region helper objects
logger: Logger;
settings: Settings;
scaler: Scaler;
stretcher: Stretcher;
zoom: Zoom;
conf: VideoData;
//#endregion
//#region HTML elements
video: any;
//#endregion
//#region data
correctedVideoDimensions: any;
currentCss: any;
currentStyleString: string;
currentPlayerStyleString: any;
currentCssValidFor: any;
currentVideoSettings: any;
lastAr: {type: any, ratio?: number} = {type: AspectRatioType.Initial};
resizerId: any;
videoAlignment: any;
userCss: string;
userCssClassName: any;
pan: any = null;
//#endregion
constructor(videoData) {
this.resizerId = (Math.random(99)*100).toFixed(0);
this.resizerId = (Math.random()*100).toFixed(0);
this.conf = videoData;
this.logger = videoData.logger;
this.video = videoData.video;
this.settings = videoData.settings;
this.extensionMode = videoData.extensionMode;
this.scaler = new Scaler(this.conf);
this.stretcher = new Stretcher(this.conf);
this.zoom = new Zoom(this.conf);
// load up default values
this.correctedVideoDimensions = {};
this.currentCss = {};
this.currentStyleString = "";
this.currentPlayerStyleString = "";
this.currentCssValidFor = {};
this.lastAr = {type: AspectRatio.Initial};
this.videoAlignment = this.settings.getDefaultVideoAlignment(window.location.hostname); // this is initial video alignment
this.destroyed = false;
@ -47,7 +72,6 @@ class Resizer {
this.canPan = false;
}
this.userCss = '';
this.userCssClassName = videoData.userCssClassName;
}
@ -74,13 +98,13 @@ class Resizer {
calculateRatioForLegacyOptions(ar){
// also present as modeToAr in Scaler.js
if (ar.type !== AspectRatio.FitWidth && ar.type !== AspectRatio.FitHeight && ar.ratio) {
if (ar.type !== AspectRatioType.FitWidth && ar.type !== AspectRatioType.FitHeight && ar.ratio) {
return ar;
}
// Skrbi za "stare" možnosti, kot na primer "na širino zaslona", "na višino zaslona" in "ponastavi".
// Približevanje opuščeno.
// handles "legacy" options, such as 'fit to widht', 'fit to height' and AspectRatio.Reset. No zoom tho
var ratioOut;
// handles "legacy" options, such as 'fit to widht', 'fit to height' and AspectRatioType.Reset. No zoom tho
let ratioOut;
if (!this.conf.video) {
this.logger.log('info', 'debug', "[Scaler.js::modeToAr] No video??",this.conf.video, "killing videoData");
@ -102,15 +126,15 @@ class Resizer {
// 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).
var fileAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
let fileAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
if (ar.type === AspectRatio.FitWidth){
if (ar.type === AspectRatioType.FitWidth){
ar.ratio = ratioOut > fileAr ? ratioOut : fileAr;
}
else if(ar.type === AspectRatio.FitHeight){
else if(ar.type === AspectRatioType.FitHeight){
ar.ratio = ratioOut < fileAr ? ratioOut : fileAr;
}
else if(ar.type === AspectRatio.Reset){
else if(ar.type === AspectRatioType.Reset){
this.logger.log('info', 'debug', "[Scaler.js::modeToAr] Using original aspect ratio -", fileAr);
ar.ratio = fileAr;
} else {
@ -128,7 +152,7 @@ class Resizer {
// 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 === AspectRatio.Automatic || ar.type === AspectRatio.Fixed) {
if (ar.type === AspectRatioType.Automatic || ar.type === AspectRatioType.Fixed) {
if (!ar.ratio || isNaN(ar.ratio)) {
return;
}
@ -140,27 +164,29 @@ class Resizer {
}
}
async setAr(ar, lastAr) {
async setAr(ar: {type: any, ratio?: number}, lastAr?: {type: any, ratio?: number}) {
if (this.destroyed) {
return;
}
if (!this.video.videoWidth || !this.video.videoHeight) {
this.logger.log('warning', 'debug', '[Resizer::setAr] <rid:'+this.resizerId+'> Video has no width or no height. This is not allowed. Aspect ratio will not be set.');
this.logger.log('warning', 'debug', '[Resizer::setAr] <rid:'+this.resizerId+'> Video has no width or no height. This is not allowed. Aspect ratio will not be set, and videoData will be uninitialized.');
this.conf.videoUnloaded();
}
this.logger.log('info', 'debug', '[Resizer::setAr] <rid:'+this.resizerId+'> trying to set ar. New ar:', ar)
this.logger.log('info', 'debug', '[Resizer::setAr] <rid:'+this.resizerId+'> trying to set ar. New ar:', ar);
if (ar == null) {
return;
}
const siteSettings = this.settings.active.sites[window.location.hostname];
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
// AspectRatio.Fixed when zooming, so let's keep that in mind
// AspectRatioType.Fixed when zooming, so let's keep that in mind
if (
(ar.type !== AspectRatio.Fixed && ar.type !== AspectRatio.Manual) // anything not these two _always_ changes AR
(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
) {
@ -172,51 +198,15 @@ class Resizer {
// 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 === AspectRatio.Automatic ||
ar.type === AspectRatio.Reset ||
ar.type === AspectRatio.Initial ) {
if (ar.type === AspectRatioType.Automatic ||
ar.type === AspectRatioType.Reset ||
ar.type === AspectRatioType.Initial ) {
// reset/undo default
this.conf.pageInfo.updateCurrentCrop(undefined);
} else {
this.conf.pageInfo.updateCurrentCrop(ar);
}
// if (ar.type === AspectRatio.Automatic ||
// ar.type === AspectRatio.Reset && this.lastAr.type === AspectRatio.Initial) {
// // some sites do things that interfere with our site (and aspect ratio setting in general)
// // first, we check whether video contains anything we don't like
// if (siteSettings?.autoarPreventConditions?.videoStyleString) {
// const styleString = (this.video.getAttribute('style') || '').split(';');
// if (siteSettings.autoarPreventConditions.videoStyleString.containsProperty) {
// const bannedProperties = siteSettings.autoarPreventConditions.videoStyleString.containsProperty;
// for (const prop in bannedProperties) {
// for (const s of styleString) {
// if (s.trim().startsWith(prop)) {
// // check if css property has a list of allowed values:
// if (bannedProperties[prop].allowedValues) {
// const styleValue = s.split(':')[1].trim();
// // check if property value is on the list of allowed values
// // if it's not, we aren't allowed to start aard
// if (bannedProperties[prop].allowedValues.indexOf(styleValue) === -1) {
// this.logger.log('error', 'debug', "%c[Resizer::setAr] video style contains forbidden css property/value combo: ", "color: #900, background: #100", prop, " — we aren't allowed to start autoar.")
// return;
// }
// } else {
// // no allowed values, no problem. We have forbidden property
// // and this means aard can't start.
// this.logger.log('info', 'debug', "%c[Resizer::setAr] video style contains forbidden css property: ", "color: #900, background: #100", prop, " — we aren't allowed to start autoar.")
// return;
// }
// }
// }
// }
// }
// }
// }
if (lastAr) {
this.lastAr = this.calculateRatioForLegacyOptions(lastAr);
ar = this.calculateRatioForLegacyOptions(ar);
@ -231,7 +221,7 @@ class Resizer {
this.lastAr = {type: ar.type, ratio: ar.ratio}
}
// if (this.extensionMode === ExtensionMode.Basic && !PlayerData.isFullScreen() && ar.type !== AspectRatio.Reset) {
// if (this.extensionMode === ExtensionMode.Basic && !PlayerData.isFullScreen() && ar.type !== AspectRatioType.Reset) {
// // don't actually apply or calculate css when using basic mode if not in fullscreen
// // ... unless we're resetting the aspect ratio to original
// return;
@ -246,20 +236,20 @@ class Resizer {
// * ar.type is auto, but stretch is set to basic basic stretch
//
// unpause when using other modes
if (ar.type !== AspectRatio.Automatic || this.stretcher.mode === Stretch.Basic) {
if (ar.type !== AspectRatioType.Automatic || this.stretcher.mode === StretchType.Basic) {
this.conf?.arDetector?.pause();
} else {
if (this.lastAr.type === AspectRatio.Automatic) {
if (this.lastAr.type === AspectRatioType.Automatic) {
this.conf?.arDetector?.unpause();
}
}
// do stretch thingy
if (this.stretcher.mode === Stretch.NoStretch
|| this.stretcher.mode === Stretch.Conditional
|| this.stretcher.mode === Stretch.FixedSource){
if (this.stretcher.mode === StretchType.NoStretch
|| this.stretcher.mode === StretchType.Conditional
|| this.stretcher.mode === StretchType.FixedSource){
var stretchFactors = this.scaler.calculateCrop(ar);
stretchFactors = this.scaler.calculateCrop(ar);
if(! stretchFactors || stretchFactors.error){
this.logger.log('error', 'debug', `[Resizer::setAr] <rid:${this.resizerId}> failed to set AR due to problem with calculating crop. Error:`, stretchFactors?.error);
@ -278,33 +268,36 @@ class Resizer {
}
}
if (this.stretcher.mode === Stretch.Conditional){
if (this.stretcher.mode === StretchType.Conditional){
this.stretcher.applyConditionalStretch(stretchFactors, ar.ratio);
} else if (this.stretcher.mode === Stretch.FixedSource) {
} else if (this.stretcher.mode === StretchType.FixedSource) {
this.stretcher.applyStretchFixedSource(stretchFactors);
}
this.logger.log('info', 'debug', "[Resizer::setAr] Processed stretch factors for ",
this.stretcher.mode === Stretch.NoStretch ? 'stretch-free crop.' :
this.stretcher.mode === Stretch.Conditional ? 'crop with conditional stretch.' : 'crop with fixed stretch',
this.stretcher.mode === StretchType.NoStretch ? 'stretch-free crop.' :
this.stretcher.mode === StretchType.Conditional ? 'crop with conditional StretchType.' : 'crop with fixed stretch',
'Stretch factors are:', stretchFactors
);
} else if (this.stretcher.mode === Stretch.Hybrid) {
var stretchFactors = this.stretcher.calculateStretch(ar.ratio);
} else if (this.stretcher.mode === StretchType.Hybrid) {
stretchFactors = this.stretcher.calculateStretch(ar.ratio);
this.logger.log('info', 'debug', '[Resizer::setAr] Processed stretch factors for hybrid stretch/crop. Stretch factors are:', stretchFactors);
} else if (this.stretcher.mode === Stretch.Fixed) {
var stretchFactors = this.stretchFactors.calculateStretchFixed(ar.ratio)
} else if (this.stretcher.mode === Stretch.Basic) {
var stretchFactors = this.stretcher.calculateBasicStretch();
this.logger.log('info', 'debug', '[Resizer::setAr] Processed stretch factors for basic stretch. Stretch factors are:', stretchFactors);
} else if (this.stretcher.mode === StretchType.Fixed) {
stretchFactors = this.stretcher.calculateStretchFixed(ar.ratio)
} else if (this.stretcher.mode === StretchType.Basic) {
stretchFactors = this.stretcher.calculateBasicStretch();
this.logger.log('info', 'debug', '[Resizer::setAr] Processed stretch factors for basic StretchType. Stretch factors are:', stretchFactors);
} else {
var stretchFactors = {xFactor: 1, yFactor: 1};
stretchFactors = {xFactor: 1, yFactor: 1};
this.logger.log('error', 'debug', '[Resizer::setAr] Okay wtf happened? If you see this, something has gone wrong', stretchFactors,"\n------[ i n f o d u m p ]------\nstretcher:", this.stretcher);
}
this.zoom.applyZoom(stretchFactors);
var translate = this.computeOffsets(stretchFactors);
this.stretcher.chromeBugMitigation(stretchFactors);
let translate = this.computeOffsets(stretchFactors);
this.applyCss(stretchFactors, translate);
}
@ -312,13 +305,13 @@ class Resizer {
toFixedAr() {
// converting to fixed AR means we also turn off autoAR
this.setAr({
ar: this.lastAr.ar,
type: AspectRatio.Fixed
ratio: this.lastAr.ratio,
type: AspectRatioType.Fixed
});
}
resetLastAr() {
this.lastAr = {type: AspectRatio.Initial};
this.lastAr = {type: AspectRatioType.Initial};
}
setLastAr(override){
@ -329,7 +322,7 @@ class Resizer {
return this.lastAr;
}
setStretchMode(stretchMode, fixedStretchRatio){
setStretchMode(stretchMode, fixedStretchRatio?){
this.stretcher.setStretchMode(stretchMode, fixedStretchRatio);
this.restore();
}
@ -340,10 +333,10 @@ class Resizer {
return;
}
// dont allow weird floats
this.videoAlignment = VideoAlignment.Center;
this.videoAlignment = VideoAlignmentType.Center;
// because non-fixed aspect ratios reset panning:
if (this.lastAr.type !== AspectRatio.Fixed) {
if (this.lastAr.type !== AspectRatioType.Fixed) {
this.toFixedAr();
}
@ -359,7 +352,7 @@ class Resizer {
}
resetPan() {
this.pan = {};
this.pan = {x: 0, y: 0};
this.videoAlignment = this.settings.getDefaultVideoAlignment(window.location.hostname);
}
@ -367,7 +360,7 @@ class Resizer {
// relativeMousePos[X|Y] - on scale from 0 to 1, how close is the mouse to player edges.
// use these values: top, left: 0, bottom, right: 1
if(! this.pan){
this.pan = {};
this.pan = {x: 0, y: 0};
}
if (this.settings.active.miscSettings.mousePanReverseMouse) {
@ -389,8 +382,8 @@ class Resizer {
this.logger.log('info', 'debug', "[Resizer::restore] <rid:"+this.resizerId+"> attempting to restore aspect ratio", {'lastAr': this.lastAr} );
// this is true until we verify that css has actually been applied
if(this.lastAr.type === AspectRatio.Initial){
this.setAr({type: AspectRatio.Reset});
if(this.lastAr.type === AspectRatioType.Initial){
this.setAr({type: AspectRatioType.Reset});
}
else {
if (this.lastAr?.ratio === null) {
@ -406,7 +399,7 @@ class Resizer {
this.setStretchMode(this.settings.active.sites[window.location.hostname]?.stretch ?? this.settings.active.sites['@global'].stretch);
this.zoom.setZoom(1);
this.resetPan();
this.setAr({type: AspectRatio.Reset});
this.setAr({type: AspectRatioType.Reset});
}
setPanMode(mode) {
@ -419,10 +412,6 @@ class Resizer {
}
}
resetPan(){
this.pan = undefined;
}
setZoom(zoomLevel, no_announce) {
this.zoom.setZoom(zoomLevel, no_announce);
}
@ -437,11 +426,11 @@ class Resizer {
}
resetCrop(){
this.setAr({type: AspectRatio.Reset});
this.setAr({type: AspectRatioType.Reset});
}
resetStretch(){
this.stretcher.setStretchMode(Stretch.NoStretch);
this.stretcher.setStretchMode(StretchType.NoStretch);
this.restore();
}
@ -512,19 +501,55 @@ class Resizer {
}
}
computeOffsets(stretchFactors){
/**
* Sometimes, sites (e.g. new reddit) will guarantee that video fits width of its container
* and let the browser figure out the height through the magic of height:auto. This is bad,
* because our addon generally relies of videos always being 100% of the height of the
* container.
*
* This sometimes leads to a situation where realVideoHeight and realVideoWidth at least
* one of which should be roughly equal to the player width or hight with the other one being
* either smaller or equal are both smaller than player width or height; and sometimes
* rather substantially. Fortunately for us, realVideo[Width|Height] and player dimensions
* never lie, which allows us to calculate the extra scale factor we need.
*
* Returned factor for this function should do fit:contain, not fit:cover.
* @param realVideoWidth real video width
* @param realVideoHeight real video height
* @param playerWidth player width
* @param playerHeight player height
* @param mode whether to
*/
computeAutoHeightCompensationFactor(realVideoWidth: number, realVideoHeight: number, playerWidth: number, playerHeight: number, mode: 'height' | 'width'): number {
const widthFactor = playerWidth / realVideoWidth;
const heightFactor = playerHeight / realVideoHeight;
return mode === 'height' ? heightFactor : widthFactor;
}
private _computeOffsetsRecursionGuard: boolean = false;
computeOffsets(stretchFactors: VideoDimensions){
this.logger.log('info', 'debug', "[Resizer::computeOffsets] <rid:"+this.resizerId+"> video will be aligned to ", this.settings.active.sites['@global'].videoAlignment);
const {realVideoWidth, realVideoHeight, marginX, marginY} = this.computeVideoDisplayedDimensions();
// correct any remaining element size discrepencies (applicable only to certain crop strategies!)
// NOTE: it's possible that we might also need to apply a similar measure for CropPillarbox strategy
// (but we'll wait for bug reports before doing so).
// We also don't compensate for height:auto if height is provided via element style
let autoHeightCompensationFactor;
if (
stretchFactors.cropStrategy === CropStrategy.CropLetterbox
&& (!stretchFactors.styleHeightCompensationFactor || stretchFactors.styleHeightCompensationFactor === 1)
) {
autoHeightCompensationFactor = this.computeAutoHeightCompensationFactor(realVideoWidth, realVideoHeight, this.conf.player.dimensions.width, this.conf.player.dimensions.height, 'height');
stretchFactors.xFactor *= autoHeightCompensationFactor;
stretchFactors.yFactor *= autoHeightCompensationFactor;
}
const wdiff = this.conf.player.dimensions.width - realVideoWidth;
const hdiff = this.conf.player.dimensions.height - realVideoHeight;
if (wdiff < 0 && hdiff < 0 && this.zoom.scale > 1) {
this.conf.player.restore();
}
const wdiffAfterZoom = realVideoWidth * stretchFactors.xFactor - this.conf.player.dimensions.width;
const hdiffAfterZoom = realVideoHeight * stretchFactors.yFactor - this.conf.player.dimensions.height;
@ -536,32 +561,38 @@ class Resizer {
if (this.pan) {
if (this.pan.relativeOffsetX || this.pan.relativeOffsetY) {
// don't offset when video is smaller than player
if(wdiffAfterZoom >= 0 || hdiffAfterZoom >= 0) {
translate.x += wdiffAfterZoom * this.pan.relativeOffsetX * this.zoom.scale;
translate.y += hdiffAfterZoom * this.pan.relativeOffsetY * this.zoom.scale;
}
} else {
if (this.videoAlignment == VideoAlignment.Left) {
if (this.videoAlignment == VideoAlignmentType.Left) {
translate.x += wdiffAfterZoom * 0.5;
}
else if (this.videoAlignment == VideoAlignment.Right) {
else if (this.videoAlignment == VideoAlignmentType.Right) {
translate.x -= wdiffAfterZoom * 0.5;
}
}
this.logger.log('info', ['debug', 'resizer'], "[Resizer::_res_computeOffsets] <rid:"+this.resizerId+"> calculated offsets:\n\n",
'---- data in ----',
this.logger.log(
'info', ['debug', 'resizer'], "[Resizer::_res_computeOffsets] <rid:"+this.resizerId+"> calculated offsets:",
'\n\n---- elements ----',
'\nplayer element: ', this.conf.player.element,
'\nvideo element: ', this.conf.video,
'\n\n---- data in ----',
'\nplayer dimensions: ', {w: this.conf.player.dimensions.width, h: this.conf.player.dimensions.height},
'\nvideo dimensions: ', {w: this.conf.video.offsetWidth, h: this.conf.video.offsetHeight},
'\nreal video dimensions:', {w: realVideoWidth, h: realVideoHeight},
'\nauto compensation: ', 'x', autoHeightCompensationFactor,
'\nstretch factors: ', stretchFactors,
'\npan & zoom: ', this.pan, this.zoom.scale,
'\nwdiff, hdiff: ', wdiff, 'x', hdiff,
'\nwdiff, hdiffAfterZoom:', wdiffAfterZoom, 'x', hdiffAfterZoom,
'\n\n---- data out ----\n',
'translate:', translate);
'translate:', translate
);
// by the way, let's do a quick sanity check whether video player is doing any fuckies wuckies
// fucky wucky examples:
@ -580,14 +611,22 @@ class Resizer {
`Video seems to be both wider and taller (or shorter and narrower) than player element at the same time. This is super duper not supposed to happen.\n\n`,
`Player element needs to be checked.`
)
// sometimes this appears to randomly recurse.
// There seems to be no way to reproduce it.
if (! this._computeOffsetsRecursionGuard) {
this._computeOffsetsRecursionGuard = true;
if (this.conf.player.checkPlayerSizeChange()) {
this.conf.player.onPlayerDimensionsChanged();
}
this._computeOffsetsRecursionGuard = false;
}
}
return translate;
}
//#region css handling
buildStyleArray(existingStyleString, extraStyleString) {
if (existingStyleString) {
const styleArray = existingStyleString.split(";");
@ -611,7 +650,7 @@ class Resizer {
}
}
for (var i in styleArray) {
for (let i in styleArray) {
styleArray[i] = styleArray[i].trim();
// some sites do 'top: 50%; left: 50%; transform: <transform>' to center videos.
// we dont wanna, because we already center videos on our own
@ -634,7 +673,7 @@ class Resizer {
buildStyleString(styleArray) {
let styleString = '';
for(var i in styleArray) {
for(let i in styleArray) {
if(styleArray[i]) {
styleString += styleArray[i] + " !important; ";
}
@ -711,6 +750,7 @@ class Resizer {
this.logger.log('info', ['debug', 'resizer'], "[Resizer::setStyleString] <rid:"+this.resizerId+"> Existing css is still valid, doing nothing.");
}
}
//#endregion
}
export default Resizer;

View File

@ -1,12 +1,45 @@
import Debug from '../../conf/Debug';
import AspectRatio from '../../../common/enums/aspect-ratio.enum';
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import BrowserDetect from '../../conf/BrowserDetect';
import VideoData from '../video-data/VideoData';
import Logger from '../Logger';
export enum CropStrategy {
/**
* Nomenclature explained:
*
* SP - stream AR < player AR
* PS - the opposite of
*
* ArDominant - given aspect ratio is bigger than stream AR and player AR
* PSDominant - stream AR or player AR are bigger than given aspect ratio
*/
CropLetterbox = 1,
NoCropPillarbox = 2,
NoCropLetterbox = 3,
CropPillarbox = 4
}
export type VideoDimensions = {
xFactor?: number;
yFactor?: number;
cropStrategy?: number;
arCorrectionFactor?: number;
styleHeightCompensationFactor?: number;
actualWidth?: number;
actualHeight?: number;
}
// računa velikost videa za približevanje/oddaljevanje
// does video size calculations for zooming/cropping
class Scaler {
// internal variables
//#region helper objects
conf: VideoData;
logger: Logger;
//#endregion
// functions
constructor(videoData) {
@ -17,13 +50,13 @@ class Scaler {
// Skrbi za "stare" možnosti, kot na primer "na širino zaslona", "na višino zaslona" in "ponastavi".
// Približevanje opuščeno.
// handles "legacy" options, such as 'fit to widht', 'fit to height' and AspectRatio.Reset. No zoom tho
// handles "legacy" options, such as 'fit to widht', 'fit to height' and AspectRatioType.Reset. No zoom tho
modeToAr (ar) {
if (ar.type !== AspectRatio.FitWidth && ar.type !== AspectRatio.FitHeight && ar.ratio) {
if (ar.type !== AspectRatioType.FitWidth && ar.type !== AspectRatioType.FitHeight && ar.ratio) {
return ar.ratio;
}
var ratioOut;
let ratioOut;
if (!this.conf.video) {
this.logger.log('error', 'debug', "[Scaler.js::modeToAr] No video??",this.conf.video, "killing videoData");
@ -44,19 +77,19 @@ class Scaler {
// 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).
var fileAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
let fileAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
if (ar.type === AspectRatio.FitWidth) {
if (ar.type === AspectRatioType.FitWidth) {
ratioOut > fileAr ? ratioOut : fileAr
ar.ratio = ratioOut;
return ratioOut;
}
else if (ar.type === AspectRatio.FitHeight) {
else if (ar.type === AspectRatioType.FitHeight) {
ratioOut < fileAr ? ratioOut : fileAr
ar.ratio = ratioOut;
return ratioOut;
}
else if (ar.type === AspectRatio.Reset) {
else if (ar.type === AspectRatioType.Reset) {
this.logger.log('info', 'debug', "[Scaler.js::modeToAr] Using original aspect ratio -", fileAr)
ar.ar = fileAr;
return fileAr;
@ -65,7 +98,7 @@ class Scaler {
return null;
}
calculateCrop(ar) {
calculateCrop(ar: {type: AspectRatioType, ratio?: number}) {
/**
* STEP 1: NORMALIZE ASPECT RATIO
*
@ -74,7 +107,7 @@ class Scaler {
* undoes any zoom that style="height:123%" on the video element adds.
*
* There are few exceptions and additional caveatss:
* * AspectRatio.FitHeight: we don't want to pre-downscale the video at all, as things
* * AspectRatioType.FitHeight: we don't want to pre-downscale the video at all, as things
* will be scaled to fit height as-is.
* * When player is wider than stream, we want to undo any height compensations site
* tacks on the video tag.
@ -85,17 +118,17 @@ class Scaler {
* * because video width is normalized on 100% of the parent, we don't need to correct
* anything when the player is wider than the video.
*/
const streamAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
const playerAr = this.conf.player.dimensions.width / this.conf.player.dimensions.height;
const streamAr = this.conf.aspectRatio;
const playerAr = this.conf.player.aspectRatio;
const heightCompensationFactor = this.conf.getHeightCompensationFactor();
const compensatedStreamAr = streamAr * heightCompensationFactor;
let arCorrectionFactor = 1;
if (ar.type !== AspectRatio.FitHeight) {
if (ar.type !== AspectRatioType.FitHeight) {
if (playerAr < compensatedStreamAr) {
arCorrectionFactor = this.conf.player.dimensions.width / this.conf.video.offsetWidth;
} else if (ar.type !== AspectRatio.Reset) {
} else if (ar.type !== AspectRatioType.Reset) {
arCorrectionFactor /= heightCompensationFactor;
}
}
@ -114,8 +147,8 @@ class Scaler {
return {error: "illegal_video_dimensions"};
}
if (ar.type === AspectRatio.Reset){
return {xFactor: arCorrectionFactor, yFactor: arCorrectionFactor}
if (ar.type === AspectRatioType.Reset){
return {xFactor: arCorrectionFactor, yFactor: arCorrectionFactor, arCorrectionFactor: arCorrectionFactor};
}
// handle fuckie-wuckies
@ -137,42 +170,62 @@ class Scaler {
// Dejansko razmerje stranic datoteke/<video> značke
// Actual aspect ratio of the file/<video> tag
if (ar.type === AspectRatio.Initial || !ar.ratio) {
if (ar.type === AspectRatioType.Initial || !ar.ratio) {
ar.ratio = streamAr;
}
this.logger.log('info', 'scaler', "[Scaler::calculateCrop] ar is " ,ar.ratio, ", file ar is", streamAr, ", this.conf.player.dimensions are ", this.conf.player.dimensions.width, "×", this.conf.player.dimensions.height, "| obj:", this.conf.player.dimensions);
var videoDimensions = {
const videoDimensions: VideoDimensions = {
xFactor: 1,
yFactor: 1,
actualWidth: 0, // width of the video (excluding pillarbox) when <video> tag height is equal to width
actualHeight: 0, // height of the video (excluding letterbox) when <video> tag height is equal to height
arCorrectionFactor: arCorrectionFactor,
styleHeightCompensationFactor: heightCompensationFactor
}
this.calculateCropCore(videoDimensions, ar.ratio, streamAr, playerAr)
return videoDimensions;
}
/**
* The act of calculating aspect ratio is separated due to resue elsewhere in the extension.
* We are doing that to avoid surprise recursions.
* @param {*} videoDimensions
* @param {*} ar
* @param {*} streamAr
* @param {*} playerAr
*/
calculateCropCore(videoDimensions: VideoDimensions, ar: number, streamAr: number, playerAr: number) {
if (streamAr < playerAr) {
if (streamAr < ar.ratio){
if (streamAr < ar){
// in this situation we have to crop letterbox on top/bottom of the player
// we cut it, but never more than the player
videoDimensions.xFactor = Math.min(ar.ratio, playerAr) / streamAr;
videoDimensions.xFactor = Math.min(ar, playerAr) / streamAr;
videoDimensions.yFactor = videoDimensions.xFactor;
videoDimensions.cropStrategy = CropStrategy.CropLetterbox;
} else {
// in this situation, we would be cutting pillarbox. Inside horizontal player.
// I don't think so. Except exceptions, we'll wait for bug reports.
videoDimensions.xFactor = 1;
videoDimensions.yFactor = 1;
videoDimensions.cropStrategy = CropStrategy.NoCropPillarbox;
}
} else {
if (streamAr < ar.ratio || playerAr < ar.ratio){
if (streamAr < ar || playerAr < ar){
// in this situation, we need to add extra letterbox on top of our letterbox
// this means we simply don't crop anything _at all_
videoDimensions.xFactor = 1;
videoDimensions.yFactor = 1;
videoDimensions.cropStrategy = CropStrategy.NoCropLetterbox;
} else {
// meant for handling pillarbox crop. not quite implemented.
videoDimensions.xFactor = streamAr / Math.min(ar.ratio, playerAr);
videoDimensions.xFactor = streamAr / Math.min(ar, playerAr);
videoDimensions.yFactor = videoDimensions.xFactor;
videoDimensions.cropStrategy = CropStrategy.CropPillarbox;
// videoDimensions.xFactor = Math.max(ar.ratio, playerAr) * fileAr;
// videoDimensions.yFactor = videoDimensions.xFactor;
}
@ -180,9 +233,11 @@ class Scaler {
this.logger.log('info', 'scaler', "[Scaler::calculateCrop] Crop factor calculated — ", videoDimensions.xFactor);
// correct the factors
videoDimensions.xFactor *= arCorrectionFactor;
videoDimensions.yFactor *= arCorrectionFactor;
// correct the scale factor
if (videoDimensions.arCorrectionFactor) {
videoDimensions.xFactor *= videoDimensions.arCorrectionFactor;
videoDimensions.yFactor *= videoDimensions.arCorrectionFactor;
}
return videoDimensions;
}

View File

@ -1,4 +1,9 @@
import Stretch from '../../../common/enums/stretch.enum';
import StretchType from '../../../common/enums/StretchType.enum';
import BrowserDetect from '../../conf/BrowserDetect';
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import VideoData from '../video-data/VideoData';
import Logger from '../Logger';
import Settings from '../Settings';
// računa vrednosti za transform-scale (x, y)
// transform: scale(x,y) se uporablja za raztegovanje videa, ne pa za približevanje
@ -6,8 +11,20 @@ import Stretch from '../../../common/enums/stretch.enum';
// transform: scale(x,y) is used for stretching, not zooming.
class Stretcher {
// internal variables
//#region flags
//#endregion
//#region helper objects
conf: VideoData;
logger: Logger;
settings: Settings;
//#endregion
//#region misc data
mode: any;
fixedStretchRatio: any;
//#endregion
// functions
constructor(videoData) {
@ -18,11 +35,11 @@ class Stretcher {
this.fixedStretchRatio = undefined;
}
setStretchMode(stretchMode, fixedStretchRatio) {
if (stretchMode === Stretch.Default) {
setStretchMode(stretchMode, fixedStretchRatio?) {
if (stretchMode === StretchType.Default) {
this.mode = this.settings.getDefaultStretchMode(window.location.hostname);
} else {
if (stretchMode === Stretch.Fixed || stretchMode == Stretch.FixedSource) {
if (stretchMode === StretchType.Fixed || stretchMode == StretchType.FixedSource) {
this.fixedStretchRatio = fixedStretchRatio;
}
this.mode = stretchMode;
@ -30,17 +47,17 @@ class Stretcher {
}
applyConditionalStretch(stretchFactors, actualAr){
var playerAr = this.conf.player.dimensions.width / this.conf.player.dimensions.height;
var videoAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
let playerAr = this.conf.player.aspectRatio;
let videoAr = this.conf.aspectRatio;
if (! actualAr){
actualAr = playerAr;
}
var newWidth = this.conf.video.offsetWidth * stretchFactors.xFactor;
var newHeight = this.conf.video.offsetHeight * stretchFactors.yFactor;
let newWidth = this.conf.video.offsetWidth * stretchFactors.xFactor;
let newHeight = this.conf.video.offsetHeight * stretchFactors.yFactor;
var actualWidth, actualHeight;
let actualWidth, actualHeight;
// determine the dimensions of the video (sans black bars) after scaling
if(actualAr < videoAr){
@ -51,11 +68,11 @@ class Stretcher {
actualWidth = newWidth;
}
var minW = this.conf.player.dimensions.width * (1 - this.settings.active.stretch.conditionalDifferencePercent);
var maxW = this.conf.player.dimensions.width * (1 + this.settings.active.stretch.conditionalDifferencePercent);
let minW = this.conf.player.dimensions.width * (1 - this.settings.active.stretch.conditionalDifferencePercent);
let maxW = this.conf.player.dimensions.width * (1 + this.settings.active.stretch.conditionalDifferencePercent);
var minH = this.conf.player.dimensions.height * (1 - this.settings.active.stretch.conditionalDifferencePercent);
var maxH = this.conf.player.dimensions.height * (1 + this.settings.active.stretch.conditionalDifferencePercent);
let minH = this.conf.player.dimensions.height * (1 - this.settings.active.stretch.conditionalDifferencePercent);
let maxH = this.conf.player.dimensions.height * (1 + this.settings.active.stretch.conditionalDifferencePercent);
if (actualWidth >= minW && actualWidth <= maxW) {
stretchFactors.xFactor *= this.conf.player.dimensions.width / actualWidth;
@ -85,25 +102,25 @@ class Stretcher {
// This means we want to calculate stretching using those values, but we don't know
// them. This means we have to calculate them.
const videoAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
if (this.conf.player.dimensions.width > this.conf.player.dimensions.height * videoAr) {
const streamAr = this.conf.aspectRatio;
if (this.conf.player.dimensions.width > this.conf.player.dimensions.height * streamAr) {
return {
xFactor: this.conf.player.dimensions.width / (this.conf.player.dimensions.height * videoAr),
xFactor: this.conf.player.dimensions.width / (this.conf.player.dimensions.height * streamAr),
yFactor: 1
};
}
return {
xFactor: 1,
yFactor: this.conf.player.dimensions.height / (this.conf.player.dimensions.width / videoAr)
yFactor: this.conf.player.dimensions.height / (this.conf.player.dimensions.width / streamAr)
};
}
applyStretchFixedSource(postCropStretchFactors) {
const videoAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
const playerAr = this.conf.player.dimensions.width / this.conf.player.dimensions.height;
const streamAr = this.conf.aspectRatio;
const playerAr = this.conf.player.aspectRatio;
const squeezeFactor = this.fixedStretchRatio / videoAr;
const squeezeFactor = this.fixedStretchRatio / streamAr;
// Whether squeezing happens on X or Y axis depends on whether required AR is wider or narrower than
// the player, in which the video is displayed
@ -113,16 +130,11 @@ class Stretcher {
this.logger.log('info', 'stretcher', `[Stretcher::applyStretchFixedSource] here's what we got:
postCropStretchFactors: x=${postCropStretchFactors.xFactor} y=${postCropStretchFactors.yFactor}
fixedStretchRatio: ${this.fixedStretchRatio}
videoAr: ${videoAr}
videoAr: ${streamAr}
playerAr: ${playerAr}
squeezeFactor: ${squeezeFactor}`, '\nvideo', this.conf.video);
if (this.fixedStretchRatio < playerAr) {
postCropStretchFactors.xFactor *= squeezeFactor;
} else {
postCropStretchFactors.yFactor *= squeezeFactor;
}
this.logger.log('info', 'stretcher', `[Stretcher::applyStretchFixedSource] here's what we'll apply:\npostCropStretchFactors: x=${postCropStretchFactors.x} y=${postCropStretchFactors.y}`);
@ -134,24 +146,21 @@ squeezeFactor: ${squeezeFactor}`, '\nvideo', this.conf.video);
}
getArCorrectionFactor() {
const streamAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
const playerAr = this.conf.player.dimensions.width / this.conf.player.dimensions.height;
let arCorrectionFactor = 1;
arCorrectionFactor = this.conf.player.dimensions.width / this.conf.video.offsetWidth;
return arCorrectionFactor;
}
calculateStretch(actualAr, playerArOverride) {
const playerAr = playerArOverride || this.conf.player.dimensions.width / this.conf.player.dimensions.height;
const streamAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
calculateStretch(actualAr, playerArOverride?) {
const playerAr = playerArOverride || this.conf.player.aspectRatio;
const streamAr = this.conf.aspectRatio;
if (! actualAr){
actualAr = playerAr;
}
var stretchFactors = {
let stretchFactors: any = {
xFactor: 1,
yFactor: 1
};
@ -187,6 +196,7 @@ squeezeFactor: ${squeezeFactor}`, '\nvideo', this.conf.video);
this.logger.log('info', 'stretcher', "[Stretcher.js::calculateStretch] stretching strategy 3")
}
} else {
// player adds LETTERBOX
@ -229,6 +239,69 @@ squeezeFactor: ${squeezeFactor}`, '\nvideo', this.conf.video);
return stretchFactors;
}
/**
* Ensure that <video> element is never both taller-ish and wider-ish than the screen, while in fullscreen
* on Chromium-based browsers.
*
* Workaround for Chrome/Edge issue where zooming too much results in video being stretched incorrectly.
*
* Bug description if the following are true:
* * user is using Chrome or Edge (but surprisingly not Opera)
* * user is using hardware acceleration
* * user is using a noVideo card
* * user is in full screen mode
* * the video is both roughly taller and roughly wider than the monitor
* Then the video will do StretchType.Basic no matter what you put in `transform: scale(x,y)`.
*
* In practice, the issue appears slightly _before_ the last condition is met (video needs to be ~3434 px wide
* in order for this bug to trigger on my 3440x1440 display).
*
* Because this issue happens regardless of how you upscale the video (doesn't matter if you use transform:scale
* or width+height or anything else), the aspect ratio needs to be limited _before_ applying arCorrectionFactor
* (note that arCorrectionFactor is usually <= 1, as it conpensates for zooming that height=[>100%] on <video>
* style attribute does).
*/
chromeBugMitigation(stretchFactors) {
if (
BrowserDetect.anyChromium
&& (this.conf.player?.dimensions?.fullscreen || ! this.settings?.active?.mitigations?.zoomLimit?.fullscreenOnly)
&& this.settings?.active?.mitigations?.zoomLimit?.enabled
) {
const playerAr = this.conf.player.aspectRatio;
const streamAr = this.conf.aspectRatio;
let maxSafeAr: number;
let arLimitFactor = this.settings?.active?.mitigations?.zoomLimit?.limit ?? 0.997;
if (playerAr >= (streamAr * 1.1)) {
maxSafeAr = (window.innerWidth * arLimitFactor) / window.innerHeight;
} else if (playerAr < (streamAr * 0.95)) {
maxSafeAr = window.innerWidth / (window.innerHeight * arLimitFactor);
} else {
// in some cases, we tolerate minor stretch to avoid tiny black bars
return;
}
const maxSafeStretchFactor = this.conf.resizer.scaler.calculateCropCore(
{
xFactor: 1,
yFactor: 1,
arCorrectionFactor: stretchFactors.arCorrectionFactor
},
maxSafeAr,
streamAr,
playerAr
).xFactor;
// console.info('Stretch factors before:', stretchFactors.xFactor, stretchFactors.yFactor, "max safe:", maxSafeStretchFactor, "max safe ar:", maxSafeAr);
stretchFactors.xFactor = Math.min(stretchFactors.xFactor, maxSafeStretchFactor);
stretchFactors.yFactor = Math.min(stretchFactors.yFactor, maxSafeStretchFactor);
return stretchFactors;
}
}
}
export default Stretcher;

View File

@ -1,19 +1,32 @@
import Debug from '../../conf/Debug';
import Logger from '../Logger';
import VideoData from '../video-data/VideoData';
// računa približevanje ter računa/popravlja odmike videa
// calculates zooming and video offsets/panning
class Zoom {
// functions
//#region flags
//#endregion
//#region helper objects
conf: VideoData;
logger: Logger;
//#endregion
//#region misc data
scale: number = 1;
logScale: number = 0;
scaleStep: number = 0.1;
minScale: number = -1; // 50% (log2(0.5) = -1)
maxScale: number = 3; // 800% (log2(8) = 3)
//#endregion
constructor(videoData) {
this.conf = videoData;
this.logger = videoData.logger;
this.scale = 1;
this.logScale = 0;
this.scaleStep = 0.1;
this.minScale = -1; // 50% (log2(0.5) = -1)
this.maxScale = 3; // 800% (log2(8) = 3)
}
reset(){
@ -41,7 +54,7 @@ class Zoom {
this.conf.announceZoom(this.scale);
}
setZoom(scale, no_announce){
setZoom(scale: number, no_announce?){
this.logger.log('info', 'debug', "[Zoom::setZoom] Setting zoom to", scale, "!");
// NOTE: SCALE IS NOT LOGARITHMIC

View File

@ -1,18 +1,9 @@
import Debug from './conf/Debug.js';
import BrowserDetect from './conf/BrowserDetect';
import CommsServer from './lib/comms/CommsServer';
import Settings from './lib/Settings';
import Logger from './lib/Logger';
/**
* NOTE: we cannot get rid of this js file. I tried for 30 seconds and I couldn't get
* extension to work unless I kept this part of extension out of the ts file.
*/
import { sleep } from '../common/js/utils';
// we need vue in bg script, so we can get vuex.
// and we need vuex so popup will be initialized
// after the first click without resorting to ugly,
// dirty hacks
import Vue from 'vue';
import Vuex from 'vuex';
import VuexWebExtensions from 'vuex-webextensions';
import UWServer from './UWServer';
var BgVars = {
arIsActive: true,
@ -20,316 +11,7 @@ var BgVars = {
currentSite: ""
}
class UWServer {
constructor() {
this.ports = [];
this.arIsActive = true;
this.hasVideos = false;
this.currentSite = "";
this.setup();
this.videoTabs = {};
this.currentTabId = 0;
this._gctimeout = undefined;
this.selectedSubitem = {
'siteSettings': undefined,
'videoSettings': undefined,
}
this.uiLoggerInitialized = false;
}
async setup() {
// logger is the first thing that goes up
const loggingOptions = {
isBackgroundScript: true,
allowLogging: true,
useConfFromStorage: true,
logAll: true,
fileOptions: {
enabled: true,
},
consoleOptions: {
enabled: true
}
};
this.logger = new Logger();
await this.logger.init(loggingOptions);
this.settings = new Settings({logger: this.logger});
await this.settings.init();
this.comms = new CommsServer(this);
this.comms.subscribe('show-logger', async () => await this.initUiAndShowLogger());
this.comms.subscribe('init-vue', async () => await this.initUi());
this.comms.subscribe('uwui-vue-initialized', () => this.uiLoggerInitialized = true);
this.comms.subscribe('emit-logs', () => {}); // we don't need to do anything, this gets forwarded to UI content script as is
if(BrowserDetect.firefox) {
browser.tabs.onActivated.addListener((m) => {this.onTabSwitched(m)});
} else if (BrowserDetect.anyChromium) {
chrome.tabs.onActivated.addListener((m) => {this.onTabSwitched(m)});
}
}
async _promisifyTabsGet(browserObj, tabId){
return new Promise( (resolve, reject) => {
browserObj.tabs.get(tabId, (tab) => resolve(tab));
});
}
async injectCss(css, sender) {
try {
if (BrowserDetect.firefox || BrowserDetect.edge) {
browser.tabs.insertCSS(sender.tab.id, {code: css, cssOrigin: 'user', frameId: sender.frameId});
} else if (BrowserDetect.anyChromium) {
chrome.tabs.insertCSS(sender.tab.id, {code: css, cssOrigin: 'user', frameId: sender.frameId});
}
} catch (e) {
this.logger.log('error','debug', '[UwServer::injectCss] Error while injecting css:', {error: e, css, sender});
}
}
async removeCss(css, sender) {
try {
if (BrowserDetect.firefox || BrowserDetect.edge) {
browser.tabs.removeCSS(sender.tab.id, {code: css, cssOrigin: 'user', frameId: sender.frameId});
} else if (BrowserDetect.anyChromium) {
// this doesn't work currently, but hopefully chrome will get this feature in the future
chrome.tabs.removeCSS(sender.tab.id, {code: css, cssOrigin: 'user', frameId: sender.frameId});
}
} catch (e) {
this.logger.log('error','debug', '[UwServer::injectCss] Error while removing css:', {error: e, css, sender});
}
}
async replaceCss(oldCss, newCss, sender) {
if (oldCss !== newCss) {
this.injectCss(newCss, sender);
this.removeCss(oldCss, sender);
}
}
extractHostname(url){
var hostname;
if (!url) {
return "<no url>";
}
// extract hostname
if (url.indexOf("://") > -1) { //find & remove protocol (http, ftp, etc.) and get hostname
hostname = url.split('/')[2];
}
else {
hostname = url.split('/')[0];
}
hostname = hostname.split(':')[0]; //find & remove port number
hostname = hostname.split('?')[0]; //find & remove "?"
return hostname;
}
async onTabSwitched(activeInfo){
this.hasVideos = false;
try {
this.currentTabId = activeInfo.tabId; // just for readability
let tab;
if (BrowserDetect.firefox) {
tab = await browser.tabs.get(this.currentTabId);
} else if (BrowserDetect.anyChromium) {
tab = await this._promisifyTabsGet(chrome, this.currentTabId);
}
this.currentSite = this.extractHostname(tab.url);
this.logger.log('info', 'debug', '[UwServer::onTabSwitched] user switched tab. New site:', this.currentSite);
} catch(e) {
this.logger.log('error', 'debug', '[UwServer::onTabSwitched] there was a problem getting currnet site:', e)
}
this.selectedSubitem = {
'siteSettings': undefined,
'videoSettings': undefined,
}
//TODO: change extension icon based on whether there's any videos on current page
}
registerVideo(sender) {
this.logger.log('info', 'comms', '[UWServer::registerVideo] Registering video.\nsender:', sender);
const tabHostname = this.extractHostname(sender.tab.url);
const frameHostname = this.extractHostname(sender.url);
// preveri za osirotele/zastarele vrednosti ter jih po potrebi izbriši
// check for orphaned/outdated values and remove them if neccessary
if (this.videoTabs[sender.tab.id]?.host != tabHostname) {
delete this.videoTabs[sender.tab.id]
} else if(this.videoTabs[sender.tab.id]?.frames[sender.frameId]?.host != frameHostname) {
delete this.videoTabs[sender.tab.id].frames[sender.frameId];
}
if (this.videoTabs[sender.tab.id]) {
this.videoTabs[sender.tab.id].frames[sender.frameId] = {
id: sender.frameId,
host: frameHostname,
url: sender.url,
registerTime: Date.now(),
}
} else {
this.videoTabs[sender.tab.id] = {
id: sender.tab.id,
host: tabHostname,
url: sender.tab.url,
frames: {}
};
this.videoTabs[sender.tab.id].frames[sender.frameId] = {
id: sender.frameId,
host: frameHostname,
url: sender.url,
registerTime: Date.now(),
}
}
this.logger.log('info', 'comms', '[UWServer::registerVideo] Video registered. current videoTabs:', this.videoTabs);
}
unregisterVideo(sender) {
this.logger.log('info', 'comms', '[UwServer::unregisterVideo] Unregistering video.\nsender:', sender);
if (this.videoTabs[sender.tab.id]) {
if ( Object.keys(this.videoTabs[sender.tab.id].frames).length <= 1) {
delete this.videoTabs[sender.tab.id]
} else {
if(this.videoTabs[sender.tab.id].frames[sender.frameId]) {
delete this.videoTabs[sender.tab.id].frames[sender.frameId];
}
}
}
this.logger.log('info', 'comms', '[UwServer::unregisterVideo] Video has been unregistered. Current videoTabs:', this.videoTabs);
}
setSelectedTab(menu, subitem) {
this.logger.log('info', 'comms', '[UwServer::setSelectedTab] saving selected tab for', menu, ':', subitem);
this.selectedSubitem[menu] = subitem;
}
async initUi() {
try {
if (BrowserDetect.firefox) {
await browser.tabs.executeScript({
file: '/ext/uw-ui.js',
allFrames: true,
});
} else if (BrowserDetect.anyChromium) {
await new Promise( resolve =>
chrome.tabs.executeScript({
file: '/ext/uw-ui.js',
allFrames: true,
}, () => resolve())
);
}
} catch (e) {
this.logger.log('ERROR', 'uwbg', 'UI initialization failed. Reason:', e);
}
}
async initUiAndShowLogger() {
// this implementation is less than optimal and very hacky, but it should work
// just fine for our use case.
this.uiLoggerInitialized = false;
await this.initUi();
await new Promise( async (resolve, reject) => {
// if content script doesn't give us a response within 5 seconds, something is
// obviously wrong and we stop waiting,
// oh and btw, resolve/reject do not break the loops, so we need to do that
// ourselves:
// https://stackoverflow.com/questions/55207256/will-resolve-in-promise-loop-break-loop-iteration
let isRejected = false;
setTimeout( async () => {isRejected = true; reject()}, 5000);
// check whether UI has been initiated on the FE. If it was, we resolve the
// promise and off we go
while (!isRejected) {
if (this.uiLoggerInitialized) {
resolve();
return; // remember the bit about resolve() not breaking the loop?
}
await sleep(100);
}
})
}
async getCurrentTab() {
if (BrowserDetect.firefox) {
return (await browser.tabs.query({active: true, currentWindow: true}))[0];
} else if (BrowserDetect.anyChromium) {
return new Promise((resolve, reject) => chrome.tabs.query({active: true, currentWindow: true}, (x) => resolve(x[0])));
}
}
async getVideoTab() {
// friendly reminder: if current tab doesn't have a video,
// there won't be anything in this.videoTabs[this.currentTabId]
const ctab = await this.getCurrentTab();
if (!ctab || !ctab.id) {
return {
host: 'INVALID SITE',
frames: [],
}
}
if (this.videoTabs[ctab.id]) {
// if video is older than PageInfo's video rescan period (+ 4000ms of grace),
// we clean it up from videoTabs[tabId].frames array.
const ageLimit = Date.now() - this.settings.active.pageInfo.timeouts.rescan - 4000;
console.log("videoTabs[tabId]:", this.videoTabs[ctab.id])
try {
for (const key in this.videoTabs[ctab.id].frames) {
if (this.videoTabs[ctab.id].frames[key].registerTime < ageLimit) {
delete this.videoTabs[ctab.id].frames[key];
}
}
} catch (e) {
// something went wrong. There's prolly no frames.
return {
host: this.extractHostname(ctab.url),
frames: [],
selected: this.selectedSubitem
}
}
return {
...this.videoTabs[ctab.id],
host: this.extractHostname(ctab.url),
selected: this.selectedSubitem
};
}
// return something more or less empty if this tab doesn't have
// a video registered for it
return {
host: this.extractHostname(ctab.url),
frames: [],
selected: this.selectedSubitem
}
}
// chrome shitiness mitigation
sendUnmarkPlayer(message) {
this.comms.sendUnmarkPlayer(message);
}
}
var server = new UWServer();
const server = new UWServer();
window.sendUnmarkPlayer = (message) => {
server.sendUnmarkPlayer(message)

View File

@ -41,13 +41,13 @@ class UwUi {
if (!this.logger) {
const loggingOptions = {
isContentScript: true,
allowLogging: true,
allowLogging: false,
useConfFromStorage: true,
fileOptions: {
enabled: false
},
consoleOptions: {
"enabled": true,
"enabled": false,
"debug": true,
"init": true,
"settings": true,

View File

@ -1,13 +1,8 @@
import Debug from './conf/Debug';
import BrowserDetect from './conf/BrowserDetect';
import ExtensionMode from '../common/enums/extension-mode.enum';
import Settings from './lib/Settings';
import ActionHandler from './lib/ActionHandler';
import Comms from './lib/comms/Comms';
import CommsClient from './lib/comms/CommsClient';
import PageInfo from './lib/video-data/PageInfo';
import Logger from './lib/Logger';
/**
* NOTE: we cannot get rid of this js file. I tried for 30 seconds and I couldn't get
* extension to work unless I kept this part of extension out of the ts file.
*/
import UWContent from './UWContent';
if(process.env.CHANNEL !== 'stable'){
console.warn("\n\n\n\n\n\n ——— Sᴛλʀᴛɪɴɢ Uʟᴛʀᴀɪɪʏ ———\n << ʟᴏᴀᴅɪɴɢ ᴍᴀɪɴ ꜰɪʟᴇ >>\n\n\n\n");
@ -23,165 +18,5 @@ if(process.env.CHANNEL !== 'stable'){
}
}
if (BrowserDetect.edge) {
HTMLCollection.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
}
class UW {
constructor(){
this.pageInfo = undefined;
this.comms = undefined;
this.settings = undefined;
this.actionHandler = undefined;
this.logger = undefined;
this.uiInitiated = false;
this.commsHandlers = {
'get-current-zoom': [() => this.pageInfo.requestCurrentZoom()],
'set-ar': [(message) => this.pageInfo.setAr({type: message.arg, ratio: message.customArg}, message.playing)],
'set-alignment': [(message) => {
this.pageInfo.setVideoAlignment(message.arg, message.playing);
this.pageInfo.restoreAr();
}],
'set-stretch': [(message) => this.pageInfo.setStretchMode(message.arg, message.playing, message.customArg)],
'set-keyboard': [(message) => this.pageInfo.setKeyboardShortcutsEnabled(message.arg)],
'autoar-start': [(message) => {
if (message.enabled !== false) {
this.pageInfo.initArDetection(message.playing);
this.pageInfo.startArDetection(message.playing);
} else {
this.pageInfo.stopArDetection(message.playing);
}
}],
'pause-processing': [(message) => this.pageInfo.pauseProcessing(message.playing)],
'resume-processing': [(message) => this.pageInfo.resumeProcessing(message.autoArStatus, message.playing)],
'set-zoom': [(message) => this.pageInfo.setZoom(message.arg, true, message.playing)],
'change-zoom': [(message) => this.pageInfo.zoomStep(message.arg, message.playing)],
'mark-player': [(message) => this.pageInfo.markPlayer(message.name, message.color)],
'unmark-player': [() => this.pageInfo.unmarkPlayer()],
'autoar-set-manual-tick': [(message) => this.pageInfo.setManualTick(message.arg)],
'autoar-tick': [() => this.pageInfo.tick()],
'set-ar-persistence': [() => this.pageInfo.setArPersistence(message.arg)],
}
}
reloadSettings() {
this.logger.log('info', 'debug', 'Things happened in the popup. Will reload extension settings.');
this.init();
}
async init(){
if (Debug.debug) {
console.log("[uw::main] loading configuration ...");
}
// logger init is the first thing that needs to run
try {
if (!this.logger) {
const loggingOptions = {
isContentScript: true,
allowLogging: true,
useConfFromStorage: true,
fileOptions: {
enabled: false
},
consoleOptions: {
"enabled": true,
"debug": true,
"init": true,
"settings": true,
"keyboard": true,
"mousemove": false,
"actionHandler": true,
"comms": true,
"playerDetect": true,
"resizer": true,
"scaler": true,
"stretcher": true,
// "videoRescan": true,
// "playerRescan": true,
"arDetect": true,
"arDetect_verbose": true
},
allowBlacklistedOrigins: {
'periodicPlayerCheck': false,
'periodicVideoStyleChangeCheck': false,
'handleMouseMove': false
}
};
this.logger = new Logger();
await this.logger.init(loggingOptions);
// show popup if logging to file is enabled
if (this.logger.isLoggingAllowed() && this.logger.isLoggingToFile()) {
console.info("[uw::init] Logging is allowed! Initalizing vue and UI!");
// CommsClient is not initiated yet, so we use static comms to send the command
Comms.sendMessage({cmd: 'show-logger'});
}
}
} catch (e) {
console.error("logger init failed!", e)
}
// init() is re-run any time settings change
if (this.comms) {
this.comms.destroy();
}
if (!this.settings) {
this.settings = new Settings({
onSettingsChanged: () => this.reloadSettings(),
logger: this.logger
});
await this.settings.init();
}
this.comms = new CommsClient('content-main-port', this.logger, this.commsHandlers);
// če smo razširitev onemogočili v nastavitvah, ne naredimo ničesar
// If extension is soft-disabled, don't do shit
var extensionMode = this.settings.getExtensionMode();
this.logger.log('info', 'debug', "[uw::init] Extension mode:" + (extensionMode < 0 ? "disabled" : extensionMode == '1' ? 'basic' : 'full'));
const isSiteDisabled = extensionMode === ExtensionMode.Disabled
if (isSiteDisabled) {
if (this.settings.getExtensionMode('@global') === ExtensionMode.Disabled) {
this.logger.log('info', 'debug', "[uw::init] EXTENSION DISABLED, THEREFORE WONT BE STARTED")
return;
}
}
try {
if (this.pageInfo) {
this.logger.log('info', 'debug', '[uw.js::setup] An instance of pageInfo already exists and will be destroyed.');
this.pageInfo.destroy();
}
this.pageInfo = new PageInfo(this.comms, this.settings, this.logger, extensionMode, isSiteDisabled);
this.logger.log('info', 'debug', "[uw.js::setup] pageInfo initialized.");
this.logger.log('info', 'debug', "[uw.js::setup] will try to initate ActionHandler.");
// start action handler only if extension is enabled for this site
if (!isSiteDisabled) {
if (this.actionHandler) {
this.actionHandler.destroy();
}
this.actionHandler = new ActionHandler(this.pageInfo);
this.actionHandler.init();
this.logger.log('info', 'debug', "[uw.js::setup] ActionHandler initiated.");
}
} catch (e) {
this.logger.log('error', 'debug', "[uw::init] FAILED TO START EXTENSION. Error:", e);
}
}
}
var main = new UW();
const main = new UWContent();
main.init();

View File

@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "Ultrawidify",
"description": "Removes black bars on ultrawide videos and offers advanced options to fix aspect ratio.",
"version": "4.5.1.3",
"version": "5.0.4",
"applications": {
"gecko": {
"id": "{cf02b1a7-a01a-4e37-a609-516a283f1ed3}"

View File

@ -111,11 +111,11 @@
<script>
import Donate from '../common/misc/Donate.vue';
import SuperAdvancedSettings from './SuperAdvancedSettings.vue';
import Debug from '../ext/conf/Debug.js';
import BrowserDetect from '../ext/conf/BrowserDetect.js';
import ExtensionConf from '../ext/conf/ExtensionConf.js';
import ObjectCopy from '../ext/lib/ObjectCopy.js';
import Settings from '../ext/lib/Settings.js';
import Debug from '../ext/conf/Debug';
import BrowserDetect from '../ext/conf/BrowserDetect';
import ExtensionConf from '../ext/conf/ExtensionConf';
import ObjectCopy from '../ext/lib/ObjectCopy';
import Settings from '../ext/lib/Settings';
import GeneralSettings from './GeneralSettings';
import ControlsSettings from './controls-settings/ControlsSettings';
import AddEditActionPopup from './controls-settings/AddEditActionPopup';

View File

@ -185,7 +185,7 @@
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.allowedMisaligned"
v-model="settings.active.arDetect.pillarTest.allowMisaligned"
/>
</div>
</div>

View File

@ -57,16 +57,16 @@
</div>
<div class="flex flex-row button-box">
<Button label="Left"
:selected="settings.active.sites['@global'].videoAlignment === VideoAlignment.Left"
@click.native="setDefaultvideoAlignment(VideoAlignment.Left)">
:selected="settings.active.sites['@global'].videoAlignment === VideoAlignmentType.Left"
@click.native="setDefaultvideoAlignment(VideoAlignmentType.Left)">
</Button>
<Button label="Center"
:selected="settings.active.sites['@global'].videoAlignment === VideoAlignment.Center"
@click.native="setDefaultvideoAlignment(VideoAlignment.Center)">
:selected="settings.active.sites['@global'].videoAlignment === VideoAlignmentType.Center"
@click.native="setDefaultvideoAlignment(VideoAlignmentType.Center)">
</Button>
<Button label="Right"
:selected="settings.active.sites['@global'].videoAlignment === VideoAlignment.Right"
@click.native="setDefaultvideoAlignment(VideoAlignment.Right)">
:selected="settings.active.sites['@global'].videoAlignment === VideoAlignmentType.Right"
@click.native="setDefaultvideoAlignment(VideoAlignmentType.Right)">
</Button>
</div>
@ -75,20 +75,20 @@
</div>
<div class="flex flex-row button-box">
<Button label="Don't stretch"
:selected="settings.active.sites['@global'].stretch === Stretch.NoStretch"
@click.native="setDefaultStretchingMode(Stretch.NoStretch)">
:selected="settings.active.sites['@global'].stretch === StretchType.NoStretch"
@click.native="setDefaultStretchingMode(StretchType.NoStretch)">
</Button>
<Button label="Basic stretch"
:selected="settings.active.sites['@global'].stretch === Stretch.Basic"
@click.native="setDefaultStretchingMode(Stretch.Basic)">
:selected="settings.active.sites['@global'].stretch === StretchType.Basic"
@click.native="setDefaultStretchingMode(StretchType.Basic)">
</Button>
<Button label="Hybrid stretch"
:selected="settings.active.sites['@global'].stretch === Stretch.Hybrid"
@click.native="setDefaultStretchingMode(Stretch.Hybrid)">
:selected="settings.active.sites['@global'].stretch === StretchType.Hybrid"
@click.native="setDefaultStretchingMode(StretchType.Hybrid)">
</Button>
<Button label="Thin borders only"
:selected="settings.active.sites['@global'].stretch === Stretch.Conditional"
@click.native="setDefaultStretchingMode(Stretch.Conditional)"
:selected="settings.active.sites['@global'].stretch === StretchType.Conditional"
@click.native="setDefaultStretchingMode(StretchType.Conditional)"
>
</Button>
</div>
@ -152,9 +152,9 @@
<script>
import Button from '../common/components/Button';
import Stretch from '../common/enums/stretch.enum';
import ExtensionMode from '../common/enums/extension-mode.enum';
import VideoAlignment from '../common/enums/video-alignment.enum';
import StretchType from '../common/enums/StretchType.enum';
import ExtensionMode from '../common/enums/ExtensionMode.enum';
import VideoAlignmentType from '../common/enums/VideoAlignmentType.enum';
import BrowserDetect from '../ext/conf/BrowserDetect';
export default {
@ -166,9 +166,9 @@ export default {
},
data () {
return {
Stretch: Stretch,
StretchType: StretchType,
ExtensionMode: ExtensionMode,
VideoAlignment: VideoAlignment,
VideoAlignmentType: VideoAlignmentType,
stretchThreshold: 0,
corruptedSettingsError: false,
downloadPermissionError: false,

View File

@ -39,7 +39,7 @@ export default {
data() {
return {
addonVersion: '[extension version not loaded. This is a bug.]',
mailtoLink: 'mailto:tamius.han@gmail.com',
mailtoLink: 'mailto:tamius.han+ultrawidify@gmail.com',
redditLink: '',
}
},

View File

@ -102,7 +102,7 @@
<script>
import ShortcutButton from '../../common/components/ShortcutButton.vue'
import Stretch from '../../common/enums/stretch.enum';
import StretchType from '../../common/enums/StretchType.enum';
import KeyboardShortcutParser from '../../common/js/KeyboardShortcutParser';
import CommandChain from './command-builder/CommandChain';
import CommandAddEdit from './command-builder/CommandAddEdit';
@ -121,7 +121,7 @@ export default {
},
data () {
return {
Stretch: Stretch,
StretchType: StretchType,
action: {
name: 'New action',
label: 'New action',

View File

@ -111,7 +111,7 @@
<script>
import Button from '../../common/components/Button';
import Stretch from '../../common/enums/stretch.enum';
import StretchType from '../../common/enums/StretchType.enum';
import ActionAlt from '../../common/components/ActionAlt';
export default {
@ -121,7 +121,7 @@ export default {
},
data () {
return {
Stretch: Stretch,
StretchType: StretchType,
tableVisibility: {
crop: true,
}

View File

@ -89,12 +89,12 @@
<script>
import ActionList from '../../../ext/conf/ActionList';
import Stretch from '../../../common/enums/stretch.enum';
import StretchType from '../../../common/enums/StretchType.enum';
export default {
data () {
return {
Stretch: Stretch,
StretchType: StretchType,
ActionList: ActionList,
selectedAction: undefined,
selectedArgument: undefined,

View File

@ -20,17 +20,6 @@
Build channel: {{BrowserDetect.processEnvChannel}}
</div>
</div>
<div v-if="BrowserDetect.isEdgeUA" style="margin: 2px 12px; border: 1px solid #fa6; color: #fa6" class="flex flex-row flex-center">
<div class="flex-nogrow flex-nosrhink flex flex-center" style="font-size: 2em">
<Icon icon="exclamation-triangle"></Icon>
</div>
<div class="flex-grow padding-right: 1em; line-height: 1">
<small>
<b>NOTE:</b> please ensure your Windows and Edge have the latest updates in order for this extension to work on DRM-protected sites.
If your Windows and Edge are not up to date, videos on sites like Netflix, Hulu, and Disney+ may not be aligned and cropped correctly.
</small>
</div>
</div>
<div
v-if="narrowPopup"
class="w100 show-more flex flex-row flex-center flex-cross-center menu-button"
@ -216,12 +205,13 @@ import Comms from '../ext/lib/comms/Comms';
import VideoPanel from './panels/VideoPanel';
import PerformancePanel from './panels/PerformancePanel';
import Settings from '../ext/lib/Settings';
import ExecAction from './js/ExecAction.js';
import ExecAction from './js/ExecAction';
import DefaultSettingsPanel from './panels/DefaultSettingsPanel';
import AboutPanel from './panels/AboutPanel';
import ExtensionMode from '../common/enums/extension-mode.enum';
import ExtensionMode from '../common/enums/ExtensionMode.enum';
import Logger from '../ext/lib/Logger';
import {ChromeShittinessMitigations as CSM} from '../common/js/ChromeShittinessMitigations';
import { browser } from 'webextension-polyfill-ts';
export default {
data () {
@ -260,7 +250,8 @@ export default {
await this.settings.init();
this.settingsInitialized = true;
const port = BrowserDetect.firefox ? browser.runtime.connect({name: 'popup-port'}) : chrome.runtime.connect({name: 'popup-port'});
// const port = BrowserDetect.firefox ? browser.runtime.connect({name: 'popup-port'}) : chrome.runtime.connect({name: 'popup-port'});
const port = browser.runtime.connect({name: 'popup-port'});
port.onMessage.addListener( (m,p) => this.processReceivedMessage(m,p));
CSM.setProperty('port', port);

View File

@ -1,6 +1,10 @@
import Comms from '../../ext/lib/comms/Comms';
import { browser } from '../../../node_modules/webextension-polyfill-ts/lib/index';
import Settings from '../../ext/lib/Settings';
class ExecAction {
settings: Settings;
site: any;
constructor(settings, site) {
this.settings = settings;
this.site = site;
@ -24,7 +28,7 @@ class ExecAction {
arg: cmd.arg,
customArg: cmd.customArg
}
Comms.sendMessage(message);
browser.runtime.sendMessage(message);
} else {
// set-ar-persistence sends stuff to content scripts as well (!)
@ -41,7 +45,7 @@ class ExecAction {
}
// this hopefully delays settings.save() until current crops are saved on the site
// and thus avoid any fucky-wuckies
await Comms.sendMessage(message);
await browser.runtime.sendMessage(message);
}
let site = this.site;
@ -59,7 +63,7 @@ class ExecAction {
this.settings.active.sites[site].stretch = cmd.arg;
} else if (cmd.action === "set-alignment") {
this.settings.active.sites[site].videoAlignment = cmd.arg;
} else if (cmd.action === "set-extension-mode") {
} else if (cmd.action === "set-ExtensionMode") {
this.settings.active.sites[site].mode = cmd.arg;
} else if (cmd.action === "set-autoar-mode") {
this.settings.active.sites[site].autoar = cmd.arg;

View File

@ -1,5 +1,5 @@
<template>
<div class="flex flex-column" style="padding-bottom: 20px">
<div class="flex flex-column" style="padding-bottom: 20px; position: relative">
<!-- <div class="">
<div class="label">Player picker</div>
<div class="desc">
@ -38,18 +38,18 @@
Player detection settings<br/><small>for {{site}}</small>
</div>
<div class="description">
Player is the frame around the video. Extension crops/stretches the video to fit the player.
If extension doesn't work, you can help a little by telling it where to look for the player.<br/>
</div>
<div class="">
<div class="">
<input :checked="!playerManualQs"
<input :checked="playerManualQs"
@change="togglePlayerManualQs"
type="checkbox"
/> Detect automatically
/> Manually specify player
</div>
<div class="flex flex-column">
<div class="">Query selectors:</div>
<div class="">Query selectors for player:</div>
<input type="text"
v-model="playerQs"
@change="updatePlayerQuerySelector"
@ -58,16 +58,15 @@
/>
</div>
<div class="">
<div class="flex flex-row">
<input :checked="playerByNodeIndex"
:disabled="!playerManualQs"
@change="toggleByNodeIndex"
type="checkbox"
/> Specify player node parent index instead
/>
<div>
Player is n-th parent of video:
</div>
<div class="flex flex-column">
<div class="">Player node parent index:</div>
<input v-model="playerParentNodeIndex"
:disabled="!playerByNodeIndex || !playerManualQs"
@change="updatePlayerParentNodeIndex"
@ -76,6 +75,31 @@
/>
</div>
<div class="description">
<small>
<b>Hint:</b> Player is a HTML element that represents the portion of the page you expect the video to play in.
You can provide player's query selector, or you can tick the 'player is the n-th parent of video'
checkbox and try entering values 1-12ish and see if anything works. Note that you need to save
settings and reload the page every time you change the number.
</small>
</div>
<div class="flex flex-row">
<input :checked="usePlayerAr"
@change="toggleUsePlayerAr"
type="checkbox"
/>
<div>
Do not use monitor AR in fullscreen
</div>
</div>
<div class="description">
<small>
<b>Hint:</b> When in full screen, the extension will assume that player element is as big as your screen.
You generally want to keep this option off, unless you like to browse in fullscreen a lot.
</small>
</div>
</div>
<div class="label">
@ -114,7 +138,7 @@
/>
</div>
<div class="flex flex-column">
<div class="flex label-secondary form-label">Additional css</div>
<div class="flex label-secondary form-label">Additional style for video element</div>
<input type="text"
v-model="videoCss"
@change="updateVideoCss"
@ -122,6 +146,68 @@
/>
</div>
<div class="label">
Browser quirk mitigations
</div>
<div class="description">
Sometimes, the extension may misbehave as a result of issues and bugs present in your browser, operating system or your GPU driver.
Some of the issues can be fixed by limiting certain functionalities of this addon.
</div>
<div class="flex flex-column">
<div
v-if="BrowserDetect.anyChromium"
class="workaround flex flex-column"
>
<div class="flex label-secondary form-label">
<input :checked="settings?.active?.mitigations?.zoomLimit?.enabled"
@change="setMitigation(['zoomLimit', 'enabled'], $event.target.checked)"
type="checkbox"
/> Limit zoom.
</div>
<div class="flex flex-row">
<div class="label-secondary form-label">
<small>Limit zoom to % of width (1=100%):</small>
</div>
<input type="number"
:value="settings?.active?.mitigations?.zoomLimit?.limit || 0.997"
step="0.001"
min="0.5"
@change="setMitigation(['zoomLimit', 'limit'], +$event.target.value)"
@blur="updateVideoQuerySelector"
:disabled="!settings?.active?.mitigations?.zoomLimit?.enabled"
/>
</div>
<div class="flex label-secondary form-label">
<input :checked="settings?.active?.mitigations?.zoomLimit?.fullscreenOnly"
@change="setMitigation(['zoomLimit', 'fullscreenOnly'], $event.target.checked)"
:disabled="!settings?.active?.mitigations?.zoomLimit?.enabled"
type="checkbox"
/> Limit zoom only while in fullscreen
</div>
<div class="description">
<small>
<b>Fix for:</b> Chrome and Edge used to have a bug where videos would get incorrectly stretched when zoomed in too far.
The issue only appeared in fullscreen, on nVidia GPUs, and with hardware acceleration enabled. While this option only
needs to be applied in fullscreen, fullscreen detection in Chrome can be a bit unreliable (depending on your OS and/or
display scaling settings). <a href="https://stuff.tamius.net/sacred-texts/2021/02/01/ultrawidify-and-chrome-2021-edition/" target="_blank">More about the issue</a>.
</small>
</div>
</div>
</div>
<div>&nbsp;</div>
<div>&nbsp;</div>
</div>
<div id="save-banner-observer-bait">
&nbsp;
</div>
<div
id="save-banner"
class="save-banner"
>
<div class="button">Save settings</div>
</div>
</div>
</template>
@ -130,9 +216,11 @@
import ShortcutButton from '../../common/components/ShortcutButton.vue';
import QsElement from '../../common/components/QsElement.vue';
import QuerySelectorSetting from '../../common/components/QuerySelectorSetting.vue';
import ExtensionMode from '../../common/enums/extension-mode.enum';
import VideoAlignment from '../../common/enums/video-alignment.enum';
import Stretch from '../../common/enums/stretch.enum';
import ExtensionMode from '../../common/enums/ExtensionMode.enum';
import VideoAlignmentType from '../../common/enums/VideoAlignmentType.enum';
import StretchType from '../../common/enums/StretchType.enum';
import BrowserDetect from '../../ext/conf/BrowserDetect';
export default {
components: {
QuerySelectorSetting,
@ -149,6 +237,8 @@ export default {
playerCss: '',
playerByNodeIndex: false,
playerParentNodeIndex: undefined,
usePlayerAr: false,
BrowserDetect
};
},
props: {
@ -169,6 +259,7 @@ export default {
this.playerQs = this.settings.active.sites[this.site].DOM.player.querySelectors;
this.playerByNodeIndex = this.settings.active.sites[this.site].DOM.player.useRelativeAncestor || this.playerByNodeIndex;
this.playerParentNodeIndex = this.settings.active.sites[this.site].DOM.player.videoAncestor;
this.usePlayerAr = this.settings.active.sites[this.site].usePlayerArInFullscreen;
} catch (e) {
// that's here just in case relevant settings for this site don't exist yet
}
@ -179,15 +270,32 @@ export default {
// that's here just in case relevant settings for this site don't exist yet
}
},
mounted() {
this.createObserver();
},
methods: {
createObserver() {
const saveButtonBait = document.getElementById('save-banner-observer-bait');
const saveButton = document.getElementById('save-banner');
const observer = new IntersectionObserver(
([e]) => {
// console.log('observer triggered. intersection ratio?', e.intersectionRatio)
saveButton.classList.toggle('floating', e.intersectionRatio < 0.95);
},
{threshold: [0, 0.5, 0.9, 0.95, 1]}
);
observer.observe(saveButtonBait);
},
ensureSettings(scope) {
if (! this.settings.active.sites[this.site]) {
this.settings.active.sites[this.site] = {
mode: ExtensionMode.Default,
autoar: ExtensionMode.Default,
type: 'user-added',
stretch: Stretch.Default,
videoAlignment: VideoAlignment.Default,
stretch: StretchType.Default,
videoAlignment: VideoAlignmentType.Default,
keyboardShortcutsEnabled: ExtensionMode.Default,
}
}
@ -248,12 +356,45 @@ export default {
this.settings.active.sites[this.site].DOM.player.useRelativeAncestor = this.playerByNodeIndex;
this.settings.save();
},
toggleUsePlayerAr() {
this.ensureSettings('player');
this.usePlayerAr = !this.usePlayerAr;
this.settings.active.sites[this.site].usePlayerArInFullscreen = this.usePlayerAr;
this.settings.save();
},
setMitigation(mitigation, value) {
// ensure mitigations object exists.
// it may not exist in the settings on first load
if (! this.settings.active.mitigations) {
this.settings.active.mitigations = {};
}
if (Array.isArray(mitigation)) {
let currentMitigationsParent = this.settings.active.mitigations;
for (let i = 0; i < mitigation.length - 1; i++) {
if (!currentMitigationsParent[mitigation[i]]) {
currentMitigationsParent[mitigation[i]] = {};
}
currentMitigationsParent = currentMitigationsParent[mitigation[i]];
}
currentMitigationsParent[mitigation[mitigation.length - 1]] = value;
} else {
this.settings.active.mitigations[mitigation] = value;
}
this.settings.save();
}
}
}
</script>
<style>
<style lang="scss">
@import '../../res/css/colors.scss';
.pp_video {
margin: 2px;
padding: 5px;
@ -279,4 +420,18 @@ export default {
padding: 2px;
border: 2px solid #f00;
}
.save-banner {
display: block;
position: sticky;
bottom: 0px;
left: 0px;
right: 0px;
background-color: #131313;
text-align: center;
&.floating {
box-shadow: 0 2rem 3rem 1rem $selected-color;
}
}
</style>

View File

@ -125,7 +125,8 @@ import ExecAction from '../js/ExecAction';
import KeyboardShortcutParser from '../../common/js/KeyboardShortcutParser';
import ShortcutButton from '../../common/components/ShortcutButton';
import ComputeActionsMixin from '../../common/mixins/ComputeActionsMixin';
import CropModePersistence from '../../common/enums/crop-mode-persistence.enum';
import CropModePersistence from '../../common/enums/CropModePersistence.enum';
import BrowserDetect from '../../ext/conf/BrowserDetect';
export default {
data() {
@ -157,7 +158,11 @@ export default {
},
methods: {
async openOptionsPage() {
(browser ?? chrome).runtime.openOptionsPage();
if (BrowserDetect.firefox) {
browser.runtime.openOptionsPage();
} else {
chrome.runtime.openOptionsPage();
}
},
execAction(action) {
this.exec.exec(action, 'page', this.frame);

View File

@ -2,29 +2,19 @@
<div>
<h2>What's new</h2>
<p>Full changelog for older versions <a href="https://github.com/tamius-han/ultrawidify/blob/master/CHANGELOG.md">is available here</a>.</p>
<p class="label">4.5.1</p>
<p class="label">5.0.4</p>
<ul>
<li>Fixed the misalignment issue on netflix ... hopefully.</li>
<li>
'Site settings' tab should now work in Chrome as well (<a href="https://github.com/tamius-han/ultrawidify/issues/126">#126</a>)
</li>
<li>
Popup interface now refreshes properly (<a href="https://github.com/tamius-han/ultrawidify/issues/127">#127</a>)
</li>
<li>
Videos should now be scaled correctly when the display is narrower than video's native aspect ratio (<a href="https://github.com/tamius-han/ultrawidify/issues/118">#118</a>)
</li>
<li>
Fullscreen videos on streamable are aligned correctly (<a href="https://github.com/tamius-han/ultrawidify/issues/118">#116</a>). Note that while this fix hasn't broken any sites I have access to and know of,
there is a small possibility that this bugfix may break something somewhere. If this happens on a site you use, please notify me via github, email or PM me on reddit.
</li>
<li>
<b>[4.5.1.1]</b> Streamable fix broke old.reddit + RES on embeds from v.redd.it and streamable.com. We're now using an alternative implementation. (<a href="https://github.com/tamius-han/ultrawidify/issues/128">#128</a>)
<b>[4.5.1.2]</b> Fixed the issue where videos would sometimes get misaligned while using hybrid stretch. (<a href="https://github.com/tamius-han/ultrawidify/issues/125">#125</a>)
<b>[4.5.1.3]</b> Added a potential fix for disney plus
<b>[4.5.1.3]</b> Microsoft Edge has fixed the bugs that prevented the extension from working properly. Popup should no longer be shown.
Updated config for disney+. Hopefully it works now. Special thanks to <a href="https://github.com/tamius-han/ultrawidify/issues/84#issuecomment-846334005" target="_blank">@jwannebo</a>.
</li>
</ul>
<p>
<small><b>NOTE from older versions:</b> zoom limitations were introduced as a workaround for a bug caused by Chrome's/Edge's faulty hardware acceleration.</small>
</p>
<p>
<small>If you experience issues with videos being stretched incorrectly at certain zoom levels, go to:</small><br/>
<small><code>extension popup > Advanced Settings > Browser quirk mitigations > limit zoom.</code></small>
</p>
</div>
</template>
<script>

6
src/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"moduleResolution": "node",
"outDir": "./ts-out",
"allowJs": true,
"target": "es2018",
"types": [
"chrome",
"node"
],
"typeRoots": [
"node_modules/@types",
"node_modules/web-ext-types"
],
},
"include": [ "./src/**/*" ],
}

View File

@ -22,19 +22,23 @@ const config = {
path: __dirname + `/dist-${process.env.BROWSER == 'firefox' ? 'ff' : process.env.BROWSER}`,
filename: '[name].js',
},
devtool: "source-map",
resolve: {
// maybe we'll move to TS some day, but today is not the day
extensions: [
// '.ts', '.tsx',
'.ts', '.tsx',
'.js', '.vue'
],
},
module: {
rules: [
// {
// test: /\.tsx?$/,
// loader: 'ts-loader',
// },
{
test: /\.ts$/,
loader: 'ts-loader',
exclude: /node_modules/
},
{
test: /\.vue$/,
loaders: 'vue-loader',
@ -121,7 +125,7 @@ const config = {
.substr(2) // YYYY -> YY
.replace('-', '') // YY-MM-DD -> YYMM-DD
.replace('-', '.') // YYMM-DD -> YYMM.DD
}.${process.env.BUILD_NUMBER}`;
}.${process.env.BUILD_NUMBER === undefined ? 0 : process.env.BUILD_NUMBER}`;
jsonContent.browser_action.default_title = "Ultrawidify Nightly";
// because we don't want web-ext to submit this as proper release
@ -137,7 +141,7 @@ const config = {
.substr(2) // YYYY -> YY
.replace('-', '') // YY-MM-DD -> YYMM-DD
.replace('-', '.') // YYMM-DD -> YYMM.DD
}.${process.env.BUILD_NUMBER}`;
}.${process.env.BUILD_NUMBER === undefined ? 0 : process.env.BUILD_NUMBER}`;
jsonContent.browser_action.default_title = "Ultrawidify Testing";
// because we don't want web-ext to submit this as proper release
@ -159,7 +163,10 @@ const config = {
}),
new webpack.DefinePlugin({
'process.env.BROWSER': JSON.stringify(process.env.BROWSER),
'process.env.CHANNEL': JSON.stringify(process.env.CHANNEL)
'process.env.CHANNEL': JSON.stringify(process.env.CHANNEL),
'__VUE_OPTIONS_API__': true,
'__VUE_PROD_DEVTOOLS__': false
})
],
optimization: {
@ -175,6 +182,9 @@ const config = {
if (config.mode === 'production') {
config.plugins = (config.plugins || []).concat([
new webpack.DefinePlugin({
'__VUE_OPTIONS_API__': true,
'__VUE_PROD_DEVTOOLS__': false,
'process.env': {
NODE_ENV: '"production"',
},