Files
upstream-mirror 1d2bfcac0c
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s
snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
2026-09-15 15:56:32 +05:00

124 lines
4.1 KiB
TypeScript

import { ICompositeOptions, ICompositor } from './ICompositor';
/**
* Canvas 2D three-pass compositor.
*
* Extracted from V1's {@code runPostProcessing} and V2's {@code _compositeFallback}.
* Uses {@code globalCompositeOperation} to blend the segmentation mask, camera
* foreground, and background into a single output frame on the CPU.
*
* Falls back gracefully when context creation fails. Callers should check
* {@link Canvas2DCompositor#isAvailable} before use.
*/
export default class Canvas2DCompositor implements ICompositor {
_canvas: HTMLCanvasElement;
_ctx: CanvasRenderingContext2D | null = null;
_maskCanvas: HTMLCanvasElement | null = null;
_maskCanvasCtx: CanvasRenderingContext2D | null = null;
/**
* Creates a Canvas2DCompositor with an internal output canvas and mask helper canvas.
*/
constructor() {
this._canvas = document.createElement('canvas');
this._ctx = this._canvas.getContext('2d');
this._maskCanvas = document.createElement('canvas');
this._maskCanvasCtx = this._maskCanvas.getContext('2d');
}
/**
* Returns true if a 2D rendering context is active and ready.
*
* @returns {boolean}
*/
get isAvailable(): boolean {
return this._ctx !== null;
}
/**
* Returns the canvas element that receives the composited output each frame.
*
* @returns {HTMLCanvasElement}
*/
get outputCanvas(): HTMLCanvasElement {
return this._canvas;
}
/**
* Composites one frame using three-pass Canvas 2D blending.
*
* 1. Draw the blurred segmentation mask (person silhouette).
* 2. Clip the camera foreground to the mask via {@code source-in}.
* 3. Draw the background behind the person via {@code destination-over}.
*
* The mask {@code ImageData} is painted onto a persistent helper canvas via
* {@code putImageData} which writes raw bytes without premultiplied-alpha
* conversion, then scaled to the output dimensions with {@code drawImage}.
*
* @param {CanvasImageSource} camera - Live camera frame.
* @param {CanvasImageSource} background - Pre-rendered background (blur or image).
* @param {ImageData} maskData - Raw segmentation mask bytes.
* @param {ICompositeOptions} options - Edge thresholds, blur radius, and first-frame flag.
* @returns {void}
*/
composite(
camera: CanvasImageSource,
background: CanvasImageSource,
maskData: ImageData,
options: ICompositeOptions): void {
const ctx = this._ctx;
const maskCanvas = this._maskCanvas;
if (!ctx || !maskCanvas) {
return;
}
const { width, height } = this._canvas;
ctx.globalCompositeOperation = 'copy';
if (maskCanvas.width !== maskData.width
|| maskCanvas.height !== maskData.height) {
maskCanvas.width = maskData.width;
maskCanvas.height = maskData.height;
}
// putImageData writes raw bytes — no premultiplied-alpha distortion.
this._maskCanvasCtx?.putImageData(maskData, 0, 0);
ctx.filter = options.cssBlurFilter;
ctx.drawImage(maskCanvas, 0, 0, width, height);
// Clip foreground to mask.
ctx.globalCompositeOperation = 'source-in';
ctx.filter = 'none';
ctx.drawImage(camera, 0, 0, width, height);
// Draw background behind the person.
ctx.globalCompositeOperation = 'destination-over';
ctx.drawImage(background as CanvasImageSource, 0, 0, width, height);
}
/**
* Releases all rendering contexts and helper canvases.
*
* @returns {void}
*/
dispose(): void {
this._ctx = null;
this._maskCanvas = null;
this._maskCanvasCtx = null;
}
/**
* Resizes the output canvas to the given dimensions.
*
* @param {number} width - New width in pixels.
* @param {number} height - New height in pixels.
* @returns {void}
*/
resize(width: number, height: number): void {
this._canvas.width = width;
this._canvas.height = height;
}
}