fix(media): smooth transfer speed and eta

This commit is contained in:
NimBold
2026-07-09 03:40:14 +03:30
parent 1baadf6ea6
commit 51258b25bc
+96 -35
View File
@@ -3,7 +3,7 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use regex::Regex;
use serde::Serialize;
use std::collections::HashMap;
use std::collections::{HashMap, VecDeque};
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::sync::OnceLock;
@@ -885,33 +885,59 @@ fn parse_human_size(s: &str) -> Option<f64> {
}
}
const MEDIA_PROGRESS_EMIT_INTERVAL: Duration = Duration::from_millis(1000);
const MEDIA_SPEED_SAMPLE_WINDOW: Duration = Duration::from_secs(8);
#[derive(Debug, Default)]
struct MediaSpeedSampler {
samples: VecDeque<(Instant, f64)>,
}
impl MediaSpeedSampler {
fn reset(&mut self) {
self.samples.clear();
}
fn sample(&mut self, downloaded_bytes: f64, now: Instant) -> Option<f64> {
if self
.samples
.back()
.is_some_and(|(_, last_bytes)| downloaded_bytes < *last_bytes)
{
self.reset();
}
self.samples.push_back((now, downloaded_bytes));
while self.samples.len() > 2 {
let Some((oldest_at, _)) = self.samples.front() else {
break;
};
if now.duration_since(*oldest_at) <= MEDIA_SPEED_SAMPLE_WINDOW {
break;
}
self.samples.pop_front();
}
let (oldest_at, oldest_bytes) = *self.samples.front()?;
let elapsed = now.duration_since(oldest_at).as_secs_f64();
if elapsed <= 0.25 || downloaded_bytes <= oldest_bytes {
return None;
}
Some((downloaded_bytes - oldest_bytes) / elapsed)
}
}
fn media_progress_speed(
progress: &MediaProgress,
now: Instant,
last_sample: &mut Option<(Instant, f64)>,
speed_sampler: &mut MediaSpeedSampler,
) -> (String, String) {
let Some(downloaded_bytes) = progress.downloaded_bytes else {
return (progress.speed.clone(), progress.eta.clone());
};
let Some((last_at, last_bytes)) = *last_sample else {
*last_sample = Some((now, downloaded_bytes));
return (progress.speed.clone(), progress.eta.clone());
};
*last_sample = Some((now, downloaded_bytes));
if downloaded_bytes < last_bytes {
return (progress.speed.clone(), progress.eta.clone());
}
let elapsed = now.duration_since(last_at).as_secs_f64();
if elapsed <= 0.05 {
return (progress.speed.clone(), progress.eta.clone());
}
let bytes_per_second = (downloaded_bytes - last_bytes) / elapsed;
if bytes_per_second > 0.0 {
if let Some(bytes_per_second) = speed_sampler.sample(downloaded_bytes, now) {
let speed_str = crate::download::format_speed(bytes_per_second);
let eta_str = if progress.fraction > 0.0 && progress.fraction < 1.0 {
let total = downloaded_bytes / progress.fraction;
@@ -951,7 +977,7 @@ fn emit_media_progress(
total_tracks: f64,
current_track: &mut f64,
last_fraction: &mut f64,
last_speed_sample: &mut Option<(Instant, f64)>,
speed_sampler: &mut MediaSpeedSampler,
last_progress_at: &mut Instant,
) {
let previous_track = *current_track;
@@ -962,12 +988,12 @@ fn emit_media_progress(
progress.fraction,
);
if *current_track != previous_track {
*last_speed_sample = None;
speed_sampler.reset();
}
let (speed, eta) = media_progress_speed(&progress, Instant::now(), last_speed_sample);
let (speed, eta) = media_progress_speed(&progress, Instant::now(), speed_sampler);
let now = Instant::now();
if now.duration_since(*last_progress_at) >= Duration::from_millis(200) {
if now.duration_since(*last_progress_at) >= MEDIA_PROGRESS_EMIT_INTERVAL {
let _ = app_handle.emit(
"download-progress",
DownloadProgressEvent {
@@ -2805,9 +2831,9 @@ pub(crate) async fn start_media_download_internal(
let _keep_alive = config_path;
let mut current_track: f64 = 0.0;
let mut last_fraction: f64 = 0.0;
let mut last_speed_sample: Option<(Instant, f64)> = None;
let mut speed_sampler = MediaSpeedSampler::default();
let mut last_progress_at = std::time::Instant::now()
.checked_sub(std::time::Duration::from_millis(200))
.checked_sub(MEDIA_PROGRESS_EMIT_INTERVAL)
.unwrap_or_else(std::time::Instant::now);
// Resolve absolute paths to bundled binaries
@@ -2944,7 +2970,7 @@ pub(crate) async fn start_media_download_internal(
total_tracks,
&mut current_track,
&mut last_fraction,
&mut last_speed_sample,
&mut speed_sampler,
&mut last_progress_at,
);
} else {
@@ -2973,7 +2999,7 @@ pub(crate) async fn start_media_download_internal(
total_tracks,
&mut current_track,
&mut last_fraction,
&mut last_speed_sample,
&mut speed_sampler,
&mut last_progress_at,
);
}
@@ -3015,7 +3041,7 @@ pub(crate) async fn start_media_download_internal(
total_tracks,
&mut current_track,
&mut last_fraction,
&mut last_speed_sample,
&mut speed_sampler,
&mut last_progress_at,
);
} else {
@@ -3037,7 +3063,7 @@ pub(crate) async fn start_media_download_internal(
total_tracks,
&mut current_track,
&mut last_fraction,
&mut last_speed_sample,
&mut speed_sampler,
&mut last_progress_at,
);
}
@@ -4596,10 +4622,11 @@ mod tests {
collect_download_uris, drain_media_output_lines, filename_from_content_disposition,
filename_from_url_disposition_query, filename_from_url_path, is_excluded_yt_dlp_format,
is_browser_cookie_extraction_error, json_lower, media_metadata_cache_key,
media_output_template, media_progress_args, media_progress_speed, normalize_speed_limit_for_aria2,
media_output_template, media_progress_args, media_progress_speed,
normalize_speed_limit_for_aria2,
parse_firelink_deep_link, parse_ffmpeg_version, parse_media_progress_line,
redact_log_line, sanitize_ytdlp_config_value, should_cleanup_media_artifacts_after_failure,
FirelinkDeepLink, MediaProgress, MEDIA_PROGRESS_PREFIX,
FirelinkDeepLink, MediaProgress, MediaSpeedSampler, MEDIA_PROGRESS_PREFIX,
};
use serde_json::json;
use std::time::{Duration, Instant};
@@ -5149,15 +5176,49 @@ mod tests {
downloaded_bytes: Some(3_097_152.0),
};
let start = Instant::now();
let mut sample = None;
let mut sampler = MediaSpeedSampler::default();
assert_eq!(media_progress_speed(&first, start, &mut sample), ("fallback".to_string(), "-".to_string()));
assert_eq!(
media_progress_speed(&second, start + Duration::from_secs(1), &mut sample),
media_progress_speed(&first, start, &mut sampler),
("fallback".to_string(), "-".to_string())
);
assert_eq!(
media_progress_speed(&second, start + Duration::from_secs(1), &mut sampler),
("2.0 MB/s".to_string(), "1s".to_string())
);
}
#[test]
fn smooths_media_speed_across_short_stalls() {
let start = Instant::now();
let mut sampler = MediaSpeedSampler::default();
let mut progress = MediaProgress {
fraction: 0.25,
speed: "-".to_string(),
eta: "-".to_string(),
size: None,
downloaded_bytes: Some(1_000_000.0),
};
assert_eq!(
media_progress_speed(&progress, start, &mut sampler),
("-".to_string(), "-".to_string())
);
progress.fraction = 0.5;
progress.downloaded_bytes = Some(3_000_000.0);
assert_eq!(
media_progress_speed(&progress, start + Duration::from_secs(1), &mut sampler),
("1.9 MB/s".to_string(), "2s".to_string())
);
progress.downloaded_bytes = Some(3_000_000.0);
assert_eq!(
media_progress_speed(&progress, start + Duration::from_secs(2), &mut sampler),
("976.6 KB/s".to_string(), "3s".to_string())
);
}
#[test]
fn parses_aria2_external_downloader_progress() {
let line = "[#2d2636 12MiB/34MiB(34%) CN:1 DL:910KiB ETA:25s]";