求助:使用AVFoundation播放iOS音乐库内容并实现后台播报
Hey there, I’ve dealt with this exact frustration before—Media Player’s background notifications are notoriously unreliable, and AVFoundation’s public API doesn’t play nice with those ipod-library:// URLs. Since you’re only using this app personally, let’s leverage private APIs to get this working smoothly.
First, Setup Background Audio
Before writing any code, make sure your app is configured for background playback:
- Open your
Info.plist - Add the
UIBackgroundModesarray if it doesn’t exist - Add the string value
audioto this array
Core Implementation: Music Player Manager
We’ll create a singleton manager to handle playback, music library integration, and speech synthesis. This uses a private MPMediaItem property to get the actual local file path of each song (since the public ipod-library URL won’t work with AVPlayer).
import UIKit import AVFoundation import MediaPlayer import AVFAudio class MusicPlayerManager: NSObject { static let shared = MusicPlayerManager() private var player: AVPlayer? private var currentPlaylist: [MPMediaItem] = [] private var currentIndex: Int = 0 private let speechSynthesizer = AVSpeechSynthesizer() private override init() { super.init() setupAudioSession() setupPlaybackNotifications() } // Configure audio session for background playback private func setupAudioSession() { do { let session = AVAudioSession.sharedInstance() try session.setCategory(.playback, mode: .default, options: [.mixWithOthers]) try session.setActive(true) } catch { print("Audio session setup failed: \(error.localizedDescription)") } } // Listen for AVFoundation playback events (works in background) private func setupPlaybackNotifications() { NotificationCenter.default.addObserver( self, selector: #selector(onTrackFinished), name: .AVPlayerItemDidPlayToEndTime, object: nil ) NotificationCenter.default.addObserver( self, selector: #selector(onPlaybackStateChanged), name: .AVPlayerRateDidChange, object: nil ) NotificationCenter.default.addObserver( self, selector: #selector(onItemStatusUpdated), name: .AVPlayerItemStatusDidChange, object: nil ) } // Load a playlist from Media Player framework func loadPlaylist(_ playlist: MPMediaItemCollection) { currentPlaylist = playlist.items currentIndex = 0 if !currentPlaylist.isEmpty { playTrack(at: currentIndex) } } // Play a track using its private local file path private func playTrack(at index: Int) { guard index < currentPlaylist.count else { return } let mediaItem = currentPlaylist[index] // Access private MPMediaItem property to get local file path guard let localFilePath = mediaItem.value(forKey: "_path") as? String else { print("Couldn't retrieve local path for track") playNextTrack() return } let fileURL = URL(fileURLWithPath: localFilePath) let playerItem = AVPlayerItem(url: fileURL) if player == nil { player = AVPlayer(playerItem: playerItem) } else { player?.replaceCurrentItem(with: playerItem) } player?.play() announceTrackInfo(mediaItem) } // Use AVSpeechSynthesizer to announce track details private func announceTrackInfo(_ item: MPMediaItem) { if speechSynthesizer.isSpeaking { speechSynthesizer.stopSpeaking(at: .immediate) } let title = item.title ?? "Unknown Track" let artist = item.artist ?? "Unknown Artist" let announcement = AVSpeechUtterance(string: "Now playing \(title) by \(artist)") announcement.voice = AVSpeechSynthesisVoice(language: "en-US") announcement.rate = 0.5 // Adjust speech rate as needed speechSynthesizer.speak(announcement) } // Handle track completion @objc private func onTrackFinished() { playNextTrack() } // Handle play/pause state changes @objc private func onPlaybackStateChanged() { guard let player = player else { return } let announcementText = player.rate == 0 ? "Playback paused" : "Playback resumed" let utterance = AVSpeechUtterance(string: announcementText) speechSynthesizer.speak(utterance) } // Handle AVPlayerItem status changes (e.g., playback errors) @objc private func onItemStatusUpdated(notification: Notification) { guard let item = notification.object as? AVPlayerItem else { return } if item.status == .failed { print("Track playback failed: \(item.error?.localizedDescription ?? "Unknown error")") playNextTrack() } } // Public control methods func togglePlayPause() { player?.rate == 0 ? player?.play() : player?.pause() } func playNextTrack() { currentIndex = (currentIndex + 1) % currentPlaylist.count playTrack(at: currentIndex) } func playPreviousTrack() { currentIndex = (currentIndex - 1 + currentPlaylist.count) % currentPlaylist.count playTrack(at: currentIndex) } }
Using the Manager in Your View Controller
Here’s how to integrate this with a basic UI to load playlists and control playback:
class MainViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() requestMusicLibraryAccess() } // Request permission to access the user's music library private func requestMusicLibraryAccess() { MPMediaLibrary.requestAuthorization { status in guard status == .authorized else { print("Music library access denied") return } DispatchQueue.main.async { self.loadUserPlaylists() } } } // Load playlists and start playing the first one private func loadUserPlaylists() { let playlistQuery = MPMediaQuery.playlists() guard let playlists = playlistQuery.collections else { return } // Replace this with playlist selection logic if needed if let firstPlaylist = playlists.first { MusicPlayerManager.shared.loadPlaylist(firstPlaylist) } } // Example button actions (connect these to UI buttons) @IBAction func playPauseTapped(_ sender: UIButton) { MusicPlayerManager.shared.togglePlayPause() } @IBAction func nextTapped(_ sender: UIButton) { MusicPlayerManager.shared.playNextTrack() } @IBAction func previousTapped(_ sender: UIButton) { MusicPlayerManager.shared.playPreviousTrack() } }
Key Notes
- Private API Warning: The
_pathproperty onMPMediaItemis not public, so this code will be rejected by the App Store. But since you’re using this personally, that’s not an issue. Keep in mind that private APIs can break with iOS updates—if this stops working in a new iOS version, you may need to find the updated private property name (tools like class-dump can help here). - Background Stability: As long as the audio session is active, AVFoundation’s notifications will fire in the background, which fixes your original Media Player framework issue.
- Speech Synthesis: The speech synthesizer will work in the background while audio is playing, since the audio session is already active. If you need to announce when no audio is playing, you may need additional configuration for background audio tasks.
内容的提问来源于stack exchange,提问作者Uncle Bill

