【Zoom Meeting SDK】TouchDesignerによるZoomオペレーションシステム

# 1. はじめに
この記事は「[Qiita Engineer Festa 2022](https://qiita.com/official-events/86d8555146e1b14fef24)」に参加するための記事です。

https://qiita.com/official-events/86d8555146e1b14fef24

初めまして。
[株式会社レイ](https://ev.ray.co.jp/)という映像機材を扱う会社でエンジニアをしています。

普段はTouchDesignerやUnrealEngineを使って、
映像オペレーションのシステムやXR表現の研究・開発をしています。

今回はZoomAPI/SDKとTouchDesignerを組み合わせた、
**Zoomオペレーションシステムをプロトタイピングしました。**

# 2.システム環境

* Windows10
* Geforce RTX 3060
* node: v16.15.1
* npm: 8.11.0
* TouchDesinger: 2022.25370
* UnrealEngine: 4.27.2

# 3. 映像屋が抱えるZoomオペの課題

私たちが企業の表彰式などの現場でオペレーションをする時、
表彰者の映像を扱うためZoomを利用することが多くあります。

その際表彰者をピン留めして不要な部分をクロップし、
メインオペレーターのスイッチャーなどに出力します。

しかし、この方法だと表彰者の数だけピン留めが必要ですし、
PCごとにZoomの設定を毎回の現場でする必要がありました。
またその分作業員も必要なので、物的・人的コストが掛かります。

（ｽﾞﾗｧ...）
![zoomEX.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/955bd4bf-c2ca-de54-0dd4-8bbd1d7b3909.png)
この課題を、**ZoomAPI/SDKを活用することで解決できないか**と考えました。

# 4. ZoomAPI/SDKの機能選定

初めは`Raw Video`が取得できるという`Video SDK`の利用を検討しました。
しかし、`Video SDK`は通常のミーティングとの互換性が無いようです。

>VideoSDKで実現できる映像音声通話は個別の通信としての扱いになるため通常のZoomミーティングやウェビナーとは互換性がありません。そのため、VideoSDKで生成された通話識別子(Session Name)へは通常のZoomクライアント及びMeetingSDKからは参加接続できないものとなります。

https://qiita.com/yosuke-sawamura/items/72df3bd91a6319ccc35c

なので、通常のミーティングにも参加できる`Meeting SDK`を利用する方針にしました。

# 5. MeetingSDKを利用した映像の取得

今回はプロトタイピングですので、公式のサンプルをそのまま利用します。
参考にしたのは、こちらの[記事](https://qiita.com/yosuke-sawamura/items/de69e73e47335cd61d68)とGithubの[リポジトリ](https://github.com/zoom/meetingsdk-web-sample)です。

https://qiita.com/yosuke-sawamura/items/de69e73e47335cd61d68

https://github.com/zoom/meetingsdk-web-sample

導入方法は上記記事とREADMEのままなので、割愛します。

`npm start`すると`127.0.0.1:9999`で入室前の画面が立ち上がるので、
事前に立ち上げた部屋の番号とパスワードを入力して、`Join`を押します。
（必要に応じて名前の変更と言語を日本語に設定して下さい）

URLは`Join`を押すたびに異なるものが発行されるので、
これを必要な分だけ繰り返せば、**1台のPCで複数の人を管理することが出来ます。**

別タブで立ち上がったURLを、`Pallete/Tools`にある`Web Browser`に入力します。
（`Web Render TOP`だと画面が操作出来ないので、`Web Browser`を使います）
これでZoomミーティングの部屋をTouchDesigner内で操作が出来るようになります。
![webB.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/6a31eb59-d84c-6d65-6822-6897ea3c5d58.png)
URLは手入力でベタ打ちしても良いのですが、数が多いと面倒なので、
`Fetch API`を使ってTouchDesignerに直接渡します。（`Fetch API`の理由は後述）

# 6. FetchAPIによるURLの送受信

発行されたURLは、http通信でTouchDesignerに渡します。
`http`や`XMLHttpRequest`を試しましたが、`CORS`に弾かれてしまいました。

HTTPレスポンスヘッダに`Access-Control-Allow-Origin`を付加すれば
通せるようなのですが、今回の検証段階では上手くいきませんでした。

そこでfetch関数の引数に`mode: "no-cors"`を加えたところ、回避出来ました。
以下、サンプルのコード`js/index.js`の103行目～に追記した箇所になります。

```javascript:index.js
      var signature = ZoomMtg.generateSDKSignature({
        meetingNumber: meetingConfig.mn,
        sdkKey: SDK_KEY,
        sdkSecret: SDK_SECRET,
        role: meetingConfig.role,
        success: function (res) {
          console.log(res.result);
          meetingConfig.signature = res.result;
          meetingConfig.sdkKey = SDK_KEY;
          var joinUrl = "/meeting.html?" + testTool.serialize(meetingConfig);
          console.log(joinUrl);
          // 別タブが開く必要が無いので、コメントアウト
          // window.open(joinUrl, "_blank");

          jsonData = {key: joinUrl};
          postUrl  = 'http://127.0.0.1:9980';

          fetch( postUrl, {
          method: 'POST',
          mode: 'no-cors',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(jsonData),
          })
          .then(response => response.json())
          .then(data => {
            console.log('Success:', jsonData);
          })
          .catch((error) => {
            console.error('Error:', error);
          });
        },
      });
```
普段JavaScriptは全く書かないので、書き方は[MDN Web Docs](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetch)の通りです。

https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetch

あくまでローカルネットワーク内でURLを渡すだけなので問題無いと思いますが、
本番環境では、`CORS`の仕組みをしっかり理解した上で実装します。（自戒）

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

POSTしたURLは、TouchDesignerの`Web Server DAT`で受け取ります。
以下、`Web Server DAT`に紐づいているコールバックのオペレータに追記した箇所です。

```python:webserver1_callbacks
import json

# URLを保持するためのTable DAT
table = op("table1")

def onHTTPRequest(webServerDAT, request, response):

	response['statusCode'] = 200 # OK
	response['statusReason'] = 'OK'
	response['data'] = '<b>TouchDesigner: </b>' + webServerDAT.name
	
	get_data = request['data']
	load_data = json.loads(get_data)
	value_data = load_data['key']

	table.appendRow("http://127.0.0.1:9999" + value_data)
	
	return response
```
後は`Table DAT`に入るURLを`Web Browser`にリファレンスすれば完了です。

# 7. TouchDesingerでの映像演出

せっかくTouchDesignerを使っているので、
簡単なインタラクティブ演出を加えていきます。

`Face Tracking CHOP`を使って顔の上に王冠を乗せ、
賑やかしのパーティクルなんかも加えて派手にします。
素材はいらすとやから拝借。（[王冠](https://www.irasutoya.com/2013/03/blog-post_18.html)と[おじいちゃん](https://www.irasutoya.com/2013/10/blog-post_2022.html)）

全体のプログラム図 ↓↓↓
![system.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/9ec321cb-d01a-f27c-eae2-e4493ec16512.png)
`Face Tracking CHOP`は、内部的にNvidia MaxineのAR SDKを利用しており、
顔のポイントや向きなどを手軽に取得することが出来るオペレータです。
ここで取得した`UV`や`width`の数値を基準に、顔の位置に合うよう調整します。

詳しくは以下チュートリアル動画を参照して下さい。

https://www.youtube.com/watch?v=NJbSm9UbCqk

次にblenderを開き、紙吹雪風のパーティクルを作ります。
こちらも今回は以下チュートリアル動画の通りなので、詳しくは（以下略）。

https://www.youtube.com/watch?v=h_lX8M8wvuk

ここでちょっとしたTIPSです。
`Channel Mix TOP`を使うと、黒背景の画像や動画を透過背景に出来ます。
方法はパラメータを`alpha1 => 1、alpha4 => 0`にするだけです。
（追記：`alpha1 => 1、alpha2 => 0, alpha4 => 0`の方が良い）
![confetti.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/38ce51d9-7c8c-5073-6373-8b3114778e8d.png)
この画を`Spout Out TOP`でUnrealEngineに渡してテクスチャに使い、
さらに同じくSpoutでUEのカメラ画をTouchDesignerに戻します。
これで、TouchDesigner側で映像のスイッチングをすることが出来ます。

UnrealEngine側のSpoutの出力には、`Live-streaming Toolkit`を利用します。
このプラグインはSpoutだけでなくNDIの入出力にも対応しており、便利です。

https://offworld.live/unreal-engine-live-streaming-toolkit

またTouchDesignerから飛ばすOSCをトリガーに、UEのマテリアルを操作します。
`Actor BP`にOSCの受信を定義し、`Event Dispatcher`で`Level BP`のイベントを発火。
![levelBP.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/4b919ab7-809d-8a9b-a079-219667accf09.png)

https://zenn.dev/daichi_gamedev/books/unreal-engine-tutorial-2/viewer/blueprint2-eventdispatcher

最終的な画がこちら ↓↓↓
![TDMovieOut.5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/097830db-11ed-5a27-3d0c-ff7a2e668e9f.gif)
今回はMixamoのBotを置いているだけですが、グリーンバックを用意すれば
UEのバーチャル空間に現実の人を合成することができ、XR表現を強化出来ます。

# 8. まとめ

今回はZoom Meeting SDKとTouchDesignerを組み合わせることで、
Zoomオペレーションシステムと映像演出をプロトタイピングしました。

これで課題解決。となれば良いのですが、現実はそういきません。
1台のPCに制御を集約するということは、それだけリスクも集中します。

リスク分散の観点から言えば、複数のPCと人を用意した方が良いかもしれません。

しかしドキュメントを見ていると様々な可能性が想像出来ますし、
今後もイベント業界でのZoomの利用シチュエーションは多くあるので、
検証を進めて現場に出せるようなシステムを開発したいと思いました。