Skip to content

fix(preview, properties, export): timeline-zero effect cache, text-template properties tabs, and audio speed export filter chain - #546

Merged
theunavailableguy merged 19 commits into
masterfrom
fix/timeline-zero-effect-playback-freeze
Nov 11, 2026
Merged

theunavailableguy merged 19 commits into
masterfrom
fix/timeline-zero-effect-playback-freeze

Conversation

@AIEraDev

@AIEraDev AIEraDev commented Nov 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

Resolves timeline-zero effect cache invalidation, text-template properties tab rendering, and audio speed export filter parity:

  1. Timeline-Zero Effect Cache & Playback Freeze:
    • computeClipVersion tracks all visual effects, filters, adjustments, colorGrade, chromaKey, animations, and conform to prevent stale cache hits on t=0.
    • Non-video timelines bypass playback_lookahead_miss check so image and text timelines composite without stalling.
    • Fixed raster layer asset matching in native render session and guarded CPAL timeout in PlaybackClock.
  2. Text-Template Properties Tab Resolution (CLY-BUG-TEXT-001):
    • Fixed PropertyPanelHeader.tsx to include text-template clip kind in TEXT_TABS resolution. Text templates now reliably display Text and Animation tabs and mount TextStyleSection and TextTemplateOverlay.
  3. Audio Export Speed Remapping & Native Eligibility:
    • Added chained atempo filter complex generation in Rust audio_filter.rs for non-unity playback speeds and asetrate for pitch shifts.
    • Updated nativeTimelineExport.ts eligibility check so default preservePitch does not spuriously disqualify cut-only videos from native export.
    • Added reset controls for volume/fades in AudioSection and AudioBasicTab, wiring AudioFXSection.

Type

  • Bug fix (label: bugfix)
  • Feature
  • Refactor

Evidence Bundle (mandatory for preview/playback/audio/text/sticker changes)

Test Output

✓ src/components/editor/properties/__tests__/PropertyPanelHeader.test.tsx (6 tests) 116ms
✓ src/components/editor/properties/__tests__/AudioSection.test.tsx (8 tests) 225ms
✓ src/lib/export/__tests__/nativeTimelineExport.test.ts (7 tests) 4ms
✓ src/components/editor/preview/__tests__/ProgramPreview.renderLoop.test.ts (200 tests) 5.59s
✓ cargo test commands::export (11 tests passed; 0 failed)

Subsystem Contract Suites

  • replayBehavioralContract.test.ts (38/38 passed)
  • audioPreviewSyncContract.test.ts (27/27 passed)
  • previewStateResetContract.test.ts (29/29 passed)
  • textEditPreviewCycle.test.ts (26/26 passed)
    Total: 120/120 passed in 4.46s.

Negative Control (bug fixes only)

  • Applied PropertyPanelHeader.test.tsx to pre-fix code → Failed: expected "vi.fn()" to be called with ['text'], received [null]
  • Applied test to PR head → Passed (6/6 tests green)

Mutation Canary

  • Known-bad patches turn tests red (npm run verify:fast passed with 200 baseline render loop tests)

Architecture-First Delivery

  • Root cause and owning subsystem:
    • PropertyPanelHeader.tsx: tab resolution omitted text-template.
    • audio_filter.rs: FFmpeg audio filter graph lacked speed/tempo filter chains.
    • nativeTimelineExport.ts: audio.speed.preservePitch checked as unsupported setting on cut-only clips.
  • Single authoritative state and execution flow:
    • Properties dispatch via historyStore.dispatch / command pattern.
    • Native raster bridge prepares text textures for compositor preview and export.
  • Affected consumers: Editor properties panel, native GPU preview, FFmpeg export pipeline.
  • Focused regression coverage: Added PropertyPanelHeader.test.tsx (6 tests), updated nativeTimelineExport.test.ts and audio_filter.rs test suite.

- Render all 3 category grids (in/out/combo) always mounted, toggled
  via CSS (contents/hidden) so state is preserved across tab switches
- Add enabled prop to CategoryGrid and useMediaAnimationCategory so
  each category only fetches when its tab is first visited
