@glissade/export-web 0.7.0-pre.0 → 0.8.0-pre.0

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 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
- async function pickCodecs(format, width, height, bitrate, needAudio) {
193
- const tryFormat = async (f) => {
194
- const video = await getFirstEncodableVideoCodec(f === "mp4" ? MP4_VIDEO : WEBM_VIDEO, {
195
- width,
196
- height,
197
- bitrate
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
- if (!video) return null;
200
- const audio = needAudio ? await getFirstEncodableAudioCodec(f === "mp4" ? MP4_AUDIO : WEBM_AUDIO) : null;
201
- if (needAudio && !audio) return null;
202
- return {
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
- for (const f of format === "auto" ? ["mp4", "webm"] : [format]) {
209
- const picked = await tryFormat(f);
210
- if (picked) return picked;
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", w, h, videoBitrate, compiled.audio.length > 0);
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.7.0-pre.0",
3
+ "version": "0.8.0-pre.0",
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/backend-canvas2d": "0.7.0-pre.0",
20
- "@glissade/scene": "0.7.0-pre.0",
21
- "@glissade/core": "0.7.0-pre.0"
19
+ "@glissade/backend-canvas2d": "0.8.0-pre.0",
20
+ "@glissade/core": "0.8.0-pre.0",
21
+ "@glissade/scene": "0.8.0-pre.0"
22
22
  },
23
23
  "repository": {
24
24
  "type": "git",