Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/components/MediaSettings/VideoBackgroundEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,24 @@
tabindex="-1"
aria-hidden="true"
@change="handleFileInput">
<div class="background-editor__debug-controls">
<label
v-for="control in debugControls"
:key="control.key"
class="background-editor__debug-control">
<span class="background-editor__debug-control-header">
<span>{{ control.label }}</span>
<span>{{ formatDebugConfigValue(control.key) }}</span>
</span>
<input
:min="control.min"
:max="control.max"
:step="control.step"
:value="debugConfigValues[control.key]"
type="range"
@input="handleDebugConfigInput(control.key, $event.target.value)">
</label>
</div>
</div>
</template>

Expand All @@ -88,6 +106,7 @@
import { useActorStore } from '../../stores/actor.ts'
import { useSettingsStore } from '../../stores/settings.ts'
import { findUniquePath } from '../../utils/fileUpload.ts'
import { VIRTUAL_BACKGROUND_DEBUG_CONFIG_RANGES, virtualBackgroundDebugConfig, setVirtualBackgroundDebugConfigValue } from '../../utils/media/effects/virtual-background/runtimeConfig.js'

Check failure on line 109 in src/components/MediaSettings/VideoBackgroundEditor.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Expected "setVirtualBackgroundDebugConfigValue" to come before "virtualBackgroundDebugConfig"

const predefinedBackgroundLabels = {
'1_office': t('spreed', 'Select virtual office background'),
Expand All @@ -100,6 +119,24 @@
'8_space_station': t('spreed', 'Select virtual space station background'),
}

const virtualBackgroundDebugControlLabels = {
DEFAULT_BLUR_PASSES: t('spreed', 'Blur passes'),
SIGMA_SPACE: t('spreed', 'Sigma space'),
SIGMA_COLOR: t('spreed', 'Sigma color'),
SPARSITY_FACTOR: t('spreed', 'Sparsity factor'),
DEFAULT_FRAME_RATE: t('spreed', 'Default frame rate'),
MAX_SEGMENTATION_FRAME_RATE: t('spreed', 'Max segmentation frame rate'),
}

const virtualBackgroundDebugControlFractionDigits = {
DEFAULT_BLUR_PASSES: 0,
SIGMA_SPACE: 1,
SIGMA_COLOR: 2,
SPARSITY_FACTOR: 2,
DEFAULT_FRAME_RATE: 0,
MAX_SEGMENTATION_FRAME_RATE: 0,
}

export default {
name: 'VideoBackgroundEditor',

Expand Down Expand Up @@ -139,6 +176,7 @@
data() {
return {
selectedBackground: undefined,
debugConfigValues: { ...virtualBackgroundDebugConfig },
}
},

Expand All @@ -162,6 +200,15 @@
relativeBackgroundsFolderPath() {
return this.settingsStore.attachmentFolder + '/Backgrounds'
},

debugControls() {
return Object.entries(VIRTUAL_BACKGROUND_DEBUG_CONFIG_RANGES).map(([key, range]) => ({
key,
label: virtualBackgroundDebugControlLabels[key],
fractionDigits: virtualBackgroundDebugControlFractionDigits[key],
...range,
}))
},
},

async mounted() {
Expand Down Expand Up @@ -268,6 +315,17 @@
this.handleSelectBackground(previewURL)
},

handleDebugConfigInput(key, value) {
this.debugConfigValues[key] = setVirtualBackgroundDebugConfigValue(key, value)
},

formatDebugConfigValue(key) {
const value = this.debugConfigValues[key]
const fractionDigits = virtualBackgroundDebugControlFractionDigits[key]

return fractionDigits > 0 ? value.toFixed(fractionDigits) : String(value)
},

