From a7742fe2ec33258436cadd40f7be8e98b87bdd49 Mon Sep 17 00:00:00 2001 From: Ednunp <29843396+Ednunp@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:52:16 +0100 Subject: [PATCH] Service: self-heal if capture isn't ready (or a device drops) at start Local checkpoint - NOT for public release. Answers "what if it looks for sound devices before their services are up?" Two layers now: - depend= Audiosrv/AudioEndpointBuilder (prior commit) makes Windows start the service only once the audio services are running. - Self-heal in the run loop: while it should be sending, if no packets have flowed for ~5s then no capture is actually running (endpoints not fully ready at boot, a device dropped, or the audio service restarted) - it re-opens the capture. And while NOT sending it already re-tries every second, so a slow-to-appear audio stack or a device that returns later is picked up automatically. So even if the service races ahead of the endpoints being fully enumerated, it keeps retrying/re-opening until audio actually flows, rather than sitting silent. Gate 25/25. Co-Authored-By: Claude Opus 4.8 --- src/RemSound.App/ServiceSendHost.cs | 37 ++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/RemSound.App/ServiceSendHost.cs b/src/RemSound.App/ServiceSendHost.cs index 78c6f2d..da5c548 100644 --- a/src/RemSound.App/ServiceSendHost.cs +++ b/src/RemSound.App/ServiceSendHost.cs @@ -112,10 +112,12 @@ public sealed class ServiceSendHost : IDisposable internal void RunLoopWithToken(CancellationToken ct, string tokenName, int pollMs, int resumeSettleMs) => RunLoopCore(ct, () => InteractivePresence.IsInteractiveAppRunning(tokenName), pollMs, resumeSettleMs); - private void RunLoopCore(CancellationToken ct, Func isAppPresent, int pollMs, int resumeSettleMs) + private void RunLoopCore(CancellationToken ct, Func isAppPresent, int pollMs, int resumeSettleMs, int healthMs = 5000) { var appWasPresent = true; // force an initial evaluation var absentSince = Environment.TickCount64; + long healthBaseline = -1; // packet count at the last health check (-1 = not yet baselined) + var lastHealthTick = Environment.TickCount64; while (!ct.IsCancellationRequested) { var appPresent = isAppPresent(); @@ -123,12 +125,41 @@ public sealed class ServiceSendHost : IDisposable { if (IsSending) Suspend(); absentSince = long.MaxValue; + healthBaseline = -1; } else { if (appWasPresent) absentSince = Environment.TickCount64; // app just left — start the settle timer - if (!IsSending && Environment.TickCount64 - absentSince >= resumeSettleMs) - Resume(); + if (Environment.TickCount64 - absentSince >= resumeSettleMs) + { + if (!IsSending) + { + // Keep trying to (re)start every poll until it succeeds — covers a boot where the + // audio stack / a device isn't ready yet, and a device that returns later. + Resume(); + healthBaseline = -1; + lastHealthTick = Environment.TickCount64; + } + else if (Environment.TickCount64 - lastHealthTick >= healthMs) + { + // Self-heal: we THINK we're sending, but if no packets have flowed since the last + // check then no capture is actually running (devices weren't ready when we started, + // a device dropped, or the audio service restarted). Re-open the capture. + lastHealthTick = Environment.TickCount64; + var packets = sender.PacketsSent; + if (healthBaseline >= 0 && packets == healthBaseline) + { + log?.Invoke("service: no audio flowing while sending — re-opening capture"); + Suspend(); + Resume(); + healthBaseline = -1; + } + else + { + healthBaseline = packets; + } + } + } } appWasPresent = appPresent; ct.WaitHandle.WaitOne(pollMs);