fix(call): volume>100% crash, picker UI freeze, native-path diagnostics

Volume crash:
- setParticipantVolume / setScreenShareVolume propagated values up to 2.0
(200%) to the per-track GainNode, but also called applyToAttachedElements
which set the raw HTMLAudioElement.volume — that property is hard-clamped
to [0, 1] and throws IndexSizeError above 1. Clip the element-path apply
at 1.0. WebAudio GainNode keeps doing the actual amplification.

Picker freeze:
- Firing ~20 captureScreenSourceThumbnail invokes in parallel caused
perceptible input freezes while each ~100KB base64 result arrived and
triggered a setState. Bounded the worker pool to 4 concurrent captures
with a queue — overall wall-clock is nearly identical and the grid stays
scrollable / clickable throughout the load.

Native-path diagnostics:
- Previous logs only fired on non-NativeCaptureUnavailable errors, so
users couldn't tell whether the native path was skipped (audio toggle
on, no sourceId) or attempted-and-failed. Added explicit info logs for
each skip reason plus an always-on warn with the underlying error when
the try block throws. Makes the next debug pass on screenshare much
quicker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
byGalax
2026-04-22 21:50:20 +02:00
parent c3e0c47d32
commit a5e930ac17
4 changed files with 53 additions and 24 deletions
@@ -49,10 +49,11 @@ export function ScreenSourcePicker({ open, onClose, onStart }: Props) {
const [error, setError] = useState<string | null>(null);
// Two-phase load: (1) fast list returns names + placeholders so the grid
// paints instantly, (2) fire one thumbnail capture per source in
// parallel. Thumbnails fill in as each capture completes — Tauri's
// command thread pool runs them concurrently so wall-clock is bounded
// by the slowest source, not the sum.
// paints instantly, (2) capture thumbnails in a bounded worker-pool so
// Tauri IPC returns don't starve the JS main thread. Firing all ~20
// captures at once caused perceptible input freezes while the base64
// blobs arrived — limiting concurrency to 4 keeps the grid scrollable
// throughout and doesn't meaningfully slow overall completion.
useEffect(() => {
if (!open) {
setSources(null);
@@ -61,27 +62,34 @@ export function ScreenSourcePicker({ open, onClose, onStart }: Props) {
return;
}
let cancelled = false;
const CONCURRENCY = 4;
void (async () => {
const list = await listScreenSources();
if (cancelled) return;
setSources(list);
// Fan out thumbnail captures. No Promise.all — we want each result
// to render as it lands, not wait for the full batch. The id-based
// setState patch means the slowest source can still be in flight
// while the user already picked one of the fast ones.
for (const src of list) {
const queue = [...list];
const pickOne = (src: typeof list[number]) => {
void (async () => {
const png = await captureScreenSourceThumbnail(src.id);
if (cancelled || png === null) return;
setSources((prev) => {
if (!prev) return prev;
const idx = prev.findIndex((s) => s.id === src.id);
if (idx === -1) return prev;
const next = prev.slice();
next[idx] = { ...prev[idx]!, thumbnailPng: png };
return next;
});
if (cancelled) return;
if (png !== null) {
setSources((prev) => {
if (!prev) return prev;
const idx = prev.findIndex((s) => s.id === src.id);
if (idx === -1) return prev;
const next = prev.slice();
next[idx] = { ...prev[idx]!, thumbnailPng: png };
return next;
});
}
const nextSrc = queue.shift();
if (nextSrc) pickOne(nextSrc);
})();
};
for (let i = 0; i < Math.min(CONCURRENCY, queue.length); i++) {
const s = queue.shift();
if (s) pickOne(s);
}
})();
return () => {