iosswiftwatchkitwatchconnectivity

WatchConnectivity - Start AVPlayer on iOS device from watch companion app


Let's say I have SwiftUI app that starts playing audio from chosen url in list:

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()
    }
}

And this app has watchOS companion app, that also has a list of stations:

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)
    }
}

By tapping row in watchOS app, I need to start AVPlayer playing on iOS device.

For this I have implemented WatchConnectivityManager that handles communication between watchOS and iOS apps:

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 func sends message with chosen link, which is received by MESSAGE RECEIVER method, and then it posts a notification with chosen link to the NotificationCenter.default. This notification is received by iOS ContentViewModel and the player starts.

It is all working good when the iOS app is in foreground, however it's not working when we go to the Home Screen and choose some url from watch app again (the iOS app is not terminated).

Here are logs, if they're helpful for someone:

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

Solution

  • Just bought Apple Watch for myself and tested again. Player's working fine when app is in background and also when the app is not opened at all. For some reason, this is not working on emulators... So if u face similar issue on emulator, try it on real device, maybe it's not even an issue at all.