loadBackground() {
// Set virtual background depending on browser storage's settings
if (BrowserStorage.getItem('virtualBackgroundEnabled') === 'true') {
Expand Down Expand Up @@ -305,6 +363,24 @@
max-height: calc(var(--background-button-height) * 3 + var(--default-grid-baseline) * 4);
overflow-y: auto;

&__debug-controls {
grid-column: 1 / -1;
display: grid;
gap: calc(var(--default-grid-baseline) * 2);
}

&__debug-control {
display: grid;
gap: var(--default-grid-baseline);
}

&__debug-control-header {
display: flex;
justify-content: space-between;
gap: calc(var(--default-grid-baseline) * 2);
font-size: 12px;
}

&__element {
border: none;
margin: 0 !important;
Expand Down
2 changes: 2 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import pinia from './stores/pinia.ts'
import { useSidebarStore } from './stores/sidebar.ts'
import { NextcloudGlobalsVuePlugin } from './utils/NextcloudGlobalsVuePlugin.js'
import { exposeVirtualBackgroundDebugConfig } from './utils/media/effects/virtual-background/runtimeConfig.js'

Check failure on line 17 in src/main.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected "./utils/media/effects/virtual-background/runtimeConfig.js" to come before "./utils/NextcloudGlobalsVuePlugin.js"

import './init.js'
// Leaflet icon patch
Expand Down Expand Up @@ -104,6 +105,7 @@
if (!window.OCA.Talk) {
window.OCA.Talk = reactive({})
}
exposeVirtualBackgroundDebugConfig(window.OCA.Talk)
OCA.Talk.instance = instance
OCA.Talk.Settings = SettingsAPI

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
timerWorkerScript,
} from './TimerWorker.js'
import WebGLCompositor from './WebGLCompositor.js'
import { DEFAULT_VIRTUAL_BACKGROUND_DEBUG_CONFIG, virtualBackgroundDebugConfig } from './runtimeConfig.js'

Check failure on line 16 in src/utils/media/effects/virtual-background/VideoStreamBackgroundEffect.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected "./runtimeConfig.js" to come before "./WebGLCompositor.js"

// Cache MediaPipe resources to avoid loading them multiple times.
let _WasmFileset = null
Expand Down Expand Up @@ -82,6 +83,10 @@
this._inputVideoElement = document.createElement('video')
this._bgChanged = false
this._prevBgMode = null
this._inputTrackFrameRate = null
this._frameRate = virtualBackgroundDebugConfig.DEFAULT_FRAME_RATE
this._runInferenceInterval = 1000 / Math.min(this._frameRate, virtualBackgroundDebugConfig.MAX_SEGMENTATION_FRAME_RATE)
this._lastInferenceTime = 0
}

/**
Expand Down Expand Up @@ -184,6 +189,55 @@
}
}

/**
* Update the input frame rate and derive the rate of segmentation read from it.
*
* @private
* @param {number|string} frameRate - Input frame rate from track settings.
* @return {void}
*/
_resolveFrameRate(frameRate) {
const configuredFrameRate = virtualBackgroundDebugConfig.DEFAULT_FRAME_RATE
const parsedFrameRate = parseInt(frameRate, 10)

if (Number.isNaN(parsedFrameRate)) {
return configuredFrameRate
}

if (configuredFrameRate === DEFAULT_VIRTUAL_BACKGROUND_DEBUG_CONFIG.DEFAULT_FRAME_RATE) {
return parsedFrameRate
}

return Math.min(parsedFrameRate, configuredFrameRate)
}

_syncFrameRateConfig() {
const nextFrameRate = this._resolveFrameRate(this._inputTrackFrameRate)
const nextRunInferenceInterval = 1000 / Math.min(nextFrameRate, virtualBackgroundDebugConfig.MAX_SEGMENTATION_FRAME_RATE)
const didFrameRateChange = this._frameRate !== nextFrameRate

this._frameRate = nextFrameRate
this._runInferenceInterval = nextRunInferenceInterval

if (!didFrameRateChange || !this._outputStream) {
return
}

const outputTrack = this._outputStream.getVideoTracks()[0]
if (!outputTrack) {
return
}

outputTrack.applyConstraints({ frameRate: this._frameRate }).catch((error) => {
console.error('Frame rate could not be adjusted in background effect', error)
})
}

_updateFrameRate(frameRate) {
this._inputTrackFrameRate = frameRate
this._syncFrameRateConfig()
}

/**
* Process MediaPipe segmentation result and update internal mask.
*
Expand Down Expand Up @@ -274,9 +328,18 @@
this._frameId = this._lastFrameId
}

// Run inference if ready
if (this._loaded && this._frameId === this._lastFrameId) {
this._syncFrameRateConfig()

const now = performance.now()

// Run inference if ready and enough time passed since last _runInference() call
// Otherwise, reuse the mask from previous segmentation read
if (
this._loaded && this._frameId === this._lastFrameId
&& now - this._lastInferenceTime >= this._runInferenceInterval
) {
this._frameId++
this._lastInferenceTime = now
this._runInference().catch((e) => console.error(e))
} else if (this._useWebGL) {
this.runPostProcessing()
Expand Down Expand Up @@ -417,14 +480,13 @@
const backgroundBlurValue = this._options.virtualBackground.blurValue * scaledBlurFactor
const edgesBlurValue = (backgroundType === VIRTUAL_BACKGROUND.BACKGROUND_TYPE.IMAGE ? 4 : 8) * scaledBlurFactor

if (!this._outputCanvasElement.width
|| !this._outputCanvasElement.height) {
return
// Update canvas size only when changed / unset
if (this._outputCanvasElement.width !== width
|| this._outputCanvasElement.height !== height) {
this._outputCanvasElement.width = width
this._outputCanvasElement.height = height
}

this._outputCanvasElement.width = width
this._outputCanvasElement.height = height

if (this._useWebGL) {
if (!this._glFx) {
return
Expand Down Expand Up @@ -606,7 +668,7 @@
const { height, frameRate, width }
= firstVideoTrack.getSettings ? firstVideoTrack.getSettings() : firstVideoTrack.getConstraints()

this._frameRate = parseInt(frameRate, 10)
this._updateFrameRate(frameRate)

this._outputCanvasElement.width = parseInt(width, 10)
this._outputCanvasElement.height = parseInt(height, 10)
Expand Down Expand Up @@ -644,6 +706,7 @@

this._frameId = -1
this._lastFrameId = -1
this._lastInferenceTime = 0

this._bgChanged = true

Expand All @@ -662,14 +725,11 @@
const { frameRate }
= firstVideoTrack.getSettings ? firstVideoTrack.getSettings() : firstVideoTrack.getConstraints()

this._frameRate = parseInt(frameRate, 10)

this._outputStream.getVideoTracks()[0].applyConstraints({ frameRate: this._frameRate }).catch((error) => {
console.error('Frame rate could not be adjusted in background effect', error)
})
this._updateFrameRate(frameRate)

this._frameId = -1
this._lastFrameId = -1
this._lastInferenceTime = 0
}

/**
Expand Down
31 changes: 6 additions & 25 deletions src/utils/media/effects/virtual-background/WebGLCompositor.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,7 @@
*/
// @flow

/**
* Number of horizontal + vertical blur iterations applied.
* Larger value - stronger blur effect. Low GPU impact.
*/
const DEFAULT_BLUR_PASSES = 3
/**
* Spatial radius of the bilateral filter.
* Larger value - smoother mask over a wider area, softer edges. High GPU impact.
*/
const SIGMA_SPACE = 5
/**
* Range sensitivity of the bilateral filter.
* Larger value: more smoothing across edges. Low GPU impact.
*/
const SIGMA_COLOR = 0.15
/**
* Sampling stride scale for the bilateral kernel.
* Larger value - faster processing, rougher mask refinement. High GPU impact.
*/
const SPARSITY_FACTOR = 1
import { virtualBackgroundDebugConfig } from './runtimeConfig.js'

/**
* WebGL-based compositor for background effects.
Expand Down Expand Up @@ -487,10 +468,10 @@ export default class WebGLCompositor {
// Calculate filter parameters
const texelWidth = 1 / width
const texelHeight = 1 / height
const step = Math.max(1, Math.sqrt(SIGMA_SPACE) * SPARSITY_FACTOR)
const radius = SIGMA_SPACE
const step = Math.max(1, Math.sqrt(virtualBackgroundDebugConfig.SIGMA_SPACE) * virtualBackgroundDebugConfig.SPARSITY_FACTOR)
const radius = virtualBackgroundDebugConfig.SIGMA_SPACE
const offset = step > 1 ? step * 0.5 : 0
const sigmaTexel = Math.max(texelWidth, texelHeight) * SIGMA_SPACE
const sigmaTexel = Math.max(texelWidth, texelHeight) * virtualBackgroundDebugConfig.SIGMA_SPACE

// Set uniforms
gl.uniform1i(gl.getUniformLocation(this.progBilateral, 'u_inputFrame'), 0)
Expand All @@ -500,7 +481,7 @@ export default class WebGLCompositor {
gl.uniform1f(gl.getUniformLocation(this.progBilateral, 'u_radius'), radius)
gl.uniform1f(gl.getUniformLocation(this.progBilateral, 'u_offset'), offset)
gl.uniform1f(gl.getUniformLocation(this.progBilateral, 'u_sigmaTexel'), sigmaTexel)
gl.uniform1f(gl.getUniformLocation(this.progBilateral, 'u_sigmaColor'), SIGMA_COLOR)
gl.uniform1f(gl.getUniformLocation(this.progBilateral, 'u_sigmaColor'), virtualBackgroundDebugConfig.SIGMA_COLOR)

// Bind textures
gl.activeTexture(gl.TEXTURE0)
Expand Down Expand Up @@ -550,7 +531,7 @@ export default class WebGLCompositor {
gl.activeTexture(gl.TEXTURE1)
gl.bindTexture(gl.TEXTURE_2D, this.texMaskFiltered)

for (let i = 0; i < DEFAULT_BLUR_PASSES; i++) {
for (let i = 0; i < virtualBackgroundDebugConfig.DEFAULT_BLUR_PASSES; i++) {
// Horizontal pass
gl.uniform2f(gl.getUniformLocation(this.progBlur, 'u_texelSize'), 0, texelHeight)
gl.bindFramebuffer(gl.FRAMEBUFFER, this.fboBlur1)
Expand Down
Loading
Loading