node-mac-recorder 2.21.40 → 2.21.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +29 -1
- package/CREAVIT_CODE_SNIPPETS.md +832 -0
- package/CREAVIT_INTEGRATION.md +590 -0
- package/CURSOR_MAPPING.md +112 -0
- package/DUAL_RECORDING_PLAN.md +243 -0
- package/MULTI_RECORDING.md +270 -0
- package/MultiWindowRecorder.js +546 -0
- package/README.md +51 -0
- package/binding.gyp +1 -0
- package/index-multiprocess.js +238 -0
- package/index.js +174 -19
- package/package.json +1 -1
- package/recorder-worker.js +399 -0
- package/src/audio_mixer.mm +269 -0
- package/src/audio_recorder.mm +9 -0
- package/src/camera_recorder.mm +457 -702
- package/src/cursor_tracker.mm +75 -60
- package/src/mac_recorder.mm +305 -68
- package/src/screen_capture_kit.h +18 -5
- package/src/screen_capture_kit.mm +1113 -433
- package/cursor-data-1751364226346.json +0 -1
- package/cursor-data-1751364314136.json +0 -1
- package/cursor-data.json +0 -1
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
# Creavit Desktop Integration - Code Snippets
|
|
2
|
+
|
|
3
|
+
## 📦 Kurulum
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# creavit.studio/desktop dizininde
|
|
7
|
+
npm install --save /path/to/node-mac-recorder
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## 🔧 Main Process (Electron)
|
|
11
|
+
|
|
12
|
+
### 1. MultiWindowRecorder Import
|
|
13
|
+
|
|
14
|
+
**Dosya:** `src/main/recording/index.ts`
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import MultiWindowRecorder from 'node-mac-recorder/MultiWindowRecorder';
|
|
18
|
+
|
|
19
|
+
// Global recorder instance
|
|
20
|
+
let currentMultiRecorder: MultiWindowRecorder | null = null;
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 2. IPC Handlers
|
|
24
|
+
|
|
25
|
+
**Dosya:** `src/main/ipc/recording-handlers.ts`
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { ipcMain } from 'electron';
|
|
29
|
+
import MultiWindowRecorder from 'node-mac-recorder/MultiWindowRecorder';
|
|
30
|
+
import path from 'path';
|
|
31
|
+
import { app } from 'electron';
|
|
32
|
+
|
|
33
|
+
let multiRecorder: MultiWindowRecorder | null = null;
|
|
34
|
+
|
|
35
|
+
// Initialize Multi-Window Recorder
|
|
36
|
+
ipcMain.handle('recording:multi-window:init', async () => {
|
|
37
|
+
if (multiRecorder) {
|
|
38
|
+
multiRecorder.destroy();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
multiRecorder = new MultiWindowRecorder({
|
|
42
|
+
frameRate: 30,
|
|
43
|
+
captureCursor: true,
|
|
44
|
+
preferScreenCaptureKit: true
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return { success: true };
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Add Window
|
|
51
|
+
ipcMain.handle('recording:multi-window:add', async (event, windowInfo) => {
|
|
52
|
+
if (!multiRecorder) {
|
|
53
|
+
throw new Error('Multi-recorder not initialized');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const index = await multiRecorder.addWindow(windowInfo);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
success: true,
|
|
60
|
+
index,
|
|
61
|
+
windowCount: multiRecorder.getWindowCount()
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Remove Window
|
|
66
|
+
ipcMain.handle('recording:multi-window:remove', async (event, index) => {
|
|
67
|
+
if (!multiRecorder) {
|
|
68
|
+
throw new Error('Multi-recorder not initialized');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
multiRecorder.removeWindow(index);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
windowCount: multiRecorder.getWindowCount()
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Start Recording
|
|
80
|
+
ipcMain.handle('recording:multi-window:start', async () => {
|
|
81
|
+
if (!multiRecorder) {
|
|
82
|
+
throw new Error('Multi-recorder not initialized');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const outputDir = path.join(app.getPath('userData'), 'recordings', `rec_${Date.now()}`);
|
|
86
|
+
|
|
87
|
+
// Create output directory
|
|
88
|
+
const fs = require('fs');
|
|
89
|
+
if (!fs.existsSync(outputDir)) {
|
|
90
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = await multiRecorder.startRecording(outputDir);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
success: true,
|
|
97
|
+
...result
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Stop Recording
|
|
102
|
+
ipcMain.handle('recording:multi-window:stop', async () => {
|
|
103
|
+
if (!multiRecorder) {
|
|
104
|
+
throw new Error('Multi-recorder not initialized');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const result = await multiRecorder.stopRecording();
|
|
108
|
+
|
|
109
|
+
// Get CRVT metadata
|
|
110
|
+
const crvtMetadata = multiRecorder.getMetadataForCRVT();
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
success: true,
|
|
114
|
+
...result,
|
|
115
|
+
crvtMetadata
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Get Status
|
|
120
|
+
ipcMain.handle('recording:multi-window:status', async () => {
|
|
121
|
+
if (!multiRecorder) {
|
|
122
|
+
return { isRecording: false, windowCount: 0 };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return multiRecorder.getStatus();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Cleanup
|
|
129
|
+
ipcMain.handle('recording:multi-window:destroy', async () => {
|
|
130
|
+
if (multiRecorder) {
|
|
131
|
+
multiRecorder.destroy();
|
|
132
|
+
multiRecorder = null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { success: true };
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## 🎨 Renderer Process (React)
|
|
140
|
+
|
|
141
|
+
### 1. Type Definitions
|
|
142
|
+
|
|
143
|
+
**Dosya:** `src/renderer/types/recording.ts`
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
export interface WindowInfo {
|
|
147
|
+
id: number;
|
|
148
|
+
appName: string;
|
|
149
|
+
title: string;
|
|
150
|
+
width: number;
|
|
151
|
+
height: number;
|
|
152
|
+
x: number;
|
|
153
|
+
y: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface MultiWindowRecordingState {
|
|
157
|
+
windows: WindowInfo[];
|
|
158
|
+
isRecording: boolean;
|
|
159
|
+
outputFiles: string[];
|
|
160
|
+
duration: number;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 2. Recording Context/Store
|
|
165
|
+
|
|
166
|
+
**Dosya:** `src/renderer/contexts/RecordingContext.tsx`
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import React, { createContext, useContext, useState } from 'react';
|
|
170
|
+
|
|
171
|
+
interface RecordingContextType {
|
|
172
|
+
selectedWindows: WindowInfo[];
|
|
173
|
+
isRecording: boolean;
|
|
174
|
+
addWindow: (window: WindowInfo) => Promise<void>;
|
|
175
|
+
removeWindow: (index: number) => void;
|
|
176
|
+
startRecording: () => Promise<void>;
|
|
177
|
+
stopRecording: () => Promise<void>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const RecordingContext = createContext<RecordingContextType | null>(null);
|
|
181
|
+
|
|
182
|
+
export const RecordingProvider: React.FC = ({ children }) => {
|
|
183
|
+
const [selectedWindows, setSelectedWindows] = useState<WindowInfo[]>([]);
|
|
184
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
185
|
+
|
|
186
|
+
const addWindow = async (window: WindowInfo) => {
|
|
187
|
+
// Initialize if first window
|
|
188
|
+
if (selectedWindows.length === 0) {
|
|
189
|
+
await window.electron.invoke('recording:multi-window:init');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Add window
|
|
193
|
+
const result = await window.electron.invoke('recording:multi-window:add', window);
|
|
194
|
+
|
|
195
|
+
if (result.success) {
|
|
196
|
+
setSelectedWindows([...selectedWindows, window]);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const removeWindow = async (index: number) => {
|
|
201
|
+
await window.electron.invoke('recording:multi-window:remove', index);
|
|
202
|
+
setSelectedWindows(selectedWindows.filter((_, i) => i !== index));
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const startRecording = async () => {
|
|
206
|
+
const result = await window.electron.invoke('recording:multi-window:start');
|
|
207
|
+
|
|
208
|
+
if (result.success) {
|
|
209
|
+
setIsRecording(true);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const stopRecording = async () => {
|
|
214
|
+
const result = await window.electron.invoke('recording:multi-window:stop');
|
|
215
|
+
|
|
216
|
+
if (result.success) {
|
|
217
|
+
setIsRecording(false);
|
|
218
|
+
|
|
219
|
+
// Create CRVT file
|
|
220
|
+
await createCRVTFile(result);
|
|
221
|
+
|
|
222
|
+
// Open editor
|
|
223
|
+
await openEditor(result.outputFiles[0]);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<RecordingContext.Provider
|
|
229
|
+
value={{
|
|
230
|
+
selectedWindows,
|
|
231
|
+
isRecording,
|
|
232
|
+
addWindow,
|
|
233
|
+
removeWindow,
|
|
234
|
+
startRecording,
|
|
235
|
+
stopRecording
|
|
236
|
+
}}
|
|
237
|
+
>
|
|
238
|
+
{children}
|
|
239
|
+
</RecordingContext.Provider>
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export const useRecording = () => {
|
|
244
|
+
const context = useContext(RecordingContext);
|
|
245
|
+
if (!context) {
|
|
246
|
+
throw new Error('useRecording must be used within RecordingProvider');
|
|
247
|
+
}
|
|
248
|
+
return context;
|
|
249
|
+
};
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 3. Multi-Window Selector Component
|
|
253
|
+
|
|
254
|
+
**Dosya:** `src/renderer/components/recording/MultiWindowSelector.tsx`
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import React from 'react';
|
|
258
|
+
import { useRecording } from '../../contexts/RecordingContext';
|
|
259
|
+
import { WindowPreviewCard } from './WindowPreviewCard';
|
|
260
|
+
|
|
261
|
+
export const MultiWindowSelector: React.FC = () => {
|
|
262
|
+
const { selectedWindows, addWindow, removeWindow, isRecording } = useRecording();
|
|
263
|
+
|
|
264
|
+
const handleSelectWindow = async (index: number) => {
|
|
265
|
+
// Show window picker overlay
|
|
266
|
+
const pickedWindow = await window.electron.invoke('window-picker:show');
|
|
267
|
+
|
|
268
|
+
if (pickedWindow) {
|
|
269
|
+
if (index < selectedWindows.length) {
|
|
270
|
+
// Replace existing
|
|
271
|
+
removeWindow(index);
|
|
272
|
+
}
|
|
273
|
+
await addWindow(pickedWindow);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<div className="multi-window-selector">
|
|
279
|
+
<div className="selector-header">
|
|
280
|
+
<h3>Kayıt Edilecek Pencereler</h3>
|
|
281
|
+
<span className="window-count">{selectedWindows.length} pencere</span>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<div className="windows-grid">
|
|
285
|
+
{/* Window Slot 1 */}
|
|
286
|
+
<div className="window-slot">
|
|
287
|
+
{selectedWindows[0] ? (
|
|
288
|
+
<WindowPreviewCard
|
|
289
|
+
window={selectedWindows[0]}
|
|
290
|
+
onReselect={() => handleSelectWindow(0)}
|
|
291
|
+
onRemove={() => removeWindow(0)}
|
|
292
|
+
disabled={isRecording}
|
|
293
|
+
/>
|
|
294
|
+
) : (
|
|
295
|
+
<button
|
|
296
|
+
className="select-window-btn"
|
|
297
|
+
onClick={() => handleSelectWindow(0)}
|
|
298
|
+
disabled={isRecording}
|
|
299
|
+
>
|
|
300
|
+
<VideoIcon />
|
|
301
|
+
<span>Pencere Seç</span>
|
|
302
|
+
</button>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
{/* Window Slot 2 - Show only if first window is selected */}
|
|
307
|
+
{selectedWindows[0] && (
|
|
308
|
+
<div className="window-slot">
|
|
309
|
+
{selectedWindows[1] ? (
|
|
310
|
+
<WindowPreviewCard
|
|
311
|
+
window={selectedWindows[1]}
|
|
312
|
+
onReselect={() => handleSelectWindow(1)}
|
|
313
|
+
onRemove={() => removeWindow(1)}
|
|
314
|
+
disabled={isRecording}
|
|
315
|
+
/>
|
|
316
|
+
) : (
|
|
317
|
+
<button
|
|
318
|
+
className="select-window-btn add-second"
|
|
319
|
+
onClick={() => handleSelectWindow(1)}
|
|
320
|
+
disabled={isRecording}
|
|
321
|
+
>
|
|
322
|
+
<PlusIcon />
|
|
323
|
+
<span>İkinci Pencere Ekle</span>
|
|
324
|
+
</button>
|
|
325
|
+
)}
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
};
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### 4. Window Preview Card
|
|
335
|
+
|
|
336
|
+
**Dosya:** `src/renderer/components/recording/WindowPreviewCard.tsx`
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
import React from 'react';
|
|
340
|
+
import { WindowInfo } from '../../types/recording';
|
|
341
|
+
|
|
342
|
+
interface Props {
|
|
343
|
+
window: WindowInfo;
|
|
344
|
+
onReselect: () => void;
|
|
345
|
+
onRemove: () => void;
|
|
346
|
+
disabled?: boolean;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export const WindowPreviewCard: React.FC<Props> = ({
|
|
350
|
+
window,
|
|
351
|
+
onReselect,
|
|
352
|
+
onRemove,
|
|
353
|
+
disabled
|
|
354
|
+
}) => {
|
|
355
|
+
return (
|
|
356
|
+
<div className="window-preview-card">
|
|
357
|
+
<div className="preview-header">
|
|
358
|
+
<span className="app-name">{window.appName}</span>
|
|
359
|
+
<button
|
|
360
|
+
className="remove-btn"
|
|
361
|
+
onClick={onRemove}
|
|
362
|
+
disabled={disabled}
|
|
363
|
+
>
|
|
364
|
+
×
|
|
365
|
+
</button>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
<div className="preview-content">
|
|
369
|
+
{/* Thumbnail buraya gelecek */}
|
|
370
|
+
<div className="window-icon">
|
|
371
|
+
<VideoIcon />
|
|
372
|
+
</div>
|
|
373
|
+
<div className="window-info">
|
|
374
|
+
<div className="window-title">{window.title || 'Başlıksız'}</div>
|
|
375
|
+
<div className="window-size">{window.width} × {window.height}</div>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<button
|
|
380
|
+
className="reselect-btn"
|
|
381
|
+
onClick={onReselect}
|
|
382
|
+
disabled={disabled}
|
|
383
|
+
>
|
|
384
|
+
Değiştir
|
|
385
|
+
</button>
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
};
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### 5. Recording Controls
|
|
392
|
+
|
|
393
|
+
**Dosya:** `src/renderer/components/recording/RecordingControls.tsx`
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
import React from 'react';
|
|
397
|
+
import { useRecording } from '../../contexts/RecordingContext';
|
|
398
|
+
|
|
399
|
+
export const RecordingControls: React.FC = () => {
|
|
400
|
+
const { selectedWindows, isRecording, startRecording, stopRecording } = useRecording();
|
|
401
|
+
|
|
402
|
+
const canStart = selectedWindows.length > 0 && !isRecording;
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
<div className="recording-controls">
|
|
406
|
+
{!isRecording ? (
|
|
407
|
+
<button
|
|
408
|
+
className="start-recording-btn"
|
|
409
|
+
onClick={startRecording}
|
|
410
|
+
disabled={!canStart}
|
|
411
|
+
>
|
|
412
|
+
<RecordIcon />
|
|
413
|
+
<span>Kayıt Başlat</span>
|
|
414
|
+
{selectedWindows.length > 1 && (
|
|
415
|
+
<span className="window-badge">{selectedWindows.length} pencere</span>
|
|
416
|
+
)}
|
|
417
|
+
</button>
|
|
418
|
+
) : (
|
|
419
|
+
<button
|
|
420
|
+
className="stop-recording-btn"
|
|
421
|
+
onClick={stopRecording}
|
|
422
|
+
>
|
|
423
|
+
<StopIcon />
|
|
424
|
+
<span>Kaydı Durdur</span>
|
|
425
|
+
</button>
|
|
426
|
+
)}
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
};
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
## 📄 CRVT File Creation
|
|
433
|
+
|
|
434
|
+
**Dosya:** `src/main/utils/crvt-creator.ts`
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
import fs from 'fs';
|
|
438
|
+
import path from 'path';
|
|
439
|
+
|
|
440
|
+
interface CRVTSegment {
|
|
441
|
+
id: string;
|
|
442
|
+
type: 'screen' | 'cursor' | 'audio' | 'camera';
|
|
443
|
+
filePath: string;
|
|
444
|
+
startTime: number;
|
|
445
|
+
endTime: number;
|
|
446
|
+
duration: number;
|
|
447
|
+
windowIndex?: number;
|
|
448
|
+
layoutRow?: number;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
interface CRVTFile {
|
|
452
|
+
version: string;
|
|
453
|
+
timestamp: number;
|
|
454
|
+
duration: number;
|
|
455
|
+
segments: CRVTSegment[];
|
|
456
|
+
multiWindow?: {
|
|
457
|
+
enabled: boolean;
|
|
458
|
+
windowCount: number;
|
|
459
|
+
windows: Array<{
|
|
460
|
+
index: number;
|
|
461
|
+
appName: string;
|
|
462
|
+
title: string;
|
|
463
|
+
filePath: string;
|
|
464
|
+
syncOffset: number;
|
|
465
|
+
}>;
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export async function createMultiWindowCRVT(
|
|
470
|
+
recordingResult: any,
|
|
471
|
+
outputDir: string
|
|
472
|
+
): Promise<string> {
|
|
473
|
+
const crvt: CRVTFile = {
|
|
474
|
+
version: '2.0',
|
|
475
|
+
timestamp: recordingResult.metadata.startTime,
|
|
476
|
+
duration: recordingResult.duration,
|
|
477
|
+
segments: [],
|
|
478
|
+
multiWindow: {
|
|
479
|
+
enabled: true,
|
|
480
|
+
windowCount: recordingResult.windowCount,
|
|
481
|
+
windows: []
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// Create segments for each window
|
|
486
|
+
recordingResult.metadata.windows.forEach((win: any, index: number) => {
|
|
487
|
+
// Screen segment
|
|
488
|
+
crvt.segments.push({
|
|
489
|
+
id: `screen_${index}_${Date.now()}`,
|
|
490
|
+
type: 'screen',
|
|
491
|
+
filePath: win.outputPath,
|
|
492
|
+
startTime: 0,
|
|
493
|
+
endTime: recordingResult.duration,
|
|
494
|
+
duration: recordingResult.duration,
|
|
495
|
+
windowIndex: index,
|
|
496
|
+
layoutRow: index
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Cursor segment (if exists)
|
|
500
|
+
const cursorFile = findCursorFile(win.outputPath);
|
|
501
|
+
if (cursorFile && fs.existsSync(cursorFile)) {
|
|
502
|
+
crvt.segments.push({
|
|
503
|
+
id: `cursor_${index}_${Date.now()}`,
|
|
504
|
+
type: 'cursor',
|
|
505
|
+
filePath: cursorFile,
|
|
506
|
+
startTime: 0,
|
|
507
|
+
endTime: recordingResult.duration,
|
|
508
|
+
duration: recordingResult.duration,
|
|
509
|
+
windowIndex: index,
|
|
510
|
+
layoutRow: index
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Add window metadata
|
|
515
|
+
crvt.multiWindow!.windows.push({
|
|
516
|
+
index,
|
|
517
|
+
appName: win.windowInfo.appName,
|
|
518
|
+
title: win.windowInfo.title,
|
|
519
|
+
filePath: win.outputPath,
|
|
520
|
+
syncOffset: win.syncOffset
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Save CRVT file
|
|
525
|
+
const crvtPath = path.join(outputDir, 'recording.crvt');
|
|
526
|
+
fs.writeFileSync(crvtPath, JSON.stringify(crvt, null, 2));
|
|
527
|
+
|
|
528
|
+
console.log(`📄 CRVT file created: ${crvtPath}`);
|
|
529
|
+
|
|
530
|
+
return crvtPath;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function findCursorFile(videoPath: string): string | null {
|
|
534
|
+
const dir = path.dirname(videoPath);
|
|
535
|
+
const basename = path.basename(videoPath, path.extname(videoPath));
|
|
536
|
+
|
|
537
|
+
// Try to find cursor file
|
|
538
|
+
const cursorPath = path.join(dir, `temp_cursor_${basename.split('_').pop()}.json`);
|
|
539
|
+
|
|
540
|
+
return fs.existsSync(cursorPath) ? cursorPath : null;
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
## 🎬 Editor Integration
|
|
545
|
+
|
|
546
|
+
**Dosya:** `src/renderer/components/editor/MultiRowTimeline.tsx`
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
import React, { useMemo } from 'react';
|
|
550
|
+
import { CRVTFile, CRVTSegment } from '../../types/crvt';
|
|
551
|
+
import { ClipSegment } from './ClipSegment';
|
|
552
|
+
|
|
553
|
+
interface Props {
|
|
554
|
+
recording: CRVTFile;
|
|
555
|
+
onSegmentSelect?: (segment: CRVTSegment) => void;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export const MultiRowTimeline: React.FC<Props> = ({
|
|
559
|
+
recording,
|
|
560
|
+
onSegmentSelect
|
|
561
|
+
}) => {
|
|
562
|
+
// Group segments by layout row
|
|
563
|
+
const segmentsByRow = useMemo(() => {
|
|
564
|
+
const rows = new Map<number, CRVTSegment[]>();
|
|
565
|
+
|
|
566
|
+
recording.segments.forEach(segment => {
|
|
567
|
+
const row = segment.layoutRow ?? 0;
|
|
568
|
+
if (!rows.has(row)) {
|
|
569
|
+
rows.set(row, []);
|
|
570
|
+
}
|
|
571
|
+
rows.get(row)!.push(segment);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
return rows;
|
|
575
|
+
}, [recording]);
|
|
576
|
+
|
|
577
|
+
return (
|
|
578
|
+
<div className="multi-row-timeline">
|
|
579
|
+
{Array.from(segmentsByRow.entries()).map(([rowIndex, segments]) => (
|
|
580
|
+
<div
|
|
581
|
+
key={rowIndex}
|
|
582
|
+
className="timeline-row"
|
|
583
|
+
data-row={rowIndex}
|
|
584
|
+
>
|
|
585
|
+
<div className="row-label">
|
|
586
|
+
<div className="row-number">Row {rowIndex + 1}</div>
|
|
587
|
+
<div className="row-app-name">
|
|
588
|
+
{recording.multiWindow?.windows[rowIndex]?.appName || `Window ${rowIndex + 1}`}
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
|
|
592
|
+
<div className="row-track">
|
|
593
|
+
{segments.map(segment => (
|
|
594
|
+
<ClipSegment
|
|
595
|
+
key={segment.id}
|
|
596
|
+
segment={segment}
|
|
597
|
+
totalDuration={recording.duration}
|
|
598
|
+
onSelect={() => onSegmentSelect?.(segment)}
|
|
599
|
+
/>
|
|
600
|
+
))}
|
|
601
|
+
</div>
|
|
602
|
+
</div>
|
|
603
|
+
))}
|
|
604
|
+
</div>
|
|
605
|
+
);
|
|
606
|
+
};
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
## 🎨 CSS Styles
|
|
610
|
+
|
|
611
|
+
**Dosya:** `src/renderer/styles/multi-window.css`
|
|
612
|
+
|
|
613
|
+
```css
|
|
614
|
+
/* Multi-Window Selector */
|
|
615
|
+
.multi-window-selector {
|
|
616
|
+
padding: 20px;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.selector-header {
|
|
620
|
+
display: flex;
|
|
621
|
+
justify-content: space-between;
|
|
622
|
+
align-items: center;
|
|
623
|
+
margin-bottom: 20px;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.window-count {
|
|
627
|
+
font-size: 14px;
|
|
628
|
+
color: #888;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.windows-grid {
|
|
632
|
+
display: grid;
|
|
633
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
634
|
+
gap: 20px;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.window-slot {
|
|
638
|
+
min-height: 200px;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.select-window-btn {
|
|
642
|
+
width: 100%;
|
|
643
|
+
height: 200px;
|
|
644
|
+
border: 2px dashed #444;
|
|
645
|
+
border-radius: 8px;
|
|
646
|
+
background: transparent;
|
|
647
|
+
color: #fff;
|
|
648
|
+
cursor: pointer;
|
|
649
|
+
display: flex;
|
|
650
|
+
flex-direction: column;
|
|
651
|
+
align-items: center;
|
|
652
|
+
justify-content: center;
|
|
653
|
+
gap: 12px;
|
|
654
|
+
transition: all 0.2s;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.select-window-btn:hover {
|
|
658
|
+
border-color: #666;
|
|
659
|
+
background: rgba(255, 255, 255, 0.05);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.select-window-btn.add-second {
|
|
663
|
+
border-color: #0066ff;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.select-window-btn.add-second:hover {
|
|
667
|
+
background: rgba(0, 102, 255, 0.1);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/* Window Preview Card */
|
|
671
|
+
.window-preview-card {
|
|
672
|
+
border: 1px solid #333;
|
|
673
|
+
border-radius: 8px;
|
|
674
|
+
background: #1a1a1a;
|
|
675
|
+
overflow: hidden;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.preview-header {
|
|
679
|
+
display: flex;
|
|
680
|
+
justify-content: space-between;
|
|
681
|
+
align-items: center;
|
|
682
|
+
padding: 12px;
|
|
683
|
+
border-bottom: 1px solid #333;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.app-name {
|
|
687
|
+
font-weight: 600;
|
|
688
|
+
color: #fff;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.remove-btn {
|
|
692
|
+
width: 24px;
|
|
693
|
+
height: 24px;
|
|
694
|
+
border: none;
|
|
695
|
+
background: #ff4444;
|
|
696
|
+
color: #fff;
|
|
697
|
+
border-radius: 4px;
|
|
698
|
+
cursor: pointer;
|
|
699
|
+
font-size: 18px;
|
|
700
|
+
line-height: 1;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.preview-content {
|
|
704
|
+
padding: 20px;
|
|
705
|
+
text-align: center;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
.window-info {
|
|
709
|
+
margin-top: 12px;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.window-title {
|
|
713
|
+
font-size: 14px;
|
|
714
|
+
color: #ccc;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.window-size {
|
|
718
|
+
font-size: 12px;
|
|
719
|
+
color: #888;
|
|
720
|
+
margin-top: 4px;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.reselect-btn {
|
|
724
|
+
width: 100%;
|
|
725
|
+
padding: 10px;
|
|
726
|
+
border: none;
|
|
727
|
+
border-top: 1px solid #333;
|
|
728
|
+
background: transparent;
|
|
729
|
+
color: #0066ff;
|
|
730
|
+
cursor: pointer;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/* Recording Controls */
|
|
734
|
+
.start-recording-btn {
|
|
735
|
+
display: flex;
|
|
736
|
+
align-items: center;
|
|
737
|
+
gap: 12px;
|
|
738
|
+
padding: 16px 32px;
|
|
739
|
+
background: #ff0000;
|
|
740
|
+
color: #fff;
|
|
741
|
+
border: none;
|
|
742
|
+
border-radius: 8px;
|
|
743
|
+
font-size: 16px;
|
|
744
|
+
font-weight: 600;
|
|
745
|
+
cursor: pointer;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.window-badge {
|
|
749
|
+
padding: 4px 8px;
|
|
750
|
+
background: rgba(255, 255, 255, 0.2);
|
|
751
|
+
border-radius: 12px;
|
|
752
|
+
font-size: 12px;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/* Multi-Row Timeline */
|
|
756
|
+
.multi-row-timeline {
|
|
757
|
+
display: flex;
|
|
758
|
+
flex-direction: column;
|
|
759
|
+
gap: 8px;
|
|
760
|
+
padding: 20px;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.timeline-row {
|
|
764
|
+
display: flex;
|
|
765
|
+
min-height: 80px;
|
|
766
|
+
border: 1px solid #333;
|
|
767
|
+
border-radius: 4px;
|
|
768
|
+
background: #1a1a1a;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.row-label {
|
|
772
|
+
width: 150px;
|
|
773
|
+
padding: 12px;
|
|
774
|
+
border-right: 1px solid #333;
|
|
775
|
+
display: flex;
|
|
776
|
+
flex-direction: column;
|
|
777
|
+
gap: 4px;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.row-number {
|
|
781
|
+
font-size: 12px;
|
|
782
|
+
color: #888;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.row-app-name {
|
|
786
|
+
font-weight: 600;
|
|
787
|
+
color: #fff;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.row-track {
|
|
791
|
+
flex: 1;
|
|
792
|
+
position: relative;
|
|
793
|
+
padding: 8px;
|
|
794
|
+
}
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
## 🚀 Kullanım Örneği
|
|
798
|
+
|
|
799
|
+
```typescript
|
|
800
|
+
// RecordingWindow.tsx
|
|
801
|
+
import React from 'react';
|
|
802
|
+
import { RecordingProvider } from './contexts/RecordingContext';
|
|
803
|
+
import { MultiWindowSelector } from './components/recording/MultiWindowSelector';
|
|
804
|
+
import { RecordingControls } from './components/recording/RecordingControls';
|
|
805
|
+
|
|
806
|
+
export const RecordingWindow: React.FC = () => {
|
|
807
|
+
return (
|
|
808
|
+
<RecordingProvider>
|
|
809
|
+
<div className="recording-window">
|
|
810
|
+
<h1>Yeni Kayıt</h1>
|
|
811
|
+
|
|
812
|
+
<MultiWindowSelector />
|
|
813
|
+
|
|
814
|
+
<RecordingControls />
|
|
815
|
+
</div>
|
|
816
|
+
</RecordingProvider>
|
|
817
|
+
);
|
|
818
|
+
};
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
## ✅ Checklist
|
|
822
|
+
|
|
823
|
+
- [ ] MultiWindowRecorder import et
|
|
824
|
+
- [ ] IPC handlers ekle
|
|
825
|
+
- [ ] RecordingContext oluştur
|
|
826
|
+
- [ ] MultiWindowSelector komponenti
|
|
827
|
+
- [ ] WindowPreviewCard komponenti
|
|
828
|
+
- [ ] RecordingControls güncelle
|
|
829
|
+
- [ ] CRVT creator implement et
|
|
830
|
+
- [ ] MultiRowTimeline komponenti
|
|
831
|
+
- [ ] CSS stilleri ekle
|
|
832
|
+
- [ ] End-to-end test
|