Feat : Control streamer
Some checks failed
release / build (push) Successful in 27s
release / verify-windows (push) Failing after 56s

This commit is contained in:
jeanotx32
2026-08-11 21:11:04 +02:00
parent 6916ff4be3
commit 37e0c73ab5
10 changed files with 386 additions and 11 deletions

View File

@@ -6,14 +6,17 @@ import type {
AgentStatus,
AgentToServer,
LogLevel,
BrowserSettings,
ServerToAgent,
WatchSettings,
} from '@stream-control/shared';
import {
DEFAULT_BROWSER_SETTINGS,
DEFAULT_WATCH_SETTINGS,
PROTOCOL_VERSION,
detectPlatform,
emptyStatus,
normalizeBrowserSettings,
normalizeWatchSettings,
safeJsonParse,
} from '@stream-control/shared';
@@ -22,6 +25,8 @@ import { runDiagnostics } from './doctor.ts';
import { ObsController } from './obs.ts';
import { StreamWatcher } from './watcher.ts';
import { currentBuildId, runningBundlePath, selfUpdate } from './updater.ts';
import { closeWindow, delay as sleep, openUrl } from './browser.ts';
import { sendHotkey } from './hotkey.ts';
import { cpuUsagePercent, diskUsage, memoryUsage } from './system.ts';
const AGENT_VERSION = '0.1.0';
@@ -50,6 +55,8 @@ const watcher = new StreamWatcher(DEFAULT_WATCH_SETTINGS, {
},
});
let browserSettings: BrowserSettings = DEFAULT_BROWSER_SETTINGS;
let socket: WebSocket | null = null;
let statusTimer: NodeJS.Timeout | null = null;
let reconnectTimer: NodeJS.Timeout | null = null;
@@ -149,6 +156,7 @@ async function handleServerMessage(message: ServerToAgent): Promise<void> {
obs.applySettings(message.obs);
applyWatchSettings(message.watch);
applyBrowserSettings(message.browser);
report('info', `Agent « ${config.name} » rattaché au serveur (id ${message.agentId})`);
if (message.autoConnectObs && !obs.isConnected) {
@@ -161,6 +169,7 @@ async function handleServerMessage(message: ServerToAgent): Promise<void> {
case 'config': {
obs.applySettings(message.obs);
applyWatchSettings(message.watch);
applyBrowserSettings(message.browser);
if (message.autoConnectObs && !obs.isConnected) {
obs.connect().catch((err: Error) => report('warn', err.message));
}
@@ -187,9 +196,68 @@ async function handleServerMessage(message: ServerToAgent): Promise<void> {
}
}
function requireUrl(params: Record<string, unknown>): string {
const url = params.url;
if (typeof url !== 'string' || !url.trim()) throw new Error('Paramètre « url » manquant');
return url.trim();
}
/**
* Aiguille une action : la surveillance et le clavier sont gérés par l'agent,
* tout le reste part vers obs-websocket.
* Séquence complète : ouvrir la page, laisser le lecteur démarrer, passer en
* plein écran, lancer l'enregistrement. Chaque étape est journalisée séparément
* pour qu'un échec désigne son maillon.
*/
async function startCapture(params: Record<string, unknown>): Promise<unknown> {
if (!browserSettings.enabled) {
throw new Error('Pilotage du navigateur désactivé sur cet agent');
}
const url = requireUrl(params);
const opened = await openUrl(browserSettings, url, report);
const wait = Number(params.readyDelayMs ?? browserSettings.readyDelayMs);
report('info', `Attente de ${Math.round(wait / 1000)} s avant le plein écran`);
await sleep(wait);
const fullscreen = watcher.snapshotSettings.fullscreen;
let fullscreenResult: unknown = 'ignoré';
if (fullscreen.enabled) {
// Un échec ici ne doit pas empêcher l'enregistrement : mieux vaut capturer
// une fenêtre non maximisée que ne rien capturer du tout.
fullscreenResult = await sendHotkey({
key: fullscreen.key,
windowMatch: fullscreen.windowMatch,
}).catch((err: Error) => {
report('warn', `Plein écran impossible : ${err.message}`);
return `échec : ${err.message}`;
});
}
await obs.execute('record.start');
report('info', `Capture démarrée pour ${url}`);
return { opened, fullscreen: fullscreenResult, recording: true };
}
async function stopCapture(): Promise<unknown> {
const result = await obs.execute('record.stop');
let closed: unknown = 'conservée';
if (browserSettings.enabled && browserSettings.closeOnStop) {
closed = await closeWindow(watcher.snapshotSettings.fullscreen.windowMatch, report).catch(
(err: Error) => {
report('warn', `Fermeture de la fenêtre impossible : ${err.message}`);
return `échec : ${err.message}`;
},
);
}
return { ...(result as object), window: closed };
}
/**
* Aiguille une action : navigateur, surveillance et clavier sont gérés par
* l'agent lui-même ; tout le reste part vers obs-websocket.
*/
async function runAction(action: AgentAction, params: Record<string, unknown>): Promise<unknown> {
switch (action) {
@@ -197,6 +265,14 @@ async function runAction(action: AgentAction, params: Record<string, unknown>):
return watcher.checkNow();
case 'hotkey.fullscreen':
return watcher.restoreFullscreen();
case 'browser.open':
return openUrl(browserSettings, requireUrl(params), report);
case 'browser.close':
return closeWindow(watcher.snapshotSettings.fullscreen.windowMatch, report);
case 'capture.start':
return startCapture(params);
case 'capture.stop':
return stopCapture();
case 'agent.update':
return selfUpdate(
typeof params.url === 'string' && params.url ? params.url : config.packageUrl,
@@ -214,6 +290,10 @@ function applyWatchSettings(raw: WatchSettings | undefined): void {
watcher.applySettings(normalizeWatchSettings(raw ?? DEFAULT_WATCH_SETTINGS));
}
function applyBrowserSettings(raw: BrowserSettings | undefined): void {
browserSettings = normalizeBrowserSettings(raw ?? DEFAULT_BROWSER_SETTINGS);
}
// --- Boucle de statut --------------------------------------------------------
async function buildStatus(): Promise<AgentStatus> {