Bluetooth AudioデバイスからTHETAを操る

## はじめに

リコーの @mShiiina です。

弊社では[RICOH THETA](https://theta360.com/ja/)という全周囲360度撮れるカメラを出しています。
<B>RICOH THETA V</B>や<b>RICOH THETA Z1</b>は、OSに<B>Android</B>を採用しています。Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます（詳細は本記事の末尾を参照）。

2020年6月のFWアップデートにより、プラグインでBluetooth Classicデバイスを使用できるようになりました。

今回は、Bluetoothイヤホン、Bluetoothオーディオを使用してリモート撮影ができるプラグインを作ってみました。動画はこんな感じです。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">◤Qiita記事公開◢<br>6/17のFWアップデートにより、THETAプラグインからBluetooth Classicも利用可能になりました！<br>今回は、その中でもオーディオ機器との連携例を紹介します。完全独立イヤホンがTHETAのリモコンに！<a href="https://t.co/uXQaoK6wFS">https://t.co/uXQaoK6wFS</a><a href="https://twitter.com/hashtag/THETA?src=hash&amp;ref_src=twsrc%5Etfw">#THETA</a> <a href="https://twitter.com/hashtag/thetaplugin?src=hash&amp;ref_src=twsrc%5Etfw">#thetaplugin</a> <a href="https://twitter.com/hashtag/Bluetooth?src=hash&amp;ref_src=twsrc%5Etfw">#Bluetooth</a> <a href="https://twitter.com/hashtag/%E5%AE%8C%E5%85%A8%E7%8B%AC%E7%AB%8B%E3%82%A4%E3%83%A4%E3%83%9B%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#完全独立イヤホン</a> <a href="https://t.co/d04cXhSeKh">pic.twitter.com/d04cXhSeKh</a></p>&mdash; THETAプラグイン開発者コミュニティ (@thetaplugin) <a href="https://twitter.com/thetaplugin/status/1278145145121075200?ref_src=twsrc%5Etfw">July 1, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script><br/><br/><br/>

**<2021/6/2 Update 情報>**
-[「ダイソーの330円片耳イヤホン（新型）でフルコントロール出来ました」](#ダイソーの330円片耳イヤホン新型でフルコントロール出来ました202162-追記)の章を追加しました。


**<2020/7/30 Update情報>** 
- 日英の二ヶ国語に対応しました。初期値は英語です。無線ボタン長押しで言語切り替えできます。プラグイン終了時に言語設定が保存されます。
- 音声ファイルをすべてAmazon Pollyで生成しなおしました。音質が向上しています。

[![](https://img.youtube.com/vi/NN33SM5_SJQ/0.jpg)](https://www.youtube.com/watch?v=NN33SM5_SJQ)

**<2020/11/24 Update情報>**
この記事のプラグインを[RICOH THETA Plug-in STORE](https://pluginstore.theta360.com/plugins/skunkworks.headset/)に公開しました！以下の点が改善されています。

- プラグイン起動前のBluetooth関連設定（スマホ連携あり/なし、リモコン機能 On/Off）を保存＆復旧するようにしました。
- プラグイン起動前の音量の保存と復旧、プラグイン動作中の音量保存と復帰をするようにしました。

以下のBluetoothAudioデバイスとTHETAを接続し、THETAをリモートで操作する方法をご紹介します。
・Bluetoothイヤホン：Anker Soundcore Liberty Air
・Bluetoothイヤホン：JPRiDE TWS-520
・Bluetoothオーディオ：Anker Soundcore Icon Mini

上記以外にも、いくつか動作確認をして末尾の章に一覧を掲載しました。その章をみて頂けるとわかるのですが、相性（接続可/不可）や使い勝手はオーディオ機器によってマチマチと思われます。私たちが全ての機器を試すことは不可能でして、、、かならずしもお持ちのイヤホンで使えるとは限らない点はご容赦ください。（とはいえ、AppleのAir Pods Proですら動いたし、大抵は大丈夫はなず！）

BluetoothAudioデバイスの操作に対してTHETAの以下の振る舞いを割り当て可能としました。
1. 静止画撮影
2. 露出補正の設定
3. 通常通知音の音量設定

露出補正の設定時は、[こちらの記事](https://qiita.com/KA-2/items/e847c780cc0fee187f89)にならって、音声を再生し設定値を読み上げています。

使用するには、以下バージョン以降のファームウェアが必要です。最新ファームウェアへのアップデートをお忘れなく！
・THETA V  : 3.40.1
・THETA Z1 : 1.50.1

## Bluetoothプロファイル

本プラグインは以下のプロファイルに対応しているBluetoothデバイスを使用できます。
・Headset Serviceプロファイル (HSP) ※マイクは使用できません。
・Audio/Video Remote Controlプロファイル (AVRCP)

## Bluetoothイヤホンの接続、操作

Anker Soundcore Liberty AirとJPRiDE TWS-520を使用しました。

**Anker Soundcore Liberty Air**

[Amazon](https://www.amazon.co.jp/Soundcore-Air%EF%BC%88%E3%83%AF%E3%82%A4%E3%83%A4%E3%83%AC%E3%82%B9%E3%82%A4%E3%83%A4%E3%83%9B%E3%83%B3-5-0%EF%BC%89%E3%80%90Qualcomm%C2%AE-cVc8-0%E3%83%8E%E3%82%A4%E3%82%BA%E3%82%AD%E3%83%A3%E3%83%B3%E3%82%BB%E3%83%AA%E3%83%B3%E3%82%B0-PSE%E8%AA%8D%E8%A8%BC%E6%B8%88%E3%81%BF%E3%80%91%EF%BC%88%E3%83%96%E3%83%A9%E3%83%83%E3%82%AF%EF%BC%89/dp/B07SPZPJ28)で7000円程度で購入できます。<br>
対応プロファイル：HSP、AVRCP<br>
![1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263023/11a288f1-eb75-319a-0088-28927186dda0.jpeg)


**JPRiDE TWS-520**

[amazon](https://www.amazon.co.jp/%E5%AE%8C%E5%85%A8%E3%83%AF%E3%82%A4%E3%83%A4%E3%83%AC%E3%82%B9%E3%82%A4%E3%83%A4%E3%83%9B%E3%83%B3-JPRiDE-TWS-520-%E5%90%84%E7%A8%AE%E9%9F%B3%E5%A3%B0%E3%82%A2%E3%82%B7%E3%82%B9%E3%82%BF%E3%83%B3%E3%83%88%E5%AF%BE%E5%BF%9C-%E6%9D%B1%E4%BA%AC%E7%99%BA%E3%82%AA%E3%83%BC%E3%83%87%E3%82%A3%E3%82%AA%E3%83%96%E3%83%A9%E3%83%B3%E3%83%89%E3%80%91/dp/B07Q9YDCNX)で5500円程度で購入できます。<br>
対応プロファイル：HSP、AVRCP<br>
![2.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263023/9542effa-4186-bfbb-6f70-7675e20d40a8.jpeg)


接続手順：
1.Soundcore Liberty Air または TWS-520 をペアリングモードにする
2.プラグインを立ち上げる(最長12秒間Bluetooth Classicデバイスを検索する)
3.接続されるまで待機する

イヤホン操作によるTHETA動作割り当て：
Bluetoothイヤホンには、タッチ式のマルチファンクションボタンがついており、接続デバイスを操作できます。今回は以下のTHETA動作を割り当てました。

|イヤホン操作|THETA動作|
|----|----|
|再生|静止画撮影|
|一時停止|静止画撮影|
|曲送り|露出補正を1ステップアップ|
|曲戻し|露出補正を1ステップダウン|

## Bluetoothオーディオの接続、操作

Anker Soundcore Icon Mini を使用しました。<br>
[Amazon](https://www.amazon.co.jp/Soundcore-Bluetooth-%E3%82%B3%E3%83%B3%E3%83%91%E3%82%AF%E3%83%88%E3%82%B9%E3%83%94%E3%83%BC%E3%82%AB%E3%83%BC-Anker%EF%BC%89%E3%80%90IP67%E5%AE%8C%E5%85%A8%E9%98%B2%E6%B0%B4%E3%83%BB%E9%98%B2%E5%A1%B5%E4%BB%95%E6%A7%98-%E3%83%91%E3%83%83%E3%82%B7%E3%83%96%E3%83%A9%E3%82%B8%E3%82%A8%E3%83%BC%E3%82%BF%E3%83%BC%E6%90%AD%E8%BC%89/dp/B07LCFFW35/ref=sr_1_2?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=soundcore+icon+mini&qid=1592460996&s=electronics&sr=1-2)で3000円以下で購入できます。
対応プロファイル：Audio/Video Remote Controlプロファイル (AVRCP)<br>
![3.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263023/7a37eae5-d122-d116-6fda-548c5fafae88.jpeg)


オーディオ操作によるTHETA動作割り当て：<br>
Bluetoothオーディオには、ボタンがついており、接続デバイスを操作できます。今回は以下のTHETA動作を割り当てました。

|オーディオ操作|THETA動作|
|----|----|
|再生|静止画撮影|
|一時停止|静止画撮影|
|曲送り|露出補正を1ステップアップ|
|曲戻し|露出補正を1ステップダウン|
|音量アップ|通常通知音の音量1ステップアップ|
|音量ダウン|通常通知音の音量1ステップダウン|

接続手順：
1.Soundcore Icon Miniをペアリングモードにする
2.プラグインを立ち上げる(最長12秒間Bluetooth Classicデバイスを検索する)
3.接続されるまで待機する


## THETA本体の操作

THETA操作によるTHETA動作割り当て：

|THETA操作|THETA動作|
|----|----|
|シャッターボタン短押し|静止画撮影|
|無線ボタン短押し|通常通知音の音量1ステップアップ|
|モードボタン短押し|通常通知音の音量1ステップダウン|
|無線ボタン長押し|日英言語切り替え|

## LED表示

THETA VではBluetoothデバイスの検索、接続、切断状態をLED3に表示します。

|LED3状態|内容|
|----|----|
|黄色点灯|Bluetoothデバイス検索中|
|青色点灯|Bluetoothデバイス検索終了 or Bluetoothデバイス切断|
|白色点灯|Bluetoothデバイス接続済み|

## OLED表示

THETA Z1ではBluetoothデバイスの検索、接続、切断状態をOLEDの3段目に表示します。

|OLED3段目状態|内容|
|----|----|
|DISCOVERY STARTED|Bluetoothデバイス検索中|
|DISCOVERY FINISHED|Bluetoothデバイス検索終了|
|CONNECTED|Bluetoothデバイス接続済み|
|DISCONNECTED|Bluetoothデバイス切断|

## webUIのKeyCode表示

webUIでプラグインが受け取ったKeyCodeを表示します。<br>
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263023/7cc15654-02bf-2f1e-99d1-ccf62e7ec21e.png)

## 制約

~~[新しく追加されたWebAPIのOptions:BluetoothRole](https://api.ricoh/docs/theta-web-api-v2.1/options/_bluetooth_role/)がCentral、Central_Peripheralの場合（=通常の動作でリモコン機能をONとしている場合）は、使用できません。THETA側でBluetooth Classicの検索処理の実行/停止/接続処理を繰り返しているためです。~~

~~プラグイン起動時の退避と終了時の復旧も可能ですが、今回は実装を省略しています。後半の「THETAアプリ側のリモコン検索機能の抑制」のところで退避と復旧の方法について触れていますのでお好みで実装を追加してください。~~

**<2020/11/24 Update情報>**
RICOH THETAプラグインストアへの公開に伴い、プラグイン起動時の退避と終了時の復旧に対応しました！



##プログラムの説明

ソースコードは[こちら](https://github.com/theta-skunkworks/theta-plugin-bluetooth-headset-sample)です。

### 全体説明

[THETAプラグインSDK](https://github.com/ricohapi/theta-plugin-sdk)をベースに
~\app\src\main\java\com\theta360\pluginapplication 配下の以下ファイルとMainActivityに手を加えました。

ファイル説明

|ファイル|説明|
|----|----|
|MainActivity.java|各種タスクの起動とキー割り当て操作を行います|
|bluetooth/BluetoothClientService.java|Bluetooth Classicデバイスの検索、接続、切断を行います|
|bluetooth/BluetoothDeviceReceiver.java|Bluetooth関連Intentのブロードキャストレシーバーです|
|bluetooth/MediaReceiver.java|AVRCPプロファイルの音量変更Intentのブロードキャストレシーバーです
|task/ChangeCaptureModeTask.java<br>task/ChangeEvTask.java<br>task/ChangeEvTask.java<br>task/ShutterButtonTask.java<br>task/SoundManagerTask.java|[こちら](https://qiita.com/KA-2/items/e847c780cc0fee187f89)にならって作成したファイルです|
|EnableBluetoothClassicTask.java |Bluetooth Classicの有効/無効を切り替えます|
|task/ChangeVolumeTask.java|音量を変更します|

### マニフェスト ファイル

本プラグインでは、AndroidManifest.xmlにパーミッションとサービスを追加しています。

**パーミッション**

Bluetooth Classicデバイスを使用するためには、以下のパーミッションを定義しています。

``` AndroidMnifestfile.xml
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
```

開発者としてプログラムを実行するときには、VysorからSettings->App->HEADSET Remoteの「Location」のパーミッションを手動で有効にしてください。
本プラグインは[公式プラグインストア](https://pluginstore.theta360.com/) での公開を予定しています。>>> [公開](https://pluginstore.theta360.com/plugins/skunkworks.headset/)しました！（2020/11/24 追記）
[公式プラグインストア](https://pluginstore.theta360.com/)からインストールした場合は、インストール時にパーミッションが有効になるため、手動での設定は必要ありません。

**サービス**

Bluetooth Classicデバイスを扱うサービスを追加しています。

``` AndroidMnifestfile.xml
        <service android:name="com.theta360.pluginapplication.bluetooth.BluetoothClientService" />
```

### メイン処理(MainActivity.java)

既存の雛形部分（キーコード解釈部）
基本的には、受け取ったkeyCodeに対応するアクションを以下コードで実行するだけです。

```MainActivity.java
execKeyProcess(keyCode2KeyProcess(keyCode));
```

### Bluetooth Classic有効/無効切り替え

Bluetooth Classicを有効にするために、プラグイン起動時にまずEnableBluetoothClassicTaskを生成します。

```MainActivity.java
        new EnableBluetoothClassicTask(getApplicationContext(),EnableBluetoothClassicTask).execute();
```

タスク内で[WebAPI Options `_bluetoothClassicEnable`](https://api.ricoh/docs/theta-web-api-v2.1/options/_bluetooth_classic_enable/)をtrueに設定し、Bluetooth Classicを有効にします。

```EnableBluetoothClassicTask.java
        HttpConnector camera = new HttpConnector("127.0.0.1:8080");
        String errorMessage = camera
                .setOption("_bluetoothClassicEnable", Boolean.toString(Boolean.TRUE));
```

Bluetoothモジュールの電源[WebAPI Options `_bluetooth_power` ](https://api.ricoh/docs/theta-web-api-v2.1/options/_bluetooth_power/)がOFFの場合は、ONに変更して完了です。

```EnableBluetoothClassicTask.java
       String bluetoothPower = camera.getOption("_bluetoothPower");
        if (bluetoothPower.equals("OFF")) {
            errorMessage = camera.setOption("_bluetoothPower", "ON");
            if (errorMessage != null) { // パラメータの設定に失敗した場合はエラーメッセージを表示
                return "NG";
            }
        }
```

Bluetooth Classicの有効/無効の変更を反映させるには、Bluetoothモジュールの電源OFF/ONを行う必要があります。
`_bluetoothClassicPower`がONの状態で`_bluetoothClassicEnable`を変更するとTHETAアプリ側はBluetoothモジュールの電源OFF/ONを自動で行います。


### Bluetooth Classicデバイスの検索、接続

Bluetooth Classicを有効にした後は、Bluetooth Classicデバイスの検索、接続を行います。こちらは、AndroidのBluetoothのAPIを使用します。

ざっくりとした流れは以下になります。
1. サービスを生成する
2. 検索を開始する
3. 発見したデバイスをペアリングする
4. ペアリングしたデバイスを接続する

まずサービスを生成します。

``` MainActivity.java
    private EnableBluetoothClassicTask.Callback mEnableBluetoothClassicTask = new EnableBluetoothClassicTask.Callback() {
        @Override
        public void onEnableBluetoothClassic(String result) {
            getApplicationContext().startService(new Intent(getApplicationContext(), BluetoothClientService.class));
        }
    };
```

次にBluetooth Classicデバイスの検索を開始します。
こちらは、Androidの仕様で開始から12秒後に停止します。検索中の場合は、一度停止してから開始します。

``` BluetoothClientService.java
    private void startClassicScan() {
        if (mBluetoothAdapter.isDiscovering()) {
            mBluetoothAdapter.cancelDiscovery();
        }
        boolean isStartDiscovery = mBluetoothAdapter.startDiscovery();
        Log.d(TAG, "startClassicScan :" + isStartDiscovery);
    }
```

接続対象のデバイスを発見した場合、ペアリングを行います。
接続対象のデバイスは、[CoD](https://www.bluetooth.com/specifications/assigned-numbers/baseband/)で判断します。
CoDは、[BluetoothSIG](https://www.bluetooth.com/ja-jp/)で定められたデバイスの用途を識別する値です。

|Device Type|Major Device Class|Minor Device Class field|値|
|----|----|----|----|
|Bluetoothオーディオ|Audio/Video|Wearable Headset Device|1028|
|Bluetoothイヤホン|Audio/Video|Headphones|1048|


``` BluetoothClientService.java
        @Override
        public void onFound(BluetoothDevice bluetoothDevice,
                BluetoothClass bluetoothClass,
                int rssi) {
            String name = bluetoothDevice.getName();
            Log.d(TAG, "name" + name);
            if (name != null) {
                int type = bluetoothDevice.getType();
                Log.d(TAG, "type" + String.valueOf(type));
                if (type == bluetoothDevice.DEVICE_TYPE_CLASSIC || type == bluetoothDevice.DEVICE_TYPE_DUAL) {
                    int classNo = bluetoothClass.getDeviceClass();
                    Log.d(TAG, "class" + classNo);
                    if ((classNo == COD_AUDIO_VIDEO_HEADPHONES) || (classNo
                            == COD_AUDIO_VIDEO_WEARABLE_HEADSET_DEVICE)) {
                        stopClassicScan();
                        bluetoothDevice.createBond();
                    }
                }
            }
        }
```
※ 「DEVICE_TYPE_DUAL」もペアリング対象にすることでApple Air Pods Proに対応できました。

ペアリング完了後に接続します。
bondStateがBOND_BONDEDになるとペアリング完了です。

``` BluetoothClientService.java
        public void onBondStateChanged(BluetoothDevice bluetoothDevice, int bondState) {
            Log.d(TAG, "onBondStateChanged");
            if (bondState == BluetoothDevice.BOND_BONDED) {
                connect(bluetoothDevice);
            }
        }
```

getProfileProxyを使用してプロファイル指定し、サービスリスナーと関連付けます。
プロファイルは、 BluetoothProfile.HEADSETを使用します。

``` BluetoothClientService.java
    private void connect(BluetoothDevice device) {
        int state = mBluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET);
        if (state == BluetoothProfile.STATE_DISCONNECTED) {
            int type = device.getType();
            if (type == BluetoothDevice.DEVICE_TYPE_CLASSIC || type == BluetoothDevice.DEVICE_TYPE_DUAL) {
                int classNo = device.getBluetoothClass().getDeviceClass();
                Log.d(TAG, "class" + classNo);
                boolean isGetProfile = false;
                if ((classNo == COD_AUDIO_VIDEO_HEADPHONES) || (classNo
                        == COD_AUDIO_VIDEO_WEARABLE_HEADSET_DEVICE)) {
                    {
                        isGetProfile = mBluetoothAdapter
                                .getProfileProxy(mContext, mServiceListener,
                                        BluetoothProfile.HEADSET);
                    }
                }

                Log.d(TAG, "isGetProfile :" + isGetProfile);
                if (isGetProfile) {
                    mBluetoothDevice = device;
                }
            }
        }
    }
```
※ 「DEVICE_TYPE_DUAL」もペアリング対象にすることでApple Air Pods Proに対応できました。


onServiceConnected()内でBluetoothHeadsetクラスのconnect()を行います。

``` BluetoothClientService.java
        public void onServiceConnected(int i, BluetoothProfile bluetoothProfile) {
            try {
                if (i == BluetoothProfile.HEADSET) {
                    Class bluetoothHeadset = Class
                            .forName("android.bluetooth.BluetoothHeadset");
                    Object object = bluetoothHeadset.cast(bluetoothProfile);
                    Method getConnectionState = bluetoothHeadset
                            .getDeclaredMethod("getConnectionState", BluetoothDevice.class);
                    int connectionState = (int) getConnectionState
                            .invoke(object, mBluetoothDevice);
                    if (connectionState == BluetoothProfile.STATE_DISCONNECTED) {
                        Method connect = bluetoothHeadset
                                .getDeclaredMethod("connect", BluetoothDevice.class);
                        boolean isConnected = (boolean) connect
                                .invoke(object, mBluetoothDevice);
                        Log.d(TAG, "isConnected : " + isConnected);
                        if (isConnected) {
                            mProfile = bluetoothProfile;
                        }
                    }
                }
            } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException |
                    InvocationTargetException e) {
                e.printStackTrace();
            }
        }
``` 

BluetoothAdapter.EXTRA_CONNECTION_STATEで取得した状態がBluetoothAdapter.STATE_CONNECTEDになれば接続完了です。

```BluetoothClientService.java
        public void onConnectionStateChanged(int connectionState) {
            Log.d(TAG, "onConnectionStateChanged");
            if (Build.MODEL.equals("RICOH THETA Z1")) {
                Intent intent = new Intent(Constants.ACTION_OLED_TEXT_SHOW);
                if (connectionState == BluetoothAdapter.STATE_DISCONNECTED) {
                    intent.putExtra(Constants.TEXT_BOTTOM, "disconnected");
                } else if (connectionState == BluetoothAdapter.STATE_CONNECTED) {
                    intent.putExtra(Constants.TEXT_BOTTOM, "connected");
                }
                sendBroadcast(intent);
            } else {
                //LED3点灯
                Intent intentLedShow = new Intent("com.theta360.plugin.ACTION_LED_SHOW");
                if (connectionState == BluetoothAdapter.STATE_DISCONNECTED) {
                    intentLedShow.putExtra("color", LedColor.BLUE.toString());
                } else if (connectionState == BluetoothAdapter.STATE_CONNECTED) {
                    intentLedShow.putExtra("color", LedColor.WHITE.toString());
                }
                intentLedShow.putExtra("target", LedTarget.LED3.toString());
                mContext.sendBroadcast(intentLedShow);
            }
        }
```

### 音量調整

本プラグインで扱う音量データは2種類です。
`1.ストリームボリューム`（AndroidのAudioManagerのパラメータ、StreamTypeはSTREAM_MUSICを使用）
 Androidが管理しています。実際に出力する音量です。
`2.シャッターボリューム`（WebAPIのOptions）
 THETAアプリが管理しています。実際に出力する音量ではありません。実際に出力する音量を変更するには、ストリームボリュームの変更が必要です。
 プラグインから[Intent](https://api.ricoh/docs/theta-plugin-reference/broadcast-intent/)を使用してTHETAアプリで音声出力する際は、シャッターボリュームからストリームボリュームの値を算出し、ストリームボリュームを設定してから音声を出力しています。
つまり、THETAアプリで音声再生する場合はストリームボリュームはIntentを投げた時点のシャッターボリュームの音量に調整されます。
シャッターボリュームは、 [WebAPI Options `_shutter_volume`](https://api.ricoh/docs/theta-web-api-v2.1/options/_shutter_volume/) で変更することができます。

本プラグインでは、2種類の操作で音量変更ができます。
・Bluetoothオーディオ操作(AVRCP)
・THETA本体ボタン操作
音量変更は、ChangeVolumeTask.javaで行っています。

Bluetoothオーディオ操作で音量変更した場合、Android側でストリームボリュームが変更され、プラグインにはストリームボリュームが変更されたことを表すIntent:`"android.media.VOLUME_CHANGED_ACTION"`が通知されます。
この時点では、シャッターボリュームは変更されていないため、プラグインで変更しています。ChangeVolumeTaskにストリームボリュームとACTION_TYPE_SET_VOLを渡してタスクを生成しています。

```MainActivity.java
    private MediaReceiver.Callback mMediaReceiverCallback = new MediaReceiver.Callback() {
        @Override
        public void onChangeVolume(int stream_type, int prev_vol, int vol) {
            if (stream_type == AudioManager.STREAM_MUSIC) {
                if (vol != prev_vol) {
                    new ChangeVolumeTask(getApplicationContext(), vol,
                            ChangeVolumeTask.ACTION_TYPE_SET_VOL).execute();
                }
            }
        }
    };
```

THETA本体ボタン操作で音量変更した場合、その時点のシャッターボリュームからストリームボリュームを算出し、1ステップ増減させた音量をシャッターボリュームとストリームボリュームにそれぞれ設定します。

```MainActivity.java
           case SET_VOL_PLUS:
                new ChangeVolumeTask(getApplicationContext(), 0,
                        ChangeVolumeTask.ACTION_TYPE_UP_VOL).execute();
                break;
            case SET_VOL_MINUS:
                new ChangeVolumeTask(getApplicationContext(), 0,
                        ChangeVolumeTask.ACTION_TYPE_DOWN_VOL).execute();
                break;
```

音量変更時の通知音声は[こちら](https://qiita.com/KA-2/items/e847c780cc0fee187f89#%E9%9F%B3%E5%A3%B0%E5%86%8D%E7%94%9F%E3%81%AE%E6%96%B9%E6%B3%95%E3%81%A8%E9%9F%B3%E9%87%8F)にならって、SoundManagerTaskで行っています。プラグインでAudioManagerを使用し、音声を再生します。
接続したBluetoothデバイスから音声出力するために再生時のStreamTypeはSTREAM_MUSICにしています。

### THETAアプリ側のリモコン検索機能の抑制
~~本プラグインでは実装していませんが、プラグインでBluetooth Client機能を使用する場合は、THETAアプリ側のBluetoothリモコン機能をOFFにする必要があります。~~（2020/11/20に[RICOH THETA Plug-in STORE](https://pluginstore.theta360.com/plugins/skunkworks.headset/)に公開したVer1.2.0から対応しました）
Bluetoothリモコン機能は[WebAPI Options:_bluetoothRole](https://api.ricoh/docs/theta-web-api-v2.1/options/_bluetooth_role/)で切り替えることができます。

|_bluetoothRole|Bluetoothリモコン機能|
|----|----|
|Peripheral|OFF|
|Central|ON|
|Central_Peripheral|ON|

例えば、プラグイン起動時にonCreate()でOptions:_bluetoothRoleを取得し、onPause()で取得済みのRoleに戻せば、プラグイン起動中のみTHETA本体のBluetoothリモコン機能をOFFすることが可能です。

###サービスの終了(2020/11/24追記）
今回、Bluetooth Classicで接続したオーディオデバイス側のボタン操作を拾うためにサービスを利用しました。一般スマートフォンでは、音楽プレーヤーアプリを作るときに利用する手法です。サービスは、正しく終了させないとアプリケーション終了後も処理が残ってしまう可能性があります。
**確実に終了させましょう。**

[Googleのドキュメント](https://developer.android.com/guide/components/services?hl=ja#Lifecycle)には以下の記述があります

```
サービスの全体の生存期間は、onCreate() が呼び出されてから、onDestroy() から戻るまでの間です。
アクティビティと同様に、サービスは onCreate() で初期セットアップを行い、onDestroy() で残りのすべてのリソースを解放します。
たとえば、音楽再生サービスでは、音楽を再生するスレッドを onCreate() で作成でき、onDestroy() でスレッドを停止できます。
```

本プラグインでも上記に従って onDestroyメソッドのOverrideを追記し、サービスの終了とメディアレシーバーの登録解除をしています。

```MainActivity.java
    @Override
    protected void onDestroy() {
        getApplicationContext()
                .stopService(new Intent(getApplicationContext(), BluetoothClientService.class));
        if (mMediaReceiver != null) {
            getApplicationContext().unregisterReceiver(mMediaReceiver);
        }

        super.onDestroy();
    }
```

## 動作確認したBluetoothオーディオ機器

身近な方々に協力して頂き、もう少し多くのBluetoothオーディオ機器で動作確認してみました。
一覧にして結果をまとめておきます。

|オーディオ機器|再生/一時停止<br>（撮影）|曲送り<br>（露出+）|曲戻し<br>（露出-）|Vol+|Vol-|
|---|---|---|---|---|---|
|Anker<br>Icon Mini<br>(スピーカー)|○|○|○|○|○|
|Anker<br>Liberty Air<br>(イヤホン)|○|△<br>操作しにくい|△<br>操作しにくい|N/A|N/A|
|Anker<br>Zolo Liberty<br>(イヤホン)|○|×<br>不安定|×<br>不安定|N/A|N/A|
|JPRiDE<br>TWS-520<br>(イヤホン)|○|○|○|N/A|N/A|
|VANKYO<br>X100<br>(イヤホン)|○|○|○|△<br>操作しにくい|△<br>操作しにくい|
|Apple<br>Air Pods Pro<br>(イヤホン)|○|○|○|N/A|N/A|

* N/Aはイヤホン側に機能がありません
* [JPRiDE TWS-520のマニュアル](http://7654ed4e6d78812.main.jp/jprmanual/TWS520_manual_ja.pdf)には1回タップで音量調節できる主旨の説明がありますが、実際にはスマートフォン(iOS,Androidどちらも)でも認識できなかったのでN/A扱いにしました。
* Anker Liberty Airは、タッチ認識位置とタッチ感度の都合、イヤホン操作自体が難かったです。
* VANKYO X100の音量操作はロングタップです。タップ中に連続してキーコードが送出されるため狙いの音量とするのが難しいです。とはいえ、他の操作は行いやすく、Amazonのタイムセールで3000円程度で購入できることもあります。THETA専用に購入してしまうのもアリかと。おすすめです。
* Apple Air Pods Proは、Android OSの機器とペアリングする際、ケースのボタンを押してペアリング状態にする必要があります。ご注意ください。


## ダイソーの330円片耳イヤホン（新型）でフルコントロール出来ました（2021/6/2 追記）
2020年の年末くらいからでしょうか、ダイソー片耳イヤホンに、以下２種類が追加されたようです。
![MicrosoftTeams-image (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263023/af9d23d1-edd0-db0d-b8f0-966afaa186bb.png)
*左：　コンパクトでボタンが少ない機種（型番 E-BT-ME1）
*右：　通常サイズでボタンがしっかりある機種（型番 E-BT-ME2）

どちらもTHETAとペアリングして、本プラグインを利用できますが、フルコントロールができるのは右側です。

|THETA動作|型番:E-BT-ME1<br>コンパクトサイズ|型番:E-BT-ME2<br>通常サイズ|
|----|----|----|
|静止画撮影|再生/一時停止|再生/一時停止|
|露出補正を1ステップアップ|曲送り|曲送り|
|露出補正を1ステップダウン|―|曲戻し|
|通常通知音の音量1ステップアップ|―|音量アップ|
|通常通知音の音量1ステップダウン|―|音量ダウン|
  
この2機種より以前から販売されている、パッケージが大きいタイプ（右の通常サイズと見栄えは同じ）では、本プラグインを利用できないのでご注意ください。
新旧どちらも店頭に並んでいますので、製品型番「E-BT-ME2」で探すのが良いと思います。

2021年5月末時点、ダイソーでは、1100円で完全独立bluetoothイヤホンも発売されたようなのですが、イヤホンをTHETAのリモコンとして使うなら、片耳のものが使いやすいです。



## まとめ

今回の事例ではBluetoothデバイスによるリモート撮影をご紹介しました。Bluetoothイヤホンを使ったお手軽なリモート操作をぜひ試してください。
イヤホンの音声アシスタント起動も試しましたが、通知を拾えたり、拾えなかったりと挙動がいまいち掴めませんでした。
今回のハマリポイントは、音量変更時の音声再生でした。プラグインでの音量変更時の通知音をTHETA側にIntentで依頼するとストリームボリューム音量の変更イベント通知が止まらない現象が発生しました。
原因は、AVRCPの音量変更イベントを拾うために、ストリームボリューム音量変化時にシャッターボリューム音量をストリームボリューム音量へ調整する処理を行っていたためです。
THETA側での音声再生時は、ストリームボリューム音量をシャッターボリューム音量へ調整してから、音声再生を行います。
そのため、プラグイン側でシャッターボリューム音量の変更、THETA側でストリームボリューム音量の変更を時間差で延々と調整し続けていました。
対策として、プラグイン側でAudioManegerを用意し、音声再生を行いました。これにより、ストリームボリュームの調整とシャッターボリュームの音量調整の同期がとれ問題を解消できました。


## RICOH THETAプラグインパートナープログラムについて

THETAプラグインをご存じない方は[こちら](https://api.ricoh/products/theta-plugin/)をご覧ください。
パートナープログラムへの登録方法は[こちら](https://qiita.com/mShiiina/items/55d98f366e650ca42251)にもまとめてあります。
Qiitaの[RICOH THETAプラグイン開発者コミュニティ TOPページ「About」](https://qiita.com/organizations/theta-plugin)に便利な記事リンク集もあります。
興味を持たれた方は[Twitter](https://twitter.com/thetaplugin)のフォローと[THETAプラグイン開発者コミュニティ(Slack)](https://join.slack.com/t/thetadeveloper/shared_invite/enQtNzcxODI2MTk2ODg0LWY5YmU2ZTIxNGYwNzlmNWZjMWMyMWFlMzY3ZmQ3NGU3ZjZiZjI0Njk4MGIxMDU5NDJiMzQ4Y2FkNDEzYmFlNGU)への参加もよろしくおねがいします｡