From a2aa0a5f970ed3d0c0027a68e8856423f2207c4f Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 12 Aug 2026 13:16:58 +0200 Subject: [PATCH] fix(apple): never sit black AND silent while the video format is unseeded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both pumps (Stage2Pipeline + the stage-1 StreamPump) dropped every AU in a silent guard while 'format' was nil — the opening IDR's parameter sets never arrived or never parsed, and under the host's infinite GOP nothing re-delivers them unless the client asks. Field 2026-08-12 (iOS, H265): sessions decoding nothing, requesting nothing, host streaming perfectly — a black stream with zero recovery requests, randomly self-healing only when some later event produced a fresh IDR. While format is nil after the first received AU, the pumps now set awaitingIDR (routing through the existing 100 ms-throttled recovery.request()) and log the state once, so a lost/unparsed opening IDR converts to a normal recovery round-trip instead of a permanent silent black. --- .../PunktfunkKit/Video/Stage2Pipeline.swift | 17 +++++++++++++++++ .../Sources/PunktfunkKit/Video/StreamPump.swift | 15 +++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/clients/apple/Sources/PunktfunkKit/Video/Stage2Pipeline.swift b/clients/apple/Sources/PunktfunkKit/Video/Stage2Pipeline.swift index bd7e9d52..17e54014 100644 --- a/clients/apple/Sources/PunktfunkKit/Video/Stage2Pipeline.swift +++ b/clients/apple/Sources/PunktfunkKit/Video/Stage2Pipeline.swift @@ -57,6 +57,8 @@ let presentDebug = ProcessInfo.processInfo.environment["PUNKTFUNK_PRESENT_DEBUG" /// to Console.app wirelessly with no env var / Xcode attach. Always on for deadline pacing (the /// stats are a few arrays + one log line per second); other pacings keep the env-gated print. private let presentLog = Logger(subsystem: "io.unom.punktfunk", category: "present") +/// Pump-side events (loss recovery, format seeding) — the stage-2 sibling of StreamPump's log. +private let pumpLog = Logger(subsystem: "io.unom.punktfunk", category: "pump") /// Decoded-frame hand-off between the decode half and the render thread. The POLICY is the /// user's presentation intent (design/apple-presentation-rebuild.md — the 2026-07 rebuild that @@ -932,6 +934,21 @@ public final class Stage2Pipeline { } awaitingIDR = false // a fresh IDR re-anchored decode — recovery complete } + if format == nil { + // No decodable format yet: the opening IDR's parameter sets never + // arrived (or never parsed), and under the host's infinite GOP nothing + // re-delivers them unless we ASK. Without this the guard below drops + // every AU silently, forever — the field "black stream, zero recovery + // requests" state (2026-08-12): the host streams perfectly, the client + // shows nothing and says nothing. awaitingIDR routes through the same + // 100 ms-throttled recovery.request() at the top of the loop. + if !awaitingIDR { + pumpLog.warning( + "video: received AUs but no decodable format (missing/unparsed parameter sets) — requesting an IDR until one seeds it" + ) + } + awaitingIDR = true + } guard let f = format, !token.isStopped else { return true } if decoder.decode(au: au, format: f) { decodeFailRun = 0 diff --git a/clients/apple/Sources/PunktfunkKit/Video/StreamPump.swift b/clients/apple/Sources/PunktfunkKit/Video/StreamPump.swift index e647f44f..215a5aea 100644 --- a/clients/apple/Sources/PunktfunkKit/Video/StreamPump.swift +++ b/clients/apple/Sources/PunktfunkKit/Video/StreamPump.swift @@ -116,6 +116,21 @@ final class StreamPump { } awaitingIDR = false // a fresh IDR re-anchored decode — recovery complete } + if format == nil { + // No decodable format yet: the opening IDR's parameter sets never + // arrived (or never parsed), and under the host's infinite GOP nothing + // re-delivers them unless we ASK. Without this the format guard below + // drops every AU silently, forever — the field "black stream, zero + // recovery requests" state (2026-08-12). awaitingIDR routes through the + // same 100 ms-throttled recovery.request() at the top of the loop. + if !awaitingIDR { + awaitingSince = Date() + pumpLog.warning( + "video: received AUs but no decodable format (missing/unparsed parameter sets) — requesting an IDR until one seeds it" + ) + } + awaitingIDR = true + } let failed = layer.status == .failed if failed { // Decode wedged hard (the cold-first-connect case — a lost/corrupt opening