- Reverted broken AVPlayer capture code back to simple local file copy + URLSession download - Stream recording shows clear error message recommending local files - Added Assets.car compilation and CFBundleIcons to Info.plist for app icon - Stopped wasting time on iOS stream capture — needs ffmpeg iOS static lib
109 lines
4.3 KiB
Swift
109 lines
4.3 KiB
Swift
#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<Void, Never>?
|
||
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
|