@glissade/export-web 0.7.0 → 0.8.0-pre.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/index.d.ts +34 -1
- package/dist/index.js +61 -17
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -88,6 +88,39 @@ interface WebExportResult {
|
|
|
88
88
|
declare class ExportUnsupportedError extends Error {
|
|
89
89
|
constructor(detail: string);
|
|
90
90
|
}
|
|
91
|
+
/** Encodability probes — the real ones wrap mediabunny; tests inject fakes. */
|
|
92
|
+
type VideoProbe = (codecs: VideoCodec[]) => Promise<VideoCodec | null>;
|
|
93
|
+
type AudioProbe = (codecs: AudioCodec[]) => Promise<AudioCodec | null>;
|
|
94
|
+
interface ExportSupport {
|
|
95
|
+
format: 'mp4' | 'webm';
|
|
96
|
+
/** first encodable video codec, or null if none. */
|
|
97
|
+
video: VideoCodec | null;
|
|
98
|
+
/** first encodable audio codec, or null if none. */
|
|
99
|
+
audio: AudioCodec | null;
|
|
100
|
+
/** true when video can encode (audio is optional — falls back to video-only). */
|
|
101
|
+
supported: boolean;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The resolved encodability matrix (§5.2): one row per container, so a UI can
|
|
105
|
+
* grey out options instead of failing mid-render. Audio absence is not a
|
|
106
|
+
* blocker — the export falls back to video-only.
|
|
107
|
+
*/
|
|
108
|
+
declare function probeExportSupport(opts?: {
|
|
109
|
+
width?: number;
|
|
110
|
+
height?: number;
|
|
111
|
+
bitrate?: number;
|
|
112
|
+
}): Promise<ExportSupport[]>;
|
|
113
|
+
/**
|
|
114
|
+
* Choose a (format, video, audio) triple. When audio is requested but no audio
|
|
115
|
+
* codec encodes, fall back to **video-only** (§5.2 — Safari 16.4–18.x is
|
|
116
|
+
* video-only) with a warning, instead of failing the whole format. Throws only
|
|
117
|
+
* when no video codec encodes at all. Probes are injected for testability.
|
|
118
|
+
*/
|
|
119
|
+
declare function pickCodecs(format: 'mp4' | 'webm' | 'auto', needAudio: boolean, probeVideo: VideoProbe, probeAudio: AudioProbe): Promise<{
|
|
120
|
+
format: 'mp4' | 'webm';
|
|
121
|
+
video: VideoCodec;
|
|
122
|
+
audio: AudioCodec | null;
|
|
123
|
+
}>;
|
|
91
124
|
/** Mix timeline audio clips sample-accurately (§5.3 browser path). Window-only (OfflineAudioContext). */
|
|
92
125
|
declare function mixAudio(clips: AudioClip[], duration: number, sampleRate?: number): Promise<AudioBuffer>;
|
|
93
126
|
/** Main-thread premix for the worker path: mixAudio flattened to transferable channels. */
|
|
@@ -101,4 +134,4 @@ declare function exportPngFrames(scene: Scene, doc: Timeline, onFrame: (frame: n
|
|
|
101
134
|
frames: number;
|
|
102
135
|
}>;
|
|
103
136
|
//#endregion
|
|
104
|
-
export { ExportUnsupportedError, type ExportWorkerRequest, type ExportWorkerResponse, MediabunnyVideoFrameSource, PremixedAudio, WebExportOptions, WebExportResult, type WorkerExportHandle, exportPngFrames, exportVideo, mixAudio, premixTimelineAudio, requestWorkerExport, serveExportRequest };
|
|
137
|
+
export { AudioProbe, ExportSupport, ExportUnsupportedError, type ExportWorkerRequest, type ExportWorkerResponse, MediabunnyVideoFrameSource, PremixedAudio, VideoProbe, WebExportOptions, WebExportResult, type WorkerExportHandle, exportPngFrames, exportVideo, mixAudio, pickCodecs, premixTimelineAudio, probeExportSupport, requestWorkerExport, serveExportRequest };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ALL_FORMATS, AudioBufferSource, AudioSample, AudioSampleSource, BlobSource, BufferTarget, CanvasSink, CanvasSource, Input, Mp4OutputFormat, Output, UrlSource, WebMOutputFormat, getFirstEncodableAudioCodec, getFirstEncodableVideoCodec } from "mediabunny";
|
|
2
|
-
import { audioOffsetSamples, compileTimeline, sampleTrack } from "@glissade/core";
|
|
2
|
+
import { audioOffsetSamples, compileTimeline, emitDevWarning, sampleTrack } from "@glissade/core";
|
|
3
3
|
import { ColdAssetError, evaluate } from "@glissade/scene";
|
|
4
4
|
import { Canvas2DBackend } from "@glissade/backend-canvas2d";
|
|
5
5
|
//#region src/videoSource.ts
|
|
@@ -189,25 +189,65 @@ const WEBM_VIDEO = [
|
|
|
189
189
|
];
|
|
190
190
|
const MP4_AUDIO = ["aac", "opus"];
|
|
191
191
|
const WEBM_AUDIO = ["opus"];
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
192
|
+
/**
|
|
193
|
+
* The resolved encodability matrix (§5.2): one row per container, so a UI can
|
|
194
|
+
* grey out options instead of failing mid-render. Audio absence is not a
|
|
195
|
+
* blocker — the export falls back to video-only.
|
|
196
|
+
*/
|
|
197
|
+
async function probeExportSupport(opts = {}) {
|
|
198
|
+
const { width = 1920, height = 1080, bitrate = 8e6 } = opts;
|
|
199
|
+
const probeVideo = (codecs) => getFirstEncodableVideoCodec(codecs, {
|
|
200
|
+
width,
|
|
201
|
+
height,
|
|
202
|
+
bitrate
|
|
203
|
+
});
|
|
204
|
+
const probeAudio = (codecs) => getFirstEncodableAudioCodec(codecs);
|
|
205
|
+
const out = [];
|
|
206
|
+
for (const format of ["mp4", "webm"]) {
|
|
207
|
+
const video = await probeVideo(format === "mp4" ? MP4_VIDEO : WEBM_VIDEO);
|
|
208
|
+
const audio = await probeAudio(format === "mp4" ? MP4_AUDIO : WEBM_AUDIO);
|
|
209
|
+
out.push({
|
|
210
|
+
format,
|
|
211
|
+
video,
|
|
212
|
+
audio,
|
|
213
|
+
supported: video !== null
|
|
198
214
|
});
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Choose a (format, video, audio) triple. When audio is requested but no audio
|
|
220
|
+
* codec encodes, fall back to **video-only** (§5.2 — Safari 16.4–18.x is
|
|
221
|
+
* video-only) with a warning, instead of failing the whole format. Throws only
|
|
222
|
+
* when no video codec encodes at all. Probes are injected for testability.
|
|
223
|
+
*/
|
|
224
|
+
async function pickCodecs(format, needAudio, probeVideo, probeAudio) {
|
|
225
|
+
let videoOnly = null;
|
|
226
|
+
for (const f of format === "auto" ? ["mp4", "webm"] : [format]) {
|
|
227
|
+
const video = await probeVideo(f === "mp4" ? MP4_VIDEO : WEBM_VIDEO);
|
|
228
|
+
if (!video) continue;
|
|
229
|
+
if (!needAudio) return {
|
|
230
|
+
format: f,
|
|
231
|
+
video,
|
|
232
|
+
audio: null
|
|
233
|
+
};
|
|
234
|
+
const audio = await probeAudio(f === "mp4" ? MP4_AUDIO : WEBM_AUDIO);
|
|
235
|
+
if (audio) return {
|
|
203
236
|
format: f,
|
|
204
237
|
video,
|
|
205
238
|
audio
|
|
206
239
|
};
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
240
|
+
videoOnly ??= {
|
|
241
|
+
format: f,
|
|
242
|
+
video
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (videoOnly) {
|
|
246
|
+
emitDevWarning(`no encodable audio codec for '${format}'; exporting video-only (§5.2)`);
|
|
247
|
+
return {
|
|
248
|
+
...videoOnly,
|
|
249
|
+
audio: null
|
|
250
|
+
};
|
|
211
251
|
}
|
|
212
252
|
throw new ExportUnsupportedError(`format '${format}'${needAudio ? " with audio" : ""}`);
|
|
213
253
|
}
|
|
@@ -263,7 +303,11 @@ async function exportVideo(scene, doc, opts = {}) {
|
|
|
263
303
|
const total = Math.max(1, Math.ceil(duration * fps));
|
|
264
304
|
const { w, h } = scene.size;
|
|
265
305
|
const videoBitrate = opts.videoBitrate ?? 8e6;
|
|
266
|
-
const picked = await pickCodecs(opts.format ?? "auto",
|
|
306
|
+
const picked = await pickCodecs(opts.format ?? "auto", compiled.audio.length > 0, (codecs) => getFirstEncodableVideoCodec(codecs, {
|
|
307
|
+
width: w,
|
|
308
|
+
height: h,
|
|
309
|
+
bitrate: videoBitrate
|
|
310
|
+
}), (codecs) => getFirstEncodableAudioCodec(codecs));
|
|
267
311
|
const canvas = new OffscreenCanvas(w, h);
|
|
268
312
|
const backend = new Canvas2DBackend(canvas);
|
|
269
313
|
scene.setTextMeasurer(backend);
|
|
@@ -376,4 +420,4 @@ async function exportPngFrames(scene, doc, onFrame, opts = {}) {
|
|
|
376
420
|
return { frames: total };
|
|
377
421
|
}
|
|
378
422
|
//#endregion
|
|
379
|
-
export { ExportUnsupportedError, MediabunnyVideoFrameSource, exportPngFrames, exportVideo, mixAudio, premixTimelineAudio, requestWorkerExport, serveExportRequest };
|
|
423
|
+
export { ExportUnsupportedError, MediabunnyVideoFrameSource, exportPngFrames, exportVideo, mixAudio, pickCodecs, premixTimelineAudio, probeExportSupport, requestWorkerExport, serveExportRequest };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/export-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0-pre.1",
|
|
4
4
|
"description": "glissade in-browser export: WebCodecs VideoEncoder + Mediabunny muxing, OfflineAudioContext audio mix. Frame-accurate, faster than realtime, no server.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"mediabunny": "^1.0.0",
|
|
19
|
-
"@glissade/
|
|
20
|
-
"@glissade/
|
|
21
|
-
"@glissade/scene": "0.
|
|
19
|
+
"@glissade/core": "0.8.0-pre.1",
|
|
20
|
+
"@glissade/backend-canvas2d": "0.8.0-pre.1",
|
|
21
|
+
"@glissade/scene": "0.8.0-pre.1"
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|