@meframe/server 0.0.1
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.
- package/README.md +63 -0
- package/dist/ServerExporter.d.ts +17 -0
- package/dist/ServerExporter.d.ts.map +1 -0
- package/dist/ServerExporter.js +251 -0
- package/dist/ServerExporter.js.map +1 -0
- package/dist/browser/puppeteer-core-launcher.d.ts +3 -0
- package/dist/browser/puppeteer-core-launcher.d.ts.map +1 -0
- package/dist/browser/puppeteer-core-launcher.js +12 -0
- package/dist/browser/puppeteer-core-launcher.js.map +1 -0
- package/dist/browser/types.d.ts +21 -0
- package/dist/browser/types.d.ts.map +1 -0
- package/dist/browser/types.js +2 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/mock/MockMultipartObjectStore.d.ts +37 -0
- package/dist/mock/MockMultipartObjectStore.d.ts.map +1 -0
- package/dist/mock/MockMultipartObjectStore.js +34 -0
- package/dist/mock/MockMultipartObjectStore.js.map +1 -0
- package/dist/runner/runner.mjs +13932 -0
- package/dist/types.d.ts +95 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/ARCHITECTURE.md +161 -0
- package/docs/INTEGRATION.md +172 -0
- package/package.json +64 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export interface MultipartObjectStore {
|
|
2
|
+
createMultipartUpload(input: {
|
|
3
|
+
key: string;
|
|
4
|
+
contentType: string;
|
|
5
|
+
metadata?: Record<string, string>;
|
|
6
|
+
}): Promise<{
|
|
7
|
+
uploadId: string;
|
|
8
|
+
}>;
|
|
9
|
+
getUploadPartUrl(input: {
|
|
10
|
+
key: string;
|
|
11
|
+
uploadId: string;
|
|
12
|
+
partNumber: number;
|
|
13
|
+
expiresInSeconds?: number;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
url: string;
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
}>;
|
|
18
|
+
completeMultipartUpload(input: {
|
|
19
|
+
key: string;
|
|
20
|
+
uploadId: string;
|
|
21
|
+
parts: Array<{
|
|
22
|
+
partNumber: number;
|
|
23
|
+
etag: string;
|
|
24
|
+
}>;
|
|
25
|
+
}): Promise<void>;
|
|
26
|
+
abortMultipartUpload(input: {
|
|
27
|
+
key: string;
|
|
28
|
+
uploadId: string;
|
|
29
|
+
}): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export interface ExportProgressEvent {
|
|
32
|
+
progress: number;
|
|
33
|
+
stage: string;
|
|
34
|
+
timeUs?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface ServerExporterOptions {
|
|
37
|
+
store: MultipartObjectStore;
|
|
38
|
+
runner?: {
|
|
39
|
+
/**
|
|
40
|
+
* Optional pre-bundled runner module code to inject into the page.
|
|
41
|
+
* When provided, @meframe/server will not read dist/runner/runner.mjs from disk.
|
|
42
|
+
*/
|
|
43
|
+
moduleCode?: string;
|
|
44
|
+
};
|
|
45
|
+
puppeteer?: {
|
|
46
|
+
headless?: boolean;
|
|
47
|
+
executablePath?: string;
|
|
48
|
+
launchArgs?: string[];
|
|
49
|
+
userDataDirBase?: string;
|
|
50
|
+
};
|
|
51
|
+
concurrency?: {
|
|
52
|
+
maxJobs: number;
|
|
53
|
+
};
|
|
54
|
+
defaults?: {
|
|
55
|
+
keyPrefix?: string;
|
|
56
|
+
partSizeBytes?: number;
|
|
57
|
+
contentType?: string;
|
|
58
|
+
};
|
|
59
|
+
onProgress?: (evt: {
|
|
60
|
+
jobId: string;
|
|
61
|
+
} & ExportProgressEvent) => void;
|
|
62
|
+
logger?: {
|
|
63
|
+
info(...args: any[]): void;
|
|
64
|
+
warn(...args: any[]): void;
|
|
65
|
+
error(...args: any[]): void;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export interface ExportToStoreInput {
|
|
69
|
+
model: any;
|
|
70
|
+
exportOptions: any;
|
|
71
|
+
/**
|
|
72
|
+
* URL to navigate before injecting the runner.
|
|
73
|
+
* This is important for Web APIs that require a secure context (e.g. WebCodecs).
|
|
74
|
+
* Recommended values:
|
|
75
|
+
* - http://127.0.0.1:<port>/ (local dev)
|
|
76
|
+
* - https://your-domain/ (production)
|
|
77
|
+
*/
|
|
78
|
+
pageUrl?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Worker files base path used by @meframe/core inside the browser.
|
|
81
|
+
*
|
|
82
|
+
* Defaults to core's default (usually `/meframe-workers` in production builds).
|
|
83
|
+
*
|
|
84
|
+
* You can pass an absolute URL to use a separately hosted worker bundle, e.g.
|
|
85
|
+
* `https://medeo.app/fe-assets/meframe-workers-2` (ensure CORS is configured if cross-origin).
|
|
86
|
+
*/
|
|
87
|
+
workerPath?: string;
|
|
88
|
+
key?: string;
|
|
89
|
+
metadata?: Record<string, string>;
|
|
90
|
+
abortSignal?: AbortSignal;
|
|
91
|
+
}
|
|
92
|
+
export interface ExportToStoreResult {
|
|
93
|
+
key: string;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
## 服务端导出架构(`@meframe/server`)
|
|
2
|
+
|
|
3
|
+
> 本文档用于描述 `packages/server` 的整体架构与实现约束,目标是**自成体系**、可直接指导后续开发与迭代。
|
|
4
|
+
|
|
5
|
+
快速上手与业务集成请读:
|
|
6
|
+
|
|
7
|
+
- `packages/server/docs/INTEGRATION.md`
|
|
8
|
+
|
|
9
|
+
### 1. 背景
|
|
10
|
+
|
|
11
|
+
`@meframe/core` 是面向 Web 的预览/导出一体化音视频处理工具。当前导出链路依赖浏览器能力(OPFS、WebCodecs、WebAudio、Worker 等),并最终生成一个完整 MP4 `Blob` 作为输出。
|
|
12
|
+
|
|
13
|
+
现在需要增加“服务端导出”能力:在 Node 环境提供一个 npm 包(对外暴露函数/类 API,而非 HTTP 接口),通过自动化驱动 Chromium 执行与 Web 端一致的导出流程,并将产物上传到对象存储(例如 S3)。最终用户的使用场景是“下载一个 `.mp4` 文件,在手机本地/微信里打开播放”。
|
|
14
|
+
|
|
15
|
+
### 2. 目标与约束
|
|
16
|
+
|
|
17
|
+
- **复用现有浏览器导出链路**:尽量复用 `@meframe/core` 的现有导出(稳定性、功能一致性)。
|
|
18
|
+
- **低内存**:不能把完整 MP4 保存在 Node 内存中再上传(并发时内存峰值会随成片大小线性放大)。
|
|
19
|
+
- **存储无关**:`@meframe/server` 不依赖任何云厂商 SDK;由上层注入“分片上传”实现,便于开源与迁移。
|
|
20
|
+
- **库优先(非 HTTP)**:对外只提供 Node API;队列/HTTP/鉴权等由业务侧自行封装。
|
|
21
|
+
- **可取消、可观测**:支持 AbortSignal 取消;支持进度事件回调(转发 core 的导出进度)。
|
|
22
|
+
- **资源 URL 不改写**:调用方传入的 model 中 `resource.uri` 必须是浏览器可直接 `fetch` 的最终 URL。
|
|
23
|
+
|
|
24
|
+
### 3. 非目标(当前阶段)
|
|
25
|
+
|
|
26
|
+
- **不实现队列/HTTP/鉴权**:本包提供库 API;上层应用自行封装。
|
|
27
|
+
- **不内置转封装/转码**:不把 ffmpeg 作为强依赖。若后续发现移动端/微信兼容性问题,可由上层应用接入“可选后处理”。
|
|
28
|
+
- **不保证 `moov` 前置(faststart)**:本阶段优先落地“普通 MP4(非 fMP4)+ 流式写出 + 不强制 moov 前置”的路线,以避免为 faststart 引入全量缓冲或复杂回写。
|
|
29
|
+
|
|
30
|
+
### 4. 总体方案
|
|
31
|
+
|
|
32
|
+
核心思路:**服务端启动真实 Chromium,在浏览器里运行 `@meframe/core` 导出;同时让 mux 输出支持“按块输出字节流”,边产出边通过 multipart 分片上传到对象存储。**
|
|
33
|
+
|
|
34
|
+
架构分为三层:
|
|
35
|
+
|
|
36
|
+
- **Node 执行器(ServerExporter)**
|
|
37
|
+
- 负责生命周期、并发控制、浏览器实例管理、与 runner 通信、以及 multipart 上传的完整性(complete/abort)。
|
|
38
|
+
- **浏览器 runner(页面脚本)**
|
|
39
|
+
- 在 Chromium 中引入 `@meframe/core`,执行导出,并把 mux 输出按块推送到上传逻辑。
|
|
40
|
+
- **MultipartObjectStore(注入的存储适配器)**
|
|
41
|
+
- 由业务侧提供 S3/OSS/COS/自建存储实现;`@meframe/server` 只依赖接口。
|
|
42
|
+
|
|
43
|
+
### 5. 对外 API
|
|
44
|
+
|
|
45
|
+
- `ServerExporter`
|
|
46
|
+
- `constructor(options: ServerExporterOptions)`
|
|
47
|
+
- `exportToStore(input: ExportToStoreInput): Promise<ExportToStoreResult>`
|
|
48
|
+
|
|
49
|
+
行为约定:
|
|
50
|
+
|
|
51
|
+
- 调用方可传 `key`;不传时 exporter 会按 `keyPrefix` 生成。
|
|
52
|
+
- `abortSignal` 一旦触发:
|
|
53
|
+
- 应尽快停止导出
|
|
54
|
+
- 应调用 `store.abortMultipartUpload` 清理未完成对象
|
|
55
|
+
- 应清理本地 profile(userDataDir)等临时资源
|
|
56
|
+
- `onProgress` 用于透传导出进度(`progress`、`stage`、可选 `timeUs`)。
|
|
57
|
+
|
|
58
|
+
### 6. 当前实现现状(最新)
|
|
59
|
+
|
|
60
|
+
已实现:
|
|
61
|
+
|
|
62
|
+
- **ServerExporter(Node)**
|
|
63
|
+
- 通过 `puppeteer-core` 启动 Chromium,并为每个 job 创建独立 page
|
|
64
|
+
- 以内联 `<script type="module">` 注入 runner 模块(默认从 `dist/runner/runner.mjs` 读取;也支持传入 `runner.moduleCode`)
|
|
65
|
+
- 通过 `page.exposeFunction` 建立 runner ↔ Node 桥接:
|
|
66
|
+
- runner 请求 part URL(Node 调 `store.getUploadPartUrl`)
|
|
67
|
+
- runner 上报 part 完成(`etag` 收集)
|
|
68
|
+
- runner 上报进度/错误/完成(转发给 `onProgress`,并驱动 complete/abort)
|
|
69
|
+
- 支持 `AbortSignal`(best-effort:通知页面 abort + `abortMultipartUpload` + 清理临时资源)
|
|
70
|
+
|
|
71
|
+
- **Runner(浏览器)**
|
|
72
|
+
- 在 Chromium 内创建 `Meframe` 并执行导出
|
|
73
|
+
- 使用 `exportMode: 'stream'` + `onMuxData(data, position)` 获取 mux 输出
|
|
74
|
+
- 依据 `position` 进行“位置写入”,并在上传前校验每个 part 是否连续覆盖(避免 MP4 损坏)
|
|
75
|
+
- 上传方式:runner 直接 `fetch(PUT)` 到 `getUploadPartUrl()` 返回的 URL(可带 headers),并读取响应头 `ETag`
|
|
76
|
+
|
|
77
|
+
示例(参考实现):
|
|
78
|
+
|
|
79
|
+
- 本地闭环:`packages/server/examples/local/`
|
|
80
|
+
- S3:`packages/server/examples/s3/`
|
|
81
|
+
|
|
82
|
+
### 6. 存储抽象:MultipartObjectStore
|
|
83
|
+
|
|
84
|
+
为实现“存储无关”,上传协议抽象为 multipart 生命周期:
|
|
85
|
+
|
|
86
|
+
- `createMultipartUpload({ key, contentType, metadata? }) -> { uploadId }`
|
|
87
|
+
- `getUploadPartUrl({ key, uploadId, partNumber, expiresInSeconds? }) -> { url, headers? }`
|
|
88
|
+
- `completeMultipartUpload({ key, uploadId, parts: [{ partNumber, etag }] })`
|
|
89
|
+
- `abortMultipartUpload({ key, uploadId })`
|
|
90
|
+
|
|
91
|
+
说明:
|
|
92
|
+
|
|
93
|
+
- `getUploadPartUrl` 允许返回额外 `headers`(某些存储需要签名头或特殊 header)。
|
|
94
|
+
- 在默认实现思路中,runner 会直接对 `url` 做 `PUT` 上传;这样 Node 不会成为大流量中转点。
|
|
95
|
+
|
|
96
|
+
### 7. 执行流程
|
|
97
|
+
|
|
98
|
+
#### 7.1 Node 侧(ServerExporter)
|
|
99
|
+
|
|
100
|
+
- 校验输入(model、导出参数、store、可选 key)。
|
|
101
|
+
- 计算输出 `key`(调用方提供或生成)。
|
|
102
|
+
- `store.createMultipartUpload` 获取 `uploadId`。
|
|
103
|
+
- 获取并发 slot(限制同时运行的 job 数;海量并发应由上层队列与水平扩容解决)。
|
|
104
|
+
- 启动 Puppeteer/Chromium:
|
|
105
|
+
- 每个 job 使用独立 `userDataDir`,隔离 OPFS/锁/缓存,避免并发任务互相影响。
|
|
106
|
+
- 打开 runner 页面(后续会在本包内提供 runner 静态资源或内联脚本)。
|
|
107
|
+
- 通过 Puppeteer 建立 Node ↔ runner 的 RPC:
|
|
108
|
+
- runner 请求 part 上传信息:Node 调 `store.getUploadPartUrl` 返回 `{ url, headers? }`
|
|
109
|
+
- runner 上报已上传 part 的 `etag`:Node 记录 `parts[]`
|
|
110
|
+
- runner 上报 progress/error/done:Node 转发给调用方,并驱动 complete/abort
|
|
111
|
+
- 成功:`store.completeMultipartUpload({ parts })`
|
|
112
|
+
- 失败/取消:`store.abortMultipartUpload`,并清理 `userDataDir` 等临时资源
|
|
113
|
+
|
|
114
|
+
补充说明(runner 注入方式):
|
|
115
|
+
|
|
116
|
+
- `@meframe/server` 默认会从 `dist/runner/runner.mjs` 读取 runner bundle 的模块代码,并以内联 `<script type="module">` 的方式注入到页面。
|
|
117
|
+
- 如需避免磁盘依赖(例如在某些部署/打包环境),可通过 `ServerExporterOptions.runner.moduleCode` 直接注入 runner 模块代码字符串。
|
|
118
|
+
|
|
119
|
+
#### 7.2 浏览器 runner 侧
|
|
120
|
+
|
|
121
|
+
- 接收 Node 注入的 `model`、`exportOptions`、`key`、`uploadId`、`partSizeBytes` 等参数。
|
|
122
|
+
- 启动 `@meframe/core` 的导出流程(例如调用 `ExportScheduler.execute` 或其上层封装)。
|
|
123
|
+
- 通过 core 的“流式 mux 输出模式”接收 MP4 字节块(chunks)。
|
|
124
|
+
- 将 chunks 累积到 `partSizeBytes` 后上传:
|
|
125
|
+
- 请求当前 `partNumber` 的上传 URL
|
|
126
|
+
- `PUT` 上传(必要时带上 `headers`)
|
|
127
|
+
- 读取响应头 `ETag` 并回报给 Node
|
|
128
|
+
- 导出结束后 flush 余量并通知 Node 完成。
|
|
129
|
+
- 上传在飞时暂停新的聚合(单并发上传),防止内存被未上传数据占满。
|
|
130
|
+
- 上传失败可在 runner 内做有限重试(指数退避 + 最大次数),耗尽后通过 Node 的 `error` RPC 触发 abort。
|
|
131
|
+
- `getUploadPartUrl` 仅在发起上传前请求(不批量预取),降低 URL 过期风险。
|
|
132
|
+
- 成功 complete/失败/取消后均需清理对应 job 的临时 profile/userDataDir。
|
|
133
|
+
|
|
134
|
+
### 8. `@meframe/core` 需要的改造(已完成)
|
|
135
|
+
|
|
136
|
+
当前 `@meframe/core` 的 mux 输出是“最终拿到完整 `Blob`”,不适合服务端高并发导出。需要在不破坏 Web 端行为的前提下引入“流式输出”:
|
|
137
|
+
|
|
138
|
+
- **Blob 模式(现有)**:保留 `finalize(): Blob`,供 Web 端继续使用。
|
|
139
|
+
- **流式模式(新增)**:
|
|
140
|
+
- `MuxManager.start({ ..., output })` 支持 `output.kind = 'stream'`
|
|
141
|
+
- 流式模式提供 `onData(chunk: Uint8Array)`(或等价 WritableStream)回调接收增量字节
|
|
142
|
+
- `MuxManager.finalize()` 在流式模式下只负责 flush/close,不返回 `Blob`
|
|
143
|
+
|
|
144
|
+
实现备注:
|
|
145
|
+
|
|
146
|
+
- 当前 core 使用 mp4-muxer(该项目已 deprecated);后续可评估迁移 Mediabunny。服务端架构应把“mux 输出 sink”作为稳定抽象,避免与具体库强绑定。
|
|
147
|
+
|
|
148
|
+
### 9. 兼容性与验证
|
|
149
|
+
|
|
150
|
+
由于本阶段不强制 `moov` 前置:
|
|
151
|
+
|
|
152
|
+
- 某些移动端/微信播放器可能要求“完整下载后播放”,或对容器结构更敏感。
|
|
153
|
+
- 必须在目标机型与微信版本上做真实验证。
|
|
154
|
+
- 若验证失败,建议由上层应用接入可选后处理(例如 remux 成 moov 前置 MP4),而不是把 ffmpeg 作为 `@meframe/server` 的强依赖。
|
|
155
|
+
|
|
156
|
+
### 10. 实现顺序建议(可按里程碑推进)
|
|
157
|
+
|
|
158
|
+
- **M1:core 增加流式 mux 输出模式**(保留 Blob 模式,确保回归风险可控)。
|
|
159
|
+
- **M2:server 实现单任务闭环**(Puppeteer + runner + multipart 上传,先跑通“能导出并上传”)。
|
|
160
|
+
- **M3:完善工程能力**(并发控制、取消、错误清理、日志/进度)。
|
|
161
|
+
- **M4:真机/微信验证与策略调整**(必要时在上层引入后处理钩子)。
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
## `@meframe/server` 集成指南(中文)
|
|
2
|
+
|
|
3
|
+
本文档面向“上层业务应用”的工程集成:如何在 Node 侧驱动 Chromium 执行 `@meframe/core` 导出,并将 MP4 以 multipart 方式上传到对象存储(如 S3)。
|
|
4
|
+
|
|
5
|
+
适用读者:后端/平台工程同学(需要把导出能力集成进服务、队列、任务系统)。
|
|
6
|
+
|
|
7
|
+
> 设计背景与约束请先阅读:
|
|
8
|
+
>
|
|
9
|
+
> - `packages/server/docs/ARCHITECTURE.md`
|
|
10
|
+
>
|
|
11
|
+
> (实现细节与注意事项已并入 ARCHITECTURE 与本文档,避免重复。)
|
|
12
|
+
|
|
13
|
+
### 1. 一句话说明:它是怎么“在服务端导出”的
|
|
14
|
+
|
|
15
|
+
- Node 进程通过 `puppeteer-core` 启动一个真实 Chromium
|
|
16
|
+
- 在 Chromium 里运行 `@meframe/core` 的导出(WebCodecs / OPFS / Workers 等都在浏览器里完成)
|
|
17
|
+
- mux 输出使用 `exportMode: 'stream'`,将 MP4 字节块按 `(data, position)` 推送出来
|
|
18
|
+
- runner 在浏览器里把字节块聚合为 multipart 分片,并对每个分片做 `fetch(PUT)` 上传
|
|
19
|
+
- Node 侧仅负责:
|
|
20
|
+
- 生成/管理 multipart upload 生命周期(create / presign / complete / abort)
|
|
21
|
+
- 记录每个 part 的 `ETag`
|
|
22
|
+
- 转发进度、处理取消、清理资源
|
|
23
|
+
|
|
24
|
+
### 2. 集成前置条件(必须满足)
|
|
25
|
+
|
|
26
|
+
#### 2.1 Chromium(必须)
|
|
27
|
+
|
|
28
|
+
`@meframe/server` 依赖 `puppeteer-core`,**不自带浏览器**。生产环境需要你提供 Chromium/Chrome 可执行文件路径。
|
|
29
|
+
|
|
30
|
+
示例(本地):
|
|
31
|
+
|
|
32
|
+
- `MEFRAME_CHROME_PATH=/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome`
|
|
33
|
+
|
|
34
|
+
#### 2.2 可信 origin(必须)
|
|
35
|
+
|
|
36
|
+
WebCodecs 需要 secure/trustworthy origin。`ServerExporter.exportToStore({ pageUrl })` 会先导航到一个可信页面,再注入 runner。
|
|
37
|
+
|
|
38
|
+
推荐:
|
|
39
|
+
|
|
40
|
+
- 本地调试:`http://127.0.0.1:<port>/`
|
|
41
|
+
- 生产:你自己的 HTTPS 页面(建议是一个非常简单的静态页)
|
|
42
|
+
|
|
43
|
+
对应示例:
|
|
44
|
+
|
|
45
|
+
- 本地:`packages/server/examples/local/local-server.mjs` 会启动 `http://127.0.0.1` 并提供 `/` 页面
|
|
46
|
+
|
|
47
|
+
#### 2.3 Worker bundle 可访问(必须)
|
|
48
|
+
|
|
49
|
+
`@meframe/core` 导出依赖 Worker。浏览器页面必须能加载到 worker 文件:
|
|
50
|
+
|
|
51
|
+
- 你需要把 `@meframe/core/dist/workers` 托管到一个可访问路径(同源或跨域需 CORS)
|
|
52
|
+
- 并在导出时传入 `workerPath`(或使用 core 的默认 workerPath 约定)
|
|
53
|
+
|
|
54
|
+
对应示例:
|
|
55
|
+
|
|
56
|
+
- `packages/server/examples/local/export-to-local.mjs` / `packages/server/examples/s3/export-to-s3.mjs`
|
|
57
|
+
- 都要求设置 `MEFRAME_WORKER_PATH`
|
|
58
|
+
|
|
59
|
+
### 3. 你需要实现什么:MultipartObjectStore(核心接口)
|
|
60
|
+
|
|
61
|
+
服务端上传能力被抽象成一个“multipart store”接口(存储无关):
|
|
62
|
+
|
|
63
|
+
- `createMultipartUpload({ key, contentType, metadata? }) -> { uploadId }`
|
|
64
|
+
- `getUploadPartUrl({ key, uploadId, partNumber, expiresInSeconds? }) -> { url, headers? }`
|
|
65
|
+
- `completeMultipartUpload({ key, uploadId, parts: [{ partNumber, etag }] })`
|
|
66
|
+
- `abortMultipartUpload({ key, uploadId })`
|
|
67
|
+
|
|
68
|
+
关键约束(非常重要):
|
|
69
|
+
|
|
70
|
+
- runner 在浏览器内对 `url` 发起 `PUT`,并要求能从响应头读取到 `ETag`
|
|
71
|
+
- 否则 runner 无法上报 `etag`,Node 无法 `completeMultipartUpload`
|
|
72
|
+
|
|
73
|
+
#### 3.1 S3 集成要点(CORS 必须配)
|
|
74
|
+
|
|
75
|
+
S3(或兼容 S3 的对象存储)需要 Bucket CORS 允许跨域 PUT,并且暴露 `ETag`:
|
|
76
|
+
|
|
77
|
+
- **AllowedMethods**: `PUT`
|
|
78
|
+
- **AllowedOrigins**: 必须包含 runner 页面 origin(也就是 `pageUrl` 的 origin)
|
|
79
|
+
- **AllowedHeaders**: `*`(或包含你需要的签名 headers)
|
|
80
|
+
- **ExposeHeaders**: 必须包含 `ETag`
|
|
81
|
+
|
|
82
|
+
对应示例:
|
|
83
|
+
|
|
84
|
+
- `packages/server/examples/s3/s3-store.mjs`:使用 AWS SDK v3 实现 `MultipartObjectStore`
|
|
85
|
+
- `packages/server/examples/local/local-server.mjs`:在本地 store 上方注释了 S3 映射关系与 CORS 要点
|
|
86
|
+
|
|
87
|
+
### 4. 业务侧典型调用方式(推荐结构)
|
|
88
|
+
|
|
89
|
+
推荐把“导出模型构建”和“上传实现”解耦:
|
|
90
|
+
|
|
91
|
+
- **上层应用负责**:把业务数据转换成 `CompositionModelData`(保证 `resource.uri` 是浏览器可 fetch 的最终 URL)
|
|
92
|
+
- **`@meframe/server` 负责**:在浏览器里执行导出 + 上传(通过 `store` 注入)
|
|
93
|
+
|
|
94
|
+
对应示例:
|
|
95
|
+
|
|
96
|
+
- 本地闭环(可跑通):`packages/server/examples/local/export-to-local.mjs`
|
|
97
|
+
- S3(参考实现):`packages/server/examples/s3/export-to-s3.mjs`
|
|
98
|
+
|
|
99
|
+
### 5. 环境变量约定(示例级)
|
|
100
|
+
|
|
101
|
+
以下 env 变量在 examples 中使用,你可以按业务习惯重命名,但含义建议保持一致:
|
|
102
|
+
|
|
103
|
+
#### 5.1 通用(local / s3 都需要)
|
|
104
|
+
|
|
105
|
+
- `MEFRAME_CHROME_PATH` / `CHROME_BIN`:Chromium/Chrome 可执行文件路径
|
|
106
|
+
- `MEFRAME_WORKER_PATH`:worker bundle base URL(例如 `https://.../meframe-workers-2`)
|
|
107
|
+
- `MEFRAME_RUNNER_PAGE_URL`(S3 示例用):可信 origin 的页面 URL(生产建议 HTTPS)
|
|
108
|
+
|
|
109
|
+
#### 5.2 S3 相关(`examples/s3/s3-store.mjs`)
|
|
110
|
+
|
|
111
|
+
- `S3_BUCKET`(必填)
|
|
112
|
+
- `S3_KEY_PREFIX`(默认 `meframe/exports/`)
|
|
113
|
+
- `AWS_REGION` / `AWS_DEFAULT_REGION`(默认 `us-east-1`)
|
|
114
|
+
- 可选(兼容 S3 的 endpoint):
|
|
115
|
+
- `S3_ENDPOINT`
|
|
116
|
+
- `S3_FORCE_PATH_STYLE=1`
|
|
117
|
+
|
|
118
|
+
AWS 凭证通过 AWS SDK 默认链路加载(env / shared config / IAM role 等)。
|
|
119
|
+
|
|
120
|
+
#### 5.3 Medeo 资源解析(示例用)
|
|
121
|
+
|
|
122
|
+
examples 使用 `@meframe/adapter-medeo` 来把 Medeo draft 转换为 `CompositionModelData`,并通过 API 获取 `storage_key` 等元信息:
|
|
123
|
+
|
|
124
|
+
- `MEDEO_API_BASE`(默认 `https://api.stg.medeo.app/api/v2/medias`)
|
|
125
|
+
- `MEDEO_API_TOKEN`(可选)
|
|
126
|
+
- `MEDEO_PUBLIC_BASE_URL`(示例里必填,用于把 `storage_key` 拼成可访问 URL)
|
|
127
|
+
|
|
128
|
+
> 生产环境通常不需要依赖 Medeo API:你的上层服务应直接提供可 fetch 的最终 URL(或你的 CDN URL)。
|
|
129
|
+
|
|
130
|
+
### 6. 取消与超时(建议)
|
|
131
|
+
|
|
132
|
+
`exportToStore` 支持 `AbortSignal`(best-effort):
|
|
133
|
+
|
|
134
|
+
- 触发取消后:
|
|
135
|
+
- runner 会中止上传/导出
|
|
136
|
+
- Node 会调用 `abortMultipartUpload`
|
|
137
|
+
- 并清理临时 profile 等资源
|
|
138
|
+
|
|
139
|
+
建议上层应用自行加:
|
|
140
|
+
|
|
141
|
+
- 单任务整体超时(例如 10min/30min)
|
|
142
|
+
- per-part 上传超时与重试策略(runner 目前已有有限重试,但业务可做更强的外层保障)
|
|
143
|
+
|
|
144
|
+
### 7. 常见问题(踩坑清单)
|
|
145
|
+
|
|
146
|
+
#### 7.1 `VideoEncoder is not available`
|
|
147
|
+
|
|
148
|
+
几乎都是因为 origin 不可信:确保传了 `pageUrl`,且它是 `http://127.0.0.1` 或 `https://...`。
|
|
149
|
+
|
|
150
|
+
#### 7.2 worker 加载失败
|
|
151
|
+
|
|
152
|
+
确保:
|
|
153
|
+
|
|
154
|
+
- `MEFRAME_WORKER_PATH` 指向的地址能在 Chromium 内打开(网络、DNS、证书)
|
|
155
|
+
- 跨域时 CORS 允许加载
|
|
156
|
+
|
|
157
|
+
#### 7.3 上传成功但最终 MP4 播放失败/损坏
|
|
158
|
+
|
|
159
|
+
multipart 上传必须满足:
|
|
160
|
+
|
|
161
|
+
- 每个 part 都完整、连续覆盖;runner 已做 range 覆盖校验
|
|
162
|
+
- `ETag` 必须正确回传给 Node 并用于 complete
|
|
163
|
+
- 对象存储端的 `CompleteMultipartUpload` parts 顺序必须正确(按 partNumber)
|
|
164
|
+
|
|
165
|
+
### 8. 示例索引
|
|
166
|
+
|
|
167
|
+
- `packages/server/examples/local/`
|
|
168
|
+
- `export-to-local.mjs`:本地闭环(本地 HTTP store + 生成 out.mp4)
|
|
169
|
+
- `local-server.mjs`:本地 multipart store + S3 映射注释
|
|
170
|
+
- `packages/server/examples/s3/`
|
|
171
|
+
- `export-to-s3.mjs`:导出并上传到 S3(参考)
|
|
172
|
+
- `s3-store.mjs`:S3 MultipartObjectStore(AWS SDK v3)
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meframe/server",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Server-side exporter for @meframe/core (browser-driven, multipart upload via injected store)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"docs",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@meframe/core": "^0.2.4",
|
|
22
|
+
"puppeteer-core": "^23.5.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@aws-sdk/client-s3": "^3.758.0",
|
|
26
|
+
"@aws-sdk/s3-request-presigner": "^3.758.0",
|
|
27
|
+
"@types/node": "^20.11.24",
|
|
28
|
+
"dotenv": "^16.4.5",
|
|
29
|
+
"vite": "^5.4.20",
|
|
30
|
+
"typescript": "^5.3.3",
|
|
31
|
+
"vitest": "^1.3.1",
|
|
32
|
+
"@meframe/adapter-medeo": "0.0.1"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.0.0"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"meframe",
|
|
39
|
+
"server",
|
|
40
|
+
"export",
|
|
41
|
+
"webcodecs",
|
|
42
|
+
"puppeteer"
|
|
43
|
+
],
|
|
44
|
+
"author": "Sheepy",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/one2x-ai/meframe",
|
|
49
|
+
"directory": "packages/server"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public",
|
|
53
|
+
"registry": "https://registry.npmjs.org/"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsc && pnpm build:runner",
|
|
57
|
+
"build:runner": "vite build --config vite.runner.config.ts",
|
|
58
|
+
"dev": "node examples/local/export-to-local.mjs",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"test:watch": "vitest",
|
|
61
|
+
"type-check": "tsc --noEmit",
|
|
62
|
+
"clean": "rm -rf dist"
|
|
63
|
+
}
|
|
64
|
+
}
|