import Foundation /// 录制进度回调 public protocol StreamRecorderDelegate: AnyObject { func recorderDidStart(_ recorder: StreamRecorderEngine) func recorder(_ recorder: StreamRecorderEngine, didUpdateProgress progress: RecordingProgress) func recorder(_ recorder: StreamRecorderEngine, didFinishWithResult result: RecordingResult) func recorder(_ recorder: StreamRecorderEngine, didFailWithError error: Error) } /// 录制进度 public struct RecordingProgress { public let currentTime: Double // 当前已录制时长(秒) public let totalDuration: Double // 总时长(秒) public let bytesWritten: Int64 // 已写入字节数 public let bitrate: Double // 当前码率 (kbps) public var percentage: Double { guard totalDuration > 0 else { return 0 } return min(currentTime / totalDuration * 100, 100) } } /// 录制结果 public struct RecordingResult { public let outputPath: String public let duration: Double public let fileSize: Int64 public let hasVideo: Bool public let hasAudio: Bool public let videoInfo: VideoInfo? public let audioInfo: AudioInfo? public let audioLevel: AudioLevel? } public struct VideoInfo { public let codec: String public let width: Int public let height: Int public let fps: Double public let pixelFormat: String } public struct AudioInfo { public let codec: String public let sampleRate: Int public let channels: Int } public struct AudioLevel { public let meanVolume: Double // dB public let maxVolume: Double // dB public let isSilent: Bool } public enum RecordingError: Error, LocalizedError { case invalidURL case noFFmpeg case noMediaTracks case processFailed(Int) case outputNotFound case unknown(String) public var errorDescription: String? { switch self { case .invalidURL: return "无效的 URL" case .noFFmpeg: return "未找到 ffmpeg,请安装: brew install ffmpeg" case .noMediaTracks: return "未找到任何媒体轨道" case .processFailed(let code): return "录制失败 (exit code: \(code))" case .outputNotFound: return "输出文件不存在" case .unknown(let msg): return msg } } } /// 录制引擎协议 public protocol StreamRecorderEngine: AnyObject { var delegate: StreamRecorderDelegate? { get set } var isRecording: Bool { get } func start(url: URL, outputPath: String, duration: Double) async throws func stop() }