import { useState, useEffect, useRef } from 'react'; import { useSettingsStore } from '../../../stores/settingsStore'; import { useUIStore } from '../../../stores/uiStore'; import type { InstanceStreamingLimits } from '@backspace/shared'; import { STANDARD_RESOLUTIONS, STANDARD_FRAMERATES, ALL_RESOLUTIONS, RESOLUTION_LABELS, HIGH_END_RESOLUTION_THRESHOLD, HIGH_END_FRAMERATE_THRESHOLD, BITRATE_MATRIX_KBPS, type Resolution, } from '@backspace/shared/src/constants'; function formatKbps(kbps: number): string { return kbps >= 1000 ? `${(kbps / 1000).toFixed(kbps % 1000 === 0 ? 0 : 1)} Mbps` : `${kbps} kbps`; } export function StreamingPanel() { const limits = useSettingsStore((s) => s.streamingLimits); const updateStreamingLimits = useSettingsStore((s) => s.updateStreamingLimits); const addToast = useUIStore((s) => s.addToast); const [draft, setDraft] = useState(null); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(''); // Matrix editor state: full grid of kbps values (integers only) const [matrixDraft, setMatrixDraft] = useState>({}); const [editingCell, setEditingCell] = useState(null); const [scaleValue, setScaleValue] = useState(1.0); const scaleSnapshot = useRef | null>(null); useEffect(() => { if (limits) setDraft({ ...limits }); }, [limits]); // Initialize matrix draft from limits + overrides useEffect(() => { if (!limits) return; const matrix: Record = {}; for (const res of STANDARD_RESOLUTIONS) { for (const fps of STANDARD_FRAMERATES) { const key = `${res}_${fps}`; matrix[key] = limits.bitrateMatrixOverrides?.[key] ?? BITRATE_MATRIX_KBPS[res]![fps as number]!; } } setMatrixDraft(matrix); }, [limits]); if (!draft) return
Loading settings...
; const getDefaultKbps = (key: string): number => { const parts = key.split('_').map(Number); const h = parts[0]!; const f = parts[1]!; return BITRATE_MATRIX_KBPS[h]?.[f] ?? 8000; }; const isOverridden = (key: string): boolean => matrixDraft[key] !== getDefaultKbps(key); const enabledResolutions = draft.allowedResolutions.filter((r): r is number => r !== 'native'); const enabledFramerates = draft.allowedFramerates; const handleScaleStart = () => { scaleSnapshot.current = { ...matrixDraft }; }; const handleScaleChange = (newScale: number) => { setScaleValue(newScale); if (!scaleSnapshot.current) return; const scaled: Record = {}; for (const [key, val] of Object.entries(scaleSnapshot.current)) { scaled[key] = Math.round(val * newScale); } setMatrixDraft(scaled); }; const handleScaleEnd = () => { scaleSnapshot.current = null; setScaleValue(1.0); }; const matrixHasChanges = (() => { if (!limits) return false; for (const [key, val] of Object.entries(matrixDraft)) { const serverVal = limits.bitrateMatrixOverrides?.[key] ?? getDefaultKbps(key); if (val !== serverVal) return true; } return false; })(); const hasChanges = JSON.stringify(draft) !== JSON.stringify(limits) || matrixHasChanges; const handleSave = async () => { setSaving(true); setSaveError(''); try { // Compute sparse overrides: only cells that differ from defaults const overrides: Record = {}; for (const [key, val] of Object.entries(matrixDraft)) { if (val !== getDefaultKbps(key)) { overrides[key] = val; } } const payload = { ...draft, bitrateMatrixOverrides: Object.keys(overrides).length > 0 ? overrides : null, }; await updateStreamingLimits(payload); addToast('Settings saved', 'success', 2000); } catch (err) { setSaveError(err instanceof Error ? err.message : 'Failed to save'); } finally { setSaving(false); } }; const handleReset = () => { if (limits) { setDraft({ ...limits }); const matrix: Record = {}; for (const res of STANDARD_RESOLUTIONS) { for (const fps of STANDARD_FRAMERATES) { const key = `${res}_${fps}`; matrix[key] = limits.bitrateMatrixOverrides?.[key] ?? BITRATE_MATRIX_KBPS[res]![fps as number]!; } } setMatrixDraft(matrix); } setSaveError(''); }; const toggleResolution = (res: number | 'native') => { const current = new Set(draft.allowedResolutions); if (current.has(res)) { if (current.size <= 1) return; current.delete(res); } else { current.add(res); } // Sort: numbers ascending, 'native' always last const nums: (number | 'native')[] = Array.from(current).filter((r): r is number => r !== 'native').sort((a, b) => a - b); if (current.has('native')) nums.push('native'); setDraft({ ...draft, allowedResolutions: nums }); }; const toggleFramerate = (fps: number) => { const current = new Set(draft.allowedFramerates); if (current.has(fps)) { if (current.size <= 1) return; current.delete(fps); } else { current.add(fps); } setDraft({ ...draft, allowedFramerates: Array.from(current).sort((a, b) => a - b) }); }; const pillBase = 'px-3 py-1.5 rounded text-[13px] font-medium transition-colors cursor-pointer select-none'; const pillOn = 'bg-accent-primary text-white'; const pillOff = 'bg-surface-elevated text-txt-secondary hover:bg-interactive-hover'; return (

Streaming

These limits apply to all users on this instance. Users can pick values within these bounds.
{/* Bandwidth */}
Bandwidth

Minimum and maximum bitrate bounds, and the step size for the quality slider.

{/* Bitrate Range */}
Bitrate Range
setDraft({ ...draft, minBitrateKbps: Number(e.target.value) })} className="w-full h-1.5 accent-accent-primary cursor-pointer appearance-none bg-interactive-muted rounded-full [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-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" />
{ const v = Number(e.target.value); if (v >= 100 && v <= 1000000) setDraft({ ...draft, minBitrateKbps: v }); }} className="input-standard w-20 px-2 py-0.5 text-xs" /> {formatKbps(draft.minBitrateKbps)}
setDraft({ ...draft, maxBitrateKbps: Number(e.target.value) })} className="w-full h-1.5 accent-accent-primary cursor-pointer appearance-none bg-interactive-muted rounded-full [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-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" />
{ const v = Number(e.target.value); if (v >= 500 && v <= 1000000) setDraft({ ...draft, maxBitrateKbps: v }); }} className="input-standard w-20 px-2 py-0.5 text-xs" /> {formatKbps(draft.maxBitrateKbps)}
{/* Bitrate Step — presets + custom input */}
Slider Step { const v = Number(e.target.value); if (v >= 50 && v <= 10000) setDraft({ ...draft, bitrateStepKbps: v }); }} className="input-standard w-14 px-1.5 py-0 text-[11px] text-center" /> kbps
{[100, 250, 500, 1000, 2500, 5000].map((step) => ( ))}
{/* Quality */}
Quality

