UNPKG

node-mac-recorder

Version:

Native macOS screen recording package for Node.js applications

591 lines (488 loc) 15.5 kB
# Creavit Desktop Multi-Window Recording Integration Plan ## 🎯 Hedef Creavit Desktop'a aynı anda iki pencere kaydı özelliğini eklemek: 1. Kullanıcı ilk pencereyi seçer (overlay ile) 2. İkinci pencereyi seçer (overlay ile) 3. "Kayıt Başlat" butonuna basınca her iki pencere de kaydedilir 4. Kayıt durdurulunca CRVT dosyası oluşturulur 5. Editor'da iki clip yan yana (multi-row layout) gösterilir ## 📋 Mimari Plan ### 1. UI/UX Flow ``` ┌─────────────────────────────────────────────────────────────┐ │ Kayıt Başlatma Penceresi │ ├─────────────────────────────────────────────────────────────┤ │ │ │ [📹 Pencere Seç] [📹 İkinci Pencere Ekle] ← YENİ BUTON │ │ │ │ ┌───────────────────┐ ┌───────────────────┐ │ │ │ Window 1 │ │ Window 2 │ │ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ │ │ │ [Preview] │ │ │ │ [Preview] │ │ │ │ │ └───────────┘ │ │ └───────────┘ │ │ │ │ Chrome │ │ Finder │ │ │ │ [Değiştir] [✕] │ │ [Değiştir] [✕] │ │ │ └───────────────────┘ └───────────────────┘ │ │ │ │ [🔴 Kayıt Başlat] │ │ │ └─────────────────────────────────────────────────────────────┘ ``` ### 2. Dosya Yapısı ``` creavit.studio/desktop/ ├── src/ │ ├── main/ │ │ ├── recording/ │ │ │ ├── MultiWindowRecorder.js ← YENİ │ │ │ ├── RecorderManager.js (mevcut - güncelle) │ │ │ └── WindowSelector.js ← YENİ │ │ └── ... │ ├── renderer/ │ │ ├── components/ │ │ │ ├── recording/ │ │ │ │ ├── MultiWindowSelector.tsx ← YENİ │ │ │ │ ├── WindowPreview.tsx ← YENİ │ │ │ │ └── RecordingControls.tsx (güncelle) │ │ │ └── editor/ │ │ │ ├── timeline/ │ │ │ │ ├── MultiRowTimeline.tsx ← YENİ │ │ │ │ └── ClipSegment.tsx (güncelle) │ │ │ └── ... │ │ └── ... │ └── shared/ │ ├── types/ │ │ └── crvt.ts (güncelle) │ └── ... └── ... ``` ## 🔧 Implementation Details ### Phase 1: Multi-Window Recorder Manager **Dosya:** `src/main/recording/MultiWindowRecorder.js` ```javascript const MacRecorder = require('node-mac-recorder/index-multiprocess'); class MultiWindowRecorder { constructor() { this.recorders = []; this.windows = []; this.isRecording = false; this.outputFiles = []; } async addWindow(windowId) { const recorder = new MacRecorder(); this.recorders.push({ recorder, windowId, outputPath: null, preview: await this.getWindowPreview(windowId) }); return this.recorders.length - 1; // index } removeWindow(index) { const recorderInfo = this.recorders[index]; if (recorderInfo && recorderInfo.recorder) { recorderInfo.recorder.destroy(); } this.recorders.splice(index, 1); } async startRecording(outputDir) { if (this.isRecording) return; const timestamp = Date.now(); this.outputFiles = []; // Start all recorders sequentially for (let i = 0; i < this.recorders.length; i++) { const recInfo = this.recorders[i]; const outputPath = path.join(outputDir, `window_${i}_${timestamp}.mov`); await recInfo.recorder.startRecording(outputPath, { windowId: recInfo.windowId, frameRate: 30, captureCursor: true, preferScreenCaptureKit: true }); this.outputFiles.push(outputPath); // Wait for ScreenCaptureKit init if (i < this.recorders.length - 1) { await new Promise(r => setTimeout(r, 1000)); } } this.isRecording = true; } async stopRecording() { if (!this.isRecording) return; // Stop all recorders in parallel await Promise.all( this.recorders.map(recInfo => recInfo.recorder.stopRecording()) ); this.isRecording = false; return this.outputFiles; } destroy() { this.recorders.forEach(recInfo => { recInfo.recorder.destroy(); }); this.recorders = []; this.outputFiles = []; } } module.exports = MultiWindowRecorder; ``` ### Phase 2: Window Selection UI **Komponente:** `MultiWindowSelector.tsx` ```typescript interface WindowInfo { id: number; appName: string; title: string; preview?: string; } interface MultiWindowSelectorProps { onWindowsSelected: (windows: WindowInfo[]) => void; } export const MultiWindowSelector: React.FC<MultiWindowSelectorProps> = ({ onWindowsSelected }) => { const [selectedWindows, setSelectedWindows] = useState<WindowInfo[]>([]); const [isSelecting, setIsSelecting] = useState(false); const [selectingIndex, setSelectingIndex] = useState<number | null>(null); const handleAddWindow = async (index: number) => { setIsSelecting(true); setSelectingIndex(index); // Show overlay window selector const selectedWindow = await window.electron.showWindowSelector(); if (selectedWindow) { const newWindows = [...selectedWindows]; newWindows[index] = selectedWindow; setSelectedWindows(newWindows); onWindowsSelected(newWindows); } setIsSelecting(false); setSelectingIndex(null); }; const handleRemoveWindow = (index: number) => { const newWindows = selectedWindows.filter((_, i) => i !== index); setSelectedWindows(newWindows); onWindowsSelected(newWindows); }; return ( <div className="multi-window-selector"> <div className="windows-grid"> {/* Window 1 */} <WindowPreview window={selectedWindows[0]} onSelect={() => handleAddWindow(0)} onRemove={() => handleRemoveWindow(0)} label="Pencere 1" isSelecting={isSelecting && selectingIndex === 0} /> {/* Add Second Window Button */} {selectedWindows[0] && !selectedWindows[1] && ( <button className="add-window-btn" onClick={() => handleAddWindow(1)} > <PlusIcon /> İkinci Pencere Ekle </button> )} {/* Window 2 */} {selectedWindows[1] && ( <WindowPreview window={selectedWindows[1]} onSelect={() => handleAddWindow(1)} onRemove={() => handleRemoveWindow(1)} label="Pencere 2" isSelecting={isSelecting && selectingIndex === 1} /> )} </div> <div className="window-count"> {selectedWindows.length} pencere seçildi </div> </div> ); }; ``` ### Phase 3: CRVT Format Extension **Tip Tanımı:** `crvt.ts` ```typescript // Mevcut CRVT formatına eklenecek interface CRVTClipSegment { id: string; type: 'screen' | 'camera' | 'audio' | 'cursor'; filePath: string; startTime: number; endTime: number; duration: number; // YENİ: Multi-window için windowIndex?: number; // Hangi pencere (0, 1, 2, ...) layoutRow?: number; // Timeline'da hangi satırda } interface CRVTRecording { version: string; timestamp: number; duration: number; segments: CRVTClipSegment[]; // YENİ: Multi-window metadata multiWindow?: { enabled: boolean; windowCount: number; windows: Array<{ index: number; appName: string; title: string; filePath: string; }>; }; } ``` **CRVT Oluşturma:** ```javascript // Multi-window recording bittiğinde async function createMultiWindowCRVT(outputFiles, metadata) { const crvt = { version: '2.0', timestamp: Date.now(), duration: calculateDuration(outputFiles[0]), segments: [], multiWindow: { enabled: true, windowCount: outputFiles.length, windows: [] } }; // Her window için segment oluştur outputFiles.forEach((filePath, index) => { // Screen segment crvt.segments.push({ id: `screen_${index}_${Date.now()}`, type: 'screen', filePath: filePath, startTime: 0, endTime: crvt.duration, duration: crvt.duration, windowIndex: index, layoutRow: index // Her pencere farklı satırda }); // Cursor segment (varsa) const cursorFile = findCursorFile(filePath); if (cursorFile) { crvt.segments.push({ id: `cursor_${index}_${Date.now()}`, type: 'cursor', filePath: cursorFile, startTime: 0, endTime: crvt.duration, duration: crvt.duration, windowIndex: index, layoutRow: index }); } // Window metadata crvt.multiWindow.windows.push({ index: index, appName: metadata[index].appName, title: metadata[index].title, filePath: filePath }); }); return crvt; } ``` ### Phase 4: Editor Multi-Row Timeline **Komponente:** `MultiRowTimeline.tsx` ```typescript export const MultiRowTimeline: React.FC<TimelineProps> = ({ recording }) => { // Group segments by layoutRow const segmentsByRow = useMemo(() => { const rows = new Map<number, CRVTClipSegment[]>(); recording.segments.forEach(segment => { const row = segment.layoutRow ?? 0; if (!rows.has(row)) { rows.set(row, []); } rows.get(row)!.push(segment); }); return rows; }, [recording]); return ( <div className="multi-row-timeline"> {Array.from(segmentsByRow.entries()).map(([rowIndex, segments]) => ( <div key={rowIndex} className="timeline-row" data-window-index={rowIndex} > <div className="row-label"> {recording.multiWindow?.windows[rowIndex]?.appName || `Window ${rowIndex + 1}`} </div> <div className="row-segments"> {segments.map(segment => ( <ClipSegment key={segment.id} segment={segment} duration={recording.duration} onSelect={() => handleSegmentSelect(segment)} /> ))} </div> </div> ))} </div> ); }; ``` **CSS Styling:** ```css .multi-row-timeline { display: flex; flex-direction: column; gap: 8px; } .timeline-row { display: flex; align-items: center; min-height: 60px; border: 1px solid #333; border-radius: 4px; background: #1a1a1a; } .row-label { width: 120px; padding: 0 12px; font-weight: 500; color: #fff; border-right: 1px solid #333; } .row-segments { flex: 1; display: flex; position: relative; padding: 8px; } ``` ## 🔄 Integration Flow ### 1. Kayıt Başlatma ```javascript // RecordingWindow.tsx const handleStartRecording = async () => { if (selectedWindows.length === 0) { showError('En az bir pencere seçin'); return; } // Multi-window recorder oluştur const multiRecorder = new MultiWindowRecorder(); // Windows ekle for (const window of selectedWindows) { await multiRecorder.addWindow(window.id); } // Kaydı başlat const outputDir = getOutputDirectory(); await multiRecorder.startRecording(outputDir); setIsRecording(true); setRecorder(multiRecorder); }; ``` ### 2. Kayıt Durdurma ```javascript const handleStopRecording = async () => { if (!recorder) return; // Tüm kayıtları durdur const outputFiles = await recorder.stopRecording(); // CRVT dosyası oluştur const crvt = await createMultiWindowCRVT(outputFiles, { windows: selectedWindows, timestamp: recordingStartTime }); // CRVT dosyasını kaydet const crvtPath = await saveCRVT(crvt); // Recorder'ı temizle recorder.destroy(); // Editor'ı aç openEditor(crvtPath); }; ``` ### 3. Editor Loading ```javascript // Editor.tsx useEffect(() => { const loadRecording = async () => { const crvt = await loadCRVT(crvtPath); // Multi-window kontrolü if (crvt.multiWindow?.enabled) { setLayoutMode('multi-row'); setWindowCount(crvt.multiWindow.windowCount); } else { setLayoutMode('single'); } setRecording(crvt); }; loadRecording(); }, [crvtPath]); ``` ## ⚠️ Kritik Noktalar ### 1. Senkronizasyon ```javascript // Her recorder'ın start timestamp'ini kaydet const syncTimestamps = { window0: recorder0StartTime, window1: recorder1StartTime, offset: recorder1StartTime - recorder0StartTime // ~1000ms }; // Editor'da offset'i hesaba kat segment.adjustedStartTime = segment.startTime - syncTimestamps.offset; ``` ### 2. Dosya Adlandırma ``` output/ ├── recording_1234567890/ │ ├── window_0_1234567890.mov (Chrome) │ ├── window_1_1234567891.mov (Finder) │ ├── temp_cursor_1234567890.json │ ├── temp_cursor_1234567891.json │ └── recording.crvt ``` ### 3. Memory Management ```javascript // Recorder'ları her zaman temizle window.addEventListener('beforeunload', () => { if (multiRecorder) { multiRecorder.destroy(); } }); ``` ## 🧪 Test Senaryoları 1. ✅ İki pencere seçimi 2. ✅ Kayıt başlatma (sıralı) 3. ✅ Paralel kayıt 4. ✅ Kayıt durdurma 5. ✅ CRVT oluşturma 6. ✅ Editor'da yükleme 7. ✅ Multi-row timeline rendering 8. ✅ Segment senkronizasyonu ## 📦 Gerekli Paketler ```json { "dependencies": { "node-mac-recorder": "latest" // Multi-process support } } ``` ## 🎨 UI/UX İyileştirmeler 1. **Preview'lar**: Her seçilen pencere için küçük önizleme 2. **Drag & Drop**: Pencereleri sürükle bırak ile sırala 3. **Real-time Preview**: Kayıt sırasında her iki pencereyi göster 4. **Sync Indicator**: Hangi pencerenin kaydedildiğini göster 5. **Timeline Zoom**: Multi-row timeline için zoom kontrolü ## 🚀 Deployment 1. `node-mac-recorder` versiyonunu güncelle 2. Electron app'i rebuild et 3. Test kullanıcıları ile beta testi 4. Production'a release ## 📝 Notlar - Her recorder kendi process'inde çalışır (izolasyon) - ScreenCaptureKit init için 1 saniye bekleme şart - CRVT formatı geriye uyumlu kalmalı - Editor mevcut single-window kayıtları açabilmeli - Performance: 2 pencere sorunsuz, 3-4 test edilmeli