@pneuma-craft/video 0.4.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - eecee02: Republish to fix stale internal dependency refs. The previous publish (`@pneuma-craft/video@0.5.0`) declared `@pneuma-craft/timeline@0.3.0` instead of `@pneuma-craft/timeline@0.4.0`, because `bun pm pack` rewrote `workspace:*` against a lockfile that `bun install --no-frozen-lockfile` had not refreshed. The CI release script now nukes `bun.lock` before re-installing, so the lockfile mirrors the bumped versions and `bun pm pack` resolves to the correct ones.
8
+
9
+ `@pneuma-craft/react` is auto-patched by changesets because it depends on `@pneuma-craft/video`; the same fix flows through to its own published deps (`timeline@0.4.0`, `video@0.5.1`).
10
+
11
+ ## 0.5.0
12
+
13
+ ### Minor Changes
14
+
15
+ - c7a09ae: Add **PreviewFrame**, a planning-layer visual attached to a track at a single time point that lets go (does not render) when a real clip on the same track covers that moment. Powers the AIGC progressive-fidelity workflow (cheap sketch → upgraded anchor → real clip) without diluting `Clip` semantics.
16
+
17
+ `@pneuma-craft/timeline` (additive)
18
+
19
+ - New types: `PreviewFrame`, `ResolvedPreviewFrame`, `ResolvedFrame.previewFrames[]`
20
+ - `Track` gains a sorted `previewFrames: PreviewFrame[]` array
21
+ - New commands (mirroring clip ops): `composition:add-preview-frame`, `remove-preview-frame`, `move-preview-frame`, `rebind-preview-frame`
22
+ - Matching events with undo compensations
23
+ - `resolveFrame()` applies a per-track let-go rule (clip wins over preview at the same time on the same track)
24
+ - `recomputeDuration()` extends to the last preview-frame time
25
+ - New utility `buildSetPreviewFrameCommand()` for agent upsert ergonomics
26
+ - `composition:add-track` now accepts `previewFrames` as optional in its input shape (defaults to `[]`)
27
+
28
+ `@pneuma-craft/video` (additive + small internal signature)
29
+
30
+ - `createFrameRenderer`'s 5th argument now accepts `{ subtitleRenderer?, includePreviewFrames? }`. The pre-existing positional `SubtitleRenderer` form is detected and normalized — existing call sites continue to compile.
31
+ - `PlaybackEngineOptions.includePreviewFrames` (defaults to `true`) — playback shows planning visuals
32
+ - `ExportEngineOptions.includePreviewFrames` (defaults to `false`) — export = finished cut by default; opt-in for review-grade exports of unfinished timelines
33
+
34
+ See `docs/specs/2026-05-09-preview-frame-design.md` for the full design and `docs/recipes/preview-frames.md` for an integration guide.
35
+
36
+ ### Patch Changes
37
+
38
+ - Updated dependencies [c7a09ae]
39
+ - @pneuma-craft/timeline@0.4.0
40
+
3
41
  All notable changes to `@pneuma-craft/video` will be documented in this file.
4
42
 
5
43
  ## 0.4.0
