import { appBasePath } from "@agent-native/core/client/api-path"; import type { ImageSegmenter } from "@mediapipe/tasks-vision"; /** * A processed camera stream whose background is blurred while the person stays * sharp (Zoom / Loom style). Produced once in the recorder engine and shared by * both the live preview bubble and the baked-in recording composite, so "what * you see is what's recorded". */ export interface CameraBlurHandle { /** The processed stream, or the original `source` stream when `active` is false. */ stream: MediaStream; /** False when segmentation was unavailable and we transparently fell back to raw. */ readonly active: boolean; /** * Update the background blur radius (px) live, without rebuilding the * segmenter. No-op on the passthrough fallback handle. */ setBlurPx(px: number): void; cleanup(): void; } export interface CameraBlurOptions { /** CSS blur radius applied to the background, in px. Default 12. */ blurPx?: number; /** * How often segmentation runs, in frames per second. Kept below the 30fps * capture rate to bound CPU/GPU cost — the small bubble does not need a fresh * mask every captured frame. Default 20. */ segmentationFps?: number; } export const DEFAULT_BLUR_PX = 12; export const MIN_BLUR_PX = 2; export const MAX_BLUR_PX = 30; const DEFAULT_SEGMENTATION_FPS = 20; const CAPTURE_FPS = 30; /** * Max dimension (px) of the frame we feed the segmenter. The model resizes to * its own 256² input internally and upsamples the mask back to this size, so * keeping the input small bounds the per-frame mask readback + alpha loop cost * regardless of the camera's native resolution. The soft mask is scaled back up * to the camera resolution with bilinear filtering when compositing. */ const SEG_MAX_DIM = 256; const MODEL_PATH = "/mediapipe/selfie_segmenter.tflite"; const WASM_PATH = "/mediapipe/wasm"; function positive(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value) && value > 0; } function sourceDimensions( video: HTMLVideoElement, stream: MediaStream, ): { width: number; height: number } { if (positive(video.videoWidth) && positive(video.videoHeight)) { return { width: video.videoWidth, height: video.videoHeight }; } const settings = stream.getVideoTracks()[0]?.getSettings(); return { width: positive(settings?.width) ? Math.round(settings.width) : 640, height: positive(settings?.height) ? Math.round(settings.height) : 480, }; } /** Hidden, off-screen `