# Page Transitions Feature Documentation for the Tour Builder Platform's page transition system using video-based animations stored directly on navigation elements. ## Overview The platform implements video-based page transitions that are configured directly on navigation elements in `tour_pages.ui_schema_json`. The system supports forward and **server-side pre-generated reversed** playback with intelligent preloading. ``` ┌─────────────────────────────────────────────────────────────────┐ │ Transition Architecture │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Data Layer │ │ │ │ tour_pages.ui_schema_json → elements[].transitionVideoUrl │ │ │ │ asset_variants.variant_type = 'reversed' │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Server Processing Layer │ │ │ │ TourPagesService → videoProcessing.ts (FFmpeg reversal) │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Execution Layer │ │ │ │ Runtime Navigation │ Transition Overlay │ useTransitionPlayback│ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Preloading Layer │ │ │ │ usePreloadOrchestrator │ usePageSwitch │ useNeighborGraph │ │ │ │ getReadyBlobUrl │ S3 Presigned URLs │ Cache API │ │ │ └─────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ## Reverse Video Architecture ### Server-Side Pre-Generated Reversed Variants The platform uses **server-side pre-computation** of reversed videos stored as separate asset variants. This approach was chosen over client-side frame-stepping for: - **Instant playback** - No client-side processing needed - **Device independence** - Works equally well on all devices - **Professional quality** - FFmpeg ensures perfect audio/video synchronization - **Reliability** - Pre-generated videos eliminate runtime failures ### Reverse Generation Flow ``` Page Save Event (create/update) ↓ TourPagesService.update() / create() ↓ processReversedVideosAndUpdateSchema() └─ For each navigation element with transitionVideoUrl: ↓ getOrGenerateReversedVariant() ├─ Check if reversed variant exists in asset_variants └─ If not: ↓ generateReversedVariant() ├─ Download original video (downloadToBuffer) ├─ Reverse with FFmpeg (videoProcessing.reverseVideo) ├─ Upload reversed variant (uploadBuffer) └─ Create asset_variants record with type='reversed' ↓ Update ui_schema_json with reverseVideoUrl ↓ Save page with updated URLs ``` ### Back Navigation Behavior Back navigation **always uses history mode** - when the user clicks a back button, they return to the previous page using the original forward transition played in reverse. **How it works:** 1. User navigates forward from Page A to Page B (forward transition plays) 2. User clicks back button on Page B 3. System finds the forward navigation element that brought the user from Page A 4. The reversed video from that forward element plays 5. User returns to Page A **Benefits:** - Simpler UX - back button works like browser back - No configuration needed - back navigation is automatic - Consistent behavior - same transition forward and back ## Data Model ### Transition Configuration in Elements Transition settings are stored directly on navigation elements: ```typescript // In tour_pages.ui_schema_json.elements { id: "nav-button-1", type: "navigation_next", label: "Next Page", // Navigation target targetPageSlug: "gallery-page", // Transition configuration transitionVideoUrl: "assets/project-xyz/transitions/zoom-in.mp4", transitionDurationSec: 2.5, transitionReverseMode: "auto_reverse", // or "separate_video" reverseVideoUrl: undefined, // Auto-populated by server on save // Position & styling xPercent: 90, yPercent: 85, iconUrl: "assets/icons/arrow.png" } ``` ### Key Transition Fields | Field | Type | Description | |-------|------|-------------| | `transitionVideoUrl` | string | URL to transition video asset | | `transitionDurationSec` | number | Duration in seconds (auto-detected from video metadata) | | `transitionReverseMode` | `'auto_reverse'` \| `'separate_video'` | How back navigation transitions should play | | `reverseVideoUrl` | string | **Auto-generated** reversed video URL (or manually uploaded for `separate_video` mode) | **Reverse Mode Options:** | Mode | Behavior | |------|----------| | `auto_reverse` | Server generates reversed video on page save | | `separate_video` | Uses manually uploaded `reverseVideoUrl` for back navigation | | _(omitted)_ | No transition on back navigation | ### Database Schema **asset_variants table** (stores reversed videos): ```sql -- Variant types enum includes 'reversed' ALTER TYPE enum_asset_variants_variant_type ADD VALUE 'reversed'; -- Columns for reversed variants assetId: UUID -- Parent asset reference variant_type: 'reversed' cdn_url: TEXT -- Public URL (S3/GCloud/local) storage_key: TEXT -- Private storage path size_mb: DECIMAL -- File size ``` **Storage path pattern:** `assets/${assetId}/reversed.mp4` ### Benefits of Inline Storage 1. **Simpler data model** - No separate transitions table to manage 2. **No ID remapping** - Videos are referenced by URL, not foreign key 3. **Per-element transitions** - Different navigation buttons can have different transitions 4. **Easy publishing** - Transitions copy with the page, no extra steps needed 5. **Automatic reversal** - Server generates reversed videos on page save ## Server Requirements ### FFmpeg Runtime FFmpeg is **required** for reversed video generation. Without it, back navigation transitions won't have pre-generated reversed videos. The project uses bundled FFmpeg binaries through the backend npm packages `ffmpeg-static` and `ffprobe-static`. Manual OS-level installation is not required for the standard setup. **Verify bundled runtime from the backend context:** ```bash cd backend node -e "console.log(require('ffmpeg-static')); console.log(require('ffprobe-static').path)" ``` ### VM Memory Risk Reverse generation can be memory-heavy. On the standard VM, a June 2026 incident was caused by the kernel OOM-killing an `ffmpeg` child process that used about 3.3 GiB RSS on a 3.8 GiB RAM VM. Because the child process belonged to the PM2 systemd unit, PM2 stopped the frontend, backend, executor, and telemetry processes, causing Apache to return `503 Service Unavailable`. See [deployment-vm.md](deployment-vm.md) for the VM recovery runbook. Implemented resource controls: 1. `videoProcessing.reverseVideo()` uses an in-process single-worker queue, so only one FFmpeg reversal runs at a time. Additional requests wait for the previous job to finish. 2. FFmpeg reversal uses `-threads 1` to reduce CPU and memory pressure. 3. FFmpeg reversal has a hard timeout (`FFMPEG_REVERSE_TIMEOUT_MS`, default `600000`, exposed as `config.resilience.ffmpeg.reverseTimeoutMs`) and kills the child process when the timeout is reached. 4. FFmpeg reversal is protected by an in-process circuit breaker (`FFMPEG_BREAKER_FAILURE_THRESHOLD`, `FFMPEG_BREAKER_COOLDOWN_MS`, `FFMPEG_BREAKER_SUCCESS_THRESHOLD`, exposed under `config.resilience.ffmpeg.breaker`) so repeated failures stop new reversal jobs during the cooldown window. 5. FFprobe metadata extraction has a timeout (`FFPROBE_TIMEOUT_MS`, default `30000`, exposed as `config.resilience.ffmpeg.ffprobeTimeoutMs`), and reverse-video logs include input/output byte sizes plus probed media metadata. 6. External file storage calls used by reversal download/upload paths are protected by the shared file-storage circuit breaker for S3/GCloud providers. 7. `TourPagesService` still deduplicates generation by transition video `storageKey`, so repeated requests for the same source video share the same generation promise. 8. Before enqueueing auto-reverse generation, `TourPagesService` validates the source asset in one place. It rejects transition videos larger than `16 GiB` unless a reversed variant already exists, and it also rejects videos whose stored `width_px`, `height_px`, `duration_sec`, and `frame_rate` imply too much decoded frame data for the VM. Asset `frame_rate` is now probed on the backend with bundled `ffprobe-static` during asset create/update. For older assets without persisted `frame_rate`, the validation path probes the stored file on demand and only falls back to a conservative `30 FPS` estimate if probing fails. 9. The page save returns a validation error so the constructor can show an explicit user-facing notification instead of silently starting a risky background job. Additional hardening still recommended: 1. Reject or downscale very large transition videos before reversal. 2. Consider running media processing in a separate worker with memory limits. ## Backend Implementation ### Video Processing Service **File:** `backend/src/services/videoProcessing.ts` FFmpeg-based video reversal: ```typescript import { isFFmpegAvailable, reverseVideo } from './videoProcessing.ts'; // Core function: reverseVideo(inputBuffer, filename) → reversedBuffer // Processing pipeline: // 1. Create temporary directory for FFmpeg operations // 2. Write input buffer to temp file // 3. Probe input media metadata with a timeout // 4. Execute FFmpeg behind the single-worker queue and circuit breaker: // - -vf reverse (video reversal) // - -af areverse (audio reversal) // - -c:v libx264 (H.264 encoding) // - -preset fast (performance preset) // - -crf 23 (compression quality) // - -c:a aac (audio encoding) // - -threads 1 (resource cap) // - kill FFmpeg if FFMPEG_REVERSE_TIMEOUT_MS is exceeded // 5. Probe output media metadata and log input/output sizes // 6. Read output buffer // 7. Clean up temporary files ``` ### Tour Pages Service Integration **File:** `backend/src/services/tour_pages.ts` Key functions: - `processReversedVideosAndUpdateSchema()` - Main orchestrator for reverse generation - `getOrGenerateReversedVariant()` - Check/generate reversed variant - `generateReversedVariant()` - Download → Reverse → Upload → Record - `regenerateProjectReversedVideos()` - Project-wide generation for missing reversed videos **Generation Pattern:** - Reversed videos are always generated for all navigation elements with transitions - Generated on-demand when page is saved (create/update) - Checked before generation to avoid duplication - Different transition videos are processed sequentially through the global FFmpeg queue; the backend does not run multiple FFmpeg reversals in parallel - Background processing keeps save requests fast ### File Service Integration **File:** `backend/src/services/file.ts` Key functions used for reverse generation: - `downloadToBuffer(privateUrl)` - Downloads file from storage provider to Buffer - `uploadBuffer(privateUrl, buffer, options)` - Uploads buffer to storage ## Frontend Types **File:** `frontend/src/types/constructor.ts` ```typescript interface CanvasElement { id: string; type: CanvasElementType; // 'navigation_next' | 'navigation_prev' | ... label: string; // Navigation targetPageSlug?: string; /** @deprecated Use targetPageSlug instead */ targetPageId?: string; // Transition transitionVideoUrl?: string; transitionDurationSec?: number; transitionReverseMode?: 'auto_reverse' | 'separate_video'; reverseVideoUrl?: string; // Auto-generated or manually uploaded // ... other fields } ``` **File:** `frontend/src/types/presentation.ts` ```typescript // Shared presentation types for RuntimePresentation and constructor.tsx // Canvas element with navigation properties (for click handling) interface NavigableElement { id: string; type: string; targetPageSlug?: string; targetPageId?: string; transitionVideoUrl?: string; reverseVideoUrl?: string; navType?: 'forward' | 'back'; navDisabled?: boolean; } // Navigation target resolved from element click interface NavigationTarget { page: RuntimePage; pageId: string; transitionVideoUrl?: string; reverseVideoUrl?: string; isBack: boolean; } // Transition phase (exported for navigation helpers) type TransitionPhase = 'idle' | 'preparing' | 'playing' | 'finishing' | 'completed'; ``` **File:** `frontend/src/hooks/useTransitionPlayback.ts` ```typescript // ReverseMode for transition playback (runtime) type ReverseMode = 'none' | 'separate'; // 'reverse' removed - now uses pre-generated videos interface TransitionConfig { videoUrl: string; storageKey?: string; // Raw storage path for cache lookup reverseMode: ReverseMode; reverseVideoUrl?: string; // Pre-generated reversed video URL reverseStorageKey?: string; // Storage key for reversed video durationSec?: number; targetPageId?: string; displayName?: string; isBack?: boolean; // Track navigation direction } // Internal playback phases type PlaybackPhase = 'idle' | 'preparing' | 'playing' | 'finishing' | 'completed'; ``` **Mapping between constructor and runtime modes:** | Constructor (`transitionReverseMode`) | Runtime (`ReverseMode`) | |---------------------------------------|-------------------------| | `'auto_reverse'` | `'separate'` (uses pre-generated `reverseVideoUrl`) | | `'separate_video'` | `'separate'` | | _(omitted)_ | `'none'` | ## Runtime Execution ### Navigation Flow **File:** `frontend/src/components/RuntimePresentation.tsx` ``` ┌──────────────────────────────────────────────────────────────┐ │ 1. User clicks navigation element │ │ └── handleElementClick(element) │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ 2. Resolve target page from slug │ │ └── pages.find(p => p.slug === element.targetPageSlug) │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ 3. Check for transition video │ │ └── element.transitionVideoUrl exists? │ └──────────────────────────────────────────────────────────────┘ │ ┌───────────────────┴───────────────────┐ │ Yes │ No ▼ ▼ ┌──────────────────────┐ ┌──────────────────────┐ │ 4a. Set transition │ │ 4b. Direct navigate │ │ state with correct │ │ to target page │ │ video URL │ │ │ │ │ │ setSelectedPageId() │ │ isBack? │ └──────────────────────┘ │ ├─ true: use │ │ │ reverseVideoUrl │ │ └─ false: use │ │ transitionVideoUrl│ └──────────┬───────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ 5. Render full-screen video overlay │ │ │ │ Forward: video.play() with transitionVideoUrl │ │ Back: video.play() with reverseVideoUrl (pre-generated) │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ 6. Video ends (or fallback timeout fires) │ │ └── finishOverlayTransition() │ │ └── applyPageSelection(targetPageId) │ │ └── Clear overlay state │ └──────────────────────────────────────────────────────────────┘ ``` ### Source URL Selection **File:** `frontend/src/hooks/useTransitionPlayback.ts` ```typescript const sourceUrl = useMemo(() => { if (!transition) return ''; // Use reversed video if back navigation with separate reversed video if (transition.isBack && transition.reverseVideoUrl) { return transition.reverseVideoUrl; } return transition.videoUrl; }, [transition]); ``` **Key Characteristics:** - No frame-stepping computation needed - Simple conditional: if back navigation AND reversed URL exists → use reversed video - Direct playback from downloaded buffer ### Transition Preview Hook **File:** `frontend/src/hooks/useTransitionPreview.ts` ```typescript // Validation before preview if (direction === 'back' && !element.reverseVideoUrl) { onError?.('Reversed video not available. Save the page to generate it.'); return; } // Preview state structure { videoUrl: element.transitionVideoUrl, reverseMode: direction === 'back' ? 'separate' : 'none', reverseVideoUrl: element.reverseVideoUrl, // Pre-reversed video URL reverseStorageKey: element.reverseVideoUrl, // Storage path for caching isBack: direction === 'back' // Track navigation direction } ``` ### Overlay Rendering **File:** `frontend/src/components/Constructor/TransitionPreviewOverlay.tsx` The `TransitionPreviewOverlay` component renders transition videos within the letterboxed canvas bounds: ```tsx ``` **Component Props:** | Prop | Type | Description | |------|------|-------------| | `videoRef` | `RefObject` | Reference managed by `useTransitionPlayback` | | `isActive` | `boolean` | Whether overlay is visible | | `isBuffering` | `boolean` | **Hides entire container** while buffering (prevents black flash) | | `letterboxStyles` | `CSSProperties` | Position/size from `useCanvasScale` | | `videoFit` | `'contain'` \| `'cover'` | Object-fit mode (default: `'contain'`) | | `opacity` | `number` | Container opacity (default: 1) | **Video Transition Flow (No Fades):** ``` 1. Click → isBuffering=true → container opacity=0 (old page visible) 2. Video ready → isBuffering=false → container opacity=1 (video first frame) 3. Video plays → last frame = new page background 4. onComplete → setTransitionPreview(null) → instant overlay removal ``` **Key Design Decision:** - Video transitions do NOT use fade effects - Video itself IS the transition (first frame = old page, last frame = new page) - Overlay removed instantly when new background is ready - This prevents any visual discontinuity ### Navigation Helpers **File:** `frontend/src/lib/navigationHelpers.ts` Shared utilities for page navigation: ```typescript import { resolveNavigationTarget, isBackNavigation, getNavigationDirection, isTransitionBlocking, hasPlayableTransition, isNavigationType, } from '../lib/navigationHelpers'; ``` | Function | Purpose | |----------|---------| | `resolveNavigationTarget(element, pages, context)` | Resolve target page from element. Back navigation always uses history mode | | `isBackNavigation(element)` | Check if element navigates backwards | | `getNavigationDirection(element)` | Get navigation direction as `'back'` or `'forward'` | | `isTransitionBlocking(transitionPhase)` | Check if transition is blocking navigation | | `hasPlayableTransition(element, direction)` | Check if element has playable transition | | `isNavigationType(elementType)` | Check if element type is a navigation type | | `resolveHistoryBackTarget(pages, currentSlug, previousPageId)` | Resolve back target from navigation history | | `findIncomingNavigationElement(pages, fromPageId, toPageId)` | Find forward element that links pages | **Playable Transition Check:** ```typescript const hasPlayableTransition = ( element: { transitionVideoUrl?: string; transitionReverseMode?: string; reverseVideoUrl?: string; }, direction: 'back' | 'forward' = 'forward', ): boolean => { if (!element.transitionVideoUrl) return false; // For back navigation, need reverse video (auto-generated or separate) if (direction === 'back' && !element.reverseVideoUrl) { return false; } return true; }; ``` ## Constructor Configuration ### Setting Up Transitions **File:** `frontend/src/pages/constructor.tsx` When editing a navigation element: 1. Select element type (`navigation_next` or `navigation_prev`) 2. Choose target page by slug 3. Select transition video from project assets 4. Duration is auto-detected from video metadata 5. Choose reverse mode for back navigation 6. **Save the page** to trigger reverse video generation ```typescript // Saving navigation element with transition const elementData = { type: selectedElementType, targetPageSlug: targetPage?.slug, transitionVideoUrl: selectedTransitionVideo?.cdn_url, transitionDurationSec: transitionDuration, transitionReverseMode: reverseMode, // 'auto_reverse' | 'separate_video' | undefined reverseVideoUrl: reverseMode === 'separate_video' ? reverseVideo?.cdn_url : undefined, // Note: reverseVideoUrl is auto-populated for 'auto_reverse' mode on save }; ``` **Reverse Mode UI Options:** | UI Option | `transitionReverseMode` | `reverseVideoUrl` | |-----------|-------------------------|-------------------| | "Auto Reverse" | `'auto_reverse'` | **Auto-generated on save** | | "Separate Video" | `'separate_video'` | User-uploaded video | | "No Reverse" | _(not set)_ | _(not set)_ | ### When to Enable Reverse | Video Type | `transitionReverseMode` | Reason | |------------|-------------------------|--------| | Zoom in/out | `'auto_reverse'` | Symmetrical animation | | Slide left/right | `'auto_reverse'` | Direction can reverse | | Fade in/out | `'auto_reverse'` | Works both directions | | Text animation | `'separate_video'` | Use dedicated reverse video | | One-way motion | _(omit)_ | Only makes sense forward | | Complex animation | `'separate_video'` | Pre-rendered reverse looks better | ## Preloading Integration ### Transition Video Preloading **File:** `frontend/src/lib/extractPageLinks.ts` Transition videos (including reversed) are extracted from navigation elements: ```typescript import { extractPageLinksAndElements } from '../lib/extractPageLinks'; // Extract navigation links (includes transition and reverse videos) const { pageLinks, preloadElements } = extractPageLinksAndElements(filteredPages); // pageLinks contains transition information: // { from_pageId, to_pageId, transition: { video_url, reverse_video_url } } ``` ### Priority Calculation Transition videos have the **highest priority** (+150): ``` Transition Priority = neighborBase + assetTypeBonus neighborBase: 1000 (current page) or 500 (neighbor) assetTypeBonus: 150 (transition - highest priority) Examples: - Current page transition: 1000 + 150 = 1150 - Neighbor (depth 1) transition: 500 + 150 = 650 ``` **Asset Type Priorities:** | Type | Priority | Reason | |------|----------|--------| | Transition | +150 | Needed immediately on click | | Image | +100 | Required for page display | | Audio | +50 | Background audio | | Video | +30 | Can stream progressively | ### Sophisticated Source Resolution Pipeline **File:** `frontend/src/hooks/useTransitionPlayback.ts` ```typescript // Source resolution order: 1. Try storage key ready blob URL (pre-downloaded) 2. Try storage key cached blob URL (Cache API) 3. Reuse cached blob URL from previous playback 4. Try ready blob URL by resolved CDN URL 5. Try cached blob URL by resolved CDN URL 6. Fetch video as blob with presigned URL or proxy fallback ``` **Storage Key Usage:** - `storageKey` and `reverseStorageKey` used for cache lookup - Allows offline access via IndexedDB - Distinguishes between original and reversed video caches ## Non-Transition Navigation (CSS Transitions) When navigating without a video transition, the system uses CSS-based transitions with settings resolved through a cascade. ### Settings Cascade Resolution Transition settings (type, duration, easing, overlay color) are resolved through a three-level cascade: ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ Transition Settings Cascade │ │ │ │ 1. Element Level (highest priority) │ │ └── ui_schema_json.elements[].transitionSettings │ │ ↓ fallback │ │ 2. Project Level (per environment) │ │ └── project_transition_settings WHERE projectId AND environment │ │ ↓ fallback │ │ 3. Global Level (platform-wide defaults) │ │ └── global_transition_defaults (single record) │ │ ↓ fallback │ │ 4. Hardcoded Fallback │ │ └── type: 'fade', durationMs: 700, easing: 'ease-in-out' │ └─────────────────────────────────────────────────────────────────────────┘ ``` **Hook:** `useTransitionSettings` resolves final settings: ```typescript const transitionSettings = useTransitionSettings({ globalDefaults, // From global_transition_defaults table projectSettings, // From project_transition_settings (environment-specific) elementSettings, // From currentElementTransitionSettings state }); // Returns: { type, durationMs, easing, overlayColor } ``` **Extracting element settings:** Use `extractElementTransitionSettings()` to convert element fields: ```typescript import { extractElementTransitionSettings } from '../types/transition'; // Extract from clicked navigation element const elementSettings = extractElementTransitionSettings(clickedElement); // Returns: { transitionType?, transitionDurationMs?, transitionEasing?, transitionOverlayColor? } ``` The function only includes fields with actual values (not empty strings), allowing cascade fallthrough when element uses "Use Project Default". **Environment-Aware Project Settings:** Project transition settings are stored per-environment and copied during publishing: - **Constructor** (dev): Edits `project_transition_settings WHERE environment='dev'` - **Save to Stage**: Copies dev settings → stage - **Publish**: Copies stage settings → production See [project-transition-settings.md](./project-transition-settings.md) for full documentation. ### CSS Transition Implementation **CSS Variables (main.css) - Single Source of Truth:** ```css :root { --crossfade-duration: 700ms; /* Smooth easing: slow start, gentle acceleration, soft landing */ --crossfade-easing: cubic-bezier(0.4, 0, 0.2, 1); } ``` **CSS Animations (main.css):** ```css @keyframes page-crossfade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes page-crossfade-out { from { opacity: 1; } to { opacity: 0; } } .animate-crossfade-in { animation: page-crossfade-in var(--crossfade-duration, 700ms) var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1)) forwards; } .animate-crossfade-out { animation: page-crossfade-out var(--crossfade-duration, 700ms) var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1)) forwards; } ``` **Easing Curve Characteristics:** - `cubic-bezier(0.4, 0, 0.2, 1)` - Material Design standard easing - Slow start - prevents abrupt appearance - Gentle acceleration through middle - Soft landing at end ### CSS Transition vs Video Transition | Navigation Type | Effect | Duration Control | Settings Source | |-----------------|--------|------------------|-----------------| | With transition video | Video overlay plays, instant removal | Video duration | Element-level only | | Without transition video | CSS fade animation | Cascade resolution | Element → Project → Global | ## File References | File | Purpose | |------|---------| | `backend/src/services/videoProcessing.ts` | FFmpeg video reversal service | | `backend/src/services/tour_pages.ts` | Reverse video generation orchestration | | `backend/src/services/file.ts` | Download/upload buffer operations | | `backend/src/services/project_transition_settings.ts` | Project transition settings service | | `backend/src/db/api/asset_variants.ts` | Reversed variant database operations | | `backend/src/db/api/project_transition_settings.ts` | Project transition settings API | | `backend/src/db/models/asset_variants.js` | Asset variants model (includes 'reversed' type) | | `backend/src/db/models/project_transition_settings.js` | Project transition settings model | | `backend/src/db/migrations/20260413091125-add-reversed-variant-type.js` | Migration for reversed variant support | | `backend/src/db/migrations/20260501000002-create-project-transition-settings.js` | Project transition settings table | | `frontend/src/css/main.css` | CSS animation keyframes and classes | | `frontend/src/pages/constructor.tsx` | Transition video selection UI | | `frontend/src/components/RuntimePresentation.tsx` | Transition overlay playback and navigation | | `frontend/src/components/TourFlowManager.tsx` | Project transition settings UI | | `frontend/src/components/Constructor/TransitionPreviewOverlay.tsx` | Canvas-aware transition video overlay | | `frontend/src/lib/extractPageLinks.ts` | Extract transition videos from navigation elements | | `frontend/src/lib/navigationHelpers.ts` | Navigation target resolution, reverse detection | | `frontend/src/stores/project_transition_settings/projectTransitionSettingsSlice.ts` | Redux store for transition settings | | `frontend/src/hooks/usePreloadOrchestrator.ts` | Preloading with ready blob URLs | | `frontend/src/hooks/usePageSwitch.ts` | Page navigation using preloaded transitions | | `frontend/src/hooks/useTransitionPlayback.ts` | Transition video playback coordination | | `frontend/src/hooks/useTransitionPreview.ts` | Transition preview with reverse validation | | `frontend/src/hooks/useTransitionSettings.ts` | Cascade resolution for transition settings | | `frontend/src/hooks/useBackgroundTransition.ts` | Background fade-out coordination | | `frontend/src/hooks/useNeighborGraph.ts` | Navigation graph for preload prioritization | | `frontend/src/types/constructor.ts` | Element type definitions | | `frontend/src/types/transition.ts` | Transition settings types | | `frontend/src/types/presentation.ts` | Runtime navigation types | | `frontend/src/config/preload.config.ts` | Preload priority weights | ## Error Handling ### Video Load Failures ```typescript videoRef.current.onerror = (e) => { console.error('Transition video failed to load:', e); // Fallback: complete navigation without video finishOverlayTransition(); }; ``` ### Missing Reversed Video ```typescript // In useTransitionPreview if (direction === 'back' && !element.reverseVideoUrl) { onError?.('Reversed video not available. Save the page to generate it.'); return; } ``` ### FFmpeg Unavailable ```typescript // In videoProcessing.ts if (!isFFmpegAvailable()) { logger.warn('FFmpeg not available, skipping reverse video generation'); return null; } ``` ## Environment Handling Transitions are environment-aware: ```typescript // Pages are filtered by environment before extraction const filteredPages = pages.filter(p => p.environment === environment); // Transitions only work between pages in the same environment const { pageLinks } = extractPageLinksAndElements(filteredPages); ``` | Environment | Context | Access | |-------------|---------|--------| | `dev` | Constructor editing | Authenticated | | `stage` | Preview/review | Authenticated | | `production` | Public runtime | Public | **Publishing:** When pages are published (dev → stage → production), transitions and reversed videos are copied as part of `ui_schema_json`. Since transitions reference video URLs (not IDs), no remapping is needed. ## Performance Considerations ### 1. Server-Side Generation Benefits | Aspect | Client-Side (Old) | Server-Side (New) | |--------|-------------------|-------------------| | Computation | During playback | At page save time | | Device Performance | Device-dependent | Instant playback | | Audio Sync | Manual filtering | FFmpeg perfect sync | | Memory Usage | High during playback | None (pre-generated) | ### 2. Video Format Use optimized video formats: - MP4 with H.264 for broad compatibility - WebM with VP9 for better compression - Keep transitions short (0.5-3 seconds) ### 3. Caching Strategy | Layer | Purpose | |-------|---------| | Cache API (< 5MB) | Fast asset storage | | IndexedDB (≥ 5MB) | Large assets, offline data | | Blob URLs | Pre-decoded for instant display | ## Troubleshooting ### Transition Doesn't Play 1. Check `transitionVideoUrl` is valid 2. Verify video is in supported format 3. Check browser console for CORS errors 4. Ensure video is preloaded (check Network tab) ### Reverse Video Not Available 1. **Save the page** - reverse videos are generated on page save 2. Check server logs for FFmpeg errors 3. Verify bundled FFmpeg paths resolve from the backend process 4. Check `asset_variants` table for `variant_type='reversed'` records 5. On the VM, check kernel logs for OOM-killed `ffmpeg` processes ### Back Navigation Has No Transition 1. Verify `transitionReverseMode` is set (`'auto_reverse'` or `'separate_video'`) 2. For `'auto_reverse'`: save the page to trigger generation 3. For `'separate_video'`: ensure `reverseVideoUrl` is set 4. Check `hasPlayableTransition(element, 'back')` returns true ### Navigation Gets Stuck 1. Check fallback timeout is firing 2. Verify `onEnded` event triggers 3. Look for JavaScript errors 4. Check transition state clears properly ### Element Settings Not Applied (Wrong Duration/Easing) **Symptom:** Element-level transition settings (duration, easing, overlay color) are ignored; transition uses project or global defaults instead. **Cause:** React's async state batching. When clicking a navigation button: 1. `setCurrentElementTransitionSettings()` schedules state update 2. `switchToPage()` starts transition immediately 3. `useTransitionSettings` resolves with OLD state (before React processes step 1) **Solution:** Use `flushSync` from `react-dom` to force synchronous state updates: ```typescript import { flushSync } from 'react-dom'; import { extractElementTransitionSettings } from '../types/transition'; // In handleElementClick: const elementSettings = extractElementTransitionSettings(clickedElement); // Force synchronous update BEFORE navigation flushSync(() => { setCurrentElementTransitionSettings(elementSettings); }); // Now transition uses correct element settings switchToPage(targetPageId, config); ``` **Files requiring this fix:** - `frontend/src/pages/constructor.tsx` - `frontend/src/components/RuntimePresentation.tsx` **Debug tip:** Add console.log inside `useTransitionSettings` to verify element settings are received: ```typescript console.log('[useTransitionSettings] element:', elementSettings); ``` If element shows as `null` when it shouldn't, the flushSync fix is missing.