VOYAGE GROUP VR室ブログ

VOYAGE GROUP VR室のブログです。コンテンツの紹介や制作方法、イベントレポートなどについて書きます。原則毎週水曜日更新。

【Web VR】IndexedDBを使って、大きい動画や3Dモデルをローカルに保存する!

こんにちは!VOYAGE GROUPでのインターンがもうすぐ終わることとなり、今までVR室でやってきたことをしみじみと思い出しているtakapikoXです。秋みたいな物悲しい気分です。まだ夏ですけど。

今回は、IndexedDBを使って動画や3Dモデルのダウンロードを工夫できないか試してみたので、皆さんにお伝えしたいと思います。

目次

WebVRにおいてIndexedDBを利用する目的とメリット

ネイティブアプリにはあまりないWeb VRならではの課題として、「データのダウンロード時間」というのがあります。

そもそもVRでは、全天球動画や3Dモデルなど、通常のアプリケーションよりは容量の大きいデータを利用することが多いですが、 ネイティブアプリにおいてはアプリケーションをDLする際にデータも同時に含めることができるので、それが実行時に問題となることは多くありません。

一方、一般的なWebの仕組みを利用した場合、リソースは原則実行時に読み込まれることになります。すなわち、ユーザーが特定のURLにアクセスした際に読み込みが開始されることになり、回線がよくないと体験を著しく損なってしまう可能性が高いです。

今回はその対策として、IndexedDBを用いて非同期でデータを読み込んで利用する方法について調べてみました。この手法を使うことで

  • ローディング画面やチュートリアル中にデータを読み込んでおくことで、実行時にアプリケーションがブロックされる苦痛が軽減される
  • 2回目以降にアクセスした際に、DLなしにスムーズにコンテンツを楽しむことができる

といったメリットがあるはずです!

IndexedDBの特長とスペック

ブラウザ側でデータを保存する機能の一つで、文字通りデータベースのように使用できます。cookieやWeb Storageに比べて、動画や3Dモデルのような大容量のデータを扱うのに適しています。詳細な使い方は以下の記事が詳しいです。

IndexedDBの使い方 - Qiita

cookie,Web Storageとの比較

ブラウザ側でのデータ保持を実現する機能としては、IndexedDB以外にもcookie, Web Storageなどがありますが、IndexedDBは、

  • 保存できる容量が大きいこと
  • 様々なデータ形式で保存できること
  • 非同期で処理を行うことができること

といった点で、大きいデータを扱う目的において他の方法よりも優れています。

またIndexedDBは、数値や文字データ以外にも、様々な形式のデータを保存することができます。blob形式にしてしまえば、ほとんどのデータ形式を保存することができます。

さらに、非同期処理を行えるということは、ユーザーの操作や画面の動きをブロックすることなく大容量のデータを読み書きできることを意味します。

対応ブラウザ

対応ブラウザについて見ていきましょう。

結論としては、最近の主要なブラウザほぼ全てで使えると言って良いのではないかと思います。(一部気を付けるべきブラウザがありますが…)

iOS

iOS8以降で利用できます。

ただし、mobile SafariはIndexedDBにデータをblob形式で保存することができません。blob形式のデータを扱うこと自体はできるので、Safari対応も考えると、arraybuffer形式でデータを保存して、使用時にblobに変換しなおすという実装にするのがよいでしょう。またiOS8のmobile SafariはIndexedDB周りにバグが多いので、対応するのであればよく確認する必要がありそうです。

Android

最近のAndroidでデフォルトブラウザとなっているChromeでは問題なく利用できるようです。

Android 4.3以前のデバイスで標準となっているAndroidブラウザに関して、いくつかのデバイスで調査を行ったところ、以下のような結果になりました。

端末名 Android OS 結果
AQUOS PHONE Xx:302SH 4.2.2 indexedDB.openが動作せず
(IndexedDBが実装されていない?)
GALAXY S III α SC-03E 4.1,1
URBANO L01 4.2.2
ARROWS Z ISW13F 4.0.3
ARROWS NX F-01F 4.2.2

