feat: integrate hwOverdrive into screen share start/stop/detect
- buildScreenShareOptions reads hwOverdrive from voiceStore instead of config.codec; both VP9 and H.264 paths now always get a VP8 SIMULCAST backup - startScreenShare activates SDP override before WebRTC negotiation and deactivates on failure - stopScreenShare and handleScreenShareUnpublished both deactivate the override and reset hwOverdrive: false - scheduleEncoderDetection checks encoderImplementation in WebRTC stats after 4s and shows a warning toast if OpenH264 (software) is detected
This commit is contained in:
@@ -4,6 +4,8 @@ import type { ScreenShareConfig } from '../stores/voiceStore';
|
|||||||
import { getStreamingLimits } from '../stores/settingsStore';
|
import { getStreamingLimits } from '../stores/settingsStore';
|
||||||
import { getPublisherPC, getMediaStreamTrack } from './livekitInternals';
|
import { getPublisherPC, getMediaStreamTrack } from './livekitInternals';
|
||||||
import { broadcastVoiceStatus } from './voice';
|
import { broadcastVoiceStatus } from './voice';
|
||||||
|
import { activate as activateHwOverdrive, deactivate as deactivateHwOverdrive } from './hwOverdrive';
|
||||||
|
import { useUIStore } from '../stores/uiStore';
|
||||||
import {
|
import {
|
||||||
STANDARD_RESOLUTIONS, STANDARD_FRAMERATES, WIDTH_MAP,
|
STANDARD_RESOLUTIONS, STANDARD_FRAMERATES, WIDTH_MAP,
|
||||||
BITRATE_MATRIX_KBPS,
|
BITRATE_MATRIX_KBPS,
|
||||||
@@ -131,11 +133,10 @@ export function buildScreenShareOptions(config: ScreenShareConfig): ScreenShareB
|
|||||||
const bps = clampedKbps * 1000;
|
const bps = clampedKbps * 1000;
|
||||||
const minBps = Math.round(bps * 0.25);
|
const minBps = Math.round(bps * 0.25);
|
||||||
|
|
||||||
// VP9: better quality per bit via libvpx (software, CPU).
|
// hwOverdrive forces H.264 with SDP profile override for hardware encoding.
|
||||||
// H.264: OpenH264 software encoder in Electron/Chrome (NOT hardware NVENC).
|
// Default is always VP9. Both paths get VP8 SIMULCAST backup (dynacast pauses
|
||||||
// When VP9 is primary, VP8 SIMULCAST backup (also libvpx, lightweight) handles
|
// the backup when no subscriber needs it — near-zero cost).
|
||||||
// incompatible viewers (Safari). Backup capped at 30fps to minimize dual-encode overhead.
|
const hwOverdrive = useVoiceStore.getState().hwOverdrive;
|
||||||
const useVp9 = config.codec !== 'h264';
|
|
||||||
|
|
||||||
// Backup encoding: cap at 30fps and proportional bitrate to keep CPU overhead low
|
// Backup encoding: cap at 30fps and proportional bitrate to keep CPU overhead low
|
||||||
const backupFps = Math.min(fps, 30);
|
const backupFps = Math.min(fps, 30);
|
||||||
@@ -144,16 +145,14 @@ export function buildScreenShareOptions(config: ScreenShareConfig): ScreenShareB
|
|||||||
return {
|
return {
|
||||||
capture: { width: captureWidth, height: captureHeight, frameRate: fps },
|
capture: { width: captureWidth, height: captureHeight, frameRate: fps },
|
||||||
publish: {
|
publish: {
|
||||||
videoCodec: useVp9 ? 'vp9' : 'h264',
|
videoCodec: hwOverdrive ? 'h264' : 'vp9',
|
||||||
videoEncoding: { maxBitrate: bps, maxFramerate: fps },
|
videoEncoding: { maxBitrate: bps, maxFramerate: fps },
|
||||||
simulcast: false,
|
simulcast: false,
|
||||||
...(useVp9 ? {
|
backupCodec: {
|
||||||
backupCodec: {
|
codec: 'vp8' as const,
|
||||||
codec: 'vp8' as const,
|
encoding: { maxBitrate: backupBps, maxFramerate: backupFps },
|
||||||
encoding: { maxBitrate: backupBps, maxFramerate: backupFps },
|
},
|
||||||
},
|
backupCodecPolicy: BackupCodecPolicy.SIMULCAST,
|
||||||
backupCodecPolicy: BackupCodecPolicy.SIMULCAST,
|
|
||||||
} : {}),
|
|
||||||
},
|
},
|
||||||
overdrive: {
|
overdrive: {
|
||||||
maxBitrate: bps,
|
maxBitrate: bps,
|
||||||
@@ -242,8 +241,14 @@ export async function applyOverdrive(
|
|||||||
export async function startScreenShare(room: Room): Promise<boolean> {
|
export async function startScreenShare(room: Room): Promise<boolean> {
|
||||||
console.log('[SS] startScreenShare called, room state:', room.state);
|
console.log('[SS] startScreenShare called, room state:', room.state);
|
||||||
const config = useVoiceStore.getState().screenShareConfig;
|
const config = useVoiceStore.getState().screenShareConfig;
|
||||||
|
const hwOverdrive = useVoiceStore.getState().hwOverdrive;
|
||||||
const opts = buildScreenShareOptions(config);
|
const opts = buildScreenShareOptions(config);
|
||||||
|
|
||||||
|
// Activate SDP profile override before WebRTC negotiation
|
||||||
|
if (hwOverdrive) {
|
||||||
|
activateHwOverdrive();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// For native mode: omit resolution constraint to capture at display's full native resolution
|
// For native mode: omit resolution constraint to capture at display's full native resolution
|
||||||
const captureOptions: any = {
|
const captureOptions: any = {
|
||||||
@@ -277,6 +282,7 @@ export async function startScreenShare(room: Room): Promise<boolean> {
|
|||||||
|
|
||||||
console.log('[SS] setScreenShareEnabled returned:', !!track);
|
console.log('[SS] setScreenShareEnabled returned:', !!track);
|
||||||
if (!track) {
|
if (!track) {
|
||||||
|
if (hwOverdrive) deactivateHwOverdrive();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,9 +295,16 @@ export async function startScreenShare(room: Room): Promise<boolean> {
|
|||||||
|
|
||||||
useVoiceStore.setState({ isScreenSharing: true });
|
useVoiceStore.setState({ isScreenSharing: true });
|
||||||
applyScreenShareOverdrive(room);
|
applyScreenShareOverdrive(room);
|
||||||
|
|
||||||
|
// Schedule hardware encoder detection
|
||||||
|
if (hwOverdrive) {
|
||||||
|
scheduleEncoderDetection(room);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ScreenShare] Failed to start screen share:', err);
|
console.error('[ScreenShare] Failed to start screen share:', err);
|
||||||
|
if (hwOverdrive) deactivateHwOverdrive();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,6 +356,50 @@ function applyScreenShareOverdrive(room: Room): void {
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Hardware encoder detection — checks WebRTC stats after stream starts
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function scheduleEncoderDetection(room: Room): void {
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (!useVoiceStore.getState().isScreenSharing) return;
|
||||||
|
if (!useVoiceStore.getState().hwOverdrive) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pc = getPublisherPC(room);
|
||||||
|
if (!pc) return;
|
||||||
|
|
||||||
|
const screenPub = room.localParticipant.getTrackPublications()
|
||||||
|
.find(p => p.source === Track.Source.ScreenShare);
|
||||||
|
if (!screenPub?.track) return;
|
||||||
|
|
||||||
|
const mediaTrack = getMediaStreamTrack(screenPub.track);
|
||||||
|
if (!mediaTrack) return;
|
||||||
|
|
||||||
|
const sender = pc.getSenders().find(s => s.track?.id === mediaTrack.id);
|
||||||
|
if (!sender) return;
|
||||||
|
|
||||||
|
const stats = await sender.getStats();
|
||||||
|
let encoderImpl: string | null = null;
|
||||||
|
stats.forEach((report: any) => {
|
||||||
|
if (report.type === 'outbound-rtp' && (report.kind === 'video' || report.mediaType === 'video')) {
|
||||||
|
encoderImpl = report.encoderImplementation ?? null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (encoderImpl && /openh264/i.test(encoderImpl)) {
|
||||||
|
useUIStore.getState().addToast(
|
||||||
|
'Hardware encoder not available — using software fallback. Switch to VP9 for better performance.',
|
||||||
|
'warning',
|
||||||
|
8000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Non-critical — silently ignore detection failures
|
||||||
|
}
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Stop screen sharing
|
// Stop screen sharing
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -353,7 +410,8 @@ export async function stopScreenShare(room: Room): Promise<void> {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ScreenShare] Failed to stop screen share:', err);
|
console.error('[ScreenShare] Failed to stop screen share:', err);
|
||||||
}
|
}
|
||||||
useVoiceStore.setState({ isScreenSharing: false });
|
deactivateHwOverdrive();
|
||||||
|
useVoiceStore.setState({ isScreenSharing: false, hwOverdrive: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -372,6 +430,7 @@ export async function changeScreenShare(room: Room): Promise<void> {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export function handleScreenShareUnpublished(): void {
|
export function handleScreenShareUnpublished(): void {
|
||||||
useVoiceStore.setState({ isScreenSharing: false });
|
deactivateHwOverdrive();
|
||||||
|
useVoiceStore.setState({ isScreenSharing: false, hwOverdrive: false });
|
||||||
broadcastVoiceStatus();
|
broadcastVoiceStatus();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user