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

架構概覽
下載引擎由三個核心模組組成,資料按管線方式依次流經各模組:
┌─────────────────────────────────────────────────┐
│ 下載引擎 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ 分片下載 │→│ 分片處理 │→│ 檔案寫入 │ │
│ │ 模組 │ │ 模組 │ │ 模組 │ │
│ │ │ │ │ │ │ │
│ │ · 並行 │ │ · 解密 │ │ · FSA 串流 │ │
│ │ · 重試 │ │ · 拼接 │ │ · StreamSaver │ │
│ │ · 限速 │ │ · 轉封裝 │ │ · Blob 兜底 │ │
│ └──────────┘ └──────────┘ └───────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 記憶體安全管理器 │ │
│ │ · 大小預估 · 閾值檢查 · 策略選擇 │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
資料流全景
從使用者點擊下載到檔案寫入磁碟的完整鏈路:
使用者點擊下載
│
▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 1. 清單解析 │ ──→ │ 2. 分片下載 │ ──→ │ 3. 分片處理 │
│ │ │ │ │ │
│ · 下載 M3U8 │ │ · Worker Pool│ │ · AES 解密 │
│ · 解析分片列表│ │ · 並行控制 │ │ · TS 拼接 │
│ · 提取金鑰 │ │ · 指數退避 │ │ · FFmpeg 轉封│
│ · 預估大小 │ │ · 進度上報 │ │ · 音影片合併 │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 6. 完成通知 │ ←── │ 5. 清理回收 │ ←── │ 4. 檔案寫入 │
│ │ │ │ │ │
│ · 桌面通知 │ │ · 釋放記憶體 │ │ · FSA 串流 │
│ · 路徑複製 │ │ · 清理暫存檔 │ │ · StreamSaver│
│ · 佇列推進 │ │ · 重設狀態 │ │ · Blob 兜底 │
└──────────────┘ └──────────────┘ └──────────────┘
引擎生命週期
下載引擎的一次完整執行經歷以下狀態轉換:
[閒置]
│ 使用者觸發下載
▼
[初始化] ──── 載入 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 串流的首尾分片通常較小,中間分片較大)。
非同步生成器模式
對於超大檔案(分片數量 > 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 次失敗 | 400ms | 400ms |
| 第 2 次失敗 | 800ms | 1200ms |
| 第 3 次失敗 | 1600ms | 2800ms |
不觸發重試的情況:
- HTTP 4xx 客戶端錯誤(403、404 等)—— 重試無意義
- CORS 錯誤 —— 策略問題,重試不會改變結果
- 加密不支援錯誤 —— 無法透過重試解決
錯誤分類
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 | 網路中斷 | "網路連線失敗,請檢查網路" |
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
}
解密流程:
- 從 M3U8 的
#EXT-X-KEY標籤提取金鑰 URI 和 IV - 下載金鑰檔案(通常是 16 位元組的二進位檔案)
- 使用 AES-128-CBC 模式解密每個分片
- 如果未指定 IV,使用分片序號作為 IV(HLS 規範預設行為)
效能考量:
- Web Crypto API 使用硬體加速,解密速度通常不是瓶頸
- 每個分片獨立解密,可以與下載並行進行
- 金鑰只需下載一次,快取在記憶體中
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 進行容器轉換。詳見 格式轉換 文件。
DASH 串流的特殊處理
DASH 串流與 HLS 串流在分片處理上有顯著差異:
HLS 串流處理:
分片 0 → 分片 1 → 分片 2 → ... → 拼接 → 輸出
DASH 串流處理(音影片分離):
初始化段 → 影片分片 0 → 影片分片 1 → ... ─┐
├→ FFmpeg 合併 → 輸出
初始化段 → 音訊分片 0 → 音訊分片 1 → ... ─┘
DASH 的 FMP4(Fragmented MP4)格式需要特殊處理:
- 初始化段(
ftyp+moovbox)必須放在檔案開頭 - 媒體段(
moof+mdatbox)按順序追加 - 音影片分離時,需要分別下載影片和音訊軌道,最後透過 FFmpeg 合併
檔案寫入模組
寫入模組按優先級選擇策略,確保在盡可能多的瀏覽器中工作。

策略一: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.html和streamsaver-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 API | StreamSaver.js | Blob |
|---|---|---|---|
| 記憶體佔用 | 16MB(恆定) | 低(串流) | 等於檔案大小 |
| 檔案大小上限 | 無限制 | 無限制 | 1.5GB |
| 瀏覽器要求 | Chrome/Edge 86+ | Service Worker 支援 | 所有瀏覽器 |
| 目錄持久化 | 支援 | 不支援 | 不支援 |
| 下載體驗 | 最佳 | 良好 | 一般 |
記憶體安全管理

大小預估
在開始下載前,引擎會取樣預估總檔案大小:
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 級別時,引擎會:
- 暫停新的分片下載請求
- 優先將已下載的分片寫入磁碟
- 釋放已完成寫入的分片緩衝區
進度追蹤
速度計算
使用滑動視窗計算即時下載速度:
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 限速,反而變慢
錯誤邊界與異常傳播
下載引擎採用分層錯誤處理架構,確保每一層的異常都能被恰當捕獲並轉化為使用者友善的提示:
┌─────────────────────────────────────────────┐
│ UI 層 │
│ · 展示使用者提示(Toast/彈窗) │
│ · 更新下載狀態為「失敗」 │
│ · 提供重試/回饋入口 │
└──────────────────┬──────────────────────────┘
│ 捕獲所有異常
┌──────────────────▼──────────────────────────┐
│ 引擎門面層 │
│ · 統一異常格式(StreamMergeError) │
│ · 附加上下文資訊(URL、分片索引、階段) │
│ · 決定是否可重試 │
└──────────────────┬──────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌────────┐ ┌──────────┐ ┌──────────┐
│下載模組│ │處理模組 │ │寫入模組 │
│ │ │ │ │ │
│FetchErr│ │CryptoErr │ │WriteErr │
└────────┘ └──────────┘ └──────────┘
錯誤類型對應:
| 底層錯誤 | 引擎錯誤類型 | 使用者提示 | 可重試 |
|---|---|---|---|
FetchError(403) | AuthError | "存取被拒絕" | 否 |
FetchError(404) | NotFoundError | "資源已過期" | 否 |
FetchError(5xx) | ServerError | "伺服器錯誤" | 是 |
TypeError: Failed to fetch | NetworkError | "網路連線失敗" | 是 |
DOMException: AbortError | CancelledError | 無提示(靜默) | — |
CryptoError | DecryptError | "解密失敗" | 否 |
FFmpegError | MergeError | "合併失敗" | 否 |
QuotaExceededError | StorageError | "儲存空間不足" | 否 |
相關文件
- 影片嗅探 — HLS/DASH 清單解析與加密偵測
- 格式轉換 — FFmpeg WASM 引擎與 TSToMP4Muxer
- 批次下載 — 佇列排程與並行控制
- 線上工具 — 寫入策略在線上工具中的實際應用
- 瀏覽器相容性 — 各瀏覽器 API 支援與降級策略
- 隱私與安全 — 本機處理,零資料上傳
- 設定參考 — 並行數、輸出格式等設定項
- 專案架構 — 引擎在整體系統中的位置與模組互動
- 貢獻指南 — 為下載引擎貢獻程式碼
- 直播回放儲存 — 超大檔案下載的實際場景
- 線上課程下載 — 課程影片下載場景
- 影片平台下載 — 主流平台下載實踐
- 常見問題排查 — 下載失敗診斷
- 已知問題 — 下載引擎的已知限制
- 常見問題解答 — 下載相關高頻問題