https://d226lax1qjow5r.cloudfront.net/blog/blogposts/add-video-capabilities-to-zendesk-with-vonage-video-api/Blog_Zendesk_VideoAPI_1200x600.png

Vonage Video API で Zendesk にビデオ機能を追加

最終更新日 May 11, 2021

所要時間:1 分

このチュートリアルでは、Vonage Video API を使用して Zendesk にビデオ、画面共有、録画機能を追加し、より豊かな顧客体験を提供できるようにします。

Zendeskを使用していないあなたには関係ないと思うかもしれませんが、実は、これらの方法を適用できるチケットシステムは他にもたくさんあります。それでも納得できないのであれば、プログラムで録画を処理し、Zendesk チケットにアップロードして、両者がダウンロードできるようにする方法をお見せしましょう。

シナリオ

  • お客様は、未解決のチケットについてサポートエンジニアと話し合いたいと考えています。彼女はサポートエンジニアとのビデオ通話をリクエストします。 Discuss Live with Javierボタンを押して、サポートエンジニアとのビデオ通話を要求します。

A customer is requesting a call with the support agen

- チケットは内部コメントで更新され、サポートエンジニアはチケットの要求者がビデオセッションを希望していることを通知されます。

Agent receives a notification that the customer requests a call

  • サポートエンジニアがセッションに参加し、チケットの内容を確認します(このケースではあまり議論することはありません😂)。彼らは通話を録音することに決め、録音が停止されると、チケットのコメントの形でアップロードされ、両方の参加者がダウンロードできるようになります。

A recording of a video call between client and support engineer

これが気になった方は、ぜひフォローを。

建築

この統合のアーキテクチャーを概観するために、以下の図をご覧いただきたい:

一方では、エンドユーザが Zendesk リクエストページからサポートエンジニアとのビデオ通話をリクエストします。サーバはリクエストを処理し、エージェントの注意を引くためにチケットを更新します。もう一方では、Zendesk を使用しているエージェントが同じセッションに参加し、ライブディスカッションを行います。

前提条件

始める前に、以下のものが必要です:

  1. Node.jsをインストールし JavaScriptの基礎知識

  2. 管理者権限を持つZendeskアカウント

  3. Zendesk App Tools (ZAT)インストール済み

  4. アカウント Amazon S3アカウント

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

Zendeskエージェント

Zendeskのアプリケーションを始めるには、以下の手順に従ってください。 最初のサポートアプリを作るチュートリアルに従います。プロジェクトディレクトリに移動し、以下のコマンドを実行してください。

zat new

アプリケーションの名前など、いくつかの情報を入力するプロンプトが表示されます。 ここでは Zendesk Video App と呼ぶことにします。.また、メールアドレスや、機能には影響しないその他のパラメータも要求されます。コマンドが実行されると、アプリケーションが作成されていることがわかります。サーバー用のフォルダも作成します。最終的なプロジェクトの構成はこのようになります。

|--Application
    |-- Server
        |-- server.js
    |-- Zendesk Video App
        |-- manifest.json
        |-- Assets
          |-- iframe.html
          |-- index.css
          |-- index.jss

私たちのアプリケーションは、Zendesk インターフェースに埋め込まれたフレームで構成され、いくつかのアクションが利用可能なビデオチャットエリアがあります。ファイルを編集して iframe.htmlファイルを編集して、エージェントがチケット内で顧客とビデオ通話できるようにする簡単なボタン要素を追加しましょう。以下のコードをあなたの iframe.html:

 <!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/combine/npm/@zendeskgarden/css-bedrock@7.0.21,npm/@zendeskgarden/css-utilities@4.3.0">
  <link href="main.css" rel="stylesheet">
