From 6ad8429cf1fcbc4fa2c8fa3766b699d23f1e0775 Mon Sep 17 00:00:00 2001 From: jeanotx32 Date: Wed, 12 Aug 2026 00:40:20 +0200 Subject: [PATCH] Feat : open link on current firefox session --- README.md | 34 +++++++++++++++ packages/agent/src/browser.ts | 31 +++++++++++++ packages/agent/src/index.ts | 21 +++++---- packages/server/src/db.ts | 43 +++++++++++++++++++ packages/shared/src/index.ts | 39 ++++++++++++++--- packages/web/src/components/AgentSettings.tsx | 30 +++++++++---- 6 files changed, 176 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 8fcb61d..d583163 100644 --- a/README.md +++ b/README.md @@ -335,6 +335,40 @@ Le délai d'amorçage n'est pas une précaution de style : un modèle qui sort d repasse « public » quelques secondes avant de se remettre en place. Déclencher sur la première lecture produirait des fichiers de dix secondes. +## Capture de fenêtre OBS et réutilisation + +La source « capture de fenêtre » d'OBS mémorise un identifiant de fenêtre X11. Fermer la +fenêtre du navigateur, ou en ouvrir une nouvelle, invalide cet identifiant : la source +devient noire et il faut la repointer à la main. + +L'agent évite donc les deux : + +- **aucun argument** par défaut (`--new-window` est proscrit) : Firefox confie l'URL à la + fenêtre déjà ouverte ; +- **à l'arrêt, page vide** plutôt que fermeture : la fenêtre survit, et le lecteur cesse de + décoder la vidéo. + +Les agents configurés avant ce correctif sont migrés au démarrage du serveur, sauf si leurs +arguments ou leur comportement d'arrêt ont été personnalisés. + +### Éviter l'accumulation d'onglets + +Avec les réglages d'usine de Firefox, un lien venu de l'extérieur ouvre un **nouvel +onglet** : chaque cycle d'enregistrement en laisse donc derrière lui, et les anciens +continuent de décoder leur page. Sur une VM d'enregistrement, règle une fois pour toutes +dans `about:config` : + +``` +browser.link.open_newwindow = 1 +``` + +L'URL remplace alors le contenu de l'onglet courant. Ouverture et page vide réutilisent le +même onglet, dans la même fenêtre — rien ne s'accumule et OBS ne perd jamais sa cible. + +Dans OBS, règle aussi la **priorité de correspondance** de la source sur « Faire +correspondre le titre, sinon trouver une fenêtre du même type » : le titre suit l'onglet +actif et change à chaque streamer. + ## API HTTP Toutes les routes hors `/api/login` exigent `Authorization: Bearer `. diff --git a/packages/agent/src/browser.ts b/packages/agent/src/browser.ts index 113aa21..7bdb4a8 100644 --- a/packages/agent/src/browser.ts +++ b/packages/agent/src/browser.ts @@ -75,6 +75,37 @@ export async function openUrl( }); } +/** + * Décharge le lecteur en confiant une page vide à la fenêtre existante. + * + * La fenêtre survit — donc la source OBS aussi — et le flux cesse d'être décodé. + * `about:blank` n'est pas une URL fournie par l'utilisateur : elle contourne + * légitimement le filtre http/https appliqué aux liens de streamers. + */ +export async function blankPage( + settings: BrowserSettings, + log: BrowserLog, +): Promise<{ command: string; url: string }> { + const args = [...settings.args, 'about:blank']; + log('info', 'Lecteur déchargé (page vide) — fenêtre conservée pour OBS', 'browser.closed'); + + const child = spawn(settings.command, args, { + detached: true, + stdio: 'ignore', + env: { ...x11Env(), MOZ_ENABLE_WAYLAND: '0' }, + }); + + return new Promise((resolve, reject) => { + child.once('error', (err: NodeJS.ErrnoException) => + reject(new Error(`Chargement de la page vide impossible : ${err.message}`)), + ); + setTimeout(() => { + child.unref(); + resolve({ command: settings.command, url: 'about:blank' }); + }, 400); + }); +} + /** Ferme la fenêtre du lecteur, sans toucher au reste de la session. */ export async function closeWindow( windowMatch: string, diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index fd68784..24f6ad5 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -31,7 +31,7 @@ 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 { blankPage, closeWindow, delay as sleep, openUrl } from './browser.ts'; import { sendHotkey } from './hotkey.ts'; import { cpuUsagePercent, diskUsage, memoryUsage } from './system.ts'; @@ -267,14 +267,19 @@ async function startCapture(params: Record): Promise { async function stopCapture(): Promise { const result = await obs.execute('record.stop'); + // Fermer la fenêtre détruit la source « capture de fenêtre » d'OBS, qu'il faut + // ensuite repointer à la main. D'où le défaut « page vide », qui décharge le + // lecteur sans faire disparaître la fenêtre. 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}`, 'command.failed'); - return `échec : ${err.message}`; - }, - ); + if (browserSettings.enabled && browserSettings.onStop !== 'keep') { + const action = + browserSettings.onStop === 'close' + ? closeWindow(watcher.snapshotSettings.fullscreen.windowMatch, report) + : blankPage(browserSettings, report); + closed = await action.catch((err: Error) => { + report('warn', `Libération de la fenêtre impossible : ${err.message}`, 'command.failed'); + return `échec : ${err.message}`; + }); } report('info', 'Capture arrêtée', 'capture.stopped'); diff --git a/packages/server/src/db.ts b/packages/server/src/db.ts index ed6c93c..04b0bc7 100644 --- a/packages/server/src/db.ts +++ b/packages/server/src/db.ts @@ -151,6 +151,49 @@ function migrateIdleStatus(): void { migrateIdleStatus(); +/** + * `--new-window` et la fermeture systématique de la fenêtre détruisaient à chaque + * capture la source « capture de fenêtre » d'OBS, qu'il fallait ensuite repointer + * à la main. Les agents configurés avant le correctif portent ces valeurs dans + * leur JSON. + * + * Même précaution qu'au-dessus : seules les configurations restées au défaut sont + * reprises, ce qui rend la migration idempotente et respecte les réglages + * personnalisés. + */ +function migrateBrowserWindowReuse(): void { + const rows = db + .prepare('SELECT id, browser_json FROM agents WHERE browser_json IS NOT NULL') + .all() as unknown as Array<{ id: string; browser_json: string }>; + const update = db.prepare('UPDATE agents SET browser_json = ? WHERE id = ?'); + let migrated = 0; + + for (const row of rows) { + const browser = safeJsonParse(row.browser_json); + if (!browser) continue; + + const atLegacyDefault = + Array.isArray(browser.args) && + browser.args.length === 1 && + browser.args[0] === '--new-window' && + browser.closeOnStop === true && + browser.onStop === undefined; + if (!atLegacyDefault) continue; + + const { closeOnStop: _legacy, ...rest } = browser; + update.run(JSON.stringify({ ...rest, args: [], onStop: 'blank' }), row.id); + migrated += 1; + } + + if (migrated > 0) { + console.log( + `Migration : réutilisation de la fenêtre du navigateur activée sur ${migrated} agent(s)`, + ); + } +} + +migrateBrowserWindowReuse(); + export interface AgentRow { id: string; name: string; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index d407452..25fcb28 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -379,20 +379,36 @@ export interface BrowserSettings { enabled: boolean; /** Exécutable du navigateur. */ command: string; - /** Arguments placés avant l'URL. */ + /** + * Arguments placés avant l'URL. Vide par défaut, et ce n'est pas un oubli : + * `--new-window` créerait une fenêtre neuve à chaque capture, avec un nouvel + * identifiant X11 — la source « capture de fenêtre » d'OBS perdrait sa cible + * et il faudrait la repointer à la main. Sans argument, Firefox confie l'URL à + * la fenêtre déjà ouverte, qu'OBS continue de capturer. + */ args: string[]; /** Délai avant l'envoi du plein écran, le temps que le lecteur démarre. */ readyDelayMs: number; - /** Fermer la fenêtre quand l'enregistrement s'arrête. */ - closeOnStop: boolean; + /** + * Sort de la fenêtre à l'arrêt de l'enregistrement. + * + * `blank` charge une page vide : la fenêtre survit, donc la source OBS aussi, + * et le lecteur cesse de décoder la vidéo. `close` ferme la fenêtre — c'est + * précisément ce qui casse la capture OBS. `keep` ne touche à rien et laisse + * le flux tourner. + */ + onStop: BrowserStopAction; } +export const BROWSER_STOP_ACTIONS = ['blank', 'keep', 'close'] as const; +export type BrowserStopAction = (typeof BROWSER_STOP_ACTIONS)[number]; + export const DEFAULT_BROWSER_SETTINGS: BrowserSettings = { enabled: false, command: 'firefox', - args: ['--new-window'], + args: [], readyDelayMs: 8000, - closeOnStop: true, + onStop: 'blank', }; export function normalizeBrowserSettings(raw: unknown): BrowserSettings { @@ -412,10 +428,21 @@ export function normalizeBrowserSettings(raw: unknown): BrowserSettings { readyDelayMs: Number.isFinite(delay) ? Math.min(Math.max(Math.round(delay), 0), 120_000) : base.readyDelayMs, - closeOnStop: input.closeOnStop !== false, + onStop: readStopAction(raw), }; } +/** Lit le sort de la fenêtre, en acceptant l'ancien booléen `closeOnStop`. */ +function readStopAction(raw: unknown): BrowserStopAction { + const input = (raw ?? {}) as Partial & { closeOnStop?: boolean }; + if ((BROWSER_STOP_ACTIONS as readonly string[]).includes(input.onStop as string)) { + return input.onStop as BrowserStopAction; + } + // Configuration écrite avant l'introduction du choix à trois valeurs. + if (typeof input.closeOnStop === 'boolean') return input.closeOnStop ? 'close' : 'keep'; + return DEFAULT_BROWSER_SETTINGS.onStop; +} + export const DEFAULT_WATCH_SETTINGS: WatchSettings = { enabled: false, provider: 'stripchat', diff --git a/packages/web/src/components/AgentSettings.tsx b/packages/web/src/components/AgentSettings.tsx index db0a92d..0701f25 100644 --- a/packages/web/src/components/AgentSettings.tsx +++ b/packages/web/src/components/AgentSettings.tsx @@ -359,8 +359,12 @@ export function AgentSettings({ agent, targets, onClose, onCommand, notify }: Pr onChange={(event) => patchBrowser({ args: event.target.value.split(/\s+/).filter(Boolean) }) } - placeholder="--new-window" + placeholder="aucun" /> + + Laisse vide : le lien part vers la fenêtre déjà ouverte. --new-window + {' '}en créerait une nouvelle à chaque capture, et OBS perdrait sa cible. + @@ -380,13 +384,23 @@ export function AgentSettings({ agent, targets, onClose, onCommand, notify }: Pr -