Add native GPU static layout export path

This commit is contained in:
wiiiii123
2026-05-03 22:43:23 +07:00
parent 816116de26
commit 9c81006e5c
62 changed files with 9819 additions and 328 deletions
+40 -1
View File
@@ -1,5 +1,10 @@
import { describe, expect, it } from "vitest";
import { selectRecordingMimeType } from "./recordingMimeType";
import {
getVideoExtensionForMimeType,
isWebmMimeType,
selectRecordingMimeType,
selectWebcamRecordingMimeType,
} from "./recordingMimeType";
describe("selectRecordingMimeType", () => {
it("prefers codecs the editor can play back", () => {
@@ -55,4 +60,38 @@ describe("selectRecordingMimeType", () => {
expect(mimeType).toBeUndefined();
});
it("prefers MP4/H.264 for webcam captures when supported", () => {
const mimeType = selectWebcamRecordingMimeType({
isTypeSupported: (type) =>
["video/mp4;codecs=avc1.42E01E", "video/webm;codecs=vp9"].includes(
type,
),
canPlayType: () => "probably",
});
expect(mimeType).toBe("video/mp4;codecs=avc1.42E01E");
});
it("falls back to WebM webcam capture when MP4 is unavailable", () => {
const mimeType = selectWebcamRecordingMimeType({
isTypeSupported: (type) =>
["video/webm;codecs=vp9", "video/webm"].includes(type),
canPlayType: () => "probably",
});
expect(mimeType).toBe("video/webm;codecs=vp9");
});
it("maps recording MIME types to the saved file extension", () => {
expect(getVideoExtensionForMimeType("video/mp4;codecs=avc1")).toBe(".mp4");
expect(getVideoExtensionForMimeType("video/webm;codecs=vp9")).toBe(".webm");
expect(getVideoExtensionForMimeType(undefined)).toBe(".webm");
});
it("detects WebM MIME types for duration repair", () => {
expect(isWebmMimeType("video/webm;codecs=vp9")).toBe(true);
expect(isWebmMimeType("video/mp4;codecs=avc1")).toBe(false);
expect(isWebmMimeType(undefined)).toBe(false);
});
});
+34 -4
View File
@@ -6,12 +6,24 @@ const RECORDING_MIME_TYPE_PREFERENCES = [
"video/webm;codecs=h264",
] as const;
const WEBCAM_RECORDING_MIME_TYPE_PREFERENCES = [
"video/mp4;codecs=avc1.42E01E",
"video/mp4;codecs=avc1",
"video/mp4;codecs=h264",
"video/mp4",
"video/webm;codecs=h264",
"video/webm;codecs=vp9",
"video/webm",
"video/webm;codecs=vp8",
] as const;
type MimeTypeSelectorOptions = {
isTypeSupported?: (type: string) => boolean;
canPlayType?: (type: string) => string;
};
export function selectRecordingMimeType(
function selectMimeTypeFromPreferences(
preferences: readonly string[],
options: MimeTypeSelectorOptions = {},
): string | undefined {
const isTypeSupported =
@@ -20,10 +32,28 @@ export function selectRecordingMimeType(
options.canPlayType ??
((type: string) => document.createElement("video").canPlayType(type));
const supportedTypes = RECORDING_MIME_TYPE_PREFERENCES.filter((type) =>
isTypeSupported(type),
);
const supportedTypes = preferences.filter((type) => isTypeSupported(type));
const playableType = supportedTypes.find((type) => canPlayType(type) !== "");
return playableType ?? supportedTypes[0];
}
export function selectRecordingMimeType(
options: MimeTypeSelectorOptions = {},
): string | undefined {
return selectMimeTypeFromPreferences(RECORDING_MIME_TYPE_PREFERENCES, options);
}
export function selectWebcamRecordingMimeType(
options: MimeTypeSelectorOptions = {},
): string | undefined {
return selectMimeTypeFromPreferences(WEBCAM_RECORDING_MIME_TYPE_PREFERENCES, options);
}
export function isWebmMimeType(mimeType: string | undefined | null): boolean {
return /^video\/webm(?:[;\s]|$)/i.test(mimeType ?? "");
}
export function getVideoExtensionForMimeType(mimeType: string | undefined | null): ".mp4" | ".webm" {
return /^video\/mp4(?:[;\s]|$)/i.test(mimeType ?? "") ? ".mp4" : ".webm";
}
+19 -8
View File
@@ -2,7 +2,12 @@ import { fixWebmDuration } from "@fix-webm-duration/fix";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { getEffectiveRecordingDurationMs } from "@/lib/mediaTiming";
import { selectRecordingMimeType } from "./recordingMimeType";
import {
getVideoExtensionForMimeType,
isWebmMimeType,
selectRecordingMimeType,
selectWebcamRecordingMimeType,
} from "./recordingMimeType";
const TARGET_FRAME_RATE = 60;
const TARGET_WIDTH = 3840;
@@ -290,6 +295,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
return selectRecordingMimeType();
}, []);
const selectWebcamMimeType = useCallback(() => {
return selectWebcamRecordingMimeType();
}, []);
const computeBitrate = (width: number, height: number) => {
const pixels = width * height;
const highFrameRateBoost =
@@ -576,7 +585,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
audio: false,
});
const mimeType = selectMimeType();
const mimeType = selectWebcamMimeType();
webcamChunks.current = [];
resolvedWebcamPath.current = null;
webcamStopPromise.current = new Promise((resolve) => {
@@ -601,7 +610,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
};
recorder.onstop = async () => {
const sessionTimestamp = recordingSessionTimestamp.current ?? Date.now();
const webcamFileName = `${RECORDING_FILE_PREFIX}${sessionTimestamp}${WEBCAM_SUFFIX}${VIDEO_FILE_EXTENSION}`;
const webcamMimeType = recorder.mimeType || mimeType;
const webcamFileName = `${RECORDING_FILE_PREFIX}${sessionTimestamp}${WEBCAM_SUFFIX}${getVideoExtensionForMimeType(webcamMimeType)}`;
try {
if (webcamChunks.current.length === 0) {
@@ -613,14 +623,15 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
0,
getRecordingDurationMs(Date.now()) - webcamTimeOffsetMs.current,
);
const webcamBlobType = recorder.mimeType || mimeType;
const webcamBlob = new Blob(
webcamChunks.current,
webcamBlobType ? { type: webcamBlobType } : undefined,
webcamMimeType ? { type: webcamMimeType } : undefined,
);
webcamChunks.current = [];
const fixedBlob = await fixWebmDuration(webcamBlob, duration);
const arrayBuffer = await fixedBlob.arrayBuffer();
const finalBlob = isWebmMimeType(webcamMimeType)
? await fixWebmDuration(webcamBlob, duration)
: webcamBlob;
const arrayBuffer = await finalBlob.arrayBuffer();
const result = await window.electronAPI.storeRecordedVideo(
arrayBuffer,
webcamFileName,
@@ -655,7 +666,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
webcamStream.current = null;
}
}
}, [getRecordingDurationMs, selectMimeType, webcamDeviceId, webcamEnabled]);
}, [getRecordingDurationMs, selectWebcamMimeType, webcamDeviceId, webcamEnabled]);
/** Start the prepared webcam MediaRecorder. Call after main recording begins. */
const beginWebcamCapture = useCallback(() => {