@pneuma-craft/video 0.4.0 → 0.5.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.cts CHANGED
@@ -134,6 +134,13 @@ interface PlaybackEngineOptions {
134
134
  * omitted, subtitle tracks render nothing (legacy behavior).
135
135
  */
136
136
  subtitleRenderer?: SubtitleRenderer;
137
+ /**
138
+ * Render preview frames (planning-layer visuals attached to time points
139
+ * on a track) when no real clip covers the current time. Defaults to
140
+ * `true` for the playback engine — the user scrubbing the timeline
141
+ * should see the agent's planned vibe before any real video clips land.
142
+ */
143
+ includePreviewFrames?: boolean;
137
144
  }
138
145
  declare function createPlaybackEngine(options?: PlaybackEngineOptions): PlaybackEngine;
139
146
 
@@ -144,10 +151,30 @@ interface ExportEngineOptions {
144
151
  * preview. When omitted, subtitle tracks are skipped during export.
145
152
  */
146
153
  subtitleRenderer?: SubtitleRenderer;
154
+ /**
155
+ * Include preview frames (planning-layer visuals attached to time points
156
+ * on a track) in the exported video. Defaults to `false` because an export
157
+ * normally represents a finished cut. Set true for review-grade exports of
158
+ * unfinished timelines — e.g., agent producing a "草样片" for the user to
159
+ * scrub before committing to expensive real renders.
160
+ */
161
+ includePreviewFrames?: boolean;
147
162
  }
148
163
  declare function createExportEngine(options?: ExportEngineOptions): ExportEngine;
149
164
 
150
- declare function createFrameRenderer(decoder: MediaDecoder, compositor: Compositor, width: number, height: number, subtitleRenderer?: SubtitleRenderer): FrameRenderer;
165
+ interface CreateFrameRendererOptions {
166
+ readonly subtitleRenderer?: SubtitleRenderer;
167
+ /**
168
+ * Render preview frames in tracks where no real clip covers the current
169
+ * time. Defaults to false — preview frames are a planning-layer visual
170
+ * that should NOT appear in final exports unless explicitly requested.
171
+ * Engines wire their own defaults: PlaybackEngine passes true, ExportEngine
172
+ * passes false.
173
+ */
174
+ readonly includePreviewFrames?: boolean;
175
+ }
176
+ type LegacyArg = SubtitleRenderer | CreateFrameRendererOptions | undefined;
177
+ declare function createFrameRenderer(decoder: MediaDecoder, compositor: Compositor, width: number, height: number, optionsOrLegacySubtitleRenderer?: LegacyArg): FrameRenderer;
151
178
 