- Track visitedTabs in MediaAnimationSection; in tab pre-seeded so it
  fetches on mount, out and combo fetch on first visit only
- Fix activePresetId and handleSelectPreset to be per-category instead
  of derived from activeTab — fixes combo showing in preset data
- Add client-side category filter in useMediaAnimationCategory to drop
  items whose raw.category does not match the requested category
- Key allPresets map by id@category to prevent cross-category stomping
- Fix button-in-button violation in PresetCard: outer wrapper changed
  from button to div with role=button and keyboard handler
…ly play

useMediaAnimationCatalogSnapshot was a plain function reading allPresets
once at mount time. When a user clicked a preset before the async fetch
completed, catalogSnapshot.presets was still empty, compileClipMotionKeyframes
received an empty catalog, found no preset, and wrote visualKeyframes: {}
to the clip — causing no animation during playback.

Fix: introduce a catalogListeners subscriber set. The fetch hook calls
notifyCatalogListeners() after writing new presets into allPresets.
useMediaAnimationCatalogSnapshot is now a real hook backed by
useSyncExternalStore so MediaAnimationSection re-renders with the
populated catalog the moment the fetch completes.
… keyframes

Three changes to make playback animate immediately after selecting a preset:

1. downloadManifest() now calls notifyCatalogListeners() after the manifest
   lands in the cache. This causes useMediaAnimationCatalogSnapshot to
   re-render with manifest-merged tracks, so the next compile sees real
   keyframe data.

2. handleDownload() always recompiles the clip after a successful download,
   removing the isApplied guard that prevented recompile when the preset
   had been selected before the manifest arrived.

3. handleSelectPreset() auto-triggers handleDownload() when the selected
   preset has no cached manifest yet. The select applies the motion config
   immediately; the download fires in the background and recompiles
   visualKeyframes with full tracks the moment it completes — no manual
   download button click required.
Image clips with media animations (visualKeyframes) went black during
playback because registerNativeImageAsset is async. The rasterizeImages
method awaited every registration promise, blocking the frame render.
buildNativeVideoProjectRequest then returned null (image raster not yet
registered), causing the entire frame to be skipped — black canvas.
The clip rendered correctly when paused because there was no real-time
pressure on the await.

Text and stickers already had nonBlockingText / nonBlockingStickers paths
that return the cached snapshot with updated animated transforms while
registration completes in the background. Images had no equivalent.

Changes:
- Add nonBlockingImages option to NativeRasterBridgeOptions
- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im-ren- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Ad visible every frame without
  re-uploading the texture
- First-frame entr- First-frame entr- First-frame entr- First-frame entr- First-frame iately on clip entry
- Store snapshot in imageSnapshotsByLayerId on each blocking resolution
  so the non-blocking path can use it on subsequent  so the non-blocking path can use it on subsequent  so the non-bloPass  so the non-blockingsPl  so the non-blocking path can use it on subsequent  so the non-blockingsting nonBlockingText/nonBlockingStickers
  flags
Tasks 1-9 complete:
- Task 1: Add PerfLogKind entries (media-animation-catalog-fetch, -download,
  -cache-hydration, -rollup) + MediaAnimationTelemetry class with 4 static
  fire-and-forget methods (recordCatalogFetch, recordManifestDownload,
  recordCacheHydration, recordAnimationEval)
- Task 2: Instrument cache hydration in mediaAnimationCache.ts with
  performance.now() timing; hydrateManifests() now returns error count
- Task 3: Instrument catalog fetch in useMediaAnimationCategory() hook;
  refactored from .then()/.catch() to async/await; capture httpStatus
- Task 4: Instrument ensureDownloaded() cache-hit + network fetch spans
  with fetchDurationMs + writeDurationMs breakdown and error reporting
- Task 5: MediaAnimationRollupCollector with hasRecordedData()/flush()/clear();
  flush() computes p95 with sort; singleton mediaAnimationRollup exported
- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: InstronR- Task 6: Instrumeed- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: Instrumn pe- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: InstrumEV- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: Iatio- Task 6: Instrument compile- Task 6: Instlat- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: t:- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: Instests__/: 37/37 passed
…on telemetry

