iPadをWebex Deviceのリモコンにしてみた

# 0. はじめに
この記事は、本記事は有志によるWebex Advent Calendar 2021のいよいよ折り返し地点の15日目 :tada: として投稿しています。

最近､徐々にオフィスへの出社も増え社内からさまざまな会議に参加する機会が増えているのではないでしょうか?
これからオフィスの会議室などで在宅勤務しているメンバーと会議をする機会も増え､会議専用のWebex Deviceを利用する機会も増えてくると思います｡この記事ではWebex DeviceのAPIを利用した端末の活用方法をご紹介します｡

# 1. 背景
CiscoのDesk Series以外のWebex Deviceではタッチパネル型のリモコンが付属しています｡これは実は進化を遂げていてTouch10と呼ばれるものからRoom Navigatorと呼ばれるものに2021年に切り替わっています｡

Touch10はこの様な形をしているリモコンでオフィスの会議室などで見たことがある方もいらっしゃるのではないでしょうか?
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/905342/8a63d0f6-2dcf-409e-1c89-aca55ccc3b05.png)

一方で以前から無線で持ち運べるタッチパネル型のリモコンのご要望は多くありました｡残念ながら無線や電源供給などの懸念があるためシスコからはタブレット型のリモコンはリリースされておりません｡
そこでこの記事ではiPadをRoom NavigatorやTouch10の代わりにリモコンとして使用できるようなプロトタイプを作成してみたいと思います｡

