mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-21 14:08:04 +02:00
Enregistrements : relancer jusqu'à la fin du match, code amont journalisé
Un match NBA a échoué après 30 refus du fournisseur en 13 min, alors qu'il restait 3 h 20 de diffusion. Le plafond MAX_FFMPEG_ATTEMPTS condamnait tout l'enregistrement sur un refus temporaire : on relance désormais tant que la fenêtre est ouverte, au rythme de ffmpeg_retry_delay (30 s au plus). - direct_proxy journalise le code HTTP du panneau : FFmpeg n'affiche que « 4XX », le code exact (ici 407) n'était visible nulle part. - direct_proxy libère les connexions de lecture orphelines du compte avant d'ouvrir le flux, comme turbo.ts et HLS. Un flux encore regardé n'est jamais coupé. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6368fd6ee2
commit
0f81df1c5e
3 files changed
+39
-37
No files matched your search
@@ -903,14 +903,24 @@ impl Streaming {
|
||||
redact_url(&target)
|
||||
);
|
||||
|
||||
// Compte à une connexion : un FFmpeg de lecture orphelin (spectateur
|
||||
// parti, session pas encore expirée) garde le slot et le panneau
|
||||
// refuse l'enregistrement. On coupe les orphelins comme les autres
|
||||
// routes ; un flux encore regardé n'est jamais coupé.
|
||||
self.claim_upstream(&playlist, &format!("direct_{stream_id}"))
|
||||
.await;
|
||||
|
||||
// Première connexion hors du générateur : elle seule décide du code
|
||||
// de réponse.
|
||||
let client = state.http.clone();
|
||||
let Some(first) = open_live_upstream(&client, &target).await else {
|
||||
println!("[Live Proxy] {stream_id} : fournisseur injoignable");
|
||||
return http::text(502, "Upstream connection failed");
|
||||
};
|
||||
let status = first.status().as_u16();
|
||||
if status != 200 {
|
||||
// FFmpeg ne journalise que « 4XX » : le code exact n'est qu'ici.
|
||||
println!("[Live Proxy] {stream_id} : refusé par le fournisseur (HTTP {status})");
|
||||
return http::text(status, "Upstream refused");
|
||||
}
|
||||
|
||||
@@ -954,8 +964,9 @@ impl Streaming {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
match open_live_upstream(&client, &target).await {
|
||||
Some(r) if r.status().as_u16() == 200 => next = Some(r),
|
||||
_ => println!(
|
||||
"[Live Proxy] {id} : reconnexion {attempt}/{LIVE_PROXY_MAX_RETRIES} échouée"
|
||||
other => println!(
|
||||
"[Live Proxy] {id} : reconnexion {attempt}/{LIVE_PROXY_MAX_RETRIES} échouée ({})",
|
||||
other.map_or("injoignable".to_string(), |r| format!("HTTP {}", r.status().as_u16()))
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,22 +74,22 @@ pub enum PostExitAction {
|
||||
Fail,
|
||||
}
|
||||
|
||||
/// Échecs FFmpeg consécutifs tolérés avant d'abandonner un enregistrement
|
||||
/// dont la fenêtre est encore ouverte. Le compteur est remis à zéro dès
|
||||
/// qu'un lancement a duré assez longtemps pour être considéré sain.
|
||||
pub const MAX_FFMPEG_ATTEMPTS: u32 = 30;
|
||||
|
||||
/// FFmpeg rend la main dès que la source se tarit : sur un flux IPTV, une
|
||||
/// coupure amont à mi-parcours ne doit pas condamner l'heure restante.
|
||||
///
|
||||
/// Aucun plafond d'échecs : tant que la fenêtre est ouverte, on relance
|
||||
/// (au rythme de [ffmpeg_retry_delay]). Sur un compte à une connexion, le
|
||||
/// panneau refuse le flux tant qu'un autre lecteur l'occupe ; abandonner
|
||||
/// au bout de quelques minutes faisait perdre tout le match (constaté en
|
||||
/// prod : 30 refus en 13 min, 3 h 20 de match perdues).
|
||||
pub fn decide_post_exit_action(
|
||||
now: DateTime<Utc>,
|
||||
end: DateTime<Utc>,
|
||||
exit_code: i32,
|
||||
consecutive_failures: u32,
|
||||
has_file: bool,
|
||||
) -> PostExitAction {
|
||||
let remaining = end - now;
|
||||
if remaining > Duration::seconds(60) && consecutive_failures + 1 < MAX_FFMPEG_ATTEMPTS {
|
||||
if remaining > Duration::seconds(60) {
|
||||
return PostExitAction::Retry;
|
||||
}
|
||||
// `255` est le code renvoyé par FFmpeg quand on l'interrompt proprement.
|
||||
@@ -189,27 +189,29 @@ mod tests {
|
||||
let n = now();
|
||||
let m40 = n + Duration::minutes(40);
|
||||
assert_eq!(
|
||||
decide_post_exit_action(n, m40, 1, 0, true),
|
||||
decide_post_exit_action(n, m40, 1, true),
|
||||
PostExitAction::Retry
|
||||
);
|
||||
assert_eq!(
|
||||
decide_post_exit_action(n, m40, 0, 2, true),
|
||||
decide_post_exit_action(n, m40, 0, true),
|
||||
PostExitAction::Retry
|
||||
);
|
||||
// Compte IPTV à une connexion occupé pendant 13 min : les refus en
|
||||
// série ne doivent pas faire abandonner les heures restantes.
|
||||
assert_eq!(
|
||||
decide_post_exit_action(n, m40, 8, false),
|
||||
PostExitAction::Retry
|
||||
);
|
||||
assert_eq!(
|
||||
decide_post_exit_action(n, m40, 1, MAX_FFMPEG_ATTEMPTS - 1, true),
|
||||
decide_post_exit_action(n, n, 0, true),
|
||||
PostExitAction::Complete
|
||||
);
|
||||
assert_eq!(
|
||||
decide_post_exit_action(n, n, 0, 0, true),
|
||||
decide_post_exit_action(n, n, 1, true),
|
||||
PostExitAction::Complete
|
||||
);
|
||||
assert_eq!(
|
||||
decide_post_exit_action(n, n, 1, 0, true),
|
||||
PostExitAction::Complete
|
||||
);
|
||||
assert_eq!(
|
||||
decide_post_exit_action(n, n, 1, 0, false),
|
||||
decide_post_exit_action(n, n, 1, false),
|
||||
PostExitAction::Fail
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ use crate::dart_time::{DartDateTime, display_local, now_display};
|
||||
use crate::db::AppDatabase;
|
||||
use crate::models::Recording;
|
||||
use crate::services::recording_decision::{
|
||||
MAX_FFMPEG_ATTEMPTS, OrphanAction, PostExitAction, RecordingAction, capture_duration,
|
||||
decide_orphan_action, decide_post_exit_action, decide_recording_action, ffmpeg_retry_delay,
|
||||
OrphanAction, PostExitAction, RecordingAction, capture_duration, decide_orphan_action,
|
||||
decide_post_exit_action, decide_recording_action, ffmpeg_retry_delay,
|
||||
};
|
||||
use crate::util::b64;
|
||||
use crate::util::log_redactor::redact_url;
|
||||
@@ -828,7 +828,7 @@ impl RecordingScheduler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Une capture qui a tenu longtemps repart d'un quota de relances neuf.
|
||||
// Une capture qui a tenu longtemps repart du délai de relance minimal.
|
||||
let (lasted, has_data) = {
|
||||
let state = active.state.lock().unwrap();
|
||||
(
|
||||
@@ -836,21 +836,11 @@ impl RecordingScheduler {
|
||||
active_has_data(&active.file_path, &state.extra_parts),
|
||||
)
|
||||
};
|
||||
let consecutive = {
|
||||
let mut state = active.state.lock().unwrap();
|
||||
if lasted > Duration::from_secs(120) && has_data {
|
||||
state.consecutive_failures = 0;
|
||||
}
|
||||
state.consecutive_failures
|
||||
};
|
||||
if lasted > Duration::from_secs(120) && has_data {
|
||||
active.state.lock().unwrap().consecutive_failures = 0;
|
||||
}
|
||||
|
||||
match decide_post_exit_action(
|
||||
Utc::now(),
|
||||
recording.end_utc(),
|
||||
exit_code,
|
||||
consecutive,
|
||||
has_data,
|
||||
) {
|
||||
match decide_post_exit_action(Utc::now(), recording.end_utc(), exit_code, has_data) {
|
||||
PostExitAction::Retry => {
|
||||
let failures = {
|
||||
let mut state = active.state.lock().unwrap();
|
||||
@@ -860,10 +850,9 @@ impl RecordingScheduler {
|
||||
};
|
||||
let delay = ffmpeg_retry_delay(failures);
|
||||
let message = format!(
|
||||
"FFmpeg s'est arrêté (code {exit_code}) après {}s, avant la fin prévue — relance dans {}s (tentative {failures}/{})",
|
||||
"FFmpeg s'est arrêté (code {exit_code}) après {}s, avant la fin prévue — relance dans {}s (échec consécutif n°{failures})",
|
||||
lasted.as_secs(),
|
||||
delay.as_secs(),
|
||||
MAX_FFMPEG_ATTEMPTS - 1
|
||||
);
|
||||
println!("[RecordingScheduler] {}: {message}", recording.title);
|
||||
active.log(&format!("\n[{}] {message}", now_display()));
|
||||
|
||||
Reference in new issue
Block a user