152
179
  interface MasterClockOptions {
153
180
  audioContext: AudioContext;
package/dist/index.d.ts CHANGED
@@ -134,6 +134,13 @@ interface PlaybackEngineOptions {
134
134
  * omitted, subtitle tracks render nothing (legacy behavior).
135
135
  */
136
136
  subtitleRenderer?: SubtitleRenderer;
137
+ /**
138
+ * Render preview frames (planning-layer visuals attached to time points
139
+ * on a track) when no real clip covers the current time. Defaults to
140
+ * `true` for the playback engine — the user scrubbing the timeline
141
+ * should see the agent's planned vibe before any real video clips land.
142
+ */
143
+ includePreviewFrames?: boolean;
137
144
  }
138
145
  declare function createPlaybackEngine(options?: PlaybackEngineOptions): PlaybackEngine;
139
146
 
@@ -144,10 +151,30 @@ interface ExportEngineOptions {
144
151
  * preview. When omitted, subtitle tracks are skipped during export.
145
152
  */
146
153
  subtitleRenderer?: SubtitleRenderer;
154
+ /**
155
+ * Include preview frames (planning-layer visuals attached to time points
156
+ * on a track) in the exported video. Defaults to `false` because an export
157
+ * normally represents a finished cut. Set true for review-grade exports of
158
+ * unfinished timelines — e.g., agent producing a "草样片" for the user to
159
+ * scrub before committing to expensive real renders.
160
+ */
161
+ includePreviewFrames?: boolean;
147
162
  }
148
163
  declare function createExportEngine(options?: ExportEngineOptions): ExportEngine;
149
164
 
150
- declare function createFrameRenderer(decoder: MediaDecoder, compositor: Compositor, width: number, height: number, subtitleRenderer?: SubtitleRenderer): FrameRenderer;
165
+ interface CreateFrameRendererOptions {
166
+ readonly subtitleRenderer?: SubtitleRenderer;
167
+ /**
168
+ * Render preview frames in tracks where no real clip covers the current
169
+ * time. Defaults to false — preview frames are a planning-layer visual
170
+ * that should NOT appear in final exports unless explicitly requested.
171
+ * Engines wire their own defaults: PlaybackEngine passes true, ExportEngine
172
+ * passes false.
173
+ */
174
+ readonly includePreviewFrames?: boolean;
175
+ }
176
+ type LegacyArg = SubtitleRenderer | CreateFrameRendererOptions | undefined;
177
+ declare function createFrameRenderer(decoder: MediaDecoder, compositor: Compositor, width: number, height: number, optionsOrLegacySubtitleRenderer?: LegacyArg): FrameRenderer;
151
178
 
152
179
  interface MasterClockOptions {
153
180
  audioContext: AudioContext;
package/dist/index.js CHANGED
@@ -436,31 +436,58 @@ async function createCompositor(width, height, type = "auto") {
436
436
 
437
437
  // src/frame-renderer.ts
438
438
  import { resolveFrame } from "@pneuma-craft/timeline";
439
- function createFrameRenderer(decoder, compositor, width, height, subtitleRenderer) {
439
+ function normalizeOptions(arg) {
440
+ if (arg === void 0) return {};
441
+ if (typeof arg === "function") return { subtitleRenderer: arg };
442
+ return arg;
443
+ }
444
+ function createFrameRenderer(decoder, compositor, width, height, optionsOrLegacySubtitleRenderer) {
445
+ const options = normalizeOptions(optionsOrLegacySubtitleRenderer);
446
+ const { subtitleRenderer, includePreviewFrames = false } = options;
440
447
  return {
441
448
  async renderFrame(composition, time) {
442
449
  const resolved = resolveFrame(composition, time);
443
- const videoClips = resolved.clips.filter((rc) => rc.track.type === "video");
444
- const subtitleClips = subtitleRenderer ? resolved.clips.filter((rc) => rc.track.type === "subtitle") : [];
450
+ const clipsByTrack = /* @__PURE__ */ new Map();
451
+ for (const rc of resolved.clips) {
452
+ const arr = clipsByTrack.get(rc.track.id);
453
+ if (arr) arr.push(rc);
454
+ else clipsByTrack.set(rc.track.id, [rc]);
455
+ }
456
+ const previewByTrack = /* @__PURE__ */ new Map();
457
+ for (const rpf of resolved.previewFrames) previewByTrack.set(rpf.track.id, rpf);
445
458
  const layers = [];
446
- for (let i = 0; i < videoClips.length; i++) {
447
- const rc = videoClips[i];
448
- const source = await decoder.decodeVideoFrame(
449
- rc.clip.assetId,
450
- rc.localTime,
451
- width,
452
- height
453
- );
454
- layers.push({
455
- source,
456
- opacity: 1,
457
- zIndex: i
458
- });
459
+ let zIndex = 0;
460
+ for (const track of composition.tracks) {
461
+ if (track.type !== "video") continue;
462
+ const trackClips = clipsByTrack.get(track.id);
463
+ if (trackClips && trackClips.length > 0) {
464
+ for (const rc of trackClips) {
465
+ const source = await decoder.decodeVideoFrame(
466
+ rc.clip.assetId,
467
+ rc.localTime,
468
+ width,
469
+ height
470
+ );
471
+ layers.push({ source, opacity: 1, zIndex: zIndex++ });
472
+ }
473
+ continue;
474
+ }
475
+ if (includePreviewFrames) {
476
+ const rpf = previewByTrack.get(track.id);
477
+ if (rpf) {
478
+ const source = await decoder.decodeVideoFrame(
479
+ rpf.previewFrame.assetId,
480
+ 0,
481
+ width,
482
+ height
483
+ );
484
+ layers.push({ source, opacity: 1, zIndex: zIndex++ });
485
+ }
486
+ }
459
487
  }
460
- if (subtitleRenderer && subtitleClips.length > 0) {
461
- const baseZ = videoClips.length;
462
- for (let i = 0; i < subtitleClips.length; i++) {
463
- const rc = subtitleClips[i];
488
+ if (subtitleRenderer) {
489
+ const subtitleClips = resolved.clips.filter((rc) => rc.track.type === "subtitle");
490
+ for (const rc of subtitleClips) {
464
491
  const rendered = await subtitleRenderer({
465
492
  clip: rc.clip,
466
493
  localTime: rc.localTime,
@@ -468,11 +495,7 @@ function createFrameRenderer(decoder, compositor, width, height, subtitleRendere
468
495
  height
469
496
  });
470
497
  if (rendered) {
471
- layers.push({
472
- source: rendered,
473
- opacity: 1,
474
- zIndex: baseZ + i
475
- });
498
+ layers.push({ source: rendered, opacity: 1, zIndex: zIndex++ });
476
499
  }
477
500
  }
478
501
  }
@@ -873,6 +896,7 @@ function createPlaybackEngine(options) {
873
896
  const compositorType = options?.compositorType ?? "canvas2d";
874
897
  const decodeTimeoutMs = options?.decodeTimeoutMs ?? DEFAULT_DECODE_TIMEOUT_MS;
875
898
  const subtitleRenderer = options?.subtitleRenderer;
899
+ const includePreviewFrames = options?.includePreviewFrames ?? true;
876
900
  let _state = "idle";
877
901
  let _playbackRate = 1;
878
902
  let _loop = null;
@@ -1041,7 +1065,7 @@ function createPlaybackEngine(options) {
1041
1065
  _compositor,
1042
1066
  composition.settings.width,
1043
1067
  composition.settings.height,
1044
- subtitleRenderer
1068
+ { subtitleRenderer, includePreviewFrames }
1045
1069
  );
1046
1070
  _clock = createMasterClock({
1047
1071
  audioContext: _audioContext,
@@ -1207,6 +1231,7 @@ import {
1207
1231
  } from "mediabunny";
1208
1232
  function createExportEngine(options) {
1209
1233
  const subtitleRenderer = options?.subtitleRenderer;
1234
+ const includePreviewFrames = options?.includePreviewFrames ?? false;
1210
1235
  const progressListeners = /* @__PURE__ */ new Set();
1211
1236
  let abortController = null;
1212
1237
  function notifyProgress(progress) {
@@ -1229,7 +1254,10 @@ function createExportEngine(options) {
1229
1254
  const decoderAudioCtx = new OfflineAudioContext(1, 1, composition.settings.sampleRate || 48e3);
1230
1255
  const decoder = createMediaDecoder(resolver, decoderAudioCtx);
1231
1256
  const compositor = await createCompositor(width, height, "canvas2d");
1232
- const renderer = createFrameRenderer(decoder, compositor, width, height, subtitleRenderer);
1257
+ const renderer = createFrameRenderer(decoder, compositor, width, height, {
1258
+ subtitleRenderer,
1259
+ includePreviewFrames
1260
+ });
1233
1261
  try {
1234
1262
  const format = options2.format === "webm" ? new WebMOutputFormat() : new Mp4OutputFormat({ fastStart: "in-memory" });
1235
1263
  const target = new BufferTarget();