WonderPlanet Tech Blog

アドバンストテクノロジー部やCTO室のメンバーを中心に最新の技術情報を発信しています。

AlexaとZoom Roomsでスマート会議スキルを開発する方法

こんにちはアドバンストテクノロジー部の@y-matsushitaです。
今回はZoom RoomsのAPIを使ってAlexaからZoom Roomsを操作するスキルを開発してみましたのでご紹介します。
先日ご紹介したAlexa for Businessで公開されているZoom for Alexaと連携させる方法とは別のものです。
開発したスキルは、以下の動作例のように日本語で繋げたい会議室を指定すると、接続や切断をすることが可能です。
これにより、ミーティングツールの使い方をよく分かっていないために会議の開始が遅くなるといった時間の無駄を削減することができます。

動作例

・「アレクサ、ズームルームで''ホール''に繋いで!」
 → "発言者の会議室" と "指定した会議室(ホール)" のZoom Roomsを接続

・「アレクサ、ズームルームで''ミーティング''を終了して!」
 →"発言者の会議室" のZoom Roomsを終了

実際の動作結果

実際の動作は先にみたほうが分かりやすいため動画でご紹介します。
youtu.be

Zoomについては紹介記事を投稿しているので、ご興味があればご覧ください。
tech.wonderpla.net

またAlexa for Businessについても、ご興味があれば合わせてご覧ください。 tech.wonderpla.net


概要&必要なもの

AWS LambdaでZoom Rooms APIを呼び出してZoom Roomsを操作します。
AlexaとZoom Roomsを活用するため、開発には以下のものを用意しておく必要があります。

Alexaスキル開発用

・Alexa 端末
・Amazon Developerアカウント
・Amazon ショッピングアカウント
(混乱を避けるため全て.co.jpリージョンであることが望ましい)
・AWS アカウント

Zoom Rooms用

・Zoom Roomsアカウント x 2
・Zoom Rooms用PC x 2
・Zoom Rooms用タブレット x 2
(接続先と接続元用に各2つずつ用意)

またPCにインストールするZoom Roomsクライアントは非公開のプレリリース版のビルドである必要があります。
Webで公開されている通常のZoom RoomsクライアントではZoom Rooms APIが使えないため、接続ができません。
こちらは非公開ではありますが、Zoomにメールするとダウンロード用のアドレスを共有してもらえました。
Zoom Rooms API - Zoom Developers

Note: These APIs need a pre-launch version of our new Zoom Room build – if you would like to try it, please send an e-mail to “developersupport@zoom.us” and we will provide you the link to download

こうして必要なものを並べると、導入のために必要なものが、かなりあるのが分かりますね。
以上の必要なものが揃ったらZoom RoomsをPCとタブレット両方にインストールしておいてください。
PCにインストールするZoom Roomsはメールで頂いたプレリリース版です。
タブレットにインストールするZoom Roomsはストアに公開されている通常のもので大丈夫です。


AlexaでZoom Roomsを操作するスキルの開発

Amazon開発者コンソールの設定では接続したい部屋名を取得するIntentを設定します。
とくに難しい設定はしなくても出来るので、詳しい解説は前回の記事をご参照ください。
tech.wonderpla.net

今回は以下のように部屋に接続するためにConnectRoomIntentを設定しました。 Alexa インテント

また{room}のスロットに部屋名として認識させたい単語を登録しておきました。
(ワンダープラネットでは会議室に名古屋名物の名前をつけているため、スロットに食べ物の名前が並んでいます笑)
Alexa スロット
この他にもミーティング開始時や終了時のインテントを追加しておきました。
ひとまず動作を見るだけであれば、先ほど設定した部屋名を取得するインテントだけで大丈夫です。


Zoom RoomsのAPI Keyの取得

次はZoom RoomsのAPIを使うために必要なKeyを取得します。
Zoom Developers – Power Up Your Apps with Video, Voice, and Screen Sharing
サインインを行ったら、画面右上のDevelopper Accountを開き、API Keyを発行しましょう。
Zoom Developer Account
Zoom Roomsのアクセストークン

