MicroAd Developers Blog

マイクロアドのエンジニアブログです。インフラ、開発、分析について発信していきます。

無停止ブラウザ配信をささえる仕組み

どうもはじめまして、アプリケーションエンジニアの築野です。

主にデジタルサイネージ配信関連に携わっております。

今回は、Webブラウザにて配信アプリを支えている仕組みについて紹介したいと思います。

デジタルサイネージとは

従来の看板や紙のポスターに代え液晶ディスプレイなどを用いて、画像や動画を表示させて情報を発信するシステムのことを指します。

f:id:microad-developer:20180806104620j:plain
マイクロアドデジタルサイネージ社では、上記写真のようなデジタルサイネージを商材としています。 こちらはゴルフ場での例ですが、施設情報などの情報更新頻度を高め、利用者の満足度を高めてリピート率の向上に繋げることを目的に導入を行ったりしています。
http://www.mads.co.jp/about-sototere/

またマイクロアドでは、以下のように社内告知等の情報共有にも利用しております。 f:id:microad-developer:20180711185818j:plain

配信で実現したいこと

街中でも最近はよく見かけるデジタルサイネージですが、設置される環境によってはネットワークが不安定だったりもします。

しかし途切れることなく、常に動画や画像などが絶えず表示され続けていると思います。

このネットワーク環境に左右されず、出来るだけ表示が止まらず配信されることを実現したい。

Webブラウザでね。

実現したこと

ネットワークの環境に左右されず動画等を表示し続けるには、配信端末自身(PC上)に 保存して表示させてしまえば問題解決となります。

ただ、普通にCドライブ等に保存してもWebブラウザから見ることはできません。

なので、Webブラウザに備わってるキャッシュの仕組みを利用するしかありません。

しかし本来備わっているキャッシュの仕組みは、あくまで一時的にウェブページを保存して 次回に同じページを見たときに素早く表示されるものであり、Webブラウザ任せのものとなるので制御ができません。

そこでHTML5で備わった、localStorage(ローカルストレージ)とindexedDBの利用を検討しました。

さすがに、まったくネットワークが繋がらない状況だと厳しいですが、 少しでも接続されている間に、表示させる動画等を予め保存させよう。

実際の利用イメージは以下のようになります。

f:id:microad-developer:20180705192852p:plain

localStorage(ローカルストレージ)とは

簡単に説明してしまうと、通常のキャッシュとは別領域にデータを保存し WebブラウザからJavaScriptを用いて利用するものとなります。

localStorageはWebStorageと呼ばれるものの1つで

  • localStorage:ブラウザが管理する持続的に保持されるもの(ブラウザを閉じても残ります)
  • sessionStorage:その名の通りページのセッションが有効の間保持されるもの

と二種類あります。

各ブラウザの実装によって誤差はありますが、およそ5MBぐらいしか保存できません。

これでは、動画を複数保存させたいとするとすぐにいっぱいになってしまいます。

そこでより多くのデータを保存できるIndexedDBというストレージの登場です。

IndexedDBとは

ファイルやblobを含む構造化された多くの種類のデータをより大容量で保存できるキーバリューストレージとなります。

ストレージの最大容量は動的であり、ハードディスクドライブのサイズに応じて変わります。

Firefoxでは、クォータマネージャーと呼ばれるものが絶えずディスク容量を監視しており、 デフォルトでは、最大10%のディスク空き領域を使用できます。

ただし基本的には、2GBが上限となります。

(Firefoxなら永続化オプションを指定することにより、この制限も解除できます。)

実際の利用方法

実際にどのように保存して再生を行うかの簡単ですが例を載せたいと思います。 (エラー処理等は考慮していませんので、実際に使う場合は注意してください。)

[HTML]

