PushToTalk: headset Stop works, but AVRCP Play routes to Music instead of starting transmission

With a joined PTChannelManager channel and setAccessoryButtonEventsEnabled(true) completing successfully, what supported condition makes a classic Bluetooth headset's AVRCP Play event start PTT transmission instead of routing to a media app?

The headset microphone and physical Stop button work. The next physical Start press produces no didBeginTransmittingFrom callback, while an on-screen Start request works again.

Tested environment

Observed on 3 October 2026:

  • iPhone 13 Pro Max, iOS 26.6.1.

  • Xcode 26.6, macOS 26.5.2.

  • boAt Rockerz 255 Pro+, classic Bluetooth A2DP/HFP.

  • Push to Talk capability, microphone permission, and audio/push-to-talk background modes enabled.

  • Default half-duplex transmission mode and Ready service status retained.

Isolation and setup

The reproduction uses an independent UIKit controller initialized before the host app runtime. No React Native, Expo audio module, Live Activity, MPRemoteCommandCenter, Now Playing metadata, audio player, transcription, or backend participates.

The controller retains its channel manager, leaves a restored channel before joining a fresh channel, and enables accessory button events in didJoinChannel.

The revised test configures the audio session at launch, before manager initialization:



let session = AVAudioSession.sharedInstance()


try session.setCategory(.playAndRecord,


                        mode: .voiceChat,


                        options: [.allowBluetoothHFP])


The relevant join callback is:



func channelManager(_ channelManager: PTChannelManager,


                    didJoinChannel channelUUID: UUID,


                    reason: PTChannelJoinReason) {


    channelManager.setAccessoryButtonEventsEnabled(true,


                                                   channelUUID: channelUUID) { error in


        // The test logs successful completion here.


    }


}


PTT owns audio activation/deactivation; the diagnostic never calls setActive. In didActivate, it reapplies the same category/mode/options, clears a speaker override, selects an available HFP input, waits for the route to settle, and starts an AVAudioEngine input tap. It verifies HFP input/output and incoming frame counts; samples are discarded. It removes the tap and stops the engine on Stop/deactivation.

The full focused sample was already supplied privately to DTS. Its controller was tested on the physical phone through the native-only launch path. The standalone Xcode project packaging passed an unsigned iPhoneOS build, but has not independently been installed on the phone.

Reproduction

  1. Connect the headset, launch the diagnostic, grant microphone permission, and join a fresh PTT channel.

  2. Confirm that enabling accessory events succeeds.

  3. Tap on-screen Start, which calls requestBeginTransmitting(channelUUID:).

  4. Wait for HFP input/output and incoming audio frames to be verified.

  5. Press and release the headset multifunction button. The delegate reports didEndTransmittingFrom with handsfree/headset source (raw 3); PTT deactivates audio and the channel remains joined.

  6. Press and release the same headset button again. Expected: PTT begin callback and microphone activation. Observed: no begin callback; Console shows AVRCP Play routed to Music.

  7. Tap on-screen Start again: transmission works. Headset Stop works again.

Selected evidence

The revised launch-configuration test produced this native trace on 3 October (UTC):



04:32:10 launch: native-only; launch audio configuration v2


04:32:10 category=PlayAndRecord; mode=VoiceChat; options=4


04:32:11 fresh channel joined


04:32:11 accessory mapping accepted


04:32:19 app start requested


04:32:19 BEGIN source=2


04:32:20 available inputs=MicrophoneBuiltIn,BluetoothHFP


04:32:20 discard engine running


04:32:21 HEADSET VERIFIED: BluetoothHFP input/output; frames=4096


04:32:22 END source=3


04:32:23 audio deactivated


No physical-headset BEGIN appeared in the native event snapshot copied at 04:32:49 UTC. Selected Console observations from the same run, converted from IST to UTC:



04:32:30.529053 AVRCP Play routed to com.apple.Music


04:32:30.535720 Music launch failed: NotFound


