Push To Talk

I am using PushToTalk in my project for using only listing audio.

steps :-

  1. App Launch :- Create PTT Channel
  2. PTT Token :- Send Token in Server
  3. App Kill :- It's Automatically restored channel using :- channelDescriptor(restoredChannelUUID channelUUID: UUID) -> PTChannelDescriptor
  4. Play audio given by incomingPushResult method

issue :-

  • I am receiving an audio link through incomingPushResult.

  • When incomingPushResult is called, it automatically restores the channel. However, the channel has already been created, resulting in two channels being created.

  • When I send the link from the server, the audio plays properly.

  • However, if I resend the same link after 5-6 seconds, the audio does not play.

  • After I leave the first channel, the same audio starts playing in the second channel. When I send the link again, the audio does not play because I left the first (main) channel.


func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
            if granted {
                DispatchQueue.main.async {
                    application.registerForRemoteNotifications()
                }
            }
        }
        
        Task {
            await self.createChannel()
        }
        return true
    }


func createChannel() async {
        do {
            channelManager = try await PTChannelManager.channelManager(delegate: self, restorationDelegate: self)
            let channelImage = UIImage(named: "ic_p")
            channelDescriptor = PTChannelDescriptor(name: "Pikachu", image: channelImage)
            let channelUUID = UUID()
            self.currentChannelUUID = channelUUID
            channelManager?.requestJoinChannel(channelUUID: channelUUID, descriptor: channelDescriptor!)
            print("PTT creating channel")
        } catch {
            print("Error creating channel: \(error)")
        }
    }
    

     func incomingPushResult(channelManager: PTChannelManager, channelUUID: UUID, pushPayload: [String : Any]) -> PTPushResult {
        guard let data = pushPayload["data"] as? [String: Any],
              let mediaLink = data["media_link"] as? String else {
            return .leaveChannel
        }
        print("incomingPTT")
        // URL to fetch the audio data from
        self.audioURL = mediaLink
        
        // Play the audio from the URL
        DispatchQueue.main.async {
            self.playSound(url: self.audioURL)
        }
        
        let participant = PTParticipant(name: mediaLink, image: .checkmark)
        return .activeRemoteParticipant(participant)
    }
    
    

    func channelDescriptor(restoredChannelUUID channelUUID: UUID) -> PTChannelDescriptor {
        let channelImage = UIImage(named: "ic_r")
        return PTChannelDescriptor(name: "Restored Channel", image: channelImage)
    }
    

    func channelManager(_ channelManager: PTChannelManager, didActivate audioSession: AVAudioSession) {
        print("Activated audio session")
        self.playSound(url: self.audioURL)
    }

Output: -

  1. App Launch

  1. After App Kill Play audio :- Audio Play Success and leave the channel

(Before Leave Channel View)

  1. After Leave Channel View

Answered by DTS Engineer in 799520022

So, the immediate issue here is this:

func createChannel() async {
        do {
...
            channelManager?.requestJoinChannel(channelUUID: channelUUID, descriptor: channelDescriptor!)
...
        } catch {
            print("Error creating channel: \(error)")
        }
    }

You cannot do that. Your app is only allowed to "directly" join a channel when it's in the foreground, so "requestJoinChannel" should only be called from the foreground and, more likely, should probably be part of your app interface where the user can start a PTT session (not automatic).

In the background, it's not necessary as "channelDescriptor(restoredChannelUUID...)" means that the system has already joined the "channel".

However, that leads to the bigger conceptual issue with the PTT framework, which is that you shouldn't think of "our channel" as the same thing as whatever your app considers "a channel". With the benefit of hindsight, we should not have used the term "channel" at all ("session" would have a been a much better term), as the term is very widely used in PTT apps which creates expectations that don't match the system actual behavior.

Your app should not attempt to create/join multiple PTChannel's, as it greatly complicates the entire architecture without providing any actual benefit. Your app can have as many channels (or whatever you choose to call them) as it wants, but it should just modify PTChannelDescriptor of it's only PTChannel to tell the user what's going on.

Note that the documentation does indicate that this is what you should do:

"You can activate only one channel at a time. A nil value indicates there isn’t an active Push to Talk channel. When this value isn’t nil, the channel is active in the user interface, and the ephemeral push token is usable."

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

So, the immediate issue here is this:

func createChannel() async {
        do {
...
            channelManager?.requestJoinChannel(channelUUID: channelUUID, descriptor: channelDescriptor!)
...
        } catch {
            print("Error creating channel: \(error)")
        }
    }

You cannot do that. Your app is only allowed to "directly" join a channel when it's in the foreground, so "requestJoinChannel" should only be called from the foreground and, more likely, should probably be part of your app interface where the user can start a PTT session (not automatic).

In the background, it's not necessary as "channelDescriptor(restoredChannelUUID...)" means that the system has already joined the "channel".

However, that leads to the bigger conceptual issue with the PTT framework, which is that you shouldn't think of "our channel" as the same thing as whatever your app considers "a channel". With the benefit of hindsight, we should not have used the term "channel" at all ("session" would have a been a much better term), as the term is very widely used in PTT apps which creates expectations that don't match the system actual behavior.

Your app should not attempt to create/join multiple PTChannel's, as it greatly complicates the entire architecture without providing any actual benefit. Your app can have as many channels (or whatever you choose to call them) as it wants, but it should just modify PTChannelDescriptor of it's only PTChannel to tell the user what's going on.

Note that the documentation does indicate that this is what you should do:

"You can activate only one channel at a time. A nil value indicates there isn’t an active Push to Talk channel. When this value isn’t nil, the channel is active in the user interface, and the ephemeral push token is usable."

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

What are the steps to play audio from a link provided in incomingPushResult when the app is killed?. The audio playback problem occurs only when the app is killed.

What are the steps to play audio from a link provided in incomingPushResult when the app is killed?. The audio playback problem occurs only when the app is killed.

For reference, we documented the general API behavior/lifecycle in "Creating a Push to Talk app". The section "Receive audio" says (edited down for clarity and broken into sequence):

  1. When the app’s server sends a PTT notification, the system starts the app in the background and calls incomingPushResultForChannelManager:channelUUID:pushPayload:.

  2. Return a PTPushResult as soon as possible and don’t block the thread.

  3. After setting pushResultForActiveRemoteParticipant:, the system activates the app’s audio session and calls the channelManager:didActivateAudioSession: method.

  4. When the app’s audio session is in an active state, begin playing back the audio it receives from the app’s server.

  5. When a remote participant finishes speaking, set setActiveRemoteParticipant:forChannelUUID:completionHandler: to nil to indicate that the app is no longer receiving audio on the channel and the system can deactivate the audio session.

That's the same sequence that's followed when the app is launched into the background. More specifically:

  1. Your app will be launched into the background.

  2. Your app will create it's PTChannelManager and establish both delegates.

  3. The state restoration delegate will be called and restoration will occur.

  4. The sequence above will occur.

The main thing to note here is that your app should NOT be calling "setActive" on the AudioSession, but should instead rely on the system to handle activation. The PTT framework uses the same underlying architecture as CallKit, which means direct activation will cause exactly the same kinds of weird failures that it causes in CallKit. Keep in mind that the issues this causes can range from relatively minor/invisible to catastrophic and weird. Exactly what/how will fail depends on the exact configuration of your app and session, which can make it very easy to assume that you're dealing with a narrow/specific bug when you're actually seeing a specific symptom of a more general problem.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Is it possible to play audio in the Background or when the app is Terminated without using PTT ?

Push To Talk
 
 
Q