</head>
<body>
  

  <div id="content"></div>
  <button id="initiatesession" class="button" onclick="initializeSession()">Initiate Session</button>
  <button id="startPublishingVideoId"  class="button" onclick="startPublishingVideo()">Turn on Video </button>
  <button id="startPublishingScreenId" class="button" onclick="startPublishingScreen()">Share Screen</button>
  <button id="handleRecording" class="button" onclick="handleRecording()">Start Recording</button>
  
  <div id="videos" >
      
    <div id="publisher" ></div>
    <div id="subscriber" ></div>
 
  </div>
    
  <script id="requester-template" type="text/x-handlebars-template">

  </script>

  <script src="https://cdn.jsdelivr.net/npm/handlebars@4.3.3/dist/handlebars.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/jquery@3.4.1/dist/jquery.min.js"></script>
  <script src="https://static.zdassets.com/zendesk_app_framework_sdk/2.0/zaf_sdk.min.js"></script>
  <script src="https://static.opentok.com/v2/js/opentok.min.js"></script>
  <script src="index.js"></script>
</body>
</html>

ボタン用の基本的なCSSも追加します。

.button {
  background-color: #008CBA;;
  border: none;
  color: black;
  padding: 15px 32px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 16px;
  margin: 4px 2px;
  cursor: pointer;
  border-radius: 12px;
}

次に、ZAFクライアントをインスタンス化する main.jsファイルを編集します。ZAF クライアントは、あなたのアプリがホストの Zendesk 製品と通信するためのものです。アプリでクライアントを使用して、イベントをリッスンしたり、プロパティを取得または設定したり、アクションを呼び出したりできます。今回のケースでは、作業中のチケットの詳細に興味があります。特に、チケットIDと要求者IDです。プロミスが満たされると、このチケットのAPIキー、セッションID、トークンを取得するためにサーバにリクエストを送ることができます。すべてのセッション生成ロジックはサーバから送られます。これについては後で説明します。

$(function() {
let client = ZAFClient.init();
client.invoke('resize', { width: '100%', height: '79vh'  });
videos.style.display = 'none';

client.get(['ticket.id', 'ticket.requester.id']).then(data => {
let user_id = data['ticket.requester.id']
let  ticket_id = data['ticket.id'];

    fetch(SERVER_BASE_URL + '/room/' + user_id + "-" + ticket_id).then(res => {
    return res.json()
      }).then(res => {
        apiKey = res.apiKey;
        sessionId = res.sessionId;
        token = res.token;
      }).catch(handleError);

  });

});

これらの値を得たので、ビデオセッションを開始するタイミングをエージェントに選択させることができます。エージェントが initializeSession関数を定義します。 Initiate sessionボタンをクリックすると起動する関数を定義します。パブリッシャーコンテナの表示を block に設定し、見えるようにします(初期状態では none に設定されています)。セッションオブジェクトをインスタンス化してセッションを開始し、パブリッシャーを初期化します。

let initializeSession = () => {
  session = OT.initSession(apiKey, sessionId);

  // Create a publisher
  publisher = OT.initPublisher('publisher', {
    insertMode: 'replace',
    publishVideo: false,
  }, handleError);

  // Connect to the session
  session.connect(token, error => {
    // If the connection is successful, initialize a publisher and publish to the session
    if (error) {
      handleError(error);
    } else {
    session.publish(publisher)
    document.getElementById("initiatesession").style.display = "none"
    }
  });
}

また イベントリスナーも作成します。ここでは archiveStartedarchiveSoppedイベントを利用してアプリケーションの状態を制御します。つまり、ビデオを公開しているのか、録画している場合はオフになっているのかを知るためです。

状態に応じて、HTMLボタンに異なる値を表示します。例えば archiveStartedを受け取ったら、すでにアーカイブ/録画が開始されているので、ボタンには「Start Archive」ではなく「Stop Archive」と表示します。コードの先頭で、ステート変数 (archiving, videoscreen) を定義しました。

また、ストリームが作成されたらすぐにサブスクライブしたい。 streamCreatedイベントをリッスンする。

