https://s3.amazonaws.com/a.storyblok.com/f/270183/49609/eb6ec9c71a/elevate_enable-audio-1.png

既存のチャットアプリケーションに音声機能を追加する

最終更新日 May 13, 2021

所要時間:8 分

チャットで誰かに長いメッセージを書いている途中で、こう思ったことはないだろうか?もちろんあります!Nexmo Client SDKをチャットアプリケーションに使用することで、それが可能になります。

その デモアプリケーション完成例は現在GitHubで見ることができる。

前提条件

ノードとNPM

始めるには、NodeとNPMのインストールが必要です。このガイドではNode 8とNPM 6を使用します。これらがインストールされ、最新であることを確認してください。

node --version npm --version

NodeとNPMの両方が正しいバージョンでインストールされている必要があります。nodejs.orgにアクセスして、正しいバージョンをインストールしてください。

ネクスモCLI

アプリケーションをセットアップするには、Nexmo CLIをインストールする必要があります。ターミナルでNPMを使ってインストールします。

npm install -g nexmo-cli@beta

ダッシュボードにあるAPIキーとシークレットを使ってNexmo CLIを設定します。

nexmo setup

Git(オプション)

gitを使ってGitHubからデモ・アプリケーションをクローンすることができます。

gitコマンドが苦手な方もご安心ください。このガイドには、プロジェクトをZIPファイルとしてダウンロードする手順が含まれています。

この を参照してください。

デモ・アプリケーション

このアプリケーションは、何よりもまず、シンプルなチャットクライアントです。2人のユーザー(設定すればそれ以上)がログインしてチャットを始めることができます。

基本インストール

このガイドをわかりやすくするために、デモ・アプリケーションをGitHubから直接クローンしてください。

git clone https://github.com/nexmo-community/enable-audio-in-chat.git

gitコマンドに不慣れな方は、以下をご利用ください。 デモ・アプリケーションをzipファイルとしてダウンロードしをダウンロードしてローカルで解凍してください。

クローンまたは解凍したら、新しいデモ・アプリケーション・ディレクトリに移動します。

cd enable-audio-in-chat

npmの依存関係をインストールします。

npm install

では、アプリケーションを起動してください。

npm start

お気に入りのブラウザーで、デフォルトのアドレスで実行されているはずのアプリケーションを表示してみてください: http://127.0.0.1:8080.

Login boxLogin box

未設定の場合、ログインボックスが表示されます。ログインできないのは、まだ誰がログインできるかわからないからです!

覚えておいてほしい、本番アプリケーションでは、実際に安全なものを準備する必要があります。

User not found - Login BoxUser not found - Login Box

アプリケーションを停止する:ターミナルかbashで、CTRL+Cを使って実行中のプロセスを停止することができます。ページを再読み込みするたびに、すべてがサーバーから新しくリクエストされるので、変更を加えても、アプリケーションを起動したり停止したりする必要はないはずです。

超シンプルなセットアップ

デモ(今あなたが実行しているもの)には、次のステップをより簡単にするスクリプトがあります。

スクリプトの仕組みスクリプトはあなたにいくつかの入力を求め、あなたが手動で行わなければならないすべてのセットアップコマンドを実行することによって、このガイドに必要なアプリケーション、会話、ユーザーを作成します。その後、デモアプリケーションの設定を生成します。 ここでコードをチェックして、いたずらなことをしていないことを確認してください。.

わあ私のスクリプトを実行したくないのですか?手動でコンフィグファイルを生成したいんですか? コンフィグファイルを手動で作成する手順を説明します。

セットアップ・スクリプトの実行

そこで、これからのステップのためにアプリケーションを設定するために、セットアップ・スクリプトを実行する。

npm run setup-script

脚本はいくつかの質問を投げかけている。

Script QuestionsScript Questions

それが終わるころには config.jsを更新する。

const USERS = {
  luke: 'eyJhbGciOiJIkpXVCJ9.eyJpYXQiOnt9fX19.EDHi1R61yh01oeZ9DYQ',
  alex: 'eyJhbGciOi234JXVCJ9.eyJpyXQiOjt9fX19.VqLdU97Fdb2ZiOfqmoQ',
}

const CONVERSATION_ID = 'CON-da9c1a6b-c2dc-4bdd-ac03-cc041ef03502'

それで、あの脚本は何をしたんだ?

