@@ -992,6 +992,81 @@ function App() {
992992 recorder . startRecording ( fullState ) ;
993993 } , [ controllers , viewMode , recorder ] ) ;
994994
995+ // Video Export Logic
996+ const [ isExportingVideo , setIsExportingVideo ] = useState ( false ) ;
997+ const mediaRecorderRef = useRef < MediaRecorder | null > ( null ) ;
998+ const recordedChunksRef = useRef < Blob [ ] > ( [ ] ) ;
999+
1000+ const handleExportVideo = useCallback ( ( ) => {
1001+ // Correct access: viewerRefs is an array of refs, so viewerRefs[0].current
1002+ if ( ! recorder . session || ! viewerRefs [ 0 ] . current ?. container ) {
1003+ if ( ! viewerRefs [ 0 ] . current ?. container ) alert ( "Canvas container not found" ) ;
1004+ return ;
1005+ }
1006+
1007+ const canvas = viewerRefs [ 0 ] . current . container . querySelector ( 'canvas' ) ;
1008+ if ( ! canvas ) {
1009+ alert ( "Could not find canvas for video export." ) ;
1010+ return ;
1011+ }
1012+
1013+ try {
1014+ // High quality capture (60fps)
1015+ const stream = canvas . captureStream ( 60 ) ;
1016+ // Prefer VP9 or H264
1017+ const mimeType = [
1018+ 'video/webm;codecs=vp9' ,
1019+ 'video/webm;codecs=h264' ,
1020+ 'video/webm;codecs=vp8' ,
1021+ 'video/webm'
1022+ ] . find ( type => MediaRecorder . isTypeSupported ( type ) ) || 'video/webm' ;
1023+
1024+ const mediaRecorder = new MediaRecorder ( stream , {
1025+ mimeType,
1026+ videoBitsPerSecond : 8000000 // 8 Mbps high quality
1027+ } ) ;
1028+
1029+ mediaRecorderRef . current = mediaRecorder ;
1030+ recordedChunksRef . current = [ ] ;
1031+
1032+ mediaRecorder . ondataavailable = ( e ) => {
1033+ if ( e . data . size > 0 ) recordedChunksRef . current . push ( e . data ) ;
1034+ } ;
1035+
1036+ mediaRecorder . onstop = ( ) => {
1037+ const blob = new Blob ( recordedChunksRef . current , { type : mimeType } ) ;
1038+ const url = URL . createObjectURL ( blob ) ;
1039+ const a = document . createElement ( 'a' ) ;
1040+ a . href = url ;
1041+ a . download = `${ recorder . session ?. metadata . title || 'session' } .webm` ;
1042+ a . click ( ) ;
1043+ URL . revokeObjectURL ( url ) ;
1044+ setIsExportingVideo ( false ) ;
1045+ } ;
1046+
1047+ // Start Workflow
1048+ setIsExportingVideo ( true ) ;
1049+ mediaRecorder . start ( ) ;
1050+
1051+ // Reset and Play
1052+ recorder . seek ( 0 ) ;
1053+ recorder . play ( ) ;
1054+
1055+ } catch ( err ) {
1056+ console . error ( "Export failed" , err ) ;
1057+ alert ( "Video export failed. See console." ) ;
1058+ setIsExportingVideo ( false ) ;
1059+ }
1060+ } , [ recorder ] ) ;
1061+
1062+ // Monitor Video Export Completion
1063+ useEffect ( ( ) => {
1064+ if ( isExportingVideo && ! recorder . isPlaying && recorder . playbackTime >= ( recorder . session ?. metadata . duration || 0 ) ) {
1065+ // Playback finished naturally
1066+ mediaRecorderRef . current ?. stop ( ) ;
1067+ }
1068+ } , [ isExportingVideo , recorder . isPlaying , recorder . playbackTime , recorder . session ] ) ;
1069+
9951070 const handleStartTour = ( ) => {
9961071 // Determine context (simple check based on dataSource or explicit logic)
9971072 const isChemicalContext = dataSource === 'pubchem' ;
@@ -2376,7 +2451,10 @@ function App() {
23762451 recorderContent = {
23772452 < RecorderControls
23782453 { ...recorder }
2379- startRecording = { handleStartRecording }
2454+ startRecording = { handleStartRecording } // Explicitly pass the no-arg handler
2455+ exportSession = { recorder . exportSession }
2456+ importSession = { recorder . importSession }
2457+ exportVideo = { handleExportVideo }
23802458 isLightMode = { isLightMode }
23812459 cardBg = { isLightMode ? 'bg-white' : 'bg-neutral-900' }
23822460 />
0 commit comments