Zoom Rooms APIの呼び出し

ここからは実際にソースコードを書いていきます。言語はNode.js6.10です。
JWT認証でZoom Rooms APIのAccess Tokenを発行してZoom Roomsへ命令を送ります。
ひとまずはLambdaに書く前にローカル環境で動作を確認してみます。

JWT認証

取得したAPI KeyとAPI Secretを使ってJWTでAccess Tokenを発行します。
JWTはnpmでインストールしておきましょう。

$ npm install jsonwebtoken

Access Tokenは以下のような記述で発行できます。

const jwt = require('jsonwebtoken');
const ZOOM_API_KEY = "取得したAPI Key";
const ZOOM_API_SEC = "取得したAPI Secret";

var payload = {
  iss: ZOOM_API_KEY,
  exp: ((new Date()).getTime() + 5000)
};
var token = jwt.sign(payload, ZOOM_API_SEC);

Zoom Roomsを起動

あらかじめnpmでsync-requestをインストールしておきます。

$ npm install sync-request

さきほどのコードで生成したtokenを用いてAPIを呼び出します。
以下のコードを叩くと設定したルームIDの会議室のインスタントミーティング(設定したルームIDの会議室をホストとして起動)が開始できます。
もし設定するルームIDが分からない場合は以下を参考にしてください。
Zoom RoomsのミーティングIDとルームIDを調べる

const syncrequest = require('sync-request');
const HOST_ZR_ID = "起動するZoom RoomsのルームID";
var option = {
   qs: {access_token: token},
   json: {method: "join",
          params: {force_accept: true}}
};
var zr_join = "https://api.zoom.us/v2/rooms/" + HOST_ZR_ID + "/meetings";
var syncres = syncrequest('POST', zr_join, option);
var syncBody = JSON.parse(syncres.getBody('utf8'));
console.log(syncBody);
if(syncBody.result){
    console.log("起動成功");
}else{
    console.log("起動失敗");
}

methodjoinにすることでZoom Roomsを起動します。ただし接続先を指定していないため、設定したルームIDの会議室をホストとして開始するだけで、他の会議室には繋がりません。

また、paramsにはforce_acceptを設定しています。これが無いとAPI実行時にerror: { code: 4009, message: 'The room client is busy.' }と出力され起動できないことがあります。設定しておくとbusy状態でも接続を強制してくれるようになります。他と接続していないときでも稀にbusyが返ってきて悩まされることがあるので、ひとまずは設定しておくとよいでしょう。

Zoom Roomsを他の会議室と繋ぐ

他の会議室からホストとなっている会議室に接続したい場合は、
以下のようにoptionのparamsに接続先のミーティングIDを指定すると接続できます。
このとき、ホスト側はすでに起動させておいてインスタントミーティングの状態にしておく必要があります。

const syncrequest = require('sync-request');
const HOST_MEETING_ID = "ホスト(起動済み)のZoom RoomsのミーティングID";
const GUEST_ZR_ID = "新たに起動する接続したいZoom RoomsのルームID";
var option = {
   qs: {access_token: token},
   json: {method: "join",
          params: {meeting_number: HOST_MEETING_ID,
                   force_accept: true}}
};
var zr_join = "https://api.zoom.us/v2/rooms/" + GUEST_ZR_ID + "/meetings";
var syncres = syncrequest('POST', zr_join, option);
var syncBody = JSON.parse(syncres.getBody('utf8'));
console.log(syncBody);
if(syncBody.result){
    console.log("接続成功");
}else{
    console.log("接続失敗");
}

Zoom RoomsのミーティングIDとルームIDを調べる

設定するZoom RoomsのミーティングIDとルームIDが分からない場合、以下の方法で確認できます。

ミーティングIDの確認

以下のリンクより管理画面へログインして各部屋のミーティングIDを見ることができます。
https://zoom.us/signin

ログインしたら左サイドバーの「Zoom Rooms」から対象のルームの編集をクリックします。
Zoom Rooms 管理ページ

