diff --git a/meetingnotes/Managers/AudioManager.swift b/meetingnotes/Managers/AudioManager.swift index 7dde7d6..5549ae4 100644 --- a/meetingnotes/Managers/AudioManager.swift +++ b/meetingnotes/Managers/AudioManager.swift @@ -29,6 +29,9 @@ class AudioManager: NSObject, ObservableObject { // Add current interim transcripts per source private var currentInterim: [AudioSource: String] = [.mic: "", .system: ""] + + // Add ping timers to keep WebSocket connections alive + private var pingTimers: [AudioSource: Timer] = [:] override init() { super.init() @@ -87,6 +90,10 @@ class AudioManager: NSObject, ObservableObject { systemSocketTask?.cancel(with: .normalClosure, reason: nil) systemSocketTask = nil + // Invalidate ping timers + pingTimers.values.forEach { $0.invalidate() } + pingTimers.removeAll() + // Reset state // (isRecording already cleared in stopRecording) @@ -268,6 +275,10 @@ class AudioManager: NSObject, ObservableObject { systemSocketTask?.cancel(with: .normalClosure, reason: nil) systemSocketTask = nil + // Invalidate ping timers + pingTimers.values.forEach { $0.invalidate() } + pingTimers.removeAll() + print("Recording stopped") } @@ -321,6 +332,22 @@ class AudioManager: NSObject, ObservableObject { // Add connection monitoring task.resume() + // Set up ping timer to keep connection alive + pingTimers[source]?.invalidate() + let pingTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in + guard let self = self else { return } + let task = source == .mic ? self.micSocketTask : self.systemSocketTask + guard let socket = task, socket.state == .running else { return } + socket.sendPing { error in + if let error = error { + print("❌ Ping failed for \(source): \(error)") + } else { + print("🏓 Ping sent for \(source)") + } + } + } + pingTimers[source] = pingTimer + let thisSession = sessionID // Monitor connection state (ignore if session changed or recording stopped) DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self, weak task] in diff --git a/meetingnotes/Services/ErrorHandler.swift b/meetingnotes/Services/ErrorHandler.swift index f1e5895..58b4e71 100644 --- a/meetingnotes/Services/ErrorHandler.swift +++ b/meetingnotes/Services/ErrorHandler.swift @@ -98,6 +98,16 @@ class ErrorHandler { } } + // Handle POSIX socket errors (e.g., "Socket is not connected") + if let nsError = error as NSError?, nsError.domain == NSPOSIXErrorDomain { + switch nsError.code { + case 57: // ENOTCONN - Socket is not connected + return true + default: + return false + } + } + // WebSocket close codes if let closeCode = (error as NSError?)?.userInfo["closeCode"] as? Int { return closeCode < 4000 // Only retry for non-API errors