diff --git a/src/common/interfaces/SettingsInterface.ts b/src/common/interfaces/SettingsInterface.ts index 8c1d1ce..4eac694 100644 --- a/src/common/interfaces/SettingsInterface.ts +++ b/src/common/interfaces/SettingsInterface.ts @@ -52,6 +52,8 @@ export type SettingsReloadComponent = 'PlayerData' | 'VideoData'; export type SettingsReloadFlags = true | SettingsReloadComponent; export interface AardSettings { + aardType: 'webgl' | 'legacy' | 'auto'; + 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. diff --git a/src/csui/PlayerOverlay.vue b/src/csui/PlayerOverlay.vue index 9991c54..27b6704 100644 --- a/src/csui/PlayerOverlay.vue +++ b/src/csui/PlayerOverlay.vue @@ -2,12 +2,11 @@
 
@@ -30,99 +29,126 @@
- -
+ +
+ + +
+ diff --git a/src/csui/src/PlayerUiPanels/PlayerUiSettings.vue b/src/csui/src/PlayerUiPanels/PlayerUiSettings.vue index 4a42b7f..2d43189 100644 --- a/src/csui/src/PlayerUiPanels/PlayerUiSettings.vue +++ b/src/csui/src/PlayerUiPanels/PlayerUiSettings.vue @@ -9,7 +9,7 @@
Enable in-player UI
- + @@ -96,6 +178,24 @@ export default { setUiPage(key, event) { }, + forceNumber(value) { + // Change EU format to US if needed + // | remove everything after second period if necessary + // | | | remove non-numeric characters + // | | | | + return value.replaceAll(',', '.').split('.', 2).join('.').replace(/[^0-9.]/g, ''); + }, + setTriggerZoneSize(key, value) { + let size = (+this.forceNumber(value) / 100); + + if (isNaN(+size)) { + size = 0.5; + } + + this.settings.active.ui.inPlayer.triggerZoneDimensions[key] = size; + this.settings.saveWithoutReload(); + }, + async openOptionsPage() { BrowserDetect.runtime.openOptionsPage(); @@ -118,4 +218,37 @@ export default { .mt-4{ margin-top: 1rem; } + +.input { + max-width: 24rem; +} + +.range-input { + display: flex; + flex-direction: row; + + * { + margin-left: 0.5rem; + margin-right: 0.5rem; + } + + + input { + max-width: 5rem; + } + + input[type=range] { + max-width: none; + } +} + +.trigger-zone-editor { + background-color: rgba(0,0,0,0.25); + + padding-bottom: 2rem; + .field { + margin-bottom: -1em; + } + +} diff --git a/src/csui/src/PlayerUiPanels/ResetBackupPanel.vue b/src/csui/src/PlayerUiPanels/ResetBackupPanel.vue new file mode 100644 index 0000000..9c32938 --- /dev/null +++ b/src/csui/src/PlayerUiPanels/ResetBackupPanel.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/src/csui/src/components/AardStatusIndicator.vue b/src/csui/src/components/AardStatusIndicator.vue new file mode 100644 index 0000000..5b59879 --- /dev/null +++ b/src/csui/src/components/AardStatusIndicator.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/src/csui/src/components/SupportLevelIndicator.vue b/src/csui/src/components/SupportLevelIndicator.vue new file mode 100644 index 0000000..51235f1 --- /dev/null +++ b/src/csui/src/components/SupportLevelIndicator.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/src/csui/src/components/TriggerZoneEditor.vue b/src/csui/src/components/TriggerZoneEditor.vue new file mode 100644 index 0000000..9d176f2 --- /dev/null +++ b/src/csui/src/components/TriggerZoneEditor.vue @@ -0,0 +1,196 @@ + + + diff --git a/src/csui/src/res-common/common.scss b/src/csui/src/res-common/common.scss index 264c2bb..fe54cb2 100644 --- a/src/csui/src/res-common/common.scss +++ b/src/csui/src/res-common/common.scss @@ -30,7 +30,7 @@ h1, h2, h3 { padding: 0; } -.button { +button, .button { background-color: rgba($blackBg, $normalTransparentOpacity); padding: 0.5rem 2rem; @@ -51,6 +51,11 @@ h1, h2, h3 { background-color: $primaryBg; border-color: rgba($primary, .5); } + + &.danger { + background-color: #ff2211 !important; + color:#000; + } } .b3 { margin: 0.25rem; diff --git a/src/csui/src/utils/UIProbeMixin.js b/src/csui/src/utils/UIProbeMixin.js index 8a45de1..ba779f1 100644 --- a/src/csui/src/utils/UIProbeMixin.js +++ b/src/csui/src/utils/UIProbeMixin.js @@ -5,14 +5,14 @@ export default { * We can handle events with the same function we use to handle events from * the content script. */ - document.addEventListener('mousemove', (event) => { - this.handleProbe({ - coords: { - x: event.clientX, - y: event.clientY - } - }, this.origin); - }); + document.addEventListener('mousemove', (event) => { + this.handleProbe({ + coords: { + x: event.clientX, + y: event.clientY + } + }, this.origin); + }); }, data() { return { @@ -26,14 +26,28 @@ export default { }, methods: { playerDimensionsUpdate(dimensions) { + if (!dimensions.width || !dimensions.height) { + this.playerDimensions = undefined; + } + console.log('player dimensions update received:', dimensions); if (dimensions?.width !== this.playerDimensions?.width || dimensions?.height !== this.playerDimensions?.height) { - this.playerDimensions = dimensions; + console.log('Player dimensions changed!', dimensions); + this.playerDimensions = dimensions; + this.updateTriggerZones(); + } + }, + updateTriggerZones() { + console.log('triggered zone style recheck. player dims:', this.playerDimensions, 'in player settings:', this.settings.active.ui); + if (this.playerDimensions && this.settings) { this.triggerZoneStyles = { - height: `${this.playerDimensions.height * 0.5}px`, - width: `${this.playerDimensions.width * 0.5}px`, + width: `${Math.round(this.playerDimensions.width * this.settings.active.ui.inPlayer.triggerZoneDimensions.width)}px`, + height: `${Math.round(this.playerDimensions.height * this.settings.active.ui.inPlayer.triggerZoneDimensions.height)}px`, transform: `translate(${(this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetX)}%, ${this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetY}%)`, }; + console.log( + 'player trigger zone css:', this.triggerZoneStyles + ); } }, diff --git a/src/ext/conf/ExtConfPatches.ts b/src/ext/conf/ExtConfPatches.ts index 671b1d8..bf45cb6 100644 --- a/src/ext/conf/ExtConfPatches.ts +++ b/src/ext/conf/ExtConfPatches.ts @@ -166,6 +166,7 @@ const ExtensionConfPatch = [ for (const domOption in userOptions.sites[site].DOMConfig) userOptions.sites[site].DOMConfig[domOption].customCss; } + userOptions.arDetect.aardType = 'auto'; userOptions.ui = { inPlayer: { enabled: true, // enable by default on new installs diff --git a/src/ext/conf/ExtensionConf.ts b/src/ext/conf/ExtensionConf.ts index 9db7a4a..829af15 100644 --- a/src/ext/conf/ExtensionConf.ts +++ b/src/ext/conf/ExtensionConf.ts @@ -15,6 +15,7 @@ if(Debug.debug) const ExtensionConf: SettingsInterface = { arDetect: { + aardType: 'auto', 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. diff --git a/src/ext/lib/Settings.ts b/src/ext/lib/Settings.ts index 3370515..09b74b0 100644 --- a/src/ext/lib/Settings.ts +++ b/src/ext/lib/Settings.ts @@ -36,6 +36,9 @@ class Settings { //#region callbacks onSettingsChanged: any; afterSettingsSaved: any; + + onChangedCallbacks: any[] = []; + afterSettingsChangedCallbacks: any[] = []; //#endregion constructor(options) { @@ -63,15 +66,31 @@ class Settings { this.logger?.log('info', 'debug', 'Does parsedSettings.preventReload exist?', parsedSettings.preventReload, "Does callback exist?", !!this.onSettingsChanged); - if (!parsedSettings.preventReload && this.onSettingsChanged) { + if (!parsedSettings.preventReload) { try { - this.onSettingsChanged(); + for (const fn of this.onChangedCallbacks) { + try { + fn(); + } catch (e) { + this.logger?.log('warn', 'settings', "[Settings] afterSettingsChanged fallback failed. It's possible that a vue component got destroyed, and this function is nothing more than vestigal remains. It would be nice if we implemented something that allows us to remove callback functions from array, and remove vue callbacks from the callback array when their respective UI component gets destroyed. Or this could be an error with the function itself. IDK, here's the error.", e) + } + } + if (this.onSettingsChanged) { + this.onSettingsChanged(); + } this.logger?.log('info', 'settings', '[Settings] Update callback finished.') } catch (e) { this.logger?.log('error', 'settings', "[Settings] CALLING UPDATE CALLBACK FAILED. Reason:", e) } } + for (const fn of this.afterSettingsChangedCallbacks) { + try { + fn(); + } catch (e) { + this.logger?.log('warn', 'settings', "[Settings] afterSettingsChanged fallback failed. It's possible that a vue component got destroyed, and this function is nothing more than vestigal remains. It would be nice if we implemented something that allows us to remove callback functions from array, and remove vue callbacks from the callback array when their respective UI component gets destroyed. Or this could be an error with the function itself. IDK, here's the error.", e) + } + } if (this.afterSettingsSaved) { this.afterSettingsSaved(); } @@ -179,6 +198,7 @@ class Settings { updateFn(this.active, this.getDefaultSettings()); } catch (e) { this.logger?.log('error', 'settings', '[Settings::applySettingsPatches] Failed to execute update function. Keeping settings object as-is. Error:', e); + } } @@ -358,6 +378,13 @@ class Settings { getSiteSettings(site: string = window.location.hostname): SiteSettings { return new SiteSettings(this, site); } + + listenOnChange(fn: () => void): void { + this.onChangedCallbacks.push(fn); + } + listenAfterChange(fn: () => void): void { + this.afterSettingsChangedCallbacks.push(fn); + } } export default Settings; diff --git a/src/ext/lib/aard/Aard.ts b/src/ext/lib/aard/Aard.ts index 23a46b7..f6f7393 100644 --- a/src/ext/lib/aard/Aard.ts +++ b/src/ext/lib/aard/Aard.ts @@ -5,6 +5,7 @@ import Settings from '../Settings'; import VideoData from '../video-data/VideoData'; import { Corner } from './enums/corner.enum'; import { VideoPlaybackState } from './enums/video-playback-state.enum'; +import { FallbackCanvas } from './gl/FallbackCanvas'; import { GlCanvas } from './gl/GlCanvas'; import { AardCanvasStore } from './interfaces/aard-canvas-store.interface'; import { AardDetectionSample, generateSampleArray, resetSamples } from './interfaces/aard-detection-sample.interface'; @@ -234,6 +235,8 @@ export class Aard { //#region internal state public status: AardStatus = initAardStatus(); private timers: AardTimers = initAardTimers(); + private inFallback: boolean = false; + private fallbackReason: any; private canvasStore: AardCanvasStore; private testResults: AardTestResults; private canvasSamples: AardDetectionSample; @@ -245,6 +248,8 @@ export class Aard { return undefined; } + this.video.setAttribute('crossOrigin', 'anonymous'); + const ratio = this.video.videoWidth / this.video.videoHeight; if (isNaN(ratio)) { return undefined; @@ -284,8 +289,10 @@ export class Aard { * This method should only ever be called from constructor. */ private init() { + + this.canvasStore = { - main: new GlCanvas(new GlCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-gl'})), + main: this.createCanvas('main-gl') }; @@ -302,6 +309,42 @@ export class Aard { this.start(); } + + private createCanvas(canvasId: string, canvasType?: 'webgl' | 'fallback') { + if (canvasType) { + if (canvasType === this.settings.active.arDetect.aardType || this.settings.active.arDetect.aardType === 'auto') { + if (canvasType === 'webgl') { + return new GlCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-gl'}); + } else if (canvasType === 'fallback') { + return new FallbackCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-fallback'}); + } else { + // TODO: throw error + } + } else { + // TODO: throw error + } + + } + + if (['auto', 'webgl'].includes(this.settings.active.arDetect.aardType)) { + try { + return new GlCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-gl'}); + } catch (e) { + if (this.settings.active.arDetect.aardType !== 'webgl') { + return new FallbackCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-fallback'}); + } + console.error('[ultrawidify|Aard::createCanvas] could not create webgl canvas:', e); + this.eventBus.send('uw-config-broadcast', {type: 'aard-error', aardErrors: {webglError: true}}); + throw e; + } + } else if (this.settings.active.arDetect.aardType === 'legacy') { + return new FallbackCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-fallback'}); + } else { + console.error('[ultrawidify|Aard::createCanvas] invalid value in settings.arDetect.aardType:', this.settings.active.arDetect.aardType); + this.eventBus.send('uw-config-broadcast', {type: 'aard-error', aardErrors: {invalidSettings: true}}); + throw 'AARD_INVALID_SETTINGS'; + } + } //#endregion /** @@ -393,8 +436,35 @@ export class Aard { do { const imageData = await new Promise( resolve => { - this.canvasStore.main.drawVideoFrame(this.video); - resolve(this.canvasStore.main.getImageData()); + try { + this.canvasStore.main.drawVideoFrame(this.video); + resolve(this.canvasStore.main.getImageData()); + } catch (e) { + if (e.name === 'SecurityError') { + this.eventBus.send('uw-config-broadcast', {type: 'aard-error', aardErrors: {cors: true}}); + this.stop(); + } + if (this.canvasStore.main instanceof FallbackCanvas) { + if (this.inFallback) { + this.eventBus.send('uw-config-broadcast', {type: 'aard-error', aardErrors: this.fallbackReason}); + this.stop(); + } else { + this.eventBus.send('uw-config-broadcast', {type: 'aard-error', aardErrors: {fallbackCanvasError: true}}); + this.stop(); + } + } else { + if (this.settings.active.arDetect.aardType === 'auto') { + this.canvasStore.main.destroy(); + this.canvasStore.main = this.createCanvas('main-gl', 'fallback'); + } + this.inFallback = true; + this.fallbackReason = {cors: true}; + + if (this.settings.active.arDetect.aardType !== 'auto') { + this.stop(); + } + } + } } ); @@ -407,6 +477,7 @@ export class Aard { ); if (this.testResults.notLetterbox) { // TODO: reset aspect ratio to "AR not applied" + console.log('NOT LETTERBOX!'); this.testResults.lastStage = 1; break; } @@ -420,6 +491,7 @@ export class Aard { this.settings.active.arDetect.canvasDimensions.sampleCanvas.width, this.settings.active.arDetect.canvasDimensions.sampleCanvas.height ); + console.log('LETTERBOX SHRINK CHECK RESULT — IS GUARDLINE INVALIDATED?', this.testResults.guardLine.invalidated) if (! this.testResults.guardLine.invalidated) { this.checkLetterboxGrow( imageData, @@ -452,12 +524,17 @@ export class Aard { // if detection is uncertain, we don't do anything at all if (this.testResults.aspectRatioUncertain) { + console.info('aspect ratio not cettain.'); + console.warn('check finished:', JSON.parse(JSON.stringify(this.testResults)), JSON.parse(JSON.stringify(this.canvasSamples)), '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'); + return; } // TODO: emit debug values if debugging is enabled this.testResults.isFinished = true; + console.warn('check finished:', JSON.parse(JSON.stringify(this.testResults)), JSON.parse(JSON.stringify(this.canvasSamples)), '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'); + // if edge width changed, emit update event. if (this.testResults.aspectRatioUpdated) { this.videoData.resizer.updateAr({ @@ -1008,7 +1085,12 @@ export class Aard { // fact that it makes the 'if' statement governing gradient detection // bit more nicely visible (instead of hidden among spagheti) this.edgeScan(imageData, width, height); + + console.log('edge scan:', JSON.parse(JSON.stringify(this.canvasSamples))); + this.validateEdgeScan(imageData, width, height); + console.log('edge scan post valid:', JSON.parse(JSON.stringify(this.canvasSamples))); + // TODO: _if gradient detection is enabled, then: this.sampleForGradient(imageData, width, height); @@ -1061,6 +1143,7 @@ export class Aard { x = 0; isImage = false; finishedRows = 0; + while (row < topEnd) { i = 0; rowOffset = row * 4 * width; @@ -1126,6 +1209,7 @@ export class Aard { || imageData[rowOffset + x + 2] > this.testResults.blackLevel; if (!isImage) { + // console.log('(row:', row, ')', 'val:', imageData[rowOffset + x], 'col', x >> 2, x, 'pxoffset:', rowOffset + x, 'len:', imageData.length) // TODO: maybe some day mark this pixel as checked by writing to alpha channel i++; continue; @@ -1272,6 +1356,7 @@ export class Aard { // didn't change meaningfully from the first, in which chance we aren't. If the brightness increased // anywhere between 'not enough' and 'too much', we mark the measurement as invalid. if (lastSubpixel - firstSubpixel > this.settings.active.arDetect.edgeDetection.gradientTestMinDelta) { + console.log('sample invalidated cus gradient:'); this.canvasSamples.top[i] = -1; } } @@ -1645,6 +1730,26 @@ export class Aard { const compensatedWidth = fileAr === canvasAr ? this.canvasStore.main.width : this.canvasStore.main.width * fileAr; + + console.log(` + ———— ASPECT RATIO CALCULATION: ————— + + canvas size: ${this.canvasStore.main.width} x ${this.canvasStore.main.height} (1:${this.canvasStore.main.width / this.canvasStore.main.height}) + file size: ${this.video.videoWidth} x ${this.video.videoHeight} (1:${this.video.videoWidth / this.video.videoHeight}) + + compensated size: ${compensatedWidth} x ${this.canvasStore.main.height} (1:${compensatedWidth / this.canvasStore.main.height}) + + letterbox height: ${this.testResults.letterboxWidth} + net video height: ${this.canvasStore.main.height - (this.testResults.letterboxWidth * 2)} + + calculated aspect ratio ----- + + ${compensatedWidth} ${compensatedWidth} ${compensatedWidth} + ——————————————— = —————————————— = —————— = ${compensatedWidth / (this.canvasStore.main.height - (this.testResults.letterboxWidth * 2))} + ${this.canvasStore.main.height} - 2 x ${this.testResults.letterboxWidth} ${this.canvasStore.main.height} - ${2 * this.testResults.letterboxWidth} ${this.canvasStore.main.height - (this.testResults.letterboxWidth * 2)} + `); + + return compensatedWidth / (this.canvasStore.main.height - (this.testResults.letterboxWidth * 2)); } diff --git a/src/ext/lib/aard/gl/FallbackCanvas.ts b/src/ext/lib/aard/gl/FallbackCanvas.ts index fd3c8fe..0a6061c 100644 --- a/src/ext/lib/aard/gl/FallbackCanvas.ts +++ b/src/ext/lib/aard/gl/FallbackCanvas.ts @@ -7,7 +7,6 @@ export class FallbackCanvas extends GlCanvas { constructor(options: GlCanvasOptions) { super(options); - this.context = this.canvas.getContext('2d'); } /** @@ -18,9 +17,14 @@ export class FallbackCanvas extends GlCanvas { destroy() { } - protected initWebgl() { } + protected initContext() { + this.context = this.canvas.getContext('2d', {desynchronized: true}); + } + + protected initWebgl() { } drawVideoFrame(video: HTMLVideoElement) { + console.log('context:', this.context, 'canvas:', this.canvas ); this.context.drawImage(video, this.context.canvas.width, this.context.canvas.height); } diff --git a/src/ext/lib/aard/gl/GlCanvas.ts b/src/ext/lib/aard/gl/GlCanvas.ts index 88ef93f..e58de54 100644 --- a/src/ext/lib/aard/gl/GlCanvas.ts +++ b/src/ext/lib/aard/gl/GlCanvas.ts @@ -95,16 +95,7 @@ export class GlCanvas { this.canvas.setAttribute('width', `${options.width}`); this.canvas.setAttribute('height', `${options.height}`); - this.gl = this.canvas.getContext('webgl'); - - if (!this.gl) { - throw new Error('WebGL not supported'); - } - if(options.id) { - this.canvas.setAttribute('id', options.id); - } - - this.frameBufferSize = options.width * options.height * 4; + this.initContext(options); this.initWebgl(); } @@ -156,6 +147,24 @@ export class GlCanvas { this.gl.deleteTexture(this.texture); } + protected initContext(options: GlCanvasOptions) { + this.gl = this.canvas.getContext( + 'webgl2', + { + preserveDrawingBuffer: true + } + ); + + if (!this.gl) { + throw new Error('WebGL not supported'); + } + if(options.id) { + this.canvas.setAttribute('id', options.id); + } + + this.frameBufferSize = options.width * options.height * 4; + } + protected initWebgl() { // Initialize the GL context this.gl.clearColor(0.0, 0.0, 0.0, 1.0); diff --git a/src/ext/lib/video-data/PlayerData.ts b/src/ext/lib/video-data/PlayerData.ts index 0f97aae..79f344c 100644 --- a/src/ext/lib/video-data/PlayerData.ts +++ b/src/ext/lib/video-data/PlayerData.ts @@ -97,6 +97,16 @@ class PlayerData { 'get-player-tree': [{ function: () => this.handlePlayerTreeRequest() }], + 'get-player-dimensions': [{ + function: () => { + console.log('received get player dimensions! -- returning:', this.dimensions) + + this.eventBus.send('uw-config-broadcast', { + type: 'player-dimensions', + data: this.dimensions + }); + } + }], 'set-mark-element': [{ // NOTE: is this still used? function: (data) => this.markElement(data) }], @@ -371,6 +381,10 @@ class PlayerData { 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 + }); } } @@ -378,6 +392,7 @@ class PlayerData { this.trackDimensionChanges(); } + //#region player element change detection /** * Starts change detection. diff --git a/src/res/img/grid_512.webp b/src/res/img/grid_512.webp new file mode 100644 index 0000000..010073b Binary files /dev/null and b/src/res/img/grid_512.webp differ