Skip to content

Commit a27514a

Browse files
AmirMohammad CheraghaliAmirMohammad Cheraghali
authored andcommitted
feat: Implement client-side video export (WebM)
1 parent 848b0b6 commit a27514a

3 files changed

Lines changed: 100 additions & 10 deletions

File tree

src/App.tsx

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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
/>

src/components/ProteinViewer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export interface ProteinViewerRef {
122122
getOrientation: () => any;
123123
setOrientation: (orientation: any) => void;
124124
getPdbBlob: () => Blob | null; // Method to extract current structure as blob
125+
container: HTMLDivElement | null; // Expose container for canvas access
125126
}
126127

127128
export const ProteinViewer = forwardRef<ProteinViewerRef, ProteinViewerProps>(({
@@ -732,6 +733,7 @@ export const ProteinViewer = forwardRef<ProteinViewerRef, ProteinViewerProps>(({
732733
return null;
733734
}
734735
},
736+
container: containerRef.current,
735737
getOrientation: () => {
736738
if (!stageRef.current || !stageRef.current.viewerControls) return null;
737739
const orientation = stageRef.current.viewerControls.getOrientation();

src/components/RecorderControls.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useRef } from 'react';
22
import {
33
Play, Pause, Circle, Square,
4-
Upload, Save
4+
Upload, Save, Film
55
} from 'lucide-react';
66
import type { RecordedSession } from '../types';
77

@@ -21,6 +21,7 @@ interface RecorderControlsProps {
2121
setPlaybackSpeed: (speed: number) => void;
2222
exportSession: () => void;
2323
importSession: (file: File) => void;
24+
exportVideo: () => void;
2425

2526
isLightMode: boolean;
2627
cardBg: string;
@@ -36,7 +37,7 @@ const formatTime = (ms: number) => {
3637
export const RecorderControls = ({
3738
isRecording, isPlaying, recordingTime, playbackTime, session, playbackSpeed,
3839
startRecording, stopRecording, play, pause, seek, setPlaybackSpeed,
39-
exportSession, importSession,
40+
exportSession, importSession, exportVideo,
4041
isLightMode, cardBg
4142
}: RecorderControlsProps) => {
4243

@@ -127,13 +128,22 @@ export const RecorderControls = ({
127128
onChange={(e) => e.target.files?.[0] && importSession(e.target.files[0])}
128129
/>
129130
{session && (
130-
<button
131-
onClick={exportSession}
132-
className="p-1 hover:text-green-500 transition-colors"
133-
title="Save Session"
134-
>
135-
<Save className="w-3.5 h-3.5" />
136-
</button>
131+
<>
132+
<button
133+
onClick={exportVideo}
134+
className="p-1 hover:text-purple-500 transition-colors"
135+
title="Export Video (WebM)"
136+
>
137+
<Film className="w-3.5 h-3.5" />
138+
</button>
139+
<button
140+
onClick={exportSession}
141+
className="p-1 hover:text-green-500 transition-colors"
142+
title="Save Session (JSON)"
143+
>
144+
<Save className="w-3.5 h-3.5" />
145+
</button>
146+
</>
137147
)}
138148
</div>
139149
)}

0 commit comments

Comments
 (0)