session.on('archiveStarted', event => {
            archiveID = event.id;
            archiving = true
            document.getElementById('handleRecording').innerHTML = 'Stop Archive';
            console.log('ARCHIVE STARTED ' + archiveID);
  });  

session.on('archiveStopped',  event => {
            archiveID = event.id;
            archiving = false
            document.getElementById('handleRecording').innerHTML = 'Start Archive';
            console.log('ARCHIVE STOPED ' + archiveID);
  });  

session.on("streamPropertyChanged", event => {
             video = event.newValue
             video ? document.getElementById("startPublishingVideoId").innerHTML = 'Turn Video off' : document.getElementById("startPublishingVideoId").innerHTML = 'Turn on Video';
            });

  session.on('streamCreated', event => {
    console.log('stream created' + event.stream)
    session.subscribe(event.stream, 'subscriber', {
      insertMode: 'append',

    }, handleError);
  });

コールバックとして渡している handleError関数は、セッションのイベントをリスニングしている間にエラーが発生した場合にアラートを投げる関数です。

let handleError = (error) => {
  if (error) {
    alert(error.message);
  }
}

私たちは handleRecording関数を作ることができる。これにより、状態に応じて異なる関数をトリガーできるようになる。

let handleRecording = () => {
  archiving ? stopArchive() : startArchive();
}

この StartArchive関数はサーバーの archive/startルートにPOSTリクエストを行います。サーバーがどのセッションが録画をトリガーしているかを知るために sessionIdを渡す必要があります。チュートリアルの後半で、セッションの記録と保存について説明します。混同しないでください。これは同じコンセプトですが、内部的には "archive "という用語を使っています :)

let startArchive = () => {
  console.log('start');
  fetch(SERVER_BASE_URL +'/archive/start', {
    method: 'post',
    headers: {
      'Content-type': 'application/json'
    },
    body: JSON.stringify({
      'sessionId': sessionId
    })
  })
  .then((response) => {
    return response.json();
  })
  .then((data) => {
    console.log('data from server when starting archiving', data)
  })
  .catch(error => console.log('errror starting archive', error))
}

関数については StopArchive関数に関しては StartArchive.とほとんど同じだが、この場合、.NETから来る archiveIDを渡す必要がある。 archiveStartedイベントを渡す必要がある。

let stopArchive = () => {
  console.log('archiveID' + archiveID);
  fetch(SERVER_BASE_URL + '/archive/' + archiveID + '/stop', {
    method: 'post',
    headers: {
      'Content-type': 'application/json'
    }
  })
  .then((response) => {
    return response.json()
  })
  .then((data) => {
    console.log('data from server when stopping archiving', data)
  })
  .catch(error => console.log('errror stopping archive', error))
}

次に、画面共有ストリームのサポートを追加する必要があります。すでに画面共有されているかどうかをチェックし、されていない場合は新しいパブリッシャーを作成する関数を作成します。この関数は画面共有ストリームのトグラーとして動作し、いくつかのイベントと連動します。

メソッドを呼び出して、ブラウザが画面共有をサポートしているかどうかをチェックします。 OT.checkScreenSharingCapabilityメソッドを呼び出すことで確認します。画面共有サポートについては checkScreenSharingCapabilityコールバックについてのドキュメントを参照してください。.古いバージョンのブラウザの場合、拡張機能をインストールする必要があるかもしれませんが、ここでは簡単のため、参加者全員が最近のブラウザを使用していると仮定します。

このケースで listen しているイベントはセッションオブジェクトではなく、パブリッシャーオブジェクトによって ディスパッチされていることに注意してください。この場合は ストリームイベントを参照してください。

