fix export cursor lag

This commit is contained in:
webadderall
2026-04-05 22:04:17 +10:00
parent b72cc10c07
commit a760892490
9 changed files with 99 additions and 14 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
"url": "https://github.com/webadderall/Recordly/issues"
},
"private": true,
"version": "1.1.15",
"version": "1.1.16",
"type": "module",
"scripts": {
"dev": "vite",
+11
View File
@@ -0,0 +1,11 @@
## Hotfix v1.1.16
Fixed cursor lag.
Join GitHub: https://github.com/webadderall/Recordly
## Hotfix v1.1.16
修复了鼠标光标延迟问题。
加入 GitHubhttps://github.com/webadderall/Recordly
+7 -2
View File
@@ -802,7 +802,11 @@ export class FrameRenderer {
}
}
async renderFrame(videoFrame: VideoFrame, timestamp: number): Promise<void> {
async renderFrame(
videoFrame: VideoFrame,
timestamp: number,
cursorTimestamp = timestamp,
): Promise<void> {
if (!this.app || !this.videoContainer || !this.cameraContainer) {
throw new Error("Renderer not initialized");
}
@@ -841,11 +845,12 @@ export class FrameRenderer {
this.updateLayout();
const timeMs = this.currentVideoTime * 1000;
const cursorTimeMs = cursorTimestamp / 1000;
if (this.cursorOverlay) {
this.cursorOverlay.update(
this.config.cursorTelemetry ?? [],
timeMs,
cursorTimeMs,
this.layoutCache.maskRect,
this.config.showCursor ?? true,
false,
+3 -2
View File
@@ -237,14 +237,15 @@ export class GifExporter {
this.config.frameRate,
this.config.trimRegions,
this.config.speedRegions,
async (videoFrame, _exportTimestampUs, sourceTimestampMs) => {
async (videoFrame, _exportTimestampUs, sourceTimestampMs, cursorTimestampMs) => {
if (this.cancelled) {
videoFrame.close();
return;
}
const sourceTimestampUs = sourceTimestampMs * 1000;
await this.renderer!.renderFrame(videoFrame, sourceTimestampUs);
const cursorTimestampUs = cursorTimestampMs * 1000;
await this.renderer!.renderFrame(videoFrame, sourceTimestampUs, cursorTimestampUs);
videoFrame.close();
this.addRenderedGifFrame(frameDelay);
+7 -2
View File
@@ -1927,7 +1927,11 @@ export class FrameRenderer {
}
}
async renderFrame(videoFrame: VideoFrame, timestamp: number): Promise<void> {
async renderFrame(
videoFrame: VideoFrame,
timestamp: number,
cursorTimestamp = timestamp,
): Promise<void> {
if (!this.app || !this.videoContainer || !this.cameraContainer || !this.videoMaskGraphics) {
throw new Error("Renderer not initialized");
}
@@ -1973,11 +1977,12 @@ export class FrameRenderer {
}
const timeMs = this.currentVideoTime * 1000;
const cursorTimeMs = cursorTimestamp / 1000;
if (this.cursorOverlay) {
this.cursorOverlay.update(
this.config.cursorTelemetry ?? [],
timeMs,
cursorTimeMs,
layoutCache.maskRect,
this.config.showCursor ?? true,
false,
+3 -2
View File
@@ -328,7 +328,7 @@ export class ModernVideoExporter {
this.config.frameRate,
this.config.trimRegions,
this.config.speedRegions,
async (videoFrame, _exportTimestampUs, sourceTimestampMs) => {
async (videoFrame, _exportTimestampUs, sourceTimestampMs, cursorTimestampMs) => {
const callbackStartedAt = this.getNowMs();
if (this.cancelled) {
videoFrame.close();
@@ -337,8 +337,9 @@ export class ModernVideoExporter {
const timestamp = frameIndex * frameDuration;
const sourceTimestampUs = sourceTimestampMs * 1000;
const cursorTimestampUs = cursorTimestampMs * 1000;
const renderStartedAt = this.getNowMs();
await this.renderer!.renderFrame(videoFrame, sourceTimestampUs);
await this.renderer!.renderFrame(videoFrame, sourceTimestampUs, cursorTimestampUs);
this.renderFrameTimeMs += this.getNowMs() - renderStartedAt;
videoFrame.close();
+30
View File
@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest';
import { getDecodedFrameStartupOffsetUs } from './streamingDecoder';
describe('getDecodedFrameStartupOffsetUs', () => {
it('ignores positive stream start metadata when the first decoded frame matches it', () => {
expect(
getDecodedFrameStartupOffsetUs(4_978_000, {
streamStartTime: 4.978,
}),
).toBe(0);
});
it('returns only the startup gap beyond the stream start timestamp', () => {
expect(
getDecodedFrameStartupOffsetUs(5_128_000, {
streamStartTime: 4.978,
}),
).toBe(150_000);
});
it('falls back to media start time and then zero when stream metadata is missing', () => {
expect(
getDecodedFrameStartupOffsetUs(250_000, {
mediaStartTime: 0.1,
}),
).toBe(150_000);
expect(getDecodedFrameStartupOffsetUs(250_000, {})).toBe(250_000);
});
});
+34 -3
View File
@@ -22,9 +22,21 @@ export interface DecodedVideoInfo {
type OnFrameCallback = (
frame: VideoFrame,
exportTimestampUs: number,
sourceTimestampMs: number
sourceTimestampMs: number,
cursorTimestampMs: number
) => Promise<void>;
export function getDecodedFrameStartupOffsetUs(
firstDecodedFrameTimestampUs: number,
metadata: Pick<DecodedVideoInfo, 'mediaStartTime' | 'streamStartTime'>
): number {
const streamStartTimeUs = Math.round(
(metadata.streamStartTime ?? metadata.mediaStartTime ?? 0) * 1_000_000
);
return Math.max(0, firstDecodedFrameTimestampUs - streamStartTimeUs);
}
/**
* Decodes video frames via web-demuxer + VideoDecoder in a single forward pass.
* Way faster than seeking an HTMLVideoElement per frame.
@@ -209,6 +221,7 @@ export class StreamingVideoDecoder {
let decodeError: Error | null = null;
let decodeDone = false;
let firstDecodedFrameTimestampUs: number | null = null;
let decodedFrameStartupOffsetUs = 0;
this.decoder = new VideoDecoder({
output: (frame: VideoFrame) => {
@@ -335,7 +348,14 @@ export class StreamingVideoDecoder {
if (sourceTimeSec >= segment.endSec - epsilonSec) return false;
const clone = new VideoFrame(heldFrame, { timestamp: heldFrame.timestamp });
await onFrame(clone, exportFrameIndex * frameDurationUs, sourceTimeSec * 1000);
const sourceTimestampMs = sourceTimeSec * 1000;
const cursorTimestampMs = sourceTimestampMs + decodedFrameStartupOffsetUs / 1000;
await onFrame(
clone,
exportFrameIndex * frameDurationUs,
sourceTimestampMs,
cursorTimestampMs,
);
segmentFrameIndex++;
exportFrameIndex++;
return true;
@@ -347,6 +367,10 @@ export class StreamingVideoDecoder {
if (firstDecodedFrameTimestampUs === null) {
firstDecodedFrameTimestampUs = frame.timestamp;
decodedFrameStartupOffsetUs = getDecodedFrameStartupOffsetUs(
firstDecodedFrameTimestampUs,
this.metadata,
);
}
const normalizedFrameTimeSec = Math.max(
@@ -419,7 +443,14 @@ export class StreamingVideoDecoder {
}
const clone = new VideoFrame(heldFrame, { timestamp: heldFrame.timestamp });
await onFrame(clone, exportFrameIndex * frameDurationUs, sourceTimeSec * 1000);
const sourceTimestampMs = sourceTimeSec * 1000;
const cursorTimestampMs = sourceTimestampMs + decodedFrameStartupOffsetUs / 1000;
await onFrame(
clone,
exportFrameIndex * frameDurationUs,
sourceTimestampMs,
cursorTimestampMs,
);
segmentFrameIndex++;
exportFrameIndex++;
}
+3 -2
View File
@@ -210,7 +210,7 @@ export class VideoExporter {
this.config.frameRate,
this.config.trimRegions,
this.config.speedRegions,
async (videoFrame, _exportTimestampUs, sourceTimestampMs) => {
async (videoFrame, _exportTimestampUs, sourceTimestampMs, cursorTimestampMs) => {
if (this.cancelled) {
videoFrame.close();
return;
@@ -218,7 +218,8 @@ export class VideoExporter {
const timestamp = frameIndex * frameDuration;
const sourceTimestampUs = sourceTimestampMs * 1000;
await this.renderer!.renderFrame(videoFrame, sourceTimestampUs);
const cursorTimestampUs = cursorTimestampMs * 1000;
await this.renderer!.renderFrame(videoFrame, sourceTimestampUs, cursorTimestampUs);
videoFrame.close();
if (useNativeEncoder) {