feat: manual bitrate override slider for screen share settings
- Add customBitrateKbps to ScreenShareConfig (null = auto matrix lookup) - Slider in Stream Settings popover: 500 kbps–20 Mbps, step 500 kbps - "Reset to Auto" clears override back to preset-derived bitrate - Live updates via existing applyOverdrive() pipeline on active streams - Persist version 5 → 6 with migration
This commit is contained in:
@@ -3,6 +3,10 @@ import { useVoiceStore } from '../../stores/voiceStore';
|
|||||||
import type { ScreenShareConfig } from '../../stores/voiceStore';
|
import type { ScreenShareConfig } from '../../stores/voiceStore';
|
||||||
import { buildScreenShareOptions } from '../../utils/screenShare';
|
import { buildScreenShareOptions } from '../../utils/screenShare';
|
||||||
|
|
||||||
|
const BITRATE_MIN = 500; // kbps
|
||||||
|
const BITRATE_MAX = 20000; // kbps
|
||||||
|
const BITRATE_STEP = 500; // kbps
|
||||||
|
|
||||||
interface ScreenShareSettingsPopoverProps {
|
interface ScreenShareSettingsPopoverProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -38,6 +42,12 @@ function formatDegradation(pref: RTCDegradationPreference): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatKbps(kbps: number): string {
|
||||||
|
return kbps >= 1000
|
||||||
|
? `${(kbps / 1000).toFixed(kbps % 1000 === 0 ? 0 : 1)} Mbps`
|
||||||
|
: `${kbps} kbps`;
|
||||||
|
}
|
||||||
|
|
||||||
export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSettingsPopoverProps) {
|
export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSettingsPopoverProps) {
|
||||||
const popoverRef = useRef<HTMLDivElement>(null);
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
const config = useVoiceStore((s) => s.screenShareConfig);
|
const config = useVoiceStore((s) => s.screenShareConfig);
|
||||||
@@ -125,6 +135,46 @@ export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSetting
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Bitrate Override */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
|
<div className="text-[11px] text-discord-text-muted font-semibold uppercase tracking-wider">
|
||||||
|
Bitrate
|
||||||
|
</div>
|
||||||
|
{config.customBitrateKbps != null && (
|
||||||
|
<button
|
||||||
|
onClick={() => setConfig({ customBitrateKbps: null })}
|
||||||
|
className="text-[11px] text-discord-blurple hover:text-[#7983f5] font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Reset to Auto
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={BITRATE_MIN}
|
||||||
|
max={BITRATE_MAX}
|
||||||
|
step={BITRATE_STEP}
|
||||||
|
value={config.customBitrateKbps ?? Math.round(result.publish.videoEncoding.maxBitrate / 1000)}
|
||||||
|
onChange={(e) => setConfig({ customBitrateKbps: Number(e.target.value) })}
|
||||||
|
className="flex-1 h-1.5 accent-discord-blurple cursor-pointer appearance-none bg-[#4e5058] rounded-full
|
||||||
|
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5
|
||||||
|
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-md
|
||||||
|
[&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:border-0
|
||||||
|
[&::-moz-range-thumb]:w-3.5 [&::-moz-range-thumb]:h-3.5 [&::-moz-range-thumb]:rounded-full
|
||||||
|
[&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:cursor-pointer"
|
||||||
|
/>
|
||||||
|
<span className={`text-[12px] font-medium min-w-[64px] text-right ${
|
||||||
|
config.customBitrateKbps != null ? 'text-discord-text-primary' : 'text-discord-text-muted'
|
||||||
|
}`}>
|
||||||
|
{config.customBitrateKbps != null
|
||||||
|
? formatKbps(config.customBitrateKbps)
|
||||||
|
: `Auto`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer — computed stats */}
|
{/* Footer — computed stats */}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface ScreenShareConfig {
|
|||||||
height: 1080 | 720 | 540;
|
height: 1080 | 720 | 540;
|
||||||
fps: 60 | 45 | 30;
|
fps: 60 | 45 | 30;
|
||||||
mode: 'gaming' | 'text';
|
mode: 'gaming' | 'text';
|
||||||
|
customBitrateKbps: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VoiceState {
|
interface VoiceState {
|
||||||
@@ -109,7 +110,7 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
inputDeviceId: 'default',
|
inputDeviceId: 'default',
|
||||||
outputDeviceId: 'default',
|
outputDeviceId: 'default',
|
||||||
focusedParticipantId: null,
|
focusedParticipantId: null,
|
||||||
screenShareConfig: { height: 720, fps: 60, mode: 'gaming' },
|
screenShareConfig: { height: 720, fps: 60, mode: 'gaming', customBitrateKbps: null },
|
||||||
participantVolumes: new Map(),
|
participantVolumes: new Map(),
|
||||||
setParticipantVolume: (userId, volume) => {
|
setParticipantVolume: (userId, volume) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
@@ -323,7 +324,7 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'opencord-voice-settings',
|
name: 'opencord-voice-settings',
|
||||||
version: 5,
|
version: 6,
|
||||||
migrate: (persistedState: any, version: number) => {
|
migrate: (persistedState: any, version: number) => {
|
||||||
if (version === 0) {
|
if (version === 0) {
|
||||||
persistedState.streamAttenuationEnabled = false;
|
persistedState.streamAttenuationEnabled = false;
|
||||||
@@ -348,6 +349,11 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
persistedState.screenShareConfig = { height, fps, mode: 'gaming' };
|
persistedState.screenShareConfig = { height, fps, mode: 'gaming' };
|
||||||
delete persistedState.videoQuality;
|
delete persistedState.videoQuality;
|
||||||
}
|
}
|
||||||
|
if (version < 6) {
|
||||||
|
if (persistedState.screenShareConfig) {
|
||||||
|
persistedState.screenShareConfig.customBitrateKbps = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
return persistedState;
|
return persistedState;
|
||||||
},
|
},
|
||||||
storage: createJSONStorage(() => localStorage),
|
storage: createJSONStorage(() => localStorage),
|
||||||
|
|||||||
@@ -53,9 +53,11 @@ const BITRATE_MATRIX: Record<number, Record<number, number>> = {
|
|||||||
const WIDTH_MAP: Record<number, number> = { 1080: 1920, 720: 1280, 540: 960 };
|
const WIDTH_MAP: Record<number, number> = { 1080: 1920, 720: 1280, 540: 960 };
|
||||||
|
|
||||||
export function buildScreenShareOptions(config: ScreenShareConfig): ScreenShareBuildResult {
|
export function buildScreenShareOptions(config: ScreenShareConfig): ScreenShareBuildResult {
|
||||||
const { height, fps, mode } = config;
|
const { height, fps, mode, customBitrateKbps } = config;
|
||||||
const width = WIDTH_MAP[height]!;
|
const width = WIDTH_MAP[height]!;
|
||||||
const maxBitrate = BITRATE_MATRIX[height]![fps]!;
|
const maxBitrate = customBitrateKbps != null
|
||||||
|
? customBitrateKbps * 1000
|
||||||
|
: BITRATE_MATRIX[height]![fps]!;
|
||||||
const minBitrate = Math.round(maxBitrate * 0.25);
|
const minBitrate = Math.round(maxBitrate * 0.25);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user