Skip to content

Commit e42ef7e

Browse files
authored
Merge pull request #21299 from wireapp/feat/offering-vga-and-hd-WPB-25235
feat: offering automated resolution changes [WPB-25235]
2 parents c4f55a6 + 21231b5 commit e42ef7e

16 files changed

Lines changed: 1153 additions & 152 deletions

File tree

apps/webapp/src/script/components/calling/VideoControls/videoBackgroundPerformancePanel/videoBackgroundPerformancePanel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ type PerformancePanelProps = {
4949
backgroundEffectsHandler: BackgroundEffectsHandler;
5050
};
5151

52-
const QUALITY_OPTIONS: readonly QualityMode[] = ['auto', 'superhigh', 'high', 'medium', 'low', 'bypass'];
52+
const QUALITY_OPTIONS: readonly QualityMode[] = ['auto', 'fhd', 'hd', 'qhd', 'nhd', 'bypass'];
5353

5454
const formatMs = (value?: number | null): string => {
5555
return typeof value === 'number' ? `${value.toFixed(1)} ms` : '-';
@@ -66,7 +66,7 @@ const formatValue = (value?: string | number | null): string => {
6666
const getMetricRows = (renderMetrics: RenderMetrics) => {
6767
return Maybe.of(renderMetrics)
6868
.map(metrics => [
69-
{label: 'Quality', value: formatValue(metrics.tier)},
69+
{label: 'Quality', value: formatValue(metrics.tier).toUpperCase()},
7070
{label: 'Total', value: formatMs(metrics.avgTotalMs)},
7171
{label: 'Segmentation', value: formatMs(metrics.avgSegmentationMs)},
7272
{label: 'GPU', value: formatMs(metrics.avgGpuMs)},
@@ -135,7 +135,7 @@ const MetricsDisplay = ({capabilityInfo}: MetricsDisplayProps) => {
135135
};
136136

137137
const qualitySelectOptions = QUALITY_OPTIONS.map(option => ({
138-
label: option,
138+
label: option.toUpperCase(),
139139
value: option,
140140
}));
141141

apps/webapp/src/script/repositories/media/backgroundEffects/backgroundEffectsController.ts

Lines changed: 118 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,20 @@
1717
*
1818
*/
1919

20+
import {PerformanceSample} from 'Repositories/media/backgroundEffects/helper/samples';
21+
import {
22+
getBestMatchingQualityTier,
23+
QualityMode,
24+
QualityTier,
25+
Resolution,
26+
resolutionIsGreaterThanOrEqualTo,
27+
TIER_DEFINITIONS,
28+
} from 'Repositories/media/backgroundEffects/quality/definitions';
29+
import {QUALITY_TIERS, QualityController} from 'Repositories/media/backgroundEffects/quality/qualityController';
2030
import {BackgroundSource} from 'Repositories/media/VideoBackgroundEffects';
2131
import {getLogger, Logger} from 'Util/logger';
2232

23-
import {
24-
type CapabilityInfo,
25-
type EffectMode,
26-
type Metrics,
27-
QualityMode,
28-
type QualityTier,
29-
} from './backgroundEffectsWorkerTypes';
33+
import {type CapabilityInfo, type EffectMode, type Metrics, Mode} from './backgroundEffectsWorkerTypes';
3034
import {detectCapabilities} from './helper/capability';
3135
import {
3236
defaultOpts,
@@ -41,10 +45,15 @@ import {runSegmenter, updateSegmenterOptions} from './pipe/segmenter';
4145
// The shader's blur radius is 30 px, so a sigma in the ~10–20 px range gives
4246
// visually useful blur. Multiply by this factor to get from the 0–1 range.
4347
const BLUR_SIGMA_SCALE = 15;
48+
const DEFAULT_FRAME_RATE = 15;
49+
const MAX_WIDTH = 256;
50+
const MAX_HEIGHT = 144;
4451

4552
export class BackgroundEffectsController {
4653
private readonly logger: Logger;
4754

55+
private inputTrack: MediaStreamTrack | null = null;
56+
4857
private worker: Worker | null = null;
4958
private options: ProcessVideoTrackOptions = defaultOpts;
5059

@@ -57,7 +66,10 @@ export class BackgroundEffectsController {
5766
requestVideoFrameCallback: false,
5867
};
5968

60-
private maxQualityTier: QualityTier = 'superhigh';
69+
private qualityController: QualityController | null = null;
70+
private qualitySampleQueue = Promise.resolve();
71+
private maxResolution: Resolution = TIER_DEFINITIONS.hd.resolution;
72+
private maxQualityTier: QualityTier = 'hd';
6173
private refcount = 0;
6274

6375
/**
@@ -79,8 +91,21 @@ export class BackgroundEffectsController {
7991
const trackCapabilities = inputTrack.getCapabilities();
8092
const trackSettings = inputTrack.getSettings();
8193
const trackConstraints = inputTrack.getConstraints();
82-
const {width, height, frameRate} = trackSettings;
94+
let {width, height, frameRate} = trackSettings;
95+
if (!width) {
96+
width = TIER_DEFINITIONS[QUALITY_TIERS.HD].resolution.width;
97+
}
98+
if (!height) {
99+
height = TIER_DEFINITIONS[QUALITY_TIERS.HD].resolution.height;
100+
}
101+
if (!frameRate) {
102+
frameRate = DEFAULT_FRAME_RATE;
103+
}
104+
this.maxResolution = {width, height};
105+
this.maxQualityTier = getBestMatchingQualityTier(this.maxResolution).tier;
106+
this.qualityController = new QualityController(frameRate, this.maxQualityTier);
83107

108+
this.inputTrack = inputTrack;
84109
this.logger.info(`start: ${width}x${height} ${frameRate}fps`, {
85110
trackCapabilities,
86111
trackSettings,
@@ -90,6 +115,8 @@ export class BackgroundEffectsController {
90115
const {readable} = new TrackProcessor({track: inputTrack});
91116

92117
const canvas = document.createElement('canvas');
118+
canvas.width = MAX_WIDTH;
119+
canvas.height = MAX_HEIGHT;
93120
const outputTrack = canvas.captureStream(frameRate).getVideoTracks()[0];
94121
const offscreen = canvas.transferControlToOffscreen();
95122

@@ -109,16 +136,30 @@ export class BackgroundEffectsController {
109136
transferables,
110137
);
111138
onWorkerMessage = ({data}: MessageEvent) => {
112-
const {name, stats} = data as {name: string; stats: Metrics};
139+
const {name} = data as {name: string};
113140
if (name === 'stats' && this.onMetrics) {
141+
const {stats} = data as {stats: Metrics};
114142
this.onMetrics(stats);
115143
}
144+
145+
if (name === 'performanceSample' && this.qualityController !== null) {
146+
const {sample, mode} = data as {sample: PerformanceSample; mode: Mode};
147+
this.enqueuePerformanceSample(sample, mode);
148+
}
116149
};
117150

118151
this.worker.addEventListener('message', onWorkerMessage);
119152
} else {
120153
const {options: workerOptions} = getWorkerOptions(resolved);
121-
await runSegmenter(offscreen, readable, workerOptions, stats => this.onMetrics?.(stats));
154+
await runSegmenter(
155+
offscreen,
156+
readable,
157+
workerOptions,
158+
stats => this.onMetrics?.(stats),
159+
(sample: PerformanceSample, mode: Mode) => {
160+
this.enqueuePerformanceSample(sample, mode);
161+
},
162+
);
122163
}
123164

124165
outputTrack.stop = () => {
@@ -176,9 +217,15 @@ export class BackgroundEffectsController {
176217
}
177218
}
178219

179-
public setQuality(quality: QualityMode): void {
220+
public async setQuality(quality: QualityMode): Promise<void> {
180221
this.logger.info('setQuality', quality);
181-
this.options = {...this.options, quality};
222+
223+
let requestedQuality = quality;
224+
if (quality !== 'auto') {
225+
requestedQuality = await this.changeResolution(quality);
226+
}
227+
228+
this.options = {...this.options, quality: requestedQuality};
182229
this.pushOptionsUpdate();
183230
}
184231

@@ -235,6 +282,64 @@ export class BackgroundEffectsController {
235282
updateSegmenterOptions(finalOptions);
236283
}
237284
}
285+
286+
private async changeResolution(quality: QualityMode): Promise<QualityMode> {
287+
if (!this.inputTrack || quality === 'auto') {
288+
return quality;
289+
}
290+
291+
const mediaConstraints = this.inputTrack.getConstraints();
292+
293+
let newResolution: Resolution;
294+
let newQualityTier: QualityTier;
295+
296+
if (quality === 'bypass') {
297+
newResolution = this.maxResolution;
298+
newQualityTier = quality;
299+
} else {
300+
const requestedResolution = TIER_DEFINITIONS[quality].resolution;
301+
302+
if (this.maxResolution === null || resolutionIsGreaterThanOrEqualTo(this.maxResolution, requestedResolution)) {
303+
newResolution = requestedResolution;
304+
newQualityTier = quality;
305+
} else {
306+
newResolution = this.maxResolution;
307+
newQualityTier = this.maxQualityTier;
308+
}
309+
}
310+
311+
mediaConstraints.width = {ideal: newResolution.width};
312+
mediaConstraints.height = {ideal: newResolution.height};
313+
314+
try {
315+
await this.inputTrack.applyConstraints(mediaConstraints);
316+
return newQualityTier;
317+
} catch (error: unknown) {
318+
this.logger.warn('Failed to change resolution', error);
319+
return this.maxQualityTier;
320+
}
321+
}
322+
323+
private enqueuePerformanceSample(sample: PerformanceSample, mode: Mode): void {
324+
this.qualitySampleQueue = this.qualitySampleQueue
325+
.then(() => this.onPerformanceSample(sample, mode))
326+
.catch((error: unknown) => {
327+
this.logger?.error?.('onPerformanceSample failed', {error});
328+
});
329+
}
330+
331+
private async onPerformanceSample(sample: PerformanceSample, mode: Mode): Promise<void> {
332+
if (this.qualityController === null) {
333+
this.logger.warn('onPerformanceSample: qualityController is null');
334+
return;
335+
}
336+
const tier = this.qualityController.update(sample, mode);
337+
338+
if (tier.tier === this.qualityController.getCurrentTier()) {
339+
return;
340+
}
341+
return this.setQuality(tier.tier);
342+
}
238343
}
239344

240345
// ---------------------------------------------------------------------------

apps/webapp/src/script/repositories/media/backgroundEffects/backgroundEffectsWorkerTypes.ts

Lines changed: 2 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
* including effect modes, quality settings, worker messages, and configuration options.
2525
*/
2626

27+
import {QualityMode, QualityTier} from 'Repositories/media/backgroundEffects/quality/definitions';
28+
2729
/**
2830
* Effect mode for background processing.
2931
* - 'blur': Applies blur effect to the background
@@ -49,15 +51,6 @@ export type Mode = Exclude<EffectMode, 'passthrough'>;
4951
*/
5052
export type DebugMode = 'off' | 'maskOverlay' | 'maskOnly' | 'edgeOnly' | 'classOverlay' | 'classOnly';
5153

52-
export type QualityTier = 'superhigh' | 'high' | 'medium' | 'low' | 'bypass';
53-
54-
/**
55-
* Quality mode for rendering performance.
56-
* - 'auto': Adaptive quality based on performance metrics
57-
* - QualityTier
58-
*/
59-
export type QualityMode = 'auto' | QualityTier;
60-
6154
/**
6255
* Optional mapping of quality tiers to segmentation model paths.
6356
*/
@@ -91,85 +84,6 @@ export type QualityPolicyResolver = (capabilities: CapabilityInfo) => QualityPol
9184
*/
9285
export type PipelineType = 'worker-webgl2' | 'main-webgl2' | 'canvas2d' | 'passthrough';
9386

94-
/**
95-
* Quality tier parameters that control rendering performance and visual quality.
96-
*
97-
* These parameters are generated by QualityController and used by WebGlRenderer
98-
* to configure the rendering pipeline. They balance visual quality against
99-
* performance by adjusting segmentation resolution, processing cadence, and
100-
* post-processing effects.
101-
*/
102-
export interface QualityTierParams {
103-
/** Quality tier identifier */
104-
tier: QualityTier;
105-
/** Segmentation mask width in pixels. Lower values reduce CPU/ML cost. */
106-
segmentationWidth: number;
107-
/** Segmentation mask height in pixels. Lower values reduce CPU/ML cost. */
108-
segmentationHeight: number;
109-
/** Segmentation cadence (process every Nth frame). Higher values reduce CPU/ML cost. */
110-
segmentationCadence: number;
111-
/** Scale factor for mask refinement pass (0-1). Lower values reduce GPU cost. */
112-
maskRefineScale: number;
113-
/** Scale factor for blur downsampling (0-1). Lower values reduce GPU cost. */
114-
blurDownsampleScale: number;
115-
/** Blur radius in pixels. Lower values reduce GPU cost. */
116-
blurRadius: number;
117-
/** Joint bilateral filter radius in pixels. Lower values reduce GPU cost. */
118-
bilateralRadius: number;
119-
/** Spatial sigma for joint bilateral filter. Controls spatial smoothing. */
120-
bilateralSpatialSigma: number;
121-
/** Range sigma for joint bilateral filter. Controls edge preservation. */
122-
bilateralRangeSigma: number;
123-
/** Lower threshold for soft matte edge (0-1). Controls where soft edges begin. */
124-
softLow: number;
125-
/** Upper threshold for soft matte edge (0-1). Controls where soft edges end. */
126-
softHigh: number;
127-
/** Lower threshold for matte cutoff (0-1). Pixels below this are considered background. */
128-
matteLow: number;
129-
/** Upper threshold for matte cutoff (0-1). Pixels above this are considered foreground. */
130-
matteHigh: number;
131-
/** Hysteresis value for matte thresholds to prevent flickering (0-1). */
132-
matteHysteresis: number;
133-
/** Temporal smoothing alpha (0-1). Higher values increase temporal stability. */
134-
temporalAlpha: number;
135-
/** If true, bypass all processing and pass through original frames. */
136-
bypass: boolean;
137-
}
138-
139-
/**
140-
* Background source for virtual background mode (static image).
141-
*
142-
* Used internally by BackgroundEffectsController to store and manage
143-
* background images for virtual background replacement.
144-
*/
145-
export interface BackgroundSourceImage {
146-
/** Discriminator for type narrowing. */
147-
type: 'image';
148-
/** Image bitmap data (transferred to worker if using worker pipeline). */
149-
bitmap: ImageBitmap;
150-
/** Image width in pixels. */
151-
width: number;
152-
/** Image height in pixels. */
153-
height: number;
154-
}
155-
156-
/**
157-
* Background source for virtual background mode (video frame).
158-
*
159-
* Used internally by BackgroundEffectsController to store and manage
160-
* background video frames for virtual background replacement.
161-
*/
162-
export interface BackgroundSourceVideoFrame {
163-
/** Discriminator for type narrowing. */
164-
type: 'video';
165-
/** Video frame bitmap data (transferred to worker if using worker pipeline). */
166-
bitmap: ImageBitmap;
167-
/** Frame width in pixels. */
168-
width: number;
169-
/** Frame height in pixels. */
170-
height: number;
171-
}
172-
17387
/**
17488
* Performance metrics tracked during frame processing.
17589
*

apps/webapp/src/script/repositories/media/backgroundEffects/helper/samples.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* Wire
3-
* Copyright (C) 2025 Wire Swiss GmbH
3+
* Copyright (C) 2026 Wire Swiss GmbH
44
*
55
* This program is free software: you can redistribute it and/or modify
66
* it under the terms of the GNU General Public License as published by

apps/webapp/src/script/repositories/media/backgroundEffects/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,12 @@ export type {EffectMode} from './backgroundEffectsWorkerTypes';
7474
*/
7575
export type {DebugMode} from './backgroundEffectsWorkerTypes';
7676

77-
export type {QualityTier} from './backgroundEffectsWorkerTypes';
77+
export type {QualityTier} from './quality/definitions';
7878

7979
/**
8080
* Quality mode type ('auto' or fixed QualityTier).
8181
*/
82-
export type {QualityMode} from './backgroundEffectsWorkerTypes';
82+
export type {QualityMode} from './quality/definitions';
8383

8484
/**
8585
* BackgroundEffectsRenderingPipeline selection type.

apps/webapp/src/script/repositories/media/backgroundEffects/pipe/options.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,12 @@ export const defaultOpts = {
9999

100100
// Segmenter options.
101101
borderSmooth: 0,
102-
smoothing: 0.8,
102+
smoothing: 1,
103103
smoothstepMin: 0.6,
104104
smoothstepMax: 0.9,
105105
restartEvery: 0,
106106
bgBlur: 0,
107-
bgBlurRadius: 30,
107+
bgBlurRadius: 5,
108108

109109
// Filter options.
110110
enableFilters: false,

apps/webapp/src/script/repositories/media/backgroundEffects/pipe/renderer.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -441,11 +441,8 @@ export class WebGLRenderer {
441441
}
442442
const {gl, fbo, storedStateTextures, stateUpdateProgram, stateUpdateLocations, blendProgram, blendLocations} = this;
443443

444-
const {displayWidth: width, displayHeight: height} = videoFrame;
445-
if (this.canvas.width !== width || this.canvas.height !== height) {
446-
this.canvas.width = width;
447-
this.canvas.height = height;
448-
}
444+
const width = this.canvas.width;
445+
const height = this.canvas.height;
449446

450447
if (!categoryTexture || !confidenceTexture) {
451448
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);

0 commit comments

Comments
 (0)