Skip to content

Commit bdf23c8

Browse files
Merge pull request #1417 from CapSoftware/cursor/optimize-editor-performance-for-large-videos-claude-4.5-opus-high-thinking-fb23
Optimize editor performance for large videos
2 parents 87ffbc2 + d654add commit bdf23c8

File tree

9 files changed

+642
-376
lines changed

9 files changed

+642
-376
lines changed

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ dbg_macro = "deny"
7070
let_underscore_future = "deny"
7171
unchecked_duration_subtraction = "deny"
7272
collapsible_if = "deny"
73-
manual_is_multiple_of = "deny"
7473
clone_on_copy = "deny"
7574
redundant_closure = "deny"
7675
ptr_arg = "deny"

apps/desktop/core

54.8 MB
Binary file not shown.

apps/desktop/src/routes/editor/Editor.tsx

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Button } from "@cap/ui-solid";
22
import { NumberField } from "@kobalte/core/number-field";
33
import { trackDeep } from "@solid-primitives/deep";
4-
import { throttle } from "@solid-primitives/scheduled";
4+
import { debounce, throttle } from "@solid-primitives/scheduled";
55
import { makePersisted } from "@solid-primitives/storage";
66
import { createMutation } from "@tanstack/solid-query";
77
import { convertFileSrc } from "@tauri-apps/api/core";
@@ -13,6 +13,7 @@ import {
1313
createSignal,
1414
Match,
1515
on,
16+
onCleanup,
1617
Show,
1718
Switch,
1819
} from "solid-js";
@@ -85,19 +86,49 @@ function Inner() {
8586
const { project, editorState, setEditorState } = useEditorContext();
8687

8788
createTauriEventListener(events.editorStateChanged, (payload) => {
88-
renderFrame.clear();
89+
renderFrameThrottled.clear();
8990
setEditorState("playbackTime", payload.playhead_position / FPS);
9091
});
9192

92-
const renderFrame = throttle((time: number) => {
93-
if (!editorState.playing) {
94-
events.renderFrameEvent.emit({
95-
frame_number: Math.max(Math.floor(time * FPS), 0),
96-
fps: FPS,
97-
resolution_base: OUTPUT_SIZE,
98-
});
93+
let rafId: number | null = null;
94+
let pendingFrameTime: number | null = null;
95+
96+
const emitFrame = (time: number) => {
97+
events.renderFrameEvent.emit({
98+
frame_number: Math.max(Math.floor(time * FPS), 0),
99+
fps: FPS,
100+
resolution_base: OUTPUT_SIZE,
101+
});
102+
};
103+
104+
const renderFrameThrottled = throttle((time: number) => {
105+
if (editorState.playing) return;
106+
107+
if (rafId !== null) {
108+
pendingFrameTime = time;
109+
return;
110+
}
111+
112+
rafId = requestAnimationFrame(() => {
113+
rafId = null;
114+
const frameTime = pendingFrameTime ?? time;
115+
pendingFrameTime = null;
116+
emitFrame(frameTime);
117+
});
118+
}, 1000 / 30);
119+
120+
const renderFrameDebounced = debounce((time: number) => {
121+
if (editorState.playing) return;
122+
emitFrame(time);
123+
}, 50);
124+
125+
onCleanup(() => {
126+
if (rafId !== null) {
127+
cancelAnimationFrame(rafId);
99128
}
100-
}, 1000 / FPS);
129+
renderFrameThrottled.clear();
130+
renderFrameDebounced.clear();
131+
});
101132

102133
const frameNumberToRender = createMemo(() => {
103134
const preview = editorState.previewTime;
@@ -108,14 +139,23 @@ function Inner() {
108139
createEffect(
109140
on(frameNumberToRender, (number) => {
110141
if (editorState.playing) return;
111-
renderFrame(number);
142+
renderFrameThrottled(number);
112143
}),
113144
);
114145

146+
let lastProjectUpdateTime = 0;
115147
createEffect(
116148
on(
117149
() => trackDeep(project),
118-
() => renderFrame(editorState.playbackTime),
150+
() => {
151+
const now = performance.now();
152+
if (now - lastProjectUpdateTime < 100) {
153+
renderFrameDebounced(editorState.playbackTime);
154+
} else {
155+
renderFrameThrottled(editorState.playbackTime);
156+
}
157+
lastProjectUpdateTime = now;
158+
},
119159
),
120160
);
121161

apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
createRoot,
1111
createSignal,
1212
For,
13+
Index,
1314
Match,
1415
onCleanup,
1516
Show,
@@ -188,7 +189,7 @@ export function ClipTrack(
188189
systemAudioWaveforms,
189190
} = useEditorContext();
190191

191-
const { secsPerPixel, duration } = useTimelineContext();
192+
const { secsPerPixel, duration, isSegmentVisible } = useTimelineContext();
192193

193194
const segments = (): Array<TimelineSegment> =>
194195
project.timeline?.segments ?? [{ start: 0, end: duration(), timescale: 1 }];
@@ -204,6 +205,21 @@ export function ClipTrack(
204205
return offsets;
205206
});
206207

208+
const visibleSegmentIndices = createMemo(() => {
209+
const segs = segments();
210+
const offsets = segmentOffsets();
211+
const visible: number[] = [];
212+
for (let i = 0; i < segs.length; i++) {
213+
const seg = segs[i];
214+
const segStart = offsets[i];
215+
const segEnd = segStart + (seg.end - seg.start) / seg.timescale;
216+
if (isSegmentVisible(segStart, segEnd)) {
217+
visible.push(i);
218+
}
219+
}
220+
return visible;
221+
});
222+
207223
function onHandleReleased() {
208224
const { transform } = editorState.timeline;
209225

@@ -223,8 +239,10 @@ export function ClipTrack(
223239
onMouseEnter={() => setEditorState("timeline", "hoveredTrack", "clip")}
224240
onMouseLeave={() => setEditorState("timeline", "hoveredTrack", null)}
225241
>
226-
<For each={segments()}>
227-
{(segment, i) => {
242+
<Index each={visibleSegmentIndices()}>
243+
{(segmentIndex) => {
244+
const i = segmentIndex;
245+
const segment = () => segments()[i()];
228246
const [startHandleDrag, setStartHandleDrag] = createSignal<null | {
229247
offset: number;
230248
initialStart: number;
@@ -235,14 +253,15 @@ export function ClipTrack(
235253
const relativeSegment = createMemo(() => {
236254
const ds = startHandleDrag();
237255
const offset = ds?.offset ?? 0;
256+
const seg = segment();
238257

239258
return {
240259
start: Math.max(prevDuration() + offset, 0),
241260
end:
242261
prevDuration() +
243-
(offset + (segment.end - segment.start)) / segment.timescale,
244-
timescale: segment.timescale,
245-
recordingSegment: segment.recordingSegment,
262+
(offset + (seg.end - seg.start)) / seg.timescale,
263+
timescale: seg.timescale,
264+
recordingSegment: seg.recordingSegment,
246265
};
247266
});
248267

@@ -269,9 +288,10 @@ export function ClipTrack(
269288
const isSelected = createMemo(() => {
270289
const selection = editorState.timeline.selection;
271290
if (!selection || selection.type !== "clip") return false;
291+
const seg = segment();
272292

273293
const segmentIndex = project.timeline?.segments?.findIndex(
274-
(s) => s.start === segment.start && s.end === segment.end,
294+
(s) => s.start === seg.start && s.end === seg.end,
275295
);
276296

277297
if (segmentIndex === undefined || segmentIndex === -1) return false;
@@ -283,7 +303,7 @@ export function ClipTrack(
283303
if (project.audio.micVolumeDb && project.audio.micVolumeDb < -30)
284304
return;
285305

286-
const idx = segment.recordingSegment ?? i();
306+
const idx = segment().recordingSegment ?? i();
287307
return micWaveforms()?.[idx] ?? [];
288308
};
289309

@@ -294,7 +314,7 @@ export function ClipTrack(
294314
)
295315
return;
296316

297-
const idx = segment.recordingSegment ?? i();
317+
const idx = segment().recordingSegment ?? i();
298318
return systemAudioWaveforms()?.[idx] ?? [];
299319
};
300320

@@ -401,8 +421,9 @@ export function ClipTrack(
401421
if (editorState.timeline.interactMode === "split") {
402422
const rect = e.currentTarget.getBoundingClientRect();
403423
const fraction = (e.clientX - rect.left) / rect.width;
424+
const seg = segment();
404425

405-
const splitTime = fraction * (segment.end - segment.start);
426+
const splitTime = fraction * (seg.end - seg.start);
406427

407428
projectActions.splitClipSegment(prevDuration() + splitTime);
408429
} else {
@@ -486,41 +507,41 @@ export function ClipTrack(
486507
}
487508
}}
488509
>
489-
{segment.timescale === 1 && (
510+
{segment().timescale === 1 && (
490511
<WaveformCanvas
491512
micWaveform={micWaveform()}
492513
systemWaveform={systemAudioWaveform()}
493-
segment={segment}
514+
segment={segment()}
494515
/>
495516
)}
496517

497-
<Markings segment={segment} prevDuration={prevDuration()} />
518+
<Markings segment={segment()} prevDuration={prevDuration()} />
498519

499520
<SegmentHandle
500521
position="start"
501522
class="opacity-0 group-hover:opacity-100"
502523
onMouseDown={(downEvent) => {
503524
if (split()) return;
525+
const seg = segment();
504526

505-
const initialStart = segment.start;
527+
const initialStart = seg.start;
506528
setStartHandleDrag({
507529
offset: 0,
508530
initialStart,
509531
});
510532

511533
const maxSegmentDuration =
512534
editorInstance.recordings.segments[
513-
segment.recordingSegment ?? 0
535+
seg.recordingSegment ?? 0
514536
].display.duration;
515537

516538
const availableTimelineDuration =
517539
editorInstance.recordingDuration -
518540
segments().reduce(
519-
(acc, segment, segmentI) =>
541+
(acc, s, segmentI) =>
520542
segmentI === i()
521543
? acc
522-
: acc +
523-
(segment.end - segment.start) / segment.timescale,
544+
: acc + (s.end - s.start) / s.timescale,
524545
0,
525546
);
526547

@@ -532,24 +553,23 @@ export function ClipTrack(
532553
const prevSegment = segments()[i() - 1];
533554
const prevSegmentIsSameClip =
534555
prevSegment?.recordingSegment !== undefined
535-
? prevSegment.recordingSegment ===
536-
segment.recordingSegment
556+
? prevSegment.recordingSegment === seg.recordingSegment
537557
: false;
538558

539559
function update(event: MouseEvent) {
540560
const newStart =
541561
initialStart +
542562
(event.clientX - downEvent.clientX) *
543563
secsPerPixel() *
544-
segment.timescale;
564+
seg.timescale;
545565

546566
const clampedStart = Math.min(
547567
Math.max(
548568
newStart,
549569
prevSegmentIsSameClip ? prevSegment.end : 0,
550-
segment.end - maxDuration,
570+
seg.end - maxDuration,
551571
),
552-
segment.end - 1,
572+
seg.end - 1,
553573
);
554574

555575
setStartHandleDrag({
@@ -590,22 +610,23 @@ export function ClipTrack(
590610
<SegmentContent class="relative justify-center items-center">
591611
{(() => {
592612
const ctx = useSegmentContext();
613+
const seg = segment();
593614

594615
return (
595616
<Show when={ctx.width() > 100}>
596617
<div class="flex flex-col gap-1 justify-center items-center text-xs whitespace-nowrap text-gray-12">
597618
<span class="text-white/70">
598619
{hasMultipleRecordingSegments()
599-
? `Clip ${segment.recordingSegment}`
620+
? `Clip ${seg.recordingSegment}`
600621
: "Clip"}
601622
</span>
602623
<div class="flex gap-1 items-center text-md dark:text-gray-12 text-gray-1">
603624
<IconLucideClock class="size-3.5" />{" "}
604-
{formatTime(segment.end - segment.start)}
605-
<Show when={segment.timescale !== 1}>
625+
{formatTime(seg.end - seg.start)}
626+
<Show when={seg.timescale !== 1}>
606627
<div class="w-0.5" />
607628
<IconLucideFastForward class="size-3" />
608-
{segment.timescale}x
629+
{seg.timescale}x
609630
</Show>
610631
</div>
611632
</div>
@@ -617,37 +638,36 @@ export function ClipTrack(
617638
position="end"
618639
class="opacity-0 group-hover:opacity-100"
619640
onMouseDown={(downEvent) => {
620-
const end = segment.end;
641+
const seg = segment();
642+
const end = seg.end;
621643

622644
if (split()) return;
623645
const maxSegmentDuration =
624646
editorInstance.recordings.segments[
625-
segment.recordingSegment ?? 0
647+
seg.recordingSegment ?? 0
626648
].display.duration;
627649

628650
const availableTimelineDuration =
629651
editorInstance.recordingDuration -
630652
segments().reduce(
631-
(acc, segment, segmentI) =>
653+
(acc, s, segmentI) =>
632654
segmentI === i()
633655
? acc
634-
: acc +
635-
(segment.end - segment.start) / segment.timescale,
656+
: acc + (s.end - s.start) / s.timescale,
636657
0,
637658
);
638659

639660
const nextSegment = segments()[i() + 1];
640661
const nextSegmentIsSameClip =
641662
nextSegment?.recordingSegment !== undefined
642-
? nextSegment.recordingSegment ===
643-
segment.recordingSegment
663+
? nextSegment.recordingSegment === seg.recordingSegment
644664
: false;
645665

646666
function update(event: MouseEvent) {
647667
const deltaRecorded =
648668
(event.clientX - downEvent.clientX) *
649669
secsPerPixel() *
650-
segment.timescale;
670+
seg.timescale;
651671
const newEnd = end + deltaRecorded;
652672

653673
setProject(
@@ -658,13 +678,12 @@ export function ClipTrack(
658678
Math.max(
659679
Math.min(
660680
newEnd,
661-
// availableTimelineDuration is in timeline seconds; convert to recorded seconds
662-
end + availableTimelineDuration * segment.timescale,
681+
end + availableTimelineDuration * seg.timescale,
663682
nextSegmentIsSameClip
664683
? nextSegment.start
665684
: maxSegmentDuration,
666685
),
667-
segment.start + 1,
686+
seg.start + 1,
668687
),
669688
);
670689
}
@@ -737,7 +756,7 @@ export function ClipTrack(
737756
</>
738757
);
739758
}}
740-
</For>
759+
</Index>
741760
</TrackRoot>
742761
);
743762
}

0 commit comments

Comments
 (0)