PTChannelManager setAccessoryButtonEventsEnabled documentation?

I would like to know more about how I can use a bluetooth PTT device with the "Push to Talk" framework on iOS. I've enabled the setAccessoryButtonEventsEnabled on the PTChannelManager, but I am unable to get it to transmit when I press the PTT button on the device.

Is there documentation that explains any additional steps I need to do in order to use a bluetooth PTT device with an iPhone using the Push to Talk framework? Our application works fine when interacting through the software UI.

I'm testing with a PrymeMax PTT device. This is the user guide: https://www.pryme.com/files/manuals/MANUAL-BTH-550-MAX.pdf

There are a lot of generic devices like this one that I would like to support. Can somebody provide more information on how this is accomplished?

I would also like to support it with iOS versions older than iOS 17 if possible.

Answered by DTS Engineer in 794857022

There are a lot of generic devices like this one that I would like to support. Can somebody provide more information on how this is accomplished?

Starting with some background context, PTT control headset generally work in one of two ways:

a) They present themselves to the system through the standard media control profiles of classic bluetooth. As far as the system is concerned, the headset is EXACTLY the same as any other bluetooth headphones/headset/head unit. Within the API, support for these accessories is what "setAccessoryButtonEventsEnabled" actually controls.

When you enable accessory button events, the system then maps whatever media events it receives onto PTT channel events. As an aside, there are actually two reasons why we provide an API to control this instead of simply enabling this automatically:

  1. Some apps specifically DON'T want to use the media system this way. For example, some PTT apps are primarily focused on "receiving" message (not transmitting) and they want the usr to be able to listen and control their music, not the app.

  2. This mapping process works really well when the user knows what's going on and using it "intentionally", but can also create REALLY strange behavior when used "blindly". For example, virtually all car head units send a "play" command to bluetooth when a device connects, which means turning on your car can generate a "transmit" request.

b) BLE (Bluetooth Low Energy) controllers implement their own bluetooth profiles to notify the controller of changes on the accessory. My understanding is that there is some amount of informal standardization around the BLE profile, but that's not something Apple is involved with so I can't provide anymore detail than that. In terms of the systems role here, your app would use the CoreBluetooth framework to connect and communicate with the accessory and then control the PTT framework based on whatever happened on the accessory. Putting that in more concrete terms, your app would receive a characteristic change meaning "transmit" through CoreBluetooth and then call "requestBeginTransmittingWithChannelUUID" to start the transmission process.

In terms of this specific accessory:

I'm testing with a PrymeMax PTT device. This is the user guide: https://www.pryme.com/files/manuals/MANUAL-BTH-550-MAX.pdf

That's a BLE controller ("b"), so you'd need to use CoreBluetooth to support it yourself. The specifics of what that would involve are something you'd need to work with the hardware vendor to determine.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

There are a lot of generic devices like this one that I would like to support. Can somebody provide more information on how this is accomplished?

Starting with some background context, PTT control headset generally work in one of two ways:

a) They present themselves to the system through the standard media control profiles of classic bluetooth. As far as the system is concerned, the headset is EXACTLY the same as any other bluetooth headphones/headset/head unit. Within the API, support for these accessories is what "setAccessoryButtonEventsEnabled" actually controls.

When you enable accessory button events, the system then maps whatever media events it receives onto PTT channel events. As an aside, there are actually two reasons why we provide an API to control this instead of simply enabling this automatically:

  1. Some apps specifically DON'T want to use the media system this way. For example, some PTT apps are primarily focused on "receiving" message (not transmitting) and they want the usr to be able to listen and control their music, not the app.

  2. This mapping process works really well when the user knows what's going on and using it "intentionally", but can also create REALLY strange behavior when used "blindly". For example, virtually all car head units send a "play" command to bluetooth when a device connects, which means turning on your car can generate a "transmit" request.

b) BLE (Bluetooth Low Energy) controllers implement their own bluetooth profiles to notify the controller of changes on the accessory. My understanding is that there is some amount of informal standardization around the BLE profile, but that's not something Apple is involved with so I can't provide anymore detail than that. In terms of the systems role here, your app would use the CoreBluetooth framework to connect and communicate with the accessory and then control the PTT framework based on whatever happened on the accessory. Putting that in more concrete terms, your app would receive a characteristic change meaning "transmit" through CoreBluetooth and then call "requestBeginTransmittingWithChannelUUID" to start the transmission process.

In terms of this specific accessory:

