はじめに
こんにちは、リコーの@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度表示にできます。
本記事で紹介しているプラグイン(ベータ版)は、下記リンク先からインストールできます。
(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を直接接続させて見る場合です。
[手順]
- 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]の方法でお試しください。
- Oculusギャラリーにダウンロードメニューがありますが、APモードではダウンロードできません。
デフォルトでは、動画は繋がれていない状態(Dual-Fisheye形式)で保存するように設定されています。
スマートフォン基本アプリから動画の「撮影時スティッチ」をONに設定しておくと、Oculus上で360度表示できる形式(Equirectangular形式)で保存されます。-
閲覧時、天頂補正されません。
撮影するときに、THETAをまっすぐ立てて撮影することをおすすめします。
※ (2019/05/23追記) 公式版では、動画をあとからスティッチング・天頂補正できるようになりました。
詳しくはマニュアルをご覧ください。
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で定義されているようです。
<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には、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に対応したファイルを読み込み、結果として返すような実装をしています。
おわりに
まだベータ版なので、使い勝手が悪かったり、不具合があることもあるかもしれません。
正式版リリースに向けて、日々改善させていこうと思います!
(2019/05/23 追記) 公式プラグインとしてリリースされました!是非試してみてください。インストールはこちら。