mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-21 22:18: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)
|
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
|
// Première connexion hors du générateur : elle seule décide du code
|
||||||
// de réponse.
|
// de réponse.
|
||||||
let client = state.http.clone();
|
let client = state.http.clone();
|
||||||
let Some(first) = open_live_upstream(&client, &target).await else {
|
let Some(first) = open_live_upstream(&client, &target).await else {
|
||||||
|
println!("[Live Proxy] {stream_id} : fournisseur injoignable");
|
||||||
return http::text(502, "Upstream connection failed");
|
return http::text(502, "Upstream connection failed");
|
||||||
};
|
};
|
||||||
let status = first.status().as_u16();
|
let status = first.status().as_u16();
|
||||||
if status != 200 {
|
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");
|
return http::text(status, "Upstream refused");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -954,8 +964,9 @@ impl Streaming {
|
|||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
match open_live_upstream(&client, &target).await {
|
match open_live_upstream(&client, &target).await {
|
||||||
Some(r) if r.status().as_u16() == 200 => next = Some(r),
|
Some(r) if r.status().as_u16() == 200 => next = Some(r),
|
||||||
_ => println!(
|
other => println!(
|
||||||
"[Live Proxy] {id} : reconnexion {attempt}/{LIVE_PROXY_MAX_RETRIES} échouée"
|
"[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,
|
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
|
/// 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.
|
/// 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(
|
pub fn decide_post_exit_action(
|
||||||
now: DateTime<Utc>,
|
now: DateTime<Utc>,
|
||||||
end: DateTime<Utc>,
|
end: DateTime<Utc>,
|
||||||
exit_code: i32,
|
exit_code: i32,
|
||||||
consecutive_failures: u32,
|
|
||||||
has_file: bool,
|
has_file: bool,
|
||||||
) -> PostExitAction {
|
) -> PostExitAction {
|
||||||
let remaining = end - now;
|
let remaining = end - now;
|
||||||
if remaining > Duration::seconds(60) && consecutive_failures + 1 < MAX_FFMPEG_ATTEMPTS {
|
if remaining > Duration::seconds(60) {
|
||||||
return PostExitAction::Retry;
|
return PostExitAction::Retry;
|
||||||
}
|
}
|
||||||
// `255` est le code renvoyé par FFmpeg quand on l'interrompt proprement.
|
// `255` est le code renvoyé par FFmpeg quand on l'interrompt proprement.
|
||||||
@@ -189,27 +189,29 @@ mod tests {
|
|||||||
let n = now();
|
let n = now();
|
||||||
let m40 = n + Duration::minutes(40);
|
let m40 = n + Duration::minutes(40);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decide_post_exit_action(n, m40, 1, 0, true),
|
decide_post_exit_action(n, m40, 1, true),
|
||||||
PostExitAction::Retry
|
PostExitAction::Retry
|
||||||
);
|
);
|
||||||
assert_eq!(
|
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
|
PostExitAction::Retry
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decide_post_exit_action(n, m40, 1, MAX_FFMPEG_ATTEMPTS - 1, true),
|
decide_post_exit_action(n, n, 0, true),
|
||||||
PostExitAction::Complete
|
PostExitAction::Complete
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decide_post_exit_action(n, n, 0, 0, true),
|
decide_post_exit_action(n, n, 1, true),
|
||||||
PostExitAction::Complete
|
PostExitAction::Complete
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decide_post_exit_action(n, n, 1, 0, true),
|
decide_post_exit_action(n, n, 1, false),
|
||||||
PostExitAction::Complete
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
decide_post_exit_action(n, n, 1, 0, false),
|
|
||||||
PostExitAction::Fail
|
PostExitAction::Fail
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ use crate::dart_time::{DartDateTime, display_local, now_display};
|
|||||||
use crate::db::AppDatabase;
|
use crate::db::AppDatabase;
|
||||||
use crate::models::Recording;
|
use crate::models::Recording;
|
||||||
use crate::services::recording_decision::{
|
use crate::services::recording_decision::{
|
||||||
MAX_FFMPEG_ATTEMPTS, OrphanAction, PostExitAction, RecordingAction, capture_duration,
|
OrphanAction, PostExitAction, RecordingAction, capture_duration, decide_orphan_action,
|
||||||
decide_orphan_action, decide_post_exit_action, decide_recording_action, ffmpeg_retry_delay,
|
decide_post_exit_action, decide_recording_action, ffmpeg_retry_delay,
|
||||||
};
|
};
|
||||||
use crate::util::b64;
|
use crate::util::b64;
|
||||||
use crate::util::log_redactor::redact_url;
|
use crate::util::log_redactor::redact_url;
|
||||||
@@ -828,7 +828,7 @@ impl RecordingScheduler {
|
|||||||
return;
|
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 (lasted, has_data) = {
|
||||||
let state = active.state.lock().unwrap();
|
let state = active.state.lock().unwrap();
|
||||||
(
|
(
|
||||||
@@ -836,21 +836,11 @@ impl RecordingScheduler {
|
|||||||
active_has_data(&active.file_path, &state.extra_parts),
|
active_has_data(&active.file_path, &state.extra_parts),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let consecutive = {
|
if lasted > Duration::from_secs(120) && has_data {
|
||||||
let mut state = active.state.lock().unwrap();
|
active.state.lock().unwrap().consecutive_failures = 0;
|
||||||
if lasted > Duration::from_secs(120) && has_data {
|
}
|
||||||
state.consecutive_failures = 0;
|
|
||||||
}
|
|
||||||
state.consecutive_failures
|
|
||||||
};
|
|
||||||
|
|
||||||
match decide_post_exit_action(
|
match decide_post_exit_action(Utc::now(), recording.end_utc(), exit_code, has_data) {
|
||||||
Utc::now(),
|
|
||||||
recording.end_utc(),
|
|
||||||
exit_code,
|
|
||||||
consecutive,
|
|
||||||
has_data,
|
|
||||||
) {
|
|
||||||
PostExitAction::Retry => {
|
PostExitAction::Retry => {
|
||||||
let failures = {
|
let failures = {
|
||||||
let mut state = active.state.lock().unwrap();
|
let mut state = active.state.lock().unwrap();
|
||||||
@@ -860,10 +850,9 @@ impl RecordingScheduler {
|
|||||||
};
|
};
|
||||||
let delay = ffmpeg_retry_delay(failures);
|
let delay = ffmpeg_retry_delay(failures);
|
||||||
let message = format!(
|
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(),
|
lasted.as_secs(),
|
||||||
delay.as_secs(),
|
delay.as_secs(),
|
||||||
MAX_FFMPEG_ATTEMPTS - 1
|
|
||||||
);
|
);
|
||||||
println!("[RecordingScheduler] {}: {message}", recording.title);
|
println!("[RecordingScheduler] {}: {message}", recording.title);
|
||||||
active.log(&format!("\n[{}] {message}", now_display()));
|
active.log(&format!("\n[{}] {message}", now_display()));
|
||||||
|
|||||||
Reference in new issue
Block a user