Feat : open link on current firefox session
This commit is contained in:
34
README.md
34
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
|
repasse « public » quelques secondes avant de se remettre en place. Déclencher sur la
|
||||||
première lecture produirait des fichiers de dix secondes.
|
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
|
## API HTTP
|
||||||
|
|
||||||
Toutes les routes hors `/api/login` exigent `Authorization: Bearer <jeton de session>`.
|
Toutes les routes hors `/api/login` exigent `Authorization: Bearer <jeton de session>`.
|
||||||
|
|||||||
@@ -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. */
|
/** Ferme la fenêtre du lecteur, sans toucher au reste de la session. */
|
||||||
export async function closeWindow(
|
export async function closeWindow(
|
||||||
windowMatch: string,
|
windowMatch: string,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { runDiagnostics } from './doctor.ts';
|
|||||||
import { ObsController } from './obs.ts';
|
import { ObsController } from './obs.ts';
|
||||||
import { StreamWatcher } from './watcher.ts';
|
import { StreamWatcher } from './watcher.ts';
|
||||||
import { currentBuildId, runningBundlePath, selfUpdate } from './updater.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 { sendHotkey } from './hotkey.ts';
|
||||||
import { cpuUsagePercent, diskUsage, memoryUsage } from './system.ts';
|
import { cpuUsagePercent, diskUsage, memoryUsage } from './system.ts';
|
||||||
|
|
||||||
@@ -267,14 +267,19 @@ async function startCapture(params: Record<string, unknown>): Promise<unknown> {
|
|||||||
async function stopCapture(): Promise<unknown> {
|
async function stopCapture(): Promise<unknown> {
|
||||||
const result = await obs.execute('record.stop');
|
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';
|
let closed: unknown = 'conservée';
|
||||||
if (browserSettings.enabled && browserSettings.closeOnStop) {
|
if (browserSettings.enabled && browserSettings.onStop !== 'keep') {
|
||||||
closed = await closeWindow(watcher.snapshotSettings.fullscreen.windowMatch, report).catch(
|
const action =
|
||||||
(err: Error) => {
|
browserSettings.onStop === 'close'
|
||||||
report('warn', `Fermeture de la fenêtre impossible : ${err.message}`, 'command.failed');
|
? closeWindow(watcher.snapshotSettings.fullscreen.windowMatch, report)
|
||||||
return `échec : ${err.message}`;
|
: 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');
|
report('info', 'Capture arrêtée', 'capture.stopped');
|
||||||
|
|||||||
@@ -151,6 +151,49 @@ function migrateIdleStatus(): void {
|
|||||||
|
|
||||||
migrateIdleStatus();
|
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<BrowserSettings & { closeOnStop?: boolean }>(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 {
|
export interface AgentRow {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -379,20 +379,36 @@ export interface BrowserSettings {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
/** Exécutable du navigateur. */
|
/** Exécutable du navigateur. */
|
||||||
command: string;
|
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[];
|
args: string[];
|
||||||
/** Délai avant l'envoi du plein écran, le temps que le lecteur démarre. */
|
/** Délai avant l'envoi du plein écran, le temps que le lecteur démarre. */
|
||||||
readyDelayMs: number;
|
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 = {
|
export const DEFAULT_BROWSER_SETTINGS: BrowserSettings = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
command: 'firefox',
|
command: 'firefox',
|
||||||
args: ['--new-window'],
|
args: [],
|
||||||
readyDelayMs: 8000,
|
readyDelayMs: 8000,
|
||||||
closeOnStop: true,
|
onStop: 'blank',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function normalizeBrowserSettings(raw: unknown): BrowserSettings {
|
export function normalizeBrowserSettings(raw: unknown): BrowserSettings {
|
||||||
@@ -412,10 +428,21 @@ export function normalizeBrowserSettings(raw: unknown): BrowserSettings {
|
|||||||
readyDelayMs: Number.isFinite(delay)
|
readyDelayMs: Number.isFinite(delay)
|
||||||
? Math.min(Math.max(Math.round(delay), 0), 120_000)
|
? Math.min(Math.max(Math.round(delay), 0), 120_000)
|
||||||
: base.readyDelayMs,
|
: 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<BrowserSettings> & { 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 = {
|
export const DEFAULT_WATCH_SETTINGS: WatchSettings = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
provider: 'stripchat',
|
provider: 'stripchat',
|
||||||
|
|||||||
@@ -359,8 +359,12 @@ export function AgentSettings({ agent, targets, onClose, onCommand, notify }: Pr
|
|||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
patchBrowser({ args: event.target.value.split(/\s+/).filter(Boolean) })
|
patchBrowser({ args: event.target.value.split(/\s+/).filter(Boolean) })
|
||||||
}
|
}
|
||||||
placeholder="--new-window"
|
placeholder="aucun"
|
||||||
/>
|
/>
|
||||||
|
<span className="muted small">
|
||||||
|
Laisse vide : le lien part vers la fenêtre déjà ouverte. <code>--new-window</code>
|
||||||
|
{' '}en créerait une nouvelle à chaque capture, et OBS perdrait sa cible.
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -380,13 +384,23 @@ export function AgentSettings({ agent, targets, onClose, onCommand, notify }: Pr
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="checkbox">
|
<label className="field">
|
||||||
<input
|
<span>À l'arrêt de l'enregistrement</span>
|
||||||
type="checkbox"
|
<select
|
||||||
checked={browser.closeOnStop}
|
value={browser.onStop}
|
||||||
onChange={(event) => patchBrowser({ closeOnStop: event.target.checked })}
|
onChange={(event) =>
|
||||||
/>
|
patchBrowser({ onStop: event.target.value as BrowserSettings['onStop'] })
|
||||||
<span>Fermer la fenêtre à l'arrêt de l'enregistrement</span>
|
}
|
||||||
|
>
|
||||||
|
<option value="blank">Charger une page vide (recommandé)</option>
|
||||||
|
<option value="keep">Ne rien faire</option>
|
||||||
|
<option value="close">Fermer la fenêtre</option>
|
||||||
|
</select>
|
||||||
|
<span className="muted small">
|
||||||
|
La page vide libère le décodeur vidéo tout en conservant la fenêtre — donc la
|
||||||
|
source OBS. Fermer la fenêtre oblige à la repointer dans OBS au prochain
|
||||||
|
enregistrement.
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<p className="muted small">
|
<p className="muted small">
|
||||||
|
|||||||
Reference in New Issue
Block a user