下載引擎架構

FlowPick 下載引擎的技術架構詳解——分片並行下載、AES-128 解密、三級寫入策略、記憶體安全管理與進度追蹤。

FlowPick 的下載引擎是整個系統的核心,負責將串流媒體分片下載、解密、合併並寫入磁碟。本文件面向希望深入理解內部機制的開發者和進階使用者。


架構概覽

下載引擎由三個核心模組組成,資料按管線方式依次流經各模組:

┌─────────────────────────────────────────────────┐
│                   下載引擎                        │
│                                                  │
│  ┌──────────┐  ┌──────────┐  ┌───────────────┐  │
│  │ 分片下載  │→│ 分片處理  │→│ 檔案寫入       │  │
│  │ 模組     │  │ 模組     │  │ 模組           │  │
│  │          │  │          │  │                │  │
│  │ · 並行   │  │ · 解密   │  │ · FSA 串流    │  │
│  │ · 重試   │  │ · 拼接   │  │ · StreamSaver │  │
│  │ · 限速   │  │ · 轉封裝  │  │ · Blob 兜底   │  │
│  └──────────┘  └──────────┘  └───────────────┘  │
│                                                  │
│  ┌─────────────────────────────────────────────┐ │
│  │              記憶體安全管理器                │ │
│  │  · 大小預估  · 閾值檢查  · 策略選擇         │ │
│  └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘

資料流全景

從使用者點擊下載到檔案寫入磁碟的完整鏈路:

使用者點擊下載
    │
    ▼
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│ 1. 清單解析   │ ──→ │ 2. 分片下載   │ ──→ │ 3. 分片處理   │
│              │     │              │     │              │
│ · 下載 M3U8  │     │ · Worker Pool│     │ · AES 解密   │
│ · 解析分片列表│     │ · 並行控制   │     │ · TS 拼接    │
│ · 提取金鑰   │     │ · 指數退避   │     │ · FFmpeg 轉封│
│ · 預估大小   │     │ · 進度上報   │     │ · 音影片合併  │
└──────────────┘     └──────────────┘     └──────┬───────┘
                                                  │
                                                  ▼
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│ 6. 完成通知   │ ←── │ 5. 清理回收   │ ←── │ 4. 檔案寫入   │
│              │     │              │     │              │
│ · 桌面通知   │     │ · 釋放記憶體 │     │ · FSA 串流   │
│ · 路徑複製   │     │ · 清理暫存檔 │     │ · StreamSaver│
│ · 佇列推進   │     │ · 重設狀態   │     │ · Blob 兜底  │
└──────────────┘     └──────────────┘     └──────────────┘
清單解析階段的具體實作(Master Playlist vs Media Playlist、加密偵測)請參閱 影片嗅探 — HLS 串流。分片處理中的 FFmpeg 轉封裝細節請參閱 格式轉換。引擎在整體系統中的位置請參閱 專案架構

引擎生命週期

下載引擎的一次完整執行經歷以下狀態轉換:

  [閒置]
    │ 使用者觸發下載
    ▼
  [初始化] ──── 載入 WASM、檢查 API 可用性、選擇寫入策略
    │
    ▼
  [清單解析] ── 下載並解析 M3U8/MPD,提取分片列表和金鑰
    │
    ▼
  [分片下載] ── Worker Pool 並行下載,即時上報進度
    │
    ▼
  [分片處理] ── 解密 → 拼接/轉封裝 → 寫入磁碟
    │
    ├── 成功 → [清理] → [完成] → [閒置]
    │
    ├── 取消 → [清理] → [閒置]
    │
    └── 失敗 → [清理] → [錯誤] → [閒置]

每個狀態轉換都會觸發對應的生命週期鉤子,UI 層透過監聽這些鉤子來更新介面狀態。關於 UI 與引擎的互動細節,請參閱 專案架構 — 資料流


分片下載模組

並行下載器

分片下載使用 Worker Pool 模式實作並行控制:

