You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 07:39:52