- Connect no longer freezes: PushDiscoveryUnicastHints resolved remembered hostnames with synchronous Dns.GetHostAddresses on the UI thread, blocking the whole window for the DNS timeout on an unresolvable name. A screen-reader user experiences that as the entire machine locking up. Resolution now runs off the UI thread. Same class of bug as the v3.0.1 UPnP-on-the-UI-thread hang. (#10) - New profile / profile switch no longer hides the window: OnShown ORed the global StartMinimised into every instance, so creating a new profile while Start minimised was on dropped the window to the tray and looked like a crash. StartMinimised now applies only to a genuine cold launch; relaunches honour the explicit per-instance flag. (#12) - Smoother WASAPI audio: the receive producer loop and sender mix loop pace themselves with WaitHandle.WaitOne, bound by the system timer (~15.6ms default). Without a fine timer the 10ms feed slips to ~16-31ms and delivers audio in chunky bursts (the desktop-render chunkiness behind Andre's dropouts/lag). New SystemTimerResolution holds a 1ms timer whenever a stream is live, independent of the opt-in Priority mode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
5abbeefe29
commit
034a7de632
@@ -20,6 +20,36 @@ internal sealed class AboutDialog : Form
|
||||
/// updates" path.</summary>
|
||||
private const string ReleaseNotes =
|
||||
"""
|
||||
RemSound v4.2
|
||||
|
||||
Three fixes, one of them a real annoyance gone.
|
||||
|
||||
Connecting to someone no longer risks a freeze.
|
||||
If you had a peer saved by name and that name
|
||||
couldn't be looked up quickly, RemSound used to
|
||||
stall for a few seconds while it waited — and
|
||||
because your screen reader waits on RemSound, the
|
||||
whole computer could seem to lock up. That lookup
|
||||
now happens out of the way, so connecting stays
|
||||
responsive.
|
||||
|
||||
Creating a new profile (Ctrl+N), or switching
|
||||
profiles, no longer drops the window to the tray
|
||||
when you have "start minimised" turned on.
|
||||
Starting minimised is meant for when RemSound
|
||||
first launches, not for something you did on
|
||||
purpose — so a new profile now comes up the normal
|
||||
way (or stays in the tray only if that's where you
|
||||
already were), instead of vanishing and looking
|
||||
like a crash.
|
||||
|
||||
And WASAPI audio is smoother. RemSound now keeps
|
||||
Windows' timing fine while it's streaming, so audio
|
||||
moves in even steps instead of arriving in clumps.
|
||||
On machines where playback was breaking up or
|
||||
running laggy on WASAPI, this should help — and you
|
||||
no longer need Priority mode switched on to get it.
|
||||
|
||||
RemSound v4.1
|
||||
|
||||
A couple of small fixes for screen-reader users. When
|
||||
|
||||
@@ -1265,13 +1265,21 @@ public sealed class MainForm : Form
|
||||
// missing (deferred so it lands after the window is fully up and can surface).
|
||||
BeginInvoke(CheckForMissingEnabledCues);
|
||||
|
||||
// Honour AppConfig.StartMinimised — drop straight to the tray after the
|
||||
// window finishes loading. Wrapped in BeginInvoke so the minimise happens
|
||||
// *after* Shown completes (otherwise the form-show + form-hide collide and
|
||||
// some virtual-machine drivers throw a redraw exception). The pending-profile
|
||||
// apply path above is unaffected — settings/devices/peers are already wired
|
||||
// up before we hide the window.
|
||||
var minimizeThisInstance = AppConfig.Load().StartMinimised || startNextInstanceMinimized;
|
||||
// "Start minimised" is a COLD-BOOT preference: drop straight to the tray when the app
|
||||
// first launches. It must NOT apply when the user deliberately creates a new profile or
|
||||
// switches profiles — those relaunch the window through the Program.cs loop. Those paths
|
||||
// already set startNextInstanceMinimized to "stay in the tray only if we were already
|
||||
// there", which is the right intent; but ORing in the global StartMinimised used to
|
||||
// override that and hide the window on every new-profile / switch, which read to the user
|
||||
// as a crash (issue #12). So gate StartMinimised to the genuine first launch, and let
|
||||
// relaunches honour only the explicit per-instance flag.
|
||||
// BeginInvoke so the minimise happens *after* Shown completes (otherwise the form-show +
|
||||
// form-hide collide and some virtual-machine drivers throw a redraw exception). The
|
||||
// pending-profile apply path above is unaffected — settings/devices/peers are already
|
||||
// wired up before we hide the window.
|
||||
var coldStart = isFirstLaunch;
|
||||
isFirstLaunch = false;
|
||||
var minimizeThisInstance = startNextInstanceMinimized || (coldStart && AppConfig.Load().StartMinimised);
|
||||
startNextInstanceMinimized = false;
|
||||
if (minimizeThisInstance)
|
||||
{
|
||||
@@ -1783,6 +1791,12 @@ public sealed class MainForm : Form
|
||||
// "loading audio driver" splash in that case.
|
||||
internal static bool startNextInstanceMinimized;
|
||||
|
||||
// True until the first MainForm of the process has shown its window. Distinguishes a genuine
|
||||
// cold launch (where the "Start minimised" preference applies) from an in-session new-profile or
|
||||
// profile-switch relaunch (where it must NOT — those would otherwise hide the window and look
|
||||
// like a crash, issue #12). Flipped false in the first OnShown.
|
||||
private static bool isFirstLaunch = true;
|
||||
|
||||
/// <summary>Switch to the profile at <paramref name="path"/> via the same close-and-relaunch
|
||||
/// flow OpenProfileFromPicker uses. The active profile gets pushed to the front of the
|
||||
/// recents list by the next MainForm constructor when it sees the loaded path.</summary>
|
||||
@@ -5062,43 +5076,51 @@ public sealed class MainForm : Form
|
||||
/// </summary>
|
||||
private void PushDiscoveryUnicastHints()
|
||||
{
|
||||
var hints = new HashSet<IPAddress>();
|
||||
|
||||
// Manual peers store IPEndPoint already.
|
||||
foreach (var peer in manualPeers.Values)
|
||||
// Snapshot the UI-thread-owned inputs HERE, then resolve hostnames OFF the UI thread.
|
||||
//
|
||||
// The comment that used to live here claimed Dns.GetHostAddresses "returns near-instantly".
|
||||
// It does for a parsed IP or an already-cached name — but for a remembered HOSTNAME that
|
||||
// can't currently resolve (an offline peer, or a Tailscale/WireGuard name while the VPN is
|
||||
// down) it BLOCKS for the system DNS timeout, seconds per entry. This method runs on the UI
|
||||
// thread on every connect / disconnect / add-peer (it's how discovery learns its VPN unicast
|
||||
// targets), so that block froze the whole window for a few seconds — which a screen-reader
|
||||
// user experiences as the entire machine locking up (issue #10). Same class of bug as the
|
||||
// v3.0.1 UPnP-on-the-UI-thread hang, in a newer feature.
|
||||
//
|
||||
// SetUnicastPeerAddresses just swaps a snapshot reference and fires an announcement, and is
|
||||
// already called from the discovery receive loop's own thread, so it's safe to call from a
|
||||
// background thread here. The hints are advisory and re-pushed frequently, so a slightly
|
||||
// stale result from an overlapping resolution is harmless.
|
||||
var seedAddresses = manualPeers.Values.Select(p => p.Address).ToList();
|
||||
var rememberedEntries = settings.LoadRememberedPeers().ToList();
|
||||
Task.Run(() =>
|
||||
{
|
||||
hints.Add(peer.Address);
|
||||
}
|
||||
// Remembered peers are stored as string entries (IP or hostname). Try to parse as IP;
|
||||
// for hostnames try a quick non-blocking DNS lookup. We do this synchronously here
|
||||
// because the remembered list is small (typically 1–10 entries) and Dns.GetHostAddresses
|
||||
// returns near-instantly for either a parsed IP or a cached hostname.
|
||||
foreach (var entry in settings.LoadRememberedPeers())
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(entry)) continue;
|
||||
if (IPAddress.TryParse(entry, out var direct))
|
||||
var hints = new HashSet<IPAddress>(seedAddresses);
|
||||
foreach (var entry in rememberedEntries)
|
||||
{
|
||||
hints.Add(direct);
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
foreach (var addr in Dns.GetHostAddresses(entry))
|
||||
if (string.IsNullOrWhiteSpace(entry)) continue;
|
||||
if (IPAddress.TryParse(entry, out var direct))
|
||||
{
|
||||
if (addr.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
||||
hints.Add(direct);
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
foreach (var addr in Dns.GetHostAddresses(entry))
|
||||
{
|
||||
hints.Add(addr);
|
||||
if (addr.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
||||
{
|
||||
hints.Add(addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Not resolvable right now — skip; re-pushed next time this method runs.
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Hostname not resolvable right now — skip silently. Will retry next time
|
||||
// PushDiscoveryUnicastHints is called.
|
||||
}
|
||||
}
|
||||
|
||||
discovery.SetUnicastPeerAddresses(hints);
|
||||
discovery.SetUnicastPeerAddresses(hints);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
tag_name on the latest GitHub release; bump it on every public release. The
|
||||
AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version
|
||||
is what the About dialog and the updater both read. -->
|
||||
<Version>4.1</Version>
|
||||
<Version>4.2</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user