|
|
@ -29,6 +29,7 @@ export interface VideoInputNodeViewProps { |
|
|
const MAX_FILE_SIZE = 200 * 1024 * 1024; // 200MB
|
|
|
const MAX_FILE_SIZE = 200 * 1024 * 1024; // 200MB
|
|
|
const ACCEPTED_FORMATS = "video/mp4,video/webm,video/quicktime"; |
|
|
const ACCEPTED_FORMATS = "video/mp4,video/webm,video/quicktime"; |
|
|
const ACCEPTED_MIME_TYPES = ACCEPTED_FORMATS.split(","); |
|
|
const ACCEPTED_MIME_TYPES = ACCEPTED_FORMATS.split(","); |
|
|
|
|
|
const VIDEO_LOAD_RETRY_DELAYS_MS = [600, 1600]; |
|
|
|
|
|
|
|
|
function getVideoNodeTitle(id: string, data: VideoInputNodeData, videoLabel: string) { |
|
|
function getVideoNodeTitle(id: string, data: VideoInputNodeData, videoLabel: string) { |
|
|
if (data.customTitle) return data.customTitle; |
|
|
if (data.customTitle) return data.customTitle; |
|
|
@ -75,11 +76,16 @@ export function VideoInputNodeView({ id, data, selected }: VideoInputNodeViewPro |
|
|
const prevFitKeyRef = useRef<string | null>(null); |
|
|
const prevFitKeyRef = useRef<string | null>(null); |
|
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false); |
|
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false); |
|
|
const [isAssetPickerOpen, setIsAssetPickerOpen] = useState(false); |
|
|
const [isAssetPickerOpen, setIsAssetPickerOpen] = useState(false); |
|
|
|
|
|
const [videoRetryNonce, setVideoRetryNonce] = useState(0); |
|
|
const isUploading = nodeData.uploadStatus === "loading"; |
|
|
const isUploading = nodeData.uploadStatus === "loading"; |
|
|
|
|
|
|
|
|
// Use blob URL for efficient playback of large base64 videos
|
|
|
// Use blob URL for efficient playback of large base64 videos
|
|
|
const playbackUrl = useVideoBlobUrl(nodeData.video ?? null); |
|
|
const playbackUrl = useVideoBlobUrl(nodeData.video ?? null); |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
setVideoRetryNonce(0); |
|
|
|
|
|
}, [playbackUrl]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
if (!nodeData.video || !nodeData.dimensions) { |
|
|
if (!nodeData.video || !nodeData.dimensions) { |
|
|
prevFitKeyRef.current = null; |
|
|
prevFitKeyRef.current = null; |
|
|
@ -387,11 +393,22 @@ export function VideoInputNodeView({ id, data, selected }: VideoInputNodeViewPro |
|
|
className="relative group w-full h-full overflow-clip rounded-lg cursor-zoom-in" |
|
|
className="relative group w-full h-full overflow-clip rounded-lg cursor-zoom-in" |
|
|
> |
|
|
> |
|
|
<video |
|
|
<video |
|
|
|
|
|
key={`${playbackUrl ?? nodeData.previewVideoPoster ?? "empty"}-${videoRetryNonce}`} |
|
|
src={playbackUrl ?? undefined} |
|
|
src={playbackUrl ?? undefined} |
|
|
poster={nodeData.previewVideoPoster} |
|
|
poster={nodeData.previewVideoPoster} |
|
|
controls={Boolean(nodeData.video)} |
|
|
controls={Boolean(nodeData.video)} |
|
|
className="w-full h-full object-contain rounded-lg" |
|
|
className="w-full h-full object-contain rounded-lg" |
|
|
preload="metadata" |
|
|
preload="metadata" |
|
|
|
|
|
onLoadedMetadata={() => setVideoRetryNonce(0)} |
|
|
|
|
|
onError={() => { |
|
|
|
|
|
const retryIndex = videoRetryNonce; |
|
|
|
|
|
const retryDelay = VIDEO_LOAD_RETRY_DELAYS_MS[retryIndex]; |
|
|
|
|
|
if (retryDelay !== undefined && playbackUrl) { |
|
|
|
|
|
window.setTimeout(() => { |
|
|
|
|
|
setVideoRetryNonce((current) => current === retryIndex ? current + 1 : current); |
|
|
|
|
|
}, retryDelay); |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
/> |
|
|
/> |
|
|
{nodeData.isOptional && ( |
|
|
{nodeData.isOptional && ( |
|
|
<span className="absolute bottom-2 left-2 text-[9px] font-medium text-neutral-300 bg-black/50 px-1.5 py-0.5 rounded"> |
|
|
<span className="absolute bottom-2 left-2 text-[9px] font-medium text-neutral-300 bg-black/50 px-1.5 py-0.5 rounded"> |
|
|
|