diff --git a/package.json b/package.json index 32e593cf..2f627e11 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/release-notes-v1.1.16.md b/release-notes-v1.1.16.md new file mode 100644 index 00000000..400b236e --- /dev/null +++ b/release-notes-v1.1.16.md @@ -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 \ No newline at end of file diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 786f9dd8..1b2b0920 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -802,7 +802,11 @@ export class FrameRenderer { } } - async renderFrame(videoFrame: VideoFrame, timestamp: number): Promise { + async renderFrame( + videoFrame: VideoFrame, + timestamp: number, + cursorTimestamp = timestamp, + ): Promise { 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, diff --git a/src/lib/exporter/gifExporter.ts b/src/lib/exporter/gifExporter.ts index aacd2039..d9c84404 100644 --- a/src/lib/exporter/gifExporter.ts +++ b/src/lib/exporter/gifExporter.ts @@ -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); diff --git a/src/lib/exporter/modernFrameRenderer.ts b/src/lib/exporter/modernFrameRenderer.ts index 08569fbb..52aaed7c 100644 --- a/src/lib/exporter/modernFrameRenderer.ts +++ b/src/lib/exporter/modernFrameRenderer.ts @@ -1927,7 +1927,11 @@ export class FrameRenderer { } } - async renderFrame(videoFrame: VideoFrame, timestamp: number): Promise { + async renderFrame( + videoFrame: VideoFrame, + timestamp: number, + cursorTimestamp = timestamp, + ): Promise { 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, diff --git a/src/lib/exporter/modernVideoExporter.ts b/src/lib/exporter/modernVideoExporter.ts index 049af21b..953c5eda 100644 --- a/src/lib/exporter/modernVideoExporter.ts +++ b/src/lib/exporter/modernVideoExporter.ts @@ -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(); diff --git a/src/lib/exporter/streamingDecoder.test.ts b/src/lib/exporter/streamingDecoder.test.ts new file mode 100644 index 00000000..e2fcd09a --- /dev/null +++ b/src/lib/exporter/streamingDecoder.test.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/src/lib/exporter/streamingDecoder.ts b/src/lib/exporter/streamingDecoder.ts index 471ff026..a7a3e177 100644 --- a/src/lib/exporter/streamingDecoder.ts +++ b/src/lib/exporter/streamingDecoder.ts @@ -22,9 +22,21 @@ export interface DecodedVideoInfo { type OnFrameCallback = ( frame: VideoFrame, exportTimestampUs: number, - sourceTimestampMs: number + sourceTimestampMs: number, + cursorTimestampMs: number ) => Promise; +export function getDecodedFrameStartupOffsetUs( + firstDecodedFrameTimestampUs: number, + metadata: Pick +): 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++; } diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index 8a939a52..834fb0bd 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -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) {