Includes: - Extended caption styling (font, shadow, dimmed color, bg toggle) - Media server, subtitle downloader, VTT parser, processing modal - Waveform tiers, thumbnail/timeline improvements, transport controls - Hybrid download model, dependency management, clip export enhancements - 21 chat summaries, 2 implementation plans, 2 design specs Co-authored-by: Cursor <cursoragent@cursor.com>
4.6 KiB
4.6 KiB
Performance, Captions, Clip UX, and Resizable Panels
Date: 2026-09-21 14:39
Task: Fix timeline performance, caption positioning, clip creation workflow, resizable panels, and deselect UX
Changes Made
1. Performance Optimization (3 files)
src/lib/components/VideoPlayer.svelte:
- Throttled
handleTimeUpdateto ~15fps (66ms interval) usingperformance.now()check. Previously everyontimeupdateevent triggered a reactive cascade throughsession.currentTime.
src/lib/components/Timeline.svelte:
- In-flight guard: Added
detailFetchInFlightflag to prevent overlapping waveform range extraction subprocess calls. Only one ffmpeg call runs at a time. - Increased debounce: Waveform detail fetch debounce increased from 300ms to 500ms.
- Playhead-only redraws: Split the monolithic redraw
$effectinto two:- Structural changes (clips, zoom, waveform data, thumbnails) trigger a full
drawMainCanvas()which saves anImageDatasnapshot. session.currentTimechanges trigger a lightweightdrawPlayheadOnly()that restores the snapshot and draws only the playhead — avoiding the expensive thumbnail/waveform/clip rendering on every time update.
- Structural changes (clips, zoom, waveform data, thumbnails) trigger a full
- Thumbnail load callback: Registered a callback via
setThumbnailLoadCallbackso thumbnail image loads trigger a targeted redraw instead of relying on the next reactive cycle.
src/lib/timeline/thumbnailRenderer.ts:
- Added
pendingLoadsSet to prevent creating duplicateImageobjects for the same path across rapid redraws. - Added
setThumbnailLoadCallbackAPI so the Timeline can request a redraw when thumbnails finish loading asynchronously. - Image
onerrorhandler cleans up the pending state.
2. Caption Positioning (1 file)
src/lib/components/VideoPlayer.svelte:
- Added
:global(video::cue)CSS to center captions at the bottom of the video with a semi-transparent black background, white text, and 16px font size. Used:global()to bypass Svelte scoping since::cueis a browser-level pseudo-element. - Added
position: relativeto the<video>element to scope cue rendering.
3. Pending In-Point Preservation (1 file)
src/lib/stores/clips.svelte.ts:
- Modified
selectClip()to only clearpendingInPointwhenid !== null(selecting a specific clip). Whenid === null(deselecting via empty timeline click), the pending in-point is preserved. This fixes the workflow: press I → click timeline to seek → press O to complete the clip.
4. Resizable Panels (3 files)
src/App.svelte:
- Wrapped
<Timeline />and<ClipList />in a new.timeline-clip-areacontainer with flex column layout. - Added a 5px
.resize-handledivider between them withcursor: ns-resizeand accent color on hover/active. - Implemented
handleResizeStart/Move/Endwithmousedown/mousemove/mouseupon<svelte:window>to drag-resize. Timeline height is clamped between 80px and (total - 60px). timelineHeightstored in local$state(defaults to 180px).
src/lib/components/Timeline.svelte:
- Changed
.timeline-containerfromheight: 160pxtoflex: 1; min-height: 80pxso it fills the space given by the parent. .timeline-wrapperset toheight: 100%.
src/lib/components/ClipList.svelte:
- Changed from
max-height: 200pxtoheight: 100%; min-height: 40pxwithbox-sizing: border-box.
5. Deselect UX (1 file)
src/lib/components/ClipList.svelte:
- Added
handleContainerClickon the.clip-listdiv that callsselectClip(null)when clicking empty space (not a child clip row). - Replaced the single "Export All" button with an
.action-buttonsrow containing a conditional "Deselect" button (shown only when a clip is selected) alongside "Export All". - Changed
role="button"torole="option"on clip rows androle="listbox"on the container for better semantics.
Build Status
- Frontend: builds cleanly (0 errors)
- Rust: compiles with only pre-existing unused variant warnings
Lessons Learned
- Svelte's scoped CSS doesn't apply to
::cuepseudo-elements since they're browser-level shadow constructs —:global()wrapper is needed. - Canvas
getImageData/putImageDatais an effective technique for "snapshot and overlay" rendering patterns, avoiding expensive full redraws when only one layer (like a playhead) changes. ontimeupdatefires at varying rates across browsers/engines; throttling to a target frame rate prevents reactive cascades from dominating the UI thread.- The
pendingInPointclearing bug was a subtle consequence ofselectClip(null)being used for two different intents (deselecting a clip vs. resetting all clip state).