Skip to content

Commit 873612b

Browse files
committed
[wip]: try to find acceptable perf/quality values
- add controls to tweak the values - AI-coded, as it's up to deletion. Worth keeping for debugging purposes? [skip ci] Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
1 parent 88a8235 commit 873612b

5 files changed

Lines changed: 221 additions & 37 deletions

File tree

src/components/MediaSettings/VideoBackgroundEditor.vue

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,24 @@
6767
tabindex="-1"
6868
aria-hidden="true"
6969
@change="handleFileInput">
70+
<div class="background-editor__debug-controls">
71+
<label
72+
v-for="control in debugControls"
73+
:key="control.key"
74+
class="background-editor__debug-control">
75+
<span class="background-editor__debug-control-header">
76+
<span>{{ control.label }}</span>
77+
<span>{{ formatDebugConfigValue(control.key) }}</span>
78+
</span>
79+
<input
80+
:min="control.min"
81+
:max="control.max"
82+
:step="control.step"
83+
:value="debugConfigValues[control.key]"
84+
type="range"
85+
@input="handleDebugConfigInput(control.key, $event.target.value)">
86+
</label>
87+
</div>
7088
</div>
7189
</template>
7290

@@ -88,6 +106,7 @@ import { getDavClient } from '../../services/DavClient.ts'
88106
import { useActorStore } from '../../stores/actor.ts'
89107
import { useSettingsStore } from '../../stores/settings.ts'
90108
import { findUniquePath } from '../../utils/fileUpload.ts'
109+
import { VIRTUAL_BACKGROUND_DEBUG_CONFIG_RANGES, virtualBackgroundDebugConfig, setVirtualBackgroundDebugConfigValue } from '../../utils/media/effects/virtual-background/runtimeConfig.js'
91110
92111
const predefinedBackgroundLabels = {
93112
'1_office': t('spreed', 'Select virtual office background'),
@@ -100,6 +119,24 @@ const predefinedBackgroundLabels = {
100119
'8_space_station': t('spreed', 'Select virtual space station background'),
101120
}
102121
122+
const virtualBackgroundDebugControlLabels = {
123+
DEFAULT_BLUR_PASSES: t('spreed', 'Blur passes'),
124+
SIGMA_SPACE: t('spreed', 'Sigma space'),
125+
SIGMA_COLOR: t('spreed', 'Sigma color'),
126+
SPARSITY_FACTOR: t('spreed', 'Sparsity factor'),
127+
DEFAULT_FRAME_RATE: t('spreed', 'Default frame rate'),
128+
MAX_SEGMENTATION_FRAME_RATE: t('spreed', 'Max segmentation frame rate'),
129+
}
130+
131+
const virtualBackgroundDebugControlFractionDigits = {
132+
DEFAULT_BLUR_PASSES: 0,
133+
SIGMA_SPACE: 1,
134+
SIGMA_COLOR: 2,
135+
SPARSITY_FACTOR: 2,
136+
DEFAULT_FRAME_RATE: 0,
137+
MAX_SEGMENTATION_FRAME_RATE: 0,
138+
}
139+
103140
export default {
104141
name: 'VideoBackgroundEditor',
105142
@@ -139,6 +176,7 @@ export default {
139176
data() {
140177
return {
141178
selectedBackground: undefined,
179+
debugConfigValues: { ...virtualBackgroundDebugConfig },
142180
}
143181
},
144182
@@ -162,6 +200,15 @@ export default {
162200
relativeBackgroundsFolderPath() {
163201
return this.settingsStore.attachmentFolder + '/Backgrounds'
164202
},
203+
204+
debugControls() {
205+
return Object.entries(VIRTUAL_BACKGROUND_DEBUG_CONFIG_RANGES).map(([key, range]) => ({
206+
key,
207+
label: virtualBackgroundDebugControlLabels[key],
208+
fractionDigits: virtualBackgroundDebugControlFractionDigits[key],
209+
...range,
210+
}))
211+
},
165212
},
166213
167214
async mounted() {
@@ -268,6 +315,17 @@ export default {
268315
this.handleSelectBackground(previewURL)
269316
},
270317
318+
handleDebugConfigInput(key, value) {
319+
this.debugConfigValues[key] = setVirtualBackgroundDebugConfigValue(key, value)
320+
},
321+
322+
formatDebugConfigValue(key) {
323+
const value = this.debugConfigValues[key]
324+
const fractionDigits = virtualBackgroundDebugControlFractionDigits[key]
325+
326+
return fractionDigits > 0 ? value.toFixed(fractionDigits) : String(value)
327+
},
328+
271329
loadBackground() {
272330
// Set virtual background depending on browser storage's settings
273331
if (BrowserStorage.getItem('virtualBackgroundEnabled') === 'true') {
@@ -305,6 +363,24 @@ export default {
305363
max-height: calc(var(--background-button-height) * 3 + var(--default-grid-baseline) * 4);
306364
overflow-y: auto;
307365
366+
&__debug-controls {
367+
grid-column: 1 / -1;
368+
display: grid;
369+
gap: calc(var(--default-grid-baseline) * 2);
370+
}
371+
372+
&__debug-control {
373+
display: grid;
374+
gap: var(--default-grid-baseline);
375+
}
376+
377+
&__debug-control-header {
378+
display: flex;
379+
justify-content: space-between;
380+
gap: calc(var(--default-grid-baseline) * 2);
381+
font-size: 12px;
382+
}
383+
308384
&__element {
309385
border: none;
310386
margin: 0 !important;

src/main.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import store from './store/index.js'
1414
import pinia from './stores/pinia.ts'
1515
import { useSidebarStore } from './stores/sidebar.ts'
1616
import { NextcloudGlobalsVuePlugin } from './utils/NextcloudGlobalsVuePlugin.js'
17+
import { exposeVirtualBackgroundDebugConfig } from './utils/media/effects/virtual-background/runtimeConfig.js'
1718

1819
import './init.js'
1920
// Leaflet icon patch
@@ -104,6 +105,7 @@ subscribe('viewer:sidebar:open', (node) => {
104105
if (!window.OCA.Talk) {
105106
window.OCA.Talk = reactive({})
106107
}
108+
exposeVirtualBackgroundDebugConfig(window.OCA.Talk)
107109
OCA.Talk.instance = instance
108110
OCA.Talk.Settings = SettingsAPI
109111

src/utils/media/effects/virtual-background/VideoStreamBackgroundEffect.js

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,11 @@ import {
1313
timerWorkerScript,
1414
} from './TimerWorker.js'
1515
import WebGLCompositor from './WebGLCompositor.js'
16+
import { DEFAULT_VIRTUAL_BACKGROUND_DEBUG_CONFIG, virtualBackgroundDebugConfig } from './runtimeConfig.js'
1617

1718
// Cache MediaPipe resources to avoid loading them multiple times.
1819
let _WasmFileset = null
1920

20-
const DEFAULT_FRAME_RATE = 30
21-
const MAX_SEGMENTATION_FRAME_RATE = 25
22-
2321
/**
2422
* Represents a modified MediaStream that applies virtual background effects
2523
* (blur, image, video, or video stream) using MediaPipe segmentation.
@@ -85,8 +83,9 @@ export default class VideoStreamBackgroundEffect {
8583
this._inputVideoElement = document.createElement('video')
8684
this._bgChanged = false
8785
this._prevBgMode = null
88-
this._frameRate = DEFAULT_FRAME_RATE
89-
this._runInferenceInterval = 1000 / MAX_SEGMENTATION_FRAME_RATE
86+
this._inputTrackFrameRate = null
87+
this._frameRate = virtualBackgroundDebugConfig.DEFAULT_FRAME_RATE
88+
this._runInferenceInterval = 1000 / Math.min(this._frameRate, virtualBackgroundDebugConfig.MAX_SEGMENTATION_FRAME_RATE)
9089
this._lastInferenceTime = 0
9190
}
9291

@@ -197,10 +196,46 @@ export default class VideoStreamBackgroundEffect {
197196
* @param {number|string} frameRate - Input frame rate from track settings.
198197
* @return {void}
199198
*/
200-
_updateFrameRate(frameRate) {
199+
_resolveFrameRate(frameRate) {
200+
const configuredFrameRate = virtualBackgroundDebugConfig.DEFAULT_FRAME_RATE
201201
const parsedFrameRate = parseInt(frameRate, 10)
202-
this._frameRate = !Number.isNaN(parsedFrameRate) ? parsedFrameRate : DEFAULT_FRAME_RATE
203-
this._runInferenceInterval = 1000 / Math.min(this._frameRate, MAX_SEGMENTATION_FRAME_RATE)
202+
203+
if (Number.isNaN(parsedFrameRate)) {
204+
return configuredFrameRate
205+
}
206+
207+
if (configuredFrameRate === DEFAULT_VIRTUAL_BACKGROUND_DEBUG_CONFIG.DEFAULT_FRAME_RATE) {
208+
return parsedFrameRate
209+
}
210+
211+
return Math.min(parsedFrameRate, configuredFrameRate)
212+
}
213+
214+
_syncFrameRateConfig() {
215+
const nextFrameRate = this._resolveFrameRate(this._inputTrackFrameRate)
216+
const nextRunInferenceInterval = 1000 / Math.min(nextFrameRate, virtualBackgroundDebugConfig.MAX_SEGMENTATION_FRAME_RATE)
217+
const didFrameRateChange = this._frameRate !== nextFrameRate
218+
219+
this._frameRate = nextFrameRate
220+
this._runInferenceInterval = nextRunInferenceInterval
221+
222+
if (!didFrameRateChange || !this._outputStream) {
223+
return
224+
}
225+
226+
const outputTrack = this._outputStream.getVideoTracks()[0]
227+
if (!outputTrack) {
228+
return
229+
}
230+
231+
outputTrack.applyConstraints({ frameRate: this._frameRate }).catch((error) => {
232+
console.error('Frame rate could not be adjusted in background effect', error)
233+
})
234+
}
235+
236+
_updateFrameRate(frameRate) {
237+
this._inputTrackFrameRate = frameRate
238+
this._syncFrameRateConfig()
204239
}
205240

206241
/**
@@ -293,6 +328,8 @@ export default class VideoStreamBackgroundEffect {
293328
this._frameId = this._lastFrameId
294329
}
295330

331+
this._syncFrameRateConfig()
332+
296333
const now = performance.now()
297334

298335
// Run inference if ready and enough time passed since last _runInference() call
@@ -690,10 +727,6 @@ export default class VideoStreamBackgroundEffect {
690727

691728
this._updateFrameRate(frameRate)
692729

693-
this._outputStream.getVideoTracks()[0].applyConstraints({ frameRate: this._frameRate }).catch((error) => {
694-
console.error('Frame rate could not be adjusted in background effect', error)
695-
})
696-
697730
this._frameId = -1
698731
this._lastFrameId = -1
699732
this._lastInferenceTime = 0

src/utils/media/effects/virtual-background/WebGLCompositor.js

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,7 @@
44
*/
55
// @flow
66

7-
/**
8-
* Number of horizontal + vertical blur iterations applied.
9-
* Larger value - stronger blur effect. Low GPU impact.
10-
*/
11-
const DEFAULT_BLUR_PASSES = 3
12-
/**
13-
* Spatial radius of the bilateral filter.
14-
* Larger value - smoother mask over a wider area, softer edges. High GPU impact.
15-
*/
16-
const SIGMA_SPACE = 10
17-
/**
18-
* Range sensitivity of the bilateral filter.
19-
* Larger value: more smoothing across edges. Low GPU impact.
20-
*/
21-
const SIGMA_COLOR = 0.15
22-
/**
23-
* Sampling stride scale for the bilateral kernel.
24-
* Larger value - faster processing, rougher mask refinement. High GPU impact.
25-
*/
26-
const SPARSITY_FACTOR = 0.66
7+
import { virtualBackgroundDebugConfig } from './runtimeConfig.js'
278

289
/**
2910
* WebGL-based compositor for background effects.
@@ -487,10 +468,13 @@ export default class WebGLCompositor {
487468
// Calculate filter parameters
488469
const texelWidth = 1 / width
489470
const texelHeight = 1 / height
490-
const step = Math.max(1, Math.sqrt(SIGMA_SPACE) * SPARSITY_FACTOR)
491-
const radius = SIGMA_SPACE
471+
const sigmaSpace = virtualBackgroundDebugConfig.SIGMA_SPACE
472+
const sigmaColor = virtualBackgroundDebugConfig.SIGMA_COLOR
473+
const sparsityFactor = virtualBackgroundDebugConfig.SPARSITY_FACTOR
474+
const step = Math.max(1, Math.sqrt(sigmaSpace) * sparsityFactor)
475+
const radius = sigmaSpace
492476
const offset = step > 1 ? step * 0.5 : 0
493-
const sigmaTexel = Math.max(texelWidth, texelHeight) * SIGMA_SPACE
477+
const sigmaTexel = Math.max(texelWidth, texelHeight) * sigmaSpace
494478

495479
// Set uniforms
496480
gl.uniform1i(gl.getUniformLocation(this.progBilateral, 'u_inputFrame'), 0)
@@ -500,7 +484,7 @@ export default class WebGLCompositor {
500484
gl.uniform1f(gl.getUniformLocation(this.progBilateral, 'u_radius'), radius)
501485
gl.uniform1f(gl.getUniformLocation(this.progBilateral, 'u_offset'), offset)
502486
gl.uniform1f(gl.getUniformLocation(this.progBilateral, 'u_sigmaTexel'), sigmaTexel)
503-
gl.uniform1f(gl.getUniformLocation(this.progBilateral, 'u_sigmaColor'), SIGMA_COLOR)
487+
gl.uniform1f(gl.getUniformLocation(this.progBilateral, 'u_sigmaColor'), sigmaColor)
504488

505489
// Bind textures
506490
gl.activeTexture(gl.TEXTURE0)
@@ -550,7 +534,9 @@ export default class WebGLCompositor {
550534
gl.activeTexture(gl.TEXTURE1)
551535
gl.bindTexture(gl.TEXTURE_2D, this.texMaskFiltered)
552536

553-
for (let i = 0; i < DEFAULT_BLUR_PASSES; i++) {
537+
const blurPasses = virtualBackgroundDebugConfig.DEFAULT_BLUR_PASSES
538+
539+
for (let i = 0; i < blurPasses; i++) {
554540
// Horizontal pass
555541
gl.uniform2f(gl.getUniformLocation(this.progBlur, 'u_texelSize'), 0, texelHeight)
556542
gl.bindFramebuffer(gl.FRAMEBUFFER, this.fboBlur1)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
export const DEFAULT_VIRTUAL_BACKGROUND_DEBUG_CONFIG = Object.freeze({
7+
DEFAULT_BLUR_PASSES: 2,
8+
SIGMA_SPACE: 9,
9+
SIGMA_COLOR: 0.25,
10+
SPARSITY_FACTOR: 0.95,
11+
DEFAULT_FRAME_RATE: 30,
12+
MAX_SEGMENTATION_FRAME_RATE: 25,
13+
})
14+
15+
export const VIRTUAL_BACKGROUND_DEBUG_CONFIG_RANGES = Object.freeze({
16+
DEFAULT_BLUR_PASSES: Object.freeze({ min: 1, max: 6, step: 1 }),
17+
SIGMA_SPACE: Object.freeze({ min: 1, max: 20, step: 0.5 }),
18+
SIGMA_COLOR: Object.freeze({ min: 0.01, max: 1, step: 0.01 }),
19+
SPARSITY_FACTOR: Object.freeze({ min: 0.1, max: 1, step: 0.01 }),
20+
DEFAULT_FRAME_RATE: Object.freeze({ min: 1, max: 60, step: 1 }),
21+
MAX_SEGMENTATION_FRAME_RATE: Object.freeze({ min: 1, max: 60, step: 1 }),
22+
})
23+
24+
const integerKeys = new Set([
25+
'DEFAULT_BLUR_PASSES',
26+
'DEFAULT_FRAME_RATE',
27+
'MAX_SEGMENTATION_FRAME_RATE',
28+
])
29+
30+
const configKeys = Object.keys(DEFAULT_VIRTUAL_BACKGROUND_DEBUG_CONFIG)
31+
32+
export const virtualBackgroundDebugConfig = {
33+
...DEFAULT_VIRTUAL_BACKGROUND_DEBUG_CONFIG,
34+
}
35+
36+
/**
37+
* Sets a debug config value after parsing and clamping it to the allowed range.
38+
*
39+
* @param {string} key - Config key to update.
40+
* @param {number|string} value - New value coming from the UI/global object.
41+
* @return {number} The normalized value stored in the shared config.
42+
*/
43+
export function setVirtualBackgroundDebugConfigValue(key, value) {
44+
if (!Object.prototype.hasOwnProperty.call(virtualBackgroundDebugConfig, key)) {
45+
throw new Error(`Unknown virtual background debug config key: ${key}`)
46+
}
47+
48+
const parsedValue = integerKeys.has(key)
49+
? Number.parseInt(value, 10)
50+
: Number.parseFloat(value)
51+
const { min, max } = VIRTUAL_BACKGROUND_DEBUG_CONFIG_RANGES[key]
52+
const fallbackValue = DEFAULT_VIRTUAL_BACKGROUND_DEBUG_CONFIG[key]
53+
const normalizedValue = Number.isNaN(parsedValue)
54+
? fallbackValue
55+
: Math.min(max, Math.max(min, parsedValue))
56+
57+
virtualBackgroundDebugConfig[key] = normalizedValue
58+
59+
return normalizedValue
60+
}
61+
62+
/**
63+
* Exposes the shared debug config on the provided Talk object.
64+
*
65+
* @param {object} talkObject - window.OCA.Talk object.
66+
* @return {object} The same object with debug config attached.
67+
*/
68+
export function exposeVirtualBackgroundDebugConfig(talkObject) {
69+
for (const key of configKeys) {
70+
Object.defineProperty(talkObject, key, {
71+
configurable: true,
72+
enumerable: true,
73+
get() {
74+
return virtualBackgroundDebugConfig[key]
75+
},
76+
set(value) {
77+
setVirtualBackgroundDebugConfigValue(key, value)
78+
},
79+
})
80+
}
81+
82+
talkObject.VIRTUAL_BACKGROUND_DEBUG_CONFIG = virtualBackgroundDebugConfig
83+
talkObject.VIRTUAL_BACKGROUND_DEBUG_CONFIG_DEFAULTS = DEFAULT_VIRTUAL_BACKGROUND_DEBUG_CONFIG
84+
talkObject.VIRTUAL_BACKGROUND_DEBUG_CONFIG_RANGES = VIRTUAL_BACKGROUND_DEBUG_CONFIG_RANGES
85+
86+
return talkObject
87+
}

0 commit comments

Comments
 (0)