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 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-12 21:52:16 +01:00
co-authored by Claude Opus 4.8
parent 91b3f0c1c1
commit a7742fe2ec
+33 -2
View File
@@ -112,10 +112,12 @@ public sealed class ServiceSendHost : IDisposable
internal void RunLoopWithToken(CancellationToken ct, string tokenName, int pollMs, int resumeSettleMs) internal void RunLoopWithToken(CancellationToken ct, string tokenName, int pollMs, int resumeSettleMs)
=> RunLoopCore(ct, () => InteractivePresence.IsInteractiveAppRunning(tokenName), pollMs, resumeSettleMs); => RunLoopCore(ct, () => InteractivePresence.IsInteractiveAppRunning(tokenName), pollMs, resumeSettleMs);
private void RunLoopCore(CancellationToken ct, Func<bool> isAppPresent, int pollMs, int resumeSettleMs) private void RunLoopCore(CancellationToken ct, Func<bool> isAppPresent, int pollMs, int resumeSettleMs, int healthMs = 5000)
{ {
var appWasPresent = true; // force an initial evaluation var appWasPresent = true; // force an initial evaluation
var absentSince = Environment.TickCount64; 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) while (!ct.IsCancellationRequested)
{ {
var appPresent = isAppPresent(); var appPresent = isAppPresent();
@@ -123,12 +125,41 @@ public sealed class ServiceSendHost : IDisposable
{ {
if (IsSending) Suspend(); if (IsSending) Suspend();
absentSince = long.MaxValue; absentSince = long.MaxValue;
healthBaseline = -1;
} }
else else
{ {
if (appWasPresent) absentSince = Environment.TickCount64; // app just left — start the settle timer if (appWasPresent) absentSince = Environment.TickCount64; // app just left — start the settle timer
if (!IsSending && Environment.TickCount64 - absentSince >= resumeSettleMs) 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(); 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; appWasPresent = appPresent;
ct.WaitHandle.WaitOne(pollMs); ct.WaitHandle.WaitOne(pollMs);