From 61e895a75a1436a8b8985b4a975238b5b2ce519e Mon Sep 17 00:00:00 2001 From: Enriquefft Date: Thu, 16 Apr 2026 14:18:40 -0500 Subject: [PATCH] fix: sanitize packet-scan range against NaN/Infinity duration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mediaInfo.duration from web-demuxer can be NaN or Infinity on Chromium Linux (same MediaRecorder bug this PR otherwise addresses). That value flowed straight into Math.max + demuxer.read() as scanEndSec, producing an invalid range argument and breaking the ground-truth packet scan. Guard both mediaInfo.duration and videoStream.duration with Number.isFinite before Math.max; validateDuration() already handled the downstream use. Drop redundant WebDemuxer.read() / getDecoderConfig() type casts while here — the generics infer the chunk/config type from the media string literal, so the `as ReadableStream` and `as AudioDecoderConfig` are no-ops. --- src/lib/exporter/audioEncoder.ts | 11 +++++------ src/lib/exporter/streamingDecoder.ts | 13 +++++++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/lib/exporter/audioEncoder.ts b/src/lib/exporter/audioEncoder.ts index e373702..611ef5b 100644 --- a/src/lib/exporter/audioEncoder.ts +++ b/src/lib/exporter/audioEncoder.ts @@ -62,7 +62,7 @@ export class AudioProcessor { ): Promise { let audioConfig: AudioDecoderConfig; try { - audioConfig = (await demuxer.getDecoderConfig("audio")) as AudioDecoderConfig; + audioConfig = await demuxer.getDecoderConfig("audio"); } catch { console.warn("[AudioProcessor] No audio track found, skipping"); return; @@ -87,11 +87,10 @@ export class AudioProcessor { typeof readEndSec === "number" && Number.isFinite(readEndSec) ? Math.max(0, readEndSec) : undefined; - const audioStream = ( + const audioStream = safeReadEndSec !== undefined ? demuxer.read("audio", 0, safeReadEndSec) - : demuxer.read("audio") - ) as ReadableStream; + : demuxer.read("audio"); const reader = audioStream.getReader(); try { @@ -396,8 +395,8 @@ export class AudioProcessor { try { await demuxer.load(file); - const audioConfig = (await demuxer.getDecoderConfig("audio")) as AudioDecoderConfig; - const reader = (demuxer.read("audio") as ReadableStream).getReader(); + const audioConfig = await demuxer.getDecoderConfig("audio"); + const reader = demuxer.read("audio").getReader(); let isFirstChunk = true; try { diff --git a/src/lib/exporter/streamingDecoder.ts b/src/lib/exporter/streamingDecoder.ts index b0866f5..fca92e2 100644 --- a/src/lib/exporter/streamingDecoder.ts +++ b/src/lib/exporter/streamingDecoder.ts @@ -231,11 +231,16 @@ export class StreamingVideoDecoder { // MediaRecorder (especially on Linux) writes unreliable container durations. // Packet timestamps are ground truth — no decode needed, just timestamp reads. // Pass explicit range because some containers are truncated without one. - const scanEndSec = Math.max(mediaInfo.duration, videoStream?.duration ?? 0, 0) + 0.5; + // Sanitize because mediaInfo.duration can be NaN/Infinity (Chromium Linux bug), + // which would propagate into demuxer.read() as an invalid endpoint. + const containerDurationSec = Number.isFinite(mediaInfo.duration) ? mediaInfo.duration : 0; + const streamDurationSec = + typeof videoStream?.duration === "number" && Number.isFinite(videoStream.duration) + ? videoStream.duration + : 0; + const scanEndSec = Math.max(containerDurationSec, streamDurationSec, 0) + 0.5; let maxPacketEndUs = 0; - const scanReader = ( - this.demuxer.read("video", 0, scanEndSec) as ReadableStream - ).getReader(); + const scanReader = this.demuxer.read("video", 0, scanEndSec).getReader(); try { while (true) { const { done, value } = await scanReader.read();