つまり、舞台裏でスクリプトは以下のステップを実行している。

  • コマンドを使ってNexmoアプリケーションを作成する。 nexmo app:createコマンドを使ってNexmoアプリケーションを作成し、そのIDを控えておく。

  • コマンドを使ってNexmoの会話を作成する。 nexmo conversation:createコマンドを使ってNexmo会話を作成し、IDを控えておく。

  • 両方のユーザーを nexmo user:createコマンドを使って両方のユーザーを作成し、IDを控えておく。

  • を使用して、両方のユーザーをNexmoの会話に追加します。 nexmo member:add.

  • 両方のユーザーがアプリケーションにアクセスするためのJWTを生成し、そのJWTを記録しておく。

  • 保持しているIDとJWTを使ってconfig.jsに設定を書き出す。

チキ・チキ・チャット・チャット

これで基本的なデモ・アプリケーションの設定は完了です!同僚とテストしているところです。

Chat textChat text

オーディオを有効にする

さぁ、これで始動です。他の人とチャットできるデモ・アプリケーションができました。次に、音声を有効にするボタンを追加して、お互いに会話できるようにします。

HTML

次のコードを index.htmlファイルの中にある

<section id="messages">
    <!-- /audio-toggle -->
    <h1>Messages</h1>
    <div id="messageFeed"></div>
    
    <textarea id="messageTextarea"></textarea>
    <br>
    <button id="send">Send</button>
  </section>

次の行を <!-- /audio-toggle -->を次のHTMLに置き換える。

<div>
      <audio id="audio">
        
      </audio>
      <div class="btn-group" data-toggle="buttons">
        <label class="btn btn-secondary">
           <span id="audioToggleText">Enable Audio</span>
        </label>
      </div>
    </div>

さて、上のコードを説明しよう。

<audio>タグが HTML 仕様に追加され、ウェブページにオーディオファイル(またはストリーム)を埋め込むことができるようになりました。このタグは、オーディオのソース(パス/URL)とバージョンを示すために使用され、異なるコンテキスト(またはブラウザ、オペレーティングシステムなど)用に複数のバージョンのオーディオを埋め込み/エンコードすることができます。

さらに、トグルとなるボタンを追加します。

この index.htmlファイルには次のようなセクションがあるはずだ。

<section id="messages">
    <div>
      <audio id="audio">
        
      </audio>
      <div class="btn-group" data-toggle="buttons">
        <label class="btn btn-secondary">
           <span id="audioToggleText">Enable Audio</span>
        </label>
      </div>
    </div>
    <h1>Messages</h1>
    <div id="messageFeed"></div>
    
    <textarea id="messageTextarea"></textarea>
    <br>
    <button id="send">Send</button>
  </section>

以上が今回のHTMLの変更点である。さて、次は何だろう?

ジャバスクリプト

次に、デモ・アプリケーションのJavaScriptを編集します。

次のコードを chat.jsファイルの中にある

constructor() {
    this.messageTextarea = document.getElementById('messageTextarea')
    this.sendButton = document.getElementById('send')
    this.loginForm = document.getElementById('login')
    this.loginButton = document.getElementById('loginButton')
    this.messages = document.getElementById('messages')
    this.messageFeed = document.getElementById('messageFeed')
    // audio-elements
    this.setupUserEvents()
  }

この行を // audio-elementsを次のJavaScriptコードに置き換える。

this.audio = document.getElementById('audio')
    this.audioToggle = document.getElementById('audioToggle')
    this.audioToggleText = document.getElementById('audioToggleText')

このコードでは3つの新しい要素を「登録」しているので、JavaScriptファイル全体でページ上の要素をより簡単に使うことができる。

同じ chat.jsファイルの中にある

// audio-toggle-event

    this.showConversationHistory(conversation)

この行を // audio-toggle-eventを次のJavaScriptコードに置き換える。

conversation.on("member:media", (member, event) => {
      console.log(`*** Member changed media state`, member, event)
      const text = `${member.user.name} <b>${event.body.audio ? 'enabled' : 'disabled'} audio in the conversation</b><br>`
      this.messageFeed.innerHTML = text + this.messageFeed.innerHTML
    })

このコードはまた、イベントのリスナーを「登録」する。 member:mediaイベントのリスナーを「登録」している。そのイベントを見ると、ブラウザに console.logをブラウザに送信します。特にデバッグに便利です。また、整形済みのテキストを作成して messageFeedを更新します。

次に chat.jsファイルの中に

})

    // audio-click-event
  }