const startPublishingScreen = () => {
  if (screenSharing === true) {
    session.unpublish(screenPublisher)
  } else {
    OT.checkScreenSharingCapability(response => {
      if (!response.supported || response.extensionRegistered === false) {
        alert('Screen share is not supported in this browser')
      } else {
        screenPublisher = OT.initPublisher('screen', {
          videoSource: 'screen'
        }, error => {
          if (error) {
            console.log(error)
          } else {
            session.publish(screenPublisher, handleError)
              .on("streamCreated", event => {
                if (event.stream.videoType === 'screen') {
                  screenSharing = true;
                  document.getElementById("startPublishingScreenId").innerHTML = 'stop screenShare'
                }
              })
              .on("streamDestroyed", event => {
                if (event.stream.videoType === 'screen') {
                  screenSharing = false
                  document.getElementById("startPublishingScreenId").innerHTML = 'start screenShare'
                }
              })
          }
        })
      }

    })
  }
}

顧客側

さて、エージェント側を稼働させたので、顧客側に Video 機能を追加することを考える必要があります。この投稿の主な目的は、エンドカスタマー(チケット要求者)とサポートエージェント(チケット譲受人)を接続することです。

そのためには、以下の手順に従います。 ヘルプセンターテーマのカスタマイズガイドに従って、チケット要求者のページコードにアクセスし、ヘルプセンターでより豊かな顧客体験を構築できるようにします。

をカスタマイズすることに興味があります。 Requests pageつまり、特定のユーザーに割り当てられたリクエストやチケットのリストです。上記のリンク先の記事で説明されているように、ヘルプセンターの HTML は編集可能なテンプレートに含まれています。これから requests_page.hbsファイルを編集します。コードは main.jsファイルにあるJavaScriptコードとよく似ています。

まずはOpentokライブラリをインポートします。これで最新版のJS SDKがダウンロードされます。

<script src="https://static.opentok.com/v2/js/opentok.min.js"></script>

パブリッシャーと購読者のビデオと、アプリケーションの機能を処理するボタンを含む基本的なマークアップを追加します。お気づきでしょうが {{assignee.avatar_url}}.と呼ばれるテンプレート言語です。 Curlybarsと呼ばれるテンプレート言語です。

この例では、ビデオ通話を開始するボタンにチケットの担当者の写真を表示しています。目的は、顧客に近い体験を提供することです。また、最初はシンプルにするために、通話を開始するボタン以外のすべてのボタンを非表示にします。HTML要素のdisplayプロパティを none.

<div>
<button class="button" onclick="initializeSession()" style="position:relative"> 
  <img src={{assignee.avatar_url}} />
  <span class="tooltiptext">Discuss live with {{assignee.name}}</span>
</button>
</div>
    
    <button id="startPublishingVideoId" class="button" onclick="toggleVideo()" style="display:none">Turn Video off</button>
    
    <button id="handleRecording" class="button" onclick="handleRecording()" style="display:none>Start video recording</button>
    
    <button id="startPublishingScreenId" class="button" onclick="startPublishingScreen()" style="display:none">Share your screen</button>

<div id="videos">
    <div id="publisher"></div>
    <div id="subscriber"></div>
</div>

コード全体で使用する変数を定義します。エージェント側で行ったように、いくつかのステート変数 (video, archivingscreenSharing).また、サーバのエンドポイントも定義します。

let sessionId;
let publisher;
let archiveId;
let screenSharing = false;
let archiving = false;
let video = true;
const SERVER_BASE_URL = 'SERVER_BASE_URL';

ここでは、エラー発生時にユーザーに警告を発するための、シンプルなエラー・ハンドラ関数を定義します。これを別の関数として定義する唯一の目的は、コードを少しすっきりさせることです。

const handleError = (error) => {
  if (error) {
    alert(error.message);
  }
}

私たちはフェッチしている apiKey, sessionIdそして tokenをサーバーから取得しています。

fetch(SERVER_BASE_URL + '/room/' + {{request.requester.id}} + '-' +{{request.id}}).then(res => {
  return res.json()
}).then(res => {
  apiKey = res.apiKey;
  sessionId = res.sessionId;
  token = res.token;
}).catch(handleError);

次に、次の initializeSession関数を追加します。顧客がサポートエージェントとのビデオ通話をリクエストすると、この関数がトリガーされます。最初は非表示だったボタンを表示し、セッションオブジェクトをインスタンス化してパブリッシャーを作成します。最後に、セッションへの接続を試みます。接続に成功したら、先に説明したように、セッションへのパブリッシュを試みます。

const initializeSession = () => {
  document.getElementById('startPublishingVideoId').style.display = "block";
  document.getElementById('handleRecording').style.display = "block";
  document.getElementById('startPublishingScreenId').style.display = "block";
  videos.style.display = 'block';
  session = OT.initSession(apiKey, sessionId);

  publisher = OT.initPublisher('publisher', {
    insertMode: 'append',
    width: '100%',
    height: '100%',
  }, handleError);

  session.connect(token, error => {

    if (error) {
      handleError(error);
    } else {
      session.publish(publisher, handleError);
    }
  });

  session.on('streamCreated', (event) => {
    session.subscribe(event.stream, 'subscriber', {
      insertMode: 'append',
      width: '100%',
      height: '100%'
    }, handleError);
  });

  session.on('archiveStarted', event => {
    archiveID = event.id;
    archiving = true
    document.getElementById('handleRecording').innerHTML = 'Stop Archive';
    console.log('ARCHIVE STARTED ' + archiveID);
  });

  session.on('archiveStopped', event => {
    archiveID = event.id;
    archiving = false
    document.getElementById('handleRecording').innerHTML = 'Start Archive';
    console.log('ARCHIVE STOPED ' + archiveID);
  });

  session.on("streamPropertyChanged", event => {
    console.log(event.newValue)
    video = event.newValue
    video ? document.getElementById("startPublishingVideoId").innerHTML = 'Turn Video off' : document.getElementById("startPublishingVideoId").innerHTML = 'Turn Video on';
  });

  session.on('streamCreated', event => {
    session.subscribe(event.stream, 'subscriber', {
      insertMode: 'append',
    }, handleError);
  });
}

三項演算子を活用して、Videoをオンにするかオフにするかを決定する。同じロジックが、録画を開始するために関数を呼び出すのか、停止するために関数を呼び出すのかを決定するためにも適用される。

const toggleVideo = () => {
video ? publisher.publishVideo(false) : publisher.publishVideo(true)
}

const handleRecording = () => {
  archiving ? stopArchive() : startArchive();
}

その startArchive()startArchive()関数は main.jsと同じに見えるので、簡単にするために省略します。また、録音を開始するオプションをサポートエージェントにだけ与え、エンドカスタマーには与えないこともできますが、これは完全にあなた次第です。より楽しくするために、録音の開始と停止の両方を許可することにします。

サーバー

サーバサイドは、エージェントまたはサポートエンジニアからのリクエストを処理するために、いくつかのルートで構成されます。

アプリケーションで使うモジュールをインポートして、環境変数を定義しよう。

apiKeyそして apiSecretは Video API の認証情報です。 ダッシュボードです。 remoteUriは、https://xxxxxx.zendesk.com/ という形式で Zendesk のエンドポイントを参照します。Zendesk 認証については、Zendesk の "API リクエストを認証するには"の記事をご覧ください。さまざまな認証方法がサポートされています。

AWSでの認証に関しては、いくつかの方法がある。 サポートされている方法がありますが、今回は環境変数を利用することにしました。この場合、SDKは環境変数として設定されたAWS認証情報を自動的に検出し、SDKリクエストに使用するため、アプリケーションで認証情報を管理する必要がなくなります。そのため、アプリケーションでクレデンシャルを管理する必要がありません。 .envファイルから変数を読み込まないのはそのためです。

const fs = require('fs');
const bodyParser = require('body-parser')
const express = require('express');
const path = require('path');
const app = express();
const _ = require('lodash');
const request = require ('request')
const ZD = require('node-zendesk');
const cors = require('cors');
const dotenv = require('dotenv')

dotenv.config();

const apiKey = process.env.apiKey
const  apiSecret = process.env.apiSecret
const AWS = require('aws-sdk');
const remoteUri = process.env.remoteUri

const client = ZD.createClient({
  username:  process.env.username,
  token:     process.env.token,
  remoteUri: process.env.remoteUri
});

const OpenTok = require('opentok');
const opentok = new OpenTok(apiKey, apiSecret);
app.use(cors());
app.use(bodyParser.json()); 
app.use(bodyParser.urlencoded({
  extended: true
}));
let ticketId
const app = express()
init()

これを index.jsファイルに追加します。

const init = () => {
app.listen(8080,  () => {
console.log('You\'re app is now ready at http://localhost:8080/');
}

セッションとトークンの作成を処理するルートは、このチケットについて議論するセッションがすでに作成されているかどうかをチェックし、もしなければ作成します。トークンの概念に馴染みがない方のために説明します。 トークンVideo API のトークンの概念をご存知ない方のために説明すると、トークンは部屋 (セッション) の鍵のようなものです。

もっとセキュアなソリューションが欲しいところだが、ここではシンプルにするために基本的なバリデーションを行うことにした。この場合 nameパラメータを受け取っています。 XXXXXX-YYYYY.両方のパート(AgentとCustomer)で行ったフェッチコールを覚えていますか?そこから来ています。

チケットのリクエスタIDが、受け取ったパラメータの2番目の部分と一致する場合にのみ、セッションとトークンを生成します。 :nameパラメータと一致した場合にのみセッションとトークンを生成します。Zendesk パッケージを使用して検証を行います。たとえば 1222-1234を受け取った場合は、 Zendesk API を使ってチケット 1234 がユーザ 1222 からのリクエストであるかどうかを調べます。もしそうでなければ、HTTP 404 を返します。

また、referer とリクエストの発信元に関するバリデーションがあることもわかります。これはリクエストが顧客から来た場合のみチケットを更新し、チケットのリクエスト者が Video セッションを希望していることをサポートエンジニアに知らせるために行われた簡単なハックです。

app.get('/room/:name', (req, res) => {
  if (!req.params.name) {
    res.status(402).end()
  }
  let roomName = req.params.name;
  let sessionId;
  let requesterId = roomName.split("-")[0]
  ticketId = roomName.split("-")[1]

  checkIfValid(ticketId, req).then(response => {

      if (response && response.toString() === requesterId) {

        if (req.headers.origin === endpoint && req.headers.referer.split("/")[3] === "hc") {
          updateTicket(ticketId)
        }

        if (roomToSessionIdDictionary[roomName]) {
          sessionId = roomToSessionIdDictionary[roomName];
          token = opentok.generateToken(sessionId);
          res.setHeader('Content-Type', 'application/json');
          res.send({
            apiKey: apiKey,
            sessionId: sessionId,
            token: token
          });
        } else {
          giveMeSession().then(session => {
              roomToSessionIdDictionary[roomName] = session.sessionId;
              token = opentok.generateToken(session.sessionId);
              res.setHeader('Content-Type', 'application/json');
              res.send({
                apiKey: apiKey,
                sessionId: session.sessionId,
                token: token
              });

            })
            .catch(e => res.status(500).send({
              error: 'createSession error:' + e
            }))
        }
      } else {
        res.status(404).end()
      }
    })
    .catch((e) => {
      res.status(404).end()
    })

})

実際のアプリケーションでは、おそらくセッションIDをデータベースに保存し、このチケットに対してセッ ションがすでに作成されているかどうかをチェックする必要があるでしょう。しかし、このチュートリアルでは、部屋名に関連付けられたセッションIDを格納する辞書を単純に使用することにしました。サーバーを再起動すると、この辞書はリセットされます。

let roomToSessionIdDictionary = {};

// returns the room name, given a session ID that was associated with it
const findRoomFromSessionId = sessionId => {
  return _.findKey(roomToSessionIdDictionary,  value => { return value === sessionId; });
}

前述のように、受信した部屋名に関連するセッションがない場合にのみ、セッションを作成します。コールバックベースのメソッドをプロミスでラップし、セッションオブジェクトを返します。

const giveMeSession = ()=>{
  return new Promise((resolve, reject) => {
        opentok.createSession({ mediaMode: 'routed' }, (err, session) => {
          if (err) {
            console.log('[Opentok - createRoutedSession] - Err', err);
            reject(err);
          }
          resolve(session);
        });
      })
    }

また、Zendeskチェックのプロミスにラップして、受け取ったチケットIDを問い合わせることで、リクエストが正当かどうかを判断できるようにしています。

const checkIfValid = (ticketId, res) => {
  return new Promise(
    (resolve, reject) => {
      client.tickets.show(ticketId, function(err, request, result){
        if (err) reject(err);
        resolve(result.requester_id);

      })
   }
 );
};

リクエストが有効で、(エージェントからではなく)顧客側から来た場合、ビデオセッションを待っている人がいることをサポートエンジニアに通知するようにチケットを更新します。

const updateTicket = (ticketId) => {
let notification  = 'The requester of the ticket would like to talk to you.'
 client.tickets.update(ticketId, {"ticket":{comment:{"body": notification, "public": false}}}, (err, req, res) => {
  if(!err){console.log('Ticket updated')                  
  }}
)}

アーカイブを開始するルートと停止するルートを定義しています。アーカイブを停止するルートはセッションIDも取ることに注意してください。これは、あなたがどのセッションIDで録画を停止しようとしているかをサーバが知るためです。

app.post('/archive/start',  (req, res) => {
  var json = req.body;
  var sessionId = json.sessionId;
  opentok.startArchive(sessionId, { name: 'testSession' },  (err, archive) => {
    if (err) {
      console.error(err);
      res.status(500).send({ error: 'startArchive error:' + err });
      return;
    }
    res.setHeader('Content-Type', 'application/json');
    res.send(archive);
  });
});

app.post('/archive/:archiveId/stop',  (req, res) => {
  opentok.stopArchive(archiveId, function (err, archive) {
    if (err) {
      console.error('error in stopArchive');
      console.error(err);
      res.status(500).send({ error: 'stopArchive error:' + err });
      return;
    }
    res.setHeader('Content-Type', 'application/json');
    res.send(archive);
  });
});

サーバーを実行する場合は で公開する。で公開し、ngrok URL を SERVER_BASE_URLとして設定します。これでビデオセッションができました!

さて、それはクールでしたが、もう一歩進んでみましょう!通話録音を動的に処理し、Zendeskにアップロードして、サポートエンジニアと顧客の両方が都合の良い時に取り出せるようにできたら素晴らしいと思いませんか?そうしましょう!

Excited

録画の取り扱い

まず、Video APIに録画したビデオをアップロードする場所を知らせる必要がある。AWS S3 エンドポイントを使用するため、以下の手順に従ってください。 Vonage Video API アーカイブで S3 ストレージを使用するガイドに従ってください。一度設定すると、Video セッションがあり、録画を開始したり停止したりすると、自動的に S3 バケットにアップロードされます。

すべてのアーカイブは OpenTok API キーを名前とする S3 バケットのサブディレクトリに保存され、各アーカイブはそのサブディレクトリにアーカイブ ID を名前として保存されます。アーカイブファイルは archive.mp4.

例えば、以下のようなAPIキーとIDを持つアーカイブを考えてみよう:

  • APIキー -- 123456

  • アーカイブID -- ab0baa3d-2539-43a6-be42-b41ff1488af3

このアーカイブのファイルは、あなたのS3バケットの以下のディレクトリにアップロードされます:

123456/ab0baa3d-2539-43a6-be42-b41ff1488af3/archive.mp4

次に、アーカイブがいつS3バケットにアップロードされたかを知る必要がある。アーカイブ関連のイベントをリッスンするルートをサーバーに設定します。Video API プラットフォームは、アーカイブのステータスが変更されると、事前に設定したコールバック URL に Webhook を送信します。

ダッシュボードに行き、使用しているプロジェクトをクリックし、サーバーのURLを https://YOUR_SERVER_URL/events.に設定します。 アーカイブ ガイドVideo API プラットフォームは、アーカイブが S3 バケットからダウンロード可能になると、available ステータスを送信します。私たちのサーバーでそのイベントをリッスンし、ダウンロードします。ロジックはすべてサーバーサイド (server.jsファイル)で処理されます。

app.post('/events',  (req, res) => {
  res.send('OK')
  if(req.body.status === 'uploaded'){
  let key = apiKey + "/" + req.body.id + "/archive.mp4"
  downloadVideo(req.body.id + ".mp4", key)
  }
})

Video API アカウントでサーバー URL を設定することを忘れないでください。そうしないと、サーバーでこれらの Webhook を受信できません。以下のようにします:

Callback URL

関数には2つの変数を渡します。 downloadVideo1つはアーカイブをダウンロードさせたい名前、もう1つはキーで、S3バケットに取得しようとしている録画がわかるようにします。

リクエストの createReadStreamメソッドを呼び出します。呼び出すと createReadStreamは、リクエストによって管理される生の HTTP ストリームを返します。生のデータ・ストリームは、Node.js Stream オブジェクトにパイプされます。これで、バケットにアップロードされた録画を動的にダウンロードできるようになります。

const downloadVideo = (name, key) => {
  var fileStream = fs.createWriteStream(name);
  s3 = new AWS.S3();
  var s3Stream = s3.getObject({Bucket: process.env.BucketName, Key: key}).createReadStream();
  s3Stream.on('error', (err) => {
  console.error(err);
  });

  s3Stream.pipe(fileStream).on('error', (err) => {
      // capture any errors that occur when writing data to the file
      console.error('File Stream:', err);
  }).on('close', () => {
      console.log('Done.');
      getToken(name)
  });
}

ファイルのダウンロードが終わったら getToken関数を呼び出していることにお気づきでしょう。これは Zendesk にファイルをアップロードするプロセスによるものです。ファイルはすでにダウンロードされているので、この時点で好きなことを行うことができます。しかし、投稿を完了させるために、Zendesk チケットに録音をアップロードしましょう。

まずトークンを取得する必要があり、次にこのトークンを渡してチケットを更新する必要があります。2番目の部分は uploadVideo.

const getToken = (archiveName) => {
  client.attachments.upload(__dirname + '/' + archiveName , {binary: false, filename: archiveName}, (err, req, result) => {
    if (err) {
      console.log("error:", err);
    }
    console.log("token:", result.upload.token);
    uploadVideo(result.upload.token, ticketId)
  })
}
const uploadVideo = (token, ticketId) =>{
  let ticket = {
  "ticket":{"comment": { "body": "This is the recording of the call", "public": true, "uploads":[token]},
  }};
  client.tickets.update(ticketId,ticket, (err, req, res) => {
    if(!err){
      console.log('ticket updated with the video recording')
    }
  })
}

デモ デモをご覧ください。このチュートリアルをあなたのニーズに合わせてアレンジし、あなたの顧客を大いに満足させ、サポート体験の真の支持者にしましょう。

このプロジェクトのコードは vonage-zendesk-integrationGitHub リポジトリにあります。

次は何を作りますか?教えてください!

シェア:

https://a.storyblok.com/f/270183/384x384/6007824739/javier-molina-sanz.png
Javier Molina Sanz

Javier studied Industrial Engineering back in Madrid where he's from. He is now one of our Solution Engineers, so if you get into trouble using our APIs he may be the one that gives you a hand. Out of work he loves playing football and travelling as much as he can.