mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 07:16:02 +00:00
Add native GPU static layout export path
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user