#if DEBUG using System.Buffers.Binary; using System.Runtime.InteropServices; using Foundation; using UIKit; using VoiceCat.Core; namespace VoiceCat.iOS; // An opt-in simulator gate for the real UIKit model and audio graph. Local development only. internal static class SimulatorSmoke { internal static void RunIfRequested() { string? endpoint = Environment.GetEnvironmentVariable("VOICECAT_SIM_SMOKE"); if (endpoint is null || !RuntimeInformation.RuntimeIdentifier.StartsWith("iossimulator", StringComparison.Ordinal)) return; UIApplication.SharedApplication.BeginInvokeOnMainThread(async () => { try { string[] parts = endpoint.Split(':', 3); Guid id = Guid.Parse("959da92f-f088-452d-a870-4c15007a6fd3"); ServerProfile guest = ServerProfile.Create(parts[0], ushort.Parse(parts[1]), ServerAuthentication.Guest, nickname: "smoke", id: id); ServerProfile profile = ServerProfile.Create(parts[0], ushort.Parse(parts[1]), ServerAuthentication.Account, "smoke", id: id); AppModel model = AppModel.Shared; bool share = Environment.GetEnvironmentVariable("VOICECAT_SIM_SMOKE_MODE") == "share"; string? ring = share ? PrepareRing() : null; model.IdentityRequested += _ => model.ResolveIdentity(true); if (Environment.GetEnvironmentVariable("VOICECAT_SIM_SMOKE_MODE") != "resume") { model.UpsertProfile(guest, null); model.UpsertProfile(profile, parts[2]); } model.Load(); if (Environment.GetEnvironmentVariable("VOICECAT_SIM_SMOKE_MODE") == "stereo") IosAudioRouter.Shared.SelectPreset(IosAudioPreset.StereoMicrophone); profile = model.Profiles.Single(value => value.Id == id); Console.Error.WriteLine("VC_SIM phase=connect"); await model.ConnectAsync(profile); Console.Error.WriteLine($"VC_SIM phase=connected audioRunning={IosAudioEngine.Shared.IsRunning}"); Require(model.IsConnected, "Account login did not connect."); if (ring is not null) { // Start a synthetic producer while the microphone is off. This exercises // screen-only subscription and the real BroadcastChanged audio recovery. await Task.Delay(300); Require(!model.ScreenRingMapped && !model.ScreenSharing, "An unchanged ring from a previous broadcast remained mapped."); WriteRing(ring, active: true, write: 3840); 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.ScreenSharing, "Screen stream did not stop."); Require(!model.ScreenRingMapped, "The stopped screen ring remained mapped."); Console.Error.WriteLine("VC_SIM phase=screenSharePassed"); } await model.ToggleVoiceAsync(); Console.Error.WriteLine($"VC_SIM phase=joined audioRunning={IosAudioEngine.Shared.IsRunning}"); await Task.Delay(2500); Require(model.VoiceJoined && IosAudioEngine.Shared.IsRunning, "The first voice graph stopped."); long beforeShare = IosAudioEngine.Shared.RenderCallbacks; IosAudioRouter.Shared.Recover("simulated screen sharing start", force: false); await Task.Delay(1000); Require(IosAudioEngine.Shared.IsRunning && IosAudioEngine.Shared.RenderCallbacks > beforeShare, "Screen sharing recovery stopped the live audio graph."); if (ring is not null) { WriteRing(ring, active: true, write: 3840); await Task.Delay(300); WriteRing(ring, active: true, write: 5760); await WaitUntil(() => model.ScreenSharing, "Screen stream did not start during a voice call."); Require(IosAudioEngine.Shared.IsRunning, "Starting screen sharing stopped the voice graph."); } await model.ToggleVoiceAsync(); if (ring is not null) { Require(model.ScreenSharing, "Leaving the microphone call unsubscribed active screen audio."); WriteRing(ring, active: false, write: 5760); await WaitUntil(() => !model.ScreenSharing, "The second screen stream did not stop."); } await model.DisconnectAsync(); await Task.Delay(3000); model.Load(); profile = model.Profiles.Single(value => value.Id == id); Console.Error.WriteLine("VC_SIM phase=reconnect"); await model.ConnectAsync(profile); Console.Error.WriteLine($"VC_SIM phase=reconnected audioRunning={IosAudioEngine.Shared.IsRunning}"); Require(model.IsConnected, "Saved account login did not reconnect."); await Task.Delay(2500); Console.Error.WriteLine($"VC_SIM phase=reconnectedSettled audioRunning={IosAudioEngine.Shared.IsRunning}"); Require(IosAudioEngine.Shared.IsRunning, "The reconnect audio graph stopped."); await model.ToggleVoiceAsync(); Console.Error.WriteLine($"VC_SIM phase=rejoined audioRunning={IosAudioEngine.Shared.IsRunning}"); await Task.Delay(2500); Console.Error.WriteLine($"VC_SIM phase=rejoinedSettled audioRunning={IosAudioEngine.Shared.IsRunning}"); Require(model.VoiceJoined && IosAudioEngine.Shared.IsRunning, "The rejoined voice graph stopped."); await model.DisconnectAsync(); Console.Error.WriteLine("VC_SIM phase=passed"); } catch (Exception exception) { Console.Error.WriteLine($"VC_SIM phase=failed exception={exception}"); } }); } private static void Require(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); } private static string PrepareRing() { string root = NSFileManager.DefaultManager.GetContainerUrl(IosConstants.AppGroup)?.Path ?? throw new InvalidOperationException("Simulator App Group is unavailable."); string path = Path.Combine(root, "voicecat", "broadcast_audio.ring"); Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var file = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); file.SetLength(64 + 96_000 * sizeof(short)); byte[] header = new byte[20]; BinaryPrimitives.WriteUInt32LittleEndian(header, 0x56434252); BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(4), 1); file.Write(header); file.Dispose(); WriteRing(path, active: true, write: 1920); return path; } private static void WriteRing(string path, bool active, ulong write) { 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); BinaryPrimitives.WriteUInt32LittleEndian(value, active ? 1u : 0u); file.Position = 16; file.Write(value, 0, 4); } private static async Task WaitUntil(Func condition, string error) { for (int attempt = 0; attempt < 30; attempt++) { if (condition()) return; await Task.Delay(100); } throw new InvalidOperationException(error); } } #endif