LoginSignup
39
23

More than 3 years have passed since last update.

Oculus Go x THETA V連携で、かんたん360度閲覧

Last updated at Posted at 2019-01-25

はじめに

こんにちは、リコーの@roohii_3です。

RICOH THETAプラグイン機能で、Oculus Goから直接THETAのファイルを見られるプラグインを作ってみました。
以前、Oculusブラウザから見る方法を紹介しましたが、今回のプラグインではOculusギャラリーアプリから見られるようになります。

DLNAに対応しているなら、Oculus Go以外の機器からでも見られる場合もあります。
たとえば、PSVR+PS4 Proの"Media Player"からTHETAの中身を見れることも確認しています。

THETAとOculus GoをWi-Fiで繋ぎ、THETAのModeボタンを長押しするだけで、
こんな感じでOculusギャラリーRICOH THETAが表示されます。
oculus_gallery_devices.png

サムネイルから見たい静止画・動画を選択すると、360度表示されます。
oculus_gallery_photos.png

もし360度表示になっていなかったら、プロジェクション設定から360度表示にできます。
oculus_gallery_projection.png

本記事で紹介しているプラグイン(ベータ版)は、下記リンク先からインストールできます。
(2019/05/23 追記) 公式プラグインとしてリリースされました!下記リンク先からインストールできます。
https://pluginstore.theta360.com/plugins/com.theta360.vrmediaconnection/
使い方の詳細はマニュアルをご覧ください。

RICOH THETA VとOculus Goをお持ちの方はぜひ試してみてください。
プラグインのインストール方法・変更方法についてはこちら

使い方

使い方には2種類あります。
※ 手順に出てくるTHETAの「スマートフォン基本アプリ」はここからダウンロードできます。

[1] THETAとOculus Goを1対1接続して使うとき

THETAをアクセスポイント(AP)モードにして、THETAにOculus Goを直接接続させて見る場合です。

[手順]

  1. THETAの無線ボタンを押して、APモードにする(無線ランプが青色で点滅します)
  2. Oculusの無線LAN設定から、THETAに接続する
  3. THETAのModeボタンを長押しし、プラグインモードにする
  4. Oculusのギャラリーを選択し、”RICOH THETA”を選ぶ

動画をみるとき、途切れる場合があります。あらかじめ、スマートフォン基本アプリからTHETAのWi-Fiの周波数帯域を5GHzに設定しておく(→詳細)ことをおすすめします。
※ ただし周波数帯域を5GHzにする場合は、屋内のみでご使用ください。

[2] THETAとOculus Goを同じ無線LAN下に置いて使うとき

THETAをクライアント(CL)モードにして、THETAとOculus Goを同じ無線LANに接続する場合の手順です。
複数のOculusから同時にTHETAの中身を見ることもできます。

