diff --git a/scripts/i18n-check.mjs b/scripts/i18n-check.mjs index ca5cb51..699ae9e 100644 --- a/scripts/i18n-check.mjs +++ b/scripts/i18n-check.mjs @@ -11,7 +11,6 @@ import path from "node:path"; const LOCALES_DIR = path.resolve("src/i18n/locales"); const BASE_LOCALE = "en"; -const COMPARE_LOCALES = ["zh-CN", "zh-TW", "es", "tr", "ko-KR"]; function getKeys(obj, prefix = "") { const keys = []; diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 80424b0..9b1cf6d 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -78,6 +78,7 @@ interface FrameRenderConfig { previewWidth?: number; previewHeight?: number; cursorTelemetry?: import("@/components/video-editor/types").CursorTelemetryPoint[]; + platform: string; } interface AnimationState { @@ -124,9 +125,11 @@ export class FrameRenderer { private smoothedAutoFocus: { cx: number; cy: number } | null = null; private prevAnimationTimeMs: number | null = null; private prevTargetProgress = 0; + private isLinux = false; constructor(config: FrameRenderConfig) { this.config = config; + this.isLinux = config.platform === "linux"; this.animationState = { scale: 1, focusX: DEFAULT_FOCUS.cx, @@ -187,8 +190,10 @@ export class FrameRenderer { this.compositeCanvas = document.createElement("canvas"); this.compositeCanvas.width = this.config.width; this.compositeCanvas.height = this.config.height; + + // On Linux, getImageData() is called frequently causing frequent CPU readback this.compositeCtx = this.compositeCanvas.getContext("2d", { - willReadFrequently: false, + willReadFrequently: this.isLinux, }); if (!this.compositeCtx) { @@ -726,7 +731,10 @@ export class FrameRenderer { private compositeWithShadows(webcamFrame?: VideoFrame | null): void { if (!this.compositeCanvas || !this.compositeCtx || !this.app) return; - const videoCanvas = this.readbackVideoCanvas(); + const videoCanvas = this.isLinux + ? this.readbackVideoCanvas() + : (this.app.canvas as HTMLCanvasElement); + const ctx = this.compositeCtx; const w = this.compositeCanvas.width; const h = this.compositeCanvas.height; diff --git a/src/lib/exporter/gifExporter.ts b/src/lib/exporter/gifExporter.ts index 58ed693..46ac6a0 100644 --- a/src/lib/exporter/gifExporter.ts +++ b/src/lib/exporter/gifExporter.ts @@ -8,6 +8,7 @@ import type { WebcamSizePreset, ZoomRegion, } from "@/components/video-editor/types"; +import { getPlatform } from "@/utils/platformUtils"; import { AsyncVideoFrameQueue } from "./asyncVideoFrameQueue"; import { FrameRenderer } from "./frameRenderer"; import { StreamingVideoDecoder } from "./streamingDecoder"; @@ -115,7 +116,10 @@ export class GifExporter { async export(): Promise { let webcamFrameQueue: AsyncVideoFrameQueue | null = null; + try { + const platform = await getPlatform(); + this.cleanup(); this.cancelled = false; @@ -153,6 +157,7 @@ export class GifExporter { previewWidth: this.config.previewWidth, previewHeight: this.config.previewHeight, cursorTelemetry: this.config.cursorTelemetry, + platform, }); await this.renderer.initialize(); diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index d007b30..42ba369 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -7,6 +7,7 @@ import type { WebcamSizePreset, ZoomRegion, } from "@/components/video-editor/types"; +import { getPlatform } from "@/utils/platformUtils"; import { AsyncVideoFrameQueue } from "./asyncVideoFrameQueue"; import { AudioProcessor } from "./audioEncoder"; import { FrameRenderer } from "./frameRenderer"; @@ -112,6 +113,8 @@ export class VideoExporter { this.fatalEncoderError = null; try { + const platform = await getPlatform(); + const streamingDecoder = new StreamingVideoDecoder(); this.streamingDecoder = streamingDecoder; const videoInfo = await streamingDecoder.loadMetadata(this.config.videoUrl); @@ -146,6 +149,7 @@ export class VideoExporter { previewWidth: this.config.previewWidth, previewHeight: this.config.previewHeight, cursorTelemetry: this.config.cursorTelemetry, + platform, }); this.renderer = renderer; await renderer.initialize(); @@ -231,25 +235,29 @@ export class VideoExporter { const canvas = renderer.getCanvas(); - // Read raw pixels from the canvas instead of passing - // the canvas directly to VideoFrame. On some Linux - // systems the GPU shared-image path (EGL/Ozone) fails - // silently, producing empty frames. - const canvasCtx = canvas.getContext("2d")!; - const imageData = canvasCtx.getImageData(0, 0, canvas.width, canvas.height); - const exportFrame = new VideoFrame(imageData.data.buffer, { - format: "RGBA", - codedWidth: canvas.width, - codedHeight: canvas.height, - timestamp, - duration: frameDuration, - colorSpace: { - primaries: "bt709", - transfer: "iec61966-2-1", - matrix: "rgb", - fullRange: true, - }, - }); + let exportFrame: VideoFrame; + + // On some Linux systems the GPU shared-image path (EGL/Ozone) fails + // silently, producing empty frames, so we force a CPU readback instead. + if (platform === "linux") { + const canvasCtx = canvas.getContext("2d")!; + const imageData = canvasCtx.getImageData(0, 0, canvas.width, canvas.height); + exportFrame = new VideoFrame(imageData.data.buffer, { + format: "RGBA", + codedWidth: canvas.width, + codedHeight: canvas.height, + timestamp, + duration: frameDuration, + colorSpace: { + primaries: "bt709", + transfer: "iec61966-2-1", + matrix: "rgb", + fullRange: true, + }, + }); + } else { + exportFrame = new VideoFrame(canvas, { timestamp, duration: frameDuration }); + } while ( this.encoder && diff --git a/src/utils/platformUtils.ts b/src/utils/platformUtils.ts index 37d5a6d..2fb57e1 100644 --- a/src/utils/platformUtils.ts +++ b/src/utils/platformUtils.ts @@ -3,7 +3,7 @@ let cachedPlatform: string | null = null; /** * Gets the current platform from Electron */ -const getPlatform = async (): Promise => { +export const getPlatform = async (): Promise => { if (cachedPlatform) return cachedPlatform; try { @@ -14,9 +14,14 @@ const getPlatform = async (): Promise => { console.warn("Failed to get platform from Electron, falling back to navigator:", error); // Fallback for development/testing let fallbackPlatform = "win32"; - if (typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform)) { - fallbackPlatform = "darwin"; + if (typeof navigator !== "undefined") { + if (/Mac|iPhone|iPad|iPod/.test(navigator.platform)) { + fallbackPlatform = "darwin"; + } else if (/Linux/.test(navigator.platform)) { + fallbackPlatform = "linux"; + } } + cachedPlatform = fallbackPlatform; return fallbackPlatform; }