|
|
|
@ -129,6 +129,7 @@ export function GenerateVideoNodeView({ id, data, selected }: GenerateVideoNodeV |
|
|
|
const nodeData = data; |
|
|
|
const { t } = useI18n(); |
|
|
|
const updateNodeData = useWorkflowStore((state) => state.updateNodeData); |
|
|
|
const isNodeHovered = useWorkflowStore((state) => state.hoveredNodeId === id); |
|
|
|
const updateVideoPreference = useGenerationPreferenceStore((state) => state.updateVideoPreference); |
|
|
|
const popiserverVideoModels = useModelStore((state) => state.byKind.video); |
|
|
|
const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false); |
|
|
|
@ -136,14 +137,28 @@ export function GenerateVideoNodeView({ id, data, selected }: GenerateVideoNodeV |
|
|
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false); |
|
|
|
const [isSaveToAssetLibraryOpen, setIsSaveToAssetLibraryOpen] = useState(false); |
|
|
|
const [videoRetryNonce, setVideoRetryNonce] = useState(0); |
|
|
|
|
|
|
|
const videoBlobUrl = useVideoBlobUrl(nodeData.outputVideo ?? null); |
|
|
|
const [hasRequestedVideoPlayback, setHasRequestedVideoPlayback] = useState(false); |
|
|
|
const [isVideoReady, setIsVideoReady] = useState(false); |
|
|
|
|
|
|
|
const currentVideoHistoryItem = nodeData.videoHistory?.[nodeData.selectedVideoHistoryIndex || 0]; |
|
|
|
const previewVideoPoster = (nodeData as GenerateVideoNodeData & { previewVideoPoster?: string }).previewVideoPoster; |
|
|
|
const videoPoster = nodeData.outputVideoCover || currentVideoHistoryItem?.videoCover || previewVideoPoster; |
|
|
|
const shouldRequestVideoPlayback = Boolean(nodeData.outputVideo && (!videoPoster || selected || isNodeHovered || hasRequestedVideoPlayback)); |
|
|
|
const videoBlobUrl = useVideoBlobUrl(shouldRequestVideoPlayback ? nodeData.outputVideo ?? null : null); |
|
|
|
const videoAutoplayRef = useVideoAutoplay(id, selected); |
|
|
|
usePreventVideoFullscreen(videoAutoplayRef); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
setVideoRetryNonce(0); |
|
|
|
}, [videoBlobUrl]); |
|
|
|
setHasRequestedVideoPlayback(false); |
|
|
|
setIsVideoReady(false); |
|
|
|
}, [nodeData.outputVideo]); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if (nodeData.outputVideo && (selected || isNodeHovered || !videoPoster)) { |
|
|
|
setHasRequestedVideoPlayback(true); |
|
|
|
} |
|
|
|
}, [isNodeHovered, nodeData.outputVideo, videoPoster, selected]); |
|
|
|
|
|
|
|
// Inline parameters infrastructure
|
|
|
|
const { inlineParametersEnabled } = useInlineParameters(); |
|
|
|
@ -269,6 +284,7 @@ export function GenerateVideoNodeView({ id, data, selected }: GenerateVideoNodeV |
|
|
|
if (history.length === 0) { |
|
|
|
updateNodeData(id, { |
|
|
|
outputVideo: null, |
|
|
|
outputVideoCover: undefined, |
|
|
|
outputVideoRef: undefined, |
|
|
|
outputVideoRemoteUrl: undefined, |
|
|
|
outputVideoRemoteExpiresAt: undefined, |
|
|
|
@ -286,6 +302,7 @@ export function GenerateVideoNodeView({ id, data, selected }: GenerateVideoNodeV |
|
|
|
if (nextHistory.length === 0) { |
|
|
|
updateNodeData(id, { |
|
|
|
outputVideo: null, |
|
|
|
outputVideoCover: undefined, |
|
|
|
outputVideoRef: undefined, |
|
|
|
outputVideoRemoteUrl: undefined, |
|
|
|
outputVideoRemoteExpiresAt: undefined, |
|
|
|
@ -306,6 +323,7 @@ export function GenerateVideoNodeView({ id, data, selected }: GenerateVideoNodeV |
|
|
|
|
|
|
|
updateNodeData(id, { |
|
|
|
outputVideo: nextVideo, |
|
|
|
outputVideoCover: nextItem.videoCover, |
|
|
|
outputVideoRef: undefined, |
|
|
|
outputVideoRemoteUrl: nextVideo && /^https?:\/\//i.test(nextVideo) ? nextVideo : undefined, |
|
|
|
outputVideoRemoteExpiresAt: undefined, |
|
|
|
@ -333,6 +351,7 @@ export function GenerateVideoNodeView({ id, data, selected }: GenerateVideoNodeV |
|
|
|
if (video) { |
|
|
|
updateNodeData(id, { |
|
|
|
outputVideo: video, |
|
|
|
outputVideoCover: videoItem.videoCover, |
|
|
|
selectedVideoHistoryIndex: newIndex, |
|
|
|
status: "idle", |
|
|
|
error: null, |
|
|
|
@ -355,6 +374,7 @@ export function GenerateVideoNodeView({ id, data, selected }: GenerateVideoNodeV |
|
|
|
if (video) { |
|
|
|
updateNodeData(id, { |
|
|
|
outputVideo: video, |
|
|
|
outputVideoCover: videoItem.videoCover, |
|
|
|
selectedVideoHistoryIndex: newIndex, |
|
|
|
status: "idle", |
|
|
|
error: null, |
|
|
|
@ -429,6 +449,7 @@ export function GenerateVideoNodeView({ id, data, selected }: GenerateVideoNodeV |
|
|
|
}, [nodeData.outputVideoStorageStatus]); |
|
|
|
|
|
|
|
const hasCarouselVideos = (nodeData.videoHistory || []).length > 1; |
|
|
|
const shouldRenderVideoElement = Boolean(nodeData.outputVideo && (!videoPoster || hasRequestedVideoPlayback)); |
|
|
|
|
|
|
|
// Track previous status to detect error transitions
|
|
|
|
const prevStatusRef = useRef(nodeData.status); |
|
|
|
@ -603,30 +624,48 @@ export function GenerateVideoNodeView({ id, data, selected }: GenerateVideoNodeV |
|
|
|
{/* Preview area */} |
|
|
|
{nodeData.outputVideo ? ( |
|
|
|
<> |
|
|
|
<video |
|
|
|
ref={videoAutoplayRef} |
|
|
|
key={`${nodeData.videoHistory?.[nodeData.selectedVideoHistoryIndex || 0]?.id ?? nodeData.outputVideo}-${videoRetryNonce}`} |
|
|
|
src={videoBlobUrl ?? undefined} |
|
|
|
controls |
|
|
|
controlsList="nofullscreen noremoteplayback" |
|
|
|
disablePictureInPicture |
|
|
|
loop |
|
|
|
muted |
|
|
|
className="w-full h-full object-contain" |
|
|
|
playsInline |
|
|
|
onDoubleClickCapture={preventVideoDoubleClick} |
|
|
|
onDoubleClick={preventVideoDoubleClick} |
|
|
|
onLoadedMetadata={() => setVideoRetryNonce(0)} |
|
|
|
onError={() => { |
|
|
|
const retryIndex = videoRetryNonce; |
|
|
|
const retryDelay = VIDEO_LOAD_RETRY_DELAYS_MS[retryIndex]; |
|
|
|
if (retryDelay !== undefined && videoBlobUrl) { |
|
|
|
window.setTimeout(() => { |
|
|
|
setVideoRetryNonce((current) => current === retryIndex ? current + 1 : current); |
|
|
|
}, retryDelay); |
|
|
|
} |
|
|
|
}} |
|
|
|
/> |
|
|
|
{videoPoster && ( |
|
|
|
<img |
|
|
|
src={videoPoster} |
|
|
|
alt="" |
|
|
|
className={`absolute inset-0 h-full w-full object-contain bg-black transition-opacity duration-150 ${ |
|
|
|
isVideoReady ? "opacity-0" : "opacity-100" |
|
|
|
}`}
|
|
|
|
draggable={false} |
|
|
|
/> |
|
|
|
)} |
|
|
|
{shouldRenderVideoElement && ( |
|
|
|
<video |
|
|
|
ref={videoAutoplayRef} |
|
|
|
key={`${currentVideoHistoryItem?.id ?? nodeData.outputVideo}-${videoRetryNonce}`} |
|
|
|
src={videoBlobUrl ?? undefined} |
|
|
|
poster={videoPoster} |
|
|
|
controls |
|
|
|
controlsList="nofullscreen noremoteplayback" |
|
|
|
disablePictureInPicture |
|
|
|
loop |
|
|
|
muted |
|
|
|
className={`${videoPoster ? "absolute inset-0 " : ""}w-full h-full object-contain`} |
|
|
|
playsInline |
|
|
|
preload={videoPoster ? "auto" : undefined} |
|
|
|
onDoubleClickCapture={preventVideoDoubleClick} |
|
|
|
onDoubleClick={preventVideoDoubleClick} |
|
|
|
onLoadedMetadata={() => { |
|
|
|
setVideoRetryNonce(0); |
|
|
|
setIsVideoReady(true); |
|
|
|
}} |
|
|
|
onError={() => { |
|
|
|
setIsVideoReady(false); |
|
|
|
const retryIndex = videoRetryNonce; |
|
|
|
const retryDelay = VIDEO_LOAD_RETRY_DELAYS_MS[retryIndex]; |
|
|
|
if (retryDelay !== undefined && videoBlobUrl) { |
|
|
|
window.setTimeout(() => { |
|
|
|
setVideoRetryNonce((current) => current === retryIndex ? current + 1 : current); |
|
|
|
}, retryDelay); |
|
|
|
} |
|
|
|
}} |
|
|
|
/> |
|
|
|
)} |
|
|
|
{storageBadge && ( |
|
|
|
<div |
|
|
|
className={`absolute top-7 right-1 px-1.5 py-0.5 rounded text-[9px] font-medium pointer-events-auto z-10 ${storageBadge.className}`} |
|
|
|
|