@@ -6,9 +6,19 @@ import {
66 useInternalNode ,
77} from '@xyflow/react' ;
88import { ScanSearch , Sparkles } from 'lucide-react' ;
9+ import {
10+ useCallback ,
11+ useEffect ,
12+ useState ,
13+ type CSSProperties ,
14+ type MouseEventHandler ,
15+ } from 'react' ;
916
1017import type { GenerationTake } from '@/core/beatcanvas/generation-history' ;
11- import { seekStaticVideoPreview } from '@/core/media/video-preview' ;
18+ import {
19+ captureStaticVideoPreview ,
20+ seekStaticVideoPreview ,
21+ } from '@/core/media/video-preview' ;
1222
1323import { getBeatCanvasNodeCopy } from './beatcanvas-node-copy' ;
1424import type { BeatCanvasFlowNode } from '../react-flow/beatcanvas-react-flow-types' ;
@@ -22,6 +32,86 @@ const FRAME_BACKGROUND =
2232 'linear-gradient(180deg, var(--beat-surface-2) 0%, var(--beat-surface) 100%)' ;
2333const PLACEHOLDER_COLOR = 'rgba(255, 255, 255, 0.22)' ;
2434
35+ function StaticVideoPoster ( {
36+ src,
37+ alt,
38+ maxEdge = 512 ,
39+ cursor = 'default' ,
40+ onDoubleClick,
41+ } : {
42+ src : string ;
43+ alt : string ;
44+ maxEdge ?: number ;
45+ cursor ?: CSSProperties [ 'cursor' ] ;
46+ onDoubleClick ?: MouseEventHandler < HTMLDivElement > ;
47+ } ) {
48+ const [ poster , setPoster ] = useState < string | null > ( null ) ;
49+
50+ useEffect ( ( ) => setPoster ( null ) , [ src ] ) ;
51+
52+ const captureFrame = useCallback (
53+ ( video : HTMLVideoElement ) => {
54+ const nextPoster = captureStaticVideoPreview ( video , maxEdge ) ;
55+ if ( nextPoster ) setPoster ( ( current ) => current ?? nextPoster ) ;
56+ } ,
57+ [ maxEdge ]
58+ ) ;
59+
60+ return (
61+ < div
62+ className = "nowheel"
63+ onDoubleClick = { onDoubleClick }
64+ style = { {
65+ position : 'relative' ,
66+ width : '100%' ,
67+ height : '100%' ,
68+ overflow : 'hidden' ,
69+ cursor,
70+ background :
71+ 'linear-gradient(145deg, var(--beat-surface-2), var(--beat-surface))' ,
72+ } }
73+ >
74+ < video
75+ src = { src }
76+ muted
77+ playsInline
78+ preload = "metadata"
79+ className = "nowheel"
80+ aria-hidden = "true"
81+ tabIndex = { - 1 }
82+ onLoadedMetadata = { ( event ) =>
83+ seekStaticVideoPreview ( event . currentTarget )
84+ }
85+ onLoadedData = { ( event ) => captureFrame ( event . currentTarget ) }
86+ onSeeked = { ( event ) => captureFrame ( event . currentTarget ) }
87+ style = { {
88+ position : 'absolute' ,
89+ inset : 0 ,
90+ width : '100%' ,
91+ height : '100%' ,
92+ objectFit : 'cover' ,
93+ opacity : 0 ,
94+ pointerEvents : 'none' ,
95+ } }
96+ />
97+ { poster ? (
98+ < img
99+ src = { poster }
100+ alt = { alt }
101+ draggable = { false }
102+ className = "nowheel"
103+ style = { {
104+ display : 'block' ,
105+ width : '100%' ,
106+ height : '100%' ,
107+ objectFit : 'cover' ,
108+ } }
109+ />
110+ ) : null }
111+ </ div >
112+ ) ;
113+ }
114+
25115export function GenerationCardNode ( {
26116 id,
27117 data,
@@ -286,28 +376,16 @@ export function GenerationCardNode({
286376 </ div >
287377 </ div >
288378 ) : cardMediaType === 'video' ? (
289- < video
290- src = { latestOutputUrl ?? undefined }
291- muted
292- playsInline
293- preload = "metadata"
294- onLoadedMetadata = { ( event ) =>
295- seekStaticVideoPreview ( event . currentTarget )
296- }
297- draggable = { false }
379+ < StaticVideoPoster
380+ key = { latestOutputUrl }
381+ src = { latestOutputUrl ?? '' }
382+ alt = { displayLabel }
383+ cursor = "zoom-in"
298384 onDoubleClick = { ( event ) => {
299385 event . preventDefault ( ) ;
300386 event . stopPropagation ( ) ;
301387 handlePreviewLatestOutput ( ) ;
302388 } }
303- className = "nowheel"
304- style = { {
305- display : 'block' ,
306- width : '100%' ,
307- height : '100%' ,
308- objectFit : 'cover' ,
309- cursor : 'zoom-in' ,
310- } }
311389 />
312390 ) : (
313391 < img
@@ -491,21 +569,11 @@ export function GenerationCardNode({
491569 } }
492570 >
493571 { take . url && take . type === 'video' ? (
494- < video
572+ < StaticVideoPoster
573+ key = { take . url }
495574 src = { take . url }
496- muted
497- playsInline
498- preload = "metadata"
499- className = "nowheel"
500- onLoadedMetadata = { ( event ) =>
501- seekStaticVideoPreview ( event . currentTarget )
502- }
503- style = { {
504- display : 'block' ,
505- width : '100%' ,
506- height : '100%' ,
507- objectFit : 'cover' ,
508- } }
575+ alt = ""
576+ maxEdge = { 96 }
509577 />
510578 ) : take . url ? (
511579 < img
0 commit comments