fix: Wait for images, videos, and fonts to load before capturing frame 0 to fix thumbnails

This commit is contained in:
2026-06-02 23:53:19 -05:00
parent cac5bd9faf
commit 17435e155d
4 changed files with 92 additions and 20 deletions
+41 -7
View File
@@ -6,13 +6,49 @@
* - Error handling for failed loads * - Error handling for failed loads
* - User-select disabled for composition use * - User-select disabled for composition use
*/ */
import React from 'react'; import React, { useEffect, useState } from 'react';
interface ImgProps extends React.ImgHTMLAttributes<HTMLImageElement> { interface ImgProps extends React.ImgHTMLAttributes<HTMLImageElement> {
src: string; src: string;
} }
export const Img: React.FC<ImgProps> = ({ style, ...props }) => ( export const Img: React.FC<ImgProps> = ({ style, onLoad, onError, ...props }) => {
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
if (typeof window !== 'undefined') {
(window as any).__pendingLoads = ((window as any).__pendingLoads || 0) + 1;
}
return () => {
if (typeof window !== 'undefined' && !isLoaded) {
(window as any).__pendingLoads--;
}
};
}, []); // Run once on mount
const handleLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
if (!isLoaded) {
setIsLoaded(true);
if (typeof window !== 'undefined') {
(window as any).__pendingLoads--;
}
}
if (onLoad) onLoad(e);
};
const handleError = (e: React.SyntheticEvent<HTMLImageElement>) => {
if (!isLoaded) {
setIsLoaded(true);
if (typeof window !== 'undefined') {
(window as any).__pendingLoads--;
}
}
const target = e.currentTarget;
target.style.opacity = '0';
if (onError) onError(e);
};
return (
<img <img
draggable={false} draggable={false}
{...props} {...props}
@@ -20,10 +56,8 @@ export const Img: React.FC<ImgProps> = ({ style, ...props }) => (
userSelect: 'none', userSelect: 'none',
...style, ...style,
}} }}
onError={(e) => { onLoad={handleLoad}
// Silently handle broken images in compositions onError={handleError}
const target = e.currentTarget;
target.style.opacity = '0';
}}
/> />
); );
};
+35 -1
View File
@@ -49,6 +49,39 @@ export const Video: React.FC<VideoProps> = ({
const { playing } = usePlayerState(); const { playing } = usePlayerState();
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const wasPlayingRef = useRef(false); const wasPlayingRef = useRef(false);
const [isReady, setIsReady] = useState(false);
// Sync initial load
useEffect(() => {
if (typeof window !== 'undefined') {
(window as any).__pendingLoads = ((window as any).__pendingLoads || 0) + 1;
}
return () => {
if (typeof window !== 'undefined' && !isReady) {
(window as any).__pendingLoads--;
}
};
}, []); // Run once on mount
const handleCanPlay = (e: React.SyntheticEvent<HTMLVideoElement, Event>) => {
if (!isReady) {
setIsReady(true);
if (typeof window !== 'undefined') {
(window as any).__pendingLoads--;
}
}
if (props.onCanPlay) props.onCanPlay(e);
};
const handleError = (e: React.SyntheticEvent<HTMLVideoElement, Event>) => {
if (!isReady) {
setIsReady(true);
if (typeof window !== 'undefined') {
(window as any).__pendingLoads--;
}
}
if (onError) onError(e);
};
// Compute the video's local time // Compute the video's local time
const videoFrame = frame + startFrom; const videoFrame = frame + startFrom;
@@ -136,7 +169,8 @@ export const Video: React.FC<VideoProps> = ({
userSelect: 'none', userSelect: 'none',
...style, ...style,
}} }}
onError={onError} onCanPlay={handleCanPlay}
onError={handleError}
/> />
); );
}; };
+3 -1
View File
@@ -136,7 +136,9 @@ export async function renderFrames(options: RenderFrameOptions): Promise<string[
requestAnimationFrame(() => { requestAnimationFrame(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
const checkVideoSeeks = () => { const checkVideoSeeks = () => {
if (!(window as any).__pendingSeeks) { const pendingSeeks = (window as any).__pendingSeeks || 0;
const pendingLoads = (window as any).__pendingLoads || 0;
if (pendingSeeks === 0 && pendingLoads === 0) {
resolve(); resolve();
} else { } else {
setTimeout(checkVideoSeeks, 10); setTimeout(checkVideoSeeks, 10);
+2
View File
@@ -56,11 +56,13 @@ export const RenderPage: React.FC = () => {
// Update ready state when config changes // Update ready state when config changes
useEffect(() => { useEffect(() => {
if (config && (window as any).__BRADLY_RENDER__) { if (config && (window as any).__BRADLY_RENDER__) {
document.fonts.ready.then(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
(window as any).__BRADLY_RENDER__.ready = true; (window as any).__BRADLY_RENDER__.ready = true;
}); });
}); });
});
} }
}, [config]); }, [config]);