PWAでカメラを使うためiOSとAndroidで異なるmanifestを読み込む

#TL;DR
・iOSのPWAでは表示モードが`standalone`の場合にカメラを開くことができない。
・Androidでの起動時には`standalone`、iOSでは`browser`で起動する。
・ページアクセス時にuserAgentからOSを判定し、OSに応じて異なるmanifestを読み込む。

#カメラを開く最小限のPWA構成
ファイル構成は下記の通りです。
icon.pngは適当な192x192の画像を用意してください。

```
root/
　├ icon.png
　├ index.html
　├ js/
　│　└ camera.js
　├ manifest.json
　└ sw.js
```

```index.html
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="manifest" href="manifest.json">
        <link rel="apple-touch-icon" href="icon.png" sizes="192x192">
    </head>

    <body onload="openCamera();">
        <video autoplay playsinline></video>
    </body>

    <script src="js/camera.js"></script>
    <script>
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('sw.js').then(function(registration) {
                console.log('ServiceWorker registration successful with scope: ', registration.scope);
            }).catch(function(err) {
                console.log('ServiceWorker registration failed: ', err);
            });
        }
    </script>
</html>
```

```sw.js
var CACHE_NAME = 'pwa-camera-test-caches';
var urlsToCache = ['/index.html', '/js/camera.js'];

self.addEventListener('install', function(event) {
    event.waitUntil(
        caches
            .open(CACHE_NAME)
            .then(function(cache) {
                return cache.addAll(urlsToCache);
            })
    );
});

self.addEventListener('fetch', function(event) {
    event.respondWith(
        caches
            .match(event.request)
            .then(function(response) {
                return response ? response : fetch(event.request);
            })
    );
});
```

```manifest.json
{
    "name": "pwa camera test",
    "short_name": "PWA CAMERA",
    "icons": [{
          "src": "icon.png",
          "sizes": "192x192",
          "type": "image/png"
        }],
    "start_url": "index.html",
    "display": "standalone"
}
```

```camera.js
function openCamera() {
    var video = document.querySelector('video');
    navigator.mediaDevices = navigator.mediaDevices
    || ((navigator.mozGetUserMedia 
    || navigator.webkitGetUserMedia) ? {
        getUserMedia: function(c) {
            return new Promise(function(y, n) {
                (navigator.mozGetUserMedia ||
                navigator.webkitGetUserMedia).call(navigator, c, y, n);
            });
        }
    } : null);
    var constraints = { video: { facingMode: 'environment', width: 1280, height: 720 } };
    navigator.mediaDevices.getUserMedia(constraints)
        .then(function(stream) {
            video.srcObject = stream;
            video.onloadedmetadata = function(e) {
                video.play();
            };
        })
        .catch(function(err) {
            console.log(err);
        });
}
```

PWA参考：[シンプルなPWAサンプルここに置いておきますね](https://qiita.com/kazaoki/items/e93b88556fcd05d28ddc)
カメラ参考：[「MediaDevices.getUserMedia() 」について](https://qiita.com/Futo23/items/bff1ce1d2e1b219b243d)

これでカメラを開く最低限のPWAアプリの完成です。
https接続のできる場所にアップロードして動作を確認します。
今回はGithub Pagesを使用しました。

**ブラウザ**

|iOS|Android|
|---|---|
|![IMG_2902.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265206/185a4d1f-9d99-93fa-a6ce-3ac765feff15.png)|![Screenshot_2019-08-07-16-37-30.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265206/752c3ee8-2985-b8fb-b945-ab912b9f0b59.jpeg)|

**インストール(ホーム画面に追加)**

|iOS|Android|
|---|---|
|![IMG_2903.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265206/e86881af-a8a7-0073-b72b-bdfa6e0618f0.png)|![Screenshot_2019-08-07-16-37-52.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265206/e0403598-93b7-b7ca-8859-1ef953a32ffb.jpeg)|

**PWA**

|iOS|Android|
|---|---|
|![IMG_2904.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265206/a8f9094d-bd42-0cdd-3957-a103e9896894.png)|![Screenshot_2019-08-07-16-38-02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265206/bc6303b5-d749-423f-5f3a-7d863782fd7f.jpeg)

冒頭で述べた通り、iOSでは表示モードが`standalone`の場合にカメラを開くことができません。
参考：[PWA来てるからカメラアプリ作れるんじゃね？と思ったら失敗した話](https://qiita.com/mu_tomoya/items/d35dbb580a561c562ca6)

表示モードを`browser`にすると単にブラウザが起動されるため、iOSでもカメラを開くことができますが、本来`standalone`で起動できるAndroidもブラウザでの起動になってしまいます。

そこで、表示モードが`standalone`と`browser`のmanifestをそれぞれ用意し、OSによって読込先を変更することで、Androidでの起動時には`standalone`、iOSでは`browser`で起動するようにします。

#OS別のmanifest読み込み
*manifest.json*と同階層に、表示モードを`browser`に変更しただけの*manifest_ios.json*を作成します。
また、*index.html*でuserAgentからOSを判別し、OSに応じて読み込むmanifestを変更します。


```manifest_ios.json
{
    "name": "pwa camera test",
    "short_name": "PWA CAMERA",
    "icons": [{
          "src": "icon.png",
          "sizes": "192x192",
          "type": "image/png"
        }],
    "start_url": "index.html",
    "display": "browser"
}
```

```index.html
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <!-- スクリプトから追加するため削除 -->
        <!-- <link rel="manifest" href="manifest.json"> -->
        <link rel="apple-touch-icon" href="icon.png" sizes="192x192">
    </head>

    <body onload="openCamera();">
        <video autoplay playsinline></video>
    </body>

    <script src="js/camera.js"></script>
    <script>
        // manifestのlinkタグを生成
        function setManifest(path) {
            const manifest = document.createElement('link');
            manifest.rel = 'manifest';
            manifest.href = path;
            document.head.appendChild(manifest);
        }

        // OSに応じて読み込むmanifestを変更
        var userAgent = navigator.userAgent.toLowerCase();
        if (userAgent.indexOf("iphone") > 0 || userAgent.indexOf("ipad") > 0) {
            setManifest('manifest_ios.json')
        } else {
            setManifest('manifest.json')
        }
        
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('sw.js').then(function(registration) {
                console.log('ServiceWorker registration successful with scope: ', registration.scope);
            }).catch(function(err) {
                console.log('ServiceWorker registration failed: ', err);
            });
        }
    </script>
</html>
```

これでホーム画面のアイコンから起動した時、AndroidではPWAの`standalone`として起動、iOSではSafariの`browser`で起動するようになりました。
iOS/Android共にカメラを使用できるほか、AndroidではPWAのキャッシュ機能を使用できるためオフラインでも動作も可能です。

#まとめ
記事作成時点ではiOSのPWAの`standalone`ではカメラを起動できなかったため、manifestをOSに応じて差し替える事で、AndroidではPWAアプリとして動作させつつiOSでのカメラ起動を可能にしました。
iOSでもSafari以外からカメラにアクセスできるようになると嬉しいですね。
あまりスマートな解決策ではない気がしているため、より良い方法がありましたらご教授ください。