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
+50 -16
View File
@@ -6,24 +6,58 @@
* - 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 }) => {
<img const [isLoaded, setIsLoaded] = useState(false);
draggable={false}
{...props} useEffect(() => {
style={{ if (typeof window !== 'undefined') {
userSelect: 'none', (window as any).__pendingLoads = ((window as any).__pendingLoads || 0) + 1;
...style, }
}} return () => {
onError={(e) => { if (typeof window !== 'undefined' && !isLoaded) {
// Silently handle broken images in compositions (window as any).__pendingLoads--;
const target = e.currentTarget; }
target.style.opacity = '0'; };
}} }, []); // 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
draggable={false}
{...props}
style={{
userSelect: 'none',
...style,
}}
onLoad={handleLoad}
onError={handleError}
/>
);
};
+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);
+4 -2
View File
@@ -56,9 +56,11 @@ 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__) {
requestAnimationFrame(() => { document.fonts.ready.then(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
(window as any).__BRADLY_RENDER__.ready = true; requestAnimationFrame(() => {
(window as any).__BRADLY_RENDER__.ready = true;
});
}); });
}); });
} }