diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index a3b5cee..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Build Electron App - -on: - push: - branches: [ main, master ] - workflow_dispatch: - -jobs: - - - build-macos: - runs-on: macos-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '22' - - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - - name: Install dependencies - run: npm ci - - - name: Install app dependencies - run: npx electron-builder install-app-deps - - - name: Build macOS app - run: npm run build:mac - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload macOS build - uses: actions/upload-artifact@v4 - with: - name: macos-installer - path: release/**/*.dmg - retention-days: 30 \ No newline at end of file diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 8b75b20..485fa6c 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -187,12 +187,21 @@ export default function VideoEditor() { const width = 1920; const height = 1080; + // Calculate visually lossless bitrate matching screen recording optimization + const totalPixels = width * height; + let bitrate = 30_000_000; + if (totalPixels > 1920 * 1080 && totalPixels <= 2560 * 1440) { + bitrate = 50_000_000; + } else if (totalPixels > 2560 * 1440) { + bitrate = 80_000_000; + } + const exporter = new VideoExporter({ videoUrl: videoPath, width, height, frameRate: 60, - bitrate: 15_000_000, + bitrate, codec: 'avc1.640033', wallpaper, zoomRegions, diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 636f31d..23c141d 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -55,12 +55,18 @@ export function useScreenRecorder(): UseScreenRecorderReturn { return; } await window.electronAPI.startMouseTracking(); + // Enable hardware acceleration and set optimal resolution/framerate constraints const mediaStream = await (navigator.mediaDevices as any).getUserMedia({ audio: false, video: { mandatory: { chromeMediaSource: "desktop", chromeMediaSourceId: selectedSource.id, + minWidth: 1920, + minHeight: 1080, + maxWidth: 3840, + maxHeight: 2160, + frameRate: { ideal: 60, max: 60 } }, }, }); @@ -71,14 +77,21 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const videoTrack = stream.current.getVideoTracks()[0]; const { width = 1920, height = 1080 } = videoTrack.getSettings(); const totalPixels = width * height; - let bitrate = 150_000_000; + // Use visually lossless bitrates optimized for quality and file size balance + let bitrate = 30_000_000; if (totalPixels > 1920 * 1080 && totalPixels <= 2560 * 1440) { - bitrate = 250_000_000; + bitrate = 50_000_000; } else if (totalPixels > 2560 * 1440) { - bitrate = 400_000_000; + bitrate = 80_000_000; } chunks.current = []; - const mimeType = "video/webm;codecs=vp9"; + // Prefer AV1 codec for better compression, fallback to VP9 then VP8 + const supportedCodecs = [ + 'video/webm;codecs=av1', + 'video/webm;codecs=vp9', + 'video/webm;codecs=vp8' + ]; + const mimeType = supportedCodecs.find(codec => MediaRecorder.isTypeSupported(codec)) || 'video/webm;codecs=vp9'; const recorder = new MediaRecorder(stream.current, { mimeType, videoBitsPerSecond: bitrate }); mediaRecorder.current = recorder; recorder.ondataavailable = e => { @@ -89,6 +102,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn { if (chunks.current.length === 0) return; const duration = Date.now() - startTime.current; const buggyBlob = new Blob(chunks.current, { type: mimeType }); + // Clear chunks early to free memory immediately after blob creation + chunks.current = []; const timestamp = Date.now(); const videoFileName = `recording-${timestamp}.webm`; const trackingFileName = `recording-${timestamp}_tracking.json`; @@ -110,7 +125,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } }; recorder.onerror = () => setRecording(false); - recorder.start(1000); + // Use larger timeslice to reduce recording overhead and improve smoothness + recorder.start(5000); startTime.current = Date.now(); setRecording(true); window.electronAPI?.setRecordingState(true); diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index e299b69..c7f06ae 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -25,6 +25,7 @@ export class VideoExporter { private encodeQueue = 0; private readonly MAX_ENCODE_QUEUE = 60; private videoDescription: Uint8Array | undefined; + private videoColorSpace: VideoColorSpaceInit | undefined; constructor(config: VideoExporterConfig) { this.config = config; @@ -164,13 +165,22 @@ export class VideoExporter { const chunk = this.encodedChunks[i]; const meta: EncodedVideoChunkMetadata = {}; - // Add decoder config for the first chunk + // Add decoder config with colorSpace metadata for the first chunk if (i === 0 && this.videoDescription) { + // Use captured colorSpace from encoder or fallback to default sRGB colorspace + const colorSpace = this.videoColorSpace || { + primaries: 'bt709', + transfer: 'iec61966-2-1', + matrix: 'rgb', + fullRange: true, + }; + meta.decoderConfig = { codec: this.config.codec || 'avc1.640033', codedWidth: this.config.width, codedHeight: this.config.height, description: this.videoDescription, + colorSpace, }; } @@ -199,11 +209,16 @@ export class VideoExporter { this.encoder = new VideoEncoder({ output: (chunk, meta) => { + // Capture decoder config metadata from encoder output if (meta?.decoderConfig?.description && !videoDescription) { const desc = meta.decoderConfig.description; videoDescription = new Uint8Array(desc instanceof ArrayBuffer ? desc : (desc as any)); this.videoDescription = videoDescription; } + // Capture colorSpace from encoder metadata if provided + if (meta?.decoderConfig?.colorSpace && !this.videoColorSpace) { + this.videoColorSpace = meta.decoderConfig.colorSpace; + } this.encodedChunks.push(chunk); this.encodeQueue--; }, @@ -265,5 +280,6 @@ export class VideoExporter { this.encodedChunks = []; this.encodeQueue = 0; this.videoDescription = undefined; + this.videoColorSpace = undefined; } }