/** * CompositorControls Component (Compact Overlay) * * A compact floating toolbar for compositor controls. * Designed to overlay on the video preview without taking extra space. * * Features: * - Compact horizontal layout bar (one row) * - Layout presets as icon buttons * - Scaling mode as icon toggle * - Hover to expand for more options */ import React, { useCallback, useState } from "react"; import type { LayoutConfig, LayoutMode, ScalingMode, MediaSource, RendererType, RendererStats, Layer, } from "@livepeer-frameworks/streamcrafter-core"; import { isLayoutAvailable } from "@livepeer-frameworks/streamcrafter-core"; // ============================================================================ // Custom Tooltip Component (instant, styled) // ============================================================================ interface TooltipProps { text: string; children: React.ReactNode; } const Tooltip: React.FC = ({ text, children }) => { const [show, setShow] = useState(false); return (
setShow(true)} onMouseLeave={() => setShow(false)} > {children} {show &&
{text}
}
); }; export interface CompositorControlsProps { // State isEnabled: boolean; isInitialized: boolean; rendererType: RendererType | null; stats: RendererStats | null; // Sources and layers sources: MediaSource[]; layers: Layer[]; // Actions onLayoutApply?: (layout: LayoutConfig) => void; onCycleSourceOrder?: (direction?: "forward" | "backward") => void; // Called when clicking active layout currentLayout?: LayoutConfig | null; // Options showStats?: boolean; className?: string; } // ============================================================================ // Compact SVG Icons (12x12) // ============================================================================ function SoloIcon() { return ( ); } function PipBRIcon() { return ( ); } function PipBLIcon() { return ( ); } function PipTRIcon() { return ( ); } function PipTLIcon() { return ( ); } function SplitHIcon() { return ( ); } function SplitVIcon() { return ( ); } function FocusLIcon() { return ( ); } function FocusRIcon() { return ( ); } function GridIcon() { return ( ); } function StackIcon() { return ( ); } // 3-source layout icons function DualPipIcon() { return ( ); } function SplitPipIcon() { return ( ); } function FeaturedIcon() { return ( ); } function FeaturedRIcon() { return ( ); } // Scaling mode icons function LetterboxIcon() { return ( ); } function CropIcon() { return ( ); } function StretchIcon() { return ( ); } // ============================================================================ // Layout Preset Definitions // ============================================================================ interface LayoutPresetUI { mode: LayoutMode; label: string; icon: React.ReactNode; minSources: number; } const LAYOUT_PRESETS_UI: LayoutPresetUI[] = [ { mode: "solo", label: "Solo", icon: , minSources: 1 }, // 2-source layouts { mode: "pip-br", label: "PiP ↘", icon: , minSources: 2 }, { mode: "pip-bl", label: "PiP ↙", icon: , minSources: 2 }, { mode: "pip-tr", label: "PiP ↗", icon: , minSources: 2 }, { mode: "pip-tl", label: "PiP ↖", icon: , minSources: 2 }, { mode: "split-h", label: "Split ⬌", icon: , minSources: 2 }, { mode: "split-v", label: "Split ⬍", icon: , minSources: 2 }, { mode: "focus-l", label: "Focus ◀", icon: , minSources: 2 }, { mode: "focus-r", label: "Focus ▶", icon: , minSources: 2 }, // 3-source layouts { mode: "pip-dual-br", label: "Main+2 PiP", icon: , minSources: 3 }, { mode: "split-pip-r", label: "Split+PiP", icon: , minSources: 3 }, // Flexible layouts (2+ sources) { mode: "featured", label: "Featured", icon: , minSources: 3 }, { mode: "featured-r", label: "Featured ▶", icon: , minSources: 3 }, { mode: "grid", label: "Grid", icon: , minSources: 2 }, { mode: "stack", label: "Stack", icon: , minSources: 2 }, ]; const SCALING_MODES: { mode: ScalingMode; icon: React.ReactNode; label: string }[] = [ { mode: "letterbox", icon: , label: "Letterbox (fit)" }, { mode: "crop", icon: , label: "Crop (fill)" }, { mode: "stretch", icon: , label: "Stretch" }, ]; // ============================================================================ // Main Component // ============================================================================ export function CompositorControls({ isEnabled, isInitialized, rendererType, stats, sources, layers, onLayoutApply, onCycleSourceOrder, currentLayout, showStats = true, className = "", }: CompositorControlsProps) { const handleLayoutSelect = useCallback( (mode: LayoutMode, e?: React.MouseEvent) => { // If clicking the already-active layout, cycle source order if (currentLayout?.mode === mode && onCycleSourceOrder) { const direction = e?.shiftKey ? "backward" : "forward"; onCycleSourceOrder(direction); return; } if (!onLayoutApply) return; const layout: LayoutConfig = { mode, scalingMode: currentLayout?.scalingMode ?? "letterbox", pipScale: 0.25, }; onLayoutApply(layout); }, [onLayoutApply, onCycleSourceOrder, currentLayout?.mode, currentLayout?.scalingMode] ); const handleScalingModeChange = useCallback( (scalingMode: ScalingMode) => { if (!onLayoutApply || !currentLayout) return; onLayoutApply({ ...currentLayout, scalingMode }); }, [onLayoutApply, currentLayout] ); // Don't render if not enabled/initialized if (!isEnabled || !isInitialized) { return null; } // Get visibility state for each source from layers const getSourceVisibility = (sourceId: string): boolean => { const layer = layers.find((l) => l.sourceId === sourceId); return layer?.visible ?? true; }; const visibleSourceCount = sources.filter((s) => getSourceVisibility(s.id)).length; const currentScalingMode = currentLayout?.scalingMode ?? "letterbox"; // Filter to only show available layouts based on source count const availableLayouts = LAYOUT_PRESETS_UI.filter((preset) => isLayoutAvailable(preset.mode, visibleSourceCount) ); return (
{/* Compact bar: Layout icons + scaling mode */}
{/* Layout section */}
Layout
{availableLayouts.map((preset) => { const isActive = currentLayout?.mode === preset.mode; return ( ); })}
{/* Separator */}
{/* Display mode section */}
Display
{SCALING_MODES.map((sm) => { const isActive = currentScalingMode === sm.mode; return ( ); })}
{/* Stats (subtle) */} {showStats && stats && ( <>
{rendererType === "webgpu" && "GPU"} {rendererType === "webgl" && "GL"} {rendererType === "canvas2d" && "2D"} {stats.fps}fps )}
); } export default CompositorControls;