@1agh/maude 0.39.1 → 0.40.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/README.md +1 -1
- package/apps/studio/annotations-context-toolbar.tsx +44 -1
- package/apps/studio/annotations-layer.tsx +253 -3
- package/apps/studio/annotations-model.ts +107 -6
- package/apps/studio/api.ts +812 -64
- package/apps/studio/bin/_html-playwright.mjs +9 -1
- package/apps/studio/bin/_pdf-playwright.mjs +8 -1
- package/apps/studio/bin/_png-playwright.mjs +8 -1
- package/apps/studio/bin/_pw-launch.mjs +54 -0
- package/apps/studio/bin/_svg-playwright.mjs +8 -1
- package/apps/studio/bin/_video-playwright.mjs +452 -0
- package/apps/studio/bin/prep.sh +8 -1
- package/apps/studio/canvas-edit.ts +1885 -104
- package/apps/studio/canvas-lib.tsx +19 -0
- package/apps/studio/canvas-list-watch.ts +6 -2
- package/apps/studio/canvas-shell.tsx +27 -0
- package/apps/studio/client/app.jsx +1029 -30
- package/apps/studio/client/github.js +7 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +860 -0
- package/apps/studio/client/panels/timeline-parse.js +229 -0
- package/apps/studio/client/panels/timeline-snap.js +55 -0
- package/apps/studio/client/styles/3-shell-maude.css +107 -0
- package/apps/studio/config.schema.json +14 -0
- package/apps/studio/context-menu.tsx +1 -1
- package/apps/studio/context.ts +113 -1
- package/apps/studio/dist/client.bundle.js +88 -16
- package/apps/studio/dist/comment-mount.js +1 -1
- package/apps/studio/dist/runtime/.min-sizes.json +11 -1
- package/apps/studio/dist/runtime/@remotion_media.js +491 -0
- package/apps/studio/dist/runtime/@remotion_player.js +56 -0
- package/apps/studio/dist/runtime/@remotion_transitions.js +300 -0
- package/apps/studio/dist/runtime/@remotion_transitions_clock-wipe.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_fade.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_flip.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_none.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_slide.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_wipe.js +1 -0
- package/apps/studio/dist/runtime/REMOTION-LICENSE.md +28 -0
- package/apps/studio/dist/runtime/remotion.js +42 -0
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/exporters/_browser-bundles.ts +117 -0
- package/apps/studio/exporters/_runtime.ts +69 -0
- package/apps/studio/exporters/html.ts +4 -3
- package/apps/studio/exporters/index.ts +28 -2
- package/apps/studio/exporters/pdf.ts +4 -3
- package/apps/studio/exporters/png.ts +5 -3
- package/apps/studio/exporters/pptx.ts +6 -4
- package/apps/studio/exporters/svg.ts +9 -5
- package/apps/studio/exporters/video-encode-lib.ts +200 -0
- package/apps/studio/exporters/video-render-lib.ts +108 -0
- package/apps/studio/exporters/video.ts +184 -0
- package/apps/studio/http.ts +535 -27
- package/apps/studio/input-router.tsx +7 -1
- package/apps/studio/runtime-bundle.ts +30 -0
- package/apps/studio/server.ts +34 -9
- package/apps/studio/test/annotations-roundtrip.test.ts +47 -0
- package/apps/studio/test/canvas-create-api.test.ts +76 -0
- package/apps/studio/test/canvas-edit.test.ts +90 -0
- package/apps/studio/test/canvas-list-watch.test.ts +49 -0
- package/apps/studio/test/canvas-media-drop.test.ts +30 -1
- package/apps/studio/test/canvas-origin-gate.test.ts +36 -0
- package/apps/studio/test/clip-addressing.test.ts +732 -0
- package/apps/studio/test/config-reload.test.ts +230 -0
- package/apps/studio/test/dns-rebinding-guard.test.ts +74 -0
- package/apps/studio/test/edit-persistence.test.ts +91 -0
- package/apps/studio/test/exporters/runtime.test.ts +59 -0
- package/apps/studio/test/file-lock.test.ts +84 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +82 -0
- package/apps/studio/test/timeline-parse.test.ts +127 -0
- package/apps/studio/test/timeline-snap.test.ts +85 -0
- package/apps/studio/test/video-asset.test.ts +163 -0
- package/apps/studio/test/video-comp-fixture.test.ts +50 -0
- package/apps/studio/test/video-comp.test.ts +168 -0
- package/apps/studio/test/video-render-bridge.test.ts +149 -0
- package/apps/studio/use-annotation-resize.tsx +6 -3
- package/apps/studio/use-canvas-media-drop.tsx +66 -4
- package/apps/studio/video-comp.tsx +444 -0
- package/apps/studio/whats-new.json +27 -0
- package/apps/studio/ws.ts +5 -0
- package/package.json +8 -8
- package/plugins/design/templates/_shell.html +25 -2
- package/plugins/design/templates/design-system-inspiration/_MAPPING.md +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// video-render-lib.ts — DDR-148 addendum: in-page whole-comp render (video +
|
|
2
|
+
// audio, one pass) via @remotion/web-renderer's renderMediaOnWeb.
|
|
3
|
+
//
|
|
4
|
+
// Runs INSIDE the capture Chromium (injected via addScriptTag, mirroring
|
|
5
|
+
// video-encode-lib.ts). Replaces the frame-step + mediabunny screenshot loop
|
|
6
|
+
// for video-comp mp4/webm export: Remotion owns the TransitionSeries offset
|
|
7
|
+
// math and `<Audio volume={fn}>` closures internally, so a single
|
|
8
|
+
// renderMediaOnWeb call produces a correctly-muxed, frame-accurate video+audio
|
|
9
|
+
// file with no hand-rolled mixing. GIF export (no audio, no renderMediaOnWeb
|
|
10
|
+
// container support) stays on the frame-step + gifenc path — proven compatible
|
|
11
|
+
// with @remotion/media (screenshot-after-seek reliably shows decoded frames,
|
|
12
|
+
// including mid-transition composites; see the DDR-148 addendum retro).
|
|
13
|
+
//
|
|
14
|
+
// Externalizes 'remotion' + '@remotion/media' (resolved via the SAME importmap
|
|
15
|
+
// bundle the page's canvas module tree uses) to avoid the dual-package hazard:
|
|
16
|
+
// the `component` passed in was created against the page's imported instances,
|
|
17
|
+
// so renderMediaOnWeb's internal Composition/context providers MUST be the
|
|
18
|
+
// same instances, or useCurrentFrame()/<Audio> volume closures bind to the
|
|
19
|
+
// wrong (default) context and the render freezes / plays silent.
|
|
20
|
+
//
|
|
21
|
+
// Offline posture (DDR-148 security): the capture CSP already locks network
|
|
22
|
+
// egress to loopback-only, which blocks web-renderer's optional telemetry
|
|
23
|
+
// (remotion.pro/api/track/register-usage-point) at the network layer — no
|
|
24
|
+
// additional code needed here. `licenseKey`/`isProduction` are still threaded
|
|
25
|
+
// through per Remotion's disclosed-license posture (MAUDE_REMOTION_LICENSE).
|
|
26
|
+
|
|
27
|
+
import { renderMediaOnWeb } from '@remotion/web-renderer';
|
|
28
|
+
import type { ComponentType } from 'react';
|
|
29
|
+
|
|
30
|
+
interface RenderVideoOpts {
|
|
31
|
+
component: ComponentType<Record<string, unknown>>;
|
|
32
|
+
inputProps?: Record<string, unknown>;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
fps: number;
|
|
36
|
+
durationInFrames: number;
|
|
37
|
+
container: 'mp4' | 'webm';
|
|
38
|
+
videoCodec?: string;
|
|
39
|
+
audioCodec?: string;
|
|
40
|
+
scale?: number;
|
|
41
|
+
muted?: boolean;
|
|
42
|
+
frameRange?: [number, number];
|
|
43
|
+
licenseKey?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface RenderVideoResult {
|
|
47
|
+
b64: string;
|
|
48
|
+
bytes: number;
|
|
49
|
+
container: string;
|
|
50
|
+
videoCodec: string | null;
|
|
51
|
+
audioCodec: string | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function bytesToB64(buf: ArrayBuffer | Uint8Array): string {
|
|
55
|
+
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
56
|
+
let bin = '';
|
|
57
|
+
const CHUNK = 0x8000;
|
|
58
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
59
|
+
bin += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
60
|
+
}
|
|
61
|
+
return btoa(bin);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function renderVideo(opts: RenderVideoOpts): Promise<RenderVideoResult> {
|
|
65
|
+
const muted = opts.muted ?? false;
|
|
66
|
+
const videoCodec = (opts.videoCodec ??
|
|
67
|
+
(opts.container === 'webm' ? 'vp9' : 'h264')) as Parameters<
|
|
68
|
+
typeof renderMediaOnWeb
|
|
69
|
+
>[0]['videoCodec'];
|
|
70
|
+
const audioCodec = muted
|
|
71
|
+
? null
|
|
72
|
+
: ((opts.audioCodec ?? (opts.container === 'webm' ? 'opus' : 'aac')) as Parameters<
|
|
73
|
+
typeof renderMediaOnWeb
|
|
74
|
+
>[0]['audioCodec']);
|
|
75
|
+
|
|
76
|
+
const result = await renderMediaOnWeb({
|
|
77
|
+
composition: {
|
|
78
|
+
component: opts.component,
|
|
79
|
+
id: 'maude-export',
|
|
80
|
+
width: opts.width,
|
|
81
|
+
height: opts.height,
|
|
82
|
+
fps: opts.fps,
|
|
83
|
+
durationInFrames: opts.durationInFrames,
|
|
84
|
+
},
|
|
85
|
+
inputProps: opts.inputProps ?? {},
|
|
86
|
+
container: opts.container,
|
|
87
|
+
videoCodec,
|
|
88
|
+
audioCodec,
|
|
89
|
+
muted,
|
|
90
|
+
scale: opts.scale ?? 1,
|
|
91
|
+
frameRange: opts.frameRange ?? null,
|
|
92
|
+
isProduction: true,
|
|
93
|
+
licenseKey: opts.licenseKey ?? 'free-license',
|
|
94
|
+
logLevel: 'error',
|
|
95
|
+
});
|
|
96
|
+
const blob = await result.getBlob();
|
|
97
|
+
const buf = new Uint8Array(await blob.arrayBuffer());
|
|
98
|
+
return {
|
|
99
|
+
b64: bytesToB64(buf),
|
|
100
|
+
bytes: buf.length,
|
|
101
|
+
container: opts.container,
|
|
102
|
+
videoCodec: videoCodec ?? null,
|
|
103
|
+
audioCodec: audioCodec ?? null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
(globalThis as unknown as { __maudeRenderVideo__: typeof renderVideo }).__maudeRenderVideo__ =
|
|
108
|
+
renderVideo;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// DDR-148 — video/animation exporter (mp4 · webm · gif).
|
|
2
|
+
//
|
|
3
|
+
// Drives bin/_video-playwright.mjs, which picks one of two capture strategies:
|
|
4
|
+
// • mp4/webm of a registered video-comp — DDR-148 addendum (audio export):
|
|
5
|
+
// window.__maude_render_video__ → @remotion/web-renderer's renderMediaOnWeb,
|
|
6
|
+
// video+audio in one pass (Remotion owns the TransitionSeries/volume-closure
|
|
7
|
+
// timeline math). `getWebRendererBundle()` supplies the in-page renderer.
|
|
8
|
+
// • everything else (gif, ordinary/CSS-WAAPI artboards, or no registered
|
|
9
|
+
// comp) — the original frame-step spine: steps the artboard frame-by-frame
|
|
10
|
+
// via window.__maude_seek__ and encodes in-page with mediabunny (H.264/avc
|
|
11
|
+
// MP4, VP9/VP8 WebM fallback) or gifenc. `getEncodeLibBundle()` supplies
|
|
12
|
+
// this in-page encoder, and doubles as the render-lib path's fallback when
|
|
13
|
+
// the target has no registered comp (the shim decides at runtime — see
|
|
14
|
+
// _video-playwright.mjs for the exact routing condition).
|
|
15
|
+
// Scope is always `artboard`. No native binaries; the capture Chromium is the
|
|
16
|
+
// same one every other exporter already uses (DDR-041).
|
|
17
|
+
|
|
18
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
19
|
+
import { tmpdir } from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
22
|
+
import { getEncodeLibBundle, getWebRendererBundle } from './_browser-bundles.ts';
|
|
23
|
+
import { exportShimPath, resolveExportRuntime } from './_runtime.ts';
|
|
24
|
+
import {
|
|
25
|
+
canvasShellUrl,
|
|
26
|
+
type ExportContext,
|
|
27
|
+
type ExportOptions,
|
|
28
|
+
type ExportResult,
|
|
29
|
+
} from './index.ts';
|
|
30
|
+
import type { Target } from './scope.ts';
|
|
31
|
+
|
|
32
|
+
// DDR-045: resolve via DEV_SERVER_ROOT, never `import.meta.dir`. See _runtime.ts.
|
|
33
|
+
const VIDEO_PLAYWRIGHT = exportShimPath('_video-playwright.mjs');
|
|
34
|
+
|
|
35
|
+
type VideoFormat = 'mp4' | 'webm' | 'gif';
|
|
36
|
+
|
|
37
|
+
const CONTENT_TYPE: Record<string, string> = {
|
|
38
|
+
mp4: 'video/mp4',
|
|
39
|
+
webm: 'video/webm',
|
|
40
|
+
gif: 'image/gif',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Hard cap so a runaway/huge comp can't spawn an unbounded screenshot loop. */
|
|
44
|
+
const MAX_FRAMES = 900; // 30 s @ 30 fps
|
|
45
|
+
|
|
46
|
+
async function runVideo(
|
|
47
|
+
format: VideoFormat,
|
|
48
|
+
targets: Target[],
|
|
49
|
+
options: ExportOptions,
|
|
50
|
+
ctx: ExportContext
|
|
51
|
+
): Promise<ExportResult> {
|
|
52
|
+
const el = targets.find((t): t is Extract<Target, { kind: 'element' }> => t.kind === 'element');
|
|
53
|
+
if (!el) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`${format} export needs an artboard target — make the artboard a video-comp or add motion`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// gif has no renderMediaOnWeb container support (and no audio need — GIF is a
|
|
60
|
+
// silent format), so it never needs the render-lib bundle; mp4/webm build it
|
|
61
|
+
// alongside the frame-step encode-lib (the shim decides per-target at runtime
|
|
62
|
+
// whether the artboard has a registered comp to hand the renderer).
|
|
63
|
+
const [lib, renderLib] = await Promise.all([
|
|
64
|
+
getEncodeLibBundle(),
|
|
65
|
+
format === 'gif' ? Promise.resolve(null) : getWebRendererBundle(),
|
|
66
|
+
]);
|
|
67
|
+
const tmp = mkdtempSync(path.join(tmpdir(), 'maude-video-'));
|
|
68
|
+
const outPath = path.join(tmp, `export.${format}`);
|
|
69
|
+
|
|
70
|
+
const args = [
|
|
71
|
+
VIDEO_PLAYWRIGHT,
|
|
72
|
+
'--url',
|
|
73
|
+
canvasShellUrl(ctx, el.file),
|
|
74
|
+
'--selector',
|
|
75
|
+
el.cssPath,
|
|
76
|
+
'--encode-lib',
|
|
77
|
+
lib,
|
|
78
|
+
'--format',
|
|
79
|
+
format,
|
|
80
|
+
'--out',
|
|
81
|
+
outPath,
|
|
82
|
+
'--timeout',
|
|
83
|
+
String((options.timeoutSec as number | undefined) ?? 120),
|
|
84
|
+
];
|
|
85
|
+
if (renderLib) args.push('--render-lib', renderLib);
|
|
86
|
+
if (el.widen) args.push('--widen', '1');
|
|
87
|
+
// Ordinary artboards animate on wall-clock (WAAPI); video-comps on frame index.
|
|
88
|
+
if (options.mode === 'ordinary') args.push('--mode', 'ordinary');
|
|
89
|
+
|
|
90
|
+
// Export-with-audio (DDR-148 addendum) — default ON; only the render-lib path
|
|
91
|
+
// (mp4/webm of a registered comp) can actually produce audio, but the flag is
|
|
92
|
+
// harmless for the frame-step fallback (video-only, always has been).
|
|
93
|
+
const wantAudio = options.audio !== false;
|
|
94
|
+
args.push('--audio', wantAudio ? '1' : '0');
|
|
95
|
+
// Remotion license posture — MAUDE_REMOTION_LICENSE env (config-driven, per
|
|
96
|
+
// project); the shim defaults to 'free-license' when unset. Network egress
|
|
97
|
+
// for web-renderer's optional telemetry is blocked at the CSP layer regardless
|
|
98
|
+
// (cspForCapture's `connect-src 'self'`, http.ts) — no additional code needed
|
|
99
|
+
// for the offline posture beyond passing the key through.
|
|
100
|
+
if (process.env.MAUDE_REMOTION_LICENSE) {
|
|
101
|
+
args.push('--license-key', process.env.MAUDE_REMOTION_LICENSE);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Resolution multiplier (1–3×) — the artboard's native size × scale. Default 2×
|
|
105
|
+
// so a 960×540 comp exports at 1920×1080 instead of a tiny native capture. The
|
|
106
|
+
// shim applies it as deviceScaleFactor AND sizes the encoder to match.
|
|
107
|
+
const scale = clampInt(options.scale, 1, 3) ?? 2;
|
|
108
|
+
args.push('--scale', String(scale));
|
|
109
|
+
|
|
110
|
+
const fps = clampInt(options.fps, 1, 60);
|
|
111
|
+
if (fps) args.push('--fps', String(fps));
|
|
112
|
+
|
|
113
|
+
const frames = resolveFrames(options, fps);
|
|
114
|
+
if (frames) args.push('--frames', String(frames));
|
|
115
|
+
|
|
116
|
+
if (format === 'gif') {
|
|
117
|
+
const colors = clampInt(options.gifColors, 2, 256);
|
|
118
|
+
if (colors) args.push('--gifColors', String(colors));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const proc = Bun.spawn([resolveExportRuntime(), ...args], {
|
|
123
|
+
cwd: path.dirname(VIDEO_PLAYWRIGHT),
|
|
124
|
+
stdout: 'pipe',
|
|
125
|
+
stderr: 'pipe',
|
|
126
|
+
});
|
|
127
|
+
const [stdout, stderr] = await Promise.all([
|
|
128
|
+
new Response(proc.stdout).text(),
|
|
129
|
+
new Response(proc.stderr).text(),
|
|
130
|
+
]);
|
|
131
|
+
const code = await proc.exited;
|
|
132
|
+
if (code !== 0) {
|
|
133
|
+
throw new Error(`_video-playwright exited ${code}: ${stderr.trim() || stdout.trim()}`);
|
|
134
|
+
}
|
|
135
|
+
const body = new Uint8Array(readFileSync(outPath));
|
|
136
|
+
// The container can differ from the request (mp4 → webm fallback when the
|
|
137
|
+
// browser has no H.264 encoder). Read the summary line for the real ext.
|
|
138
|
+
let ext: string = format;
|
|
139
|
+
try {
|
|
140
|
+
const lastLine = stdout.trim().split('\n').filter(Boolean).pop() ?? '{}';
|
|
141
|
+
const summary = JSON.parse(lastLine) as { container?: string };
|
|
142
|
+
if (summary.container) ext = summary.container;
|
|
143
|
+
} catch {
|
|
144
|
+
/* keep the requested ext */
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
filename: `${el.canvasSlug}.${ext}`,
|
|
148
|
+
contentType: CONTENT_TYPE[ext] ?? 'application/octet-stream',
|
|
149
|
+
body,
|
|
150
|
+
};
|
|
151
|
+
} finally {
|
|
152
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Coerce an option to an int within [lo, hi], or undefined when absent/invalid. */
|
|
157
|
+
function clampInt(raw: unknown, lo: number, hi: number): number | undefined {
|
|
158
|
+
const n = Math.round(Number(raw));
|
|
159
|
+
if (!Number.isFinite(n)) return undefined;
|
|
160
|
+
return Math.max(lo, Math.min(hi, n));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Resolve the frame count from options (frames | durationMs×fps), capped. */
|
|
164
|
+
function resolveFrames(options: ExportOptions, fps: number | undefined): number | undefined {
|
|
165
|
+
const explicit = clampInt(options.frames, 1, MAX_FRAMES);
|
|
166
|
+
if (explicit) return explicit;
|
|
167
|
+
const durationMs = Number(options.durationMs);
|
|
168
|
+
if (Number.isFinite(durationMs) && durationMs > 0 && fps) {
|
|
169
|
+
return Math.min(MAX_FRAMES, Math.max(1, Math.round((durationMs / 1000) * fps)));
|
|
170
|
+
}
|
|
171
|
+
// No hint → the shim falls back to the in-page comp meta (durationInFrames),
|
|
172
|
+
// still clamped there. Pass the ceiling so an unbounded comp can't run away.
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const mp4 = {
|
|
177
|
+
run: (t: Target[], o: ExportOptions, c: ExportContext) => runVideo('mp4', t, o, c),
|
|
178
|
+
};
|
|
179
|
+
export const webm = {
|
|
180
|
+
run: (t: Target[], o: ExportOptions, c: ExportContext) => runVideo('webm', t, o, c),
|
|
181
|
+
};
|
|
182
|
+
export const gif = {
|
|
183
|
+
run: (t: Target[], o: ExportOptions, c: ExportContext) => runVideo('gif', t, o, c),
|
|
184
|
+
};
|