fix(webcam): correct sync offset direction

This commit is contained in:
webadderall
2026-04-26 20:10:38 +10:00
parent 99ea9df18d
commit ca05a3fcc0
6 changed files with 64 additions and 14 deletions
@@ -124,7 +124,7 @@ import { clampFocusToStage as clampFocusToStageUtil } from "./videoPlayback/focu
import { layoutVideoContent as layoutVideoContentUtil } from "./videoPlayback/layoutUtils";
import { updateOverlayIndicator } from "./videoPlayback/overlayUtils";
import { createVideoEventHandlers } from "./videoPlayback/videoEventHandlers";
import { getWebcamPreviewTargetTimeSeconds } from "./videoPlayback/webcamSync";
import { getWebcamMediaTargetTimeSeconds } from "./videoPlayback/webcamSync";
import { findDominantRegion } from "./videoPlayback/zoomRegionUtils";
import {
applyZoomTransform,
@@ -1233,7 +1233,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
return;
}
const targetTime = getWebcamPreviewTargetTimeSeconds({
const targetTime = getWebcamMediaTargetTimeSeconds({
currentTime,
webcamDuration: Number.isFinite(webcamVideo.duration) ? webcamVideo.duration : null,
timeOffsetMs: webcam.timeOffsetMs,
@@ -1,25 +1,28 @@
import { describe, expect, it } from "vitest";
import { getWebcamPreviewTargetTimeSeconds } from "./webcamSync";
import {
getWebcamMediaTargetTimeSeconds,
getWebcamPreviewTargetTimeSeconds,
} from "./webcamSync";
describe("getWebcamPreviewTargetTimeSeconds", () => {
it("applies positive webcam offsets", () => {
it("subtracts positive webcam offsets when the webcam started after the main capture", () => {
expect(
getWebcamPreviewTargetTimeSeconds({
currentTime: 10,
webcamDuration: 20,
timeOffsetMs: 250,
}),
).toBe(10.25);
).toBe(9.75);
});
it("clamps negative webcam offsets to zero", () => {
it("adds negative webcam offsets when the webcam started before the main capture", () => {
expect(
getWebcamPreviewTargetTimeSeconds({
currentTime: 0.1,
webcamDuration: 20,
timeOffsetMs: -250,
}),
).toBe(0);
).toBe(0.35);
});
it("falls back to the unshifted time when the offset is invalid", () => {
@@ -37,8 +40,20 @@ describe("getWebcamPreviewTargetTimeSeconds", () => {
getWebcamPreviewTargetTimeSeconds({
currentTime: 8.9,
webcamDuration: 9,
timeOffsetMs: 500,
timeOffsetMs: -500,
}),
).toBe(9);
});
});
describe("getWebcamMediaTargetTimeSeconds", () => {
it("clamps positive offsets at zero when the main timeline is earlier than the webcam start", () => {
expect(
getWebcamMediaTargetTimeSeconds({
currentTime: 0.1,
webcamDuration: 20,
timeOffsetMs: 250,
}),
).toBe(0);
});
});
@@ -1,6 +1,6 @@
import { clampMediaTimeToDuration } from "@/lib/mediaTiming";
export function getWebcamPreviewTargetTimeSeconds({
export function getWebcamMediaTargetTimeSeconds({
currentTime,
webcamDuration,
timeOffsetMs,
@@ -10,6 +10,8 @@ export function getWebcamPreviewTargetTimeSeconds({
timeOffsetMs?: number | null;
}): number {
const safeOffsetMs = Number.isFinite(timeOffsetMs) ? (timeOffsetMs ?? 0) : 0;
const shiftedTime = currentTime + safeOffsetMs / 1000;
const shiftedTime = currentTime - safeOffsetMs / 1000;
return clampMediaTimeToDuration(shiftedTime, webcamDuration);
}
export const getWebcamPreviewTargetTimeSeconds = getWebcamMediaTargetTimeSeconds;
+18
View File
@@ -278,6 +278,24 @@ describe("FrameRenderer webcam export path", () => {
expect(renderer.webcamSeekPromise).toBeNull();
});
it("subtracts stored webcam offsets during export sync", async () => {
const renderer = createRenderer() as unknown as FrameRendererTestAccess & {
config: { webcam?: { timeOffsetMs?: number } };
};
const webcamVideo = new FakeVideoElement({ duration: 10, currentTime: 0.25 });
renderer.webcamVideoElement = webcamVideo;
renderer.config.webcam = {
...(renderer.config.webcam ?? {}),
timeOffsetMs: 250,
};
await renderer.syncWebcamFrame(2);
expect(webcamVideo.currentTime).toBe(1.75);
expect(renderer.lastSyncedWebcamTime).toBe(1.75);
expect(renderer.webcamSeekPromise).toBeNull();
});
it("falls back to animation frame when requestVideoFrameCallback does not fire", async () => {
const renderer = createRenderer() as unknown as FrameRendererTestAccess;
const webcamVideo = new FakeVideoElement({
+11 -2
View File
@@ -50,6 +50,7 @@ import {
getWebcamOverlayPosition,
getWebcamOverlaySizePx,
} from "@/components/video-editor/webcamOverlay";
import { getWebcamMediaTargetTimeSeconds } from "@/components/video-editor/videoPlayback/webcamSync";
import { getAssetPath, getRenderableAssetUrl } from "@/lib/assetPath";
import { extensionHost } from "@/lib/extensions";
import {
@@ -693,8 +694,16 @@ export class FrameRenderer {
}
private async syncWebcamFrame(targetTime: number): Promise<void> {
const webcamTargetTime = getWebcamMediaTargetTimeSeconds({
currentTime: targetTime,
webcamDuration: Number.isFinite(this.webcamVideoElement?.duration)
? this.webcamVideoElement?.duration
: null,
timeOffsetMs: this.config.webcam?.timeOffsetMs,
});
if (this.webcamForwardFrameSource) {
const clampedTime = clampMediaTimeToDuration(targetTime, null);
const clampedTime = clampMediaTimeToDuration(webcamTargetTime, null);
const decodedFrame = await this.webcamForwardFrameSource.getFrameAtTime(clampedTime);
this.closeWebcamDecodedFrame();
this.webcamDecodedFrame = decodedFrame;
@@ -710,7 +719,7 @@ export class FrameRenderer {
}
const clampedTime = clampMediaTimeToDuration(
targetTime,
webcamTargetTime,
Number.isFinite(webcamVideo.duration) ? webcamVideo.duration : null,
);
+8 -2
View File
@@ -56,6 +56,7 @@ import {
getWebcamOverlayPosition,
getWebcamOverlaySizePx,
} from "@/components/video-editor/webcamOverlay";
import { getWebcamMediaTargetTimeSeconds } from "@/components/video-editor/videoPlayback/webcamSync";
import { getAssetPath, getRenderableAssetUrl } from "@/lib/assetPath";
import { extensionHost } from "@/lib/extensions";
import {
@@ -1850,8 +1851,13 @@ export class FrameRenderer {
}
private async syncWebcamFrame(targetTime: number): Promise<void> {
const webcamTimeOffsetSec = (this.config.webcam?.timeOffsetMs ?? 0) / 1000;
const webcamTargetTime = Math.max(0, targetTime + webcamTimeOffsetSec);
const webcamTargetTime = getWebcamMediaTargetTimeSeconds({
currentTime: targetTime,
webcamDuration: Number.isFinite(this.webcamVideoElement?.duration)
? this.webcamVideoElement?.duration
: null,
timeOffsetMs: this.config.webcam?.timeOffsetMs,
});
if (this.webcamForwardFrameSource) {
const clampedTime = clampMediaTimeToDuration(webcamTargetTime, null);