I'm testing with a PrymeMax PTT device. This is the user guide: https://www.pryme.com/files/manuals/MANUAL-BTH-550-MAX.pdf

That's a BLE controller ("b"), so you'd need to use CoreBluetooth to support it yourself. The specifics of what that would involve are something you'd need to work with the hardware vendor to determine.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thank you for the quick response! This information really helps a lot. Could you provide more information about option "a" that you described above?

The linked documentation states, "If your app doesn’t map these button events to transmission actions...". Does this mean that my application needs to enable this functionality using setAccessoryButtonEventsEnabled and then map the button presses? If so, is this mapping process documented?

Should this button mapping work when my application is not in the foreground and the user is joined to the PTT channel ID that is specified when my app calls setAccessoryButtonEventsEnabled?

Which buttons in the standard media profile map to the various actions within the PTT UI? For example, does the play/pause button map to start/stop recording?

The linked documentation states, "If your app doesn’t map these button events to transmission actions...".

As a bit of background here, PushToTalk apps were originally designed and implemented without any direct support from the system. Basically, someone realized that the combination of the "voip" background category, mixable playback audio sessions, and PlayAndRecord + NowPlaying made it possible to make a reasonably good PTT app. PTT apps didn't have any direct API support until the iOS 13 CallKit requirements broke their previous implementation. Some creative workarounds allowed them to keep working until we introduce the PTT framework as the "full" solution.

That background is what leads to the phrasing above- most of the developers using the PTT framework aren't creating a new PTT app, they're PORTING their existing PTT (which works fine using the older workarounds) to the new architecture/framework.

What that sentence is actually saying "this flag replaces the old NowPlaying hacks you were using before and you can turn this to "false" if you won't want hardware buttons.

Does this mean that my application needs to enable this functionality using setAccessoryButtonEventsEnabled and then map the button presses?

No. As far as your application is concerned, you don't actually "do" anything. Making the behavior here concrete, if you plug in set of our wired headphones into your phone and "click" on the wire button, then:

setAccessoryButtonEventsEnabled == true-> your app will receive "didBeginTransmittingFromSource", allowing you to start a transmission.

setAccessoryButtonEventsEnabled == false-> nothing will happen.

If so, is this mapping process documented?

No, mostly because the "basic behavior" is "does what people expect" but the detailed implementation is a miserable mess of janky details. As a VERY vague summary, The bluetooth spec has two different system for "audio accessory commands", one for "playback only" (A2DP + AVRCP), one for "playback and recording" (HFP). Some accessories switch back and forth between them (so the same button actually sends different commands depending on accessory state), some accessories have different buttons for the different modes. There is an intermediate layer in this system that conceals many of these issues, however, that also means that it's pretty difficult to know EXACTLY what will happen on a particular piece of hardware:

As in example of "basic" vs "detail":

For example, does the play/pause button map to start/stop recording?

Basic Answer: Yep, that's what happens.

Actual Answer: What do you mean? Play/Pause are part of AVRCP, not HFP. There's no "Play" command once you're recording!

Summarizing all of these issues:

  1. The broad goal here is to allow users to use the hardware buttons of "common" bluetooth headsets to start transmissions.

  2. The expectation is that "pressing play" will start/stop transmission. If you experiment with common headsets, I believe you'll find that this works great.

  3. The bluetooth ecosystem is so large and diverse that there WILL be hardware that does weird/strange things when used with the PTT framework. The most direct example is car head units, many of which will end up doing weird things.

  4. There isn't any reliable way for the system to differentiate between those two cases (2 & 3), so apps are expected to work with their users to help them get the experience they want.

In practice, all of this works ends up working out pretty well. The kind of user who relies on hardware transmission tends to have a specific hardware configuration they use for this and are aware that other bluetooth devices can disrupt what they're doing. The most common failure points are also cases that would be really weird to use for PTT, so they also come up less than you'd think.

Should this button mapping work when my application is not in the foreground

Yes. The point of this API is to allow the user to use "their headset" to start transmitting from the background.

One side note on this point:

and the user is joined to the PTT channel ID that is specified when my app calls setAccessoryButtonEventsEnabled?

With the benefit of hindsight, the PTT API should NOT have used the term "channel" in the API, as it brings along to many expectations and implications. Within the PTT framework, there isn't really any "channel" as such. Your app only has one connection into the PTT system, which we happened to name "channelUUID". You should create it when your app starts up and you should not attempt to destroy/recreate it.

