recording optimizations

This commit is contained in:
Siddharth
2025-11-22 22:28:58 -07:00
parent e14ecbff56
commit 55a373c7ef
4 changed files with 48 additions and 50 deletions
-43
View File
@@ -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
+10 -1
View File
@@ -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,
+21 -5
View File
@@ -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);
+17 -1
View File
@@ -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;
}
}