はじめに
アプリケーションをPWA(Progressive Web Apps)化する際のメリットの1つとしてキャッシュ機能があると思いますが、Service Workerで動画をキャッシュするとsafariで再生できない現象が発生します。
今回はその対応方法としてService WorkerにおけるRangeリクエストへの対応方法をご紹介します。
前提として、ionic(angular)、SW-Toolboxを使用しています。
問題点
ネットーワーク経由で動画を取得している時には問題なく動画が再生されるが、キャッシュから取得すると再生されない。(safariのみ)
ソースコード(抜粋)は以下の通り。
<video [poster]="posterUrl" playsinline>
<source [src]="videoUrl" type="video/mp4" />
</video>
export class HogeComponent {
@Input() posterUrl: string;
@Input() videoUrl: string;
:
}
'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を受け取るとそれ以降そのファイルを読み込まない仕様になっているため、キャッシュから動画を取得する際にはファイルが読み込まれず再生されていなかった。
<キャッシュ(Service Worker)からの場合のHTTPヘッダ>
解決策
Service WorkerでRangeリクエストが来た際にステータスコード206で返却するようにレスポンスを上書きする。
リクエストがきた際にキャッシュがある場合はキャッシュから取得し、ない場合はネットワークから取得するだけの処理は下記のようになる。
(ファイルサーバーのURLを https://hoge.hoge
としています。)
// 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リクエストが来た場合の処理を追加します。
:
// 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));
});
}
})
});
:
実行すると下記の様な結果となります。
これでService WorkerのRangeリクエスト対応は完了です。
ただし、このままキャッシュをガンガン保存するとキャッシュ容量の上限に達してUncaught (in promise) DOMException: Quota exceeded.
というエラーが発生してしまう可能性が高いので、Service Workerにてキャッシュの制限を行うのがオススメです。
下記の例ではキャッシュの保持期限と保持数を定義しています。
:
self.toolbox.options.cache = {
name: 'ionic-cache',
maxAgeSeconds: 60 * 60 * 24,
maxEntries: 50
};
:
<参考>