REVAMP : Firefox handling
This commit is contained in:
@@ -6,6 +6,7 @@ import type {
|
||||
AgentEvent,
|
||||
AgentStatus,
|
||||
AgentToServer,
|
||||
FullscreenSettings,
|
||||
LogLevel,
|
||||
BrowserSettings,
|
||||
PresetApplyResult,
|
||||
@@ -32,6 +33,8 @@ import { ObsController } from './obs.ts';
|
||||
import { StreamWatcher } from './watcher.ts';
|
||||
import { currentBuildId, runningBundlePath, selfUpdate } from './updater.ts';
|
||||
import { blankPage, closeWindow, delay as sleep, openUrl } from './browser.ts';
|
||||
import { FirefoxController } from './firefox.ts';
|
||||
import { describeFullscreen, type FullscreenOutcome } from './fullscreen.ts';
|
||||
import { sendHotkey } from './hotkey.ts';
|
||||
import { cpuUsagePercent, diskUsage, memoryUsage } from './system.ts';
|
||||
|
||||
@@ -64,9 +67,18 @@ const watcher = new StreamWatcher(DEFAULT_WATCH_SETTINGS, {
|
||||
stopRecording: async () => {
|
||||
await stopCapture();
|
||||
},
|
||||
restoreFullscreen: (settings) => restoreFullscreen(settings),
|
||||
});
|
||||
|
||||
let browserSettings: BrowserSettings = DEFAULT_BROWSER_SETTINGS;
|
||||
/**
|
||||
* Instance Firefox pilotée, créée à la volée.
|
||||
*
|
||||
* Elle n'existe que dans le mode `bidi` : la construire d'office lancerait un
|
||||
* navigateur sur toutes les VM, y compris celles où l'opérateur ouvre les pages
|
||||
* lui-même.
|
||||
*/
|
||||
let firefox: FirefoxController | null = null;
|
||||
let recordingSettings: RecordingSettings = DEFAULT_RECORDING_SETTINGS;
|
||||
/** Un preset attend d'être appliqué : OBS était injoignable ou occupé. */
|
||||
let presetPending = false;
|
||||
@@ -238,9 +250,7 @@ async function startCapture(params: Record<string, unknown>): Promise<unknown> {
|
||||
}
|
||||
|
||||
const url = requireUrl(params);
|
||||
const opened = await openUrl(browserSettings, url, report, {
|
||||
forceXWayland: watcher.snapshotSettings.fullscreen.enabled,
|
||||
});
|
||||
const opened = await openPage(url);
|
||||
|
||||
const wait = Number(params.readyDelayMs ?? browserSettings.readyDelayMs);
|
||||
report('info', `Attente de ${Math.round(wait / 1000)} s avant le plein écran`);
|
||||
@@ -251,13 +261,19 @@ async function startCapture(params: Record<string, unknown>): Promise<unknown> {
|
||||
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}`, 'fullscreen.failed');
|
||||
return `échec : ${err.message}`;
|
||||
});
|
||||
fullscreenResult = await restoreFullscreen(fullscreen)
|
||||
.then((outcome) => {
|
||||
report(
|
||||
outcome.confirmed === false ? 'warn' : 'info',
|
||||
describeFullscreen(outcome, fullscreen.key),
|
||||
outcome.confirmed === false ? 'fullscreen.failed' : 'fullscreen.restored',
|
||||
);
|
||||
return outcome;
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
report('warn', `Plein écran impossible : ${err.message}`, 'fullscreen.failed');
|
||||
return `échec : ${err.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
await obs.execute('record.start');
|
||||
@@ -266,6 +282,27 @@ async function startCapture(params: Record<string, unknown>): Promise<unknown> {
|
||||
return { opened, fullscreen: fullscreenResult, recording: true };
|
||||
}
|
||||
|
||||
/** Ouvre une page par la voie correspondant au mode de pilotage. */
|
||||
async function openPage(url: string): Promise<unknown> {
|
||||
const bidi = driver();
|
||||
if (bidi) return bidi.navigate(url);
|
||||
return openUrl(browserSettings, url, report, {
|
||||
forceXWayland: watcher.snapshotSettings.fullscreen.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
/** Décharge ou ferme la fenêtre, selon le réglage et le mode de pilotage. */
|
||||
async function releasePage(): Promise<unknown> {
|
||||
const bidi = driver();
|
||||
if (bidi) return browserSettings.onStop === 'close' ? bidi.quit() : bidi.blank();
|
||||
|
||||
return browserSettings.onStop === 'close'
|
||||
? closeWindow(watcher.snapshotSettings.fullscreen.windowMatch, report)
|
||||
: blankPage(browserSettings, report, {
|
||||
forceXWayland: watcher.snapshotSettings.fullscreen.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
async function stopCapture(): Promise<unknown> {
|
||||
const result = await obs.execute('record.stop');
|
||||
|
||||
@@ -274,13 +311,7 @@ async function stopCapture(): Promise<unknown> {
|
||||
// lecteur sans faire disparaître la fenêtre.
|
||||
let closed: unknown = 'conservée';
|
||||
if (browserSettings.enabled && browserSettings.onStop !== 'keep') {
|
||||
const action =
|
||||
browserSettings.onStop === 'close'
|
||||
? closeWindow(watcher.snapshotSettings.fullscreen.windowMatch, report)
|
||||
: blankPage(browserSettings, report, {
|
||||
forceXWayland: watcher.snapshotSettings.fullscreen.enabled,
|
||||
});
|
||||
closed = await action.catch((err: Error) => {
|
||||
closed = await releasePage().catch((err: Error) => {
|
||||
report('warn', `Libération de la fenêtre impossible : ${err.message}`, 'command.failed');
|
||||
return `échec : ${err.message}`;
|
||||
});
|
||||
@@ -301,11 +332,9 @@ async function runAction(action: AgentAction, params: Record<string, unknown>):
|
||||
case 'hotkey.fullscreen':
|
||||
return watcher.restoreFullscreen();
|
||||
case 'browser.open':
|
||||
return openUrl(browserSettings, requireUrl(params), report, {
|
||||
forceXWayland: watcher.snapshotSettings.fullscreen.enabled,
|
||||
});
|
||||
return openPage(requireUrl(params));
|
||||
case 'browser.close':
|
||||
return closeWindow(watcher.snapshotSettings.fullscreen.windowMatch, report);
|
||||
return driver()?.quit() ?? closeWindow(watcher.snapshotSettings.fullscreen.windowMatch, report);
|
||||
case 'capture.start':
|
||||
return startCapture(params);
|
||||
case 'capture.stop':
|
||||
@@ -331,6 +360,35 @@ function applyWatchSettings(raw: WatchSettings | undefined): void {
|
||||
|
||||
function applyBrowserSettings(raw: BrowserSettings | undefined): void {
|
||||
browserSettings = normalizeBrowserSettings(raw ?? DEFAULT_BROWSER_SETTINGS);
|
||||
|
||||
if (browserSettings.mode !== 'bidi') {
|
||||
// On repasse en mode `launch` : le canal n'a plus de sens, mais on laisse
|
||||
// volontairement la fenêtre ouverte — OBS la capture peut-être encore.
|
||||
firefox?.dispose();
|
||||
firefox = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (firefox) firefox.applySettings(browserSettings);
|
||||
else firefox = new FirefoxController(browserSettings, report);
|
||||
}
|
||||
|
||||
/** Contrôleur BiDi, ou `null` si l'agent n'est pas dans ce mode. */
|
||||
function driver(): FirefoxController | null {
|
||||
return browserSettings.mode === 'bidi' ? firefox : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rappelle le plein écran par la voie correspondant au mode de pilotage.
|
||||
*
|
||||
* En BiDi, la demande part dans Firefox et son effet est relu dans la page. En
|
||||
* `launch`, on en reste à une touche envoyée au serveur d'affichage, sans
|
||||
* moyen de savoir ce qu'elle a produit — d'où le `confirmed` absent.
|
||||
*/
|
||||
async function restoreFullscreen(settings: FullscreenSettings): Promise<FullscreenOutcome> {
|
||||
const bidi = driver();
|
||||
if (bidi) return bidi.restoreFullscreen(settings);
|
||||
return sendHotkey({ key: settings.key, windowMatch: settings.windowMatch });
|
||||
}
|
||||
|
||||
// --- Presets d'enregistrement ------------------------------------------------
|
||||
@@ -416,6 +474,7 @@ async function buildStatus(): Promise<AgentStatus> {
|
||||
...emptyStatus(),
|
||||
...snapshot,
|
||||
watch: watcher.snapshot,
|
||||
browser: driver()?.state,
|
||||
buildId: BUILD_ID,
|
||||
canSelfUpdate: Boolean(runningBundlePath() && config.packageUrl),
|
||||
lastRecordingPath: obs.recordingPath,
|
||||
@@ -462,6 +521,9 @@ async function shutdown(signal: string): Promise<void> {
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
watcher.stop();
|
||||
// Le canal BiDi se ferme, mais pas Firefox : sa fenêtre est la source de
|
||||
// capture d'OBS, et l'enregistrement en cours doit lui survivre.
|
||||
firefox?.dispose();
|
||||
// L'enregistrement OBS en cours n'est volontairement pas interrompu.
|
||||
await obs.disconnect().catch(() => undefined);
|
||||
socket?.close(1000, 'Arrêt de l\'agent');
|
||||
|
||||
Reference in New Issue
Block a user