fix(start): a second instance can no longer kill the app on Go Live
Reported 2026-11-06: a Release instance crashed when clicking Go Live in test mode, while a Debug
instance was recording its window. The Windows event log gave the cause:
System.IO.IOException: All pipe instances are busy.
at System.IO.Pipes.NamedPipeServerStream.Create(...)
at ytLive.Services.Audio.NamedPipeAudioWriter.Start(...)
at ytLive.Services.Audio.AudioMixer.StartLive(...)
at MainViewModel.StartStreamingSession(...)
at MainViewModel.BeginTestStream(...)
at MainViewModel.StartFromPrimaryButton(...)
at System.Windows.Controls.Button.OnClick()
Two defects, both fixed:
1. **A button click could end the process.** `StartLive` throws when the pipe name is taken, and nothing
on the go-live path caught it. `TryStartAudioPipe` now catches, logs, toasts ("Another copy of
LlamaCasty is already using the audio pipe") and rolls the session back. Recording-only takes the same
path.
2. **The pipe name was a shared constant**, so a second instance could never go live on the same machine.
`InstanceProfile.AudioPipeName` appends the dev instance id (DEBUG only); Release keeps the plain name
so the normal single-instance case is unchanged. The encoder reads the same derived name, so mixer and
ffmpeg cannot disagree.
Tests: `AudioPipeInstanceTests` — a second server on one name throws (the collision is real), dev names
derive per instance, and a writer on a taken name throws rather than silently reusing the pipe.
Debug and Release both build clean.
This commit is contained in:
@@ -66,12 +66,34 @@ public partial class MainViewModel : ViewModelBase
|
||||
_liveElapsed = TimeSpan.Zero;
|
||||
_recordLength = TimeSpan.Zero;
|
||||
ResetHealth(StreamStatus.Offline);
|
||||
_audioMixer.StartLive(EncoderOptions.DefaultAudioPipeName);
|
||||
if (!TryStartAudioPipe()) return;
|
||||
IsRecording = true;
|
||||
LiveScene = StagedScene;
|
||||
await _framePump.StartAsync();
|
||||
}
|
||||
|
||||
/// <summary>Starts the live audio pipe, or explains why it cannot and rolls the session back.
|
||||
/// <para>This used to throw straight out of the Go Live button: the pipe server allows one instance,
|
||||
/// so with a second LlamaCasty running on the same machine the click killed the app
|
||||
/// ("All pipe instances are busy", 2026-11-06). A click must never be able to end the process.</para>
|
||||
/// </summary>
|
||||
private bool TryStartAudioPipe()
|
||||
{
|
||||
try
|
||||
{
|
||||
_audioMixer.StartLive(InstanceProfile.AudioPipeName(EncoderOptions.DefaultAudioPipeName));
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLog.Write(ex, "Audio pipe could not be started — session aborted");
|
||||
_notifications.Error("Couldn't start the audio",
|
||||
"Another copy of LlamaCasty is already using the audio pipe. Close it, then try again.");
|
||||
StopStream();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginGoLive(bool alsoRecord)
|
||||
{
|
||||
var dialog = new GoLiveViewModel(() => SignInAsync(), _youtubeAuth.CurrentChannel)
|
||||
@@ -124,7 +146,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
// persisted, and reset when the session ends.
|
||||
_sessionStartedAtUtc = DateTime.UtcNow;
|
||||
ResetHealth(StreamStatus.Streaming);
|
||||
_audioMixer.StartLive(EncoderOptions.DefaultAudioPipeName);
|
||||
if (!TryStartAudioPipe()) return;
|
||||
_liveElapsed = TimeSpan.Zero;
|
||||
_recordLength = TimeSpan.Zero;
|
||||
|
||||
@@ -471,6 +493,8 @@ public partial class MainViewModel : ViewModelBase
|
||||
InputHeight = compositor.OutputHeight,
|
||||
Fps = quality.Fps,
|
||||
BitrateKbps = (int)Math.Round(quality.Bitrate * 1000),
|
||||
// Same derivation as the mixer's StartLive call: a dev instance serves its own pipe.
|
||||
AudioPipeName = InstanceProfile.AudioPipeName(EncoderOptions.DefaultAudioPipeName),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user