##はじめに
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を用いると変更できるので、なんとか簡略化したい、と思ったのがきっかけです。
適当にNode-RED内でWebアプリ化してスマホからATEM mini叩けるようにした。悪くない pic.twitter.com/lDbwuxlGwM
— ぽちゃも (@pochamost) April 16, 2021
なお、今回検証したのは最もベーシックなATEM miniです。ATEM mini Pro、Pro ISO等上級機については確認していません。
用意するもの
- ATEM mini
- ATEM miniとスマホがそれぞれ接続できるLAN環境
- Raspberry Pi
- Stretch以上のバージョンが必要
- 常駐させられるなら通常のPCやLinux端末でも可
- スマートフォン(今回はAndroid端末を使用)
###環境情報
-
ATEM mini
- Software version: 8.5.1
-
Raspberry Pi 2 Model B
- Raspbian GNU/Linux 9.13 (stretch)
- Node.js v14.16.1
- Node-RED v1.3.2
- blackmagic-atem-nodered v2.2.8
- jQuery 3.6.0
- uikit 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アドレスの設定を行います。
設定を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-start
また、インストール時にsystemdへの登録が行われているので、自動起動するよう設定をしておきます。
$ sudo systemctl enable nodered.service
ATEM操作用ノードの構築
Node-REDのインストールが完了したら、まずATEM操作用ノードパッケージをインストールします。
Raspberry Piまたは同じLAN上の端末のブラウザから、Node-REDにアクセスします。
http://[Raspberry PiのIPアドレス]:1880/
「ノードを追加」→「blackmagic-atem-nodered」を検索し、「ノードを追加」でインストールします。
インストールが完了すると、画面左側のノード一覧の中に「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ノードを追加、コードとして以下のような内容を書き込みます。
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アドレスを入力します。
全ての設定が完了したら画面右上の「デプロイ」をクリック。injectノードをクリックすることで、ATEM miniが反応して操作が行えるはずです。
Web API化
続いて、スマホ(Webアプリ)からこの操作を行えるように、HTTPのエンドポイントを作ります。
といってもHTTPトリガはNode-REDに最初から用意されているので、前項で入力として使用したinject
の代わりに、http in
ノードを接続するだけです。
http in
ノードをダブルクリックして、各種設定を行います。
URL
のところがアクセスするエンドポイントのURLになるので、わかりやすいようなパスにしておきます。
上記画像のURL: /dve1
の場合、http://[Raspberry PiのIPアドレス]:1880/dve1
にアクセスすることでトリガが発火します。
http in
ノードは、対応するhttp response
ノードがないとレスポンスを返すことが出来ず、クライアント側の処理が停止してしまうため、template
ノードを挟んで応答メッセージを定義し、http response
ノードへ接続します。
http response
ノードは、ステータスコードとして200
(OK)を入力しておきます。
操作項目を増やす場合は、http in
、function
を必要分だけ複製して編集します。
ATEMノード
、template
、http response
は1つで動作します。
(本当はATEMの操作が完了したかどうかちゃんと確認してレスポンスで返したほうが良いのでしょうが…苦笑)
PinP 4入力切替フローの例(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"
}
]
Webアプリとしてまとめる
Node-REDでは、静的なコンテンツをそのままHTTP経由でホスティングする仕組みがあります。
はじめに、~/.node-red/settings.js
を編集し、この仕組みを有効化しておきます。
httpStatic
という変数に、コンテンツのルートディレクトリを指定します。
httpStatic: '/home/pi/node-red-static/',
今回の場合は、piユーザのホームフォルダにnode-red-static
というディレクトリを作成し使用しました。
設定ファイルを保存し、Node-REDを再起動しておきます。
$ sudo systemctl restart nodered.service
あとは操作用のHTMLファイル(例: atemctrl.html)を作成します。
今回はjQueryとCSSフレームワークのuikitを用いて作成しました。
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
タグの設定をしています。
<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の操作ができるはずです!