mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 06:46:09 +00:00
fix export cursor lag
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
## Hotfix v1.1.16
|
||||
|
||||
Fixed cursor lag.
|
||||
|
||||
Join GitHub: https://github.com/webadderall/Recordly
|
||||
|
||||
## Hotfix v1.1.16
|
||||
|
||||
修复了鼠标光标延迟问题。
|
||||
|
||||
加入 GitHub:https://github.com/webadderall/Recordly
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user