rebroadcast: fix RTMP acknowledgement uint32 overflow (#1980)

The RTMP client's totalBytesReceived counter grows unbounded as a
JavaScript number. When it exceeds 2^31 (~2.15 GB received), the
writeUInt32BE call in sendAcknowledgementIfNeeded throws a RangeError
because JavaScript bitwise operations produce signed 32-bit integers.
This crashes the RTMP session and drops the video stream.

For a high-bitrate camera like the Reolink D340P (~4 Mbps main stream),
this overflow occurs after approximately 90 minutes of continuous
streaming, causing periodic stream drops at a consistent interval.

Fix by using the unsigned right shift operator (>>> 0) to keep
totalBytesReceived and bytesToAck in the unsigned uint32 range [0,
4294967295], matching the RTMP spec's sequence number wrapping behavior.

Co-authored-by: Josh Casada <joshcasada@Joshs-Mac-mini.ts.net lan>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
JoshADC
2026-02-08 21:53:46 -05:00
committed by GitHub
parent 440926ef73
commit 7112e049cb

View File

@@ -398,7 +398,7 @@ export class RtmpClient {
// Count: basic header (1 byte) + message header (0-11 bytes) + extended timestamp (0-4 bytes) + payload
const extTimestampSize = (hasExtendedTimestamp || chunkStream.hasExtendedTimestamp) ? 4 : 0;
const bytesInChunk = 1 + headerSize + extTimestampSize + chunkDataSize;
this.totalBytesReceived += bytesInChunk;
this.totalBytesReceived = (this.totalBytesReceived + bytesInChunk) >>> 0;
// Send window acknowledgement if threshold exceeded
this.sendAcknowledgementIfNeeded();
@@ -421,12 +421,14 @@ export class RtmpClient {
* Send acknowledgement if window threshold exceeded
*/
private sendAcknowledgementIfNeeded(): void {
const bytesToAck = this.totalBytesReceived - this.lastAcknowledgementBytes;
// Handle uint32 wrap-around: if totalBytesReceived wrapped past 0,
// bytesToAck will be a large positive number, which is correct.
const bytesToAck = (this.totalBytesReceived - this.lastAcknowledgementBytes) >>> 0;
if (bytesToAck >= this.windowAckSize) {
this.lastAcknowledgementBytes = this.totalBytesReceived;
console.log(`Sending acknowledgement: ${this.lastAcknowledgementBytes} bytes received (${bytesToAck} since last ACK)`);
const data = Buffer.alloc(4);
data.writeUInt32BE(this.lastAcknowledgementBytes & 0xFFFFFFFF, 0);
data.writeUInt32BE(this.lastAcknowledgementBytes, 0);
this.sendMessage(2, 0, RtmpMessageType.ACKNOWLEDGEMENT, 0, data);
}
}