[事前準備]
1. スマートフォン基本アプリより、THETAを接続する無線LANのSSIDとパスワード設定を行う
(→[YouTube]設定方法マニュアル

[手順]
1. THETAの無線ボタンを押して、CLモードにする(無線ランプが緑色で点滅します)
2. 無線ランプが点滅から点灯に変わり、THETAが無線LANに接続されたことを確認する
3. Oculusの無線LAN設定から、THETAと同じ無線LANに接続する
4. THETAのModeボタンを長押しし、プラグインモードにする
5. Oculusのギャラリーを選択し、”RICOH THETA”を選ぶ

注意点

  • 動画転送のパフォーマンスは無線LAN環境に依存するので、途切れることがあります。
    THETAの動画撮影設定で、ビットレートを「Low」に設定することや、5GHz無線LANを使うことで状況が改善されることがあります。
  • Oculus Goでは[1]、[2]の方法どちらでもできますが、機種によっては[1]の方法で使えない場合があります。PSVRでは[1]の方法は使えないため、[2]の方法でお試しください。
  • Oculusギャラリーにダウンロードメニューがありますが、APモードではダウンロードできません。
  • デフォルトでは、動画は繋がれていない状態(Dual-Fisheye形式)で保存するように設定されています。
    スマートフォン基本アプリから動画の「撮影時スティッチ」をONに設定しておくと、Oculus上で360度表示できる形式(Equirectangular形式)で保存されます。
  • 閲覧時、天頂補正されません。
    撮影するときに、THETAをまっすぐ立てて撮影することをおすすめします。

    ※ (2019/05/23追記) 公式版では、動画をあとからスティッチング・天頂補正できるようになりました。 詳しくはマニュアルをご覧ください。

RICOH THETAプラグインについて

RICOH THETAって何?という方にご説明すると、RICOH THETAとは、弊社で出している全周囲360度の映像を撮れるカメラのことです。THETAシリーズの一つであるRICOH THETA VAndroidで動いています。「RICOH THETAプラグイン」とはTHETAをカスタマイズできる機能のことであり、Androidアプリを作る感覚で作ることができます。

RICOH THETAプラグイン開発者コミュニティでは、他にもプラグインについての記事を書いています。興味を持たれた方がいれば、ぜひ読んでみてください。プラグイン詳細についてはこちら。興味を持たれた方はtwitterのフォローとTHETAプラグイン開発コミュニティ(slack)への参加もぜひどうぞ。

実現方法

ここからは、本プラグインの実現方法について書きます。

本プラグインは、DLNAのDMS機能にならって実装しました。DMSは、映像や音楽などのコンテンツを他のDLNA対応機器に配信する機能のことです。THETAがコンテンツを配信するためのサーバーとなり、Oculus Go側はコンテンツを見るためのクライアントになります。
※ 本プラグインでは、DMSのすべての機能を実装したわけではありません。そのため、本プラグインでDLNA・DMS機能すべてを行える保証はないことにご留意ください。

私自身、勉強中なので具体的には説明できませんが、DLNAUPnPという機器同士を接続するプロトコルを基盤にしているようです。機器間の情報のやりとりにはXMLが用いられていますが、実際のコンテンツの配信はHTTPで実現しているようです。

Cling

UPnP・DLNA周りのややこしい処理は、Clingというライブラリに任せました。
ネットワーク内の機器の認識や、機器間の情報やりとりなど、ほとんどClingがやってくれました。XMLの生成もClingがまかなってくれて、実際のところXMLに直接触れることはありませんでした。

実装の際には、Cling Coreマニュアルに「5. Cling on Android」という項目があり、それを参考にしました。本プラグインのようなメディアサーバーを作る場合は、Cling Supportマニュアルの「3. Accessing and providing MediaServers」の項目が参考になります。

デバイス情報

Cling Coreマニュアルの「5.3. Creating a UPnP device」には、UPnPサービスの実装例が載っています。本プラグイン用にアレンジすると、以下のようなコードになります。
また、このコードの記述に従ってデバイス情報のXMLが自動生成されます。他のDLNAデバイスに生成されたXMLが送られ、THETAの基本情報・サービス情報が認識されます。

コードの中身を簡単に説明すると、
コードのはじめの方では、メディアサーバーに相当するデバイス(今回の例はTHETA)の基本情報を設定しています。
コードの中ほどでは、デバイスに紐づけるためのサービスの定義をしています。1つのデバイスに対し、機能に応じて1つ以上のサービスを設定します。メディアサーバーの場合は「コネクションマネージャ」や「コンテンツディレクトリ」というサービスが必要です。
コードの終わりの方で、定義した基本情報やサービスなどをデバイスに紐づけています。

protected LocalDevice createDevice()
        throws ValidationException, LocalServiceBindingException {

    // デバイスの基本情報を定義
    DeviceType type = new UDADeviceType("MediaServer", 1);
    DeviceDetails details = new DeviceDetails(
            "RICOH THETA",
            new ManufacturerDetails("RICOH"),
            new ModelDetails(
                    "VR Media Connection BETA",
                    "VR Media Connection for RICOH THETA",
                    "0.1.0"
            )
    );

    // サービス(機能)を定義
    LocalService contentDirectory = new AnnotationLocalServiceBinder()
            .read(ContentDirectoryService.class);
    contentDirectory.setManager(
            new DefaultServiceManager<ContentDirectoryService>(
                    contentDirectory, ContentDirectoryService.class));

    LocalService connectionManager = new AnnotationLocalServiceBinder()
            .read(ConnectionManagerService.class);
    connectionManager.setManager(
            new DefaultServiceManager<ConnectionManagerService>(
                    connectionManager, null) {
                @Override
                protected ConnectionManagerService createServiceInstance() throws Exception {
                    return new ConnectionManagerService(sourceProtocols, null);
                }
            }
    );

    // アイコンの読み込み
    Bitmap icon = BitmapFactory.decodeResource(getResources(), R.drawable.theta_image_01);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    icon.compress(Bitmap.CompressFormat.PNG, 100, baos);

    // 上で定義したものをLocalDeviceに紐づける
    return new LocalDevice(
            new DeviceIdentity(udn),
            type,
            details,
            new Icon("image/png",
                    icon.getWidth(), icon.getHeight(), 8,
                    "icon.png", baos.toByteArray()),
            new LocalService[]{
                    connectionManager, contentDirectory});
}

コンテンツ情報の生成

クライアント側にコンテンツの情報を伝える際もXMLが使われます。

THETAで扱うコンテンツは動画と静止画ですが、静止画の情報は下記のような形式になっています。「DIDL-Lite」形式というもので、UPnPで定義されているようです。

<item id="(コンテンツID)" parentID="(親コンテンツID)" restricted="1">
    <dc:title>(ファイルタイトル)</dc:title>
    <dc:creator>(作成者)</dc:creator>
    <upnp:class>object.item.imageItem</upnp:class>
    <upnp:albumArtURI>(サムネイルのURL)</upnp:albumArtURI>
    <res protocolInfo="http-get:*:image/jpeg" size="(ファイルサイズ)" resolution="(解像度)">
        (コンテンツのURL)
    </res>
</item>

上記のxmlは、下記のようにClingのItemクラスにコンテンツ情報を格納すると自動生成されます。

String MIMETYPE_JPEG = "image/jpeg";

Res res = new Res(new MimeType(
        MIMETYPE_JPEG.substring(0, MIMETYPE_JPEG.indexOf('/')),
        MIMETYPE_JPEG.substring(MIMETYPE_JPEG.indexOf('/') + 1)),
        filesize,
        fileUrl);
res.setResolution(width, height);

ImageItem imageItem = new ImageItem("id", "parentId", "title", "creatorName", res);
imageItem.addProperty(new DIDLObject.Property.UPNP.ALBUM_ART_URI(new URI(thumbnailUrl)));
imageItem.setRestricted(true);

ディレクトリの情報はContainerクラス、動画や静止画などの情報はItemクラスで管理します。Itemには、ImageItemVideoItemなど、コンテンツ内容に特化した派生クラスがいくつかあるようです。

上記のXMLとコード中で設定しているコンテンツID(id)親コンテンツID(parentId) へは任意の文字列を設定します。
「親コンテンツ」と呼んでいるのはここでは「ディレクトリ」のことですが、ディレクトリとその下のコンテンツは、これら2つのIDをセットとして紐づいています。これにより、コンテンツの階層構造が作られます。なお、ルートとなるContainerの親コンテンツIDへは、"-1"を設定する必要があるようです1

Clingを使いこなせれば不要かもしれませんが、私は「ContainerとItemの構造を『コンテンツID』で管理するようなクラス」を別途用意しました。

コンテンツ配信

マニュアルにあるように、メディアサーバー側にはContentDirectoryを実装する必要があるようです。
マニュアルの例にならって、本プラグインでもAbstractContentDirectoryServiceを継承したクラスを作りました。このクラスのbrowse()は、ざっくりと書くと下記コードのような感じです。

@Override
public BrowseResult browse(String objectID, BrowseFlag browseFlag,
                            String filter,
                            long firstResult, long maxResults,
                            SortCriterion[] orderby) throws ContentDirectoryException {

    try {
        DIDLContent didl = new DIDLContent();

        DIDLObject content = ContentTree.getNode(objectID).getContent();

        if (content instanceof Item) {
                didl.addItem((Item) content);
                return new BrowseResult(new DIDLParser().generate(didl), 1, 1);

        } else {
                for (Container container : ((Container) content).getContainers()) {
                didl.addContainer(container);
                }
                for (Item item : ((Container) content).getItems()) {
                didl.addItem(item);
                }

                String xml = new DIDLParser().generate(didl);
                return new BrowseResult(xml,
                        ((Container) content).getChildCount(),
                        ((Container) content).getChildCount());
        }

    } catch (Exception ex) {
        throw new ContentDirectoryException(
                ContentDirectoryErrorCode.CANNOT_PROCESS,
                ex.toString()
        );
    }
}

コード中に出てくるContentTreeは、「コンテンツ情報の生成」項で触れた「ContainerとItemの構造を『コンテンツID』で管理するクラス」です。コンテンツIDをキーにして、それに対応したDIDLObjectContainerItemもDIDLObjectの派生クラスです)を取り出すようにしています。
browse()の引数のobjectIDは「コンテンツID」と同等の値となっており、これにより適当なDIDLObjectContentTreeから取り出します。
取り出したDIDLObjectDIDLContentaddContainer()あるいはaddItem()し、DIDLParser().generate()によってDIDL-Lite形式のXMLに変換されます。

