From dbcb69278fd613336b875d2116e3f533d726b350 Mon Sep 17 00:00:00 2001 From: gramps Date: Fri, 6 Nov 2026 12:30:48 -0800 Subject: [PATCH] fix(audio): a DEBUG build never shares the pipe name with Release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creator was recording on the Release build and hit "Couldn't start the audio — another copy of LlamaCasty is already using the audio pipe" when going live on Debug. The guard did its job (no crash), but the collision should not have been possible: the pipe name only suffixed when `YTLIVE_INSTANCE` was set, and a Debug launched straight from the IDE has no id — so it asked for the same `ytllive_audio` the Release build was serving. DEBUG now always suffixes: the claimed instance id when there is one, otherwise the process id. Any number of dev builds can run beside the Release app, and each Debug process is distinct from every other. Release is unchanged — it resolves to the plain name, and the whole mechanism disappears in `#if DEBUG` as before. `TryStartAudioPipe` now logs the resolved name (`Audio pipe: …`), so the next report of this kind is one grep instead of a code hunt. Tests: AudioPipeInstanceTests pins that an id-less DEBUG process never resolves to the bare name; 572 green, 0 warnings. --- HANDOFF.md | 2 +- Helpers/InstanceProfile.cs | 10 +++++----- ViewModels/MainViewModel.Streaming.Operations.cs | 4 +++- ytLive.Tests/AudioPipeInstanceTests.cs | 7 +++++++ 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 0253886..2816eaa 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -35,6 +35,6 @@ - Pack URIs for bundled assets must be assembly-qualified (`/LlamaCasty;component/Assets/…`): the unqualified form resolves against the entry assembly, which is the test host under `dotnet test`. - Media sessions are keyed by **path + look** (`MainViewModel.MediaSessionKey`); never acquire by raw path. -- **The audio pipe is one instance per machine.** DEBUG appends its id (`InstanceProfile.AudioPipeName`). +- **The audio pipe allows ONE server per name**; DEBUG suffixes it (id, else pid), Release does not. - Integration tests constructing `MainWindow` must set a temporary `LayoutPathOverride` and clear it in `finally`. - Bundled FFmpeg stays outside the single-file exe (`ExcludeFromSingleFile="true"`). diff --git a/Helpers/InstanceProfile.cs b/Helpers/InstanceProfile.cs index d066915..d44ee96 100644 --- a/Helpers/InstanceProfile.cs +++ b/Helpers/InstanceProfile.cs @@ -70,13 +70,13 @@ public static class InstanceProfile #endif /// The audio pipe name for this process. The pipe server allows exactly ONE instance, so a - /// shared name meant the second app on the machine could never go live — it threw - /// "All pipe instances are busy" out of the Go Live click and ended the process (2026-11-06: two - /// instances, one recording the other). A dev instance appends its id; Release, and therefore the - /// first instance, keeps the plain name. + /// shared name means the second app on the machine can never go live — it throws "All pipe instances + /// are busy" (2026-11-06: Release recording, Debug trying to go live). Release keeps the plain name; + /// a DEBUG build ALWAYS appends a suffix — its claimed id, or the process id when it has none — so a + /// debug run launched straight from the IDE can never collide with the Release build beside it. public static string AudioPipeName(string baseName) #if DEBUG - => Id is { } id ? $"{baseName}_{id}" : baseName; + => Id is { } id ? $"{baseName}_{id}" : $"{baseName}_pid{Environment.ProcessId}"; #else => baseName; #endif diff --git a/ViewModels/MainViewModel.Streaming.Operations.cs b/ViewModels/MainViewModel.Streaming.Operations.cs index 7332e0b..ac1ca12 100644 --- a/ViewModels/MainViewModel.Streaming.Operations.cs +++ b/ViewModels/MainViewModel.Streaming.Operations.cs @@ -79,9 +79,11 @@ public partial class MainViewModel : ViewModelBase /// private bool TryStartAudioPipe() { + var pipeName = InstanceProfile.AudioPipeName(EncoderOptions.DefaultAudioPipeName); try { - _audioMixer.StartLive(InstanceProfile.AudioPipeName(EncoderOptions.DefaultAudioPipeName)); + _audioMixer.StartLive(pipeName); + AppLog.Write($"Audio pipe: {pipeName}"); return true; } catch (Exception ex) diff --git a/ytLive.Tests/AudioPipeInstanceTests.cs b/ytLive.Tests/AudioPipeInstanceTests.cs index c864351..8598cb9 100644 --- a/ytLive.Tests/AudioPipeInstanceTests.cs +++ b/ytLive.Tests/AudioPipeInstanceTests.cs @@ -36,7 +36,14 @@ public sealed class AudioPipeInstanceTests : IDisposable public void ADevInstanceServesItsOwnPipeName() { Environment.SetEnvironmentVariable(InstanceProfile.InstanceVariable, null); +#if DEBUG + // No claimed id (launched from the IDE) must still never collide with the Release build's pipe. + var unnamed = InstanceProfile.AudioPipeName("ytllive_audio"); + Assert.StartsWith("ytllive_audio_pid", unnamed); + Assert.Equal(unnamed, InstanceProfile.AudioPipeName("ytllive_audio")); +#else Assert.Equal("ytllive_audio", InstanceProfile.AudioPipeName("ytllive_audio")); +#endif Environment.SetEnvironmentVariable(InstanceProfile.InstanceVariable, "2"); Assert.Equal("ytllive_audio_2", InstanceProfile.AudioPipeName("ytllive_audio"));