Merge remote-tracking branch 'origin/master' into stable

This commit is contained in:
Tamius Han 2021-09-19 20:45:40 +02:00
commit 7509456899
17 changed files with 684 additions and 735 deletions

2
.gitignore vendored
View File

@ -13,3 +13,5 @@ build/
src/res/img/git-ignore/
test/debug-configs/
debugging-resources/

View File

@ -1,23 +0,0 @@
image: node:current
cache:
paths:
- node_modules/
- .yarn
stages:
- prep
- build
- pack
install deps:
stage: prep
script: yarn install
build:
stage: build
script: npm run build
create zip:
stage: pack
script: npm run build-zip

View File

@ -1,7 +1,5 @@
{
"cSpell.words": [
"PILLARBOX",
"PILLARBOXED",
"aard",
"ardetector",
"autodetect",
@ -16,6 +14,7 @@
"csui",
"decycle",
"disneyplus",
"endregion",
"equalish",
"fith",
"fitw",
@ -24,6 +23,7 @@
"gmail",
"guardline",
"han",
"haram",
"iframe",
"imgur",
"insta",
@ -34,6 +34,8 @@
"nogrow",
"noshrink",
"outro",
"PILLARBOX",
"PILLARBOXED",
"polyfill",
"recursing",
"reddit",

View File

@ -18,6 +18,21 @@
## v5.x (current major)
### v5.1.0
* Re-enable logger
* Move aspect ratio autodetection to requestAnimationFrame
* Fix netflix
### v5.0.7
* Videos of square-ish aspect ratios on 1440p (and lower) resolutions now no longer get misaligned ([#162](https://github.com/tamius-han/ultrawidify/issues/162))
* Alignment of featured videos on youtube channel page should now also be fixed
### v5.0.6
* Added configuration for metaivi.com based on user feedback ([#160](https://github.com/tamius-han/ultrawidify/issues/160))
* Removed ExtConfPatches for versions < 4.5.0, because nobody should be using a build of this extension that's over a year old
### v5.0.5
* improved UX a bit

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "ultrawidify",
"version": "5.0.4",
"version": "5.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@ -1,6 +1,6 @@
{
"name": "ultrawidify",
"version": "5.0.5",
"version": "5.1.0",
"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": {

View File

@ -33,6 +33,9 @@
<h2>Logger configuration</h2>
</div>
<div class="flex flex-row flex-end w100">
<div v-if="!showTextMode" class="button" @click="loadDefaultConfig()">
Default config
</div>
<div v-if="!showTextMode" class="button" @click="showTextMode = true">
<Icon icon="clipboard-plus" style="font-size: 2em"></Icon>&nbsp; Paste config ...
</div>
@ -56,7 +59,7 @@
<template v-else>
<JsonObject label="logger-settings"
:value="currentSettings"
:ignoreKeys="{'allowLogging': true}"
:ignoreKeys="{'allowLogging': false}"
@change="updateSettingsUi"
></JsonObject>
</template>
@ -107,11 +110,16 @@
</template>
<template v-else>
<div class="panel-middle scrollable flex-grow">
<div v-if="!parsedSettings" class="text-center w100">
Please paste logger config into the text box to the left.
<div>
<p>Here's express usage tutorial on how to use the logger.</p>
<p>Quick rundown of all the options you can put into logger configuration can be found <a href="https://github.com/tamius-han/ultrawidify/wiki/Development&Debugging:-Logger-options" target="_blank">here</a>.</p>
<p>If you want logging results to appear here, in this window, you need to put appropriate configuration in the fileOptions section of the settings. You can edit the settings by clicking 'paste config' button.</p>
<p>To start logging to console, it's enough to click that tiny 'save' button down there, under logger options (<code>logger-settings.allowLogging</code> must be set to true. Then depending on where you want logging to happen you also need to enable either <code>logger-settings.fileOptions.enabled</code> or <code>logger-settings.consoleOptions.enabled</code>)</p>
<p>You can quickly toggle values for various components by clicking on "true" or "false".</p>
<p>Click the small 'save' down at the bottom of this window to immediately apply the logger configuration changes.</p>
<p>Yes, I know this is not a pinnacle of user-friendliness, nor a pinnacle of web design. It was put together very quickly, mostly for my convenience. I have plans to move extension UI from the popup into the player, tho, and these plans will make this window obsolete. Because of that, I plan to do absolutely nothing about the state of this window. Wait for the extension redesign pls.</p>
</div>
<div v-else-if="confHasError" class="warn">
<div v-if="confHasError" class="warn">
Logger configuration contains an error. Cannot start logging.
</div>
<div v-else-if="lastSettings && lastSettings.allowLogging && lastSettings.consoleOptions && lastSettings.consoleOptions.enabled"
@ -160,7 +168,7 @@
<script>
import { mapState } from 'vuex';
import Logger from '../ext/lib/Logger';
import Logger, { baseLoggingOptions } from '../ext/lib/Logger';
import Comms from '../ext/lib/comms/Comms';
import IO from '../common/js/IO';
import JsonObject from '../common/components/JsonEditor/JsonObject';
@ -196,6 +204,12 @@ export default {
}, {
header: "Tracer",
subheader: "I'm already printing stack traces"
}, {
header: "Grûmsh",
subheader: "He who watches"
}, {
header: "Situation Room/The Council",
subheader: "We will always be watching"
}];
this.header = headerRotation[Math.floor(+Date.now() / (3600000*24)) % headerRotation.length] || this.header;
@ -233,8 +247,13 @@ export default {
}
},
methods: {
loadDefaultConfig() {
this.lastSettings = baseLoggingOptions;
this.parsedSettings = JSON.stringify(this.lastSettings, null, 2) || '';
this.currentSettings = JSON.parse(JSON.stringify(this.lastSettings));
},
async getLoggerSettings() {
this.lastSettings = await Logger.getConfig() || {};
this.lastSettings = await Logger.getConfig() || baseLoggingOptions;
this.parsedSettings = JSON.stringify(this.lastSettings, null, 2) || '';
this.currentSettings = JSON.parse(JSON.stringify(this.lastSettings));
},
@ -276,6 +295,8 @@ export default {
this.logStringified = undefined;
}
this.$store.dispatch('uw-hide-logger');
this.showLoggerUi = false;
},
closePopupAndStopLogging() {
Logger.saveConfig({...this.lastSettings, allowLogging: false});
@ -319,6 +340,7 @@ export default {
font-size: 14px !important;
box-sizing: border-box !important;
pointer-events: auto !important;
}
div {

View File

@ -7,391 +7,6 @@ import BrowserDetect from './BrowserDetect';
const ExtensionConfPatch = [
{
forVersion: '4.2.0',
sites: {
"old.reddit.com" : {
type: 'testing',
DOM: {
player: {
manual: true,
useRelativeAncestor: false,
querySelectors: '.reddit-video-player-root, .media-preview-content'
}
},
css: '',
},
"www.reddit.com" : {
type: 'testing',
DOM: {
player: {
manual: true,
useRelativeAncestor: false,
querySelectors: '.reddit-video-player-root, .media-preview-content'
}
},
css: '',
},
"www.youtube.com" : {
DOM: {
player: {
manual: true,
querySelectors: "#movie_player, #player",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: "",
}
}
},
}
}, {
forVersion: '4.2.3.1',
sites: {
"old.reddit.com" : {
type: 'testing',
DOM: {
player: {
manual: true,
useRelativeAncestor: false,
querySelectors: '.media-preview-content, .reddit-video-player-root'
}
},
css: '',
},
"www.reddit.com" : {
type: 'testing',
DOM: {
player: {
manual: true,
useRelativeAncestor: false,
querySelectors: '.media-preview-content, .reddit-video-player-root'
}
},
css: '',
},
"www.youtube.com" : {
DOM: {
player: {
manual: true,
querySelectors: "#movie_player, #player",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: "",
}
}
},
}
}, {
forVersion: '4.3.0',
sites: {
"old.reddit.com" : {
type: 'testing',
DOM: {
player: {
manual: false,
useRelativeAncestor: false,
querySelectors: '.reddit-video-player-root, .media-preview-content'
}
},
css: 'video {\n width: 100% !important;\n height: 100% !important;\n}',
},
"www.reddit.com" : {
type: 'testing',
DOM: {
player: {
manual: false,
useRelativeAncestor: false,
querySelectors: '.reddit-video-player-root, .media-preview-content'
}
},
css: 'video {\n width: 100% !important;\n height: 100% !important;\n}',
},
}
}, {
forVersion: '4.3.1.1',
sites: {
'www.twitch.tv': {
DOM: {
player: {
manual: false,
querySelectors: "",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: ""
}
}
}
}
}, {
forVersion: '4.4.0',
updateFn: (userOptions, defaultOptions) => {
// remove 'press P to toggle panning mode' thing
const togglePan = userOptions.actions.find(x => x.cmd && x.cmd.length === 1 && x.cmd[0].action === 'toggle-pan');
if (togglePan) {
togglePan.scopes = {};
}
// add new actions
userOptions.actions.push({
name: 'Don\'t persist crop',
label: 'Never persist',
cmd: [{
action: 'set-ar-persistence',
arg: 0,
}],
scopes: {
site: {
show: true,
},
global: {
show: true,
}
},
playerUi: {
show: true,
}
}, {
userAdded: true,
name: 'Persist crop while on page',
label: 'Until page load',
cmd: [{
action: 'set-ar-persistence',
arg: 1,
}],
scopes: {
site: {
show: true,
},
global: {
show: true,
}
},
playerUi: {
show: true,
}
}, {
userAdded: true,
name: 'Persist crop for current session',
label: 'Current session',
cmd: [{
action: 'set-ar-persistence',
arg: 2,
}],
scopes: {
site: {
show: true,
},
global: {
show: true,
}
},
playerUi: {
show: true,
}
}, {
name: 'Persist until manually reset',
label: 'Always persist',
cmd: [{
action: 'set-ar-persistence',
arg: 3,
}],
scopes: {
site: {
show: true,
},
global: {
show: true,
}
},
playerUi: {
show: true,
}
}, {
name: 'Default crop persistence',
label: 'Default',
cmd: [{
action: 'set-ar-persistence',
arg: -1,
}],
scopes: {
site: {
show: true,
},
},
playerUi: {
show: true,
}
});
// patch shortcuts for non-latin layouts, but only if the user hasn't changed default keys
for (const action of userOptions.actions) {
if (!action.cmd || action.cmd.length !== 1) {
continue;
}
try {
// if this fails, then action doesn't have keyboard shortcut associated with it, so we skip it
const actionDefaults = defaultOptions.actions.find(x => x.cmd && x.cmd.length === 1 // (redundant, default actions have exactly 1 cmd in array)
&& x.cmd[0].action === action.cmd[0].action
&& x.scopes.page
&& x.scopes.page.shortcut
&& x.scopes.page.shortcut.length === 1
&& x.scopes.page.shortcut[0].key === action.scopes.page.shortcut[0].key // this can throw exception, and it's okay
);
if (actionDefaults === undefined) {
continue;
}
// update 'code' property for shortcut
action.scopes.page.shortcut[0]['code'] = actionDefaults.scopes.page.shortcut[0].code;
} catch (e) {
continue;
}
}
}
}, {
forVersion: '4.4.1.1',
sites: {
"www.disneyplus.com": {
DOM: {
player: {
periodicallyRefreshPlayerElement: true,
}
}
},
}
}, {
forVersion: '4.4.2',
updateFn: (userOptions, defaultOptions) => {
try {
userOptions.actions.push(
{
name: 'Stretch source to 4:3',
label: '4:3 stretch (src)',
cmd: [{
action: 'set-stretch',
arg: StretchType.FixedSource,
customArg: 1.33,
}],
scopes: {
page: {
show: true
}
},
playerUi: {
show: true,
path: 'crop'
}
}, {
name: 'Stretch source to 16:9',
label: '16:9 stretch (src)',
cmd: [{
action: 'set-stretch',
arg: StretchType.FixedSource,
customArg: 1.77,
}],
scopes: {
page: {
show: true,
}
},
playerUi: {
show: true,
path: 'crop'
}
});
} catch (e) {
console.error("PROBLEM APPLYING SETTINGS", e);
}
}
}, {
forVersion: '4.4.3.1',
sites: {
"www.disneyplus.com": {
mode: ExtensionMode.Enabled,
autoar: ExtensionMode.Enabled,
autoarFallback: ExtensionMode.Enabled,
override: true, // ignore value localStorage in favour of this
stretch: StretchType.Default,
videoAlignment: VideoAlignmentType.Default,
keyboardShortcutsEnabled: ExtensionMode.Default,
DOM: {
player: {
periodicallyRefreshPlayerElement: true,
}
}
}
}
}, {
forVersion: '4.4.7',
updateFn: (userOptions, defaultOptions) => {
if (!userOptions.sites['www.netflix.com'].DOM) {
userOptions.sites['www.netflix.com']['DOM'] = {
"player": {
"manual": true,
"querySelectors": ".VideoContainer",
"additionalCss": "",
"useRelativeAncestor": false,
"playerNodeCss": ""
}
}
}
if (!userOptions.sites['www.disneyplus.com']) {
userOptions.sites['www.disneyplus.com'] = {
mode: ExtensionMode.Enabled,
autoar: ExtensionMode.Enabled,
override: false,
type: 'community',
stretch: StretchType.Default,
videoAlignment: VideoAlignmentType.Default,
keyboardShortcutsEnabled: ExtensionMode.Default,
arPersistence: true, // persist aspect ratio between different videos
autoarPreventConditions: { // prevents autoar on following conditions
videoStyleString: { // if video style string thing does anything of what follows
containsProperty: { // if video style string has any of these properties (listed as keys)
'height': { // if 'height' property is present in style attribute, we prevent autoar from running
allowedValues: [ // unless attribute is equal to anything in here. Optional.
'100%'
]
}
// 'width': true // this would prevent aard from running if <video> had a 'width' property in style, regardless of value
// could also be an empty object, in theory.
}
}
},
DOM: {
"player": {
"manual": true,
"querySelectors": ".btn-media-clients",
"additionalCss": "",
"useRelativeAncestor": false,
"playerNodeCss": ""
}
}
}
} else {
userOptions.sites['wwww.disneyplus.com']['DOM'] = {
"player": {
"manual": true,
"querySelectors": ".btn-media-clients",
"additionalCss": "",
"useRelativeAncestor": false,
"playerNodeCss": ""
}
}
}
}
}, {
forVersion: '4.4.9',
sites: {
"www.youtube.com": {
override: true,
DOM: {
player: {
manual: true
}
}
}
}
}, {
forVersion: '4.5.0',
sites: {
"www.wakanim.tv": {
@ -517,7 +132,7 @@ const ExtensionConfPatch = [
"app.plex.tv": {
mode: 3,
autoar: 3,
type: "user-added",
type: "community",
stretch: -1,
videoAlignment: -1,
keyboardShortcutsEnabled: 0,
@ -533,6 +148,99 @@ const ExtensionConfPatch = [
css: "body {\n background-color: #000;\n}\n\n.application {\n background-color: #000;\n}"
}
}
}, {
forVersion: '5.0.6',
sites: {
"metaivi.com": {
mode: 0,
autoar: 0,
type: "community",
stretch: -1,
videoAlignment: -1,
DOM: {
video: {
manual: false,
querySelectors: "",
additionalCss: "position: absolute !important;"
},
player: {
manual: false,
querySelectors: "",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: ""
}
},
"css": ""
},
"piped.kavin.rocks": {
mode: 0,
autoar: 0,
type: 'community',
autoarFallback: 0,
stretch: 0,
videoAlignment: -1,
keyboardShortcutsEnabled: 0,
DOM: {
player: {
manual: false,
querySelectors: "",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: ""
}
},
css: ".shaka-video-container {\n flex-direction: column !important;\n}"
},
},
updateFn: (userOptions, defaultOptions) => {
// 5.0.5 initially incorrectly had app.plex.tv marked as 'user-added'
// when 'user-added' is generally reserved for marking sites with user-
// changed configuration. Site patches submitted by community should have
// 'community' type. extConfPatch for 5.0.5 was also retroactively corrected.
userOptions.sites['app.plex.tv'].type = 'community';
userOptions.sites['piped.kavin.rocks'] = {
mode: 0,
autoar: 0,
type: 'community',
autoarFallback: 0,
stretch: 0,
videoAlignment: -1,
keyboardShortcutsEnabled: 0,
DOM: {
player: {
manual: false,
querySelectors: "",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: ""
}
},
css: ".shaka-video-container {\n flex-direction: column !important;\n}"
};
}
}, {
forVersion: '5.0.7',
updateFn: (userOptions, defaultOptions) => {
// 5.0.5 initially incorrectly had app.plex.tv marked as 'user-added'
// when 'user-added' is generally reserved for marking sites with user-
// changed configuration. Site patches submitted by community should have
// 'community' type. extConfPatch for 5.0.5 was also retroactively corrected.
userOptions.sites['www.youtube.com'].DOM.player = {
manual: true,
querySelectors: "#movie_player, #player, #c4-player",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: "",
}
}
}, {
forVersion: '5.0.8',
updateFn: (userOptions, defaultOptions) => {
userOptions.sites['www.netflix.com'].DOM.player = {
manual: false
}
}
}
];

View File

@ -1031,8 +1031,8 @@ const ExtensionConf: SettingsInterface = {
keyboardShortcutsEnabled: ExtensionMode.Default,
DOM: {
player: {
manual: false,
querySelectors: "#movie_player, #player",
manual: true,
querySelectors: "#movie_player, #player, #c4-player",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: "",
@ -1048,13 +1048,9 @@ const ExtensionConf: SettingsInterface = {
videoAlignment: VideoAlignmentType.Default,
keyboardShortcutsEnabled: ExtensionMode.Default,
arPersistence: true, // persist aspect ratio between different videos
"DOM": {
"player": {
"manual": true,
"querySelectors": ".VideoContainer",
"additionalCss": "",
"useRelativeAncestor": false,
"playerNodeCss": ""
DOM: {
player: {
manual: false, // as of 2021-09, netflix no longer requires manual player class. This may change in the future tho.
}
}
},
@ -1195,7 +1191,7 @@ const ExtensionConf: SettingsInterface = {
"app.plex.tv": {
mode: 3,
autoar: 3,
type: "user-added",
type: "community",
stretch: -1,
videoAlignment: -1,
keyboardShortcutsEnabled: 0,
@ -1209,7 +1205,48 @@ const ExtensionConf: SettingsInterface = {
}
},
css: "body {\n background-color: #000;\n}\n\n.application {\n background-color: #000;\n}"
}
},
"metaivi.com": {
mode: 0,
autoar: 0,
type: "community",
stretch: -1,
videoAlignment: -1,
DOM: {
video: {
manual: false,
querySelectors: "",
additionalCss: "position: absolute !important;"
},
player: {
manual: false,
querySelectors: "",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: ""
}
},
"css": ""
},
"piped.kavin.rocks": {
mode: 0,
autoar: 0,
type: 'community',
autoarFallback: 0,
stretch: 0,
videoAlignment: -1,
keyboardShortcutsEnabled: 0,
DOM: {
player: {
manual: false,
querySelectors: "",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: ""
}
},
css: ".shaka-video-container {\n flex-direction: column !important;\n}"
},
}
}

View File

@ -314,23 +314,31 @@ class ActionHandler {
handleKeyup(event) {
this.logger.log('info', 'keyboard', "%c[ActionHandler::handleKeyup] we pressed a key: ", "color: #ff0", event.key , " | keyup: ", event.keyup, "event:", event);
if (this.preventAction(event)) {
this.logger.log('info', 'keyboard', "[ActionHandler::handleKeyup] we are in a text box or something. Doing nothing.");
return;
}
try {
if (this.preventAction(event)) {
this.logger.log('info', 'keyboard', "[ActionHandler::handleKeyup] we are in a text box or something. Doing nothing.");
return;
}
this.execAction(this.keyUpActions, event);
this.execAction(this.keyUpActions, event);
} catch (e) {
this.logger.log('info', 'debug', '[ActionHandler::handleKeyup] Failed to handle keyup!', e);
}
}
handleKeydown(event) {
this.logger.log('info', 'keyboard', "%c[ActionHandler::handleKeydown] we pressed a key: ", "color: #ff0", event.key , " | keydown: ", event.keydown, "event:", event)
if (this.preventAction(event)) {
this.logger.log('info', 'keyboard', "[ActionHandler::handleKeydown] we are in a text box or something. Doing nothing.");
return;
}
try {
if (this.preventAction(event)) {
this.logger.log('info', 'keyboard', "[ActionHandler::handleKeydown] we are in a text box or something. Doing nothing.");
return;
}
this.execAction(this.keyDownActions, event);
this.execAction(this.keyDownActions, event);
} catch (e) {
this.logger.log('info', 'debug', '[ActionHandler::handleKeydown] Failed to handle keydown!', e);
}
}
handleMouseMove(event, videoData?: VideoData) {

View File

@ -91,6 +91,10 @@ class Logger {
constructor(options?: {vuexStore?: any, uwInstance?: any}) {
this.vuexStore = options?.vuexStore;
this.uwInstance = options?.uwInstance;
browser.storage.onChanged.addListener((changes, area) => {
this.storageChangeListener(changes, area)
});
}
static saveConfig(conf: LoggerConfig) {
@ -190,6 +194,34 @@ class Logger {
});
}
storageChangeListener(changes, area) {
if (!changes.uwLogger) {
console.info('We dont have any logging settings, not processing frther');
return;
}
try {
this.conf = JSON.parse(changes.uwLogger.newValue);
} catch (e) {
console.warn('[uwLogger] Error while trying to parse new conf for logger:', e, '\nWe received the following changes:', changes, 'for area:', area);
}
// This code can only execute if user tried to enable or disable logging
// through the popup. In cases like this, we do not gate the console.log
// behind a check, since we _always_ want to have this feedback in response
// to an action.
console.info(
'[uwLogger] logger config changed! New configuration:',
this.conf, '\nraw changes:', changes, 'area?', area,
'\n————————————————————————————————————————————————————————————————————————\n\n\n\n\n\n\n\n\n\n\n\n-----\nLogging with new settings starts now.'
);
// initiate loger if need be
if (!this.startTime) {
this.init(this.conf);
}
}
setVuexStore(store) {
this.vuexStore = store;
}
@ -386,7 +418,7 @@ class Logger {
}
canLogFile(component) {
if (!this.conf.fileOptions.enabled || this.temp_disable) {
if (!this.conf?.fileOptions?.enabled || this.temp_disable) {
return false;
}
if (Array.isArray(component) && component.length ) {

View File

@ -14,6 +14,13 @@ import Logger from '../Logger';
import VideoData from '../video-data/VideoData';
import Settings from '../Settings';
enum VideoPlaybackState {
Playing,
Paused,
Ended,
Error
}
class ArDetector {
logger: Logger;
conf: VideoData;
@ -44,6 +51,7 @@ class ArDetector {
canDoFallbackMode: boolean = false;
// helper objects
private animationFrameHandle: any;
private attachedCanvas: HTMLCanvasElement;
canvas: HTMLCanvasElement;
private blackframeCanvas: HTMLCanvasElement;
@ -56,7 +64,83 @@ class ArDetector {
private detectedAr: any;
private canvasDrawWindowHOffset: number;
private sampleCols_current: number;
private timers = {
nextFrameCheckTime: Date.now()
}
private status = {
lastVideoStatus: VideoPlaybackState.Playing
}
//#region debug variables
private performanceConfig = {
sampleCountForAverages: 32
}
private performance = {
animationFrame: {
lastTime: 0,
currentIndex: 0,
sampleTime: []
},
drawImage: {
currentIndex: 0,
sampleTime: [],
},
getImageData: {
currentIndex: 0,
sampleTime: [],
},
aard: {
currentIndex: 0,
sampleTime: [],
}
};
//#endregion
//#region getters
get defaultAr() {
const ratio = this.video.videoWidth / this.video.videoHeight;
if (isNaN(ratio)) {
return undefined;
}
return ratio;
}
//#endregion getters
//#region debug getters
/**
* We get one animation frame per this many ms. This means that our autodetection
* stuff must run in less than this many ms. This valuz is averaged out over multiple
* samples for better accuracy.
*
* Returns value in ms.
*
* A very important caveat: if autodetection takes up too much time, it WILL artificially
* increase time budget. Therefore, you should use (and firstly even implement) getTimeBudget()
* that turns off autodetection for a second or so to gather accurate timing info.
*/
get eyeballedTimeBudget() {
let sum;
for (let i = 0; i < this.performance.animationFrame.sampleTime.length; i++) {
sum += this.performance.animationFrame.sampleTime[i];
}
return sum / this.performance.animationFrame.sampleTime.length;
}
/**
* Converts time budget (eyeballed) into actual framerate. Since eyeballed time budget rises
* if our autodetection takes too long, it's still good enough for calculating framerate
*/
get fps() {
return 1000 / this.eyeballedTimeBudget;
}
//#endregion
//#region lifecycle
constructor(videoData){
this.logger = videoData.logger;
this.conf = videoData;
@ -73,14 +157,6 @@ class ArDetector {
this.logger.log('info', 'init', `[ArDetector::ctor] creating new ArDetector. arid: ${this.arid}`);
}
setManualTick(manualTick) {
this.manualTickEnabled = manualTick;
}
tick() {
this._nextTick = true;
}
init(){
this.logger.log('info', 'init', `[ArDetect::init] <@${this.arid}> Initializing autodetection.`);
@ -95,12 +171,6 @@ class ArDetector {
}
}
destroy(){
this.logger.log('info', 'init', `%c[ArDetect::destroy] <@${this.arid}> Destroying aard.`, _ard_console_stop);
// this.debugCanvas.destroy();
this.stop();
}
setup(cwidth?: number, cheight?: number){
this.logger.log('info', 'init', `[ArDetect::setup] <@${this.arid}> Starting autodetection setup.`);
//
@ -215,12 +285,13 @@ 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: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
this.conf.resizer.setLastAr({type: AspectRatioType.Automatic, ratio: this.defaultAr});
this.canvasImageDataRowLength = cwidth << 2;
this.noLetterboxCanvasReset = false;
if (this.settings.canStartAutoAr() ) {
// this.main();
this.start();
}
@ -232,6 +303,14 @@ class ArDetector {
this.conf.arSetupComplete = true;
}
destroy(){
this.logger.log('info', 'init', `%c[ArDetect::destroy] <@${this.arid}> Destroying aard.`, _ard_console_stop);
// this.debugCanvas.destroy();
this.halt();
}
//#endregion lifecycle
//#region AARD control
start() {
if (this.settings.canStartAutoAr()) {
this.logger.log('info', 'debug', `"%c[ArDetect::start] <@${this.arid}> Starting automatic aspect ratio detection`, _ard_console_start);
@ -242,27 +321,38 @@ class ArDetector {
if (this.conf.resizer.lastAr.type === AspectRatioType.Automatic) {
// ensure first autodetection will run in any case
this.conf.resizer.setLastAr({type: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
this.conf.resizer.setLastAr({type: AspectRatioType.Automatic, ratio: this.defaultAr});
}
// launch main() if it's currently not running:
this.main();
// automatic detection starts halted. If halted=false when main first starts, extension won't run
// this._paused is undefined the first time we run this function, which is effectively the same thing
// as false. Still, we'll explicitly fix this here.
this._paused = false;
this._halted = false;
this._paused = false;
// start autodetection
this.startLoop();
// automatic detection starts halted. If halted=false when main first starts, extension won't run
// this._paused is undefined the first time we run this function, which is effectively the same thing
// as false. Still, we'll explicitly fix this here.
}
startLoop() {
if (this.animationFrameHandle) {
window.cancelAnimationFrame(this.animationFrameHandle);
}
this.animationFrameHandle = window.requestAnimationFrame( (ts) => this.animationFrameBootstrap(ts));
}
stop() {
if (this.animationFrameHandle) {
window.cancelAnimationFrame(this.animationFrameHandle);
}
this.logger.log('info', 'debug', `"%c[ArDetect::stop] <@${this.arid}> Stopping AnimationFrame loop.`, _ard_console_stop);
}
unpause() {
// pause only if we were running before. Don't pause if we aren't running
// (we are running when _halted is neither true nor undefined)
if (this._paused && this._halted === false) {
this._paused = true;
}
this.startLoop();
}
pause() {
@ -271,108 +361,76 @@ class ArDetector {
if (this._halted === false) {
this._paused = true;
}
this.stop();
}
stop(){
this.logger.log('info', 'debug', `"%c[ArDetect::stop] <@${this.arid}> Stopping automatic aspect ratio detection`, _ard_console_stop);
halt(){
this.logger.log('info', 'debug', `"%c[ArDetect::stop] <@${this.arid}> Halting automatic aspect ratio detection`, _ard_console_stop);
this.stop();
this._halted = true;
// this.conf.resizer.setArLastAr();
}
async main() {
if (this._paused) {
// unpause if paused
this._paused = false;
return; // main loop still keeps executing. Return is needed to avoid a million instances of autodetection
}
if (!this._halted) {
// we are already running, don't run twice
// this would have handled the 'paused' from before, actually.
return;
}
let exitedRetries = 10;
while (!this._exited && exitedRetries --> 0) {
this.logger.log('warn', 'debug', `[ArDetect::main] <@${this.arid}> We are trying to start another instance of autodetection on current video, but the previous instance hasn't exited yet. Waiting for old instance to exit ...`);
await sleep(this.settings.active.arDetect.timers.tickrate);
}
if (!this._exited) {
this.logger.log('error', 'debug', `[ArDetect::main] <@${this.arid}> Previous instance didn't exit in time. Not starting a new one.`);
return;
}
this.logger.log('info', 'debug', `%c[ArDetect::main] <@${this.arid}> Previous instance didn't exit in time. Not starting a new one.`);
// we need to unhalt:
this._halted = false;
this._exited = false;
// set initial timestamps so frame check will trigger the first time we run the loop
let lastFrameCheckStartTime = Date.now() - (this.settings.active.arDetect.timers.playing << 1);
const frameCheckTimes = new Array(10).fill(-1);
let frameCheckBufferIndex = 0;
let fcstart, fctime;
while (this && !this._halted) {
// NOTE: we separated tickrate and inter-check timeouts so that when video switches
// state from 'paused' to 'playing', we don't need to wait for the rest of the longer
// paused state timeout to finish.
if ( (!this.manualTickEnabled && this.canTriggerFrameCheck(lastFrameCheckStartTime)) || this._nextTick) {
this._nextTick = false;
lastFrameCheckStartTime = Date.now();
fcstart = performance.now();
try {
this.frameCheck();
} catch (e) {
this.logger.log('error', 'debug', `%c[ArDetect::main] <@${this.arid}> Frame check failed:`, "color: #000, background: #f00", e);
}
if (Debug.performanceMetrics) {
fctime = performance.now() - fcstart;
frameCheckTimes[frameCheckBufferIndex % frameCheckTimes.length] = fctime;
this.conf.pageInfo.sendPerformanceUpdate({frameCheckTimes: frameCheckTimes, lastFrameCheckTime: fctime});
++frameCheckBufferIndex;
}
}
await sleep(this.settings.active.arDetect.timers.tickrate);
}
this.logger.log('info', 'debug', `%c[ArDetect::main] <@${this.arid}> Main autodetection loop exited. Halted? ${this._halted}`, _ard_console_stop);
this._exited = true;
setManualTick(manualTick) {
this.manualTickEnabled = manualTick;
}
canTriggerFrameCheck(lastFrameCheckStartTime) {
if (this._paused) {
return false;
}
if (this.video.ended || this.video.paused){
// we slow down if ended or pausing. Detecting is pointless.
// we don't stop outright in case seeking happens during pause/after video was
// ended and video gets into 'playing' state again
return Date.now() - lastFrameCheckStartTime > this.settings.active.arDetect.timers.paused;
}
if (this.video.error){
// če je video pavziran, še vedno skušamo zaznati razmerje stranic - ampak bolj poredko.
// if the video is paused, we still do autodetection. We just do it less often.
return Date.now() - lastFrameCheckStartTime > this.settings.active.arDetect.timers.error;
}
return Date.now() - lastFrameCheckStartTime > this.settings.active.arDetect.timers.playing;
tick() {
this._nextTick = true;
}
//#endregion
//#region helper functions (general)
isRunning(){
return ! (this._halted || this._paused || this._exited);
}
private getVideoPlaybackState(): VideoPlaybackState {
try {
if (this.video.ended) {
return VideoPlaybackState.Ended;
} else if (this.video.paused) {
return VideoPlaybackState.Paused;
} else if (this.video.error) {
return VideoPlaybackState.Error;
} else {
return VideoPlaybackState.Playing;
}
} catch (e) {
this.logger.log('warn', 'debug', `[ArDetect::getVideoPlaybackState] There was an error while determining video playback state.`, e);
return VideoPlaybackState.Error;
}
}
scheduleInitRestart(timeout?: number, force_reset?: boolean){
/**
* Checks whether conditions for granting a frame check are fulfilled
* @returns
*/
private canTriggerFrameCheck() {
if (this._paused || this._halted || this._exited) {
return false;
}
// if video was paused & we know that we already checked that frame,
// we will not check it again.
const videoState = this.getVideoPlaybackState();
if (videoState !== VideoPlaybackState.Playing) {
if (this.status.lastVideoStatus === videoState) {
return false;
}
}
this.status.lastVideoStatus = videoState;
if (Date.now() < this.timers.nextFrameCheckTime) {
return false;
}
this.timers.nextFrameCheckTime = Date.now() + this.settings.active.arDetect.timers.playing;
return true;
}
private scheduleInitRestart(timeout?: number, force_reset?: boolean){
if(! timeout){
timeout = 100;
}
@ -385,7 +443,7 @@ class ArDetector {
this.setupTimer = setTimeout(function(){
ths.setupTimer = null;
try{
ths.main();
ths.start();
} catch(e) {
this.logger('error', 'debug', `[ArDetector::scheduleInitRestart] <@${this.arid}> Failed to start main(). Error:`,e);
}
@ -395,10 +453,7 @@ class ArDetector {
);
}
//#region helper functions (general)
attachCanvas(canvas){
private attachCanvas(canvas){
if(this.attachedCanvas)
this.attachedCanvas.remove();
@ -412,25 +467,92 @@ class ArDetector {
.appendChild(canvas);
}
canvasReadyForDrawWindow(){
private canvasReadyForDrawWindow(){
this.logger.log('info', 'debug', `%c[ArDetect::canvasReadyForDrawWindow] <@${this.arid}> canvas is ${this.canvas.height === window.innerHeight ? '' : 'NOT '}ready for drawWindow(). Canvas height: ${this.canvas.height}px; window inner height: ${window.innerHeight}px.`)
return this.canvas.height == window.innerHeight
}
getTimeout(baseTimeout, startTime){
let execTime = (performance.now() - startTime);
return baseTimeout;
/**
* Adds execution time sample for performance metrics
* @param performanceObject
* @param executionTime
*/
private addPerformanceTimeMeasure(performanceObject, executionTime) {
performanceObject.sampleTime[performanceObject.currentIndex] = executionTime;
performanceObject.currentIndex = (performanceObject.currentIndex + 1) % this.performanceConfig.sampleCountForAverages;
}
//#endregion
//#region helper functions (performance measurements)
/**
* Returns time ultrawidify spends on certain aspects of autodetection.
*
* The returned object contains the following:
*
* eyeballedTimeBudget a very inaccurate time budget
* fps framerate at which we run
* aardTime time spent on average frameCheck loop.
* It's a nearly useless metric, because
* frameCheck can exit very early.
* drawWindowTime how much time browser spends on executing
* drawWindow() calls.
* getImageData how much time browser spends on executing
* getImageData() calls.
*
* Most of these are on "per frame" basis and averaged.
*/
getTimings() {
let drawWindowTime = 0, getImageDataTime = 0, aardTime = 0;
// drawImage and getImageData are of same length and use same everything
for (let i = 0; i < this.performance.drawImage.sampleTime.length; i++) {
drawWindowTime += this.performance.drawImage.sampleTime[i] ?? 0;
getImageDataTime += this.performance.getImageData.sampleTime[i] ?? 0;
}
drawWindowTime /= this.performance.drawImage.sampleTime.length;
getImageDataTime /= this.performance.getImageData.sampleTime.length;
return {
eyeballedTimeBudget: this.eyeballedTimeBudget,
fps: this.fps,
drawWindowTime,
getImageDataTime
}
}
//#endregion
getDefaultAr() {
const ratio = this.video.videoWidth / this.video.videoHeight;
if (isNaN(ratio)) {
return undefined;
/**
* This is the "main loop" for aspect ratio autodetection
*/
private async animationFrameBootstrap(timestamp: number) {
// do timekeeping first
this.addPerformanceTimeMeasure(this.performance.animationFrame, timestamp - this.performance.animationFrame.lastTime);
this.performance.animationFrame.lastTime = timestamp;
// trigger frame check, if we're allowed to
if ( (!this.manualTickEnabled && this.canTriggerFrameCheck()) || this._nextTick) {
this._nextTick = false;
try {
const startTime = performance.now();
await this.frameCheck();
this.addPerformanceTimeMeasure(this.performance.aard, performance.now() - startTime);
} catch (e) {
this.logger.log('error', 'debug', `%c[ArDetect::main] <@${this.arid}> Frame check failed:`, "color: #000, background: #f00", e);
}
}
if (this && !this._halted && !this._paused) {
this.animationFrameHandle = window.requestAnimationFrame( (ts) => this.animationFrameBootstrap(ts));
} else if (this._halted) {
this.logger.log('info', 'debug', `%c[ArDetect::main] <@${this.arid}> Main autodetection loop exited. Halted? ${this._halted}`, _ard_console_stop);
this._exited = true;
} else {
this.logger.log('info', 'debug', `[ArDetect::main] <@${this.arid}> Not renewing animation frame for some reason. Paused? ${this._paused}; Halted?: ${this._halted}, Exited?: ${this._exited}`);
}
return ratio;
}
calculateArFromEdges(edges) {
@ -498,6 +620,10 @@ class ArDetector {
}
processAr(trueAr){
if (!this.isRunning()) {
this.logger.log('warn', 'debug', `[ArDetect::processAr] <@${this.arid}> Trying to change aspect ratio while AARD is paused.`);
return;
}
this.detectedAr = trueAr;
// poglejmo, če se je razmerje stranic spremenilo
@ -538,7 +664,9 @@ class ArDetector {
id = undefined;
}
frameCheck(){
async frameCheck(){
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::processAr] <@${this.arid}> Starting frame check.`);
if(! this.video){
this.logger.log('error', 'debug', `%c[ArDetect::frameCheck] <@${this.arid}> Video went missing. Destroying current instance of videoData.`);
this.conf.destroy();
@ -549,43 +677,41 @@ class ArDetector {
this.init();
}
let startTime = performance.now();
let startTime;
let partialDrawImageTime = 0;
let sampleCols = this.sampleCols.slice(0);
//
// [0] blackframe tests (they also determine whether we need fallback mode)
//
try {
this.blackframeContext.drawImage(this.video, 0, 0, this.blackframeCanvas.width, this.blackframeCanvas.height);
startTime = performance.now();
// do it in ghetto async. This way, other javascript function should be able to
// get a chance to do something _before_ we process our data
await new Promise<void>(
resolve => {
this.blackframeContext.drawImage(this.video, 0, 0, this.blackframeCanvas.width, this.blackframeCanvas.height);
resolve();
}
);
partialDrawImageTime += performance.now() - startTime;
// special browsers require special tests
// if (this.hasDRM()) {
// this.fallbackMode = false;
// throw 'VIDEO_DRM_PROTECTED';
// }
this.fallbackMode = false;
// If we detected DRM and if we're here, this means we're also using Google Chrome.
// And if we're here while DRM is detected, we know we'll be looking at black frames.
// We won't be able to do anything useful, therefore we're just gonna call it quits.
if (this.conf.hasDrm) {
this.logger.log('info', 'debug', 'we have drm, doing nothing.', this.conf.hasDrm);
return;
}
} 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);
// in case of DRM errors, we need to prevent the execution to reach the aspec
// 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
this.stop();
this.halt();
let newCanvasWidth = window.innerHeight * (this.video.videoWidth / this.video.videoHeight);
let newCanvasHeight = window.innerHeight;
@ -606,14 +732,19 @@ class ArDetector {
// smaller sample for blackframe check
this.fallbackMode = true;
startTime = performance.now();
try {
(this.context as any).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);
} 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
}
// draw blackframe sample from our main sample:
this.blackframeContext.drawImage(this.canvas, this.blackframeCanvas.width, this.blackframeCanvas.height)
await new Promise<void>(resolve => {
this.blackframeContext.drawImage(this.canvas, this.blackframeCanvas.width, this.blackframeCanvas.height);
resolve();
});
partialDrawImageTime += performance.now() - startTime;
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] canvas.drawImage seems to have worked`, "color:#000; backgroud:#2f5;");
}
@ -625,21 +756,28 @@ class ArDetector {
return;
}
// if we are in fallback mode, then frame has already been drawn to the main canvas.
// if we are in normal mode though, the frame has yet to be drawn
if (!this.fallbackMode) {
this.context.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
startTime = performance.now();
await new Promise<void>(resolve => {
this.context.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
resolve();
})
partialDrawImageTime += performance.now() - startTime;
}
this.addPerformanceTimeMeasure(this.performance.drawImage, partialDrawImageTime);
startTime = performance.now();
const imageData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height).data;
this.addPerformanceTimeMeasure(this.performance.getImageData, performance.now() - startTime);
if (! this.fastLetterboxPresenceTest(imageData, sampleCols) ) {
// Če ne zaznamo letterboxa, kličemo reset. Lahko, da je bilo razmerje stranic popravljeno na roke. Možno je tudi,
// 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: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
this.conf.resizer.updateAr({type: AspectRatioType.Automatic, ratio: this.defaultAr});
this.guardLine.reset();
this.noLetterboxCanvasReset = true;
@ -649,6 +787,7 @@ class ArDetector {
return;
}
// Če preverjamo naprej, potem moramo postaviti to vrednost nazaj na 'false'. V nasprotnem primeru se bo
// css resetiral enkrat na video/pageload namesto vsakič, ko so za nekaj časa obrobe odstranejene
// if we look further we need to reset this value back to false. Otherwise we'll only get CSS reset once
@ -681,34 +820,29 @@ class ArDetector {
// (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: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
this.logger.log('warn', 'arDetect_verbose', `%c[ArDetect::frameCheck] <@${this.arid}> We are in fallback mode and blackbar failed. Reverting to initial aspect ratio.`);
this.conf.resizer.setAr({type: AspectRatioType.Automatic, ratio: this.defaultAr});
this.guardLine.reset();
this.noLetterboxCanvasReset = true;
this.clearImageData(imageData);
return;
}
// će se razmerje stranic spreminja iz ožjega na širšega, potem najprej poglejmo za prisotnostjo navpičnih črnih obrob.
// če so prisotne navpične obrobe tudi na levi in desni strani, potlej obstaja možnost, da gre za logo na črnem ozadju.
// v tem primeru obstaja nevarnost, da porežemo preveč. Ker obstaja dovolj velika možnost, da bi porezali preveč, rajši
// ne naredimo ničesar.
//
// če je pillarbox zaznan v primeru spremembe iz ožjega na širše razmerje stranice, razmerje povrnemo na privzeto vrednost.
//
}
// If aspect ratio changes from narrower to wider, we first check for presence of pillarbox. Presence of pillarbox indicates
// a chance of a logo on black background. We could cut easily cut too much. Because there's a somewhat significant chance
// 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(guardLineOut.blackbarFail){
this.logger.log('info', 'arDetect', `[ArDetect::frameCheck] Detected blackbar violation and pillarbox. Resetting to default aspect ratio.`);
this.conf.resizer.setAr({type: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
this.conf.resizer.setAr({type: AspectRatioType.Automatic, ratio: this.defaultAr});
this.guardLine.reset();
}
} else {
this.logger.log('info', 'arDetect_verbose', `[ArDetect::frameCheck] Guardline failed, blackbar didn't, and we got pillarbox. Doing nothing.`);
}
this.clearImageData(imageData);
return;
@ -776,7 +910,7 @@ class ArDetector {
// 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: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
// this.conf.resizer.setAr({type: AspectRatioType.Automatic, ratio: this.defaultAr});
}
this.clearImageData(imageData);
@ -919,6 +1053,13 @@ class ArDetector {
};
}
/**
* Does a quick test to see if the aspect ratio is correct
* Returns 'true' if there's a chance of letterbox existing, false if not.
* @param imageData
* @param sampleCols
* @returns
*/
fastLetterboxPresenceTest(imageData, sampleCols) {
// fast test to see if aspect ratio is correct.
// returns 'true' if presence of letterbox is possible.
@ -958,8 +1099,11 @@ class ArDetector {
);
}
if (currentMin < this.blackLevel)
if (currentMin < this.blackLevel) {
this.blackLevel = currentMin
}
return true;
}

View File

@ -29,7 +29,7 @@ class PageInfo {
logger: Logger;
settings: Settings;
comms: CommsClient;
videos: VideoData[] = [];
videos: {videoData: VideoData, element: HTMLVideoElement}[] = [];
//#endregion
//#region misc stuff
@ -113,8 +113,8 @@ class PageInfo {
}
for (let video of this.videos) {
try {
(this.comms.unregisterVideo as any)(video.vdid)
video.destroy();
(this.comms.unregisterVideo as any)(video.videoData.vdid)
video.videoData.destroy();
} catch (e) {
this.logger.log('error', ['debug', 'init'], '[PageInfo::destroy] unable to destroy video! Error:', e);
}
@ -135,8 +135,10 @@ class PageInfo {
reset() {
for(let video of this.videos) {
video.destroy();
video.videoData.destroy();
video.videoData = null;
}
this.videos = [];
this.rescan(RescanReason.MANUAL);
}
@ -159,7 +161,7 @@ class PageInfo {
getVideos(host) {
if (this.settings.active.sites[host]?.DOM?.video?.manual
&& this.settings.active.sites[host]?.DOM?.video?.querySelectors){
const videos = document.querySelectorAll(this.settings.active.sites[host].DOM.video.querySelectors);
const videos = document.querySelectorAll(this.settings.active.sites[host].DOM.video.querySelectors) as NodeListOf<HTMLVideoElement>;
if (videos.length) {
return videos;
@ -172,9 +174,25 @@ class PageInfo {
return this.readOnly ? this.hasVideos : this.videos.length;
}
rescan(rescanReason){
const oldVideoCount = this.videos.length;
/**
* Re-scans the page for videos. Removes any videos that no longer exist from our list
* of videos. Destroys all videoData objects for all the videos that don't have their
* own <video> html element on the page.
* @param rescanReason Why was the rescan triggered. Mostly used for logging.
* @returns
*/
rescan(rescanReason?: RescanReason){
// is there any video data objects that had their HTML elements removed but not yet
// destroyed? We clean that up here.
const orphans = this.videos.filter(x => !document.body.contains(x.element));
for (const orphan of orphans) {
orphan.videoData.destroy();
}
// remove all destroyed videos.
this.videos = this.videos.filter(x => !x.videoData.destroyed);
// add new videos
try{
let vids = this.getVideos(window.location.hostname);
@ -190,65 +208,49 @@ class PageInfo {
// add new videos
this.hasVideos = false;
let videoExists = false;
let video, v;
let videoExists = false;
for (const videoElement of vids) {
// do not re-add videos that we already track:
if (this.videos.find(x => x.element.isEqualNode(videoElement))) {
continue;
}
for (video of vids) {
// če najdemo samo en video z višino in širino, to pomeni, da imamo na strani veljavne videe
// če trenutni video nima definiranih teh vrednostih, preskočimo vse nadaljnja preverjanja
// <===[:::::::]===>
// if we find even a single video with width and height, that means the page has valid videos
// if video lacks either of the two properties, we skip all further checks cos pointless
if(video.offsetWidth && video.offsetHeight){
this.hasVideos = true;
if (this.readOnly) {
// in lite mode, we're done. This is all the info we want, but we want to actually start doing
// things that interfere with the website. We still want to be running a rescan, tho.
if(rescanReason == RescanReason.PERIODIC){
this.scheduleRescan(RescanReason.PERIODIC);
}
return;
}
} else {
if(!videoElement.offsetWidth || !videoElement.offsetHeight) {
continue;
}
// at this point, we're certain that we found new videos. Let's update some properties:
this.hasVideos = true;
videoExists = false;
// if PageInfo is marked as "readOnly", we actually aren't adding any videos to anything because
// that's super haram. We're only interested in whether
if (this.readOnly) {
// in lite mode, we're done. This is all the info we want, but we want to actually start doing
// things that interfere with the website. We still want to be running a rescan, tho.
for (v of this.videos) {
if (v.destroyed) {
continue; //TODO: if destroyed video is same as current video, copy aspect ratio settings to current video
}
if (v.video == video) {
videoExists = true;
break;
if(rescanReason == RescanReason.PERIODIC){
this.scheduleRescan(RescanReason.PERIODIC);
}
return;
}
if (videoExists) {
continue;
} else {
this.logger.log('info', 'videoRescan', "[PageInfo::rescan] found new video candidate:", video, "NOTE:: Video initialization starts here:\n--------------------------------\n")
try {
v = new VideoData(video, this.settings, this);
this.videos.push(v);
} catch (e) {
this.logger.log('error', 'debug', "rescan error: failed to initialize videoData. Skipping this video.",e);
}
this.logger.log('info', 'videoRescan', "[PageInfo::rescan] found new video candidate:", videoElement, "NOTE:: Video initialization starts here:\n--------------------------------\n")
this.logger.log('info', 'videoRescan', "END VIDEO INITIALIZATION\n\n\n-------------------------------------\nvideos[] is now this:", this.videos,"\n\n\n\n\n\n\n\n")
try {
const newVideo = new VideoData(videoElement, this.settings, this);
this.videos.push({videoData: newVideo, element: videoElement});
} catch (e) {
this.logger.log('error', 'debug', "rescan error: failed to initialize videoData. Skipping this video.",e);
}
this.logger.log('info', 'videoRescan', "END VIDEO INITIALIZATION\n\n\n-------------------------------------\nvideos[] is now this:", this.videos,"\n\n\n\n\n\n\n\n")
}
this.removeDestroyed();
// če smo ostali brez videev, potem odregistriraj stran.
// če nismo ostali brez videev, potem registriraj stran.
//
// if we're left without videos on the current page, we unregister the page.
// if we have videos, we call register.
if (this.comms) {
@ -280,27 +282,23 @@ class PageInfo {
}
} catch(e) {
// če pride do zajeba, potem lahko domnevamo da na strani ni nobenega videa. Uničimo vse objekte videoData
// da preprečimo večkratno inicializacijo. Če smo se z našim ugibom zmotili, potem se bodo vsi videi ponovno
// našli ob naslednjem preiskovanju
//
// if we encounter a fuckup, we can assume that no videos were found on the page. We destroy all videoData
// objects to prevent multiple initialization (which happened, but I don't know why). No biggie if we destroyed
// videoData objects in error — they'll be back in the next rescan
this.logger.log('error', 'debug', "rescan error: — destroying all videoData objects",e);
for (const v of this.videos) {
v.destroy();
v.videoData.destroy();
}
this.videos = [];
return;
}
if(rescanReason == RescanReason.PERIODIC){
this.scheduleRescan(RescanReason.PERIODIC);
}
}
removeDestroyed(){
this.videos = this.videos.filter( vid => vid.destroyed === false);
this.videos = this.videos.filter( vid => vid.videoData.destroyed === false);
}
scheduleRescan(rescanReason){
@ -358,14 +356,14 @@ class PageInfo {
initArDetection(playingOnly){
if (playingOnly) {
for(let vd of this.videos){
if(vd.isPlaying()) {
vd.initArDetection();
if(vd.videoData.isPlaying()) {
vd.videoData.initArDetection();
}
}
return;
} else {
for(let vd of this.videos){
vd.initArDetection();
vd.videoData.initArDetection();
}
}
}
@ -376,13 +374,13 @@ class PageInfo {
pauseProcessing(playingOnly){
if (playingOnly) {
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.pause();
if (vd.videoData.isPlaying()) {
vd.videoData.pause();
}
}
} else {
for(let vd of this.videos){
vd.pause();
vd.videoData.pause();
}
}
}
@ -390,18 +388,18 @@ class PageInfo {
resumeProcessing(resumeAutoar = false, playingOnly = false){
if (playingOnly) {
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.resume();
if (vd.videoData.isPlaying()) {
vd.videoData.resume();
if(resumeAutoar){
vd.resumeAutoAr();
vd.videoData.resumeAutoAr();
}
}
}
} else {
for(let vd of this.videos){
vd.resume();
vd.videoData.resume();
if(resumeAutoar){
vd.resumeAutoAr();
vd.videoData.resumeAutoAr();
}
}
}
@ -414,13 +412,13 @@ class PageInfo {
}
if (playingOnly) {
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.startArDetection();
if (vd.videoData.isPlaying()) {
vd.videoData.startArDetection();
}
}
} else {
for(let vd of this.videos){
vd.startArDetection();
vd.videoData.startArDetection();
}
}
}
@ -428,13 +426,13 @@ class PageInfo {
stopArDetection(playingOnly){
if (playingOnly) {
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.stopArDetection();
if (vd.videoData.isPlaying()) {
vd.videoData.stopArDetection();
}
}
} else {
for(let vd of this.videos){
vd.stopArDetection();
vd.videoData.stopArDetection();
}
}
}
@ -449,8 +447,8 @@ class PageInfo {
try {
for (let vd of this.videos) {
if (!playingOnly || vd.isPlaying()) {
vd.resetLastAr();
if (!playingOnly || vd.videoData.isPlaying()) {
vd.videoData.resetLastAr();
}
}
} catch (e) {
@ -464,14 +462,14 @@ class PageInfo {
// TODO: find a way to only change aspect ratio for one video
if (ar === AspectRatioType.Reset) {
for (let vd of this.videos) {
if (!playingOnly || vd.isPlaying()) {
vd.resetAr();
if (!playingOnly || vd.videoData.isPlaying()) {
vd.videoData.resetAr();
}
}
} else {
for (let vd of this.videos) {
if (!playingOnly || vd.isPlaying()) {
vd.setAr(ar)
if (!playingOnly || vd.videoData.isPlaying()) {
vd.videoData.setAr(ar)
}
}
}
@ -480,13 +478,13 @@ class PageInfo {
setVideoAlignment(videoAlignment, playingOnly) {
if (playingOnly) {
for(let vd of this.videos) {
if (vd.isPlaying()) {
vd.setVideoAlignment(videoAlignment)
if (vd.videoData.isPlaying()) {
vd.videoData.setVideoAlignment(videoAlignment)
}
}
} else {
for(let vd of this.videos) {
vd.setVideoAlignment(videoAlignment)
vd.videoData.setVideoAlignment(videoAlignment)
}
}
}
@ -494,13 +492,13 @@ class PageInfo {
setPanMode(mode, playingOnly?: boolean) {
if (playingOnly) {
for(let vd of this.videos) {
if (vd.isPlaying()) {
vd.setPanMode(mode);
if (vd.videoData.isPlaying()) {
vd.videoData.setPanMode(mode);
}
}
} else {
for(let vd of this.videos) {
vd.setPanMode(mode);
vd.videoData.setPanMode(mode);
}
}
}
@ -508,13 +506,13 @@ class PageInfo {
restoreAr(playingOnly?: boolean) {
if (playingOnly) {
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.restoreAr();
if (vd.videoData.isPlaying()) {
vd.videoData.restoreAr();
}
}
} else {
for(let vd of this.videos){
vd.restoreAr();
vd.videoData.restoreAr();
}
}
}
@ -524,13 +522,13 @@ class PageInfo {
if (playingOnly) {
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.setStretchMode(stretchMode, fixedStretchRatio)
if (vd.videoData.isPlaying()) {
vd.videoData.setStretchMode(stretchMode, fixedStretchRatio)
}
}
} else {
for(let vd of this.videos){
vd.setStretchMode(stretchMode, fixedStretchRatio)
vd.videoData.setStretchMode(stretchMode, fixedStretchRatio)
}
}
}
@ -538,33 +536,33 @@ class PageInfo {
setZoom(zoomLevel, no_announce?: boolean, playingOnly?: boolean) {
if (playingOnly) {
for(let vd of this.videos) {
if (vd.isPlaying()) {
vd.setZoom(zoomLevel, no_announce);
if (vd.videoData.isPlaying()) {
vd.videoData.setZoom(zoomLevel, no_announce);
}
}
} else {
for(let vd of this.videos) {
vd.setZoom(zoomLevel, no_announce);
vd.videoData.setZoom(zoomLevel, no_announce);
}
}
}
zoomStep(step, playingOnly?: boolean) {
for(let vd of this.videos){
if (!playingOnly || vd.isPlaying()) {
vd.zoomStep(step);
if (!playingOnly || vd.videoData.isPlaying()) {
vd.videoData.zoomStep(step);
}
}
}
markPlayer(name, color) {
for (let vd of this.videos) {
vd.markPlayer(name,color);
vd.videoData.markPlayer(name,color);
}
}
unmarkPlayer() {
for (let vd of this.videos) {
vd.unmarkPlayer();
vd.videoData.unmarkPlayer();
}
}
@ -579,13 +577,13 @@ class PageInfo {
setManualTick(manualTick) {
for(let vd of this.videos) {
vd.setManualTick(manualTick);
vd.videoData.setManualTick(manualTick);
}
}
tick() {
for(let vd of this.videos) {
vd.tick();
vd.videoData.tick();
}
}

View File

@ -36,6 +36,7 @@ class VideoData {
userCssClassName: string;
validationId: number;
dimensions: any;
hasDrm: boolean;
//#endregion
//#region helper objects
@ -322,7 +323,7 @@ class VideoData {
this.pause();
this.destroyed = true;
try {
this.arDetector.stop();
this.arDetector.halt();
this.arDetector.destroy();
} catch (e) {}
this.arDetector = undefined;
@ -436,6 +437,9 @@ class VideoData {
* instead you should be calling processDimensionChanged() wrapper function.
*/
private _processDimensionsChanged() {
if (!this.player) {
this.logger.log('warn', 'debug', `[VideoData::_processDimensionsChanged] Player is not defined. This is super haram.`, this.player)
}
// 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
@ -583,6 +587,9 @@ class VideoData {
if (hasDrm(this.video)) {
this.player.showNotification('AARD_DRM');
this.hasDrm = true;
} else {
this.hasDrm = false;
}
if (!this.arDetector) {
@ -601,14 +608,14 @@ class VideoData {
stopArDetection() {
if (this.arDetector) {
this.arDetector.stop();
this.arDetector.halt();
}
}
pause(){
this.paused = true;
if(this.arDetector){
this.arDetector.stop();
this.arDetector.halt();
}
if(this.player){
this.player.stop();

View File

@ -218,7 +218,7 @@ class Resizer {
this.logger.log('info', 'resizer', `[Resizer::setAr] <${this.resizerId}> Something wrong with ar or the player. Doing nothing.`);
return;
}
this.lastAr = {type: ar.type, ratio: ar.ratio}
this.lastAr = {type: ar.type, ratio: ar.ratio};
}
// if (this.extensionMode === ExtensionMode.Basic && !PlayerData.isFullScreen() && ar.type !== AspectRatioType.Reset) {
@ -293,7 +293,6 @@ class Resizer {
}
this.zoom.applyZoom(stretchFactors);
this.stretcher.chromeBugMitigation(stretchFactors);
let translate = this.computeOffsets(stretchFactors);

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": "5.0.5",
"version": "5.1.0",
"applications": {
"gecko": {
"id": "{cf02b1a7-a01a-4e37-a609-516a283f1ed3}"

View File

@ -2,23 +2,21 @@
<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">5.0.5</p>
<p class="label">5.1.0</p>
<ul>
<li>
In 'Advanced Settings' tab of the popup: Player Detection Settings are now a bit less of a mess.
Under the hood changes: aspect ratio autodetection now uses requestAnimationFrame instead of a setTimeout/setInterval-based loop.
</li>
<li>
Fixed the background issue with app.plex.tv (<a href="https://github.com/tamius-han/ultrawidify/issues/158" target="_blank">#158</a>).<br/>
<small><b>NOTE:</b> if you're using self-hosted plex, you will have to configure the extension for flex yourself. Refer to the <a href="https://github.com/tamius-han/ultrawidify/issues/158" target="_blank">github issue</a> for details.</small>
Logger is sorta fixed.
</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. Yes I know this message has been here since march, but nothing has changed.</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>
Hopefully that didn't break anything too much.
</p>
<p><small>
<b>Known issues:</b> zooming is limited in Chromium-based browsers. This is a browser bug that no extension can fix. See <a href="https://github.com/tamius-han/ultrawidify/discussions/161" target="_blank">this</a> for more info.
</small></p>
</div>
</template>
<script>