tmp files & video editor preview
This commit is contained in:
+78
-14
@@ -1,8 +1,8 @@
|
||||
import { BrowserWindow, screen, ipcMain, desktopCapturer, app } from "electron";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
import { uIOhook } from "uiohook-napi";
|
||||
import fs from "node:fs/promises";
|
||||
import { uIOhook } from "uiohook-napi";
|
||||
const __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
|
||||
const APP_ROOT = path.join(__dirname$1, "..");
|
||||
const VITE_DEV_SERVER_URL$1 = process.env["VITE_DEV_SERVER_URL"];
|
||||
@@ -54,7 +54,8 @@ function createEditorWindow() {
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname$1, "preload.mjs"),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true
|
||||
contextIsolation: true,
|
||||
webSecurity: false
|
||||
}
|
||||
});
|
||||
win.webContents.on("did-finish-load", () => {
|
||||
@@ -214,14 +215,13 @@ let selectedSource = null;
|
||||
function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, getMainWindow, getSourceSelectorWindow) {
|
||||
ipcMain.handle("get-sources", async (_, opts) => {
|
||||
const sources = await desktopCapturer.getSources(opts);
|
||||
const processedSources = sources.map((source) => ({
|
||||
return sources.map((source) => ({
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
display_id: source.display_id,
|
||||
thumbnail: source.thumbnail ? source.thumbnail.toDataURL() : null,
|
||||
appIcon: source.appIcon ? source.appIcon.toDataURL() : null
|
||||
}));
|
||||
return processedSources;
|
||||
});
|
||||
ipcMain.handle("select-source", (_, source) => {
|
||||
selectedSource = source;
|
||||
@@ -255,31 +255,90 @@ function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, g
|
||||
ipcMain.handle("stop-mouse-tracking", () => {
|
||||
return stopMouseTracking();
|
||||
});
|
||||
ipcMain.handle("save-mouse-tracking-data", async (_, videoFileName) => {
|
||||
ipcMain.handle("store-recorded-video", async (_, videoData, fileName) => {
|
||||
try {
|
||||
const videoPath = path.join(RECORDINGS_DIR, fileName);
|
||||
await fs.writeFile(videoPath, Buffer.from(videoData));
|
||||
return {
|
||||
success: true,
|
||||
path: videoPath,
|
||||
message: "Video stored successfully"
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to store video:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to store video",
|
||||
error: String(error)
|
||||
};
|
||||
}
|
||||
});
|
||||
ipcMain.handle("store-mouse-tracking-data", async (_, fileName) => {
|
||||
try {
|
||||
const data = getTrackingData();
|
||||
if (data.length === 0) {
|
||||
return { success: false, message: "No tracking data to save" };
|
||||
}
|
||||
const jsonFileName = videoFileName.replace(".webm", "_tracking.json");
|
||||
const filePath = path.join(process.env.HOME || "", "Downloads", jsonFileName);
|
||||
await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
||||
const trackingPath = path.join(RECORDINGS_DIR, fileName);
|
||||
await fs.writeFile(trackingPath, JSON.stringify(data, null, 2), "utf-8");
|
||||
return {
|
||||
success: true,
|
||||
message: "Tracking data saved",
|
||||
filePath,
|
||||
eventCount: data.length
|
||||
path: trackingPath,
|
||||
eventCount: data.length,
|
||||
message: "Mouse tracking data stored successfully"
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to store mouse tracking data:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to save tracking data",
|
||||
message: "Failed to store mouse tracking data",
|
||||
error: String(error)
|
||||
};
|
||||
}
|
||||
});
|
||||
ipcMain.handle("get-recorded-video-path", async () => {
|
||||
try {
|
||||
const files = await fs.readdir(RECORDINGS_DIR);
|
||||
const videoFiles = files.filter((file) => file.endsWith(".webm"));
|
||||
if (videoFiles.length === 0) {
|
||||
return { success: false, message: "No recorded video found" };
|
||||
}
|
||||
const latestVideo = videoFiles.sort().reverse()[0];
|
||||
const videoPath = path.join(RECORDINGS_DIR, latestVideo);
|
||||
return { success: true, path: videoPath };
|
||||
} catch (error) {
|
||||
console.error("Failed to get video path:", error);
|
||||
return { success: false, message: "Failed to get video path", error: String(error) };
|
||||
}
|
||||
});
|
||||
}
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings");
|
||||
async function cleanupOldRecordings() {
|
||||
try {
|
||||
const files = await fs.readdir(RECORDINGS_DIR);
|
||||
const now = Date.now();
|
||||
const maxAge = 7 * 24 * 60 * 60 * 1e3;
|
||||
for (const file of files) {
|
||||
const filePath = path.join(RECORDINGS_DIR, file);
|
||||
const stats = await fs.stat(filePath);
|
||||
if (now - stats.mtimeMs > maxAge) {
|
||||
await fs.unlink(filePath);
|
||||
console.log(`Deleted old recording: ${file}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to cleanup old recordings:", error);
|
||||
}
|
||||
}
|
||||
async function ensureRecordingsDir() {
|
||||
try {
|
||||
await fs.mkdir(RECORDINGS_DIR, { recursive: true });
|
||||
console.log("Recordings directory ready:", RECORDINGS_DIR);
|
||||
} catch (error) {
|
||||
console.error("Failed to create recordings directory:", error);
|
||||
}
|
||||
}
|
||||
process.env.APP_ROOT = path.join(__dirname, "..");
|
||||
const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"];
|
||||
const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron");
|
||||
@@ -316,10 +375,14 @@ app.on("activate", () => {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
app.on("will-quit", () => {
|
||||
app.on("before-quit", async (event) => {
|
||||
event.preventDefault();
|
||||
cleanupMouseTracking();
|
||||
await cleanupOldRecordings();
|
||||
app.exit(0);
|
||||
});
|
||||
app.whenReady().then(() => {
|
||||
app.whenReady().then(async () => {
|
||||
await ensureRecordingsDir();
|
||||
registerIpcHandlers(
|
||||
createEditorWindowWrapper,
|
||||
createSourceSelectorWindowWrapper,
|
||||
@@ -330,6 +393,7 @@ app.whenReady().then(() => {
|
||||
});
|
||||
export {
|
||||
MAIN_DIST,
|
||||
RECORDINGS_DIR,
|
||||
RENDERER_DIST,
|
||||
VITE_DEV_SERVER_URL
|
||||
};
|
||||
|
||||
@@ -22,7 +22,13 @@ electron.contextBridge.exposeInMainWorld("electronAPI", {
|
||||
stopMouseTracking: () => {
|
||||
return electron.ipcRenderer.invoke("stop-mouse-tracking");
|
||||
},
|
||||
saveMouseTrackingData: (videoFileName) => {
|
||||
return electron.ipcRenderer.invoke("save-mouse-tracking-data", videoFileName);
|
||||
storeRecordedVideo: (videoData, fileName) => {
|
||||
return electron.ipcRenderer.invoke("store-recorded-video", videoData, fileName);
|
||||
},
|
||||
storeMouseTrackingData: (fileName) => {
|
||||
return electron.ipcRenderer.invoke("store-mouse-tracking-data", fileName);
|
||||
},
|
||||
getRecordedVideoPath: () => {
|
||||
return electron.ipcRenderer.invoke("get-recorded-video-path");
|
||||
}
|
||||
});
|
||||
|
||||
+53
-26
@@ -2,8 +2,8 @@ import { ipcMain, desktopCapturer, BrowserWindow } from 'electron'
|
||||
import { startMouseTracking, stopMouseTracking, getTrackingData } from './mouseTracking'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { RECORDINGS_DIR } from '../main'
|
||||
|
||||
// Store selected source
|
||||
let selectedSource: any = null
|
||||
|
||||
export function registerIpcHandlers(
|
||||
@@ -12,21 +12,17 @@ export function registerIpcHandlers(
|
||||
getMainWindow: () => BrowserWindow | null,
|
||||
getSourceSelectorWindow: () => BrowserWindow | null
|
||||
) {
|
||||
// Get available desktop capturer sources
|
||||
ipcMain.handle('get-sources', async (_, opts) => {
|
||||
const sources = await desktopCapturer.getSources(opts)
|
||||
const processedSources = sources.map(source => ({
|
||||
return sources.map(source => ({
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
display_id: source.display_id,
|
||||
thumbnail: source.thumbnail ? source.thumbnail.toDataURL() : null,
|
||||
appIcon: source.appIcon ? source.appIcon.toDataURL() : null
|
||||
}))
|
||||
|
||||
return processedSources
|
||||
})
|
||||
|
||||
// Select a source for recording
|
||||
ipcMain.handle('select-source', (_, source) => {
|
||||
selectedSource = source
|
||||
const sourceSelectorWin = getSourceSelectorWindow()
|
||||
@@ -36,12 +32,10 @@ export function registerIpcHandlers(
|
||||
return selectedSource
|
||||
})
|
||||
|
||||
// Get the currently selected source
|
||||
ipcMain.handle('get-selected-source', () => {
|
||||
return selectedSource
|
||||
})
|
||||
|
||||
// Open the source selector window
|
||||
ipcMain.handle('open-source-selector', () => {
|
||||
const sourceSelectorWin = getSourceSelectorWindow()
|
||||
if (sourceSelectorWin) {
|
||||
@@ -51,7 +45,6 @@ export function registerIpcHandlers(
|
||||
createSourceSelectorWindow()
|
||||
})
|
||||
|
||||
// Switch from HUD overlay to editor window
|
||||
ipcMain.handle('switch-to-editor', () => {
|
||||
const mainWin = getMainWindow()
|
||||
if (mainWin) {
|
||||
@@ -60,18 +53,35 @@ export function registerIpcHandlers(
|
||||
createEditorWindow()
|
||||
})
|
||||
|
||||
// Start mouse tracking
|
||||
ipcMain.handle('start-mouse-tracking', () => {
|
||||
return startMouseTracking()
|
||||
})
|
||||
|
||||
// Stop mouse tracking
|
||||
ipcMain.handle('stop-mouse-tracking', () => {
|
||||
return stopMouseTracking()
|
||||
})
|
||||
|
||||
// Save mouse tracking data to file
|
||||
ipcMain.handle('save-mouse-tracking-data', async (_, videoFileName: string) => {
|
||||
ipcMain.handle('store-recorded-video', async (_, videoData: ArrayBuffer, fileName: string) => {
|
||||
try {
|
||||
const videoPath = path.join(RECORDINGS_DIR, fileName)
|
||||
await fs.writeFile(videoPath, Buffer.from(videoData))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
path: videoPath,
|
||||
message: 'Video stored successfully'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to store video:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to store video',
|
||||
error: String(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('store-mouse-tracking-data', async (_, fileName: string) => {
|
||||
try {
|
||||
const data = getTrackingData()
|
||||
|
||||
@@ -79,24 +89,41 @@ export function registerIpcHandlers(
|
||||
return { success: false, message: 'No tracking data to save' }
|
||||
}
|
||||
|
||||
// Save to the same directory as the video, with .json extension
|
||||
const jsonFileName = videoFileName.replace('.webm', '_tracking.json')
|
||||
const filePath = path.join(process.env.HOME || '', 'Downloads', jsonFileName)
|
||||
const trackingPath = path.join(RECORDINGS_DIR, fileName)
|
||||
await fs.writeFile(trackingPath, JSON.stringify(data, null, 2), 'utf-8')
|
||||
|
||||
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Tracking data saved',
|
||||
filePath,
|
||||
eventCount: data.length
|
||||
return {
|
||||
success: true,
|
||||
path: trackingPath,
|
||||
eventCount: data.length,
|
||||
message: 'Mouse tracking data stored successfully'
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to save tracking data',
|
||||
console.error('Failed to store mouse tracking data:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to store mouse tracking data',
|
||||
error: String(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('get-recorded-video-path', async () => {
|
||||
try {
|
||||
const files = await fs.readdir(RECORDINGS_DIR)
|
||||
const videoFiles = files.filter(file => file.endsWith('.webm'))
|
||||
|
||||
if (videoFiles.length === 0) {
|
||||
return { success: false, message: 'No recorded video found' }
|
||||
}
|
||||
|
||||
const latestVideo = videoFiles.sort().reverse()[0]
|
||||
const videoPath = path.join(RECORDINGS_DIR, latestVideo)
|
||||
|
||||
return { success: true, path: videoPath }
|
||||
} catch (error) {
|
||||
console.error('Failed to get video path:', error)
|
||||
return { success: false, message: 'Failed to get video path', error: String(error) }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+42
-2
@@ -1,12 +1,45 @@
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs/promises'
|
||||
import { createHudOverlayWindow, createEditorWindow, createSourceSelectorWindow } from './windows'
|
||||
import { registerIpcHandlers } from './ipc/handlers'
|
||||
import { cleanupMouseTracking } from './ipc/mouseTracking'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export const RECORDINGS_DIR = path.join(app.getPath('userData'), 'recordings')
|
||||
|
||||
// Cleanup old recordings (older than 1 day)
|
||||
async function cleanupOldRecordings() {
|
||||
try {
|
||||
const files = await fs.readdir(RECORDINGS_DIR)
|
||||
const now = Date.now()
|
||||
const maxAge = 1 * 24 * 60 * 60 * 1000
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(RECORDINGS_DIR, file)
|
||||
const stats = await fs.stat(filePath)
|
||||
|
||||
if (now - stats.mtimeMs > maxAge) {
|
||||
await fs.unlink(filePath)
|
||||
console.log(`Deleted old recording: ${file}`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup old recordings:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureRecordingsDir() {
|
||||
try {
|
||||
await fs.mkdir(RECORDINGS_DIR, { recursive: true })
|
||||
console.log('Recordings directory ready:', RECORDINGS_DIR)
|
||||
} catch (error) {
|
||||
console.error('Failed to create recordings directory:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// The built directory structure
|
||||
//
|
||||
// ├─┬─┬ dist
|
||||
@@ -68,12 +101,19 @@ app.on('activate', () => {
|
||||
}
|
||||
})
|
||||
|
||||
app.on('will-quit', () => {
|
||||
// Cleanup old recordings on quit (both macOS and other platforms)
|
||||
app.on('before-quit', async (event) => {
|
||||
event.preventDefault()
|
||||
cleanupMouseTracking()
|
||||
await cleanupOldRecordings()
|
||||
app.exit(0)
|
||||
})
|
||||
|
||||
// Register all IPC handlers when app is ready
|
||||
app.whenReady().then(() => {
|
||||
app.whenReady().then(async () => {
|
||||
// Ensure recordings directory exists
|
||||
await ensureRecordingsDir()
|
||||
|
||||
registerIpcHandlers(
|
||||
createEditorWindowWrapper,
|
||||
createSourceSelectorWindowWrapper,
|
||||
|
||||
+8
-2
@@ -22,7 +22,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
stopMouseTracking: () => {
|
||||
return ipcRenderer.invoke('stop-mouse-tracking')
|
||||
},
|
||||
saveMouseTrackingData: (videoFileName: string) => {
|
||||
return ipcRenderer.invoke('save-mouse-tracking-data', videoFileName)
|
||||
storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => {
|
||||
return ipcRenderer.invoke('store-recorded-video', videoData, fileName)
|
||||
},
|
||||
storeMouseTrackingData: (fileName: string) => {
|
||||
return ipcRenderer.invoke('store-mouse-tracking-data', fileName)
|
||||
},
|
||||
getRecordedVideoPath: () => {
|
||||
return ipcRenderer.invoke('get-recorded-video-path')
|
||||
}
|
||||
})
|
||||
@@ -61,6 +61,7 @@ export function createEditorWindow(): BrowserWindow {
|
||||
preload: path.join(__dirname, 'preload.mjs'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
webSecurity: false,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
+2
-6
@@ -1,5 +1,6 @@
|
||||
import { LaunchWindow } from "./components/LaunchWindow";
|
||||
import { SourceSelector } from "./components/SourceSelector";
|
||||
import { VideoEditor } from "./components/VideoEditor";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function App() {
|
||||
@@ -28,12 +29,7 @@ export default function App() {
|
||||
}
|
||||
|
||||
if (windowType === 'editor') {
|
||||
return (
|
||||
<div className="w-full h-full bg-background text-foreground p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Video Editor</h1>
|
||||
<p>Recording stopped. Video editor interface coming soon...</p>
|
||||
</div>
|
||||
);
|
||||
return <VideoEditor />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function VideoEditor() {
|
||||
const [videoPath, setVideoPath] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadVideo();
|
||||
}, []);
|
||||
|
||||
const loadVideo = async () => {
|
||||
try {
|
||||
const result = await window.electronAPI.getRecordedVideoPath();
|
||||
|
||||
if (result.success && result.path) {
|
||||
setVideoPath(`file://${result.path}`);
|
||||
console.log('Loading video from:', result.path);
|
||||
} else {
|
||||
setError(result.message || 'Failed to load video');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Error loading video: ' + String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-background">
|
||||
<div className="text-foreground">Loading video...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-background">
|
||||
<div className="text-destructive">{error}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-background p-6">
|
||||
<h1 className="text-2xl font-bold mb-4 text-foreground">Video Editor</h1>
|
||||
|
||||
<div className="flex-1 flex items-center justify-center bg-black rounded-lg overflow-hidden">
|
||||
{videoPath && (
|
||||
<video
|
||||
src={videoPath}
|
||||
controls
|
||||
autoPlay
|
||||
className="max-w-full max-h-full"
|
||||
onError={(e) => {
|
||||
console.error('Video playback error:', e);
|
||||
setError('Failed to play video');
|
||||
}}
|
||||
onLoadedMetadata={(e) => {
|
||||
const video = e.currentTarget;
|
||||
console.log('Video loaded:', {
|
||||
duration: video.duration,
|
||||
width: video.videoWidth,
|
||||
height: video.videoHeight
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-sm text-muted-foreground">
|
||||
Video path: {videoPath}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -28,7 +28,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
|
||||
const startRecording = async () => {
|
||||
try {
|
||||
// Get the selected source from the main process
|
||||
const selectedSource = await window.electronAPI.getSelectedSource();
|
||||
|
||||
if (!selectedSource) {
|
||||
@@ -36,10 +35,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start mouse tracking
|
||||
await window.electronAPI.startMouseTracking();
|
||||
|
||||
// Use the selected source
|
||||
const stream = await (navigator.mediaDevices as any).getUserMedia({
|
||||
audio: false,
|
||||
video: {
|
||||
@@ -55,55 +52,90 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
throw new Error("Failed to get media stream");
|
||||
}
|
||||
|
||||
const videoTrack = streamRef.current.getVideoTracks()[0];
|
||||
const settings = videoTrack.getSettings();
|
||||
const width = settings.width || 1920;
|
||||
const height = settings.height || 1080;
|
||||
const totalPixels = width * height;
|
||||
|
||||
let bitrate: number;
|
||||
if (totalPixels <= 1920 * 1080) {
|
||||
bitrate = 150_000_000; // 150 Mbps for 1080p
|
||||
} else if (totalPixels <= 2560 * 1440) {
|
||||
bitrate = 250_000_000; // 250 Mbps for 1440p
|
||||
} else {
|
||||
bitrate = 400_000_000; // 400 Mbps for 4K
|
||||
}
|
||||
|
||||
console.log(`Recording at ${width}x${height} with bitrate: ${bitrate / 1_000_000} Mbps`);
|
||||
|
||||
chunksRef.current = [];
|
||||
let mimeType = "video/webm;codecs=vp9";
|
||||
const mimeType = "video/webm;codecs=vp9";
|
||||
const recorder = new MediaRecorder(streamRef.current, {
|
||||
mimeType,
|
||||
videoBitsPerSecond: 16_000_000,
|
||||
videoBitsPerSecond: bitrate,
|
||||
});
|
||||
mediaRecorderRef.current = recorder;
|
||||
|
||||
recorder.ondataavailable = (e) => {
|
||||
if (e.data && e.data.size > 0) {
|
||||
chunksRef.current.push(e.data);
|
||||
}
|
||||
};
|
||||
|
||||
recorder.onstop = async () => {
|
||||
if (streamRef.current) {
|
||||
streamRef.current.getTracks().forEach((track) => track.stop());
|
||||
streamRef.current = null;
|
||||
}
|
||||
// Don't stop stream here - already stopped in stopRecording for immediate indicator removal
|
||||
// Just cleanup the ref
|
||||
streamRef.current = null;
|
||||
|
||||
if (chunksRef.current.length === 0) return;
|
||||
|
||||
const blob = new Blob(chunksRef.current, { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
const videoBlob = new Blob(chunksRef.current, { type: mimeType });
|
||||
const timestamp = Date.now();
|
||||
const videoFileName = `recording-${timestamp}.webm`;
|
||||
const trackingFileName = `recording-${timestamp}_tracking.json`;
|
||||
|
||||
// Generate filename with timestamp
|
||||
const videoFileName = `recording-${Date.now()}.webm`;
|
||||
a.download = videoFileName;
|
||||
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
|
||||
// Save mouse tracking data alongside the video
|
||||
try {
|
||||
const result = await window.electronAPI.saveMouseTrackingData(videoFileName);
|
||||
if (!result.success) {
|
||||
console.warn('Failed to save tracking data:', result.message);
|
||||
const arrayBuffer = await videoBlob.arrayBuffer();
|
||||
|
||||
console.log(`Saving video: ${videoFileName} (${(arrayBuffer.byteLength / 1024 / 1024).toFixed(2)} MB)`);
|
||||
|
||||
const videoResult = await window.electronAPI.storeRecordedVideo(
|
||||
arrayBuffer,
|
||||
videoFileName
|
||||
);
|
||||
|
||||
if (videoResult.success) {
|
||||
console.log('✅ Video stored:', videoResult.path);
|
||||
} else {
|
||||
console.error('❌ Failed to store video:', videoResult.message);
|
||||
}
|
||||
|
||||
const trackingResult = await window.electronAPI.storeMouseTrackingData(trackingFileName);
|
||||
|
||||
if (trackingResult.success) {
|
||||
console.log('Mouse tracking stored:', trackingResult.path);
|
||||
console.log(`Captured ${trackingResult.eventCount} mouse events`);
|
||||
} else {
|
||||
console.warn('Failed to store tracking:', trackingResult.message);
|
||||
}
|
||||
|
||||
console.log('Opening editor window...');
|
||||
await window.electronAPI.switchToEditor();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error saving tracking data:', error);
|
||||
console.error('Error saving recording:', error);
|
||||
}
|
||||
};
|
||||
|
||||
recorder.onerror = () => {
|
||||
setRecording(false);
|
||||
};
|
||||
|
||||
recorder.start(1000);
|
||||
setRecording(true);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.error('Failed to start recording:', error);
|
||||
setRecording(false);
|
||||
if (streamRef.current) {
|
||||
streamRef.current.getTracks().forEach((track) => track.stop());
|
||||
@@ -117,10 +149,14 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
mediaRecorderRef.current &&
|
||||
mediaRecorderRef.current.state === "recording"
|
||||
) {
|
||||
// Stop stream tracks IMMEDIATELY to remove macOS status indicator
|
||||
if (streamRef.current) {
|
||||
streamRef.current.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
|
||||
mediaRecorderRef.current.stop();
|
||||
setRecording(false);
|
||||
|
||||
// Stop mouse tracking
|
||||
window.electronAPI.stopMouseTracking();
|
||||
}
|
||||
};
|
||||
|
||||
Vendored
+15
-2
@@ -17,11 +17,24 @@ interface Window {
|
||||
getSelectedSource: () => Promise<any>
|
||||
startMouseTracking: () => Promise<{ success: boolean; startTime?: number }>
|
||||
stopMouseTracking: () => Promise<{ success: boolean; data?: any }>
|
||||
saveMouseTrackingData: (videoFileName: string) => Promise<{
|
||||
storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{
|
||||
success: boolean
|
||||
path?: string
|
||||
message: string
|
||||
filePath?: string
|
||||
error?: string
|
||||
}>
|
||||
storeMouseTrackingData: (fileName: string) => Promise<{
|
||||
success: boolean
|
||||
path?: string
|
||||
eventCount?: number
|
||||
message: string
|
||||
error?: string
|
||||
}>
|
||||
getRecordedVideoPath: () => Promise<{
|
||||
success: boolean
|
||||
path?: string
|
||||
message?: string
|
||||
error?: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user