Zoom Meeting SDK Web Component viewを試してみた

1. はじめに

この記事は「Qiita Engineer Festa 2022」に参加するための記事です。

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

株式会社フレクトの先端技術室に務めるエンジニアです。今回はZoom meeting sdkのComponent viewを実装してみました。

2.Component viewとClient viewの違い

Client viewの方がnativeのzoomに似ていて、Web版zoomという感じになります。それに対して、Component viewはZoomウェジット化されたUIになります。

両者の内部が違って、<b>サポートされている機能も異なります。</b>

例えば、Client viewの方は初期化後でも言語が変更できるが、Component viewの方は変更ができません。逆にComponent viewはUIの位置と表示の有無の変更ができますが、Client viewの方が基本変更できません。

両者の使い分け方法としては、できるだけZoomの見た目に再現したいならClient view、いろいろなUIの変更を行いたいならComponent viewになると思います。下のような構築はComponent viewしかできません。

<img src="https://marketplace.zoom.us/docs/images/sdk/msdk-web-component-positioning.png" alt="Component view">

3.システム環境

Amazon EC2のARMアーキテクチャで実装してみた。Instance環境はt4g.medium。

<b>最初はt4g.smallで足りると思ってましたが、githubの<code>npm build</code>がメモリー不足で実施できませんでした。</b>従って、t4g.mediumが必要です。

実装方法は簡単で、<code>github cloneして</code>,<code>npm install && npm run</code>で完了です。後は API_KEY and API_SECRET <code>tools/nav.js</code>に追加するだけです。

ユーザー視点からどうすればもっと使いやすのか、２点を改良してみました。

4.Component viewの改良1 ワンクリックでMeetingに参加する

Component view一つの不便なところは、ワンクリック参加という機能が存在してないです。現在の使用はLoginページでmeeting ID、パスコードやuser nameを入力して入室する形になります。

しかし、皆様は普段zoomを使っている時パスコード以外は入力しないですよね。こういう観点で見ると、手間がかかります。

Pythonでワンクリック参加機能を実装してみました。

流れとしては、まずZoom APIでユーザーのmeeting IDを検索します。次はsignaureを生成します。最後はワンクリックで参加リンクを生成します。

<ol>
  <li>meeting IDを検索</li>

現時点instant meetingをAPIでは検知できません。

<b>Access Tokenの生成について、JWT appは非推奨になったため、Server-to-server-oauth-appがおすすめです。</b>

https://marketplace.zoom.us/docs/guides/build/server-to-server-oauth-app

Server-to-server-oauth-appがない場合は、まず作成する必要があります。

以下のコードでaccess tokenが生成できます。client_id、client_secretとaccount_idをそれぞれを入れ替えてください。

```
encodedData = base64.b64encode(bytes(f"{client_id}:{client_secret}", "ISO-8859-1")).decode("ascii")
headers= f"Authorization: Basic {encodedData}"
join_url = ''

headers = {
    'Authorization': 'Basic {}'.format(encodedData),
}
params = {
    'grant_type': 'account_credentials',
}

response = requests.post('https://zoom.us/oauth/token?grant_type=account_credentials&account_id={}'.format("account_id"), params=params, headers=headers)
```
Access tokenが作成できたため、いろんなAPI callが実現できます。今回は最新のmeeting idを取得します。

```
# get meetings
headers = {
    'authorization': "Bearer {}".format(json.loads(response.text)['access_token']),
    'content-type': "application/json"
    }
response = requests.get("https://api.zoom.us/v2/users/cT62Uo3aSh-sroNrY_tUrg/meetings", headers=headers)
meetings = json.loads(response.text)
try:
    number = len(meetings["meetings"])
    id = (meetings["meetings"][number-1]['id'])
except Exception as e:
    print("Some error has occured")
```

Zoom APIではパスコードを取得できません。普通の参加URLも暗号化されてます。解決方法は、パスコードを無効にするか（無料アカウントではできない）、またはパスコードを固定する方法があります。セキュリティ観点ではパスコード無効はおすすめしないため、パスコードを固定してます。


<li>Generate sinature </li>
Generate signatureの部分は公式ドキュメントのままを使います。

https://marketplace.zoom.us/docs/sdk/native-sdks/web/signature/

```
def generateSignature(data):
    ts = int(round(time.time() * 1000)) - 30000;
    msg = data['apiKey'] + str(data['meetingNumber']) + str(ts) + str(data['role']);
    message = base64.b64encode(bytes(msg, 'utf-8'));
    secret = bytes(data['apiSecret'], 'utf-8')
    hash = hmac.new(secret, message, hashlib.sha256);
    hash =  base64.b64encode(hash.digest());
    hash = hash.decode("utf-8");
    tmpString = "%s.%s.%s.%s.%s" % (data['apiKey'], str(data['meetingNumber']), str(ts), str(data['role']), hash);
    signature = base64.b64encode(bytes(tmpString, "utf-8"));
    signature = signature.decode("utf-8");
    return signature.rstrip("=");
```

