import { useState } from 'react'; import type { AgentAction, AgentView, BrowserSettings, PresetApplyResult, RecordingPreset, RecordingSettings, SessionImportResult, StreamState, WatchSettings, WatchTarget, } from '@stream-control/shared'; import { RECORDING_ENCODERS, RECORDING_PRESETS, findRecordingPreset } from '@stream-control/shared'; import { api } from '../api'; interface Props { agent: AgentView; /** Streamers déjà suivis : ils alimentent la sélection ci-dessous. */ targets: WatchTarget[]; onClose: () => void; onCommand: (id: string, action: AgentAction, params?: Record) => Promise; notify: (message: string, tone?: 'info' | 'error') => void; } const STATE_HINTS: Record = { public: 'en direct', private: 'show privé', offline: 'hors-ligne', unknown: '', }; const QUALITY_LABELS: Record = { Small: 'qualité haute', HQ: 'qualité très haute', Lossless: 'sans perte', }; /** Résume en une ligne ce que le preset impose réellement à OBS. */ function describePreset(preset: RecordingPreset): string { return [ preset.height ? `${preset.height}p` : 'définition de la scène', `${preset.fps} fps`, QUALITY_LABELS[preset.quality], preset.format === 'mkv' ? 'MKV' : 'MP4', `audio ${preset.audioBitrateKbps} kb/s`, ].join(' · '); } function cpuGauge(cost: number): string { return '●'.repeat(cost) + '○'.repeat(4 - cost); } /** Compte rendu de l'agent : ce qu'OBS a accepté, refusé, et ce qui reste à faire. */ function PresetReport({ result }: { result: PresetApplyResult }) { return (

{result.presetLabel} appliqué · {result.video} ·{' '} {result.applied.length} paramètre(s) écrit(s)

{result.skipped.length > 0 && (

Refusé par OBS : {result.skipped.join(', ')}

)} {result.restartRequired && (

L'encodeur a changé. OBS continuera d'utiliser le précédent jusqu'à son redémarrage.

)}
); } export function AgentSettings({ agent, targets, onClose, onCommand, notify }: Props) { const [name, setName] = useState(agent.name); const [host, setHost] = useState(agent.obs.host); const [port, setPort] = useState(String(agent.obs.port)); const [password, setPassword] = useState(agent.obs.password); const [autoConnect, setAutoConnect] = useState(agent.autoConnectObs); const [notes, setNotes] = useState(agent.notes ?? ''); const [directory, setDirectory] = useState(agent.status.recordDirectory ?? ''); const [token, setToken] = useState(null); const [busy, setBusy] = useState(false); const [watch, setWatch] = useState(agent.watch); const [browser, setBrowser] = useState(agent.browser); const [recording, setRecording] = useState(agent.recording); const [presetResult, setPresetResult] = useState(null); const [applyingPreset, setApplyingPreset] = useState(false); const [importResult, setImportResult] = useState(null); const [importingSession, setImportingSession] = useState(false); const preset = findRecordingPreset(recording.presetId) ?? RECORDING_PRESETS[0]!; const encoderInfo = RECORDING_ENCODERS[recording.encoder]; // Saisie libre : soit l'opérateur la demande, soit le pseudo déjà configuré // ne fait pas partie des streamers suivis. const [manualUsername, setManualUsername] = useState( () => Boolean(agent.watch.username) && !targets.some((t) => t.username === agent.watch.username), ); function patchBrowser(patch: Partial) { setBrowser((current) => ({ ...current, ...patch })); } function patchWatch(patch: Partial) { setWatch((current) => ({ ...current, ...patch })); } function patchFullscreen(patch: Partial) { setWatch((current) => ({ ...current, fullscreen: { ...current.fullscreen, ...patch } })); } /** * Applique le preset actuellement sélectionné, enregistré ou non : c'est un * essai, on veut voir le compte rendu avant de figer le choix. */ async function applyPreset() { setApplyingPreset(true); setPresetResult(null); try { const { data } = await api.command(agent.id, 'preset.apply', { presetId: recording.presetId, encoder: recording.encoder, }); setPresetResult(data as PresetApplyResult); } catch (err) { notify(err instanceof Error ? err.message : 'Application du preset impossible', 'error'); } finally { setApplyingPreset(false); } } /** * Copie les cookies du Firefox personnel dans le profil piloté, pour que la * capture démarre déjà connectée. Ferme au passage l'instance pilotée si * elle tournait — la prochaine capture la relance avec les cookies neufs. */ async function importSession() { setImportingSession(true); setImportResult(null); try { const { data } = await api.command(agent.id, 'browser.importSession'); setImportResult(data as SessionImportResult); notify('Session importée'); } catch (err) { notify(err instanceof Error ? err.message : "Import de session impossible", 'error'); } finally { setImportingSession(false); } } async function save() { setBusy(true); try { await api.updateAgent(agent.id, { name, notes, autoConnectObs: autoConnect, obs: { host, port: Number(port), password }, watch, browser, recording, } as Partial); notify('Configuration enregistrée'); onClose(); } catch (err) { notify(err instanceof Error ? err.message : 'Enregistrement impossible', 'error'); } finally { setBusy(false); } } async function applyDirectory() { if (!directory.trim()) return; await onCommand(agent.id, 'recordDirectory.set', { directory: directory.trim() }); } async function rotate() { if (!confirm("Régénérer le jeton ? L'agent sera déconnecté jusqu'à sa reconfiguration.")) return; try { const result = await api.rotateToken(agent.id); setToken(result.token); } catch (err) { notify(err instanceof Error ? err.message : 'Rotation impossible', 'error'); } } async function remove() { if (!confirm(`Supprimer définitivement l'agent « ${agent.name} » ?`)) return; try { await api.deleteAgent(agent.id); notify('Agent supprimé'); onClose(); } catch (err) { notify(err instanceof Error ? err.message : 'Suppression impossible', 'error'); } } return (
event.stopPropagation()}>

Configuration · {agent.name}