const downloadSegmentsConcurrently = async (
  segments: SegmentInfo[],
  onSegmentDownloaded: (buffer: ArrayBuffer, index: number) => void
) => {
  const concurrency = Math.min(config.concurrency, 8)
  let nextIndex = 0

  const worker = async () => {
    while (nextIndex < segments.length) {
      const index = nextIndex++
      const buffer = await downloadWithRetry(segments[index]!)
      onSegmentDownloaded(buffer, index)
    }
  }

  const workers = Array.from(
    { length: Math.min(concurrency, segments.length) },
    () => worker()
  )
  await Promise.all(workers)
}

設計要點

  • 使用共享的 nextIndex 計數器分配任務,避免預先分片導致負載不均
  • Worker 數量不超過分片總數,避免建立閒置 Worker
  • 每個 Worker 獨立執行,單個分片失敗不影響其他 Worker

Worker Pool 深入

Worker Pool 模式的核心優勢在於動態負載均衡。與預先將分片陣列切分為 N 等份不同,共享計數器確保:

預先分片(不推薦):
Worker 1: [分片 0-49]   ← 如果這些分片較大,Worker 1 成為瓶頸
Worker 2: [分片 50-99]  ← 可能提前完成,然後閒置等待

共享計數器(FlowPick 採用):
Worker 1: 分片 0 → 分片 3 → 分片 5 → ...
Worker 2: 分片 1 → 分片 4 → 分片 7 → ...
Worker 3: 分片 2 → 分片 6 → 分片 8 → ...

每個 Worker 完成目前分片後立即取得下一個未分配的分片,直到所有分片處理完畢。這種模式天然適應分片大小不均的場景(如 HLS 串流的首尾分片通常較小,中間分片較大)。

並行數透過 config.concurrency 控制,上限為 8。使用者可以在 設定參考 中調整預設值,或在下載介面即時修改。關於批次下載場景下的佇列排程,請參閱 批次下載 — 並行數選擇指南

非同步生成器模式

對於超大檔案(分片數量 > 500),引擎使用非同步生成器(AsyncGenerator)模式避免一次性將所有分片資料載入到記憶體:

async function* segmentGenerator(
  segments: SegmentInfo[],
  signal?: AbortSignal
): AsyncGenerator<ArrayBuffer> {
  for (const segment of segments) {
    if (signal?.aborted) break
    const buffer = await downloadWithRetry(segment)
    yield buffer
  }
}

與陣列模式的區別

維度陣列模式生成器模式
記憶體峰值所有分片同時在記憶體中僅目前處理的分片在記憶體中
適用場景分片數 < 500分片數 > 500
進度追蹤已知總數,精確百分比已知總數,精確百分比
取消回應需等待目前批次完成立即回應

引擎根據分片數量自動選擇模式,無需使用者干預。關於超大檔案下載的實際場景,請參閱 直播回放儲存

重試機制

分片下載失敗時採用指數退避重試:

const downloadWithRetry = async (
  segment: SegmentInfo,
  maxRetries = 3
): Promise<ArrayBuffer> => {
  let lastError: unknown
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await downloadAndDecryptSegment(segment)
    } catch (e) {
      lastError = e
      if (attempt < maxRetries - 1) {
        // 指數退避:400ms, 800ms, 1600ms
        await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 400))
      }
    }
  }
  throw lastError
}

重試策略

嘗試次數延遲累計等待
第 1 次失敗400ms400ms
第 2 次失敗800ms1200ms
第 3 次失敗1600ms2800ms

不觸發重試的情況

  • HTTP 4xx 客戶端錯誤(403、404 等)—— 重試無意義
  • CORS 錯誤 —— 策略問題,重試不會改變結果
  • 加密不支援錯誤 —— 無法透過重試解決
重試機制與 線上工具 — 錯誤處理 中的錯誤分類表配合使用。HTTP 4xx 和 CORS 錯誤直接拋出,由上層 UI 展示對應的使用者提示。關於下載中途失敗的排查,請參閱 常見問題排查 — 下載中途失敗

