Feat : handle infinite throbber
This commit is contained in:
44
README.md
44
README.md
@@ -30,6 +30,9 @@ IP publique nécessaire, et obs-websocket reste sur `127.0.0.1`.
|
||||
- Reconnexion automatique de bout en bout (agent → serveur, agent → OBS, dashboard → serveur).
|
||||
- [Pause automatique pendant les shows privés](#pause-automatique-pendant-les-shows-privés)
|
||||
(Stripchat), avec reprise et rappel du plein écran au retour du flux public.
|
||||
- Le plein écran est [mesuré, pas supposé](#mesurer-plutôt-que-demander), relu pendant la
|
||||
capture et rétabli s'il se perd ; un
|
||||
[lecteur figé](#lecteur-figé--rechargement-automatique) est détecté et la page rechargée.
|
||||
- [Clôture différée sur passage hors-ligne](#passage-hors-ligne) : une coupure brève
|
||||
ne découpe pas le fichier, une vraie fin de diffusion le termine.
|
||||
- [Enregistrement automatique](#enregistrement-automatique) d'un streamer sur une VM
|
||||
@@ -253,6 +256,47 @@ occupe le lecteur. Le constat part au journal **avec les mesures qui l'ont motiv
|
||||
après coup. Après trois rappels sans effet, la surveillance se suspend jusqu'au prochain
|
||||
enregistrement plutôt que d'insister toutes les 30 s.
|
||||
|
||||
##### Lecteur figé : rechargement automatique
|
||||
|
||||
Le lecteur reste parfois bloqué sur son indicateur de chargement sans jamais repartir. Rien ne
|
||||
le signale : la plateforme donne toujours le stream public, OBS continue d'enregistrer un
|
||||
écran d'attente, et seul un rechargement de la page débloque la situation. L'agent le fait
|
||||
maintenant lui-même, après **30 s sans la moindre avancée de la lecture** (réglable par agent
|
||||
dans « Pilotage du navigateur », `0` désactive, plancher à 10 s).
|
||||
|
||||
**Le blocage se mesure à la position de lecture, pas à l'apparition d'un indicateur.**
|
||||
`video.currentTime` est une propriété standard de HTML : elle survivra à n'importe quelle
|
||||
refonte de l'habillage du lecteur, là où guetter une classe CSS de *throbber* se casserait à
|
||||
la première. Une mise en mémoire tampon de quelques secondes est normale sur un direct — c'est
|
||||
l'absence prolongée d'avancée qui distingue la panne.
|
||||
|
||||
**`paused` ne court-circuite rien, et c'est délibéré.** Vérifié en conditions réelles contre un
|
||||
Firefox piloté : selon la façon dont le flux se rompt, le lecteur reste tantôt « en lecture »
|
||||
avec son indicateur (`paused: false`, `readyState` retombé à 2), tantôt suspendu de lui-même
|
||||
(`paused: true`). Les deux se soldent par une position qui n'avance plus et appellent le même
|
||||
remède ; les distinguer aurait laissé passer la moitié des cas.
|
||||
|
||||
La reprise rejoue **exactement la séquence d'ouverture** — rechargement, attente que le lecteur
|
||||
démarre, plein écran, puis qualité — le code étant partagé avec le démarrage de capture plutôt
|
||||
que dupliqué. Pendant ce temps les deux surveillances s'abstiennent : un rechargement fait
|
||||
forcément perdre plein écran et lecture, et les laisser réagir déclencherait la reprise en
|
||||
cours d'exécution.
|
||||
|
||||
Trois garde-fous, parce que recharger pendant un enregistrement n'est pas anodin :
|
||||
|
||||
- **uniquement pendant une capture active et non suspendue** — hors capture il n'y a rien à
|
||||
sauver, et une pause de show privé arrête légitimement la lecture ;
|
||||
- **uniquement si la veille donne le stream public**, quand elle est activée. Sans ce second
|
||||
garde-fou, un opérateur ayant désactivé la mise en pause automatique verrait la page se
|
||||
recharger en boucle à chaque fin de diffusion — le flux s'arrête alors légitimement sans
|
||||
qu'OBS se mette en pause ;
|
||||
- **trois rechargements enchaînés au maximum.** Si recharger n'y change rien, la cause est
|
||||
ailleurs, et boucler ne ferait que hacher le fichier toutes les 30 s. Le compteur repart dès
|
||||
que la lecture avance à nouveau, et à chaque nouvelle capture.
|
||||
|
||||
Le constat part au journal avec les relevés qui l'ont motivé (« position 412,3 s, readyState 2 »),
|
||||
pour distinguer après coup un flux qui manque de données d'un lecteur qui s'est suspendu.
|
||||
|
||||
**Un seul envoi à la fois, et pas plus d'un toutes les 10 s.** Deux mécanismes indépendants
|
||||
peuvent réclamer ce rappel : la séquence d'ouverture (une fois, à l'arrivée sur la page) et
|
||||
la surveillance de l'agent, qui le redemande de lui-même après un show privé. Sans
|
||||
|
||||
@@ -79,6 +79,27 @@ export function describeMetrics(
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantané de la lecture, tel que le lecteur le rapporte.
|
||||
*
|
||||
* Lu sur l'élément `<video>` standard, sans rien connaître du lecteur du site :
|
||||
* ces propriétés sont celles de la spécification HTML, elles survivront à
|
||||
* n'importe quelle refonte de l'habillage.
|
||||
*/
|
||||
export interface PlaybackSample {
|
||||
/** Position de lecture, en secondes. C'est son avancée qui prouve que le flux vit. */
|
||||
currentTime: number;
|
||||
/** Lecture volontairement suspendue : ce n'est pas un blocage. */
|
||||
paused: boolean;
|
||||
/**
|
||||
* `HTMLMediaElement.readyState`, de 0 (rien) à 4 (assez de données).
|
||||
* Purement indicatif ici, pour le journal : c'est l'avancée du temps qui
|
||||
* tranche, pas cette valeur — un lecteur peut afficher `HAVE_ENOUGH_DATA` et
|
||||
* rester figé.
|
||||
*/
|
||||
readyState: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Croissance minimale de la hauteur occupée pour conclure au plein écran.
|
||||
*
|
||||
@@ -762,6 +783,33 @@ export class FirefoxController {
|
||||
return { fullscreen, metrics, baseline: this.baseline };
|
||||
}
|
||||
|
||||
/**
|
||||
* Relève l'état de lecture, ou `null` si la page ne montre aucun lecteur.
|
||||
*
|
||||
* Comme {@link checkFullscreen}, ne réveille jamais Firefox : c'est une
|
||||
* observation, elle ne doit rien provoquer.
|
||||
*/
|
||||
async samplePlayback(): Promise<PlaybackSample | null> {
|
||||
if (this.client?.isOpen !== true || !this.context) return null;
|
||||
|
||||
const raw = await this.evaluate(`JSON.stringify((() => {
|
||||
const video = document.querySelector('video');
|
||||
if (!video) return null;
|
||||
return {
|
||||
currentTime: video.currentTime,
|
||||
paused: video.paused,
|
||||
readyState: video.readyState,
|
||||
};
|
||||
})())`).catch(() => null);
|
||||
|
||||
if (typeof raw !== 'string') return null;
|
||||
try {
|
||||
return JSON.parse(raw) as PlaybackSample | null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixe le repère du mode fenêtré, une fois par page.
|
||||
*
|
||||
|
||||
@@ -33,7 +33,12 @@ 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, describeMetrics, type StreamQualityOutcome } from './firefox.ts';
|
||||
import {
|
||||
FirefoxController,
|
||||
describeMetrics,
|
||||
type PlaybackSample,
|
||||
type StreamQualityOutcome,
|
||||
} from './firefox.ts';
|
||||
import { describeFullscreen, type FullscreenOutcome } from './fullscreen.ts';
|
||||
import { sendHotkey } from './hotkey.ts';
|
||||
import { cpuUsagePercent, diskUsage, memoryUsage } from './system.ts';
|
||||
@@ -255,11 +260,36 @@ async function startCapture(params: Record<string, unknown>): Promise<unknown> {
|
||||
// Une nouvelle capture repart avec ses propres chances : un abandon décidé sur
|
||||
// la précédente ne doit pas laisser celle-ci sans surveillance.
|
||||
fullscreenMisses = 0;
|
||||
stallRecoveries = 0;
|
||||
resetPlaybackTracking();
|
||||
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`);
|
||||
await sleep(wait);
|
||||
const fullscreenResult = await preparePlayer(wait);
|
||||
|
||||
// Dernière chance d'écrire un preset en attente : une fois l'enregistrement
|
||||
// commencé, OBS refuse toute modification jusqu'à l'arrêt. Sans cet appel
|
||||
// explicite, l'écriture dépendait du hasard d'un cycle de statut (toutes les
|
||||
// 2 s) tombant avant le démarrage — perdu, le preset restait en attente
|
||||
// jusqu'au prochain arrêt, et cet enregistrement tournait sur l'ancien réglage.
|
||||
await drainPresetApply();
|
||||
await obs.execute('record.start');
|
||||
report('info', `Capture démarrée pour ${url}`, 'capture.started');
|
||||
|
||||
return { opened, fullscreen: fullscreenResult, recording: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Laisse le lecteur démarrer, puis le met en état : plein écran, puis qualité.
|
||||
*
|
||||
* Extrait de {@link startCapture} parce que la reprise après blocage
|
||||
* ({@link recoverStalledPlayer}) doit rejouer exactement la même séquence : un
|
||||
* rechargement de page repart d'un lecteur nu, tout comme une ouverture.
|
||||
* Dupliquer l'enchaînement aurait laissé les deux copies diverger.
|
||||
*/
|
||||
async function preparePlayer(waitMs: number): Promise<unknown> {
|
||||
report('info', `Attente de ${Math.round(waitMs / 1000)} s avant le plein écran`);
|
||||
await sleep(waitMs);
|
||||
|
||||
const fullscreen = watcher.snapshotSettings.fullscreen;
|
||||
let fullscreenResult: unknown = 'ignoré';
|
||||
@@ -294,17 +324,7 @@ async function startCapture(params: Record<string, unknown>): Promise<unknown> {
|
||||
// l'éloignement du curseur reste la dernière chose à toucher la page avant
|
||||
// l'enregistrement.
|
||||
await selectMaxStreamQuality();
|
||||
|
||||
// Dernière chance d'écrire un preset en attente : une fois l'enregistrement
|
||||
// commencé, OBS refuse toute modification jusqu'à l'arrêt. Sans cet appel
|
||||
// explicite, l'écriture dépendait du hasard d'un cycle de statut (toutes les
|
||||
// 2 s) tombant avant le démarrage — perdu, le preset restait en attente
|
||||
// jusqu'au prochain arrêt, et cet enregistrement tournait sur l'ancien réglage.
|
||||
await drainPresetApply();
|
||||
await obs.execute('record.start');
|
||||
report('info', `Capture démarrée pour ${url}`, 'capture.started');
|
||||
|
||||
return { opened, fullscreen: fullscreenResult, recording: true };
|
||||
return fullscreenResult;
|
||||
}
|
||||
|
||||
/** Ouvre une page par la voie correspondant au mode de pilotage. */
|
||||
@@ -497,6 +517,161 @@ let fullscreenMisses = 0;
|
||||
const MAX_FULLSCREEN_MISSES = 3;
|
||||
let fullscreenTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
/**
|
||||
* Fréquence d'échantillonnage de la lecture.
|
||||
*
|
||||
* Plus serrée que la relecture du plein écran : c'est l'écart entre deux
|
||||
* relevés qui borne la précision du seuil de blocage. À 10 s, un blocage
|
||||
* configuré à 30 s est constaté entre 30 et 40 s après coup.
|
||||
*/
|
||||
const PLAYBACK_SAMPLE_MS = 10_000;
|
||||
let playbackTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
/** Dernière position de lecture observée, et l'instant où elle a bougé. */
|
||||
let lastPlaybackTime: number | null = null;
|
||||
let lastProgressAt = 0;
|
||||
/**
|
||||
* Rechargements enchaînés sans que la lecture reparte. Au-delà, on cesse : si
|
||||
* recharger n'y fait rien, le problème est ailleurs, et boucler ne ferait que
|
||||
* hacher l'enregistrement toutes les 30 s. Le compteur repart dès que la
|
||||
* lecture avance à nouveau, et à chaque nouvelle capture.
|
||||
*/
|
||||
let stallRecoveries = 0;
|
||||
const MAX_STALL_RECOVERIES = 3;
|
||||
/**
|
||||
* Une reprise est en cours : le lecteur est délibérément en train d'être
|
||||
* rechargé. Les deux surveillances s'abstiennent le temps que ça se fasse —
|
||||
* sans quoi le rechargement, qui fait forcément perdre plein écran et lecture,
|
||||
* déclencherait la reprise qu'il est justement en train d'exécuter.
|
||||
*/
|
||||
let recovering = false;
|
||||
|
||||
/** Repart de zéro : plus rien à comparer sur ce lecteur. */
|
||||
function resetPlaybackTracking(): void {
|
||||
lastPlaybackTime = null;
|
||||
lastProgressAt = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Détecte un lecteur figé et le remet en route.
|
||||
*
|
||||
* Le lecteur reste parfois coincé sur son indicateur de chargement sans jamais
|
||||
* repartir. Rien ne le signale : la plateforme donne toujours le stream public,
|
||||
* OBS continue d'enregistrer, et seul un rechargement de la page débloque la
|
||||
* situation. C'était jusqu'ici une intervention manuelle.
|
||||
*
|
||||
* Le blocage se mesure à l'absence d'avancée de la position de lecture, et non
|
||||
* à l'apparition d'un indicateur : c'est le symptôme réel, il ne dépend
|
||||
* d'aucune classe CSS, et il survivra à n'importe quelle refonte du lecteur.
|
||||
*/
|
||||
async function watchPlayback(): Promise<void> {
|
||||
const bidi = driver();
|
||||
const timeout = browserSettings.stallTimeoutMs;
|
||||
if (!bidi || timeout <= 0 || recovering) return;
|
||||
if (stallRecoveries >= MAX_STALL_RECOVERIES) return;
|
||||
|
||||
// Même garde que la surveillance du plein écran : hors capture il n'y a rien
|
||||
// à sauver, et une pause de show privé arrête légitimement la lecture — la
|
||||
// prendre pour un blocage rechargerait la page à chaque passage en privé.
|
||||
const record = await obs.recordState().catch(() => null);
|
||||
if (!record?.active || record.paused) {
|
||||
resetPlaybackTracking();
|
||||
return;
|
||||
}
|
||||
|
||||
// Second garde-fou, indispensable quand la mise en pause automatique est
|
||||
// désactivée : le flux s'arrête alors légitimement (show privé, fin de
|
||||
// diffusion) sans qu'OBS se mette en pause, et un lecteur à l'arrêt n'a rien
|
||||
// d'anormal. Recharger n'y changerait rien, et la boucle hacherait le fichier.
|
||||
if (watcher.snapshotSettings.enabled && watcher.snapshot.state !== 'public') {
|
||||
resetPlaybackTracking();
|
||||
return;
|
||||
}
|
||||
|
||||
const sample = await bidi.samplePlayback();
|
||||
if (!sample) {
|
||||
resetPlaybackTracking();
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
// Une position qui recule ou qui saute désigne un lecteur remplacé ou un
|
||||
// repositionnement du direct : c'est du mouvement, donc le contraire d'un
|
||||
// blocage.
|
||||
if (lastPlaybackTime === null || sample.currentTime !== lastPlaybackTime) {
|
||||
lastPlaybackTime = sample.currentTime;
|
||||
lastProgressAt = now;
|
||||
// La lecture repart : les rechargements précédents ont porté leurs fruits,
|
||||
// ou le blocage s'est résorbé seul.
|
||||
stallRecoveries = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// `paused` ne court-circuite rien, et c'est délibéré. Vérifié en conditions
|
||||
// réelles : selon la façon dont le flux se rompt, le lecteur reste tantôt
|
||||
// « en lecture » avec un indicateur de chargement, tantôt suspendu de
|
||||
// lui-même. Les deux se soldent par une position qui n'avance plus, et les
|
||||
// deux appellent le même remède — le distinguer ne ferait que laisser passer
|
||||
// la moitié des cas.
|
||||
const stalledFor = now - lastProgressAt;
|
||||
if (stalledFor < timeout) return;
|
||||
|
||||
await recoverStalledPlayer(stalledFor, sample);
|
||||
}
|
||||
|
||||
/** Recharge la page, puis remet le lecteur en état — plein écran et qualité. */
|
||||
async function recoverStalledPlayer(stalledFor: number, sample: PlaybackSample): Promise<void> {
|
||||
const bidi = driver();
|
||||
const url = bidi?.state.url;
|
||||
if (!bidi || !url || url === 'about:blank') {
|
||||
resetPlaybackTracking();
|
||||
return;
|
||||
}
|
||||
|
||||
recovering = true;
|
||||
stallRecoveries += 1;
|
||||
// Les relevés partent avec le constat : c'est ce qui permet de distinguer
|
||||
// après coup un flux qui manque de données d'un lecteur qui s'est suspendu.
|
||||
report(
|
||||
'warn',
|
||||
`Lecteur figé depuis ${Math.round(stalledFor / 1000)} s ` +
|
||||
`(position ${sample.currentTime.toFixed(1)} s, readyState ${sample.readyState}` +
|
||||
`${sample.paused ? ', suspendu' : ''}) — rechargement de la page ` +
|
||||
`(${stallRecoveries}/${MAX_STALL_RECOVERIES})`,
|
||||
'browser.opened',
|
||||
);
|
||||
|
||||
try {
|
||||
await bidi.navigate(url);
|
||||
// Même séquence qu'à l'ouverture d'une capture : la page revient nue, il
|
||||
// faut lui rendre le plein écran et la qualité. Le compteur d'échecs du
|
||||
// plein écran repart aussi — c'est une page neuve, pas la précédente.
|
||||
fullscreenMisses = 0;
|
||||
await preparePlayer(browserSettings.readyDelayMs);
|
||||
report('info', 'Lecteur rechargé et remis en état', 'browser.opened');
|
||||
if (stallRecoveries >= MAX_STALL_RECOVERIES) {
|
||||
report(
|
||||
'warn',
|
||||
`Le lecteur s'est figé ${MAX_STALL_RECOVERIES} fois de suite : surveillance suspendue ` +
|
||||
"jusqu'au prochain enregistrement. Recharger n'y change rien, la cause est ailleurs.",
|
||||
'command.failed',
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
report(
|
||||
'error',
|
||||
`Reprise du lecteur figé impossible : ${err instanceof Error ? err.message : String(err)}`,
|
||||
'command.failed',
|
||||
);
|
||||
} finally {
|
||||
// Après coup, pas avant : le temps passé à recharger ne doit pas compter
|
||||
// comme une absence d'avancée, sans quoi la reprise suivante partirait
|
||||
// immédiatement.
|
||||
resetPlaybackTracking();
|
||||
recovering = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que le lecteur occupe toujours la fenêtre, et le rétablit sinon.
|
||||
*
|
||||
@@ -511,6 +686,9 @@ async function watchFullscreen(): Promise<void> {
|
||||
const bidi = driver();
|
||||
const settings = watcher.snapshotSettings.fullscreen;
|
||||
if (!bidi || !settings.enabled || fullscreenMisses >= MAX_FULLSCREEN_MISSES) return;
|
||||
// Un rechargement en cours fait forcément perdre le plein écran : le constater
|
||||
// ici enverrait une touche au milieu de la reprise, qui le rétablit déjà.
|
||||
if (recovering) return;
|
||||
|
||||
// Uniquement pendant une capture réellement en cours. Hors capture il n'y a
|
||||
// rien à cadrer ; en pause (show privé), l'overlay du site occupe le lecteur
|
||||
@@ -719,6 +897,13 @@ function startStatusLoop(): void {
|
||||
});
|
||||
}, FULLSCREEN_WATCH_MS);
|
||||
fullscreenTimer.unref?.();
|
||||
|
||||
playbackTimer = setInterval(() => {
|
||||
void watchPlayback().catch((err: unknown) => {
|
||||
console.error('Surveillance de la lecture en échec :', err);
|
||||
});
|
||||
}, PLAYBACK_SAMPLE_MS);
|
||||
playbackTimer.unref?.();
|
||||
}
|
||||
|
||||
function stopStatusLoop(): void {
|
||||
@@ -726,6 +911,8 @@ function stopStatusLoop(): void {
|
||||
statusTimer = null;
|
||||
if (fullscreenTimer) clearInterval(fullscreenTimer);
|
||||
fullscreenTimer = null;
|
||||
if (playbackTimer) clearInterval(playbackTimer);
|
||||
playbackTimer = null;
|
||||
}
|
||||
|
||||
// --- Cycle de vie ------------------------------------------------------------
|
||||
|
||||
@@ -458,6 +458,20 @@ export interface BrowserSettings {
|
||||
* « Firefox est déjà ouvert ».
|
||||
*/
|
||||
profileDir: string;
|
||||
/**
|
||||
* Durée de blocage du lecteur au-delà de laquelle l'agent recharge la page.
|
||||
* `0` désactive la reprise.
|
||||
*
|
||||
* Le lecteur se fige parfois sur son indicateur de chargement sans jamais
|
||||
* repartir, le flux ne reprenant qu'après un rechargement manuel. Rien dans le
|
||||
* statut du stream ne le signale : la plateforme le donne toujours public, et
|
||||
* OBS continue d'enregistrer un écran d'attente.
|
||||
*
|
||||
* Le seuil se mesure au temps sans la moindre avancée de la lecture, pas à
|
||||
* l'apparition d'un indicateur : c'est le blocage réel qui compte, et une
|
||||
* mise en mémoire tampon de quelques secondes est normale sur un direct.
|
||||
*/
|
||||
stallTimeoutMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -496,6 +510,7 @@ export const DEFAULT_BROWSER_SETTINGS: BrowserSettings = {
|
||||
onStop: 'blank',
|
||||
remotePort: 9222,
|
||||
profileDir: '',
|
||||
stallTimeoutMs: 30_000,
|
||||
};
|
||||
|
||||
export function normalizeBrowserSettings(raw: unknown): BrowserSettings {
|
||||
@@ -503,6 +518,7 @@ export function normalizeBrowserSettings(raw: unknown): BrowserSettings {
|
||||
const base = DEFAULT_BROWSER_SETTINGS;
|
||||
const delay = Number(input.readyDelayMs);
|
||||
const port = Number(input.remotePort);
|
||||
const stall = Number(input.stallTimeoutMs);
|
||||
|
||||
return {
|
||||
enabled: input.enabled === true,
|
||||
@@ -526,6 +542,14 @@ export function normalizeBrowserSettings(raw: unknown): BrowserSettings {
|
||||
? Math.min(Math.max(Math.round(port), 1024), 65_535)
|
||||
: base.remotePort,
|
||||
profileDir: typeof input.profileDir === 'string' ? input.profileDir.trim() : base.profileDir,
|
||||
// Plancher à 10 s hors désactivation : en deçà, une mise en mémoire tampon
|
||||
// ordinaire suffirait à déclencher un rechargement, et le remède serait pire
|
||||
// que le mal. La borne haute laisse la place à un flux très instable.
|
||||
stallTimeoutMs: Number.isFinite(stall)
|
||||
? stall <= 0
|
||||
? 0
|
||||
: Math.min(Math.max(Math.round(stall), 10_000), 600_000)
|
||||
: base.stallTimeoutMs,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -511,6 +511,27 @@ export function AgentSettings({ agent, targets, onClose, onCommand, notify }: Pr
|
||||
Le temps que la page charge et que le lecteur démarre.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{browser.mode === 'bidi' && (
|
||||
<label className="field grow">
|
||||
<span>Recharger si le lecteur se fige (s)</span>
|
||||
<input
|
||||
value={String(Math.round(browser.stallTimeoutMs / 1000))}
|
||||
onChange={(event) =>
|
||||
patchBrowser({ stallTimeoutMs: (Number(event.target.value) || 0) * 1000 })
|
||||
}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
<span className="muted small">
|
||||
Le lecteur reste parfois bloqué sur son indicateur de chargement sans jamais
|
||||
repartir : rien ne le signale, le stream est toujours annoncé public et OBS
|
||||
continue d'enregistrer un écran d'attente. Passé ce délai <em>sans la moindre
|
||||
avancée de la lecture</em>, l'agent recharge la page puis remet le plein écran
|
||||
et la qualité. <strong>0 désactive</strong> ; en dessous de 10 s, une simple
|
||||
mise en mémoire tampon suffirait à déclencher un rechargement.
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<label className="field">
|
||||
|
||||
Reference in New Issue
Block a user