项目架构
本文档面向希望深入理解 FlowPick 内部设计的开发者,涵盖系统架构、模块职责、数据流和关键设计决策。

系统架构概览
FlowPick 由两个主要产品形态组成:浏览器扩展和在线工具网站。两者共享核心下载引擎,但在媒体检测和网络请求处理上有所不同。
┌─────────────────────────────────────────────────────────┐
│ FlowPick │
│ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ 浏览器扩展 │ │ 在线工具网站 │ │
│ │ │ │ │ │
│ │ · 网络请求监控 │ │ · M3U8 下载器 │ │
│ │ · 媒体自动检测 │ │ · DASH 下载器 │ │
│ │ · 弹出窗口 UI │ │ · 文档系统 │ │
│ │ · 跨域请求能力 │ │ · 首页展示 │ │
│ └────────┬─────────┘ └────────────┬─────────────┘ │
│ │ │ │
│ └───────────┬───────────────┘ │
│ │ │
│ ┌───────────▼───────────────┐ │
│ │ 核心下载引擎 │ │
│ │ │ │
│ │ · useStreamMerge.ts │ │
│ │ · useFFmpeg.ts │ │
│ │ · 分片下载与并发控制 │ │
│ │ · AES-128 解密 │ │
│ │ · 流式文件写入 │ │
│ │ · 格式转换 │ │
│ └───────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
两种产品形态的定位:
| 维度 | 浏览器扩展 | 在线工具网站 |
|---|---|---|
| 目标用户 | 日常高频使用者 | 临时使用者、无法安装扩展的环境 |
| 核心优势 | 自动检测、一键下载 | 无需安装、跨平台 |
| 技术约束 | Manifest V3 限制 | 同源策略限制 |
| 入口方式 | 工具栏图标点击 | 直接访问网页 URL |
模块详解
核心下载引擎
下载引擎由两个 composable 组成,位于 app/composables/。

