perf(call): H.264 + contentHint + 30fps default for screen-share
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1310,9 +1310,17 @@ export function CallProvider({ children }: { children: ReactNode }) {
|
|||||||
stream.getTracks().forEach((t) => t.stop());
|
stream.getTracks().forEach((t) => t.stop());
|
||||||
throw new Error('no video track from chromeMediaSource');
|
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, {
|
const videoPub = await lp.publishTrack(videoMst, {
|
||||||
source: LkTrack.Source.ScreenShare,
|
source: LkTrack.Source.ScreenShare,
|
||||||
videoCodec: 'vp9',
|
videoCodec: 'h264',
|
||||||
});
|
});
|
||||||
const audioRes = await startWasapiAudio(LkTrack);
|
const audioRes = await startWasapiAudio(LkTrack);
|
||||||
videoMst.addEventListener('ended', () => {
|
videoMst.addEventListener('ended', () => {
|
||||||
@@ -1358,9 +1366,10 @@ export function CallProvider({ children }: { children: ReactNode }) {
|
|||||||
nativeCaptureRef.current = null;
|
nativeCaptureRef.current = null;
|
||||||
throw new NativeCaptureUnavailable('canvas stream produced no video track');
|
throw new NativeCaptureUnavailable('canvas stream produced no video track');
|
||||||
}
|
}
|
||||||
|
videoMst.contentHint = 'detail';
|
||||||
const videoPub = await lp.publishTrack(videoMst, {
|
const videoPub = await lp.publishTrack(videoMst, {
|
||||||
source: LkTrack.Source.ScreenShare,
|
source: LkTrack.Source.ScreenShare,
|
||||||
videoCodec: 'vp9',
|
videoCodec: 'h264',
|
||||||
});
|
});
|
||||||
const audioRes = await startWasapiAudio(LkTrack);
|
const audioRes = await startWasapiAudio(LkTrack);
|
||||||
videoMst.addEventListener('ended', () => {
|
videoMst.addEventListener('ended', () => {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export interface PresetParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PRESET_PARAMS: Record<ScreenSharePreset, PresetParams> = {
|
const PRESET_PARAMS: Record<ScreenSharePreset, PresetParams> = {
|
||||||
auto: { dims: null, framerate: 60, bitrateKbps: 8000, label: 'Auto (Original)' },
|
auto: { dims: null, framerate: 30, bitrateKbps: 8000, label: 'Auto (Original)' },
|
||||||
'720p30': {
|
'720p30': {
|
||||||
dims: { width: 1280, height: 720 },
|
dims: { width: 1280, height: 720 },
|
||||||
framerate: 30,
|
framerate: 30,
|
||||||
|
|||||||
Reference in New Issue
Block a user