Merge fix/wgc-display-webcam-sync into main

This commit is contained in:
webadderall committed 2026-03-26 22:09:59 +11:00
commit f1df2911cb
9 files changed
+69 -18

No files matched your search

+2 -1
View File
@@ -183,10 +183,11 @@ interface Window {
setCurrentRecordingSession: (session: {
videoPath: string;
webcamPath?: string | null;
timeOffsetMs?: number;
}) => Promise<{ success: boolean }>;
getCurrentRecordingSession: () => Promise<{
success: boolean;
session?: { videoPath: string; webcamPath?: string | null };
session?: { videoPath: string; webcamPath?: string | null; timeOffsetMs?: number };
}>;
getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>;
clearCurrentVideoPath: () => Promise<{ success: boolean }>;
+25 -4
View File
@@ -81,12 +81,14 @@ type WindowBounds = {
type RecordingSessionData = {
videoPath: string
webcamPath?: string | null
timeOffsetMs?: number
}
type RecordingSessionManifest = {
version: 1
version: 1 | 2
videoFileName: string
webcamFileName?: string | null
timeOffsetMs?: number
}
type ProjectLibraryEntry = {
@@ -384,6 +386,7 @@ async function loadProjectFromPath(projectPath: string) {
currentRecordingSession = {
videoPath: mediaSources.videoPath,
webcamPath: mediaSources.webcamPath,
timeOffsetMs: mediaSources.timeOffsetMs,
}
await rememberRecentProject(normalizedPath)
@@ -420,6 +423,7 @@ async function resolveProjectMediaSources(project: unknown): Promise<
success: true
videoPath: string
webcamPath: string | null
timeOffsetMs: number
}
| {
success: false
@@ -453,6 +457,9 @@ async function resolveProjectMediaSources(project: unknown): Promise<
typeof (project as { editor?: { webcam?: { sourcePath?: unknown } } }).editor?.webcam?.sourcePath === 'string'
? ((project as { editor?: { webcam?: { sourcePath?: string } } }).editor?.webcam?.sourcePath ?? null)
: null
const timeOffsetMs = normalizeRecordingTimeOffsetMs(
(project as { editor?: { webcam?: { timeOffsetMs?: unknown } } }).editor?.webcam?.timeOffsetMs,
)
const normalizedWebcamPath = normalizeVideoSourcePath(rawWebcamPath)
if (!normalizedWebcamPath) {
@@ -460,6 +467,7 @@ async function resolveProjectMediaSources(project: unknown): Promise<
success: true,
videoPath: normalizedVideoPath,
webcamPath: null,
timeOffsetMs,
}
}
@@ -469,16 +477,24 @@ async function resolveProjectMediaSources(project: unknown): Promise<
success: true,
videoPath: normalizedVideoPath,
webcamPath: normalizedWebcamPath,
timeOffsetMs,
}
} catch {
return {
success: true,
videoPath: normalizedVideoPath,
webcamPath: null,
timeOffsetMs,
}
}
}
function normalizeRecordingTimeOffsetMs(value: unknown): number {
return typeof value === 'number' && Number.isFinite(value)
? Math.round(value)
: 0
}
function getRecordingSessionManifestPath(videoPath: string) {
const extension = path.extname(videoPath)
const baseName = path.basename(videoPath, extension)
@@ -500,9 +516,10 @@ async function persistRecordingSessionManifest(session: RecordingSessionData): P
}
const manifest: RecordingSessionManifest = {
version: 1,
version: 2,
videoFileName: path.basename(normalizedVideoPath),
webcamFileName: path.basename(normalizedWebcamPath),
timeOffsetMs: normalizeRecordingTimeOffsetMs(session.timeOffsetMs),
}
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8')
@@ -519,7 +536,7 @@ async function resolveRecordingSessionManifest(videoPath?: string | null): Promi
try {
const content = await fs.readFile(manifestPath, 'utf-8')
const parsed = JSON.parse(content) as Partial<RecordingSessionManifest>
if (parsed.version !== 1) {
if (parsed.version !== 1 && parsed.version !== 2) {
return null
}
@@ -531,6 +548,7 @@ async function resolveRecordingSessionManifest(videoPath?: string | null): Promi
return {
videoPath: normalizedVideoPath,
webcamPath: null,
timeOffsetMs: 0,
}
}
@@ -540,6 +558,7 @@ async function resolveRecordingSessionManifest(videoPath?: string | null): Promi
return {
videoPath: normalizedVideoPath,
webcamPath,
timeOffsetMs: normalizeRecordingTimeOffsetMs(parsed.timeOffsetMs),
}
} catch {
return null
@@ -594,6 +613,7 @@ async function resolveRecordingSession(videoPath?: string | null): Promise<Recor
return {
videoPath: normalizedVideoPath,
webcamPath: linkedWebcamPath,
timeOffsetMs: 0,
}
}
@@ -4289,12 +4309,13 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
return { success: true, webcamPath: resolvedSession.webcamPath ?? null }
})
ipcMain.handle('set-current-recording-session', async (_, session: { videoPath: string; webcamPath?: string | null }) => {
ipcMain.handle('set-current-recording-session', async (_, session: { videoPath: string; webcamPath?: string | null; timeOffsetMs?: number }) => {
const normalizedVideoPath = normalizeVideoSourcePath(session.videoPath) ?? session.videoPath
currentVideoPath = normalizedVideoPath
currentRecordingSession = {
videoPath: normalizedVideoPath,
webcamPath: normalizeVideoSourcePath(session.webcamPath ?? null),
timeOffsetMs: normalizeRecordingTimeOffsetMs(session.timeOffsetMs),
}
currentProjectPath = null
await persistRecordingSessionManifest(currentRecordingSession)
@@ -36,11 +36,7 @@ HMONITOR findMonitorByDisplayId(int64_t displayId) {
}
}
if (!monitors.empty()) {
return monitors[0].handle;
}
return MonitorFromPoint({0, 0}, MONITOR_DEFAULTTOPRIMARY);
return nullptr;
}
MonitorInfo getMonitorInfo(HMONITOR monitor) {
+1 -1
View File
@@ -194,7 +194,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
setCurrentVideoPath: (path: string) => {
return ipcRenderer.invoke("set-current-video-path", path);
},
setCurrentRecordingSession: (session: { videoPath: string; webcamPath?: string | null }) => {
setCurrentRecordingSession: (session: { videoPath: string; webcamPath?: string | null; timeOffsetMs?: number }) => {
return ipcRenderer.invoke("set-current-recording-session", session);
},
getCurrentRecordingSession: () => {
+8 -3
View File
@@ -1175,7 +1175,7 @@ export default function VideoEditor() {
}, [currentPersistedEditorState, currentSourcePath]);
const syncRecordingSessionWebcam = useCallback(
async (webcamPath: string | null) => {
async (webcamPath: string | null, timeOffsetMs = 0) => {
if (!currentSourcePath || !window.electronAPI.setCurrentRecordingSession) {
return;
}
@@ -1183,6 +1183,7 @@ export default function VideoEditor() {
await window.electronAPI.setCurrentRecordingSession({
videoPath: currentSourcePath,
webcamPath,
timeOffsetMs,
});
},
[currentSourcePath],
@@ -1213,9 +1214,10 @@ export default function VideoEditor() {
...prev,
enabled: true,
sourcePath: result.path ?? null,
timeOffsetMs: 0,
}));
await syncRecordingSessionWebcam(result.path);
await syncRecordingSessionWebcam(result.path, 0);
toast.success(t("settings.effects.webcamFootageAdded"));
}, [syncRecordingSessionWebcam, t]);
@@ -1224,9 +1226,10 @@ export default function VideoEditor() {
...prev,
enabled: false,
sourcePath: null,
timeOffsetMs: 0,
}));
await syncRecordingSessionWebcam(null);
await syncRecordingSessionWebcam(null, 0);
toast.success(t("settings.effects.webcamFootageRemoved"));
}, [syncRecordingSessionWebcam, t]);
@@ -1295,6 +1298,7 @@ export default function VideoEditor() {
...prev,
enabled: Boolean(sessionResult.session?.webcamPath),
sourcePath: sessionResult.session?.webcamPath ?? null,
timeOffsetMs: sessionResult.session?.timeOffsetMs ?? 0,
}));
return;
}
@@ -1310,6 +1314,7 @@ export default function VideoEditor() {
...prev,
enabled: false,
sourcePath: null,
timeOffsetMs: 0,
}));
} else {
setError("No video to load. Please record or select a video.");
@@ -945,7 +945,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
}
const targetTime = clampMediaTimeToDuration(
currentTime,
Math.max(0, currentTime - (webcam.timeOffsetMs ?? 0) / 1000),
Number.isFinite(webcamVideo.duration) ? webcamVideo.duration : null,
);
@@ -35,6 +35,7 @@ import {
DEFAULT_WEBCAM_REACT_TO_ZOOM,
DEFAULT_WEBCAM_SHADOW,
DEFAULT_WEBCAM_SIZE,
DEFAULT_WEBCAM_TIME_OFFSET_MS,
DEFAULT_FIGURE_DATA,
DEFAULT_PLAYBACK_SPEED,
DEFAULT_ZOOM_DEPTH,
@@ -585,6 +586,9 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
enabled:
typeof webcam.enabled === "boolean" ? webcam.enabled : DEFAULT_WEBCAM_OVERLAY.enabled,
sourcePath: webcamSourcePath,
timeOffsetMs: isFiniteNumber(webcam.timeOffsetMs)
? Math.round(clamp(webcam.timeOffsetMs, -30_000, 30_000))
: DEFAULT_WEBCAM_TIME_OFFSET_MS,
mirror: typeof webcam.mirror === "boolean" ? webcam.mirror : DEFAULT_WEBCAM_OVERLAY.mirror,
positionPreset:
webcam.positionPreset === "top-left" ||
+3
View File
@@ -69,6 +69,7 @@ export type WebcamPositionPreset =
export interface WebcamOverlaySettings {
enabled: boolean;
sourcePath: string | null;
timeOffsetMs: number;
mirror: boolean;
corner: WebcamCorner;
positionPreset: WebcamPositionPreset;
@@ -104,10 +105,12 @@ export const DEFAULT_WEBCAM_MARGIN = 24;
export const DEFAULT_WEBCAM_POSITION_PRESET: WebcamPositionPreset = "bottom-right";
export const DEFAULT_WEBCAM_POSITION_X = 1;
export const DEFAULT_WEBCAM_POSITION_Y = 1;
export const DEFAULT_WEBCAM_TIME_OFFSET_MS = 0;
export const DEFAULT_WEBCAM_OVERLAY: WebcamOverlaySettings = {
enabled: false,
sourcePath: null,
timeOffsetMs: DEFAULT_WEBCAM_TIME_OFFSET_MS,
mirror: true,
corner: "bottom-right",
positionPreset: DEFAULT_WEBCAM_POSITION_PRESET,
+24 -3
View File
@@ -79,6 +79,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
const chunks = useRef<Blob[]>([]);
const webcamChunks = useRef<Blob[]>([]);
const startTime = useRef<number>(0);
const webcamStartTime = useRef<number | null>(null);
const webcamTimeOffsetMs = useRef(0);
const recordingSessionTimestamp = useRef<number | null>(null);
const nativeScreenRecording = useRef(false);
const nativeWindowsRecording = useRef(false);
@@ -265,6 +267,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
await window.electronAPI.setCurrentRecordingSession({
videoPath,
webcamPath,
timeOffsetMs: webcamTimeOffsetMs.current,
});
} else {
await window.electronAPI.setCurrentVideoPath(videoPath);
@@ -293,6 +296,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
const startWebcamRecorder = useCallback(async () => {
if (!webcamEnabled) {
pendingWebcamPathPromise.current = Promise.resolve(null);
webcamStartTime.current = null;
webcamTimeOffsetMs.current = 0;
return;
}
@@ -345,7 +350,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
return;
}
const duration = getRecordingDurationMs(Date.now());
const duration = Math.max(0, getRecordingDurationMs(Date.now()) - webcamTimeOffsetMs.current);
const webcamBlob = new Blob(webcamChunks.current, { type: mimeType });
webcamChunks.current = [];
const fixedBlob = await fixWebmDuration(webcamBlob, duration);
@@ -358,6 +363,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
} finally {
webcamStopResolver.current = null;
webcamRecorder.current = null;
webcamStartTime.current = null;
if (webcamStream.current) {
webcamStream.current.getTracks().forEach((track) => track.stop());
webcamStream.current = null;
@@ -365,12 +371,15 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}
};
webcamStartTime.current = Date.now();
recorder.start(RECORDER_TIMESLICE_MS);
} catch (error) {
console.warn("Failed to start webcam recording; continuing without webcam layer:", error);
pendingWebcamPathPromise.current = Promise.resolve(null);
webcamStopPromise.current = Promise.resolve(null);
webcamRecorder.current = null;
webcamStartTime.current = null;
webcamTimeOffsetMs.current = 0;
if (webcamStream.current) {
webcamStream.current.getTracks().forEach((track) => track.stop());
webcamStream.current = null;
@@ -524,6 +533,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
recordingSessionTimestamp.current = Date.now();
resetRecordingClock(recordingSessionTimestamp.current);
webcamStartTime.current = null;
webcamTimeOffsetMs.current = 0;
await startWebcamRecorder();
const platform = await window.electronAPI.getPlatform();
@@ -596,9 +607,13 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}
if (nativeResult.success) {
const mainStartedAt = Date.now();
nativeScreenRecording.current = true;
nativeWindowsRecording.current = useNativeWindowsCapture;
resetRecordingClock(Date.now());
webcamTimeOffsetMs.current = webcamStartTime.current === null
? 0
: webcamStartTime.current - mainStartedAt;
resetRecordingClock(mainStartedAt);
setRecording(true);
window.electronAPI?.setRecordingState(true);
@@ -824,8 +839,12 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
recorder.onerror = () => {
setRecording(false);
};
const mainStartedAt = Date.now();
resetRecordingClock(mainStartedAt);
webcamTimeOffsetMs.current = webcamStartTime.current === null
? 0
: webcamStartTime.current - mainStartedAt;
recorder.start(RECORDER_TIMESLICE_MS);
resetRecordingClock(Date.now());
setRecording(true);
window.electronAPI?.setRecordingState(true);
} catch (error) {
@@ -909,6 +928,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
webcamRecorder.current = null;
webcamStream.current?.getTracks().forEach((t) => t.stop());
webcamStream.current = null;
webcamStartTime.current = null;
webcamTimeOffsetMs.current = 0;
pendingWebcamPathPromise.current = null;
if (nativeScreenRecording.current) {