@cut-workbench/core 0.4.0 → 0.4.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/dist/chunks/{canvas-size-Ct61OYjW.cjs → canvas-size-DIABzEW2.cjs} +212 -44
- package/dist/chunks/canvas-size-DIABzEW2.cjs.map +1 -0
- package/dist/chunks/{canvas-size-CM2oKApi.js → canvas-size-Dlnng74w.js} +168 -30
- package/dist/chunks/canvas-size-Dlnng74w.js.map +1 -0
- package/dist/chunks/id-CJsdlGei.js +35 -0
- package/dist/chunks/id-CJsdlGei.js.map +1 -0
- package/dist/chunks/id-DaCxD4-5.cjs +70 -0
- package/dist/chunks/id-DaCxD4-5.cjs.map +1 -0
- package/dist/chunks/{snap-DyIuO2hU.cjs → snap-Cfk15JVg.cjs} +2 -22
- package/dist/chunks/{snap-DyIuO2hU.cjs.map → snap-Cfk15JVg.cjs.map} +1 -1
- package/dist/chunks/{snap-CBV9G4xq.js → snap-DpkVPVJN.js} +3 -17
- package/dist/chunks/{snap-CBV9G4xq.js.map → snap-DpkVPVJN.js.map} +1 -1
- package/dist/chunks/thumbnails-DEjnao5r.cjs +81 -0
- package/dist/chunks/thumbnails-DEjnao5r.cjs.map +1 -0
- package/dist/chunks/thumbnails-qH0dX_Zs.js +64 -0
- package/dist/chunks/thumbnails-qH0dX_Zs.js.map +1 -0
- package/dist/chunks/{time-CnMfSVYX.cjs → time-CYEd91qN.cjs} +1 -69
- package/dist/chunks/time-CYEd91qN.cjs.map +1 -0
- package/dist/chunks/{time-C_gvkVl1.js → time-ChF1GwCy.js} +2 -34
- package/dist/chunks/time-ChF1GwCy.js.map +1 -0
- package/dist/chunks/{waveform-Ie-G5Lle.cjs → waveform-BYwDdKpC.cjs} +34 -15
- package/dist/chunks/waveform-BYwDdKpC.cjs.map +1 -0
- package/dist/chunks/{waveform-DJkRtMct.js → waveform-uyGNE01H.js} +32 -13
- package/dist/chunks/waveform-uyGNE01H.js.map +1 -0
- package/dist/engine/index.cjs +668 -76
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.js +648 -57
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/webav/exporter.d.ts +3 -1
- package/dist/engine/webav/import-files.d.ts +17 -2
- package/dist/engine/webav/import-pipeline.d.ts +10 -0
- package/dist/engine/webav/types.d.ts +15 -0
- package/dist/index.cjs +18 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/lib/error.d.ts +6 -0
- package/dist/media/thumbnails.d.ts +8 -0
- package/dist/project/schema.d.ts +16 -4
- package/dist/timeline/index.cjs +10 -6
- package/dist/timeline/index.d.ts +1 -0
- package/dist/timeline/index.js +4 -4
- package/dist/timeline/normalize.d.ts +28 -0
- package/dist/timeline/ops.d.ts +0 -4
- package/dist/timeline/snap.d.ts +0 -2
- package/dist/types.d.ts +14 -2
- package/package.json +1 -1
- package/dist/chunks/canvas-size-CM2oKApi.js.map +0 -1
- package/dist/chunks/canvas-size-Ct61OYjW.cjs.map +0 -1
- package/dist/chunks/import-pipeline-A8KkqtZT.cjs +0 -610
- package/dist/chunks/import-pipeline-A8KkqtZT.cjs.map +0 -1
- package/dist/chunks/import-pipeline-DLIdgrIb.js +0 -521
- package/dist/chunks/import-pipeline-DLIdgrIb.js.map +0 -1
- package/dist/chunks/time-C_gvkVl1.js.map +0 -1
- package/dist/chunks/time-CnMfSVYX.cjs.map +0 -1
- package/dist/chunks/waveform-DJkRtMct.js.map +0 -1
- package/dist/chunks/waveform-Ie-G5Lle.cjs.map +0 -1
package/dist/engine/index.cjs
CHANGED
|
@@ -1,12 +1,304 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_rolldown_runtime = require("../chunks/rolldown-runtime-B2SfzNBj.cjs");
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
3
|
+
const require_id = require("../chunks/id-DaCxD4-5.cjs");
|
|
4
|
+
const require_time = require("../chunks/time-CYEd91qN.cjs");
|
|
5
|
+
const require_canvas_size = require("../chunks/canvas-size-DIABzEW2.cjs");
|
|
6
|
+
const require_waveform = require("../chunks/waveform-BYwDdKpC.cjs");
|
|
6
7
|
const require_file_reader = require("../chunks/file-reader-DfMEluGr.cjs");
|
|
7
|
-
const
|
|
8
|
+
const require_thumbnails = require("../chunks/thumbnails-DEjnao5r.cjs");
|
|
8
9
|
let _webav_av_cliper = require("@webav/av-cliper");
|
|
9
10
|
let _webav_av_canvas = require("@webav/av-canvas");
|
|
11
|
+
// src/engine/webav/styles.ts
|
|
12
|
+
var DEFAULT_TEXT_STYLE = {
|
|
13
|
+
fontSize: 64,
|
|
14
|
+
color: "#ffffff",
|
|
15
|
+
fontFamily: "sans-serif",
|
|
16
|
+
fontWeight: 600,
|
|
17
|
+
fontStyle: "normal",
|
|
18
|
+
strokeColor: "#000000",
|
|
19
|
+
strokeWidth: 0,
|
|
20
|
+
shadowColor: "rgba(0,0,0,0.6)",
|
|
21
|
+
shadowBlur: 8,
|
|
22
|
+
align: "center",
|
|
23
|
+
background: "transparent"
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* 字幕默认字体。
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ 不要写 'Noto Sans SC' —— 大多数系统并未安装该字体,`EmbedSubtitlesClip` 会把
|
|
29
|
+
* `ctx.font` 设为 `30px Noto Sans SC`,此时浏览器回退到默认字体,中文可能显示为方框。
|
|
30
|
+
* 使用系统字体栈(sans-serif)由浏览器挑选可用的中文字体,最稳。
|
|
31
|
+
*/
|
|
32
|
+
var DEFAULT_SUBTITLE_STYLE = {
|
|
33
|
+
color: "#ffffff",
|
|
34
|
+
textBgColor: null,
|
|
35
|
+
fontFamily: "sans-serif",
|
|
36
|
+
fontSize: 30,
|
|
37
|
+
strokeStyle: "#000000",
|
|
38
|
+
lineWidth: null,
|
|
39
|
+
bottomOffset: 30
|
|
40
|
+
};
|
|
41
|
+
// src/engine/webav/text-clip.ts
|
|
42
|
+
function cssFromStyle(style) {
|
|
43
|
+
const shadow = style.shadowBlur > 0 ? `text-shadow: 0 2px ${style.shadowBlur}px ${style.shadowColor};` : "";
|
|
44
|
+
const stroke = style.strokeWidth > 0 ? `-webkit-text-stroke: ${style.strokeWidth}px ${style.strokeColor};` : "";
|
|
45
|
+
return [
|
|
46
|
+
`font-size:${style.fontSize}px`,
|
|
47
|
+
`color:${style.color}`,
|
|
48
|
+
`font-family:${style.fontFamily}`,
|
|
49
|
+
`font-weight:${style.fontWeight}`,
|
|
50
|
+
`font-style:${style.fontStyle}`,
|
|
51
|
+
`text-align:${style.align}`,
|
|
52
|
+
`background:${style.background}`,
|
|
53
|
+
"line-height:1.25",
|
|
54
|
+
"padding:8px 16px",
|
|
55
|
+
"white-space:pre-wrap",
|
|
56
|
+
"max-width:80%",
|
|
57
|
+
stroke,
|
|
58
|
+
shadow
|
|
59
|
+
].filter(Boolean).join(";");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 文字素材。内部缓存最近一次渲染的 ImageBitmap,按「文本 + 样式」做键避免重复渲染。
|
|
63
|
+
*/
|
|
64
|
+
var TextClip = class TextClip {
|
|
65
|
+
ready;
|
|
66
|
+
opts;
|
|
67
|
+
metaValue;
|
|
68
|
+
cacheKey = "";
|
|
69
|
+
cachedBitmap = null;
|
|
70
|
+
destroyed = false;
|
|
71
|
+
constructor(options) {
|
|
72
|
+
this.opts = {
|
|
73
|
+
...options,
|
|
74
|
+
style: {
|
|
75
|
+
...DEFAULT_TEXT_STYLE,
|
|
76
|
+
...options.style
|
|
77
|
+
},
|
|
78
|
+
animation: options.animation ?? "none",
|
|
79
|
+
charsPerSecond: options.charsPerSecond ?? 12,
|
|
80
|
+
animOffsetUs: options.animOffsetUs ?? 0
|
|
81
|
+
};
|
|
82
|
+
this.metaValue = {
|
|
83
|
+
width: options.width,
|
|
84
|
+
height: options.height,
|
|
85
|
+
duration: Math.max(1, options.durationUs)
|
|
86
|
+
};
|
|
87
|
+
this.ready = Promise.resolve(this.metaValue);
|
|
88
|
+
}
|
|
89
|
+
get meta() {
|
|
90
|
+
return this.metaValue;
|
|
91
|
+
}
|
|
92
|
+
visibleTextAt(timeUs) {
|
|
93
|
+
const { text, animation, charsPerSecond } = this.opts;
|
|
94
|
+
if (animation === "none") return text;
|
|
95
|
+
const phaseUs = timeUs + this.opts.animOffsetUs;
|
|
96
|
+
if (animation === "typewriter") {
|
|
97
|
+
const visible = Math.floor(phaseUs / 1e6 * charsPerSecond);
|
|
98
|
+
if (visible <= 0) return null;
|
|
99
|
+
return text.slice(0, visible);
|
|
100
|
+
}
|
|
101
|
+
return Math.floor(phaseUs / 1e6 * 2) % 2 === 0 ? text : null;
|
|
102
|
+
}
|
|
103
|
+
async tick(time) {
|
|
104
|
+
if (this.destroyed) return { state: "done" };
|
|
105
|
+
if (time >= this.opts.durationUs) return { state: "done" };
|
|
106
|
+
const visible = this.visibleTextAt(time);
|
|
107
|
+
if (visible == null || visible.length === 0) return {
|
|
108
|
+
video: await this.renderEmpty(),
|
|
109
|
+
state: "success"
|
|
110
|
+
};
|
|
111
|
+
const key = `${visible}|${JSON.stringify(this.opts.style)}`;
|
|
112
|
+
if (key !== this.cacheKey || this.cachedBitmap == null) {
|
|
113
|
+
this.cachedBitmap?.close();
|
|
114
|
+
this.cachedBitmap = await this.renderTextToFrame(visible);
|
|
115
|
+
this.cacheKey = key;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
video: await createImageBitmap(this.cachedBitmap),
|
|
119
|
+
state: "success"
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 把文字渲染成「整帧」位图(工程分辨率)。
|
|
124
|
+
*
|
|
125
|
+
* 为什么不能直接把 renderTxt2ImgBitmap 的结果当帧用:
|
|
126
|
+
* 它返回的是文字自身尺寸的小图(几十到几百像素宽),Sprite 会把它拉伸到 rect 的
|
|
127
|
+
* 1280×720 —— 文字被放大后严重模糊,且居中位置由 Sprite 决定而非文字块决定。
|
|
128
|
+
* 因此这里先渲染文字小图,再按比例居中合成到工程尺寸的画布上。
|
|
129
|
+
*/
|
|
130
|
+
async renderTextToFrame(text) {
|
|
131
|
+
const { width, height } = this.opts;
|
|
132
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
133
|
+
const ctx = canvas.getContext("2d");
|
|
134
|
+
if (ctx == null) throw new Error("无法创建文字渲染上下文");
|
|
135
|
+
const layer = await (0, _webav_av_cliper.renderTxt2ImgBitmap)(text, cssFromStyle(this.opts.style));
|
|
136
|
+
const align = this.opts.style.align;
|
|
137
|
+
const margin = Math.round(width * .05);
|
|
138
|
+
const maxWidth = width - margin * 2;
|
|
139
|
+
const scale = layer.width > maxWidth ? maxWidth / layer.width : 1;
|
|
140
|
+
const drawW = Math.round(layer.width * scale);
|
|
141
|
+
const drawH = Math.round(layer.height * scale);
|
|
142
|
+
const x = align === "left" ? margin : align === "right" ? width - margin - drawW : (width - drawW) / 2;
|
|
143
|
+
const y = Math.round((height - drawH) / 2);
|
|
144
|
+
ctx.drawImage(layer, x, y, drawW, drawH);
|
|
145
|
+
layer.close();
|
|
146
|
+
return canvas.transferToImageBitmap();
|
|
147
|
+
}
|
|
148
|
+
emptyBitmap = null;
|
|
149
|
+
async renderEmpty() {
|
|
150
|
+
if (this.emptyBitmap != null) return this.emptyBitmap;
|
|
151
|
+
const canvas = new OffscreenCanvas(this.opts.width, this.opts.height);
|
|
152
|
+
this.emptyBitmap = canvas.transferToImageBitmap();
|
|
153
|
+
return this.emptyBitmap;
|
|
154
|
+
}
|
|
155
|
+
async split(time) {
|
|
156
|
+
if (time <= 0 || time >= this.opts.durationUs) throw new Error("\"time\" out of bounds");
|
|
157
|
+
const left = new TextClip({
|
|
158
|
+
...this.opts,
|
|
159
|
+
durationUs: time
|
|
160
|
+
});
|
|
161
|
+
const right = new TextClip({
|
|
162
|
+
...this.opts,
|
|
163
|
+
durationUs: this.opts.durationUs - time,
|
|
164
|
+
animOffsetUs: this.opts.animOffsetUs + time
|
|
165
|
+
});
|
|
166
|
+
await Promise.all([left.ready, right.ready]);
|
|
167
|
+
return [left, right];
|
|
168
|
+
}
|
|
169
|
+
async clone() {
|
|
170
|
+
const copy = new TextClip({ ...this.opts });
|
|
171
|
+
await copy.ready;
|
|
172
|
+
return copy;
|
|
173
|
+
}
|
|
174
|
+
destroy() {
|
|
175
|
+
this.destroyed = true;
|
|
176
|
+
this.cachedBitmap?.close();
|
|
177
|
+
this.cachedBitmap = null;
|
|
178
|
+
this.emptyBitmap?.close();
|
|
179
|
+
this.emptyBitmap = null;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
// src/engine/webav/clip-factory.ts
|
|
183
|
+
/**
|
|
184
|
+
* File → IClip 的分派工厂。
|
|
185
|
+
*
|
|
186
|
+
* ⚠️ 本文件是 `@webav/*` 的合法引用点之一(另一个是 core/webav 下的其他模块)。
|
|
187
|
+
* 不要在 features / store / components 中直接 import WebAV。
|
|
188
|
+
*
|
|
189
|
+
* 所有 clip 实例的生命周期由 `ClipRegistry` 统一管理,这里只负责创建。
|
|
190
|
+
*/
|
|
191
|
+
/**
|
|
192
|
+
* 创建素材的「母版」clip 并读取 meta。
|
|
193
|
+
*
|
|
194
|
+
* 对静态图/文字/字幕,`meta.duration` 可能是 Infinity,这里会归一成有限值:
|
|
195
|
+
* 真正的时长由时间轴 clip 决定,母版只用于 clone 与抽帧。
|
|
196
|
+
*/
|
|
197
|
+
async function createClipFromFile(file, kind) {
|
|
198
|
+
switch (kind) {
|
|
199
|
+
case "video": {
|
|
200
|
+
const clip = new _webav_av_cliper.MP4Clip(require_file_reader.fileToStream(file));
|
|
201
|
+
const meta = await clip.ready;
|
|
202
|
+
return {
|
|
203
|
+
clip,
|
|
204
|
+
meta: {
|
|
205
|
+
durationUs: meta.duration,
|
|
206
|
+
width: meta.width,
|
|
207
|
+
height: meta.height,
|
|
208
|
+
hasAudio: meta.audioChanCount > 0
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
case "audio": {
|
|
213
|
+
const clip = new _webav_av_cliper.AudioClip(require_file_reader.fileToStream(file));
|
|
214
|
+
return {
|
|
215
|
+
clip,
|
|
216
|
+
meta: {
|
|
217
|
+
durationUs: (await clip.ready).duration,
|
|
218
|
+
width: 0,
|
|
219
|
+
height: 0,
|
|
220
|
+
hasAudio: true
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
case "image": {
|
|
225
|
+
const clip = new _webav_av_cliper.ImgClip(require_file_reader.fileToStream(file));
|
|
226
|
+
const meta = await clip.ready;
|
|
227
|
+
return {
|
|
228
|
+
clip,
|
|
229
|
+
meta: {
|
|
230
|
+
durationUs: Number.isFinite(meta.duration) ? meta.duration : require_id.DEFAULT_STILL_DURATION_US,
|
|
231
|
+
width: meta.width,
|
|
232
|
+
height: meta.height,
|
|
233
|
+
hasAudio: false
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
case "subtitle": {
|
|
238
|
+
const text = await file.text();
|
|
239
|
+
const clip = new _webav_av_cliper.EmbedSubtitlesClip(text, {
|
|
240
|
+
videoWidth: 1280,
|
|
241
|
+
videoHeight: 720
|
|
242
|
+
});
|
|
243
|
+
const meta = await clip.ready;
|
|
244
|
+
return {
|
|
245
|
+
clip,
|
|
246
|
+
meta: {
|
|
247
|
+
durationUs: Number.isFinite(meta.duration) ? meta.duration : require_id.DEFAULT_STILL_DURATION_US,
|
|
248
|
+
width: meta.width,
|
|
249
|
+
height: meta.height,
|
|
250
|
+
hasAudio: false
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
default: throw new Error(`Unsupported asset kind: ${kind}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** 文字素材:不走文件,直接由 TextClip 生成 */
|
|
258
|
+
function createTextClip(init) {
|
|
259
|
+
return new TextClip({
|
|
260
|
+
text: init.text,
|
|
261
|
+
style: init.style,
|
|
262
|
+
width: init.width,
|
|
263
|
+
height: init.height,
|
|
264
|
+
durationUs: init.durationUs
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function createSubtitleClip(init) {
|
|
268
|
+
return new _webav_av_cliper.EmbedSubtitlesClip(init.srt, {
|
|
269
|
+
videoWidth: init.width,
|
|
270
|
+
videoHeight: init.height,
|
|
271
|
+
color: init.style.color,
|
|
272
|
+
textBgColor: init.style.textBgColor,
|
|
273
|
+
fontFamily: init.style.fontFamily,
|
|
274
|
+
fontSize: init.style.fontSize,
|
|
275
|
+
strokeStyle: init.style.strokeStyle,
|
|
276
|
+
lineWidth: init.style.lineWidth,
|
|
277
|
+
bottomOffset: init.style.bottomOffset
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/** Worker 内重建 clip:只支持可序列化的素材类型 */
|
|
281
|
+
function createClipFromBuffer(buffer, kind, opts) {
|
|
282
|
+
switch (kind) {
|
|
283
|
+
case "video": return new _webav_av_cliper.MP4Clip(require_file_reader.bufferToStream(buffer));
|
|
284
|
+
case "audio": return new _webav_av_cliper.AudioClip(require_file_reader.bufferToStream(buffer));
|
|
285
|
+
case "image": return new _webav_av_cliper.ImgClip(require_file_reader.bufferToStream(buffer));
|
|
286
|
+
case "subtitle": return createSubtitleClip({
|
|
287
|
+
srt: opts?.srt ?? "",
|
|
288
|
+
style: opts?.subtitleStyle ?? DEFAULT_SUBTITLE_STYLE,
|
|
289
|
+
width: opts?.width ?? 1280,
|
|
290
|
+
height: opts?.height ?? 720
|
|
291
|
+
});
|
|
292
|
+
case "text": return createTextClip({
|
|
293
|
+
text: opts?.text ?? "",
|
|
294
|
+
style: opts?.textStyle ?? DEFAULT_TEXT_STYLE,
|
|
295
|
+
width: opts?.width ?? 1280,
|
|
296
|
+
height: opts?.height ?? 720,
|
|
297
|
+
durationUs: opts?.durationUs ?? 5e6
|
|
298
|
+
});
|
|
299
|
+
default: throw new Error(`Worker 内不支持重建素材类型:${kind}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
10
302
|
// src/engine/webav/offset-clip.ts
|
|
11
303
|
var OffsetClip = class OffsetClip {
|
|
12
304
|
ready;
|
|
@@ -22,7 +314,10 @@ var OffsetClip = class OffsetClip {
|
|
|
22
314
|
height: 0,
|
|
23
315
|
duration: opts.durationUs
|
|
24
316
|
};
|
|
25
|
-
this.ready = inner.ready.then((meta) => {
|
|
317
|
+
this.ready = inner.ready.then(async (meta) => {
|
|
318
|
+
try {
|
|
319
|
+
await inner.tick(opts.inPointUs);
|
|
320
|
+
} catch {}
|
|
26
321
|
this.metaValue = {
|
|
27
322
|
width: meta.width,
|
|
28
323
|
height: meta.height,
|
|
@@ -96,10 +391,10 @@ var ClipRegistry = class {
|
|
|
96
391
|
* 这两个类型无法 clone(clone 会带走旧的 duration / 内容),必须每次新建。
|
|
97
392
|
*/
|
|
98
393
|
buildProceduralClip(params, asset) {
|
|
99
|
-
if (asset.kind === "text") return
|
|
394
|
+
if (asset.kind === "text") return createTextClip({
|
|
100
395
|
text: params.text ?? asset.text ?? "文本",
|
|
101
396
|
style: {
|
|
102
|
-
...
|
|
397
|
+
...DEFAULT_TEXT_STYLE,
|
|
103
398
|
...asset.textStyle ?? {},
|
|
104
399
|
...params.textStyle ?? {}
|
|
105
400
|
},
|
|
@@ -107,10 +402,10 @@ var ClipRegistry = class {
|
|
|
107
402
|
height: params.projectHeight,
|
|
108
403
|
durationUs: params.durationUs
|
|
109
404
|
});
|
|
110
|
-
return
|
|
405
|
+
return createSubtitleClip({
|
|
111
406
|
srt: params.srt ?? asset.srt ?? "",
|
|
112
407
|
style: {
|
|
113
|
-
...
|
|
408
|
+
...DEFAULT_SUBTITLE_STYLE,
|
|
114
409
|
...asset.subtitleStyle ?? {},
|
|
115
410
|
...params.subtitleStyle ?? {}
|
|
116
411
|
},
|
|
@@ -131,7 +426,7 @@ var ClipRegistry = class {
|
|
|
131
426
|
let clip;
|
|
132
427
|
if (asset.kind === "text" || asset.kind === "subtitle") clip = this.buildProceduralClip(params, asset);
|
|
133
428
|
else if (runtime.master) clip = await runtime.master.clone();
|
|
134
|
-
else if (runtime.file) clip = (await
|
|
429
|
+
else if (runtime.file) clip = (await createClipFromFile(runtime.file, asset.kind)).clip;
|
|
135
430
|
else throw new Error(`素材缺少可用的数据源:${asset.name}`);
|
|
136
431
|
const wrapped = params.inPointUs > 0 ? new OffsetClip(clip, {
|
|
137
432
|
inPointUs: params.inPointUs,
|
|
@@ -177,7 +472,7 @@ var ClipRegistry = class {
|
|
|
177
472
|
runtime?.master?.destroy();
|
|
178
473
|
} catch {}
|
|
179
474
|
this.assets.delete(assetId);
|
|
180
|
-
|
|
475
|
+
require_thumbnails.releaseThumbnails(assetId);
|
|
181
476
|
}
|
|
182
477
|
destroyAllInstances() {
|
|
183
478
|
for (const [id] of this.derived) this.destroyInstance(id);
|
|
@@ -189,7 +484,7 @@ var ClipRegistry = class {
|
|
|
189
484
|
try {
|
|
190
485
|
runtime.master?.destroy();
|
|
191
486
|
} catch {}
|
|
192
|
-
|
|
487
|
+
require_thumbnails.releaseThumbnails(assetId);
|
|
193
488
|
}
|
|
194
489
|
this.assets.clear();
|
|
195
490
|
}
|
|
@@ -503,17 +798,23 @@ function createEditorRuntime() {
|
|
|
503
798
|
}
|
|
504
799
|
};
|
|
505
800
|
}
|
|
801
|
+
// src/lib/error.ts
|
|
802
|
+
/**
|
|
803
|
+
* 异常 → 可展示文本。
|
|
804
|
+
*
|
|
805
|
+
* 各模块都会在 catch 里把 `unknown` 转成字符串,集中一处避免多份实现漂移。
|
|
806
|
+
*/
|
|
807
|
+
function errorMessage(err) {
|
|
808
|
+
return err instanceof Error ? err.message : String(err);
|
|
809
|
+
}
|
|
506
810
|
// src/editor/create-editor.ts
|
|
507
811
|
function resolveBridge(bridge) {
|
|
508
812
|
if (bridge != null) return bridge;
|
|
509
813
|
return require_waveform.createWebBridge();
|
|
510
814
|
}
|
|
511
815
|
var loadImportFiles = () => Promise.resolve().then(() => import_files_exports);
|
|
512
|
-
var loadImportPipeline = () => Promise.resolve().then(() =>
|
|
816
|
+
var loadImportPipeline = () => Promise.resolve().then(() => import_pipeline_exports);
|
|
513
817
|
var loadExportRunner = () => Promise.resolve().then(() => export_runner_exports);
|
|
514
|
-
function errorMessage$1(err) {
|
|
515
|
-
return err instanceof Error ? err.message : String(err);
|
|
516
|
-
}
|
|
517
818
|
/**
|
|
518
819
|
* 创建编辑器实例。
|
|
519
820
|
*
|
|
@@ -546,7 +847,10 @@ function createEditor(options = {}) {
|
|
|
546
847
|
let disposed = false;
|
|
547
848
|
let lastPlaying = false;
|
|
548
849
|
let capabilityPromise = null;
|
|
850
|
+
/** 防抖计时器:最后一次变更后等待 intervalMs 再写盘 */
|
|
549
851
|
let autoSaveTimer = null;
|
|
852
|
+
/** 串行化写盘:自动保存与手动保存永不并发(并发会交错写同一 namespace) */
|
|
853
|
+
let saveChain = Promise.resolve({ ok: true });
|
|
550
854
|
let prevProject = store.getState().project;
|
|
551
855
|
let prevAssets = store.getState().assets;
|
|
552
856
|
let prevSelection = store.getState().selectedClipIds;
|
|
@@ -565,11 +869,20 @@ function createEditor(options = {}) {
|
|
|
565
869
|
ns: namespace
|
|
566
870
|
};
|
|
567
871
|
}
|
|
872
|
+
/**
|
|
873
|
+
* 自动保存调度(防抖)。
|
|
874
|
+
*
|
|
875
|
+
* 语义是「最后一次结构性变更后静置 intervalMs 才写盘」,而不是
|
|
876
|
+
* 「第一次变更后固定 intervalMs 写盘」:
|
|
877
|
+
* 后者在持续编辑(拖滑块、连续裁剪)时会中途写一次,既浪费 I/O,
|
|
878
|
+
* 又会在宿主展示「保存中」时产生无意义的闪烁。每次新变更都重置计时器。
|
|
879
|
+
*/
|
|
568
880
|
function scheduleAutoSave() {
|
|
569
|
-
if (autoSave == null || disposed
|
|
881
|
+
if (autoSave == null || disposed) return;
|
|
882
|
+
if (autoSaveTimer != null) clearTimeout(autoSaveTimer);
|
|
570
883
|
autoSaveTimer = setTimeout(() => {
|
|
571
884
|
autoSaveTimer = null;
|
|
572
|
-
|
|
885
|
+
enqueueSave().catch((err) => {
|
|
573
886
|
bus.emit("error", {
|
|
574
887
|
scope: "auto-save",
|
|
575
888
|
error: err
|
|
@@ -577,6 +890,40 @@ function createEditor(options = {}) {
|
|
|
577
890
|
});
|
|
578
891
|
}, autoSave.intervalMs ?? 2e3);
|
|
579
892
|
}
|
|
893
|
+
/** 把一次保存排进队列,返回该次保存的结果 */
|
|
894
|
+
function enqueueSave() {
|
|
895
|
+
const run = saveChain.then(() => writeProject());
|
|
896
|
+
saveChain = run.catch(() => ({ ok: false }));
|
|
897
|
+
return run;
|
|
898
|
+
}
|
|
899
|
+
/** 真正写盘 + 广播 project:saved */
|
|
900
|
+
async function writeProject() {
|
|
901
|
+
const { port, ns } = saveTarget();
|
|
902
|
+
const projectAtStart = store.getState().project;
|
|
903
|
+
const result = await port.save(ns, editor.serialize());
|
|
904
|
+
const changedDuringSave = store.getState().project !== projectAtStart;
|
|
905
|
+
if (result.ok) {
|
|
906
|
+
if (!changedDuringSave) store.getState().markSaved();
|
|
907
|
+
else scheduleAutoSave();
|
|
908
|
+
bus.emit("project:saved", result.path != null ? {
|
|
909
|
+
ok: true,
|
|
910
|
+
path: result.path
|
|
911
|
+
} : { ok: true });
|
|
912
|
+
} else bus.emit("project:saved", {
|
|
913
|
+
ok: false,
|
|
914
|
+
...result.error != null ? { error: result.error } : {},
|
|
915
|
+
...result.path != null ? { path: result.path } : {}
|
|
916
|
+
});
|
|
917
|
+
return result;
|
|
918
|
+
}
|
|
919
|
+
/** 卸载 / 关闭前把待写的防抖任务立刻执行 */
|
|
920
|
+
function flushAutoSave() {
|
|
921
|
+
if (autoSaveTimer != null) {
|
|
922
|
+
clearTimeout(autoSaveTimer);
|
|
923
|
+
autoSaveTimer = null;
|
|
924
|
+
}
|
|
925
|
+
return enqueueSave();
|
|
926
|
+
}
|
|
580
927
|
const unsubscribeStore = store.subscribe((state) => {
|
|
581
928
|
if (disposed) return;
|
|
582
929
|
if (state.project !== prevProject) {
|
|
@@ -585,11 +932,11 @@ function createEditor(options = {}) {
|
|
|
585
932
|
project: state.project,
|
|
586
933
|
dirty: state.dirty
|
|
587
934
|
});
|
|
935
|
+
if (autoSave != null) scheduleAutoSave();
|
|
588
936
|
}
|
|
589
937
|
if (state.dirty !== prevDirty) {
|
|
590
938
|
prevDirty = state.dirty;
|
|
591
939
|
bus.emit("project:dirty", { dirty: state.dirty });
|
|
592
|
-
if (state.dirty && autoSave != null) scheduleAutoSave();
|
|
593
940
|
}
|
|
594
941
|
if (state.assets !== prevAssets) {
|
|
595
942
|
prevAssets = state.assets;
|
|
@@ -695,8 +1042,10 @@ function createEditor(options = {}) {
|
|
|
695
1042
|
if (file == null) continue;
|
|
696
1043
|
try {
|
|
697
1044
|
const result = await importFile(runtime.clips, file, void 0, { id: asset.id });
|
|
1045
|
+
const { regenerateDerived } = await loadImportPipeline();
|
|
1046
|
+
const withDerived = await regenerateDerived(runtime.clips, result.asset, file);
|
|
698
1047
|
store.getState().replaceAsset(asset.id, {
|
|
699
|
-
...
|
|
1048
|
+
...withDerived,
|
|
700
1049
|
id: asset.id
|
|
701
1050
|
});
|
|
702
1051
|
stillMissing.delete(asset.id);
|
|
@@ -718,20 +1067,7 @@ function createEditor(options = {}) {
|
|
|
718
1067
|
return { missingAssetIds: remaining };
|
|
719
1068
|
},
|
|
720
1069
|
async save() {
|
|
721
|
-
|
|
722
|
-
const result = await port.save(ns, editor.serialize());
|
|
723
|
-
if (result.ok) {
|
|
724
|
-
store.getState().markSaved();
|
|
725
|
-
bus.emit("project:saved", result.path != null ? {
|
|
726
|
-
ok: true,
|
|
727
|
-
path: result.path
|
|
728
|
-
} : { ok: true });
|
|
729
|
-
} else bus.emit("project:saved", {
|
|
730
|
-
ok: false,
|
|
731
|
-
...result.error != null ? { error: result.error } : {},
|
|
732
|
-
...result.path != null ? { path: result.path } : {}
|
|
733
|
-
});
|
|
734
|
-
return result;
|
|
1070
|
+
return enqueueSave();
|
|
735
1071
|
},
|
|
736
1072
|
async saveAs(nextStorage, nextNamespace) {
|
|
737
1073
|
storage = nextStorage;
|
|
@@ -741,7 +1077,7 @@ function createEditor(options = {}) {
|
|
|
741
1077
|
storage: nextStorage,
|
|
742
1078
|
namespace: nextNamespace
|
|
743
1079
|
};
|
|
744
|
-
return
|
|
1080
|
+
return enqueueSave();
|
|
745
1081
|
},
|
|
746
1082
|
isDirty: () => store.getState().dirty,
|
|
747
1083
|
isEmpty: () => store.getState().project.clips.length === 0,
|
|
@@ -783,8 +1119,10 @@ function createEditor(options = {}) {
|
|
|
783
1119
|
const { importFile } = await loadImportPipeline();
|
|
784
1120
|
if (!store.getState().assets.some((a) => a.id === assetId)) throw new Error(`素材不存在:${assetId}`);
|
|
785
1121
|
const result = await importFile(runtime.clips, file, void 0, { id: assetId });
|
|
1122
|
+
const { regenerateDerived } = await loadImportPipeline();
|
|
1123
|
+
const withDerived = await regenerateDerived(runtime.clips, result.asset, file);
|
|
786
1124
|
store.getState().replaceAsset(assetId, {
|
|
787
|
-
...
|
|
1125
|
+
...withDerived,
|
|
788
1126
|
id: assetId
|
|
789
1127
|
});
|
|
790
1128
|
store.getState().clearMissing(assetId);
|
|
@@ -896,7 +1234,7 @@ function createEditor(options = {}) {
|
|
|
896
1234
|
} catch (err) {
|
|
897
1235
|
if (cancelled || err instanceof require_waveform.ExportCancelledError) throw err instanceof require_waveform.ExportCancelledError ? err : new require_waveform.ExportCancelledError();
|
|
898
1236
|
bus.emit("export:error", {
|
|
899
|
-
message: errorMessage
|
|
1237
|
+
message: errorMessage(err),
|
|
900
1238
|
error: err
|
|
901
1239
|
});
|
|
902
1240
|
throw err;
|
|
@@ -916,7 +1254,7 @@ function createEditor(options = {}) {
|
|
|
916
1254
|
} catch (err) {
|
|
917
1255
|
return {
|
|
918
1256
|
ok: false,
|
|
919
|
-
error: errorMessage
|
|
1257
|
+
error: errorMessage(err)
|
|
920
1258
|
};
|
|
921
1259
|
}
|
|
922
1260
|
},
|
|
@@ -927,18 +1265,229 @@ function createEditor(options = {}) {
|
|
|
927
1265
|
async dispose() {
|
|
928
1266
|
if (disposed) return;
|
|
929
1267
|
disposed = true;
|
|
930
|
-
|
|
931
|
-
clearTimeout(autoSaveTimer);
|
|
932
|
-
autoSaveTimer = null;
|
|
933
|
-
}
|
|
1268
|
+
const pending = flushAutoSave();
|
|
934
1269
|
unsubscribeStore();
|
|
935
1270
|
bus.emit("dispose", {});
|
|
936
1271
|
await runtime.dispose();
|
|
1272
|
+
try {
|
|
1273
|
+
await pending;
|
|
1274
|
+
} catch {}
|
|
937
1275
|
bus.clear();
|
|
938
1276
|
}
|
|
939
1277
|
};
|
|
940
1278
|
return editor;
|
|
941
1279
|
}
|
|
1280
|
+
// src/engine/webav/import-pipeline.ts
|
|
1281
|
+
/**
|
|
1282
|
+
* 素材导入流水线(供 feature 与 Lab 共用)。
|
|
1283
|
+
*
|
|
1284
|
+
* 步骤:校验类型 → 创建母版 IClip → 读 meta → 抽缩略图 / 波形 → 写入 registry 与 store。
|
|
1285
|
+
*
|
|
1286
|
+
* ⚠️ 所有 WebAV 调用都落在 core 内;本文件位于 core/webav 下,允许 import '@webav/*'。
|
|
1287
|
+
*/
|
|
1288
|
+
var import_pipeline_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
1289
|
+
UnsupportedFileError: () => require_file_reader.UnsupportedFileError,
|
|
1290
|
+
createSubtitleAsset: () => createSubtitleAsset,
|
|
1291
|
+
createTextAsset: () => createTextAsset,
|
|
1292
|
+
importFile: () => importFile,
|
|
1293
|
+
loadAudioClipForAsset: () => loadAudioClipForAsset,
|
|
1294
|
+
loadPcmForAsset: () => loadPcmForAsset,
|
|
1295
|
+
regenerateDerived: () => regenerateDerived
|
|
1296
|
+
});
|
|
1297
|
+
var THUMBNAIL_COUNT = 4;
|
|
1298
|
+
var THUMBNAIL_WIDTH = 120;
|
|
1299
|
+
var WAVEFORM_BUCKETS = 400;
|
|
1300
|
+
/**
|
|
1301
|
+
* 导入单个文件,生成素材记录。
|
|
1302
|
+
*
|
|
1303
|
+
* @param onProgress 0~1 进度(用于卡片骨架/进度条)
|
|
1304
|
+
* @throws UnsupportedFileError 类型不支持时
|
|
1305
|
+
*/
|
|
1306
|
+
async function importFile(registry, file, onProgress, options = {}) {
|
|
1307
|
+
const detected = require_file_reader.detectFileType(file);
|
|
1308
|
+
onProgress?.(.1);
|
|
1309
|
+
const { clip, meta } = await createClipFromFile(file, detected.kind);
|
|
1310
|
+
onProgress?.(.4);
|
|
1311
|
+
const assetId = options.id ?? require_id.createId("asset");
|
|
1312
|
+
const asset = {
|
|
1313
|
+
id: assetId,
|
|
1314
|
+
name: file.name,
|
|
1315
|
+
kind: detected.kind,
|
|
1316
|
+
mimeType: detected.mimeType,
|
|
1317
|
+
size: file.size,
|
|
1318
|
+
durationUs: require_time.asUs(meta.durationUs),
|
|
1319
|
+
width: meta.width,
|
|
1320
|
+
height: meta.height,
|
|
1321
|
+
hasAudio: meta.hasAudio,
|
|
1322
|
+
createdAt: Date.now()
|
|
1323
|
+
};
|
|
1324
|
+
if (detected.kind === "video") {
|
|
1325
|
+
try {
|
|
1326
|
+
asset.thumbnailUrls = (await require_thumbnails.generateThumbnails(clip, assetId, THUMBNAIL_COUNT, meta.durationUs, THUMBNAIL_WIDTH)).map((f) => f.url);
|
|
1327
|
+
} catch (err) {
|
|
1328
|
+
console.warn("[importFile] 缩略图生成失败", err);
|
|
1329
|
+
}
|
|
1330
|
+
onProgress?.(.7);
|
|
1331
|
+
}
|
|
1332
|
+
if (detected.kind === "audio" || detected.kind === "video" && meta.hasAudio) {
|
|
1333
|
+
try {
|
|
1334
|
+
if (detected.kind === "audio") {
|
|
1335
|
+
const pcm = clip.getPCMData();
|
|
1336
|
+
asset.waveform = require_waveform.computeWaveform(pcm, WAVEFORM_BUCKETS);
|
|
1337
|
+
}
|
|
1338
|
+
} catch (err) {
|
|
1339
|
+
console.warn("[importFile] 波形生成失败", err);
|
|
1340
|
+
}
|
|
1341
|
+
onProgress?.(.85);
|
|
1342
|
+
}
|
|
1343
|
+
registry.registerAsset(asset, file, clip);
|
|
1344
|
+
onProgress?.(1);
|
|
1345
|
+
return {
|
|
1346
|
+
asset,
|
|
1347
|
+
clipId: assetId,
|
|
1348
|
+
master: clip
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* 为一个「已有元数据」的素材重新生成派生数据(缩略图 / 波形)。
|
|
1353
|
+
*
|
|
1354
|
+
* 用于重新打开工程后 `resolveFile` / `reconnectAsset` 找回文件时:
|
|
1355
|
+
* 元数据里不含 thumbnailUrls(那是会话内 object URL,刷新即失效),
|
|
1356
|
+
* 如果不补这一步,素材卡片会一直显示图标占位 —— 用户看到的就是「资源坏了」。
|
|
1357
|
+
*
|
|
1358
|
+
* 失败只记警告:派生数据缺失不影响素材可用性。
|
|
1359
|
+
*/
|
|
1360
|
+
async function regenerateDerived(registry, asset, file) {
|
|
1361
|
+
const master = registry.getMaster(asset.id);
|
|
1362
|
+
if (master == null) return asset;
|
|
1363
|
+
const next = { ...asset };
|
|
1364
|
+
if (asset.kind === "video") {
|
|
1365
|
+
const { regenerateThumbnails } = await Promise.resolve().then(() => require("../chunks/thumbnails-DEjnao5r.cjs")).then((n) => n.thumbnails_exports);
|
|
1366
|
+
try {
|
|
1367
|
+
next.thumbnailUrls = (await regenerateThumbnails(master, asset.id, THUMBNAIL_COUNT, asset.durationUs, THUMBNAIL_WIDTH)).map((f) => f.url);
|
|
1368
|
+
} catch (err) {
|
|
1369
|
+
console.warn("[regenerateDerived] 缩略图生成失败", asset.id, err);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (asset.kind === "audio" || asset.kind === "video" && asset.hasAudio) try {
|
|
1373
|
+
if (asset.kind === "audio") {
|
|
1374
|
+
const pcm = master.getPCMData();
|
|
1375
|
+
next.waveform = require_waveform.computeWaveform(pcm, WAVEFORM_BUCKETS);
|
|
1376
|
+
}
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
console.warn("[regenerateDerived] 波形生成失败", asset.id, err);
|
|
1379
|
+
}
|
|
1380
|
+
return next;
|
|
1381
|
+
}
|
|
1382
|
+
/** 新建文字素材(非文件来源,无需二进制) */
|
|
1383
|
+
function createTextAsset(params) {
|
|
1384
|
+
const assetId = params.id ?? require_id.createId("asset");
|
|
1385
|
+
const style = {
|
|
1386
|
+
...DEFAULT_TEXT_STYLE,
|
|
1387
|
+
...params.style
|
|
1388
|
+
};
|
|
1389
|
+
const durationUs = params.durationUs ?? 5e6;
|
|
1390
|
+
const master = createTextClip({
|
|
1391
|
+
text: params.text,
|
|
1392
|
+
style,
|
|
1393
|
+
width: params.width,
|
|
1394
|
+
height: params.height,
|
|
1395
|
+
durationUs
|
|
1396
|
+
});
|
|
1397
|
+
const asset = {
|
|
1398
|
+
id: assetId,
|
|
1399
|
+
name: params.text.slice(0, 24) || "文字",
|
|
1400
|
+
kind: "text",
|
|
1401
|
+
mimeType: "text/x-title",
|
|
1402
|
+
size: 0,
|
|
1403
|
+
durationUs: require_time.asUs(durationUs),
|
|
1404
|
+
width: params.width,
|
|
1405
|
+
height: params.height,
|
|
1406
|
+
hasAudio: false,
|
|
1407
|
+
createdAt: Date.now(),
|
|
1408
|
+
text: params.text,
|
|
1409
|
+
textStyle: style
|
|
1410
|
+
};
|
|
1411
|
+
params.registry.registerAsset(asset, null, master);
|
|
1412
|
+
return {
|
|
1413
|
+
asset,
|
|
1414
|
+
clipId: assetId
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* 解码素材的完整 PCM(48kHz 双声道),供 Worker 导出使用。
|
|
1419
|
+
*
|
|
1420
|
+
* ⚠️ 实测:Worker 内没有 AudioContext / OfflineAudioContext,
|
|
1421
|
+
* `new AudioClip(stream)` 会在 `decodeAudioData` 处抛错,因此必须在主线程解码后
|
|
1422
|
+
* 把 Float32Array 传过去(`new AudioClip(Float32Array[])` 走纯归一化分支)。
|
|
1423
|
+
*
|
|
1424
|
+
* @returns 声道数组;失败或素材无音轨时返回 undefined
|
|
1425
|
+
*/
|
|
1426
|
+
async function loadPcmForAsset(registry, assetId) {
|
|
1427
|
+
if (registry.getAsset(assetId) == null) return void 0;
|
|
1428
|
+
const file = registry.getFile(assetId);
|
|
1429
|
+
if (file == null) return void 0;
|
|
1430
|
+
try {
|
|
1431
|
+
const clip = new _webav_av_cliper.AudioClip(require_file_reader.fileToStream(file));
|
|
1432
|
+
await clip.ready;
|
|
1433
|
+
const pcm = clip.getPCMData().map((channel) => new Float32Array(channel));
|
|
1434
|
+
clip.destroy();
|
|
1435
|
+
return pcm;
|
|
1436
|
+
} catch (err) {
|
|
1437
|
+
console.warn("[loadPcmForAsset] 音频解码失败", err);
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
/** 解析素材的音频 PCM(供预览音频引擎注册) */
|
|
1442
|
+
async function loadAudioClipForAsset(registry, assetId) {
|
|
1443
|
+
const file = registry.getFile(assetId);
|
|
1444
|
+
if (file == null) return null;
|
|
1445
|
+
const asset = registry.getAsset(assetId);
|
|
1446
|
+
if (asset == null) return null;
|
|
1447
|
+
if (asset.kind !== "audio" && asset.kind !== "video") return null;
|
|
1448
|
+
const clip = new _webav_av_cliper.AudioClip(require_file_reader.fileToStream(file));
|
|
1449
|
+
await clip.ready;
|
|
1450
|
+
return clip;
|
|
1451
|
+
}
|
|
1452
|
+
/** 用第一条字幕文本给素材命名(无内容时回退为「字幕」) */
|
|
1453
|
+
function subtitleName(srt) {
|
|
1454
|
+
const base = (srt.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0 && !line.includes("-->") && !/^\d+$/.test(line)) ?? "") || "字幕";
|
|
1455
|
+
return base.length > 20 ? `${base.slice(0, 20)}…` : base;
|
|
1456
|
+
}
|
|
1457
|
+
/** 新建字幕素材(内容来自粘贴的 SRT,无源文件) */
|
|
1458
|
+
function createSubtitleAsset(params) {
|
|
1459
|
+
const assetId = params.id ?? require_id.createId("asset");
|
|
1460
|
+
const style = {
|
|
1461
|
+
...DEFAULT_SUBTITLE_STYLE,
|
|
1462
|
+
...params.style
|
|
1463
|
+
};
|
|
1464
|
+
const durationUs = params.durationUs ?? 5e6;
|
|
1465
|
+
const master = createSubtitleClip({
|
|
1466
|
+
srt: params.srt,
|
|
1467
|
+
style,
|
|
1468
|
+
width: params.width,
|
|
1469
|
+
height: params.height
|
|
1470
|
+
});
|
|
1471
|
+
const asset = {
|
|
1472
|
+
id: assetId,
|
|
1473
|
+
name: subtitleName(params.srt),
|
|
1474
|
+
kind: "subtitle",
|
|
1475
|
+
mimeType: "text/plain",
|
|
1476
|
+
size: params.srt.length,
|
|
1477
|
+
durationUs: require_time.asUs(durationUs),
|
|
1478
|
+
width: params.width,
|
|
1479
|
+
height: params.height,
|
|
1480
|
+
hasAudio: false,
|
|
1481
|
+
createdAt: Date.now(),
|
|
1482
|
+
srt: params.srt,
|
|
1483
|
+
subtitleStyle: style
|
|
1484
|
+
};
|
|
1485
|
+
params.registry.registerAsset(asset, null, master);
|
|
1486
|
+
return {
|
|
1487
|
+
asset,
|
|
1488
|
+
clipId: assetId
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
942
1491
|
// src/engine/webav/import-files.ts
|
|
943
1492
|
var import_files_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ importFiles: () => importFiles });
|
|
944
1493
|
/** 找出可以「重连」的素材 id(同名 + 同类型 + 当前确实缺少二进制) */
|
|
@@ -946,9 +1495,6 @@ function findReconnectableId(state, registry, file, kind) {
|
|
|
946
1495
|
if (state.missingAssetIds.length === 0) return null;
|
|
947
1496
|
return state.assets.find((asset) => state.missingAssetIds.includes(asset.id) && asset.kind === kind && asset.name === file.name && registry.getFile(asset.id) == null)?.id ?? null;
|
|
948
1497
|
}
|
|
949
|
-
function errorMessage(err) {
|
|
950
|
-
return err instanceof Error ? err.message : String(err);
|
|
951
|
-
}
|
|
952
1498
|
/**
|
|
953
1499
|
* 批量导入素材。
|
|
954
1500
|
*
|
|
@@ -971,7 +1517,7 @@ async function importFiles(registry, store, files, options = {}) {
|
|
|
971
1517
|
const requestedId = reconnectId ?? idFor?.(file, index) ?? null;
|
|
972
1518
|
const assetId = requestedId != null && requestedId !== "" ? requestedId : void 0;
|
|
973
1519
|
if (reconnectId != null) registry.removeAsset(reconnectId);
|
|
974
|
-
const result = await
|
|
1520
|
+
const result = await importFile(registry, file, (progress) => onProgress?.(index, progress), assetId != null ? { id: assetId } : void 0);
|
|
975
1521
|
if (reconnectId != null) {
|
|
976
1522
|
store.getState().replaceAsset(reconnectId, result.asset);
|
|
977
1523
|
store.getState().clearMissing(reconnectId);
|
|
@@ -1190,11 +1736,12 @@ function mapClipToOutput(clip, source, target) {
|
|
|
1190
1736
|
* @param instance 该 clip 独占的 IClip
|
|
1191
1737
|
* @param clip 时间轴数据(提供 volume / fadeIn / fadeOut)
|
|
1192
1738
|
*/
|
|
1193
|
-
function withClipGain(instance, clip) {
|
|
1194
|
-
|
|
1739
|
+
function withClipGain(instance, clip, options = {}) {
|
|
1740
|
+
const volume = options.trackMuted === true ? 0 : clip.volume;
|
|
1741
|
+
if (!(clip.muted || options.trackMuted === true || volume !== 1 || clip.fadeInUs > 0 || clip.fadeOutUs > 0)) return instance;
|
|
1195
1742
|
if (clip.muted) return instance;
|
|
1196
1743
|
return new GainClip(instance, {
|
|
1197
|
-
volume
|
|
1744
|
+
volume,
|
|
1198
1745
|
fadeInUs: clip.fadeInUs,
|
|
1199
1746
|
fadeOutUs: clip.fadeOutUs,
|
|
1200
1747
|
durationUs: clip.durationUs,
|
|
@@ -1242,7 +1789,7 @@ async function buildCombinator(inputs, opts) {
|
|
|
1242
1789
|
const { instance, track } = item;
|
|
1243
1790
|
const clip = mapClipToOutput(item.clip, sourceSize, targetSize);
|
|
1244
1791
|
if (track.kind === "audio" && !opts.includeAudio) continue;
|
|
1245
|
-
|
|
1792
|
+
const trackMuted = track.muted;
|
|
1246
1793
|
if (clip.muted) continue;
|
|
1247
1794
|
const duration = require_canvas_size.clampStillDuration(clip.durationUs);
|
|
1248
1795
|
if (clip.durationUs <= 0) {
|
|
@@ -1256,7 +1803,7 @@ async function buildCombinator(inputs, opts) {
|
|
|
1256
1803
|
...clip,
|
|
1257
1804
|
durationUs: duration
|
|
1258
1805
|
};
|
|
1259
|
-
const sprite = createOffscreenSprite(withClipGain(instance, normalized), normalized, { zIndex });
|
|
1806
|
+
const sprite = createOffscreenSprite(withClipGain(instance, normalized, { trackMuted }), normalized, { zIndex });
|
|
1260
1807
|
applyOpacityFade(sprite, normalized);
|
|
1261
1808
|
await combinator.addSprite(sprite, { main: false });
|
|
1262
1809
|
maxTimeUs = Math.max(maxTimeUs, clip.startUs + duration);
|
|
@@ -1363,7 +1910,7 @@ async function buildWorkerPayload(registry, project, assets, settings, maxTimeUs
|
|
|
1363
1910
|
continue;
|
|
1364
1911
|
}
|
|
1365
1912
|
let pcm;
|
|
1366
|
-
if (settings.includeAudio && (asset.kind === "audio" || asset.kind === "video" && asset.hasAudio)) pcm = await
|
|
1913
|
+
if (settings.includeAudio && (asset.kind === "audio" || asset.kind === "video" && asset.hasAudio)) pcm = await loadPcmForAsset(registry, asset.id);
|
|
1367
1914
|
payloadAssets.push({
|
|
1368
1915
|
id: asset.id,
|
|
1369
1916
|
kind: asset.kind,
|
|
@@ -1396,6 +1943,12 @@ async function buildWorkerPayload(registry, project, assets, settings, maxTimeUs
|
|
|
1396
1943
|
includeAudio: settings.includeAudio,
|
|
1397
1944
|
bgColor: "#000000",
|
|
1398
1945
|
maxTimeUs,
|
|
1946
|
+
tracks: project.tracks.map((t) => ({
|
|
1947
|
+
id: t.id,
|
|
1948
|
+
kind: t.kind,
|
|
1949
|
+
hidden: t.hidden,
|
|
1950
|
+
muted: t.muted
|
|
1951
|
+
})),
|
|
1399
1952
|
clips
|
|
1400
1953
|
},
|
|
1401
1954
|
missing
|
|
@@ -1670,6 +2223,7 @@ var SpriteSynchronizer = class {
|
|
|
1670
2223
|
applyClipToSprite(existing, clip);
|
|
1671
2224
|
existing.zIndex = zIndexOf(project.tracks, clip.trackId, clip);
|
|
1672
2225
|
existing.visible = !track.hidden;
|
|
2226
|
+
if (previous == null || previous.durationUs !== clip.durationUs || previous.fadeInUs !== clip.fadeInUs || previous.fadeOutUs !== clip.fadeOutUs || previous.opacity !== clip.opacity) applyOpacityFade(existing, clip);
|
|
1673
2227
|
this.lastClips.set(clip.id, clip);
|
|
1674
2228
|
continue;
|
|
1675
2229
|
}
|
|
@@ -1679,7 +2233,7 @@ var SpriteSynchronizer = class {
|
|
|
1679
2233
|
this.registry.destroyInstance(clip.id);
|
|
1680
2234
|
}
|
|
1681
2235
|
try {
|
|
1682
|
-
const
|
|
2236
|
+
const sprite = createVisibleSprite(await this.registry.createInstance({
|
|
1683
2237
|
timelineClipId: clip.id,
|
|
1684
2238
|
assetId: clip.assetId,
|
|
1685
2239
|
inPointUs: clip.inPointUs,
|
|
@@ -1690,9 +2244,9 @@ var SpriteSynchronizer = class {
|
|
|
1690
2244
|
subtitleStyle: clip.subtitleStyle,
|
|
1691
2245
|
projectWidth: project.width,
|
|
1692
2246
|
projectHeight: project.height
|
|
1693
|
-
});
|
|
1694
|
-
const sprite = createVisibleSprite(track.kind === "audio" ? instance : instance, clip, { zIndex: zIndexOf(project.tracks, clip.trackId, clip) });
|
|
2247
|
+
}), clip, { zIndex: zIndexOf(project.tracks, clip.trackId, clip) });
|
|
1695
2248
|
sprite.visible = !track.hidden;
|
|
2249
|
+
applyOpacityFade(sprite, clip);
|
|
1696
2250
|
if (track.kind === "audio" && asset.kind === "audio") {
|
|
1697
2251
|
sprite.rect.w = 0;
|
|
1698
2252
|
sprite.rect.h = 0;
|
|
@@ -2063,6 +2617,16 @@ var cancelled = false;
|
|
|
2063
2617
|
function toStream(blob) {
|
|
2064
2618
|
return blob.stream();
|
|
2065
2619
|
}
|
|
2620
|
+
/** 轨道静音:音量置 0(画面不受影响) */
|
|
2621
|
+
function withSilence(clip) {
|
|
2622
|
+
return new GainClip(clip, {
|
|
2623
|
+
volume: 0,
|
|
2624
|
+
fadeInUs: 0,
|
|
2625
|
+
fadeOutUs: 0,
|
|
2626
|
+
durationUs: Number.MAX_SAFE_INTEGER,
|
|
2627
|
+
playbackRate: 1
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2066
2630
|
/** 音量 / 淡入淡出包装(与主线程 exporter.withClipGain 保持一致的语义) */
|
|
2067
2631
|
function withGain(clip, timeline) {
|
|
2068
2632
|
if (timeline.volume === 1 && timeline.fadeInUs === 0 && timeline.fadeOutUs === 0) return clip;
|
|
@@ -2125,6 +2689,7 @@ async function buildClipInWorker(asset) {
|
|
|
2125
2689
|
*/
|
|
2126
2690
|
async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
|
|
2127
2691
|
const assetMap = new Map(assets.map((a) => [a.id, a]));
|
|
2692
|
+
const trackMap = new Map((timeline.tracks ?? []).map((t) => [t.id, t]));
|
|
2128
2693
|
const combinator = new _webav_av_cliper.Combinator({
|
|
2129
2694
|
width: timeline.width,
|
|
2130
2695
|
height: timeline.height,
|
|
@@ -2133,7 +2698,8 @@ async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
|
|
|
2133
2698
|
bgColor: timeline.bgColor,
|
|
2134
2699
|
audio: timeline.includeAudio ? void 0 : false
|
|
2135
2700
|
});
|
|
2136
|
-
const
|
|
2701
|
+
const masterCache = /* @__PURE__ */ new Map();
|
|
2702
|
+
const derived = [];
|
|
2137
2703
|
const skipped = [];
|
|
2138
2704
|
const sourceSize = {
|
|
2139
2705
|
width: timeline.projectWidth ?? timeline.width,
|
|
@@ -2165,11 +2731,14 @@ async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
|
|
|
2165
2731
|
});
|
|
2166
2732
|
continue;
|
|
2167
2733
|
}
|
|
2734
|
+
const track = trackMap.get(item.clip.trackId);
|
|
2735
|
+
if (track?.hidden === true) continue;
|
|
2168
2736
|
if (item.clip.muted) continue;
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2737
|
+
if (item.kind === "audio" && !timeline.includeAudio) continue;
|
|
2738
|
+
let master = masterCache.get(asset.id);
|
|
2739
|
+
if (master == null) try {
|
|
2740
|
+
master = await buildClipInWorker(asset);
|
|
2741
|
+
masterCache.set(asset.id, master);
|
|
2173
2742
|
} catch (err) {
|
|
2174
2743
|
const reason = err instanceof Error ? err.message : String(err);
|
|
2175
2744
|
console.warn("[export.worker] clip skipped:", item.clip.id, reason);
|
|
@@ -2180,7 +2749,24 @@ async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
|
|
|
2180
2749
|
continue;
|
|
2181
2750
|
}
|
|
2182
2751
|
const tc = item.clip;
|
|
2183
|
-
|
|
2752
|
+
let instance;
|
|
2753
|
+
try {
|
|
2754
|
+
instance = await master.clone();
|
|
2755
|
+
} catch (err) {
|
|
2756
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
2757
|
+
skipped.push({
|
|
2758
|
+
clipId: tc.id,
|
|
2759
|
+
reason
|
|
2760
|
+
});
|
|
2761
|
+
continue;
|
|
2762
|
+
}
|
|
2763
|
+
if (tc.inPointUs > 0) instance = new OffsetClip(instance, {
|
|
2764
|
+
inPointUs: tc.inPointUs,
|
|
2765
|
+
durationUs: tc.durationUs * tc.playbackRate
|
|
2766
|
+
});
|
|
2767
|
+
derived.push(instance);
|
|
2768
|
+
const gainInput = track?.muted === true ? withSilence(instance) : instance;
|
|
2769
|
+
const sprite = new _webav_av_cliper.OffscreenSprite(withGain(gainInput, tc));
|
|
2184
2770
|
const mapped = mapTransformToOutput(tc.transform, sourceSize, targetSize);
|
|
2185
2771
|
sprite.rect.x = mapped.x;
|
|
2186
2772
|
sprite.rect.y = mapped.y;
|
|
@@ -2195,6 +2781,7 @@ async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
|
|
|
2195
2781
|
duration: tc.durationUs,
|
|
2196
2782
|
playbackRate: tc.playbackRate
|
|
2197
2783
|
};
|
|
2784
|
+
applyOpacityFade(sprite, tc);
|
|
2198
2785
|
zIndex += 1;
|
|
2199
2786
|
maxTimeUs = Math.max(maxTimeUs, tc.startUs + tc.durationUs);
|
|
2200
2787
|
await combinator.addSprite(sprite, { main: false });
|
|
@@ -2216,10 +2803,14 @@ async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
|
|
|
2216
2803
|
skipped
|
|
2217
2804
|
};
|
|
2218
2805
|
} finally {
|
|
2219
|
-
for (const clip of
|
|
2806
|
+
for (const clip of derived) try {
|
|
2807
|
+
clip.destroy();
|
|
2808
|
+
} catch {}
|
|
2809
|
+
derived.length = 0;
|
|
2810
|
+
for (const clip of masterCache.values()) try {
|
|
2220
2811
|
clip.destroy();
|
|
2221
2812
|
} catch {}
|
|
2222
|
-
|
|
2813
|
+
masterCache.clear();
|
|
2223
2814
|
try {
|
|
2224
2815
|
combinator.destroy();
|
|
2225
2816
|
} catch {}
|
|
@@ -2258,8 +2849,8 @@ function handleWorkerMessage(event) {
|
|
|
2258
2849
|
});
|
|
2259
2850
|
}
|
|
2260
2851
|
exports.ClipRegistry = ClipRegistry;
|
|
2261
|
-
exports.DEFAULT_SUBTITLE_STYLE =
|
|
2262
|
-
exports.DEFAULT_TEXT_STYLE =
|
|
2852
|
+
exports.DEFAULT_SUBTITLE_STYLE = DEFAULT_SUBTITLE_STYLE;
|
|
2853
|
+
exports.DEFAULT_TEXT_STYLE = DEFAULT_TEXT_STYLE;
|
|
2263
2854
|
exports.ENGINE_DEPENDENCIES_INSTALL_COMMAND = require_waveform.ENGINE_DEPENDENCIES_INSTALL_COMMAND;
|
|
2264
2855
|
exports.GainClip = GainClip;
|
|
2265
2856
|
exports.OffsetClip = OffsetClip;
|
|
@@ -2268,7 +2859,7 @@ exports.PlaybackClock = PlaybackClock;
|
|
|
2268
2859
|
exports.PreviewAudioEngine = PreviewAudioEngine;
|
|
2269
2860
|
exports.PreviewCanvas = PreviewCanvas;
|
|
2270
2861
|
exports.SpriteSynchronizer = SpriteSynchronizer;
|
|
2271
|
-
exports.TextClip =
|
|
2862
|
+
exports.TextClip = TextClip;
|
|
2272
2863
|
exports.UnsupportedFileError = require_file_reader.UnsupportedFileError;
|
|
2273
2864
|
exports.WorkerUnsupportedClipError = WorkerUnsupportedClipError;
|
|
2274
2865
|
exports.applyClipToSprite = applyClipToSprite;
|
|
@@ -2280,33 +2871,34 @@ exports.canUseWorker = canUseWorker;
|
|
|
2280
2871
|
exports.checkExportSupport = checkExportSupport;
|
|
2281
2872
|
exports.computeExportDuration = computeExportDuration;
|
|
2282
2873
|
exports.computeTimelineEnd = computeTimelineEnd;
|
|
2283
|
-
exports.createClipFromBuffer =
|
|
2284
|
-
exports.createClipFromFile =
|
|
2874
|
+
exports.createClipFromBuffer = createClipFromBuffer;
|
|
2875
|
+
exports.createClipFromFile = createClipFromFile;
|
|
2285
2876
|
exports.createEditor = createEditor;
|
|
2286
2877
|
exports.createEditorRuntime = createEditorRuntime;
|
|
2287
2878
|
exports.createEditorStore = require_waveform.createEditorStore;
|
|
2288
2879
|
exports.createEventBus = require_waveform.createEventBus;
|
|
2289
2880
|
exports.createHistoryStore = require_waveform.createHistoryStore;
|
|
2290
2881
|
exports.createOffscreenSprite = createOffscreenSprite;
|
|
2291
|
-
exports.createSubtitleAsset =
|
|
2292
|
-
exports.createSubtitleClip =
|
|
2293
|
-
exports.createTextAsset =
|
|
2294
|
-
exports.createTextClip =
|
|
2882
|
+
exports.createSubtitleAsset = createSubtitleAsset;
|
|
2883
|
+
exports.createSubtitleClip = createSubtitleClip;
|
|
2884
|
+
exports.createTextAsset = createTextAsset;
|
|
2885
|
+
exports.createTextClip = createTextClip;
|
|
2295
2886
|
exports.createVisibleSprite = createVisibleSprite;
|
|
2296
|
-
exports.cssFromStyle =
|
|
2887
|
+
exports.cssFromStyle = cssFromStyle;
|
|
2297
2888
|
exports.detectDependencies = require_waveform.detectDependencies;
|
|
2298
2889
|
exports.detectEnvironment = require_waveform.detectEnvironment;
|
|
2299
2890
|
exports.detectOpfs = require_waveform.detectOpfs;
|
|
2300
2891
|
exports.exportProject = exportProject;
|
|
2301
2892
|
exports.fixFMP4DurationStream = fixFMP4DurationStream;
|
|
2302
2893
|
exports.handleWorkerMessage = handleWorkerMessage;
|
|
2303
|
-
exports.importFile =
|
|
2894
|
+
exports.importFile = importFile;
|
|
2304
2895
|
exports.importFiles = importFiles;
|
|
2305
2896
|
exports.isEnvironmentUsable = require_waveform.isEnvironmentUsable;
|
|
2306
|
-
exports.loadAudioClipForAsset =
|
|
2307
|
-
exports.loadPcmForAsset =
|
|
2897
|
+
exports.loadAudioClipForAsset = loadAudioClipForAsset;
|
|
2898
|
+
exports.loadPcmForAsset = loadPcmForAsset;
|
|
2308
2899
|
exports.mapClipToOutput = mapClipToOutput;
|
|
2309
2900
|
exports.mapTransformToOutput = mapTransformToOutput;
|
|
2901
|
+
exports.regenerateDerived = regenerateDerived;
|
|
2310
2902
|
exports.runExportInWorker = runExportInWorker;
|
|
2311
2903
|
exports.selectAssetById = require_waveform.selectAssetById;
|
|
2312
2904
|
exports.selectAssets = require_waveform.selectAssets;
|