- {/* Video preview */}
-
-
-
0}
- shadowIntensity={shadowIntensity}
- showBlur={showBlur}
- motionBlurAmount={motionBlurAmount}
- borderRadius={borderRadius}
- padding={padding}
- cropRegion={cropRegion}
- trimRegions={trimRegions}
- speedRegions={speedRegions}
- annotationRegions={annotationRegions}
- selectedAnnotationId={selectedAnnotationId}
- onSelectAnnotation={handleSelectAnnotation}
- onAnnotationPositionChange={handleAnnotationPositionChange}
- onAnnotationSizeChange={handleAnnotationSizeChange}
- />
+
+
+
+ {/* Top section: video preview and controls */}
+
+
+ {/* Video preview */}
+
+
+ 0}
+ shadowIntensity={shadowIntensity}
+ showBlur={showBlur}
+ motionBlurAmount={motionBlurAmount}
+ borderRadius={borderRadius}
+ padding={padding}
+ cropRegion={cropRegion}
+ trimRegions={trimRegions}
+ speedRegions={speedRegions}
+ annotationRegions={annotationRegions}
+ selectedAnnotationId={selectedAnnotationId}
+ onSelectAnnotation={handleSelectAnnotation}
+ onAnnotationPositionChange={handleAnnotationPositionChange}
+ onAnnotationSizeChange={handleAnnotationSizeChange}
+ />
+
+
+ {/* Playback controls */}
+
- {/* Playback controls */}
-
-
+
+
+
+
+
+
+ {/* Timeline section */}
+
+
+ pushState({ aspectRatio: ar })}
+ />
-
-
+
+
+
-
-
-
+
+
+
- {/* Timeline section */}
-
-
- pushState({ aspectRatio: ar })}
- />
-
-
-
-
-
- {/* Right section: settings panel */}
-
pushState({ wallpaper: w })}
- selectedZoomDepth={
- selectedZoomId ? zoomRegions.find((z) => z.id === selectedZoomId)?.depth : null
- }
- onZoomDepthChange={(depth) => selectedZoomId && handleZoomDepthChange(depth)}
- selectedZoomId={selectedZoomId}
- onZoomDelete={handleZoomDelete}
- selectedTrimId={selectedTrimId}
- onTrimDelete={handleTrimDelete}
- shadowIntensity={shadowIntensity}
- onShadowChange={(v) => updateState({ shadowIntensity: v })}
- onShadowCommit={commitState}
- showBlur={showBlur}
- onBlurChange={(v) => pushState({ showBlur: v })}
- motionBlurAmount={motionBlurAmount}
- onMotionBlurChange={(v) => updateState({ motionBlurAmount: v })}
- onMotionBlurCommit={commitState}
- borderRadius={borderRadius}
- onBorderRadiusChange={(v) => updateState({ borderRadius: v })}
- onBorderRadiusCommit={commitState}
- padding={padding}
- onPaddingChange={(v) => updateState({ padding: v })}
- onPaddingCommit={commitState}
- cropRegion={cropRegion}
- onCropChange={(r) => pushState({ cropRegion: r })}
- aspectRatio={aspectRatio}
- videoElement={videoPlaybackRef.current?.video || null}
- exportQuality={exportQuality}
- onExportQualityChange={setExportQuality}
- exportFormat={exportFormat}
- onExportFormatChange={setExportFormat}
- gifFrameRate={gifFrameRate}
- onGifFrameRateChange={setGifFrameRate}
- gifLoop={gifLoop}
- onGifLoopChange={setGifLoop}
- gifSizePreset={gifSizePreset}
- onGifSizePresetChange={setGifSizePreset}
- gifOutputDimensions={calculateOutputDimensions(
- videoPlaybackRef.current?.video?.videoWidth || 1920,
- videoPlaybackRef.current?.video?.videoHeight || 1080,
- gifSizePreset,
- GIF_SIZE_PRESETS,
- )}
- onExport={handleOpenExportDialog}
- selectedAnnotationId={selectedAnnotationId}
- annotationRegions={annotationRegions}
- onAnnotationContentChange={handleAnnotationContentChange}
- onAnnotationTypeChange={handleAnnotationTypeChange}
- onAnnotationStyleChange={handleAnnotationStyleChange}
- onAnnotationFigureDataChange={handleAnnotationFigureDataChange}
- onAnnotationDelete={handleAnnotationDelete}
- onSaveProject={handleSaveProject}
- onLoadProject={handleLoadProject}
- selectedSpeedId={selectedSpeedId}
- selectedSpeedValue={
- selectedSpeedId
- ? (speedRegions.find((r) => r.id === selectedSpeedId)?.speed ?? null)
- : null
- }
- onSpeedChange={handleSpeedChange}
- onSpeedDelete={handleSpeedDelete}
- />
+ {/* Right section: settings panel */}
+
+ pushState({ wallpaper: w })}
+ selectedZoomDepth={
+ selectedZoomId ? zoomRegions.find((z) => z.id === selectedZoomId)?.depth : null
+ }
+ onZoomDepthChange={(depth) => selectedZoomId && handleZoomDepthChange(depth)}
+ selectedZoomId={selectedZoomId}
+ onZoomDelete={handleZoomDelete}
+ selectedTrimId={selectedTrimId}
+ onTrimDelete={handleTrimDelete}
+ shadowIntensity={shadowIntensity}
+ onShadowChange={(v) => updateState({ shadowIntensity: v })}
+ onShadowCommit={commitState}
+ showBlur={showBlur}
+ onBlurChange={(v) => pushState({ showBlur: v })}
+ motionBlurAmount={motionBlurAmount}
+ onMotionBlurChange={(v) => updateState({ motionBlurAmount: v })}
+ onMotionBlurCommit={commitState}
+ borderRadius={borderRadius}
+ onBorderRadiusChange={(v) => updateState({ borderRadius: v })}
+ onBorderRadiusCommit={commitState}
+ padding={padding}
+ onPaddingChange={(v) => updateState({ padding: v })}
+ onPaddingCommit={commitState}
+ cropRegion={cropRegion}
+ onCropChange={(r) => pushState({ cropRegion: r })}
+ aspectRatio={aspectRatio}
+ videoElement={videoPlaybackRef.current?.video || null}
+ exportQuality={exportQuality}
+ onExportQualityChange={setExportQuality}
+ exportFormat={exportFormat}
+ onExportFormatChange={setExportFormat}
+ gifFrameRate={gifFrameRate}
+ onGifFrameRateChange={setGifFrameRate}
+ gifLoop={gifLoop}
+ onGifLoopChange={setGifLoop}
+ gifSizePreset={gifSizePreset}
+ onGifSizePresetChange={setGifSizePreset}
+ gifOutputDimensions={calculateOutputDimensions(
+ videoPlaybackRef.current?.video?.videoWidth || 1920,
+ videoPlaybackRef.current?.video?.videoHeight || 1080,
+ gifSizePreset,
+ GIF_SIZE_PRESETS,
+ )}
+ onExport={handleOpenExportDialog}
+ selectedAnnotationId={selectedAnnotationId}
+ annotationRegions={annotationRegions}
+ onAnnotationContentChange={handleAnnotationContentChange}
+ onAnnotationTypeChange={handleAnnotationTypeChange}
+ onAnnotationStyleChange={handleAnnotationStyleChange}
+ onAnnotationFigureDataChange={handleAnnotationFigureDataChange}
+ onAnnotationDelete={handleAnnotationDelete}
+ onSaveProject={handleSaveProject}
+ onLoadProject={handleLoadProject}
+ selectedSpeedId={selectedSpeedId}
+ selectedSpeedValue={
+ selectedSpeedId
+ ? (speedRegions.find((r) => r.id === selectedSpeedId)?.speed ?? null)
+ : null
+ }
+ onSpeedChange={handleSpeedChange}
+ onSpeedDelete={handleSpeedDelete}
+ />
+
+
diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts
index 19893f1..707b94f 100644
--- a/src/hooks/useScreenRecorder.ts
+++ b/src/hooks/useScreenRecorder.ts
@@ -40,6 +40,7 @@ const WEBCAM_TARGET_FRAME_RATE = 30;
type UseScreenRecorderReturn = {
recording: boolean;
toggleRecording: () => void;
+ restartRecording: () => void;
microphoneEnabled: boolean;
setMicrophoneEnabled: (enabled: boolean) => void;
microphoneDeviceId: string | undefined;
@@ -94,6 +95,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
const recordingId = useRef
(0);
const finalizingRecordingId = useRef(null);
const allowAutoFinalize = useRef(false);
+ const discardRecordingId = useRef(null);
+ const restarting = useRef(false);
const selectMimeType = () => {
const preferred = [
@@ -206,6 +209,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
void (async () => {
try {
const screenBlob = await activeScreenRecorder.recordedBlobPromise;
+ if (discardRecordingId.current === activeRecordingId) {
+ return;
+ }
if (screenBlob.size === 0) {
return;
}
@@ -253,6 +259,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
if (finalizingRecordingId.current === activeRecordingId) {
finalizingRecordingId.current = null;
}
+ if (discardRecordingId.current === activeRecordingId) {
+ discardRecordingId.current = null;
+ }
}
})();
},
@@ -306,6 +315,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
return () => {
if (cleanup) cleanup();
allowAutoFinalize.current = false;
+ restarting.current = false;
+ discardRecordingId.current = null;
if (screenRecorder.current?.recorder.state === "recording") {
try {
@@ -546,9 +557,48 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
recording ? stopRecording.current() : startRecording();
};
+ const restartRecording = async () => {
+ if (restarting.current) return;
+
+ const activeScreenRecorder = screenRecorder.current;
+ if (!activeScreenRecorder || activeScreenRecorder.recorder.state !== "recording") return;
+
+ const activeWebcamRecorder = webcamRecorder.current;
+ const activeRecordingId = recordingId.current;
+
+ restarting.current = true;
+ discardRecordingId.current = activeRecordingId;
+
+ const stopPromises = [
+ new Promise((resolve) => {
+ activeScreenRecorder.recorder.addEventListener("stop", () => resolve(), { once: true });
+ }),
+ ];
+
+ if (activeWebcamRecorder?.recorder.state === "recording") {
+ stopPromises.push(
+ new Promise((resolve) => {
+ activeWebcamRecorder.recorder.addEventListener("stop", () => resolve(), {
+ once: true,
+ });
+ }),
+ );
+ }
+
+ stopRecording.current();
+ await Promise.all(stopPromises);
+
+ try {
+ await startRecording();
+ } finally {
+ restarting.current = false;
+ }
+ };
+
return {
recording,
toggleRecording,
+ restartRecording,
microphoneEnabled,
setMicrophoneEnabled,
microphoneDeviceId,