feat(call): split resolution/fps in share picker + restore window state after fullscreen
ScreenSharePickerModal now exposes Auflösung (Auto · 720p · 1080p · 1440p · 4K) and FPS (30 · 60) as separate pill rows instead of bundled quality presets — users can pick "1440p · 30 fps" or "4K · 30 fps" which the old preset list didn't surface. The underlying screenShareSettings framerateOverride slot already existed; the modal just stopped resetting it to null on every start and now plumbs the chosen FPS through to startScreenShare. Cinema-mode fullscreen on Windows had two defects: 1. Maximized → fullscreen left the taskbar drawn on top of the window because DWM kept the maximized work-area constraints. We now unmaximize first so DWM recomposes cleanly and setFullScreen actually covers the whole monitor including the taskbar strip. 2. Esc out of cinema came back as a small floating window even when the user had been maximized before clicking the Vollbild button — the unmaximize from (1) was never undone. We now memo the pre-fullscreen maximized flag per window-id and call win.maximize() once the leave-full-screen event has fired. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,13 +26,65 @@ const TABS = [
|
||||
];
|
||||
type TabId = (typeof TABS)[number]['id'];
|
||||
|
||||
const QUALITY_PILLS: { id: ScreenSharePreset; label: string }[] = [
|
||||
// Resolution and framerate are picked independently. The preset table
|
||||
// (screenShareSettings.ts) still provides the per-tier bitrate/dimension
|
||||
// caps, so we map (res, fps) → existing preset and rely on
|
||||
// `framerateOverride` for the non-default framerate combinations
|
||||
// (e.g. 1440p · 30, 4K · 30).
|
||||
type ResChoice = 'auto' | '720p' | '1080p' | '1440p' | '4k';
|
||||
type FpsChoice = 30 | 60;
|
||||
|
||||
const RES_PILLS: { id: ResChoice; label: string }[] = [
|
||||
{ id: 'auto', label: 'Auto' },
|
||||
{ id: '720p60', label: '720p · 60' },
|
||||
{ id: '1080p60', label: '1080p · 60' },
|
||||
{ id: '1440p60', label: '1440p · 60' },
|
||||
{ id: '720p', label: '720p' },
|
||||
{ id: '1080p', label: '1080p' },
|
||||
{ id: '1440p', label: '1440p' },
|
||||
{ id: '4k', label: '4K' },
|
||||
];
|
||||
|
||||
const FPS_PILLS: { id: FpsChoice; label: string }[] = [
|
||||
{ id: 30, label: '30 fps' },
|
||||
{ id: 60, label: '60 fps' },
|
||||
];
|
||||
|
||||
function presetForResFps(res: ResChoice, fps: FpsChoice): ScreenSharePreset {
|
||||
switch (res) {
|
||||
case 'auto':
|
||||
return 'auto';
|
||||
case '720p':
|
||||
return fps === 60 ? '720p60' : '720p30';
|
||||
case '1080p':
|
||||
return fps === 60 ? '1080p60' : '1080p30';
|
||||
case '1440p':
|
||||
return '1440p60';
|
||||
case '4k':
|
||||
return '4k60';
|
||||
}
|
||||
}
|
||||
|
||||
function decomposePreset(
|
||||
p: ScreenSharePreset,
|
||||
framerateOverride: number | null,
|
||||
): { res: ResChoice; fps: FpsChoice } {
|
||||
const fallback: FpsChoice = framerateOverride === 30 ? 30 : 60;
|
||||
switch (p) {
|
||||
case 'auto':
|
||||
return { res: 'auto', fps: framerateOverride === 60 ? 60 : 30 };
|
||||
case '720p30':
|
||||
return { res: '720p', fps: 30 };
|
||||
case '720p60':
|
||||
return { res: '720p', fps: 60 };
|
||||
case '1080p30':
|
||||
return { res: '1080p', fps: 30 };
|
||||
case '1080p60':
|
||||
return { res: '1080p', fps: 60 };
|
||||
case '1440p60':
|
||||
return { res: '1440p', fps: fallback };
|
||||
case '4k60':
|
||||
return { res: '4k', fps: fallback };
|
||||
}
|
||||
}
|
||||
|
||||
const THUMBNAIL_REFRESH_MS = 3500;
|
||||
|
||||
export function ScreenSharePickerModal({ onClose }: Props) {
|
||||
@@ -43,9 +95,14 @@ export function ScreenSharePickerModal({ onClose }: Props) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
const [preset, setPreset] = useState<ScreenSharePreset>(
|
||||
() => getScreenShareSettings().preset,
|
||||
);
|
||||
const [res, setRes] = useState<ResChoice>(() => {
|
||||
const s = getScreenShareSettings();
|
||||
return decomposePreset(s.preset, s.framerateOverride).res;
|
||||
});
|
||||
const [fps, setFps] = useState<FpsChoice>(() => {
|
||||
const s = getScreenShareSettings();
|
||||
return decomposePreset(s.preset, s.framerateOverride).fps;
|
||||
});
|
||||
const [audio, setAudio] = useState<boolean>(
|
||||
() => getScreenShareSettings().includeSystemAudio,
|
||||
);
|
||||
@@ -109,13 +166,14 @@ export function ScreenSharePickerModal({ onClose }: Props) {
|
||||
setError(null);
|
||||
setBusy(true);
|
||||
try {
|
||||
// Persist quality + audio toggle. Also force-clear any stale duck
|
||||
// setting users may have inherited from earlier builds — the
|
||||
// native loopback addon excludes the app's own audio at OS level
|
||||
// now, so JS-side ducking (which muted the user's incoming peer
|
||||
// audio) is no longer needed and was causing "I can't hear anyone".
|
||||
const preset = presetForResFps(res, fps);
|
||||
// Persist resolution + fps + audio. The duck flag is force-cleared
|
||||
// because the native loopback addon now excludes the app's own audio
|
||||
// at OS level; JS-side ducking (which also muted incoming peer audio)
|
||||
// was causing "I can't hear anyone" on earlier builds.
|
||||
updateScreenShareSettings({
|
||||
preset,
|
||||
framerateOverride: fps,
|
||||
includeSystemAudio: audio,
|
||||
duckRemoteAudioWhileSharing: false,
|
||||
});
|
||||
@@ -125,7 +183,7 @@ export function ScreenSharePickerModal({ onClose }: Props) {
|
||||
await startScreenShare({
|
||||
preset,
|
||||
displaySurface: tab === 'screen' ? 'monitor' : 'window',
|
||||
framerate: null,
|
||||
framerate: fps,
|
||||
// Forward the picked source id so the native loopback path can
|
||||
// switch into INCLUDE_TARGET_PROCESS_TREE for window-shares
|
||||
// (parses HWND from `window:<HWND>:0`). For screen-shares this
|
||||
@@ -271,16 +329,42 @@ export function ScreenSharePickerModal({ onClose }: Props) {
|
||||
<div className="flex flex-wrap items-center gap-x-5 gap-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wide text-fg-muted">
|
||||
Qualität
|
||||
Auflösung
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
{QUALITY_PILLS.map((q) => {
|
||||
const active = preset === q.id;
|
||||
{RES_PILLS.map((q) => {
|
||||
const active = res === q.id;
|
||||
return (
|
||||
<button
|
||||
key={q.id}
|
||||
type="button"
|
||||
onClick={() => setPreset(q.id)}
|
||||
onClick={() => setRes(q.id)}
|
||||
className={
|
||||
'cursor-pointer rounded-md border px-2.5 py-1 text-[11px] font-medium transition focus:outline-none ' +
|
||||
(active
|
||||
? 'border-accent bg-accent/10 text-fg'
|
||||
: 'border-line bg-surface-3 text-fg-muted hover:text-fg')
|
||||
}
|
||||
>
|
||||
{q.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wide text-fg-muted">
|
||||
FPS
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
{FPS_PILLS.map((q) => {
|
||||
const active = fps === q.id;
|
||||
return (
|
||||
<button
|
||||
key={q.id}
|
||||
type="button"
|
||||
onClick={() => setFps(q.id)}
|
||||
className={
|
||||
'cursor-pointer rounded-md border px-2.5 py-1 text-[11px] font-medium transition focus:outline-none ' +
|
||||
(active
|
||||
|
||||
Reference in New Issue
Block a user