このbrowse()は、「クライアントがサーバーへ最初にアクセスしたとき」と、そのあと「クライアントが下層ディレクトリへアクセスするたび」に呼ばれるようです。
クライアントの選択に応じたコンテンツIDが引数objectIDとして送られますが、最初にアクセスするときはルートContainerのコンテンツIDが指定されるようです。(おそらく、親コンテンツIDに”-1”を設定したものがルートであると判断しているのだと思います1。ルートContainerを取得できれば、コンテンツID・親コンテンツIDを使って数珠つなぎ的に下層ディレクトリが分かります。)
クライアント側はbrowse()経由でDIDL-Lite形式のコンテンツ情報を受け取り、受け取った情報をプレーヤー等に反映します。

個別の静止画や動画を配信する際にはHTTPが使われます。
「コンテンツ情報の生成」項のコードにあるResfileUrlという値が設定されていますが、クライアントからコンテンツ配信の要求があったときには、このURLを通じて配信します。
本プラグイン内ではNanoHTTPDを使用しています。コンテンツ配信の要求があると、指定されたURLに対応したファイルを読み込み、結果として返すような実装をしています。

おわりに

まだベータ版なので、使い勝手が悪かったり、不具合があることもあるかもしれません。
正式版リリースに向けて、日々改善させていこうと思います!

(2019/05/23 追記) 公式プラグインとしてリリースされました!是非試してみてください。インストールはこちら


  1. UPnP仕様の”ContentDirectory:4” > ”5.2.13 Hierarchical location”項(p.41)より 

39
23
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
39
23