From ad251d85622b8c032a9678c34d4b043dc6e3f5d3 Mon Sep 17 00:00:00 2001 From: "samzong.lu" Date: Fri, 20 Sep 2024 13:46:45 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Support=20macos=20media=20c?= =?UTF-8?q?ontrol=20play/pause=20music=20(close=20#3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AppDelegate.swift | 29 ++++++++- MacMusicPlayer.xcodeproj/project.pbxproj | 13 ++++ .../xcdebugger/Breakpoints_v2.xcbkptlist | 18 ++++++ MacMusicPlayer/Info.plist | 4 ++ Managers/PlayerManager.swift | 61 ++++++++++++++++--- 5 files changed, 115 insertions(+), 10 deletions(-) diff --git a/AppDelegate.swift b/AppDelegate.swift index ba0902e..a3c2448 100644 --- a/AppDelegate.swift +++ b/AppDelegate.swift @@ -6,7 +6,9 @@ // import Cocoa +import MediaPlayer import SwiftUI +import AVFoundation class AppDelegate: NSObject, NSApplicationDelegate { var statusItem: NSStatusItem? @@ -26,6 +28,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } setupMenu() + setupRemoteCommandCenter() } func setupMenu() { @@ -51,7 +54,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { menu.addItem(NSMenuItem.separator()) // Reconfigure folder - menu.addItem(NSMenuItem(title: "Sources", action: #selector(reconfigureFolder), keyEquivalent: "")) + menu.addItem(NSMenuItem(title: "Sources", action: #selector(reconfigureFolder), keyEquivalent: "s")) menu.addItem(NSMenuItem.separator()) @@ -63,6 +66,30 @@ class AppDelegate: NSObject, NSApplicationDelegate { NotificationCenter.default.addObserver(self, selector: #selector(updateMenuItems), name: NSNotification.Name("PlaybackStateChanged"), object: nil) } + func setupRemoteCommandCenter() { + let commandCenter = MPRemoteCommandCenter.shared() + + commandCenter.playCommand.addTarget { [weak self] _ in + self?.playerManager.play() + return .success + } + + commandCenter.pauseCommand.addTarget { [weak self] _ in + self?.playerManager.pause() + return .success + } + + commandCenter.nextTrackCommand.addTarget { [weak self] _ in + self?.playerManager.playNext() + return .success + } + + commandCenter.previousTrackCommand.addTarget { [weak self] _ in + self?.playerManager.playPrevious() + return .success + } + } + @objc func toggleMenu() { updateMenuItems() statusItem?.menu = menu diff --git a/MacMusicPlayer.xcodeproj/project.pbxproj b/MacMusicPlayer.xcodeproj/project.pbxproj index ae2150e..44176d6 100644 --- a/MacMusicPlayer.xcodeproj/project.pbxproj +++ b/MacMusicPlayer.xcodeproj/project.pbxproj @@ -25,9 +25,22 @@ 991C445C2C9AB7FA00F2BB76 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 999217BB2C9D42B2008F5CC3 /* Exceptions for "MacMusicPlayer" folder in "MacMusicPlayer" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 991C44362C9A9C6E00F2BB76 /* MacMusicPlayer */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ 991C44392C9A9C6E00F2BB76 /* MacMusicPlayer */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 999217BB2C9D42B2008F5CC3 /* Exceptions for "MacMusicPlayer" folder in "MacMusicPlayer" target */, + ); path = MacMusicPlayer; sourceTree = ""; }; diff --git a/MacMusicPlayer.xcodeproj/xcuserdata/samzonglu.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/MacMusicPlayer.xcodeproj/xcuserdata/samzonglu.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index af22ef7..a83663f 100644 --- a/MacMusicPlayer.xcodeproj/xcuserdata/samzonglu.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/MacMusicPlayer.xcodeproj/xcuserdata/samzonglu.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -3,4 +3,22 @@ uuid = "C5D46313-4BC6-4EEB-9A79-353627BEAB5B" type = "1" version = "2.0"> + + + + + + diff --git a/MacMusicPlayer/Info.plist b/MacMusicPlayer/Info.plist index 7a643ed..6303cf3 100644 --- a/MacMusicPlayer/Info.plist +++ b/MacMusicPlayer/Info.plist @@ -17,5 +17,9 @@ + UIBackgroundModes + + audio + diff --git a/Managers/PlayerManager.swift b/Managers/PlayerManager.swift index 0aec208..f26fce3 100644 --- a/Managers/PlayerManager.swift +++ b/Managers/PlayerManager.swift @@ -8,6 +8,7 @@ import Foundation import AVFoundation import SwiftUI +import MediaPlayer class PlayerManager: NSObject, ObservableObject, AVAudioPlayerDelegate { @Published var playlist: [Track] = [] @@ -23,12 +24,15 @@ class PlayerManager: NSObject, ObservableObject, AVAudioPlayerDelegate { } private var player: AVAudioPlayer? private var currentIndex = 0 - + var audioEngine: AVAudioEngine! + var playerNode: AVAudioPlayerNode! + override init() { super.init() + setupAudioEngine() loadSavedMusicFolder() } - + private func loadSavedMusicFolder() { if let savedPath = UserDefaults.standard.string(forKey: "MusicFolderPath") { loadTracksFromMusicFolder(URL(fileURLWithPath: savedPath)) @@ -36,7 +40,7 @@ class PlayerManager: NSObject, ObservableObject, AVAudioPlayerDelegate { requestMusicFolderAccess() } } - + func requestMusicFolderAccess() { DispatchQueue.main.async { let openPanel = NSOpenPanel() @@ -53,7 +57,38 @@ class PlayerManager: NSObject, ObservableObject, AVAudioPlayerDelegate { } } } - + + func setupAudioEngine() { + audioEngine = AVAudioEngine() + playerNode = AVAudioPlayerNode() + audioEngine.attach(playerNode) + let mainMixer = audioEngine.mainMixerNode + audioEngine.connect(playerNode, to: mainMixer, format: nil) + + do { + try audioEngine.start() + } catch { + print("Failed to start audio engine: \(error)") + } + } + + func updateNowPlayingInfo() { + var nowPlayingInfo = [String: Any]() + + if let currentTrack = currentTrack { + nowPlayingInfo[MPMediaItemPropertyTitle] = currentTrack.title + nowPlayingInfo[MPMediaItemPropertyArtist] = currentTrack.artist + + if let player = player { + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = player.duration + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player.currentTime + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.isPlaying ? 1.0 : 0.0 + } + } + + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + private func loadTracksFromMusicFolder(_ folderURL: URL) { do { let fileURLs = try FileManager.default.contentsOfDirectory(at: folderURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) @@ -77,7 +112,7 @@ class PlayerManager: NSObject, ObservableObject, AVAudioPlayerDelegate { print("Error accessing Music folder: \(error)") } } - + func play() { guard let track = currentTrack else { print("No current track to play") @@ -93,28 +128,36 @@ class PlayerManager: NSObject, ObservableObject, AVAudioPlayerDelegate { } catch { print("Could not create player for \(track.title): \(error)") } + + updateNowPlayingInfo() } - + func pause() { player?.pause() isPlaying = false print("Paused playback") + + updateNowPlayingInfo() } - + func playNext() { guard !playlist.isEmpty else { return } currentIndex = (currentIndex + 1) % playlist.count currentTrack = playlist[currentIndex] play() + + updateNowPlayingInfo() } - + func playPrevious() { guard !playlist.isEmpty else { return } currentIndex = (currentIndex - 1 + playlist.count) % playlist.count currentTrack = playlist[currentIndex] play() + + updateNowPlayingInfo() } - + // AVAudioPlayerDelegate 方法 func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { if flag {