mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 15:25:44 +00:00
Merge pull request #209 from lout33/fix/export-stream-offset-timeline
fix(export): preserve stream-start offsets during decode
This commit is contained in:
@@ -5,7 +5,6 @@ import {
|
||||
Download,
|
||||
FolderOpen,
|
||||
MousePointer2,
|
||||
Puzzle,
|
||||
Redo2,
|
||||
Save,
|
||||
Sparkles,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { WebDemuxer } from "web-demuxer";
|
||||
import { getEffectiveVideoStreamDurationSeconds } from "@/lib/mediaTiming";
|
||||
import { getDecodedFrameTimelineOffsetUs } from "./streamingDecoder";
|
||||
|
||||
const DEFAULT_MAX_DECODE_QUEUE = 12;
|
||||
const DEFAULT_MAX_PENDING_FRAMES = 32;
|
||||
@@ -35,6 +36,7 @@ export class ForwardFrameSource {
|
||||
private heldFrameSec = 0;
|
||||
private lastTargetTimeSec = 0;
|
||||
private firstFrameTimestampUs: number | null = null;
|
||||
private frameTimelineOffsetUs = 0;
|
||||
|
||||
private toLocalFilePath(resourceUrl: string): string | null {
|
||||
if (!resourceUrl.startsWith("file:")) {
|
||||
@@ -292,8 +294,12 @@ export class ForwardFrameSource {
|
||||
return null;
|
||||
}
|
||||
this.firstFrameTimestampUs = firstFrame.timestamp;
|
||||
this.frameTimelineOffsetUs = getDecodedFrameTimelineOffsetUs(
|
||||
firstFrame.timestamp,
|
||||
this.metadata,
|
||||
);
|
||||
this.heldFrame = firstFrame;
|
||||
this.heldFrameSec = 0;
|
||||
this.heldFrameSec = Math.max(0, this.frameTimelineOffsetUs / 1_000_000);
|
||||
}
|
||||
|
||||
while (!this.cancelled) {
|
||||
@@ -308,7 +314,10 @@ export class ForwardFrameSource {
|
||||
this.heldFrameSec,
|
||||
Math.max(
|
||||
0,
|
||||
(nextFrame.timestamp - (this.firstFrameTimestampUs ?? nextFrame.timestamp)) / 1_000_000,
|
||||
(
|
||||
nextFrame.timestamp - (this.firstFrameTimestampUs ?? nextFrame.timestamp) +
|
||||
this.frameTimelineOffsetUs
|
||||
) / 1_000_000,
|
||||
),
|
||||
);
|
||||
const handoffBoundarySec = (this.heldFrameSec + nextFrameSec) / 2;
|
||||
@@ -387,5 +396,6 @@ export class ForwardFrameSource {
|
||||
this.decodeError = null;
|
||||
this.lastTargetTimeSec = 0;
|
||||
this.firstFrameTimestampUs = null;
|
||||
this.frameTimelineOffsetUs = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getDecodedFrameStartupOffsetUs } from './streamingDecoder';
|
||||
import {
|
||||
getDecodedFrameStartupOffsetUs,
|
||||
getDecodedFrameTimelineOffsetUs,
|
||||
} from './streamingDecoder';
|
||||
|
||||
describe('getDecodedFrameStartupOffsetUs', () => {
|
||||
it('ignores positive stream start metadata when the first decoded frame matches it', () => {
|
||||
@@ -27,4 +30,32 @@ describe('getDecodedFrameStartupOffsetUs', () => {
|
||||
|
||||
expect(getDecodedFrameStartupOffsetUs(250_000, {})).toBe(250_000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDecodedFrameTimelineOffsetUs', () => {
|
||||
it('preserves a non-zero stream start time when decoded timestamps match the stream start', () => {
|
||||
expect(
|
||||
getDecodedFrameTimelineOffsetUs(6_741_667, {
|
||||
mediaStartTime: 0,
|
||||
streamStartTime: 6.741667,
|
||||
}),
|
||||
).toBe(6_741_667);
|
||||
});
|
||||
|
||||
it('includes both the stream start offset and any startup gap beyond it', () => {
|
||||
expect(
|
||||
getDecodedFrameTimelineOffsetUs(5_128_000, {
|
||||
mediaStartTime: 0,
|
||||
streamStartTime: 4.978,
|
||||
}),
|
||||
).toBe(5_128_000);
|
||||
});
|
||||
|
||||
it('falls back to a media-relative startup gap when stream metadata is missing', () => {
|
||||
expect(
|
||||
getDecodedFrameTimelineOffsetUs(250_000, {
|
||||
mediaStartTime: 0.1,
|
||||
}),
|
||||
).toBe(150_000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,16 +26,31 @@ type OnFrameCallback = (
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
export function getDecodedFrameTimelineOffsetUs(
|
||||
firstDecodedFrameTimestampUs: number,
|
||||
metadata: Pick<DecodedVideoInfo, 'mediaStartTime' | 'streamStartTime'>
|
||||
): number {
|
||||
const mediaStartTimeUs = Math.round((metadata.mediaStartTime ?? 0) * 1_000_000);
|
||||
const streamStartTimeUs = Math.round(
|
||||
(metadata.streamStartTime ?? metadata.mediaStartTime ?? 0) * 1_000_000
|
||||
);
|
||||
|
||||
return (
|
||||
Math.max(0, streamStartTimeUs - mediaStartTimeUs) +
|
||||
getDecodedFrameStartupOffsetUs(firstDecodedFrameTimestampUs, metadata)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes video frames via web-demuxer + VideoDecoder in a single forward pass.
|
||||
@@ -217,10 +232,11 @@ export class StreamingVideoDecoder {
|
||||
// Async frame queue — decoder pushes, consumer pulls
|
||||
this.pendingFrames.length = 0
|
||||
const pendingFrames = this.pendingFrames
|
||||
let frameResolve: ((frame: VideoFrame | null) => void) | null = null;
|
||||
let decodeError: Error | null = null;
|
||||
let decodeDone = false;
|
||||
let firstDecodedFrameTimestampUs: number | null = null;
|
||||
let frameResolve: ((frame: VideoFrame | null) => void) | null = null;
|
||||
let decodeError: Error | null = null;
|
||||
let decodeDone = false;
|
||||
let firstDecodedFrameTimestampUs: number | null = null;
|
||||
let decodedFrameTimelineOffsetUs = 0;
|
||||
|
||||
this.decoder = new VideoDecoder({
|
||||
output: (frame: VideoFrame) => {
|
||||
@@ -363,14 +379,19 @@ export class StreamingVideoDecoder {
|
||||
const frame = await getNextFrame();
|
||||
if (!frame) break;
|
||||
|
||||
if (firstDecodedFrameTimestampUs === null) {
|
||||
firstDecodedFrameTimestampUs = frame.timestamp;
|
||||
}
|
||||
|
||||
const normalizedFrameTimeSec = Math.max(
|
||||
0,
|
||||
(frame.timestamp - firstDecodedFrameTimestampUs) / 1_000_000,
|
||||
);
|
||||
if (firstDecodedFrameTimestampUs === null) {
|
||||
firstDecodedFrameTimestampUs = frame.timestamp;
|
||||
decodedFrameTimelineOffsetUs = getDecodedFrameTimelineOffsetUs(
|
||||
firstDecodedFrameTimestampUs,
|
||||
this.metadata
|
||||
);
|
||||
}
|
||||
|
||||
const normalizedFrameTimeSec = Math.max(
|
||||
0,
|
||||
(frame.timestamp - firstDecodedFrameTimestampUs + decodedFrameTimelineOffsetUs) /
|
||||
1_000_000,
|
||||
);
|
||||
const frameTimeSec: number =
|
||||
lastDecodedFrameSec === null
|
||||
? normalizedFrameTimeSec
|
||||
|
||||
Reference in New Issue
Block a user