@rapidmx/meet-plugin 0.13.1 → 0.14.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.
Files changed (41) hide show
  1. package/README.md +55 -55
  2. package/apps/meet/[token].tsx +341 -341
  3. package/apps/meet/_CallControls.tsx +366 -394
  4. package/apps/meet/_CallView.tsx +987 -848
  5. package/apps/meet/_DiagnosticsWindow.tsx +80 -0
  6. package/apps/meet/_MeetLobby.tsx +224 -224
  7. package/apps/meet/_ParticipantTile.tsx +169 -169
  8. package/apps/meet/_ParticipantsDrawer.tsx +10 -104
  9. package/apps/meet/_SettingsModal.tsx +152 -0
  10. package/apps/meet/_icons.tsx +80 -82
  11. package/apps/meet/_meetApi.ts +212 -212
  12. package/apps/shared/media/filters/VideoFilterProcessor.ts +408 -408
  13. package/apps/shared/media/filters/mlModels.ts +174 -174
  14. package/apps/shared/push/GuestSignalingClient.ts +359 -359
  15. package/apps/shared/relay/RelayClient.ts +7 -2
  16. package/apps/shared/webrtc/MeshConnectionManager.ts +949 -795
  17. package/apps/shared/webrtc/types.ts +247 -222
  18. package/dist/apps/meet/_CallControls.d.ts +3 -8
  19. package/dist/apps/meet/_CallControls.js +3 -10
  20. package/dist/apps/meet/_CallView.d.ts +15 -2
  21. package/dist/apps/meet/_CallView.js +70 -4
  22. package/dist/apps/meet/_DiagnosticsWindow.d.ts +25 -0
  23. package/dist/apps/meet/_DiagnosticsWindow.js +45 -0
  24. package/dist/apps/meet/_ParticipantsDrawer.d.ts +10 -21
  25. package/dist/apps/meet/_ParticipantsDrawer.js +9 -39
  26. package/dist/apps/meet/_SettingsModal.d.ts +28 -0
  27. package/dist/apps/meet/_SettingsModal.js +59 -0
  28. package/dist/apps/meet/_icons.d.ts +1 -1
  29. package/dist/apps/meet/_icons.js +1 -1
  30. package/dist/apps/shared/relay/RelayClient.d.ts +6 -1
  31. package/dist/apps/shared/relay/RelayClient.js +7 -2
  32. package/dist/apps/shared/webrtc/MeshConnectionManager.d.ts +55 -2
  33. package/dist/apps/shared/webrtc/MeshConnectionManager.js +153 -9
  34. package/dist/apps/shared/webrtc/types.d.ts +25 -1
  35. package/dist/lib/util/RedisRelayBus.js +61 -5
  36. package/dist/lib/util/RedisRelayBus.js.map +1 -1
  37. package/dist/lib/util/RelayHub.js +10 -3
  38. package/dist/lib/util/RelayHub.js.map +1 -1
  39. package/dist/types/util/RedisRelayBus.d.ts +45 -6
  40. package/dist/types/util/RelayHub.d.ts +9 -2
  41. package/package.json +208 -208
