fix: Wait for images, videos, and fonts to load before capturing frame 0 to fix thumbnails
This commit is contained in:
@@ -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';
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user