fix: align native cursor preview and export
This commit is contained in:
@@ -651,7 +651,7 @@ export function registerIpcHandlers(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("set-recording-state", async (_, recording: boolean) => {
|
ipcMain.handle("set-recording-state", async (_, recording: boolean, recordingId?: number) => {
|
||||||
if (recording) {
|
if (recording) {
|
||||||
if (cursorRecordingSession) {
|
if (cursorRecordingSession) {
|
||||||
pendingCursorRecordingData = await cursorRecordingSession.stop();
|
pendingCursorRecordingData = await cursorRecordingSession.stop();
|
||||||
@@ -665,6 +665,8 @@ export function registerIpcHandlers(
|
|||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
sampleIntervalMs: CURSOR_SAMPLE_INTERVAL_MS,
|
sampleIntervalMs: CURSOR_SAMPLE_INTERVAL_MS,
|
||||||
sourceId: getSelectedSourceId(),
|
sourceId: getSelectedSourceId(),
|
||||||
|
startTimeMs:
|
||||||
|
typeof recordingId === "number" && Number.isFinite(recordingId) ? recordingId : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -824,6 +826,7 @@ export function registerIpcHandlers(
|
|||||||
return { success: false, canceled: true };
|
return { success: false, canceled: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
approveFilePath(result.filePaths[0]);
|
||||||
currentProjectPath = null;
|
currentProjectPath = null;
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -863,6 +866,32 @@ export function registerIpcHandlers(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("read-binary-file", async (_, filePath: string) => {
|
||||||
|
try {
|
||||||
|
const normalizedPath = await approveReadableVideoPath(filePath);
|
||||||
|
if (!normalizedPath) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "File path is not approved or is not a supported video file",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fs.readFile(normalizedPath);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength),
|
||||||
|
path: normalizedPath,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to read binary file:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Failed to read binary file",
|
||||||
|
error: String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"save-project-file",
|
"save-project-file",
|
||||||
async (_, projectData: unknown, suggestedName?: string, existingProjectPath?: string) => {
|
async (_, projectData: unknown, suggestedName?: string, existingProjectPath?: string) => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface CreateCursorRecordingSessionOptions {
|
|||||||
platform: NodeJS.Platform;
|
platform: NodeJS.Platform;
|
||||||
sampleIntervalMs: number;
|
sampleIntervalMs: number;
|
||||||
sourceId?: string | null;
|
sourceId?: string | null;
|
||||||
|
startTimeMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCursorRecordingSession(
|
export function createCursorRecordingSession(
|
||||||
@@ -20,6 +21,7 @@ export function createCursorRecordingSession(
|
|||||||
maxSamples: options.maxSamples,
|
maxSamples: options.maxSamples,
|
||||||
sampleIntervalMs: options.sampleIntervalMs,
|
sampleIntervalMs: options.sampleIntervalMs,
|
||||||
sourceId: options.sourceId,
|
sourceId: options.sourceId,
|
||||||
|
startTimeMs: options.startTimeMs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,5 +29,6 @@ export function createCursorRecordingSession(
|
|||||||
getDisplayBounds: options.getDisplayBounds,
|
getDisplayBounds: options.getDisplayBounds,
|
||||||
maxSamples: options.maxSamples,
|
maxSamples: options.maxSamples,
|
||||||
sampleIntervalMs: options.sampleIntervalMs,
|
sampleIntervalMs: options.sampleIntervalMs,
|
||||||
|
startTimeMs: options.startTimeMs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface TelemetryRecordingSessionOptions {
|
|||||||
getDisplayBounds: () => Rectangle | null;
|
getDisplayBounds: () => Rectangle | null;
|
||||||
maxSamples: number;
|
maxSamples: number;
|
||||||
sampleIntervalMs: number;
|
sampleIntervalMs: number;
|
||||||
|
startTimeMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clamp(value: number, min: number, max: number) {
|
function clamp(value: number, min: number, max: number) {
|
||||||
@@ -21,7 +22,7 @@ export class TelemetryRecordingSession implements CursorRecordingSession {
|
|||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
this.samples = [];
|
this.samples = [];
|
||||||
this.startTimeMs = Date.now();
|
this.startTimeMs = this.options.startTimeMs ?? Date.now();
|
||||||
this.captureSample();
|
this.captureSample();
|
||||||
this.interval = setInterval(() => {
|
this.interval = setInterval(() => {
|
||||||
this.captureSample();
|
this.captureSample();
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export class WindowsNativeRecordingSession implements CursorRecordingSession {
|
|||||||
this.assets.clear();
|
this.assets.clear();
|
||||||
this.samples = [];
|
this.samples = [];
|
||||||
this.lineBuffer = "";
|
this.lineBuffer = "";
|
||||||
this.startTimeMs = Date.now();
|
this.startTimeMs = this.options.startTimeMs ?? Date.now();
|
||||||
this.sampleCount = 0;
|
this.sampleCount = 0;
|
||||||
this.outOfBoundsSampleCount = 0;
|
this.outOfBoundsSampleCount = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -49,4 +49,5 @@ export interface WindowsNativeRecordingSessionOptions {
|
|||||||
maxSamples: number;
|
maxSamples: number;
|
||||||
sampleIntervalMs: number;
|
sampleIntervalMs: number;
|
||||||
sourceId?: string | null;
|
sourceId?: string | null;
|
||||||
|
startTimeMs?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,10 +214,6 @@ try {
|
|||||||
await editorWindow.waitForLoadState("domcontentloaded");
|
await editorWindow.waitForLoadState("domcontentloaded");
|
||||||
await editorWindow.waitForSelector("video", { state: "attached", timeout: 30_000 });
|
await editorWindow.waitForSelector("video", { state: "attached", timeout: 30_000 });
|
||||||
await editorWindow.waitForSelector("canvas", { state: "attached", timeout: 30_000 });
|
await editorWindow.waitForSelector("canvas", { state: "attached", timeout: 30_000 });
|
||||||
await editorWindow.waitForSelector('img[aria-hidden="true"]', {
|
|
||||||
state: "attached",
|
|
||||||
timeout: 30_000,
|
|
||||||
});
|
|
||||||
|
|
||||||
await editorWindow.setViewportSize({ width: 1280, height: 800 });
|
await editorWindow.setViewportSize({ width: 1280, height: 800 });
|
||||||
await editorWindow.evaluate(async () => {
|
await editorWindow.evaluate(async () => {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
} from "@/lib/compositeLayout";
|
} from "@/lib/compositeLayout";
|
||||||
import {
|
import {
|
||||||
hasNativeCursorRecordingData,
|
hasNativeCursorRecordingData,
|
||||||
projectNativeCursorToStage,
|
projectNativeCursorToLocal,
|
||||||
resolveInterpolatedNativeCursorFrame,
|
resolveInterpolatedNativeCursorFrame,
|
||||||
resolveNativeCursorRenderAsset,
|
resolveNativeCursorRenderAsset,
|
||||||
} from "@/lib/cursor/nativeCursor";
|
} from "@/lib/cursor/nativeCursor";
|
||||||
@@ -841,6 +841,12 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
|||||||
cursorContainerRef.current = cursorContainer;
|
cursorContainerRef.current = cursorContainer;
|
||||||
cameraContainer.addChild(cursorContainer);
|
cameraContainer.addChild(cursorContainer);
|
||||||
|
|
||||||
|
const nativeCursorSprite = new Sprite(Texture.EMPTY);
|
||||||
|
nativeCursorSprite.visible = false;
|
||||||
|
nativeCursorSprite.eventMode = "none";
|
||||||
|
nativeCursorSpriteRef.current = nativeCursorSprite;
|
||||||
|
cursorContainer.addChild(nativeCursorSprite);
|
||||||
|
|
||||||
// Cursor overlay - rendered above the masked video
|
// Cursor overlay - rendered above the masked video
|
||||||
if (cursorOverlayEnabled) {
|
if (cursorOverlayEnabled) {
|
||||||
const cursorOverlay = new PixiCursorOverlay({
|
const cursorOverlay = new PixiCursorOverlay({
|
||||||
@@ -863,6 +869,8 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
|||||||
cursorOverlayRef.current.destroy();
|
cursorOverlayRef.current.destroy();
|
||||||
cursorOverlayRef.current = null;
|
cursorOverlayRef.current = null;
|
||||||
}
|
}
|
||||||
|
nativeCursorSpriteRef.current = null;
|
||||||
|
nativeCursorTextureIdRef.current = null;
|
||||||
if (app && app.renderer) {
|
if (app && app.renderer) {
|
||||||
app.destroy(true, {
|
app.destroy(true, {
|
||||||
children: true,
|
children: true,
|
||||||
@@ -1296,25 +1304,18 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update native cursor image position at ticker rate (60fps)
|
// Update native cursor sprite in the same PIXI coordinate space as the video.
|
||||||
const nativeCursorImg = nativeCursorImgRef.current;
|
const nativeCursorSprite = nativeCursorSpriteRef.current;
|
||||||
if (nativeCursorImg) {
|
if (nativeCursorSprite) {
|
||||||
const cameraContainerRc = cameraContainerRef.current;
|
|
||||||
const videoContainerRc = videoContainerRef.current;
|
const videoContainerRc = videoContainerRef.current;
|
||||||
if (
|
if (hasNativeCursorRecordingRef.current && showCursorRef.current && videoContainerRc) {
|
||||||
hasNativeCursorRecordingRef.current &&
|
|
||||||
showCursorRef.current &&
|
|
||||||
cameraContainerRc &&
|
|
||||||
videoContainerRc
|
|
||||||
) {
|
|
||||||
const timeMs = currentTimeRef.current; // already in ms
|
const timeMs = currentTimeRef.current; // already in ms
|
||||||
const frame = resolveInterpolatedNativeCursorFrame(
|
const frame = resolveInterpolatedNativeCursorFrame(
|
||||||
cursorRecordingDataRef.current,
|
cursorRecordingDataRef.current,
|
||||||
timeMs,
|
timeMs,
|
||||||
);
|
);
|
||||||
if (frame) {
|
if (frame) {
|
||||||
const projectedPoint = projectNativeCursorToStage({
|
const projectedPoint = projectNativeCursorToLocal({
|
||||||
cameraContainer: cameraContainerRc,
|
|
||||||
cropRegion: cropRegionRef.current ?? { x: 0, y: 0, width: 1, height: 1 },
|
cropRegion: cropRegionRef.current ?? { x: 0, y: 0, width: 1, height: 1 },
|
||||||
maskRect: baseMaskRef.current,
|
maskRect: baseMaskRef.current,
|
||||||
videoContainerPosition: {
|
videoContainerPosition: {
|
||||||
@@ -1330,23 +1331,25 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
|||||||
frame.sample,
|
frame.sample,
|
||||||
);
|
);
|
||||||
const scale = Math.max(0, cursorSizeRef.current);
|
const scale = Math.max(0, cursorSizeRef.current);
|
||||||
if (nativeCursorImg.dataset.cursorId !== renderAsset.id) {
|
if (nativeCursorTextureIdRef.current !== renderAsset.id) {
|
||||||
nativeCursorImg.src = renderAsset.imageDataUrl;
|
nativeCursorSprite.texture = Texture.from(renderAsset.imageDataUrl);
|
||||||
nativeCursorImg.dataset.cursorId = renderAsset.id;
|
nativeCursorTextureIdRef.current = renderAsset.id;
|
||||||
}
|
}
|
||||||
nativeCursorImg.style.left = `${projectedPoint.x - renderAsset.hotspotX * scale}px`;
|
nativeCursorSprite.position.set(
|
||||||
nativeCursorImg.style.top = `${projectedPoint.y - renderAsset.hotspotY * scale}px`;
|
projectedPoint.x - renderAsset.hotspotX * scale,
|
||||||
nativeCursorImg.style.width = `${renderAsset.width * scale}px`;
|
projectedPoint.y - renderAsset.hotspotY * scale,
|
||||||
nativeCursorImg.style.height = `${renderAsset.height * scale}px`;
|
);
|
||||||
nativeCursorImg.style.display = "block";
|
nativeCursorSprite.width = renderAsset.width * scale;
|
||||||
|
nativeCursorSprite.height = renderAsset.height * scale;
|
||||||
|
nativeCursorSprite.visible = true;
|
||||||
} else {
|
} else {
|
||||||
nativeCursorImg.style.display = "none";
|
nativeCursorSprite.visible = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
nativeCursorImg.style.display = "none";
|
nativeCursorSprite.visible = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
nativeCursorImg.style.display = "none";
|
nativeCursorSprite.visible = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1638,19 +1641,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
|||||||
className="absolute rounded-md border border-[#34B27B]/80 bg-[#34B27B]/20 shadow-[0_0_0_1px_rgba(52,178,123,0.35)]"
|
className="absolute rounded-md border border-[#34B27B]/80 bg-[#34B27B]/20 shadow-[0_0_0_1px_rgba(52,178,123,0.35)]"
|
||||||
style={{ display: "none", pointerEvents: "none" }}
|
style={{ display: "none", pointerEvents: "none" }}
|
||||||
/>
|
/>
|
||||||
{hasNativeCursorRecording ? (
|
|
||||||
<img
|
|
||||||
ref={nativeCursorImgRef}
|
|
||||||
alt=""
|
|
||||||
aria-hidden="true"
|
|
||||||
className="absolute select-none"
|
|
||||||
style={{
|
|
||||||
display: "none",
|
|
||||||
pointerEvents: "none",
|
|
||||||
userSelect: "none",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const filteredAnnotations = (annotationRegions || []).filter((annotation) => {
|
const filteredAnnotations = (annotationRegions || []).filter((annotation) => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -719,6 +719,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recordingId.current = Date.now();
|
||||||
|
const activeRecordingId = recordingId.current;
|
||||||
screenRecorder.current = createRecorderHandle(stream.current, {
|
screenRecorder.current = createRecorderHandle(stream.current, {
|
||||||
mimeType,
|
mimeType,
|
||||||
videoBitsPerSecond,
|
videoBitsPerSecond,
|
||||||
@@ -741,9 +743,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
recordingId.current = Date.now();
|
|
||||||
accumulatedDurationMs.current = 0;
|
accumulatedDurationMs.current = 0;
|
||||||
segmentStartedAt.current = Date.now();
|
segmentStartedAt.current = activeRecordingId;
|
||||||
allowAutoFinalize.current = true;
|
allowAutoFinalize.current = true;
|
||||||
setRecording(true);
|
setRecording(true);
|
||||||
setPaused(false);
|
setPaused(false);
|
||||||
@@ -752,7 +753,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
|||||||
|
|
||||||
const activeScreenRecorder = screenRecorder.current;
|
const activeScreenRecorder = screenRecorder.current;
|
||||||
const activeWebcamRecorder = webcamRecorder.current;
|
const activeWebcamRecorder = webcamRecorder.current;
|
||||||
const activeRecordingId = recordingId.current;
|
|
||||||
if (activeScreenRecorder) {
|
if (activeScreenRecorder) {
|
||||||
activeScreenRecorder.recorder.addEventListener(
|
activeScreenRecorder.recorder.addEventListener(
|
||||||
"stop",
|
"stop",
|
||||||
|
|||||||
@@ -27,13 +27,16 @@ export interface ActiveNativeCursorFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectNativeCursorOptions {
|
interface ProjectNativeCursorOptions {
|
||||||
cameraContainer: Container;
|
|
||||||
cropRegion: CropRegion;
|
cropRegion: CropRegion;
|
||||||
maskRect: { x: number; y: number; width: number; height: number };
|
maskRect: { x: number; y: number; width: number; height: number };
|
||||||
videoContainerPosition: { x: number; y: number };
|
videoContainerPosition: { x: number; y: number };
|
||||||
sample: CursorRecordingSample;
|
sample: CursorRecordingSample;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProjectNativeCursorToStageOptions extends ProjectNativeCursorOptions {
|
||||||
|
cameraContainer: Container;
|
||||||
|
}
|
||||||
|
|
||||||
function clamp(value: number, min: number, max: number) {
|
function clamp(value: number, min: number, max: number) {
|
||||||
return Math.min(max, Math.max(min, value));
|
return Math.min(max, Math.max(min, value));
|
||||||
}
|
}
|
||||||
@@ -264,8 +267,7 @@ export function resolveInterpolatedNativeCursorFrame(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function projectNativeCursorToStage({
|
export function projectNativeCursorToLocal({
|
||||||
cameraContainer,
|
|
||||||
cropRegion,
|
cropRegion,
|
||||||
maskRect,
|
maskRect,
|
||||||
videoContainerPosition,
|
videoContainerPosition,
|
||||||
@@ -276,11 +278,20 @@ export function projectNativeCursorToStage({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const localPoint = new Point(
|
return new Point(
|
||||||
videoContainerPosition.x + maskRect.x + croppedPosition.cx * maskRect.width,
|
videoContainerPosition.x + maskRect.x + croppedPosition.cx * maskRect.width,
|
||||||
videoContainerPosition.y + maskRect.y + croppedPosition.cy * maskRect.height,
|
videoContainerPosition.y + maskRect.y + croppedPosition.cy * maskRect.height,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function projectNativeCursorToStage({
|
||||||
|
cameraContainer,
|
||||||
|
...options
|
||||||
|
}: ProjectNativeCursorToStageOptions) {
|
||||||
|
const localPoint = projectNativeCursorToLocal(options);
|
||||||
|
if (!localPoint) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return cameraContainer.toGlobal(localPoint);
|
return cameraContainer.toGlobal(localPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user