というわけなので、Android標準ブラウザでは基本的にIndexedDBは動かないと考えてしまってよさそうです。

デスクトップ

MDNのドキュメントによれば、デスクトップ環境ではIndexedDB、blobともに、ほぼ全てのブラウザの最新版でサポートされているようです。

IndexedDB - Web API インターフェイス | MDN

Blob - Web API インターフェイス | MDN

※ なお、モバイル端末でのblob利用についてはほとんど「?」と記載されていますが、Safari8,9,10, Google Chrome58, Firefox48 に関しては、モバイル端末上でblobが動作することを確認しました。

容量

最後に、容量についてです。以下の記事で各ブラウザにおけるIndexedDBの容量が計測されていますが、こちらの記事は2014年1月公開とやや古いので、Browser Storage Abuserというツールを使用して、現在の主要ブラウザにおけるIndexedDBの容量を改めて調べてみました。

www.html5rocks.com

とはいえ、基本的に「up to quota」という概念で、ユーザーにパーミッションをとらない場合は、端末の空き容量の10%までがアプリケーションに割り当てられるようですが、端末の空き容量をいっぱいにするのが困難だったので、今回は十分に空きのある端末で試しています。

また、実際に端末にデータを保存することで限界を見極めるツールなので、今回は時間の都合上1GBまでしか試行していません。ご了承ください。

モバイル

Google Chrome 58 Firefox 48 Safari 10
more than 1GB more than 1GB more than 1GB

デスクトップ

Google Chrome 58 Firefox 54 Microsoft Edge 38
more than 1GB more than 1GB 500MB

主要なブラウザは、十分に空きがある場合は基本的に1GB以上の容量があるようです。が、空き容量によるということは、「必ず利用できるわけではない」ということでもあるので、使えなかった場合を想定した実装が必要になりそうですね。

実際に使ってみる

ここからは、実際にIndexedDBを利用する方法について説明していきます。

動画をIndexedDBに保存してみる

まず動画をIndexedDBに保存して再利用してみます。 コードの流れですが、mobile Safariにも対応できるように、以下のようにします。

  1. IndexedDBが最新版かどうか確認
  2. 最新版でなければ動画データをarraybufferとしてサーバーからダウンロードし保存、最新版ならばIndexedDBからロード
  3. ロードしたarraybufferをblob形式に変換
  4. blobデータからBlob URLを生成
  5. Blob URLをビデオタグに埋め込む

実際にコードに書き起こしてみるとこうなります

//1.IndexedDBを開く
var idbReq = IndexedDB.open("test", version);
var is_db_updated = false;
console.log("opening IndexedDB");

//2.DBの新規作成時、またはバージョン変更時に実行するコード
idbReq.onupgradeneeded = function (event) {
    var db = event.target.result;
    is_db_updated = true;
    console.log("update database");

    //過去にDBを作成したことがなければ新規にテーブルを作成。if文後半はios8用の記述
    if (event.oldVersion < 1 || (event.oldVersion & 0x7fffffffffffffff) < 1) {
        console.log("create database");
        event.target.result.createObjectStore("video", { keyPath: "video_id" });
    }

    //onupdateneededで呼ばれたtransactionが終了してから処理を行う(既存のtransactionが新規のtransactionをブロックしないように)
    event.target.transaction.oncomplete = function () {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', movie_url, true);
        xhr.responseType = 'arraybuffer';
        //IndexedDBを使用した場合との時間差を明確にするため、XMLHttpRequestのキャッシュを無効化(本来は必要なし)
        xhr.setRequestHeader('Pragma', 'no-cache');
        xhr.setRequestHeader('Cache-Control', 'no-cache');
        xhr.setRequestHeader('If-Modified-Since', 'Thu, 01 Jun 1970 00:00:00 GMT');


    //HttpRequestが正しく行われた場合
        xhr.onload = function (e) {
            console.log("movie is loaded")
            try {
        //"video"オブジェクトストアを読みreadwrite権限付きで使用することを宣言
            var transaction = db.transaction("video", "readwrite");
            //オブジェクトストアの取り出し
                var videoStore = transaction.objectStore("video");

                var arraybuffer = e.target.response;

                //データの追加
                videoStore.put({ video_id: "1", video: arraybuffer });
                //データのblob化
                var blob = new Blob([arraybuffer], { type: 'video/mp4' });
        //blobデータから疑似URLを生成し、ビデオタグへ埋め込み
                var URL = window.URL || window.webkitURL;
                video.src = URL.createObjectURL(blob);

                console.log("updated IndexedDB");
            } catch (e) {
                console.log(e.message);
            }
        }
        xhr.send();
    }
}

