ATEM miniスイッチャーをスマホから操作する (PinPソースを変更する)

##はじめに
[ATEM mini](https://www.blackmagicdesign.com/jp/products/atemmini)はBlackmagic Designが開発しているHDMIスイッチャーシリーズです。昨今のコロナ禍でリモートワークや自宅配信が増え、一時期は品薄で入手困難な時期もありました。

そんなATEM mini、もちろん本体だけでもある程度操作は可能なのですが、PCの[ATEM Software Control](https://www.blackmagicdesign.com/jp/products/atemmini/software)を用いると更に高度なことができたりします。

ただ…いちいちATEM Software Controlを起動して操作するのが面倒だったりします。
今回は、ATEM miniのPicture in Picture(以下PinP)のソースをスマホから制御できるようにしてみました。

ATEM mini単体だとPinPで重ねられるソースが `HDMI入力1` で固定なのですが、ATEM Software Controlを用いると変更できるので、なんとか簡略化したい、と思ったのがきっかけです。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">適当にNode-RED内でWebアプリ化してスマホからATEM mini叩けるようにした。悪くない <a href="https://t.co/lDbwuxlGwM">pic.twitter.com/lDbwuxlGwM</a></p>&mdash; ぽちゃも (@pochamost) <a href="https://twitter.com/pochamost/status/1383122426909786113?ref_src=twsrc%5Etfw">April 16, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

なお、今回検証したのは最もベーシックなATEM miniです。ATEM mini Pro、Pro ISO等上級機については確認していません。

## 用意するもの

* ATEM mini
* ATEM miniとスマホがそれぞれ接続できるLAN環境
* Raspberry Pi
    * Stretch以上のバージョンが必要
    * 常駐させられるなら通常のPCやLinux端末でも可
* スマートフォン(今回はAndroid端末を使用)

###環境情報
* [ATEM mini](https://www.blackmagicdesign.com/jp/products/atemmini)
    * Software version: 8.5.1
* [Raspberry Pi 2 Model B](https://www.raspberrypi.org/products/raspberry-pi-2-model-b/)
    * Raspbian GNU/Linux 9.13 (stretch)
    * Node.js v14.16.1
    * Node-RED v1.3.2
        * [blackmagic-atem-nodered](https://flows.nodered.org/node/blackmagic-atem-nodered) v2.2.8
    * [jQuery](https://jquery.com/download/) 3.6.0
    * [uikit](https://getuikit.com/docs/installation) 3.6.18
* Galaxy A20 (SCV46)
    * Android 10
    * Google Chrome 88.0.4324.181

## ATEM miniの初期設定
今回のプログラムではATEM miniをLAN経由で操作を行います。

はじめに、一度PCとUSB接続し、ATEM Setupを起動して接続するLAN環境に合わせたIPアドレスの設定を行います。

![atemsetup.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194453/5483e057-5aa0-6bbe-a5f8-d2810700588b.png)
(192.168.0.xの場合の例)

設定をATEMに保存したら、LANへ接続して、PC等からpingが通るか確認しておきましょう。

## 制御方法

ATEMスイッチャー操作用のNode-RED操作ライブラリ(ノード)があるとのことだったので、Node-REDを中心に構成しています。

一応、Blackmagic Designでは[各種SDK](https://www.blackmagicdesign.com/jp/developer/)を公開しているようなのですが、~~いまいちATEM miniシリーズの遠隔操作方法が分からなかったので、~~ 既存のライブラリを使用させていただきました。


参考にさせていただいた記事:

https://qiita.com/kitazaki/items/73ab5d5441951092a1d3

Node-REDでWebアプリを作り、スマホから操作するイメージです。

### Node-REDの準備
今回は常駐させておきたかったので、Raspberry PiでNode-REDを常時稼働させることとにしました。

一時的に使用するのであればWindows/Mac等のNode-RED環境でも大丈夫です。

Node-RED日本ユーザー会がRaspberry Piへのインストール方法を公開しているので、そちらのガイドにしたがって作業します。

[Raspberry Piで実行する : Node-RED日本ユーザ会](https://nodered.jp/docs/getting-started/raspberrypi)

基本的にはインストールスクリプトが用意されているので、この通り実行します。

```bash:インストールスクリプトの実行
$ bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)
```

なお、同ページにも記載がありますが**最低でもRaspbian Stretch以降のバージョンが必要**となっているので注意してください。
(手元の環境がJessieのままだったのでOSのアップグレードから始めることになった)

無事にインストールが完了したら、一旦Node-REDを起動しておきます。

```bash:Node-REDの起動
$ node-red-start
```

また、インストール時にsystemdへの登録が行われているので、自動起動するよう設定をしておきます。

```bash:Node-REDの自動起動設定
$ sudo systemctl enable nodered.service
```

### ATEM操作用ノードの構築
Node-REDのインストールが完了したら、まずATEM操作用ノードパッケージをインストールします。

Raspberry Piまたは同じLAN上の端末のブラウザから、Node-REDにアクセスします。

```Markdown:Node-REDへのアクセス
http://[Raspberry PiのIPアドレス]:1880/
```

右上のメニュー→「パレットの管理」 を選択
![node_red-addpallete.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194453/97ca782b-cf5e-5eac-16ac-d1c80dfbb93c.png)

「ノードを追加」→「blackmagic-atem-nodered」を検索し、「ノードを追加」でインストールします。
![node_red-add-node.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194453/64a01ac9-4aca-eb05-0f80-66ad8deb086d.png)

インストールが完了すると、画面左側のノード一覧の中に「BlackMagic」というセクションが増えているはずです。


ここからは実際にATEM miniへ送るコマンドを用意していきます。
基本的にはJSON形式でcmd、dataをそれぞれ指定して送る形となっているようです。

cmd(コマンド)とdata(データ型)については、同ノードのヘルプがあるのでそちらを参考にしました。

『haydendonald/blackmagic-atem-nodered · GitHub』
[https://github.com/haydendonald/blackmagic-atem-nodered/blob/master/howToUse.md](https://github.com/haydendonald/blackmagic-atem-nodered/blob/master/howToUse.md)

今回実現したかったATEM miniのPinPの設定については、Upstream KeyerのDVE Fill Sourceを変更することで実現できるはずなのですが…どうもこのライブラリでプレフィックスが提供されている`upstreamKeyer`系列の命令ではうまく行かなかったので、[こちらのプロトコル情報](https://www.skaarhoj.com/discover/blackmagic-atem-switcher-protocol)を参考に、RAW形式でデータを送ることにしました。

Node-REDの画面上でfunctionノードを追加、コードとして以下のような内容を書き込みます。

```js:Change&#x20;DVE&#x20;Fillsource&#x20;to&#x20;Input&#x20;2
var msg1 = {
    "payload": {
        "cmd": "raw",
        "data": {
            "name": "CKeF",
            "packet": new Buffer.from([0,0,0,2])
        }
    }
}
return msg1;
```

`cmd`は`raw`を指定、`data`の`name`で指定している`CKeF`というのが`Key Fill`に関するものという宣言で、`packet`で渡している4列のリストの4番目の引数が`Video Source`のIDとなります。

```Video Source```のIDについては、

* 0 : Black
* 1 : Input 1
* 2 : Input 2
* 3 : Input 3
* 4 : Input 4

などとなっているようです。
(この他にもColor BarやMedia Player等も参照できるようです。)

functionノードが準備できたら、試しに```injectノード```→```functionノード```→```ATEMノード```を追加し直列に接続します。

ATEMノードを追加した際、ダブルクリックしてATEM miniへ接続する設定を行います。
```Name```は適当に設定し、```Network```のペンアイコンをクリックして、新たにATEM miniの名前(任意)とATEM miniのIPアドレスを入力します。
![nodered_edit-atemnode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194453/951e3890-561a-20a4-5b18-6e4c1aeb7fea.png)
![nodered_edit-atem-ip.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194453/bb252168-0ebc-a09b-d132-989b5da1a9f2.png)

全ての設定が完了したら画面右上の「デプロイ」をクリック。injectノードをクリックすることで、ATEM miniが反応して操作が行えるはずです。
![nodered-atem-sample1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194453/35414f91-2a4e-74fd-f858-9c58ed04a5ad.png)

### Web API化
続いて、スマホ(Webアプリ)からこの操作を行えるように、HTTPのエンドポイントを作ります。
といってもHTTPトリガはNode-REDに最初から用意されているので、前項で入力として使用した`inject`の代わりに、`http in`ノードを接続するだけです。

`http in`ノードをダブルクリックして、各種設定を行います。
`URL`のところがアクセスするエンドポイントのURLになるので、わかりやすいようなパスにしておきます。
![nodered-httpin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194453/1bd8a23d-76d6-7eaa-5a0b-0561756b85ab.png)

上記画像の`URL: /dve1`の場合、`http://[Raspberry PiのIPアドレス]:1880/dve1`にアクセスすることでトリガが発火します。

`http in`ノードは、対応する`http response`ノードがないとレスポンスを返すことが出来ず、クライアント側の処理が停止してしまうため、`template`ノードを挟んで応答メッセージを定義し、`http response`ノードへ接続します。
![nodered-http-template-ex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194453/fc7a4359-1470-d09a-eb8d-96bc57a46bd0.png)
`http response`ノードは、ステータスコードとして`200`(OK)を入力しておきます。![nodered-http-response.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194453/e2849562-06a7-4c84-7ae8-09b0495541f4.png)


以上でNode-REDでの基本形は完成です。
![nodered-atem-baseformex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194453/9dd2cf51-c41d-a415-c59e-889bbd46fb80.png)

操作項目を増やす場合は、`http in`、`function`を必要分だけ複製して編集します。
`ATEMノード`、`template`、`http response`は1つで動作します。
(本当はATEMの操作が完了したかどうかちゃんと確認してレスポンスで返したほうが良いのでしょうが…苦笑)


<details><summary>PinP 4入力切替フローの例(JSON)</summary><div>

```json:atemctrl.json
[
    {
        "id": "fc0cbc1d.b7914",
        "type": "tab",
        "label": "ATEM mini PinP Controller",
        "disabled": false,
        "info": ""
    },
    {
        "id": "a45f34e4.5df8e8",
        "type": "function",
        "z": "fc0cbc1d.b7914",
        "name": "Turn On DVE ",
        "func": "var msg1 = {\n    \"payload\": {\n        \"cmd\": \"upstreamKeyer\",\n        \"data\": {\n            \"ME\": 0,\n            \"id\": 0,\n            \"state\": true\n        }\n    }\n}\nreturn msg1;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 540,
        "y": 260,
        "wires": [
            [
                "cce77a1b.b25468"
            ]
        ]
    },
    {
        "id": "b9b44a35.357908",
        "type": "inject",
        "z": "fc0cbc1d.b7914",
        "name": "",
        "repeat": "",
        "crontab": "",
        "once": false,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 210,
        "y": 260,
        "wires": [
            [
                "a45f34e4.5df8e8"
            ]
        ]
    },
    {
        "id": "9e5adc41.4686c",
        "type": "debug",
        "z": "fc0cbc1d.b7914",
        "name": "",
        "active": true,
        "console": "false",
        "complete": "false",
        "x": 990,
        "y": 400,
        "wires": []
    },
    {
        "id": "e236b21.fdd685",
        "type": "inject",
        "z": "fc0cbc1d.b7914",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 210,
        "y": 660,
        "wires": [
            [
                "b025d011.0c8ee"
            ]
        ]
    },
    {
        "id": "b025d011.0c8ee",
        "type": "function",
        "z": "fc0cbc1d.b7914",
        "name": "Turn off DVE",
        "func": "var msg1 = {\n    \"payload\": {\n        \"cmd\": \"upstreamKeyer\",\n        \"data\": {\n            \"ME\": 0,\n            \"id\": 0,\n            \"state\": false\n        }\n    }\n}\nreturn msg1;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 540,
        "y": 660,
        "wires": [
            [
                "cce77a1b.b25468"
            ]
        ]
    },
    {
        "id": "76ac31.1c7703d",
        "type": "function",
        "z": "fc0cbc1d.b7914",
        "name": "Change DVE To Input 2",
        "func": "var msg1 = {\n    \"payload\": {\n        \"cmd\": \"raw\",\n        \"data\": {\n            //\"name\": \"CDsF\",\n            \"name\": \"CKeF\",\n            \"packet\": new Buffer.from([0,0,0,2])\n        }\n    }\n}\nreturn msg1;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 500,
        "y": 420,
        "wires": [
            [
                "cce77a1b.b25468"
            ]
        ]
    },
    {
        "id": "e4374a82.a943d8",
        "type": "inject",
        "z": "fc0cbc1d.b7914",
        "name": "",
        "repeat": "",
        "crontab": "",
        "once": false,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 210,
        "y": 420,
        "wires": [
            [
                "76ac31.1c7703d"
            ]
        ]
    },
    {
        "id": "95c19ecd.88818",
        "type": "function",
        "z": "fc0cbc1d.b7914",
        "name": "Change DVE To Input 1",
        "func": "var msg1 = {\n    \"payload\": {\n        \"cmd\": \"raw\",\n        \"data\": {\n            \"name\": \"CKeF\",\n            \"packet\": new Buffer.from([0,0,0,1])\n        }\n    }\n}\nreturn msg1;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 500,
        "y": 340,
        "wires": [
            [
                "cce77a1b.b25468"
            ]
        ]
    },
    {
        "id": "4ad22c50.336b84",
        "type": "inject",
        "z": "fc0cbc1d.b7914",
        "name": "",
        "repeat": "",
        "crontab": "",
        "once": false,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 210,
        "y": 340,
        "wires": [
            [
                "95c19ecd.88818"
            ]
        ]
    },
    {
        "id": "79eb56fd.a32808",
        "type": "function",
        "z": "fc0cbc1d.b7914",
        "name": "Change DVE To Input 4",
        "func": "var msg1 = {\n    \"payload\": {\n        \"cmd\": \"raw\",\n        \"data\": {\n            //\"name\": \"CDsF\",\n            \"name\": \"CKeF\",\n            \"packet\": new Buffer.from([0,0,0,4])\n        }\n    }\n}\nreturn msg1;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 500,
        "y": 580,
        "wires": [
            [
                "cce77a1b.b25468"
            ]
        ]
    },
    {
        "id": "7c4d7953.f7b728",
        "type": "inject",
        "z": "fc0cbc1d.b7914",
        "name": "",
        "repeat": "",
        "crontab": "",
        "once": false,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 210,
        "y": 580,
        "wires": [
            [
                "79eb56fd.a32808"
            ]
        ]
    },
    {
        "id": "9e3b9137.9d3d1",
        "type": "function",
        "z": "fc0cbc1d.b7914",
        "name": "Change DVE To Input 3",
        "func": "var msg1 = {\n    \"payload\": {\n        \"cmd\": \"raw\",\n        \"data\": {\n            //\"name\": \"CDsF\",\n            \"name\": \"CKeF\",\n            \"packet\": new Buffer.from([0,0,0,3])\n        }\n    }\n}\nreturn msg1;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 500,
        "y": 500,
        "wires": [
            [
                "cce77a1b.b25468"
            ]
        ]
    },
    {
        "id": "5cc28b7e.0b6174",
        "type": "inject",
        "z": "fc0cbc1d.b7914",
        "name": "",
        "repeat": "",
        "crontab": "",
        "once": false,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 210,
        "y": 500,
        "wires": [
            [
                "9e3b9137.9d3d1"
            ]
        ]
    },
    {
        "id": "122f299f.215bf6",
        "type": "http in",
        "z": "fc0cbc1d.b7914",
        "name": "",
        "url": "/dve1",
        "method": "get",
        "upload": false,
        "swaggerDoc": "",
        "x": 190,
        "y": 380,
        "wires": [
            [
                "95c19ecd.88818",
                "c98b6bba.5af508"
            ]
        ]
    },
    {
        "id": "29bb88cb.247a28",
        "type": "http response",
        "z": "fc0cbc1d.b7914",
        "name": "",
        "statusCode": "200",
        "headers": {},
        "x": 700,
        "y": 740,
        "wires": []
    },
    {
        "id": "c98b6bba.5af508",
        "type": "template",
        "z": "fc0cbc1d.b7914",
        "name": "Response Template",
        "field": "payload",
        "fieldType": "msg",
        "format": "html",
        "syntax": "mustache",
        "template": "<p>Success</p>",
        "output": "str",
        "x": 510,
        "y": 740,
        "wires": [
            [
                "29bb88cb.247a28"
            ]
        ]
    },
    {
        "id": "be9f0fdc.c5576",
        "type": "http in",
        "z": "fc0cbc1d.b7914",
        "name": "",
        "url": "/dve2",
        "method": "get",
        "upload": false,
        "swaggerDoc": "",
        "x": 190,
        "y": 460,
        "wires": [
            [
                "76ac31.1c7703d",
                "c98b6bba.5af508"
            ]
        ]
    },
    {
        "id": "f6b8ae0.09bb75",
        "type": "http in",
        "z": "fc0cbc1d.b7914",
        "name": "",
        "url": "/dve3",
        "method": "get",
        "upload": false,
        "swaggerDoc": "",
        "x": 190,
        "y": 540,
        "wires": [
            [
                "9e3b9137.9d3d1",
                "c98b6bba.5af508"
            ]
        ]
    },
    {
        "id": "1338254c.31378b",
        "type": "http in",
        "z": "fc0cbc1d.b7914",
        "name": "",
        "url": "/dve4",
        "method": "get",
        "upload": false,
        "swaggerDoc": "",
        "x": 190,
        "y": 620,
        "wires": [
            [
                "79eb56fd.a32808",
                "c98b6bba.5af508"
            ]
        ]
    },
    {
        "id": "806f4226.bcda3",
        "type": "http in",
        "z": "fc0cbc1d.b7914",
        "name": "",
        "url": "/dveon",
        "method": "get",
        "upload": false,
        "swaggerDoc": "",
        "x": 210,
        "y": 300,
        "wires": [
            [
                "a45f34e4.5df8e8",
                "c98b6bba.5af508"
            ]
        ]
    },
    {
        "id": "fd6cb2c1.4a415",
        "type": "http in",
        "z": "fc0cbc1d.b7914",
        "name": "",
        "url": "/dveoff",
        "method": "get",
        "upload": false,
        "swaggerDoc": "",
        "x": 200,
        "y": 700,
        "wires": [
            [
                "b025d011.0c8ee",
                "c98b6bba.5af508"
            ]
        ]
    },
    {
        "id": "cce77a1b.b25468",
        "type": "atem-atem",
        "z": "fc0cbc1d.b7914",
        "name": "ATEM mini",
        "network": "35d02c46.168f84",
        "outputMode": "supported",
        "sendTime": "yes",
        "sendInitialData": "yes",
        "sendStatusUpdates": "yes",
        "x": 790,
        "y": 400,
        "wires": [
            [
                "9e5adc41.4686c"
            ]
        ]
    },
    {
        "id": "35d02c46.168f84",
        "type": "atem-network",
        "name": "ATEM mini 1",
        "ipAddress": "192.168.0.32"
    }
]
```
</div></details>

![nodered-atemctrl_out.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194453/4f8002ab-118e-6915-effd-46cc8eb03a42.png)


### Webアプリとしてまとめる
Node-REDでは、静的なコンテンツをそのままHTTP経由でホスティングする仕組みがあります。
はじめに、`~/.node-red/settings.js`を編集し、この仕組みを有効化しておきます。

`httpStatic`という変数に、コンテンツのルートディレクトリを指定します。

```js:~/.node-red/settings.js&#x20;106行目付近
httpStatic: '/home/pi/node-red-static/',
```
今回の場合は、piユーザのホームフォルダに`node-red-static`というディレクトリを作成し使用しました。
設定ファイルを保存し、Node-REDを再起動しておきます。

```bash:Node-REDの再起動
$ sudo systemctl restart nodered.service
```

あとは操作用のHTMLファイル(例: atemctrl.html)を作成します。
今回は[jQuery](https://code.jquery.com/)とCSSフレームワークの[uikit](https://getuikit.com/docs/introduction)を用いて作成しました。

<details><summary>HTMLの例</summary><div>

```html:atemctrl.html
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
        <meta name="viewport" content="width=device-width;">

        <meta name="mobile-web-app-capable" content="yes">
        <meta name="apple-mobile-web-app-capable" content="yes">

        <title>ATEM mini PiP Ctrl</title>

        <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
        <link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
        <link rel="icon" type="image/png" href="/android-touch-icon.png" sizes="192x192">

        <!-- UIkit CSS -->
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.6.19/dist/css/uikit.min.css" />

        <!-- UIkit JS -->
        <script src="https://cdn.jsdelivr.net/npm/uikit@3.6.19/dist/js/uikit.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/uikit@3.6.19/dist/js/uikit-icons.min.js"></script>


        <!-- jQuery  -->
        <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

        <style>
            body{
                background-color: black;
            }
            .div_btn{
                opacity: 1.0;
            }
            .div_btn:hover{
                opacity: 0.8;
            }
        </style>
    </head>
    <body>
        <div class="uk-padding-small uk-margin-remove uk-text-center">
            <h1 class="uk-margin-remove" style="color:lightgrey">ATEM mini PinP Control</h1>
        </div>
        <div class="uk-padding-small uk-height-1-1 uk-child-height-1-1">
            <div class="uk-grid-small uk-child-width-1-2 uk-text-center uk-grid-match" style="height:40%" uk-grid>
                <div>
                    <div class="uk-card uk-card-default uk-card-body uk-card-hover uk-flex uk-flex-middle uk-flex-center div_btn" com_id="dveon">
                        <div class="uk-text-lead">PinP ON</div>
                    </div>
                </div>
                <div>
                    <div class="uk-card uk-card-default uk-card-body uk-card-hover uk-flex uk-flex-middle uk-flex-center div_btn" com_id="dveoff">
                        <div class="uk-text-lead">PinP OFF</div>
                    </div>
                </div>
            </div>

            <div class="uk-grid-small uk-child-width-1-4 uk-text-center uk-grid-match" style="height:40%;" uk-grid>
                <div>
                    <div class="uk-card uk-card-default uk-card-body uk-card-hover uk-flex uk-flex-middle uk-flex-center div_btn" com_id="dve1">
                        <div class="uk-text-lead">HDMI 1</div>
                    </div>
                </div>
                <div>
                    <div class="uk-card uk-card-default uk-card-body uk-card-hover uk-flex uk-flex-middle uk-flex-center div_btn" com_id="dve2">
                        <div class="uk-text-lead">HDMI 2</div>
                    </div>
                </div>
                <div>
                    <div class="uk-card uk-card-default uk-card-body uk-card-hover uk-flex uk-flex-middle uk-flex-center div_btn" com_id="dve3">
                        <div class="uk-text-lead">HDMI 3</div>
                    </div>
                </div>
                <div>
                    <div class="uk-card uk-card-default uk-card-body uk-card-hover uk-flex uk-flex-middle uk-flex-center div_btn" com_id="dve4">
                        <div class="uk-text-lead">HDMI 4</div>
                    </div>
                </div>
            </div>
        </div>
        <script>
            $(".div_btn").click(function(){
                var apipath="/"+$(this).attr('com_id');
                $.ajax(apipath, {type: 'get'})
                .done(function(data){
                    //window.alert('success')
                })
                .fail(function(){
                    window.alert('fail')
                })
            });
        </script>
    </body>
</html>
```

</div>
</details>

基本的には各ボタンUIをタップした際に、ajaxで先程作ったエンドポイントを叩く、という構成にしてみました。
(上記サンプルコードではjQueryとuikitをCDNから読み込んでいますが、ダウンロードして同ディレクトリに置いた方が全てローカルで完結して良いかもしれません。)

また、Webアプリとしてスマホで全画面動作するように`head`タグ内でいくつか`meta`タグの設定をしています。

```html:ホーム画面追加時に全画面Webアプリとして起動する設定
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
```

```html:ホーム画面追加時のアイコン設定
<!-- favicon -->
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">

<!-- iOS用 -->
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">

<!-- Android用 -->
<link rel="icon" type="image/png" href="/android-touch-icon.png" sizes="192x192">
```

※アイコン画像は適当に用意してください。


以上が完了したら、ATEM miniおよびRaspberry Piと同一LANにあるスマートフォンのChromeまたはSafariから、`http://[Raspberry PiのIPアドレス]:1880/atemctrl.html`にアクセスし、`ホーム画面に追加`を行います。


ホーム画面に追加されたアイコンから起動すると、全画面でアプリっぽく起動し、各ボタンをタップすることでATEM miniの操作ができるはずです！

画像はAndroid(Galaxy A20)で起動した様子です
![Screenshot_20210417-133116_Chrome.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194453/c7838f2b-ffb1-52f8-41ab-385c43f79965.png)