recording optimizations
This commit is contained in:
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user