From a0d1978fb6f349d917faebd3039361a3f000e720 Mon Sep 17 00:00:00 2001 From: der richter Date: Sun, 29 Dec 2024 21:21:03 +0100 Subject: [PATCH] mac/remote: add Quick Look thumbnail as cover fallback since we load covers/images directly in a our own thread and the quick look thumbnailing also works asynchronously on its own thread, we need sync/coordinate both threads. otherwise it would be possible that a cover from a different file could overwrite the cover of the current file. to prevent this every cover generation has a unique timestamp and only covers for the last cover timestamp will ever be shown. we are also not interested in icon type thumbnails, since those are just generic file type icons. quick look thumbnails can only be generated if quick look can read those files. files unknown to macOS don't work out of the box and you need to install a quick look extension for this, "QuickLook Video" for mkv. --- osdep/mac/remote_command_center.swift | 58 ++++++++++++++++++++------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/osdep/mac/remote_command_center.swift b/osdep/mac/remote_command_center.swift index 950ba45087662..60c9324e63ba4 100644 --- a/osdep/mac/remote_command_center.swift +++ b/osdep/mac/remote_command_center.swift @@ -17,6 +17,7 @@ import Cocoa import MediaPlayer +import QuickLookThumbnailing extension RemoteCommandCenter { typealias ConfigHandler = (MPRemoteCommandEvent) -> (MPRemoteCommandHandlerStatus) @@ -55,8 +56,12 @@ class RemoteCommandCenter: EventSubscriber { var album: String? { didSet { updateInfoCenter() } } var artist: String? { didSet { updateInfoCenter() } } var path: String? + + let coverLock = NSLock() + var coverTime: UInt64 = mach_absolute_time() var coverPath: String? var cover: NSImage? { didSet { updateInfoCenter() } } + var coverThumb: NSImage? { didSet { updateInfoCenter() } } var defaultCover: NSImage let queue: DispatchQueue = DispatchQueue(label: "io.mpv.remote.queue") @@ -150,7 +155,7 @@ class RemoteCommandCenter: EventSubscriber { } func updateInfoCenter() { - let cover = cover ?? defaultCover + let cover = cover ?? coverThumb ?? defaultCover infoCenter.playbackState = isPaused ? .paused : .playing infoCenter.nowPlayingInfo = (infoCenter.nowPlayingInfo ?? [:]).merging([ MPNowPlayingInfoPropertyMediaType: NSNumber(value: MPNowPlayingInfoMediaType.video.rawValue), @@ -166,6 +171,19 @@ class RemoteCommandCenter: EventSubscriber { } func updateCover(tracks: [Any?]) { + coverLock.withLock { + coverTime = mach_absolute_time() + coverPath = nil + cover = nil + coverThumb = nil + + // generate cover image on separate thread + queue.async { self.generateCover(tracks: tracks, time: self.coverTime) } + generateCoverThumb(time: self.coverTime) + } + } + + func generateCover(tracks: [Any?], time: UInt64) { var imageCoverPath: String? var externalCoverPath: String? @@ -185,21 +203,33 @@ class RemoteCommandCenter: EventSubscriber { } } - // read cover image on separate thread - queue.async { - guard let path = imageCoverPath ?? externalCoverPath else { - self.cover = nil - self.coverPath = nil - return - } - if self.coverPath == path { return } + guard let path = imageCoverPath ?? externalCoverPath, coverPath != path else { return } + + var image = NSImage(contentsOf: URL(fileURLWithPath: path)) + if let url = URL(string: path), image == nil { + image = NSImage(contentsOf: url) + } + + coverLock.withLock { + guard time == coverTime else { return } + cover = image + coverPath = path + } + } + + func generateCoverThumb(time: UInt64) { + guard let path = path else { return } - var image = NSImage(contentsOf: URL(fileURLWithPath: path)) - if let url = URL(string: path), image == nil { - image = NSImage(contentsOf: url) + let request = QLThumbnailGenerator.Request(fileAt: URL(fileURLWithPath: path), + size: CGSize(width: 2000, height: 2000), + scale: 1, + representationTypes: .all) + QLThumbnailGenerator.shared.generateBestRepresentation(for: request) { thumbnail, error in + self.coverLock.withLock { + guard time == self.coverTime else { return } + guard let image = thumbnail?.nsImage, thumbnail?.type != .icon else { return } + self.coverThumb = image } - self.cover = image - self.coverPath = path } }