@meframe/server 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -153,6 +153,10 @@
153
153
  - 必须在目标机型与微信版本上做真实验证。
154
154
  - 若验证失败,建议由上层应用接入可选后处理(例如 remux 成 moov 前置 MP4),而不是把 ffmpeg 作为 `@meframe/server` 的强依赖。
155
155
 
156
+ 补充:Linux 上 WebCodecs AAC 编码不可用时的“video-only 降级 + FFmpeg 后处理混音”的背景与完整方案见:
157
+
158
+ - `packages/server/docs/AUDIO_MIX_FFMPEG_PLAN.md`
159
+
156
160
  ### 10. 实现顺序建议(可按里程碑推进)
157
161
 
158
162
  - **M1:core 增加流式 mux 输出模式**(保留 Blob 模式,确保回归风险可控)。
@@ -0,0 +1,145 @@
1
+ ## Linux 导出 MP4 无音频:背景与 FFmpeg 后处理方案(规划)
2
+
3
+ ### 1. 背景:为什么会导出“无声 MP4”
4
+
5
+ `@meframe/server` 的导出本质是在 **真实 Chromium** 中运行 `@meframe/core` 的浏览器导出链路(WebAudio 混音 + WebCodecs 编码 + MP4 mux)。
6
+
7
+ 在部分 Linux 环境(即使使用 `google-chrome-stable`),WebCodecs 的 **AAC 音频编码**可能不可用:
8
+
9
+ - `AudioEncoder.isConfigSupported({ codec: 'mp4a.40.2', ... })` 返回 `supported: false`
10
+ - `@meframe/core` 在创建音频编码器(AAC)时会失败
11
+
12
+ 为了避免任务直接失败、至少产出可播放的视频文件,`@meframe/core` 已实现**能力探测与降级**:
13
+
14
+ - AAC 支持不足时:导出 **video-only MP4**(不创建音轨、不启动音频编码)
15
+ - 并通过导出进度 message 给出原因(可观测)
16
+
17
+ 这解决了 “Linux 导出直接报错” 的大问题,但也带来一个必然结果:**导出 MP4 没有声音**。
18
+
19
+ ### 2. 为什么不能简单“把源视频的音轨 copy 回去”
20
+
21
+ 在业务真实使用里,音频通常来自**时间轴混音**,而不是单一源视频的原声。
22
+
23
+ `packages/server/examples/local/video-model.json` 展示了典型结构:
24
+
25
+ - `tracks[].kind === 'audio'`:
26
+ - `speech`:多个语音片段(多段 mp3),每段有 `startUs` / `durationUs` / `audioConfig.volume`
27
+ - `bgm`:通常为覆盖全片的背景音乐(单段 mp3),有 `audioConfig.volume`
28
+ - `tracks[].kind === 'video'` 的每个 clip 也可能带 `audioConfig`:这代表**视频原声是否参与混音**(需求侧需明确,一般是“要”)
29
+
30
+ 因此,“直接复用某一个源文件音轨”无法得到正确结果。
31
+
32
+ ### 3. 目标:在 Linux 环境产出“含混音音频的 MP4(H.264 + AAC)”
33
+
34
+ 目标产物:
35
+
36
+ - 容器:MP4(移动端原生播放器覆盖率最高)
37
+ - 视频:保持 `@meframe/core` 输出的 H.264(`-c:v copy`,避免二次转码)
38
+ - 音频:服务端用 FFmpeg 生成混音音轨并编码 AAC(`-c:a aac`)
39
+
40
+ ### 4. 总体方案:Core 降级 + Server 后处理(FFmpeg)
41
+
42
+ #### 4.1 Core 行为(已完成)
43
+
44
+ - 若 WebCodecs AAC 不支持:导出 video-only MP4(仍可流式 multipart upload)
45
+
46
+ #### 4.2 Server 后处理(规划)
47
+
48
+ 当检测到导出为 video-only(或业务强制要求音频)时,在 `@meframe/server` 增加一个“可选后处理”步骤:
49
+
50
+ 1. 从 store 获取已上传的 **video-only MP4** 的可下载 URL(通常是 presigned GET URL)
51
+ 2. 从 `video-model.json` 中解析所有参与混音的音频来源:
52
+ - speech track clips(多个资源)
53
+ - bgm track clips(可能是 loop/长段)
54
+ - video track clips 的原声(从对应视频资源 URL 提取音频)
55
+ 3. 生成 FFmpeg `-filter_complex`:
56
+ - 对每个 clip 执行:
57
+ - `atrim`:按 `trimStartUs`/`durationUs` 截取
58
+ - `adelay`:按 `startUs` 对齐到时间轴
59
+ - `volume`:按 `audioConfig.volume` 调整
60
+ - muted:直接跳过
61
+ - 把所有 clip 进行 `amix` 叠加为单一混音流
62
+ 4. 将混音流编码为 AAC,并与 video-only MP4 mux 成最终 MP4:
63
+ - 视频:`-map 0:v:0 -c:v copy`
64
+ - 音频:`-map [mixed] -c:a aac -b:a 128k`(比特率可配置)
65
+ 5. 将最终 MP4 上传回 store(覆盖原 key 或写入新 key)
66
+
67
+ > 注意:此方案把“混音 + AAC 编码”的工作完全交给 server/FFmpeg,绕过 WebCodecs AAC 的平台差异。
68
+
69
+ ### 5. FFmpeg filtergraph 生成规则(从 model 到命令)
70
+
71
+ #### 5.1 输入实体
72
+
73
+ - `model.durationUs`
74
+ - `model.tracks[]`:
75
+ - `track.kind in ['audio', 'video']`
76
+ - `clip.startUs`, `clip.durationUs`, `clip.trimStartUs?`
77
+ - `clip.audioConfig.muted`, `clip.audioConfig.volume`
78
+ - `model.resources[resourceId].uri`
79
+
80
+ #### 5.2 clip 处理(概念)
81
+
82
+ 对每个 clip 生成一个音频分支:
83
+
84
+ - **选择输入**:
85
+ - speech/bgm clip:输入为对应 `resources[resourceId].uri`(音频文件)
86
+ - video clip 原声:输入为对应视频 `resources[resourceId].uri`,取其音轨
87
+ - **裁剪**:
88
+ - `trimStartUs` 用 `-ss` 或 `atrim=start=...`
89
+ - `durationUs` 用 `-t` 或 `atrim=duration=...`
90
+ - **对齐**:
91
+ - `adelay=${startMs}|${startMs}`(双声道示例)
92
+ - **音量**:
93
+ - `volume=${volume}`
94
+
95
+ 最后:
96
+
97
+ - `amix=inputs=N:normalize=0` 叠加所有分支
98
+ - 视需求可加 `alimiter` 避免削波
99
+
100
+ #### 5.3 时间单位换算
101
+
102
+ - `startUs` / `durationUs` / `trimStartUs`:microseconds
103
+ - `adelay`:milliseconds
104
+ - `atrim`:seconds(可用小数)
105
+
106
+ ### 6. 资源 URL 读取
107
+
108
+ FFmpeg 可以直接读取 `https://...` URL(需要 server 侧网络可达)。
109
+
110
+ 约束/注意:
111
+
112
+ - 若资源需要鉴权 header(例如 token),FFmpeg 需通过 `-headers` 传入;因此 “resolveResourceUrl” 应支持返回 headers。
113
+ - 若资源服务端不允许被 FFmpeg 直连(内网限制、短时签名、SNI/TLS 限制),则需要先下载到本地临时文件再处理(这会改变内存/磁盘策略)。
114
+
115
+ ### 7. 与 @meframe/server 的集成点(建议设计)
116
+
117
+ 当前 `@meframe/server` 的对外接口 `ServerExporterOptions` 只有 multipart store、浏览器配置、进度回调等。
118
+
119
+ 建议保持默认行为不变,新增一个**可选**后处理开关(规划):
120
+
121
+ - `postProcess.ffmpegAudioMix.enabled`
122
+ - `postProcess.ffmpegAudioMix.resolveUploadedUrl(key)`:获取导出后 video-only MP4 的可下载 URL(presigned GET)
123
+ - `postProcess.ffmpegAudioMix.outputKey`:最终 MP4 输出 key(可覆盖原 key 或写新 key)
124
+ - `ffmpegPath` / `audioBitrate` 等可配
125
+
126
+ 实现上,后处理可放在:
127
+
128
+ - `ServerExporterBase.exportToStore()` 完成 multipart upload 之后、返回结果之前
129
+
130
+ ### 8. 可观测性与失败策略
131
+
132
+ - 导出阶段:
133
+ - 若 core 降级为 video-only,应通过 progress message 记录原因(已在 core 侧实现)
134
+ - 后处理阶段(FFmpeg):
135
+ - 应单独上报阶段(例如 `stage: 'postprocess'`,`progress` 可粗粒度)
136
+ - FFmpeg 失败时:
137
+ - 可配置策略:失败则返回无声视频(保持可用)或失败则 job 失败(更严格)
138
+
139
+ ### 9. 为什么“不建议先导出 WebM 再转 MP4”
140
+
141
+ 从架构复杂度与成本看:
142
+
143
+ - `@meframe/core` 当前没有 WebM mux 实现(需要新增 WebM mux 与相关测试)
144
+ - WebM 常见需要 VP8/VP9/AV1 + Opus;转 MP4 通常意味着**视频也要重编码**(画质/性能代价大)
145
+ - 而本方案可以 **视频 copy**,只让 FFmpeg 处理音频,整体更可控、改动更小
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meframe/server",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Server-side exporter for @meframe/core (browser-driven, multipart upload via injected store)",
5
5
  "type": "module",
6
6
  "main": "./dist/esm/index.js",
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "dependencies": {
22
22
  "puppeteer-core": "^23.5.0",
23
- "@meframe/core": "0.2.9"
23
+ "@meframe/core": "0.3.1"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@aws-sdk/client-s3": "^3.758.0",
@@ -30,7 +30,7 @@
30
30
  "vite": "^5.4.20",
31
31
  "typescript": "^5.3.3",
32
32
  "vitest": "^1.3.1",
33
- "@meframe/adapter-medeo": "0.0.2"
33
+ "@meframe/adapter-medeo": "0.0.5"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=18.0.0"