この行を // audio-click-eventを次のJavaScriptコードに置き換える。

    this.audioToggle.addEventListener('click', () => {
      const buttonContainer = this.audioToggle.parentNode
      if (this.audioToggle.checked) {
        this.audioToggleText.innerHTML = 'Disable Audio'
        buttonContainer.classList.add('btn-danger')
        buttonContainer.classList.add('active')
        buttonContainer.classList.remove('btn-secondary')
        this.conversation.media.enable().then(stream => {
          // Older browsers may not have srcObject
          if ("srcObject" in this.audio) {
            this.audio.srcObject = stream
          } else {
            // Avoid using this in new browsers, as it is going away.
            this.audio.src = window.URL.createObjectURL(stream)
          }

          this.audio.onloadedmetadata = () => {
            this.audio.play()
          }

          this.eventLogger('member:media')()
        }).catch(this.errorLogger)
      } else {
        this.audioToggleText.innerHTML = 'Enable Audio'
        buttonContainer.classList.remove('btn-danger')
        buttonContainer.classList.remove('active')
        buttonContainer.classList.add('btn-secondary')
        this.conversation.media.disable().then(this.eventLogger('member:media')).catch(this.errorLogger)
      }
    })

このコードは大掛かりだ。そして、これもリスナーを登録する。今回は、ユーザーが先ほど追加した audioToggleボタンをクリックしたときをリスニングしています。

ユーザーがボタンをクリックし、そのボタンがすでにオンになっていれば、オフに切り替わる。オフに設定されている場合はオンに設定されます。

オンの場合、タグにオーディオストリームのURLを追加することでオーディオを有効にし、ボタンのスタイルを更新します。したがって、オフの場合は、タグからオーディオストリームのURLを削除してオーディオを無効にし、ボタンのスタイルを更新します。

ファイル全体は chat.jsファイル全体は次のようになるはずだ(長い)。

class ChatApp {
  constructor() {
    this.messageTextarea = document.getElementById('messageTextarea')
    this.sendButton = document.getElementById('send')
    this.loginForm = document.getElementById('login')
    this.loginButton = document.getElementById('loginButton')
    this.messages = document.getElementById('messages')
    this.messageFeed = document.getElementById('messageFeed')
    this.audio = document.getElementById('audio')
    this.audioToggle = document.getElementById('audioToggle')
    this.audioToggleText = document.getElementById('audioToggleText')
    this.setupUserEvents()
  }

  joinConversation(userToken) {
    new NexmoClient({ debug: false })
      .createSession(userToken)
      .then(app => {
        console.log('*** Logged into app', app)
        return app.getConversation(CONVERSATION_ID)
      })
      .then((conversation) => {
        console.log('*** Joined conversation', conversation)
        this.setupConversationEvents(conversation)
      })
      .catch(this.errorLogger)
  }

  showConversationHistory(conversation) {
    conversation
      .getEvents({ page_size: 20 })
      .then((events_page) => {
        var eventsHistory = ""

        events_page.items.forEach((value, key) => {
          if (conversation.members.get(value.from)) {
            const date = new Date(Date.parse(value.timestamp))
            switch (value.type) {
              case 'text':
                eventsHistory = `${conversation.members.get(value.from).user.name} @ ${date.toLocaleString('en-GB')}: <b>${value.body.text}</b><br>` + eventsHistory
                break;
              case 'member:joined':
                eventsHistory = `${conversation.members.get(value.from).user.name} @ ${date.toLocaleString('en-GB')}: <b>joined the conversation</b><br>` + eventsHistory
                break;
            }
          }
        })

        this.messageFeed.innerHTML = eventsHistory + this.messageFeed.innerHTML
      })
      .catch(this.errorLogger)
  }

  setupConversationEvents(conversation) {
    this.conversation = conversation
    this.messages.style.display = "block"

    // Bind to events on the conversation
    conversation.on('text', (sender, message) => {
      const date = new Date(Date.parse(message.timestamp))
      console.log('*** Message received', sender, message)
      const text = `${sender.user.name} @ ${date.toLocaleString('en-GB')}: <b>${message.body.text}</b><br>`
      this.messageFeed.innerHTML = text + this.messageFeed.innerHTML
    })

    conversation.on("member:joined", (member, event) => {
      const date = new Date(Date.parse(event.timestamp))
      console.log(`*** ${member.user.name} joined the conversation`)
      const text = `${member.user.name} @ ${date.toLocaleString('en-GB')}: <b>joined the conversation</b><br>`
      this.messageFeed.innerHTML = text + this.messageFeed.innerHTML
    })

    conversation.on("member:media", (member, event) => {
      console.log(`*** Member changed media state`, member, event)
      const text = `${member.user.name} <b>${event.body.audio ? 'enabled' : 'disabled'} audio in the conversation</b><br>`
      this.messageFeed.innerHTML = text + this.messageFeed.innerHTML
    })

    this.showConversationHistory(conversation)
  }

