Merge pull request #448 from theopfr/fix/cpu-readback-only-for-linux
fix: improve performance on windows and macos by passing canvas direclty to `VideoFrame()`
This commit is contained in:
@@ -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 = [];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<ExportResult> {
|
||||
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();
|
||||
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -3,7 +3,7 @@ let cachedPlatform: string | null = null;
|
||||
/**
|
||||
* Gets the current platform from Electron
|
||||
*/
|
||||
const getPlatform = async (): Promise<string> => {
|
||||
export const getPlatform = async (): Promise<string> => {
|
||||
if (cachedPlatform) return cachedPlatform;
|
||||
|
||||
try {
|
||||
@@ -14,9 +14,14 @@ const getPlatform = async (): Promise<string> => {
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user