From f9bbcdee47c481037b47fc17cc4e9986ab337fdf Mon Sep 17 00:00:00 2001 From: byGalax Date: Wed, 22 Apr 2026 23:37:22 +0200 Subject: [PATCH] perf(call): H.264 + contentHint + 30fps default for screen-share MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The real cost in the "frame by frame" stutter wasn't the custom capture path — it was LiveKit re-encoding via VP9 software with L3T3_KEY SVC (three spatial × three temporal layers, all CPU). Switching the per-publish codec to H.264 lets Chromium's hardware encoder take over on Windows and sidesteps the SVC mode entirely (H.264 has no SVC). Also pushes `contentHint = 'detail'` on the track — setScreenShareEnabled does this internally, the manual publishTrack paths had been missing it, which changes how the encoder allocates its frame budget for static UI content. Auto preset default framerate 60 → 30. 60fps desktop share burns three full-res encodes per frame at sizes up to 4K; 30 is what getDisplayMedia practically delivers anyway. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/desktop/src/context/CallContext.tsx | 13 +++++++++++-- apps/desktop/src/lib/screenShareSettings.ts | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/context/CallContext.tsx b/apps/desktop/src/context/CallContext.tsx index ff853f3..9185d12 100644 --- a/apps/desktop/src/context/CallContext.tsx +++ b/apps/desktop/src/context/CallContext.tsx @@ -1310,9 +1310,17 @@ export function CallProvider({ children }: { children: ReactNode }) { stream.getTracks().forEach((t) => t.stop()); throw new Error('no video track from chromeMediaSource'); } + // H.264 lets Chromium's hardware encoder take over on Windows + // (Intel QuickSync / NVENC / AMD VCE) — VP9 is software-only in + // Chromium's WebRTC path and collapses under the L3T3_KEY + // scalability mode we set globally for camera. `contentHint` is + // what setScreenShareEnabled sets internally; pushing it by hand + // here tells the encoder to weight sharpness over smoothness, + // which is right for UI/text-heavy screen content. + videoMst.contentHint = 'detail'; const videoPub = await lp.publishTrack(videoMst, { source: LkTrack.Source.ScreenShare, - videoCodec: 'vp9', + videoCodec: 'h264', }); const audioRes = await startWasapiAudio(LkTrack); videoMst.addEventListener('ended', () => { @@ -1358,9 +1366,10 @@ export function CallProvider({ children }: { children: ReactNode }) { nativeCaptureRef.current = null; throw new NativeCaptureUnavailable('canvas stream produced no video track'); } + videoMst.contentHint = 'detail'; const videoPub = await lp.publishTrack(videoMst, { source: LkTrack.Source.ScreenShare, - videoCodec: 'vp9', + videoCodec: 'h264', }); const audioRes = await startWasapiAudio(LkTrack); videoMst.addEventListener('ended', () => { diff --git a/apps/desktop/src/lib/screenShareSettings.ts b/apps/desktop/src/lib/screenShareSettings.ts index 6fb3cfe..b7a9515 100644 --- a/apps/desktop/src/lib/screenShareSettings.ts +++ b/apps/desktop/src/lib/screenShareSettings.ts @@ -46,7 +46,7 @@ export interface PresetParams { } const PRESET_PARAMS: Record = { - auto: { dims: null, framerate: 60, bitrateKbps: 8000, label: 'Auto (Original)' }, + auto: { dims: null, framerate: 30, bitrateKbps: 8000, label: 'Auto (Original)' }, '720p30': { dims: { width: 1280, height: 720 }, framerate: 30,