錯誤分類

class FetchError extends Error {
  constructor(
    message: string,
    public status: number,
    public url: string
  ) {
    super(message)
    this.name = 'FetchError'
  }
}

錯誤按 HTTP 狀態碼分類處理:

狀態碼錯誤類型使用者提示
403認證/授權失敗"存取被拒絕,請檢查 URL 是否有效"
404資源不存在"分片未找到,串流可能已過期"
502/503/504伺服器錯誤"伺服器暫時不可用,請稍後重試"
CORS跨域限制"跨域請求被阻止,建議使用擴充功能版本"
Network網路中斷"網路連線失敗,請檢查網路"
遇到 CORS 錯誤時,建議安裝 瀏覽器擴充功能,擴充功能透過 webRequest 權限可以繞過同源策略。更多排查方法請參閱 常見問題排查。關於 CORS 的技術原理和限制,請參閱 已知限制 — 瀏覽器限制

分片處理模組

AES-128 解密

對於加密的 HLS 串流,FlowPick 使用 Web Crypto API 在瀏覽器中解密:

const decryptAES128 = async (
  encryptedData: ArrayBuffer,
  key: ArrayBuffer,
  iv?: Uint8Array
): Promise<ArrayBuffer> => {
  const keyBytes = await crypto.subtle.importKey(
    'raw', key,
    { name: 'AES-CBC' },
    false,
    ['decrypt']
  )

  const ivBuffer = iv ? new Uint8Array(iv) : new Uint8Array(16)

  const decrypted = await crypto.subtle.decrypt(
    { name: 'AES-CBC', iv: ivBuffer },
    keyBytes,
    encryptedData
  )

  return decrypted
}

解密流程

  1. 從 M3U8 的 #EXT-X-KEY 標籤提取金鑰 URI 和 IV
  2. 下載金鑰檔案(通常是 16 位元組的二進位檔案)
  3. 使用 AES-128-CBC 模式解密每個分片
  4. 如果未指定 IV,使用分片序號作為 IV(HLS 規範預設行為)

效能考量

  • Web Crypto API 使用硬體加速,解密速度通常不是瓶頸
  • 每個分片獨立解密,可以與下載並行進行
  • 金鑰只需下載一次,快取在記憶體中
所有解密操作都在瀏覽器本機完成,金鑰和分片資料不會上傳到任何伺服器。關於加密偵測的更多細節,請參閱 影片嗅探 — 加密串流。關於隱私保護策略,請參閱 隱私與安全。關於不支援的加密類型(Widevine、PlayReady 等),請參閱 已知限制 — DRM 保護內容

TS 分片拼接

對於 TS 輸出格式,分片直接按二進位拼接:

// 虛擬碼
const merged = new Uint8Array(totalSize)
let offset = 0
for (const segment of segments) {
  merged.set(new Uint8Array(segment), offset)
  offset += segment.byteLength
}

這種方式零 CPU 開銷,速度僅受記憶體複製速度限制。

FFmpeg WASM 轉封裝

對於 MP4 輸出格式,使用 FFmpeg WASM 進行容器轉換。詳見 格式轉換 文件。

如果只需要 TS 格式,選擇 TS 輸出可以完全跳過 FFmpeg,大幅提升合併速度。TS 檔案可以用 VLC、PotPlayer 等播放器直接播放。關於輸出格式的選擇建議,請參閱 格式轉換 — 輸出格式選擇

DASH 串流的特殊處理

DASH 串流與 HLS 串流在分片處理上有顯著差異:

HLS 串流處理:
  分片 0 → 分片 1 → 分片 2 → ... → 拼接 → 輸出

DASH 串流處理(音影片分離):
  初始化段 → 影片分片 0 → 影片分片 1 → ... ─┐
                                              ├→ FFmpeg 合併 → 輸出
  初始化段 → 音訊分片 0 → 音訊分片 1 → ... ─┘

