import Foundation struct ServerConfig: Equatable { let baseURL: URL let livekitURL: URL enum ConfigError: LocalizedError, Equatable { case notHTTPS(String) case invalidURL(String) var errorDescription: String? { switch self { case .notHTTPS(let raw): return "Server URL must use https: \(raw)" case .invalidURL(let raw): return "Invalid server URL: \(raw)" } } } static func parse(_ raw: String) -> Result { let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return .failure(.invalidURL(raw)) } var candidate = trimmed if !candidate.contains("://") { candidate = "https://" + candidate } guard let components = URLComponents(string: candidate), let scheme = components.scheme?.lowercased(), let host = components.host, !host.isEmpty else { return .failure(.invalidURL(raw)) } if scheme != "https" { return .failure(.notHTTPS(raw)) } var c = components c.path = "" c.fragment = nil guard let base = c.url else { return .failure(.invalidURL(raw)) } // LAN deployment: LiveKit is reachable directly on :7880 (plain ws). // Real domains use wss via same origin. The SDK derives /rtc from the base. let livekit: URL if Self.isPrivateIP(host) { livekit = URL(string: "ws://\(host):7880")! } else { let portSuffix = components.port.map { ":\($0)" } ?? "" livekit = URL(string: "wss://\(host)\(portSuffix)/livekit")! } return .success(ServerConfig(baseURL: base, livekitURL: livekit)) } static func isPrivateIP(_ host: String) -> Bool { let parts = host.split(separator: ".").compactMap { UInt8($0) } guard parts.count == 4 else { return false } return parts[0] == 10 || (parts[0] == 172 && (16...31).contains(parts[1])) || (parts[0] == 192 && parts[1] == 168) } var host: String { baseURL.host ?? "" } }