mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
fix(enregistrements): un enregistrement mené à terme n'est plus marqué « échoué »
Chaque enregistrement arrivé au bout de sa fenêtre était marqué « Échoué / Interruption inattendue du serveur » alors que le fichier était bien sur le disque : le tick lisait la base AVANT d'arrêter les captures terminées, si bien que l'instantané annonçait encore « recording » pour un enregistrement déjà retiré de la table des processus actifs — la détection d'orphelin le requalifiait aussitôt en échec. La base est désormais lue après les arrêts, et la requalification revérifie le statut courant. Deux autres façons de perdre un enregistrement sont corrigées au passage : - FFmpeg livre encore des morceaux de stderr après la résolution de `exitCode` ; écrire sur l'`IOSink` déjà fermé levait une `StateError` depuis un callback de stream, donc une erreur asynchrone non rattrapée qui tue l'isolate — et avec lui le serveur et toutes les captures en cours. Le log passe par un écrivain tolérant et n'est fermé qu'une fois stdout et stderr drainés. - Une coupure amont terminait la capture définitivement. FFmpeg reçoit maintenant les options de reconnexion (comme le proxy live), et le planificateur relance la capture sur la fin de fenêtre quand le process sort trop tôt (backoff 3→30 s, quota remis à zéro après une capture saine). Les parties issues des relances sont recollées dans le fichier principal via le demuxer `concat`, la lecture reste donc un seul fichier. Également : - reprise des captures interrompues par un redémarrage du conteneur tant que la fenêtre est ouverte, au lieu d'un échec sec ; fichier partiel conservé (statut « terminé ») quand la fenêtre est passée - `-t` calculé sur le temps restant jusqu'à la fin programmée : un démarrage tardif ne rogne plus la fin du programme - `-hide_banner -nostats` : le log d'enregistrement redevient lisible (et ne pèse plus des mégaoctets, il est relu en entier par l'API) - `RECORDINGS_DIR` et `FFMPEG_PATH` surchargeables, et le motif d'erreur est effacé au (re)démarrage d'une capture Vérifié avec un faux ffmpeg sur un planificateur réel : avant correctif, un enregistrement mené jusqu'à la fin de fenêtre ressort « failed / Interruption inattendue du serveur » ; après, « completed » — de même que la reprise après interruption, la relance après coupure et la fusion des parties. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5xWYTbtZwJuYB3E51K243
This commit is contained in:
5 files changed
+756
-113
No files matched your search
@@ -70,4 +70,176 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('decideOrphanAction', () {
|
||||
test('resumes while the window is still open', () {
|
||||
expect(
|
||||
decideOrphanAction(
|
||||
now: now,
|
||||
endTime: now.add(const Duration(minutes: 30)),
|
||||
hasFile: true,
|
||||
),
|
||||
OrphanAction.resume,
|
||||
);
|
||||
});
|
||||
|
||||
test('resumes even without a file (crash before the first byte)', () {
|
||||
expect(
|
||||
decideOrphanAction(
|
||||
now: now,
|
||||
endTime: now.add(const Duration(minutes: 30)),
|
||||
hasFile: false,
|
||||
),
|
||||
OrphanAction.resume,
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps a partial file when the window has closed', () {
|
||||
expect(
|
||||
decideOrphanAction(
|
||||
now: now,
|
||||
endTime: now.subtract(const Duration(minutes: 5)),
|
||||
hasFile: true,
|
||||
),
|
||||
OrphanAction.finish,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails when the window has closed with nothing captured', () {
|
||||
expect(
|
||||
decideOrphanAction(
|
||||
now: now,
|
||||
endTime: now.subtract(const Duration(minutes: 5)),
|
||||
hasFile: false,
|
||||
),
|
||||
OrphanAction.fail,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not resume for the last seconds of the window', () {
|
||||
expect(
|
||||
decideOrphanAction(
|
||||
now: now,
|
||||
endTime: now.add(const Duration(seconds: 20)),
|
||||
hasFile: true,
|
||||
),
|
||||
OrphanAction.finish,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('decidePostExitAction', () {
|
||||
test('retries when ffmpeg dies mid-window', () {
|
||||
expect(
|
||||
decidePostExitAction(
|
||||
now: now,
|
||||
endTime: now.add(const Duration(minutes: 40)),
|
||||
exitCode: 1,
|
||||
consecutiveFailures: 0,
|
||||
hasFile: true,
|
||||
),
|
||||
PostExitAction.retry,
|
||||
);
|
||||
});
|
||||
|
||||
test('retries on a clean exit too (upstream ended early)', () {
|
||||
expect(
|
||||
decidePostExitAction(
|
||||
now: now,
|
||||
endTime: now.add(const Duration(minutes: 40)),
|
||||
exitCode: 0,
|
||||
consecutiveFailures: 2,
|
||||
hasFile: true,
|
||||
),
|
||||
PostExitAction.retry,
|
||||
);
|
||||
});
|
||||
|
||||
test('stops retrying once the attempt budget is spent', () {
|
||||
expect(
|
||||
decidePostExitAction(
|
||||
now: now,
|
||||
endTime: now.add(const Duration(minutes: 40)),
|
||||
exitCode: 1,
|
||||
consecutiveFailures: maxFfmpegAttempts - 1,
|
||||
hasFile: true,
|
||||
),
|
||||
PostExitAction.complete,
|
||||
);
|
||||
});
|
||||
|
||||
test('completes at the end of the window', () {
|
||||
expect(
|
||||
decidePostExitAction(
|
||||
now: now,
|
||||
endTime: now,
|
||||
exitCode: 0,
|
||||
consecutiveFailures: 0,
|
||||
hasFile: true,
|
||||
),
|
||||
PostExitAction.complete,
|
||||
);
|
||||
});
|
||||
|
||||
test('completes when ffmpeg errored but a file was captured', () {
|
||||
expect(
|
||||
decidePostExitAction(
|
||||
now: now,
|
||||
endTime: now,
|
||||
exitCode: 1,
|
||||
consecutiveFailures: 0,
|
||||
hasFile: true,
|
||||
),
|
||||
PostExitAction.complete,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails when nothing was captured and the window is over', () {
|
||||
expect(
|
||||
decidePostExitAction(
|
||||
now: now,
|
||||
endTime: now,
|
||||
exitCode: 1,
|
||||
consecutiveFailures: 0,
|
||||
hasFile: false,
|
||||
),
|
||||
PostExitAction.fail,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('captureDuration', () {
|
||||
test('uses the time left until the scheduled end, not the planned length',
|
||||
() {
|
||||
// Démarrage avec 2 minutes de retard sur une fenêtre d'une heure.
|
||||
expect(
|
||||
captureDuration(
|
||||
now: now,
|
||||
endTime: now.add(const Duration(minutes: 58)),
|
||||
),
|
||||
const Duration(minutes: 58),
|
||||
);
|
||||
});
|
||||
|
||||
test('never asks ffmpeg for a zero or negative duration', () {
|
||||
expect(
|
||||
captureDuration(
|
||||
now: now,
|
||||
endTime: now.subtract(const Duration(minutes: 5)),
|
||||
),
|
||||
const Duration(seconds: 30),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('ffmpegRetryDelay', () {
|
||||
test('backs off on repeated failures and caps at 30s', () {
|
||||
expect(ffmpegRetryDelay(1), const Duration(seconds: 3));
|
||||
expect(ffmpegRetryDelay(2), const Duration(seconds: 6));
|
||||
expect(ffmpegRetryDelay(3), const Duration(seconds: 12));
|
||||
expect(ffmpegRetryDelay(4), const Duration(seconds: 24));
|
||||
expect(ffmpegRetryDelay(5), const Duration(seconds: 30));
|
||||
expect(ffmpegRetryDelay(20), const Duration(seconds: 30));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user