- Use selectedClip.kind (ClipKind) instead of selectedClip.type which
  does not exist on Clip and always produced 'unknown' (two call sites
  in MediaAnimationSection.tsx)
- Wrap performance.now() - t0 in Math.round() on both the success and
  error paths in useMediaAnimationCategory, consistent with all other
  timing calls in the telemetry stack (mediaAnimationCatalog.ts)
…y demand (Bug 13)

Problem: 100% frame drop on first play (droppedFramesRatio=1, 169/169 dropped)
because Rust WorkPlanner only populated lookahead while already in Play mode.
Three compounding delays: configureNativePlaybackRender IPC not established,
serial audio IPC chain, cold HEVC decode (50–500ms).

Fix (four sites in NativeProgramPreview.tsx):

1. primeNativeLookahead() — new closure-local function (same scope as
   prefetchUpcomingNativeVideo) that calls queueNativeFrame fire-and-forget
   for LOOKAHEAD_PRIME_FRAMES=6 frames while still paused. Uses
   'video:prime:' cache-key namespace to avoid colliding with existing
   prefetch entries. Emits playback-trace telemetry via perfLogService.

2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused-settle trigger — pr tr2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused- de2. paused-settle trigger — prim2. paused-settle trigger — prim2. pauseder 2. paused-settle trigger — prim2. paused-settle trigger — prim2. pausedam2. paused-settle trigger — prim2. paused-settle trsure2. paused-settle trigger — prim2. paused-sditionally (not
   deferred until persistentNativePausedEligible is satisfi   deferred until persistentNativePausedEligible is satisfi   deferred erifi   deferred until persistentNativePausedEligible is satisfi  Pre   deferred until persistentNativePausedEligible is satisfi   deferre3 regression tests)
…ss testable

- mediaAnimationCatalog.ts: replace (p as any).phase with p.phase at both
  filter sites in getCatalogSnapshot() — RemoteTextAnimationPreset already
  types phase: MotionBehaviorCategory, no cast needed (review blocker)