04:32:35.852134 the same command timed out after 5.326288 seconds


Music had been removed during troubleshooting. Before removal, the user observed Music opening; the supplied timestamped excerpts were captured after removal. Removal explains the launch failure, but does not explain why the joined PTT channel is bypassed.

An earlier native-only fresh-channel control also showed Play routing to Music before any app-requested transmission, with no BEGIN or audio activation in the callback trace. That earlier control configured the category in didActivate; it was not a fresh-start test of the revised launch-configuration variant. The revised test above establishes that configuring the category at launch did not fix the post-Stop failure.

Questions

I reviewed the accessory mapping explanation in https://developer.apple.com/forums/thread/804205 and the launch audio configuration guidance in WWDC22 session 10117.

  1. Is a discrete AVRCP Play command from this type of consumer headset supported for PTT Start in this state, or is a different command form required?

  2. Does the playback-mode qualifier in that forum guidance require an additional supported setup step? Could the category/route sequence described above bypass accessory mapping despite successful completion?

  3. What targeted diagnostic or controlled configuration would distinguish accessory-command compatibility from an iOS routing defect?

The observations establish a routing/callback mismatch in this tested configuration; I am not assuming an iOS defect or that the headset is unsupported.

With a joined PTChannelManager channel and setAccessoryButtonEventsEnabled(true) completing CFPlugin-based what supported condition makes a classic Bluetooth headset's AVRCP Play event start PTT transmission instead of routing to a media app?

Unfortunately, there isn't one. We looked at (and initially implemented) having setAccessoryButtonEventsEnabled take over AVRCP, but that functionality was ultimately disabled due to weird behavior/privacy concerns. FYI, the main issue here is that the vast majority of car head units automatically issue a "play" command when the accessory attaches, which would mean you started transmitting every time you started your car. We've looked into ways to mitigate these issues, but unfortunately, we couldn't find any way to reliably identify/filter these accessories, leading to the current state of things. If you haven't already, I'd encourage you to file a bug asking for the PTT framework to support, then post the bug number back here. This is obviously a known issue, but we're still interested in understanding developer interest in this.

In terms of what "setAccessoryButtonEventsEnabled" does today, its main impact is to widen support for wired USB HID headsets.

In terms of what you're seeing here:

The headset microphone and physical Stop button work.

...this is actually an (expected) side effect of the headset's own behavior. When you started transmitting, the headset switched to HFP, which means the control event stop sent was an HFP event (basically, "hang up"), not AVRCP. That was automatically routed to our "call" infrastructure, which then ended the transmission.

The next physical Start press produces no didBeginTransmittingFrom callback, while an on-screen Start request works again.

...and then the headset switched back to AVRCP, breaking event delivery.

If you want to support AVRCP, you'll need to make your app the Now Playing app and call "requestBeginTransmittingWithChannelUUID" yourself. A few things to be aware of if you decide to implement this:

  1. I haven't tried this with the new Now Playing framework, just MPNowPlayingInfoCenter. I'd expect the new framework to work fine, but haven't tested it to be sure.

  2. There is a "block" in place to prevent apps from activating a recording session from the background, which will also prevent requestBeginTransmittingWithChannelUUID from working. To bypass that block, you need to set your playbackState to MPNowPlayingPlaybackStatePlaying before you call requestBeginTransmittingWithChannelUUID.

  3. The playbackState transition is asynchronous, so you'll need to change the state, return from the event handler, then send requestBeginTransmittingWithChannelUUID. Note that the playback state here is specifically about the now playing state, NOT your audio session, so you can be "playing" even though your audio session hasn't actually activated.

  4. Be aware that CoreBluetooth also affects the same "block" I referenced in #2, which means having your app authorized to use Bluetooth will change the details of when your app is allowed to start transmitting. If your app supports Bluetooth as well, make sure you specifically test and debug your now playing support without Bluetooth authorization.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware