preloading fix
This commit is contained in:
parent
b8f2274572
commit
41713d1274
@ -349,13 +349,15 @@ Assets are preloaded directly from S3 for better performance:
|
|||||||
// 1. Request presigned URLs (max 50 per batch, 1-hour expiry)
|
// 1. Request presigned URLs (max 50 per batch, 1-hour expiry)
|
||||||
POST /api/file/presign { urls: ["assets/img.jpg", ...] }
|
POST /api/file/presign { urls: ["assets/img.jpg", ...] }
|
||||||
|
|
||||||
// 2. Download directly from S3 → Store in Cache API
|
// 2. Download directly from S3 → Store in Cache API (dual key)
|
||||||
// 3. Create blob URL → Decode image → Store in readyBlobUrlsRef
|
// 3. Create blob URL → Decode image → Store in readyBlobUrlsRef (dual key)
|
||||||
|
|
||||||
// 4. Instant lookup during navigation (O(1))
|
// 4. Instant lookup during navigation (O(1)) - use storage key for reliability
|
||||||
const blobUrl = preloadOrchestrator.getReadyBlobUrl(originalUrl);
|
const blobUrl = preloadOrchestrator.getReadyBlobUrl(storageKey);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Storage Key Mapping:** Assets are cached under both download URL and canonical storage key (e.g., `assets/project/video.mp4`). Lookups prioritize storage key because presigned URL signatures change on each resolution. This ensures cache hits even after URL regeneration or page refresh.
|
||||||
|
|
||||||
**Preload Priority:**
|
**Preload Priority:**
|
||||||
| Type | Priority | Notes |
|
| Type | Priority | Notes |
|
||||||
|------|----------|-------|
|
|------|----------|-------|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -31,6 +31,7 @@ export default function ImageField({
|
|||||||
fill
|
fill
|
||||||
sizes='100vw'
|
sizes='100vw'
|
||||||
className={`rounded-full object-cover bg-gray-100 dark:bg-dark-900 ${imageClassName}`}
|
className={`rounded-full object-cover bg-gray-100 dark:bg-dark-900 ${imageClassName}`}
|
||||||
|
unoptimized
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -51,6 +51,7 @@ export default function RuntimePresentation({
|
|||||||
const [transitionPreview, setTransitionPreview] = useState<{
|
const [transitionPreview, setTransitionPreview] = useState<{
|
||||||
targetPageId: string;
|
targetPageId: string;
|
||||||
videoUrl: string;
|
videoUrl: string;
|
||||||
|
storageKey: string;
|
||||||
isReverse: boolean;
|
isReverse: boolean;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@ -120,6 +121,7 @@ export default function RuntimePresentation({
|
|||||||
transition: transitionPreview
|
transition: transitionPreview
|
||||||
? {
|
? {
|
||||||
videoUrl: transitionPreview.videoUrl,
|
videoUrl: transitionPreview.videoUrl,
|
||||||
|
storageKey: transitionPreview.storageKey,
|
||||||
reverseMode: transitionPreview.isReverse ? 'reverse' : 'none',
|
reverseMode: transitionPreview.isReverse ? 'reverse' : 'none',
|
||||||
targetPageId: transitionPreview.targetPageId,
|
targetPageId: transitionPreview.targetPageId,
|
||||||
displayName: 'Transition',
|
displayName: 'Transition',
|
||||||
@ -156,6 +158,7 @@ export default function RuntimePresentation({
|
|||||||
preload: {
|
preload: {
|
||||||
preloadedUrls: preloadOrchestrator?.preloadedUrls || new Set(),
|
preloadedUrls: preloadOrchestrator?.preloadedUrls || new Set(),
|
||||||
getCachedBlobUrl: preloadOrchestrator?.getCachedBlobUrl,
|
getCachedBlobUrl: preloadOrchestrator?.getCachedBlobUrl,
|
||||||
|
getReadyBlobUrl: preloadOrchestrator?.getReadyBlobUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -374,6 +377,7 @@ export default function RuntimePresentation({
|
|||||||
setTransitionPreview({
|
setTransitionPreview({
|
||||||
targetPageId,
|
targetPageId,
|
||||||
videoUrl: resolveAssetPlaybackUrl(transitionVideoUrl),
|
videoUrl: resolveAssetPlaybackUrl(transitionVideoUrl),
|
||||||
|
storageKey: transitionVideoUrl, // Raw storage path for cache lookup
|
||||||
isReverse: isBack,
|
isReverse: isBack,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -682,28 +686,46 @@ export default function RuntimePresentation({
|
|||||||
backgroundPosition: 'center',
|
backgroundPosition: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Background image element - CSS backgroundImage provides instant display,
|
{/* Background image element - CSS backgroundImage provides instant display.
|
||||||
Image component enhances with optimized loading. bg-black prevents white flash. */}
|
Use native img for blob URLs to prevent repeated fetch requests from Next.js Image. */}
|
||||||
{backgroundImageUrl && !backgroundVideoUrl && (
|
{backgroundImageUrl && !backgroundVideoUrl && (
|
||||||
<div className='absolute inset-0 pointer-events-none'>
|
<div className='absolute inset-0 pointer-events-none'>
|
||||||
<Image
|
{backgroundImageUrl.startsWith('blob:') ? (
|
||||||
key={backgroundImageUrl}
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
src={backgroundImageUrl}
|
<img
|
||||||
alt=''
|
key={backgroundImageUrl}
|
||||||
fill
|
src={backgroundImageUrl}
|
||||||
sizes='100vw'
|
alt=''
|
||||||
className='object-cover'
|
className='absolute inset-0 w-full h-full object-cover'
|
||||||
priority
|
onLoad={() => {
|
||||||
unoptimized
|
setIsBackgroundReady(true);
|
||||||
onLoad={() => {
|
pageSwitch.markBackgroundReady();
|
||||||
setIsBackgroundReady(true);
|
}}
|
||||||
pageSwitch.markBackgroundReady();
|
onError={() => {
|
||||||
}}
|
setIsBackgroundReady(true);
|
||||||
onError={() => {
|
pageSwitch.markBackgroundReady();
|
||||||
setIsBackgroundReady(true);
|
}}
|
||||||
pageSwitch.markBackgroundReady();
|
/>
|
||||||
}}
|
) : (
|
||||||
/>
|
<Image
|
||||||
|
key={backgroundImageUrl}
|
||||||
|
src={backgroundImageUrl}
|
||||||
|
alt=''
|
||||||
|
fill
|
||||||
|
sizes='100vw'
|
||||||
|
className='object-cover'
|
||||||
|
priority
|
||||||
|
unoptimized
|
||||||
|
onLoad={() => {
|
||||||
|
setIsBackgroundReady(true);
|
||||||
|
pageSwitch.markBackgroundReady();
|
||||||
|
}}
|
||||||
|
onError={() => {
|
||||||
|
setIsBackgroundReady(true);
|
||||||
|
pageSwitch.markBackgroundReady();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export default function UserAvatar({
|
|||||||
fill
|
fill
|
||||||
sizes='100vw'
|
sizes='100vw'
|
||||||
className='rounded-full object-cover bg-gray-100 dark:bg-slate-800'
|
className='rounded-full object-cover bg-gray-100 dark:bg-slate-800'
|
||||||
|
unoptimized
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -225,18 +225,47 @@ export function usePageSwitch(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a storage path to a displayable URL.
|
* Resolve a storage path to a displayable URL.
|
||||||
* Priority: 1) ready blob URL (instant, already decoded), 2) cached blob URL, 3) presigned URL with fallback
|
* Priority: 1) ready blob URL by storage path, 2) cached blob URL by storage path,
|
||||||
|
* 3) ready blob URL by resolved URL, 4) cached blob URL, 5) presigned URL with fallback
|
||||||
*/
|
*/
|
||||||
const resolveToDisplayUrl = useCallback(
|
const resolveToDisplayUrl = useCallback(
|
||||||
async (storagePath: string | undefined): Promise<string> => {
|
async (storagePath: string | undefined): Promise<string> => {
|
||||||
if (!storagePath) return '';
|
if (!storagePath) return '';
|
||||||
|
|
||||||
|
const cache = preloadCacheRef.current;
|
||||||
|
|
||||||
|
// 1. Try in-memory storage path lookup first (instant, same session)
|
||||||
|
if (cache?.getReadyBlobUrl) {
|
||||||
|
const readyUrl = cache.getReadyBlobUrl(storagePath);
|
||||||
|
if (readyUrl) {
|
||||||
|
logger.info('Using ready blob URL (storage key)', {
|
||||||
|
storagePath: storagePath.slice(-50),
|
||||||
|
});
|
||||||
|
return readyUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try persistent cache by storage path (survives page refresh)
|
||||||
|
if (cache?.getCachedBlobUrl) {
|
||||||
|
try {
|
||||||
|
const blobUrl = await cache.getCachedBlobUrl(storagePath);
|
||||||
|
if (blobUrl) {
|
||||||
|
createdBlobUrlsRef.current.add(blobUrl);
|
||||||
|
logger.info('Using cached blob URL (storage key)', {
|
||||||
|
storagePath: storagePath.slice(-50),
|
||||||
|
});
|
||||||
|
return blobUrl;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through to URL resolution
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Resolve to playback URL and try lookup (fallback for resolved URLs)
|
||||||
const originalUrl = resolveAssetPlaybackUrl(storagePath);
|
const originalUrl = resolveAssetPlaybackUrl(storagePath);
|
||||||
if (!originalUrl) return '';
|
if (!originalUrl) return '';
|
||||||
|
|
||||||
const cache = preloadCacheRef.current;
|
// Try instant blob URL lookup by resolved URL
|
||||||
|
|
||||||
// 1. Try instant blob URL lookup (already decoded, ready to paint)
|
|
||||||
if (cache?.getReadyBlobUrl) {
|
if (cache?.getReadyBlobUrl) {
|
||||||
const readyUrl = cache.getReadyBlobUrl(originalUrl);
|
const readyUrl = cache.getReadyBlobUrl(originalUrl);
|
||||||
if (readyUrl) {
|
if (readyUrl) {
|
||||||
@ -245,7 +274,7 @@ export function usePageSwitch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fallback: try cached blob URL (creates new blob, needs decode)
|
// Fallback: try cached blob URL by resolved URL
|
||||||
if (cache?.getCachedBlobUrl && cache?.preloadedUrls?.has(originalUrl)) {
|
if (cache?.getCachedBlobUrl && cache?.preloadedUrls?.has(originalUrl)) {
|
||||||
try {
|
try {
|
||||||
const blobUrl = await cache.getCachedBlobUrl(originalUrl);
|
const blobUrl = await cache.getCachedBlobUrl(originalUrl);
|
||||||
@ -257,11 +286,11 @@ export function usePageSwitch(
|
|||||||
return blobUrl;
|
return blobUrl;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fall through to fallback
|
// Fall through
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Load with presigned URL fallback (handles CORS failures)
|
// Load with presigned URL fallback (handles CORS failures)
|
||||||
const storageKey = isRelativeStoragePath(storagePath)
|
const storageKey = isRelativeStoragePath(storagePath)
|
||||||
? storagePath
|
? storagePath
|
||||||
: undefined;
|
: undefined;
|
||||||
@ -271,18 +300,41 @@ export function usePageSwitch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve video/audio URL (no fallback needed, just blob check)
|
* Resolve video/audio URL.
|
||||||
|
* Priority: 1) ready blob URL by storage path, 2) cached blob URL by storage path,
|
||||||
|
* 3) ready blob URL by resolved URL, 4) cached blob URL, 5) resolved URL
|
||||||
*/
|
*/
|
||||||
const resolveMediaUrl = useCallback(
|
const resolveMediaUrl = useCallback(
|
||||||
async (storagePath: string | undefined): Promise<string> => {
|
async (storagePath: string | undefined): Promise<string> => {
|
||||||
if (!storagePath) return '';
|
if (!storagePath) return '';
|
||||||
|
|
||||||
|
const cache = preloadCacheRef.current;
|
||||||
|
|
||||||
|
// 1. Try in-memory storage path lookup first (instant, same session)
|
||||||
|
if (cache?.getReadyBlobUrl) {
|
||||||
|
const readyUrl = cache.getReadyBlobUrl(storagePath);
|
||||||
|
if (readyUrl) {
|
||||||
|
return readyUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try persistent cache by storage path (survives page refresh)
|
||||||
|
if (cache?.getCachedBlobUrl) {
|
||||||
|
try {
|
||||||
|
const blobUrl = await cache.getCachedBlobUrl(storagePath);
|
||||||
|
if (blobUrl) {
|
||||||
|
createdBlobUrlsRef.current.add(blobUrl);
|
||||||
|
return blobUrl;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Resolve URL and try lookup by resolved URL
|
||||||
const originalUrl = resolveAssetPlaybackUrl(storagePath);
|
const originalUrl = resolveAssetPlaybackUrl(storagePath);
|
||||||
if (!originalUrl) return '';
|
if (!originalUrl) return '';
|
||||||
|
|
||||||
const cache = preloadCacheRef.current;
|
|
||||||
|
|
||||||
// 1. Try instant blob URL lookup first
|
|
||||||
if (cache?.getReadyBlobUrl) {
|
if (cache?.getReadyBlobUrl) {
|
||||||
const readyUrl = cache.getReadyBlobUrl(originalUrl);
|
const readyUrl = cache.getReadyBlobUrl(originalUrl);
|
||||||
if (readyUrl) {
|
if (readyUrl) {
|
||||||
@ -290,7 +342,6 @@ export function usePageSwitch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fallback: try cached blob URL
|
|
||||||
if (cache?.getCachedBlobUrl && cache?.preloadedUrls?.has(originalUrl)) {
|
if (cache?.getCachedBlobUrl && cache?.preloadedUrls?.has(originalUrl)) {
|
||||||
try {
|
try {
|
||||||
const blobUrl = await cache.getCachedBlobUrl(originalUrl);
|
const blobUrl = await cache.getCachedBlobUrl(originalUrl);
|
||||||
|
|||||||
@ -273,9 +273,10 @@ export function usePreloadOrchestrator(
|
|||||||
/**
|
/**
|
||||||
* Create a blob URL from cache and decode if image.
|
* Create a blob URL from cache and decode if image.
|
||||||
* Stores the ready-to-display blob URL in readyBlobUrlsRef.
|
* Stores the ready-to-display blob URL in readyBlobUrlsRef.
|
||||||
|
* If storageKey is provided, also maps the storage key to the blob URL for canonical lookup.
|
||||||
*/
|
*/
|
||||||
const createReadyBlobUrl = useCallback(
|
const createReadyBlobUrl = useCallback(
|
||||||
async (url: string): Promise<void> => {
|
async (url: string, storageKey?: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
// Get blob from Cache API
|
// Get blob from Cache API
|
||||||
const blob = await StorageManager.getAsset(url);
|
const blob = await StorageManager.getAsset(url);
|
||||||
@ -294,12 +295,19 @@ export function usePreloadOrchestrator(
|
|||||||
await decodeImage(blobUrl);
|
await decodeImage(blobUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store ready blob URL
|
// Store ready blob URL keyed by download URL
|
||||||
readyBlobUrlsRef.current.set(url, blobUrl);
|
readyBlobUrlsRef.current.set(url, blobUrl);
|
||||||
preloadedUrls.add(url);
|
preloadedUrls.add(url);
|
||||||
|
|
||||||
|
// Also map storage key for canonical lookup (most reliable)
|
||||||
|
if (storageKey) {
|
||||||
|
readyBlobUrlsRef.current.set(storageKey, blobUrl);
|
||||||
|
preloadedUrls.add(storageKey);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('[PRELOAD] Asset ready', {
|
logger.info('[PRELOAD] Asset ready', {
|
||||||
url: url.slice(-50),
|
url: url.slice(-50),
|
||||||
|
storageKey: storageKey?.slice(-50),
|
||||||
blobUrl: blobUrl.slice(0, 30),
|
blobUrl: blobUrl.slice(0, 30),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -345,7 +353,7 @@ export function usePreloadOrchestrator(
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
logger.info('[PRELOAD] Already cached', { url: item.url.slice(-50) });
|
logger.info('[PRELOAD] Already cached', { url: item.url.slice(-50) });
|
||||||
// Create blob URL and decode - makes asset ready to display instantly
|
// Create blob URL and decode - makes asset ready to display instantly
|
||||||
await createReadyBlobUrl(item.url);
|
await createReadyBlobUrl(item.url, item.storageKey);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,18 +370,34 @@ export function usePreloadOrchestrator(
|
|||||||
logger.info('[PRELOAD] Download complete', {
|
logger.info('[PRELOAD] Download complete', {
|
||||||
url: item.url.slice(-50),
|
url: item.url.slice(-50),
|
||||||
});
|
});
|
||||||
await createReadyBlobUrl(item.url);
|
await createReadyBlobUrl(item.url, item.storageKey);
|
||||||
if (isPresignedUrl(item.url)) {
|
if (isPresignedUrl(item.url)) {
|
||||||
markPresignedUrlsVerified();
|
markPresignedUrlsVerified();
|
||||||
}
|
}
|
||||||
// Also map proxy URL to the same blob URL for fallback lookup
|
// Map proxy URL and storage key to the same blob URL for fallback lookup
|
||||||
if (item.storageKey) {
|
if (item.storageKey) {
|
||||||
const proxyUrl = buildProxyUrl(item.storageKey);
|
|
||||||
const blobUrl = readyBlobUrlsRef.current.get(item.url);
|
const blobUrl = readyBlobUrlsRef.current.get(item.url);
|
||||||
if (blobUrl) {
|
if (blobUrl) {
|
||||||
|
// Map proxy URL for fallback compatibility
|
||||||
|
const proxyUrl = buildProxyUrl(item.storageKey);
|
||||||
readyBlobUrlsRef.current.set(proxyUrl, blobUrl);
|
readyBlobUrlsRef.current.set(proxyUrl, blobUrl);
|
||||||
preloadedUrls.add(proxyUrl);
|
preloadedUrls.add(proxyUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store in Cache API under storage key for post-refresh lookups
|
||||||
|
if (typeof caches !== 'undefined') {
|
||||||
|
try {
|
||||||
|
const cache = await caches.open(OFFLINE_CONFIG.cacheNames.assets);
|
||||||
|
const existingResponse = await cache.match(item.url);
|
||||||
|
if (existingResponse) {
|
||||||
|
await cache.put(item.storageKey, existingResponse.clone());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('[PRELOAD] Failed to store under storage key', {
|
||||||
|
storageKey: item.storageKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(async (err) => {
|
.catch(async (err) => {
|
||||||
@ -396,7 +420,7 @@ export function usePreloadOrchestrator(
|
|||||||
logger.info('[PRELOAD] Proxy download complete', {
|
logger.info('[PRELOAD] Proxy download complete', {
|
||||||
url: proxyUrl.slice(-60),
|
url: proxyUrl.slice(-60),
|
||||||
});
|
});
|
||||||
await createReadyBlobUrl(proxyUrl);
|
await createReadyBlobUrl(proxyUrl, item.storageKey);
|
||||||
} catch (retryErr) {
|
} catch (retryErr) {
|
||||||
logger.error('[PRELOAD] Proxy download also failed', {
|
logger.error('[PRELOAD] Proxy download also failed', {
|
||||||
url: proxyUrl.slice(-60),
|
url: proxyUrl.slice(-60),
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export type ReverseMode = 'none' | 'reverse' | 'separate';
|
|||||||
|
|
||||||
export interface TransitionConfig {
|
export interface TransitionConfig {
|
||||||
videoUrl: string;
|
videoUrl: string;
|
||||||
|
storageKey?: string; // Raw storage path for cache lookup
|
||||||
reverseMode: ReverseMode;
|
reverseMode: ReverseMode;
|
||||||
reverseVideoUrl?: string;
|
reverseVideoUrl?: string;
|
||||||
durationSec?: number;
|
durationSec?: number;
|
||||||
@ -42,6 +43,7 @@ export interface UseTransitionPlaybackOptions {
|
|||||||
preload?: {
|
preload?: {
|
||||||
preloadedUrls?: Set<string>;
|
preloadedUrls?: Set<string>;
|
||||||
getCachedBlobUrl?: (url: string) => Promise<string | null>;
|
getCachedBlobUrl?: (url: string) => Promise<string | null>;
|
||||||
|
getReadyBlobUrl?: (url: string) => string | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,6 +416,38 @@ export function useTransitionPlayback(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resolvePlayableSource = async (): Promise<string> => {
|
const resolvePlayableSource = async (): Promise<string> => {
|
||||||
|
// 1. Try storage key lookup first (most reliable for cache hits)
|
||||||
|
const getReadyBlobUrl = preloadRef.current?.getReadyBlobUrl;
|
||||||
|
const storageKey = currentTransition.storageKey;
|
||||||
|
if (getReadyBlobUrl && storageKey) {
|
||||||
|
const readyUrl = getReadyBlobUrl(storageKey);
|
||||||
|
if (readyUrl) {
|
||||||
|
logger.info('Using ready blob URL from storage key', {
|
||||||
|
storageKey: storageKey.slice(-50),
|
||||||
|
});
|
||||||
|
return readyUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try cached blob URL by storage key (post-refresh scenario)
|
||||||
|
const getCachedBlobUrl = preloadRef.current?.getCachedBlobUrl;
|
||||||
|
if (getCachedBlobUrl && storageKey) {
|
||||||
|
try {
|
||||||
|
const cachedBlobUrl = await getCachedBlobUrl(storageKey);
|
||||||
|
if (cachedBlobUrl) {
|
||||||
|
logger.info('Using cached blob URL from storage key', {
|
||||||
|
storageKey: storageKey.slice(-50),
|
||||||
|
});
|
||||||
|
lastLoadedBlobUrlRef.current = cachedBlobUrl;
|
||||||
|
lastLoadedSourceUrlRef.current = sourceUrl;
|
||||||
|
return cachedBlobUrl;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Reuse cached blob URL if same source (existing logic)
|
||||||
if (
|
if (
|
||||||
lastLoadedBlobUrlRef.current &&
|
lastLoadedBlobUrlRef.current &&
|
||||||
lastLoadedSourceUrlRef.current === sourceUrl
|
lastLoadedSourceUrlRef.current === sourceUrl
|
||||||
@ -439,7 +473,18 @@ export function useTransitionPlayback(
|
|||||||
return sourceUrl;
|
return sourceUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCachedBlobUrl = preloadRef.current?.getCachedBlobUrl;
|
// 4. Try ready blob URL by resolved URL
|
||||||
|
if (getReadyBlobUrl) {
|
||||||
|
const readyUrl = getReadyBlobUrl(sourceUrl);
|
||||||
|
if (readyUrl) {
|
||||||
|
logger.info('Using ready blob URL from resolved URL', {
|
||||||
|
url: sourceUrl.slice(-50),
|
||||||
|
});
|
||||||
|
return readyUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Try cached blob URL by resolved URL
|
||||||
if (getCachedBlobUrl) {
|
if (getCachedBlobUrl) {
|
||||||
try {
|
try {
|
||||||
const cachedBlobUrl = await getCachedBlobUrl(sourceUrl);
|
const cachedBlobUrl = await getCachedBlobUrl(sourceUrl);
|
||||||
@ -458,6 +503,7 @@ export function useTransitionPlayback(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6. Fetch video as blob (network fallback)
|
||||||
logger.info('Fetching video as blob for seeking support', {
|
logger.info('Fetching video as blob for seeking support', {
|
||||||
reverseMode: currentTransition.reverseMode,
|
reverseMode: currentTransition.reverseMode,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -86,8 +86,10 @@ type DragElementState = {
|
|||||||
|
|
||||||
type TransitionPreviewState = {
|
type TransitionPreviewState = {
|
||||||
videoUrl: string;
|
videoUrl: string;
|
||||||
|
storageKey: string; // Raw storage path for cache lookup
|
||||||
reverseMode: 'none' | 'reverse' | 'separate';
|
reverseMode: 'none' | 'reverse' | 'separate';
|
||||||
reverseVideoUrl?: string;
|
reverseVideoUrl?: string;
|
||||||
|
reverseStorageKey?: string; // Raw storage path for reverse video cache lookup
|
||||||
durationSec?: number;
|
durationSec?: number;
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
@ -679,6 +681,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
transition: transitionPreview
|
transition: transitionPreview
|
||||||
? {
|
? {
|
||||||
videoUrl: resolveAssetPlaybackUrl(transitionPreview.videoUrl),
|
videoUrl: resolveAssetPlaybackUrl(transitionPreview.videoUrl),
|
||||||
|
storageKey: transitionPreview.storageKey,
|
||||||
reverseMode: transitionPreview.reverseMode,
|
reverseMode: transitionPreview.reverseMode,
|
||||||
reverseVideoUrl: transitionPreview.reverseVideoUrl
|
reverseVideoUrl: transitionPreview.reverseVideoUrl
|
||||||
? resolveAssetPlaybackUrl(transitionPreview.reverseVideoUrl)
|
? resolveAssetPlaybackUrl(transitionPreview.reverseVideoUrl)
|
||||||
@ -723,6 +726,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
preload: {
|
preload: {
|
||||||
preloadedUrls: preloadOrchestrator.preloadedUrls,
|
preloadedUrls: preloadOrchestrator.preloadedUrls,
|
||||||
getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl,
|
getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl,
|
||||||
|
getReadyBlobUrl: preloadOrchestrator.getReadyBlobUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2019,6 +2023,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
|
|
||||||
setTransitionPreview({
|
setTransitionPreview({
|
||||||
videoUrl: element.transitionVideoUrl,
|
videoUrl: element.transitionVideoUrl,
|
||||||
|
storageKey: element.transitionVideoUrl, // Raw storage path for cache lookup
|
||||||
reverseMode:
|
reverseMode:
|
||||||
direction === 'forward'
|
direction === 'forward'
|
||||||
? 'none'
|
? 'none'
|
||||||
@ -2026,6 +2031,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
? 'separate'
|
? 'separate'
|
||||||
: 'reverse',
|
: 'reverse',
|
||||||
reverseVideoUrl: element.reverseVideoUrl,
|
reverseVideoUrl: element.reverseVideoUrl,
|
||||||
|
reverseStorageKey: element.reverseVideoUrl,
|
||||||
durationSec: element.transitionDurationSec,
|
durationSec: element.transitionDurationSec,
|
||||||
title: `${element.navLabel || element.label} · ${direction}`,
|
title: `${element.navLabel || element.label} · ${direction}`,
|
||||||
});
|
});
|
||||||
@ -2187,12 +2193,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
if (element.iconUrl) {
|
if (element.iconUrl) {
|
||||||
return (
|
return (
|
||||||
<div className='relative h-auto w-auto max-h-[220px] max-w-[220px]'>
|
<div className='relative h-auto w-auto max-h-[220px] max-w-[220px]'>
|
||||||
<NextImage
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
src={resolveAssetPlaybackUrl(element.iconUrl)}
|
src={resolveAssetPlaybackUrl(element.iconUrl)}
|
||||||
alt='Tooltip icon'
|
alt='Tooltip icon'
|
||||||
fill
|
className='max-h-[220px] max-w-[220px] object-contain'
|
||||||
sizes='100vw'
|
|
||||||
className='object-contain'
|
|
||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -2213,12 +2218,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
if (element.iconUrl) {
|
if (element.iconUrl) {
|
||||||
return (
|
return (
|
||||||
<div className='relative h-auto w-auto max-h-[220px] max-w-[220px]'>
|
<div className='relative h-auto w-auto max-h-[220px] max-w-[220px]'>
|
||||||
<NextImage
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
src={resolveAssetPlaybackUrl(element.iconUrl)}
|
src={resolveAssetPlaybackUrl(element.iconUrl)}
|
||||||
alt='Description icon'
|
alt='Description icon'
|
||||||
fill
|
className='max-h-[220px] max-w-[220px] object-contain'
|
||||||
sizes='100vw'
|
|
||||||
className='object-contain'
|
|
||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -2272,12 +2276,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
>
|
>
|
||||||
{card.imageUrl ? (
|
{card.imageUrl ? (
|
||||||
<div className='relative h-full w-full'>
|
<div className='relative h-full w-full'>
|
||||||
<NextImage
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
src={resolveAssetPlaybackUrl(card.imageUrl)}
|
src={resolveAssetPlaybackUrl(card.imageUrl)}
|
||||||
alt={card.title || 'Gallery card'}
|
alt={card.title || 'Gallery card'}
|
||||||
fill
|
className='absolute inset-0 w-full h-full object-cover'
|
||||||
sizes='100vw'
|
|
||||||
className='object-cover'
|
|
||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -2303,12 +2306,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
<div className='h-20 overflow-hidden rounded bg-gray-100'>
|
<div className='h-20 overflow-hidden rounded bg-gray-100'>
|
||||||
{firstSlide?.imageUrl ? (
|
{firstSlide?.imageUrl ? (
|
||||||
<div className='relative h-full w-full'>
|
<div className='relative h-full w-full'>
|
||||||
<NextImage
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
src={resolveAssetPlaybackUrl(firstSlide.imageUrl)}
|
src={resolveAssetPlaybackUrl(firstSlide.imageUrl)}
|
||||||
alt={firstSlide.caption || 'Carousel slide'}
|
alt={firstSlide.caption || 'Carousel slide'}
|
||||||
fill
|
className='absolute inset-0 w-full h-full object-cover'
|
||||||
sizes='100vw'
|
|
||||||
className='object-cover'
|
|
||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -2326,12 +2328,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
<span className='flex items-center gap-1'>
|
<span className='flex items-center gap-1'>
|
||||||
{element.carouselPrevIconUrl ? (
|
{element.carouselPrevIconUrl ? (
|
||||||
<div className='relative h-3 w-3'>
|
<div className='relative h-3 w-3'>
|
||||||
<NextImage
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
src={resolveAssetPlaybackUrl(element.carouselPrevIconUrl)}
|
src={resolveAssetPlaybackUrl(element.carouselPrevIconUrl)}
|
||||||
alt='Previous icon'
|
alt='Previous icon'
|
||||||
fill
|
className='w-3 h-3 object-contain'
|
||||||
sizes='100vw'
|
|
||||||
className='object-contain'
|
|
||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -2342,12 +2343,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
Next
|
Next
|
||||||
{element.carouselNextIconUrl ? (
|
{element.carouselNextIconUrl ? (
|
||||||
<div className='relative h-3 w-3'>
|
<div className='relative h-3 w-3'>
|
||||||
<NextImage
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
src={resolveAssetPlaybackUrl(element.carouselNextIconUrl)}
|
src={resolveAssetPlaybackUrl(element.carouselNextIconUrl)}
|
||||||
alt='Next icon'
|
alt='Next icon'
|
||||||
fill
|
className='w-3 h-3 object-contain'
|
||||||
sizes='100vw'
|
|
||||||
className='object-contain'
|
|
||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -2602,21 +2602,36 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
className='absolute inset-0 bg-black overflow-hidden'
|
className='absolute inset-0 bg-black overflow-hidden'
|
||||||
style={canvasBackgroundStyle}
|
style={canvasBackgroundStyle}
|
||||||
>
|
>
|
||||||
{/* Background image - CSS backgroundImage on canvas provides instant display,
|
{/* Background image - CSS backgroundImage on canvas provides instant display.
|
||||||
NextImage enhances with optimized loading. bg-black prevents white flash. */}
|
Use native img for blob URLs to prevent repeated fetch requests from Next.js Image.
|
||||||
|
NextImage only used for non-blob URLs that benefit from optimization. */}
|
||||||
{backgroundImageSrc ? (
|
{backgroundImageSrc ? (
|
||||||
<div className='absolute inset-0 h-full w-full pointer-events-none select-none'>
|
<div className='absolute inset-0 h-full w-full pointer-events-none select-none'>
|
||||||
<NextImage
|
{backgroundImageSrc.startsWith('blob:') ? (
|
||||||
key={`bg_image_${backgroundImageSrc}`}
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
src={backgroundImageSrc}
|
<img
|
||||||
alt='Background'
|
key={`bg_image_${backgroundImageSrc}`}
|
||||||
fill
|
src={backgroundImageSrc}
|
||||||
sizes='100vw'
|
alt='Background'
|
||||||
className='object-cover'
|
className='absolute inset-0 w-full h-full object-cover'
|
||||||
draggable={false}
|
draggable={false}
|
||||||
onLoad={() => pageSwitch.markBackgroundReady()}
|
onLoad={() => pageSwitch.markBackgroundReady()}
|
||||||
onError={() => pageSwitch.markBackgroundReady()}
|
onError={() => pageSwitch.markBackgroundReady()}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<NextImage
|
||||||
|
key={`bg_image_${backgroundImageSrc}`}
|
||||||
|
src={backgroundImageSrc}
|
||||||
|
alt='Background'
|
||||||
|
fill
|
||||||
|
sizes='100vw'
|
||||||
|
className='object-cover'
|
||||||
|
draggable={false}
|
||||||
|
unoptimized
|
||||||
|
onLoad={() => pageSwitch.markBackgroundReady()}
|
||||||
|
onError={() => pageSwitch.markBackgroundReady()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user