<html lang="ja">
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <script src="view.js"></script>
    <style type="text/css">
        body {
            margin: 0px;
            background-color: #000;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .view,
        video {
            width: 100%;
        }
    </style>
</head>
<body>
    <div id="view" class="view"></div>
</body>
</html>

HTMLは表示を表示させるだけとなりますので、シンプルなものとしました。

<div id="view" class="view"></div>

上記の箇所に再生させる為のvideタグを設置して動画を表示させます。

[JS]

let db;

// インターネットより動画の取得
async function download() {
  return new Promise(resolve => {
    let xmlhttp = new XMLHttpRequest();
    xmlhttp.onloadend = function() {
      resolve(this.response);
    };
    xmlhttp.open("GET", "/blog/sample.mp4");
    xmlhttp.responseType = "blob";
    xmlhttp.send();
  });
}

// 取得した動画をIndexedDBに保存
async function cache(downLoadData) {
  return new Promise(resolve => {
    // DB接続
    let conn = indexedDB.open("sample_client_db");
    // DB作成
    let cretiveCache = void 0;
    conn.onupgradeneeded = function(ev) {
      // DBが存在しない場合作成する
      db = ev.target.result;
      // オブジェクトストアの作成
      cretiveCache = db.createObjectStore("sample_cache", {
        keyPath: ["creativeid"]
      });
      // 検索用INDEXの作成
      cretiveCache.createIndex("idx_cache_creative_id", "creativeid");
    };
    conn.onsuccess = function(ev) {
      db = ev.target.result;
      let tx = db.transaction(["sample_cache"], "readwrite");
      let store = tx.objectStore("sample_cache");
      // 動画をDBに保存する
      store.put({
        creativeid: 1,
        blob: new Blob([downLoadData])
      });
      resolve("success");
    };
  });
}

// IndexedDBに保存された動画の再生
async function play() {
  let tx = db.transaction(["sample_cache"], "readwrite");
  let store = tx.objectStore("sample_cache");
  let cacheIdx = store.index("idx_cache_creative_id");
  let creativeFileStorage = [];
  // 保存された動画の取得
  let request = cacheIdx.openCursor(1);
  request.onsuccess = function(event) {
    let cursor = event.target.result;
    creativeFileStorage = cursor.value;
    // DBに保存された動画を取得
    let videoUrl = window.URL.createObjectURL(creativeFileStorage.blob);
    //再生用videoタグ作成
    let video = document.createElement("video");
    video.src = videoUrl;
    video.autoplay = true;
    video.setAttribute("controls", "");
    document.getElementById("view").appendChild(video);
  };
}

async function load() {
  download()
    .then(cache)
    .then(play);
}
window.addEventListener("load", load);

JSは、少し長くなりましたが

// DB接続
let conn = indexedDB.open("sample_client_db");

indexedDB.openで、DB名を指定して接続をします。

そして、以下の箇所で保存と取得が行えます。

// 動画をDBに保存する
store.put({
  creativeid: 1,
  blob: new Blob([downLoadData])
});

ダウンロードした動画ファイルをBlob型として保存しています。

// 保存された動画の取得
let request = cacheIdx.openCursor(1);

このように利用するだけでしたらとても簡単に行うことができます。

実際に動作させて本当に保存されているかは、Webブラウザの開発ツールで中身を確認できます。

ここでは、Firefoxで確認したいと思います。

メニュー >> ウェブ開発 >> ストレージインスペクタ f:id:microad-developer:20180705185452p:plain

※ストレージインスペクタがメニューに表示されていない場合は、 開発ツールを表示させてから、ツールの右上にある「オプション」または「設定」を選択していただき、 標準の開発ツールの項目にある「ストレージ」にチェックを入れれば、表示されるようになります。

1:"{"creativeid":1,"blob":{}}"

と保存されているデータを確認することが出来ます。

なお、blobは入っていないように見えますが実際には保存されていますのでご安心を

はまったところ

当初はChromeを採用しようとしていて検証を行っていましたが、 ファイルを削除して新たに保存しようとしても、なぜか容量オーバーになってしまうことがありました。

どうやらChromeは削除自体はできるのですが、(調査時点では)物理的に削除されるタイミングはあくまでブラウザ任せとなるので、削除しても利用できる領域が即座に確保できないようでした。

どうも各ブラウザによって多少の実装が異なる様子...

ブラウザ毎の対応状況による動作の違い

f:id:microad-developer:20180705193051p:plain

参考:https://developer.mozilla.org/ja/docs/Web/API/IndexedDB_API#Browser_compatibility

また上限の2GBをギリギリまで保存してしまうと、削除も保存もできなくなるので注意してください。

削除する際にある程度の空き領域がないと削除を行うことが出来なくなり、しかたなくIndexedDBを手動で消す自体に陥ります。

いろいろな検証を経て現在はFirefoxで稼働させていますが、そろそろバージョンアップの必要も出始めてきています。

(IndexedDB周りが理由でのバージョンアップではないのですが、ここでは省略させていただきます。)

最後に

  • jsでのマルチスレッド ( Web Woker )
  • 拡張機能 ( Add-ons ) など他にも支えているものはあります。

また起動時にネットワークが切れたら、そもそも最初のURLにアクセスができない。

ここを解決したいなと思ったところ「プログレッシブウェブアプリ」で解決の道もあり模索の道は続きます。

また別の機会にかけたらと思います。