@@ -21,14 +21,16 @@ export class VirtualBackground {
2121
2222 private canvas ! : OffscreenCanvas ;
2323 private segmenter : ImageSegmenter | null = null ;
24- private isSegmenterReady = false ;
2524 private webGlRenderer ! : WebGLRenderer ;
2625 private abortController : AbortController ;
2726
2827 private segmenterDelayTotal = 0 ;
2928 private frames = 0 ;
3029 private lastStatsTime = 0 ;
3130
31+ private latestCategoryMask : WebGLTexture | undefined = undefined ;
32+ private latestConfidenceMask : WebGLTexture | undefined = undefined ;
33+
3234 constructor (
3335 private readonly track : MediaStreamVideoTrack ,
3436 private readonly options : BackgroundOptions = { } ,
@@ -59,22 +61,43 @@ export class VirtualBackground {
5961
6062 const opts = await this . initializeSegmenterOptions ( ) ;
6163
64+ let lastFrameTime = - 1 ;
6265 const transformStream = new TransformStream < VideoFrame , VideoFrame > ( {
63- transform : async ( frame , controller ) => {
66+ transform : ( frame , controller ) => {
6467 try {
6568 if ( this . abortController . signal . aborted ) {
66- return frame . close ( ) ;
69+ frame . close ( ) ;
70+ return ;
6771 }
6872
69- const processed = await this . transform ( frame , opts ) ;
70- controller . enqueue ( processed ) ;
71- } catch ( e ) {
72- console . error ( '[virtual-background] error processing frame:' , e ) ;
73- this . hooks . onError ?.( e ) ;
73+ if (
74+ this . canvas . width !== frame . displayWidth ||
75+ this . canvas . height !== frame . displayHeight
76+ ) {
77+ this . canvas . width = frame . displayWidth ;
78+ this . canvas . height = frame . displayHeight ;
79+ }
7480
75- if ( ! this . abortController . signal . aborted ) {
76- controller . enqueue ( frame ) ;
81+ const currentTime = frame . timestamp ;
82+ const hasNewFrame = currentTime !== lastFrameTime ;
83+
84+ lastFrameTime = currentTime ;
85+ if ( hasNewFrame ) {
86+ this . runSegmentation ( frame ) ;
7787 }
88+ this . webGlRenderer . render (
89+ frame ,
90+ opts ,
91+ this . latestCategoryMask ,
92+ this . latestConfidenceMask ,
93+ ) ;
94+
95+ controller . enqueue (
96+ new VideoFrame ( this . canvas , { timestamp : frame . timestamp } ) ,
97+ ) ;
98+ } catch ( e ) {
99+ console . error ( '[virtual-background] error processing frame:' , e ) ;
100+ onError ?.( e ) ;
78101 } finally {
79102 frame . close ( ) ;
80103 }
@@ -84,7 +107,6 @@ export class VirtualBackground {
84107 this . segmenter . close ( ) ;
85108 this . segmenter = null ;
86109 }
87- this . isSegmenterReady = false ;
88110 } ,
89111 } ) ;
90112
@@ -103,6 +125,47 @@ export class VirtualBackground {
103125 return this . generator ;
104126 }
105127
128+ private runSegmentation ( frame : VideoFrame ) {
129+ if ( ! this . segmenter ) return ;
130+
131+ const start = performance . now ( ) ;
132+
133+ this . segmenter . segmentForVideo ( frame , frame . timestamp , ( result ) => {
134+ try {
135+ this . latestCategoryMask = result . categoryMask ?. getAsWebGLTexture ( ) ;
136+
137+ this . latestConfidenceMask =
138+ result . confidenceMasks ?. [ 0 ] ?. getAsWebGLTexture ( ) ;
139+
140+ const now = performance . now ( ) ;
141+ this . segmenterDelayTotal += now - start ;
142+ this . frames ++ ;
143+
144+ if ( this . lastStatsTime === 0 ) {
145+ this . lastStatsTime = now ;
146+ }
147+
148+ if ( now - this . lastStatsTime > 1000 ) {
149+ const delay =
150+ Math . round ( ( this . segmenterDelayTotal / this . frames ) * 100 ) / 100 ;
151+ const fps = Math . round (
152+ ( 1000 * this . frames ) / ( now - this . lastStatsTime ) ,
153+ ) ;
154+
155+ this . hooks . onStats ?.( { delay, fps, timestamp : now } ) ;
156+
157+ this . lastStatsTime = now ;
158+ this . segmenterDelayTotal = 0 ;
159+ this . frames = 0 ;
160+ }
161+ } catch ( err ) {
162+ console . error ( '[virtual-background] segmentation error:' , err ) ;
163+ } finally {
164+ result . close ( ) ;
165+ }
166+ } ) ;
167+ }
168+
106169 /**
107170 * Loads and initializes the MediaPipe `ImageSegmenter`.
108171 */
@@ -130,82 +193,14 @@ export class VirtualBackground {
130193 outputConfidenceMasks : true ,
131194 canvas : this . canvas ,
132195 } ) ;
133-
134- this . isSegmenterReady = true ;
135196 } catch ( error ) {
136197 console . error (
137198 '[virtual-background] Failed to initialize MediaPipe segmenter:' ,
138199 error ,
139200 ) ;
140- this . isSegmenterReady = false ;
141201 }
142202 }
143203
144- /**
145- * Processes a single video frame.
146- *
147- * Performs segmentation via MediaPipe and then composites the frame
148- * through the WebGL renderer to apply background effects.
149- *
150- * @param frame - The incoming frame from the processor.
151- * @param opts - The segmentation options to use.
152- *
153- * @returns A new `VideoFrame` containing the processed image.
154- */
155- private async transform (
156- frame : VideoFrame ,
157- opts : SegmenterOptions ,
158- ) : Promise < VideoFrame > {
159- if ( this . isSegmenterReady && this . segmenter ) {
160- try {
161- const start = performance . now ( ) ;
162- await new Promise < void > ( ( resolve ) => {
163- this . segmenter ! . segmentForVideo ( frame , frame . timestamp , ( result ) => {
164- const categoryMask = result . categoryMask ! . getAsWebGLTexture ( ) ;
165- const confidenceMask =
166- result . confidenceMasks ! [ 0 ] . getAsWebGLTexture ( ) ;
167-
168- this . webGlRenderer . render (
169- frame ,
170- opts ,
171- categoryMask ,
172- confidenceMask ,
173- ) ;
174-
175- const now = performance . now ( ) ;
176- this . segmenterDelayTotal += now - start ;
177- this . frames ++ ;
178-
179- if ( this . lastStatsTime === 0 ) {
180- this . lastStatsTime = now ;
181- }
182-
183- if ( now - this . lastStatsTime > 1000 ) {
184- const delay =
185- Math . round ( ( this . segmenterDelayTotal / this . frames ) * 100 ) /
186- 100 ;
187- const fps = Math . round (
188- ( 1000 * this . frames ) / ( now - this . lastStatsTime ) ,
189- ) ;
190-
191- this . hooks . onStats ?.( { delay, fps, timestamp : now } ) ;
192-
193- this . lastStatsTime = now ;
194- this . segmenterDelayTotal = 0 ;
195- this . frames = 0 ;
196- }
197-
198- resolve ( ) ;
199- } ) ;
200- } ) ;
201- } catch ( error ) {
202- console . error ( '[virtual-background] Error during segmentation:' , error ) ;
203- }
204- }
205-
206- return new VideoFrame ( this . canvas , { timestamp : frame . timestamp } ) ;
207- }
208-
209204 private async loadBackground ( url : string | undefined ) {
210205 if ( ! url ) {
211206 return ;
@@ -271,6 +266,5 @@ export class VirtualBackground {
271266 this . segmenter . close ( ) ;
272267 this . segmenter = null ;
273268 }
274- this . isSegmenterReady = false ;
275269 }
276270}
0 commit comments