下载引擎架构

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"存储空间不足"
关于各类错误的详细排查步骤,请参阅 常见问题排查。关于引擎的已知限制和边界情况,请参阅 已知问题。关于如何为引擎贡献错误处理改进,请参阅 贡献指南

相关文档