  errorLogger(error) {
    console.log(error)
  }

  eventLogger(event) {
    return () => {
      console.log("'%s' event was sent", event)
    }
  }

  setupUserEvents() {
    this.sendButton.addEventListener('click', () => {
      this.conversation.sendText(this.messageTextarea.value)
        .then(() => {
            this.eventLogger('text')()
            this.messageTextarea.value = ''
        })
        .catch(this.errorLogger)
    })
  
    this.loginForm.addEventListener('submit', (event) => {
      event.preventDefault()
      const userName = this.loginForm.children.username.value
      const userToken = this.authenticate(userName)
      this.loginForm.children.username.value = ''
      if (userToken) {
        this.joinConversation(userToken)
        this.loginForm.style.display = 'none'
      } else {
        alert('user not found')
      }
    })

    this.audioToggle.addEventListener('click', () => {
      const buttonContainer = this.audioToggle.parentNode
      if (this.audioToggle.checked) {
        this.audioToggleText.innerHTML = 'Disable Audio'
        buttonContainer.classList.add('btn-danger')
        buttonContainer.classList.add('active')
        buttonContainer.classList.remove('btn-secondary')
        this.conversation.media.enable().then(stream => {
          // Older browsers may not have srcObject
          if ("srcObject" in this.audio) {
            this.audio.srcObject = stream
          } else {
            // Avoid using this in new browsers, as it is going away.
            this.audio.src = window.URL.createObjectURL(stream)
          }

          this.audio.onloadedmetadata = () => {
            this.audio.play()
          }

          this.eventLogger('member:media')()
        }).catch(this.errorLogger)
      } else {
        this.audioToggleText.innerHTML = 'Enable Audio'
        buttonContainer.classList.remove('btn-danger')
        buttonContainer.classList.remove('active')
        buttonContainer.classList.add('btn-secondary')
        this.conversation.media.disable().then(this.eventLogger('member:media')).catch(this.errorLogger)
      }
    })
  }

  authenticate(username) {
    return USERS[username] || null
  }
}
new ChatApp()

すべてうまくいったと仮定して、次のように実行する。 npm startでアプリケーションを開く。 http://127.0.0.1:8080.すでに実行中であれば、ページを更新するだけで最新版が表示されるはずです。

Login boxLogin box

JavaScriptが動作しておらず、NPMを再起動しても解決しない場合は、CTRL+F5でウェブページを更新してください。

設定したテスト用認証情報を使ってログインする。

Login using credentialsLogin using credentials

これでログインが完了し、先ほどと同じようにメッセージフィードが表示され、オーディオを有効にするボタンが表示されます。音声を有効にする」をクリックしてください。

アプリケーションにマイクの使用を許可します。これはMacOS版Chromeの場合です。他のブラウザやOSでは異なる場合があります。

Allow microphoneAllow microphone

両方のユーザーがログインし、オーディオを有効にした状態で、2人のユーザー間で会話を行うことができます。

Two user convoTwo user convo

この時点で、もしあなたがコーヒーショップにいて、私のように2つのブラウザ・ウィンドウでテストしているなら、うるさいフィードバック・ループを発生させたときに、コーヒーを飲んでいる同僚から顰蹙を買うことを覚悟しておいてほしい。

これで、「オーディオを無効にする」をクリックして、マイクをオフに戻すことができます。他のユーザーには、あなたがオーディオを無効にしたことが通知されます。

Disable AudioDisable Audio

結果

ウェブユーザー間の音声コミュニケーションを可能にする必要性は常にあり、当社のClient SDKはそのための完璧なソリューションです。

ぜひ試してみてください。 コミュニティ・スラックまたは下記のコメント欄でお知らせください。

シェア:

https://a.storyblok.com/f/270183/250x250/451101b4f0/lukeoliff.png
Luke Oliffヴォネージの卒業生

フレンドリーな技術教育者、家族思い、多様性チャンピオン、たぶんちょっと議論しすぎ。元バックエンドエンジニア。JavaScript(フロントエンドまたはバックエンド)、素晴らしいVue.js、DevOps、DevSecOps、JamStackのことなら何でも。DEV.toのライター