少しスクロールするとhttps://zoom.us/j/〇〇〇〇という形式でミーティングIDを確認できます。
またこれ以外にもタブレットのZoom RoomsアプリからSettingsを開いても確認することができます。
Zoom Rooms ミーティングID

ルームIDの確認

ルームIDは設定画面での記載箇所が見つからなかったため、Zoom Rooms APIのListを使って確認しました。
以下のコードでAPI KeyとAPI Secretを設定して実行すれば、アカウントに紐づくZoom RoomsのルームIDと名前の一覧が取得できます。

const jwt = require('jsonwebtoken');
const syncrequest = require('sync-request');
const ZOOM_API_KEY = "取得したAPI Key";
const ZOOM_API_SEC = "取得したAPI Secret";

var payload = {
  iss: ZOOM_API_KEY,
  exp: ((new Date()).getTime() + 5000)
};
var token = jwt.sign(payload, ZOOM_API_SEC);

var option = {
   qs: {access_token: token},
   json: {method: "list"}
};

var zr_list = "https://api.zoom.us/v2/rooms/zrlist";
var syncres = syncrequest('POST', zr_list, option);
console.log(syncres.getBody('utf8'));

Alexaを使ってAWS Lambdaから実行

ローカル環境で動作することが確認できたら、ここからはAWS Lambdaにソースを移して声で実行してみます。
簡単にAlexa用に書くと以下のようになります。AWS Lambdaに登録が完了したらARNをAmazon開発者コンソールにコピーしてください。

"use strict";
const jwt = require('jsonwebtoken');
const syncrequest = require('sync-request');
const Alexa = require("alexa-sdk");
const SKILL_NAME = "ZOOM"
const APP_ID = "amzn1.ask.skill.xxxxx";
const ZOOM_API_KEY = "取得したAPI Key";
const ZOOM_API_SEC = "取得したAPI Secret";
const HOST_ZR_INFO = { zr_name: '起動するZoom Roomsの会議室名', zr_id: '起動するZoom RoomsのルームID', meeting_id: '起動するZoom Roomsのミーティング'ID };
const GUEST_ZR_INFO = { zr_name: '接続したいZoom Roomsの会議室名', zr_id: '接続したいZoom RoomsのルームID', meeting_id: '接続したいZoom RoomsのミーティングID' };

//ステートの定義
const states = {
  CONNECT: '_CONNECTMODE'
};

var languageString = {
    "ja-JP": {
        "translation": {
            "WELCOME_MESSAGE": "ズームルームへようこそ。",
            "HELP_MESSAGE": "ほかの部屋と接続したい場合は、ルーム名を教えてください。",
            "UNHANDLED_MESSAGE": "すみません、よく聞きとれませんでした。",
            "CONNECT_MESSAGE": "%sに接続します。",
            "ERROR_CONNECT_MESSAGE": "接続に失敗しました。",
            "EXIT_MESSAGE": "さようなら。"
        }
    }
};

exports.handler = function(event, context, callback) {
  var alexa = Alexa.handler(event, context);
  alexa.resources = languageString;
  alexa.dynamoDBTableName = 'ZoomRooms';
  alexa.registerHandlers(handlers,connectHandlers);
  alexa.execute();
};

// 初期ハンドラ
var handlers = {
  'LaunchRequest': function () {
    this.emit(':ask', this.t("WELCOME_MESSAGE") + this.t("HELP_MESSAGE"));
  },
  'AMAZON.HelpIntent': function () {
    this.emit(':ask', this.t("HELP_MESSAGE"));
  },
  'AMAZON.CancelIntent': function() {
    this.emit(':tell', this.t("EXIT_MESSAGE"));
  },
  'AMAZON.StopIntent': function() {
    this.emit(':tell', this.t("EXIT_MESSAGE"));
  },
  'ConnectRoomIntent': function() {
    this.handler.state = states.CONNECT;
    this.emitWithState("ConnectRoomIntent");
  },
  'Unhandled': function () {
    this.emit(':ask', this.t("UNHANDLED_MESSAGE"));
  }
};

