Oculus Go x THETA V連携で、かんたん360度閲覧
はじめに
こんにちは、リコーの@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 が表示されます。
サムネイルから見たい静止画・動画を選択すると、360度表示されます。
もし360度表示になっていなかったら、 プロジェクション設定 から360度表示にできます。
本記事で紹介しているプラグイン(ベータ版)は、下記リンク先からインストールできます。
RICOH THETA VとOculus Goをお持ちの方はぜひ試してみてください。
なお、あくまでもベータ版なので完全な動作保証はできません。
プラグインのインストール方法・変更方法についてはこちら。
使い方
使い方には2種類あります。
※ 手順に出てくるTHETAの「スマートフォン基本アプリ」はここからダウンロードできます。
[1] THETAとOculus Goを1対1接続して使うとき
THETAを アクセスポイント(AP)モード にして、THETAにOculus Goを直接接続させて見る場合です。
[手順]
- THETAの無線ボタンを押して、 APモード にする(無線ランプが 青色 で点滅します)
- Oculusの無線LAN設定から、THETAに接続する
- THETAの Modeボタン を長押しし、プラグインモードにする
- Oculusのギャラリーを選択し、” RICOH THETA ”を選ぶ
動画をみるとき、途切れる場合があります。あらかじめ、スマートフォン基本アプリからTHETAのWi-Fiの周波数帯域を 5GHz に設定しておく(→詳細)ことをおすすめします。
※ ただし周波数帯域を5GHzにする場合は、屋内のみでご使用ください。
[2] THETAとOculus Goを同じ無線LAN下に置いて使うとき
THETAを クライアント(CL)モード にして、THETAとOculus Goを同じ無線LANに接続する場合の手順です。
複数のOculusから同時にTHETAの中身を見ることもできます。
[事前準備]
- スマートフォン基本アプリより、THETAを接続する無線LANのSSIDとパスワード設定を行う
(→[YouTube]設定方法、マニュアル)
[手順]
- THETAの無線ボタンを押して、 CLモード にする(無線ランプが 緑色 で点滅します)
- 無線ランプが 点滅 から 点灯 に変わり、THETAが無線LANに接続されたことを確認する
- Oculusの無線LAN設定から、THETAと同じ無線LANに接続する
- THETAの Modeボタン を長押しし、プラグインモードにする
- Oculusのギャラリーを選択し、” RICOH THETA ”を選ぶ
注意点
- あくまでもβ版です。完全な動作保証はできません。
- 動画転送のパフォーマンスは無線LAN環境に依存するので、途切れることがあります。
THETAの動画撮影設定で、ビットレートを「Low」に設定することや、5GHz無線LANを使うことで状況が改善されることがあります。 - Oculus Goでは[1]、[2]の方法どちらでもできますが、機種によっては[1]の方法で使えない場合があります。PSVRでは[1]の方法は使えないため、[2]の方法でお試しください。
- デフォルトでは、動画は繋がれていない状態(Dual-Fisheye形式)で保存するように設定されています。
スマートフォン基本アプリから動画の「撮影時スティッチ」を ON に設定しておくと、Oculus上で360度表示できる形式(Equirectangular形式)で保存されます。 - 閲覧時、天頂補正されません。
撮影するときに、THETAをまっすぐ立てて撮影することをおすすめします。 - Oculusギャラリーに ダウンロード メニューがありますが、APモードではダウンロードできません。
RICOH THETAプラグインについて
RICOH THETAって何?という方にご説明すると、RICOH THETAとは、弊社で出している全周囲360度の映像を撮れるカメラのことです。THETAシリーズの一つである RICOH THETA V は Android で動いています。「RICOH THETAプラグイン」とはTHETAをカスタマイズできる機能のことであり、Androidアプリを作る感覚で作ることができます。
RICOH THETAプラグイン開発者コミュニティでは、他にもプラグインについての記事を書いています。興味を持たれた方がいれば、ぜひ読んでみてください。プラグイン詳細についてはこちら。興味を持たれた方はtwitterのフォローとTHETAプラグイン開発コミュニティ(slack)への参加もぜひどうぞ。
実現方法
ここからは、本プラグインの実現方法について書きます。
本プラグインは、DLNAのDMS機能にならって実装しました。DMSは、映像や音楽などのコンテンツを他のDLNA対応機器に配信する機能のことです。THETAがコンテンツを配信するためのサーバーとなり、Oculus Go側はコンテンツを見るためのクライアントになります。
※ 本プラグインでは、DMSのすべての機能を実装したわけではありません。そのため、本プラグインでDLNA・DMS機能すべてを行える保証はないことにご留意ください。
私自身、勉強中なので具体的には説明できませんが、 DLNA は UPnP という機器同士を接続するプロトコルを基盤にしているようです。機器間の情報のやりとりには 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で定義されているようです。
(ファイルタイトル) (作成者) object.item.imageItem (サムネイルのURL) (コンテンツのURL)上記の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には、 ImageItem
や VideoItem
など、コンテンツ内容に特化した派生クラスがいくつかあるようです。
上記の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をキーにして、それに対応した DIDLObject
( Container
も Item
もDIDLObjectの派生クラスです)を取り出すようにしています。
browse()
の引数の objectID は「コンテンツID」と同等の値となっており、これにより適当な DIDLObject
を ContentTree
から取り出します。
取り出した DIDLObject
は DIDLContent
に addContainer()
あるいは addItem()
し、 DIDLParser().generate()
によってDIDL-Lite形式のXMLに変換されます。
この browse()
は、「クライアントがサーバーへ最初にアクセスしたとき」と、そのあと「クライアントが下層ディレクトリへアクセスするたび」に呼ばれるようです。
クライアントの選択に応じたコンテンツIDが引数 objectID
として送られますが、最初にアクセスするときはルートContainerのコンテンツIDが指定されるようです。(おそらく、親コンテンツIDに”-1”を設定したものがルートであると判断しているのだと思います(1)。ルートContainerを取得できれば、コンテンツID・親コンテンツIDを使って数珠つなぎ的に下層ディレクトリが分かります。)
クライアント側はbrowse()経由でDIDL-Lite形式のコンテンツ情報を受け取り、受け取った情報をプレーヤー等に反映します。
個別の静止画や動画を配信する際には HTTP が使われます。
「コンテンツ情報の生成」項のコードにある Res
へ fileUrl
という値が設定されていますが、クライアントからコンテンツ配信の要求があったときには、このURLを通じて配信します。
本プラグイン内ではNanoHTTPDを使用しています。コンテンツ配信の要求があると、指定されたURLに対応したファイルを読み込み、結果として返すような実装をしています。
おわりに
まだベータ版なので、使い勝手が悪かったり、不具合があることもあるかもしれません。
正式版リリースに向けて、日々改善させていこうと思います!
- UPnP仕様の”ContentDirectory:4” > ”5.2.13 Hierarchical location”項(p.41)より