Stabilize iOS reconnect and screen audio sessions
This commit is contained in:
@@ -18,9 +18,15 @@ internal sealed class BroadcastAudioPump : IAsyncDisposable
|
||||
private uint streamId;
|
||||
private bool active;
|
||||
private int generation;
|
||||
private ulong observedWrite;
|
||||
private ulong idleWrite;
|
||||
private bool idleWriteSeen;
|
||||
|
||||
internal event Action? Changed;
|
||||
internal bool IsActive => active;
|
||||
#if DEBUG
|
||||
internal bool IsMappingOpen => view is not null;
|
||||
#endif
|
||||
|
||||
internal void Start(VoiceCatClient owner)
|
||||
{
|
||||
@@ -37,8 +43,17 @@ internal sealed class BroadcastAudioPump : IAsyncDisposable
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try { DrainAsync(token).GetAwaiter().GetResult(); }
|
||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException or OperationCanceledException) { CloseMapping(); }
|
||||
if (!token.IsCancellationRequested) Thread.Sleep(5);
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested) { break; }
|
||||
catch (Exception exception)
|
||||
{
|
||||
// A rejected screen stream is a session error, not an unhandled exception on
|
||||
// this background Thread. Keep the app and microphone call alive.
|
||||
Console.Error.WriteLine($"VoiceCat screen audio pump failed: {exception}");
|
||||
CloseMapping(); StopStream(); SetActive(false);
|
||||
if (!token.IsCancellationRequested) Thread.Sleep(1000);
|
||||
}
|
||||
// The shared file must not remain mapped while the app is idle or suspending.
|
||||
if (!token.IsCancellationRequested) Thread.Sleep(view is null ? 100 : 5);
|
||||
}
|
||||
}
|
||||
finally { CloseMapping(); }
|
||||
@@ -49,11 +64,18 @@ internal sealed class BroadcastAudioPump : IAsyncDisposable
|
||||
if (!OpenMapping()) return;
|
||||
MemoryMappedViewAccessor pages = view!;
|
||||
bool active = pages.ReadUInt32(16) != 0;
|
||||
if (!active) { StopStream(); SetActive(false); return; }
|
||||
if (!active) { StopStream(); SetActive(false); CloseMapping(); return; }
|
||||
VoiceCatClient owner = client ?? throw new IOException("Client disconnected.");
|
||||
if (streamId == 0)
|
||||
{
|
||||
// A ring left marked active by a killed extension must not start a phantom stream.
|
||||
// Wait for a fresh producer write after this connection opened the mapping.
|
||||
ulong currentWrite = pages.ReadUInt64(24);
|
||||
if (currentWrite == observedWrite) return;
|
||||
observedWrite = currentWrite;
|
||||
int startGeneration = Volatile.Read(ref generation);
|
||||
VoiceSubscriptionResult subscription = await owner.SubscribeVoiceAsync(cancellationToken: token).ConfigureAwait(false);
|
||||
if (!subscription.Ok) throw new InvalidOperationException(subscription.Error);
|
||||
StreamInfo stream = await owner.StartStreamAsync(StreamKind.StreamScreenAudio, "Screen audio", 2, token).ConfigureAwait(false);
|
||||
if (startGeneration != Volatile.Read(ref generation) || pages.ReadUInt32(16) == 0 || !ReferenceEquals(client, owner))
|
||||
{
|
||||
@@ -77,11 +99,10 @@ internal sealed class BroadcastAudioPump : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// The producer opens the ring with O_CREAT and never replaces it, so one mapping covers the
|
||||
// pump's whole life. Creating and disposing a mapping (plus its container lookup and path
|
||||
// strings) every 5 ms tick allocated steadily at 200 Hz and churned the GC under long calls;
|
||||
// the mapping is now built on the ring's transitions and reused until the file disappears or
|
||||
// a drain fails.
|
||||
// Keep a mapping only while a broadcast is active. An inactive mapping holds an open handle
|
||||
// into the shared App Group container through suspension, which iOS may terminate as a shared
|
||||
// file lock (0xdead10cc). Poll the small header with a short-lived read when idle, then map
|
||||
// once for the active broadcast's high-rate drain.
|
||||
private bool OpenMapping()
|
||||
{
|
||||
path ??= ResolvePath();
|
||||
@@ -92,7 +113,28 @@ internal sealed class BroadcastAudioPump : IAsyncDisposable
|
||||
if (present) return true;
|
||||
CloseMapping(); return false;
|
||||
}
|
||||
if (!present) return false;
|
||||
if (!present) { idleWriteSeen = false; return false; }
|
||||
using (var file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
|
||||
{
|
||||
if (file.Length < Header) { idleWriteSeen = false; return false; }
|
||||
Span<byte> header = stackalloc byte[32];
|
||||
file.ReadExactly(header);
|
||||
if (System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(header) != Magic ||
|
||||
System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(header[4..]) != Version ||
|
||||
System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(header[16..]) == 0)
|
||||
{
|
||||
idleWriteSeen = false;
|
||||
return false;
|
||||
}
|
||||
ulong currentWrite = System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(header[24..]);
|
||||
if (!idleWriteSeen)
|
||||
{
|
||||
idleWrite = currentWrite;
|
||||
idleWriteSeen = true;
|
||||
return false;
|
||||
}
|
||||
if (currentWrite == idleWrite) return false;
|
||||
}
|
||||
MemoryMappedFile candidate = MemoryMappedFile.CreateFromFile(path, FileMode.Open, null, Header + Capacity * sizeof(short), MemoryMappedFileAccess.ReadWrite);
|
||||
try
|
||||
{
|
||||
@@ -101,7 +143,7 @@ internal sealed class BroadcastAudioPump : IAsyncDisposable
|
||||
{
|
||||
pages.Dispose(); throw new InvalidDataException("Unsupported broadcast ring.");
|
||||
}
|
||||
map = candidate; view = pages; return true;
|
||||
map = candidate; view = pages; observedWrite = idleWrite; return true;
|
||||
}
|
||||
catch { candidate.Dispose(); throw; }
|
||||
}
|
||||
@@ -116,12 +158,14 @@ internal sealed class BroadcastAudioPump : IAsyncDisposable
|
||||
{
|
||||
view?.Dispose(); view = null;
|
||||
map?.Dispose(); map = null;
|
||||
observedWrite = 0;
|
||||
idleWriteSeen = false;
|
||||
}
|
||||
|
||||
private void StopStream()
|
||||
{
|
||||
uint id = streamId; streamId = 0; if (id == 0 || client?.State != ClientConnectionState.Connected) return;
|
||||
try { client.StopStream(id); } catch (Exception exception) when (exception is IOException or InvalidOperationException) { }
|
||||
try { client.StopStream(id); } catch (Exception exception) when (exception is IOException or InvalidOperationException or ObjectDisposedException) { }
|
||||
}
|
||||
|
||||
internal void RequestStop() { Interlocked.Increment(ref generation); SetActive(false); }
|
||||
|
||||
Reference in New Issue
Block a user