//3.IndexedDBが正常に開かれたときの処理
idbReq.onsuccess = function (event) {
  //IndexedDBがupdateされていなければ
    if (!is_db_updated) {
        console.log("opened DB");
        var db = event.target.result;
        //"video"オブジェクトストアを読みreadonly権限付きで使用することを宣言
        var transaction = db.transaction("video", "readonly");
        //オブジェクトストアの取り出し
        var videoStore = transaction.objectStore("video");
        //オブジェクトストアへ取り出しリクエスト
        var getReq = videoStore.get("1");

        //videoオブジェクトストアからのデータの取り出し
        getReq.onsuccess = function (event) {
            try {
                //データのblob化
                var blob = new Blob([event.target.result.video], { type: 'video/mp4' });
        //blobデータから疑似URLを生成し、ビデオタグへ埋め込み
                var URL = window.URL || window.webkitURL;
                video.src = URL.createObjectURL(blob);
            } catch (e) {
                console.log(e.message);
            }
        }
        getReq.onerror = function (event) {
            console.log("db is opened, but failed data load");
        }
    }
}

実際に作ってみたページがこちらになります。

サンプルを開く

「Play/Pause」ボタンを押すと動画のロードが始まり、ロードが終わり次第動画を再生するようになっていますが、1回目と比べて、リロードして2回目以降の動画のロード時間が早くなっているのがわかりやすいと思います。

3DモデルもIndexedDBに保存してみる

次は、3DモデルをIndexedDBに保存してみます。

blobで扱うために、glTF形式で3Dモデルを保存しておくとよさそうです。

「glTF」は3Dモデルのファイル形式の一つで、WebGLにおいては特定ライブラリに依存しない汎用フォーマットです。開発元のクロノス・グループによると、3Dモデルにおけるjpegのような広く浸透したフォーマットを目指しているとのこと。すごいですね!

以下のサイトでより詳しく説明されていますので、興味ある方はぜひ。

qiita.com

一度3DモデルをglTF形式にしてしまえば、後は動画の場合と同じようにでIndexedDBに保存することができます。

以下、3DモデルをIndexedDBに保存して再利用するサンプルです。

サンプルを開く

ページのロードが始まり次第、3Dモデルを読み込んで表示します。

はじめはドゥードゥーのモデルを使用していたのですが、データ量が小さいためか(4.39MB)、IndexedDBを使用することによる恩恵を感じにくかったので、使用するデータをより大容量のもの(18.0MB)に変更したところ、IndexedDBによる高速化が感じられるようになりました。

2回目以降の動画(3Dモデル)の読み込みが速くなっているのが実感できると思います。

まとめ

今回実装したアプリケーションは、単純に一度ダウンロードしたデータを保存しておくというシンプルなものでしたが、実際に利用する際は、VRモードに入る前にローディング画面を入れたり、チュートリアル中にデータを読み込んでおくなどの工夫をすることで、より快適なユーザ体験を提供できるようになるでしょう。

Web VRを実用化していくにあたって、ローディング時間というのは避けて通ることはできない課題だと思いますので、ぜひ今回ご紹介した方法が参考になれば!

最後に、今回作成したソースコードはこちらのリポジトリにあります。

Githubリポジトリを閲覧する