fix: add device diagnostic log (miniplayer_diag.log in Documents) to trace playback issues
This commit is contained in:
parent
37d7338823
commit
e81af97437
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user