fix: add device diagnostic log (miniplayer_diag.log in Documents) to trace playback issues

This commit is contained in:
yumoqing 2026-07-02 22:58:57 +08:00
parent 37d7338823
commit e81af97437

View File

@ -122,6 +122,28 @@ final class PlayerBridge: ObservableObject {
#if os(iOS) #if os(iOS)
let hlsRecorder = HLSRecorder() let hlsRecorder = HLSRecorder()
let pipController = PiPController() 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 #endif
// MARK: - // MARK: -
@ -132,6 +154,8 @@ final class PlayerBridge: ObservableObject {
#elseif os(iOS) #elseif os(iOS)
// iOS: + PiP // iOS: + PiP
configureAudioSession() configureAudioSession()
//
startDiagnosticLog()
#endif #endif
player.volume = volume player.volume = volume
@ -242,7 +266,7 @@ final class PlayerBridge: ObservableObject {
currentIndex = index currentIndex = index
let item = queue[index] let item = queue[index]
print("[MiniPlayer] playIndex(\(index)): \(item.url.absoluteString)") diagLog("playIndex(\(index)): \(item.url.lastPathComponent)")
// //
if item.url.isFileURL { if item.url.isFileURL {
@ -250,7 +274,7 @@ final class PlayerBridge: ObservableObject {
let exists = FileManager.default.fileExists(atPath: path) let exists = FileManager.default.fileExists(atPath: path)
let readable = FileManager.default.isReadableFile(atPath: path) let readable = FileManager.default.isReadableFile(atPath: path)
let fileSize = (try? FileManager.default.attributesOfItem(atPath: path)[.size] as? Int) ?? 0 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 // KVO
@ -271,7 +295,7 @@ final class PlayerBridge: ObservableObject {
player.replaceCurrentItem(with: playerItem) player.replaceCurrentItem(with: playerItem)
player.play() player.play()
isPlaying = true 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 cachedDuration = 0
progressRatio = 0 progressRatio = 0
@ -304,7 +328,7 @@ final class PlayerBridge: ObservableObject {
// KVO 线 DispatchQueue.main.async // KVO 线 DispatchQueue.main.async
itemStatusObserver = playerItem.observe(\.status, options: [.new]) { [weak self] pi, _ in 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 { DispatchQueue.main.async {
switch pi.status { switch pi.status {
case .readyToPlay: case .readyToPlay:
@ -326,9 +350,7 @@ final class PlayerBridge: ObservableObject {
let errMsg = err?.localizedDescription ?? "Unknown error" let errMsg = err?.localizedDescription ?? "Unknown error"
let errCode = (err as NSError?)?.code ?? 0 let errCode = (err as NSError?)?.code ?? 0
let isLocalFile = item.url.isFileURL let isLocalFile = item.url.isFileURL
NSLog("[MiniPlayer] ❌ AVPlayerItem failed: error=%@, code=%d, domain=%@, localFile=%d, url=%@", self?.diagLog("AVPlayerItem FAILED: \(errMsg) code=\(errCode) localFile=\(isLocalFile)")
errMsg, errCode, (err as NSError?)?.domain ?? "none", isLocalFile ? 1 : 0,
item.url.absoluteString)
self?.playerStatus = .error(errMsg) self?.playerStatus = .error(errMsg)
self?.loadingTimer?.invalidate() self?.loadingTimer?.invalidate()
self?.loadingTimer = nil self?.loadingTimer = nil
@ -574,8 +596,10 @@ final class PlayerBridge: ObservableObject {
func addItem(url: URL, name: String, type: String) { func addItem(url: URL, name: String, type: String) {
let urlString = url.absoluteString let urlString = url.absoluteString
print("[MiniPlayer] addItem: name=\(name), type=\(type), scheme=\(url.scheme ?? "nil")")
// URL // URL
if queue.contains(where: { $0.url.absoluteString == urlString }) { if queue.contains(where: { $0.url.absoluteString == urlString }) {
print("[MiniPlayer] addItem: already in queue, skipping")
showToast("⚠️ \(name) already in queue") showToast("⚠️ \(name) already in queue")
return return
} }
@ -583,7 +607,11 @@ final class PlayerBridge: ObservableObject {
let libItem = library.addItem(url: urlString, name: name, type: type) let libItem = library.addItem(url: urlString, name: name, type: type)
queue.append(MediaItem(id: UUID().uuidString, url: url, name: name, queue.append(MediaItem(id: UUID().uuidString, url: url, name: name,
mediaType: type, libraryID: libItem.id)) 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) { func removeItem(at index: Int) {
@ -729,8 +757,11 @@ final class PlayerBridge: ObservableObject {
/// iOS /// iOS
func handleFileImport(_ urls: [URL]) { func handleFileImport(_ urls: [URL]) {
diagLog("handleFileImport: \(urls.count) URLs")
for url in urls { for url in urls {
diagLog("processing: \(url.lastPathComponent)")
let didStartAccessing = url.startAccessingSecurityScopedResource() let didStartAccessing = url.startAccessingSecurityScopedResource()
diagLog("startAccessingSecurityScopedResource: \(didStartAccessing)")
defer { defer {
if didStartAccessing { url.stopAccessingSecurityScopedResource() } if didStartAccessing { url.stopAccessingSecurityScopedResource() }
} }
@ -747,13 +778,15 @@ final class PlayerBridge: ObservableObject {
let ext = url.pathExtension let ext = url.pathExtension
let newURL = docsDir.appendingPathComponent("\(nameWithoutExt)_\(ts).\(ext)") let newURL = docsDir.appendingPathComponent("\(nameWithoutExt)_\(ts).\(ext)")
try FileManager.default.copyItem(at: url, to: newURL) try FileManager.default.copyItem(at: url, to: newURL)
diagLog("copied to: \(newURL.lastPathComponent)")
addItem(url: newURL, name: url.lastPathComponent, type: "file") addItem(url: newURL, name: url.lastPathComponent, type: "file")
} else { } else {
try FileManager.default.copyItem(at: url, to: localURL) try FileManager.default.copyItem(at: url, to: localURL)
diagLog("copied to: \(localURL.lastPathComponent)")
addItem(url: localURL, name: url.lastPathComponent, type: "file") addItem(url: localURL, name: url.lastPathComponent, type: "file")
} }
} catch { } catch {
// URL iCloud diagLog("copy failed: \(error.localizedDescription)")
addItem(url: url, name: url.lastPathComponent, type: "file") addItem(url: url, name: url.lastPathComponent, type: "file")
} }
} }