@voltkit-rtc/client 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +102 -0
- package/dist/cjs/diagnostics.d.ts +1 -1
- package/dist/cjs/diagnostics.js +1 -1
- package/dist/cjs/effects-core.d.ts +95 -0
- package/dist/cjs/effects-core.d.ts.map +1 -0
- package/dist/cjs/effects-core.js +154 -0
- package/dist/cjs/effects-core.js.map +1 -0
- package/dist/cjs/effects-gl.d.ts +79 -0
- package/dist/cjs/effects-gl.d.ts.map +1 -0
- package/dist/cjs/effects-gl.js +351 -0
- package/dist/cjs/effects-gl.js.map +1 -0
- package/dist/cjs/index.d.ts +57 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +278 -64
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/noise-suppression.d.ts +44 -0
- package/dist/cjs/noise-suppression.d.ts.map +1 -0
- package/dist/cjs/noise-suppression.js +103 -0
- package/dist/cjs/noise-suppression.js.map +1 -0
- package/dist/cjs/processors.d.ts +34 -0
- package/dist/cjs/processors.d.ts.map +1 -0
- package/dist/cjs/processors.js +10 -0
- package/dist/cjs/processors.js.map +1 -0
- package/dist/cjs/video-effects.d.ts +93 -0
- package/dist/cjs/video-effects.d.ts.map +1 -0
- package/dist/cjs/video-effects.js +493 -0
- package/dist/cjs/video-effects.js.map +1 -0
- package/dist/esm/diagnostics.d.ts +1 -1
- package/dist/esm/diagnostics.js +1 -1
- package/dist/esm/effects-core.d.ts +95 -0
- package/dist/esm/effects-core.d.ts.map +1 -0
- package/dist/esm/effects-core.js +142 -0
- package/dist/esm/effects-core.js.map +1 -0
- package/dist/esm/effects-gl.d.ts +79 -0
- package/dist/esm/effects-gl.d.ts.map +1 -0
- package/dist/esm/effects-gl.js +347 -0
- package/dist/esm/effects-gl.js.map +1 -0
- package/dist/esm/index.d.ts +57 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +278 -64
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/noise-suppression.d.ts +44 -0
- package/dist/esm/noise-suppression.d.ts.map +1 -0
- package/dist/esm/noise-suppression.js +99 -0
- package/dist/esm/noise-suppression.js.map +1 -0
- package/dist/esm/processors.d.ts +34 -0
- package/dist/esm/processors.d.ts.map +1 -0
- package/dist/esm/processors.js +9 -0
- package/dist/esm/processors.js.map +1 -0
- package/dist/esm/video-effects.d.ts +93 -0
- package/dist/esm/video-effects.d.ts.map +1 -0
- package/dist/esm/video-effects.js +488 -0
- package/dist/esm/video-effects.js.map +1 -0
- package/package.json +31 -1
- package/src/diagnostics.ts +1 -1
- package/src/effects-core.ts +177 -0
- package/src/effects-gl.ts +384 -0
- package/src/index.ts +276 -63
- package/src/noise-suppression.ts +135 -0
- package/src/processors.ts +35 -0
- package/src/video-effects.ts +552 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
- **AI noise suppression: `@voltkit-rtc/client/noise-suppression`** (Agora AI Denoiser, LiveKit Krisp, ZEGOCLOUD
|
|
6
|
+
AI noise suppression). `createNoiseSuppressor({ workletUrl?, wasmUrl?, simdWasmUrl? })` is RNNoise in an
|
|
7
|
+
AudioWorklet (`@sapphi-red/web-noise-suppressor` 0.4.1, MIT, loaded only when used); files from jsDelivr at the
|
|
8
|
+
pinned version unless self-hosted. In Chromium, rumble-like noise went down by more than 60 dB.
|
|
9
|
+
- **`setAudioProcessor(processor | null)` and `audioProcessorInUse`** (LiveKit `setProcessor`): the microphone goes
|
|
10
|
+
through an `AudioTrackProcessor` (`{ name, process(track), destroy() }`) before it is sent. Swapped in place while
|
|
11
|
+
live; kept across device switches, a recovered microphone, reconnects and audio-quality changes. A failing
|
|
12
|
+
processor sends the microphone unprocessed and emits `error`. `mic-silent` is not checked while one is on.
|
|
13
|
+
- **Virtual background and beauty: `@voltkit-rtc/client/video-effects`** (LiveKit track processors, Agora/ZEGOCLOUD
|
|
14
|
+
virtual background and beauty, TRTC video effects). `createVideoEffects({ background: 'none' | 'blur' | { image },
|
|
15
|
+
blur, beauty, wasmPath?, modelUrl?, maxFrameRate?, onOverload? })`, `update()` live, `isVideoEffectsSupported()`.
|
|
16
|
+
WebGL2: mask-weighted blur (no halo), edges refined against the picture, cover-fitted images, beauty without a
|
|
17
|
+
face model. MediaPipe's selfie segmenter (landscape model for wide pictures, square for tall), from the optional
|
|
18
|
+
peer `@mediapipe/tasks-vision` at exactly 0.10.35 (later versions report usage to Google); nothing behind the
|
|
19
|
+
person shows until it is found. Insertable streams in Chrome/Edge, a hidden video on a worker timer elsewhere;
|
|
20
|
+
Chime-style adaptive segmentation on slow devices.
|
|
21
|
+
- **`setVideoProcessor(processor | null)` and `videoProcessorInUse`**: the camera goes through a `VideoTrackProcessor`
|
|
22
|
+
(`{ name, process(track), destroy() }`, same size out) before it is sent; the processed picture is also what `local`
|
|
23
|
+
shows. Swapped in place while live; kept across `switchDevice`, `replaceVideoTrack`, reconnects and quality changes,
|
|
24
|
+
without restarting the processor. The camera behind it is the one switched, constrained, watched and released.
|
|
25
|
+
A failing processor sends the camera unprocessed and emits `error`. `camera-fps-low` is not checked while one is on.
|
|
26
|
+
- **A faster first picture** (Agora/TRTC fast first frame). The join asks for the media transports and uses the ones
|
|
27
|
+
the server made with it (`join {transports}`, controllers from 2026-10-20; older ones are asked as before). The
|
|
28
|
+
DTLS parameters are sent without waiting for the reply, since mediasoup takes them after its handshake has begun:
|
|
29
|
+
ICE starts a round trip sooner, and a refusal rejoins. A video's first layers no longer wait before its resume.
|
|
30
|
+
Up to three round trips fewer before the first publish or the first video.
|
|
31
|
+
|
|
3
32
|
## 0.8.0
|
|
4
33
|
|
|
5
34
|
- **Media recovery:** send and receive transports restart ICE after failure or
|
package/README.md
CHANGED
|
@@ -245,6 +245,102 @@ off only for headphones or line-in music, or listeners hear their own echo.
|
|
|
245
245
|
Music (`publishMusic`) is a separate 128 kbps stereo track and is not affected:
|
|
246
246
|
it is HD and free whatever the microphone's quality (see [Music](#music)).
|
|
247
247
|
|
|
248
|
+
### Noise suppression (0.9.0)
|
|
249
|
+
|
|
250
|
+
AI noise suppression for the microphone, as Agora's AI Denoiser, LiveKit's
|
|
251
|
+
Krisp filter and ZEGOCLOUD's AI noise suppression do: RNNoise, a small neural
|
|
252
|
+
network, runs in the browser's audio thread and takes fans, traffic, air
|
|
253
|
+
conditioning and room rumble out of the voice. It is a separate import, so apps
|
|
254
|
+
that do not use it never download it:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
import { createNoiseSuppressor } from '@voltkit-rtc/client/noise-suppression';
|
|
258
|
+
|
|
259
|
+
await client.setAudioProcessor(createNoiseSuppressor()); // before or after publish
|
|
260
|
+
client.audioProcessorInUse; // the processor, or null
|
|
261
|
+
await client.setAudioProcessor(null); // send the microphone as it is again
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
- It applies to the microphone published now (swapped in place: listeners hear
|
|
265
|
+
no gap) and to every one after it: `switchDevice('audioinput', …)`, a
|
|
266
|
+
microphone recovered after unplugging, a reconnect, `setAudioQuality`.
|
|
267
|
+
Music and screen-share audio are not processed.
|
|
268
|
+
- The worklet (64 kB) and the model (150 kB) load the first time it runs, from
|
|
269
|
+
jsDelivr at the exact version this SDK was tested with. Under a strict
|
|
270
|
+
Content-Security-Policy, copy `rnnoise/workletProcessor.js`, `rnnoise.wasm`
|
|
271
|
+
and `rnnoise_simd.wasm` from `node_modules/@sapphi-red/web-noise-suppressor/dist`
|
|
272
|
+
to your site and pass their URLs:
|
|
273
|
+
`createNoiseSuppressor({ workletUrl, wasmUrl, simdWasmUrl })`.
|
|
274
|
+
- If it cannot start (the files are blocked, an old browser), the microphone
|
|
275
|
+
still goes out, unprocessed, and `error` says why. The next microphone tries
|
|
276
|
+
again.
|
|
277
|
+
- Turn it on from a click (a toggle, the join button): browsers start audio
|
|
278
|
+
processing only after the page has been touched, and the SDK resumes it on
|
|
279
|
+
the first tap or key press.
|
|
280
|
+
- `mic-silent` is not reported while a processor is on: a quiet room is
|
|
281
|
+
near-silence after denoising. Your own level in `audioLevels` is what
|
|
282
|
+
listeners hear.
|
|
283
|
+
- Measured in Chromium: rumble-like noise (fans, traffic, AC) went down by more
|
|
284
|
+
than 60 dB; steady white hiss, which it was not trained on, by 7-12 dB. Keep
|
|
285
|
+
the browser's own `echoCancellation`; its `noiseSuppression` can stay on too.
|
|
286
|
+
|
|
287
|
+
Your own processor works the same way: anything with `name`,
|
|
288
|
+
`process(track) → Promise<MediaStreamTrack>` and `destroy()`
|
|
289
|
+
(`AudioTrackProcessor`), such as a licensed Krisp build.
|
|
290
|
+
|
|
291
|
+
### Virtual background and beauty (0.9.0)
|
|
292
|
+
|
|
293
|
+
A blurred or replaced background and beauty for the camera, as LiveKit's track
|
|
294
|
+
processors, Agora's and ZEGOCLOUD's virtual background and beauty, and TRTC's
|
|
295
|
+
video effects do: in the browser, on the GPU. A separate import, so apps that do
|
|
296
|
+
not use it never download it:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
import { createVideoEffects, isVideoEffectsSupported } from '@voltkit-rtc/client/video-effects';
|
|
300
|
+
|
|
301
|
+
if (isVideoEffectsSupported()) {
|
|
302
|
+
const effects = createVideoEffects({ background: 'blur', beauty: true });
|
|
303
|
+
await client.setVideoProcessor(effects); // before or after publish
|
|
304
|
+
client.videoProcessorInUse; // the processor, or null
|
|
305
|
+
|
|
306
|
+
await effects.update({ background: { image: '/office.jpg' } }); // live: no pause for viewers
|
|
307
|
+
await effects.update({ blur: 8, beauty: { smooth: 0.7, brighten: 0.4 } });
|
|
308
|
+
await client.setVideoProcessor(null); // the camera as it is again
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
- `background`: `'none'`, `'blur'` (`blur` 1-10, default 5) or `{ image }` (a
|
|
313
|
+
URL, Blob, image, canvas or ImageBitmap), fitted like `object-fit: cover`.
|
|
314
|
+
- `beauty`: `true`, or `smooth`, `brighten`, `redness`, `sharpen` (0-1;
|
|
315
|
+
defaults 0.5, 0.3, 0.1, 0.3). Skin is smoothed with an edge-preserving filter
|
|
316
|
+
(eyes, lips and teeth stay sharp) and lightly brightened.
|
|
317
|
+
- What you see in `local` is what viewers get. Switching cameras
|
|
318
|
+
(`switchDevice`), `replaceVideoTrack`, reconnects and quality changes keep the
|
|
319
|
+
effects without restarting them. A paused camera costs no processing.
|
|
320
|
+
- **Background effects need MediaPipe**: `npm install @mediapipe/tasks-vision@0.10.35`
|
|
321
|
+
(exactly that version: later ones report usage to Google). Its wasm (3 MB
|
|
322
|
+
compressed) and the person model (250 kB) load the first time a background is
|
|
323
|
+
used, from jsDelivr and Google. To serve them yourself (a strict CSP), copy
|
|
324
|
+
`node_modules/@mediapipe/tasks-vision/wasm` to your site and pass `wasmPath`,
|
|
325
|
+
and `modelUrl` for the model. Beauty alone needs none of this.
|
|
326
|
+
- Nothing behind you shows while the model loads: the background is fully
|
|
327
|
+
blurred or replaced until you are found. If the effects cannot start, the
|
|
328
|
+
camera goes out as it is and `error` says why, so watch `error` when privacy
|
|
329
|
+
matters.
|
|
330
|
+
- Webcams (wide pictures) use MediaPipe's landscape model and phones (tall
|
|
331
|
+
pictures) the square one, which is what each traces best.
|
|
332
|
+
- A slow device finds you only every few frames and reuses the outline (the
|
|
333
|
+
picture keeps the camera's frame rate). `onOverload` tells you when even that
|
|
334
|
+
is not enough: turn the background off or lower the camera's size.
|
|
335
|
+
- Chrome and Edge process each frame as it arrives and keep going in a
|
|
336
|
+
background tab. Firefox and Safari draw from a hidden video on a timer, which
|
|
337
|
+
a background tab can slow down.
|
|
338
|
+
|
|
339
|
+
Your own processor works the same way: anything with `name`,
|
|
340
|
+
`process(track) → Promise<MediaStreamTrack>` (same size and frame rate) and
|
|
341
|
+
`destroy()` (`VideoTrackProcessor`). The SDK stops a picture it no longer sends;
|
|
342
|
+
stop drawing it once its `readyState` is `'ended'`.
|
|
343
|
+
|
|
248
344
|
### Project media settings
|
|
249
345
|
|
|
250
346
|
A project's owner chooses what it may publish: audio and video on or off, the
|
|
@@ -677,6 +773,12 @@ StreamClient.prepareConnection(wsUrl); // on the room screen; never rejects
|
|
|
677
773
|
await client.connect();
|
|
678
774
|
```
|
|
679
775
|
|
|
776
|
+
From 0.9.0 the first picture comes sooner, with nothing to change in your app:
|
|
777
|
+
the join brings the media transports with it (controllers from 2026-10-20), and
|
|
778
|
+
neither the DTLS handshake nor a video's first layer waits for an
|
|
779
|
+
acknowledgement any more. Each is a round trip saved, 50-150 ms on a mobile
|
|
780
|
+
network, for the first publish and the first video alike.
|
|
781
|
+
|
|
680
782
|
### Check before going live (0.8.0)
|
|
681
783
|
|
|
682
784
|
```ts
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* attach to a support ticket: the SDK's recent events (a ring buffer) and its state now.
|
|
7
7
|
*/
|
|
8
8
|
/** This package's version; kept equal to package.json by a test. */
|
|
9
|
-
export declare const SDK_VERSION = "0.
|
|
9
|
+
export declare const SDK_VERSION = "0.9.0";
|
|
10
10
|
/** How many recent events the log keeps. */
|
|
11
11
|
export declare const DIAGNOSTICS_LOG_SIZE = 200;
|
|
12
12
|
export interface DiagnosticsEntry {
|
package/dist/cjs/diagnostics.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.DiagnosticsLog = exports.DIAGNOSTICS_LOG_SIZE = exports.SDK_VERSION = void 0;
|
|
11
11
|
/** This package's version; kept equal to package.json by a test. */
|
|
12
|
-
exports.SDK_VERSION = '0.
|
|
12
|
+
exports.SDK_VERSION = '0.9.0';
|
|
13
13
|
/** How many recent events the log keeps. */
|
|
14
14
|
exports.DIAGNOSTICS_LOG_SIZE = 200;
|
|
15
15
|
/** The events worth keeping for support, and how to summarise each. */
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video effects, the parts that need no browser (docs/implementation-progress/RTC_PERF_PROGRESS.md P3-5): settings,
|
|
3
|
+
* fitting a background image, the size segmentation runs at, and pacing it on a slow device. Kept apart from the
|
|
4
|
+
* WebGL and MediaPipe code so they are tested in Node.
|
|
5
|
+
*/
|
|
6
|
+
/** A background picture: a URL, or anything the browser can draw. */
|
|
7
|
+
export type BackgroundImageSource = string | Blob | ImageBitmap | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas;
|
|
8
|
+
/** What happens behind the person: nothing, a blur, or a picture. */
|
|
9
|
+
export type BackgroundSetting = 'none' | 'blur' | {
|
|
10
|
+
image: BackgroundImageSource;
|
|
11
|
+
};
|
|
12
|
+
/** Beauty, each 0-1 (Agora's beauty effect has the same four). */
|
|
13
|
+
export interface BeautyOptions {
|
|
14
|
+
/** Skin smoothing. Default 0.5. */
|
|
15
|
+
smooth?: number;
|
|
16
|
+
/** Brightening. Default 0.3. */
|
|
17
|
+
brighten?: number;
|
|
18
|
+
/** A rosier skin. Default 0.1. */
|
|
19
|
+
redness?: number;
|
|
20
|
+
/** Sharper eyes, hair and edges (not skin). Default 0.3. */
|
|
21
|
+
sharpen?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface BeautySettings {
|
|
24
|
+
smooth: number;
|
|
25
|
+
brighten: number;
|
|
26
|
+
redness: number;
|
|
27
|
+
sharpen: number;
|
|
28
|
+
}
|
|
29
|
+
export declare const DEFAULT_BEAUTY: Readonly<BeautySettings>;
|
|
30
|
+
/** The blur behind the person, 1 (light) to 10 (strong). */
|
|
31
|
+
export declare const DEFAULT_BLUR = 5;
|
|
32
|
+
/** Beauty settings from what the app passed: `true` is the defaults, `false`/`null` is off. */
|
|
33
|
+
export declare function beautySettings(options: BeautyOptions | boolean | null | undefined): BeautySettings | null;
|
|
34
|
+
/** A blur level from what the app passed: 1-10, rounded; anything else is the default. */
|
|
35
|
+
export declare function blurLevel(value: unknown): number;
|
|
36
|
+
/**
|
|
37
|
+
* The blur's Gaussian sigma, in texels of the quarter-size picture it is computed on: a share of the picture's height,
|
|
38
|
+
* so a level looks the same at 480p and 1080p.
|
|
39
|
+
*/
|
|
40
|
+
export declare function blurSigma(level: number, blurHeight: number): number;
|
|
41
|
+
/** What the background setting asks of the pipeline. */
|
|
42
|
+
export declare function backgroundMode(setting: BackgroundSetting | undefined | null): 'none' | 'blur' | 'image';
|
|
43
|
+
/**
|
|
44
|
+
* Fit a background picture to the frame the way CSS `object-fit: cover` does: it fills the frame, cropped evenly on
|
|
45
|
+
* the long side, never stretched. Returns the texture transform `imageUv = frameUv * scale + offset`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function coverFit(imageWidth: number, imageHeight: number, frameWidth: number, frameHeight: number): {
|
|
48
|
+
sx: number;
|
|
49
|
+
sy: number;
|
|
50
|
+
ox: number;
|
|
51
|
+
oy: number;
|
|
52
|
+
};
|
|
53
|
+
/** The size the person is found at: the frame scaled so its longer side is `longSide` (the model's input). */
|
|
54
|
+
export declare function segmentationSize(width: number, height: number, longSide?: number): {
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
};
|
|
58
|
+
/** How fast the mask follows a new one: lower is steadier between frames, higher follows movement sooner. */
|
|
59
|
+
export declare const MASK_FOLLOW = 0.65;
|
|
60
|
+
/**
|
|
61
|
+
* Smooth the person mask between frames (Stream's temporal smoothing), against flicker at the edges, and write it as
|
|
62
|
+
* bytes for the GPU. `steady` keeps the running mask; a new size starts it over.
|
|
63
|
+
*/
|
|
64
|
+
export declare function smoothMask(confidence: Float32Array, steady: Float32Array | null, out: Uint8Array, follow?: number): Float32Array;
|
|
65
|
+
/** The most frames a mask is reused for before the device is called overloaded. */
|
|
66
|
+
export declare const MAX_SEGMENT_EVERY = 6;
|
|
67
|
+
/**
|
|
68
|
+
* Amazon Chime's adaptive background filter: finding the person is the expensive part, so when it takes too much of
|
|
69
|
+
* each frame's time it runs only every Nth frame and the last mask is reused -- the picture still runs at the camera's
|
|
70
|
+
* rate. N comes back down once it is cheap again. `overloaded` is the point where even that is not enough.
|
|
71
|
+
*/
|
|
72
|
+
export declare class SegmentationPacer {
|
|
73
|
+
private readonly maxEvery;
|
|
74
|
+
private average;
|
|
75
|
+
private changedAt;
|
|
76
|
+
private every;
|
|
77
|
+
constructor(maxEvery?: number);
|
|
78
|
+
/** Run segmentation on every this-many frames. */
|
|
79
|
+
get interval(): number;
|
|
80
|
+
/** Average milliseconds one segmentation takes. */
|
|
81
|
+
get segmentMs(): number;
|
|
82
|
+
shouldSegment(frameIndex: number): boolean;
|
|
83
|
+
/** The share of the frame time segmentation takes now, given how often it runs. */
|
|
84
|
+
load(frameIntervalMs: number): number;
|
|
85
|
+
/** Whether segmenting even this rarely still takes too long. */
|
|
86
|
+
overloaded(frameIntervalMs: number): boolean;
|
|
87
|
+
/** One segmentation took `ms`, at a frame interval of `frameIntervalMs`. */
|
|
88
|
+
report(ms: number, frameIntervalMs: number, now: number): void;
|
|
89
|
+
}
|
|
90
|
+
/** Timestamps MediaPipe's video mode accepts: in milliseconds and always increasing. */
|
|
91
|
+
export declare class MonotonicClock {
|
|
92
|
+
private last;
|
|
93
|
+
next(now: number): number;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=effects-core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effects-core.d.ts","sourceRoot":"","sources":["../../src/effects-core.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Video effects, the parts that need no browser (docs/implementation-progress/RTC_PERF_PROGRESS.md P3-5): settings,
|
|
4
|
+
* fitting a background image, the size segmentation runs at, and pacing it on a slow device. Kept apart from the
|
|
5
|
+
* WebGL and MediaPipe code so they are tested in Node.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.MonotonicClock = exports.SegmentationPacer = exports.MAX_SEGMENT_EVERY = exports.MASK_FOLLOW = exports.DEFAULT_BLUR = exports.DEFAULT_BEAUTY = void 0;
|
|
9
|
+
exports.beautySettings = beautySettings;
|
|
10
|
+
exports.blurLevel = blurLevel;
|
|
11
|
+
exports.blurSigma = blurSigma;
|
|
12
|
+
exports.backgroundMode = backgroundMode;
|
|
13
|
+
exports.coverFit = coverFit;
|
|
14
|
+
exports.segmentationSize = segmentationSize;
|
|
15
|
+
exports.smoothMask = smoothMask;
|
|
16
|
+
exports.DEFAULT_BEAUTY = Object.freeze({ smooth: 0.5, brighten: 0.3, redness: 0.1, sharpen: 0.3 });
|
|
17
|
+
/** The blur behind the person, 1 (light) to 10 (strong). */
|
|
18
|
+
exports.DEFAULT_BLUR = 5;
|
|
19
|
+
const unit = (value, fallback) => typeof value === 'number' && Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : fallback;
|
|
20
|
+
/** Beauty settings from what the app passed: `true` is the defaults, `false`/`null` is off. */
|
|
21
|
+
function beautySettings(options) {
|
|
22
|
+
if (!options)
|
|
23
|
+
return null;
|
|
24
|
+
const o = options === true ? {} : options;
|
|
25
|
+
return {
|
|
26
|
+
smooth: unit(o.smooth, exports.DEFAULT_BEAUTY.smooth),
|
|
27
|
+
brighten: unit(o.brighten, exports.DEFAULT_BEAUTY.brighten),
|
|
28
|
+
redness: unit(o.redness, exports.DEFAULT_BEAUTY.redness),
|
|
29
|
+
sharpen: unit(o.sharpen, exports.DEFAULT_BEAUTY.sharpen),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** A blur level from what the app passed: 1-10, rounded; anything else is the default. */
|
|
33
|
+
function blurLevel(value) {
|
|
34
|
+
return typeof value === 'number' && Number.isFinite(value) ? Math.min(10, Math.max(1, Math.round(value))) : exports.DEFAULT_BLUR;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The blur's Gaussian sigma, in texels of the quarter-size picture it is computed on: a share of the picture's height,
|
|
38
|
+
* so a level looks the same at 480p and 1080p.
|
|
39
|
+
*/
|
|
40
|
+
function blurSigma(level, blurHeight) {
|
|
41
|
+
return Math.max(1, (blurLevel(level) / 10) * (blurHeight / 14));
|
|
42
|
+
}
|
|
43
|
+
/** What the background setting asks of the pipeline. */
|
|
44
|
+
function backgroundMode(setting) {
|
|
45
|
+
if (setting === 'blur')
|
|
46
|
+
return 'blur';
|
|
47
|
+
if (setting && typeof setting === 'object' && 'image' in setting && setting.image)
|
|
48
|
+
return 'image';
|
|
49
|
+
return 'none';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Fit a background picture to the frame the way CSS `object-fit: cover` does: it fills the frame, cropped evenly on
|
|
53
|
+
* the long side, never stretched. Returns the texture transform `imageUv = frameUv * scale + offset`.
|
|
54
|
+
*/
|
|
55
|
+
function coverFit(imageWidth, imageHeight, frameWidth, frameHeight) {
|
|
56
|
+
if (!(imageWidth > 0 && imageHeight > 0 && frameWidth > 0 && frameHeight > 0))
|
|
57
|
+
return { sx: 1, sy: 1, ox: 0, oy: 0 };
|
|
58
|
+
const image = imageWidth / imageHeight;
|
|
59
|
+
const frame = frameWidth / frameHeight;
|
|
60
|
+
if (image > frame) {
|
|
61
|
+
// Wider than the frame: its sides are cropped.
|
|
62
|
+
const sx = frame / image;
|
|
63
|
+
return { sx, sy: 1, ox: (1 - sx) / 2, oy: 0 };
|
|
64
|
+
}
|
|
65
|
+
const sy = image / frame;
|
|
66
|
+
return { sx: 1, sy, ox: 0, oy: (1 - sy) / 2 };
|
|
67
|
+
}
|
|
68
|
+
/** The size the person is found at: the frame scaled so its longer side is `longSide` (the model's input). */
|
|
69
|
+
function segmentationSize(width, height, longSide = 256) {
|
|
70
|
+
if (!(width > 0 && height > 0))
|
|
71
|
+
return { width: longSide, height: longSide };
|
|
72
|
+
const scale = longSide / Math.max(width, height);
|
|
73
|
+
return { width: Math.max(1, Math.round(width * scale)), height: Math.max(1, Math.round(height * scale)) };
|
|
74
|
+
}
|
|
75
|
+
/** How fast the mask follows a new one: lower is steadier between frames, higher follows movement sooner. */
|
|
76
|
+
exports.MASK_FOLLOW = 0.65;
|
|
77
|
+
/**
|
|
78
|
+
* Smooth the person mask between frames (Stream's temporal smoothing), against flicker at the edges, and write it as
|
|
79
|
+
* bytes for the GPU. `steady` keeps the running mask; a new size starts it over.
|
|
80
|
+
*/
|
|
81
|
+
function smoothMask(confidence, steady, out, follow = exports.MASK_FOLLOW) {
|
|
82
|
+
const fresh = steady === null || steady.length !== confidence.length;
|
|
83
|
+
const running = fresh ? new Float32Array(confidence) : steady;
|
|
84
|
+
for (let i = 0; i < confidence.length; i += 1) {
|
|
85
|
+
const value = Math.min(1, Math.max(0, confidence[i]));
|
|
86
|
+
const next = fresh ? value : running[i] + (value - running[i]) * follow;
|
|
87
|
+
running[i] = next;
|
|
88
|
+
out[i] = Math.round(next * 255);
|
|
89
|
+
}
|
|
90
|
+
return running;
|
|
91
|
+
}
|
|
92
|
+
/** The most frames a mask is reused for before the device is called overloaded. */
|
|
93
|
+
exports.MAX_SEGMENT_EVERY = 6;
|
|
94
|
+
/**
|
|
95
|
+
* Amazon Chime's adaptive background filter: finding the person is the expensive part, so when it takes too much of
|
|
96
|
+
* each frame's time it runs only every Nth frame and the last mask is reused -- the picture still runs at the camera's
|
|
97
|
+
* rate. N comes back down once it is cheap again. `overloaded` is the point where even that is not enough.
|
|
98
|
+
*/
|
|
99
|
+
class SegmentationPacer {
|
|
100
|
+
maxEvery;
|
|
101
|
+
average = null;
|
|
102
|
+
changedAt = Number.NEGATIVE_INFINITY;
|
|
103
|
+
every = 1;
|
|
104
|
+
constructor(maxEvery = exports.MAX_SEGMENT_EVERY) {
|
|
105
|
+
this.maxEvery = maxEvery;
|
|
106
|
+
}
|
|
107
|
+
/** Run segmentation on every this-many frames. */
|
|
108
|
+
get interval() {
|
|
109
|
+
return this.every;
|
|
110
|
+
}
|
|
111
|
+
/** Average milliseconds one segmentation takes. */
|
|
112
|
+
get segmentMs() {
|
|
113
|
+
return this.average ?? 0;
|
|
114
|
+
}
|
|
115
|
+
shouldSegment(frameIndex) {
|
|
116
|
+
return frameIndex % this.every === 0;
|
|
117
|
+
}
|
|
118
|
+
/** The share of the frame time segmentation takes now, given how often it runs. */
|
|
119
|
+
load(frameIntervalMs) {
|
|
120
|
+
if (this.average === null || !(frameIntervalMs > 0))
|
|
121
|
+
return 0;
|
|
122
|
+
return this.average / (this.every * frameIntervalMs);
|
|
123
|
+
}
|
|
124
|
+
/** Whether segmenting even this rarely still takes too long. */
|
|
125
|
+
overloaded(frameIntervalMs) {
|
|
126
|
+
return this.every >= this.maxEvery && this.load(frameIntervalMs) > 0.3;
|
|
127
|
+
}
|
|
128
|
+
/** One segmentation took `ms`, at a frame interval of `frameIntervalMs`. */
|
|
129
|
+
report(ms, frameIntervalMs, now) {
|
|
130
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
131
|
+
return;
|
|
132
|
+
this.average = this.average === null ? ms : this.average * 0.8 + ms * 0.2;
|
|
133
|
+
const load = this.load(frameIntervalMs);
|
|
134
|
+
if (load > 0.3 && this.every < this.maxEvery && now - this.changedAt >= 1000) {
|
|
135
|
+
this.every += 1;
|
|
136
|
+
this.changedAt = now;
|
|
137
|
+
}
|
|
138
|
+
else if (load < 0.12 && this.every > 1 && now - this.changedAt >= 3000) {
|
|
139
|
+
this.every -= 1;
|
|
140
|
+
this.changedAt = now;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
exports.SegmentationPacer = SegmentationPacer;
|
|
145
|
+
/** Timestamps MediaPipe's video mode accepts: in milliseconds and always increasing. */
|
|
146
|
+
class MonotonicClock {
|
|
147
|
+
last = 0;
|
|
148
|
+
next(now) {
|
|
149
|
+
this.last = Math.max(this.last + 1, Math.round(now));
|
|
150
|
+
return this.last;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
exports.MonotonicClock = MonotonicClock;
|
|
154
|
+
//# sourceMappingURL=effects-core.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effects-core.js","sourceRoot":"","sources":["../../src/effects-core.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WebGL2 side of video effects (docs/implementation-progress/RTC_PERF_PROGRESS.md P3-5): the camera frame and the
|
|
3
|
+
* person mask in, the finished picture out on one canvas. Done on the GPU as LiveKit, Stream, Twilio and TRTC do:
|
|
4
|
+
*
|
|
5
|
+
* - **Blur** at a quarter of the size (cheap at any resolution), weighted by the mask so the person's own colours never
|
|
6
|
+
* bleed into the blur around them -- no halo (Stream's mask-weighted blur) -- in two separable Gaussian passes.
|
|
7
|
+
* - **Edges** follow the person's outline rather than the mask's coarse grid: the mask is sampled jointly with the
|
|
8
|
+
* frame's colours (a joint bilateral filter, as Stream and Twilio refine theirs).
|
|
9
|
+
* - **Background picture** fitted like `object-fit: cover`.
|
|
10
|
+
* - **Beauty** without a face model: skin found by its colour (YCbCr), smoothed with an edge-preserving filter so eyes
|
|
11
|
+
* and lips stay sharp, brightened in the midtones, a touch rosier, and detail off the skin sharpened.
|
|
12
|
+
*
|
|
13
|
+
* Every texture is kept in picture order (the first row is the top); only the last pass, to the canvas, flips.
|
|
14
|
+
*/
|
|
15
|
+
import type { BeautySettings } from './effects-core.js';
|
|
16
|
+
export interface RenderSettings {
|
|
17
|
+
mode: 'none' | 'blur' | 'image';
|
|
18
|
+
/** Gaussian sigma in quarter-size texels. */
|
|
19
|
+
blurSigma: number;
|
|
20
|
+
beauty: BeautySettings | null;
|
|
21
|
+
/** False until the first mask: everything is treated as background, so nothing behind the person shows meanwhile. */
|
|
22
|
+
maskReady: boolean;
|
|
23
|
+
/** Where the image texture maps (coverFit). */
|
|
24
|
+
cover: {
|
|
25
|
+
sx: number;
|
|
26
|
+
sy: number;
|
|
27
|
+
ox: number;
|
|
28
|
+
oy: number;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export declare class EffectsRenderer {
|
|
32
|
+
readonly canvas: HTMLCanvasElement | OffscreenCanvas;
|
|
33
|
+
private readonly gl;
|
|
34
|
+
private readonly downsample;
|
|
35
|
+
private readonly blur;
|
|
36
|
+
private readonly composite;
|
|
37
|
+
private readonly frameTexture;
|
|
38
|
+
private readonly maskTexture;
|
|
39
|
+
private readonly pictureTexture;
|
|
40
|
+
private readonly blurTextures;
|
|
41
|
+
private readonly blurBuffers;
|
|
42
|
+
private readonly vertices;
|
|
43
|
+
private readonly floatTargets;
|
|
44
|
+
private width;
|
|
45
|
+
private height;
|
|
46
|
+
private blurWidth;
|
|
47
|
+
private blurHeight;
|
|
48
|
+
private maskWidth;
|
|
49
|
+
private maskHeight;
|
|
50
|
+
private pictureSize;
|
|
51
|
+
constructor(canvas: HTMLCanvasElement | OffscreenCanvas);
|
|
52
|
+
get lost(): boolean;
|
|
53
|
+
/** The picture's size; the canvas and the blur follow it. */
|
|
54
|
+
setSize(width: number, height: number): void;
|
|
55
|
+
get size(): {
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
blurHeight: number;
|
|
59
|
+
};
|
|
60
|
+
/** The camera frame (a VideoFrame or a playing video). */
|
|
61
|
+
uploadFrame(source: TexImageSource): void;
|
|
62
|
+
/** The person mask, one byte per pixel, picture order. */
|
|
63
|
+
uploadMask(data: Uint8Array, width: number, height: number): void;
|
|
64
|
+
/** The background picture, or null to let it go. */
|
|
65
|
+
setPicture(image: TexImageSource | null, width?: number, height?: number): void;
|
|
66
|
+
get picture(): {
|
|
67
|
+
width: number;
|
|
68
|
+
height: number;
|
|
69
|
+
} | null;
|
|
70
|
+
/** Draw the finished picture onto the canvas. */
|
|
71
|
+
render(settings: RenderSettings): void;
|
|
72
|
+
/** Let the GPU resources go. */
|
|
73
|
+
dispose(): void;
|
|
74
|
+
private pass;
|
|
75
|
+
private bindTexture;
|
|
76
|
+
private texture;
|
|
77
|
+
private program;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=effects-gl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effects-gl.d.ts","sourceRoot":"","sources":["../../src/effects-gl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAuIxD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAChC,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAC9B,qHAAqH;IACrH,SAAS,EAAE,OAAO,CAAC;IACnB,+CAA+C;IAC/C,KAAK,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3D;AAOD,qBAAa,eAAe;IAC1B,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,eAAe,CAAC;IACrD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAyB;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAU;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAU;IAC/B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAU;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAe;IAC3C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAe;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA+B;IAC5D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAuC;IACnE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,WAAW,CAAkD;gBAEzD,MAAM,EAAE,iBAAiB,GAAG,eAAe;IA0BvD,IAAI,IAAI,IAAI,OAAO,CAElB;IAED,6DAA6D;IAC7D,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAmB5C,IAAI,IAAI,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAEhE;IAED,0DAA0D;IAC1D,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAOzC,0DAA0D;IAC1D,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IASjE,oDAAoD;IACpD,UAAU,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,EAAE,KAAK,SAAI,EAAE,MAAM,SAAI,GAAG,IAAI;IAarE,IAAI,OAAO,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAEtD;IAED,iDAAiD;IACjD,MAAM,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI;IA6CtC,gCAAgC;IAChC,OAAO,IAAI,IAAI;IASf,OAAO,CAAC,IAAI;IAaZ,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,OAAO;IAWf,OAAO,CAAC,OAAO;CAoBhB"}
|