@@ -1,174 +1,174 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- /**
6
- * The two machine-learning models behind the filters, run in the browser with MediaPipe Tasks:
7
- *
8
- * - **Person segmentation** (`createPersonSegmenter`) - which pixels are the participant: what the background
9
- * blur and the custom background need.
10
- * - **Face landmarks** (`createFaceTracker`) - where the participant's face is: what the accessories need.
11
- *
12
- * Nothing here is loaded until a participant turns a filter on that needs it, and each model is loaded once and
13
- * shared. The MediaPipe library itself is `import()`ed on demand for the same reason - a participant who never uses a
14
- * filter never downloads it.
15
- *
16
- * **Where the assets come from**: by default the WebAssembly runtime is fetched from jsDelivr and the models from
17
- * Google's public MediaPipe model bucket. That is a request to a third party, so an administrator can host them
18
- * instead (the `mail:videoconf:effects:assets_url` setting, delivered by the join response): the runtime's `wasm/`
19
- * directory from the `@mediapipe/tasks-vision` package, plus `selfie_segmenter.tflite` and `face_landmarker.task`,
20
- * all under one base URL.
21
- *
22
- * Everything the rest of the pipeline needs from MediaPipe is behind the small `PersonSegmenter`/`FaceTracker`
23
- * interfaces, so `VideoFilterProcessor` never touches the library and its tests use fakes.
24
- */
25
- import type { FaceLandmark } from "./accessories.js";
26
-
27
- /** Must match the installed `@mediapipe/tasks-vision` (a test checks): the `.wasm` fetched at runtime has to be the
28
- * version the JavaScript was built against. */
29
- export const MEDIAPIPE_VERSION = "1.0.1";
30
-
31
- const CDN_WASM = `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${MEDIAPIPE_VERSION}/wasm`;
32
- const CDN_SEGMENTER_MODEL =
33
- "https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite";
34
- const CDN_FACE_MODEL =
35
- "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task";
36
-
37
- export interface ModelUrls {
38
- wasm: string;
39
- segmenter: string;
40
- face: string;
41
- }
42
-
43
- /** Where to fetch the runtime and models from - the CDN when `assetsUrl` is empty, else under `assetsUrl`. */
44
- export function modelUrls(assetsUrl?: string): ModelUrls {
45
- if (!assetsUrl) {
46
- return { wasm: CDN_WASM, segmenter: CDN_SEGMENTER_MODEL, face: CDN_FACE_MODEL };
47
- }
48
- const base = assetsUrl.replace(/\/+$/, "");
49
- return { wasm: `${base}/wasm`, segmenter: `${base}/selfie_segmenter.tflite`, face: `${base}/face_landmarker.task` };
50
- }
51
-
52
- /** How likely each pixel of a frame is to be the participant, 0-1. */
53
- export interface PersonMask {
54
- width: number;
55
- height: number;
56
- confidence: Float32Array;
57
- }
58
-
59
- export interface PersonSegmenter {
60
- /** Segments `frame`, or `null` when the model produced nothing for it. */
61
- segment(frame: HTMLVideoElement, timestampMs: number): PersonMask | null;
62
- }
63
-
64
- export interface FaceTracker {
65
- /** The landmarks of the first face in `frame`, or `null` when there is none. */
66
- detect(frame: HTMLVideoElement, timestampMs: number): FaceLandmark[] | null;
67
- }
68
-
69
- /** Which of a segmentation model's masks is the person: the one labelled as such (the selfie model's only label is
70
- * "selfie"), else the last (models list the background first). */
71
- export function personMaskIndex(labels: string[], maskCount: number): number {
72
- const index = labels.findIndex((label) => /person|selfie|foreground/i.test(label));
73
- return index >= 0 && index < maskCount ? index : maskCount - 1;
74
- }
75
-
76
- /** MediaPipe rejects a frame whose timestamp isn't later than the last one it saw. */
77
- function monotonic(): (timestampMs: number) => number {
78
- let last = -1;
79
- return (timestampMs) => {
80
- last = Math.max(timestampMs, last + 1);
81
- return last;
82
- };
83
- }
84
-
85
- type Vision = typeof import("@mediapipe/tasks-vision");
86
-
87
- /** Creates a task on the GPU, or on the CPU when the GPU can't run it (no WebGL, or a driver that refuses). */
88
- async function withFallback<T>(create: (delegate: "GPU" | "CPU") => Promise<T>): Promise<T> {
89
- try {
90
- return await create("GPU");
91
- } catch {
92
- return create("CPU");
93
- }
94
- }
95
-
96
- async function loadSegmenter(assetsUrl?: string): Promise<PersonSegmenter> {
97
- const urls = modelUrls(assetsUrl);
98
- const vision: Vision = await import("@mediapipe/tasks-vision");
99
- const fileset = await vision.FilesetResolver.forVisionTasks(urls.wasm);
100
- const segmenter = await withFallback((delegate) =>
101
- vision.ImageSegmenter.createFromOptions(fileset, {
102
- baseOptions: { modelAssetPath: urls.segmenter, delegate },
103
- runningMode: "VIDEO",
104
- outputConfidenceMasks: true,
105
- outputCategoryMask: false,
106
- }),
107
- );
108
- const labels = segmenter.getLabels();
109
- const stamp = monotonic();
110
- return {
111
- segment(frame, timestampMs) {
112
- let mask: PersonMask | null = null;
113
- segmenter.segmentForVideo(frame, stamp(timestampMs), (result) => {
114
- const masks = result.confidenceMasks;
115
- if (!masks?.length) {
116
- return;
117
- }
118
- const person = masks[personMaskIndex(labels, masks.length)];
119
- // The mask is only valid inside this callback, so it is copied out.
120
- mask = { width: person.width, height: person.height, confidence: new Float32Array(person.getAsFloat32Array()) };
121
- });
122
- return mask;
123
- },
124
- };
125
- }
126
-
127
- async function loadFaceTracker(assetsUrl?: string): Promise<FaceTracker> {
128
- const urls = modelUrls(assetsUrl);
129
- const vision: Vision = await import("@mediapipe/tasks-vision");
130
- const fileset = await vision.FilesetResolver.forVisionTasks(urls.wasm);
131
- const landmarker = await withFallback((delegate) =>
132
- vision.FaceLandmarker.createFromOptions(fileset, {
133
- baseOptions: { modelAssetPath: urls.face, delegate },
134
- runningMode: "VIDEO",
135
- numFaces: 1,
136
- }),
137
- );
138
- const stamp = monotonic();
139
- return {
140
- detect(frame, timestampMs) {
141
- return landmarker.detectForVideo(frame, stamp(timestampMs)).faceLandmarks[0] ?? null;
142
- },
143
- };
144
- }
145
-
146
- const segmenters = new Map<string, Promise<PersonSegmenter>>();
147
- const faceTrackers = new Map<string, Promise<FaceTracker>>();
148
-
149
- /** Loads `load` once per key, and forgets a failure so the next attempt (a retry) tries again. */
150
- function shared<T>(cache: Map<string, Promise<T>>, key: string, load: () => Promise<T>): Promise<T> {
151
- let pending = cache.get(key);
152
- if (!pending) {
153
- pending = load();
154
- cache.set(key, pending);
155
- pending.catch(() => cache.delete(key));
156
- }
157
- return pending;
158
- }
159
-
160
- /** The person-segmentation model, loaded on first use and shared from then on. Rejects if it can't be loaded. */
161
- export function createPersonSegmenter(assetsUrl?: string): Promise<PersonSegmenter> {
162
- return shared(segmenters, assetsUrl ?? "", () => loadSegmenter(assetsUrl));
163
- }
164
-
165
- /** The face-landmark model, loaded on first use and shared from then on. Rejects if it can't be loaded. */
166
- export function createFaceTracker(assetsUrl?: string): Promise<FaceTracker> {
167
- return shared(faceTrackers, assetsUrl ?? "", () => loadFaceTracker(assetsUrl));
168
- }
169
-
170
- /** Forgets every loaded model - for tests. */
171
- export function resetModelCache(): void {
172
- segmenters.clear();
173
- faceTrackers.clear();
174
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * The two machine-learning models behind the filters, run in the browser with MediaPipe Tasks:
7
+ *
8
+ * - **Person segmentation** (`createPersonSegmenter`) - which pixels are the participant: what the background
9
+ * blur and the custom background need.
10
+ * - **Face landmarks** (`createFaceTracker`) - where the participant's face is: what the accessories need.
11
+ *
12
+ * Nothing here is loaded until a participant turns a filter on that needs it, and each model is loaded once and
13
+ * shared. The MediaPipe library itself is `import()`ed on demand for the same reason - a participant who never uses a
14
+ * filter never downloads it.
15
+ *
16
+ * **Where the assets come from**: by default the WebAssembly runtime is fetched from jsDelivr and the models from
17
+ * Google's public MediaPipe model bucket. That is a request to a third party, so an administrator can host them
18
+ * instead (the `mail:videoconf:effects:assets_url` setting, delivered by the join response): the runtime's `wasm/`
19
+ * directory from the `@mediapipe/tasks-vision` package, plus `selfie_segmenter.tflite` and `face_landmarker.task`,
20
+ * all under one base URL.
21
+ *
22
+ * Everything the rest of the pipeline needs from MediaPipe is behind the small `PersonSegmenter`/`FaceTracker`
23
+ * interfaces, so `VideoFilterProcessor` never touches the library and its tests use fakes.
24
+ */
25
+ import type { FaceLandmark } from "./accessories.js";
26
+
27
+ /** Must match the installed `@mediapipe/tasks-vision` (a test checks): the `.wasm` fetched at runtime has to be the
28
+ * version the JavaScript was built against. */
29
+ export const MEDIAPIPE_VERSION = "1.0.1";
30
+
31
+ const CDN_WASM = `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${MEDIAPIPE_VERSION}/wasm`;
32
+ const CDN_SEGMENTER_MODEL =
33
+ "https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite";
34
+ const CDN_FACE_MODEL =
35
+ "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task";
36
+
37
+ export interface ModelUrls {
38
+ wasm: string;
39
+ segmenter: string;
40
+ face: string;
41
+ }
42
+
43
+ /** Where to fetch the runtime and models from - the CDN when `assetsUrl` is empty, else under `assetsUrl`. */
44
+ export function modelUrls(assetsUrl?: string): ModelUrls {
45
+ if (!assetsUrl) {
46
+ return { wasm: CDN_WASM, segmenter: CDN_SEGMENTER_MODEL, face: CDN_FACE_MODEL };
47
+ }
48
+ const base = assetsUrl.replace(/\/+$/, "");
49
+ return { wasm: `${base}/wasm`, segmenter: `${base}/selfie_segmenter.tflite`, face: `${base}/face_landmarker.task` };
50
+ }
51
+
52
+ /** How likely each pixel of a frame is to be the participant, 0-1. */
53
+ export interface PersonMask {
54
+ width: number;
55
+ height: number;
56
+ confidence: Float32Array;
57
+ }
58
+
59
+ export interface PersonSegmenter {
60
+ /** Segments `frame`, or `null` when the model produced nothing for it. */
61
+ segment(frame: HTMLVideoElement, timestampMs: number): PersonMask | null;
62
+ }
63
+
64
+ export interface FaceTracker {
65
+ /** The landmarks of the first face in `frame`, or `null` when there is none. */
66
+ detect(frame: HTMLVideoElement, timestampMs: number): FaceLandmark[] | null;
67
+ }
68
+
69
+ /** Which of a segmentation model's masks is the person: the one labelled as such (the selfie model's only label is
70
+ * "selfie"), else the last (models list the background first). */
71
+ export function personMaskIndex(labels: string[], maskCount: number): number {
72
+ const index = labels.findIndex((label) => /person|selfie|foreground/i.test(label));
73
+ return index >= 0 && index < maskCount ? index : maskCount - 1;
74
+ }
75
+
76
+ /** MediaPipe rejects a frame whose timestamp isn't later than the last one it saw. */
77
+ function monotonic(): (timestampMs: number) => number {
78
+ let last = -1;
79
+ return (timestampMs) => {
80
+ last = Math.max(timestampMs, last + 1);
81
+ return last;
82
+ };
83
+ }
84
+
85
+ type Vision = typeof import("@mediapipe/tasks-vision");
86
+
87
+ /** Creates a task on the GPU, or on the CPU when the GPU can't run it (no WebGL, or a driver that refuses). */
88
+ async function withFallback<T>(create: (delegate: "GPU" | "CPU") => Promise<T>): Promise<T> {
89
+ try {
90
+ return await create("GPU");
91
+ } catch {
92
+ return create("CPU");
93
+ }
94
+ }
95
+
96
+ async function loadSegmenter(assetsUrl?: string): Promise<PersonSegmenter> {
97
+ const urls = modelUrls(assetsUrl);
98
+ const vision: Vision = await import("@mediapipe/tasks-vision");
99
+ const fileset = await vision.FilesetResolver.forVisionTasks(urls.wasm);
100
+ const segmenter = await withFallback((delegate) =>
101
+ vision.ImageSegmenter.createFromOptions(fileset, {
102
+ baseOptions: { modelAssetPath: urls.segmenter, delegate },
103
+ runningMode: "VIDEO",
104
+ outputConfidenceMasks: true,
105
+ outputCategoryMask: false,
106
+ }),
107
+ );
108
+ const labels = segmenter.getLabels();
109
+ const stamp = monotonic();
110
+ return {
111
+ segment(frame, timestampMs) {
112
+ let mask: PersonMask | null = null;
113
+ segmenter.segmentForVideo(frame, stamp(timestampMs), (result) => {
114
+ const masks = result.confidenceMasks;
115
+ if (!masks?.length) {
116
+ return;
117
+ }
118
+ const person = masks[personMaskIndex(labels, masks.length)];
119
+ // The mask is only valid inside this callback, so it is copied out.
120
+ mask = { width: person.width, height: person.height, confidence: new Float32Array(person.getAsFloat32Array()) };
121
+ });
122
+ return mask;
123
+ },
124
+ };
125
+ }
126
+
127
+ async function loadFaceTracker(assetsUrl?: string): Promise<FaceTracker> {
128
+ const urls = modelUrls(assetsUrl);
129
+ const vision: Vision = await import("@mediapipe/tasks-vision");
130
+ const fileset = await vision.FilesetResolver.forVisionTasks(urls.wasm);
131
+ const landmarker = await withFallback((delegate) =>
132
+ vision.FaceLandmarker.createFromOptions(fileset, {
133
+ baseOptions: { modelAssetPath: urls.face, delegate },
134
+ runningMode: "VIDEO",
135
+ numFaces: 1,
136
+ }),
137
+ );
138
+ const stamp = monotonic();
139
+ return {
140
+ detect(frame, timestampMs) {
141
+ return landmarker.detectForVideo(frame, stamp(timestampMs)).faceLandmarks[0] ?? null;
142
+ },
143
+ };
144
+ }
145
+
146
+ const segmenters = new Map<string, Promise<PersonSegmenter>>();
147
+ const faceTrackers = new Map<string, Promise<FaceTracker>>();
148
+
149
+ /** Loads `load` once per key, and forgets a failure so the next attempt (a retry) tries again. */
150
+ function shared<T>(cache: Map<string, Promise<T>>, key: string, load: () => Promise<T>): Promise<T> {
151
+ let pending = cache.get(key);
152
+ if (!pending) {
153
+ pending = load();
154
+ cache.set(key, pending);
155
+ pending.catch(() => cache.delete(key));
156
+ }
157
+ return pending;
158
+ }
159
+
160
+ /** The person-segmentation model, loaded on first use and shared from then on. Rejects if it can't be loaded. */
161
+ export function createPersonSegmenter(assetsUrl?: string): Promise<PersonSegmenter> {
162
+ return shared(segmenters, assetsUrl ?? "", () => loadSegmenter(assetsUrl));
163
+ }
164
+
165
+ /** The face-landmark model, loaded on first use and shared from then on. Rejects if it can't be loaded. */
166
+ export function createFaceTracker(assetsUrl?: string): Promise<FaceTracker> {
167
+ return shared(faceTrackers, assetsUrl ?? "", () => loadFaceTracker(assetsUrl));
168
+ }
169
+
170
+ /** Forgets every loaded model - for tests. */
171
+ export function resetModelCache(): void {
172
+ segmenters.clear();
173
+ faceTrackers.clear();
174
+ }