potential fix for OOM

This commit is contained in:
2026-05-02 00:05:11 -07:00
parent 0ec8f0faf9
commit acd73436a7
2 changed files with 45 additions and 9 deletions

View File

@@ -121,6 +121,8 @@ Important behavior:
- Uses bounded branch queues via `createRelayInputBranch`. - Uses bounded branch queues via `createRelayInputBranch`.
- Pauses upstream reading while any branch queue exceeds half of `MAX_RELAY_BRANCH_QUEUE_BYTES`. - Pauses upstream reading while any branch queue exceeds half of `MAX_RELAY_BRANCH_QUEUE_BYTES`.
- Stops playback if any branch queue exceeds `MAX_RELAY_BRANCH_QUEUE_BYTES`. - Stops playback if any branch queue exceeds `MAX_RELAY_BRANCH_QUEUE_BYTES`.
- Backpressure accounting must include both chunks queued in JavaScript and bytes already written to ffmpeg stdin while waiting for `drain`. Otherwise fast movie sources can outrun realtime ffmpeg consumption and grow Node heap until OOM.
- When waiting for relay capacity, wait only on branches that are actually over the pause threshold. Including already-ready branches in a `Promise.race` can create an immediate-resolution spin loop.
Relay mode works best for sequential stream containers such as MPEG-TS/IPTV. It may be less reliable for file formats that require seeking or late metadata, such as some MP4 files. Relay mode works best for sequential stream containers such as MPEG-TS/IPTV. It may be less reliable for file formats that require seeking or late metadata, such as some MP4 files.

View File

@@ -1417,6 +1417,7 @@ function createRelayInputBranch(stream, onError) {
let accepting = true; let accepting = true;
let ending = false; let ending = false;
let drainPending = false; let drainPending = false;
let streamPendingBytes = 0;
let queue = []; let queue = [];
let queueBytes = 0; let queueBytes = 0;
let peakBytes = 0; let peakBytes = 0;
@@ -1424,18 +1425,28 @@ function createRelayInputBranch(stream, onError) {
stream.on('drain', () => { stream.on('drain', () => {
drainPending = false; drainPending = false;
streamPendingBytes = 0;
updatePeakBytes();
notifyWaiters();
flush(); flush();
}); });
stream.on('error', (error) => { stream.on('error', (error) => {
accepting = false; accepting = false;
streamPendingBytes = 0;
notifyWaiters(); notifyWaiters();
onError(error); onError(error);
}); });
stream.on('close', () => {
accepting = false;
streamPendingBytes = 0;
notifyWaiters();
});
return { return {
get queueBytes() { get queueBytes() {
return queueBytes; return getPendingBytes();
}, },
get peakBytes() { get peakBytes() {
return peakBytes; return peakBytes;
@@ -1451,9 +1462,9 @@ function createRelayInputBranch(stream, onError) {
queue.push(chunk); queue.push(chunk);
queueBytes += chunk.length; queueBytes += chunk.length;
peakBytes = Math.max(peakBytes, queueBytes); updatePeakBytes();
notifyWaiters(); notifyWaiters();
return queueBytes <= MAX_RELAY_BRANCH_QUEUE_BYTES; return getPendingBytes() <= MAX_RELAY_BRANCH_QUEUE_BYTES;
}, },
end() { end() {
accepting = false; accepting = false;
@@ -1465,6 +1476,7 @@ function createRelayInputBranch(stream, onError) {
ending = false; ending = false;
queue = []; queue = [];
queueBytes = 0; queueBytes = 0;
streamPendingBytes = 0;
notifyWaiters(); notifyWaiters();
if (!stream.destroyed) { if (!stream.destroyed) {
@@ -1472,7 +1484,7 @@ function createRelayInputBranch(stream, onError) {
} }
}, },
waitForCapacity() { waitForCapacity() {
if (queueBytes <= RELAY_BRANCH_PAUSE_BYTES || !accepting || stream.destroyed) { if (getPendingBytes() <= RELAY_BRANCH_PAUSE_BYTES || !accepting || stream.destroyed) {
return Promise.resolve(); return Promise.resolve();
} }
@@ -1484,10 +1496,17 @@ function createRelayInputBranch(stream, onError) {
function writeNow(chunk) { function writeNow(chunk) {
try { try {
drainPending = !stream.write(chunk); if (!stream.write(chunk)) {
drainPending = true;
streamPendingBytes += chunk.length;
updatePeakBytes();
}
notifyWaiters();
return true; return true;
} catch (error) { } catch (error) {
accepting = false; accepting = false;
streamPendingBytes = 0;
notifyWaiters(); notifyWaiters();
onError(error); onError(error);
return false; return false;
@@ -1498,11 +1517,12 @@ function createRelayInputBranch(stream, onError) {
while (queue.length > 0 && !drainPending && !stream.destroyed) { while (queue.length > 0 && !drainPending && !stream.destroyed) {
const chunk = queue.shift(); const chunk = queue.shift();
queueBytes -= chunk.length; queueBytes -= chunk.length;
notifyWaiters();
if (!writeNow(chunk)) { if (!writeNow(chunk)) {
return; return;
} }
notifyWaiters();
} }
if (ending && queue.length === 0 && !drainPending && !stream.destroyed) { if (ending && queue.length === 0 && !drainPending && !stream.destroyed) {
@@ -1511,7 +1531,7 @@ function createRelayInputBranch(stream, onError) {
} }
function notifyWaiters() { function notifyWaiters() {
if (queueBytes > RELAY_BRANCH_PAUSE_BYTES && accepting && !stream.destroyed) { if (getPendingBytes() > RELAY_BRANCH_PAUSE_BYTES && accepting && !stream.destroyed) {
return; return;
} }
@@ -1522,11 +1542,25 @@ function createRelayInputBranch(stream, onError) {
resolve(); resolve();
} }
} }
function getPendingBytes() {
return queueBytes + streamPendingBytes;
}
function updatePeakBytes() {
peakBytes = Math.max(peakBytes, getPendingBytes());
}
} }
async function waitForRelayCapacity(branches, shouldStop) { async function waitForRelayCapacity(branches, shouldStop) {
while (!shouldStop() && branches.some((branch) => branch.queueBytes > RELAY_BRANCH_PAUSE_BYTES)) { while (!shouldStop()) {
await Promise.race(branches.map((branch) => branch.waitForCapacity())); const blockedBranches = branches.filter((branch) => branch.queueBytes > RELAY_BRANCH_PAUSE_BYTES);
if (blockedBranches.length === 0) {
return;
}
await Promise.race(blockedBranches.map((branch) => branch.waitForCapacity()));
} }
} }