[JAVA] I made a THETA API client that can be used for plug-in development

Spherical camera RICOH THETA plug-in I made a client for THETA API v2.1 that can be used for development, so I will introduce the contents.

The source code is available on GitHub. https://github.com/theta4j/theta-web-api

Motivation for development

As the title suggests, I made it for use in RICOH THETA plug-in development.

RICOH THETA V runs on an Android-based OS, and you can freely develop and use apps by registering as a developer. The Android app developed for THETA is called a plug-in.

And the familiar THETA API as the THETA API can also be accessed from the plugin. Since the API server is running inside THETA, access it with 127.0.0.1. In other words, you can access the THETA API from the plugin to shoot and configure.

Since the plugin is actually an Android app, the development language is Kotlin or Java. However, I couldn't find the THETA API library for Java, and the official SDK just contained a bit of sample code. Perhaps the THETA API is a simple API that just exchanges JSON over HTTP, so there is no motivation to implement a client library.

That's why I created a Java implementation of the THETA API client.

How to use

The introduction has become long, but I will show you how to use it. Repository sample is written in Java, so I will write it in Kotlin here.

Add to project

This library is published by MavenCentral and JCenter, so Gradle can be installed by just adding two lines to build.gradle.

repositories {
    ...
    jcenter() //Add this line
}

dependencies {
    ...
    implementation 'org.theta4j:theta-web-api:1.4.0' //Add this line
}

Creating a THETA object

Basically, create an instance of the ʻorg.theta4j.webapi.Theta` class and call the method that grows there to use it.

How to create a theta object depends on how you connect to THETA.

//When using from THETA plug-in
//The endpoint is http://127.0.0.1:Become 8080
val theta = Theta.createForPlugin()

//THETA is Wi-In the case of Fi master mode(AP mode)
//The endpoint is http://192.168.1.Become 1
val theta = Theta.create()

//THETA Wi-When Fi is in slave mode and Digest authentication is disabled(CL mode)
//* IP address varies depending on the environment
val theta = Theta.create("http://192.168.100.34")

//THETA Wi-If Fi is in slave mode and Digest authentication is enabled(CL mode)
//* IP address varies depending on the environment
val theta = Theta.create("http://192.168.100.34", "username", "password")

Shooting still images

Taking a still image is very easy. Just call the takePicture method.

theta.takePicture()

Shooting still images and getting results

However, the shooting command is not actually completed immediately, so if you want the result, do as follows.

//Not completed synchronously!!
// res.res until state is DONE.result is null
var res = theta.takePicture()

//Polling at 100ms intervals
while(res.state != CommandState.DONE) {
    res = theta.commandStatus(res)
    Thread.sleep(100)
}

println(res.result) //The URL of the shooting result is displayed

It is useful to define a helper function like this:

fun <R> waitForDone(response: CommandResponse<R>): CommandResponse<R> {
    var res = response //Since the formal argument is val, redefine the variable of 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) //The URL of the shooting result is displayed
}

Get and set options

Option settings and support values can be called with the getOptions method.

The following is an example of setting the maximum value while acquiring the shutter sound setting value and support value at the same time.

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 and SHUTTER_VOLUME_SUPPORT are constants defined in the ʻorg.theta4j.webapi.Options` class.

Use the getOption / setOption methods to get and set a single option value.

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

Option settings and still image shooting

When shooting a still image with shutter speed priority and 10-second exposure, the code is as follows.

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()
}

Get live view

You can also get the video for live preview. Since you can get a JPEG byte string, you can decode it with BitmapFactory on Android.

theta.livePreview.use { stream ->
    while(true) {
        val frame = stream.nextFrame()              //JPEG byte string for one sheet(InputStream)
        val bmp = BitmapFactory.decodeStream(frame) //Decode(Android example)
        //Here, bmp drawing process, etc.
    }
}

For other usage, please refer to Javadoc or ask a question in the comment section.

Use with Android or THETA plugin

Be careful when using it with Android or THETA plugins.

First, add the following line to ʻAndroidManifest.xml` because you need internet permissions.

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

Also, methods with I / O access, such as Theta # takePicture and Theta # getOptions, cannot be executed in the UI thread.

override fun onKeyDown(keyCode: Int, keyEvent: KeyEvent) {
    //Key events etc. are executed in the UI thread
    theta.takePicture() //This will result in an error
}

ʻExecute it in another thread using ExecutorService` etc.

private val executor = Executors.newSingleThreadExecutor()

override fun onKeyDown(keyCode: Int, keyEvent: KeyEvent) {
    executor.submit {
        theta.takePicture() //OK as it is not a UI thread
    }
}

About design

From here on, we will talk about design.

Design goals

First, we set some goals for development.

Basically, I was conscious of having it widely used for purposes other than plug-in development.

Library used

