diff --git a/frontend/src/config/preload.config.ts b/frontend/src/config/preload.config.ts index 6ac8f91..958452a 100644 --- a/frontend/src/config/preload.config.ts +++ b/frontend/src/config/preload.config.ts @@ -20,10 +20,10 @@ export const PRELOAD_CONFIG = { currentPage: 1000, neighborBase: 500, assetType: { + transition: 150, // Transitions preloaded for faster start image: 100, // Backgrounds load first audio: 50, video: 30, - // Note: transitions are cached on first playback, not preloaded } as Record, variant: { thumbnail: 50, @@ -68,11 +68,11 @@ export const PRELOAD_CONFIG = { // Partial preload settings (online mode only) // Download only first N bytes of videos/audio for faster Phase 1 completion // Playback uses presigned URL directly (browser handles remaining buffering) - // Note: Transitions are cached on first playback, not preloaded partialPreload: { enabled: true, videoMaxBytes: 5 * 1024 * 1024, // 5MB (~5 seconds of video) audioMaxBytes: 512 * 1024, // 512KB (~5 seconds of audio) + transitionMaxBytes: 3 * 1024 * 1024, // 3MB (~3 seconds of transition video) }, // Asset URL field names in element content_json (camelCase) diff --git a/frontend/src/hooks/useNeighborGraph.ts b/frontend/src/hooks/useNeighborGraph.ts index b27f548..8d05274 100644 --- a/frontend/src/hooks/useNeighborGraph.ts +++ b/frontend/src/hooks/useNeighborGraph.ts @@ -74,14 +74,12 @@ function extractAssetsFromContent( for (const [key, value] of Object.entries(obj)) { if (typeof value === 'string' && value && urlFields.includes(key)) { // Classify asset type based on field name - // Skip transition fields - transitions are cached on first playback const lowerKey = key.toLowerCase(); - if (lowerKey.includes('transition')) { - continue; // Skip transitions - } - let assetType: 'video' | 'audio' | 'image'; - if (lowerKey.includes('video')) { + let assetType: 'video' | 'audio' | 'image' | 'transition'; + if (lowerKey.includes('transition')) { + assetType = 'transition'; + } else if (lowerKey.includes('video')) { assetType = 'video'; } else if (lowerKey.includes('audio')) { assetType = 'audio'; @@ -239,14 +237,25 @@ export function useNeighborGraph( }); }); - // Add transition videos (transition is eagerly loaded in page_links) + // Extract transition videos from page_links for preloading const matchingLinks = pageLinks.filter( (link) => link.is_active !== false && pageIds.includes(link.from_pageId || ''), ); - // Note: Transition videos are NOT extracted for preloading. - // They are cached on first playback via useTransitionPlayback.cacheBlob() + matchingLinks.forEach((link) => { + // Extract transition video URL from link.transition + const transition = link.transition as { video_url?: string } | undefined; + if (transition?.video_url && !seenUrls.has(transition.video_url)) { + seenUrls.add(transition.video_url); + assets.push({ + url: transition.video_url, + pageId: link.from_pageId || '', + assetType: 'transition', + priority: 0, // Will be calculated later with transition priority + }); + } + }); return assets; }; diff --git a/frontend/src/hooks/usePreloadOrchestrator.ts b/frontend/src/hooks/usePreloadOrchestrator.ts index 7f8954f..ce01e9d 100644 --- a/frontend/src/hooks/usePreloadOrchestrator.ts +++ b/frontend/src/hooks/usePreloadOrchestrator.ts @@ -529,12 +529,18 @@ export function usePreloadOrchestrator( ) => { // Helper to determine max bytes for partial preload (online mode only) // IMPORTANT: Only applies to NEIGHBOR pages, not the current page + // Transitions always use partial preload (regardless of page) const getMaxBytesForAsset = ( assetType: 'image' | 'video' | 'audio' | 'transition' | 'other', isNeighborPage: boolean, ): number | undefined => { if (!PRELOAD_CONFIG.partialPreload.enabled) return undefined; + // Transitions always use partial preload - they need just enough to start quickly + if (assetType === 'transition') { + return PRELOAD_CONFIG.partialPreload.transitionMaxBytes; + } + // Current page assets should be fully downloaded for best UX if (!isNeighborPage) return undefined; @@ -557,11 +563,6 @@ export function usePreloadOrchestrator( assetType: 'image' | 'video' | 'audio' | 'transition' | 'other', pageId: string, ): Promise | null => { - // Skip transitions - they're cached on first playback via useTransitionPlayback - if (assetType === 'transition') { - return null; - } - const resolvedUrl = resolveUrl(storageKey, presignedUrls); if (!resolvedUrl) return null; @@ -577,8 +578,9 @@ export function usePreloadOrchestrator( // Determine if partial preload applies (neighbor pages only, media files only) const isNeighborPage = pageId !== currentPageId; const maxBytes = getMaxBytesForAsset(assetType, isNeighborPage); - // For partial downloads, don't create blob URL - playback uses presigned URL - const createBlobUrl = maxBytes === undefined; + // Create blob URL for images (instant navigation) and full downloads + // Partial downloads (video/audio/transition) use presigned URL directly for playback + const createBlobUrl = assetType === 'image' || maxBytes === undefined; return downloadManager .addJob({ @@ -690,11 +692,11 @@ export function usePreloadOrchestrator( // ============================================ // PHASE 2: Preload everything else (don't wait) // - Current page element assets (full downloads) - // - Transition videos (partial preload - 3MB) // - Neighbor page backgrounds (partial preload for video/audio) // - Neighbor page element assets (partial preload for video/audio) + // - Transition videos from page links (partial preload - 3MB) // ============================================ - logger.info('[PRELOAD] Phase 2: Preloading transitions and neighbors'); + logger.info('[PRELOAD] Phase 2: Preloading neighbors and transitions'); // Current page element assets (moved from Phase 1 for faster startup) const currentPageAssets = assets.filter(