@nodetool-ai/video-nodes 0.8.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.
Files changed (152) hide show
  1. package/README.md +58 -0
  2. package/dist/.nodetool-build-stamp +1 -0
  3. package/dist/index.d.ts +9 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +11 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/nodes/ffmpeg-helpers.d.ts +77 -0
  8. package/dist/nodes/ffmpeg-helpers.d.ts.map +1 -0
  9. package/dist/nodes/ffmpeg-helpers.js +220 -0
  10. package/dist/nodes/ffmpeg-helpers.js.map +1 -0
  11. package/dist/nodes/lib-video-download.d.ts +29 -0
  12. package/dist/nodes/lib-video-download.d.ts.map +1 -0
  13. package/dist/nodes/lib-video-download.js +353 -0
  14. package/dist/nodes/lib-video-download.js.map +1 -0
  15. package/dist/nodes/model3d/analysis.d.ts +16 -0
  16. package/dist/nodes/model3d/analysis.d.ts.map +1 -0
  17. package/dist/nodes/model3d/analysis.js +38 -0
  18. package/dist/nodes/model3d/analysis.js.map +1 -0
  19. package/dist/nodes/model3d/base.d.ts +28 -0
  20. package/dist/nodes/model3d/base.d.ts.map +1 -0
  21. package/dist/nodes/model3d/base.js +20 -0
  22. package/dist/nodes/model3d/base.js.map +1 -0
  23. package/dist/nodes/model3d/boolean-ops.d.ts +2 -0
  24. package/dist/nodes/model3d/boolean-ops.d.ts.map +1 -0
  25. package/dist/nodes/model3d/boolean-ops.js +172 -0
  26. package/dist/nodes/model3d/boolean-ops.js.map +1 -0
  27. package/dist/nodes/model3d/cleanup.d.ts +56 -0
  28. package/dist/nodes/model3d/cleanup.d.ts.map +1 -0
  29. package/dist/nodes/model3d/cleanup.js +144 -0
  30. package/dist/nodes/model3d/cleanup.js.map +1 -0
  31. package/dist/nodes/model3d/convert.d.ts +27 -0
  32. package/dist/nodes/model3d/convert.d.ts.map +1 -0
  33. package/dist/nodes/model3d/convert.js +66 -0
  34. package/dist/nodes/model3d/convert.js.map +1 -0
  35. package/dist/nodes/model3d/defaults.d.ts +34 -0
  36. package/dist/nodes/model3d/defaults.d.ts.map +1 -0
  37. package/dist/nodes/model3d/defaults.js +28 -0
  38. package/dist/nodes/model3d/defaults.js.map +1 -0
  39. package/dist/nodes/model3d/document-ops.d.ts +4 -0
  40. package/dist/nodes/model3d/document-ops.d.ts.map +1 -0
  41. package/dist/nodes/model3d/document-ops.js +40 -0
  42. package/dist/nodes/model3d/document-ops.js.map +1 -0
  43. package/dist/nodes/model3d/generation.d.ts +56 -0
  44. package/dist/nodes/model3d/generation.d.ts.map +1 -0
  45. package/dist/nodes/model3d/generation.js +203 -0
  46. package/dist/nodes/model3d/generation.js.map +1 -0
  47. package/dist/nodes/model3d/glb.d.ts +29 -0
  48. package/dist/nodes/model3d/glb.d.ts.map +1 -0
  49. package/dist/nodes/model3d/glb.js +255 -0
  50. package/dist/nodes/model3d/glb.js.map +1 -0
  51. package/dist/nodes/model3d/index.d.ts +22 -0
  52. package/dist/nodes/model3d/index.d.ts.map +1 -0
  53. package/dist/nodes/model3d/index.js +41 -0
  54. package/dist/nodes/model3d/index.js.map +1 -0
  55. package/dist/nodes/model3d/io.d.ts +81 -0
  56. package/dist/nodes/model3d/io.d.ts.map +1 -0
  57. package/dist/nodes/model3d/io.js +162 -0
  58. package/dist/nodes/model3d/io.js.map +1 -0
  59. package/dist/nodes/model3d/mesh-ops.d.ts +40 -0
  60. package/dist/nodes/model3d/mesh-ops.d.ts.map +1 -0
  61. package/dist/nodes/model3d/mesh-ops.js +775 -0
  62. package/dist/nodes/model3d/mesh-ops.js.map +1 -0
  63. package/dist/nodes/model3d/nodes.d.ts +2 -0
  64. package/dist/nodes/model3d/nodes.d.ts.map +1 -0
  65. package/dist/nodes/model3d/nodes.js +2 -0
  66. package/dist/nodes/model3d/nodes.js.map +1 -0
  67. package/dist/nodes/model3d/ops.d.ts +44 -0
  68. package/dist/nodes/model3d/ops.d.ts.map +1 -0
  69. package/dist/nodes/model3d/ops.js +142 -0
  70. package/dist/nodes/model3d/ops.js.map +1 -0
  71. package/dist/nodes/model3d/render.d.ts +51 -0
  72. package/dist/nodes/model3d/render.d.ts.map +1 -0
  73. package/dist/nodes/model3d/render.js +124 -0
  74. package/dist/nodes/model3d/render.js.map +1 -0
  75. package/dist/nodes/model3d/render3d-core.d.ts +177 -0
  76. package/dist/nodes/model3d/render3d-core.d.ts.map +1 -0
  77. package/dist/nodes/model3d/render3d-core.js +366 -0
  78. package/dist/nodes/model3d/render3d-core.js.map +1 -0
  79. package/dist/nodes/model3d/render3d-headless.d.ts +75 -0
  80. package/dist/nodes/model3d/render3d-headless.d.ts.map +1 -0
  81. package/dist/nodes/model3d/render3d-headless.js +190 -0
  82. package/dist/nodes/model3d/render3d-headless.js.map +1 -0
  83. package/dist/nodes/model3d/render3d-page.d.ts +16 -0
  84. package/dist/nodes/model3d/render3d-page.d.ts.map +1 -0
  85. package/dist/nodes/model3d/render3d-page.js +51 -0
  86. package/dist/nodes/model3d/render3d-page.js.map +1 -0
  87. package/dist/nodes/model3d/transforms.d.ts +56 -0
  88. package/dist/nodes/model3d/transforms.d.ts.map +1 -0
  89. package/dist/nodes/model3d/transforms.js +148 -0
  90. package/dist/nodes/model3d/transforms.js.map +1 -0
  91. package/dist/nodes/model3d/types.d.ts +82 -0
  92. package/dist/nodes/model3d/types.d.ts.map +1 -0
  93. package/dist/nodes/model3d/types.js +2 -0
  94. package/dist/nodes/model3d/types.js.map +1 -0
  95. package/dist/nodes/model3d/utils.d.ts +46 -0
  96. package/dist/nodes/model3d/utils.d.ts.map +1 -0
  97. package/dist/nodes/model3d/utils.js +190 -0
  98. package/dist/nodes/model3d/utils.js.map +1 -0
  99. package/dist/nodes/model3d.d.ts +2 -0
  100. package/dist/nodes/model3d.d.ts.map +1 -0
  101. package/dist/nodes/model3d.js +2 -0
  102. package/dist/nodes/model3d.js.map +1 -0
  103. package/dist/nodes/placeholder-values.d.ts +10 -0
  104. package/dist/nodes/placeholder-values.d.ts.map +1 -0
  105. package/dist/nodes/placeholder-values.js +19 -0
  106. package/dist/nodes/placeholder-values.js.map +1 -0
  107. package/dist/nodes/script.d.ts +161 -0
  108. package/dist/nodes/script.d.ts.map +1 -0
  109. package/dist/nodes/script.js +673 -0
  110. package/dist/nodes/script.js.map +1 -0
  111. package/dist/nodes/storyboard.d.ts +269 -0
  112. package/dist/nodes/storyboard.d.ts.map +1 -0
  113. package/dist/nodes/storyboard.js +1017 -0
  114. package/dist/nodes/storyboard.js.map +1 -0
  115. package/dist/nodes/timeline/assetFiles.d.ts +19 -0
  116. package/dist/nodes/timeline/assetFiles.d.ts.map +1 -0
  117. package/dist/nodes/timeline/assetFiles.js +52 -0
  118. package/dist/nodes/timeline/assetFiles.js.map +1 -0
  119. package/dist/nodes/timeline/compositeRender.d.ts +60 -0
  120. package/dist/nodes/timeline/compositeRender.d.ts.map +1 -0
  121. package/dist/nodes/timeline/compositeRender.js +508 -0
  122. package/dist/nodes/timeline/compositeRender.js.map +1 -0
  123. package/dist/nodes/timeline/contactSheet.d.ts +43 -0
  124. package/dist/nodes/timeline/contactSheet.d.ts.map +1 -0
  125. package/dist/nodes/timeline/contactSheet.js +78 -0
  126. package/dist/nodes/timeline/contactSheet.js.map +1 -0
  127. package/dist/nodes/timeline/outputFormats.d.ts +69 -0
  128. package/dist/nodes/timeline/outputFormats.d.ts.map +1 -0
  129. package/dist/nodes/timeline/outputFormats.js +173 -0
  130. package/dist/nodes/timeline/outputFormats.js.map +1 -0
  131. package/dist/nodes/timeline/pngSequence.d.ts +44 -0
  132. package/dist/nodes/timeline/pngSequence.d.ts.map +1 -0
  133. package/dist/nodes/timeline/pngSequence.js +172 -0
  134. package/dist/nodes/timeline/pngSequence.js.map +1 -0
  135. package/dist/nodes/timeline/rasterizers.d.ts +47 -0
  136. package/dist/nodes/timeline/rasterizers.d.ts.map +1 -0
  137. package/dist/nodes/timeline/rasterizers.js +103 -0
  138. package/dist/nodes/timeline/rasterizers.js.map +1 -0
  139. package/dist/nodes/timeline/rawFrames.d.ts +149 -0
  140. package/dist/nodes/timeline/rawFrames.d.ts.map +1 -0
  141. package/dist/nodes/timeline/rawFrames.js +598 -0
  142. package/dist/nodes/timeline/rawFrames.js.map +1 -0
  143. package/dist/nodes/timeline.d.ts +206 -0
  144. package/dist/nodes/timeline.d.ts.map +1 -0
  145. package/dist/nodes/timeline.js +1395 -0
  146. package/dist/nodes/timeline.js.map +1 -0
  147. package/dist/nodes/video.d.ts +705 -0
  148. package/dist/nodes/video.d.ts.map +1 -0
  149. package/dist/nodes/video.js +2672 -0
  150. package/dist/nodes/video.js.map +1 -0
  151. package/dist/render3d-page.js +4118 -0
  152. package/package.json +95 -0