Available resolution and frame rate options for screen sharing.

{/* Allowed Resolutions */}
Allowed Resolutions
{ALL_RESOLUTIONS.map((res) => ( ))}
{/* Allowed Frame Rates */}
Allowed Frame Rates
{STANDARD_FRAMERATES.map((fps) => ( ))}
{/* High-end resource warning */} {(draft.allowedResolutions.some( (r) => r === 'native' || (typeof r === 'number' && r >= HIGH_END_RESOLUTION_THRESHOLD) ) || draft.allowedFramerates.some( (f) => f >= HIGH_END_FRAMERATE_THRESHOLD )) && (
High-performance settings enabled.{' '} Streaming above 1080p or 60 fps requires significant client-side CPU/GPU encoding power and can saturate server bandwidth, especially when streams are routed through TURN. High-end configurations (e.g., 4K at 120 fps) can require up to 45 Mbps per active stream. Ensure your infrastructure can handle this load before enabling these options for all users.
)} {/* Bitrate Matrix */}
Bitrate Matrix

Default bitrates per resolution and frame rate. Edit individual cells or scale all values at once.

{/* Scale slider */}
Scale All
×{scaleValue.toFixed(2)}
handleScaleChange(Number(e.target.value))} onPointerUp={handleScaleEnd} onPointerCancel={handleScaleEnd} className="w-full h-1.5 accent-accent-primary cursor-pointer appearance-none bg-interactive-muted rounded-full [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-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" />
Drag to scale all bitrates. Fine-tune individual cells below.
{/* Matrix grid */}
{enabledFramerates.map((fps) => ( ))} {enabledResolutions.map((res) => ( {enabledFramerates.map((fps) => { const key = `${res}_${fps}`; const val = matrixDraft[key] ?? getDefaultKbps(key); const overridden = isOverridden(key); const exceedsCap = val > draft.maxBitrateKbps; return ( ); })} ))}
{fps} fps
{RESOLUTION_LABELS[res as Resolution] ?? `${res}p`} {editingCell === key ? ( { const mbps = parseFloat(e.target.value); if (!isNaN(mbps) && mbps > 0) { setMatrixDraft({ ...matrixDraft, [key]: Math.round(mbps * 1000) }); } setEditingCell(null); }} onKeyDown={(e) => { if (e.key === 'Enter') (e.target as HTMLInputElement).blur(); if (e.key === 'Escape') setEditingCell(null); }} /> ) : ( )}
{/* Reset all button */} {Object.entries(matrixDraft).some(([key, val]) => val !== getDefaultKbps(key)) && ( )}
{/* Coherence warning */} {Object.entries(matrixDraft).some(([, val]) => val > draft.maxBitrateKbps) && (
Matrix values exceed global cap.{' '} Some bitrate values exceed your maximum of {formatKbps(draft.maxBitrateKbps)}. Streams at those combos will be quality-degraded to your cap.
)} {/* Save / Reset */} {saveError && (
{saveError}
)} {hasChanges && (
)}
); }