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

@@ -111,6 +111,7 @@ export function handleAgentConnection(
statusIntervalMs: config.statusIntervalMs,
autoConnectObs: record.autoConnectObs,
watch: record.watch,
browser: record.browser,
});
break;
}

View File

@@ -5,6 +5,7 @@ import {
AGENT_ACTIONS,
DEFAULT_OBS_SETTINGS,
isAgentAction,
normalizeBrowserSettings,
normalizeWatchSettings,
parseStripchatUsername,
} from '@stream-control/shared';
@@ -89,6 +90,7 @@ api.patch('/agents/:id', (req, res) => {
autoConnectObs:
typeof req.body?.autoConnectObs === 'boolean' ? req.body.autoConnectObs : record.autoConnectObs,
watch: normalizeWatchSettings(req.body?.watch ?? record.watch),
browser: normalizeBrowserSettings(req.body?.browser ?? record.browser),
notes: typeof req.body?.notes === 'string' ? req.body.notes : record.notes,
});
@@ -144,7 +146,9 @@ api.post('/agents/:id/command', async (req, res) => {
}
try {
const data = await hub.sendCommand(record.id, action, req.body?.params);
const timeoutMs =
action === 'capture.start' ? record.browser.readyDelayMs + 30_000 : undefined;
const data = await hub.sendCommand(record.id, action, req.body?.params, timeoutMs);
hub.log(record.id, 'info', `Commande « ${action} » exécutée`);
res.json({ ok: true, data });
} catch (err) {
@@ -303,6 +307,7 @@ api.post('/watchlist/:id/record', async (req, res) => {
obs: record.obs,
autoConnectObs: record.autoConnectObs,
watch: normalizeWatchSettings({ ...record.watch, enabled: true, username: target.username }),
browser: record.browser,
notes: record.notes,
});
@@ -313,7 +318,21 @@ api.post('/watchlist/:id/record', async (req, res) => {
}
try {
await hub.sendCommand(record.id, 'record.start');
// Avec le pilotage du navigateur, un seul appel enchaîne ouverture de la
// page, plein écran et enregistrement. Sinon on se contente de lancer OBS,
// en supposant la page déjà ouverte par l'opérateur.
if (record.browser.enabled) {
// La séquence attend le chargement de la page : le délai d'attente doit
// dépasser readyDelayMs, sinon la commande expire avant d'avoir abouti.
await hub.sendCommand(
record.id,
'capture.start',
{ url: target.url },
record.browser.readyDelayMs + 30_000,
);
} else {
await hub.sendCommand(record.id, 'record.start');
}
hub.log(
record.id,
'info',
@@ -334,7 +353,11 @@ api.post('/watchlist/:id/stop', async (req, res) => {
return;
}
try {
await hub.sendCommand(target.agentId, 'record.stop');
const agent = agentsRepo.get(target.agentId);
await hub.sendCommand(
target.agentId,
agent?.browser.enabled ? 'capture.stop' : 'record.stop',
);
res.json({ ok: true });
} catch (err) {
res.status(502).json({ ok: false, error: err instanceof Error ? err.message : String(err) });

View File

@@ -7,12 +7,15 @@ import type {
ObsSettings,
Platform,
StreamState,
BrowserSettings,
WatchSettings,
WatchTarget,
} from '@stream-control/shared';
import {
DEFAULT_BROWSER_SETTINGS,
DEFAULT_OBS_SETTINGS,
DEFAULT_WATCH_SETTINGS,
normalizeBrowserSettings,
normalizeWatchSettings,
safeJsonParse,
stripchatProfileUrl,
@@ -86,6 +89,7 @@ function addColumnIfMissing(table: string, column: string, definition: string):
// Surveillance du stream source : stockée en JSON, le schéma évolue plus vite
// que la table (nouveaux fournisseurs, nouveaux statuts).
addColumnIfMissing('agents', 'watch_json', 'TEXT');
addColumnIfMissing('agents', 'browser_json', 'TEXT');
// Enrichissement des profils surveillés : photo et historique de diffusion.
addColumnIfMissing('watch_targets', 'avatar_url', 'TEXT');
@@ -105,6 +109,7 @@ export interface AgentRow {
obs_password: string;
auto_connect: number;
watch_json: string | null;
browser_json: string | null;
notes: string | null;
created_at: number;
last_seen_at: number | null;
@@ -120,6 +125,7 @@ export interface AgentRecord {
obs: ObsSettings;
autoConnectObs: boolean;
watch: WatchSettings;
browser: BrowserSettings;
notes: string | null;
createdAt: number;
lastSeenAt: number | null;
@@ -142,6 +148,9 @@ function toRecord(row: AgentRow): AgentRecord {
watch: normalizeWatchSettings(
row.watch_json ? safeJsonParse<WatchSettings>(row.watch_json) : DEFAULT_WATCH_SETTINGS,
),
browser: normalizeBrowserSettings(
row.browser_json ? safeJsonParse<BrowserSettings>(row.browser_json) : DEFAULT_BROWSER_SETTINGS,
),
notes: row.notes,
createdAt: Number(row.created_at),
lastSeenAt: row.last_seen_at === null ? null : Number(row.last_seen_at),
@@ -155,8 +164,8 @@ const stmts = {
insertAgent: db.prepare(`
INSERT INTO agents (id, name, hostname, platform, agent_version, token_hash,
obs_host, obs_port, obs_password, auto_connect, watch_json,
notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
browser_json, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`),
updateIdentity: db.prepare(`
UPDATE agents SET hostname = ?, platform = ?, agent_version = ?, last_seen_at = ?
@@ -164,7 +173,7 @@ const stmts = {
`),
updateSettings: db.prepare(`
UPDATE agents SET name = ?, obs_host = ?, obs_port = ?, obs_password = ?,
auto_connect = ?, watch_json = ?, notes = ?
auto_connect = ?, watch_json = ?, browser_json = ?, notes = ?
WHERE id = ?
`),
touchAgent: db.prepare('UPDATE agents SET last_seen_at = ? WHERE id = ?'),
@@ -207,6 +216,7 @@ export const agentsRepo = {
obs?: Partial<ObsSettings>;
autoConnectObs?: boolean;
watch?: WatchSettings;
browser?: BrowserSettings;
notes?: string | null;
}): AgentRecord {
const obs = { ...DEFAULT_OBS_SETTINGS, ...input.obs };
@@ -222,6 +232,7 @@ export const agentsRepo = {
obs.password,
input.autoConnectObs === false ? 0 : 1,
JSON.stringify(normalizeWatchSettings(input.watch ?? DEFAULT_WATCH_SETTINGS)),
JSON.stringify(normalizeBrowserSettings(input.browser ?? DEFAULT_BROWSER_SETTINGS)),
input.notes ?? null,
Date.now(),
);
@@ -250,6 +261,7 @@ export const agentsRepo = {
obs: ObsSettings;
autoConnectObs: boolean;
watch: WatchSettings;
browser: BrowserSettings;
notes: string | null;
},
): void {
@@ -260,6 +272,7 @@ export const agentsRepo = {
settings.obs.password,
settings.autoConnectObs ? 1 : 0,
JSON.stringify(normalizeWatchSettings(settings.watch)),
JSON.stringify(normalizeBrowserSettings(settings.browser)),
settings.notes,
id,
);

View File

@@ -103,6 +103,7 @@ class Hub {
agentId: string,
action: AgentAction,
params?: Record<string, unknown>,
timeoutMs = config.commandTimeoutMs,
): Promise<unknown> {
const connection = this.connections.get(agentId);
if (!connection) throw new Error('Agent hors-ligne');
@@ -114,7 +115,7 @@ class Hub {
const timer = setTimeout(() => {
connection.pending.delete(requestId);
reject(new Error(`Timeout : l'agent n'a pas répondu à « ${action} »`));
}, config.commandTimeoutMs);
}, timeoutMs);
connection.pending.set(requestId, { resolve, reject, timer });
@@ -137,6 +138,7 @@ class Hub {
obs: record.obs,
autoConnectObs: record.autoConnectObs,
watch: record.watch,
browser: record.browser,
};
connection.socket.send(JSON.stringify(message));
}
@@ -174,6 +176,7 @@ class Hub {
obs: { ...record.obs, password: record.obs.password ? '********' : '' },
autoConnectObs: record.autoConnectObs,
watch: record.watch,
browser: record.browser,
notes: record.notes,
status: this.statusOf(record.id),
};