mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-19 01:28:06 +00:00
fix: speed calculation
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
import { toast } from "sonner-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useDownload } from "@/providers/DownloadProvider";
|
||||
import { calculateSmoothedETA } from "@/providers/Downloads/hooks/useDownloadSpeedCalculator";
|
||||
import { JobStatus } from "@/providers/Downloads/types";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
import { formatTimeString } from "@/utils/time";
|
||||
@@ -62,16 +63,23 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const eta = (p: JobStatus) => {
|
||||
if (!p.speed || p.speed <= 0 || !p.estimatedTotalSizeBytes) return null;
|
||||
const eta = useMemo(() => {
|
||||
if (!process.estimatedTotalSizeBytes || !process.bytesDownloaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bytesRemaining = p.estimatedTotalSizeBytes - (p.bytesDownloaded || 0);
|
||||
if (bytesRemaining <= 0) return null;
|
||||
const secondsRemaining = calculateSmoothedETA(
|
||||
process.id,
|
||||
process.bytesDownloaded,
|
||||
process.estimatedTotalSizeBytes,
|
||||
);
|
||||
|
||||
const secondsRemaining = bytesRemaining / p.speed;
|
||||
if (!secondsRemaining || secondsRemaining <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return formatTimeString(secondsRemaining, "s");
|
||||
};
|
||||
}, [process.id, process.bytesDownloaded, process.estimatedTotalSizeBytes]);
|
||||
|
||||
const base64Image = useMemo(() => {
|
||||
return storage.getString(process.item.Id!);
|
||||
@@ -161,9 +169,9 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
||||
{bytesToMB(process.speed).toFixed(2)} MB/s
|
||||
</Text>
|
||||
)}
|
||||
{eta(process) && (
|
||||
{eta && (
|
||||
<Text className='text-xs'>
|
||||
{t("home.downloads.eta", { eta: eta(process) })}
|
||||
{t("home.downloads.eta", { eta: eta })}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -19,6 +19,7 @@ class DownloadSessionDelegate: NSObject, URLSessionDownloadDelegate {
|
||||
init(module: BackgroundDownloaderModule) {
|
||||
self.module = module
|
||||
super.init()
|
||||
print("[DownloadSessionDelegate] Delegate initialized with module: \(String(describing: module))")
|
||||
}
|
||||
|
||||
func urlSession(
|
||||
@@ -141,10 +142,73 @@ public class BackgroundDownloaderModule: Module {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
session.getAllTasks { tasks in
|
||||
if let downloadTask = tasks.first(where: { $0.taskIdentifier == taskId }) {
|
||||
print("[BackgroundDownloader] Task state after 0.5s: \(self.taskStateString(downloadTask.state))")
|
||||
print("[BackgroundDownloader] === 0.5s CHECK ===")
|
||||
print("[BackgroundDownloader] Task state: \(self.taskStateString(downloadTask.state))")
|
||||
if let response = downloadTask.response as? HTTPURLResponse {
|
||||
print("[BackgroundDownloader] Response status: \(response.statusCode)")
|
||||
print("[BackgroundDownloader] Expected content length: \(response.expectedContentLength)")
|
||||
} else {
|
||||
print("[BackgroundDownloader] No HTTP response yet after 0.5s")
|
||||
}
|
||||
} else {
|
||||
print("[BackgroundDownloader] Task not found after 0.5s")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Additional diagnostics at 1s, 2s, and 3s
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
session.getAllTasks { tasks in
|
||||
if let downloadTask = tasks.first(where: { $0.taskIdentifier == taskId }) {
|
||||
print("[BackgroundDownloader] === 1s CHECK ===")
|
||||
print("[BackgroundDownloader] Task state: \(self.taskStateString(downloadTask.state))")
|
||||
print("[BackgroundDownloader] Task error: \(String(describing: downloadTask.error))")
|
||||
print("[BackgroundDownloader] Current request URL: \(downloadTask.currentRequest?.url?.absoluteString ?? "nil")")
|
||||
print("[BackgroundDownloader] Original request URL: \(downloadTask.originalRequest?.url?.absoluteString ?? "nil")")
|
||||
|
||||
if let response = downloadTask.response as? HTTPURLResponse {
|
||||
print("[BackgroundDownloader] HTTP Status: \(response.statusCode)")
|
||||
print("[BackgroundDownloader] Expected content length: \(response.expectedContentLength)")
|
||||
print("[BackgroundDownloader] All headers: \(response.allHeaderFields)")
|
||||
} else {
|
||||
print("[BackgroundDownloader] ⚠️ STILL NO HTTP RESPONSE after 1s")
|
||||
}
|
||||
|
||||
let countOfBytesReceived = downloadTask.countOfBytesReceived
|
||||
if countOfBytesReceived > 0 {
|
||||
print("[BackgroundDownloader] Bytes received: \(countOfBytesReceived)")
|
||||
} else {
|
||||
print("[BackgroundDownloader] ⚠️ NO BYTES RECEIVED YET")
|
||||
}
|
||||
} else {
|
||||
print("[BackgroundDownloader] ⚠️ Task disappeared after 1s")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
session.getAllTasks { tasks in
|
||||
if let downloadTask = tasks.first(where: { $0.taskIdentifier == taskId }) {
|
||||
print("[BackgroundDownloader] === 2s CHECK ===")
|
||||
print("[BackgroundDownloader] Task state: \(self.taskStateString(downloadTask.state))")
|
||||
let countOfBytesReceived = downloadTask.countOfBytesReceived
|
||||
print("[BackgroundDownloader] Bytes received: \(countOfBytesReceived)")
|
||||
if downloadTask.error != nil {
|
||||
print("[BackgroundDownloader] ⚠️ Task has error: \(String(describing: downloadTask.error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
||||
session.getAllTasks { tasks in
|
||||
if let downloadTask = tasks.first(where: { $0.taskIdentifier == taskId }) {
|
||||
print("[BackgroundDownloader] === 3s CHECK ===")
|
||||
print("[BackgroundDownloader] Task state: \(self.taskStateString(downloadTask.state))")
|
||||
let countOfBytesReceived = downloadTask.countOfBytesReceived
|
||||
print("[BackgroundDownloader] Bytes received: \(countOfBytesReceived)")
|
||||
if downloadTask.error != nil {
|
||||
print("[BackgroundDownloader] ⚠️ Task has error: \(String(describing: downloadTask.error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,17 +237,20 @@ public class BackgroundDownloaderModule: Module {
|
||||
|
||||
AsyncFunction("getActiveDownloads") { () -> [[String: Any]] in
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
let downloadTasks = self.downloadTasks
|
||||
let taskStateString = self.taskStateString
|
||||
|
||||
self.session?.getAllTasks { tasks in
|
||||
let activeDownloads = tasks.compactMap { task -> [String: Any]? in
|
||||
guard task is URLSessionDownloadTask,
|
||||
let info = self.downloadTasks[task.taskIdentifier] else {
|
||||
let info = downloadTasks[task.taskIdentifier] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return [
|
||||
"taskId": task.taskIdentifier,
|
||||
"url": info.url,
|
||||
"state": self.taskStateString(task.state)
|
||||
"state": taskStateString(task.state)
|
||||
]
|
||||
}
|
||||
continuation.resume(returning: activeDownloads)
|
||||
@@ -210,6 +277,15 @@ public class BackgroundDownloaderModule: Module {
|
||||
)
|
||||
|
||||
print("[BackgroundDownloader] URLSession initialized with delegate: \(String(describing: self.sessionDelegate))")
|
||||
print("[BackgroundDownloader] Session identifier: \(config.identifier ?? "nil")")
|
||||
print("[BackgroundDownloader] Delegate queue: nil (uses default)")
|
||||
|
||||
// Verify delegate is connected
|
||||
if let session = self.session, session.delegate != nil {
|
||||
print("[BackgroundDownloader] ✅ Delegate successfully attached to session")
|
||||
} else {
|
||||
print("[BackgroundDownloader] ⚠️ DELEGATE NOT ATTACHED!")
|
||||
}
|
||||
}
|
||||
|
||||
private func taskStateString(_ state: URLSessionTask.State) -> String {
|
||||
|
||||
@@ -4,11 +4,13 @@ interface SpeedDataPoint {
|
||||
}
|
||||
|
||||
const WINDOW_DURATION = 60000; // 1 minute in ms
|
||||
const MIN_DATA_POINTS = 3; // Need at least 3 points for accurate speed
|
||||
const MIN_DATA_POINTS = 5; // Need at least 5 points for accurate speed
|
||||
const MAX_REASONABLE_SPEED = 1024 * 1024 * 1024; // 1 GB/s sanity check
|
||||
const EMA_ALPHA = 0.2; // Smoothing factor for EMA (lower = smoother, 0-1 range)
|
||||
|
||||
// Private state
|
||||
const dataPoints = new Map<string, SpeedDataPoint[]>();
|
||||
const emaSpeed = new Map<string, number>(); // Store EMA speed for each process
|
||||
|
||||
function isValidBytes(bytes: number): boolean {
|
||||
return typeof bytes === "number" && Number.isFinite(bytes) && bytes >= 0;
|
||||
@@ -161,7 +163,8 @@ export function calculateWeightedSpeed(processId: string): number | undefined {
|
||||
}
|
||||
|
||||
// More recent points get exponentially higher weight
|
||||
const weight = 2 ** i;
|
||||
// Using 1.3 instead of 2 for gentler weighting (less sensitive to recent changes)
|
||||
const weight = 1.3 ** i;
|
||||
totalWeightedSpeed += speed * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
@@ -180,12 +183,74 @@ export function calculateWeightedSpeed(processId: string): number | undefined {
|
||||
return weightedSpeed;
|
||||
}
|
||||
|
||||
// Calculate ETA in seconds
|
||||
export function calculateETA(
|
||||
processId: string,
|
||||
bytesDownloaded: number,
|
||||
totalBytes: number,
|
||||
): number | undefined {
|
||||
const speed = calculateWeightedSpeed(processId);
|
||||
|
||||
if (!speed || speed <= 0 || !totalBytes || totalBytes <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const bytesRemaining = totalBytes - bytesDownloaded;
|
||||
if (bytesRemaining <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const secondsRemaining = bytesRemaining / speed;
|
||||
|
||||
// Sanity check
|
||||
if (!Number.isFinite(secondsRemaining) || secondsRemaining < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return secondsRemaining;
|
||||
}
|
||||
|
||||
// Calculate smoothed ETA using Exponential Moving Average (EMA)
|
||||
// This provides much smoother ETA estimates, reducing jumpy time estimates
|
||||
const emaETA = new Map<string, number>();
|
||||
|
||||
export function calculateSmoothedETA(
|
||||
processId: string,
|
||||
bytesDownloaded: number,
|
||||
totalBytes: number,
|
||||
): number | undefined {
|
||||
const currentETA = calculateETA(processId, bytesDownloaded, totalBytes);
|
||||
|
||||
if (currentETA === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const previousEma = emaETA.get(processId);
|
||||
|
||||
if (previousEma === undefined) {
|
||||
// First calculation, initialize with current ETA
|
||||
emaETA.set(processId, currentETA);
|
||||
return currentETA;
|
||||
}
|
||||
|
||||
// EMA formula: EMA(t) = α * current + (1 - α) * EMA(t-1)
|
||||
// Lower alpha = smoother but slower to respond
|
||||
const smoothed = EMA_ALPHA * currentETA + (1 - EMA_ALPHA) * previousEma;
|
||||
|
||||
emaETA.set(processId, smoothed);
|
||||
return smoothed;
|
||||
}
|
||||
|
||||
export function clearSpeedData(processId: string): void {
|
||||
dataPoints.delete(processId);
|
||||
emaSpeed.delete(processId);
|
||||
emaETA.delete(processId);
|
||||
}
|
||||
|
||||
export function resetAllSpeedData(): void {
|
||||
dataPoints.clear();
|
||||
emaSpeed.clear();
|
||||
emaETA.clear();
|
||||
}
|
||||
|
||||
// Debug function to inspect current state
|
||||
|
||||
Reference in New Issue
Block a user