- ProgramPreview.renderLoop.test.ts: add videoLayersByFrame option to
  makePrimingHarness so the scene.visualLayers guard in primeNativeLookahead
  is exercised rather than hardcoded to true (review finding #3)
The updateSource() path was missing a markNativeAudioUnavailable() call
for the 'genuinely no audio clips' branch. When updateSource() fires after
initialize() (via the pendingSource drain), it would overwrite the flag
that initialize() correctly set — leaving hasNativeClockPosition false and
permanently blocking nativeAudioClockReady for silent projects.

The result: pressing play on a project with no audio clips produced 1 frame
attempt then froze — the render loop never entered persistentNativePlaybackEligible
because nativeAudioClockReady stayed false, so animations never ran.

initialize() already had the correct guard; this aligns updateSource() to match.
The allClipsHavePaths / resolving branch (paths not yet hydrated) is unchanged —
that path correctly skips the call since audio will join later.
…yback freeze

- Invalidate evaluation cache when effects, filters, adjustments, colorGrade, or animations change in computeClipVersion
- Guard lookahead pre-decode outcome in present_native_frame_internal when timeline has no video layers (image/text only)
- Match raster layers by asset_id in NativeRenderSession and guard is_text with prefix check
- Reset lastNativePlaybackRequestKey on transition to playing so t=0 frame demand is dispatched
- Add !this._nativeAudioUnavailable guard to CPAL init timeout in PlaybackClock.time
- Add comprehensive deterministic reproduction matrix and regression tests
… ingestion parity

- compile 'scale' property symmetrically to width and height keyframes to prevent aspect-ratio distortion
- clear legacy entranceAnimation and exitAnimation on modern motion preset selection to prevent split-brain collisions
- make subcategory normalization case-insensitive in animationCatalog
- add comprehensive textAnimationPipeline integration suite covering ingestion, composition, scaling, t=0 evaluation, and GPU quad scaling
…Section architecture

- Extract preset grid into standalone PhaseGrid component with PresetCard parity
- Keep all three tabs (in/out/loop) permanently mounted using CSS contents/hidden toggling
- Implement visitedTabs lazy-fetch gating with useAnimationPhase(phase, enabled)
- Remove legacy subcategories in favor of All vs Favorites filter matching media animations
- Add TextAnimationCacheManager for disk persistence under BaseDirectory.AppCache
- Support thumbnail, WebM hover preview video, and download indicator states
- Add comprehensive behavioral unit tests for lazy gating and lifecycle persistence
…n when downloading

- In PresetCard, invoke onSelect() unconditionally when card is clicked, and call onDownload() to fetch manifest in background if not yet cached
- Pass motionOverride to handleDownload so keyframe compilation dispatches updates to timeline clip as soon as manifest arrives
- Ensure clicking an un-downloaded preset immediately sets inPreset on the selected clip instead of requiring a second click
Fixes: Applying a stagger-based preset (e.g. 'Flip In') rotated the
entire text bounding box as a rigid rectangle instead of flipping
individual characters one by one — breaking parity with Clypra Studio.

Root cause: motionPresets.ts compiled ALL preset tracks into clip-level
visualKeyframes (6 channels: x, y, width, height, rotation, opacity).
This discarded stagger config entirely and mapped 'rotation' to the
whole-layer rotation channel.

Fix (5-part):
1. glyphAnimator.ts (NEW): Port calculateStaggerProgress +
   evaluatePresetTransform math from clypra-studio's animationEvaluator.ts.
   Identical stagger math ensures visual output matches Studio exactly.

2. EvaluatedTextLayer (types.ts): Add optional motion, motionInProgress,
   and motionOutProgress fields forwarded from evaluator to rasterizer.

3. evaluator.ts: Compute normalized in/out progress [0,1] from clip.motion
   and attach to textLayer alongside the motion config.

4. textRasterizer.ts: When a stagger-based preset is active, render each
   character individually with its own translate/rotate/scale/skewX/blur/
   tracking transforms evaluated per glyph via glyphAnimator.

5. motionPresets.ts: Guard stagger presets with isGlyphAnimatedPreset()
   so they are never compiled into clip-level visualKeyframes.

6. nativeTextPreview.ts: Include rounded motionInProgress/motionOutProgress
   in buildNativeTextKeyObject so animated frames get unique cache keys.

Tests: 27 new regression tests in glyphAnimator.test.ts covering stagger
math, easing, track interpolation, and the core per-glyph divergence
(different glyphs at same overall progress must get different transforms).

verify:fast: 4/4 passed (TypeScript clean, architecture invariants, 156
preview render loop tests, telemetry audit).
…mation sticky footer

- Remove unpolished inline duration slider from MotionPresetSection
- Extend PropertiesPanel sticky footer duration control to support text clips
- Synchronize active animation sub-tab (in/out/loop) between preset section and panel footer
- Target inDuration or outDuration based on active category and preset state
- Add pointer drag-scrubbing with setPointerCapture to duration slider track
- Compile responsive visualKeyframes via text animation catalog for text clips
- Add automated regression tests in PropertiesPanelTextAnimation.test.tsx and update MotionPresetSection.test.tsx
…io speed export filter chain

- Fix PropertyPanelHeader to include text-template clip kind in TEXT_TABS resolution (CLY-BUG-TEXT-001)
- Add regression test suite for PropertyPanelHeader across all clip kinds
- Implement chained atempo filter complex generation in audio_filter.rs for export speed remapping
- Fix nativeTimelineExport eligibility check to prevent preservePitch from disqualifying cut-only video
- Add reset buttons for volume and fades in AudioSection and AudioBasicTab, wiring AudioFXSection
- Update types, tests, and otioImporter for export audio clip speed configuration
@AIEraDev AIEraDev changed the title fix(preview): resolve timeline-zero effect cache invalidation and playback freeze fix(preview, properties, export): timeline-zero effect cache, text-template properties tabs, and audio speed export filter chain Nov 11, 2026
@theunavailableguy
theunavailableguy merged commit 74e2fd9 into master Nov 11, 2026
9 checks passed
@theunavailableguy
theunavailableguy deleted the fix/timeline-zero-effect-playback-freeze branch November 11, 2026 02:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants