🀄️

Vonage Video Express with VCR

はじめに

みなさん、こんにちは。KDDI ウェブコミュニケーションズで CPaaS のエバンジェリストをしている高橋です。
この記事では、Vonage Video Express について解説をします。

本記事の対象となる読者

  • Vonage Video Express について学習したい方
  • Vonage Cloud Runtime の使い方をある程度理解している方

Vonage とは

Vonage_logo

Vonage は、米国ニュージャージー州に本社を置く、CPaaS(Communication Platform as a Service)企業です。
もともとは VoIP(Voice over IP)企業としてスタートしましたが、いくつかの企業買収を行うことで、コミュニケーションサービス全般をサポートすることができる企業に発展しました。現在はスウェーデンの大手通信機器会社エリクソンの傘下に入っています。

2024年2月14日より、株式会社KDDIウェブコミュニケーションズ(以後、KWC)が Vonage の再販事業を開始することとなりました。
KWC経由でアカウントを開設する場合、Vonageで直接開設したアカウントとは一部仕様が異なります。(※提供する機能面において違いはありません)

なお、本記事ではKWC経由でのアカウントを使って説明を行います。

Vonage Video Express とは

Vonage が提供する Video 機能には大きく分けて、Vonage Video APIVonage Video Express があります。
前者は、フルカスタマイズなビデオアプリケーションを作成することができる反面、UIの構築も含めてすべて開発が必要となります。一方後者は、HTMLやJavaScriptが理解できるウェブ開発者であれば、簡単にビデオアプリケーションを作成することができます。

利用料金は完全従量制となっており、会議に参加している1ユーザーあたり、0.58円/分〜となります。料金の詳細については、こちらをご覧ください。

Vonage Cloud Runtime とは

Vonage Cloud Runtime (VCR)は、Vonage が提供するクラウド型のアプリケーションサーバーです。
VCR上に構築されたアプリケーションはVonage上にホスティングされるため、皆さん側でAWSなどの外部サーバーを準備する必要がありません。
これにより、Vonage API を使ってすぐにPoCやテストが行えるほか、外部のAPIサービスと連携したアプリケーションを構築することもできます。
VCR については、以下の記事もぜひご覧ください。

用語定義

用語 説明
セッション(Session) ビデオ会議を行うためのルームをセッションと呼びます。同じセッションに複数のユーザーが参加することでビデオ会議が実現します。セッションに有効期限はなく、一度会議が終了した後に同じセッションを使って再び会議を行うこともできます。
トークン(Token) ユーザーがセッションに参加するための認証キーです。トークンには有効期限があり、通常は会議に参加する前にサーバーサイドでトークンを生成して利用します。トークンはあくまでもセッションに参加するときだけに使うものなので、会議中に有効期限が切れても会議から外れることはありません。

全体構成図

本記事で構築するアプリケーションの全体構成は以下のとおりとなります。

全体構成図

  • Vonage上には、ビデオ会議用のアプリケーションが1つ作成されます。
  • アプリケーションには、VCR環境が1つ作成されます。
  • VCR内には、コーディングを行うためのVCRアプリケーションが作成されるので、そちらにコーディングを行います。
  • 作成したVCRアプリケーションをVCR上でデプロイをすることで、インスタンスが立ち上がります。
  • インスタンスには、VCRアプリケーションで指定したエンドポイントが設定されるので、外部からはそのエンドポイントにアクセスすることでプログラムを実行できます。
  • Vonage上のアプリケーションで、Video Express の認証情報に利用するための公開鍵と秘密鍵を生成し、秘密鍵はVCR上に保管して利用します。

アプリケーションとVCRの準備

では最初にVCR環境を作成します。
VCRを作成すると、自動的にVonageアプリケーションも作成されます。

Vonageダッシュボードにログイン

こちらのページから、Vonage ダッシュボードにログインをします。
Vonage本家でアカウントを作成した場合は、こちらからログインしてください。

Code Hub

  • Code Hub タブに遷移します。
    Developer Page

テンプレートが表示されます。

  • 一覧からStarter Projectを見つけて、クリックします。

Starter Project

  • Starter Project の詳細ページに遷移します。

Starter Project get code

  • 画面中央に表示されている Get Code タブを選択します。
  • Create a new deveopment environmentボタンを押します。

Set up your workspace

  • Regionは、「AWS - Asia Pacific」を選択します。
  • Workspace nameは、「Video Express」としておきます。
  • Continueボタンを押します。

しばらくすると画面が遷移し、以下のようなコーディングページが表示されます。

VCR Init page

秘密鍵の生成

コーディングに入る前に、Vonageアプリケーション内で秘密鍵を作成していきます。

  • ブラウザで、Vonage ダッシュボードを開いているタブに移動します。
  • アプリケーションの一覧から今作成したVideo Expressを選択します(表示されない場合はリロードしてください)。
  • 画面右側にある編集ボタンを押します。

Vonageアプリケーション

  • 公開鍵と秘密鍵を生成ボタンを押します。

秘密鍵の生成

  • private.keyが生成され、自動的にダウンロードされます。

保存

  • 画面右下の変更を保存ボタンを押します。

では次に、今作成した秘密鍵をVCRアプリケーションに取り込んでみましょう。

private.key

  • プロジェクトの直下に、privateというフォルダを新規に作成します。
  • 先ほど作成したprivate.keyを今作成したフォルダにインポートします(ドラッグアンドドロップでインポートできます)。

続いて、秘密鍵のパスを環境変数に指定します。

  • vcr.ymlを選択します。

  • project/nameを「video-express」に書き換えます。

  • environmentの内容を以下のように書き換えます。

  environment:
    - name: PRIVATE_KEY_PATH
      value: "./private/private.key"

vcr.yml

以上で秘密鍵の準備は完了です。

サーバーサイドのコーディング

続いて、サーバーサイドのプログラムを作っていきましょう。今回作る必要があるのはトークンの作成プログラムです。
トークンの作成には、秘匿しなくていけない情報が含まれるため、会議に参加する直前にサーバー側で作成するのが一般的です。
トークンを作成する際に必要となる情報は以下のとおりです。

項目 説明
アプリケーションID VonageアプリケーションのIDです。先程作成した秘密鍵に対応する公開鍵が設定されている必要があります。
秘密鍵のパス 先ほど作成したprivate.keyのパスです。
ユーザー名 セッションに参加するユーザーの名前です。

上の2つについては、環境変数から取得します。アプリケーションIDについては、規定の環境変数(VCR_API_APPLICATION_ID)があるため、vcr.ymlに記述する必要はありません。

ユーザー名については、トークンの作成リクエストを取得する際に、Body部にて取得するようにします。

エディター画面に戻って、プロジェクトの直下にあるindex.jsを以下の内容に書き換えてください。

import { Auth } from "@vonage/auth";
import { Vonage } from "@vonage/server-sdk";
import { vcr } from "@vonage/vcr-sdk";
import express from 'express';
import fs from 'fs'

const app = express();
const port = process.env.VCR_PORT;

const SESSION_FILE = './session.json'

const credentials = new Auth({
    applicationId: process.env.API_APPLICATION_ID,
    privateKey: process.env.PRIVATE_KEY_PATH
})

const vonage = new Vonage(credentials);

app.use(express.json());
app.use(express.static('public'));

app.post('/token', async (req, res) => {
    console.log(`🐞 token called. ${req.body.userName || ''}`)
    let sessionId = ''
    try {
        // Session create when it not exist
        let session = fs.existsSync(SESSION_FILE) ? JSON.parse(fs.readFileSync(SESSION_FILE), 'utf8') : ''
        if (!session) {
            console.log(`🐞 session not exist.`)
            // Create sessionId
            session = await vonage.video.createSession({ mediaMode: "routed"})
            sessionId = session.sessionId
            fs.writeFileSync(SESSION_FILE, JSON.stringify(session))
        } else {
            sessionId = session.sessionId
        }
    } catch (error) {
        console.error(`👺 Create session error. ${error.message}`)
        res.send(error.message).sendStatus(500)
    }
    try {
        const userName = req.body.userName || ''
        if (!userName) {
            throw new Error('userName not set.')
        }
        // JWT create
        const options = {
            role: "publisher",
            expireTime: Math.round(new Date().getTime() / 1000) - 30 + 24 * 60 * 60, // in one day
            data: `name=${userName}`,
            initialLayoutClassList: ["focus"]
        }
        const token = vonage.video.generateClientToken(sessionId, options)

        res.json({"apiKey": process.env.API_APPLICATION_ID, "token": token, "sessionId": sessionId})
    } catch (error) {
        console.error(`👺 Generate token error. ${error.message}`)
        res.sendStatus(500)
    }
})

app.get('/_/health', async (req, res) => {
    res.sendStatus(200);
});

app.listen(port, () => {
    console.log(`App listening on port ${port}`)
});

では、重要な部分について解説をしていきます。
トークンを作成するのは、22〜60行目のapp.post('/token', async (req, res) => { ... });の部分です。

トークンを作成する前にセッションを作成していることに注目してください。セッションとは Video Express での会議室のことで、トークンを作成する際に会議室を識別するためのsessionIdが必要となります。
セッションは一度作成すると有効期限はなく、いつでも同じセッションを使い続けることができます。
そこで、一度作成したセッションをVCRのプロジェクト直下にsession.jsonとして記録することで、すでにセッションがある場合はそれを利用するようにしています。
そして、session.jsonがない場合にのみ、新規にセッションを作成しています(31行目)。セッションの作成に関するドキュメントは、こちらを参照してください。

トークンの作成は、53行目のconst token = vonage.video.generateClientToken(sessionId, options)で行っています。トークンの作成に関するドキュメントは、こちらを参照してください。

package.jsonの設定

今回作成したコードでは、@vonage/authというライブラリを利用しているため、このタイミングでpackage.jsonも修正していきます。

エディター画面で、package.jsonを開いて、以下の内容に変更します。

{
  "name": "video-express",
  "type": "module",
  "version": "1.0.0",
  "description": "Vonage Video Express with VCR",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "debug": "vcr debug"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@vonage/auth": "^1.10.0",
    "@vonage/server-sdk": "^3.14.0",
    "@vonage/vcr-sdk": "1.2.0",
    "express": "4.19.2"
  }
}

index.htmlを作成

  • public > index.htmlを選択します。
  • index.htmlの内容を以下のように書き換えます。
<!DOCTYPE html>
<html lang = "ja">
  <head>
    <meta charset = "UTF-8">
    <script src="https://static.opentok.com/v1/js/video-express.js"></script>
    <style>
      body {
          margin: 0;
      }        
      /* Assuming the name of the PreviewPublisher's container is previewContainer */
      #previewContainer {
          display: flex;
          flex-direction: column-reverse;
          align-items: center;
          justify-content: flex-end;
          border-color: black;
          border-radius: 5px;
          border-style: dashed;
          width: 640px;
          height: 480px;
          padding: 20px;
      }        
      /* Assuming the name of the Room's container is roomContainer */
      #roomContainer {
          width: 100vw;
          height: calc(100vh - 90px);
          background-color: #ddd;
          position: relative;
      }
      #roomContainer > .OT_publisher {
          top: 25px;
          right: 25px;
          position: absolute;
          border-radius: 10px;
      }
      #roomContainer > .OT_screenshare {
          top: 25px;
          left: 25px;
          position: absolute;
          border-radius: 10px;
      }      
    </style>
    <script src="https://cdn.tailwindcss.com"></script>
    <script>
      tailwind.config = {
        theme: {
          spacing: {
            '1': '8px',
            '2': '12px'
          }
        }
      }
    </script>
    <script>
      let audioInputs;
      let videoInputs;
      let listMic;
      let listCamera;
      let currentAudioIndex = 0;
      let currentVideoIndex = 0;
      let userName;
      let btnJoin;
      let btnLeave;

      window.onload = async () => {
        listCamera = document.getElementById('camera');
        listMic = document.getElementById('mic');
        userName = document.getElementById('userName')
        btnJoin = document.getElementById('btnJoin')
        btnLeave = document.getElementById('btnLeave')
        
        console.log(`🐞 Camera and Mic devices capture`)
        // Camera and Mic devices capture
  
        try {
          const devices = await VideoExpress.getDevices();
          audioInputs = devices.filter((device) => device.kind === 'audioInput');
          audioInputs.forEach((device, index) => {
            listMic.options.add(new Option(device.label, device.deviceId));

          });
          videoInputs = devices.filter((device) => device.kind === 'videoInput');
          videoInputs.forEach((device, index) => {
            listCamera.options.add(new Option(device.label, device.deviceId));
          });
  
            
        } catch (error) {
          console.error(`🐞 Error: ${error.message}`)        
        }
      }
      const changeMic = async () => {
        console.log(`🐞 changeMic ${listMic.value}`)
        currentAudioIndex = listMic.selectedIndex;
        if (room) room.camera.setAudioDevice(audioInputs[currentAudioIndex].deviceId)
      };

      const changeCamera = async () => {
        console.log(`🐞 changeCamera ${listCamera.value}`)
        currentVideoIndex = listCamera.selectedIndex;
        if (room) room.camera.setVideoDevice(videoInputs[currentVideoIndex].deviceId)
      };

      let room = null
      const join = async () => {
        if (!userName.value) {
          alert('お名前を登録してください。')
          return false
        }
        btnJoin.disabled = true
        try {
          const response = await fetch('/token', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({'userName': userName.value})
          })
          const json = await response.json()

          // Join room
          room = new VideoExpress.Room({
            apiKey: json.apiKey,
            sessionId: json.sessionId,
            token: json.token,
            roomContainer: 'roomContainer',
            managedLayoutOptions: {
              layoutMode: 'mobile'
            }
          });
          
          // Join room
          await room.join();
          room.camera.setAudioDevice(audioInputs[currentAudioIndex].deviceId)
          room.camera.setVideoDevice(videoInputs[currentVideoIndex].deviceId)
          btnJoin.hidden = true
          btnJoin.disabled = false
          btnLeave.hidden = false
          
          room.on('disconnected', (reason) => {
            console.log(`🐞 Disconnected. ${reason}`)
          })
        } catch(error) {
          btnJoin.hidden = true
          btnJoin.disabled = false
          btnLeave.hidden = false
          alert(error.message)
          return false
        }
      }
      const leave = async () => {
        // Leave room
        room.leave()
        room = null
        btnJoin.hidden = false
        btnJoin.disabled = false
        btnLeave.hidden = true
        document.getElementById('layoutContainerWrapper').remove()        
      }
    </script>
    <title> Vonage Video Express </title>
  </head>
  <body class="center">
    <div class="m-2">
      <label htmlFor="inline-last-name" class="p-2">お名前:
        <input id="userName" class="bg-gray-200 p-1" type="text" defaultValue="" placeholder="お名前" />
      </label>
      <button id="btnJoin" class="text-white font-bold bg-blue-400 p-2 rounded" onclick="join()">会議に参加</button>
      <button id="btnLeave" class="text-white font-bold bg-red-400 p-2 rounded" onclick="leave()" hidden>退出</button>
    </div>
    <div class="m-2">
      <label htmlFor="camera" class="p-2">カメラ:
        <select id="camera" class="bg-gray-200 p-1" onChange="changeCamera()" required>
        </select>
      </label>
      <label htmlFor="mic" class="p-2">マイク:
        <select id="mic" class="bg-gray-200 p-1" onChange="changeMic()" required>
        </select>
      </label>
    </div>
    <div id="roomContainer"></div>
  </body>
</html>

プログラムがとても長くなっていますが、このHTMLファイルをブラウザで開くことでビデオ会議ができるようになります。

デプロイ

作成したプログラムをサーバー上にデプロイします。

  • Terminalメニューの中のNew Terminalを選択します。

New Terminal

画面下部にターミナルウィンドウが開きます。

  • ターミナルウィンドウで、以下のコマンドを入力します。
npm install && vcr deploy

デプロイがスタートします。デプロイは1分程度かかります。

 ℹ️  creating new project for project "video-express"
 ✅ project "video-express" created: project_id="37ac8f3b-14b6-4c3a-9e90-d8f83b47763b"
working directory: /home/coder/project
 ℹ️  compressing directory: /home/coder/project
 ℹ️  compressed: size 8659767 byte(s) contents 5225 file(s)... done.
 ℹ️  uploading source code...
 ✅ source code uploaded
 ℹ️  creating package from source code...
 ℹ️  package id: e188c785-09ac-4695-ab55-0826ffd8e7a8
 ℹ️  package build status is 'pending' - image will begin building soon...
 ℹ️  package build status is 'building' - image is being built...
 ✅ package build status is 'completed' - image is ready to be deployed!
 ✅ instance has been deployed!
      instance id: aaac0359-d1b4-47dd-af7b-38dae597f4f7
      instance service name: neru-XXXXXXXX-video-express-dev
      instance host address: https://neru-XXXXXXXX-video-express-dev.apse1.runtime.vonage.cloud

上の例のように、instance has been deployed!が表示されればOKです。

テスト

最後に表示されたhost addressをコピーしてブラウザに貼り付けます。

以下のようなページが表示されます。

Hello World

  • お名前の左側に何か適当に文字を入力して、会議に参加ボタンを押します。初回はカメラとマイクへのアクセス許可ダイアログが表示されますので、許可をクリックします。

アクセス許可
テスト

ブラウザを複数開いて、同じURLにアクセスしてテレビ会議ができることを確認します。

まとめ

今回は、Vonage Video Express を使って、ビデオ会議のデモプログラムを作成してみました。今回は触れませんでしたが、Video Express ではバーチャル背景や、画面共有なども利用することができます。
Vonage Video Express や Video API については、従来 TokBox として提供されていたこともあって、古いドキュメントがネット上にも多数残っています。
最新の Vonage Video Express の各種ドキュメントは以下のサイトにありますので、今後何か調べる場合はこちらのドキュメントをご利用ください。

KWCPLUS

Discussion