基于WatchConnectivity,从watchOS配套App启动iOS设备的AVPlayer
iOS后台无法响应WatchOS触发的AVPlayer播放问题
我有一款SwiftUI应用,iOS端通过列表选择URL启动AVPlayer播放音频,代码实现如下:
iOS端核心代码
struct ContentView: View { @ObservedObject var viewModel: ContentViewModel var body: some View { List(PlayerLinks.links, id: \.self) { link in Button(link) { viewModel.play(from: link) } } } } class ContentViewModel: ObservableObject { let player = Player() init() { NotificationCenter.default.addObserver( self, selector: #selector(notificationAction), name: .newLinkChosen, object: nil ) } func play(from url: String) { player.play(from: url) } @objc func notificationAction(_ notification: Notification) { if let userInfo = notification.userInfo, let newURL = userInfo["newLink"] as? String { self.play(from: newURL) } } } class Player { var player: AVPlayer? init() { do { try AVAudioSession.sharedInstance() .setCategory( AVAudioSession.Category.playback, mode: AVAudioSession.Mode.default, options: [] ) } catch let error as NSError { print(error.localizedDescription) } } func play(from url: String) { let playerItem = AVPlayerItem(url: URL(string: url)!) player = AVPlayer(playerItem: playerItem) player?.play() } }
WatchOS端配套代码
应用配有WatchOS配套App,同样包含音频站点列表:
struct ContentWatchView: View { @ObservedObject var viewModel: ContentWatchViewModel var body: some View { List(PlayerLinks.links, id: \.self) { link in Button(link) { viewModel.onRowSelected(url: link) } } } } class ContentWatchViewModel: ObservableObject { var connectivityManager: WatchConnectivityManager init(connectivityManager: WatchConnectivityManager) { self.connectivityManager = connectivityManager } func onRowSelected(url: String) { connectivityManager.sendPlayerLinkToIOS(url) } }
WatchConnectivity跨端通信实现
为实现点击WatchOS列表项启动iOS设备AVPlayer播放音频,我实现了WatchConnectivityManager处理跨端通信:
class WatchConnectivityManager: NSObject, ObservableObject, WCSessionDelegate { private let session: WCSession = WCSession.default var isReachable = false static var shared = WatchConnectivityManager() override init() { super.init() if WCSession.isSupported() { session.delegate = self session.activate() } } func session( _ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error? ) { #if os(iOS) print("ACTIVATED ON IOS") #elseif os(watchOS) print("ACTIVATED ON WATCHOS") #endif DispatchQueue.main.async { self.isReachable = session.isReachable } } func sessionReachabilityDidChange(_ session: WCSession) { DispatchQueue.main.async { self.isReachable = session.isReachable } } #if os(iOS) func sessionDidDeactivate(_ session: WCSession) { session.activate() } func sessionDidBecomeInactive(_ session: WCSession) { print("Session did become inactive: \(session.activationState.rawValue)") } func sessionWatchStateDidChange(_ session: WCSession) { print("Session watch state did change: \(session.activationState.rawValue)") } #endif // MARK: MESSAGE RECEIVER func session( _ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void ) { #if os(iOS) if let action = message["action"] as? String, action == "newPlayerLinkChosen", let link = message["link"] as? String { DispatchQueue.main.async { NotificationCenter.default.post( name: .newLinkChosen, object: nil, userInfo: ["newLink": link] ) replyHandler(["success": true]) } } else { replyHandler(["success": false]) } #endif } // MARK: MESSAGE SENDERS #if os(watchOS) func sendPlayerLinkToIOS(_ link: String) { let message = [ "action": "newPlayerLinkChosen", "link": link ] session.sendMessage(message) { replyHandler in print(replyHandler) } errorHandler: { error in print(error.localizedDescription) } } #endif } extension Notification.Name { static let newLinkChosen = Notification.Name("NewLinkChosen") }
问题现象
sendPlayerLinkToIOS方法发送选中的链接,iOS端接收后通过NotificationCenter.default发送通知,ContentViewModel收到通知后启动播放器。iOS App在前台时功能正常,但退至主屏幕(未终止)时,从WatchOS选择URL无法触发播放。
相关日志
ACTIVATED ON IOS 2023-08-18 16:23:38.651377+0300 RemotePlayer[20479:6162721] [plugin] AddInstanceForFactory: No factory registered for id <CFUUID 0x6000022fce00> F8BB1C28-BAE8-11D6-9C31-00039315CD46 2023-08-18 16:23:52.046769+0300 RemotePlayer[20479:6163115] [AMCP] 4611 HALC_ProxyIOContext.cpp:783 HALC_ProxyIOContext::_StartIO(): Client running as an adaptive unboosted daemon 2023-08-18 16:23:52.047123+0300 RemotePlayer[20479:6163115] HALPlugIn.cpp:519 HALPlugIn::StartIOProc: got an error from the plug-in routine, Error: 1852797029 (nope) 2023-08-18 16:23:52.048409+0300 RemotePlayer[20479:6163115] [aqme] AQMEIO.cpp:211 error 1852797029 2023-08-18 16:23:52.049634+0300 RemotePlayer[20479:6163115] [aqme] MEDeviceStreamClient.cpp:431 AQME Default-InputOutput: client stopping after failed start: <CA_UISoundClientBase@0x149514830>; running count now 0 2023-08-18 16:23:52.050233+0300 RemotePlayer[20479:6163115] CA_UISoundClient.cpp:285 CA_UISoundClientBase::StartPlaying: AddRunningClient failed (status = 1852797029). 2023-08-18 16:23:53.738535+0300 RemotePlayer[20479:6163570] [AMCP] 59139 HALC_ProxyIOContext.cpp:783 HALC_ProxyIOContext::_StartIO(): Client running as an adaptive unboosted daemon 2023-08-18 16:23:53.741183+0300 RemotePlayer[20479:6163570] HALPlugIn.cpp:519 HALPlugIn::StartIOProc: got an error from the plug-in routine, Error: 1852797029 (nope) 2023-08-18 16:23:53.744653+0300 RemotePlayer[20479:6163570] [aqme] AQMEIO.cpp:211 error 1852797029 2023-08-18 16:23:53.745684+0300 RemotePlayer[20479:6163570] [aqme] MEDeviceStreamClient.cpp:431 AQME Default-InputOutput: client stopping after failed start: <AudioQueueObject@0x14a809000; Unknown figplayer; [20479]; play>; running count now 0
解决方案
1. 配置后台音频权限
在iOS项目的Info.plist中添加后台模式权限,开启音频播放:
<key>UIBackgroundModes</key> <array> <string>audio</string> </array>
2. 优化AVAudioSession配置
在Player类的初始化方法中,添加必要选项并激活音频会话:
init() { do { try AVAudioSession.sharedInstance() .setCategory( AVAudioSession.Category.playback, mode: AVAudioSession.Mode.default, options: [.mixWithOthers, .allowAirPlay] ) try AVAudioSession.sharedInstance().setActive(true) } catch let error as NSError { print(error.localizedDescription) } }
3. 后台接收消息时强制激活音频会话
修改iOS端接收Watch消息的逻辑,先激活音频会话再触发播放:
// MARK: MESSAGE RECEIVER func session( _ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void ) { #if os(iOS) if let action = message["action"] as? String, action == "newPlayerLinkChosen", let link = message["link"] as? String { DispatchQueue.main.async { // 先激活音频会话 do { try AVAudioSession.sharedInstance().setActive(true) } catch { print("Failed to activate audio session: \(error)") } NotificationCenter.default.post( name: .newLinkChosen, object: nil, userInfo: ["newLink": link] ) replyHandler(["success": true]) } } else { replyHandler(["success": false]) } #endif }
4. 复用AVPlayer实例(可选优化)
避免每次播放创建新实例,提升后台稳定性:
class Player { private let player = AVPlayer() init() { do { try AVAudioSession.sharedInstance() .setCategory( AVAudioSession.Category.playback, mode: AVAudioSession.Mode.default, options: [.mixWithOthers, .allowAirPlay] ) try AVAudioSession.sharedInstance().setActive(true) } catch let error as NSError { print(error.localizedDescription) } } func play(from url: String) { guard let url = URL(string: url) else { return } let playerItem = AVPlayerItem(url: url) player.replaceCurrentItem(with: playerItem) player.play() } }
内容的提问来源于stack exchange,提问作者Vasyl Nadtochii
相关产品推荐
相关产品推荐

