// // LivePushReadinessViewModel.swift // suixinkan // // Created by Codex on 2026/6/25. // import AVFoundation import Foundation import Network /// 直播推流权限状态。 enum LivePushPermissionState: Equatable { case unknown case granted case denied var displayText: String { switch self { case .unknown: "未检查" case .granted: "已授权" case .denied: "未授权" } } } /// 直播推流网络状态。 enum LivePushNetworkState: Equatable { case unknown case unavailable case wifi case cellular case other var displayText: String { switch self { case .unknown: "检测中" case .unavailable: "网络不可用" case .wifi: "Wi-Fi" case .cellular: "蜂窝网络" case .other: "其他网络" } } } /// 推流 SDK 适配协议。后续接入真实 RTMP/RTC SDK 时替换默认实现。 protocol LivePushAdapter { var name: String { get } var isAvailable: Bool { get } /// prepare 业务逻辑。 func prepare(pushURL: URL) async throws /// start 业务逻辑。 func start() async throws /// stop 业务逻辑。 func stop() async throws /// dispose 业务逻辑。 func dispose() async } /// 权限提供者协议,隔离 AVFoundation 便于测试。 protocol LivePermissionProviding { /// camera权限相关逻辑。 func cameraPermission() async -> LivePushPermissionState /// microphone权限相关逻辑。 func microphonePermission() async -> LivePushPermissionState } /// 网络监听协议,隔离 NWPathMonitor 便于测试。 protocol LiveNetworkMonitoring: AnyObject { var currentState: LivePushNetworkState { get } /// start 业务逻辑。 func start(_ onChange: @escaping @Sendable (LivePushNetworkState) -> Void) /// stop 业务逻辑。 func stop() } /// LivePushReadiness错误类型定义。 enum LivePushReadinessError: LocalizedError, Equatable { case missingPushURL case invalidPushURL case permissionDenied case networkUnavailable case sdkUnavailable var errorDescription: String? { switch self { case .missingPushURL: "暂无推流地址" case .invalidPushURL: "推流地址格式无效" case .permissionDenied: "请先开启相机和麦克风权限" case .networkUnavailable: "当前网络不可用" case .sdkUnavailable: "当前版本未接入真推流 SDK" } } } /// UnsupportedLivePushAdapter,业务类型定义。 struct UnsupportedLivePushAdapter: LivePushAdapter { let name = "未接入推流 SDK" let isAvailable = false /// prepare 业务逻辑。 func prepare(pushURL: URL) async throws { throw LivePushReadinessError.sdkUnavailable } /// start 业务逻辑。 func start() async throws { throw LivePushReadinessError.sdkUnavailable } /// stop 业务逻辑。 func stop() async throws {} /// dispose 业务逻辑。 func dispose() async {} } /// SystemLivePermission能力提供者,封装外部依赖。 struct SystemLivePermissionProvider: LivePermissionProviding { /// camera权限相关逻辑。 func cameraPermission() async -> LivePushPermissionState { await permission(for: .video) } /// microphone权限相关逻辑。 func microphonePermission() async -> LivePushPermissionState { await permission(for: .audio) } /// permission 业务逻辑。 private func permission(for mediaType: AVMediaType) async -> LivePushPermissionState { switch AVCaptureDevice.authorizationStatus(for: mediaType) { case .authorized: return .granted case .notDetermined: let granted = await AVCaptureDevice.requestAccess(for: mediaType) return granted ? .granted : .denied case .denied, .restricted: return .denied @unknown default: return .denied } } } /// SystemLiveNetworkMonitor 类型定义。 final class SystemLiveNetworkMonitor: LiveNetworkMonitoring { var onChange: (() -> Void)? private let monitor = NWPathMonitor() private let queue = DispatchQueue(label: "com.suixinkan.live.network") private(set) var currentState: LivePushNetworkState = .unknown /// start 业务逻辑。 func start(_ onChange: @escaping @Sendable (LivePushNetworkState) -> Void) { monitor.pathUpdateHandler = { [weak self] path in let state = Self.state(from: path) self?.currentState = state onChange(state) } monitor.start(queue: queue) } /// stop 业务逻辑。 func stop() { monitor.cancel() } /// state 业务逻辑。 private static func state(from path: NWPath) -> LivePushNetworkState { guard path.status == .satisfied else { return .unavailable } if path.usesInterfaceType(.wifi) { return .wifi } if path.usesInterfaceType(.cellular) { return .cellular } return .other } } @MainActor /// 推流准备 ViewModel,检查权限、网络和默认 SDK 可用性。 final class LivePushReadinessViewModel { var onChange: (() -> Void)? var cameraPermission: LivePushPermissionState = .unknown { didSet { onChange?() } } var microphonePermission: LivePushPermissionState = .unknown { didSet { onChange?() } } var networkState: LivePushNetworkState = .unknown { didSet { onChange?() } } var prepared = false { didSet { onChange?() } } var running = false { didSet { onChange?() } } var errorMessage: String? { didSet { onChange?() } } let adapterName: String let sdkStatusText: String private let permissionProvider: any LivePermissionProviding private let networkMonitor: any LiveNetworkMonitoring private let adapter: any LivePushAdapter private var pushURL: URL? /// 初始化实例。 init( permissionProvider: any LivePermissionProviding = SystemLivePermissionProvider(), networkMonitor: any LiveNetworkMonitoring = SystemLiveNetworkMonitor(), adapter: any LivePushAdapter = UnsupportedLivePushAdapter() ) { self.permissionProvider = permissionProvider self.networkMonitor = networkMonitor self.adapter = adapter self.adapterName = adapter.name self.sdkStatusText = adapter.isAvailable ? "已接入" : "未接入真推流 SDK" self.networkState = networkMonitor.currentState } /// 配置展示内容。 func configure(pushURL rawValue: String) { let value = rawValue.liveTrimmed guard !value.isEmpty else { pushURL = nil errorMessage = LivePushReadinessError.missingPushURL.localizedDescription return } guard let url = URL(string: value), url.scheme?.isEmpty == false else { pushURL = nil errorMessage = LivePushReadinessError.invalidPushURL.localizedDescription return } pushURL = url errorMessage = nil } /// startMonitoring相关逻辑。 func startMonitoring() { networkState = networkMonitor.currentState networkMonitor.start { [weak self] state in Task { @MainActor in self?.networkState = state } } } /// stopMonitoring相关逻辑。 func stopMonitoring() { networkMonitor.stop() } /// refreshPermissions相关逻辑。 func refreshPermissions() async { async let camera = permissionProvider.cameraPermission() async let microphone = permissionProvider.microphonePermission() cameraPermission = await camera microphonePermission = await microphone } /// runDiagnostics相关逻辑。 func runDiagnostics() throws { do { try validateReadiness() } catch { errorMessage = error.localizedDescription throw error } guard adapter.isAvailable else { errorMessage = LivePushReadinessError.sdkUnavailable.localizedDescription throw LivePushReadinessError.sdkUnavailable } errorMessage = nil } /// prepare 业务逻辑。 func prepare() async throws { try validateReadiness() guard let pushURL else { throw LivePushReadinessError.missingPushURL } do { try await adapter.prepare(pushURL: pushURL) prepared = true errorMessage = nil } catch { errorMessage = error.localizedDescription throw error } } /// start推送相关逻辑。 func startPush() async throws { try validateReadiness() guard adapter.isAvailable else { errorMessage = LivePushReadinessError.sdkUnavailable.localizedDescription throw LivePushReadinessError.sdkUnavailable } try await adapter.start() running = true prepared = true errorMessage = nil } /// stop推送相关逻辑。 func stopPush() async { try? await adapter.stop() running = false } /// dispose 业务逻辑。 func dispose() async { stopMonitoring() await adapter.dispose() running = false prepared = false } /// 校验Readiness输入或状态。 private func validateReadiness() throws { guard pushURL != nil else { throw LivePushReadinessError.missingPushURL } guard cameraPermission != .denied, microphonePermission != .denied else { throw LivePushReadinessError.permissionDenied } guard networkState != .unavailable else { throw LivePushReadinessError.networkUnavailable } } }