DASH 的 FMP4(Fragmented MP4)格式需要特殊處理:

  1. 初始化段ftyp + moov box)必須放在檔案開頭
  2. 媒體段moof + mdat box)按順序追加
  3. 音影片分離時,需要分別下載影片和音訊軌道,最後透過 FFmpeg 合併
DASH 串流的清單解析細節請參閱 影片嗅探 — DASH 串流。FMP4 重組的具體實作請參閱 格式轉換 — FMP4 重組。關於 DASH 音影片分離的已知問題,請參閱 已知限制 — 分離音影片串流

檔案寫入模組

寫入模組按優先級選擇策略,確保在盡可能多的瀏覽器中工作。

策略一:File System Access API(最優)

async function createFSAStream(
  filename: string,
  dirHandle?: FileSystemDirectoryHandle | null
): Promise<WritableStream<Uint8Array>> {
  let handle: FileSystemFileHandle

  if (dirHandle) {
    // 已有目錄權限,直接建立檔案
    handle = await dirHandle.getFileHandle(filename, { create: true })
  } else {
    // 彈窗讓使用者選擇儲存位置
    handle = await window.showSaveFilePicker!({
      suggestedName: filename,
      types: [{
        description: 'Video',
        accept: { 'video/mp4': ['.mp4'] }
      }]
    })
  }

  const writable = await handle.createWritable()

  return new WritableStream<Uint8Array>({
    async write(chunk) { await writable.write(chunk) },
    async close() { await writable.close() },
    async abort(reason) { await writable.abort(reason) }
  }, {
    highWaterMark: 16 * 1024 * 1024 // 16MB 緩衝區
  })
}

優勢

  • 串流寫入,記憶體佔用恆定(僅 16MB 緩衝區)
  • 支援任意大小的檔案
  • 目錄持久化後無需反覆彈窗

限制

  • 僅 Chrome 86+ 和 Edge 86+ 支援
  • 需要使用者手勢觸發(showDirectoryPicker 必須在使用者點擊事件中呼叫)

策略二:StreamSaver.js(備選)

async function createStreamSaverStream(
  filename: string,
  fileSize?: number
): Promise<WritableStream<Uint8Array>> {
  const ss = await getStreamSaver()
  const fileStream = ss.createWriteStream(filename, { size: fileSize })
  return fileStream
}

優勢

  • 串流寫入,記憶體佔用低
  • 相容性優於 FSA API

限制

  • 需要 Service Worker 支援
  • 需要 mitm.htmlstreamsaver-sw.js 正確部署
  • 部分企業網路環境可能阻止 Service Worker

策略三:Blob 兜底

