Swift开发macOS应用AVPlayer无法播放用户本地音频文件如何解决
问题原因
本地音频无法播放、duration返回nan的问题由3个核心原因导致:
- macOS沙盒权限限制:开启App Sandbox的应用(包括Swift Playground)无法直接通过硬编码的文件路径访问用户目录下的资源,哪怕
checkResourceIsReachable()返回true,实际读取音频元数据时也会被系统拦截,导致资源加载失败。 - 资源加载时序错误:
AVPlayerItem初始化后会异步加载媒体元数据,初始化完成后立刻读取duration、调用play()时资源还未加载完成,自然会拿到nan值,播放指令也不会生效。 - 生命周期持有错误:如果把
AVPlayer、AVPlayerItem定义为函数内的局部变量,初始化完成后对象会被ARC自动回收,音频会直接停止播放,且不会抛出明确报错。
另外示例代码中硬编码的路径开头多写了一个斜杠,虽然系统会自动容错解析,但建议使用标准单斜杠开头的文件路径格式/Users/xxx/ThreeThing.mp3。
可行实现方案
1. 正确处理文件访问权限(沙盒环境必需)
不要只持久化文件路径字符串,沙盒环境下路径本身不携带访问权限,需要通过安全作用域bookmark来持久化用户的文件授权:
import AppKit import AVFoundation // 打开文件选择器时获取授权 func selectAudioFile() -> URL? { let openPanel = NSOpenPanel() openPanel.allowsMultipleSelection = false openPanel.canChooseDirectories = false openPanel.canChooseFiles = true openPanel.allowedContentTypes = [.mp3, .mpeg4Audio, .audio] let response = openPanel.runModal() guard response == .OK, let selectedURL = openPanel.url else { return nil } // 生成可持久化的权限bookmark do { let bookmarkData = try selectedURL.bookmarkData( options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil ) // 把bookmarkData存到UserDefaults或本地存储,不要只存path字符串 UserDefaults.standard.set(bookmarkData, forKey: "savedAudioBookmark") } catch { print("生成权限凭证失败: \(error.localizedDescription)") } return selectedURL } // 读取已保存的音频文件时解析权限 func getSavedAudioURL() -> URL? { guard let bookmarkData = UserDefaults.standard.data(forKey: "savedAudioBookmark") else { return nil } var isStale = false do { let audioURL = try URL( resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale ) // 申请临时访问权限,播放结束后必须调用stopAccessingSecurityScopedResource释放 guard audioURL.startAccessingSecurityScopedResource() else { print("无文件访问权限") return nil } return audioURL } catch { print("解析权限凭证失败: \(error.localizedDescription)") return nil } }
2. 监听资源加载状态,就绪后再播放
不要初始化完播放器立刻执行播放/读取属性操作,通过KVO监听AVPlayerItem的状态,等资源加载到readyToPlay状态后再操作:
// 播放器、观察者需要全局/类属性强持有,避免被ARC回收 var audioPlayer: AVPlayer? var itemStatusObserver: NSKeyValueObservation? func playLocalAudio(url: URL) { // 先释放之前的播放器和观察者 stopAudio() let playerItem = AVPlayerItem(url: url) audioPlayer = AVPlayer(playerItem: playerItem) audioPlayer?.volume = 1 // 监听资源加载状态 itemStatusObserver = playerItem.observe(\.status, options: [.new]) { [weak self] item, _ in switch item.status { case .readyToPlay: // 此时资源加载完成,读取duration、调用play才会生效 let totalDuration = item.duration.seconds print("音频加载完成,总时长: \(totalDuration)s") self?.audioPlayer?.play() case .failed: print("音频加载失败: \(item.error?.localizedDescription ?? "未知错误")") url.stopAccessingSecurityScopedResource() case .unknown: fallthrough @unknown default: break } } } func stopAudio() { audioPlayer?.pause() audioPlayer = nil itemStatusObserver?.invalidate() // 释放文件访问权限 getSavedAudioURL()?.stopAccessingSecurityScopedResource() }
3. Swift Playground调试配置
如果是在Swift Playground中测试,需要添加以下配置开启常驻执行和文件访问权限,否则异步逻辑会提前终止:
import PlaygroundSupport // 开启常驻执行,避免主线程跑完直接退出 PlaygroundPage.current.needsIndefiniteExecution = true
测试时建议把音频文件放到Playground的Resources目录下,或者通过Playground的文件授权API获取路径访问权限,不要直接硬编码用户目录路径。
注意事项
- 网络音频可以正常播放是因为网络请求不需要本地文件系统的沙盒权限,且网络请求本身的异步延迟刚好给了AVPlayer足够的缓冲时间,所以之前的写法在网络场景下碰巧能运行。
- 不需要把音频文件打包进App Bundle,只要通过系统文件选择器拿到用户授权,就可以合法访问用户选中的任意本地音频文件。
内容的提问来源于stack exchange,提问作者afishershin
相关产品推荐
相关产品推荐

