Help us understand the problem. What is going on with this article?

Service Worker(PWA)でRangeリクエストに対応する

More than 1 year has passed since last update.

はじめに

アプリケーションをPWA(Progressive Web Apps)化する際のメリットの1つとしてキャッシュ機能があると思いますが、Service Workerで動画をキャッシュするとsafariで再生できない現象が発生します。
今回はその対応方法としてService WorkerにおけるRangeリクエストへの対応方法をご紹介します。
前提として、ionic(angular)、SW-Toolboxを使用しています。

問題点

ネットーワーク経由で動画を取得している時には問題なく動画が再生されるが、キャッシュから取得すると再生されない。(safariのみ)
ソースコード(抜粋)は以下の通り。

hoge.html
<video [poster]="posterUrl" playsinline>
  <source [src]="videoUrl" type="video/mp4" />
</video>
hoge.ts
export class HogeComponent {
  @Input() posterUrl: string;
  @Input() videoUrl: string;
    :
}
service-worker.js
'use strict';
importScripts('./build/sw-toolbox.js');

self.toolbox.options.cache = {
  name: 'ionic-cache'
};

self.toolbox.precache(
  [
    './build/main.js',
    './build/vendor.js',
    './build/main.css',
    './build/polyfills.js',
    'index.html',
    'manifest.json'
  ]
);

self.toolbox.router.any('/*', self.toolbox.fastest);
self.toolbox.router.default = self.toolbox.networkFirst;

原因

クライアントがサーバーから動画を取得する際に、HTTP Rangeリクエストをサーバーに行なっていたが、その際のステータスコードは

  • ネットワーク経由で取得: 206(Partial Content)
  • キャッシュから取得: 200(OK)

となっていた。(キャッシュはRangeリクエストに対応していないため、部分的にデータを取得することができていなかった。)

SafariではHTTP Rangeリクエストに対してレスポンスコード200を受け取るとそれ以降そのファイルを読み込まない仕様になっているため、キャッシュから動画を取得する際にはファイルが読み込まれず再生されていなかった。

<ネットワークからの場合のHTTPヘッダ>
sw-log-ok.png

<キャッシュ(Service Worker)からの場合のHTTPヘッダ>
sw2-log-ng.png

解決策

Service WorkerでRangeリクエストが来た際にステータスコード206で返却するようにレスポンスを上書きする。
リクエストがきた際にキャッシュがある場合はキャッシュから取得し、ない場合はネットワークから取得するだけの処理は下記のようになる。
(ファイルサーバーのURLを https://hoge.hoge としています。)

service-worker.js
// Match URLs that begin with https://hoge.hoge
self.toolbox.router.get(/^https:\/\/hoge.hoge\//, (request) => {
  return new Promise((resolve, reject) => {
    caches.match(request).then((response) => {
      if (response) {
        // from cache
        resolve(response);
      }
      // from network
      return fetch(request)
        .then((response) => resolve(response))
        .catch((error) => reject(error));
    });
  });
});

上記処理だけだとRangeリクエストに対応できていないので、Rangeリクエストが来た場合の処理を追加します。

service-worker.js
  :

// Match URLs that begin with https://hoge.hoge
self.toolbox.router.get(/^https:\/\/hoge.hoge\//, (request) => {
  return new Promise((resolve, reject) => {
    if (request.headers.get('range')) {
      // Range request
      let rangeHeader = request.headers.get('range');
      let rangeMatch = rangeHeader.match(/^bytes\=(\d+)\-(\d+)?/)
      let pos = Number(rangeMatch[1]);
      let pos2 = rangeMatch[2];
      if (pos2) { pos2 = Number(pos2); }

      caches.open('ionic-cache')
        .then((cache) => {
          return cache.match(request.url);
        }).then((res) => {
          if (!res) {
            return fetch(request.url).then(res => res.arrayBuffer());
          } else {
            return res.arrayBuffer();
          }
        }).then((ab) => {
          let responseHeaders = {
            status: 206,
            statusText: 'Partial Content',
            headers: [
              ['Content-Type', 'video/mp4'],
              ['Content-Range', 'bytes ' + pos + '-' + (pos2 || (ab.byteLength - 1)) + '/' + ab.byteLength]]
          };

          let abSliced = {};
          if (pos2 > 0){
            abSliced = ab.slice(pos, pos2 + 1);
          } else {
            abSliced = ab.slice(pos);
          }

          resolve(new Response(abSliced, responseHeaders));
        });
    } else {
      // Non-range request
      caches.match(request).then((response) => {
        if (response) {
          // from cache
          resolve(response);
        }
        // from network
        return fetch(request)
          .then((response) => resolve(response))
          .catch((error) => reject(error));
      });
    }
  })
});

  :

実行すると下記の様な結果となります。

sw-log-ok2.png

これでService WorkerのRangeリクエスト対応は完了です。
ただし、このままキャッシュをガンガン保存するとキャッシュ容量の上限に達してUncaught (in promise) DOMException: Quota exceeded.というエラーが発生してしまう可能性が高いので、Service Workerにてキャッシュの制限を行うのがオススメです。
下記の例ではキャッシュの保持期限と保持数を定義しています。

service-worker.js
  : 

self.toolbox.options.cache = {
  name: 'ionic-cache',
  maxAgeSeconds: 60 * 60 * 24,
  maxEntries: 50
};

  : 

<参考>

biga816
TypeScriptが好き。
http://tanakas.org/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away