// 虛擬碼
const blob = new Blob([mergedData], { type: 'video/mp4' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)

限制

  • 整個檔案載入到記憶體中
  • 硬限制 1.5GB(MEMORY_CONFIG.maxBlobSize
  • 超過 800MB 時控制台警告(MEMORY_CONFIG.warnBlobSize

策略對比總結

維度FSA APIStreamSaver.jsBlob
記憶體佔用16MB(恆定)低(串流)等於檔案大小
檔案大小上限無限制無限制1.5GB
瀏覽器要求Chrome/Edge 86+Service Worker 支援所有瀏覽器
目錄持久化支援不支援不支援
下載體驗最佳良好一般
關於各瀏覽器對這三種策略的支援情況,請參閱 瀏覽器相容性 — 功能降級策略。關於線上工具中的寫入策略使用,請參閱 線上工具 — 寫入策略優先級。關於 Blob 模式的大小限制和解決方案,請參閱 已知限制 — 檔案大小限制

記憶體安全管理

大小預估

在開始下載前,引擎會取樣預估總檔案大小:

async function estimateSegmentsSize(
  segments: ArrayBuffer[] | AsyncGenerator<ArrayBuffer>,
  totalCount: number
): Promise<{ estimatedSize: number; isEstimate: boolean }> {
  // 取樣數量:min(5, max(1, totalCount * 0.1))
  const sampleCount = Math.min(5, Math.max(1, Math.floor(totalCount * 0.1)))

  // 下載前幾個分片並計算平均大小
  let sampledTotal = 0
  for (let i = 0; i < sampleCount; i++) {
    sampledTotal += sampleSegment.byteLength
  }

  const avgSample = sampledTotal / sampleCount
  return {
    estimatedSize: Math.round(avgSample * totalCount),
    isEstimate: true
  }
}

取樣策略說明

  • 取樣數量取 min(5, max(1, 分片總數 × 10%)),確保小串流至少取樣 1 個,大串流最多取樣 5 個
  • 取樣結果標記 isEstimate: true,UI 層據此顯示「約 XX MB」而非精確值
  • 對於 HLS 串流,如果 Master Playlist 中聲明了 BANDWIDTH 屬性,引擎會優先使用該值作為預估參考

策略選擇邏輯

預估大小 < 800MB  → 任意策略均可
預估大小 800MB-1.5GB → 優先串流寫入,Blob 模式顯示警告
預估大小 > 1.5GB → 強制串流寫入,Blob 模式拒絕

記憶體設定常數

const MEMORY_CONFIG = {
  maxBlobSize: 1500 * 1024 * 1024,    // 1.5GB 硬限制
  warnBlobSize: 800 * 1024 * 1024,    // 800MB 警告閾值
}

執行時記憶體監控

除了下載前的預估,引擎在執行期間也會持續監控記憶體壓力:

const checkMemoryPressure = (): 'normal' | 'warning' | 'critical' => {
  // 檢查目前已分配的分片緩衝區總大小
  const allocatedMB = allocatedBuffers.reduce((sum, buf) => sum + buf.byteLength, 0) / 1048576

  if (allocatedMB > 1200) return 'critical'
  if (allocatedMB > 600) return 'warning'
  return 'normal'
}

當記憶體壓力達到 critical 級別時,引擎會:

  • 暫停新的分片下載請求
  • 優先將已下載的分片寫入磁碟
  • 釋放已完成寫入的分片緩衝區
記憶體安全是 FlowPick 下載引擎的核心設計考量之一。對於超大檔案(如 20GB+ 的直播回放),引擎會強制使用串流寫入策略,確保瀏覽器不會因記憶體不足而崩潰。關於大檔案下載的實際場景,請參閱 直播回放儲存。關於大檔案下載失敗的排查,請參閱 常見問題排查 — 大檔案下載失敗

進度追蹤

速度計算

使用滑動視窗計算即時下載速度:

const updateSpeedAndTime = () => {
  const elapsed = (Date.now() - downloadStartTime) / 1000
  const bytesPerSecond = downloadedBytes / elapsed

  // 格式化速度顯示
  if (bytesPerSecond > 1024 * 1024) {
    speed = `${(bytesPerSecond / 1048576).toFixed(1)} MB/s`
  } else if (bytesPerSecond > 1024) {
    speed = `${(bytesPerSecond / 1024).toFixed(1)} KB/s`
  } else {
    speed = `${bytesPerSecond.toFixed(0)} B/s`
  }
}

剩餘時間估算

剩餘時間 = (剩餘分片數 × 平均分片大小) / 目前速度

使用節流函數(500ms 間隔)更新顯示,避免頻繁 DOM 更新。

進度階段

階段進度範圍說明
解析0% ~ 5%下載和解析清單檔案
下載5% ~ 85%並行下載分片
合併85% ~ 100%拼接/轉封裝分片

進度資料結構

export interface StreamMergeProgress {
  phase: 'downloading' | 'merging'
  percent: number
  downloadedBytes: number
  totalBytes: number
  speed: string
  eta: string
}

UI 層透過 onProgress 回呼接收進度更新,並根據 phase 欄位切換介面狀態(下載中 / 合併中)。關於進度在 UI 層的展示方式,請參閱 專案架構 — useStreamMerge.ts

進度資訊在 線上工具 和擴充功能彈出視窗中即時顯示。批次下載場景下,每個資源的進度獨立追蹤,詳見 批次下載 — 佇列進度。關於下載速度慢的排查,請參閱 常見問題排查 — 下載速度慢

目錄持久化

權限模型

// 選擇目錄並請求持久化權限
async function pickSaveDirectory(): Promise<string | null> {
  const handle = await window.showDirectoryPicker!({
    mode: 'readwrite',
    id: 'flowpick-save-dir',
    startIn: 'downloads'
  })

  const permissionState = await handle.requestPermission({
    mode: 'readwrite'
  })

  if (permissionState === 'granted') {
    savedDirHandle = handle
    localStorage.setItem('flowpick_save_dir_name', handle.name)
    return handle.name
  }
}

權限恢復

頁面載入時靜默檢查權限狀態:

async function checkSavedDirPermission(): Promise<boolean> {
  if (!savedDirHandle) return false

  try {
    const status = await navigator.permissions.query({
      name: 'file-system',
      handle: savedDirHandle
    })
    return status.state === 'granted'
  } catch {
    // query 失敗時用實際操作測試
    const testFile = await savedDirHandle
      .getFileHandle(`__fp_test_${Date.now()}__`, { create: true })
    const w = await testFile.createWritable()
    await w.close()
    return true
  }
}

權限生命週期

首次使用:
  使用者點擊「選擇目錄」 → showDirectoryPicker() 彈窗 → 使用者選擇 → 權限授予 → 快取控制代碼

再次開啟頁面:
  讀取快取的目錄名稱 → permissions.query() 靜默檢查 → 權限有效 → 直接使用

權限失效時:
  permissions.query() 回傳 'denied' → 清除快取 → 提示使用者重新選擇目錄
目錄持久化功能在 線上工具 和擴充功能中均可使用。權限狀態透過 localStorage 快取目錄名稱,透過 navigator.permissions.query() 靜默檢查實際權限。關於瀏覽器相容性,請參閱 瀏覽器相容性 — File System Access API

取消與清理

下載支援透過 AbortSignal 取消:

export interface StreamMergeOptions {
  segments: ArrayBuffer[] | AsyncGenerator<ArrayBuffer>
  totalSegments: number
  filename: string
  outputFormat?: 'mp4' | 'ts'
  onProgress?: (progress: StreamMergeProgress) => void
  signal?: AbortSignal  // 取消訊號
}

取消時的清理步驟

取消操作觸發後,引擎按以下順序執行清理:

使用者點擊取消
    │
    ▼
1. signal.abort() 觸發
    │
    ▼
2. 中斷所有進行中的 fetch 請求
   └── AbortController 關聯的所有 fetch 立即拋出 AbortError
    │
    ▼
3. 停止 Worker Pool
   └── 所有 Worker 偵測到 signal.aborted,退出迴圈
    │
    ▼
4. 清理 FFmpeg 虛擬檔案系統
   └── 刪除 filelist.txt、segment_*.ts、output.* 等暫存檔案
    │
    ▼
5. 釋放已分配的記憶體緩衝區
   └── 將所有 ArrayBuffer 參考置空,等待 GC 回收
    │
    ▼
6. 丟棄不完整的檔案
   └── FSA 模式:呼叫 writable.abort()
   └── StreamSaver 模式:呼叫 writable.abort()
   └── Blob 模式:不觸發下載,直接釋放 Blob
    │
    ▼
7. 重設引擎狀態
   └── 清除進度資料、速度統計、錯誤資訊

取消的傳播機制

AbortSignal 在整個下載鏈路中逐層傳遞:

UI 層 AbortController
    │
    ├──→ 分片下載 Worker Pool(中斷 fetch)
    ├──→ 分片處理管線(跳過剩餘分片)
    ├──→ FFmpeg WASM(終止轉封裝)
    └──→ 檔案寫入串流(abort 寫入)

每一層都獨立檢查 signal.aborted,確保取消操作能快速回應。即使在 FFmpeg 轉封裝過程中取消,引擎也會等待目前 FFmpeg 操作完成一個原子步驟後再終止,避免虛擬檔案系統損壞。

取消操作在 批次下載 場景中尤為重要——使用者可以取消佇列中的某個下載而不影響其他正在進行的任務。關於取消後的佇列行為,請參閱 批次下載 — 佇列管理

效能基準

以下資料基於 Chrome 126 + 100Mbps 網路環境測試,僅供參考:

場景檔案大小並行數下載耗時合併耗時總耗時
短影片(TS 輸出)~50MB(30 分片)4~8s<1s~9s
短影片(MP4 輸出)~50MB(30 分片)4~8s~3s~11s
長影片(TS 輸出)~500MB(200 分片)6~45s~2s~47s
長影片(MP4 輸出)~500MB(200 分片)6~45s~15s~60s
超大檔案(TS 輸出)~2GB(800 分片)8~3min~8s~3min
DASH 音影片分離~300MB 影片 + ~30MB 音訊4~30s~12s~42s

影響因素

因素影響程度說明
CDN 速度分片下載速度的上限
並行執行緒數4-6 執行緒通常是最優區間
輸出格式TS 輸出跳過 FFmpeg,合併更快
SharedArrayBuffer多執行緒 FFmpeg 比單執行緒快 40-60%
加密串流Web Crypto API 硬體加速,解密開銷極小

並行數與效能的關係

並行數 1:████████████████████████████  慢,頻寬利用率低
並行數 2:██████████████████  較快,基本可用
並行數 4:██████████  快,推薦預設值
並行數 6:████████  很快,接近最優
並行數 8:████████  與 6 基本持平,邊際收益遞減
並行數 12+:████████  可能觸發 CDN 限速,反而變慢
選擇 TS 輸出格式可以跳過 FFmpeg 合併階段,對於超大檔案能節省 10-20% 的總耗時。TS 檔案可以用 VLC、PotPlayer、IINA 等播放器直接播放。關於下載速度慢的更多排查方法,請參閱 常見問題排查 — 下載速度慢

錯誤邊界與異常傳播

下載引擎採用分層錯誤處理架構,確保每一層的異常都能被恰當捕獲並轉化為使用者友善的提示:

┌─────────────────────────────────────────────┐
│                 UI 層                        │
│  · 展示使用者提示(Toast/彈窗)              │
│  · 更新下載狀態為「失敗」                    │
│  · 提供重試/回饋入口                         │
└──────────────────┬──────────────────────────┘
                   │ 捕獲所有異常
┌──────────────────▼──────────────────────────┐
│              引擎門面層                       │
│  · 統一異常格式(StreamMergeError)          │
│  · 附加上下文資訊(URL、分片索引、階段)      │
│  · 決定是否可重試                            │
└──────────────────┬──────────────────────────┘
                   │
    ┌──────────────┼──────────────┐
    ▼              ▼              ▼
┌────────┐  ┌──────────┐  ┌──────────┐
│下載模組│  │處理模組  │  │寫入模組  │
│        │  │          │  │          │
│FetchErr│  │CryptoErr │  │WriteErr  │
└────────┘  └──────────┘  └──────────┘

錯誤類型對應

底層錯誤引擎錯誤類型使用者提示可重試
FetchError(403)AuthError"存取被拒絕"
FetchError(404)NotFoundError"資源已過期"
FetchError(5xx)ServerError"伺服器錯誤"
TypeError: Failed to fetchNetworkError"網路連線失敗"
DOMException: AbortErrorCancelledError無提示(靜默)
CryptoErrorDecryptError"解密失敗"
FFmpegErrorMergeError"合併失敗"
QuotaExceededErrorStorageError"儲存空間不足"
關於各類錯誤的詳細排查步驟,請參閱 常見問題排查。關於引擎的已知限制和邊界情況,請參閱 已知問題。關於如何為引擎貢獻錯誤處理改進,請參閱 貢獻指南

相關文件