Merge remote-tracking branch 'origin/main' into feat/zoom-hold-preview
# Conflicts: # src/components/video-editor/VideoPlayback.tsx
This commit is contained in:
Vendored
+8
@@ -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>;
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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%",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>);
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user