プラグイン開発に使えるTHETA APIクライアントをつくってみた

全天球カメラ [RICOH THETA のプラグイン](https://theta360.com/ja/ricoh_plugins/) 開発に使える THETA API v2.1 のクライアントを作ってみたので、その中身を紹介します。

ソースコードは GitHub で公開しています。https://github.com/theta4j/theta-web-api

## 開発の動機

タイトルにあるように RICOH THETA のプラグイン開発に使うために作りました。

RICOH THETA V は Android ベースの OS で動いていて、開発者登録をすれば自由にアプリを開発して使えます。この THETA 向けに開発された Android アプリのことをプラグインと呼んでいます。

そして、THETA の API としておなじみの [THETA API](https://developers.theta360.com/ja/docs/v2.1/api_reference/) はプラグインからもアクセスできます。API サーバーが THETA 内部で動いているので `127.0.0.1` でアクセスします。つまり、プラグインから THETA API にアクセスして、撮影や設定ができます。

プラグインの実態は Android アプリなので、開発言語は Kotlin か Java です。しかし、Java 向けの THETA API のライブラリが見つからず、公式 SDK にはサンプルコードがちょろっと入っているだけでした。おそらく、THETA API は HTTP で JSON をやり取りするだけのシンプルな API なので、クライアントライブラリを実装するモチベーションがないのでしょう。

というわけで THETA API クライアントの Java 実装を作りました。

## 使い方

前置きが長くなりましたが、使い方を紹介します。[リポジトリのサンプル](https://github.com/theta4j/theta-web-api)は Java で書いているので、ここでは Kotlin で書きます。

### プロジェクトへの追加

このライブラリは MavenCentral で公開しているので、Gradle なら `build.gradle` に 2行追加するだけで導入できます。

```gradle
repositories {
    ...
    mavenCentral() // この行を追加
}

dependencies {
    ...
    implementation 'org.theta4j:theta-web-api:1.6.0' // この行を追加
}
```

### THETA オブジェクトの作成

基本的には `org.theta4j.webapi.Theta` クラスのインスタンスを作って、そこに生えているメソッドを呼んで使います。

THETA への接続方法によって theta オブジェクトの作り方が異なります。

```kotlin
// THETAプラグインから使う場合
// エンドポイントが http://127.0.0.1:8080 となる
val theta = Theta.createForPlugin()

// THETA が Wi-Fi の親機モードの場合 (APモード)
// エンドポイントが http://192.168.1.1 となる
val theta = Theta.create()

// THETA の Wi-Fi が子機モードで Digest 認証が無効になっている場合 (CLモード)
// ※IPアドレスは環境によって異なる
val theta = Theta.create("http://192.168.100.34")

// THETA の Wi-Fi が子機モードで Digest 認証が有効になっている場合 (CLモード)
// ※IPアドレスは環境によって異なる
val theta = Theta.create("http://192.168.100.34", "username", "password")
```

### 静止画の撮影

静止画の撮影は非常に簡単です。`takePicture` メソッドを呼ぶだけです。

```kotlin
theta.takePicture()
```

### 静止画の撮影と結果の取得

ただし、撮影コマンドは実際は即時完了しないので、結果が欲しい場合は以下のようにします。

```kotlin
// 同期的に完了しない!!
// res.state が DONE になるまで res.result は null
var res = theta.takePicture()

// 100ms 間隔でポーリング
while(res.state != CommandState.DONE) {
    res = theta.commandStatus(res)
    Thread.sleep(100)
}

println(res.result) // 撮影結果のURLが表示される
```

次のような拡張関数を定義すると便利です。

```kotlin
fun <R> Theta.waitForDone(response: CommandResponse<R>): CommandResponse<R> {
    var res = response // 仮引数は val なので var の変数を再定義
    while (res.state != CommandState.DONE) {
        res = commandStatus(res)
        Thread.sleep(100)
    }
    return res
}

fun main(args : Array<String>) {
    val res = theta.waitForDone(theta.takePicture())
    println(res.result) // 撮影結果のURLが表示される
}
```

### オプションの取得と設定

オプションの設定値とサポート値は `getOptions` メソッドで呼び出せます。

シャッター音の設定値とサポート値を同時に取得しつつ、最大値に設定する例は以下のようになります。

```kotlin
val opts = theta.getOptions(SHUTTER_VOLUME, SHUTTER_VOLUME_SUPPORT)
val volume opts.get(SHUTTER_VOLUME)
val support = opts.get(SHUTTER_VOLUME_SUPPORT)

println("Current Volume : $volume")

theta.setOption(SHUTTER_VOLUME, support.maxShutterVolume)
```

`SHUTTER_VOLUME` や `SHUTTER_VOLUME_SUPPORT` は `org.theta4j.webapi.Options` クラスに定義された定数です。

単一のオプション値を取得・設定する場合は `getOption` / `setOption` メソッドを使います。

```kotlin
val shutterVolume = theta.getOption(SHUTTER_VOLUME)
theta.setOption(SHUTTER_SPEED, ShutterSpeed._1_100)
```

### オプション設定と静止画撮影

シャッタースピード優先、10秒露光で静止画を撮影する場合、以下のようなコードになります。

```kotlin
import org.theta4j.webapi.*
import org.theta4j.webapi.Options.*

val theta = Theta.createForPlugin()

fun main(args : Array<String>) {
    val opts = OptionSet.Builder()
        .put(CAPTURE_MODE, CaptureMode.IMAGE)
        .put(EXPOSURE_PROGRAM, ExposureProgram.SHUTTER_SPEED)
        .put(SHUTTER_SPEED, ShutterSpeed._10)
        .build()
    theta.setOptions(opts)
    theta.takePicture()
}
```

### ライブビューの取得

ライブプレビュー用の映像も取得可能です。JPEG のバイト列が得られるので Android の場合は `BitmapFactory` でデコードします。

```kotlin
theta.livePreview.use { stream ->
    while(true) {
        val frame = stream.nextFrame()              // 1枚分のJPEGバイト列 (InputStream)
        val bmp = BitmapFactory.decodeStream(frame) // デコード (Androidの例)
        // ここで bmp の描画処理等
    }
}
```

その他の使い方については [Javadoc](https://javadoc.io/doc/org.theta4j/theta-web-api) を参照していただくか、コメント欄でご質問ください。

### Android もしくは THETA プラグインで使う

Android もしくは THETA プラグインで使う場合は注意が必要です。

まず、インターネットのパーミッションが必要なので `AndroidManifest.xml` に以下の行を追加します。

```xml
<uses-permission android:name="android.permission.INTERNET"/>
```

また、`Theta#takePicture` や `Theta#getOptions` といった、I/O アクセスが伴うメソッドは UI スレッドで実行できません。

```kotlin
override fun onKeyDown(keyCode: Int, keyEvent: KeyEvent) {
    // キーイベントなどは UI スレッドで実行される
    theta.takePicture() // これはエラーになる
}
```

`ExecutorService` などを使って、別スレッドで実行してください。

```kotlin
private val executor = Executors.newSingleThreadExecutor()

override fun onKeyDown(keyCode: Int, keyEvent: KeyEvent) {
    executor.submit {
        theta.takePicture() // UI スレッドではないので OK
    }
}
```

## 設計について

ここからは設計に関する話です。

### 設計の目標

まず、開発にあたって、いくつか目標を建てました。

* Web API の機能を網羅する
* 開発言語は Java
* Android 対応 (ただし Android 固有の機能は使わず JVM 言語を広くターゲットにする)
* JCenter で公開する
* タイプセーフにこだわる (ジェネリクスを活用する)

基本的にプラグイン開発用途以外にも広く使ってもらうことを意識しました。

### 使用ライブラリ

前述した通り、THETA API は 基本的に HTTP で JSON をやり取りする API です。つまり、 HTTP のライブラリと JSON のライブラリが必要です。

HTTP は `HttpURLConnection` でも十分かな…とも思ったのですが、THETA V はクライアントモードで動かすときに、Digest 認証を要求してきます。okhttp 向けの[良さそうな Digest 認証のライブラリ](https://github.com/rburgst/okhttp-digest)を見つけたので、okhttp を使うことにしました。ドキュメントも充実していて良さそうです。

JSON については [JSON-B](http://json-b.net/) を使おうと思ったのですが、Android では動きませんでした。ここでは深追いせずに [GSON](https://github.com/google/gson) を使うことにしました。

### 構成

[THETA API](https://developers.theta360.com/ja/docs/v2.1/api_reference/) は [Open Spherical Camera API](https://developers.google.com/streetview/open-spherical-camera/) (OSC API) をベースに、独自の拡張が加えられています。ですので、今回は OSC API 用のパッケージと THETA 独自拡張部分でパッケージを分けています。

| パッケージ         | 概要                                         |
| ------------------ | -------------------------------------------- |
| org.theta4j.osc    | Open Spherical Camera API に関するパッケージ |
| org.theta4j.webapi | OSC API のTHETA 独自拡張に関するパッケージ   |

ただし `org.theta4j.osc` パッケージは最小限にして、サードベンダーに仕様拡張が許されている機能はすべて `org.theta4j.webapi` に実装する方針にしました。

たとえば、`caemra.startCapture` は OSC API で定義されたコマンドですが THETA API では `_mode` という拡張パラメータが追加されています。したがって、`webapi` パッケージに定義します。

一方 `camera.takePicture` コマンドも OSC API で定義されたコマンドですが THETA API での拡張仕様はありません。したがって `camera.takeCapture` は `osc` パッケージに定義することも考えられます。

しかし、拡張の有無で所属パッケージが分かれているのはややこしいですし、将来にわたって拡張が入らないとは限らないので、拡張が許されている機能はすべて `webapi` パッケージに定義することとしました。

### タイプセーフ

OSC API にはオプションの設定/取得機能があります。これは複数の異なる型を持ったオプション値をまとめて設定/取得します。

たとえば、Bluetooth 電源とシャッタースピードを同時に設定したい場合は以下のような JSON を送信します。この例では String と Number が混在しています。

```json
{
    "options": {
        "_bluetoothPower": "ON",
        "shutterSpeed": 0.01
    }
}
```

これは `java.lang.String` 型のキーと 任意の型の値の組み合わせなので、Java で表現すると `Map<String, Object>` となります。しかし、値の型を `java.lang.Object` とすると、値を取り出すときにダウンキャストが必要になり、全然タイプセーフになりません。

そこで、オプション値の集合を保持する以下のようなクラスを定義しました。引数 `type` に適切な Class オブジェクトを指定すれば、`type` と `value` の型に齟齬があるとコンパイルエラーとなり、コンパイル時に問題を発見できます。これは、 Effective Java 第2版で「型安全な異種コンテナー」として紹介されているパターンです。

```java
public final class OptionSet {
    public <T> void put(Class<T> type, String name, T value);
    public <T> T get(Class<T> type, String name);
}

public static void main(String[] args) {
    OptionSet opts = ...;
    opts.put(BigDecimal.class, "shutterSpeed", "foo") // 型不一致のためコンパイルエラー
}
```

しかし、`type` と `name` の組み合わせは一定です。たとえば `shutterSpeed` の型は常に Number であり、 String や Boolean になることはありません。

そのため、実際には以下のような `type` と `name` を 合体させた `Option<T>` 型を定義して使っています。

```java
public interface Option<T> {
    Class<T> getType();
    String getName();
}

public final class Options {
    public static final Option<BigDecimal> SHUTTER_SPEED = OptionImpl.create("shutterSpeed", BigDecimal.class)
}

public final class OptionSet {
    public <T> void put(Option<T> option, T value);
    ...
}

public static void main(String[] args) {
    OptionSet opts = ...;
    opts.put(Options.SHUTTER_SPEED, "foo") // 型不一致のためコンパイルエラー
}
```

コマンド実行機能に関しても「型安全な異種コンテナー」のパターンを使ってタイプセーフに設計しています。

## まとめ

THETA API のクライアント実装について、使い方と設計を簡単に紹介しました。 THETA API を使ったアプリやプラグインを開発したい方の一助になれば幸いです。

[RICOH THETA のプラグイン](https://theta360.com/ja/ricoh_plugins/) は簡単に開発できます。特に Android アプリの知識がある人ならすぐに開発できますので、ぜひチャレンジしてみてください。

プラグイン開発の始め方については以下の記事に簡単にまとまっています。

[【THETAプラグイン開発】THETAでRICOH THETA Plug-in SDKを動かす方法](https://qiita.com/yokazuya/items/cda20420ee0b1b58e03f)