Preserve iOS screen audio during stream startup
Build and test / test (macos-latest) (push) Canceled after 0s
Build and test / test (ubuntu-24.04) (push) Canceled after 0s
Build and test / test (windows-latest) (push) Canceled after 0s
Build and test / apple-client (push) Canceled after 0s

This commit is contained in:
2026-09-27 17:56:18 +02:00
parent 3dbe763598
commit 4bad264605
6 changed files with 65 additions and 15 deletions
+1
View File
@@ -53,6 +53,7 @@ internal sealed class AppModel
internal bool ScreenSharing => broadcast?.IsActive == true;
#if DEBUG
internal bool ScreenRingMapped => broadcast?.IsMappingOpen == true;
internal long ScreenEncodedPackets => broadcast?.EncodedPackets ?? 0;
#endif
internal bool IsBackgrounded => backgrounded;
internal float MicrophoneLevel { get; private set; }
@@ -26,6 +26,7 @@ internal sealed class BroadcastAudioPump : IAsyncDisposable
internal bool IsActive => active;
#if DEBUG
internal bool IsMappingOpen => view is not null;
internal long EncodedPackets => streamId == 0 ? 0 : client?.Audio.GetLocalDiagnostics(streamId).EncodedPackets ?? 0;
#endif
internal void Start(VoiceCatClient owner)
@@ -82,7 +83,13 @@ internal sealed class BroadcastAudioPump : IAsyncDisposable
try { owner.StopStream(stream.StreamId); } catch (Exception exception) when (exception is IOException or InvalidOperationException) { }
return;
}
streamId = stream.StreamId; pages.Write(32, pages.ReadUInt64(24)); SetActive(true);
// Keep the newest 120 ms captured during stream negotiation. This covers the
// largest codec frame plus the input buffer target; dropping through the latest
// write loses short sounds before the first drain can feed them.
ulong latestWrite = pages.ReadUInt64(24);
ulong earliest = latestWrite > (ulong)(Frame * 12) ? latestWrite - (ulong)(Frame * 12) : 0;
pages.Write(32, Math.Max(pages.ReadUInt64(32), earliest));
streamId = stream.StreamId; SetActive(true);
}
ulong write = pages.ReadUInt64(24), read = pages.ReadUInt64(32);
if (write - read > Capacity) read = write - Capacity;
+43 -8
View File
@@ -46,10 +46,20 @@ internal static class SimulatorSmoke
await Task.Delay(300);
Require(!model.ScreenRingMapped && !model.ScreenSharing,
"An unchanged ring from a previous broadcast remained mapped.");
WriteRing(ring, active: true, write: 3840);
for (int frame = 0; frame < 6; frame++) WriteScreenPcm(ring);
await WaitUntil(() => model.ScreenSharing, "Screen stream did not start.");
Require(IosAudioEngine.Shared.IsRunning, "Screen sharing stopped the audio graph.");
WriteRing(ring, active: false, write: 3840);
await WaitUntil(() => model.ScreenEncodedPackets > 0,
"Screen PCM captured before stream negotiation was discarded.");
long encodedBefore = model.ScreenEncodedPackets;
for (int frame = 0; frame < 12; frame++)
{
WriteScreenPcm(ring);
await Task.Delay(20);
}
await WaitUntil(() => model.ScreenEncodedPackets > encodedBefore,
"Screen PCM did not reach the encoder while the microphone was off.");
WriteRing(ring, active: false);
await WaitUntil(() => !model.ScreenSharing, "Screen stream did not stop.");
Require(!model.ScreenRingMapped, "The stopped screen ring remained mapped.");
Console.Error.WriteLine("VC_SIM phase=screenSharePassed");
@@ -65,9 +75,9 @@ internal static class SimulatorSmoke
"Screen sharing recovery stopped the live audio graph.");
if (ring is not null)
{
WriteRing(ring, active: true, write: 3840);
WriteRing(ring, active: true);
await Task.Delay(300);
WriteRing(ring, active: true, write: 5760);
WriteScreenPcm(ring);
await WaitUntil(() => model.ScreenSharing, "Screen stream did not start during a voice call.");
Require(IosAudioEngine.Shared.IsRunning, "Starting screen sharing stopped the voice graph.");
}
@@ -75,7 +85,7 @@ internal static class SimulatorSmoke
if (ring is not null)
{
Require(model.ScreenSharing, "Leaving the microphone call unsubscribed active screen audio.");
WriteRing(ring, active: false, write: 5760);
WriteRing(ring, active: false);
await WaitUntil(() => !model.ScreenSharing, "The second screen stream did not stop.");
}
await model.DisconnectAsync();
@@ -123,16 +133,41 @@ internal static class SimulatorSmoke
return path;
}
private static void WriteRing(string path, bool active, ulong write)
private static void WriteRing(string path, bool active, ulong? write = null)
{
using var file = new FileStream(path, FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
byte[] value = new byte[8];
BinaryPrimitives.WriteUInt64LittleEndian(value, write);
file.Position = 24; file.Write(value);
if (write is { } position)
{
BinaryPrimitives.WriteUInt64LittleEndian(value, position);
file.Position = 24; file.Write(value);
}
BinaryPrimitives.WriteUInt32LittleEndian(value, active ? 1u : 0u);
file.Position = 16; file.Write(value, 0, 4);
}
private static void WriteScreenPcm(string path)
{
using var file = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
Span<byte> index = stackalloc byte[8];
file.Position = 24; file.ReadExactly(index);
ulong write = BinaryPrimitives.ReadUInt64LittleEndian(index);
byte[] pcm = new byte[960 * 2 * sizeof(short)];
for (int sample = 0; sample < pcm.Length / sizeof(short); sample++)
BinaryPrimitives.WriteInt16LittleEndian(pcm.AsSpan(sample * sizeof(short)),
(short)(sample % 64 < 32 ? 6000 : -6000));
int start = (int)(write % 96_000), first = Math.Min(pcm.Length / sizeof(short), 96_000 - start);
file.Position = 64 + start * sizeof(short);
file.Write(pcm.AsSpan(0, first * sizeof(short)));
if (first < pcm.Length / sizeof(short))
{
file.Position = 64;
file.Write(pcm.AsSpan(first * sizeof(short)));
}
BinaryPrimitives.WriteUInt64LittleEndian(index, write + (ulong)(pcm.Length / sizeof(short)));
file.Position = 24; file.Write(index);
}
private static async Task WaitUntil(Func<bool> condition, string error)
{
for (int attempt = 0; attempt < 30; attempt++)