From af12da42637c59113a34c83a5bda34c15db72446 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Tue, 9 Dec 2025 23:30:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=B8=8B=E8=BD=BD=E9=87=8D?= =?UTF-8?q?=E8=AF=95=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/play/page.tsx | 59 ++++++++++--------- src/components/DownloadPanel.tsx | 14 ++++- src/contexts/DownloadContext.tsx | 7 +++ src/lib/m3u8-downloader.ts | 98 ++++++++++++++++++++++++++++++-- 4 files changed, 139 insertions(+), 39 deletions(-) diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index 1ab7890..1f6463b 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -3581,11 +3581,15 @@ function PlayPageClient() { - {/* IDM 下载按钮 */} + {/* IDM */} diff --git a/src/components/DownloadPanel.tsx b/src/components/DownloadPanel.tsx index c951e67..adad9f0 100644 --- a/src/components/DownloadPanel.tsx +++ b/src/components/DownloadPanel.tsx @@ -5,7 +5,7 @@ import { useDownload } from '@/contexts/DownloadContext'; import { M3U8DownloadTask } from '@/lib/m3u8-downloader'; export function DownloadPanel() { - const { tasks, showDownloadPanel, setShowDownloadPanel, startTask, pauseTask, cancelTask, getProgress } = useDownload(); + const { tasks, showDownloadPanel, setShowDownloadPanel, startTask, pauseTask, cancelTask, retryFailedSegments, getProgress } = useDownload(); if (!showDownloadPanel) { return null; @@ -127,8 +127,16 @@ export function DownloadPanel() { {/* 错误信息 */} {task.errorNum > 0 && ( -
- {task.errorNum} 个片段下载失败 +
+
+ {task.errorNum} 个片段下载失败 +
+
)} diff --git a/src/contexts/DownloadContext.tsx b/src/contexts/DownloadContext.tsx index 7c47922..a1e9bf8 100644 --- a/src/contexts/DownloadContext.tsx +++ b/src/contexts/DownloadContext.tsx @@ -10,6 +10,7 @@ interface DownloadContextType { startTask: (taskId: string) => void; pauseTask: (taskId: string) => void; cancelTask: (taskId: string) => void; + retryFailedSegments: (taskId: string) => void; getProgress: (taskId: string) => number; downloadingCount: number; showDownloadPanel: boolean; @@ -62,6 +63,11 @@ export function DownloadProvider({ children }: { children: React.ReactNode }) { setTasks(downloader.getAllTasks()); }, [downloader]); + const retryFailedSegments = useCallback((taskId: string) => { + downloader.retryFailedSegments(taskId); + setTasks(downloader.getAllTasks()); + }, [downloader]); + const getProgress = useCallback((taskId: string) => { return downloader.getProgress(taskId); }, [downloader]); @@ -77,6 +83,7 @@ export function DownloadProvider({ children }: { children: React.ReactNode }) { startTask, pauseTask, cancelTask, + retryFailedSegments, getProgress, downloadingCount, showDownloadPanel, diff --git a/src/lib/m3u8-downloader.ts b/src/lib/m3u8-downloader.ts index f183d1d..dd96b14 100644 --- a/src/lib/m3u8-downloader.ts +++ b/src/lib/m3u8-downloader.ts @@ -13,7 +13,11 @@ export interface M3U8DownloadTask { title: string; type: 'TS' | 'MP4'; status: 'ready' | 'downloading' | 'pause' | 'done' | 'error'; - finishList: Array<{ title: string; status: '' | 'is-downloading' | 'is-success' | 'is-error' }>; + finishList: Array<{ + title: string; + status: '' | 'is-downloading' | 'is-success' | 'is-error'; + retryCount?: number; // 重试次数 + }>; tsUrlList: string[]; requests: XMLHttpRequest[]; mediaFileList: ArrayBuffer[]; @@ -231,6 +235,40 @@ export class M3U8Downloader { return (task.finishNum / task.rangeDownload.targetSegment) * 100; } + /** + * 重试所有失败的片段 + */ + retryFailedSegments(taskId: string): void { + const task = this.tasks.get(taskId); + if (!task) return; + + // 重置所有失败片段的状态 + let hasError = false; + task.finishList.forEach((item) => { + if (item.status === 'is-error') { + item.status = ''; + item.retryCount = 0; + hasError = true; + } + }); + + if (hasError) { + task.errorNum = 0; + task.status = 'downloading'; + + // 找到第一个失败的片段索引 + let firstErrorIndex = task.rangeDownload.endSegment; + for (let i = task.rangeDownload.startSegment - 1; i < task.rangeDownload.endSegment; i++) { + if (task.finishList[i] && task.finishList[i].status === '') { + firstErrorIndex = Math.min(firstErrorIndex, i); + } + } + + task.downloadIndex = firstErrorIndex; + this.downloadTS(task); + } + } + /** * 下载 TS 片段 */ @@ -247,6 +285,9 @@ export class M3U8Downloader { if (task.finishList[index] && task.finishList[index].status === '') { task.finishList[index].status = 'is-downloading'; + if (!task.finishList[index].retryCount) { + task.finishList[index].retryCount = 0; + } const xhr = new XMLHttpRequest(); xhr.responseType = 'arraybuffer'; @@ -259,9 +300,36 @@ export class M3U8Downloader { } }); } else { - task.errorNum++; - task.finishList[index].status = 'is-error'; - this.options.onError?.(task, `片段 ${index} 下载失败`); + // 下载失败,检查是否需要重试 + const maxRetries = 3; + const currentRetry = task.finishList[index].retryCount || 0; + + if (currentRetry < maxRetries) { + // 重试 + task.finishList[index].retryCount = currentRetry + 1; + task.finishList[index].status = ''; + console.log(`片段 ${index} 下载失败,正在重试 (${currentRetry + 1}/${maxRetries})...`); + + // 延迟重试,避免立即重试 + setTimeout(() => { + if (task.status !== 'pause') { + download(); + } + }, 1000 * (currentRetry + 1)); // 递增延迟 + } else { + // 重试次数用完,标记为最终失败 + task.errorNum++; + task.finishList[index].status = 'is-error'; + this.options.onError?.(task, `片段 ${index} 下载失败(已重试 ${maxRetries} 次)`); + + // 检查是否所有片段都已处理完成 + if (task.finishNum + task.errorNum === task.rangeDownload.targetSegment) { + if (task.errorNum > 0) { + task.status = 'pause'; + this.options.onError?.(task, `下载完成,但有 ${task.errorNum} 个片段失败`); + } + } + } if (task.downloadIndex < task.rangeDownload.endSegment) { !isPause && download(); @@ -357,6 +425,7 @@ export class M3U8Downloader { * 获取 M3U8 文件 */ private async fetchM3U8(url: string): Promise { + console.log('fetchM3U8 - 请求 URL:', url); return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = () => { @@ -364,6 +433,7 @@ export class M3U8Downloader { if (xhr.status >= 200 && xhr.status < 300) { resolve(xhr.responseText); } else { + console.error('fetchM3U8 失败 - URL:', url, 'Status:', xhr.status); reject(new Error(`HTTP ${xhr.status}`)); } } @@ -428,14 +498,30 @@ export class M3U8Downloader { */ private applyURL(targetURL: string, baseURL: string): string { if (targetURL.indexOf('http') === 0) { + // 如果目标 URL 包含 0.0.0.0,替换为当前浏览器的 host + if (targetURL.includes('0.0.0.0')) { + const currentOrigin = `${window.location.protocol}//${window.location.host}`; + return targetURL.replace(/https?:\/\/0\.0\.0\.0:\d+/, currentOrigin); + } return targetURL; } else if (targetURL[0] === '/') { const domain = baseURL.split('/'); - return domain[0] + '//' + domain[2] + targetURL; + let origin = domain[0] + '//' + domain[2]; + // 如果 origin 包含 0.0.0.0,替换为当前浏览器的 host + if (origin.includes('0.0.0.0')) { + origin = `${window.location.protocol}//${window.location.host}`; + } + return origin + targetURL; } else { const domain = baseURL.split('/'); domain.pop(); - return domain.join('/') + '/' + targetURL; + let result = domain.join('/') + '/' + targetURL; + // 如果结果包含 0.0.0.0,替换为当前浏览器的 host + if (result.includes('0.0.0.0')) { + const currentOrigin = `${window.location.protocol}//${window.location.host}`; + result = result.replace(/https?:\/\/0\.0\.0\.0:\d+/, currentOrigin); + } + return result; } }