// 接続ステート
var connectHandlers = Alexa.CreateStateHandler(states.CONNECT, {
  'ConnectRoomIntent': function() {
    var response = "";
    var asyncToken = getToken(ZOOM_API_KEY, ZOOM_API_SEC);
    //ホストを起動
    var isOpenHost = connectionRoom(asyncToken, HOST_ZR_INFO);
    if(isOpenHost){
      //ホストが正常に起動出来ていたら、依頼された部屋と接続
      if(connectionRoom(asyncToken, GUEST_ZR_INFO, HOST_ZR_INFO)){
        response = this.t("CONNECT_MESSAGE", GUEST_ZR_INFO.zr_name);
      }
    }
    if(!response){
      // 失敗時はエラーメッセージを流す
      response = this.t("ERROR_CONNECT_MESSAGE");
    }
    this.emit(':tell', response);
  },
  'Unhandled': function() {
    this.emit(':ask', this.t("UNHANDLED_MESSAGE"));
  }
});

// 部屋を起動(AcessToken, 起動する部屋, 接続先の部屋)
function connectionRoom(token, startRoom, connectRoom){
  var option = {
     qs: {access_token: token},
     json: {method: "join",
            params: {force_accept: true}}
  };
  if(connectRoom){
    // 接続先がある場合、optionに接続先のミーティングIDを追加
    option.json.params.meeting_number = connectRoom.meeting_id;
  }
  var zr_join = "https://api.zoom.us/v2/rooms/" + startRoom.zr_id + "/meetings";
  var syncres = syncrequest('POST', zr_join, option);
  var syncBody = JSON.parse(syncres.getBody('utf8'));
  console.log(syncBody);
  if(syncBody.result){
      console.log("接続成功");
      return true;
  }else{
      console.log("接続失敗");
      return false;
  }
};

// ZoomRooms接続用トークンを発行
function getToken(api_key, api_sec){
  var payload = {
    iss: api_key,
    exp: ((new Date()).getTime() + 15000)
  };
  return jwt.sign(payload, api_sec);
}

上記のコードでは、Zoom Roomsの接続先がGUEST_ZR_INFOに固定となっているため、
実際に運用する場合はConnectRoomIntentで取得した部屋名からIDを割り出すコードは必要になるかと思います。
またendのmethodなどを駆使すればAlexaでZoom Roomsを終了したりすることも可能です。


Zoom Rooms API

上記のコードで扱ったZoom Roomsの起動、接続以外にも以下のようなことが可能です。
凝ったことはできませんが、基本的な機能は一通り使えるようです。

Zoom Rooms API
Restart POST https://api.zoom.us/v2/rooms/{roomId}/zrclient
"method": "restart"
ズームルームクライアントを再起動する。
List POST https://api.zoom.us/v2/rooms/zrlist
"method": "list"
登録されているズームルームのIDと名前の取得する。
Leave POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "leave"
進行中の会議をそのままにして会議から退出。指定の部屋がホストだった場合は会議を終了する。
Join POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "join"
指定の会議が開かれている場合はそこに参加。 指定の会議が開かれていない場合はホストとなりインスタントミーティングを開始する。
Invite POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "invite"
ズームルームへ連絡先を招待する。
Schedule POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "schedule"
会議をスケジュールする。
Cancel POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "cancel"
スケジュールされた会議をキャンセルする。
End POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "end"
指定した部屋がホストだった場合は会議を終了する。指定した部屋が他の部屋にJOINしていた場合は、会議をLeaveする。

最後に

今回はZoom RoomsのAPIを使ってAlexa用のスマート会議室スキルを開発しました。
現在は一部の会議室で検証中ですが、会議前のモタつきが軽減されるので大変便利です。
色々と必要なものが多いためハードルが高めではありますが、
もし既にZoom Roomを使っていたりスマート会議室に興味があるのであれば、チャレンジしてみてはいかがでしょうか。

参考

Zoom Rooms API - Zoom Developers
Zoom for Alexa - GitHub