Merge remote-tracking branch 'origin/main' into feat/zoom-hold-preview

# Conflicts:
#	src/components/video-editor/VideoPlayback.tsx
This commit is contained in:
Siddharth
2026-05-22 20:15:00 -07:00
18 changed files with 564 additions and 154 deletions
+8
View File
@@ -117,6 +117,14 @@ interface Window {
discarded?: boolean; discarded?: boolean;
error?: string; error?: string;
}>; }>;
pauseNativeWindowsRecording: () => Promise<{
success: boolean;
error?: string;
}>;
resumeNativeWindowsRecording: () => Promise<{
success: boolean;
error?: string;
}>;
startNativeMacRecording: ( startNativeMacRecording: (
request: import("../src/lib/nativeMacRecording").NativeMacRecordingRequest, request: import("../src/lib/nativeMacRecording").NativeMacRecordingRequest,
) => Promise<import("../src/lib/nativeMacRecording").NativeMacRecordingStartResult>; ) => Promise<import("../src/lib/nativeMacRecording").NativeMacRecordingStartResult>;
+75
View File
@@ -384,6 +384,10 @@ let nativeWindowsCaptureWebcamTargetPath: string | null = null;
let nativeWindowsCaptureRecordingId: number | null = null; let nativeWindowsCaptureRecordingId: number | null = null;
let nativeWindowsCursorOffsetMs = 0; let nativeWindowsCursorOffsetMs = 0;
let nativeWindowsCursorCaptureMode: CursorCaptureMode = "editable-overlay"; let nativeWindowsCursorCaptureMode: CursorCaptureMode = "editable-overlay";
let nativeWindowsCursorRecordingStartMs = 0;
let nativeWindowsPauseStartedAtMs: number | null = null;
let nativeWindowsPauseRanges: Array<{ startMs: number; endMs: number }> = [];
let nativeWindowsIsPaused = false;
const NATIVE_WINDOWS_CAPTURE_STOP_TIMEOUT_MS = 15_000; const NATIVE_WINDOWS_CAPTURE_STOP_TIMEOUT_MS = 15_000;
let nativeMacCaptureProcess: ChildProcessWithoutNullStreams | null = null; let nativeMacCaptureProcess: ChildProcessWithoutNullStreams | null = null;
let nativeMacCaptureOutput = ""; let nativeMacCaptureOutput = "";
@@ -873,6 +877,18 @@ function completeNativeMacCursorPauseRange(endMs = Date.now()) {
nativeMacPauseStartedAtMs = null; nativeMacPauseStartedAtMs = null;
} }
function completeNativeWindowsCursorPauseRange(endMs = Date.now()) {
if (nativeWindowsPauseStartedAtMs === null || nativeWindowsCursorRecordingStartMs <= 0) {
return;
}
nativeWindowsPauseRanges.push({
startMs: Math.max(0, nativeWindowsPauseStartedAtMs - nativeWindowsCursorRecordingStartMs),
endMs: Math.max(0, endMs - nativeWindowsCursorRecordingStartMs),
});
nativeWindowsPauseStartedAtMs = null;
}
function waitForNativeWindowsCaptureStart(proc: ChildProcessWithoutNullStreams) { function waitForNativeWindowsCaptureStart(proc: ChildProcessWithoutNullStreams) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
@@ -1583,9 +1599,14 @@ export function registerIpcHandlers(
nativeWindowsCaptureRecordingId = recordingId; nativeWindowsCaptureRecordingId = recordingId;
nativeWindowsCursorOffsetMs = 0; nativeWindowsCursorOffsetMs = 0;
nativeWindowsCursorCaptureMode = cursorCaptureMode; nativeWindowsCursorCaptureMode = cursorCaptureMode;
nativeWindowsCursorRecordingStartMs = 0;
nativeWindowsPauseStartedAtMs = null;
nativeWindowsPauseRanges = [];
nativeWindowsIsPaused = false;
const cursorStartTimeMs = Date.now(); const cursorStartTimeMs = Date.now();
if (cursorCaptureMode === "editable-overlay") { if (cursorCaptureMode === "editable-overlay") {
nativeWindowsCursorRecordingStartMs = cursorStartTimeMs;
await startCursorRecording(cursorStartTimeMs); await startCursorRecording(cursorStartTimeMs);
console.info("[native-wgc] cursor sampler ready", { console.info("[native-wgc] cursor sampler ready", {
cursorStartTimeMs, cursorStartTimeMs,
@@ -1635,6 +1656,10 @@ export function registerIpcHandlers(
nativeWindowsCaptureRecordingId = null; nativeWindowsCaptureRecordingId = null;
nativeWindowsCursorOffsetMs = 0; nativeWindowsCursorOffsetMs = 0;
nativeWindowsCursorCaptureMode = "editable-overlay"; nativeWindowsCursorCaptureMode = "editable-overlay";
nativeWindowsCursorRecordingStartMs = 0;
nativeWindowsPauseStartedAtMs = null;
nativeWindowsPauseRanges = [];
nativeWindowsIsPaused = false;
await stopCursorRecording(); await stopCursorRecording();
return { success: false, error: String(error) }; return { success: false, error: String(error) };
} }
@@ -1836,6 +1861,50 @@ export function registerIpcHandlers(
} }
}); });
ipcMain.handle("pause-native-windows-recording", async () => {
const proc = nativeWindowsCaptureProcess;
if (!proc) {
return { success: false, error: "Native Windows capture is not running." };
}
if (nativeWindowsIsPaused) {
return { success: true };
}
if (!proc.stdin.writable) {
return { success: false, error: "Native Windows capture command channel is closed." };
}
try {
proc.stdin.write("pause\n");
nativeWindowsIsPaused = true;
nativeWindowsPauseStartedAtMs = Date.now();
return { success: true };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
});
ipcMain.handle("resume-native-windows-recording", async () => {
const proc = nativeWindowsCaptureProcess;
if (!proc) {
return { success: false, error: "Native Windows capture is not running." };
}
if (!nativeWindowsIsPaused) {
return { success: true };
}
if (!proc.stdin.writable) {
return { success: false, error: "Native Windows capture command channel is closed." };
}
try {
proc.stdin.write("resume\n");
completeNativeWindowsCursorPauseRange();
nativeWindowsIsPaused = false;
return { success: true };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
});
ipcMain.handle("stop-native-windows-recording", async (_, discard?: boolean) => { ipcMain.handle("stop-native-windows-recording", async (_, discard?: boolean) => {
const proc = nativeWindowsCaptureProcess; const proc = nativeWindowsCaptureProcess;
const preferredPath = nativeWindowsCaptureTargetPath; const preferredPath = nativeWindowsCaptureTargetPath;
@@ -1848,6 +1917,7 @@ export function registerIpcHandlers(
} }
try { try {
completeNativeWindowsCursorPauseRange();
const stoppedPathPromise = waitForNativeWindowsCaptureStop(proc); const stoppedPathPromise = waitForNativeWindowsCaptureStop(proc);
proc.stdin.write("stop\n"); proc.stdin.write("stop\n");
const stoppedPath = await stoppedPathPromise; const stoppedPath = await stoppedPathPromise;
@@ -1872,6 +1942,7 @@ export function registerIpcHandlers(
} }
if (cursorCaptureMode === "editable-overlay") { if (cursorCaptureMode === "editable-overlay") {
compactPendingCursorTelemetryPauseRanges(nativeWindowsPauseRanges);
shiftPendingCursorTelemetry(nativeWindowsCursorOffsetMs); shiftPendingCursorTelemetry(nativeWindowsCursorOffsetMs);
await writePendingCursorTelemetry(screenVideoPath); await writePendingCursorTelemetry(screenVideoPath);
} }
@@ -1913,6 +1984,10 @@ export function registerIpcHandlers(
nativeWindowsCaptureRecordingId = null; nativeWindowsCaptureRecordingId = null;
nativeWindowsCursorOffsetMs = 0; nativeWindowsCursorOffsetMs = 0;
nativeWindowsCursorCaptureMode = "editable-overlay"; nativeWindowsCursorCaptureMode = "editable-overlay";
nativeWindowsCursorRecordingStartMs = 0;
nativeWindowsPauseStartedAtMs = null;
nativeWindowsPauseRanges = [];
nativeWindowsIsPaused = false;
const source = selectedSource || { name: "Screen" }; const source = selectedSource || { name: "Screen" };
if (onRecordingStateChange) { if (onRecordingStateChange) {
onRecordingStateChange(false, source.name); onRecordingStateChange(false, source.name);
@@ -279,6 +279,7 @@ bool AudioMixer::start() {
stopRequested_ = false; stopRequested_ = false;
emittedFrames_ = 0; emittedFrames_ = 0;
timelineStarted_ = false; timelineStarted_ = false;
paused_ = false;
thread_ = std::thread([this] { thread_ = std::thread([this] {
mixLoop(); mixLoop();
}); });
@@ -296,6 +297,18 @@ void AudioMixer::beginTimeline() {
cv_.notify_all(); cv_.notify_all();
} }
void AudioMixer::setPaused(bool paused) {
{
std::scoped_lock lock(mutex_);
paused_ = paused;
if (paused_) {
systemQueue_.clear();
microphoneQueue_.clear();
}
}
cv_.notify_all();
}
void AudioMixer::stop() { void AudioMixer::stop() {
stopRequested_ = true; stopRequested_ = true;
cv_.notify_all(); cv_.notify_all();
@@ -311,6 +324,9 @@ void AudioMixer::pushSystem(const BYTE* data, DWORD byteCount) {
{ {
std::scoped_lock lock(mutex_); std::scoped_lock lock(mutex_);
if (paused_) {
return;
}
append(systemQueue_, data, byteCount, systemFormat_, 1.0); append(systemQueue_, data, byteCount, systemFormat_, 1.0);
} }
cv_.notify_all(); cv_.notify_all();
@@ -323,6 +339,9 @@ void AudioMixer::pushMicrophone(const BYTE* data, DWORD byteCount) {
{ {
std::scoped_lock lock(mutex_); std::scoped_lock lock(mutex_);
if (paused_) {
return;
}
append(microphoneQueue_, data, byteCount, microphoneFormat_, microphoneGain_); append(microphoneQueue_, data, byteCount, microphoneFormat_, microphoneGain_);
} }
cv_.notify_all(); cv_.notify_all();
@@ -371,13 +390,13 @@ void AudioMixer::mixLoop() {
const bool hasMicrophone = !includeMicrophone_ || microphoneQueue_.size() >= chunkBytes; const bool hasMicrophone = !includeMicrophone_ || microphoneQueue_.size() >= chunkBytes;
const bool hasAnySource = !systemQueue_.empty() || !microphoneQueue_.empty(); const bool hasAnySource = !systemQueue_.empty() || !microphoneQueue_.empty();
return stopRequested_.load() || return stopRequested_.load() ||
(timelineStarted_ && (hasSystem || hasMicrophone) && hasAnySource); (timelineStarted_ && !paused_ && (hasSystem || hasMicrophone) && hasAnySource);
}); });
if (stopRequested_) { if (stopRequested_) {
break; break;
} }
if (!timelineStarted_) { if (!timelineStarted_ || paused_) {
continue; continue;
} }
@@ -52,6 +52,7 @@ public:
bool start(); bool start();
void beginTimeline(); void beginTimeline();
void setPaused(bool paused);
void stop(); void stop();
void pushSystem(const BYTE* data, DWORD byteCount); void pushSystem(const BYTE* data, DWORD byteCount);
void pushMicrophone(const BYTE* data, DWORD byteCount); void pushMicrophone(const BYTE* data, DWORD byteCount);
@@ -81,5 +82,6 @@ private:
std::thread thread_; std::thread thread_;
std::atomic<bool> stopRequested_ = false; std::atomic<bool> stopRequested_ = false;
bool timelineStarted_ = false; bool timelineStarted_ = false;
bool paused_ = false;
uint64_t emittedFrames_ = 0; uint64_t emittedFrames_ = 0;
}; };
+89 -32
View File
@@ -13,6 +13,7 @@
#include <condition_variable> #include <condition_variable>
#include <cctype> #include <cctype>
#include <cstdint> #include <cstdint>
#include <functional>
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
@@ -50,6 +51,37 @@ struct CaptureConfig {
int webcamFps = 0; int webcamFps = 0;
}; };
struct CaptureControl {
std::atomic<bool> stopRequested = false;
std::atomic<bool> paused = false;
std::mutex mutex;
std::condition_variable cv;
std::chrono::steady_clock::time_point pauseStartedAt;
std::chrono::steady_clock::duration totalPausedDuration{};
int64_t pausedDurationHns() {
std::scoped_lock lock(mutex);
auto total = totalPausedDuration;
if (paused.load()) {
total += std::chrono::steady_clock::now() - pauseStartedAt;
}
return std::chrono::duration_cast<std::chrono::nanoseconds>(total).count() / 100;
}
void setPaused(bool nextPaused) {
std::scoped_lock lock(mutex);
if (nextPaused == paused.load()) {
return;
}
if (nextPaused) {
pauseStartedAt = std::chrono::steady_clock::now();
} else {
totalPausedDuration += std::chrono::steady_clock::now() - pauseStartedAt;
}
paused = nextPaused;
}
};
std::wstring utf8ToWide(const std::string& value) { std::wstring utf8ToWide(const std::string& value) {
if (value.empty()) { if (value.empty()) {
return {}; return {};
@@ -319,17 +351,31 @@ bool parseConfig(const std::string& json, CaptureConfig& config) {
return true; return true;
} }
void readStopCommands(std::atomic<bool>& stopRequested, std::condition_variable& cv) { void readCaptureCommands(CaptureControl& control, const std::function<void(bool)>& onPauseChanged) {
std::string line; std::string line;
while (std::getline(std::cin, line)) { while (std::getline(std::cin, line)) {
if (line == "stop" || line == "q" || line == "quit") { if (line == "stop" || line == "q" || line == "quit") {
stopRequested = true; control.stopRequested = true;
cv.notify_all(); control.cv.notify_all();
return; return;
} }
if (line == "pause") {
control.setPaused(true);
onPauseChanged(true);
std::cout << "{\"event\":\"recording-paused\",\"schemaVersion\":2}" << std::endl;
control.cv.notify_all();
continue;
}
if (line == "resume") {
control.setPaused(false);
onPauseChanged(false);
std::cout << "{\"event\":\"recording-resumed\",\"schemaVersion\":2}" << std::endl;
control.cv.notify_all();
continue;
}
} }
stopRequested = true; control.stopRequested = true;
cv.notify_all(); control.cv.notify_all();
} }
} // namespace } // namespace
@@ -489,8 +535,7 @@ int main(int argc, char* argv[]) {
} }
std::mutex mutex; std::mutex mutex;
std::condition_variable cv; CaptureControl control;
std::atomic<bool> stopRequested = false;
std::atomic<bool> firstFrameWritten = false; std::atomic<bool> firstFrameWritten = false;
std::atomic<bool> encodeFailed = false; std::atomic<bool> encodeFailed = false;
Microsoft::WRL::ComPtr<ID3D11Texture2D> latestFrameTexture; Microsoft::WRL::ComPtr<ID3D11Texture2D> latestFrameTexture;
@@ -503,7 +548,7 @@ int main(int argc, char* argv[]) {
bool hasVisibleWebcamFrame = false; bool hasVisibleWebcamFrame = false;
session.setFrameCallback([&](ID3D11Texture2D* texture, int64_t timestampHns) { session.setFrameCallback([&](ID3D11Texture2D* texture, int64_t timestampHns) {
if (stopRequested) { if (control.stopRequested || control.paused) {
return; return;
} }
@@ -516,8 +561,8 @@ int main(int argc, char* argv[]) {
desc.MiscFlags = 0; desc.MiscFlags = 0;
if (FAILED(session.device()->CreateTexture2D(&desc, nullptr, &latestFrameTexture))) { if (FAILED(session.device()->CreateTexture2D(&desc, nullptr, &latestFrameTexture))) {
encodeFailed = true; encodeFailed = true;
stopRequested = true; control.stopRequested = true;
cv.notify_all(); control.cv.notify_all();
return; return;
} }
} }
@@ -525,20 +570,29 @@ int main(int argc, char* argv[]) {
session.context()->CopyResource(latestFrameTexture.Get(), texture); session.context()->CopyResource(latestFrameTexture.Get(), texture);
latestFrameTimestampHns = timestampHns; latestFrameTimestampHns = timestampHns;
if (!firstFrameWritten.exchange(true)) { if (!firstFrameWritten.exchange(true)) {
cv.notify_all(); control.cv.notify_all();
} }
}); });
auto writeVideoFrames = [&]() { auto writeVideoFrames = [&]() {
const auto startedAt = std::chrono::steady_clock::now(); const auto frameDuration = std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::duration<double>(1.0 / config.fps));
uint64_t frameIndex = 0; uint64_t frameIndex = 0;
uint64_t lastWrittenWebcamSequence = 0; uint64_t lastWrittenWebcamSequence = 0;
uint64_t webcamOutputFrameIndex = 0; uint64_t webcamOutputFrameIndex = 0;
int64_t lastEncodedVideoTimestampHns = -1; int64_t lastEncodedVideoTimestampHns = -1;
while (!stopRequested && !encodeFailed) { while (!control.stopRequested && !encodeFailed) {
{ {
std::scoped_lock lock(mutex); std::unique_lock lock(mutex);
control.cv.wait(lock, [&] {
return control.stopRequested.load() ||
encodeFailed.load() ||
(!control.paused.load() && latestFrameTexture);
});
if (control.stopRequested || encodeFailed) {
break;
}
if (webcamActive) { if (webcamActive) {
WebcamFrameSnapshot candidateWebcamFrame; WebcamFrameSnapshot candidateWebcamFrame;
if (webcamCapture.copyLatestFrame(candidateWebcamFrame) && if (webcamCapture.copyLatestFrame(candidateWebcamFrame) &&
@@ -564,7 +618,9 @@ int main(int argc, char* argv[]) {
firstFrameTimestampHns = sourceTimestampHns; firstFrameTimestampHns = sourceTimestampHns;
} }
int64_t frameTimestampHns = int64_t frameTimestampHns =
std::max<int64_t>(0, sourceTimestampHns - firstFrameTimestampHns); std::max<int64_t>(
0,
sourceTimestampHns - firstFrameTimestampHns - control.pausedDurationHns());
if (lastEncodedVideoTimestampHns >= 0 && if (lastEncodedVideoTimestampHns >= 0 &&
frameTimestampHns <= lastEncodedVideoTimestampHns) { frameTimestampHns <= lastEncodedVideoTimestampHns) {
frameTimestampHns = frameTimestampHns =
@@ -588,8 +644,8 @@ int main(int argc, char* argv[]) {
frameTimestampHns, frameTimestampHns,
!writeSeparateWebcam && webcamFrame.data ? &webcamFrame : nullptr)) { !writeSeparateWebcam && webcamFrame.data ? &webcamFrame : nullptr)) {
encodeFailed = true; encodeFailed = true;
stopRequested = true; control.stopRequested = true;
cv.notify_all(); control.cv.notify_all();
return; return;
} }
if (latestFrameTexture) { if (latestFrameTexture) {
@@ -598,10 +654,7 @@ int main(int argc, char* argv[]) {
} }
frameIndex += 1; frameIndex += 1;
const auto nextDeadline = startedAt + std::this_thread::sleep_for(frameDuration);
std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::duration<double>(static_cast<double>(frameIndex) / config.fps));
std::this_thread::sleep_until(nextDeadline);
} }
}; };
@@ -633,8 +686,8 @@ int main(int argc, char* argv[]) {
[&](const BYTE* data, DWORD byteCount, int64_t timestampHns, int64_t durationHns) { [&](const BYTE* data, DWORD byteCount, int64_t timestampHns, int64_t durationHns) {
if (!encoder.writeAudio(data, byteCount, timestampHns, durationHns)) { if (!encoder.writeAudio(data, byteCount, timestampHns, durationHns)) {
encodeFailed = true; encodeFailed = true;
stopRequested = true; control.stopRequested = true;
cv.notify_all(); control.cv.notify_all();
return false; return false;
} }
return true; return true;
@@ -649,7 +702,7 @@ int main(int argc, char* argv[]) {
if (!microphoneCapture.start([&](const BYTE* data, DWORD byteCount, int64_t timestampHns, int64_t durationHns) { if (!microphoneCapture.start([&](const BYTE* data, DWORD byteCount, int64_t timestampHns, int64_t durationHns) {
(void)timestampHns; (void)timestampHns;
(void)durationHns; (void)durationHns;
if (stopRequested || !audioMixer) { if (control.stopRequested || !audioMixer) {
return; return;
} }
@@ -665,7 +718,7 @@ int main(int argc, char* argv[]) {
if (!loopbackCapture.start([&](const BYTE* data, DWORD byteCount, int64_t timestampHns, int64_t durationHns) { if (!loopbackCapture.start([&](const BYTE* data, DWORD byteCount, int64_t timestampHns, int64_t durationHns) {
(void)timestampHns; (void)timestampHns;
(void)durationHns; (void)durationHns;
if (stopRequested || !audioMixer) { if (control.stopRequested || !audioMixer) {
return; return;
} }
@@ -726,16 +779,20 @@ int main(int argc, char* argv[]) {
return 1; return 1;
} }
std::thread stdinThread(readStopCommands, std::ref(stopRequested), std::ref(cv)); std::thread stdinThread(readCaptureCommands, std::ref(control), [&](bool isPaused) {
if (audioMixer) {
audioMixer->setPaused(isPaused);
}
});
{ {
std::unique_lock lock(mutex); std::unique_lock lock(mutex);
const bool started = cv.wait_for(lock, std::chrono::seconds(10), [&] { const bool started = control.cv.wait_for(lock, std::chrono::seconds(10), [&] {
return firstFrameWritten.load() || stopRequested.load(); return firstFrameWritten.load() || control.stopRequested.load();
}); });
if (!started || !firstFrameWritten) { if (!started || !firstFrameWritten) {
stopRequested = true; control.stopRequested = true;
cv.notify_all(); control.cv.notify_all();
if (stdinThread.joinable()) { if (stdinThread.joinable()) {
stdinThread.detach(); stdinThread.detach();
} }
@@ -761,8 +818,8 @@ int main(int argc, char* argv[]) {
{ {
std::unique_lock lock(mutex); std::unique_lock lock(mutex);
cv.wait(lock, [&] { control.cv.wait(lock, [&] {
return stopRequested.load(); return control.stopRequested.load();
}); });
} }
+6
View File
@@ -87,6 +87,12 @@ contextBridge.exposeInMainWorld("electronAPI", {
stopNativeWindowsRecording: (discard?: boolean) => { stopNativeWindowsRecording: (discard?: boolean) => {
return ipcRenderer.invoke("stop-native-windows-recording", discard); return ipcRenderer.invoke("stop-native-windows-recording", discard);
}, },
pauseNativeWindowsRecording: () => {
return ipcRenderer.invoke("pause-native-windows-recording");
},
resumeNativeWindowsRecording: () => {
return ipcRenderer.invoke("resume-native-windows-recording");
},
startNativeMacRecording: (request: NativeMacRecordingRequest) => { startNativeMacRecording: (request: NativeMacRecordingRequest) => {
return ipcRenderer.invoke("start-native-mac-recording", request); return ipcRenderer.invoke("start-native-mac-recording", request);
}, },
+19 -2
View File
@@ -37,6 +37,21 @@ function findVcVarsAll() {
return null; return null;
} }
function findWindowsSdkUmLibDir() {
const sdkLibRoot = "C:\\Program Files (x86)\\Windows Kits\\10\\Lib";
if (!fs.existsSync(sdkLibRoot)) {
return null;
}
return fs
.readdirSync(sdkLibRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => path.join(sdkLibRoot, entry.name, "um", "x64"))
.filter((candidate) => fs.existsSync(path.join(candidate, "kernel32.lib")))
.sort()
.at(-1);
}
function run(command, args, options = {}) { function run(command, args, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const child = spawn(command, args, { const child = spawn(command, args, {
@@ -64,6 +79,8 @@ async function runInVsEnv(command) {
); );
} }
const sdkUmLibDir = findWindowsSdkUmLibDir();
const cmdPath = path.join(os.tmpdir(), `openscreen-build-wgc-${process.pid}-${Date.now()}.cmd`); const cmdPath = path.join(os.tmpdir(), `openscreen-build-wgc-${process.pid}-${Date.now()}.cmd`);
fs.writeFileSync( fs.writeFileSync(
cmdPath, cmdPath,
@@ -72,9 +89,9 @@ async function runInVsEnv(command) {
`call "${vcvarsAll}" x64`, `call "${vcvarsAll}" x64`,
"if errorlevel 1 exit /b %errorlevel%", "if errorlevel 1 exit /b %errorlevel%",
`if not exist "${COMPAT_LIB_DIR}" mkdir "${COMPAT_LIB_DIR}"`, `if not exist "${COMPAT_LIB_DIR}" mkdir "${COMPAT_LIB_DIR}"`,
`for %%L in (gdi32.lib winspool.lib shell32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.lib) do if not exist "%WindowsSdkDir%Lib\\%WindowsSDKLibVersion%um\\x64\\%%L" copy /Y "%WindowsSdkDir%Lib\\%WindowsSDKLibVersion%um\\x64\\kernel32.Lib" "${COMPAT_LIB_DIR}\\%%L" >nul`, `for %%L in (gdi32.lib gdiplus.lib winspool.lib shell32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.lib) do if not exist "%WindowsSdkDir%Lib\\%WindowsSDKLibVersion%um\\x64\\%%L" copy /Y "%WindowsSdkDir%Lib\\%WindowsSDKLibVersion%um\\x64\\kernel32.Lib" "${COMPAT_LIB_DIR}\\%%L" >nul`,
"if errorlevel 1 exit /b %errorlevel%", "if errorlevel 1 exit /b %errorlevel%",
`set "LIB=${COMPAT_LIB_DIR};%LIB%"`, `set "LIB=${sdkUmLibDir ? `${sdkUmLibDir};` : ""}%LIB%;${COMPAT_LIB_DIR}"`,
command, command,
"exit /b %errorlevel%", "exit /b %errorlevel%",
"", "",
+3 -2
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { type AspectRatio } from "@/utils/aspectRatioUtils"; import { type AspectRatio } from "@/utils/aspectRatioUtils";
import { DEFAULT_SOURCE_DIMENSIONS } from "./editorDefaults";
interface CropRegion { interface CropRegion {
x: number; // 0-1 normalized x: number; // 0-1 normalized
@@ -32,8 +33,8 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
const ctx = canvas.getContext("2d", { alpha: false }); const ctx = canvas.getContext("2d", { alpha: false });
if (!ctx) return; if (!ctx) return;
canvas.width = videoElement.videoWidth || 1920; canvas.width = videoElement.videoWidth || DEFAULT_SOURCE_DIMENSIONS.width;
canvas.height = videoElement.videoHeight || 1080; canvas.height = videoElement.videoHeight || DEFAULT_SOURCE_DIMENSIONS.height;
const draw = () => { const draw = () => {
if (videoElement.readyState >= 2) { if (videoElement.readyState >= 2) {
+26 -19
View File
@@ -54,6 +54,14 @@ import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel";
import { BlurSettingsPanel } from "./BlurSettingsPanel"; import { BlurSettingsPanel } from "./BlurSettingsPanel";
import { CropControl } from "./CropControl"; import { CropControl } from "./CropControl";
import { parseCustomPlaybackSpeedInput } from "./customPlaybackSpeed"; import { parseCustomPlaybackSpeedInput } from "./customPlaybackSpeed";
import {
DEFAULT_CURSOR_SETTINGS,
DEFAULT_EDITOR_LAYOUT_SETTINGS,
DEFAULT_EXPORT_SETTINGS,
DEFAULT_GIF_SETTINGS,
DEFAULT_SOURCE_DIMENSIONS,
DEFAULT_WEBCAM_SETTINGS,
} from "./editorDefaults";
import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp"; import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp";
import type { import type {
AnnotationRegion, AnnotationRegion,
@@ -71,7 +79,6 @@ import type {
ZoomFocusMode, ZoomFocusMode,
} from "./types"; } from "./types";
import { import {
DEFAULT_WEBCAM_SIZE_PRESET,
MAX_ZOOM_SCALE, MAX_ZOOM_SCALE,
MIN_ZOOM_SCALE, MIN_ZOOM_SCALE,
ROTATION_3D_PRESET_ORDER, ROTATION_3D_PRESET_ORDER,
@@ -387,24 +394,24 @@ export function SettingsPanel({
borderRadius = 0, borderRadius = 0,
onBorderRadiusChange, onBorderRadiusChange,
onBorderRadiusCommit, onBorderRadiusCommit,
padding = 50, padding = DEFAULT_EDITOR_LAYOUT_SETTINGS.padding,
onPaddingChange, onPaddingChange,
onPaddingCommit, onPaddingCommit,
cropRegion, cropRegion,
onCropChange, onCropChange,
aspectRatio, aspectRatio,
videoElement, videoElement,
exportQuality = "good", exportQuality = DEFAULT_EXPORT_SETTINGS.quality,
onExportQualityChange, onExportQualityChange,
exportFormat = "mp4", exportFormat = DEFAULT_EXPORT_SETTINGS.format,
onExportFormatChange, onExportFormatChange,
gifFrameRate = 15, gifFrameRate = DEFAULT_GIF_SETTINGS.frameRate,
onGifFrameRateChange, onGifFrameRateChange,
gifLoop = true, gifLoop = DEFAULT_GIF_SETTINGS.loop,
onGifLoopChange, onGifLoopChange,
gifSizePreset = "medium", gifSizePreset = DEFAULT_GIF_SETTINGS.sizePreset,
onGifSizePresetChange, onGifSizePresetChange,
gifOutputDimensions = { width: 1280, height: 720 }, gifOutputDimensions = DEFAULT_GIF_SETTINGS.outputDimensions,
onExport, onExport,
unsavedExport, unsavedExport,
onSaveUnsavedExport, onSaveUnsavedExport,
@@ -426,25 +433,25 @@ export function SettingsPanel({
onSpeedChange, onSpeedChange,
onSpeedDelete, onSpeedDelete,
hasWebcam = false, hasWebcam = false,
webcamLayoutPreset = "picture-in-picture", webcamLayoutPreset = DEFAULT_WEBCAM_SETTINGS.layoutPreset,
onWebcamLayoutPresetChange, onWebcamLayoutPresetChange,
webcamMaskShape = "rectangle", webcamMaskShape = DEFAULT_WEBCAM_SETTINGS.maskShape,
onWebcamMaskShapeChange, onWebcamMaskShapeChange,
webcamSizePreset = DEFAULT_WEBCAM_SIZE_PRESET, webcamSizePreset = DEFAULT_WEBCAM_SETTINGS.sizePreset,
onWebcamSizePresetChange, onWebcamSizePresetChange,
onWebcamSizePresetCommit, onWebcamSizePresetCommit,
onSaveDiagnostic, onSaveDiagnostic,
showCursor = true, showCursor = DEFAULT_CURSOR_SETTINGS.show,
onShowCursorChange, onShowCursorChange,
cursorSize = 3.0, cursorSize = DEFAULT_CURSOR_SETTINGS.size,
onCursorSizeChange, onCursorSizeChange,
cursorSmoothing = 0.67, cursorSmoothing = DEFAULT_CURSOR_SETTINGS.smoothing,
onCursorSmoothingChange, onCursorSmoothingChange,
cursorMotionBlur = 0.35, cursorMotionBlur = DEFAULT_CURSOR_SETTINGS.motionBlur,
onCursorMotionBlurChange, onCursorMotionBlurChange,
cursorClickBounce = 2.5, cursorClickBounce = DEFAULT_CURSOR_SETTINGS.clickBounce,
onCursorClickBounceChange, onCursorClickBounceChange,
cursorClipToBounds = false, cursorClipToBounds = DEFAULT_CURSOR_SETTINGS.clipToBounds,
onCursorClipToBoundsChange, onCursorClipToBoundsChange,
hasCursorData = false, hasCursorData = false,
showCursorSettings = true, showCursorSettings = true,
@@ -483,8 +490,8 @@ export function SettingsPanel({
const [cropAspectRatio, setCropAspectRatio] = useState(""); const [cropAspectRatio, setCropAspectRatio] = useState("");
const isPortraitCanvas = isPortraitAspectRatio(aspectRatio); const isPortraitCanvas = isPortraitAspectRatio(aspectRatio);
const videoWidth = videoElement?.videoWidth || 1920; const videoWidth = videoElement?.videoWidth || DEFAULT_SOURCE_DIMENSIONS.width;
const videoHeight = videoElement?.videoHeight || 1080; const videoHeight = videoElement?.videoHeight || DEFAULT_SOURCE_DIMENSIONS.height;
const handleCropNumericChange = useCallback( const handleCropNumericChange = useCallback(
(field: "x" | "y" | "width" | "height", pixelValue: number) => { (field: "x" | "y" | "width" | "height", pixelValue: number) => {
+45 -30
View File
@@ -49,6 +49,12 @@ import {
isPortraitAspectRatio, isPortraitAspectRatio,
} from "@/utils/aspectRatioUtils"; } from "@/utils/aspectRatioUtils";
import { ExportDialog } from "./ExportDialog"; import { ExportDialog } from "./ExportDialog";
import {
DEFAULT_CURSOR_SETTINGS,
DEFAULT_EXPORT_SETTINGS,
DEFAULT_GIF_SETTINGS,
DEFAULT_SOURCE_DIMENSIONS,
} from "./editorDefaults";
import PlaybackControls from "./PlaybackControls"; import PlaybackControls from "./PlaybackControls";
import { import {
createProjectData, createProjectData,
@@ -71,11 +77,6 @@ import {
DEFAULT_ANNOTATION_SIZE, DEFAULT_ANNOTATION_SIZE,
DEFAULT_ANNOTATION_STYLE, DEFAULT_ANNOTATION_STYLE,
DEFAULT_BLUR_DATA, DEFAULT_BLUR_DATA,
DEFAULT_CURSOR_CLICK_BOUNCE,
DEFAULT_CURSOR_CLIP_TO_BOUNDS,
DEFAULT_CURSOR_MOTION_BLUR,
DEFAULT_CURSOR_SIZE,
DEFAULT_CURSOR_SMOOTHING,
DEFAULT_FIGURE_DATA, DEFAULT_FIGURE_DATA,
DEFAULT_PLAYBACK_SPEED, DEFAULT_PLAYBACK_SPEED,
DEFAULT_ZOOM_DEPTH, DEFAULT_ZOOM_DEPTH,
@@ -204,11 +205,15 @@ export default function VideoEditor() {
const [exportError, setExportError] = useState<string | null>(null); const [exportError, setExportError] = useState<string | null>(null);
const [showExportDialog, setShowExportDialog] = useState(false); const [showExportDialog, setShowExportDialog] = useState(false);
const [showNewRecordingDialog, setShowNewRecordingDialog] = useState(false); const [showNewRecordingDialog, setShowNewRecordingDialog] = useState(false);
const [exportQuality, setExportQuality] = useState<ExportQuality>("good"); const [exportQuality, setExportQuality] = useState<ExportQuality>(
const [exportFormat, setExportFormat] = useState<ExportFormat>("mp4"); DEFAULT_EXPORT_SETTINGS.quality,
const [gifFrameRate, setGifFrameRate] = useState<GifFrameRate>(15); );
const [gifLoop, setGifLoop] = useState(true); const [exportFormat, setExportFormat] = useState<ExportFormat>(DEFAULT_EXPORT_SETTINGS.format);
const [gifSizePreset, setGifSizePreset] = useState<GifSizePreset>("medium"); const [gifFrameRate, setGifFrameRate] = useState<GifFrameRate>(DEFAULT_GIF_SETTINGS.frameRate);
const [gifLoop, setGifLoop] = useState(DEFAULT_GIF_SETTINGS.loop);
const [gifSizePreset, setGifSizePreset] = useState<GifSizePreset>(
DEFAULT_GIF_SETTINGS.sizePreset,
);
const [exportedFilePath, setExportedFilePath] = useState<string | null>(null); const [exportedFilePath, setExportedFilePath] = useState<string | null>(null);
const [lastSavedSnapshot, setLastSavedSnapshot] = useState<string | null>(null); const [lastSavedSnapshot, setLastSavedSnapshot] = useState<string | null>(null);
const [unsavedExport, setUnsavedExport] = useState<{ const [unsavedExport, setUnsavedExport] = useState<{
@@ -239,12 +244,14 @@ export default function VideoEditor() {
}, [cursorRecordingData, cursorTelemetry]); }, [cursorRecordingData, cursorTelemetry]);
// Cursor & motion blur visual settings (non-undoable preferences) // Cursor & motion blur visual settings (non-undoable preferences)
const [showCursor, setShowCursor] = useState(true); const [showCursor, setShowCursor] = useState(DEFAULT_CURSOR_SETTINGS.show);
const [cursorSize, setCursorSize] = useState(DEFAULT_CURSOR_SIZE); const [cursorSize, setCursorSize] = useState(DEFAULT_CURSOR_SETTINGS.size);
const [cursorSmoothing, setCursorSmoothing] = useState(DEFAULT_CURSOR_SMOOTHING); const [cursorSmoothing, setCursorSmoothing] = useState(DEFAULT_CURSOR_SETTINGS.smoothing);
const [cursorMotionBlur, setCursorMotionBlur] = useState(DEFAULT_CURSOR_MOTION_BLUR); const [cursorMotionBlur, setCursorMotionBlur] = useState(DEFAULT_CURSOR_SETTINGS.motionBlur);
const [cursorClickBounce, setCursorClickBounce] = useState(DEFAULT_CURSOR_CLICK_BOUNCE); const [cursorClickBounce, setCursorClickBounce] = useState(DEFAULT_CURSOR_SETTINGS.clickBounce);
const [cursorClipToBounds, setCursorClipToBounds] = useState(DEFAULT_CURSOR_CLIP_TO_BOUNDS); const [cursorClipToBounds, setCursorClipToBounds] = useState(
DEFAULT_CURSOR_SETTINGS.clipToBounds,
);
const [nativePlatform, setNativePlatform] = useState<NativePlatform | null>(null); const [nativePlatform, setNativePlatform] = useState<NativePlatform | null>(null);
const [recordingCursorCaptureMode, setRecordingCursorCaptureMode] = const [recordingCursorCaptureMode, setRecordingCursorCaptureMode] =
useState<CursorCaptureMode | null>(null); useState<CursorCaptureMode | null>(null);
@@ -1576,8 +1583,8 @@ export default function VideoEditor() {
videoPlaybackRef.current?.pause(); videoPlaybackRef.current?.pause();
} }
const sourceWidth = video.videoWidth || 1920; const sourceWidth = video.videoWidth || DEFAULT_SOURCE_DIMENSIONS.width;
const sourceHeight = video.videoHeight || 1080; const sourceHeight = video.videoHeight || DEFAULT_SOURCE_DIMENSIONS.height;
const effectiveSourceDimensions = calculateEffectiveSourceDimensions( const effectiveSourceDimensions = calculateEffectiveSourceDimensions(
sourceWidth, sourceWidth,
sourceHeight, sourceHeight,
@@ -1591,8 +1598,8 @@ export default function VideoEditor() {
// Get preview CONTAINER dimensions for scaling // Get preview CONTAINER dimensions for scaling
const playbackRef = videoPlaybackRef.current; const playbackRef = videoPlaybackRef.current;
const containerElement = playbackRef?.containerRef?.current; const containerElement = playbackRef?.containerRef?.current;
const previewWidth = containerElement?.clientWidth || 1920; const previewWidth = containerElement?.clientWidth || DEFAULT_SOURCE_DIMENSIONS.width;
const previewHeight = containerElement?.clientHeight || 1080; const previewHeight = containerElement?.clientHeight || DEFAULT_SOURCE_DIMENSIONS.height;
if (settings.format === "gif" && settings.gifConfig) { if (settings.format === "gif" && settings.gifConfig) {
// GIF Export // GIF Export
@@ -1846,8 +1853,8 @@ export default function VideoEditor() {
} }
// Build export settings from current state // Build export settings from current state
const sourceWidth = video.videoWidth || 1920; const sourceWidth = video.videoWidth || DEFAULT_SOURCE_DIMENSIONS.width;
const sourceHeight = video.videoHeight || 1080; const sourceHeight = video.videoHeight || DEFAULT_SOURCE_DIMENSIONS.height;
const effectiveSourceDimensions = calculateEffectiveSourceDimensions( const effectiveSourceDimensions = calculateEffectiveSourceDimensions(
sourceWidth, sourceWidth,
sourceHeight, sourceHeight,
@@ -2051,8 +2058,10 @@ export default function VideoEditor() {
aspectRatio: aspectRatio:
aspectRatio === "native" aspectRatio === "native"
? getNativeAspectRatioValue( ? getNativeAspectRatioValue(
videoPlaybackRef.current?.video?.videoWidth || 1920, videoPlaybackRef.current?.video?.videoWidth ||
videoPlaybackRef.current?.video?.videoHeight || 1080, DEFAULT_SOURCE_DIMENSIONS.width,
videoPlaybackRef.current?.video?.videoHeight ||
DEFAULT_SOURCE_DIMENSIONS.height,
cropRegion, cropRegion,
) )
: getAspectRatioValue(aspectRatio), : getAspectRatioValue(aspectRatio),
@@ -2221,21 +2230,27 @@ export default function VideoEditor() {
onGifSizePresetChange={setGifSizePreset} onGifSizePresetChange={setGifSizePreset}
gifOutputDimensions={calculateOutputDimensions( gifOutputDimensions={calculateOutputDimensions(
calculateEffectiveSourceDimensions( calculateEffectiveSourceDimensions(
videoPlaybackRef.current?.video?.videoWidth || 1920, videoPlaybackRef.current?.video?.videoWidth ||
videoPlaybackRef.current?.video?.videoHeight || 1080, DEFAULT_SOURCE_DIMENSIONS.width,
videoPlaybackRef.current?.video?.videoHeight ||
DEFAULT_SOURCE_DIMENSIONS.height,
cropRegion, cropRegion,
).width, ).width,
calculateEffectiveSourceDimensions( calculateEffectiveSourceDimensions(
videoPlaybackRef.current?.video?.videoWidth || 1920, videoPlaybackRef.current?.video?.videoWidth ||
videoPlaybackRef.current?.video?.videoHeight || 1080, DEFAULT_SOURCE_DIMENSIONS.width,
videoPlaybackRef.current?.video?.videoHeight ||
DEFAULT_SOURCE_DIMENSIONS.height,
cropRegion, cropRegion,
).height, ).height,
gifSizePreset, gifSizePreset,
GIF_SIZE_PRESETS, GIF_SIZE_PRESETS,
aspectRatio === "native" aspectRatio === "native"
? getNativeAspectRatioValue( ? getNativeAspectRatioValue(
videoPlaybackRef.current?.video?.videoWidth || 1920, videoPlaybackRef.current?.video?.videoWidth ||
videoPlaybackRef.current?.video?.videoHeight || 1080, DEFAULT_SOURCE_DIMENSIONS.width,
videoPlaybackRef.current?.video?.videoHeight ||
DEFAULT_SOURCE_DIMENSIONS.height,
cropRegion, cropRegion,
) )
: getAspectRatioValue(aspectRatio), : getAspectRatioValue(aspectRatio),
+13 -12
View File
@@ -49,15 +49,16 @@ import {
getNativeAspectRatioValue, getNativeAspectRatioValue,
} from "@/utils/aspectRatioUtils"; } from "@/utils/aspectRatioUtils";
import { AnnotationOverlay } from "./AnnotationOverlay"; import { AnnotationOverlay } from "./AnnotationOverlay";
import {
DEFAULT_CURSOR_SETTINGS,
DEFAULT_EDITOR_LAYOUT_SETTINGS,
DEFAULT_SOURCE_DIMENSIONS,
} from "./editorDefaults";
import { import {
type AnnotationRegion, type AnnotationRegion,
type BlurData, type BlurData,
type CursorTelemetryPoint, type CursorTelemetryPoint,
computeRotation3DContainScale, computeRotation3DContainScale,
DEFAULT_CURSOR_CLICK_BOUNCE,
DEFAULT_CURSOR_MOTION_BLUR,
DEFAULT_CURSOR_SIZE,
DEFAULT_CURSOR_SMOOTHING,
DEFAULT_ROTATION_3D, DEFAULT_ROTATION_3D,
isRotation3DIdentity, isRotation3DIdentity,
lerpRotation3D, lerpRotation3D,
@@ -247,7 +248,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
showBlur, showBlur,
motionBlurAmount = 0, motionBlurAmount = 0,
borderRadius = 0, borderRadius = 0,
padding = 50, padding = DEFAULT_EDITOR_LAYOUT_SETTINGS.padding,
cropRegion, cropRegion,
trimRegions = [], trimRegions = [],
speedRegions = [], speedRegions = [],
@@ -268,11 +269,11 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
cursorTelemetry = [], cursorTelemetry = [],
cursorClickTimestamps = [], cursorClickTimestamps = [],
showCursor = false, showCursor = false,
cursorSize = DEFAULT_CURSOR_SIZE, cursorSize = DEFAULT_CURSOR_SETTINGS.size,
cursorSmoothing = DEFAULT_CURSOR_SMOOTHING, cursorSmoothing = DEFAULT_CURSOR_SETTINGS.smoothing,
cursorMotionBlur = DEFAULT_CURSOR_MOTION_BLUR, cursorMotionBlur = DEFAULT_CURSOR_SETTINGS.motionBlur,
cursorClickBounce = DEFAULT_CURSOR_CLICK_BOUNCE, cursorClickBounce = DEFAULT_CURSOR_SETTINGS.clickBounce,
cursorClipToBounds = false, cursorClipToBounds = DEFAULT_CURSOR_SETTINGS.clipToBounds,
isPreviewingZoom = false, isPreviewingZoom = false,
}, },
ref, ref,
@@ -1834,8 +1835,8 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
aspectRatio, aspectRatio,
aspectRatio === "native" aspectRatio === "native"
? getNativeAspectRatioValue( ? getNativeAspectRatioValue(
lockedVideoDimensionsRef.current?.width || 1920, lockedVideoDimensionsRef.current?.width || DEFAULT_SOURCE_DIMENSIONS.width,
lockedVideoDimensionsRef.current?.height || 1080, lockedVideoDimensionsRef.current?.height || DEFAULT_SOURCE_DIMENSIONS.height,
cropRegion, cropRegion,
) )
: undefined, : undefined,
@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { INITIAL_EDITOR_STATE } from "@/hooks/useEditorHistory";
import { DEFAULT_PREFS } from "@/lib/userPreferences";
import {
DEFAULT_EDITOR_APPEARANCE_SETTINGS,
DEFAULT_EDITOR_LAYOUT_SETTINGS,
DEFAULT_EXPORT_SETTINGS,
DEFAULT_GIF_SETTINGS,
DEFAULT_WEBCAM_SETTINGS,
} from "./editorDefaults";
import { normalizeProjectEditor } from "./projectPersistence";
describe("editor defaults SSOT", () => {
it("keeps history defaults aligned with editor defaults", () => {
expect(INITIAL_EDITOR_STATE).toMatchObject({
...DEFAULT_EDITOR_APPEARANCE_SETTINGS,
padding: DEFAULT_EDITOR_LAYOUT_SETTINGS.padding,
aspectRatio: DEFAULT_EDITOR_LAYOUT_SETTINGS.aspectRatio,
cropRegion: DEFAULT_EDITOR_LAYOUT_SETTINGS.cropRegion,
wallpaper: DEFAULT_EDITOR_LAYOUT_SETTINGS.wallpaper,
webcamLayoutPreset: DEFAULT_WEBCAM_SETTINGS.layoutPreset,
webcamMaskShape: DEFAULT_WEBCAM_SETTINGS.maskShape,
webcamSizePreset: DEFAULT_WEBCAM_SETTINGS.sizePreset,
webcamPosition: DEFAULT_WEBCAM_SETTINGS.position,
});
});
it("keeps user preference defaults aligned with editor and export defaults", () => {
expect(DEFAULT_PREFS).toMatchObject({
padding: DEFAULT_EDITOR_LAYOUT_SETTINGS.padding,
aspectRatio: DEFAULT_EDITOR_LAYOUT_SETTINGS.aspectRatio,
exportQuality: DEFAULT_EXPORT_SETTINGS.quality,
exportFormat: DEFAULT_EXPORT_SETTINGS.format,
});
});
it("keeps project fallback normalization aligned with editor defaults", () => {
expect(normalizeProjectEditor({})).toMatchObject({
...DEFAULT_EDITOR_APPEARANCE_SETTINGS,
padding: DEFAULT_EDITOR_LAYOUT_SETTINGS.padding,
cropRegion: DEFAULT_EDITOR_LAYOUT_SETTINGS.cropRegion,
wallpaper: DEFAULT_EDITOR_LAYOUT_SETTINGS.wallpaper,
aspectRatio: DEFAULT_EDITOR_LAYOUT_SETTINGS.aspectRatio,
webcamLayoutPreset: DEFAULT_WEBCAM_SETTINGS.layoutPreset,
webcamMaskShape: DEFAULT_WEBCAM_SETTINGS.maskShape,
webcamSizePreset: DEFAULT_WEBCAM_SETTINGS.sizePreset,
webcamPosition: DEFAULT_WEBCAM_SETTINGS.position,
exportQuality: DEFAULT_EXPORT_SETTINGS.quality,
exportFormat: DEFAULT_EXPORT_SETTINGS.format,
gifFrameRate: DEFAULT_GIF_SETTINGS.frameRate,
gifLoop: DEFAULT_GIF_SETTINGS.loop,
gifSizePreset: DEFAULT_GIF_SETTINGS.sizePreset,
});
});
});
@@ -0,0 +1,95 @@
import type { ExportFormat, ExportQuality, GifFrameRate, GifSizePreset } from "@/lib/exporter";
import { DEFAULT_WALLPAPER } from "@/lib/wallpaper";
import type { AspectRatio } from "@/utils/aspectRatioUtils";
import {
type CursorVisualSettings,
DEFAULT_CROP_REGION,
DEFAULT_CURSOR_CLICK_BOUNCE,
DEFAULT_CURSOR_CLIP_TO_BOUNDS,
DEFAULT_CURSOR_MOTION_BLUR,
DEFAULT_CURSOR_SIZE,
DEFAULT_CURSOR_SMOOTHING,
DEFAULT_WEBCAM_LAYOUT_PRESET,
DEFAULT_WEBCAM_MASK_SHAPE,
DEFAULT_WEBCAM_POSITION,
DEFAULT_WEBCAM_SIZE_PRESET,
type WebcamLayoutPreset,
type WebcamMaskShape,
type WebcamPosition,
type WebcamSizePreset,
} from "./types";
export const DEFAULT_SOURCE_DIMENSIONS = {
width: 1920,
height: 1080,
} as const;
export const DEFAULT_GIF_OUTPUT_DIMENSIONS = {
width: 1280,
height: 720,
} as const;
export const DEFAULT_EDITOR_APPEARANCE_SETTINGS: {
shadowIntensity: number;
showBlur: boolean;
motionBlurAmount: number;
borderRadius: number;
} = {
shadowIntensity: 0,
showBlur: false,
motionBlurAmount: 0,
borderRadius: 0,
};
export const DEFAULT_EDITOR_LAYOUT_SETTINGS: {
padding: number;
aspectRatio: AspectRatio;
cropRegion: typeof DEFAULT_CROP_REGION;
wallpaper: string;
} = {
padding: 50,
aspectRatio: "16:9",
cropRegion: DEFAULT_CROP_REGION,
wallpaper: DEFAULT_WALLPAPER,
};
export const DEFAULT_WEBCAM_SETTINGS = {
layoutPreset: DEFAULT_WEBCAM_LAYOUT_PRESET,
maskShape: DEFAULT_WEBCAM_MASK_SHAPE,
sizePreset: DEFAULT_WEBCAM_SIZE_PRESET,
position: DEFAULT_WEBCAM_POSITION,
} as const satisfies {
layoutPreset: WebcamLayoutPreset;
maskShape: WebcamMaskShape;
sizePreset: WebcamSizePreset;
position: WebcamPosition | null;
};
export const DEFAULT_CURSOR_SETTINGS: CursorVisualSettings & { show: boolean } = {
show: true,
size: DEFAULT_CURSOR_SIZE,
smoothing: DEFAULT_CURSOR_SMOOTHING,
motionBlur: DEFAULT_CURSOR_MOTION_BLUR,
clickBounce: DEFAULT_CURSOR_CLICK_BOUNCE,
clipToBounds: DEFAULT_CURSOR_CLIP_TO_BOUNDS,
};
export const DEFAULT_EXPORT_SETTINGS: {
quality: ExportQuality;
format: ExportFormat;
} = {
quality: "good",
format: "mp4",
};
export const DEFAULT_GIF_SETTINGS: {
frameRate: GifFrameRate;
loop: boolean;
sizePreset: GifSizePreset;
outputDimensions: typeof DEFAULT_GIF_OUTPUT_DIMENSIONS;
} = {
frameRate: 15,
loop: true,
sizePreset: "medium",
outputDimensions: DEFAULT_GIF_OUTPUT_DIMENSIONS,
};
@@ -4,6 +4,13 @@ import type { ProjectMedia } from "@/lib/recordingSession";
import { normalizeProjectMedia } from "@/lib/recordingSession"; import { normalizeProjectMedia } from "@/lib/recordingSession";
import { DEFAULT_WALLPAPER, WALLPAPER_PATHS } from "@/lib/wallpaper"; import { DEFAULT_WALLPAPER, WALLPAPER_PATHS } from "@/lib/wallpaper";
import { ASPECT_RATIOS, type AspectRatio, isPortraitAspectRatio } from "@/utils/aspectRatioUtils"; import { ASPECT_RATIOS, type AspectRatio, isPortraitAspectRatio } from "@/utils/aspectRatioUtils";
import {
DEFAULT_EDITOR_APPEARANCE_SETTINGS,
DEFAULT_EDITOR_LAYOUT_SETTINGS,
DEFAULT_EXPORT_SETTINGS,
DEFAULT_GIF_SETTINGS,
DEFAULT_WEBCAM_SETTINGS,
} from "./editorDefaults";
import { import {
type AnnotationRegion, type AnnotationRegion,
type CropRegion, type CropRegion,
@@ -15,14 +22,10 @@ import {
DEFAULT_BLUR_DATA, DEFAULT_BLUR_DATA,
DEFAULT_BLUR_FREEHAND_POINTS, DEFAULT_BLUR_FREEHAND_POINTS,
DEFAULT_BLUR_INTENSITY, DEFAULT_BLUR_INTENSITY,
DEFAULT_CROP_REGION,
DEFAULT_FIGURE_DATA, DEFAULT_FIGURE_DATA,
DEFAULT_PLAYBACK_SPEED, DEFAULT_PLAYBACK_SPEED,
DEFAULT_WEBCAM_LAYOUT_PRESET,
DEFAULT_WEBCAM_MASK_SHAPE,
DEFAULT_WEBCAM_POSITION,
DEFAULT_WEBCAM_SIZE_PRESET,
DEFAULT_ZOOM_DEPTH, DEFAULT_ZOOM_DEPTH,
DEFAULT_ZOOM_MOTION_BLUR,
MAX_BLUR_BLOCK_SIZE, MAX_BLUR_BLOCK_SIZE,
MAX_BLUR_INTENSITY, MAX_BLUR_INTENSITY,
MAX_PLAYBACK_SPEED, MAX_PLAYBACK_SPEED,
@@ -104,13 +107,13 @@ function computeNormalizedWebcamLayoutPreset(
case "vertical-stack": case "vertical-stack":
return isPortraitAspectRatio(normalizedAspectRatio) return isPortraitAspectRatio(normalizedAspectRatio)
? webcamLayoutPreset ? webcamLayoutPreset
: DEFAULT_WEBCAM_LAYOUT_PRESET; : DEFAULT_WEBCAM_SETTINGS.layoutPreset;
case "dual-frame": case "dual-frame":
return isPortraitAspectRatio(normalizedAspectRatio) return isPortraitAspectRatio(normalizedAspectRatio)
? DEFAULT_WEBCAM_LAYOUT_PRESET ? DEFAULT_WEBCAM_SETTINGS.layoutPreset
: webcamLayoutPreset; : webcamLayoutPreset;
default: default:
return DEFAULT_WEBCAM_LAYOUT_PRESET; return DEFAULT_WEBCAM_SETTINGS.layoutPreset;
} }
} }
@@ -211,7 +214,7 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
editor.aspectRatio as AspectRatio, editor.aspectRatio as AspectRatio,
) )
? (editor.aspectRatio as AspectRatio) ? (editor.aspectRatio as AspectRatio)
: "16:9"; : DEFAULT_EDITOR_LAYOUT_SETTINGS.aspectRatio;
const normalizedWebcamLayoutPreset = computeNormalizedWebcamLayoutPreset( const normalizedWebcamLayoutPreset = computeNormalizedWebcamLayoutPreset(
editor.webcamLayoutPreset, editor.webcamLayoutPreset,
normalizedAspectRatio, normalizedAspectRatio,
@@ -226,7 +229,7 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
cx: clamp((editor.webcamPosition as WebcamPosition).cx, 0, 1), cx: clamp((editor.webcamPosition as WebcamPosition).cx, 0, 1),
cy: clamp((editor.webcamPosition as WebcamPosition).cy, 0, 1), cy: clamp((editor.webcamPosition as WebcamPosition).cy, 0, 1),
} }
: DEFAULT_WEBCAM_POSITION; : DEFAULT_WEBCAM_SETTINGS.position;
const normalizedZoomRegions: ZoomRegion[] = Array.isArray(editor.zoomRegions) const normalizedZoomRegions: ZoomRegion[] = Array.isArray(editor.zoomRegions)
? editor.zoomRegions ? editor.zoomRegions
@@ -413,16 +416,16 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
const rawCropX = isFiniteNumber(editor.cropRegion?.x) const rawCropX = isFiniteNumber(editor.cropRegion?.x)
? editor.cropRegion.x ? editor.cropRegion.x
: DEFAULT_CROP_REGION.x; : DEFAULT_EDITOR_LAYOUT_SETTINGS.cropRegion.x;
const rawCropY = isFiniteNumber(editor.cropRegion?.y) const rawCropY = isFiniteNumber(editor.cropRegion?.y)
? editor.cropRegion.y ? editor.cropRegion.y
: DEFAULT_CROP_REGION.y; : DEFAULT_EDITOR_LAYOUT_SETTINGS.cropRegion.y;
const rawCropWidth = isFiniteNumber(editor.cropRegion?.width) const rawCropWidth = isFiniteNumber(editor.cropRegion?.width)
? editor.cropRegion.width ? editor.cropRegion.width
: DEFAULT_CROP_REGION.width; : DEFAULT_EDITOR_LAYOUT_SETTINGS.cropRegion.width;
const rawCropHeight = isFiniteNumber(editor.cropRegion?.height) const rawCropHeight = isFiniteNumber(editor.cropRegion?.height)
? editor.cropRegion.height ? editor.cropRegion.height
: DEFAULT_CROP_REGION.height; : DEFAULT_EDITOR_LAYOUT_SETTINGS.cropRegion.height;
const cropX = clamp(rawCropX, 0, 1); const cropX = clamp(rawCropX, 0, 1);
const cropY = clamp(rawCropY, 0, 1); const cropY = clamp(rawCropY, 0, 1);
@@ -433,18 +436,29 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
wallpaper: wallpaper:
typeof editor.wallpaper === "string" typeof editor.wallpaper === "string"
? normalizeWallpaperValue(editor.wallpaper) ? normalizeWallpaperValue(editor.wallpaper)
: DEFAULT_WALLPAPER, : DEFAULT_EDITOR_LAYOUT_SETTINGS.wallpaper,
shadowIntensity: typeof editor.shadowIntensity === "number" ? editor.shadowIntensity : 0, shadowIntensity:
showBlur: typeof editor.showBlur === "boolean" ? editor.showBlur : false, typeof editor.shadowIntensity === "number"
? editor.shadowIntensity
: DEFAULT_EDITOR_APPEARANCE_SETTINGS.shadowIntensity,
showBlur:
typeof editor.showBlur === "boolean"
? editor.showBlur
: DEFAULT_EDITOR_APPEARANCE_SETTINGS.showBlur,
motionBlurAmount: isFiniteNumber(editor.motionBlurAmount) motionBlurAmount: isFiniteNumber(editor.motionBlurAmount)
? clamp(editor.motionBlurAmount, 0, 1) ? clamp(editor.motionBlurAmount, 0, 1)
: typeof (editor as { motionBlurEnabled?: unknown }).motionBlurEnabled === "boolean" : typeof (editor as { motionBlurEnabled?: unknown }).motionBlurEnabled === "boolean"
? (editor as { motionBlurEnabled?: boolean }).motionBlurEnabled ? (editor as { motionBlurEnabled?: boolean }).motionBlurEnabled
? 0.35 ? DEFAULT_ZOOM_MOTION_BLUR
: 0 : DEFAULT_EDITOR_APPEARANCE_SETTINGS.motionBlurAmount
: 0, : DEFAULT_EDITOR_APPEARANCE_SETTINGS.motionBlurAmount,
borderRadius: typeof editor.borderRadius === "number" ? editor.borderRadius : 0, borderRadius:
padding: isFiniteNumber(editor.padding) ? clamp(editor.padding, 0, 100) : 50, typeof editor.borderRadius === "number"
? editor.borderRadius
: DEFAULT_EDITOR_APPEARANCE_SETTINGS.borderRadius,
padding: isFiniteNumber(editor.padding)
? clamp(editor.padding, 0, 100)
: DEFAULT_EDITOR_LAYOUT_SETTINGS.padding,
cropRegion: { cropRegion: {
x: cropX, x: cropX,
y: cropY, y: cropY,
@@ -463,31 +477,31 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
editor.webcamMaskShape === "square" || editor.webcamMaskShape === "square" ||
editor.webcamMaskShape === "rounded" editor.webcamMaskShape === "rounded"
? editor.webcamMaskShape ? editor.webcamMaskShape
: DEFAULT_WEBCAM_MASK_SHAPE, : DEFAULT_WEBCAM_SETTINGS.maskShape,
webcamSizePreset: webcamSizePreset:
typeof editor.webcamSizePreset === "number" && isFiniteNumber(editor.webcamSizePreset) typeof editor.webcamSizePreset === "number" && isFiniteNumber(editor.webcamSizePreset)
? Math.max(10, Math.min(50, editor.webcamSizePreset)) ? Math.max(10, Math.min(50, editor.webcamSizePreset))
: DEFAULT_WEBCAM_SIZE_PRESET, : DEFAULT_WEBCAM_SETTINGS.sizePreset,
webcamPosition: normalizedWebcamPosition, webcamPosition: normalizedWebcamPosition,
exportQuality: exportQuality:
editor.exportQuality === "medium" || editor.exportQuality === "source" editor.exportQuality === "medium" || editor.exportQuality === "source"
? editor.exportQuality ? editor.exportQuality
: "good", : DEFAULT_EXPORT_SETTINGS.quality,
exportFormat: editor.exportFormat === "gif" ? "gif" : "mp4", exportFormat: editor.exportFormat === "gif" ? "gif" : DEFAULT_EXPORT_SETTINGS.format,
gifFrameRate: gifFrameRate:
editor.gifFrameRate === 15 || editor.gifFrameRate === 15 ||
editor.gifFrameRate === 20 || editor.gifFrameRate === 20 ||
editor.gifFrameRate === 25 || editor.gifFrameRate === 25 ||
editor.gifFrameRate === 30 editor.gifFrameRate === 30
? editor.gifFrameRate ? editor.gifFrameRate
: 15, : DEFAULT_GIF_SETTINGS.frameRate,
gifLoop: typeof editor.gifLoop === "boolean" ? editor.gifLoop : true, gifLoop: typeof editor.gifLoop === "boolean" ? editor.gifLoop : DEFAULT_GIF_SETTINGS.loop,
gifSizePreset: gifSizePreset:
editor.gifSizePreset === "medium" || editor.gifSizePreset === "medium" ||
editor.gifSizePreset === "large" || editor.gifSizePreset === "large" ||
editor.gifSizePreset === "original" editor.gifSizePreset === "original"
? editor.gifSizePreset ? editor.gifSizePreset
: "medium", : DEFAULT_GIF_SETTINGS.sizePreset,
}; };
} }
+1
View File
@@ -188,6 +188,7 @@ export interface CursorVisualSettings {
smoothing: number; smoothing: number;
motionBlur: number; motionBlur: number;
clickBounce: number; clickBounce: number;
clipToBounds: boolean;
} }
export const DEFAULT_CURSOR_SIZE = 3.0; export const DEFAULT_CURSOR_SIZE = 3.0;
+17 -19
View File
@@ -1,4 +1,9 @@
import { useCallback, useRef, useState } from "react"; import { useCallback, useRef, useState } from "react";
import {
DEFAULT_EDITOR_APPEARANCE_SETTINGS,
DEFAULT_EDITOR_LAYOUT_SETTINGS,
DEFAULT_WEBCAM_SETTINGS,
} from "@/components/video-editor/editorDefaults";
import type { import type {
AnnotationRegion, AnnotationRegion,
CropRegion, CropRegion,
@@ -10,14 +15,7 @@ import type {
WebcamSizePreset, WebcamSizePreset,
ZoomRegion, ZoomRegion,
} from "@/components/video-editor/types"; } from "@/components/video-editor/types";
import { import { DEFAULT_CROP_REGION } from "@/components/video-editor/types";
DEFAULT_CROP_REGION,
DEFAULT_WEBCAM_LAYOUT_PRESET,
DEFAULT_WEBCAM_MASK_SHAPE,
DEFAULT_WEBCAM_POSITION,
DEFAULT_WEBCAM_SIZE_PRESET,
} from "@/components/video-editor/types";
import { DEFAULT_WALLPAPER } from "@/lib/wallpaper";
import type { AspectRatio } from "@/utils/aspectRatioUtils"; import type { AspectRatio } from "@/utils/aspectRatioUtils";
// Undoable state — selection IDs are intentionally excluded (undoing a // Undoable state — selection IDs are intentionally excluded (undoing a
@@ -47,17 +45,17 @@ export const INITIAL_EDITOR_STATE: EditorState = {
speedRegions: [], speedRegions: [],
annotationRegions: [], annotationRegions: [],
cropRegion: DEFAULT_CROP_REGION, cropRegion: DEFAULT_CROP_REGION,
wallpaper: DEFAULT_WALLPAPER, wallpaper: DEFAULT_EDITOR_LAYOUT_SETTINGS.wallpaper,
shadowIntensity: 0, shadowIntensity: DEFAULT_EDITOR_APPEARANCE_SETTINGS.shadowIntensity,
showBlur: false, showBlur: DEFAULT_EDITOR_APPEARANCE_SETTINGS.showBlur,
motionBlurAmount: 0, motionBlurAmount: DEFAULT_EDITOR_APPEARANCE_SETTINGS.motionBlurAmount,
borderRadius: 0, borderRadius: DEFAULT_EDITOR_APPEARANCE_SETTINGS.borderRadius,
padding: 50, padding: DEFAULT_EDITOR_LAYOUT_SETTINGS.padding,
aspectRatio: "16:9", aspectRatio: DEFAULT_EDITOR_LAYOUT_SETTINGS.aspectRatio,
webcamLayoutPreset: DEFAULT_WEBCAM_LAYOUT_PRESET, webcamLayoutPreset: DEFAULT_WEBCAM_SETTINGS.layoutPreset,
webcamMaskShape: DEFAULT_WEBCAM_MASK_SHAPE, webcamMaskShape: DEFAULT_WEBCAM_SETTINGS.maskShape,
webcamSizePreset: DEFAULT_WEBCAM_SIZE_PRESET, webcamSizePreset: DEFAULT_WEBCAM_SETTINGS.sizePreset,
webcamPosition: DEFAULT_WEBCAM_POSITION, webcamPosition: DEFAULT_WEBCAM_SETTINGS.position,
}; };
type StateUpdate = Partial<EditorState> | ((prev: EditorState) => Partial<EditorState>); type StateUpdate = Partial<EditorState> | ((prev: EditorState) => Partial<EditorState>);
+37 -2
View File
@@ -82,6 +82,7 @@ type RecorderHandle = {
type NativeWindowsRecordingHandle = { type NativeWindowsRecordingHandle = {
recordingId: number; recordingId: number;
finalizing: boolean; finalizing: boolean;
paused: boolean;
webcamRecorder: RecorderHandle | null; webcamRecorder: RecorderHandle | null;
}; };
@@ -148,9 +149,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
const webcamAcquireId = useRef(0); const webcamAcquireId = useRef(0);
const canPauseRecording = const canPauseRecording =
recording && recording &&
!nativeWindowsRecording.current &&
Boolean( Boolean(
(nativeMacRecording.current && !nativeMacRecording.current.finalizing) || (nativeWindowsRecording.current && !nativeWindowsRecording.current.finalizing) ||
(nativeMacRecording.current && !nativeMacRecording.current.finalizing) ||
(screenRecorder.current && screenRecorder.current.recorder.state !== "inactive"), (screenRecorder.current && screenRecorder.current.recorder.state !== "inactive"),
); );
@@ -875,6 +876,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
nativeWindowsRecording.current = { nativeWindowsRecording.current = {
recordingId: result.recordingId, recordingId: result.recordingId,
finalizing: false, finalizing: false,
paused: false,
webcamRecorder: browserWebcamRecorder, webcamRecorder: browserWebcamRecorder,
}; };
webcamRecorder.current = browserWebcamRecorder; webcamRecorder.current = browserWebcamRecorder;
@@ -1403,6 +1405,39 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}; };
const togglePaused = () => { const togglePaused = () => {
const activeNativeWindowsRecording = nativeWindowsRecording.current;
if (activeNativeWindowsRecording && !activeNativeWindowsRecording.finalizing) {
void (async () => {
try {
if (activeNativeWindowsRecording.paused) {
const result = await window.electronAPI.resumeNativeWindowsRecording();
if (!result.success) {
throw new Error(result.error ?? "Failed to resume native Windows recording");
}
activeNativeWindowsRecording.paused = false;
segmentStartedAt.current = Date.now();
setPaused(false);
return;
}
const pausedAtMs = getRecordingDurationMs();
const result = await window.electronAPI.pauseNativeWindowsRecording();
if (!result.success) {
throw new Error(result.error ?? "Failed to pause native Windows recording");
}
activeNativeWindowsRecording.paused = true;
accumulatedDurationMs.current = pausedAtMs;
segmentStartedAt.current = null;
setElapsedSeconds(Math.floor(accumulatedDurationMs.current / 1000));
setPaused(true);
} catch (error) {
console.error("Failed to toggle native Windows pause state:", error);
toast.error(error instanceof Error ? error.message : "Failed to toggle pause state");
}
})();
return;
}
const activeNativeMacRecording = nativeMacRecording.current; const activeNativeMacRecording = nativeMacRecording.current;
if (activeNativeMacRecording && !activeNativeMacRecording.finalizing) { if (activeNativeMacRecording && !activeNativeMacRecording.finalizing) {
void (async () => { void (async () => {
+9 -5
View File
@@ -1,3 +1,7 @@
import {
DEFAULT_EDITOR_LAYOUT_SETTINGS,
DEFAULT_EXPORT_SETTINGS,
} from "@/components/video-editor/editorDefaults";
import type { ExportFormat, ExportQuality } from "@/lib/exporter"; import type { ExportFormat, ExportQuality } from "@/lib/exporter";
import type { AspectRatio } from "@/utils/aspectRatioUtils"; import type { AspectRatio } from "@/utils/aspectRatioUtils";
@@ -27,11 +31,11 @@ export interface UserPreferences {
exportFolder: string | null; exportFolder: string | null;
} }
const DEFAULT_PREFS: UserPreferences = { export const DEFAULT_PREFS: UserPreferences = {
padding: 50, padding: DEFAULT_EDITOR_LAYOUT_SETTINGS.padding,
aspectRatio: "16:9", aspectRatio: DEFAULT_EDITOR_LAYOUT_SETTINGS.aspectRatio,
exportQuality: "good", exportQuality: DEFAULT_EXPORT_SETTINGS.quality,
exportFormat: "mp4", exportFormat: DEFAULT_EXPORT_SETTINGS.format,
exportFolder: null, exportFolder: null,
}; };