mirror of
https://github.com/yattee/yattee.git
synced 2026-02-21 02:09:46 +00:00
Yattee v2 rewrite
This commit is contained in:
290
Yattee/Services/Player/MPV/MPVLogging.swift
Normal file
290
Yattee/Services/Player/MPV/MPVLogging.swift
Normal file
@@ -0,0 +1,290 @@
|
||||
//
|
||||
// MPVLogging.swift
|
||||
// Yattee
|
||||
//
|
||||
// Centralized MPV rendering diagnostic logging.
|
||||
// Logs to Console (print) AND LoggingService for persistence.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if os(iOS) || os(tvOS)
|
||||
import OpenGLES
|
||||
#elseif os(macOS)
|
||||
import OpenGL
|
||||
#endif
|
||||
|
||||
/// Centralized MPV rendering diagnostic logging.
|
||||
/// Use this to diagnose rare rendering issues (black/green screen while audio plays).
|
||||
enum MPVLogging {
|
||||
// MARK: - Setting Check
|
||||
|
||||
/// Thread-safe cached check for verbose logging setting.
|
||||
/// Uses atomic operations for thread safety without locks.
|
||||
private static var _cachedIsEnabled: Bool = false
|
||||
private static var _lastCheckTime: UInt64 = 0
|
||||
private static let cacheDurationNanos: UInt64 = 1_000_000_000 // 1 second
|
||||
|
||||
/// Check if verbose logging is enabled (cached for performance).
|
||||
/// Safe to call from any thread.
|
||||
private static func isEnabled() -> Bool {
|
||||
let now = DispatchTime.now().uptimeNanoseconds
|
||||
|
||||
// Refresh cache every second
|
||||
if now - _lastCheckTime > cacheDurationNanos {
|
||||
_lastCheckTime = now
|
||||
// Read from UserDefaults directly for thread safety
|
||||
// (SettingsManager is @MainActor)
|
||||
_cachedIsEnabled = UserDefaults.standard.bool(forKey: "verboseMPVLogging")
|
||||
}
|
||||
|
||||
return _cachedIsEnabled
|
||||
}
|
||||
|
||||
// MARK: - Logging Functions
|
||||
|
||||
/// Log a verbose MPV rendering diagnostic message.
|
||||
/// Only logs if verbose MPV logging is enabled in settings.
|
||||
/// Thread-safe and can be called from any queue.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - message: The main log message
|
||||
/// - details: Optional additional details
|
||||
/// - file: Source file (auto-captured)
|
||||
/// - function: Function name (auto-captured)
|
||||
/// - line: Line number (auto-captured)
|
||||
static func log(
|
||||
_ message: String,
|
||||
details: String? = nil,
|
||||
file: String = #file,
|
||||
function: String = #function,
|
||||
line: Int = #line
|
||||
) {
|
||||
guard isEnabled() else { return }
|
||||
|
||||
let timestamp = Self.timestamp()
|
||||
let threadName = Self.threadName()
|
||||
let fileName = (file as NSString).lastPathComponent
|
||||
|
||||
let fullMessage = "[MPV-Verbose] [\(timestamp)] [\(threadName)] \(message)"
|
||||
|
||||
// Log to Console immediately (thread-safe)
|
||||
print(fullMessage)
|
||||
if let details {
|
||||
print(" \(details)")
|
||||
}
|
||||
print(" [\(fileName):\(line) \(function)]")
|
||||
|
||||
// Log to LoggingService on MainActor for persistence
|
||||
let logDetails = details.map { "\($0)\n[\(fileName):\(line) \(function)]" }
|
||||
?? "[\(fileName):\(line) \(function)]"
|
||||
|
||||
Task { @MainActor in
|
||||
LoggingService.shared.log(
|
||||
level: .debug,
|
||||
category: .mpv,
|
||||
message: "[MPV-Verbose] \(message)",
|
||||
details: logDetails
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Log with warning level for potential issues.
|
||||
static func warn(
|
||||
_ message: String,
|
||||
details: String? = nil,
|
||||
file: String = #file,
|
||||
function: String = #function,
|
||||
line: Int = #line
|
||||
) {
|
||||
guard isEnabled() else { return }
|
||||
|
||||
let timestamp = Self.timestamp()
|
||||
let threadName = Self.threadName()
|
||||
let fileName = (file as NSString).lastPathComponent
|
||||
|
||||
let fullMessage = "[MPV-Verbose] ⚠️ \(timestamp)] [\(threadName)] \(message)"
|
||||
|
||||
print(fullMessage)
|
||||
if let details {
|
||||
print(" \(details)")
|
||||
}
|
||||
print(" [\(fileName):\(line) \(function)]")
|
||||
|
||||
let logDetails = details.map { "\($0)\n[\(fileName):\(line) \(function)]" }
|
||||
?? "[\(fileName):\(line) \(function)]"
|
||||
|
||||
Task { @MainActor in
|
||||
LoggingService.shared.log(
|
||||
level: .warning,
|
||||
category: .mpv,
|
||||
message: "[MPV-Verbose] \(message)",
|
||||
details: logDetails
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Log OpenGL/EAGL state for debugging context and framebuffer issues.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - prefix: Description of the operation (e.g., "createFramebuffer")
|
||||
/// - framebuffer: The framebuffer ID
|
||||
/// - renderbuffer: The renderbuffer ID
|
||||
/// - width: Framebuffer width
|
||||
/// - height: Framebuffer height
|
||||
/// - contextCurrent: Whether the GL context is current
|
||||
/// - framebufferComplete: Whether the framebuffer is complete (nil if not checked)
|
||||
static func logGLState(
|
||||
_ prefix: String,
|
||||
framebuffer: UInt32,
|
||||
renderbuffer: UInt32,
|
||||
width: Int32,
|
||||
height: Int32,
|
||||
contextCurrent: Bool,
|
||||
framebufferComplete: Bool? = nil
|
||||
) {
|
||||
var state = "FB:\(framebuffer) RB:\(renderbuffer) \(width)x\(height) ctx:\(contextCurrent ? "✓" : "✗")"
|
||||
if let complete = framebufferComplete {
|
||||
state += " complete:\(complete ? "✓" : "✗")"
|
||||
}
|
||||
|
||||
log("\(prefix): \(state)")
|
||||
}
|
||||
|
||||
/// Log display link state changes.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - action: The action being performed (e.g., "start", "stop", "pause")
|
||||
/// - isPaused: Current paused state
|
||||
/// - targetFPS: Target frame rate if applicable
|
||||
/// - reason: Optional reason for the action
|
||||
static func logDisplayLink(
|
||||
_ action: String,
|
||||
isPaused: Bool? = nil,
|
||||
targetFPS: Double? = nil,
|
||||
reason: String? = nil
|
||||
) {
|
||||
var details: [String] = []
|
||||
if let isPaused {
|
||||
details.append("paused:\(isPaused)")
|
||||
}
|
||||
if let targetFPS {
|
||||
details.append("targetFPS:\(String(format: "%.1f", targetFPS))")
|
||||
}
|
||||
if let reason {
|
||||
details.append("reason:\(reason)")
|
||||
}
|
||||
|
||||
let detailsStr = details.isEmpty ? nil : details.joined(separator: " ")
|
||||
log("DisplayLink \(action)", details: detailsStr)
|
||||
}
|
||||
|
||||
/// Log view lifecycle events.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - event: The lifecycle event (e.g., "willMove(toSuperview:)", "didMoveToSuperview")
|
||||
/// - hasSuperview: Whether the view has a superview after the event
|
||||
/// - details: Additional context
|
||||
static func logViewLifecycle(
|
||||
_ event: String,
|
||||
hasSuperview: Bool,
|
||||
details: String? = nil
|
||||
) {
|
||||
log("View \(event)", details: "hasSuperview:\(hasSuperview)" + (details.map { " \($0)" } ?? ""))
|
||||
}
|
||||
|
||||
/// Log app lifecycle / scene phase transitions.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - event: The lifecycle event
|
||||
/// - isPiPActive: Whether PiP is currently active
|
||||
/// - isRendering: Whether rendering is active
|
||||
static func logAppLifecycle(
|
||||
_ event: String,
|
||||
isPiPActive: Bool? = nil,
|
||||
isRendering: Bool? = nil
|
||||
) {
|
||||
var details: [String] = []
|
||||
if let isPiPActive {
|
||||
details.append("pip:\(isPiPActive)")
|
||||
}
|
||||
if let isRendering {
|
||||
details.append("rendering:\(isRendering)")
|
||||
}
|
||||
|
||||
let detailsStr = details.isEmpty ? nil : details.joined(separator: " ")
|
||||
log("App \(event)", details: detailsStr)
|
||||
}
|
||||
|
||||
/// Log rotation and fullscreen transitions.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - event: The transition event
|
||||
/// - fromOrientation: Previous orientation if applicable
|
||||
/// - toOrientation: Target orientation if applicable
|
||||
static func logTransition(
|
||||
_ event: String,
|
||||
fromSize: CGSize? = nil,
|
||||
toSize: CGSize? = nil
|
||||
) {
|
||||
var details: [String] = []
|
||||
if let fromSize {
|
||||
details.append("from:\(Int(fromSize.width))x\(Int(fromSize.height))")
|
||||
}
|
||||
if let toSize {
|
||||
details.append("to:\(Int(toSize.width))x\(Int(toSize.height))")
|
||||
}
|
||||
|
||||
let detailsStr = details.isEmpty ? nil : details.joined(separator: " ")
|
||||
log("Transition \(event)", details: detailsStr)
|
||||
}
|
||||
|
||||
/// Log render operations (use sparingly to avoid log spam).
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - event: The render event
|
||||
/// - fbo: Framebuffer being rendered to
|
||||
/// - width: Render width
|
||||
/// - height: Render height
|
||||
/// - success: Whether the operation succeeded
|
||||
static func logRender(
|
||||
_ event: String,
|
||||
fbo: Int32? = nil,
|
||||
width: Int32? = nil,
|
||||
height: Int32? = nil,
|
||||
success: Bool? = nil
|
||||
) {
|
||||
var details: [String] = []
|
||||
if let fbo {
|
||||
details.append("fbo:\(fbo)")
|
||||
}
|
||||
if let width, let height {
|
||||
details.append("\(width)x\(height)")
|
||||
}
|
||||
if let success {
|
||||
details.append(success ? "✓" : "✗")
|
||||
}
|
||||
|
||||
let detailsStr = details.isEmpty ? nil : details.joined(separator: " ")
|
||||
log("Render \(event)", details: detailsStr)
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
private static func timestamp() -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm:ss.SSS"
|
||||
return formatter.string(from: Date())
|
||||
}
|
||||
|
||||
private static func threadName() -> String {
|
||||
if Thread.isMainThread {
|
||||
return "main"
|
||||
}
|
||||
if let name = Thread.current.name, !name.isEmpty {
|
||||
return name
|
||||
}
|
||||
// Get queue label if available
|
||||
let label = String(cString: __dispatch_queue_get_label(nil), encoding: .utf8) ?? "unknown"
|
||||
return label
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user