# 2.実現方法
どのように制御するかというとほかの記事でも紹介されているWebex DeviceのAPIを利用します｡これはxAPIと呼ばれているものでAPIの機能一覧はこちらの[ページ](https://roomos.cisco.com/xapi)にまとめられています｡
今回の構成ではWebex DeviceにWebsocket経由でxAPIの実行を行います

## 2.1システム構成
今回のシステムではiPadのブラウザでアクセスできるWebアプリの形式でリモコンを利用できるようにしています｡
構成としてはサーバ上にあるWebアプリにiPadなどからアクセスし､Webアプリを実行することでリモコンとして動作させます｡


## 2.2 環境
今回は開発を簡単にするためにFlaskを使用して開発しています｡全体の構成要素としては以下のコンポーネントを使用しています｡

- Webex Device(Desk Proで動作確認)
- [RoomOS xAPIs](https://roomos.cisco.com/xapi)
- Python
    - Python Module: Flask
    - Ciscoが作成したPythonのxAPIのライブラリ[XoWS](https://github.com/cisco-ce/pyxows)

## 2.3 実現する機能
今回のリモコンではなるべくシンプルにするために以下の2つの機能だけを実装しています｡xAPIのガイドにある関数はこの方法を応用することで実現できるのでこれ以外の機能の実現も比較的容易かと思います｡

- 会議番号でのWebexへの発信
- ミュートの有効化/解除

# 3. 開発
## 3.1 開発環境の用意
今回の開発はPythonで行っています｡OSはWindows/MacOS/Linuxどれでも大丈夫だと思います｡私はMacOSで作成､実行しています
また､Pythonのバージョンは3.9で行っています

- Flaskのインストール 
-
今回の環境で利用するPythonのライブラリ Flaskをインストールします｡

```python
pip install Flask
```

- XoWSのインストール
-
つぎに開発を簡単にするためにシスコが提供しているPython用のライブラリであるXoWSをインストールします｡
こちらのソースファイルは[こちら](https://github.com/cisco-ce/pyxows)からダウンロード可能です

```python
git clone [github link here]
cd pyxows
python setup.py install
```

## 3.2 端末側でのユーザの用意
セキュアにコマンドの実行を行うために端末側でコマンドを実行するユーザを普段管理に用いているユーザとは別に用意します｡
Webex DeviceのWeb GUIにアクセスし､Users>Create Userをクリックします｡
つぎに以下の画面からユーザ名､Role､パスワードを設定します｡今回の用途ではIntegratorおよびUserの権限を付与しています｡ここで付与する権限によって変更できるパラメータが変化します｡
各権限で実行できるコマンドはコマンドのリファレンス(例: [Webex Join](https://roomos.cisco.com/xapi/Command.Webex.Join/?search=webex%20join)など)にあるUser rolesを参照することで確認可能です｡
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/905342/99fb81b7-796d-1094-0875-ac66b7e43fb2.png)

## 3.3 Pythonでの開発
今回のアプリでは特定のPathにリクエストがきた場合にコマンドを実行するように設計しています｡この部分の機能はFlaskを使用しており以下のようにPathごとに来たリクエストの種類によって実行する関数を制御しています｡
今回は3つPathを作っており､'/'がリモコンの画面のインターフェイス､'/mute_device'がミュートの制御､'/join'がWebex会議への参加です｡
後者の2つはリクエストの種類としてPOSTが来た場合に動作します｡

```python
@app.route('/')
def index():
    asyncio.run(start())
    return render_template('index.html')

@app.route('/mute_device', methods=['POST'])
# Mute Device route
def mute_device():
    print('Muting device')
    asyncio.run(mute_device())
    return str(system_info['is_muted'])

@app.route('/join', methods=['POST'])
# Join Webex
def join():
    number_to_dial = request.form.get('number_to_dial')
    response = asyncio.run(join(number_to_dial))
    return number_to_dial
```

次に実際にどのように発信やミュートの操作が行われるかを見てみましょう｡

* Webexへの発信
前述したようにこのプログラムではxAPIをベースにアプリケーションを操作しています｡
Webexへの発信は[こちらの関数](https://roomos.cisco.com/xapi/Command.Webex.Join/)を使用しています｡
これを実行するためには発信先の番号が必要になりますが､発信先の番号はPOSTのBodyで送られてくるように設計しています｡
そのためこの発信先の番号を変数として以下のようなコマンドひとつで実行しています

```python
await client.xCommand(['Webex','Join'], number=number_to_dial)
```

* ミュート
発信の場合と同様にミュートの操作も[xAPI](https://roomos.cisco.com/xapi/Command.Audio.Microphones.Mute/)で行います｡ミュートの場合注意が必要なのがボタン一つでミュートの解除､有効化を行なうために現在のミュートのステータスに応じて解除/有効化を切り替えています｡
その場合ミュートの解除の前に以下のようにステータスの取得を行なっています

```python
system_info['is_muted'] = await client.xGet(['Status', 'Audio', 'Microphones', 'Mute'])
```
その結果に応じて以下のように実行するアクションを変えています｡

```python
if(system_info['is_muted'] == 'Off'):
            await client.xCommand(['Audio', 'Microphones', 'Mute'])
        else:
            await client.xCommand(['Audio', 'Microphones', 'Unmute'])
```


以下に全体のソースコードを載せておきます｡
<details><summary>ソースコード</summary><div>

```python
import xows
import asyncio
from flask import Flask, render_template, request

app = Flask(__name__)

# Global variables
count = 0
system_info ={}

@app.route('/')
def index():
    asyncio.run(start())
    return render_template('index.html')

@app.route('/mute_device', methods=['POST'])
# Mute Device route
def mute_device():
    print('Muting device')
    asyncio.run(mute_device())
    return str(system_info['is_muted'])

@app.route('/join', methods=['POST'])
# Join Webex
def join():
    number_to_dial = request.form.get('number_to_dial')
    response = asyncio.run(join(number_to_dial))
    return number_to_dial

async def start():
    global system_info
    async with xows.XoWSClient('device-ip-here', username='api-user', password='password-here') as client:

        system_info['is_muted'] = await client.xGet(['Status', 'Audio', 'Microphones', 'Mute'])

        print(system_info)

async def mute_device():
    global system_info
    async with xows.XoWSClient('device-ip-here', username='api-user', password='password-here') as client:

        system_info['is_muted'] = await client.xGet(['Status', 'Audio', 'Microphones', 'Mute'])
        print(system_info)
        if(system_info['is_muted'] == 'Off'):
            await client.xCommand(['Audio', 'Microphones', 'Mute'])
        else:
            await client.xCommand(['Audio', 'Microphones', 'Unmute'])

        system_info['is_muted'] = await client.xGet(['Status', 'Audio', 'Microphones', 'Mute'])

async def join(number_to_dial):
    print('Dialing to: ' + number_to_dial)
    async with xows.XoWSClient('device-ip-here', username='api-user', password='password-here') as client:
        print('Command:',
            await client.xCommand(['Webex','Join'], number=number_to_dial))

        await client.wait_until_closed()

if __name__ == "__main__":
    app.run()
```
</div></details>

## 3.4 リモコンGUI作成
手元にあるiPadをリモコンとして利用するために以下のようなHTMLページを作成しました｡見た目はCSSなどを使用していないので可能な限り省エネでシンプルになるようにしています｡
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/905342/bcacdcd7-c4e1-1e3f-23dd-55457e0043ad.png)

このページではボタンを押したときに画面遷移させずにPOSTさせるためにAJAXを使用しています｡もう少し頑張るとしたら現在のミュートの状況に応じてミュートボタンの色や表示を変更することでよりTouch10などのリモコンに近づけることができると思います｡上で作成したミュート機能のPythonの関数はミュートの状況にうじてPOSTへのレスポンスが変わっているのでそれに応じてオブジェクトを書き換えるだけです｡
リモコン画面の背景やボタン配置､ボタンの形などはいくらでも実物に似せることができるのでUIを似せてみるのも面白いかもしれません｡

以下にリモコン部分のソースコードを載せておきます｡

<details><summary>index.html</summary><div>

``` html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Webexリモコン</title>
</head>
<body>
    <h1>Webexへの発信</h1>
    <p>会議番号 : <input type="text" id="main" /><button id="send">送信</button></p>
    <div id="return"></div>
    <script>
    $(function(){
      $("#send").on("click", function(event){
        let id = $("#main").val();
        $.ajax({
          type: "POST",
          url: "/join",
          data: { "number_to_dial" : id },
          dataType : "json"
        });
      });
    });
    </script>

    <h1>ミュート</h1>
    <p><button id="mute">ミュートボタン</button></p>
    <div id="mute"></div>
    <script>
    $(function(){
      $("#mute").on("click", function(event){
        let id = $("#mute").val();
        $.ajax({
          type: "POST",
          url: "/mute_device",
          data: { },
          dataType : "json"
        });
      });
    });
    </script>
</body>
</html>
```
</div></details>

# まとめ
この記事ではWebex Deviceに備わっているAPIを利用してiPadでWebex Deviceの操作を行ないました｡ここで紹介したのは単純な発信やミュートの動作のみですがほかにもカメラのプリセットの呼び出しやボリュームの調整など応用の可能性は無限です｡
Webex自体にも豊富なAPIが存在しており、今回紹介したWebsocket以外にもクラウド経由での実装など環境や用途に応じて様々な実装を行なうことが可能です｡
ご興味を持っていただけたらCisco DevNet や Cisco Webex for Develper のページもぜひ覗いてみてください。

# 参考文献
xAPI over WebSocket - XoWS (CE9.7.x)
https://community.cisco.com/t5/collaboration-voice-and-video/xapi-over-websocket-xows-ce9-7-x/ba-p/3831553
GVE_DevNet_RoomOS_Endpoints_Control
https://github.com/gve-sw/GVE_DevNet_RoomOS_Endpoints_Control
pyxows
https://developer.cisco.com/codeexchange/github/repo/cisco-ce/pyxows/
pyxows(レポジトリ)
https://github.com/cisco-ce/pyxows

# 免責事項
本サイトおよび対応するコメントにおいて表明される意見は、投稿者本人の個人的意見であり、シスコの意見ではありません。本サイトの内容は、情報の提供のみを目的として掲載されており、シスコや他の関係者による推奨や表明を目的としたものではありません。各利用者は、本Webサイトへの掲載により、投稿、リンクその他の方法でアップロードした全ての情報の内容に対して全責任を負い、本Web サイトの利用に関するあらゆる責任からシスコを免責することに同意したものとします。