@lalalic/markcut 1.2.0 → 2.1.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/AGENTS.md +40 -17
- package/README.md +7 -3
- package/SKILL.md +0 -2
- package/docs/edit-mode.md +1 -1
- package/docs/label-mode.md +6 -4
- package/docs/markdown-descriptive.md +34 -1
- package/docs/system-prompt-edit.md +19 -0
- package/package.json +1 -1
- package/src/config.mjs +8 -3
- package/src/descriptive/compiler.test.ts +42 -42
- package/src/descriptive/compiler.ts +41 -52
- package/src/descriptive/markdown.ts +8 -4
- package/src/descriptive/resolve.test.ts +14 -7
- package/src/descriptive/resolve.ts +148 -20
- package/src/player/browser.tsx +178 -54
- package/src/player/bundle/player.js +1149 -566
- package/src/player/components/EditControls.tsx +92 -0
- package/src/player/components/HeaderBar.tsx +60 -0
- package/src/player/components/LabelControls.tsx +367 -0
- package/src/player/components/SceneThumbnails.tsx +60 -0
- package/src/player/components/VariantBar.tsx +39 -0
- package/src/player/components/index.ts +5 -0
- package/src/player/pipeline.mjs +130 -66
- package/src/player/pipeline.ts +3 -1
- package/src/player/server-shared.mjs +5 -7
- package/src/player/server.mjs +336 -239
- package/src/render/cli.mjs +4 -6
- package/src/schema/index.ts +20 -23
- package/src/types/Audio.tsx +25 -33
- package/src/types/Component.tsx +18 -24
- package/src/types/Effect.tsx +31 -39
- package/src/types/Image.tsx +23 -30
- package/src/types/Include.tsx +70 -76
- package/src/types/Map.tsx +48 -44
- package/src/types/Rhythm.tsx +19 -27
- package/src/types/Video.tsx +40 -47
- package/src/utils/index.ts +23 -10
- package/src/vision/cli.mjs +6 -6
- package/templates/courseware/TEMPLATE.md +16 -10
- package/tests/fixtures/audio.json +4 -2
- package/tests/fixtures/basic.json +2 -1
- package/tests/fixtures/component-all.json +4 -2
- package/tests/fixtures/components.json +4 -2
- package/tests/fixtures/effects.json +12 -6
- package/tests/fixtures/full.json +8 -4
- package/tests/fixtures/map.json +2 -1
- package/tests/fixtures/md/dialogue.md +12 -0
- package/tests/fixtures/md/edge-cases.md +9 -0
- package/tests/fixtures/scenes.json +4 -2
- package/tests/fixtures/subtitle.json +6 -3
- package/tests/fixtures/subvideo.json +11 -6
- package/tests/fixtures/templates/courseware.md +61 -4
- package/tests/fixtures/tween-visual.json +2 -6
- package/tests/fixtures/video-series.json +6 -14
- package/tests/md-descriptive.test.ts +170 -0
- package/tests/render.test.ts +32 -16
- package/tests/schema.test.ts +6 -3
- package/tests/server.test.ts +9 -6
- package/tests/utils.ts +4 -4
- package/scripts/artlist-dl.mjs +0 -190
- package/src/player/label-server.mjs +0 -599
package/src/render/cli.mjs
CHANGED
|
@@ -243,21 +243,19 @@ async function main() {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
if (args.label || args.edit) {
|
|
246
|
-
const playerServer =
|
|
247
|
-
? join(__dirname, "..", "player", "label-server.mjs")
|
|
248
|
-
: join(__dirname, "..", "player", "server.mjs");
|
|
246
|
+
const playerServer = join(__dirname, "..", "player", "server.mjs");
|
|
249
247
|
if (!existsSync(playerServer)) {
|
|
250
248
|
console.error("Player server not found at", playerServer);
|
|
251
249
|
process.exit(1);
|
|
252
250
|
}
|
|
253
|
-
const
|
|
251
|
+
const labelFlag = args.label ? "--label" : "";
|
|
254
252
|
const editFlag = args.edit ? "--edit" : "";
|
|
255
253
|
const portFlag = `--port=${args.port || 3001}`;
|
|
256
254
|
const fileFlag = args.file || join(ROOT, "video.json");
|
|
257
255
|
const port = args.port || 3001;
|
|
258
256
|
// Pass variant flags to the server
|
|
259
257
|
const variantFlags = args.variant.map(v => `--variant=${v}`);
|
|
260
|
-
const serverArgs = [playerServer, resolve(fileFlag),
|
|
258
|
+
const serverArgs = [playerServer, resolve(fileFlag), labelFlag, editFlag, portFlag, ...variantFlags].filter(Boolean);
|
|
261
259
|
const child = spawn("node", serverArgs, { cwd: ROOT, stdio: ["ignore", "pipe", "inherit"] });
|
|
262
260
|
let serverReady = false;
|
|
263
261
|
let stdoutBuffer = "";
|
|
@@ -267,7 +265,7 @@ async function main() {
|
|
|
267
265
|
process.stdout.write(chunk);
|
|
268
266
|
if (!serverReady && !args.noBrowser) {
|
|
269
267
|
stdoutBuffer += chunk.toString();
|
|
270
|
-
if (stdoutBuffer.includes("Player ready")
|
|
268
|
+
if (stdoutBuffer.includes("Player ready")) {
|
|
271
269
|
serverReady = true;
|
|
272
270
|
|
|
273
271
|
try {
|
package/src/schema/index.ts
CHANGED
|
@@ -6,25 +6,21 @@ import { uid } from "../utils/index";
|
|
|
6
6
|
* Render-only — no prompts, no async transforms, no providers.
|
|
7
7
|
*
|
|
8
8
|
* A stream tree is rooted at `root`. Folders contain children.
|
|
9
|
-
* Leaf streams (video/audio/image/component)
|
|
10
|
-
* describing when they appear
|
|
9
|
+
* Leaf streams (video/audio/image/component) carry timing on their base
|
|
10
|
+
* fields (start/end/duration/startFrom/endAt) describing when they appear
|
|
11
|
+
* on the timeline.
|
|
11
12
|
*
|
|
12
13
|
* Subtitles are a root-level overlay: root.subtitle points to a VTT file
|
|
13
14
|
* that covers the full video duration. Generated by the TTS pipeline.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
effectId: z.string().optional(),
|
|
24
|
-
style: z.string().optional().describe("inline css"),
|
|
25
|
-
volume: z.number().min(0).max(1).optional(),
|
|
26
|
-
});
|
|
27
|
-
export type Action = z.infer<typeof action>;
|
|
17
|
+
/**
|
|
18
|
+
* Leaf timing lives directly on BaseShape (start/end/duration/startFrom/endAt).
|
|
19
|
+
* The old `actions[]` array has been removed — every leaf occupies exactly one
|
|
20
|
+
* span on its parent's timeline. `end` is the source of truth for duration;
|
|
21
|
+
* `duration` is a normalized convenience (end = start + duration when end is
|
|
22
|
+
* absent). `loop`/`volume`/`style` moved onto the stream type itself.
|
|
23
|
+
*/
|
|
28
24
|
|
|
29
25
|
export const eventSpec = z.object({
|
|
30
26
|
when: z.string().describe("'start' | 'end' | '50%' | '2.5s'"),
|
|
@@ -39,6 +35,11 @@ const BaseShape = {
|
|
|
39
35
|
style: z.string().optional().describe("inline css"),
|
|
40
36
|
visible: z.boolean().default(true),
|
|
41
37
|
isBackground: z.boolean().optional(),
|
|
38
|
+
start: z.number().min(0).default(0).describe("seconds, relative to parent timeline (0 inside series parents)"),
|
|
39
|
+
end: z.number().min(0).optional().describe("seconds, relative to parent timeline — source of truth for duration"),
|
|
40
|
+
duration: z.number().min(0).optional().describe("convenience; normalized to end = start + duration when end is absent"),
|
|
41
|
+
startFrom: z.number().min(0).optional().describe("trim seconds from source start (video/audio)"),
|
|
42
|
+
endAt: z.number().min(0).optional().describe("trim seconds at source end (video/audio)"),
|
|
42
43
|
durationInSeconds: z.number().optional().describe("set by engine; do not edit by hand"),
|
|
43
44
|
on: eventSpec.optional().describe("event that fires at a specific frame, mutating registered component state"),
|
|
44
45
|
};
|
|
@@ -102,7 +103,7 @@ export const video = base.extend({
|
|
|
102
103
|
playbackRate: z.number().optional(),
|
|
103
104
|
width: z.number().default(1080),
|
|
104
105
|
height: z.number().default(1920),
|
|
105
|
-
|
|
106
|
+
loop: z.number().int().min(1).optional().describe(">1 = loop count"),
|
|
106
107
|
});
|
|
107
108
|
export type Video = z.infer<typeof video>;
|
|
108
109
|
|
|
@@ -111,7 +112,8 @@ export const audio = base.extend({
|
|
|
111
112
|
src: z.string().optional(),
|
|
112
113
|
volume: z.number().min(0).max(1).default(1),
|
|
113
114
|
foreground: z.boolean().optional().describe("ducks parent video audio while playing"),
|
|
114
|
-
|
|
115
|
+
loop: z.number().int().min(1).optional().describe(">1 = loop count"),
|
|
116
|
+
speaker: z.string().optional().describe("speaker name for multi-turn dialogue; set by resolveDialogue"),
|
|
115
117
|
});
|
|
116
118
|
export type Audio = z.infer<typeof audio>;
|
|
117
119
|
|
|
@@ -119,7 +121,6 @@ export const image = base.extend({
|
|
|
119
121
|
type: z.literal("image").default("image"),
|
|
120
122
|
src: z.string().optional(),
|
|
121
123
|
fit: z.enum(["contain", "cover", "fill"]).default("contain"),
|
|
122
|
-
actions: z.array(action).min(1).default(() => [action.parse({})]),
|
|
123
124
|
});
|
|
124
125
|
export type Image = z.infer<typeof image>;
|
|
125
126
|
|
|
@@ -134,7 +135,6 @@ export const component = base.extend({
|
|
|
134
135
|
type: z.literal("component").default("component"),
|
|
135
136
|
jsx: z.string().describe("usage JSX expression compiled at runtime; tag names resolved from imports"),
|
|
136
137
|
data: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional().describe("extra variables (e.g. from ~~~md source code fences) available in JSX scope"),
|
|
137
|
-
actions: z.array(action).min(1).default(() => [action.parse({})]),
|
|
138
138
|
});
|
|
139
139
|
export type Component = z.infer<typeof component>;
|
|
140
140
|
|
|
@@ -153,7 +153,6 @@ export const effect = base.extend({
|
|
|
153
153
|
.optional()
|
|
154
154
|
.describe('inline keyframes: { "0": { opacity: "0" }, "100": { opacity: "1" } }'),
|
|
155
155
|
children: z.array(z.lazy((): z.ZodTypeAny => stream)).default(() => []),
|
|
156
|
-
actions: z.array(action).min(1).default(() => [action.parse({})]),
|
|
157
156
|
});
|
|
158
157
|
export type Effect = z.infer<typeof effect>;
|
|
159
158
|
|
|
@@ -166,7 +165,6 @@ export const rhythm = base.extend({
|
|
|
166
165
|
volume: z.number().min(0).max(1).default(1),
|
|
167
166
|
spots: z.array(z.number()).optional().describe("pre-computed beat timestamps in seconds"),
|
|
168
167
|
children: z.array(z.lazy((): z.ZodTypeAny => stream)).default(() => []),
|
|
169
|
-
actions: z.array(action).min(1).default(() => [action.parse({})]),
|
|
170
168
|
});
|
|
171
169
|
export type Rhythm = z.infer<typeof rhythm>;
|
|
172
170
|
|
|
@@ -177,7 +175,7 @@ export type Rhythm = z.infer<typeof rhythm>;
|
|
|
177
175
|
// - A scene-based video.json (has `meta` and `scenes`)
|
|
178
176
|
//
|
|
179
177
|
// Usage:
|
|
180
|
-
// { type: "include", src: "./path/to/video.json",
|
|
178
|
+
// { type: "include", src: "./path/to/video.json", start: 0, end: 5 }
|
|
181
179
|
//
|
|
182
180
|
// Falls back to inline `children` if `src` is not set (legacy behavior).
|
|
183
181
|
// ---------------------------------------------------------------------------
|
|
@@ -186,7 +184,6 @@ export const include = base.extend({
|
|
|
186
184
|
src: z.string().optional().describe("path or URL to video JSON file (stream tree or scene-based)"),
|
|
187
185
|
volume: z.number().min(0).max(1).default(1),
|
|
188
186
|
children: z.array(z.lazy((): z.ZodTypeAny => stream)).default(() => []),
|
|
189
|
-
actions: z.array(action).min(1).default(() => [action.parse({})]),
|
|
190
187
|
imports: z.string().optional().describe("component bundle URL for sub-video (set by engine)"),
|
|
191
188
|
});
|
|
192
189
|
export type Include = z.infer<typeof include>;
|
|
@@ -234,7 +231,7 @@ export const mapStream = base.extend({
|
|
|
234
231
|
mapType: z.enum(["roadmap", "satellite", "hybrid", "terrain"]).default("roadmap").describe("Google Maps style"),
|
|
235
232
|
travelMode: z.enum(["DRIVING", "WALKING", "BICYCLING", "TRANSIT"]).default("DRIVING").describe("Directions API travel mode"),
|
|
236
233
|
routeMarker: z.string().default("🚗").describe("emoji/character for the animated traveling marker"),
|
|
237
|
-
|
|
234
|
+
googleMapsApiKey: z.string().optional().describe("injected by compiler from GOOGLE_MAPS_API_KEY env var"),
|
|
238
235
|
});
|
|
239
236
|
export type MapStream = z.infer<typeof mapStream>;
|
|
240
237
|
|
package/src/types/Audio.tsx
CHANGED
|
@@ -13,44 +13,36 @@ export function AudioLeaf({ stream }: { stream: Audio }) {
|
|
|
13
13
|
const { fps } = useVideoConfig();
|
|
14
14
|
const environment = useRemotionEnvironment();
|
|
15
15
|
const ctx = React.useContext(AudioContext);
|
|
16
|
-
const
|
|
16
|
+
const start = stream.start ?? 0;
|
|
17
|
+
const end = stream.end ?? start + (stream.duration ?? 1);
|
|
18
|
+
const totalDur = stream.durationInSeconds ?? end;
|
|
17
19
|
useFrameEvents(stream.on, Math.max(1, Math.floor(totalDur * fps)));
|
|
18
20
|
if (!stream.src) return null;
|
|
19
21
|
if (environment.isStudio) return null;
|
|
20
22
|
|
|
21
23
|
const resolvedSrc = resolveAudioSrc(stream.src);
|
|
22
|
-
|
|
24
|
+
const startFrom = stream.startFrom ?? 0;
|
|
25
|
+
const endAt = stream.endAt ?? totalDur;
|
|
26
|
+
const volume = stream.volume ?? 1;
|
|
27
|
+
const playbackRate = stream.loop ? 1 : toPlaybackRate((endAt - startFrom) / (end - start));
|
|
23
28
|
return (
|
|
24
|
-
|
|
25
|
-
{stream.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
src={resolvedSrc}
|
|
43
|
-
startFrom={Math.floor(startFrom * fps)}
|
|
44
|
-
endAt={Math.floor(startFrom * fps) + Math.floor(((endAt - startFrom) * fps) / playbackRate)}
|
|
45
|
-
muted={volume === 0 || !!ctx?.foreground}
|
|
46
|
-
volume={volume}
|
|
47
|
-
loop={(a.loop ?? 1) > 1}
|
|
48
|
-
playbackRate={playbackRate}
|
|
49
|
-
showInTimeline={false}
|
|
50
|
-
/>
|
|
51
|
-
</Sequence>
|
|
52
|
-
);
|
|
53
|
-
})}
|
|
54
|
-
</>
|
|
29
|
+
<Sequence
|
|
30
|
+
name={stream.src ?? "audio"}
|
|
31
|
+
durationInFrames={Math.max(1, Math.floor(fps * (end - start)))}
|
|
32
|
+
from={Math.floor(fps * start)}
|
|
33
|
+
layout="none"
|
|
34
|
+
showInTimeline={false}
|
|
35
|
+
>
|
|
36
|
+
<RemotionAudio
|
|
37
|
+
src={resolvedSrc}
|
|
38
|
+
startFrom={Math.floor(startFrom * fps)}
|
|
39
|
+
endAt={Math.floor(startFrom * fps) + Math.floor(((endAt - startFrom) * fps) / playbackRate)}
|
|
40
|
+
muted={volume === 0 || !!ctx?.foreground}
|
|
41
|
+
volume={volume}
|
|
42
|
+
loop={(stream.loop ?? 1) > 1}
|
|
43
|
+
playbackRate={playbackRate}
|
|
44
|
+
showInTimeline={false}
|
|
45
|
+
/>
|
|
46
|
+
</Sequence>
|
|
55
47
|
);
|
|
56
48
|
}
|
package/src/types/Component.tsx
CHANGED
|
@@ -74,31 +74,25 @@ export function ComponentLeaf({ stream }: { stream: Component }) {
|
|
|
74
74
|
|
|
75
75
|
if (!stream.jsx) return null;
|
|
76
76
|
|
|
77
|
+
const start = stream.start ?? 0;
|
|
78
|
+
const end = stream.end ?? start + (stream.duration ?? 1);
|
|
79
|
+
const durFrames = Math.max(1, Math.floor(fps * (end - start)));
|
|
80
|
+
|
|
77
81
|
return (
|
|
78
|
-
|
|
79
|
-
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
components={components}
|
|
93
|
-
data={bindings}
|
|
94
|
-
action={a}
|
|
95
|
-
durFrames={durFrames}
|
|
96
|
-
on={stream.on}
|
|
97
|
-
/>
|
|
98
|
-
</Sequence>
|
|
99
|
-
);
|
|
100
|
-
})}
|
|
101
|
-
</>
|
|
82
|
+
<Sequence
|
|
83
|
+
durationInFrames={durFrames}
|
|
84
|
+
from={Math.floor(fps * start)}
|
|
85
|
+
layout="none"
|
|
86
|
+
>
|
|
87
|
+
<EventAwareComponent
|
|
88
|
+
jsx={stream.jsx}
|
|
89
|
+
components={components}
|
|
90
|
+
data={bindings}
|
|
91
|
+
action={{ start, end }}
|
|
92
|
+
durFrames={durFrames}
|
|
93
|
+
on={stream.on}
|
|
94
|
+
/>
|
|
95
|
+
</Sequence>
|
|
102
96
|
);
|
|
103
97
|
}
|
|
104
98
|
|
package/src/types/Effect.tsx
CHANGED
|
@@ -23,57 +23,49 @@ export function EffectWrapper({
|
|
|
23
23
|
}) {
|
|
24
24
|
const frame = useCurrentFrame();
|
|
25
25
|
const { fps, width, height } = useVideoConfig();
|
|
26
|
-
const
|
|
26
|
+
const startSec = stream.start ?? 0;
|
|
27
|
+
const endSec = stream.end ?? startSec + (stream.duration ?? 1);
|
|
28
|
+
const totalDur = stream.durationInSeconds ?? endSec;
|
|
27
29
|
useFrameEvents(stream.on, Math.max(1, Math.floor(totalDur * fps)));
|
|
28
30
|
|
|
29
|
-
const actions = stream.actions ?? [];
|
|
30
|
-
|
|
31
31
|
const styles = React.useMemo(() => {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const end = Math.ceil(action.end * fps);
|
|
37
|
-
const durationInFrames = end - start;
|
|
38
|
-
if (durationInFrames <= 0) continue;
|
|
32
|
+
const start = Math.ceil(startSec * fps);
|
|
33
|
+
const end = Math.ceil(endSec * fps);
|
|
34
|
+
const durationInFrames = end - start;
|
|
35
|
+
if (durationInFrames <= 0) return [] as Record<string, string>[];
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
const animation = stream.animation;
|
|
38
|
+
const timingFn = stream.animationTimingFunction;
|
|
39
|
+
const iterCount = stream.animationIterationCount ?? 1;
|
|
40
|
+
const style = (cssJS(stream.style) ?? {}) as Record<string, string>;
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
42
|
+
// Handle iteration count: loop the animation within the span
|
|
43
|
+
let currentFrame = frame;
|
|
44
|
+
if (iterCount > 0 && durationInFrames > 0) {
|
|
45
|
+
const iteration = Math.floor((frame - start) / durationInFrames);
|
|
46
|
+
if (iteration < iterCount) {
|
|
47
|
+
currentFrame = start + ((frame - start) % durationInFrames);
|
|
52
48
|
}
|
|
49
|
+
}
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (animation) {
|
|
58
|
-
const config = resolveAnimation(animation, stream.customKeyframes);
|
|
59
|
-
if (config) {
|
|
60
|
-
const animStyle = interpolateKeyframes(config, actionFrame, {
|
|
61
|
-
fps,
|
|
62
|
-
durationInSeconds: durationInFrames / fps,
|
|
63
|
-
timingFunction: timingFn,
|
|
64
|
-
});
|
|
65
|
-
if (animStyle) Object.assign(style, animStyle);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
51
|
+
if (currentFrame >= start && currentFrame < end) {
|
|
52
|
+
const actionFrame = currentFrame - start;
|
|
68
53
|
|
|
69
|
-
|
|
70
|
-
|
|
54
|
+
if (animation) {
|
|
55
|
+
const config = resolveAnimation(animation, stream.customKeyframes);
|
|
56
|
+
if (config) {
|
|
57
|
+
const animStyle = interpolateKeyframes(config, actionFrame, {
|
|
58
|
+
fps,
|
|
59
|
+
durationInSeconds: durationInFrames / fps,
|
|
60
|
+
timingFunction: timingFn,
|
|
61
|
+
});
|
|
62
|
+
if (animStyle) Object.assign(style, animStyle);
|
|
71
63
|
}
|
|
72
64
|
}
|
|
73
65
|
}
|
|
74
66
|
|
|
75
|
-
return
|
|
76
|
-
}, [frame, fps,
|
|
67
|
+
return Object.keys(style).length > 0 ? [style] : [];
|
|
68
|
+
}, [frame, fps, startSec, endSec, stream.animation, stream.animationTimingFunction, stream.animationIterationCount, stream.customKeyframes, stream.style]);
|
|
77
69
|
|
|
78
70
|
if (styles.length === 0) return <>{children}</>;
|
|
79
71
|
|
package/src/types/Image.tsx
CHANGED
|
@@ -12,40 +12,33 @@ function resolveImageSrc(src: string): string {
|
|
|
12
12
|
|
|
13
13
|
export function ImageLeaf({ stream }: { stream: Image }) {
|
|
14
14
|
const { fps } = useVideoConfig();
|
|
15
|
-
const
|
|
15
|
+
const start = stream.start ?? 0;
|
|
16
|
+
const end = stream.end ?? start + (stream.duration ?? 1);
|
|
17
|
+
const totalDur = stream.durationInSeconds ?? end;
|
|
16
18
|
useFrameEvents(stream.on, Math.max(1, Math.floor(totalDur * fps)));
|
|
17
19
|
if (!stream.src) return null;
|
|
18
20
|
const resolvedSrc = resolveImageSrc(stream.src);
|
|
19
21
|
|
|
20
22
|
return (
|
|
21
|
-
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
e.stopPropagation();
|
|
42
|
-
return false;
|
|
43
|
-
}}
|
|
44
|
-
/>
|
|
45
|
-
</FrameSyncStyle>
|
|
46
|
-
</Sequence>
|
|
47
|
-
);
|
|
48
|
-
})}
|
|
49
|
-
</>
|
|
23
|
+
<Sequence
|
|
24
|
+
durationInFrames={Math.max(1, Math.floor(fps * (end - start)))}
|
|
25
|
+
from={Math.floor(fps * start)}
|
|
26
|
+
layout="none"
|
|
27
|
+
>
|
|
28
|
+
<FrameSyncStyle style={cssJS(stream.style)}>
|
|
29
|
+
<Img
|
|
30
|
+
src={resolvedSrc}
|
|
31
|
+
style={{
|
|
32
|
+
width: "100%",
|
|
33
|
+
height: "100%",
|
|
34
|
+
objectFit: stream.fit,
|
|
35
|
+
}}
|
|
36
|
+
onDragStart={(e) => {
|
|
37
|
+
e.stopPropagation();
|
|
38
|
+
return false;
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
</FrameSyncStyle>
|
|
42
|
+
</Sequence>
|
|
50
43
|
);
|
|
51
44
|
}
|
package/src/types/Include.tsx
CHANGED
|
@@ -104,17 +104,20 @@ function useSubVideoRegistry(importsUrl: string | undefined):
|
|
|
104
104
|
* registered by the sub-video are available during its rendering.
|
|
105
105
|
*
|
|
106
106
|
* Usage:
|
|
107
|
-
* { type: "include", src: "./path/to/video.json",
|
|
107
|
+
* { type: "include", src: "./path/to/video.json", start: 0, end: 5 }
|
|
108
108
|
*/
|
|
109
109
|
export function IncludeLeaf({ stream }: { stream: IncludeStream }) {
|
|
110
110
|
const { fps: parentFps, width: parentWidth, height: parentHeight } = useVideoConfig();
|
|
111
111
|
const parentCompose = React.useContext(ComposeContext);
|
|
112
112
|
const { Container } = parentCompose;
|
|
113
113
|
const parentAudio = React.useContext(AudioContext);
|
|
114
|
-
const
|
|
114
|
+
const start = stream.start ?? 0;
|
|
115
|
+
const end = stream.end ?? (stream.durationInSeconds ?? (start + (stream.duration ?? 1)));
|
|
116
|
+
const totalDur = stream.durationInSeconds ?? end;
|
|
115
117
|
useFrameEvents(stream.on, Math.max(1, Math.floor(totalDur * parentFps)));
|
|
116
118
|
|
|
117
|
-
|
|
119
|
+
const hasContent = !!stream.src || (stream.children?.length ?? 0) > 0;
|
|
120
|
+
if (!hasContent) return null;
|
|
118
121
|
|
|
119
122
|
// ── External JSON loading ─────────────────────────────────────────────
|
|
120
123
|
const [externalData, setExternalData] = React.useState<unknown | null>(null);
|
|
@@ -296,87 +299,78 @@ export function IncludeLeaf({ stream }: { stream: IncludeStream }) {
|
|
|
296
299
|
);
|
|
297
300
|
}, [externalData, loadError, parentFps, parentWidth, parentHeight]);
|
|
298
301
|
|
|
299
|
-
// ──
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
type="include"
|
|
319
|
-
style={{
|
|
320
|
-
...cssJS(stream.style) as React.CSSProperties,
|
|
321
|
-
width: parentWidth,
|
|
322
|
-
height: parentHeight,
|
|
323
|
-
overflow: "hidden",
|
|
324
|
-
position: "relative",
|
|
325
|
-
}}
|
|
326
|
-
className={`include ${toClassName(stream.id ?? "")}`}
|
|
327
|
-
>
|
|
328
|
-
<FolderLeaf stream={stream as any} />
|
|
329
|
-
</Container>
|
|
330
|
-
</Sequence>
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// ── External src rendering ─────────────────────────────────────
|
|
335
|
-
return (
|
|
336
|
-
<Sequence
|
|
337
|
-
key={a.id}
|
|
338
|
-
durationInFrames={durFrames}
|
|
339
|
-
from={Math.floor(parentFps * start)}
|
|
340
|
-
layout="none"
|
|
341
|
-
showInTimeline={false}
|
|
302
|
+
// ── Single-span rendering ────────────────────────────────────────
|
|
303
|
+
const dur = Math.max(0.1, end - start);
|
|
304
|
+
const durFrames = Math.max(1, Math.floor(parentFps * dur));
|
|
305
|
+
|
|
306
|
+
// ── Inline children fallback (legacy) ─────────────────────────
|
|
307
|
+
let innerNode: React.ReactNode;
|
|
308
|
+
if (!stream.src && stream.children?.length) {
|
|
309
|
+
innerNode = (
|
|
310
|
+
<Container
|
|
311
|
+
id={stream.id}
|
|
312
|
+
type="include"
|
|
313
|
+
style={{
|
|
314
|
+
...cssJS(stream.style) as React.CSSProperties,
|
|
315
|
+
width: parentWidth,
|
|
316
|
+
height: parentHeight,
|
|
317
|
+
overflow: "hidden",
|
|
318
|
+
position: "relative",
|
|
319
|
+
}}
|
|
320
|
+
className={`include ${toClassName(stream.id ?? "")}`}
|
|
342
321
|
>
|
|
343
|
-
{
|
|
344
|
-
|
|
345
|
-
style={{
|
|
346
|
-
width: parentWidth,
|
|
347
|
-
height: parentHeight,
|
|
348
|
-
overflow: "hidden",
|
|
349
|
-
position: "relative",
|
|
350
|
-
}}
|
|
351
|
-
>
|
|
352
|
-
{renderExternalContent()}
|
|
353
|
-
</div>
|
|
354
|
-
) : (
|
|
355
|
-
<div
|
|
356
|
-
style={{
|
|
357
|
-
width: parentWidth,
|
|
358
|
-
height: parentHeight,
|
|
359
|
-
display: "flex",
|
|
360
|
-
alignItems: "center",
|
|
361
|
-
justifyContent: "center",
|
|
362
|
-
color: "#666",
|
|
363
|
-
fontSize: 20,
|
|
364
|
-
fontFamily: "monospace",
|
|
365
|
-
}}
|
|
366
|
-
>
|
|
367
|
-
Loading… {stream.src}
|
|
368
|
-
</div>
|
|
369
|
-
)}
|
|
370
|
-
</Sequence>
|
|
322
|
+
<FolderLeaf stream={stream as any} />
|
|
323
|
+
</Container>
|
|
371
324
|
);
|
|
372
|
-
}
|
|
325
|
+
} else if (externalData || loadError) {
|
|
326
|
+
innerNode = (
|
|
327
|
+
<div
|
|
328
|
+
style={{
|
|
329
|
+
width: parentWidth,
|
|
330
|
+
height: parentHeight,
|
|
331
|
+
overflow: "hidden",
|
|
332
|
+
position: "relative",
|
|
333
|
+
}}
|
|
334
|
+
>
|
|
335
|
+
{renderExternalContent()}
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
} else {
|
|
339
|
+
innerNode = (
|
|
340
|
+
<div
|
|
341
|
+
style={{
|
|
342
|
+
width: parentWidth,
|
|
343
|
+
height: parentHeight,
|
|
344
|
+
display: "flex",
|
|
345
|
+
alignItems: "center",
|
|
346
|
+
justifyContent: "center",
|
|
347
|
+
color: "#666",
|
|
348
|
+
fontSize: 20,
|
|
349
|
+
fontFamily: "monospace",
|
|
350
|
+
}}
|
|
351
|
+
>
|
|
352
|
+
Loading… {stream.src}
|
|
353
|
+
</div>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const sequence = (
|
|
358
|
+
<Sequence
|
|
359
|
+
durationInFrames={durFrames}
|
|
360
|
+
from={Math.floor(parentFps * start)}
|
|
361
|
+
layout="none"
|
|
362
|
+
showInTimeline={false}
|
|
363
|
+
>
|
|
364
|
+
{innerNode}
|
|
365
|
+
</Sequence>
|
|
366
|
+
);
|
|
373
367
|
|
|
374
368
|
// ── Determine if we need a nested ComposeContext ───────────────────
|
|
375
369
|
const needsNestedContext = subRegistry !== null && subRegistry !== parentCompose.components;
|
|
376
370
|
|
|
377
371
|
const inner = (
|
|
378
372
|
<AudioContext.Provider value={audioCtx as any}>
|
|
379
|
-
{
|
|
373
|
+
{sequence}
|
|
380
374
|
</AudioContext.Provider>
|
|
381
375
|
);
|
|
382
376
|
|