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:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user