fix(goLive): keep IDR in own slot so decoder always has a reference (was blank)
The tail-drop rewrite let a P-frame supersede a pending keyframe before the emit tick fired, so the decoder never received an IDR → blank GoLive tile. Give keyframes their own slot (pendingKey) that P-frames cannot steal, and only emit a P-frame once at least one IDR has been shown (haveReference). IDR is always emitted first when present so the reference re-establishes.
This commit is contained in:
@@ -399,11 +399,15 @@ export async function demux(
|
|||||||
opts.frameRate ?? (vInfo.framerate_num / vInfo.framerate_den || 30);
|
opts.frameRate ?? (vInfo.framerate_num / vInfo.framerate_den || 30);
|
||||||
const emitIntervalMs = 1000 / videoFps;
|
const emitIntervalMs = 1000 / videoFps;
|
||||||
|
|
||||||
// The single frame we will emit on the next tick. Only the newest survives;
|
// The frames we will emit. Keyframes (IDR) get their OWN slot that P-frames
|
||||||
// keyframes are never superseded so the decoder always gets its IDRs.
|
// can never supersede — a missing IDR means the decoder has no reference and
|
||||||
let latestAu: Buffer | null = null;
|
// shows a BLANK tile. P-frames keep only the newest (tail-drop); older ones
|
||||||
let latestIsKey = false;
|
// are skipped. A P-frame is only emitted once we have already shown at least
|
||||||
let skippedFrames = 0; // frames superseded before they could be shown
|
// one keyframe (reference established), otherwise it is dropped.
|
||||||
|
let pendingKey: Buffer | null = null; // most recent IDR, not yet emitted
|
||||||
|
let latestP: Buffer | null = null; // newest P-frame, not yet emitted
|
||||||
|
let haveReference = false; // an IDR has been shown
|
||||||
|
let skippedFrames = 0; // P-frames superseded/useless before emit
|
||||||
|
|
||||||
const flushAccessUnit = () => {
|
const flushAccessUnit = () => {
|
||||||
if (pendingNals.length === 0) return;
|
if (pendingNals.length === 0) return;
|
||||||
@@ -417,28 +421,36 @@ export async function demux(
|
|||||||
pendingNals = [];
|
pendingNals = [];
|
||||||
pendingHasSlice = false;
|
pendingHasSlice = false;
|
||||||
pendingIsKey = false;
|
pendingIsKey = false;
|
||||||
// Tail-drop: keep only the newest frame. A keyframe always wins (never
|
|
||||||
// superseded by later non-keys in the same tick window) so the decoder
|
|
||||||
// keeps getting IDRs; a non-key only replaces a pending non-key.
|
|
||||||
if (isKey) {
|
if (isKey) {
|
||||||
latestAu = au;
|
// Keyframe: always kept in its own slot, never superseded by a later
|
||||||
latestIsKey = true;
|
// P-frame (that was the previous bug → decoder got no IDR → blank).
|
||||||
} else if (latestAu === null || latestIsKey) {
|
pendingKey = au;
|
||||||
latestAu = au;
|
|
||||||
latestIsKey = false;
|
|
||||||
} else {
|
} else {
|
||||||
skippedFrames++;
|
// P-frame: keep only the newest; drop the previous one.
|
||||||
|
if (latestP !== null) skippedFrames++;
|
||||||
|
latestP = au;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Steady real-time emission clock. Emits the freshest buffered frame once
|
// Steady real-time emission clock. Emits the freshest frame once per tick
|
||||||
// per tick (≈ videoFps); never buffers more than one, so no backlog and
|
// (≈ videoFps); never buffers more than one of each kind, so no backlog and
|
||||||
// no lag. This — not the encoder rate — defines playback speed.
|
// no lag. This — not the encoder rate — defines playback speed. Order: an
|
||||||
|
// IDR is always emitted first when present so the decoder re-establishes a
|
||||||
|
// reference; otherwise emit the newest P-frame once a reference exists.
|
||||||
const emitTick = () => {
|
const emitTick = () => {
|
||||||
if (latestAu === null) return;
|
let au: Buffer | null = null;
|
||||||
const au = latestAu;
|
let isKey = false;
|
||||||
const isKey = latestIsKey;
|
if (pendingKey !== null) {
|
||||||
latestAu = null;
|
au = pendingKey;
|
||||||
|
isKey = true;
|
||||||
|
pendingKey = null;
|
||||||
|
haveReference = true;
|
||||||
|
} else if (latestP !== null && haveReference) {
|
||||||
|
au = latestP;
|
||||||
|
isKey = false;
|
||||||
|
latestP = null;
|
||||||
|
}
|
||||||
|
if (au === null) return;
|
||||||
vPipe.write({
|
vPipe.write({
|
||||||
data: au,
|
data: au,
|
||||||
// One frame at videoFps: duration=1 in a 1/fps timebase →
|
// One frame at videoFps: duration=1 in a 1/fps timebase →
|
||||||
|
|||||||
Reference in New Issue
Block a user