- Add createTextSource function with automatic OBS text input detection - Implement createStreamGroup to create groups within team scenes instead of separate scenes - Add team name text overlays positioned at top-left of each stream - Refactor stream switching to use stream group names for cleaner organization - Update setActive API to write stream group names to files - Fix getActive API to return correct screen position data - Improve team UUID assignment when adding streams - Remove manage streams section from home page for cleaner UI - Add vertical spacing to streams list to match teams page - Support dynamic text input kinds (text_ft2_source_v2, text_gdiplus, etc.) This creates a much cleaner OBS structure with 10 team scenes containing grouped stream sources rather than 200+ individual stream scenes, while adding team name text overlays for better stream identification. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
200 lines
No EOL
5.8 KiB
TypeScript
200 lines
No EOL
5.8 KiB
TypeScript
// Performance utilities and hooks for optimization
|
|
|
|
import React, { useMemo, useCallback, useRef } from 'react';
|
|
|
|
// Debounce hook for preventing excessive API calls
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function useDebounce<T extends (...args: any[]) => any>(
|
|
callback: T,
|
|
delay: number
|
|
): T {
|
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
return useCallback((...args: Parameters<T>) => {
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current);
|
|
}
|
|
|
|
timeoutRef.current = setTimeout(() => {
|
|
callback(...args);
|
|
}, delay);
|
|
}, [callback, delay]) as T;
|
|
}
|
|
|
|
// Throttle hook for limiting function calls
|
|
export function useThrottle<T extends (...args: unknown[]) => unknown>(
|
|
callback: T,
|
|
delay: number
|
|
): T {
|
|
const lastCallRef = useRef<number>(0);
|
|
|
|
return useCallback((...args: Parameters<T>) => {
|
|
const now = Date.now();
|
|
if (now - lastCallRef.current >= delay) {
|
|
lastCallRef.current = now;
|
|
callback(...args);
|
|
}
|
|
}, [callback, delay]) as T;
|
|
}
|
|
|
|
// Memoized stream lookup utilities
|
|
export function createStreamLookupMaps(streams: Array<{ id: number; obs_source_name: string; name: string }>) {
|
|
const sourceToIdMap = new Map<string, number>();
|
|
const idToStreamMap = new Map<number, { id: number; obs_source_name: string; name: string }>();
|
|
|
|
streams.forEach(stream => {
|
|
// Generate stream group name to match what's written to files
|
|
const streamGroupName = `${stream.name.toLowerCase().replace(/\s+/g, '_')}_stream`;
|
|
sourceToIdMap.set(streamGroupName, stream.id);
|
|
idToStreamMap.set(stream.id, stream);
|
|
});
|
|
|
|
return { sourceToIdMap, idToStreamMap };
|
|
}
|
|
|
|
// Hook version for React components
|
|
export function useStreamLookupMaps(streams: Array<{ id: number; obs_source_name: string; name: string }>) {
|
|
return useMemo(() => {
|
|
return createStreamLookupMaps(streams);
|
|
}, [streams]);
|
|
}
|
|
|
|
// Efficient active source lookup
|
|
export function useActiveSourceLookup(
|
|
streams: Array<{ id: number; obs_source_name: string; name: string }>,
|
|
activeSources: Record<string, string | null>
|
|
) {
|
|
const { sourceToIdMap } = useStreamLookupMaps(streams);
|
|
|
|
return useMemo(() => {
|
|
const activeSourceIds: Record<string, number | null> = {};
|
|
|
|
Object.entries(activeSources).forEach(([screen, sourceName]) => {
|
|
activeSourceIds[screen] = sourceName ? sourceToIdMap.get(sourceName) || null : null;
|
|
});
|
|
|
|
return activeSourceIds;
|
|
}, [activeSources, sourceToIdMap]);
|
|
}
|
|
|
|
// Performance monitoring utilities
|
|
export class PerformanceMonitor {
|
|
private static metrics: Map<string, number[]> = new Map();
|
|
|
|
static startTimer(label: string): () => void {
|
|
const start = performance.now();
|
|
|
|
return () => {
|
|
const duration = performance.now() - start;
|
|
|
|
if (!this.metrics.has(label)) {
|
|
this.metrics.set(label, []);
|
|
}
|
|
|
|
this.metrics.get(label)!.push(duration);
|
|
|
|
// Keep only last 100 measurements
|
|
if (this.metrics.get(label)!.length > 100) {
|
|
this.metrics.get(label)!.shift();
|
|
}
|
|
};
|
|
}
|
|
|
|
static getMetrics(label: string) {
|
|
const measurements = this.metrics.get(label) || [];
|
|
if (measurements.length === 0) return null;
|
|
|
|
const avg = measurements.reduce((a, b) => a + b, 0) / measurements.length;
|
|
const min = Math.min(...measurements);
|
|
const max = Math.max(...measurements);
|
|
|
|
return { avg, min, max, count: measurements.length };
|
|
}
|
|
|
|
static getAllMetrics() {
|
|
const result: Record<string, ReturnType<typeof PerformanceMonitor.getMetrics>> = {};
|
|
this.metrics.forEach((_, label) => {
|
|
result[label] = this.getMetrics(label);
|
|
});
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// React component performance wrapper
|
|
export function withPerformanceMonitoring<P extends object>(
|
|
Component: React.ComponentType<P>,
|
|
componentName: string
|
|
): React.ComponentType<P> {
|
|
return function PerformanceMonitoredComponent(props: P) {
|
|
const endTimer = PerformanceMonitor.startTimer(`${componentName}_render`);
|
|
|
|
try {
|
|
const result = React.createElement(Component, props);
|
|
endTimer();
|
|
return result;
|
|
} catch (error) {
|
|
endTimer();
|
|
throw error;
|
|
}
|
|
};
|
|
}
|
|
|
|
// Visibility API hook for pausing updates when not visible
|
|
export function usePageVisibility() {
|
|
const [isVisible, setIsVisible] = React.useState(true);
|
|
|
|
React.useEffect(() => {
|
|
if (typeof document === 'undefined') return; // SSR check
|
|
|
|
const handleVisibilityChange = () => {
|
|
setIsVisible(!document.hidden);
|
|
};
|
|
|
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
return () => {
|
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
};
|
|
}, []);
|
|
|
|
return isVisible;
|
|
}
|
|
|
|
// Smart polling hook that respects visibility and connection status
|
|
export function useSmartPolling(
|
|
callback: () => void | Promise<void>,
|
|
interval: number,
|
|
dependencies: unknown[] = []
|
|
) {
|
|
const isVisible = usePageVisibility();
|
|
const callbackRef = useRef(callback);
|
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
// Update callback ref
|
|
React.useEffect(() => {
|
|
callbackRef.current = callback;
|
|
}, [callback]);
|
|
|
|
React.useEffect(() => {
|
|
// Clear any existing interval before setting up a new one
|
|
if (intervalRef.current) {
|
|
clearInterval(intervalRef.current);
|
|
intervalRef.current = null;
|
|
}
|
|
|
|
if (isVisible) {
|
|
// Start polling when visible
|
|
callbackRef.current();
|
|
intervalRef.current = setInterval(() => {
|
|
callbackRef.current();
|
|
}, interval);
|
|
}
|
|
|
|
return () => {
|
|
if (intervalRef.current) {
|
|
clearInterval(intervalRef.current);
|
|
intervalRef.current = null;
|
|
}
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [interval, isVisible, ...dependencies]);
|
|
} |