#if os(iOS) import Foundation import AVFoundation import UIKit /// iOS 录制器 — 录制本地文件,流媒体暂不支持(CBS 有 DRM 保护,需 ffmpeg iOS) @MainActor final class HLSRecorder: ObservableObject { @Published var isRecording = false @Published var durationText = "00:00" var onRecordingSaved: ((URL) -> Void)? var onError: ((String) -> Void)? private var recordTask: Task? private var startTime: Date? private var timer: Timer? func startRecording(url: URL, player: AVPlayer) { guard !isRecording else { return } isRecording = true startTime = Date() timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in Task { @MainActor in guard let self, let start = self.startTime else { return } let elapsed = Int(Date().timeIntervalSince(start)) self.durationText = String(format: "%02d:%02d:%02d", elapsed / 3600, (elapsed % 3600) / 60, elapsed % 60) } } recordTask = Task { [weak self] in guard let self else { return } do { let outputURL: URL if url.isFileURL { outputURL = try await self.copyLocalFile(url: url) } else { // 流媒体:当前 iOS 不支持直接录制 HLS 流 // CBS 等有 DRM/CDN 保护,需 ffmpeg iOS 静态库 // 先用 URLSession 下载尝试(仅对无保护的 HTTP 流有效) outputURL = try await self.downloadFile(url: url) } let attrs = try? FileManager.default.attributesOfItem(atPath: outputURL.path) let size = (attrs?[.size] as? Int64) ?? 0 NSLog("[MiniPlayer] Recording complete: %@ (%lld bytes)", outputURL.path, size) await MainActor.run { self.isRecording = false self.timer?.invalidate() if size > 1024 { self.onRecordingSaved?(outputURL) } else { self.onError?("Recording too small (\(size) bytes) — stream may be DRM-protected. Try recording a local file.") try? FileManager.default.removeItem(at: outputURL) } } } catch { NSLog("[MiniPlayer] Recording error: %@", error.localizedDescription) await MainActor.run { self.isRecording = false self.timer?.invalidate() self.onError?(error.localizedDescription) } } } } func stopRecording() { guard isRecording else { return } // 对于本地文件复制,立即完成;对于网络下载,取消任务 recordTask?.cancel() isRecording = false timer?.invalidate() } // MARK: - 本地文件复制 private func copyLocalFile(url: URL) async throws -> URL { let ext = url.pathExtension.isEmpty ? "mp4" : url.pathExtension let outputURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] .appendingPathComponent("MiniPlayer_Recording_\(Int(Date().timeIntervalSince1970)).\(ext)") if FileManager.default.fileExists(atPath: outputURL.path) { try FileManager.default.removeItem(at: outputURL) } try FileManager.default.copyItem(at: url, to: outputURL) return outputURL } // MARK: - 网络流下载(仅对无保护 HTTP 流有效) private func downloadFile(url: URL) async throws -> URL { let ext = url.pathExtension.isEmpty ? "mp4" : url.pathExtension let outputURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] .appendingPathComponent("MiniPlayer_Recording_\(Int(Date().timeIntervalSince1970)).\(ext)") if FileManager.default.fileExists(atPath: outputURL.path) { try FileManager.default.removeItem(at: outputURL) } let (tempURL, _) = try await URLSession.shared.download(from: url) try FileManager.default.moveItem(at: tempURL, to: outputURL) return outputURL } } #endif