Re-add legacy AR detect

This commit is contained in:
Tamius Han 2025-10-14 18:52:22 +02:00
parent ddffbe92a0
commit c0c29759fa
13 changed files with 2765 additions and 176 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.98",
"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.98",
"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,7 +164,7 @@
<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>
@ -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

@ -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;
}