useStreamMerge.ts
负责流媒体分片的下载、解密、合并和写入。这是整个系统最核心的模块。
主要职责:
| 功能 | 实现方式 |
|---|---|
| 分片并发下载 | Worker Pool 模式,共享任务计数器 |
| 重试机制 | 指数退避,最多 3 次 |
| AES-128 解密 | Web Crypto API |
| 分片拼接 | 二进制直接拼接(TS)或 FFmpeg 转封装(MP4) |
| 文件写入 | FSA → StreamSaver → Blob 三级降级 |
| 进度追踪 | 滑动窗口速度计算 + 剩余时间估算 |
| 内存管理 | 采样预估 + 阈值检查 + 策略选择 |
导出接口:
export interface StreamMergeOptions {
segments: ArrayBuffer[] | AsyncGenerator<ArrayBuffer>
totalSegments: number
filename: string
outputFormat?: 'mp4' | 'ts'
onProgress?: (progress: StreamMergeProgress) => void
signal?: AbortSignal
}
export interface StreamMergeProgress {
phase: 'downloading' | 'merging'
percent: number
downloadedBytes: number
totalBytes: number
speed: string
eta: string
}
useFFmpeg.ts
负责 FFmpeg WASM 的加载、生命周期管理和媒体处理命令执行。
主要职责:
| 功能 | 实现方式 |
|---|---|
| WASM 加载 | 按需加载,支持多线程/单线程自动切换 |
| 多线程检测 | 检查 SharedArrayBuffer 和 crossOriginIsolated |
| TS → MP4 转封装 | ffmpeg -i input.ts -c copy output.mp4 |
| 音视频合并 | 分别写入虚拟 FS,执行合并命令 |
| FMP4 重组 | 处理 DASH 的初始化段 + 媒体段 |
| 进度回调 | 解析 FFmpeg 输出的 time 信息 |
导出接口:
export const useFFmpeg = () => {
return {
ffmpeg: Ref<FFmpegType | null>
loaded: Ref<boolean>
loading: Ref<boolean>
error: Ref<string | null>
load: () => Promise<void>
mergeToMP4: (options: MergeOptions) => Promise<ArrayBuffer>
mergeAudioVideo: (options: MergeAudioVideoOptions) => Promise<ArrayBuffer>
mergeFmp4: (options: MergeFmp4Options) => Promise<ArrayBuffer>
}
}
页面模块
M3U8 下载器 (m3u8-downloader.vue)
处理 HLS 流媒体协议的下载页面。
处理流程:
用户输入 URL
↓
fetch M3U8 内容
↓
m3u8-parser 解析
↓
┌─ Master Playlist? ──→ 展示画质列表 → 用户选择
│
└─ Media Playlist? ──→ 提取分片列表
↓
检查 #EXT-X-KEY(加密信息)
↓
下载密钥文件(如有)
↓
并发下载分片 + 解密
↓
合并/转封装
↓
写入磁盘
关键依赖:
m3u8-parser:解析 M3U8 清单useStreamMerge:分片下载与合并useFFmpeg:格式转换
DASH 下载器 (dash-downloader.vue)
处理 DASH 流媒体协议的下载页面。
处理流程:
用户输入 URL
↓
fetch MPD 内容
↓
mpd-parser 解析
↓
检查 ContentProtection(DRM)
↓
提取 AdaptationSet(视频/音频)
↓
展示流列表 → 用户选择
↓
下载初始化段(如有)
↓
并发下载媒体段 + 解密
↓
FMP4 重组 / 音视频合并
↓
写入磁盘
关键依赖:
mpd-parser:解析 MPD 清单useStreamMerge:分片下载与合并useFFmpeg:FMP4 重组与音视频合并
文档系统
基于 Nuxt Content v3 构建,文档以 Markdown 文件形式存放在 content/ 目录。
目录结构:
content/zh-Hans/
├── 1.docs/ # 文档主体
│ ├── 1.getting-started/ # 快速开始
│ ├── 2.features/ # 功能特性
│ ├── 3.advanced/ # 高级指南
│ ├── 5.troubleshooting/ # 故障排除
│ └── 6.developer/ # 开发者文档
├── 4.changelog/ # 更新日志
└── 6.legal/ # 法律条款
导航系统:每个目录下的 .navigation.yml 定义该分类的标题和图标。Nuxt Content 自动收集所有文档并生成侧边栏导航。
文档布局:app/layouts/docs.vue 提供统一的文档页面布局,包含顶部导航、侧边栏和内容区域。
数据流
完整下载流程
┌─────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 获取清单 │ → │ 解析清单 │ → │ 下载分片 │ → │ 合并写入 │
└─────────┘ └──────────┘ └──────────┘ └──────────┘
│ │ │ │
▼ ▼ ▼ ▼
fetch() m3u8-parser Worker Pool useStreamMerge
mpd-parser AES-128 解密 useFFmpeg
fetch() 并发 FSA/SS/Blob
各阶段详细说明:
| 阶段 | 输入 | 处理 | 输出 | 可能失败原因 |
|---|---|---|---|---|
| 获取清单 | M3U8/MPD URL | HTTP GET 请求 | 清单文本 | 网络错误、URL 过期、404 |
| 解析清单 | 清单文本 | m3u8/mpd-parser 解析 | 分片列表 + 元数据 | 格式不标准、DRM 保护 |
| 下载分片 | 分片 URL 列表 | Worker Pool 并发下载 | ArrayBuffer 数组 | CORS、限流、token 过期 |
| 合并写入 | ArrayBuffer 数组 | 拼接/转封装 + 写入 | 磁盘文件 | 内存不足、写入权限 |
写入策略选择流程
开始写入
│
▼
File System Access API 可用?
│
├── 是 → 使用 FSA 流式写入(最优)
│ · 16MB 缓冲区
│ · 支持任意大小
│
└── 否 → StreamSaver 可用?
│
├── 是 → 使用 StreamSaver 流式写入
│ · Service Worker 代理
│ · 支持大文件
│
└── 否 → 预估文件大小
│
├── < 1.5GB → Blob 模式
│ · 全部加载到内存
│ · 触发浏览器下载
│
└── > 1.5GB → 拒绝下载
· 提示切换浏览器
关键设计决策
为什么选择 FFmpeg WASM 而不是服务端处理?
决策:所有媒体处理在浏览器端完成。
理由:
- 隐私保护:用户文件不上传服务器
- 零服务器成本:无需维护转码服务器集群
- 即时可用:无需等待上传和排队
- 离线能力:理论上可在 PWA 模式下离线使用
代价:
- 性能低于原生 FFmpeg(约 10-20%)
- 首次加载需要下载 WASM 文件(约 8MB)
- 受浏览器内存限制
为什么使用三级写入降级策略?
决策:FSA → StreamSaver → Blob 的优先级链。
理由:
- FSA API 是最优方案,但仅 Chrome/Edge 支持
- StreamSaver 兼容性更广,但依赖 Service Worker
- Blob 是兜底方案,确保所有浏览器都能下载
代价:
- 需要维护三套写入逻辑
- 降级行为可能导致用户体验不一致
为什么并发下载使用 Worker Pool 而非预先分片?
决策:使用共享计数器 + 动态任务分配。
理由:
- 分片大小不均时,预先分片会导致部分 Worker 空闲
- 共享计数器确保所有 Worker 持续工作直到任务完成
- 实现简单,无需复杂的负载均衡逻辑
为什么扩展和在线工具共享核心引擎?
决策:useStreamMerge 和 useFFmpeg 作为独立 composable,不依赖扩展 API。
理由:
- 代码复用,避免维护两套下载逻辑
- 在线工具可作为扩展功能的演示和降级方案
- 便于测试:核心逻辑可在普通网页环境中调试
依赖关系图
m3u8-downloader.vue ──────┐
├──→ useStreamMerge.ts ──→ Web Crypto API
dash-downloader.vue ──────┤ │ fetch API
│ ├──→ useFFmpeg.ts ──→ @ffmpeg/ffmpeg
扩展弹出窗口 ─────────────┘ │ @ffmpeg/util
├──→ File System Access API
├──→ StreamSaver.js
└──→ Blob API
文档系统 ──→ @nuxt/content ──→ Markdown 文件
└──→ @nuxt/ui ──→ Tailwind CSS
依赖关系说明:
| 依赖 | 类型 | 说明 |
|---|---|---|
useStreamMerge → useFFmpeg | 可选依赖 | 仅 MP4 输出或音视频合并时需要 |
useStreamMerge → Web Crypto API | 条件依赖 | 仅 AES-128 加密流时需要 |
useStreamMerge → FSA / StreamSaver / Blob | 互斥选择 | 按优先级选择一种写入方式 |
页面 → useStreamMerge | 直接依赖 | 所有下载页面都依赖核心引擎 |
文档系统 → @nuxt/content | 框架依赖 | Nuxt Content 模块 |
扩展架构
扩展部分(不在本仓库中)的结构:
flowpick-extension/
├── manifest.json # Manifest V3 配置
├── background/
│ └── service-worker.js # Service Worker(网络监控)
├── popup/
│ ├── popup.html # 弹出窗口
│ ├── popup.js # 弹出窗口逻辑
│ └── popup.css # 弹出窗口样式
├── content/
│ └── content.js # 内容脚本(页面注入)
└── assets/
└── icons/ # 扩展图标
扩展通过 webRequest API 监控网络请求,检测媒体资源后通过消息传递将 URL 列表发送给弹出窗口。弹出窗口复用在线工具的核心下载逻辑。
扩展通信流程:
Content Script ←→ Background Service Worker ←→ Popup UI
│ │ │
页面 DOM 访问 网络请求监控 用户交互界面
媒体元素检测 M3U8/MPD 过滤 下载触发与管理