Feat : OBS presets
Some checks failed
release / build (push) Successful in 26s
release / verify-windows (push) Failing after 1m0s

This commit is contained in:
jeanotx32
2026-08-11 22:24:45 +02:00
parent 3b614c5d05
commit 03b1963150
11 changed files with 710 additions and 8 deletions

View File

@@ -7,16 +7,21 @@ import type {
AgentToServer,
LogLevel,
BrowserSettings,
PresetApplyResult,
RecordingSettings,
ServerToAgent,
WatchSettings,
} from '@stream-control/shared';
import {
DEFAULT_BROWSER_SETTINGS,
DEFAULT_RECORDING_SETTINGS,
DEFAULT_WATCH_SETTINGS,
PROTOCOL_VERSION,
detectPlatform,
emptyStatus,
isRecordingEncoder,
normalizeBrowserSettings,
normalizeRecordingSettings,
normalizeWatchSettings,
safeJsonParse,
} from '@stream-control/shared';
@@ -56,6 +61,9 @@ const watcher = new StreamWatcher(DEFAULT_WATCH_SETTINGS, {
});
let browserSettings: BrowserSettings = DEFAULT_BROWSER_SETTINGS;
let recordingSettings: RecordingSettings = DEFAULT_RECORDING_SETTINGS;
/** Un preset attend d'être appliqué : OBS était injoignable ou occupé. */
let presetPending = false;
let socket: WebSocket | null = null;
let statusTimer: NodeJS.Timeout | null = null;
@@ -157,6 +165,7 @@ async function handleServerMessage(message: ServerToAgent): Promise<void> {
obs.applySettings(message.obs);
applyWatchSettings(message.watch);
applyBrowserSettings(message.browser);
applyRecordingSettings(message.recording);
report('info', `Agent « ${config.name} » rattaché au serveur (id ${message.agentId})`);
if (message.autoConnectObs && !obs.isConnected) {
@@ -170,6 +179,7 @@ async function handleServerMessage(message: ServerToAgent): Promise<void> {
obs.applySettings(message.obs);
applyWatchSettings(message.watch);
applyBrowserSettings(message.browser);
applyRecordingSettings(message.recording);
if (message.autoConnectObs && !obs.isConnected) {
obs.connect().catch((err: Error) => report('warn', err.message));
}
@@ -273,6 +283,8 @@ async function runAction(action: AgentAction, params: Record<string, unknown>):
return startCapture(params);
case 'capture.stop':
return stopCapture();
case 'preset.apply':
return applyPresetNow(params);
case 'agent.update':
return selfUpdate(
typeof params.url === 'string' && params.url ? params.url : config.packageUrl,
@@ -294,6 +306,70 @@ function applyBrowserSettings(raw: BrowserSettings | undefined): void {
browserSettings = normalizeBrowserSettings(raw ?? DEFAULT_BROWSER_SETTINGS);
}
// --- Presets d'enregistrement ------------------------------------------------
/**
* Enregistre le preset voulu. L'application effective est différée : OBS peut
* être déconnecté ou en pleine capture au moment où la configuration arrive.
*/
function applyRecordingSettings(raw: RecordingSettings | undefined): void {
const next = normalizeRecordingSettings(raw ?? DEFAULT_RECORDING_SETTINGS);
const changed =
next.enabled !== recordingSettings.enabled ||
next.presetId !== recordingSettings.presetId ||
next.encoder !== recordingSettings.encoder;
recordingSettings = next;
if (next.enabled && changed) presetPending = true;
}
/**
* Applique le preset en attente dès que les conditions le permettent.
*
* Un échec ne relance pas la tentative : une clé refusée par cette version d'OBS
* le sera à chaque cycle, et marteler l'API n'y changerait rien. Le prochain
* enregistrement de la configuration, ou le bouton du dashboard, réessaiera.
*/
async function drainPresetApply(): Promise<void> {
if (!presetPending || !recordingSettings.enabled || !obs.isConnected) return;
const state = await obs.recordState().catch(() => null);
if (!state || state.active) return; // jamais pendant une capture
presetPending = false;
try {
report('info', describePreset(await obs.applyRecordingPreset(recordingSettings)));
} catch (err) {
report('warn', `Preset non appliqué : ${err instanceof Error ? err.message : String(err)}`);
}
}
/** Application à la demande : passe outre l'interrupteur, l'opérateur a tranché. */
async function applyPresetNow(params: Record<string, unknown>): Promise<PresetApplyResult> {
const result = await obs.applyRecordingPreset({
enabled: true,
presetId:
typeof params.presetId === 'string' && params.presetId
? params.presetId
: recordingSettings.presetId,
encoder: isRecordingEncoder(params.encoder) ? params.encoder : recordingSettings.encoder,
});
presetPending = false;
report('info', describePreset(result));
return result;
}
function describePreset(result: PresetApplyResult): string {
const parts = [
`Preset « ${result.presetLabel} » appliqué — ${result.encoder}, ${result.video}`,
];
if (result.skipped.length > 0) parts.push(`refusé par OBS : ${result.skipped.join(', ')}`);
if (result.restartRequired) {
parts.push("l'encodeur a changé : redémarre OBS pour qu'il le charge");
}
return parts.join(' — ');
}
// --- Boucle de statut --------------------------------------------------------
async function buildStatus(): Promise<AgentStatus> {
@@ -320,6 +396,9 @@ async function buildStatus(): Promise<AgentStatus> {
async function pushStatus(): Promise<void> {
if (socket?.readyState !== WebSocket.OPEN) return;
try {
// Le cycle de statut sert aussi d'horloge au preset en attente : il tourne
// déjà, et il s'exécute justement quand OBS vient de redevenir joignable.
await drainPresetApply();
send({ type: 'status', status: await buildStatus() });
} catch (err) {
console.error('Collecte de statut en échec :', err);