Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import { VideoIcon } from "lucide-react";
import ReactPlayer from "react-player";
import { useInView } from "react-intersection-observer";

import { useVideoThumbnail } from "@/hooks/useVideoThumbnail";
Expand Down Expand Up @@ -72,9 +73,38 @@ const LoadingState = React.forwardRef<HTMLDivElement>((_, ref) => (
));
LoadingState.displayName = "LoadingState";

/**
* Fallback video player state
* Shows ReactPlayer when thumbnail generation fails (e.g., due to CORS)
* ReactPlayer handles cross-origin videos without CORS issues
*/
const FallbackVideoPlayer: React.FC<{ url: string }> = ({ url }) => {
return (
<div className="size-full overflow-hidden rounded-sm bg-primary-foreground">
<ReactPlayer
url={url}
width="100%"
height="100%"
playing={false}
muted
light={false}
controls={false}
config={{
file: {
attributes: {
preload: "metadata",
style: { objectFit: "contain" },
},
},
}}
/>
</div>
);
};

/**
* Error/fallback state indicator
* Shows video icon when thumbnail generation fails
* Shows video icon when thumbnail generation fails and video can't be loaded
*/
const ErrorState = React.forwardRef<HTMLDivElement>((_, ref) => (
<ThumbnailContainer ref={ref}>
Expand All @@ -98,11 +128,26 @@ const hasThumbnailFailed = (
return hasError || (!isLoading && !thumbnailUrl);
};

/**
* Check if the URL is cross-origin (different domain)
*/
const isCrossOrigin = (url: string): boolean => {
if (url.startsWith("data:")) return false;

try {
const urlObj = new URL(url, window.location.href);
return urlObj.origin !== window.location.origin;
} catch {
return false;
}
};

/**
* VideoThumbnail component
*
* Generates and displays a thumbnail for a video file with lazy loading support.
* Uses intersection observer to only generate thumbnails when visible in viewport.
* For cross-origin videos, uses ReactPlayer directly to avoid CORS issues.
*
* @example
* ```tsx
Expand All @@ -113,18 +158,25 @@ const hasThumbnailFailed = (
* ```
*/
const VideoThumbnail: React.FC<VideoThumbnailProps> = ({ videoUrl, name }) => {
const isVideoUrlCrossOrigin = isCrossOrigin(videoUrl);

// Only generate thumbnail when component is visible in viewport
const { ref: containerRef, inView: isVisible } =
useInView(INTERSECTION_OPTIONS);

// Generate thumbnail from video
// Generate thumbnail from video (will be skipped for cross-origin URLs in render)
const { thumbnailUrl, isLoading, hasError } = useVideoThumbnail(
videoUrl,
{},
isVisible,
isVisible && !isVideoUrlCrossOrigin,
);

// Render error state
// For cross-origin URLs, use ReactPlayer directly to avoid CORS errors
if (isVideoUrlCrossOrigin) {
return <FallbackVideoPlayer url={videoUrl} />;
}

// Render error state for same-origin videos that failed
if (hasThumbnailFailed(hasError, isLoading, thumbnailUrl)) {
return <ErrorState ref={containerRef} />;
}
Expand Down