Compare commits

..

4 Commits

29 changed files with 3134 additions and 553 deletions

View File

@ -8,6 +8,7 @@
* Setting inheritance/overriding is not thoroughly tested and may be full of edge cases. * Setting inheritance/overriding is not thoroughly tested and may be full of edge cases.
* Added validation to custom aspect ratio entry menu. Corrected parsing of aspect ratios given in the X:Y format, even though aspect ratios should be ideally given as a single number. * Added validation to custom aspect ratio entry menu. Corrected parsing of aspect ratios given in the X:Y format, even though aspect ratios should be ideally given as a single number.
* Autodetection can now scan for subtitles. * Autodetection can now scan for subtitles.
* New experimental autodetection (which is secretly just slightly modified subtitle check)
### v6.3.0 ### v6.3.0
* Added zoom segment to in-player UI and popup. * Added zoom segment to in-player UI and popup.

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "ultrawidify", "name": "ultrawidify",
"version": "6.3.97", "version": "6.3.993",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "ultrawidify", "name": "ultrawidify",
"version": "6.3.97", "version": "6.3.993",
"description": "Aspect ratio fixer for youtube and other sites, with automatic aspect ratio detection. Supports ultrawide and other ratios.", "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>", "author": "Tamius Han <tamius.han@gmail.com>",
"scripts": { "scripts": {

View File

@ -90,6 +90,7 @@ export interface AardSubtitleScanOptions {
export interface AardSettings { export interface AardSettings {
aardType: 'webgl' | 'legacy' | 'auto'; aardType: 'webgl' | 'legacy' | 'auto';
useLegacy: boolean,
earlyStopOptions: { earlyStopOptions: {
stopAfterFirstDetection: boolean; stopAfterFirstDetection: boolean;
@ -183,6 +184,131 @@ export interface AardSettings {
} }
} }
export interface AardLegacySettings {
aardType: 'webgl' | 'legacy' | 'auto';
earlyStopOptions: {
stopAfterFirstDetection: boolean;
stopAfterTimeout: boolean;
stopTimeout: number;
},
polling: {
runInBackgroundTabs: AardPollingOptions;
runOnSmallVideos: AardPollingOptions;
}
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
playingReduced: number, // while video/player element has insufficient size
paused: number, // while paused
error: number, // after error
minimumTimeout: number,
tickrate: number, // 1 tick every this many milliseconds
},
subtitles: AardSubtitleScanOptions,
autoDisable: { // settings for automatically disabling the extension
onFirstChange: boolean,
ifNotChanged: boolean,
ifNotChangedTimeout: number,
ifSubtitles: boolean;
},
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,
},
},
blackLevels: {
defaultBlack: number, // By default, pixels darker than this are considered black.
// (If detection algorithm detects darker blacks, black is considered darkest detected pixel)
blackTolerance: number, // If pixel is more than this much brighter than blackLevel, it's considered not black
// It is not considered a valid image detection if gradient detection is enabled
imageDelta: number, // When gradient detection is enabled, pixels this much brighter than black skip gradient detection
}
sampling: {
edgePosition: number; // % of width (max 0.33). Pixels up to this far away from either edge may contain logo.
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,
},
// pls deprecate and move things used
edgeDetection: {
slopeTestWidth: number,
gradientTestSamples: number, // we check this many pixels below (or above) the suspected edge to check for gradient
gradientTestBlackThreshold: number, // if pixel in test sample is brighter than that, we aren't looking at gradient
gradientTestDeltaThreshold: number, // if delta between two adjacent pixels in gradient test exceeds this, it's not gradient
thresholds: {
edgeDetectionLimit: number, // during scanning of the edge, quit after edge gets detected at this many points
minQualitySingleEdge: number, // At least one of the detected must reach this quality
minQualitySecondEdge: number, // The other edge must reach this quality (must be smaller or equal to single edge quality)
}
gradientThreshold: number, // if more than this percentage (0-1) is detected as gradient, we mark edge as gradient
gradientTestMinDelta: number, // if difference between test row and before row is MORE than this -> not gradient
gradientTestMinDeltaAfter: number, // if difference between test row and after row is LESS than this -> not gradient
gradientTestMaxDeltaAfter: number, // if difference between test row and after row is MORE than this -> not gradient
maxLetterboxOffset: number, // Upper and lower letterbox can be different by this many (% of height)
// Previous iteration variables VVVV
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)
edgeMismatchTolerancePx: number,// corners and center are considered equal if they differ by at most this many px
minValidImage: number, // if more than this % (0-1) of row is image, we confirm image regardless of other criteria except gradient
maxEdgeSegments: number, // if edge has more than this many segments, we consider it unreliable
minEdgeSegmentSize: number,
averageEdgeThreshold: number, // average edge must be this many px
},
letterboxOrientationScan: {
letterboxLimit: number, // how many non-black pixels we can detect before ruling out letterbox
pillarboxLimit: number, // how many non-black pixels we can detect before ruling out pillarbox
}
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
}
}
interface DevSettings { interface DevSettings {
loadFromSnapshot: boolean, loadFromSnapshot: boolean,
} }
@ -194,7 +320,8 @@ interface SettingsInterface {
} }
dev: DevSettings, dev: DevSettings,
arDetect: AardSettings, aardLegacy: AardLegacySettings,
aard: AardSettings,
ui: { ui: {
inPlayer: { inPlayer: {

View File

@ -11,19 +11,29 @@
<div class="settings-segment"> <div class="settings-segment">
<div class="field">
<div class="label">Autodetection mode (requires page reload)</div>
<div class="select">
<select v-model="settings.active.aard.useLegacy" @change="aardLegacyModeChanged">
<option :value="true">Legacy</option>
<option :value="false">Experimental</option>
</select>
</div>
</div>
<div class="field"> <div class="field">
<div class="label">Autodetection frequency (time between samples)</div> <div class="label">Autodetection frequency (time between samples)</div>
<div class="range-input"> <div class="range-input">
<input <input
type="range" type="range"
:value="Math.log(settings.active.arDetect.timers.playing)" :value="Math.log(aardSettings.timers.playing)"
@change="setArCheckFrequency($event.target.value)" @change="setArCheckFrequency($event.target.value)"
min="2.3" min="2.3"
max="9.3" max="9.3"
step="0.01" step="0.01"
/> />
<input <input
v-model="settings.active.arDetect.timers.playing" v-model="aardSettings.timers.playing"
@change="setArCheckFrequency($event.target.value)" @change="setArCheckFrequency($event.target.value)"
class="input" class="input"
type="text" type="text"
@ -37,14 +47,14 @@
<div class="range-input"> <div class="range-input">
<input <input
type="range" type="range"
:value="Math.log(settings.active.arDetect.timers.playingReduced)" :value="Math.log(aardSettings.timers.playingReduced)"
@change="setArCheckFrequency($event.target.value, 'playingReduced')" @change="setArCheckFrequency($event.target.value, 'playingReduced')"
min="2.3" min="2.3"
max="9.3" max="9.3"
step="0.01" step="0.01"
/> />
<input <input
v-model="settings.active.arDetect.timers.playingReduced" v-model="aardSettings.timers.playingReduced"
@change="setArCheckFrequency($event.target.value, 'playingReduced')" @change="setArCheckFrequency($event.target.value, 'playingReduced')"
class="input" class="input"
type="text" type="text"
@ -56,64 +66,64 @@
<div class="field"> <div class="field">
<div class="label">Poll for aspect ratio changes in background tabs:</div> <div class="label">Poll for aspect ratio changes in background tabs:</div>
<div class="select"> <div class="select">
<select v-model="settings.active.arDetect.polling.runInBackgroundTabs" @change="settings.saveWithoutReload"> <select v-model="aardSettings.polling.runInBackgroundTabs" @change="settings.saveWithoutReload">
<option :value="AardPollingOptions.No">Never</option> <option :value="AardPollingOptions.No">Never</option>
<option :value="AardPollingOptions.Reduced">Use reduced polling rate</option> <option :value="AardPollingOptions.Reduced">Use reduced polling rate</option>
<option :value="AardPollingOptions.Full">Use normal polling rate</option> <option :value="AardPollingOptions.Full">Use normal polling rate</option>
</select> </select>
</div> </div>
</div> </div>
<div v-if="settings.active.arDetect.polling.runInBackgroundTabs === AardPollingOptions.Full" class="hint warn"> <div v-if="aardSettings.polling.runInBackgroundTabs === AardPollingOptions.Full" class="hint warn">
Using normal polling rate in background tabs is NOT recommended. Using normal polling rate in background tabs is NOT recommended.
</div> </div>
<div class="field"> <div class="field">
<div class="label">Poll for aspect ratio changes in small players:</div> <div class="label">Poll for aspect ratio changes in small players:</div>
<div class="select"> <div class="select">
<select v-model="settings.active.arDetect.polling.runOnSmallVideos" @change="settings.saveWithoutReload"> <select v-model="aardSettings.polling.runOnSmallVideos" @change="settings.saveWithoutReload">
<option :value="AardPollingOptions.No">Never</option> <option :value="AardPollingOptions.No">Never</option>
<option :value="AardPollingOptions.Reduced">Use reduced polling rate</option> <option :value="AardPollingOptions.Reduced">Use reduced polling rate</option>
<option :value="AardPollingOptions.Full">Use normal polling rate</option> <option :value="AardPollingOptions.Full">Use normal polling rate</option>
</select> </select>
</div> </div>
</div> </div>
<div v-if="settings.active.arDetect.polling.runOnSmallVideos === AardPollingOptions.Full" class="hint warn"> <div v-if="aardSettings.polling.runOnSmallVideos === AardPollingOptions.Full" class="hint warn">
Using normal polling rate on small videos is NOT recommended. Using normal polling rate on small videos is NOT recommended.
</div> </div>
<div class="field"> <div class="field">
<div class="label">Stop autodetection after first detection:</div> <div class="label">Stop autodetection after first detection:</div>
<div class="select"> <div class="select">
<select v-model="settings.active.arDetect.autoDisable.onFirstChange" @change="settings.saveWithoutReload"> <select v-model="aardSettings.autoDisable.onFirstChange" @change="settings.saveWithoutReload">
<option :value="true">Yes</option> <option :value="true">Yes</option>
<option :value="false">No</option> <option :value="false">No</option>
</select> </select>
</div> </div>
</div> </div>
<div class="field" :class="{disabled: settings.active.arDetect.autoDisable.onFirstChange}"> <div class="field" :class="{disabled: aardSettings.autoDisable.onFirstChange}">
<div class="label">Stop autodetection if aspect ratio doesn't change for some time:</div> <div class="label">Stop autodetection if aspect ratio doesn't change for some time:</div>
<div class="select"> <div class="select">
<select v-model="settings.active.arDetect.autoDisable.ifNotChanged" @change="settings.saveWithoutReload"> <select v-model="aardSettings.autoDisable.ifNotChanged" @change="settings.saveWithoutReload">
<option :value="true">Yes</option> <option :value="true">Yes</option>
<option :value="false">No</option> <option :value="false">No</option>
</select> </select>
</div> </div>
</div> </div>
<div v-if="settings.active.arDetect.autoDisable.ifNotChanged" class="field"> <div v-if="aardSettings.autoDisable.ifNotChanged" class="field">
<div class="label">Stop autodetection if aspect ratio doesn't change for:</div> <div class="label">Stop autodetection if aspect ratio doesn't change for:</div>
<div class="range-input"> <div class="range-input">
<input <input
type="range" type="range"
:value="Math.log(settings.active.arDetect.autoDisable.ifNotChangedTimeout / 10)" :value="Math.log(aardSettings.autoDisable.ifNotChangedTimeout / 10)"
@change="setAutoDisableTimeout($event.target.value, 10)" @change="setAutoDisableTimeout($event.target.value, 10)"
min="2.3" min="2.3"
max="9.3" max="9.3"
step="0.01" step="0.01"
/> />
<input <input
:value="settings.active.arDetect.autoDisable.ifNotChangedTimeout / 1000" :value="aardSettings.autoDisable.ifNotChangedTimeout / 1000"
@change="setAutoDisableTimeout($event.target.value, 1000)" @change="setAutoDisableTimeout($event.target.value, 1000)"
class="input" class="input"
type="text" type="text"
@ -125,7 +135,7 @@
<div class="field"> <div class="field">
<div class="label">Autodetection canvas type:</div> <div class="label">Autodetection canvas type:</div>
<div class="select"> <div class="select">
<select v-model="settings.active.arDetect.aardType" @change="settings.saveWithoutReload"> <select v-model="aardSettings.aardType" @change="settings.saveWithoutReload">
<option value="auto">Automatic</option> <option value="auto">Automatic</option>
<option value="webgl">WebGL only</option> <option value="webgl">WebGL only</option>
<option value="legacy">Legacy / fallback</option> <option value="legacy">Legacy / fallback</option>
@ -136,7 +146,7 @@
<div class="field"> <div class="field">
<div class="label">Maximum allowed vertical video misalignment:</div> <div class="label">Maximum allowed vertical video misalignment:</div>
<div class="input"> <div class="input">
<input v-model="settings.active.arDetect.allowedMisaligned" /> <input v-model="aardSettings.allowedMisaligned" />
</div> </div>
</div> </div>
<div class="hint"> <div class="hint">
@ -154,8 +164,8 @@
<div class="field"> <div class="field">
<div class="label">Subtitle detection:</div> <div class="label">Subtitle detection:</div>
<div class="select"> <div class="select">
<select v-model="settings.active.arDetect.subtitles.subtitleCropMode" @change="settings.saveWithoutReload"> <select v-model="aardSettings.subtitles.subtitleCropMode" @change="settings.saveWithoutReload">
<option :value="AardSubtitleCropMode.DisableScan">Do not detect subtitles</option> <!-- <option :value="AardSubtitleCropMode.DisableScan">Do not detect subtitles</option> -->
<option :value="AardSubtitleCropMode.ResetAR">Do not crop while subtitles are on screen</option> <option :value="AardSubtitleCropMode.ResetAR">Do not crop while subtitles are on screen</option>
<option :value="AardSubtitleCropMode.ResetAndDisable">Stop autodetection if subtitles are detected</option> <option :value="AardSubtitleCropMode.ResetAndDisable">Stop autodetection if subtitles are detected</option>
<option :value="AardSubtitleCropMode.CropSubtitles">Always crop subtitles</option> <option :value="AardSubtitleCropMode.CropSubtitles">Always crop subtitles</option>
@ -163,19 +173,19 @@
</div> </div>
</div> </div>
<div v-if="settings.active.arDetect.subtitles.subtitleCropMode === AardSubtitleCropMode.ResetAR" class="field"> <div v-if="aardSettings.subtitles.subtitleCropMode === AardSubtitleCropMode.ResetAR" class="field">
<div class="label">Wait before resuming detection:</div> <div class="label">Wait before resuming detection:</div>
<div class="range-input"> <div class="range-input">
<input <input
type="range" type="range"
:value="Math.log(settings.active.arDetect.subtitles.resumeAfter / 10)" :value="Math.log(aardSettings.subtitles.resumeAfter / 10)"
@change="setSubtitleTimeout($event.target.value, 10)" @change="setSubtitleTimeout($event.target.value, 10)"
min="2.3" min="2.3"
max="9.3" max="9.3"
step="0.01" step="0.01"
/> />
<input <input
:value="settings.active.arDetect.subtitles.resumeAfter / 1000" :value="aardSettings.subtitles.resumeAfter / 1000"
@change="setSubtitleTimeout($event.target.value, 1000)" @change="setSubtitleTimeout($event.target.value, 1000)"
class="input" class="input"
type="text" type="text"
@ -261,6 +271,9 @@ export default {
} }
}, },
computed: { computed: {
aardSettings() {
return this.settings.active.aard.useLegacy ? this.settings.active.aardLegacy : this.settings.active.aard;
}
}, },
created() { created() {
this.eventBus.subscribe( this.eventBus.subscribe(
@ -285,15 +298,15 @@ export default {
BrowserDetect.runtime.openOptionsPage(); BrowserDetect.runtime.openOptionsPage();
}, },
setArCheckFrequency(event, timer) { setArCheckFrequency(event, timer) {
this.settings.active.arDetect.timers[timer ?? 'playing'] = Math.floor(Math.pow(Math.E, event)); this.aardSettings.timers[timer ?? 'playing'] = Math.floor(Math.pow(Math.E, event));
this.settings.saveWithoutReload(); this.settings.saveWithoutReload();
}, },
setAutoDisableTimeout(event, multiplier) { setAutoDisableTimeout(event, multiplier) {
this.settings.active.arDetect.autoDisable.ifNotChangedTimeout = Math.floor(Math.pow(Math.E, event)) * multiplier; this.aardSettings.autoDisable.ifNotChangedTimeout = Math.floor(Math.pow(Math.E, event)) * multiplier;
this.settings.saveWithoutReload(); this.settings.saveWithoutReload();
}, },
setSubtitleTimeout(event, multiplier) { setSubtitleTimeout(event, multiplier) {
this.settings.active.arDetect.subtitles.resumeAfter = Math.floor(Math.pow(Math.E, event)) * multiplier; this.aardSettings.subtitles.resumeAfter = Math.floor(Math.pow(Math.E, event)) * multiplier;
this.settings.saveWithoutReload(); this.settings.saveWithoutReload();
}, },
refreshGraph() { refreshGraph() {
@ -311,6 +324,9 @@ export default {
saveDebugUiSettings() { saveDebugUiSettings() {
this.settings.active.ui.dev.aardDebugOverlay = JSON.parse(JSON.stringify(this.settingsJson)); this.settings.active.ui.dev.aardDebugOverlay = JSON.parse(JSON.stringify(this.settingsJson));
this.settings.saveWithoutReload(); this.settings.saveWithoutReload();
},
aardLegacyModeChanged() {
this.settings.save();
} }
}, },

View File

@ -11,6 +11,10 @@
<li>Embedded sites now inherit settings of the parent frame. <small>However, this hasn't been tested for all edge cases and may contain bugs.</small></li> <li>Embedded sites now inherit settings of the parent frame. <small>However, this hasn't been tested for all edge cases and may contain bugs.</small></li>
<li>Added validation to custom aspect ratio entry menu. Corrected parsing of aspect ratios given in the X:Y format, even though aspect ratios should be ideally given as a single number.</li> <li>Added validation to custom aspect ratio entry menu. Corrected parsing of aspect ratios given in the X:Y format, even though aspect ratios should be ideally given as a single number.</li>
<li>Autodetection can be set to stop after first aspect ratio detection, or after a period of no changes.</li> <li>Autodetection can be set to stop after first aspect ratio detection, or after a period of no changes.</li>
<li>
There is a new, experiental mode for autodetection. At this point, it can be manually enabled in the autodetection settings. It will become the default option in 2026.<br/>
If you enable experimental mode, please consider reporting problems <a href="https://github.com/tamius-han/ultrawidify/issues/291" target="_blank">in this thread</a> on Github.
</li>
</ul> </ul>
</div> </div>

View File

@ -255,17 +255,25 @@ const ExtensionConfPatch = Object.freeze([
{ {
forVersion: '6.3.93', forVersion: '6.3.93',
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface) => { updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface) => {
userOptions.arDetect.polling = defaultOptions.arDetect.polling; (userOptions as any).arDetect.polling = defaultOptions.aard.polling;
userOptions.arDetect.subtitles = defaultOptions.arDetect.subtitles; (userOptions as any).arDetect.subtitles = defaultOptions.aard.subtitles;
userOptions.arDetect.autoDisable = defaultOptions.arDetect.autoDisable; (userOptions as any).arDetect.autoDisable = defaultOptions.aard.autoDisable;
} }
}, },
{ {
forVersion: '6.3.97', forVersion: '6.3.98',
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface) => { updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface) => {
userOptions.arDetect.letterboxOrientationScan = defaultOptions.arDetect.letterboxOrientationScan; (userOptions as any).arDetect.letterboxOrientationScan = defaultOptions.aard.letterboxOrientationScan;
userOptions.arDetect.edgeDetection = defaultOptions.arDetect.edgeDetection; (userOptions as any).arDetect.edgeDetection = defaultOptions.aard.edgeDetection;
userOptions.arDetect.subtitles = defaultOptions.arDetect.subtitles; (userOptions as any).arDetect.subtitles = defaultOptions.aard.subtitles;
}
},
{
forVersion: '6.3.98',
upgradeFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface) => {
userOptions.aard = defaultOptions.aard;
userOptions.aardLegacy = defaultOptions.aardLegacy;
delete (userOptions as any).arDetect;
} }
} }

View File

@ -21,7 +21,7 @@ const ExtensionConf: SettingsInterface = {
loadFromSnapshot: false, loadFromSnapshot: false,
}, },
arDetect: { aardLegacy: {
aardType: 'auto', aardType: 'auto',
polling: { polling: {
@ -88,6 +88,140 @@ const ExtensionConf: SettingsInterface = {
}, },
}, },
blackLevels: {
defaultBlack: 16,
blackTolerance: 4,
imageDelta: 16,
},
sampling: {
edgePosition: 0.25,
staticCols: 16, // we take a column at [0-n]/n-th parts along the width and sample it
randomCols: 0, // we add this many randomly selected columns to the static columns
staticRows: 9, // forms grid with staticSampleCols. Determined in the same way. For black frame checks
},
edgeDetection: {
slopeTestWidth: 8,
gradientTestSamples: 8,
gradientTestBlackThreshold: 16,
gradientTestDeltaThreshold: 32,
gradientTestMinDelta: 8,
thresholds: {
edgeDetectionLimit: 12,
minQualitySingleEdge: 6,
minQualitySecondEdge: 3,
},
maxLetterboxOffset: 0.1,
sampleWidth: 8, // we take a sample this wide for edge detection
detectionThreshold: 4, // sample needs to have this many non-black pixels to be a valid edge
confirmationThreshold: 1, //
singleSideConfirmationThreshold: 3, // 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: 0.15, // if edge candidate sits with count greater than this*all_samples, it can't be logo
// or watermark.
edgeTolerancePx: 1, // we check for black edge violation this far from detection point
edgeTolerancePercent: null, // we check for black edge detection this % of height from detection point. unused
middleIgnoredArea: 0.2, // we ignore this % of canvas height towards edges while detecting aspect ratios
minColsForSearch: 0.5, // 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)
edgeMismatchTolerancePx: 3, // corners and center are considered equal if they differ by at most this many px
gradientThreshold: 0.5, // if more than this percentage (0-1) is detected as gradient, we mark edge as gradient
gradientTestMinDeltaAfter: 2, // if difference between test row and after row is LESS than this -> not gradient
gradientTestMaxDeltaAfter: 12, // if difference between test row and after row is MORE than this -> not gradient
minValidImage: 0.7, // if more than this % (0-1) of row is image, we confirm image regardless of other criteria except gradient
maxEdgeSegments: 8, // if edge has more than this many segments, we consider it unreliable
minEdgeSegmentSize: 2,
averageEdgeThreshold: 16, // average(ish) edge must be this many px
},
pillarTest: {
ignoreThinPillarsPx: 5, // ignore pillars that are less than this many pixels thick.
allowMisaligned: 0.05 // left and right edge can vary this much (%)
},
textLineTest: {
nonTextPulse: 0.10, // 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: 10, // this is a threshold to confirm we're seeing text.
pulsesToConfirmIfHalfBlack: 5, // this is the threshold to confirm we're seeing text if longest black pulse
// is over 50% of the canvas width
testRowOffset: 0.02 // we test this % of height from detected edge
}
},
aard: {
aardType: 'auto',
useLegacy: true,
polling: {
runInBackgroundTabs: AardPollingOptions.Reduced,
runOnSmallVideos: AardPollingOptions.Reduced
},
letterboxOrientationScan: {
letterboxLimit: 8,
pillarboxLimit: 8
},
subtitles: {
subtitleCropMode: AardSubtitleCropMode.ResetAR,
resumeAfter: 5000,
scanSpacing: 5,
scanMargin: 0.25,
maxValidLetter: 24,
subtitleSubpixelThresholdOff: 8,
subtitleSubpixelThresholdOn: 192,
minDetections: 8,
minImageLineDetections: 8,
refiningScanSpacing: 8,
refiningScanInitialIterations: 12,
maxPotentialSubtitleMisalignment: 32,
},
earlyStopOptions: {
stopAfterFirstDetection: false,
stopAfterTimeout: false,
stopTimeout: 30,
},
disabledReason: "", // if automatic aspect ratio has been disabled, show reason
allowedMisaligned: 0.05, // top and bottom letterbox thickness can differ by this much.
// Any more and we don't adjust ar.
allowedArVariance: 0.0125,// amount by which old ar can differ from the new (1 = 100%)
timers: { // autodetection frequency
playing: 333, // while playing
playingReduced: 5000, // while playing at small sizes
paused: 3000, // while paused
error: 3000, // after error
minimumTimeout: 5,
tickrate: 10, // 1 tick every this many milliseconds
},
autoDisable: { // settings for automatically disabling the extension
onFirstChange: false, // disable once we have a stable aspect ratio
ifNotChanged: false, // disable if Ar hasn't changed for this long
ifNotChangedTimeout: 20000, // if user enables ifNotChangedTimeout, we default to 20s
ifSubtitles: false, // disable if subtitles are detected
},
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: 16,
height: 9,
},
sampleCanvas: { // size of image sample for detecting aspect ratio. Bigger size means more accurate results,
// at the expense of performance
width: 640,
height: 360,
},
},
blackLevels: { blackLevels: {
defaultBlack: 16, defaultBlack: 16,
blackTolerance: 4, blackTolerance: 4,

View File

@ -97,6 +97,8 @@ class Logger {
} }
static saveConfig(conf: LoggerConfig) { static saveConfig(conf: LoggerConfig) {
console.warn('LEGACY LOGGER IS STILL BEING CALLED FROM SOMEWHERE!', new Error().stack);
if (process.env.CHANNEL === 'dev') { if (process.env.CHANNEL === 'dev') {
console.info('Saving logger conf:', conf) console.info('Saving logger conf:', conf)
} }

View File

@ -152,20 +152,20 @@ export class Aard {
this.canvasSamples = { this.canvasSamples = {
top: generateSampleArray( top: generateSampleArray(
this.settings.active.arDetect.sampling.staticCols, this.settings.active.aard.sampling.staticCols,
this.settings.active.arDetect.canvasDimensions.sampleCanvas.width this.settings.active.aard.canvasDimensions.sampleCanvas.width
), ),
bottom: generateSampleArray( bottom: generateSampleArray(
this.settings.active.arDetect.sampling.staticCols, this.settings.active.aard.sampling.staticCols,
this.settings.active.arDetect.canvasDimensions.sampleCanvas.width this.settings.active.aard.canvasDimensions.sampleCanvas.width
), ),
left: generateSampleArray( left: generateSampleArray(
this.settings.active.arDetect.sampling.staticCols, this.settings.active.aard.sampling.staticCols,
this.settings.active.arDetect.canvasDimensions.sampleCanvas.height this.settings.active.aard.canvasDimensions.sampleCanvas.height
), ),
right: generateSampleArray( right: generateSampleArray(
this.settings.active.arDetect.sampling.staticCols, this.settings.active.aard.sampling.staticCols,
this.settings.active.arDetect.canvasDimensions.sampleCanvas.height this.settings.active.aard.canvasDimensions.sampleCanvas.height
) )
}; };
@ -188,14 +188,14 @@ export class Aard {
} }
private createCanvas(canvasId: string, canvasType?: 'webgl' | 'legacy') { private createCanvas(canvasId: string, canvasType?: 'webgl' | 'legacy') {
ROW_SIZE = this.settings.active.arDetect.canvasDimensions.sampleCanvas.width * PIXEL_SIZE; ROW_SIZE = this.settings.active.aard.canvasDimensions.sampleCanvas.width * PIXEL_SIZE;
if (canvasType) { if (canvasType) {
if (canvasType === this.settings.active.arDetect.aardType || this.settings.active.arDetect.aardType === 'auto') { if (canvasType === this.settings.active.aard.aardType || this.settings.active.aard.aardType === 'auto') {
if (canvasType === 'webgl') { if (canvasType === 'webgl') {
return new GlCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-gl'}); return new GlCanvas({...this.settings.active.aard.canvasDimensions.sampleCanvas, id: 'main-gl'});
} else if (canvasType === 'legacy') { } else if (canvasType === 'legacy') {
return new FallbackCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-legacy'}); return new FallbackCanvas({...this.settings.active.aard.canvasDimensions.sampleCanvas, id: 'main-legacy'});
} else { } else {
// TODO: throw error // TODO: throw error
} }
@ -205,21 +205,21 @@ export class Aard {
} }
if (['auto', 'webgl'].includes(this.settings.active.arDetect.aardType)) { if (['auto', 'webgl'].includes(this.settings.active.aard.aardType)) {
try { try {
return new GlCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-gl'}); return new GlCanvas({...this.settings.active.aard.canvasDimensions.sampleCanvas, id: 'main-gl'});
} catch (e) { } catch (e) {
if (this.settings.active.arDetect.aardType !== 'webgl') { if (this.settings.active.aard.aardType !== 'webgl') {
return new FallbackCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-legacy'}); return new FallbackCanvas({...this.settings.active.aard.canvasDimensions.sampleCanvas, id: 'main-legacy'});
} }
this.logger.error('createCanvas', 'could not create webgl canvas:', e); this.logger.error('createCanvas', 'could not create webgl canvas:', e);
this.eventBus.send('uw-config-broadcast', {type: 'aard-error', aardErrors: {webglError: true}}); this.eventBus.send('uw-config-broadcast', {type: 'aard-error', aardErrors: {webglError: true}});
throw e; throw e;
} }
} else if (this.settings.active.arDetect.aardType === 'legacy') { } else if (this.settings.active.aard.aardType === 'legacy') {
return new FallbackCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-legacy'}); return new FallbackCanvas({...this.settings.active.aard.canvasDimensions.sampleCanvas, id: 'main-legacy'});
} else { } else {
this.logger.error('createCanvas', 'invalid value in settings.arDetect.aardType:', this.settings.active.arDetect.aardType); this.logger.error('createCanvas', 'invalid value in settings.arDetect.aardType:', this.settings.active.aard.aardType);
this.eventBus.send('uw-config-broadcast', {type: 'aard-error', aardErrors: {invalidSettings: true}}); this.eventBus.send('uw-config-broadcast', {type: 'aard-error', aardErrors: {invalidSettings: true}});
throw 'AARD_INVALID_SETTINGS'; throw 'AARD_INVALID_SETTINGS';
} }
@ -231,7 +231,7 @@ export class Aard {
*/ */
private showDebugCanvas() { private showDebugCanvas() {
if (!this.canvasStore.debug) { if (!this.canvasStore.debug) {
this.canvasStore.debug = new GlDebugCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'uw-debug-gl'}); this.canvasStore.debug = new GlDebugCanvas({...this.settings.active.aard.canvasDimensions.sampleCanvas, id: 'uw-debug-gl'});
} }
this.canvasStore.debug.enableFx(); this.canvasStore.debug.enableFx();
if (!this.debugConfig.debugUi) { if (!this.debugConfig.debugUi) {
@ -290,8 +290,8 @@ export class Aard {
} }
// do full reset of test samples // do full reset of test samples
this.testResults = initAardTestResults(this.settings.active.arDetect); this.testResults = initAardTestResults(this.settings.active.aard);
this.verticalTestResults = initAardTestResults(this.settings.active.arDetect); this.verticalTestResults = initAardTestResults(this.settings.active.aard);
if (this.animationFrame) { if (this.animationFrame) {
window.cancelAnimationFrame(this.animationFrame); window.cancelAnimationFrame(this.animationFrame);
@ -301,8 +301,8 @@ export class Aard {
this.animationFrame = window.requestAnimationFrame( (ts: DOMHighResTimeStamp) => this.onAnimationFrame(ts)); this.animationFrame = window.requestAnimationFrame( (ts: DOMHighResTimeStamp) => this.onAnimationFrame(ts));
// set auto-disable timer if detection timeout is set // set auto-disable timer if detection timeout is set
if (this.settings.active.arDetect.autoDisable.ifNotChanged) { if (this.settings.active.aard.autoDisable.ifNotChanged) {
this.timers.autoDisableAt = Date.now() + this.settings.active.arDetect.autoDisable.ifNotChangedTimeout; this.timers.autoDisableAt = Date.now() + this.settings.active.aard.autoDisable.ifNotChangedTimeout;
} }
} }
@ -314,8 +314,8 @@ export class Aard {
this.stop(); this.stop();
if (options?.noCache) { if (options?.noCache) {
this.testResults = initAardTestResults(this.settings.active.arDetect); this.testResults = initAardTestResults(this.settings.active.aard);
this.verticalTestResults = initAardTestResults(this.settings.active.arDetect); this.verticalTestResults = initAardTestResults(this.settings.active.aard);
} }
this.main(); this.main();
@ -342,7 +342,7 @@ export class Aard {
// if video was paused & we know that we already checked that frame, // if video was paused & we know that we already checked that frame,
// we will not check it again. // we will not check it again.
const videoState = this.getVideoPlaybackState(); const videoState = this.getVideoPlaybackState();
const polling = this.settings.active.arDetect.polling; const polling = this.settings.active.aard.polling;
const now = Date.now(); const now = Date.now();
if (videoState !== VideoPlaybackState.Playing) { if (videoState !== VideoPlaybackState.Playing) {
@ -375,8 +375,8 @@ export class Aard {
return false; return false;
} }
this.timers.nextFrameCheckTime = now + this.settings.active.arDetect.timers.playing; this.timers.nextFrameCheckTime = now + this.settings.active.aard.timers.playing;
this.timers.reducedPollingNextCheckTime = now + this.settings.active.arDetect.timers.playingReduced; this.timers.reducedPollingNextCheckTime = now + this.settings.active.aard.timers.playingReduced;
return true; return true;
} }
@ -403,7 +403,7 @@ export class Aard {
* Main loop for scanning aspect ratio changes * Main loop for scanning aspect ratio changes
*/ */
private async main() { private async main() {
const arConf = this.settings.active.arDetect; const arConf = this.settings.active.aard;
try { try {
this.timer.next(); this.timer.next();
@ -745,8 +745,8 @@ export class Aard {
const lastPixelOffset = ROW_SIZE - PIXEL_SIZE; const lastPixelOffset = ROW_SIZE - PIXEL_SIZE;
const imageSize = ROW_SIZE * height; const imageSize = ROW_SIZE * height;
const xLimit = this.settings.active.arDetect.letterboxOrientationScan.letterboxLimit; const xLimit = this.settings.active.aard.letterboxOrientationScan.letterboxLimit;
const yLimit = this.settings.active.arDetect.letterboxOrientationScan.pillarboxLimit; const yLimit = this.settings.active.aard.letterboxOrientationScan.pillarboxLimit;
let letterbox = true, pillarbox = true; let letterbox = true, pillarbox = true;
let xCount = 0, yCount = 0; let xCount = 0, yCount = 0;
@ -844,7 +844,7 @@ export class Aard {
*/ */
private updateLetterboxEdgeCandidates(crossDimension: number, topCandidate: number, bottomCandidate: number) { private updateLetterboxEdgeCandidates(crossDimension: number, topCandidate: number, bottomCandidate: number) {
const bottomDistance = (crossDimension - bottomCandidate); const bottomDistance = (crossDimension - bottomCandidate);
const maxOffset = ~~(crossDimension * this.settings.active.arDetect.edgeDetection.maxLetterboxOffset); const maxOffset = ~~(crossDimension * this.settings.active.aard.edgeDetection.maxLetterboxOffset);
const diff = Math.abs(topCandidate - bottomDistance); const diff = Math.abs(topCandidate - bottomDistance);
const candidateAvg = ~~((topCandidate + bottomDistance) * 0.5); const candidateAvg = ~~((topCandidate + bottomDistance) * 0.5);
@ -869,7 +869,7 @@ export class Aard {
* @returns * @returns
*/ */
private subtitleScan(imageData: Uint8Array, width: number, height: number, skipAdvancedScan: boolean) { private subtitleScan(imageData: Uint8Array, width: number, height: number, skipAdvancedScan: boolean) {
const scanConf = this.settings.active.arDetect.subtitles; const scanConf = this.settings.active.aard.subtitles;
this.testResults.subtitleDetected = false; this.testResults.subtitleDetected = false;
@ -931,8 +931,8 @@ export class Aard {
) { ) {
results.uncertain = false; results.uncertain = false;
const scanConf = this.settings.active.arDetect.subtitles; const scanConf = this.settings.active.aard.subtitles;
const arConf = this.settings.active.arDetect; const arConf = this.settings.active.aard;
let letterCount, imageSegmentCount, potentialFadedLetterCount, potentialFadedLetterCountInvalidated, nonGradientPixelCount, letterSize, imageSize, imageSegmentSize, imageWeightedSize, segmentWeights, imageSegmentAlignment, imageSegmentAlignmentSamples, let letterCount, imageSegmentCount, potentialFadedLetterCount, potentialFadedLetterCountInvalidated, nonGradientPixelCount, letterSize, imageSize, imageSegmentSize, imageWeightedSize, segmentWeights, imageSegmentAlignment, imageSegmentAlignmentSamples,
isOnLetter, isOnImage, isBlank, isOnLetter, isOnImage, isBlank,
@ -940,7 +940,7 @@ export class Aard {
let rowStart, rowEnd, rowMid, rowGTA, rowGTB; // GT = gradient test let rowStart, rowEnd, rowMid, rowGTA, rowGTB; // GT = gradient test
let imageConfirmPass = false, subtitleConfirmPass = false; let imageConfirmPass = false, subtitleConfirmPass = false;
let innerIteration, outerIteration = 0; let outerIteration = 0;
const rowMargin = Math.floor(scanConf.scanMargin * ROW_SIZE); const rowMargin = Math.floor(scanConf.scanMargin * ROW_SIZE);
const imageThreshold = Math.floor((ROW_SIZE - (rowMargin * 2)) * arConf.edgeDetection.minValidImage * PIXEL_SIZE_FRACTION); const imageThreshold = Math.floor((ROW_SIZE - (rowMargin * 2)) * arConf.edgeDetection.minValidImage * PIXEL_SIZE_FRACTION);
@ -953,8 +953,11 @@ export class Aard {
(scanSpacing > 0 && searchRow < endRow) || (scanSpacing < 0 && searchRow > endRow); (scanSpacing > 0 && searchRow < endRow) || (scanSpacing < 0 && searchRow > endRow);
searchRow += scanSpacing searchRow += scanSpacing
) { ) {
innerIteration = 0; if (++outerIteration > height) {
outerIteration++; // console.warn('[ultrawidify|aard::subtitleScanRegionLinear] — scan got stuck in an infinite loop. This shouldn\'t happen.');
results.uncertain;
break outerLoop;
}
letterCount = 0; letterCount = 0;
potentialFadedLetterCount = 0; potentialFadedLetterCount = 0;
@ -1019,8 +1022,6 @@ export class Aard {
*/ */
while (rowStart < rowEnd) { while (rowStart < rowEnd) {
innerIteration++;
const r = imageData[rowStart], g = imageData[rowStart + 1], b = imageData[rowStart + 2]; const r = imageData[rowStart], g = imageData[rowStart + 1], b = imageData[rowStart + 2];
const on = r > scanConf.subtitleSubpixelThresholdOn const on = r > scanConf.subtitleSubpixelThresholdOn
@ -1316,7 +1317,7 @@ export class Aard {
const maxRatio = Math.max(ar, this.testResults.activeAspectRatio); const maxRatio = Math.max(ar, this.testResults.activeAspectRatio);
const diff = Math.abs(ar - this.testResults.activeAspectRatio); const diff = Math.abs(ar - this.testResults.activeAspectRatio);
if ((diff / maxRatio) > this.settings.active.arDetect.allowedArVariance || options?.forceReset) { if ((diff / maxRatio) > this.settings.active.aard.allowedArVariance || options?.forceReset) {
this.videoData.resizer.updateAr({ this.videoData.resizer.updateAr({
type: AspectRatioType.AutomaticUpdate, type: AspectRatioType.AutomaticUpdate,
ratio: ar, ratio: ar,
@ -1326,11 +1327,11 @@ export class Aard {
this.testResults.activeAspectRatio = ar; this.testResults.activeAspectRatio = ar;
if (!options?.uncertainDetection) { if (!options?.uncertainDetection) {
if (this.settings.active.arDetect.autoDisable.onFirstChange) { if (this.settings.active.aard.autoDisable.onFirstChange) {
this.status.autoDisabled = true; this.status.autoDisabled = true;
} }
if (this.settings.active.arDetect.autoDisable.ifNotChanged) { if (this.settings.active.aard.autoDisable.ifNotChanged) {
this.timers.autoDisableAt = Date.now() + this.settings.active.arDetect.autoDisable.ifNotChangedTimeout; this.timers.autoDisableAt = Date.now() + this.settings.active.aard.autoDisable.ifNotChangedTimeout;
} }
} }

View File

@ -1,4 +1,7 @@
import { Aard } from './Aard';
import { AardLegacy } from './AardLegacy';
import { AardPerformanceData } from './AardTimers'; import { AardPerformanceData } from './AardTimers';
import { FallbackCanvas } from './gl/FallbackCanvas';
export class AardDebugUi { export class AardDebugUi {
@ -76,37 +79,11 @@ export class AardDebugUi {
</div> </div>
<div id="uw-aard-debug-ui_body" style="display: flex; flex-direction: row; width: 100%"> <div id="uw-aard-debug-ui_body" style="display: flex; flex-direction: row; width: 100%; margin-top: 8rem;">
<div style=""> <div style="">
<div id="uw-aard-debug_aard-sample-canvas" style="min-width: 640px"></div> <div id="uw-aard-debug_aard-sample-canvas" style="min-width: 640px"></div>
<div style="background: black; color: #fff"; font-size: 24px;">AARD IN</div> <div style="background: black; color: #fff"; font-size: 24px;">AARD IN</div>
<div style="background: black; color: #ccc; padding: 1rem">
<div>
<span style="color: rgb(0.1, 0.1, 0.35)"></span>
Black level sample
</div>
<div>
<span style="color: rgb(0.3, 1.0, 0.6)"></span>
<span style="color: rgb(0.1, 0.5, 0.3)"></span>
Guard line (middle/corner) OK
</div>
<div>
<span style="color: rgb(1.0, 0.1, 0.1)"></span>
<span style="color: rgb(0.5, 0.0, 0.0)"></span>
Guard line (middle/corner) violation
</div>
<div>
Image line <span style="color: rgb(0.7, 0.7, 0.7)"></span> image, <span style="color: rgb(0.2, 0.2, 0.6)"></span> no image
</div>
<div>
Edge scan <span style="color: rgb(0.1, 0.1, 0.4)"></span> probe, <span style="color: rgb(0.4, 0.4, 1.0)"></span> hit
</div>
<div>
Slope test <span style="color: rgb(0.4, 0.4, 1.0)"></span> ok, <span style="color: rgb(1.0, 0.0, 0.0)"></span> fail
</div>
</div>
<div style="pointer-events: all"> <div style="pointer-events: all">
<button id="uw-aard-debug-ui_enable-stop-on-change" style="">Pause video on aspect ratio change</button> <button id="uw-aard-debug-ui_enable-stop-on-change" style="">Pause video on aspect ratio change</button>
<button id="uw-aard-debug-ui_disable-stop-on-change" style="display: none">Stop pausing video on aspect ratio change</button> <button id="uw-aard-debug-ui_disable-stop-on-change" style="display: none">Stop pausing video on aspect ratio change</button>
@ -125,10 +102,9 @@ export class AardDebugUi {
<pre id="uw-aard-results"></pre> <pre id="uw-aard-results"></pre>
</div> </div>
</div> </div>
<div style="width: 1920px"> <div style="width: 1920px; border: 2px dotted #142; margin-right:2rem;">
<div id="uw-aard-debug_aard-output" style="zoom: 3; image-rendering: pixelated;"></div> <div id="uw-aard-debug_aard-output" style="zoom: 3; image-rendering: pixelated;"></div>
<div style="background: black; color: #fff; font-size: 24px;">AARD RESULT</div> <div style="background: black; color: #fff; font-size: 24px;">AARD RESULT</div>
</div> </div>
</div> </div>
</div> </div>
@ -194,9 +170,19 @@ export class AardDebugUi {
const popupContent = ` const popupContent = `
<h2 style="color: #fa6; margin-bottom: 1rem">Detailed performance analysis:</h2> <h2 style="color: #fa6; margin-bottom: 1rem">Detailed performance analysis:</h2>
<div>
<span style="color: #fff">About:</span><br/>
Aard version: <span style="color: #fa6">${this.aard instanceof AardLegacy ? 'legacy' : this.aard instanceof Aard ? 'experimental' : 'unknown'}</span> <small>(instanceof AardLegacy? ${this.aard instanceof AardLegacy}, Aard? ${this.aard instanceof Aard})</small><br/>
canvas type: <span style="color: #fa6">${!this.aard.canvasStore.main ? '<canvas not initialized>' : this.aard.canvasStore.main instanceof FallbackCanvas ? '2dCanvas' : 'webgl'}</span> |
legacy default: <span style="color: #fa6">${this.aard.settings.active.aardLegacy.aardType}</span>,
experimental default: <span style="color: #fa6">${this.aard.settings.active.aard.aardType}</span>
</div>
<br/>
<div style="width: 100%; display: flex; flex-direction: column"> <div style="width: 100%; display: flex; flex-direction: column">
<div style="margin-bottom: 1rem;"> <div style="margin-bottom: 1rem;">
${this.generateRawTimes(this.aard.timer.average)} ${this.generateRawTimes(this.aard.timer.current)}
</div> </div>
<div style="color: #fff">Stage times (not cumulative):</div> <div style="color: #fff">Stage times (not cumulative):</div>
<div style="display: flex; flex-direction: row; width: 100%; height: 150px"> <div style="display: flex; flex-direction: row; width: 100%; height: 150px">
@ -214,12 +200,14 @@ export class AardDebugUi {
</div> </div>
<div style="margin-bottom: 1rem;"> <div style="margin-bottom: 1rem;">
${this.generateRawTimes(this.aard.timer.average)} ${this.generateRawTimes(this.aard.timer.lastChange)}
</div> </div>
<div style="display: flex; flex-direction: row; width: 100%; height: 150px"> <div style="display: flex; flex-direction: row; width: 100%; height: 150px">
<div style="width: 160px; text-align: right; padding-right: 4px;">Last change:</div> <div style="width: 160px; text-align: right; padding-right: 4px;">Last change:</div>
<div style="flex-grow: 1;">${this.generateMiniGraphBar(this.aard.timer.lastChange, true)}</div> <div style="flex-grow: 1;">${this.generateMiniGraphBar(this.aard.timer.lastChange, true)}</div>
</div> </div>
<!-- <pre>${JSON.stringify({current: this.aard.timer.current, average: this.aard.timer.average, lastChange: this.aard.timer.lastChange}, null, 2)}</pre> -->
</div> </div>
` `
@ -298,23 +286,23 @@ export class AardDebugUi {
total += fastBlackLevel; total += fastBlackLevel;
const guardLineStart = fastBlackLevelStart + fastBlackLevel; const guardLineStart = fastBlackLevelStart + fastBlackLevel;
const guardLine = Math.max(perf.guardLine - total, 0); const guardLine = (perf.guardLine !== undefined && perf.guardLine !== -1) ? Math.max(perf.guardLine - total, 0) : 0;
total += guardLine; total += guardLine;
const edgeScanStart = guardLineStart + guardLine; const edgeScanStart = guardLineStart + guardLine;
const edgeScan = Math.max(perf.edgeScan - total, 0); const edgeScan = (perf.edgeScan !== undefined && perf.edgeScan !== -1) ? Math.max(perf.edgeScan - total, 0) : 0;
total += edgeScan; total += edgeScan;
const gradientStart = edgeScanStart + edgeScan; const gradientStart = edgeScanStart + edgeScan;
const gradient = Math.max(perf.gradient - total, 0); const gradient = (perf.gradient !== undefined && perf.gradient !== -1) ? Math.max(perf.gradient - total, 0) : 0;
total += gradient; total += gradient;
const scanResultsStart = gradientStart + gradient; const scanResultsStart = gradientStart + gradient;
const scanResults = Math.max(perf.scanResults - total, 0); const scanResults = (perf.scanResults !== undefined && perf.scanResults !== -1) ? Math.max(perf.scanResults - total, 0) : 0;
total += scanResults; total += scanResults;
const subtitleScanStart = scanResultsStart + scanResults; const subtitleScanStart = scanResultsStart + scanResults;
const subtitleScan = Math.max(perf.scanResults - total, 0); const subtitleScan = Math.max(perf.subtitleScan - total, 0);
total += subtitleScan; total += subtitleScan;
return ` return `
@ -344,12 +332,10 @@ export class AardDebugUi {
<div style="position: absolute; top: ${detailed ? '74px' : '2px'}; left: ${scanResultsStart}%; min-width: 1px; width: ${scanResults}%; background: #80f; height: 12px;"></div> <div style="position: absolute; top: ${detailed ? '74px' : '2px'}; left: ${scanResultsStart}%; min-width: 1px; width: ${scanResults}%; background: #80f; height: 12px;"></div>
${this.getBarLabel(scanResults, scanResultsStart, 74, `scan results processing: ${scanResults.toFixed(2)} ms`, detailed)} ${this.getBarLabel(scanResults, scanResultsStart, 74, `scan results processing: ${scanResults.toFixed(2)} ms`, detailed)}
${this.getBarLabel(0, scanResults + scanResultsStart, 88, `total: ${total.toFixed(2)} ms`, detailed, 'color: #fff;')} <div style="position: absolute; top: ${detailed ? '86px' : '2px'}; left: ${subtitleScanStart}%; min-width: 1px; width: ${subtitleScan}%; background: rgba(234, 204, 84, 1); height: 12px;"></div>
${this.getBarLabel(subtitleScan, subtitleScanStart, 86, `subtitle scan: ${subtitleScan.toFixed(2)} ms`, detailed)}
<div style="position: absolute; top: ${detailed ? '74px' : '2px'}; left: ${subtitleScanStart}%; min-width: 1px; width: ${subtitleScan}%; background: rgba(234, 204, 84, 1); height: 12px;"></div> ${this.getBarLabel(0, subtitleScan + subtitleScanStart, 98, `total: ${total.toFixed(2)} ms`, detailed, 'color: #fff;')}
${this.getBarLabel(scanResults, scanResultsStart, 74, `scan results processing: ${subtitleScan.toFixed(2)} ms`, detailed)}
${this.getBarLabel(0, scanResults + scanResultsStart, 88, `total: ${total.toFixed(2)} ms`, detailed, 'color: #fff;')}
<!-- 60/30 fps markers --> <!-- 60/30 fps markers -->
<div style="position: absolute; top: ${detailed ? '-12px' : '0'}; left: 16.666%; width: 1px; border-left: 1px dashed #4f9; height: ${detailed ? '112px' : '12px'}; padding-left: 2px; background-color: rgba(0,0,0,0.5); z-index: ${detailed ? '5' : '2'}000;">60fps</div> <div style="position: absolute; top: ${detailed ? '-12px' : '0'}; left: 16.666%; width: 1px; border-left: 1px dashed #4f9; height: ${detailed ? '112px' : '12px'}; padding-left: 2px; background-color: rgba(0,0,0,0.5); z-index: ${detailed ? '5' : '2'}000;">60fps</div>
@ -359,7 +345,7 @@ export class AardDebugUi {
} }
_lastAr: undefined; _lastAr: undefined;
updateTestResults(testResults) { updateTestResults(testResults, timers) {
this.updatePerformanceResults(); this.updatePerformanceResults();
if (testResults.aspectRatioUpdated && this.pauseOnArCheck) { if (testResults.aspectRatioUpdated && this.pauseOnArCheck) {
@ -378,52 +364,13 @@ export class AardDebugUi {
Active: ${ar}, changed since last check? ${testResults.aspectRatioUpdated} letterbox width: ${testResults.letterboxWidth} offset ${testResults.letterboxOffset}<br/> Active: ${ar}, changed since last check? ${testResults.aspectRatioUpdated} letterbox width: ${testResults.letterboxWidth} offset ${testResults.letterboxOffset}<br/>
<sup>(last: ${this._lastAr})</sup> <sup>(last: ${this._lastAr})</sup>
Paused until? ${Date.now() < timers.pauseUntil ? ((timers.pauseUntil - Date.now()) / 1000) + 's' : 'not paused'};
<sup>now: ${Date.now()}ms; until:${timers.pauseUntil}ms; diff: ${(+timers.pauseUntil - Date.now())} </sup>
image in black level probe (aka "not letterbox"): ${testResults.notLetterbox} image in black level probe (aka "not letterbox"): ${testResults.notLetterbox}
`; `;
this._lastAr = ar; this._lastAr = ar;
if (testResults.notLetterbox) {
resultsDiv.innerHTML = out;
return;
}
out = `${out}
-- UNCERTAIN FLAGS
AR: ${testResults.aspectRatioUncertain} (reason: ${testResults.aspectRatioUncertainReason ?? 'n/a'}); top row: ${testResults.topRowUncertain}; bottom row: ${testResults.bottomRowUncertain}${
testResults.aspectRatioInvalid ? `\nINVALID_AR (reason: ${testResults.aspectRatioInvalidReason ?? 'n/a'})` : ''}
-- GUARD & IMAGE LINE
bottom guard: ${testResults.guardLine.bottom} image: ${testResults.guardLine.invalidated ? 'n/a' : testResults.imageLine.bottom}
top guard: ${testResults.guardLine.top} image: ${testResults.guardLine.invalidated ? 'n/a' : testResults.imageLine.top}
guard line ${testResults.guardLine.invalidated ? 'INVALIDATED' : 'valid'} image line ${testResults.guardLine.invalidated ? '<skipped test>' : testResults.imageLine.invalidated ? 'INVALIDATED' : 'valid'}
corner invalidations (invalid pixels -> verdict)
LEFT CENTER RIGHT
bottom: ${testResults.guardLine.cornerPixelsViolated[0]} ${testResults.guardLine.cornerViolated[0] ? '❌' : '◽'} ${testResults.guardLine.cornerPixelsViolated[1]} ${testResults.guardLine.cornerViolated[1] ? '❌' : '◽'}
top: ${testResults.guardLine.cornerPixelsViolated[2]} ${testResults.guardLine.cornerViolated[2] ? '❌' : '◽'} ${testResults.guardLine.cornerPixelsViolated[3]} ${testResults.guardLine.cornerViolated[3] ? '❌' : '◽'}
-- AR SCAN ${testResults.lastStage < 1 ? `
DID NOT RUN THIS FRAME` : `
LEFT CENTER RIGHT CANDIDATE
BOTTOM
distance: ${testResults.aspectRatioCheck.bottomRows[0]} ${testResults.aspectRatioCheck.bottomRows[1]} ${testResults.aspectRatioCheck.bottomRows[2]} ${testResults.aspectRatioCheck.bottomCandidate}
quality: ${testResults.aspectRatioCheck.bottomQuality[0]} ${testResults.aspectRatioCheck.bottomQuality[1]} ${testResults.aspectRatioCheck.bottomQuality[2]} ${testResults.aspectRatioCheck.bottomCandidateQuality}
TOP
distance: ${testResults.aspectRatioCheck.topRows[0]} ${testResults.aspectRatioCheck.topRows[1]} ${testResults.aspectRatioCheck.topRows[2]} ${testResults.aspectRatioCheck.topCandidate}
quality: ${testResults.aspectRatioCheck.topQuality[0]} ${testResults.aspectRatioCheck.topQuality[1]} ${testResults.aspectRatioCheck.topQuality[2]} ${testResults.aspectRatioCheck.topCandidateQuality}
Diff matrix:
R-L C-R C-L
bottom: ${testResults.aspectRatioCheck.bottomRowsDifferenceMatrix[0]} ${testResults.aspectRatioCheck.bottomRowsDifferenceMatrix[1]} ${testResults.aspectRatioCheck.bottomRowsDifferenceMatrix[2]}
top: ${testResults.aspectRatioCheck.topRowsDifferenceMatrix[0]} ${testResults.aspectRatioCheck.topRowsDifferenceMatrix[1]} ${testResults.aspectRatioCheck.topRowsDifferenceMatrix[2]}
`}
`;
resultsDiv.innerHTML = out; resultsDiv.innerHTML = out;
} }

File diff suppressed because it is too large Load Diff

View File

@ -4,11 +4,13 @@ export interface AardPerformanceData {
getImage: number; getImage: number;
fastBlackLevel: number; fastBlackLevel: number;
guardLine: number; // actually times both guard line and image line checks
edgeScan: number; // includes validation step // optional ones are only available in legacy aard
gradient: number; guardLine?: number; // actually times both guard line and image line checks
edgeScan?: number; // includes validation step
gradient?: number;
scanResults?: number;
subtitleScan: number; subtitleScan: number;
scanResults: number;
} }
@ -82,36 +84,52 @@ export class AardTimer {
getAverage() { getAverage() {
for (let i = 0; i < this.aardPerformanceDataBuffer.length; i++) { for (let i = 0; i < this.aardPerformanceDataBuffer.length; i++) {
const sample = this.aardPerformanceDataBuffer[i]; const sample = this.aardPerformanceDataBuffer[i];
if (sample.draw !== -1) { if (sample.draw !== -1) {
this.average.draw += sample.draw; this.average.draw += sample.draw;
} }
if (sample.getImage !== -1) { if (sample.getImage !== -1) {
this.average.getImage += sample.getImage; this.average.getImage += sample.getImage;
} }
if (sample.fastBlackLevel !== -1) { if (sample.fastBlackLevel !== -1 && this.average.fastBlackLevel !== null) {
this.average.fastBlackLevel += sample.fastBlackLevel; this.average.fastBlackLevel += sample.fastBlackLevel;
} else {
this.average.fastBlackLevel = null;
} }
if (sample.guardLine !== -1) { if (sample.guardLine !== -1 && this.average.guardLine !== null) {
this.average.guardLine += sample.guardLine; this.average.guardLine += sample.guardLine;
} else {
this.average.guardLine = null;
} }
if (sample.edgeScan !== -1) { if (sample.edgeScan !== -1 && this.average.edgeScan !== null) {
this.average.edgeScan += sample.edgeScan; this.average.edgeScan += sample.edgeScan;
} else {
this.average.edgeScan = null;
} }
if (sample.gradient !== -1) { if (sample.gradient !== -1 && this.average.gradient !== null) {
this.average.gradient += sample.gradient; this.average.gradient += sample.gradient;
} else {
this.average.edgeScan = null;
} }
if (sample.scanResults !== -1) { if (sample.scanResults !== -1 && this.average.scanResults !== null) {
this.average.scanResults += sample.scanResults; this.average.scanResults += sample.scanResults;
} else {
this.average.scanResults = null;
}
if (sample.subtitleScan !== -1 && this.average.subtitleScan !== null) {
this.average.subtitleScan += sample.subtitleScan;
} else {
this.average.subtitleScan = null;
} }
} }
this.average.draw /= this.aardPerformanceDataBuffer.length; this.average.draw /= this.aardPerformanceDataBuffer.length;
this.average.getImage /= this.aardPerformanceDataBuffer.length; this.average.getImage /= this.aardPerformanceDataBuffer.length;
this.average.fastBlackLevel /= this.aardPerformanceDataBuffer.length; this.average.fastBlackLevel /= this.aardPerformanceDataBuffer.length;
this.average.guardLine /= this.aardPerformanceDataBuffer.length; this.average.guardLine = this.average.guardLine === null ? -1 : (this.average.guardLine / this.aardPerformanceDataBuffer.length);
this.average.edgeScan /= this.aardPerformanceDataBuffer.length; this.average.edgeScan = this.average.edgeScan === null ? -1 : (this.average.guardLine / this.aardPerformanceDataBuffer.length);
this.average.gradient /= this.aardPerformanceDataBuffer.length; this.average.gradient = this.average.gradient === null ? -1 : (this.average.guardLine / this.aardPerformanceDataBuffer.length);
this.average.scanResults /= this.aardPerformanceDataBuffer.length; this.average.scanResults = this.average.scanResults === null ? -1 : (this.average.guardLine / this.aardPerformanceDataBuffer.length);
this.average.subtitleScan /= this.aardPerformanceDataBuffer.length; this.average.subtitleScan = this.average.subtitleScan === null ? -1 : (this.average.subtitleScan /this.aardPerformanceDataBuffer.length);
} }
} }

View File

@ -0,0 +1,189 @@
import { AardLegacySettings, AardSettings } from '../../../../common/interfaces/SettingsInterface'
import { AardUncertainReason } from '../enums/aard-letterbox-uncertain-reason.enum'
import { LetterboxOrientation } from '../enums/letterbox-orientation.enum'
export interface AardTestResult_SubtitleRegion {
firstBlank: number,
lastBlank: number,
firstSubtitle: number,
lastSubtitle: number,
firstImage: number,
lastImage: number,
uncertain: boolean,
}
export interface AardLegacyTestResults {
isFinished: boolean,
lastStage: number,
letterboxOrientation: LetterboxOrientation,
lastValidLetterboxOrientation: LetterboxOrientation,
subtitleDetected: boolean,
blackLevel: number, // is cumulative
blackThreshold: number, // is cumulative
guardLine: {
top: number, // is cumulative
bottom: number, // is cumulative
invalidated: boolean,
cornerViolated: [boolean, boolean, boolean, boolean],
cornerPixelsViolated: [0,0,0,0],
front?: number,
back?: number,
},
imageLine: {
top: number, // is cumulative
bottom: number, // is cumulative
invalidated: boolean
}
aspectRatioCheck: {
topRows: [number, number, number],
topQuality: [number, number, number],
bottomRows: [number, number, number],
bottomQuality: [number, number, number],
topCandidate: number,
topCandidateQuality: number,
bottomCandidate: number,
bottomCandidateDistance: number,
bottomCandidateQuality: number,
topRowsDifferenceMatrix: [number, number, number],
bottomRowsDifferenceMatrix: [number, number, number],
frontCandidate: number,
backCandidate: number,
},
aspectRatioUncertain: boolean,
topRowUncertain: boolean,
bottomRowUncertain: boolean,
aspectRatioUpdated: boolean,
activeAspectRatio: number, // is cumulative
letterboxSize: number,
letterboxOffset: number,
logoDetected: [boolean, boolean, boolean, boolean],
aspectRatioInvalid: boolean,
subtitleScan: {
top: number,
bottom: number,
regions: {
top: AardTestResult_SubtitleRegion,
bottom: AardTestResult_SubtitleRegion
}
},
notLetterbox: boolean,
aspectRatioUncertainEdges: number,
aspectRatioUncertainReason?: AardUncertainReason,
aspectRatioInvalidReason?: string,
}
export function initAardTestResults(settings: AardLegacySettings): AardLegacyTestResults {
return {
isFinished: true,
lastStage: 0,
letterboxOrientation: LetterboxOrientation.NotKnown,
lastValidLetterboxOrientation: LetterboxOrientation.NotKnown,
blackLevel: settings.blackLevels.defaultBlack,
blackThreshold: 16,
guardLine: {
top: -1,
bottom: -1,
invalidated: false,
cornerViolated: [false, false, false, false],
cornerPixelsViolated: [0,0,0,0],
front: -1,
back: -1,
},
imageLine: {
top: -1,
bottom: -1,
invalidated: false,
},
aspectRatioCheck: {
topRows: [-1, -1, -1],
topQuality: [0, 0, 0],
bottomRows: [-1, -1, -1],
bottomQuality: [0, 0, 0],
topCandidate: 0,
topCandidateQuality: 0,
bottomCandidate: 0,
bottomCandidateDistance: 0,
bottomCandidateQuality: 0,
topRowsDifferenceMatrix: [0, 0, 0],
bottomRowsDifferenceMatrix: [0, 0, 0],
frontCandidate: 0,
backCandidate: 0,
},
aspectRatioUncertain: false,
aspectRatioUncertainEdges: 0,
topRowUncertain: false,
bottomRowUncertain: false,
subtitleDetected: false,
subtitleScan: {
top: -1,
bottom: -1,
regions: {
top: {
firstBlank: -1,
lastBlank: -1,
firstSubtitle: -1,
lastSubtitle: -1,
firstImage: -1,
lastImage: -1,
uncertain: false,
},
bottom: {
firstBlank: -1,
lastBlank: -1,
firstSubtitle: -1,
lastSubtitle: -1,
firstImage: -1,
lastImage: -1,
uncertain: false,
}
}
},
aspectRatioUpdated: false,
activeAspectRatio: 0,
letterboxSize: 0,
letterboxOffset: 0,
logoDetected: [false, false, false, false],
aspectRatioInvalid: false,
notLetterbox: false,
}
}
export function resetGuardLine(results: AardLegacyTestResults) {
results.guardLine.front = -1;
results.guardLine.back = -1;
}
export function resetAardTestResults(results: AardLegacyTestResults): void {
results.isFinished = false;
results.lastStage = 0;
results.aspectRatioUpdated = false;
results.aspectRatioUncertainReason = null;
results.aspectRatioInvalid = false;
results.letterboxOrientation = LetterboxOrientation.NotKnown;
}
export function resetSubtitleScanResults(results: AardLegacyTestResults): void {
results.subtitleScan.top = -1;
results.subtitleScan.bottom = -1;
results.subtitleScan.regions.top.firstBlank = -1;
results.subtitleScan.regions.top.lastBlank = -1;
results.subtitleScan.regions.top.firstSubtitle = -1;
results.subtitleScan.regions.top.lastSubtitle = -1;
results.subtitleScan.regions.top.firstImage = -1;
results.subtitleScan.regions.top.lastImage = -1;
results.subtitleScan.regions.bottom.firstBlank = -1;
results.subtitleScan.regions.bottom.lastBlank = -1;
results.subtitleScan.regions.bottom.firstSubtitle = -1;
results.subtitleScan.regions.bottom.lastSubtitle = -1;
results.subtitleScan.regions.bottom.firstImage = -1;
results.subtitleScan.regions.bottom.lastImage = -1;
}

View File

@ -113,7 +113,7 @@ export class LogAggregator {
private storageChangeListener(changes, area) { private storageChangeListener(changes, area) {
if (!changes[STORAGE_LOG_SETTINGS_KEY]) { if (!changes[STORAGE_LOG_SETTINGS_KEY]) {
console.info('We dont have any logging settings, not processing frther', changes); // console.info('We dont have any logging settings, not processing frther', changes);
return; return;
} }

View File

@ -213,6 +213,8 @@ class Settings {
} }
); );
// this.active.newFeatureTracker = {};
// apply all remaining patches // apply all remaining patches
this.logger?.info('applySettingsPatches', `There are ${ExtensionConfPatch.length - index} settings patches to apply`); this.logger?.info('applySettingsPatches', `There are ${ExtensionConfPatch.length - index} settings patches to apply`);
@ -288,19 +290,6 @@ class Settings {
return this.active; return this.active;
} }
// This means extension update happened.
// btw fun fact — we can do version rollbacks, which might come in handy while testing
this.active.version = this.version;
// if extension has been updated, update existing settings with any options added in the
// new version. In addition to that, we remove old keys that are no longer used.
const patched = ObjectCopy.addNew(settings, this.default);
this.logger?.info('init',"Results from ObjectCopy.addNew()?", patched, "\n\nSettings from storage", settings, "\ndefault?", this.default);
if (patched) {
this.active = patched;
}
// in case settings in previous version contained a fucky wucky, we overwrite existing settings with a patch // in case settings in previous version contained a fucky wucky, we overwrite existing settings with a patch
this.applySettingsPatches(oldVersion); this.applySettingsPatches(oldVersion);

View File

@ -11,6 +11,7 @@ import PageInfo from './PageInfo';
import { RunLevel } from '../../enum/run-level.enum'; import { RunLevel } from '../../enum/run-level.enum';
import { ExtensionEnvironment } from '../../../common/interfaces/SettingsInterface'; import { ExtensionEnvironment } from '../../../common/interfaces/SettingsInterface';
import { ComponentLogger } from '../logging/ComponentLogger'; import { ComponentLogger } from '../logging/ComponentLogger';
import { collectionHas, equalish } from '../../util/comparators';
if (process.env.CHANNEL !== 'stable'){ if (process.env.CHANNEL !== 'stable'){
console.info("Loading: PlayerData.js"); console.info("Loading: PlayerData.js");
@ -96,6 +97,7 @@ class PlayerData {
isTooSmall: boolean = true; isTooSmall: boolean = true;
//#endregion //#endregion
//#region misc stuff //#region misc stuff
extensionMode: any; extensionMode: any;
dimensions: PlayerDimensions; dimensions: PlayerDimensions;
@ -144,11 +146,12 @@ class PlayerData {
private dimensionChangeListener = { private dimensionChangeListener = {
that: this, that: this,
handleEvent: function(event: Event) { handleEvent: function(event: Event) {
this.that.trackEnvironmentChanges(event); this.that.requestTick();
this.that.trackDimensionChanges()
} }
} }
private fallbackDimensionChangeInterval: any;
/** /**
* Gets player aspect ratio. If in full screen, it returns screen aspect ratio unless settings say otherwise. * Gets player aspect ratio. If in full screen, it returns screen aspect ratio unless settings say otherwise.
*/ */
@ -158,8 +161,7 @@ class PlayerData {
return window.innerWidth / window.innerHeight; return window.innerWidth / window.innerHeight;
} }
if (!this.dimensions) { if (!this.dimensions) {
this.trackDimensionChanges(); return (this.element?.scrollWidth ?? this.videoElement.videoWidth) / (this.element?.scrollHeight ?? this.videoElement.videoHeight);
this.trackEnvironmentChanges();
} }
return this.dimensions.width / this.dimensions.height; return this.dimensions.width / this.dimensions.height;
@ -225,7 +227,6 @@ class PlayerData {
} }
this.startChangeDetection(); this.startChangeDetection();
document.addEventListener('fullscreenchange', this.dimensionChangeListener); document.addEventListener('fullscreenchange', this.dimensionChangeListener);
// we want to reload on storage changes // we want to reload on storage changes
@ -236,35 +237,6 @@ class PlayerData {
} }
} }
/**
* Checks if player dimensions are too small for autodetection to run.
* Full screen is never too small, even if it's
* @param playerDimensions
* @returns
*/
private checkIfTooSmall(playerDimensions?: PlayerDimensions) {
if (playerDimensions) {
return !playerDimensions.fullscreen && (playerDimensions.width < 1208 || playerDimensions.height < 720);
} else {
return !document.fullscreenElement && (this.element.clientWidth < 1208 || this.element.clientHeight < 720);
}
}
private reloadPlayerDataConfig(siteConfUpdate) {
// this.siteSettings = siteConfUpdate;
this.updatePlayer();
this.periodicallyRefreshPlayerElement = false;
try {
this.periodicallyRefreshPlayerElement = this.siteSettings.data.currentDOMConfig.periodicallyRefreshPlayerElement;
} catch (e) {
// no biggie — that means we don't have any special settings for this site.
}
// because this is often caused by the UI
this.handlePlayerTreeRequest();
}
/** /**
* Initializes event bus * Initializes event bus
*/ */
@ -276,18 +248,27 @@ class PlayerData {
} }
} }
/** private initFallbackDimensionMonitor() {
* Completely stops everything the extension is doing this.stopFallbackDimensionMonitor();
*/ this.fallbackDimensionChangeInterval = setInterval( () => {
destroy() { this.processTick();
document.removeEventListener('fullscreenchange', this.dimensionChangeListener); }, 2000);
this.stopChangeDetection();
this.ui?.destroy();
this.notificationService?.destroy();
} }
//#endregion
deferredUiInitialization(playerDimensions) { /**
* Stops manually scanning for dimension changes
*/
stopFallbackDimensionMonitor() {
clearImmediate(this.fallbackDimensionChangeInterval);
this.fallbackDimensionChangeInterval = undefined;
}
/**
* Initialized player UI at a later time.
* @param playerDimensions
* @returns
*/
deferredUiInitialization(playerDimensions = this.dimensions) {
if (this.ui || this.siteSettings.data.enableUI.fullscreen === ExtensionMode.Disabled) { if (this.ui || this.siteSettings.data.enableUI.fullscreen === ExtensionMode.Disabled) {
return; return;
} }
@ -325,6 +306,44 @@ class PlayerData {
} }
} }
/**
* Completely stops everything the extension is doing
*/
destroy() {
document.removeEventListener('fullscreenchange', this.dimensionChangeListener);
this.stopChangeDetection();
this.ui?.destroy();
this.notificationService?.destroy();
}
//#endregion
//#region utils
/**
* Checks if player dimensions are too small for autodetection to run.
* Full screen is never too small, even if it's
* @param playerDimensions
* @returns
*/
private checkIfTooSmall() {
return !document.fullscreenElement && (this.element.clientWidth < 1208 || this.element.clientHeight < 720);
}
private reloadPlayerDataConfig(siteConfUpdate) {
// this.siteSettings = siteConfUpdate;
this.updatePlayer();
this.periodicallyRefreshPlayerElement = false;
try {
this.periodicallyRefreshPlayerElement = this.siteSettings.data.currentDOMConfig.periodicallyRefreshPlayerElement;
} catch (e) {
// no biggie — that means we don't have any special settings for this site.
}
// because this is often caused by the UI
this.handlePlayerTreeRequest();
}
/** /**
* Sets extension runLevel and sets or unsets appropriate css classes as necessary * Sets extension runLevel and sets or unsets appropriate css classes as necessary
* @param runLevel * @param runLevel
@ -356,161 +375,12 @@ class PlayerData {
this.runLevel = runLevel; this.runLevel = runLevel;
} }
/** //#endregion
* Detects whether player element is in theater mode or not.
* If theater mode changed, emits event.
* @returns whether player is in theater mode
*/
private detectTheaterMode() {
const oldTheaterMode = this.isTheaterMode;
const newTheaterMode = this.equalish(window.innerWidth, this.element.offsetWidth, 32);
this.isTheaterMode = newTheaterMode;
// theater mode changed
if (oldTheaterMode !== newTheaterMode) {
if (newTheaterMode) {
this.eventBus.send('player-theater-enter', {});
} else {
this.eventBus.send('player-theater-exit', {});
}
}
return newTheaterMode;
}
trackEnvironmentChanges() {
if (this.environment !== this.lastEnvironment) {
this.lastEnvironment = this.environment;
this.eventBus.send('uw-environment-change', {newEnvironment: this.environment});
}
}
/**
*
*/
trackDimensionChanges() {
if (this._isTrackDimensionChangesActive) {
// this shouldn't really get called, _ever_ ... but sometimes it happens
console.warn('[PlayerData::trackDimensionChanges] trackDimensionChanges is already active!');
return;
}
this._isTrackDimensionChangesActive = true;
try {
// get player dimensions _once_
let currentPlayerDimensions;
let fsChanged = this.isFullscreen !== !!document.fullscreenElement;
this.isFullscreen = !!document.fullscreenElement;
if (this.isFullscreen) {
currentPlayerDimensions = {
width: window.innerWidth,
height: window.innerHeight,
};
} else {
currentPlayerDimensions = {
width: this.element.offsetWidth,
height: this.element.offsetHeight
};
this.detectTheaterMode();
}
// defer creating UI
this.deferredUiInitialization(currentPlayerDimensions);
// if dimensions of the player box are the same as the last known
// dimensions, we don't have to do anything ... in theory. In practice,
// sometimes restore-ar doesn't appear to register the first time, and
// this function doesn't really run often enough to warrant finding a
// real, optimized fix.
if (
this.dimensions?.width == currentPlayerDimensions.width
&& this.dimensions?.height == currentPlayerDimensions.height
) {
this.eventBus.send('restore-ar', null);
this.eventBus.send('delayed-restore-ar', {delay: 500});
this.dimensions = currentPlayerDimensions;
this._isTrackDimensionChangesActive = false;
return;
}
// in every other case, we need to check if the player is still
// big enough to warrant our extension running.
this.handleSizeConstraints(currentPlayerDimensions);
// this.handleDimensionChanges(currentPlayerDimensions, this.dimensions);
// Save current dimensions to avoid triggering this function pointlessly
this.dimensions = currentPlayerDimensions;
if (fsChanged) {
this.updatePlayer();
}
} catch (e) {
}
this._isTrackDimensionChangesActive = false;
}
/**
* Checks if extension is allowed to run in current environment.
* @param currentPlayerDimensions
*/
private handleSizeConstraints(currentPlayerDimensions: PlayerDimensions) {
// Check if extension is allowed to run in current combination of theater + full screen
const canEnable = this.siteSettings.isEnabledForEnvironment(this.isFullscreen, this.isTheaterMode) === ExtensionMode.Enabled;
if (this.runLevel === RunLevel.Off && canEnable) {
this.eventBus.send('restore-ar', null);
// must be called after
this.handleDimensionChanges(currentPlayerDimensions, this.dimensions);
} else if (!canEnable && this.runLevel !== RunLevel.Off) {
// must be called before
this.handleDimensionChanges(currentPlayerDimensions, this.dimensions);
this.setRunLevel(RunLevel.Off);
}
}
private handleDimensionChanges(newDimensions: PlayerDimensions, oldDimensions: PlayerDimensions) {
if (this.runLevel === RunLevel.Off ) {
this.logger.info('handleDimensionChanges', "player size changed, but PlayerDetect is in disabled state. The player element is probably too small.");
return;
}
// this 'if' is just here for debugging — real code starts later. It's safe to collapse and
// ignore the contents of this if (unless we need to change how logging works)
this.logger.info('handleDimensionChanges', "player size potentially changed.\n\nold dimensions:", oldDimensions, '\nnew dimensions:', newDimensions);
// if size doesn't match, trigger onPlayerDimensionChange
if (
newDimensions?.width != oldDimensions?.width
|| newDimensions?.height != oldDimensions?.height
|| newDimensions?.fullscreen != oldDimensions?.fullscreen
){
// If player size changes, we restore aspect ratio
this.eventBus.send('restore-ar', null);
this.eventBus.send('delayed-restore-ar', {delay: 500});
// this.videoData.resizer?.restore();
this.eventBus.send('uw-config-broadcast', {
type: 'player-dimensions',
data: newDimensions
});
this.isTooSmall = this.checkIfTooSmall(newDimensions);
}
}
onPlayerDimensionsChanged: ResizeObserverCallback = _.debounce( onPlayerDimensionsChanged: ResizeObserverCallback = _.debounce(
(mutationList?, observer?) => { (mutationList?, observer?) => {
this.trackDimensionChanges(); this._requestTick(true);
this.trackEnvironmentChanges();
}, },
250, // do it once per this many ms 250, // do it once per this many ms
{ {
@ -519,6 +389,112 @@ class PlayerData {
} }
); );
private _requestTick(immediately?: boolean) {
if (immediately) {
this.processTick();
}
}
// Currently just a dummy, but at some point we could try to avoid running
// processTick on every tick — instead, running it only when needed.
// But today is not that day.
requestTick() {
this._requestTick();
}
/**
* Checks if anything changed
* @returns
*/
private processTick() {
let changeDetected = false;
let changes = {
player: false,
fullScreen: false,
theater: false,
dimensions: false,
}
let fs, newPlayerCandidate, isTheaterMode, currentDimensions;
fullScreenCheck:
{
fs = !!document.fullscreenElement;
if (fs !== this.isFullscreen) {
changes.fullScreen = true;
changeDetected = true;
}
}
playerCheck:
{
newPlayerCandidate = this.getPlayer();
if (!newPlayerCandidate) {
console.warn('[uw->PlayerData::processTick()] — player not detected — dimensions will not be checked')
return;
}
if (newPlayerCandidate !== this.element) {
changes.player = true;
changeDetected = true;
}
}
theaterModeCheck:
{
isTheaterMode = equalish(window.innerWidth, this.element.offsetWidth, 32);
if (isTheaterMode !== this.isTheaterMode) {
changes.theater = true;
changeDetected = true;
}
}
dimensionCheck:
{
if (this.isFullscreen) {
currentDimensions = {
width: window.innerWidth,
height: window.innerHeight,
};
} else {
currentDimensions = {
width: this.element.offsetWidth,
height: this.element.offsetHeight
};
}
if (currentDimensions.width !== this.dimensions?.width || currentDimensions.height !== this.dimensions?.height) {
changes.dimensions = true;
changeDetected = true;
}
}
// we only commit changes if no errors were encountered
commitChanges:
{
if (changes.fullScreen) this.isFullscreen = fs;
if (changes.player) this.element = newPlayerCandidate;
if (changes.theater) this.isTheaterMode = isTheaterMode;
if (changes.dimensions) this.dimensions = currentDimensions;
this.isTooSmall = this.checkIfTooSmall();
}
const canEnable = this.siteSettings.isEnabledForEnvironment(this.isFullscreen, this.isTheaterMode) === ExtensionMode.Enabled;
if (this.runLevel === RunLevel.Off && canEnable) {
this.eventBus.send('restore-ar', null);
} else if (!canEnable && this.runLevel !== RunLevel.Off) {
// must be called before
this.eventBus.send('restore-ar', null);
this.setRunLevel(RunLevel.Off);
}
if (changeDetected) {
this.deferredUiInitialization();
this.eventBus.send('restore-ar', null);
this.eventBus.send('delayed-restore-ar', {delay: 500});
}
}
//#region player element change detection //#region player element change detection
/** /**
@ -546,46 +522,13 @@ class PlayerData {
// legacy mode still exists, but acts as a fallback for observers and is triggered less // legacy mode still exists, but acts as a fallback for observers and is triggered less
// frequently in order to avoid too many pointless checks // frequently in order to avoid too many pointless checks
this.legacyChangeDetection(); this.initFallbackDimensionMonitor();
}
async legacyChangeDetection() {
while (!this.halted) {
await sleep(1000);
try {
this.forceRefreshPlayerElement();
} catch (e) {
console.error('[PlayerData::legacycd] this message is pretty high on the list of messages you shouldn\'t see', e);
}
}
}
doPeriodicPlayerElementChangeCheck() {
if (this.periodicallyRefreshPlayerElement) {
this.forceRefreshPlayerElement();
}
} }
stopChangeDetection(){ stopChangeDetection(){
this.observer.disconnect(); this.observer.disconnect();
} }
//#region helper functions
collectionHas(collection, element) {
for (let i = 0, len = collection.length; i < len; i++) {
if (collection[i] == element) {
return true;
}
}
return false;
}
equalish(a,b, tolerance) {
return a > b - tolerance && a < b + tolerance;
}
//#endregion
private getElementStack(): ElementStack { private getElementStack(): ElementStack {
const elementStack: ElementStack = [{ const elementStack: ElementStack = [{
element: this.videoElement, element: this.videoElement,
@ -617,19 +560,22 @@ class PlayerData {
return this.elementStack; return this.elementStack;
} }
updatePlayer(options?: {verbose?: boolean, newElement?: HTMLElement}) { updatePlayer(options?: {verbose?: boolean, newElement?: HTMLElement}) {
const newPlayer = options?.newElement ?? this.getPlayer(options); const newPlayer = options?.newElement ?? this.getPlayer(options);
if (newPlayer === this.element) { if (newPlayer === this.element || !newPlayer) {
return; return;
} }
this.observer?.unobserve(this.element);
// clean up and re-initialize UI // clean up and re-initialize UI
this.ui?.destroy(); this.ui?.destroy();
delete this.ui; delete this.ui;
// Don't forget to also move observer to the new player
// (if observer exists)
this.element = newPlayer; this.element = newPlayer;
this.observer?.observe(this.element);
this.ui = new UI( this.ui = new UI(
'ultrawidifyUi', 'ultrawidifyUi',
@ -642,8 +588,7 @@ class PlayerData {
} }
); );
this.trackDimensionChanges(); this.requestTick();
this.trackEnvironmentChanges();
} }
/** /**
@ -695,10 +640,14 @@ class PlayerData {
return playerCandidate.element; return playerCandidate.element;
} else { } else {
const playerCandidate = this.getPlayerAuto(elementStack, videoWidth, videoHeight); const playerCandidate = this.getPlayerAuto(elementStack, videoWidth, videoHeight);
if (playerCandidate === null) {
console.warn('[uw::getPlayer] getPlayerAuto returned null — no player detected?');
} else {
playerCandidate.heuristics['activePlayer'] = true; playerCandidate.heuristics['activePlayer'] = true;
return playerCandidate.element; return playerCandidate.element;
} }
} }
}
private nextPlayerElement() { private nextPlayerElement() {
@ -713,8 +662,8 @@ class PlayerData {
const elementStack = this.getElementStack(); const elementStack = this.getElementStack();
if ( if (
this.equalish(elementStack[currentIndex].element.offsetWidth, elementStack[nextIndex].element.offsetWidth, 2) equalish(elementStack[currentIndex].element.offsetWidth, elementStack[nextIndex].element.offsetWidth, 2)
&& this.equalish(elementStack[currentIndex].element.offsetHeight, elementStack[nextIndex].element.offsetHeight, 2) && equalish(elementStack[currentIndex].element.offsetHeight, elementStack[nextIndex].element.offsetHeight, 2)
) { ) {
// this.siteSettings.set('playerAutoConfig.initialIndex', this.siteSettings.data.playerAutoConfig.initialIndex + 1, {noSave: true}); // this.siteSettings.set('playerAutoConfig.initialIndex', this.siteSettings.data.playerAutoConfig.initialIndex + 1, {noSave: true});
// this.siteSettings.set('playerAutoConfig.modified', true); // this.siteSettings.set('playerAutoConfig.modified', true);
@ -765,8 +714,9 @@ class PlayerData {
// Don't bother thinking about this too much, as any "thinking" was quickly // Don't bother thinking about this too much, as any "thinking" was quickly
// corrected by bugs caused by various edge cases. // corrected by bugs caused by various edge cases.
if ( if (
this.equalish(elementData.height, videoHeight, 5)
|| this.equalish(elementData.width, videoWidth, 5) equalish(elementData.height, videoHeight, 5)
|| equalish(elementData.width, videoWidth, 5)
) { ) {
let score = 1000; let score = 1000;
@ -877,14 +827,14 @@ class PlayerData {
const allSelectors = document.querySelectorAll(queryString); const allSelectors = document.querySelectorAll(queryString);
for (const element of elementStack) { for (const element of elementStack) {
if (this.collectionHas(allSelectors, element.element)) { if (collectionHas(allSelectors, element.element)) {
let score = 100; let score = 100;
// we award points to elements which match video size in one // we award points to elements which match video size in one
// dimension and exceed it in the other // dimension and exceed it in the other
if ( if (
(element.width >= videoWidth && this.equalish(element.height, videoHeight, 2)) (element.width >= videoWidth && equalish(element.height, videoHeight, 2))
|| (element.height >= videoHeight && this.equalish(element.width, videoWidth, 2)) || (element.height >= videoHeight && equalish(element.width, videoWidth, 2))
) { ) {
score += 75; score += 75;
} }
@ -917,8 +867,6 @@ class PlayerData {
private handlePlayerTreeRequest() { private handlePlayerTreeRequest() {
// this populates this.elementStack fully // this populates this.elementStack fully
this.updatePlayer({verbose: true}); this.updatePlayer({verbose: true});
console.log('tree:', JSON.parse(JSON.stringify(this.elementStack)));
console.log('————————————————————— handling player tree request!')
this.eventBus.send('uw-config-broadcast', {type: 'player-tree', config: JSON.parse(JSON.stringify(this.elementStack))}); this.eventBus.send('uw-config-broadcast', {type: 'player-tree', config: JSON.parse(JSON.stringify(this.elementStack))});
} }
@ -933,7 +881,7 @@ class PlayerData {
this.markedElement.remove(); this.markedElement.remove();
} }
const elementBB = this.elementStack[data.parentIndex].element.getBoundingClientRect(); const elementBB = this.elementStack[data.parentIndex].element.getBoundingClientRect();;
// console.log('element bounding box:', elementBB); // console.log('element bounding box:', elementBB);

View File

@ -17,6 +17,7 @@ import ExtensionMode from '../../../common/enums/ExtensionMode.enum';
import { ExtensionEnvironment } from '../../../common/interfaces/SettingsInterface'; import { ExtensionEnvironment } from '../../../common/interfaces/SettingsInterface';
import { LogAggregator } from '../logging/LogAggregator'; import { LogAggregator } from '../logging/LogAggregator';
import { ComponentLogger } from '../logging/ComponentLogger'; import { ComponentLogger } from '../logging/ComponentLogger';
import { AardLegacy } from '../aard/AardLegacy';
/** /**
* VideoData handles CSS for the video element. * VideoData handles CSS for the video element.
@ -73,7 +74,7 @@ class VideoData {
player: PlayerData; player: PlayerData;
resizer: Resizer; resizer: Resizer;
aard: Aard; aard: Aard | AardLegacy;
eventBus: EventBus; eventBus: EventBus;
extensionStatus: ExtensionStatus; extensionStatus: ExtensionStatus;
@ -259,7 +260,7 @@ class VideoData {
this.resizer = new Resizer(this); this.resizer = new Resizer(this);
try { try {
this.aard = new Aard(this); // this starts Ar detection. needs optional parameter that prevents ArDetector from starting this.aard = this.settings.active.aard.useLegacy ? new AardLegacy(this) : new Aard(this); // this starts Ar detection. needs optional parameter that prevents ArDetector from starting
} catch (e) { } catch (e) {
console.error('Failed to start Aard!', e); console.error('Failed to start Aard!', e);
} }
@ -702,7 +703,7 @@ class VideoData {
return; return;
} }
if (! this.aard){ if (! this.aard){
this.aard = new Aard(this); this.aard = this.settings.active.aard.useLegacy ? new AardLegacy(this): new Aard(this);
} }
} }

View File

@ -65,6 +65,8 @@ class Resizer {
private effectiveZoom: {x: number, y: number} = {x: 1, y: 1}; private effectiveZoom: {x: number, y: number} = {x: 1, y: 1};
private pendingAr?: {ar: Ar, lastAr?: Ar};
_lastAr: Ar = {type: AspectRatioType.Initial}; _lastAr: Ar = {type: AspectRatioType.Initial};
set lastAr(x: Ar) { set lastAr(x: Ar) {
// emit updates for UI when setting lastAr, but only if AR really changed // emit updates for UI when setting lastAr, but only if AR really changed
@ -332,6 +334,19 @@ class Resizer {
return; return;
} }
// If we are missing some data that's necessary for crop calculations,
// set pendingAr and quit
if (!this.videoData.player.dimensions?.width || !this.videoData.player.dimensions?.height) {
this.pendingAr = {ar, lastAr};
this.videoData.player.requestTick();
return;
}
this.pendingAr = null;
if (! this.videoData.player.dimensions) {
this.videoData.player.updatePlayer();
}
// If no aspect ratio is applied AND if no stretch mode is active, // If no aspect ratio is applied AND if no stretch mode is active,
// we disable our CSS in order to prevent breaking websites by default, // we disable our CSS in order to prevent breaking websites by default,
// without any human interaction // without any human interaction
@ -465,8 +480,12 @@ class Resizer {
// this.videoData.eventBus.send('announce-zoom', this.manualZoom ? {x: this.zoom.scale, y: this.zoom.scaleY} : this.zoom.effectiveZoom); // this.videoData.eventBus.send('announce-zoom', this.manualZoom ? {x: this.zoom.scale, y: this.zoom.scaleY} : this.zoom.effectiveZoom);
// } // }
let translate = this.computeOffsets(stretchFactors, options?.ar); try {
const translate = this.computeOffsets(stretchFactors, options?.ar);
this.applyCss(stretchFactors, translate); this.applyCss(stretchFactors, translate);
} catch (e) {
// don't apply CSS if there's an error
}
} }
toFixedAr() { toFixedAr() {
@ -567,6 +586,10 @@ class Resizer {
* @returns * @returns
*/ */
restore() { restore() {
if (this.pendingAr) {
this.setAr(this.pendingAr.ar, this.pendingAr.lastAr);
return;
}
if (!this.manualZoom) { if (!this.manualZoom) {
this.logger.info('restore', `<rid:${this.resizerId}> attempting to restore aspect ratio`, {'lastAr': this.lastAr} ); this.logger.info('restore', `<rid:${this.resizerId}> attempting to restore aspect ratio`, {'lastAr': this.lastAr} );
@ -858,16 +881,15 @@ class Resizer {
) { ) {
this.logger.warn('computeOffsets', `<rid:${this.resizerId}> We are getting some incredibly funny results here.\n\n`, this.logger.warn('computeOffsets', `<rid:${this.resizerId}> We are getting some incredibly funny results here.\n\n`,
`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`, `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`,
`Something is probably undefined:\n`,
`\n videoData.video offset w x h:`, this.videoData.video.offsetWidth, 'x', this.videoData.video.offsetHeight,
`\n videoData.player.dimensions w x h:`, this.videoData.player.dimensions.width, 'x', this.videoData.player.dimensions.height,
`Player element needs to be checked.` `Player element needs to be checked.`
); );
// request dimension change tick.
// sometimes this appears to randomly recurse. // this will cause
// There seems to be no way to reproduce it. this.videoData.player.requestTick();
if (! this._computeOffsetsRecursionGuard) { throw 'DIMENSIONS_ERROR';
this._computeOffsetsRecursionGuard = true;
this.videoData.player.trackDimensionChanges();
this._computeOffsetsRecursionGuard = false;
}
} }
return translate; return translate;

View File

@ -0,0 +1,12 @@
export function collectionHas(collection, element): boolean {
for (let i = 0, len = collection.length; i < len; i++) {
if (collection[i] == element) {
return true;
}
}
return false;
}
export function equalish(a: number,b: number, tolerance: number): boolean {
return a > b - tolerance && a < b + tolerance;
}

View File

@ -4,21 +4,29 @@
Ultrawidify has been updated. Ultrawidify has been updated.
</div> </div>
<div class="body flex-grow"> <div class="body flex-grow">
<h1>Where do you want to use Ultrawidify?</h1> <h1>Where should Ultrawidify run by default</h1>
<div class="flex flex-row"> <div class="flex flex-row">
<div class="" <div class=""
@click="() => {}" @click="() => {}"
> >
All sites<br/> All websites<br/>
<small>(Some sites are disabled by default. Requires access to all sites)</small> <small>(Some sites are disabled by default.)</small>
</div> </div>
<div> <div>
Default sites and sites I explicitly allow On sites that people say are working<br/>
<small>(And the sites I explicitly allow)</small>
</div>
<div>
Officially supported sites*<br/>
<small>(And the sites I explicitly allow)</small>
</div> </div>
<div> <div>
Only the sites I explicitly allow Only the sites I explicitly allow
</div> </div>
</div> </div>
<div>
*Ultrawidify still needs access to all websites for technical and historical technical reasons.
</div>
<h1>Try to automatically detect aspect ratio?</h1> <h1>Try to automatically detect aspect ratio?</h1>
<div class="flex flex-row"> <div class="flex flex-row">

View File

@ -1,86 +1,76 @@
<template> <template>
<div class="flex flex-col h100"> <div class="flex flex-col h100 justify-center items-center">
<div class="header flex-nogrow flex-noshrink"> <template v-if="!settingsInitialized">Please wait ...</template>
Thank you for installing Ultrawidify. <template v-else>
</div>
<div class="body flex-grow"> <div class="body flex-grow">
<p>Before we're ready to go, there are three quick questions. You will be able to change these later.</p> <h1>Ultrawidify has been updated</h1>
<h1>Where do you want to use Ultrawidify?</h1> <br/>
<div class="flex flex-row"> <p>This update introduces some new experimental features:</p>
<div class="" <b>
@click="() => {}" What do you want to if there are subtitles in the video?
> </b>
All sites<br/> <div class="select">
<small>(Some sites are disabled by default. Requires access to all sites)</small> <select v-model="placeholderSubtitleCrop">
</div> <option :value="AardSubtitleCropMode.ResetAR">Reset aspect ratio while subtitles are visible</option>
<div> <option :value="AardSubtitleCropMode.ResetAndDisable">Reset aspect ratio and stop autodetection for the video</option>
Default sites and sites I explicitly allow <option :value="AardSubtitleCropMode.CropSubtitles">Crop subtitles</option>
</div> </select>
<div>
Only the sites I explicitly allow
</div>
</div> </div>
<h1>Should Ultrawidify automatically detect aspect ratio where possible?</h1> <br/>
<div class="flex flex-row"> <b>Use experimental aspect ratio detection?</b>
<div class=""> <div class="select">
Yes <select v-model="settings.active.aard.useLegacy">
</div> <option :value="true">Use legacy detection</option>
<div> <option :value="false">Use experimental detection</option>
Only on sites I allow </select>
</div>
<div class="">
Never
</div> </div>
<p>Experimental aspect ratio detection should be more accurate, but it hasn't been extensively tested yet.</p>
<p>If you enable experimental mode, please consider reporting problems <a href="https://github.com/tamius-han/ultrawidify/issues/291" target="_blank">in this thread</a> on Github.</p>
<p>Experimental detection will become the default in 2026 unless people report issues.</p>
<br/>
<br/>
<div class="flex flex-row w-full justify-center items-center">
<button v-if="!settingsSaved" class="button primary" @click="saveSettings">
Save preferences
</button>
<template v-else>Your settings have been saved.</template>
</div> </div>
<h1>Do you want to see update notes when extension receives updates?</h1> <br/>
<p>Update notes will open a new tab, just like this one.</p> <br/>
<div class="flex flex-row">
<div class=""> <p>You can always change your settings later.</p>
Yes, even for the tiniest changes
</div>
<div class="">
Yes, but only for the big/important ones
</div>
<div class="">
No, never.
</div>
</div>
</div> </div>
<div class="footer flex-nogrow flex-noshrink"> <div class="footer flex-nogrow flex-noshrink">
</div> </div>
</template>
</div> </div>
</template> </template>
<script> <script>
import BrowserDetect from '../../ext/conf/BrowserDetect'; import BrowserDetect from '@src/ext/conf/BrowserDetect';
import { LogAggregator } from '@src/ext/lib/logging/LogAggregator'; import { LogAggregator } from '@src/ext/lib/logging/LogAggregator';
import { ComponentLogger } from '@src/ext/lib/logging/ComponentLogger'; import { ComponentLogger } from '@src/ext/lib/logging/ComponentLogger';
import Settings from '@src/ext/lib/settings/Settings';
import { AardSubtitleCropMode } from '@src/ext/lib/aard/enums/aard-subtitle-crop-mode.enum';
export default { export default {
data () { data () {
return { return {
selectedTab: 'video', AardSubtitleCropMode,
selectedFrame: '__all',
selectedSite: '',
activeFrames: [],
activeSites: [],
port: BrowserDetect.firefox ? chrome.runtime.connect({name: 'popup-port'}) : chrome.runtime.connect({name: 'popup-port'}),
comms: new Comms(),
frameStore: {},
frameStoreCount: 0,
performance: {},
site: null,
currentZoom: 1,
settings: {}, settings: {},
settingsInitialized: false, settingsInitialized: false,
logAggregator: {}, logAggregator: {},
logger: {}, logger: {},
siteTabDisabled: false, placeholderSubtitleCrop: AardSubtitleCropMode.ResetAR,
videoTabDisabled: false, settingsSaved: false
canShowVideoTab: {canShow: true, warning: true},
showWhatsNew: false,
} }
}, },
async created() { async created() {
@ -89,26 +79,36 @@ export default {
this.settings = new Settings({updateCallback: () => this.updateConfig(), logAggregator: this.logAggregator}); this.settings = new Settings({updateCallback: () => this.updateConfig(), logAggregator: this.logAggregator});
await this.settings.init(); await this.settings.init();
this.placeholderSubtitleCrop = (this.settings.active.aard.useLegacy ? this.settings.active.aardLegacy.subtitles?.subtitleCropMode : this.settings.active.aard.subtitles?.subtitleCropMode) ?? AardSubtitleCropMode.ResetAR;
this.settingsInitialized = true; this.settingsInitialized = true;
}, },
components: { components: {
}, },
methods: { methods: {
updateConfig() { async updateConfig() {
this.settings.init(); await this.settings.init();
this.$nextTick( () => this.$forceUpdate()); this.$nextTick( () => this.$forceUpdate());
},
saveSettings() {
this.settings.active[this.settings.active.aard.useLegacy ? 'aardLegacy' : 'aard'].subtitles.subtitleCropMode = this.placeholderSubtitleCrop;
this.settings.save();
this.settingsSaved = true;
} }
} }
} }
</script> </script>
<style src="../../res/css/font/overpass.css"></style> <style src="@csui/res/css/font/overpass.css"></style>
<style src="../../res/css/font/overpass-mono.css"></style> <style src="@csui/res/css/font/overpass-mono.css"></style>
<style src="../../res/css/flex.scss"></style> <style src="@csui/res/css/flex.scss"></style>
<style src="../../res/css/common.scss"></style> <style src="@csui/res/css/common.scss"></style>
<style lang="scss" scoped> <style lang="scss" scoped>
html, body { p {
font-size: 0.9rem !important;
}
body {
width: 800px !important; width: 800px !important;
max-width: 800px !important; max-width: 800px !important;
padding: 0px; padding: 0px;

View File

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="popup.css">
<% if (NODE_ENV === 'development') { %>
<!-- Load some resources only in development environment -->
<% } %>
</head>
<body style="width: 800px; height: 600px; overflow: hidden !important">
<div id="app">
</div>
<script src="first-time.js"></script>
</body>
</html>

View File

@ -1,11 +0,0 @@
import Vue from 'vue'
import App from './App'
// global.browser = require('webextension-polyfill')
// Vue.prototype.$browser = global.browser
/* eslint-disable no-new */
new Vue({
el: '#app',
render: h => h(App)
})

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ultrawidify updated</title>
</head>
<body style="width: 100dvw; height: 100dvh">
<div id="app">
app should load here
</div>
<script src="updated.js" type="module"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
import { createApp } from 'vue';
import App from './App';
import mdiVue from 'mdi-vue/v3';
import * as mdijs from '@mdi/js';
// import '@src/res-common/common.scss';
// NOTE — this is in-player interface for ultrawidify
// it is injected into the page in UI.init()
createApp(App)
.use(mdiVue, {icons: mdijs})
.mount('#app');

View File

@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Ultrawidify", "name": "Ultrawidify",
"description": "Removes black bars on ultrawide videos and offers advanced options to fix aspect ratio.", "description": "Removes black bars on ultrawide videos and offers advanced options to fix aspect ratio.",
"version": "6.3.97", "version": "6.3.993",
"icons": { "icons": {
"32":"res/icons/uw-32.png", "32":"res/icons/uw-32.png",
"64":"res/icons/uw-64.png" "64":"res/icons/uw-64.png"
@ -41,7 +41,8 @@
"res/img/settings/about-bg.png", "res/img/settings/about-bg.png",
"res/icons/*", "res/icons/*",
"res/img/*", "res/img/*",
"csui/*" "csui/*",
"install/*"
], ],
"matches": [ "matches": [
"*://*/*" "*://*/*"

View File

@ -12,3 +12,12 @@ var BgVars = {
} }
const server = new UWServer(); const server = new UWServer();
// add update listener
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === "update") {
chrome.tabs.create({
url: chrome.runtime.getURL("install/updated/updated.html")
});
}
});

View File

@ -20,6 +20,7 @@ const config = {
'csui/csui-popup': './csui/csui-popup.js', 'csui/csui-popup': './csui/csui-popup.js',
'csui/csui': './csui/csui.js', 'csui/csui': './csui/csui.js',
// 'install/first-time/first-time':'./install/first-time/first-time.js', // 'install/first-time/first-time':'./install/first-time/first-time.js',
'install/updated/updated': './install/updated/updated.js',
}, },
output: { output: {
path: __dirname + `/dist-${process.env.BROWSER == 'firefox' ? 'ff' : process.env.BROWSER}`, path: __dirname + `/dist-${process.env.BROWSER == 'firefox' ? 'ff' : process.env.BROWSER}`,
@ -125,6 +126,7 @@ const config = {
{ from: 'res', to: 'res', ignore: ['css', 'css/**']}, { from: 'res', to: 'res', ignore: ['css', 'css/**']},
{ from: 'ext', to: 'ext', ignore: ['conf/*', 'lib/**']}, { from: 'ext', to: 'ext', ignore: ['conf/*', 'lib/**']},
{ from: 'csui', to: 'csui', ignore: ['src']}, { from: 'csui', to: 'csui', ignore: ['src']},
{ from: 'install', to: 'install' },
// we need to get webextension-polyfill and put it in common/lib // we need to get webextension-polyfill and put it in common/lib
{ from: '../node_modules/webextension-polyfill/dist/browser-polyfill.js', to: 'common/lib/browser-polyfill.js'}, { from: '../node_modules/webextension-polyfill/dist/browser-polyfill.js', to: 'common/lib/browser-polyfill.js'},
@ -138,6 +140,7 @@ const config = {
{ from: 'csui/csui-overlay-dark.html', to: 'csui/csui-dark.html', transform: transformHtml }, { from: 'csui/csui-overlay-dark.html', to: 'csui/csui-dark.html', transform: transformHtml },
{ from: 'csui/csui-overlay-light.html', to: 'csui/csui-light.html', transform: transformHtml }, { from: 'csui/csui-overlay-light.html', to: 'csui/csui-light.html', transform: transformHtml },
// { from: 'install/first-time/first-time.html', to: 'install/first-time/first-time.html', transform: transformHtml}, // { from: 'install/first-time/first-time.html', to: 'install/first-time/first-time.html', transform: transformHtml},
{ from: 'install/updated/updated.html', to: 'install/updated/updated.html', transform: transformHtml },
{ {
from: 'manifest.json', from: 'manifest.json',
to: 'manifest.json', to: 'manifest.json',