Edited at

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

全天球カメラ RICOH THETA のプラグイン 開発に使える 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 はプラグインからもアクセスできます。API サーバーが THETA 内部で動いているので 127.0.0.1 でアクセスします。つまり、プラグインから THETA API にアクセスして、撮影や設定ができます。

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

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


使い方

前置きが長くなりましたが、使い方を紹介します。リポジトリのサンプルは Java で書いているので、ここでは Kotlin で書きます。


プロジェクトへの追加

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

repositories {

...
jcenter() // この行を追加
}

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


THETA オブジェクトの作成

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

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

// 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 メソッドを呼ぶだけです。

theta.takePicture()


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

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

// 同期的に完了しない!!

// 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が表示される

次のようなヘルパー関数を定義すると便利です。

fun <R> waitForDone(response: CommandResponse<R>): CommandResponse<R> {

var res = response // 仮引数は val なので var の変数を再定義
while (res.state != CommandState.DONE) {
res = theta.commandStatus(res)
Thread.sleep(100)
}
return res
}

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


オプションの取得と設定

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

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

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_VOLUMESHUTTER_VOLUME_SUPPORTorg.theta4j.webapi.Options クラスに定義された定数です。

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

val shutterVolume = theta.getOption(SHUTTER_VOLUME)

theta.setOption(SHUTTER_SPEED, ShutterSpeed._1_100)


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

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

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 でデコードします。

theta.livePreview.use { stream ->

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

その他の使い方については Javadoc を参照していただくか、コメント欄でご質問ください。


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

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

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

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

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

override fun onKeyDown(keyCode: Int, keyEvent: KeyEvent) {

// キーイベントなどは UI スレッドで実行される
theta.takePicture() // これはエラーになる
}

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

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 認証のライブラリを見つけたので、okhttp を使うことにしました。ドキュメントも充実していて良さそうです。

JSON については JSON-B を使おうと思ったのですが、Android では動きませんでした。ここでは深追いせずに GSON を使うことにしました。


構成

THETA APIOpen Spherical Camera API (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 という拡張パラメータが追加されています。一方 camera.takePicture コマンドも OSC API で定義されたコマンドですが THETA API での拡張仕様はありません。したがって camera.takeCaptureosc パッケージに定義することも考えられます。しかし、拡張の有無で所属パッケージが分かれているのはややこしいですし、将来にわたって拡張が入らないとは限らないので、コマンド類はすべて webapi パッケージに実装しています。


タイプセーフ

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

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

{

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

これは java.lang.String 型のキーと 任意の型のキーの組み合わせなので、Java で表現すると Map<String, Object> となります。しかし、値の型を java.lang.Object とすると、全然タイプセーフになりません。

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

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") // 型不一致のためコンパイルエラー
}

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

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

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 <T> T get(Option<T> option);
}

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

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


まとめ

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

RICOH THETA のプラグイン は簡単に開発できます。特に Android アプリの知識がある人ならすぐに開発できますので、ぜひチャレンジしてみてください。

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

【THETAプラグイン開発】THETAでRICOH THETA Plug-in SDKを動かす方法