domma-cms 0.107.1 → 0.109.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/admin/css/admin.css +1 -1
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/lib/sidebar-size.js +1 -0
- package/admin/js/templates/docs/usage-tools.html +7 -0
- package/package.json +1 -1
- package/plugins/_lib/public/call/blur-clock.js +7 -0
- package/plugins/_lib/public/call/blur.js +225 -0
- package/plugins/_lib/public/call/call.css +1 -1
- package/plugins/_lib/public/call/call.js +103 -9
- package/plugins/_lib/public/call/vendor/mediapipe/selfie_segmenter_landscape.tflite +0 -0
- package/plugins/_lib/public/call/vendor/mediapipe/vision_bundle.mjs +2 -0
- package/plugins/_lib/public/call/vendor/mediapipe/vision_wasm_internal.js +8841 -0
- package/plugins/_lib/public/call/vendor/mediapipe/vision_wasm_internal.wasm +0 -0
- package/scripts/build.js +6 -0
- package/server/server.js +3 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background blur for the call page - the camera's picture with everything but the person blurred.
|
|
3
|
+
*
|
|
4
|
+
* Runs entirely in this browser: MediaPipe's selfie segmenter (vendored under ./vendor/mediapipe, Apache 2.0,
|
|
5
|
+
* its usage logging patched out) marks which pixels are the person, and a canvas draws the frame blurred with the
|
|
6
|
+
* person sharp on top. The canvas becomes a video track (captureStream) that call.js sends instead of the
|
|
7
|
+
* camera's. No frame leaves the browser except as the call's own video, and nothing is fetched from anyone but
|
|
8
|
+
* this site.
|
|
9
|
+
*
|
|
10
|
+
* Loaded on demand (import()) the first time blur is turned on - about 12 MB, compressed in transit - so a call
|
|
11
|
+
* that never blurs costs nothing.
|
|
12
|
+
*
|
|
13
|
+
* const blur = await startBlur(cameraTrack); // throws when this browser cannot run it (or not within 20 s)
|
|
14
|
+
* blur.track // the blurred track to send and show
|
|
15
|
+
* blur.paused = true // skip the work (a screen is being shared)
|
|
16
|
+
* blur.stop()
|
|
17
|
+
*
|
|
18
|
+
* blur.track ends by itself - an 'ended' event, then call.js puts the camera back - when this machine cannot keep
|
|
19
|
+
* up (a frame costing over SLOW_MS for SLOW_FOR) or the segmenter keeps failing; `blur.reason` says which.
|
|
20
|
+
*
|
|
21
|
+
* The beat comes from a tiny Worker (blur-clock.js), not requestAnimationFrame or the page's own timers: a
|
|
22
|
+
* background tab stops rAF and slows timers to once a second, and the others would see a frozen picture whenever
|
|
23
|
+
* the caller looked at another tab. Each frame leaves at least as long idle as the last one took, so a slow machine
|
|
24
|
+
* drops frames rather than its buttons and chat.
|
|
25
|
+
*/
|
|
26
|
+
import {ImageSegmenter} from './vendor/mediapipe/vision_bundle.mjs';
|
|
27
|
+
|
|
28
|
+
const HERE = new URL('./vendor/mediapipe/', import.meta.url);
|
|
29
|
+
const MAX_WIDTH = 1280; // the camera is asked for 720p; anything larger is drawn at this width
|
|
30
|
+
const BG_SCALE = 6; // the background is blurred at 1/6 size, then scaled back up (cheap and smooth)
|
|
31
|
+
const KEEP = 0.35; // how much of the last frame's mask carries over (steadier edges, less flicker)
|
|
32
|
+
const SLOW_MS = 250; // a frame this slow on average (under 4 a second, with the idle gap)...
|
|
33
|
+
const SLOW_FOR = 5000; // ...for this long, and blur gives up
|
|
34
|
+
const START_LIMIT = 20000; // loading and the first frame, or it is not going to work here
|
|
35
|
+
|
|
36
|
+
let segmenterReady = null;
|
|
37
|
+
|
|
38
|
+
/** One segmenter for the page: the GPU where it works, the CPU otherwise. Dropped (and rebuilt next time) if it breaks. */
|
|
39
|
+
function segmenter() {
|
|
40
|
+
segmenterReady ??= (async () => {
|
|
41
|
+
const fileset = {
|
|
42
|
+
wasmLoaderPath: new URL('vision_wasm_internal.js', HERE).href,
|
|
43
|
+
wasmBinaryPath: new URL('vision_wasm_internal.wasm', HERE).href
|
|
44
|
+
};
|
|
45
|
+
const options = (delegate) => ({
|
|
46
|
+
baseOptions: {modelAssetPath: new URL('selfie_segmenter_landscape.tflite', HERE).href, delegate},
|
|
47
|
+
runningMode: 'VIDEO',
|
|
48
|
+
outputCategoryMask: false,
|
|
49
|
+
outputConfidenceMasks: true
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
return await ImageSegmenter.createFromOptions(fileset, options('GPU'));
|
|
53
|
+
} catch {
|
|
54
|
+
return ImageSegmenter.createFromOptions(fileset, options('CPU'));
|
|
55
|
+
}
|
|
56
|
+
})();
|
|
57
|
+
segmenterReady.catch(() => { segmenterReady = null; }); // a failed load may be tried again
|
|
58
|
+
return segmenterReady;
|
|
59
|
+
}
|
|
60
|
+
function dropSegmenter(seg) {
|
|
61
|
+
segmenterReady = null;
|
|
62
|
+
try { seg?.close?.(); } catch { /* already gone */ }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const canvas = (w, h) => Object.assign(document.createElement('canvas'), {width: w, height: h});
|
|
66
|
+
const smooth = (x) => (x <= 0.25 ? 0 : x >= 0.75 ? 1 : (x - 0.25) * 2); // a soft threshold: firm edges, no halo of half-person
|
|
67
|
+
const timeout = (ms, what) => new Promise((_, reject) => setTimeout(() => reject(new Error(`${what} took too long`)), ms));
|
|
68
|
+
|
|
69
|
+
/** The beat: a Worker's timer, which a background tab does not slow down. The page's own timer where there is none. */
|
|
70
|
+
function clock(tick) {
|
|
71
|
+
let worker = null;
|
|
72
|
+
try {
|
|
73
|
+
worker = new Worker(new URL(`./blur-clock.js${new URL(import.meta.url).search}`, import.meta.url));
|
|
74
|
+
worker.onmessage = () => tick();
|
|
75
|
+
} catch { worker = null; }
|
|
76
|
+
let timer = null;
|
|
77
|
+
return {
|
|
78
|
+
after(ms) {
|
|
79
|
+
if (worker) worker.postMessage(ms);
|
|
80
|
+
else timer = setTimeout(tick, ms);
|
|
81
|
+
},
|
|
82
|
+
stop() { worker?.terminate(); clearTimeout(timer); }
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Start blurring a camera track.
|
|
88
|
+
*
|
|
89
|
+
* @param {MediaStreamTrack} source - the camera's video track (left running; the caller owns it)
|
|
90
|
+
* @returns {Promise<{track: MediaStreamTrack, stop: () => void, paused: boolean, reason: string}>}
|
|
91
|
+
*/
|
|
92
|
+
export async function startBlur(source) {
|
|
93
|
+
const video = document.createElement('video');
|
|
94
|
+
video.muted = true;
|
|
95
|
+
video.playsInline = true;
|
|
96
|
+
try {
|
|
97
|
+
const seg = await Promise.race([(async () => {
|
|
98
|
+
const s = await segmenter();
|
|
99
|
+
video.srcObject = new MediaStream([source]);
|
|
100
|
+
await video.play();
|
|
101
|
+
if (!video.videoWidth) await new Promise(r => video.addEventListener('loadeddata', r, {once: true}));
|
|
102
|
+
return s;
|
|
103
|
+
})(), timeout(START_LIMIT, 'Starting blur')]);
|
|
104
|
+
return run(seg, source, video);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
video.pause();
|
|
107
|
+
video.srcObject = null;
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function run(seg, source, video) {
|
|
113
|
+
const out = canvas(16, 16);
|
|
114
|
+
const g = out.getContext('2d');
|
|
115
|
+
const bg = canvas(16, 9);
|
|
116
|
+
const bgc = bg.getContext('2d');
|
|
117
|
+
const filters = 'filter' in bgc;
|
|
118
|
+
const person = canvas(16, 16);
|
|
119
|
+
const pc = person.getContext('2d');
|
|
120
|
+
let W = 0; let H = 0;
|
|
121
|
+
let maskCanvas = null; let mc = null; let maskImage = null; let last = null;
|
|
122
|
+
|
|
123
|
+
/** Size everything to the camera's picture - again whenever it changes (a phone turned, the browser adapting it). */
|
|
124
|
+
function fit() {
|
|
125
|
+
if (!video.videoWidth || !video.videoHeight) return false;
|
|
126
|
+
const scale = Math.min(1, MAX_WIDTH / video.videoWidth);
|
|
127
|
+
const w = Math.round(video.videoWidth * scale); const h = Math.round(video.videoHeight * scale);
|
|
128
|
+
if (w === W && h === H) return true;
|
|
129
|
+
W = w; H = h;
|
|
130
|
+
out.width = person.width = W; out.height = person.height = H;
|
|
131
|
+
bg.width = Math.max(16, Math.round(W / BG_SCALE)); bg.height = Math.max(9, Math.round(H / BG_SCALE));
|
|
132
|
+
if (filters) bgc.filter = 'blur(2px)';
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
fit();
|
|
136
|
+
// The canvas paints once before the first mask, so the track never starts with an empty frame.
|
|
137
|
+
g.drawImage(video, 0, 0, W, H);
|
|
138
|
+
const track = out.captureStream(30).getVideoTracks()[0];
|
|
139
|
+
|
|
140
|
+
function drawMask(mask) {
|
|
141
|
+
const w = mask.width; const h = mask.height;
|
|
142
|
+
if (!maskCanvas || maskCanvas.width !== w || maskCanvas.height !== h) {
|
|
143
|
+
maskCanvas = canvas(w, h);
|
|
144
|
+
mc = maskCanvas.getContext('2d');
|
|
145
|
+
maskImage = mc.createImageData(w, h);
|
|
146
|
+
last = new Float32Array(w * h);
|
|
147
|
+
}
|
|
148
|
+
const data = mask.getAsFloat32Array();
|
|
149
|
+
const px = maskImage.data;
|
|
150
|
+
for (let i = 0; i < data.length; i++) {
|
|
151
|
+
const v = last[i] = data[i] * (1 - KEEP) + last[i] * KEEP;
|
|
152
|
+
px[i * 4 + 3] = smooth(v) * 255;
|
|
153
|
+
}
|
|
154
|
+
mc.putImageData(maskImage, 0, 0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function compose() {
|
|
158
|
+
// The background: shrunk and blurred, stretched back over the whole frame. Drawn a little past each edge,
|
|
159
|
+
// because a blurred picture fades out at its own border and the last frame would show through.
|
|
160
|
+
const pad = 2;
|
|
161
|
+
bgc.drawImage(video, -pad, -pad, bg.width + pad * 2, bg.height + pad * 2);
|
|
162
|
+
g.imageSmoothingEnabled = true;
|
|
163
|
+
g.imageSmoothingQuality = 'high';
|
|
164
|
+
g.drawImage(bg, 0, 0, W, H);
|
|
165
|
+
// The person: the mask (stretched, which feathers it) filled with the sharp frame.
|
|
166
|
+
pc.globalCompositeOperation = 'copy';
|
|
167
|
+
pc.imageSmoothingEnabled = true;
|
|
168
|
+
pc.drawImage(maskCanvas, 0, 0, W, H);
|
|
169
|
+
pc.globalCompositeOperation = 'source-in';
|
|
170
|
+
pc.drawImage(video, 0, 0, W, H);
|
|
171
|
+
g.drawImage(person, 0, 0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const self = {track, paused: false, reason: '', stop};
|
|
175
|
+
let stopped = false;
|
|
176
|
+
let lastTs = 0;
|
|
177
|
+
let cost = 16; // a running average of one frame's work, in ms
|
|
178
|
+
let slowSince = 0;
|
|
179
|
+
let failures = 0;
|
|
180
|
+
const beat = clock(frame);
|
|
181
|
+
|
|
182
|
+
function frame() {
|
|
183
|
+
if (stopped) return;
|
|
184
|
+
const started = performance.now();
|
|
185
|
+
if (source.readyState === 'ended') { stop(); return; }
|
|
186
|
+
if (!self.paused && source.enabled && video.readyState >= 2 && fit()) {
|
|
187
|
+
let result = null;
|
|
188
|
+
try {
|
|
189
|
+
const ts = Math.max(lastTs + 1, started);
|
|
190
|
+
lastTs = ts;
|
|
191
|
+
result = seg.segmentForVideo(video, ts);
|
|
192
|
+
const mask = result?.confidenceMasks?.[0];
|
|
193
|
+
if (mask) { drawMask(mask); compose(); failures = 0; }
|
|
194
|
+
} catch (err) {
|
|
195
|
+
failures += 1;
|
|
196
|
+
if (failures === 1) console.warn('[call] blur:', err?.message || err);
|
|
197
|
+
if (failures > 30) { dropSegmenter(seg); stop('failed'); return; }
|
|
198
|
+
} finally {
|
|
199
|
+
try { result?.close?.(); } catch { /* nothing to free */ }
|
|
200
|
+
}
|
|
201
|
+
cost = cost * 0.9 + (performance.now() - started) * 0.1;
|
|
202
|
+
if (cost > SLOW_MS) {
|
|
203
|
+
slowSince ||= started;
|
|
204
|
+
if (started - slowSince > SLOW_FOR) { stop('slow'); return; }
|
|
205
|
+
} else slowSince = 0;
|
|
206
|
+
}
|
|
207
|
+
const spent = performance.now() - started;
|
|
208
|
+
const every = cost > 28 ? 1000 / 15 : 1000 / 30;
|
|
209
|
+
beat.after(Math.max(cost, every - spent, 4));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function stop(reason = '') {
|
|
213
|
+
if (stopped) return;
|
|
214
|
+
stopped = true;
|
|
215
|
+
self.reason = reason;
|
|
216
|
+
beat.stop();
|
|
217
|
+
video.pause();
|
|
218
|
+
video.srcObject = null;
|
|
219
|
+
track.stop();
|
|
220
|
+
track.dispatchEvent(new Event('ended'));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
frame();
|
|
224
|
+
return self;
|
|
225
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.dmc{--dmc-accent: #4f7cff;--dmc-accent-solid: color-mix(in srgb, var(--dmc-accent) 84%, #000);--dmc-accent-soft: color-mix(in srgb, var(--dmc-accent) 18%, transparent);--dmc-accent-text: color-mix(in srgb, var(--dmc-accent) 55%, #fff);--dmc-gap: 8px;--dmc-base: #1b1d21;--dmc-panel: #25282d;--dmc-raised: #2f3338;--dmc-line: #363a40;--dmc-on-dark: #e8eaed;--dmc-muted-dark: #9aa0a8;--dmc-red: #d9453f;--dmc-red-hover: #c23a35;--dmc-page: #f3f4f6;--dmc-card: #fff;--dmc-ink: #1f2328;--dmc-ink-muted: #5b6470;--dmc-border: #dde0e4;--dmc-offset: 0px;--dmc-left: 0px;--dmc-right: 0px;display:flex;flex-direction:column;gap:1rem;color:var(--dmc-ink)}.dmc [hidden]{display:none!important}html.dmc-page,html.dmc-page body{overflow:hidden!important}.dmc.dmc--app{position:fixed;z-index:40;top:var(--dmc-offset);left:var(--dmc-left);right:var(--dmc-right);bottom:0;margin:0;background:var(--dmc-page);overflow-y:auto;overscroll-behavior:contain;padding:2.5rem 1rem;align-items:center;justify-content:safe center;gap:1rem}.dmc.dmc--app[data-stage=in]{padding:0;background:var(--dmc-base);overflow:hidden}.dmc.dmc--app>*{width:min(60rem,100%);flex:none}.dmc.dmc--app>.dmc-call{width:100%;flex:1 1 auto}.dmc-head{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}.dmc.dmc--app:not([data-stage=check])>.dmc-head{width:min(28rem,100%);justify-content:center;text-align:center}.dmc-title{font-size:1.35rem;font-weight:600;margin:0;flex:1 1 auto;letter-spacing:-.01em;color:var(--dmc-ink)}.dmc-btn{font:inherit;font-size:.9rem;cursor:pointer;padding:.5rem .95rem;min-height:2.5rem;border-radius:8px;border:1px solid var(--dmc-border);background:var(--dmc-card);color:inherit;display:inline-flex;align-items:center;justify-content:center;gap:.45rem;position:relative;font-weight:500;transition:background-color .15s,border-color .15s,color .15s}.dmc-btn:hover{border-color:#b9bec5;background:#f7f8f9}.dmc-btn:disabled{opacity:.55;cursor:default}.dmc-btn:focus-visible,.dmc-compose:focus-visible,.dmc-input:focus-visible{outline:2px solid var(--dmc-accent);outline-offset:2px}.dmc-i{display:inline-flex}.dmc-btn svg,.dmc-count svg,.dmc-note svg,.dmc-chip svg{width:18px;height:18px;flex:none}.dmc-btn.is-off{background:#fdecec;border-color:#f3c4c2;color:#b3261e}.dmc-btn--go{background:var(--dmc-accent-solid);border-color:transparent;color:#fff;font-weight:600}.dmc-btn--go:hover{background:color-mix(in srgb,var(--dmc-accent) 74%,#000);border-color:transparent}.dmc-btn--small{padding:.3rem .65rem;min-height:2rem;font-size:.82rem}.dmc-panel{display:grid;grid-template-columns:minmax(0,3fr) minmax(0,2fr);gap:1.75rem;align-items:center;background:var(--dmc-card);border:1px solid var(--dmc-border);border-radius:12px;padding:1.5rem;box-shadow:0 1px 2px #1018280d,0 8px 24px #1018280f}.dmc-panel--msg{display:flex;flex-direction:column;align-items:center;text-align:center;padding:2.5rem 2rem;gap:.25rem;width:min(28rem,100%)!important;align-self:center}.dmc-panel--msg h2{margin:.75rem 0 .25rem;font-size:1.2rem;font-weight:600;color:var(--dmc-ink)}.dmc-panel--msg p{color:var(--dmc-ink-muted);margin:0 0 1.25rem;font-size:.92rem;line-height:1.5}.dmc-pulse{width:64px;height:64px;border-radius:50%;display:grid;place-items:center;color:var(--dmc-accent-solid);background:var(--dmc-accent-soft);box-shadow:0 0 color-mix(in srgb,var(--dmc-accent) 35%,transparent);animation:dmc-pulse 2.2s infinite}.dmc-pulse svg{width:26px;height:26px}@keyframes dmc-pulse{70%{box-shadow:0 0 0 16px transparent}to{box-shadow:0 0 0 0 transparent}}.dmc-preview{position:relative;aspect-ratio:16 / 9;background:var(--dmc-base);border-radius:10px;overflow:hidden}.dmc-preview video,.dmc-tile video{width:100%;height:100%;object-fit:cover;display:block}.dmc-self,.dmc-self-preview{transform:scaleX(-1)}.dmc-nocam{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.6rem;color:var(--dmc-muted-dark);margin:0;font-size:.9rem}.dmc-avatar-dot{width:76px;height:76px;border-radius:50%;display:grid;place-items:center;background:var(--dmc-raised);color:var(--dmc-on-dark);font-size:1.8rem;font-weight:600}.dmc-preview-tools{position:absolute;left:0;right:0;bottom:.75rem;display:flex;justify-content:center;gap:.5rem}.dmc-round{width:44px;height:44px;min-height:0;padding:0;border-radius:10px;border:1px solid rgba(255,255,255,.14);background:#1b1d21b8;color:#fff;backdrop-filter:blur(6px)}.dmc-round:hover{background:#2f3338e6;border-color:#ffffff4d}.dmc-round.is-off{background:var(--dmc-red);border-color:transparent;color:#fff}.dmc-check h2{margin:0 0 .35rem;font-size:1.25rem;font-weight:600;color:var(--dmc-ink)}.dmc-label{display:grid;gap:.35rem;margin:1rem 0 .25rem;font-weight:500;font-size:.88rem;color:var(--dmc-ink)}.dmc-input{font:inherit;font-size:.95rem;padding:.55rem .75rem;min-height:2.5rem;border:1px solid var(--dmc-border);border-radius:8px;background:#fff;color:var(--dmc-ink);min-width:0}.dmc-label .dmc-input:focus{outline:0;border-color:var(--dmc-accent);box-shadow:0 0 0 3px var(--dmc-accent-soft)}.dmc-row{display:flex;gap:.5rem;flex-wrap:wrap;margin:.9rem 0}.dmc-chipbtn{font-size:.86rem}.dmc-note{color:var(--dmc-ink-muted);font-size:.9rem;line-height:1.45;margin:0}.dmc-note--rec{display:flex;gap:.5rem;align-items:flex-start;font-size:.85rem;margin:.25rem 0 0}.dmc-note--rec svg{color:var(--dmc-red);fill:currentColor;width:10px;height:10px;margin-top:.35rem}.dmc-error{color:#b3261e;font-weight:500;font-size:.9rem;margin:.5rem 0 0}.dmc-check .dmc-btn--go{width:100%;min-height:2.75rem;margin-top:1rem;font-size:.95rem}.dmc-call{display:flex;color:var(--dmc-on-dark);background:var(--dmc-base);overflow:hidden;color-scheme:dark;min-height:0;font-size:14px}.dmc-stage{position:relative;flex:1 1 auto;min-width:0;display:flex;flex-direction:column}.dmc-top{order:-1;flex:none;display:flex;align-items:center;justify-content:space-between;gap:1rem;height:44px;padding:0 1rem;background:var(--dmc-panel);border-bottom:1px solid var(--dmc-line)}.dmc-top-l{display:flex;align-items:center;gap:.75rem;min-width:0}.dmc-top .dmc-top-title{margin:0;font-size:.92rem;line-height:1.3;font-weight:600;letter-spacing:0;color:var(--dmc-on-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-timer{flex:none;font-size:.82rem;color:var(--dmc-muted-dark);font-variant-numeric:tabular-nums;padding-left:.75rem;border-left:1px solid var(--dmc-line)}.dmc-top-r{display:flex;align-items:center;gap:.6rem;flex:none}.dmc-rec{display:inline-flex;align-items:center;gap:.4rem;color:#ff8a85;background:#d9453f29;border:1px solid rgba(217,69,63,.45);border-radius:6px;padding:.12rem .5rem;font-size:.74rem;font-weight:600;letter-spacing:.02em}.dmc-rec-dot{width:7px;height:7px;border-radius:50%;background:#ff5a52;animation:dmc-blink 1.4s infinite}@keyframes dmc-blink{50%{opacity:.3}}.dmc-count{display:inline-flex;align-items:center;gap:.35rem;font-size:.82rem;color:var(--dmc-muted-dark);font-variant-numeric:tabular-nums}.dmc-count svg{width:16px;height:16px}.dmc-main{position:relative;flex:1 1 auto;min-height:0}.dmc-remote{position:absolute;inset:var(--dmc-gap);display:flex;flex-wrap:wrap;align-content:center;justify-content:center;gap:var(--dmc-gap);--cols: 1;--rows: 1}.dmc-cell{min-width:0;min-height:0;display:flex;width:var(--tile-w, calc((100% - (var(--cols) - 1) * var(--dmc-gap)) / var(--cols)));height:var(--tile-h, calc((100% - (var(--rows) - 1) * var(--dmc-gap)) / var(--rows)))}.dmc-main[data-tiles="2"] .dmc-remote{--cols: 2}.dmc-main[data-tiles="3"] .dmc-remote,.dmc-main[data-tiles="4"] .dmc-remote{--cols: 2;--rows: 2}.dmc-main[data-tiles="5"] .dmc-remote,.dmc-main[data-tiles="6"] .dmc-remote{--cols: 3;--rows: 2}.dmc-tile{position:relative;margin:0;background:var(--dmc-panel);border-radius:8px;overflow:hidden}.dmc-cell .dmc-tile{flex:1 1 auto;width:100%;height:100%}.dmc-tile video{position:relative;z-index:1}.dmc-avatar{position:absolute;inset:0;display:grid;place-items:center;z-index:0}.dmc-avatar span{width:clamp(3rem,18%,6rem);aspect-ratio:1;border-radius:50%;display:grid;place-items:center;background:var(--dmc-raised);color:var(--dmc-on-dark);font-size:clamp(1.1rem,2.4vw,2.2rem);font-weight:600}.dmc-tile--me .dmc-avatar{z-index:2;background:var(--dmc-panel)}.dmc-chip{position:absolute;left:.5rem;bottom:.5rem;z-index:3;display:inline-flex;align-items:center;gap:.3rem;max-width:calc(100% - 1rem);padding:.18rem .5rem;border-radius:6px;background:#1b1d21c7;color:var(--dmc-on-dark);font-size:.78rem;font-weight:500;line-height:1.35}.dmc-chip>span:last-child{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-chip-mute{display:inline-flex;color:#ff8a85}.dmc-chip-mute svg{width:13px;height:13px}.dmc-tile--me{position:absolute;z-index:2;right:calc(var(--dmc-gap) * 2);bottom:calc(var(--dmc-gap) * 2);width:clamp(10rem,17%,15rem);aspect-ratio:16 / 9;box-shadow:0 6px 20px #00000073;outline:1px solid rgba(255,255,255,.14)}.dmc-tile--me .dmc-chip{left:.35rem;bottom:.35rem;font-size:.7rem;padding:.1rem .4rem}.dmc-main[data-tiles="0"] .dmc-tile--me{inset:0;width:auto;aspect-ratio:auto;border-radius:0;outline:0;box-shadow:none}.dmc-main[data-tiles="0"] .dmc-tile--me .dmc-chip{left:.75rem;bottom:.75rem;font-size:.78rem;padding:.18rem .5rem}.dmc-main[data-tiles="0"] .dmc-tile--me:after{content:"";position:absolute;inset:0;z-index:2;background:#1b1d218c;pointer-events:none}.dmc-alone{position:absolute;inset:0;z-index:2;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:1rem;pointer-events:none}.dmc-alone-title{margin:0;font-size:1.1rem;font-weight:600;color:#fff}.dmc-alone-sub{margin:.35rem 0 0;color:#c9cdd2;font-size:.88rem}.dmc-waiting{position:absolute;z-index:4;top:calc(44px + .75rem);right:.75rem;width:min(20rem,calc(100% - 1.5rem));padding:.85rem;border-radius:10px;background:var(--dmc-raised);box-shadow:0 12px 32px #00000073;border:1px solid #40454c;animation:dmc-drop .2s ease}@keyframes dmc-drop{0%{opacity:0;transform:translateY(-4px)}}.dmc-waiting h3{display:flex;align-items:center;gap:.5rem;margin:0 0 .7rem;font-size:.85rem;font-weight:600;color:var(--dmc-on-dark)}.dmc-waiting-dot{width:8px;height:8px;border-radius:50%;background:var(--dmc-accent);box-shadow:0 0 0 3px var(--dmc-accent-soft)}.dmc-waiting ul{list-style:none;margin:0;padding:0;display:grid;gap:.7rem;max-height:14rem;overflow-y:auto}.dmc-waiting li{display:grid;grid-template-columns:auto 1fr;align-items:center;gap:.4rem .6rem}.dmc-winitial{width:2rem;height:2rem;border-radius:50%;display:grid;place-items:center;background:#454a52;color:#fff;font-weight:600;font-size:.85rem}.dmc-wname{font-weight:500;color:var(--dmc-on-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-wbtns{grid-column:2;display:flex;gap:.4rem}.dmc-waiting .dmc-btn svg{width:15px;height:15px}.dmc-waiting .dmc-btn--go{background:var(--dmc-accent-solid);color:#fff}.dmc-btn--ghost{background:transparent;color:var(--dmc-on-dark);border-color:#4a4f57}.dmc-btn--ghost:hover{background:#ffffff0f;border-color:#626872}.dmc-bar{flex:none;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:.5rem;padding:.5rem 1rem;background:var(--dmc-panel);border-top:1px solid var(--dmc-line)}.dmc-bar-mid{display:flex;align-items:center;gap:.35rem}.dmc-bar-end{display:flex;justify-content:flex-end}.dmc-tool{flex-direction:column;gap:3px;min-width:4.25rem;height:3.5rem;min-height:0;padding:.3rem .6rem;border-radius:10px;border:1px solid transparent;background:transparent;color:var(--dmc-on-dark);flex:none;font-weight:400}.dmc-tool:hover{background:var(--dmc-raised);border-color:transparent}.dmc-tool svg{width:20px;height:20px}.dmc-lbl{font-size:.72rem;line-height:1;color:var(--dmc-muted-dark);white-space:nowrap}.dmc-tool:hover .dmc-lbl{color:var(--dmc-on-dark)}.dmc-tool.is-off{background:transparent;color:#ff8a85}.dmc-tool.is-off:hover{background:#d9453f24}.dmc-tool.is-off .dmc-lbl{color:#ff8a85}.dmc-tool.is-on{background:var(--dmc-accent-soft);color:var(--dmc-accent-text)}.dmc-tool.is-on .dmc-lbl{color:var(--dmc-accent-text)}.dmc-btn--rec svg{color:var(--dmc-red);fill:currentColor;width:16px;height:16px;margin:2px 0}.dmc-btn--rec.is-on{background:#d9453f2e;color:#ff8a85}.dmc-btn--rec.is-on .dmc-lbl{color:#ff8a85}.dmc-btn--rec.is-on svg{color:#ff5a52;animation:dmc-blink 1.4s infinite}.dmc-btn--leave{background:var(--dmc-red);color:#fff;flex-direction:row;gap:.45rem;height:2.75rem;min-width:0;padding:0 1.1rem;border-radius:10px;font-weight:600}.dmc-btn--leave:hover{background:var(--dmc-red-hover)}.dmc-btn--leave .dmc-lbl,.dmc-btn--leave:hover .dmc-lbl{color:#fff;font-size:.85rem}.dmc-btn--leave svg{width:18px;height:18px}.dmc-badge{position:absolute;top:2px;right:10px;min-width:1.1rem;height:1.1rem;padding:0 .28rem;border-radius:999px;background:var(--dmc-accent-solid);color:#fff;font-size:.66rem;font-weight:600;line-height:1.1rem;text-align:center;box-shadow:0 0 0 2px var(--dmc-panel)}.dmc-chat{flex:none;width:0;display:flex;flex-direction:column;background:var(--dmc-panel);color:var(--dmc-on-dark);overflow:hidden;transition:width .2s ease;border-left:1px solid var(--dmc-line);font-size:14px}.dmc-call:not(.is-chat) .dmc-chat{border-left-width:0}.dmc-call.is-chat .dmc-chat{width:20rem}.dmc-chat>*{min-width:20rem}.dmc-chat-head{flex:none;display:flex;align-items:center;gap:.5rem;height:44px;padding:0 .5rem 0 1rem;border-bottom:1px solid var(--dmc-line)}.dmc-chat-titles{flex:1;min-width:0;display:flex;align-items:baseline;gap:.6rem}.dmc-chat-titles h3{margin:0;font-size:.92rem;font-weight:600;color:var(--dmc-on-dark)}.dmc-chat-titles span{font-size:.75rem;color:var(--dmc-muted-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-chat-back,.dmc-chat-close{width:2rem;height:2rem;border:0;border-radius:8px;background:transparent;color:var(--dmc-muted-dark);cursor:pointer;display:grid;place-items:center}.dmc-chat-back:hover,.dmc-chat-close:hover{background:var(--dmc-raised);color:var(--dmc-on-dark)}.dmc-chat-back:focus-visible,.dmc-chat-close:focus-visible{outline:2px solid var(--dmc-accent);outline-offset:1px}.dmc-chat-back svg,.dmc-chat-close svg{width:18px;height:18px}.dmc-chat-back{display:none}.dmc-chat-body{position:relative;flex:1;min-height:0;display:flex}.dmc-messages{list-style:none;margin:0;padding:.5rem 1rem 1rem;flex:1;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:#43484f transparent}.dmc-chat-empty{position:absolute;inset:auto 1.5rem 45%;margin:0;text-align:center;color:var(--dmc-muted-dark);font-size:.85rem;line-height:1.5}.dmc-msg{display:flex;flex-direction:column;margin-top:3px}.dmc-msg.is-first{margin-top:.85rem}.dmc-msg.is-mine{align-items:flex-end}.dmc-msg.is-other{align-items:flex-start}.dmc-msg.is-sys{align-items:center;margin-top:.6rem}.dmc-msg.is-sys+.dmc-msg.is-sys:not(.has-day){margin-top:.2rem}.dmc-msg.has-day.is-first{margin-top:.35rem}.dmc-day{align-self:stretch;display:flex;align-items:center;gap:.75rem;margin:.5rem 0;color:var(--dmc-muted-dark);font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em}.dmc-day:before,.dmc-day:after{content:"";flex:1;height:1px;background:var(--dmc-line)}.dmc-sys{color:#80868e;font-size:.76rem;line-height:1.4;text-align:center}.dmc-meta{display:flex;align-items:baseline;gap:.5rem;margin:0 .15rem .2rem;max-width:88%}.dmc-from{font-size:.8rem;font-weight:500;color:#8fb0ff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-time{font-size:.7rem;color:#80868e;font-variant-numeric:tabular-nums;flex:none}.dmc-bubble{max-width:88%;padding:.45rem .7rem;border-radius:8px;background:var(--dmc-raised);color:var(--dmc-on-dark);line-height:1.42;overflow-wrap:anywhere}.is-mine .dmc-bubble{background:color-mix(in srgb,var(--dmc-accent) 20%,var(--dmc-panel));color:#f1f4ff}.dmc-text{white-space:pre-wrap}.dmc-msg[data-hue="1"] .dmc-from{color:#6fc2ad}.dmc-msg[data-hue="2"] .dmc-from{color:#d6ad72}.dmc-msg[data-hue="3"] .dmc-from{color:#bf9fdc}.dmc-msg[data-hue="4"] .dmc-from{color:#e0948e}.dmc-msg[data-hue="5"] .dmc-from{color:#9fcb8d}.dmc-msg[data-hue="6"] .dmc-from{color:#7fbad6}.dmc-msg[data-hue="7"] .dmc-from{color:#d49fba}.dmc-newchip{position:absolute;left:50%;bottom:.75rem;transform:translate(-50%);display:inline-flex;align-items:center;gap:.35rem;cursor:pointer;border:1px solid #40454c;border-radius:8px;padding:.35rem .7rem;background:var(--dmc-raised);color:var(--dmc-on-dark);font:inherit;font-size:.78rem;box-shadow:0 4px 14px #0006}.dmc-newchip svg{width:14px;height:14px;color:var(--dmc-accent-text)}.dmc-send{flex:none;display:flex;align-items:flex-end;gap:.5rem;padding:.75rem 1rem;border-top:1px solid var(--dmc-line)}.dmc-compose{flex:1;resize:none;border:1px solid #40454c;border-radius:8px;padding:.5rem .7rem;min-height:0;background:var(--dmc-base);color:var(--dmc-on-dark);line-height:1.35rem;font-size:14px;overflow-y:hidden;max-height:calc(6.4rem + 2px);display:block}.dmc-compose::placeholder{color:#80868e}.dmc-compose:focus{outline:0;border-color:var(--dmc-accent);box-shadow:0 0 0 3px var(--dmc-accent-soft)}.dmc-sendbtn{width:2.4rem;height:2.4rem;min-height:0;padding:0;border:0;border-radius:8px;background:var(--dmc-accent-solid);color:#fff;flex:none}.dmc-sendbtn:hover{background:color-mix(in srgb,var(--dmc-accent) 74%,#000)}.dmc-sendbtn:disabled{opacity:1;background:var(--dmc-raised);color:#80868e}.dmc-sendbtn svg{width:17px;height:17px;margin-left:-2px}.dmc-invite{flex:none;width:0;display:flex;flex-direction:column;background:var(--dmc-panel);color:var(--dmc-on-dark);overflow:hidden;transition:width .2s ease;border-left:0 solid var(--dmc-line);font-size:14px}.dmc-invite.is-open{width:22rem;border-left-width:1px}.dmc-invite>*{min-width:22rem}.dmc .dmc-invite ul{margin:0;padding:0;list-style:none}.dmc .dmc-invite li{margin:0}.dmc-inv-body{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:.85rem 1rem 1rem;scrollbar-width:thin;scrollbar-color:#43484f transparent}.dmc-inv-input{font:inherit;font-size:14px;width:100%;min-width:0;min-height:2.4rem;padding:.45rem .7rem;border:1px solid #40454c;border-radius:8px;background:var(--dmc-base);color:var(--dmc-on-dark)}.dmc-inv-input::placeholder{color:#80868e}.dmc-inv-input:focus{outline:0;border-color:var(--dmc-accent);box-shadow:0 0 0 3px var(--dmc-accent-soft)}.dmc-inv-search{position:relative;display:block}.dmc-inv-search .dmc-inv-input{padding-left:2.2rem}.dmc-inv-sico{position:absolute;left:.7rem;top:50%;transform:translateY(-50%);display:inline-flex;color:#80868e;pointer-events:none}.dmc-inv-sico svg{width:16px;height:16px}.dmc-inv-label{margin:1rem 0 .4rem;font-size:.72rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--dmc-muted-dark)}.dmc-inv-chips{margin-top:.75rem}.dmc-inv-chips ul{display:flex;flex-wrap:wrap;gap:.35rem}.dmc-inv-chip{display:inline-flex;align-items:center;gap:.35rem;max-width:100%;padding:.2rem .25rem .2rem .6rem;border-radius:6px;background:var(--dmc-accent-soft);color:var(--dmc-accent-text);font-size:.8rem}.dmc-inv-chipname{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#f1f4ff;font-weight:500}.dmc-inv-chipkind{font-size:.68rem;opacity:.85}.dmc-inv-chipx{display:grid;place-items:center;width:1.4rem;height:1.4rem;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer;padding:0}.dmc-inv-chipx:hover{background:#ffffff1a}.dmc-inv-chipx:focus-visible{outline:2px solid var(--dmc-accent);outline-offset:1px}.dmc-inv-chipx svg{width:13px;height:13px}.dmc-inv-list{list-style:none;margin:0;padding:0;display:grid;gap:2px}.dmc-inv-row{width:100%;display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:.6rem;padding:.45rem .5rem;border:1px solid transparent;border-radius:8px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dmc-inv-row:hover{background:var(--dmc-raised)}.dmc-inv-row:focus-visible{outline:2px solid var(--dmc-accent);outline-offset:-2px}.dmc-inv-row.is-picked{background:var(--dmc-accent-soft)}.dmc-inv-av{width:2rem;height:2rem;border-radius:50%;display:grid;place-items:center;background:#454a52;color:#fff;font-weight:600;font-size:.82rem}.dmc-inv-who{min-width:0;display:flex;flex-direction:column}.dmc-inv-name{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-inv-mail{font-size:.75rem;color:var(--dmc-muted-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-inv-kind{font-size:.68rem;font-weight:600;padding:.1rem .4rem;border-radius:4px;border:1px solid #4a4f57;color:var(--dmc-muted-dark)}.dmc-inv-kind[data-kind=user]{border-color:transparent;background:#3a3f46;color:#c9cdd2}.dmc-inv-tick{width:1.25rem;height:1.25rem;border-radius:50%;display:grid;place-items:center;border:1.5px solid #5a6069;color:transparent}.dmc-inv-tick svg{width:12px;height:12px}.dmc-inv-row.is-picked .dmc-inv-tick{background:var(--dmc-accent-solid);border-color:transparent;color:#fff}.dmc-inv-only{margin:.25rem 0 0;color:var(--dmc-on-dark);font-size:.88rem;line-height:1.5}.dmc-inv-empty{margin:.5rem .25rem;color:var(--dmc-muted-dark);font-size:.85rem;line-height:1.45}.dmc-inv-pair{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr) auto;gap:.4rem}.dmc-inv-add{width:2.4rem;min-height:2.4rem;padding:0;background:var(--dmc-raised);border-color:#40454c;color:var(--dmc-on-dark)}.dmc-inv-add:hover{background:#3a3f46;border-color:#5a6069}.dmc-inv-add svg{width:17px;height:17px}.dmc-inv-foot{flex:none;border-top:1px solid var(--dmc-line);padding:.75rem 1rem calc(.85rem + env(safe-area-inset-bottom,0px));display:grid;gap:.55rem}.dmc-inv-via{border:0;margin:0;padding:0;min-width:0;display:flex;flex-wrap:wrap;align-items:center;gap:.35rem 1rem}.dmc-inv-via legend{float:left;width:100%;margin:0 0 .1rem;padding:0;font-size:.72rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--dmc-muted-dark)}.dmc-inv-via label{display:inline-flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.86rem}.dmc-inv-via input{width:16px;height:16px;margin:0;accent-color:var(--dmc-accent)}.dmc-inv-hint{margin:0;font-size:.75rem;line-height:1.45;color:#80868e}.dmc-inv-status{margin:0;font-size:.82rem;line-height:1.4;color:#9fcb8d}.dmc-inv-status.is-err{color:#ff8a85}.dmc-inv-results{list-style:none;margin:0;padding:0;display:grid;gap:.25rem;max-height:7.5rem;overflow-y:auto;font-size:.8rem}.dmc-inv-res{display:flex;gap:.4rem;line-height:1.35;color:var(--dmc-muted-dark)}.dmc-inv-res:before{content:"";flex:none;width:6px;height:6px;margin-top:.4em;border-radius:50%;background:#6fb56a}.dmc-inv-res.is-err:before{background:#e0605a}.dmc-inv-rname{color:var(--dmc-on-dark);font-weight:500;flex:none;max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dmc-inv-link{font-size:.78rem;color:var(--dmc-muted-dark)}.dmc-inv-btns{display:flex;gap:.5rem}.dmc-inv-btns .dmc-btn{flex:1;min-height:2.5rem}.dmc-inv-copy svg,.dmc-inv-send svg{width:16px;height:16px}.dmc-inv-send:disabled{opacity:1;background:var(--dmc-raised);color:#80868e}.dmc-tool--invite svg{width:20px;height:20px}@media(max-width:760px){.dmc.dmc--app{padding:1rem .75rem}.dmc-panel{grid-template-columns:1fr;gap:1.1rem;padding:1rem}.dmc-panel--msg{padding:2rem 1.25rem}.dmc-title{font-size:1.15rem}.dmc-top{padding:0 .75rem}.dmc-waiting{left:.5rem;right:.5rem;top:calc(44px + .5rem);width:auto}.dmc-bar{display:flex;justify-content:space-around;gap:.25rem;padding:.45rem .4rem calc(.45rem + env(safe-area-inset-bottom,0px))}.dmc-bar-mid{display:contents}.dmc-tool{width:44px;height:44px;min-width:0;padding:0;background:var(--dmc-raised)}.dmc-tool.is-off{background:#d9453f33}.dmc-lbl{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%)}.dmc-bar-side:empty{display:none}.dmc-bar-end{display:contents}.dmc-btn--leave{width:52px;padding:0;background:var(--dmc-red)}.dmc-badge{top:-4px;right:-4px}.dmc-tile--me{right:.5rem;bottom:.5rem}.dmc.dmc--app:has(.dmc-call.is-chat){z-index:1000}.dmc-chat{position:fixed;inset:0;z-index:1000;width:auto;border:0;transform:translateY(100%);transition:transform .22s ease,visibility 0s .22s;visibility:hidden}.dmc-call.is-chat .dmc-chat{width:auto;transform:none;visibility:visible;transition:transform .22s ease}.dmc-chat>*{min-width:0}.dmc-chat-back{display:grid}.dmc-chat-close{display:none}.dmc-chat-head{padding:0 .75rem 0 .35rem}.dmc-send{padding-bottom:calc(.75rem + env(safe-area-inset-bottom,0px))}.dmc.dmc--app:has(.dmc-invite.is-open){z-index:1000}.dmc-invite{position:fixed;inset:0;z-index:1000;width:auto;border:0;transform:translateY(100%);transition:transform .22s ease,visibility 0s .22s;visibility:hidden}.dmc-invite.is-open{width:auto;border:0;transform:none;visibility:visible;transition:transform .22s ease}.dmc-invite>*{min-width:0}}@media(max-width:760px)and (orientation:portrait){.dmc-tile--me{width:clamp(6rem,26%,8.5rem);aspect-ratio:3 / 4}}@media(prefers-reduced-motion:reduce){.dmc *,.dmc *:after{animation:none!important;transition:none!important}}
|
|
1
|
+
.dmc{--dmc-accent: #4f7cff;--dmc-accent-solid: color-mix(in srgb, var(--dmc-accent) 84%, #000);--dmc-accent-soft: color-mix(in srgb, var(--dmc-accent) 18%, transparent);--dmc-accent-text: color-mix(in srgb, var(--dmc-accent) 55%, #fff);--dmc-gap: 8px;--dmc-base: #1b1d21;--dmc-panel: #25282d;--dmc-raised: #2f3338;--dmc-line: #363a40;--dmc-on-dark: #e8eaed;--dmc-muted-dark: #9aa0a8;--dmc-red: #d9453f;--dmc-red-hover: #c23a35;--dmc-page: #f3f4f6;--dmc-card: #fff;--dmc-ink: #1f2328;--dmc-ink-muted: #5b6470;--dmc-border: #dde0e4;--dmc-offset: 0px;--dmc-left: 0px;--dmc-right: 0px;display:flex;flex-direction:column;gap:1rem;color:var(--dmc-ink)}.dmc [hidden]{display:none!important}html.dmc-page,html.dmc-page body{overflow:hidden!important}.dmc.dmc--app{position:fixed;z-index:40;top:var(--dmc-offset);left:var(--dmc-left);right:var(--dmc-right);bottom:0;margin:0;background:var(--dmc-page);overflow-y:auto;overscroll-behavior:contain;padding:2.5rem 1rem;align-items:center;justify-content:safe center;gap:1rem}.dmc.dmc--app[data-stage=in]{padding:0;background:var(--dmc-base);overflow:hidden}.dmc.dmc--app>*{width:min(60rem,100%);flex:none}.dmc.dmc--app>.dmc-call{width:100%;flex:1 1 auto}.dmc-head{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}.dmc.dmc--app:not([data-stage=check])>.dmc-head{width:min(28rem,100%);justify-content:center;text-align:center}.dmc-title{font-size:1.35rem;font-weight:600;margin:0;flex:1 1 auto;letter-spacing:-.01em;color:var(--dmc-ink)}.dmc-btn{font:inherit;font-size:.9rem;cursor:pointer;padding:.5rem .95rem;min-height:2.5rem;border-radius:8px;border:1px solid var(--dmc-border);background:var(--dmc-card);color:inherit;display:inline-flex;align-items:center;justify-content:center;gap:.45rem;position:relative;font-weight:500;transition:background-color .15s,border-color .15s,color .15s}.dmc-btn:hover{border-color:#b9bec5;background:#f7f8f9}.dmc-btn:disabled{opacity:.55;cursor:default}.dmc-btn:focus-visible,.dmc-compose:focus-visible,.dmc-input:focus-visible{outline:2px solid var(--dmc-accent);outline-offset:2px}.dmc-i{display:inline-flex}.dmc-btn svg,.dmc-count svg,.dmc-note svg,.dmc-chip svg{width:18px;height:18px;flex:none}.dmc-btn.is-off{background:#fdecec;border-color:#f3c4c2;color:#b3261e}.dmc-btn--go{background:var(--dmc-accent-solid);border-color:transparent;color:#fff;font-weight:600}.dmc-btn--go:hover{background:color-mix(in srgb,var(--dmc-accent) 74%,#000);border-color:transparent}.dmc-btn--small{padding:.3rem .65rem;min-height:2rem;font-size:.82rem}.dmc-panel{display:grid;grid-template-columns:minmax(0,3fr) minmax(0,2fr);gap:1.75rem;align-items:center;background:var(--dmc-card);border:1px solid var(--dmc-border);border-radius:12px;padding:1.5rem;box-shadow:0 1px 2px #1018280d,0 8px 24px #1018280f}.dmc-panel--msg{display:flex;flex-direction:column;align-items:center;text-align:center;padding:2.5rem 2rem;gap:.25rem;width:min(28rem,100%)!important;align-self:center}.dmc-panel--msg h2{margin:.75rem 0 .25rem;font-size:1.2rem;font-weight:600;color:var(--dmc-ink)}.dmc-panel--msg p{color:var(--dmc-ink-muted);margin:0 0 1.25rem;font-size:.92rem;line-height:1.5}.dmc-pulse{width:64px;height:64px;border-radius:50%;display:grid;place-items:center;color:var(--dmc-accent-solid);background:var(--dmc-accent-soft);box-shadow:0 0 color-mix(in srgb,var(--dmc-accent) 35%,transparent);animation:dmc-pulse 2.2s infinite}.dmc-pulse svg{width:26px;height:26px}@keyframes dmc-pulse{70%{box-shadow:0 0 0 16px transparent}to{box-shadow:0 0 0 0 transparent}}.dmc-preview{position:relative;aspect-ratio:16 / 9;background:var(--dmc-base);border-radius:10px;overflow:hidden}.dmc-preview video,.dmc-tile video{width:100%;height:100%;object-fit:cover;display:block}.dmc-self,.dmc-self-preview{transform:scaleX(-1)}.dmc-nocam{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.6rem;color:var(--dmc-muted-dark);margin:0;font-size:.9rem}.dmc-avatar-dot{width:76px;height:76px;border-radius:50%;display:grid;place-items:center;background:var(--dmc-raised);color:var(--dmc-on-dark);font-size:1.8rem;font-weight:600}.dmc-preview-tools{position:absolute;left:0;right:0;bottom:.75rem;display:flex;justify-content:center;gap:.5rem}.dmc-round{width:44px;height:44px;min-height:0;padding:0;border-radius:10px;border:1px solid rgba(255,255,255,.14);background:#1b1d21b8;color:#fff;backdrop-filter:blur(6px)}.dmc-round:hover{background:#2f3338e6;border-color:#ffffff4d}.dmc-round.is-off{background:var(--dmc-red);border-color:transparent;color:#fff}.dmc-check h2{margin:0 0 .35rem;font-size:1.25rem;font-weight:600;color:var(--dmc-ink)}.dmc-label{display:grid;gap:.35rem;margin:1rem 0 .25rem;font-weight:500;font-size:.88rem;color:var(--dmc-ink)}.dmc-input{font:inherit;font-size:.95rem;padding:.55rem .75rem;min-height:2.5rem;border:1px solid var(--dmc-border);border-radius:8px;background:#fff;color:var(--dmc-ink);min-width:0}.dmc-label .dmc-input:focus{outline:0;border-color:var(--dmc-accent);box-shadow:0 0 0 3px var(--dmc-accent-soft)}.dmc-row{display:flex;gap:.5rem;flex-wrap:wrap;margin:.9rem 0}.dmc-chipbtn{font-size:.86rem}.dmc-round.is-on{background:var(--dmc-accent-solid);border-color:transparent;color:#fff}.dmc-chipbtn.is-on{background:var(--dmc-accent-soft);border-color:var(--dmc-accent);color:inherit}.dmc-note{color:var(--dmc-ink-muted);font-size:.9rem;line-height:1.45;margin:0}.dmc-note--rec{display:flex;gap:.5rem;align-items:flex-start;font-size:.85rem;margin:.25rem 0 0}.dmc-note--rec svg{color:var(--dmc-red);fill:currentColor;width:10px;height:10px;margin-top:.35rem}.dmc-error{color:#b3261e;font-weight:500;font-size:.9rem;margin:.5rem 0 0}.dmc-check .dmc-btn--go{width:100%;min-height:2.75rem;margin-top:1rem;font-size:.95rem}.dmc-call{display:flex;color:var(--dmc-on-dark);background:var(--dmc-base);overflow:hidden;color-scheme:dark;min-height:0;font-size:14px}.dmc-stage{position:relative;flex:1 1 auto;min-width:0;display:flex;flex-direction:column}.dmc-top{order:-1;flex:none;display:flex;align-items:center;justify-content:space-between;gap:1rem;height:44px;padding:0 1rem;background:var(--dmc-panel);border-bottom:1px solid var(--dmc-line)}.dmc-top-l{display:flex;align-items:center;gap:.75rem;min-width:0}.dmc-top .dmc-top-title{margin:0;font-size:.92rem;line-height:1.3;font-weight:600;letter-spacing:0;color:var(--dmc-on-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-timer{flex:none;font-size:.82rem;color:var(--dmc-muted-dark);font-variant-numeric:tabular-nums;padding-left:.75rem;border-left:1px solid var(--dmc-line)}.dmc-top-r{display:flex;align-items:center;gap:.6rem;flex:none}.dmc-rec{display:inline-flex;align-items:center;gap:.4rem;color:#ff8a85;background:#d9453f29;border:1px solid rgba(217,69,63,.45);border-radius:6px;padding:.12rem .5rem;font-size:.74rem;font-weight:600;letter-spacing:.02em}.dmc-rec-dot{width:7px;height:7px;border-radius:50%;background:#ff5a52;animation:dmc-blink 1.4s infinite}@keyframes dmc-blink{50%{opacity:.3}}.dmc-count{display:inline-flex;align-items:center;gap:.35rem;font-size:.82rem;color:var(--dmc-muted-dark);font-variant-numeric:tabular-nums}.dmc-count svg{width:16px;height:16px}.dmc-main{position:relative;flex:1 1 auto;min-height:0}.dmc-remote{position:absolute;inset:var(--dmc-gap);display:flex;flex-wrap:wrap;align-content:center;justify-content:center;gap:var(--dmc-gap);--cols: 1;--rows: 1}.dmc-cell{min-width:0;min-height:0;display:flex;width:var(--tile-w, calc((100% - (var(--cols) - 1) * var(--dmc-gap)) / var(--cols)));height:var(--tile-h, calc((100% - (var(--rows) - 1) * var(--dmc-gap)) / var(--rows)))}.dmc-main[data-tiles="2"] .dmc-remote{--cols: 2}.dmc-main[data-tiles="3"] .dmc-remote,.dmc-main[data-tiles="4"] .dmc-remote{--cols: 2;--rows: 2}.dmc-main[data-tiles="5"] .dmc-remote,.dmc-main[data-tiles="6"] .dmc-remote{--cols: 3;--rows: 2}.dmc-tile{position:relative;margin:0;background:var(--dmc-panel);border-radius:8px;overflow:hidden}.dmc-cell .dmc-tile{flex:1 1 auto;width:100%;height:100%}.dmc-tile video{position:relative;z-index:1}.dmc-avatar{position:absolute;inset:0;display:grid;place-items:center;z-index:0}.dmc-avatar span{width:clamp(3rem,18%,6rem);aspect-ratio:1;border-radius:50%;display:grid;place-items:center;background:var(--dmc-raised);color:var(--dmc-on-dark);font-size:clamp(1.1rem,2.4vw,2.2rem);font-weight:600}.dmc-tile--me .dmc-avatar{z-index:2;background:var(--dmc-panel)}.dmc-chip{position:absolute;left:.5rem;bottom:.5rem;z-index:3;display:inline-flex;align-items:center;gap:.3rem;max-width:calc(100% - 1rem);padding:.18rem .5rem;border-radius:6px;background:#1b1d21c7;color:var(--dmc-on-dark);font-size:.78rem;font-weight:500;line-height:1.35}.dmc-chip>span:last-child{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-chip-mute{display:inline-flex;color:#ff8a85}.dmc-chip-mute svg{width:13px;height:13px}.dmc-tile--me{position:absolute;z-index:2;right:calc(var(--dmc-gap) * 2);bottom:calc(var(--dmc-gap) * 2);width:clamp(10rem,17%,15rem);aspect-ratio:16 / 9;box-shadow:0 6px 20px #00000073;outline:1px solid rgba(255,255,255,.14)}.dmc-tile--me .dmc-chip{left:.35rem;bottom:.35rem;font-size:.7rem;padding:.1rem .4rem}.dmc-main[data-tiles="0"] .dmc-tile--me{inset:0;width:auto;aspect-ratio:auto;border-radius:0;outline:0;box-shadow:none}.dmc-main[data-tiles="0"] .dmc-tile--me .dmc-chip{left:.75rem;bottom:.75rem;font-size:.78rem;padding:.18rem .5rem}.dmc-main[data-tiles="0"] .dmc-tile--me:after{content:"";position:absolute;inset:0;z-index:2;background:#1b1d218c;pointer-events:none}.dmc-alone{position:absolute;inset:0;z-index:2;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:1rem;pointer-events:none}.dmc-alone-title{margin:0;font-size:1.1rem;font-weight:600;color:#fff}.dmc-alone-sub{margin:.35rem 0 0;color:#c9cdd2;font-size:.88rem}.dmc-waiting{position:absolute;z-index:4;top:calc(44px + .75rem);right:.75rem;width:min(20rem,calc(100% - 1.5rem));padding:.85rem;border-radius:10px;background:var(--dmc-raised);box-shadow:0 12px 32px #00000073;border:1px solid #40454c;animation:dmc-drop .2s ease}@keyframes dmc-drop{0%{opacity:0;transform:translateY(-4px)}}.dmc-waiting h3{display:flex;align-items:center;gap:.5rem;margin:0 0 .7rem;font-size:.85rem;font-weight:600;color:var(--dmc-on-dark)}.dmc-waiting-dot{width:8px;height:8px;border-radius:50%;background:var(--dmc-accent);box-shadow:0 0 0 3px var(--dmc-accent-soft)}.dmc-waiting ul{list-style:none;margin:0;padding:0;display:grid;gap:.7rem;max-height:14rem;overflow-y:auto}.dmc-waiting li{display:grid;grid-template-columns:auto 1fr;align-items:center;gap:.4rem .6rem}.dmc-winitial{width:2rem;height:2rem;border-radius:50%;display:grid;place-items:center;background:#454a52;color:#fff;font-weight:600;font-size:.85rem}.dmc-wname{font-weight:500;color:var(--dmc-on-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-wbtns{grid-column:2;display:flex;gap:.4rem}.dmc-waiting .dmc-btn svg{width:15px;height:15px}.dmc-waiting .dmc-btn--go{background:var(--dmc-accent-solid);color:#fff}.dmc-btn--ghost{background:transparent;color:var(--dmc-on-dark);border-color:#4a4f57}.dmc-btn--ghost:hover{background:#ffffff0f;border-color:#626872}.dmc-bar{flex:none;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:.5rem;padding:.5rem 1rem;background:var(--dmc-panel);border-top:1px solid var(--dmc-line)}.dmc-bar-mid{display:flex;align-items:center;gap:.35rem}.dmc-bar-end{display:flex;justify-content:flex-end}.dmc-tool{flex-direction:column;gap:3px;min-width:4.25rem;height:3.5rem;min-height:0;padding:.3rem .6rem;border-radius:10px;border:1px solid transparent;background:transparent;color:var(--dmc-on-dark);flex:none;font-weight:400}.dmc-tool:hover{background:var(--dmc-raised);border-color:transparent}.dmc-tool svg{width:20px;height:20px}.dmc-lbl{font-size:.72rem;line-height:1;color:var(--dmc-muted-dark);white-space:nowrap}.dmc-tool:hover .dmc-lbl{color:var(--dmc-on-dark)}.dmc-tool.is-off{background:transparent;color:#ff8a85}.dmc-tool.is-off:hover{background:#d9453f24}.dmc-tool.is-off .dmc-lbl{color:#ff8a85}.dmc-tool.is-on{background:var(--dmc-accent-soft);color:var(--dmc-accent-text)}.dmc-tool.is-on .dmc-lbl{color:var(--dmc-accent-text)}.dmc-btn--rec svg{color:var(--dmc-red);fill:currentColor;width:16px;height:16px;margin:2px 0}.dmc-btn--rec.is-on{background:#d9453f2e;color:#ff8a85}.dmc-btn--rec.is-on .dmc-lbl{color:#ff8a85}.dmc-btn--rec.is-on svg{color:#ff5a52;animation:dmc-blink 1.4s infinite}.dmc-btn--leave{background:var(--dmc-red);color:#fff;flex-direction:row;gap:.45rem;height:2.75rem;min-width:0;padding:0 1.1rem;border-radius:10px;font-weight:600}.dmc-btn--leave:hover{background:var(--dmc-red-hover)}.dmc-btn--leave .dmc-lbl,.dmc-btn--leave:hover .dmc-lbl{color:#fff;font-size:.85rem}.dmc-btn--leave svg{width:18px;height:18px}.dmc-badge{position:absolute;top:2px;right:10px;min-width:1.1rem;height:1.1rem;padding:0 .28rem;border-radius:999px;background:var(--dmc-accent-solid);color:#fff;font-size:.66rem;font-weight:600;line-height:1.1rem;text-align:center;box-shadow:0 0 0 2px var(--dmc-panel)}.dmc-chat{flex:none;width:0;display:flex;flex-direction:column;background:var(--dmc-panel);color:var(--dmc-on-dark);overflow:hidden;transition:width .2s ease;border-left:1px solid var(--dmc-line);font-size:14px}.dmc-call:not(.is-chat) .dmc-chat{border-left-width:0}.dmc-call.is-chat .dmc-chat{width:20rem}.dmc-chat>*{min-width:20rem}.dmc-chat-head{flex:none;display:flex;align-items:center;gap:.5rem;height:44px;padding:0 .5rem 0 1rem;border-bottom:1px solid var(--dmc-line)}.dmc-chat-titles{flex:1;min-width:0;display:flex;align-items:baseline;gap:.6rem}.dmc-chat-titles h3{margin:0;font-size:.92rem;font-weight:600;color:var(--dmc-on-dark)}.dmc-chat-titles span{font-size:.75rem;color:var(--dmc-muted-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-chat-back,.dmc-chat-close{width:2rem;height:2rem;border:0;border-radius:8px;background:transparent;color:var(--dmc-muted-dark);cursor:pointer;display:grid;place-items:center}.dmc-chat-back:hover,.dmc-chat-close:hover{background:var(--dmc-raised);color:var(--dmc-on-dark)}.dmc-chat-back:focus-visible,.dmc-chat-close:focus-visible{outline:2px solid var(--dmc-accent);outline-offset:1px}.dmc-chat-back svg,.dmc-chat-close svg{width:18px;height:18px}.dmc-chat-back{display:none}.dmc-chat-body{position:relative;flex:1;min-height:0;display:flex}.dmc-messages{list-style:none;margin:0;padding:.5rem 1rem 1rem;flex:1;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:#43484f transparent}.dmc-chat-empty{position:absolute;inset:auto 1.5rem 45%;margin:0;text-align:center;color:var(--dmc-muted-dark);font-size:.85rem;line-height:1.5}.dmc-msg{display:flex;flex-direction:column;margin-top:3px}.dmc-msg.is-first{margin-top:.85rem}.dmc-msg.is-mine{align-items:flex-end}.dmc-msg.is-other{align-items:flex-start}.dmc-msg.is-sys{align-items:center;margin-top:.6rem}.dmc-msg.is-sys+.dmc-msg.is-sys:not(.has-day){margin-top:.2rem}.dmc-msg.has-day.is-first{margin-top:.35rem}.dmc-day{align-self:stretch;display:flex;align-items:center;gap:.75rem;margin:.5rem 0;color:var(--dmc-muted-dark);font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em}.dmc-day:before,.dmc-day:after{content:"";flex:1;height:1px;background:var(--dmc-line)}.dmc-sys{color:#80868e;font-size:.76rem;line-height:1.4;text-align:center}.dmc-meta{display:flex;align-items:baseline;gap:.5rem;margin:0 .15rem .2rem;max-width:88%}.dmc-from{font-size:.8rem;font-weight:500;color:#8fb0ff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-time{font-size:.7rem;color:#80868e;font-variant-numeric:tabular-nums;flex:none}.dmc-bubble{max-width:88%;padding:.45rem .7rem;border-radius:8px;background:var(--dmc-raised);color:var(--dmc-on-dark);line-height:1.42;overflow-wrap:anywhere}.is-mine .dmc-bubble{background:color-mix(in srgb,var(--dmc-accent) 20%,var(--dmc-panel));color:#f1f4ff}.dmc-text{white-space:pre-wrap}.dmc-msg[data-hue="1"] .dmc-from{color:#6fc2ad}.dmc-msg[data-hue="2"] .dmc-from{color:#d6ad72}.dmc-msg[data-hue="3"] .dmc-from{color:#bf9fdc}.dmc-msg[data-hue="4"] .dmc-from{color:#e0948e}.dmc-msg[data-hue="5"] .dmc-from{color:#9fcb8d}.dmc-msg[data-hue="6"] .dmc-from{color:#7fbad6}.dmc-msg[data-hue="7"] .dmc-from{color:#d49fba}.dmc-newchip{position:absolute;left:50%;bottom:.75rem;transform:translate(-50%);display:inline-flex;align-items:center;gap:.35rem;cursor:pointer;border:1px solid #40454c;border-radius:8px;padding:.35rem .7rem;background:var(--dmc-raised);color:var(--dmc-on-dark);font:inherit;font-size:.78rem;box-shadow:0 4px 14px #0006}.dmc-newchip svg{width:14px;height:14px;color:var(--dmc-accent-text)}.dmc-send{flex:none;display:flex;align-items:flex-end;gap:.5rem;padding:.75rem 1rem;border-top:1px solid var(--dmc-line)}.dmc-compose{flex:1;resize:none;border:1px solid #40454c;border-radius:8px;padding:.5rem .7rem;min-height:0;background:var(--dmc-base);color:var(--dmc-on-dark);line-height:1.35rem;font-size:14px;overflow-y:hidden;max-height:calc(6.4rem + 2px);display:block}.dmc-compose::placeholder{color:#80868e}.dmc-compose:focus{outline:0;border-color:var(--dmc-accent);box-shadow:0 0 0 3px var(--dmc-accent-soft)}.dmc-sendbtn{width:2.4rem;height:2.4rem;min-height:0;padding:0;border:0;border-radius:8px;background:var(--dmc-accent-solid);color:#fff;flex:none}.dmc-sendbtn:hover{background:color-mix(in srgb,var(--dmc-accent) 74%,#000)}.dmc-sendbtn:disabled{opacity:1;background:var(--dmc-raised);color:#80868e}.dmc-sendbtn svg{width:17px;height:17px;margin-left:-2px}.dmc-invite{flex:none;width:0;display:flex;flex-direction:column;background:var(--dmc-panel);color:var(--dmc-on-dark);overflow:hidden;transition:width .2s ease;border-left:0 solid var(--dmc-line);font-size:14px}.dmc-invite.is-open{width:22rem;border-left-width:1px}.dmc-invite>*{min-width:22rem}.dmc .dmc-invite ul{margin:0;padding:0;list-style:none}.dmc .dmc-invite li{margin:0}.dmc-inv-body{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:.85rem 1rem 1rem;scrollbar-width:thin;scrollbar-color:#43484f transparent}.dmc-inv-input{font:inherit;font-size:14px;width:100%;min-width:0;min-height:2.4rem;padding:.45rem .7rem;border:1px solid #40454c;border-radius:8px;background:var(--dmc-base);color:var(--dmc-on-dark)}.dmc-inv-input::placeholder{color:#80868e}.dmc-inv-input:focus{outline:0;border-color:var(--dmc-accent);box-shadow:0 0 0 3px var(--dmc-accent-soft)}.dmc-inv-search{position:relative;display:block}.dmc-inv-search .dmc-inv-input{padding-left:2.2rem}.dmc-inv-sico{position:absolute;left:.7rem;top:50%;transform:translateY(-50%);display:inline-flex;color:#80868e;pointer-events:none}.dmc-inv-sico svg{width:16px;height:16px}.dmc-inv-label{margin:1rem 0 .4rem;font-size:.72rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--dmc-muted-dark)}.dmc-inv-chips{margin-top:.75rem}.dmc-inv-chips ul{display:flex;flex-wrap:wrap;gap:.35rem}.dmc-inv-chip{display:inline-flex;align-items:center;gap:.35rem;max-width:100%;padding:.2rem .25rem .2rem .6rem;border-radius:6px;background:var(--dmc-accent-soft);color:var(--dmc-accent-text);font-size:.8rem}.dmc-inv-chipname{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#f1f4ff;font-weight:500}.dmc-inv-chipkind{font-size:.68rem;opacity:.85}.dmc-inv-chipx{display:grid;place-items:center;width:1.4rem;height:1.4rem;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer;padding:0}.dmc-inv-chipx:hover{background:#ffffff1a}.dmc-inv-chipx:focus-visible{outline:2px solid var(--dmc-accent);outline-offset:1px}.dmc-inv-chipx svg{width:13px;height:13px}.dmc-inv-list{list-style:none;margin:0;padding:0;display:grid;gap:2px}.dmc-inv-row{width:100%;display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:.6rem;padding:.45rem .5rem;border:1px solid transparent;border-radius:8px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dmc-inv-row:hover{background:var(--dmc-raised)}.dmc-inv-row:focus-visible{outline:2px solid var(--dmc-accent);outline-offset:-2px}.dmc-inv-row.is-picked{background:var(--dmc-accent-soft)}.dmc-inv-av{width:2rem;height:2rem;border-radius:50%;display:grid;place-items:center;background:#454a52;color:#fff;font-weight:600;font-size:.82rem}.dmc-inv-who{min-width:0;display:flex;flex-direction:column}.dmc-inv-name{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-inv-mail{font-size:.75rem;color:var(--dmc-muted-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-inv-kind{font-size:.68rem;font-weight:600;padding:.1rem .4rem;border-radius:4px;border:1px solid #4a4f57;color:var(--dmc-muted-dark)}.dmc-inv-kind[data-kind=user]{border-color:transparent;background:#3a3f46;color:#c9cdd2}.dmc-inv-tick{width:1.25rem;height:1.25rem;border-radius:50%;display:grid;place-items:center;border:1.5px solid #5a6069;color:transparent}.dmc-inv-tick svg{width:12px;height:12px}.dmc-inv-row.is-picked .dmc-inv-tick{background:var(--dmc-accent-solid);border-color:transparent;color:#fff}.dmc-inv-only{margin:.25rem 0 0;color:var(--dmc-on-dark);font-size:.88rem;line-height:1.5}.dmc-inv-empty{margin:.5rem .25rem;color:var(--dmc-muted-dark);font-size:.85rem;line-height:1.45}.dmc-inv-pair{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr) auto;gap:.4rem}.dmc-inv-add{width:2.4rem;min-height:2.4rem;padding:0;background:var(--dmc-raised);border-color:#40454c;color:var(--dmc-on-dark)}.dmc-inv-add:hover{background:#3a3f46;border-color:#5a6069}.dmc-inv-add svg{width:17px;height:17px}.dmc-inv-foot{flex:none;border-top:1px solid var(--dmc-line);padding:.75rem 1rem calc(.85rem + env(safe-area-inset-bottom,0px));display:grid;gap:.55rem}.dmc-inv-via{border:0;margin:0;padding:0;min-width:0;display:flex;flex-wrap:wrap;align-items:center;gap:.35rem 1rem}.dmc-inv-via legend{float:left;width:100%;margin:0 0 .1rem;padding:0;font-size:.72rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--dmc-muted-dark)}.dmc-inv-via label{display:inline-flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.86rem}.dmc-inv-via input{width:16px;height:16px;margin:0;accent-color:var(--dmc-accent)}.dmc-inv-hint{margin:0;font-size:.75rem;line-height:1.45;color:#80868e}.dmc-inv-status{margin:0;font-size:.82rem;line-height:1.4;color:#9fcb8d}.dmc-inv-status.is-err{color:#ff8a85}.dmc-inv-results{list-style:none;margin:0;padding:0;display:grid;gap:.25rem;max-height:7.5rem;overflow-y:auto;font-size:.8rem}.dmc-inv-res{display:flex;gap:.4rem;line-height:1.35;color:var(--dmc-muted-dark)}.dmc-inv-res:before{content:"";flex:none;width:6px;height:6px;margin-top:.4em;border-radius:50%;background:#6fb56a}.dmc-inv-res.is-err:before{background:#e0605a}.dmc-inv-rname{color:var(--dmc-on-dark);font-weight:500;flex:none;max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dmc-inv-link{font-size:.78rem;color:var(--dmc-muted-dark)}.dmc-inv-btns{display:flex;gap:.5rem}.dmc-inv-btns .dmc-btn{flex:1;min-height:2.5rem}.dmc-inv-copy svg,.dmc-inv-send svg{width:16px;height:16px}.dmc-inv-send:disabled{opacity:1;background:var(--dmc-raised);color:#80868e}.dmc-tool--invite svg{width:20px;height:20px}@media(max-width:760px){.dmc.dmc--app{padding:1rem .75rem}.dmc-panel{grid-template-columns:1fr;gap:1.1rem;padding:1rem}.dmc-panel--msg{padding:2rem 1.25rem}.dmc-title{font-size:1.15rem}.dmc-top{padding:0 .75rem}.dmc-waiting{left:.5rem;right:.5rem;top:calc(44px + .5rem);width:auto}.dmc-bar{display:flex;justify-content:space-around;gap:.25rem;padding:.45rem .4rem calc(.45rem + env(safe-area-inset-bottom,0px))}.dmc-bar-mid{display:contents}.dmc-tool{width:44px;height:44px;min-width:0;padding:0;background:var(--dmc-raised)}.dmc-tool.is-off{background:#d9453f33}.dmc-lbl{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%)}.dmc-bar-side:empty{display:none}.dmc-bar-end{display:contents}.dmc-btn--leave{width:52px;padding:0;background:var(--dmc-red)}.dmc-badge{top:-4px;right:-4px}.dmc-tile--me{right:.5rem;bottom:.5rem}.dmc.dmc--app:has(.dmc-call.is-chat){z-index:1000}.dmc-chat{position:fixed;inset:0;z-index:1000;width:auto;border:0;transform:translateY(100%);transition:transform .22s ease,visibility 0s .22s;visibility:hidden}.dmc-call.is-chat .dmc-chat{width:auto;transform:none;visibility:visible;transition:transform .22s ease}.dmc-chat>*{min-width:0}.dmc-chat-back{display:grid}.dmc-chat-close{display:none}.dmc-chat-head{padding:0 .75rem 0 .35rem}.dmc-send{padding-bottom:calc(.75rem + env(safe-area-inset-bottom,0px))}.dmc.dmc--app:has(.dmc-invite.is-open){z-index:1000}.dmc-invite{position:fixed;inset:0;z-index:1000;width:auto;border:0;transform:translateY(100%);transition:transform .22s ease,visibility 0s .22s;visibility:hidden}.dmc-invite.is-open{width:auto;border:0;transform:none;visibility:visible;transition:transform .22s ease}.dmc-invite>*{min-width:0}}@media(max-width:760px)and (orientation:portrait){.dmc-tile--me{width:clamp(6rem,26%,8.5rem);aspect-ratio:3 / 4}}@media(prefers-reduced-motion:reduce){.dmc *,.dmc *:after{animation:none!important;transition:none!important}}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* The screen is domma-reactive (M) - stages, controls, people waiting, chat -
|
|
13
13
|
* and plain code only where bindings cannot reach: the <video> elements'
|
|
14
14
|
* streams, the recording canvas, and the RTCPeerConnections themselves.
|
|
15
|
+
* Background blur is blur.js, imported the first time it is turned on; the choice
|
|
16
|
+
* is remembered in this browser (localStorage 'dm-call:blur').
|
|
15
17
|
* Browsers connect to each other directly (a mesh - up to 6 people); the
|
|
16
18
|
* site only passes the messages that set that up (see _lib/call/routes.js).
|
|
17
19
|
* Every piece of text goes in through bindings (textContent), never as HTML.
|
|
@@ -44,6 +46,7 @@ const TEMPLATE = `
|
|
|
44
46
|
<div class="dmc-preview-tools">
|
|
45
47
|
<button type="button" class="dmc-btn dmc-round" data-on-click="toggleMic" data-bind-class="!micOn.value && 'is-off'" data-bind-aria-pressed="!micOn.value" data-bind-aria-label="micOn.value ? 'Turn the microphone off' : 'Turn the microphone on'">${pair('micOn.value', 'mic', 'mic-off')}</button>
|
|
46
48
|
<button type="button" class="dmc-btn dmc-round" data-on-click="toggleCam" data-bind-class="!camOn.value && 'is-off'" data-bind-aria-pressed="!camOn.value" data-bind-aria-label="camOn.value ? 'Turn the camera off' : 'Turn the camera on'">${pair('camOn.value', 'video', 'video-off')}</button>
|
|
49
|
+
<button type="button" class="dmc-btn dmc-round dmc-round--blur" data-on-click="toggleBlur" data-bind-hidden="!blurOffered.value" data-bind-disabled="blurBusy.value" data-bind-class="blurOn.value && 'is-on'" data-bind-aria-pressed="blurOn.value" data-bind-aria-label="blurOn.value ? 'Stop blurring your background' : 'Blur your background'" data-bind-title="blurLabel.value">${ico('sparkles')}</button>
|
|
47
50
|
</div>
|
|
48
51
|
</div>
|
|
49
52
|
<div class="dmc-check">
|
|
@@ -54,7 +57,9 @@ const TEMPLATE = `
|
|
|
54
57
|
<div class="dmc-row">
|
|
55
58
|
<button type="button" class="dmc-btn dmc-chipbtn" data-on-click="toggleMic" data-bind-class="!micOn.value && 'is-off'" data-bind-aria-pressed="!micOn.value">${pair('micOn.value', 'mic', 'mic-off')}<span data-bind-text="micOn.value ? 'Mic on' : 'Mic off'"></span></button>
|
|
56
59
|
<button type="button" class="dmc-btn dmc-chipbtn" data-on-click="toggleCam" data-bind-class="!camOn.value && 'is-off'" data-bind-aria-pressed="!camOn.value">${pair('camOn.value', 'video', 'video-off')}<span data-bind-text="camOn.value ? 'Camera on' : 'Camera off'"></span></button>
|
|
60
|
+
<button type="button" class="dmc-btn dmc-chipbtn" data-on-click="toggleBlur" data-bind-hidden="!blurOffered.value" data-bind-disabled="blurBusy.value" data-bind-class="blurOn.value && 'is-on'" data-bind-aria-pressed="blurOn.value">${ico('sparkles')}<span data-bind-text="blurLabel.value"></span></button>
|
|
57
61
|
</div>
|
|
62
|
+
<p class="dmc-note" data-bind-hidden="!blurNote.value" data-bind-text="blurNote.value" role="status"></p>
|
|
58
63
|
<p class="dmc-note dmc-note--rec" data-bind-hidden="!recordingAllowed.value">${ico('circle')}<span>The host may record this call. You will see a red Recording notice while they do.</span></p>
|
|
59
64
|
<p class="dmc-error" data-bind-hidden="!error.value" data-bind-text="error.value" role="alert"></p>
|
|
60
65
|
<button type="button" class="dmc-btn dmc-btn--go" data-on-click="join" data-bind-disabled="busy.value">${ico('phone')}<span>Join the call</span></button>
|
|
@@ -118,6 +123,7 @@ const TEMPLATE = `
|
|
|
118
123
|
<div class="dmc-bar-mid">
|
|
119
124
|
<button type="button" class="dmc-btn dmc-tool" data-on-click="toggleMic" data-bind-class="!micOn.value && 'is-off'" data-bind-aria-pressed="!micOn.value" data-bind-aria-label="micOn.value ? 'Mute' : 'Unmute'">${pair('micOn.value', 'mic', 'mic-off')}<span class="dmc-lbl" data-bind-text="micOn.value ? 'Mute' : 'Unmute'"></span></button>
|
|
120
125
|
<button type="button" class="dmc-btn dmc-tool" data-on-click="toggleCam" data-bind-class="!camOn.value && 'is-off'" data-bind-aria-pressed="!camOn.value" data-bind-aria-label="camOn.value ? 'Stop video' : 'Start video'">${pair('camOn.value', 'video', 'video-off')}<span class="dmc-lbl" data-bind-text="camOn.value ? 'Stop video' : 'Start video'"></span></button>
|
|
126
|
+
<button type="button" class="dmc-btn dmc-tool dmc-tool--blur" data-on-click="toggleBlur" data-bind-hidden="!blurOffered.value" data-bind-disabled="blurBusy.value" data-bind-class="blurOn.value && 'is-on'" data-bind-aria-pressed="blurOn.value" data-bind-aria-label="blurOn.value ? 'Stop blurring your background' : 'Blur your background'" data-bind-title="blurOn.value ? 'Stop blurring your background' : 'Blur your background'">${ico('sparkles')}<span class="dmc-lbl" data-bind-text="blurLabel.value"></span></button>
|
|
121
127
|
<button type="button" class="dmc-btn dmc-tool dmc-tool--share" data-on-click="toggleShare" data-bind-hidden="!canShare" data-bind-class="sharing.value && 'is-on'" data-bind-aria-pressed="sharing.value" data-bind-aria-label="sharing.value ? 'Stop sharing' : 'Share your screen'">${ico('monitor')}<span class="dmc-lbl" data-bind-text="sharing.value ? 'Stop share' : 'Share'"></span></button>
|
|
122
128
|
<button type="button" class="dmc-btn dmc-tool dmc-tool--chat" data-on-click="toggleSide" data-bind-class="sideOpen.value && 'is-on'" data-bind-aria-pressed="sideOpen.value" data-bind-aria-label="chatLabel.value">${ico('message-circle')}<span class="dmc-lbl">Chat</span><span class="dmc-badge" data-bind-hidden="!unread.value || sideOpen.value" data-bind-text="unreadText.value"></span></button>
|
|
123
129
|
<button type="button" class="dmc-btn dmc-tool dmc-tool--invite" data-slot="invite" data-on-click="toggleInvite" data-bind-hidden="!canInvite.value" data-bind-class="inviteOpen.value && 'is-on'" data-bind-aria-pressed="inviteOpen.value" aria-label="Add people">${ico('user-plus')}<span class="dmc-lbl">Add people</span></button>
|
|
@@ -235,6 +241,17 @@ function mount(el) {
|
|
|
235
241
|
}
|
|
236
242
|
|
|
237
243
|
// ---- State the screen shows -------------------------------------------------------
|
|
244
|
+
// Blur needs WebAssembly, a canvas that can become a video track, and WebGL2 (MediaPipe reads frames through it
|
|
245
|
+
// even when it segments on the CPU) - offered only where all three are there.
|
|
246
|
+
const canBlur = typeof WebAssembly === 'object' && typeof HTMLCanvasElement.prototype.captureStream === 'function'
|
|
247
|
+
&& (() => {
|
|
248
|
+
try {
|
|
249
|
+
const gl = document.createElement('canvas').getContext('webgl2');
|
|
250
|
+
gl?.getExtension('WEBGL_lose_context')?.loseContext(); // only asked; give the context straight back
|
|
251
|
+
return Boolean(gl);
|
|
252
|
+
} catch { return false; }
|
|
253
|
+
})();
|
|
254
|
+
let blurWanted = false; // what the caller last asked for (a slow start may finish after they changed their mind)
|
|
238
255
|
const stage = M.observable('check'); // check | waiting | in | ended | away | error
|
|
239
256
|
const name = M.observable(cfg.name || '');
|
|
240
257
|
const micOn = M.observable(true);
|
|
@@ -274,6 +291,14 @@ function mount(el) {
|
|
|
274
291
|
const canSend = M.computed(() => /\S/.test(draft.value || ''));
|
|
275
292
|
const title = M.observable(cfg.title || 'Call');
|
|
276
293
|
|
|
294
|
+
// ---- Background blur (blur.js, loaded the first time it is turned on) ------------------
|
|
295
|
+
const hasCam = M.observable(false);
|
|
296
|
+
const blurOn = M.observable(false);
|
|
297
|
+
const blurBusy = M.observable(false);
|
|
298
|
+
const blurNote = M.observable('');
|
|
299
|
+
const blurOffered = M.computed(() => canBlur && hasCam.value);
|
|
300
|
+
const blurLabel = M.computed(() => (blurBusy.value ? 'Starting blur…' : blurOn.value ? 'Blur on' : 'Blur'));
|
|
301
|
+
|
|
277
302
|
// ---- Add people (the host, when the Tool allows it: join answers canInvite) ----------------
|
|
278
303
|
const canInvite = M.observable(false);
|
|
279
304
|
// What the Tool offers this host (join's inviteOptions): search the team (+ contacts when typed), contacts and
|
|
@@ -325,13 +350,69 @@ function mount(el) {
|
|
|
325
350
|
// peers is kept in step with tiles (a plain observable, not a computed over a computed).
|
|
326
351
|
const stopTiles = M.effect(() => { tiles.value = String(Math.min(6, peers.value.length)); });
|
|
327
352
|
|
|
353
|
+
let blur = null; // {track, stop, stream} while the background is blurred
|
|
354
|
+
const BLUR_KEY = 'dm-call:blur';
|
|
355
|
+
/** The picture this browser sends while not sharing a screen: the blurred one, or the camera's. */
|
|
356
|
+
const camVideo = () => blur?.track || local?.getVideoTracks()[0] || null;
|
|
357
|
+
|
|
328
358
|
const selfVideos = () => el.querySelectorAll('.dmc-self, .dmc-self-preview');
|
|
329
|
-
const
|
|
359
|
+
const selfStream = () => screen || blur?.stream || local;
|
|
360
|
+
const showSelf = () => { const s = selfStream(); selfVideos().forEach(v => { v.srcObject = s; }); };
|
|
361
|
+
/** Each connection's video sender: the camera's, or one added for a screen when there was no camera. */
|
|
362
|
+
const videoSender = (entry) => entry.pc.getSenders().find(s => s.track?.kind === 'video') || entry.videoSender || null;
|
|
363
|
+
/** Put the current camera picture (blurred or not) on every connection - not while a screen is shared. */
|
|
364
|
+
const sendCam = () => { if (!screen) pcs.forEach(entry => videoSender(entry)?.replaceTrack(camVideo())); };
|
|
365
|
+
|
|
366
|
+
async function startBlurring() {
|
|
367
|
+
const cam = local?.getVideoTracks()[0];
|
|
368
|
+
if (!cam || blur || blurBusy.value) return;
|
|
369
|
+
blurBusy.value = true;
|
|
370
|
+
blurNote.value = '';
|
|
371
|
+
try {
|
|
372
|
+
const mod = await import(new URL(`./blur.js${new URL(import.meta.url).search}`, import.meta.url).href);
|
|
373
|
+
const started = await mod.startBlur(cam);
|
|
374
|
+
if (!local || local.getVideoTracks()[0] !== cam || !blurWanted) { started.stop(); return; } // left, switched off or lost the camera meanwhile
|
|
375
|
+
blur = Object.assign(started, {stream: new MediaStream([started.track])});
|
|
376
|
+
blur.track.enabled = camOn.value;
|
|
377
|
+
blur.paused = Boolean(screen);
|
|
378
|
+
// It stops itself if this machine cannot keep it going: back to the plain camera, and say so.
|
|
379
|
+
blur.track.addEventListener('ended', () => {
|
|
380
|
+
if (blur !== started) return;
|
|
381
|
+
blur = null; blurOn.value = false; sendCam(); showSelf();
|
|
382
|
+
blurSay(started.reason === 'slow' ? 'Background blur turned off - this device could not keep up.'
|
|
383
|
+
: 'Background blur stopped working, so it is off. You can try turning it on again.');
|
|
384
|
+
});
|
|
385
|
+
blurOn.value = true;
|
|
386
|
+
sendCam();
|
|
387
|
+
showSelf();
|
|
388
|
+
} catch (err) {
|
|
389
|
+
console.warn('[call] blur:', err?.message || err);
|
|
390
|
+
blurOn.value = false;
|
|
391
|
+
blurSay('Background blur could not start in this browser.');
|
|
392
|
+
try { localStorage.removeItem(BLUR_KEY); } catch { /* storage blocked */ }
|
|
393
|
+
} finally { blurBusy.value = false; }
|
|
394
|
+
}
|
|
395
|
+
/** A blur notice: under the buttons before the call, and as a line in the chat (this browser only) once in it. */
|
|
396
|
+
let blurSaid = 0;
|
|
397
|
+
function blurSay(text) {
|
|
398
|
+
blurNote.value = text;
|
|
399
|
+
if (stage.value === 'in') addItem({id: `blur-${Date.now()}-${++blurSaid}`, kind: 'system', at: new Date().toISOString(), text});
|
|
400
|
+
}
|
|
401
|
+
function stopBlurring() {
|
|
402
|
+
const was = blur;
|
|
403
|
+
blur = null;
|
|
404
|
+
blurOn.value = false;
|
|
405
|
+
if (!was) return;
|
|
406
|
+
sendCam();
|
|
407
|
+
showSelf();
|
|
408
|
+
was.stop();
|
|
409
|
+
}
|
|
330
410
|
|
|
331
411
|
async function getDevices() {
|
|
332
412
|
try {
|
|
333
413
|
local = await navigator.mediaDevices.getUserMedia({video: {width: {ideal: 1280}, height: {ideal: 720}}, audio: true});
|
|
334
414
|
deviceNote.value = 'Your camera and microphone are working.';
|
|
415
|
+
hasCam.value = local.getVideoTracks().length > 0;
|
|
335
416
|
} catch {
|
|
336
417
|
try {
|
|
337
418
|
local = await navigator.mediaDevices.getUserMedia({audio: true});
|
|
@@ -345,10 +426,13 @@ function mount(el) {
|
|
|
345
426
|
}
|
|
346
427
|
}
|
|
347
428
|
showSelf();
|
|
429
|
+
try { blurWanted = localStorage.getItem(BLUR_KEY) === '1'; } catch { /* storage blocked */ }
|
|
430
|
+
if (blurWanted && canBlur && hasCam.value) startBlurring();
|
|
348
431
|
}
|
|
349
432
|
const setTracks = () => {
|
|
350
433
|
local?.getAudioTracks().forEach(t => { t.enabled = micOn.value; });
|
|
351
434
|
local?.getVideoTracks().forEach(t => { t.enabled = camOn.value; });
|
|
435
|
+
if (blur) blur.track.enabled = camOn.value;
|
|
352
436
|
};
|
|
353
437
|
|
|
354
438
|
// ---- Peer connections (perfect negotiation) ----------------------------------------
|
|
@@ -357,7 +441,8 @@ function mount(el) {
|
|
|
357
441
|
const pc = new RTCPeerConnection({iceServers});
|
|
358
442
|
const entry = {pc, polite: me.id > peer.id, makingOffer: false, ignore: false, stream: new MediaStream(), chain: Promise.resolve()};
|
|
359
443
|
pcs.set(peer.id, entry);
|
|
360
|
-
const
|
|
444
|
+
const cam = camVideo();
|
|
445
|
+
const out = [...(local?.getAudioTracks() || []), ...(screen ? screen.getVideoTracks() : cam ? [cam] : [])];
|
|
361
446
|
out.forEach(track => pc.addTrack(track, local));
|
|
362
447
|
pc.ontrack = (e) => { entry.stream.addTrack(e.track); attach(); if (e.track.kind === 'audio') addToMix(e.track); };
|
|
363
448
|
pc.onicecandidate = (e) => { if (e.candidate) post('signal', {to: peer.id, data: {candidate: e.candidate}}); };
|
|
@@ -606,6 +691,8 @@ function mount(el) {
|
|
|
606
691
|
clearInterval(clockTimer); clockTimer = null;
|
|
607
692
|
stage.value = kind;
|
|
608
693
|
stopRecording();
|
|
694
|
+
blurWanted = false;
|
|
695
|
+
stopBlurring();
|
|
609
696
|
for (const id of [...pcs.keys()]) dropPeer(id);
|
|
610
697
|
[local, screen].forEach(s => s?.getTracks().forEach(t => t.stop()));
|
|
611
698
|
me = null;
|
|
@@ -672,7 +759,7 @@ function mount(el) {
|
|
|
672
759
|
title, people, tiles, chatLabel, askName: cfg.askName !== false, canShare: Boolean(navigator.mediaDevices?.getDisplayMedia), canRecord,
|
|
673
760
|
stage, name, initial, micOn, camOn, sharing, recording, iRecord, recordingAllowed, host, busy, error, deviceNote, sideOpen, unread,
|
|
674
761
|
unreadText, draft, canSend, peers, waiting, waitingTitle, messages, newBelow, endTitle, endText, meCaption, countText, countTitle,
|
|
675
|
-
clock, chatSub, aloneText,
|
|
762
|
+
clock, chatSub, aloneText, blurOn, blurBusy, blurNote, blurOffered, blurLabel,
|
|
676
763
|
canInvite, inviteSearch, inviteTyped, inviteCopy, inviteOpen, query, found, picked, typedName, typedEmail, viaNotify, viaMessage, viaEmail, canMessage,
|
|
677
764
|
inviteBusy, inviteNote, inviteBad, sent, copiedLink, inviteSub, searchHint, foundTitle, emptyText, viaHint, canSendInvites, sendLabel,
|
|
678
765
|
|
|
@@ -749,6 +836,13 @@ function mount(el) {
|
|
|
749
836
|
},
|
|
750
837
|
|
|
751
838
|
toggleMic() { micOn.value = !micOn.value; setTracks(); },
|
|
839
|
+
toggleBlur() {
|
|
840
|
+
if (blurBusy.value) return;
|
|
841
|
+
blurWanted = !blurOn.value;
|
|
842
|
+
try { if (blurWanted) localStorage.setItem(BLUR_KEY, '1'); else localStorage.removeItem(BLUR_KEY); } catch { /* storage blocked */ }
|
|
843
|
+
blurNote.value = '';
|
|
844
|
+
if (blurWanted) startBlurring(); else stopBlurring();
|
|
845
|
+
},
|
|
752
846
|
toggleCam() {
|
|
753
847
|
if (!local?.getVideoTracks().length) { error.value = 'No camera is available.'; return; }
|
|
754
848
|
camOn.value = !camOn.value; setTracks();
|
|
@@ -826,15 +920,14 @@ function mount(el) {
|
|
|
826
920
|
},
|
|
827
921
|
|
|
828
922
|
async toggleShare() {
|
|
829
|
-
//
|
|
830
|
-
// (remembered, so sharing again reuses it rather than adding another).
|
|
831
|
-
const videoSender = (entry) => entry.pc.getSenders().find(s => s.track?.kind === 'video') || entry.videoSender || null;
|
|
923
|
+
// A screen goes out on the camera's sender, or one added for it when there was no camera
|
|
924
|
+
// (remembered as entry.videoSender, so sharing again reuses it rather than adding another).
|
|
832
925
|
if (sharing.value) {
|
|
833
926
|
screen?.getTracks().forEach(t => t.stop());
|
|
834
927
|
screen = null;
|
|
835
928
|
sharing.value = false;
|
|
836
|
-
|
|
837
|
-
|
|
929
|
+
if (blur) blur.paused = false;
|
|
930
|
+
sendCam();
|
|
838
931
|
showSelf();
|
|
839
932
|
return;
|
|
840
933
|
}
|
|
@@ -843,6 +936,7 @@ function mount(el) {
|
|
|
843
936
|
const track = screen.getVideoTracks()[0];
|
|
844
937
|
track.onended = () => { if (sharing.value) model.toggleShare(); };
|
|
845
938
|
sharing.value = true;
|
|
939
|
+
if (blur) blur.paused = true; // nobody sees it while the screen is shown
|
|
846
940
|
pcs.forEach(entry => {
|
|
847
941
|
const sender = videoSender(entry);
|
|
848
942
|
if (sender) sender.replaceTrack(track);
|
|
@@ -909,7 +1003,7 @@ function mount(el) {
|
|
|
909
1003
|
el.querySelector('.dmc-messages')?.addEventListener('scroll', () => { if (newBelow.value && nearBottom()) newBelow.value = false; }, {passive: true});
|
|
910
1004
|
window.addEventListener('pagehide', () => { if (me) post('leave', {}, {keepalive: true}); });
|
|
911
1005
|
getDevices().then(setTracks);
|
|
912
|
-
return () => { stopLayout?.(); stopAttach?.(); stopScan?.(); stopTiles?.(); clearInterval(clockTimer); handleBindings?.dispose?.(); };
|
|
1006
|
+
return () => { stopLayout?.(); stopAttach?.(); stopScan?.(); stopTiles?.(); clearInterval(clockTimer); blurWanted = false; stopBlurring(); handleBindings?.dispose?.(); };
|
|
913
1007
|
}
|
|
914
1008
|
|
|
915
1009
|
document.querySelectorAll('[data-dm-call]').forEach(mount);
|