Help us understand the problem. What is going on with this article?

Classic Bluetooth について iOS アプリ開発者ができること

More than 5 years have passed since last update.

Bluetooth Low Energy については Core Bluetooth で色々と制御できますが、Classic Bluetooth(以降クラシックBT)については基本的に開発者は制御できません1

bt.jpg

そう、確かにアプリ内からクラシックBTデバイスと接続したり、データを送るとか送らないとか制御したり、通信を切断したり、といったことはできないのですが、そんな中でもアプリ側から一切クラシックBTデバイスの存在を感知することができないのかというとそうでもなく、いくつかの「口」はあります。

というわけでそういう「口」を集めてみました。他にもあればぜひ教えてください!

ボタン操作イベントの取得

BTイヤフォンの再生・停止等のボタン操作イベントを取得するには、次のように取得開始を宣言し、かつファーストレスポンダになります。

UIApplication.sharedApplication().beginReceivingRemoteControlEvents()
self.becomeFirstResponder()

イベントが飛んで来ると、remoteControlReceivedWithEvent: が呼ばれます。

override func remoteControlReceivedWithEvent(event: UIEvent?) {
    guard event?.type == .RemoteControl else { return }

    if let event = event {

        switch event.subtype {

        case .RemoteControlPlay:
            print("Play")
        case .RemoteControlPause:
            print("Pause")
        case .RemoteControlStop:
            print("Stop")
        case .RemoteControlTogglePlayPause:
            print("TogglePlayPause")
        case .RemoteControlNextTrack:
            print("NextTrack")
        case .RemoteControlPreviousTrack:
            print("PreviousTrack")
        case .RemoteControlBeginSeekingBackward:
            print("BeginSeekingBackward")
        case .RemoteControlEndSeekingBackward:
            print("EndSeekingBackward")
        case .RemoteControlBeginSeekingForward:
            print("BeginSeekingForward")
        case .RemoteControlEndSeekingForward:
            print("EndSeekingForward")
        default:
            print("Others")
        }
    }
}

ただし、オーディオセッションカテゴリによっては、イベントがアプリまで飛んでこなくなります(OSでは拾っているが、アプリまで渡してくれない)。試したところでは、AVAudioSessionCategoryPlayback では取得可、AVAudioSessionCategoryPlayAndRecord では取得不可。

MPRemoteCommandCenter

上記の方法は今でも deprecated になってはいないものの、リファレンスによると、

In iOS 7.1 and later, use the shared MPRemoteCommandCenter object to register for remote control events. You do not need to call this method when using the shared command center object.

と、iOS 7.1 以降では MPRemoteCommandCenter を使うように書かれています。

MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
[commandCenter.playCommand addTargetUsingBlock:^(MPRemoteCommandEvent *event) {
    // Begin playing the current track.
    [[MyPlayer sharedPlayer] play];
}

参考: Remote Control Events - Event Handling Guide for iOS

要注意!

記事を書きながら、念のため手元でシンプルなデモをつくって試したところこれがなぜか動作しない・・・!(BTイヤフォンのボタンイベントが飛んでこない)以前、試したときは動いたのに・・・

もちろんファーストレスポンダになるのを忘れる、というよくあるハマりポイントはクリアしています。過去に試したときと条件を合わせるため、同じBTデバイスを使い、iOS 8 で、同じオーディオセッションカテゴリ(Playback)を用いてもダメ。

で、ふと気付きました。

システムの音楽プレイヤーがボタンイベントを捕まえている状況だとアプリまでイベントが飛んで来ないのではないかと。ここでいう「システムの音楽プレイヤー」とは、iOS標準のミュージックアプリおよび [MPMusicPlayerController systemMusicPlayer] でアクセスできるプレイヤーのことです。

そこで AVAudioPlayer を使ってアプリ内で音楽を再生しつつクラシックBTデバイスのボタンを操作してみたところ・・・動作しました!

というわけで上述の仮説で合っているようです。

接続/切断をフック

これもイヤフォンやヘッドセット等のオーディオデバイス限定にはなりますが、そういったデバイスが接続/切断されるとき、オーディオの入力・出力ルート(iOS の世界では Audio Route と呼ばれる)の変更通知 AVAudioSessionRouteChangeNotification が飛んできます。

というわけでこれを監視しておき、

NSNotificationCenter.defaultCenter().addObserverForName(
    AVAudioSessionRouteChangeNotification,
    object: nil,
    queue: nil) { (notification) -> Void in

        // 通知を受け取ったときの処理
}

この状態でイヤフォンやヘッドセットをつなぐと、オーディオルート変更通知が飛んでくるので、そこで今のオーディオルートの入力ポートと出力ポートがそれぞれ何なのか、ということを知ることができます。

AVAudioSessionRouteDescription *route = [[AVAudioSession sharedInstance] currentRoute];
AVAudioSessionPortDescription *inPort  = route.inputs.firstObject;
AVAudioSessionPortDescription *outPort = route.outputs.firstObject;
let route  = AVAudioSession.sharedInstance().currentRoute
let inPort  = route.inputs.first
let outPort = route.outputs.first

ここで、AVAudioSessionPortDescriptionportType からそのポートのタイプを取得でき、たとえば A2DP プロファイルのBTイヤフォンを繋いだ場合は、portTypeAVAudioSessionPortBluetoothA2DP (中身の文字列は BluetoothA2DPOutput)に、HFP プロファイルのBTヘッドセットを繋いだ場合は AVAudioSessionPortBluetoothHFP になるので、これを用いればクラシックBTデバイスの接続・切断をフックできることになります。

ただ、この方法も万能ではなくて、音楽再生中とか、録音中とか、オーディオデバイスが機能している状態じゃないと変更通知が飛んできません。(詳細な条件は要調査)

いつでもどうしても確実にフックしたい場合は currentRoute をポーリングするとかになるのかと思います(が、やったことないのでこれも何か思わぬ落とし穴があるかもしれません)。

接続デバイス名の取得

前項にて現在のオーディオルート情報 AVAudioSessionRouteDescription を取得し、そこから入力/出力ポートの情報を保持する AVAudioSessionPortDescription オブジェクトを取り出しました。

この portName プロパティから、デバイス名を取得できます。

let route  = AVAudioSession.sharedInstance().currentRoute
let outPort = route.outputs.first
if let outPort = outPort {
    print("out port name:\(outPort.portName)")
}

(BTイヤフォン接続時の実行結果)

out port name:Jabra Rox Wireless v2.5.4

(これ以外に、UID というプロパティもありますが、xx:xx:xx:xx:xx:xx-tacl とMACアドレスらしきものが入っている場合があったり、単に Wired Headphones と入っている場合があったりで、統一性がなく何かの判別には使いづらいかなと感じました)

入力・出力デバイスの選択

AVAudioSessionRouteDescriptioninputsoutputs と、複数の入出力ポートを持てるようになっています。AVAudioSession に setPreferredInput: というメソッドがあるので、入力デバイスは明示的に選択できそうです。

/* Select a preferred input port for audio routing. If the input port is already part of the current audio route, this will have no effect.
   Otherwise, selecting an input port for routing will initiate a route change to use the preferred input port, provided that the application's
   session controls audio routing. Setting a nil value will clear the preference. */

public func setPreferredInput(inPort: AVAudioSessionPortDescription?) throws

ただ setPreferredOutput: というメソッドは見当たらないので、出力デバイスが複数ある場合はどうするんでしょうか。inputと連動するのか、別のメソッドがあるのか。まだこのあたりは試したことないので、今度挙動を確かめてみます。

つづく

AVAudioSession のヘッダを見ているとまだありそうなので、引き続き何か分かり次第追記していきます。


  1. MFiプログラム締結時(External Accessory framework 利用可能時)は例外 

shu223
フリーランスiOSエンジニア 著書:『iOS×BLE Core Bluetooth プログラミング』『Metal入門』『実践ARKit』『Depth in Depth』『iOSアプリ開発 達人のレシピ100』他 GitHubの累計スター数24,000超
http://shu223.hatenablog.com/
engineerlife
技術力をベースに人生を謳歌する人たちのコミュニティです。
https://community.camp-fire.jp/projects/view/280040
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away