<li>参加リンクを生成</li>
Meetingサーバーを通すためには<code>name={}&mn={}&email=&pwd={}&role=0&lang=en-Us&signature={}&china=0&apiKey={}
</code>を入力する必要があります。それぞれのパラメータを説明します。

<code>name</code>はユーザー名です。このユーザー名は平文ではなく、base64に暗号化した必要があります。

<code>mn</code>は先ほど取得したmeeting numberです。

<code>email</code>は入力必要がありませんので、そのまま空にします。

<code>pw</code>はパスコードです。固定にしているなら、環境変数で取得できます。

<code>role</code>はホストか参加者のことです。0は参加者、1はホスト。

<code>lang</code>は言語です。

<code>signature</code>は２番で生成したsignatureです。

<code>china</code>は中国大陸であるかどうかです。そのままにします。

<code>apiKey</code>はSDKのAPIのapiKeyです。Oauth appのapikeyではなくため、間違いしないように。

以下のコードはワンクリックリンクを生成できます。

```
data = {'apiKey': api_key,
                'apiSecret': "",
                'meetingNumber': ,
                'role': 0}
signature=(generateSignature(data))
meeting_number = number
password = ''
sdk_Key = ''
name = base64.urlsafe_b64encode(b"{}".format("user_name"))
meeting_url = 'https://input_your_site.com/cdn.html?name={}&mn={}&email=&pwd={}&role=0&lang=en-Us&signature={}&china=0&apiKey={}'.\
format(codecs.decode(name),meeting_number, password, signature, api_key)
```

HTTPSを実施しないといろんな機能が制限されてます。カメラが使えない、またチャットも使えません。少なくてもself-signed certificateを使いましょう。

先ほど生成したリンクをサーバーで```return '<a href={}>Join the zoom meeting</a>'.format(meeting_url)```で返せばリンクを生成できます。それをクリックすれば何も入力せずに参加できます。signatureは有効期限があるため、使う直前までに呼びましょう。

</ol>

5.Component viewの改良2 チャットUIを最初に表示する

Component viewもう一つの不便なところは、チャットボックスが存在してますが、ユーザーがクリックしないと表示しません。現時点SDKでは調整できません。しかも２回クリックが必要で、チャットが必要と想定されている場合は、ユーザーとして面倒です。

https://devforum.zoom.us/t/show-chat-ui-on-meeting-join/65151

よく使われている<code>$(window).bind(）</code>は、ウェジットがロード完了したか確認できません。従って使うとエラーになります。

解決方法としては、Jqueryを実施し、自動的にelementをクリックすれば、最初に出るようになります。

<code>public/cdn.html</code>というファイルを以下のように追加すればできます。既存コードは省略されてます。

```
<!DOCTYPE html>
<html>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>

</head>
<body>
    <script>    
    // https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists
      function waitForElm(selector) {
          return new Promise(resolve => {
              if (document.querySelector(selector)) {
                  return resolve(document.querySelector(selector));
              }

              const observer = new MutationObserver(mutations => {
                  if (document.querySelector(selector)) {
                      resolve(document.querySelector(selector));
                      observer.disconnect();
                  }
              });

              observer.observe(document.body, {
                  childList: true,
                  subtree: true
              });
          });
      }
      waitForElm('.zmwebsdk-MuiSvgIcon-root').then((elm) => {
          $(".zmwebsdk-MuiSvgIcon-root")[8].closest("button").click();
          $(".zmwebsdk-MuiListItem-button")[0].click()
      });
    </script>
</body>
</html>
```
参照元
https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists

簡単に説明すると、Jqueryを使用してelementを２回をクリックだけです。もちろんClient view自体はロードする時間がかかりますので、<code>zmwebsdk-MuiSvgIcon-root</code>で表示できるまで待つ必要があります。

6.次のStep

今回Web Component view機能を試して、改善してみました。まだ改善できていない、次実現したいところをリストします：
  1.Instant meetingが作成した後、自動的にサーバーに送信する仕組み、またInvite LinkとInvitation textどちらもはmeeting情報をコピーした後、自動的にmeeting情報を引き出します。
  2.ユーザーのIPを検知して、自動的に言語を変更します。
  3.現在ユーザーが自由にウェジットを動かすことはできますが、サーバー側は一応初期化した後は変更ができません。初期化後でもJavascriptでUIの位置とサイズを変更します。

最後までお読みいただきありがとうございます。