@@ -0,0 +1,508 @@
1
+ /**
2
+ * Frame-by-frame render of a timeline sequence, server-side.
3
+ *
4
+ * This is the editor's export path with the browser taken out: the same
5
+ * {@link computeActiveLayersWithHorizon} scene description, the same animation
6
+ * sampling, the same placement math, and the same GPU compositor
7
+ * ({@link HeadlessFrameCompositor}) — so a workflow render and the preview the
8
+ * user watched are the same picture. Only the boundaries differ: ffmpeg decodes
9
+ * clip media into RGBA and encodes the composited frames, in place of
10
+ * `<video>`/WebCodecs.
11
+ */
12
+ import { hasTimeRemap } from "@nodetool-ai/timeline";
13
+ import { HeadlessFrameCompositor, clipSourceTimeSec, computeActiveLayersWithHorizon, createAnimationCompileCache, hasActiveAnimation, measureTextWith, motionBlurSampleTimes, layerShutterTime, resolveFrameMotionBlur, shutterWindowIsStatic, resolveAnimatedLayerProps, resolveTextStaggerContext, trackZ } from "@nodetool-ai/timeline/render";
14
+ import { createCanvas } from "@napi-rs/canvas";
15
+ import { setImmediate } from "node:timers/promises";
16
+ import { decodeImageRgba, fitWithin, openFrameEncoder, openSourceFrameStream, openVideoFrameStream, probeVideoSize } from "./rawFrames.js";
17
+ import { openPngSequenceEncoder } from "./pngSequence.js";
18
+ import { resolveTimelineOutput } from "./outputFormats.js";
19
+ import { NodeRasterizer } from "./rasterizers.js";
20
+ /** Let the API service pending requests during a frame with many raster layers. */
21
+ const YIELD_EVERY_LAYERS = 8;
22
+ /** The GPU device is acquired lazily; this is what "no GPU here" looks like. */
23
+ export class CompositorUnavailableError extends Error {
24
+ constructor(cause) {
25
+ super("The timeline compositor needs a WebGPU device and none is available: " +
26
+ (cause instanceof Error ? cause.message : String(cause)));
27
+ this.name = "CompositorUnavailableError";
28
+ this.cause = cause;
29
+ }
30
+ }
31
+ /** The rejection a cancelled render throws, named the way callers test for. */
32
+ function abortError() {
33
+ return new DOMException("Timeline render cancelled", "AbortError");
34
+ }
35
+ async function awaitWithAbort(pending, signal) {
36
+ if (!signal)
37
+ return pending;
38
+ if (signal.aborted)
39
+ throw abortError();
40
+ let onAbort;
41
+ const aborted = new Promise((_resolve, reject) => {
42
+ onAbort = () => reject(abortError());
43
+ signal.addEventListener("abort", onAbort, { once: true });
44
+ });
45
+ try {
46
+ return await Promise.race([pending, aborted]);
47
+ }
48
+ finally {
49
+ if (onAbort)
50
+ signal.removeEventListener("abort", onAbort);
51
+ }
52
+ }
53
+ async function acquireDevice() {
54
+ try {
55
+ const { getNodeGPUDevice } = await import("@nodetool-ai/gpu/node");
56
+ return await getNodeGPUDevice();
57
+ }
58
+ catch (error) {
59
+ throw new CompositorUnavailableError(error);
60
+ }
61
+ }
62
+ export async function renderTimelineComposited(opts) {
63
+ const { sequence, fps, durationMs, resolveAssetPath, outPath, signal } = opts;
64
+ const output = opts.output ?? resolveTimelineOutput({ format: "mp4" });
65
+ // H.264 and the yuv420p family require even dimensions.
66
+ const width = Math.max(2, Math.floor(opts.width / 2) * 2);
67
+ const height = Math.max(2, Math.floor(opts.height / 2) * 2);
68
+ const totalFrames = Math.max(1, Math.round((durationMs / 1000) * fps));
69
+ const frameMs = 1000 / fps;
70
+ // Positions, font sizes and stroke widths are in sequence pixels. A render
71
+ // smaller than the sequence (a preview scale) samples and rasterizes at the
72
+ // sequence size and lets the compositor scale the result to the frame, so
73
+ // the layout matches the full-size render.
74
+ const referenceWidth = sequence.width > 0 ? sequence.width : width;
75
+ const referenceHeight = sequence.height > 0 ? sequence.height : height;
76
+ const frameSize = { width, height };
77
+ const canvas = {
78
+ width: referenceWidth,
79
+ height: referenceHeight,
80
+ // A `"line"` stagger is counted against the wrapped line count, so the
81
+ // count measures through the same kind of context the rasterizer draws on.
82
+ // SAFETY: `RasterContext2D` is the subset of the 2D canvas API the
83
+ // measurement uses, and a skia context provides all of it.
84
+ measureText: measureTextWith(createCanvas(1, 1).getContext("2d"))
85
+ };
86
+ if (signal?.aborted)
87
+ throw abortError();
88
+ const device = await acquireDevice();
89
+ if (signal?.aborted)
90
+ throw abortError();
91
+ const compositor = new HeadlessFrameCompositor(device, width, height);
92
+ compositor.setReferenceSize(referenceWidth, referenceHeight);
93
+ const rasterizer = new NodeRasterizer(referenceWidth, referenceHeight);
94
+ const frameIndices = opts.frames
95
+ ? [...new Set(opts.frames)]
96
+ .filter((frame) => Number.isInteger(frame) && frame >= 0 && frame < totalFrames)
97
+ .sort((a, b) => a - b)
98
+ : Array.from({ length: totalFrames }, (_, frame) => frame);
99
+ const { writeFrame } = opts;
100
+ const encoder = writeFrame
101
+ ? null
102
+ : output.format === "png_sequence"
103
+ ? openPngSequenceEncoder({
104
+ outPath,
105
+ width,
106
+ height,
107
+ fps,
108
+ alpha: output.alpha
109
+ })
110
+ : openFrameEncoder({
111
+ outPath,
112
+ width,
113
+ height,
114
+ fps,
115
+ encoderArgs: output.encoderArgs
116
+ });
117
+ const animCache = createAnimationCompileCache();
118
+ const videoSources = new Map();
119
+ const images = new Map();
120
+ const assetPaths = new Map();
121
+ const skippedClips = new Set();
122
+ const closeVideoSources = () => {
123
+ for (const source of videoSources.values())
124
+ source.close();
125
+ videoSources.clear();
126
+ };
127
+ signal?.addEventListener("abort", closeVideoSources, { once: true });
128
+ const pathFor = (assetId) => {
129
+ let pending = assetPaths.get(assetId);
130
+ if (!pending) {
131
+ pending = resolveAssetPath(assetId);
132
+ assetPaths.set(assetId, pending);
133
+ }
134
+ return pending;
135
+ };
136
+ const imageFor = async (assetId) => {
137
+ if (images.has(assetId))
138
+ return images.get(assetId) ?? null;
139
+ const file = await pathFor(assetId);
140
+ if (signal?.aborted)
141
+ throw abortError();
142
+ let decoded = null;
143
+ if (file) {
144
+ const size = await probeVideoSize(file);
145
+ if (signal?.aborted)
146
+ throw abortError();
147
+ if (size)
148
+ decoded = await decodeImageRgba(file, fitWithin(size, frameSize));
149
+ }
150
+ if (signal?.aborted)
151
+ throw abortError();
152
+ images.set(assetId, decoded);
153
+ return decoded;
154
+ };
155
+ const videoFor = async (clip, assetId) => {
156
+ // Keyed by clip *and* asset: a clip with a generated matte decodes two
157
+ // videos at the same source time — the picture and its mask — and keying on
158
+ // the clip alone handed the mask decode back the picture's stream.
159
+ const key = `${clip.id}:${assetId}`;
160
+ const existing = videoSources.get(key);
161
+ if (existing)
162
+ return existing;
163
+ const file = await pathFor(assetId);
164
+ if (signal?.aborted)
165
+ throw abortError();
166
+ if (!file)
167
+ return null;
168
+ const size = await probeVideoSize(file);
169
+ if (signal?.aborted)
170
+ throw abortError();
171
+ if (!size)
172
+ return null;
173
+ const decodeSize = fitWithin(size, frameSize);
174
+ // `clipSourceTimeSec` at the clip's own start is its in point — or, for a
175
+ // remapped clip, its curve's first source position — expressed the same way
176
+ // the preview seeks, so the first decoded frame is the frame the preview
177
+ // shows at the cut.
178
+ const startSec = clipSourceTimeSec(clip, clip.startMs);
179
+ const endMs = clip.startMs + clip.durationMs;
180
+ let source;
181
+ if (hasTimeRemap(clip)) {
182
+ // A curve can hold, revisit or reverse through the source, which a
183
+ // forward-only stream cannot serve; this one reopens ffmpeg to seek back.
184
+ const stream = openSourceFrameStream({
185
+ filePath: file,
186
+ size: decodeSize,
187
+ fps,
188
+ startSec
189
+ });
190
+ const frameVersions = new WeakMap();
191
+ let nextFrameVersion = 0;
192
+ source = {
193
+ width: stream.width,
194
+ height: stream.height,
195
+ endMs,
196
+ async frameAt(timeMs) {
197
+ const sourceSec = clipSourceTimeSec(clip, timeMs);
198
+ const rgba = await stream.frameAtSourceSec(sourceSec);
199
+ if (!rgba)
200
+ return null;
201
+ // A retained or held frame returns the same array. A newly decoded
202
+ // frame needs an upload even when a nearby source time rounds to the
203
+ // same timeline-frame index.
204
+ let version = frameVersions.get(rgba);
205
+ if (version === undefined) {
206
+ version = `${key}@${nextFrameVersion++}`;
207
+ frameVersions.set(rgba, version);
208
+ }
209
+ return {
210
+ rgba,
211
+ version
212
+ };
213
+ },
214
+ close: () => stream.close()
215
+ };
216
+ }
217
+ else {
218
+ const stream = openVideoFrameStream({
219
+ filePath: file,
220
+ size: decodeSize,
221
+ fps,
222
+ startSec,
223
+ speed: clip.speedBaked ? 1 : Math.max(0.0001, clip.speedMultiplier ?? 1)
224
+ });
225
+ source = {
226
+ width: stream.width,
227
+ height: stream.height,
228
+ endMs,
229
+ async frameAt(timeMs) {
230
+ const index = Math.max(0, Math.round(((timeMs - clip.startMs) * fps) / 1000));
231
+ const rgba = await stream.frameAt(index);
232
+ if (!rgba)
233
+ return null;
234
+ return { rgba, version: `${key}#${index}` };
235
+ },
236
+ close: () => stream.close()
237
+ };
238
+ }
239
+ videoSources.set(key, source);
240
+ return source;
241
+ };
242
+ try {
243
+ /**
244
+ * The scene at one instant, as something the compositor can composite.
245
+ *
246
+ * One call is a whole frame with motion blur off, and one sample of the
247
+ * shutter window with it on — the layers are resolved the same way either
248
+ * way, so a blurred render is N of the render it would otherwise have been.
249
+ */
250
+ const sampleAt = async (timeMs, frameTimeMs = timeMs, sampleIndex = 0, sampleCount = 1) => {
251
+ const layers = [];
252
+ const { layers: active, adjustments, precomposites } = computeActiveLayersWithHorizon(sequence.tracks, sequence.clips, timeMs, {
253
+ canvas,
254
+ animationCache: animCache,
255
+ mediaTracks: sequence.mediaTracks,
256
+ camera2d: sequence.camera2d,
257
+ tempo: sequence.tempo,
258
+ layerTimeMs: (clip) => layerShutterTime(clip, frameTimeMs, sampleIndex, sampleCount, frameMs, output.motionBlur)
259
+ });
260
+ /**
261
+ * A resolved layer as something the compositor can upload: its pixels,
262
+ * its own shape mask rasterized at that size, and — for a matted layer —
263
+ * the matte source resolved the same way.
264
+ *
265
+ * A matte source is not in `active`; the scene model held it back so it
266
+ * never draws itself, which is why it is decoded here and nowhere else.
267
+ */
268
+ const frameLayerFor = async (layer, idPrefix = "") => {
269
+ const layerTimeMs = layerShutterTime(layer.clip, frameTimeMs, sampleIndex, sampleCount, frameMs, output.motionBlur);
270
+ const anim = resolveAnimatedLayerProps(layer, layerTimeMs, canvas, animCache, { mediaTracks: sequence.mediaTracks ?? [], clips: sequence.clips, tempo: sequence.tempo });
271
+ const common = {
272
+ opacity: anim.opacity,
273
+ blendMode: layer.blendMode,
274
+ zIndex: trackZ(layer.trackIndex),
275
+ stackOrder: layer.stackOrder,
276
+ transform: anim.transform,
277
+ parentMatrix: layer.parentMatrix,
278
+ precomposeGroupId: layer.precomposeGroupId,
279
+ mask: anim.mask,
280
+ borderRadius: anim.borderRadius ?? layer.borderRadius,
281
+ crop: layer.crop,
282
+ effects: anim.effects ?? layer.effects,
283
+ trackEffects: layer.trackEffects,
284
+ transition: layer.transition
285
+ };
286
+ /** Attach the layer's own shape mask, rasterized at its source size. */
287
+ const finish = (built) => {
288
+ const shape = anim.clipMask;
289
+ if (!shape)
290
+ return built;
291
+ const raster = rasterizer.mask(shape, built.source.width, built.source.height);
292
+ if (raster)
293
+ built.shapeMask = raster;
294
+ return built;
295
+ };
296
+ const id = (kind) => `${idPrefix}${kind}:${layer.clipId}`;
297
+ if (layer.kind === "caption" && layer.caption) {
298
+ const raster = rasterizer.caption(layer.caption);
299
+ if (!raster)
300
+ return null;
301
+ return finish({
302
+ ...common,
303
+ id: id("c"),
304
+ source: raster,
305
+ // Captions are drawn at frame resolution and composite
306
+ // untransformed — by the clip's transform and by its group's.
307
+ transform: undefined,
308
+ parentMatrix: undefined,
309
+ borderRadius: undefined,
310
+ crop: undefined,
311
+ effects: undefined,
312
+ trackEffects: undefined,
313
+ // The cut's opacity is already in `opacity`; dropping the record
314
+ // here keeps its geometry — and a dip's one solid — off a layer
315
+ // that composites untransformed anyway.
316
+ transition: undefined
317
+ });
318
+ }
319
+ if (layer.kind === "text" && anim.textStyle) {
320
+ const stagger = resolveTextStaggerContext(layer.clip, layerTimeMs, canvas, animCache, sequence.tempo);
321
+ const raster = rasterizer.text(anim.textStyle, stagger);
322
+ if (!raster)
323
+ return null;
324
+ return finish({ ...common, id: id("t"), source: raster });
325
+ }
326
+ if (layer.kind === "shape") {
327
+ // The animated style carries a driven trim range; without it a trim
328
+ // animation would rasterize its first frame and hold.
329
+ const shapeStyle = anim.shapeStyle ?? layer.shapeStyle;
330
+ if (!shapeStyle)
331
+ return null;
332
+ const raster = rasterizer.shape(shapeStyle);
333
+ if (!raster)
334
+ return null;
335
+ return finish({ ...common, id: id("s"), source: raster });
336
+ }
337
+ if (!layer.assetId)
338
+ return null;
339
+ if (layer.kind === "video") {
340
+ const stream = await videoFor(layer.clip, layer.assetId);
341
+ if (!stream) {
342
+ skippedClips.add(layer.clip.name);
343
+ return null;
344
+ }
345
+ const decoded = await stream.frameAt(layerTimeMs);
346
+ if (!decoded)
347
+ return null;
348
+ return finish({
349
+ ...common,
350
+ id: id("v"),
351
+ source: {
352
+ rgba: decoded.rgba,
353
+ width: stream.width,
354
+ height: stream.height,
355
+ version: decoded.version
356
+ }
357
+ });
358
+ }
359
+ const image = await imageFor(layer.assetId);
360
+ if (!image) {
361
+ skippedClips.add(layer.clip.name);
362
+ return null;
363
+ }
364
+ return finish({
365
+ ...common,
366
+ id: id("i"),
367
+ source: { ...image, version: layer.assetId }
368
+ });
369
+ };
370
+ for (let index = 0; index < active.length; index++) {
371
+ if (index > 0 && index % YIELD_EVERY_LAYERS === 0) {
372
+ await setImmediate();
373
+ }
374
+ if (signal?.aborted)
375
+ throw abortError();
376
+ const layer = active[index];
377
+ const built = await frameLayerFor(layer);
378
+ if (signal?.aborted)
379
+ throw abortError();
380
+ if (!built)
381
+ continue;
382
+ if (layer.matte) {
383
+ const source = await frameLayerFor(layer.matte.layer, "m:");
384
+ if (signal?.aborted)
385
+ throw abortError();
386
+ if (source) {
387
+ built.matte = {
388
+ mode: layer.matte.mode,
389
+ invert: layer.matte.invert,
390
+ layer: source,
391
+ strength: layer.matte.strength,
392
+ featherPx: layer.matte.featherPx
393
+ };
394
+ }
395
+ }
396
+ layers.push(built);
397
+ }
398
+ return {
399
+ layers,
400
+ adjustments: adjustments.map((adjustment) => ({
401
+ id: adjustment.clipId,
402
+ zIndex: trackZ(adjustment.trackIndex),
403
+ opacity: adjustment.opacity,
404
+ effects: adjustment.effects,
405
+ shapeMask: adjustment.mask
406
+ ? rasterizer.mask(adjustment.mask, referenceWidth, referenceHeight) ?? undefined
407
+ : undefined,
408
+ wipe: adjustment.wipe,
409
+ precomposeGroupId: adjustment.precomposeGroupId
410
+ })),
411
+ precomposites: precomposites.map((group) => ({
412
+ id: group.clipId,
413
+ zIndex: trackZ(group.trackIndex),
414
+ stackOrder: group.stackOrder,
415
+ opacity: group.opacity,
416
+ blendMode: group.blendMode,
417
+ effects: group.effects,
418
+ transition: group.transition,
419
+ precomposeGroupId: group.precomposeGroupId
420
+ }))
421
+ };
422
+ };
423
+ /**
424
+ * True when the shutter window holds one picture, so N samples would
425
+ * average N copies of it.
426
+ *
427
+ * Resolving the layer set is cheap — it decodes nothing — and it answers
428
+ * the question that N decodes and N composites would otherwise pay for. A
429
+ * held title at 8 samples is the case: 8× the render for the frame it
430
+ * already had.
431
+ */
432
+ const staticAt = (timeMs, samplesPerFrame) => {
433
+ if (samplesPerFrame <= 1)
434
+ return false;
435
+ const { layers, adjustments } = computeActiveLayersWithHorizon(sequence.tracks, sequence.clips, timeMs, {
436
+ canvas,
437
+ animationCache: animCache,
438
+ mediaTracks: sequence.mediaTracks,
439
+ camera2d: sequence.camera2d,
440
+ tempo: sequence.tempo
441
+ });
442
+ // The animation check follows each record's clip and parent chain.
443
+ // Adjustments have no source layer, so include their clips here.
444
+ const animationLayers = [
445
+ ...layers,
446
+ ...adjustments.map((adjustment) => ({
447
+ kind: "shape",
448
+ clip: adjustment.clip,
449
+ clipId: adjustment.clipId,
450
+ trackIndex: adjustment.trackIndex,
451
+ blendMode: "normal",
452
+ opacity: adjustment.opacity,
453
+ assetId: undefined
454
+ }))
455
+ ];
456
+ return shutterWindowIsStatic(layers, hasActiveAnimation(animationLayers, timeMs, canvas, animCache, sequence.clips, sequence.tempo));
457
+ };
458
+ for (const [done, frame] of frameIndices.entries()) {
459
+ if (signal?.aborted)
460
+ throw abortError();
461
+ const timeMs = (frame * 1000) / fps;
462
+ const motionBlur = resolveFrameMotionBlur(sequence.clips, output.motionBlur, timeMs, frameMs, sequence.tracks);
463
+ for (const [sourceKey, source] of videoSources) {
464
+ if (source.endMs < timeMs) {
465
+ source.close();
466
+ videoSources.delete(sourceKey);
467
+ }
468
+ }
469
+ const samples = [];
470
+ // Sequential, not concurrent: a clip's frames come off one forward-only
471
+ // ffmpeg stream, so two samples decoding at once would race for it.
472
+ const sampleTimes = staticAt(timeMs, motionBlur.samplesPerFrame)
473
+ ? [timeMs]
474
+ : motionBlurSampleTimes(timeMs, frameMs, motionBlur);
475
+ for (const sampleMs of sampleTimes) {
476
+ // Per sample, not per frame: with 32 samples a cancelled render would
477
+ // otherwise finish 32 composites and decodes before it noticed.
478
+ if (signal?.aborted)
479
+ throw abortError();
480
+ samples.push(await sampleAt(sampleMs, timeMs, samples.length, sampleTimes.length));
481
+ if (signal?.aborted)
482
+ throw abortError();
483
+ }
484
+ const pixels = await awaitWithAbort(compositor.renderFrameSamples(samples, { alpha: output.alpha }), signal);
485
+ if (signal?.aborted)
486
+ throw abortError();
487
+ await awaitWithAbort(writeFrame
488
+ ? writeFrame(frame, pixels)
489
+ : (encoder?.write(pixels) ?? Promise.resolve()), signal);
490
+ opts.onProgress?.(done + 1, frameIndices.length);
491
+ }
492
+ if (signal?.aborted)
493
+ throw abortError();
494
+ if (encoder)
495
+ await awaitWithAbort(encoder.finish(), signal);
496
+ return { totalFrames: frameIndices.length, skippedClips: [...skippedClips] };
497
+ }
498
+ catch (error) {
499
+ encoder?.abort();
500
+ throw error;
501
+ }
502
+ finally {
503
+ signal?.removeEventListener("abort", closeVideoSources);
504
+ closeVideoSources();
505
+ compositor.dispose();
506
+ }
507
+ }
508
+ //# sourceMappingURL=compositeRender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compositeRender.js","sourceRoot":"","sources":["../../../src/nodes/timeline/compositeRender.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A contact sheet: rendered frames tiled into one PNG, each cell labelled in
3
+ * its top-left corner. `nodetool timeline render --sheet` writes one so a
4
+ * frame-by-frame review is a single image rather than a folder of stills.
5
+ *
6
+ * Frames are drawn into the sheet as they arrive, so only the sheet and the
7
+ * frame being drawn are in memory.
8
+ */
9
+ export interface ContactSheetLayout {
10
+ columns: number;
11
+ rows: number;
12
+ cellWidth: number;
13
+ cellHeight: number;
14
+ /** Space between cells and around the sheet's edge. */
15
+ gap: number;
16
+ width: number;
17
+ height: number;
18
+ }
19
+ /**
20
+ * Lay out `count` frames in `columns` columns. Cells keep the frame's aspect
21
+ * and shrink so the sheet is at most `maxWidth` wide. They never grow past the
22
+ * frame's own size.
23
+ */
24
+ export declare function contactSheetLayout(options: {
25
+ count: number;
26
+ columns: number;
27
+ frameWidth: number;
28
+ frameHeight: number;
29
+ maxWidth?: number;
30
+ }): ContactSheetLayout;
31
+ /** The top-left pixel of the cell at `index`, filled row by row. */
32
+ export declare function cellOrigin(layout: ContactSheetLayout, index: number): {
33
+ x: number;
34
+ y: number;
35
+ };
36
+ export interface ContactSheet {
37
+ /** Draw straight-alpha RGBA8 pixels scaled into cell `index`, then its label. */
38
+ draw(index: number, label: string, rgba: Uint8Array, width: number, height: number): void;
39
+ /** The sheet as PNG bytes. */
40
+ encode(): Buffer;
41
+ }
42
+ export declare function createContactSheet(layout: ContactSheetLayout): ContactSheet;
43
+ //# sourceMappingURL=contactSheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contactSheet.d.ts","sourceRoot":"","sources":["../../../src/nodes/timeline/contactSheet.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,kBAAkB,CA2BrB;AAED,oEAAoE;AACpE,wBAAgB,UAAU,CACxB,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,MAAM,GACZ;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAO1B;AAED,MAAM,WAAW,YAAY;IAC3B,iFAAiF;IACjF,IAAI,CACF,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,IAAI,CAAC;IACR,8BAA8B;IAC9B,MAAM,IAAI,MAAM,CAAC;CAClB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAgC3E"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * A contact sheet: rendered frames tiled into one PNG, each cell labelled in
3
+ * its top-left corner. `nodetool timeline render --sheet` writes one so a
4
+ * frame-by-frame review is a single image rather than a folder of stills.
5
+ *
6
+ * Frames are drawn into the sheet as they arrive, so only the sheet and the
7
+ * frame being drawn are in memory.
8
+ */
9
+ import { createCanvas } from "@napi-rs/canvas";
10
+ import { registerBundledFonts } from "@nodetool-ai/timeline/fonts/node";
11
+ /** Widest sheet, in pixels, before cells are scaled down to fit. */
12
+ const MAX_SHEET_WIDTH = 2400;
13
+ const GAP = 8;
14
+ /**
15
+ * Lay out `count` frames in `columns` columns. Cells keep the frame's aspect
16
+ * and shrink so the sheet is at most `maxWidth` wide. They never grow past the
17
+ * frame's own size.
18
+ */
19
+ export function contactSheetLayout(options) {
20
+ const { count, frameWidth, frameHeight } = options;
21
+ if (!Number.isInteger(options.columns) || options.columns < 1) {
22
+ throw new Error(`A contact sheet needs a whole number of columns (got ${options.columns}).`);
23
+ }
24
+ const columns = Math.min(options.columns, Math.max(1, count));
25
+ const rows = Math.max(1, Math.ceil(count / columns));
26
+ const maxWidth = options.maxWidth ?? MAX_SHEET_WIDTH;
27
+ const cellWidth = Math.max(1, Math.min(frameWidth, Math.floor((maxWidth - GAP * (columns + 1)) / columns)));
28
+ const cellHeight = Math.max(1, Math.round((cellWidth * frameHeight) / frameWidth));
29
+ return {
30
+ columns,
31
+ rows,
32
+ cellWidth,
33
+ cellHeight,
34
+ gap: GAP,
35
+ width: columns * cellWidth + (columns + 1) * GAP,
36
+ height: rows * cellHeight + (rows + 1) * GAP
37
+ };
38
+ }
39
+ /** The top-left pixel of the cell at `index`, filled row by row. */
40
+ export function cellOrigin(layout, index) {
41
+ const column = index % layout.columns;
42
+ const row = Math.floor(index / layout.columns);
43
+ return {
44
+ x: layout.gap + column * (layout.cellWidth + layout.gap),
45
+ y: layout.gap + row * (layout.cellHeight + layout.gap)
46
+ };
47
+ }
48
+ export function createContactSheet(layout) {
49
+ registerBundledFonts();
50
+ const sheet = createCanvas(layout.width, layout.height);
51
+ const ctx = sheet.getContext("2d");
52
+ ctx.fillStyle = "#202020";
53
+ ctx.fillRect(0, 0, layout.width, layout.height);
54
+ const fontSize = Math.max(12, Math.round(layout.cellHeight * 0.08));
55
+ const padding = Math.round(fontSize * 0.35);
56
+ return {
57
+ draw(index, label, rgba, width, height) {
58
+ const frame = createCanvas(width, height);
59
+ const frameCtx = frame.getContext("2d");
60
+ const image = frameCtx.createImageData(width, height);
61
+ image.data.set(rgba);
62
+ frameCtx.putImageData(image, 0, 0);
63
+ const { x, y } = cellOrigin(layout, index);
64
+ ctx.drawImage(frame, x, y, layout.cellWidth, layout.cellHeight);
65
+ ctx.font = `600 ${fontSize}px Inter, sans-serif`;
66
+ ctx.textBaseline = "top";
67
+ const textWidth = ctx.measureText(label).width;
68
+ ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
69
+ ctx.fillRect(x, y, textWidth + padding * 2, fontSize + padding * 2);
70
+ ctx.fillStyle = "#ffffff";
71
+ ctx.fillText(label, x + padding, y + padding);
72
+ },
73
+ encode() {
74
+ return sheet.toBuffer("image/png");
75
+ }
76
+ };
77
+ }
78
+ //# sourceMappingURL=contactSheet.js.map