diff --git a/Sources/PlayerBridge.swift b/Sources/PlayerBridge.swift index 94cd570..88c20dd 100644 --- a/Sources/PlayerBridge.swift +++ b/Sources/PlayerBridge.swift @@ -122,6 +122,28 @@ final class PlayerBridge: ObservableObject { #if os(iOS) let hlsRecorder = HLSRecorder() let pipController = PiPController() + + // 诊断日志 + private var diagLogURL: URL? + private var diagLogHandle: FileHandle? + + private func startDiagnosticLog() { + let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + diagLogURL = docs.appendingPathComponent("miniplayer_diag.log") + // 清空旧日志 + try? "".write(to: diagLogURL!, atomically: true, encoding: .utf8) + diagLogHandle = try? FileHandle(forWritingTo: diagLogURL!) + diagLogHandle?.seekToEndOfFile() + diagLog("=== MiniPlayer diagnostic log started ===") + } + + func diagLog(_ msg: String) { + let line = "\(Date()): \(msg)\n" + print("[MiniPlayer] \(msg)") + if let data = line.data(using: .utf8) { + try? diagLogHandle?.write(contentsOf: data) + } + } #endif // MARK: - 初始化 @@ -132,6 +154,8 @@ final class PlayerBridge: ObservableObject { #elseif os(iOS) // iOS: 配置音频会话(支持后台播放 + PiP) configureAudioSession() + // 开启诊断日志文件 + startDiagnosticLog() #endif player.volume = volume @@ -242,7 +266,7 @@ final class PlayerBridge: ObservableObject { currentIndex = index let item = queue[index] - print("[MiniPlayer] playIndex(\(index)): \(item.url.absoluteString)") + diagLog("playIndex(\(index)): \(item.url.lastPathComponent)") // 本地文件诊断:检查文件是否存在和可读 if item.url.isFileURL { @@ -250,7 +274,7 @@ final class PlayerBridge: ObservableObject { let exists = FileManager.default.fileExists(atPath: path) let readable = FileManager.default.isReadableFile(atPath: path) let fileSize = (try? FileManager.default.attributesOfItem(atPath: path)[.size] as? Int) ?? 0 - print("[MiniPlayer] Local file check: exists=\(exists), readable=\(readable), size=\(fileSize), path=\(path)") + diagLog("Local file check: exists=\(exists) readable=\(readable) size=\(fileSize)") } // 先清除旧的 KVO,避免野指针 @@ -271,7 +295,7 @@ final class PlayerBridge: ObservableObject { player.replaceCurrentItem(with: playerItem) player.play() isPlaying = true - print("[MiniPlayer] player.play() called, rate=\(player.rate), timeControlStatus=\(player.timeControlStatus.rawValue)") + diagLog("play() called, rate=\(player.rate) timeControlStatus=\(player.timeControlStatus.rawValue)") cachedDuration = 0 progressRatio = 0 @@ -304,7 +328,7 @@ final class PlayerBridge: ObservableObject { // KVO 可能从非主线程回调,用 DispatchQueue.main.async itemStatusObserver = playerItem.observe(\.status, options: [.new]) { [weak self] pi, _ in - print("[MiniPlayer] KVO status changed: \(pi.status.rawValue), error=\(pi.error?.localizedDescription ?? "none")") + self?.diagLog("KVO status: \(pi.status.rawValue)") DispatchQueue.main.async { switch pi.status { case .readyToPlay: @@ -326,9 +350,7 @@ final class PlayerBridge: ObservableObject { let errMsg = err?.localizedDescription ?? "Unknown error" let errCode = (err as NSError?)?.code ?? 0 let isLocalFile = item.url.isFileURL - NSLog("[MiniPlayer] ❌ AVPlayerItem failed: error=%@, code=%d, domain=%@, localFile=%d, url=%@", - errMsg, errCode, (err as NSError?)?.domain ?? "none", isLocalFile ? 1 : 0, - item.url.absoluteString) + self?.diagLog("AVPlayerItem FAILED: \(errMsg) code=\(errCode) localFile=\(isLocalFile)") self?.playerStatus = .error(errMsg) self?.loadingTimer?.invalidate() self?.loadingTimer = nil @@ -574,8 +596,10 @@ final class PlayerBridge: ObservableObject { func addItem(url: URL, name: String, type: String) { let urlString = url.absoluteString + print("[MiniPlayer] addItem: name=\(name), type=\(type), scheme=\(url.scheme ?? "nil")") // 检查队列是否已有相同URL if queue.contains(where: { $0.url.absoluteString == urlString }) { + print("[MiniPlayer] addItem: already in queue, skipping") showToast("⚠️ \(name) already in queue") return } @@ -583,7 +607,11 @@ final class PlayerBridge: ObservableObject { let libItem = library.addItem(url: urlString, name: name, type: type) queue.append(MediaItem(id: UUID().uuidString, url: url, name: name, mediaType: type, libraryID: libItem.id)) - if queue.count == 1 { playIndex(0) } + print("[MiniPlayer] addItem: added to queue, count=\(queue.count)") + if queue.count == 1 { + print("[MiniPlayer] addItem: first item, calling playIndex(0)") + playIndex(0) + } } func removeItem(at index: Int) { @@ -729,8 +757,11 @@ final class PlayerBridge: ObservableObject { /// 处理 iOS 文件选择器结果 func handleFileImport(_ urls: [URL]) { + diagLog("handleFileImport: \(urls.count) URLs") for url in urls { + diagLog("processing: \(url.lastPathComponent)") let didStartAccessing = url.startAccessingSecurityScopedResource() + diagLog("startAccessingSecurityScopedResource: \(didStartAccessing)") defer { if didStartAccessing { url.stopAccessingSecurityScopedResource() } } @@ -747,13 +778,15 @@ final class PlayerBridge: ObservableObject { let ext = url.pathExtension let newURL = docsDir.appendingPathComponent("\(nameWithoutExt)_\(ts).\(ext)") try FileManager.default.copyItem(at: url, to: newURL) + diagLog("copied to: \(newURL.lastPathComponent)") addItem(url: newURL, name: url.lastPathComponent, type: "file") } else { try FileManager.default.copyItem(at: url, to: localURL) + diagLog("copied to: \(localURL.lastPathComponent)") addItem(url: localURL, name: url.lastPathComponent, type: "file") } } catch { - // 复制失败则直接用原始 URL(可能是 iCloud 文件等) + diagLog("copy failed: \(error.localizedDescription)") addItem(url: url, name: url.lastPathComponent, type: "file") } }