As mentioned above, THETA API is basically an API for exchanging JSON over HTTP. That is, you need an HTTP library and a JSON library.

I thought that HttpURLConnection would be enough for HTTP, but THETA V requires Digest authentication when running in client mode. I found a good looking Digest authentication library for okhttp, so I decided to use okhttp. The documentation is also substantial and looks good.

I wanted to use JSON-B for JSON, but it didn't work on Android. I decided to use GSON instead of chasing deeply.

Constitution

THETA API is [Open Spherical Camera API](https://developers.google.com/streetview/open-spherical- It is based on camera /) (OSC API) with its own extensions. Therefore, this time, the package for OSC API and the package for THETA's original extension are separated.

package Overview
org.theta4j.osc Package for Open Spherical Camera API
org.theta4j.webapi Package for THETA's own extension of OSC API

However, we decided to minimize the ʻorg.theta4j.osc package and implement all the features that third vendors are allowed to extend in ʻorg.theta4j.webapi.

For example, caemra.startCapture is a command defined in the OSC API, but the THETA API adds an extension parameter called _mode. Therefore, define it in the webapi package.

On the other hand, the camera.takePicture command is also a command defined by the OSC API, but there is no extended specification in the THETA API. Therefore, camera.takeCapture could be defined in the ʻosc` package.

However, it is confusing that the packages to which the extension belongs are separated depending on the presence or absence of the extension, and it is not always the case that the extension will not be included in the future, so I decided to define all the functions that are allowed to be extended in the webapi package.

Type safe

The OSC API has an optional setting / acquisition function. It sets / retrieves option values with different types at once.

For example, if you want to set the Bluetooth power and shutter speed at the same time, send the following JSON. In this example, String and Number are mixed.

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

This is a combination of a key of type java.lang.String and a value of any type, so in Java it would beMap <String, Object>. However, if the value type is java.lang.Object, then downcasting is required when retrieving the value, which makes it type-safe at all.

Therefore, I defined the following class that holds a set of option values. If you specify an appropriate Class object for the argument type, a compile error will occur if there is a discrepancy between the types of type and value, and you can find the problem at compile time. This is the pattern introduced in Effective Java 2nd Edition as "Type-Safe Heterogeneous Containers".

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") //Compile error due to type mismatch
}

However, the combination of type and name is constant. For example, the type of shutterSpeed is always Number, never String or Boolean.

Therefore, in reality, the following ʻOption type, which is a combination oftype and name`, is defined and used.

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") //Compile error due to type mismatch
}

The command execution function is also designed to be type-safe using the pattern of "type-safe heterogeneous containers".

Summary

We briefly introduced the usage and design of the client implementation of THETA API. We hope it helps those who want to develop apps and plugins that use the THETA API.

RICOH THETA Plugins is easy to develop. Especially if you have knowledge of Android apps, you can develop it immediately, so please try it.

The following article briefly summarizes how to get started with plugin development.

[THETA plug-in development] How to run the RICOH THETA Plug-in SDK with THETA

Recommended Posts

I made a THETA API client that can be used for plug-in development
I made a question that can be used for a technical interview
I made a plugin for IntelliJ IDEA
I made an API client for Nature Remo
[Ruby] I made a simple Ping client
I made a class that can use JUMAN and KNP from Java
Technology excerpt that can be used for creating EC sites in Java training
I made a Diff tool for Java files
I made a viewer app that displays a PDF
Organize methods that can be used with StringUtils
[Ruby] Methods that can be used with strings
A collection of RSpecs that I used frequently
I used Docker for my portfolio as a beginner, so I hope that even 1mm will be helpful to someone.
I wrote a test code (Junit & mockit) for the code that calls the AWS API (Java)
I made a Docker image of SDAPS for Japanese
I made a check tool for the release module
I made a method to ask for Premium Friday
[Swift] API used for apps that passed the selection
I made a Restful server and client in Spring.
I made a library that works like a Safari tab !!
I made a library for displaying tutorials on Android.
I made a Wrapper that calls KNP from Java
About the matter that hidden_field can be used insanely
Convenient shortcut keys that can be used in Eclipse
I tried a puzzle that can only be solved by the bottom 10% of bad engineers
I made a reply function for the Rails Tutorial extension (Part 4): A function that makes the user unique
I made a function to register images with API in Spring Framework. Part 2 (Client Edition)
Summary of css selectors that can be used with Nookogiri
I made a development environment with rails6 + docker + postgreSQL + Materialize.
Create a jar file that can be executed in Gradle
I made a plugin to execute jextract with Gradle task
Problems that can easily be mistaken for Java and JavaScript
Find a Switch statement that can be converted to a Switch expression
I made a mod that instantly calls a vehicle with Minecraft
[Personal development] I made a site to recruit test users!
Object-oriented design that can be used when you want to return a response in form format
I can no longer connect to a VM with a Docker container that can be connected via SSH