Critically, that connection has NOTHING to do with any other abstractions/concepts your app chooses to implement. The user choosing a different channel in your app doesn't mean that your channel manager should actually change.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

I would like to further follow up on this since I am exactly one of the developers who use PlayAndRecord + NowPlaying hacks, and now trying to migrate to use Push to Talk framework with its declared support of setAccessoryButtonEventsEnabled.

In short, I tried the Bluetooth headset (Powerbeats Pro) and pressing the button on the device cannot trigger didBeginTransmittingFromSource in a straightforward sample project which use PushToTalk framework and channel joined. Pressing the talk button on system UI also worked.

You did mention "wired headphones" in your comment but I did not have wired headphones with me so cannot verify. However, since both wired and bluetooth headsets are sending media events when pressing the button, I would expect it should also work for Bluetooth headset? My hacks of using NowPlaying center did work with Bluetooth headset when I press the button on it.

I am having this same issue. I see in the docs it says setAccessoryButtonEventsEnabled should be enabled by default. However when on a PTT channel, neither AirPods nor a bluetooth speaker response when using the play/pause actions. I can't find any example, and in the tutorials it says just enabled CoreBluetooth, but I've done that. Do you have any guidance on how to have the expected behaviour of triggering mute/unmute based on bluetooth device actions?

So a bit more detail on the problem. I'm using LiveKit, and it looks like the SDK configures some parts of the audio session. Currently the bluetooth accessories work like a audio call (mute/unmute) rather than like a PPT channel (I would expect transmit/stop transmitting).

Here is the default configuration the Livekit SDK does https://github.com/livekit/client-sdk-swift/blob/1f5959f787805a4b364f228ccfb413c1c4944748/Sources/LiveKit/Track/AudioManager.swift#L153


        DispatchQueue.webRTC.async { [weak self] in

            guard let self = self else { return }

            // prepare config
            let configuration = RTCAudioSessionConfiguration.webRTC()
            var categoryOptions: AVAudioSession.CategoryOptions = []

            if newState.trackState == .remoteOnly && newState.preferSpeakerOutput {
                configuration.category = AVAudioSession.Category.playback.rawValue
                configuration.mode = AVAudioSession.Mode.spokenAudio.rawValue

            } else if [.localOnly, .localAndRemote].contains(newState.trackState) ||
                        (newState.trackState == .remoteOnly && !newState.preferSpeakerOutput) {

                configuration.category = AVAudioSession.Category.playAndRecord.rawValue

                if newState.preferSpeakerOutput {
                    // use .videoChat if speakerOutput is preferred
                    configuration.mode = AVAudioSession.Mode.videoChat.rawValue
                } else {
                    // use .voiceChat if speakerOutput is not preferred
                    configuration.mode = AVAudioSession.Mode.voiceChat.rawValue
                }

                categoryOptions = [.allowBluetooth, .allowBluetoothA2DP]

            } else {
                configuration.category = AVAudioSession.Category.soloAmbient.rawValue
                configuration.mode = AVAudioSession.Mode.default.rawValue
            }

            configuration.categoryOptions = categoryOptions

            var setActive: Bool?

            if newState.trackState != .none, oldState.trackState == .none {
                // activate audio session when there is any local/remote audio track
                setActive = true
            } else if newState.trackState == .none, oldState.trackState != .none {
                // deactivate audio session when there are no more local/remote audio tracks
                setActive = false
            }

            // configure session
            let session = RTCAudioSession.sharedInstance()
            session.lockForConfiguration()
            // always unlock
            defer { session.unlockForConfiguration() }

            do {
                self.log("configuring audio session category: \(configuration.category), mode: \(configuration.mode), setActive: \(String(describing: setActive))")

                if let setActive = setActive {
                    try session.setConfiguration(configuration, active: setActive)
                } else {
                    try session.setConfiguration(configuration)
                }

            } catch let error {
                self.log("Failed to configure audio session with error: \(error)", .error)
            }
        }
    }
    #endif
}

Its possible to override, but there is no enum element for pushToTalk. Can I get any help? This has been very hard to get to a working example of what's claimed in the documentation: "accessory button events map to begin and end transmission actions" https://developer.apple.com/documentation/pushtotalk/ptchannelmanager/setaccessorybuttoneventsenabled%28_:channeluuid:completionhandler:%29?language=objc

Thank you

PTChannelManager setAccessoryButtonEventsEnabled documentation?
 
 
Q