package/dist/index.cjs CHANGED
@@ -471,31 +471,58 @@ async function createCompositor(width, height, type = "auto") {
471
471
 
472
472
  // src/frame-renderer.ts
473
473
  var import_timeline = require("@pneuma-craft/timeline");
474
- function createFrameRenderer(decoder, compositor, width, height, subtitleRenderer) {
474
+ function normalizeOptions(arg) {
475
+ if (arg === void 0) return {};
476
+ if (typeof arg === "function") return { subtitleRenderer: arg };
477
+ return arg;
478
+ }
479
+ function createFrameRenderer(decoder, compositor, width, height, optionsOrLegacySubtitleRenderer) {
480
+ const options = normalizeOptions(optionsOrLegacySubtitleRenderer);
481
+ const { subtitleRenderer, includePreviewFrames = false } = options;
475
482
  return {
476
483
  async renderFrame(composition, time) {
477
484
  const resolved = (0, import_timeline.resolveFrame)(composition, time);
478
- const videoClips = resolved.clips.filter((rc) => rc.track.type === "video");
479
- const subtitleClips = subtitleRenderer ? resolved.clips.filter((rc) => rc.track.type === "subtitle") : [];
485
+ const clipsByTrack = /* @__PURE__ */ new Map();
486
+ for (const rc of resolved.clips) {
487
+ const arr = clipsByTrack.get(rc.track.id);
488
+ if (arr) arr.push(rc);
489
+ else clipsByTrack.set(rc.track.id, [rc]);
490
+ }
491
+ const previewByTrack = /* @__PURE__ */ new Map();
492
+ for (const rpf of resolved.previewFrames) previewByTrack.set(rpf.track.id, rpf);
480
493
  const layers = [];
481
- for (let i = 0; i < videoClips.length; i++) {
482
- const rc = videoClips[i];
483
- const source = await decoder.decodeVideoFrame(
484
- rc.clip.assetId,
485
- rc.localTime,
486
- width,
487
- height
488
- );
489
- layers.push({
490
- source,
491
- opacity: 1,
492
- zIndex: i
493
- });
494
+ let zIndex = 0;
495
+ for (const track of composition.tracks) {
496
+ if (track.type !== "video") continue;
497
+ const trackClips = clipsByTrack.get(track.id);
498
+ if (trackClips && trackClips.length > 0) {
499
+ for (const rc of trackClips) {
500
+ const source = await decoder.decodeVideoFrame(
501
+ rc.clip.assetId,
502
+ rc.localTime,
503
+ width,
504
+ height
505
+ );
506
+ layers.push({ source, opacity: 1, zIndex: zIndex++ });
507
+ }
508
+ continue;
509
+ }
510
+ if (includePreviewFrames) {
511
+ const rpf = previewByTrack.get(track.id);
512
+ if (rpf) {
513
+ const source = await decoder.decodeVideoFrame(
514
+ rpf.previewFrame.assetId,
515
+ 0,
516
+ width,
517
+ height
518
+ );
519
+ layers.push({ source, opacity: 1, zIndex: zIndex++ });
520
+ }
521
+ }
494
522
  }
495
- if (subtitleRenderer && subtitleClips.length > 0) {
496
- const baseZ = videoClips.length;
497
- for (let i = 0; i < subtitleClips.length; i++) {
498
- const rc = subtitleClips[i];
523
+ if (subtitleRenderer) {
524
+ const subtitleClips = resolved.clips.filter((rc) => rc.track.type === "subtitle");
525
+ for (const rc of subtitleClips) {
499
526
  const rendered = await subtitleRenderer({
500
527
  clip: rc.clip,
501
528
  localTime: rc.localTime,
@@ -503,11 +530,7 @@ function createFrameRenderer(decoder, compositor, width, height, subtitleRendere
503
530
  height
504
531
  });
505
532
  if (rendered) {
506
- layers.push({
507
- source: rendered,
508
- opacity: 1,
509
- zIndex: baseZ + i
510
- });
533
+ layers.push({ source: rendered, opacity: 1, zIndex: zIndex++ });
511
534
  }
512
535
  }
513
536
  }
@@ -908,6 +931,7 @@ function createPlaybackEngine(options) {
908
931
  const compositorType = options?.compositorType ?? "canvas2d";
909
932
  const decodeTimeoutMs = options?.decodeTimeoutMs ?? DEFAULT_DECODE_TIMEOUT_MS;
910
933
  const subtitleRenderer = options?.subtitleRenderer;
934
+ const includePreviewFrames = options?.includePreviewFrames ?? true;
911
935
  let _state = "idle";
912
936
  let _playbackRate = 1;
913
937
  let _loop = null;
@@ -1076,7 +1100,7 @@ function createPlaybackEngine(options) {
1076
1100
  _compositor,
1077
1101
  composition.settings.width,
1078
1102
  composition.settings.height,
1079
- subtitleRenderer
1103
+ { subtitleRenderer, includePreviewFrames }
1080
1104
  );
1081
1105
  _clock = createMasterClock({
1082
1106
  audioContext: _audioContext,
@@ -1235,6 +1259,7 @@ function createOfflineAudioRenderer() {
1235
1259
  var import_mediabunny2 = require("mediabunny");
1236
1260
  function createExportEngine(options) {
1237
1261
  const subtitleRenderer = options?.subtitleRenderer;
1262
+ const includePreviewFrames = options?.includePreviewFrames ?? false;
1238
1263
  const progressListeners = /* @__PURE__ */ new Set();
1239
1264
  let abortController = null;
1240
1265
  function notifyProgress(progress) {
@@ -1257,7 +1282,10 @@ function createExportEngine(options) {
1257
1282
  const decoderAudioCtx = new OfflineAudioContext(1, 1, composition.settings.sampleRate || 48e3);
1258
1283
  const decoder = createMediaDecoder(resolver, decoderAudioCtx);
1259
1284
  const compositor = await createCompositor(width, height, "canvas2d");
1260
- const renderer = createFrameRenderer(decoder, compositor, width, height, subtitleRenderer);
1285
+ const renderer = createFrameRenderer(decoder, compositor, width, height, {
1286
+ subtitleRenderer,
1287
+ includePreviewFrames
1288
+ });
1261
1289
  try {
1262
1290
  const format = options2.format === "webm" ? new import_mediabunny2.WebMOutputFormat() : new import_mediabunny2.Mp4OutputFormat({ fastStart: "in-memory" });
1263
1291
  const target = new import_mediabunny2.BufferTarget();