Eddie
Eddie

Reputation: 1941

Control AVAudioRecorder from bluetooth commands

I've got this app that allows users to record stuff around. Then when they have Bluetooth accessory (say, headset), they should be able to start/stop the recording session by the play/pause button on the Bluetooth device.

I've successfully implemented the starting session using MPRemoteCommandCenter`

let rcCenter = MPRemoteCommandCenter.shared()
rcCenter.nextTrackCommand.isEnabled = false
rcCenter.nextTrackCommand.addTarget { _ in return .success }

rcCenter.previousTrackCommand.isEnabled = false
rcCenter.previousTrackCommand.addTarget { _ in return .success }

rcCenter.togglePlayPauseCommand.isEnabled = true
rcCenter.playCommand.isEnabled = true
rcCenter.pauseCommand.isEnabled = true
rcCenter.stopCommand.isEnabled = true

rcCenter.togglePlayPauseCommand.addTarget(self, action: #selector(remotePlayPauseAction(_:)))
rcCenter.playCommand.addTarget(self, action: #selector(remotePlayPauseAction(_:)))
rcCenter.pauseCommand.addTarget(self, action: #selector(remotePlayPauseAction(_:)))
rcCenter.stopCommand.addTarget(self, action: #selector(remotePlayPauseAction(_:)))

However, it never runs into my pause/stop action from bluetooth. I wasn't aware of what happened until I saw this in device's console:

default 18:19:46.609673 +0700 bluetoothd Received 'get play status' request from device

default 18:19:46.671092 +0700 bluetoothd Received AVRCP Play command from device

// sending audio session states because app is recording blah blah...

default 18:19:48.246780 +0700 bluetoothd AudioSendThread starting

This triggered when app not recording -> receive a play command from bluetoothhd -> start recording -> sending session to bluetooth.

And then, I'll press the play/pause button again and get this log:

default 18:20:09.650855 +0700 bluetoothd Received call hangup event (AT+CHUP) from device default 18:20:09.651273 +0700 bluetoothd Found ongoing virtual call - Acking device and notifying upper layers.

=> it looks like the phone is receiving end call signal (note that there's no call in progress, just a recording in session)

So, how do I handler this event? I've tried to used CallKit but still no use:

let callCenter = CXCallObserver()
callCenter.setDelegate(self, queue: nil)

func callObserver(_ callObserver: CXCallObserver, callChanged call: CXCall) {
   print("Hi!") // not jump in!
}

Update

I've tried to switch back to old style handler:

UIApplication.shared.beginReceivingRemoteControlEvents()
becomeFirstResponder()
/////////////////
override func remoteControlReceived(with event: UIEvent?) {
    guard let event = event, event.type == .remoteControl else { return }
   // Start / pause actions
}

And still got the same result (can start but can't stop, getting events play and call hangup just like before). Now my guess has switched to AVAudioSession's mode and categories but still got no clues.

do {
    let session = AVAudioSession.shared()
    // What is the correct params here? 
    // try session.setCategory(.playAndRecord, mode: .voiceChat, options: [.allowBluetoothA2DP, .allowBluetooth])
    // try session.setCategory(.record, options: .allowBluetooth)
    session.setActive(true)
} catch { print(error) }

Upvotes: 1

Views: 427

Answers (1)

Jeremy Jao
Jeremy Jao

Reputation: 380

The problem seems to be bluetooth hardware-related. Sorry I can't give you much if any advice here.

I'm doing some ridiculous C++ recording logic that doesn't use AVAudioProducer (I'm using the AudioToolBox Framework) but I'm essentially getting the same issue as you.

All the market devices I've tried (Jabra OTE23, Jaybird X3, Sony WH1000XM2, official Star Trek Communication Badge) exhibit the same behavior where whenever the microphone is on, the bluetooth device will think it's on a virtual call and will only emit call AVRCP events. There doesn't seem to be a way for iOS to listen to these call AVRCP events for some reason.

There is one internal device in my company that (probably accidentally) emits BOTH pause and hangup events whenever the microphone is on and device is in a recording state. As a result, I'm able to pause and resume recording. Do note however this device was specifically designed for recording audio and whatnot. Logs here for proof:

default 15:38:53.994336 -0500   bluetoothd  Received call hangup event (AT+CHUP) from device <private>
default 15:38:53.998181 -0500   bluetoothd  Found ongoing virtual call - Acking device and notifying upper layers.
default 15:38:54.001477 -0500   locationd   service mask 0x1 event type 2 event 104 result 0
default 15:38:54.186054 -0500   powerlogHelperd {"msg":"CLCopyAppsUsingLocation", "event":"activity"}
default 15:38:54.473038 -0500   bluetoothd  Received AVRCP Pause command from device <private>
default 15:38:54.475637 -0500   BTAvrcp Request: Command = <Pause>, SenderDevice = <M’s iPhone>, SenderBundleIdentifier = <BTAvrcp>, SenderPID = <419>, commandID = <B46341E9-7B98-4ACE-94A3-D84A8F4A63B1>, remote control interface = <com.apple.AVRCP>, appOptions = <0>, options = <{kMRMediaRemoteOptionSourceID = "00:13:8A:20:1C:70";}> (null) for origin-M’s iPhone-1280262988/client-com.mmodal.SDK.SettingsTest-1015 (SettingsTest)/player-MediaRemote-DefaultPlayer

Why is this device acting differently than the other mainstream devices? I don't know yet. I really just wish iOS (and Android) had a way to listen to all raw AVRCP events, but that doesn't seem to be the case right now.

Bluetooth is a crazy tech stack...

Upvotes: 1

Related Questions