2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-04-17

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

そんなATEM mini、もちろん本体だけでもある程度操作は可能なのですが、PCのATEM Software Controlを用いると更に高度なことができたりします。

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

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

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

用意するもの

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

###環境情報

ATEM miniの初期設定

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

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

atemsetup.png
(192.168.0.xの場合の例)

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

制御方法

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

一応、Blackmagic Designでは各種SDKを公開しているようなのですが、いまいちATEM miniシリーズの遠隔操作方法が分からなかったので、 既存のライブラリを使用させていただきました。

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

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

Node-REDの準備

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

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

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

Raspberry Piで実行する : Node-RED日本ユーザ会

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

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

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

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

Node-REDの起動
$ node-red-start

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

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

ATEM操作用ノードの構築

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

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

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

右上のメニュー→「パレットの管理」 を選択
node_red-addpallete.png

「ノードを追加」→「blackmagic-atem-nodered」を検索し、「ノードを追加」でインストールします。
node_red-add-node.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

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

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

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

cmdrawを指定、datanameで指定している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
nodered_edit-atem-ip.png

全ての設定が完了したら画面右上の「デプロイ」をクリック。injectノードをクリックすることで、ATEM miniが反応して操作が行えるはずです。
nodered-atem-sample1.png

Web API化

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

http inノードをダブルクリックして、各種設定を行います。
URLのところがアクセスするエンドポイントのURLになるので、わかりやすいようなパスにしておきます。
nodered-httpin.png

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

http inノードは、対応するhttp responseノードがないとレスポンスを返すことが出来ず、クライアント側の処理が停止してしまうため、templateノードを挟んで応答メッセージを定義し、http responseノードへ接続します。
nodered-http-template-ex.png
http responseノードは、ステータスコードとして200(OK)を入力しておきます。nodered-http-response.png

以上でNode-REDでの基本形は完成です。
nodered-atem-baseformex.png

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

PinP 4入力切替フローの例(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"
    }
]

nodered-atemctrl_out.png

Webアプリとしてまとめる

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

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

~/.node-red/settings.js 106行目付近
httpStatic: '/home/pi/node-red-static/',

今回の場合は、piユーザのホームフォルダにnode-red-staticというディレクトリを作成し使用しました。
設定ファイルを保存し、Node-REDを再起動しておきます。

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

あとは操作用のHTMLファイル(例: atemctrl.html)を作成します。
今回はjQueryとCSSフレームワークのuikitを用いて作成しました。

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>

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

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

ホーム画面追加時に全画面Webアプリとして起動する設定
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
ホーム画面追加時のアイコン設定
<!-- 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

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?