🐨

Azure Communication Serviceを使ってHololens2とPCで通話できるようにする

2024/02/03に公開

やりたいこと

Hololens2装着者とブラウザを開いているPC作業者間で通話ができるようにしたいです。
Azure Communication Serviceを使って実現したので紹介します。

やらないこと

Hololens2上では相手側(PC作業者)の映像は表示させません。
現在調査中ですが、技術的に難しそうなので別の記事に書きます。

PC側のWebアプリはAzure Communication UI Libraryというのがあるので、そこから適当に作成しています。

https://azure.github.io/communication-ui-library/?path=/story/overview--page

Azure Communication Serviceとは?

Microsoftが提供する、Azure上で動くフルマネージドなコミュニケーションプラットフォームです。
テキストチャット、音声通話、ビデオ通話、SMS メッセージング、およびその他の通信機能を提供しています。
高いスケーラビリティと信頼性を持ちながら、独自のブランディングとカスタマイズ可能なユーザーエクスペリエンスを提供することができ、
インフラストラクチャのセットアップや管理に時間を費やすことなく、コミュニケーション機能に集中できる。Azure の強力なセキュリティ機能と合わせて、エンドツーエンドのソリューションを構築することが可能です。

https://learn.microsoft.com/ja-jp/azure/communication-services/overview

価格

1ユーザー1分あたり0.596円とのこと。

https://azure.microsoft.com/ja-jp/pricing/details/communication-services/

リソース作成

「communication」と検索すると出てくるかなと思います。



あとはサブスクリプション・リソースグループを選択して、
適当なリソース名とリージョンを選択すれば作成できます。

事前準備

1. プロジェクト作成

Unity2022.3.15LTSの3Dアプリで作成します。
※バージョンは比較的新しいLTSであれば問題ないです。


2. 必要なパッケージの導入

Mixed Reality Feature Toolからインストールします。

https://learn.microsoft.com/ja-jp/windows/mixed-reality/develop/unity/welcome-to-mr-feature-tool


Azure Communication Services Calling Unity SDKを導入します。

また、MRTK2も導入するので、
・Mixed Reality Toolkit Foundation
・Mixed Reality OpenXR Plugin
の二つも導入します。


3. ボタンとステートを表示するテキストを用意

適当にボタンとテキストを配置する。


実装

通話といっても方法は何種類かあります。
・こちらから話したい人にコールする
・向こう側からコールされるのを待機する
・グループ通話
・ルーム通話

今回は諸事情あって、グループ通話を使います。


処理の流れ

  1. アプリ起動時にACSの認証処理を走らせる
  2. グループ通話ボタンをクリックすることで、指定のGroup IDに参加する
  3. PC(Web)側も画面描画時にACSの認証処理をして、指定のGroupIDに参加


先にソースコード全体

ACSManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.ComTypes;

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;
using UnityEngine.WSA;
using TMPro;

using Azure.Communication.Calling.UnityClient;


public class ACSManager : MonoBehaviour
{
    private Call call;
    private CallClient callClient;
    private CallAgent callAgent;
    private DeviceManager deviceManager;
    private LocalOutgoingAudioStream micStream;
    private LocalVideoStream cameraStream;
    public string CalleeIdentity { get; set; }

    public TMP_Text callStatus;
    public static ACSManager Instance;

    private void Awake()
    {
        callClient = new CallClient();
        InitCallAgentAndDeviceManagerAsync();
    }

    private void Update()
    {       
        if (call != null)
        {
            switch (call.State)
            {
                case CallState.Connected:
                    if (callStatus.text != "ACS: Connected")
                    {
                        callStatus.text = "ACS: Connected";                
                    }
                       
                    break;
                case CallState.Disconnected:
                    if (callStatus.text != "ACS: Disconnected")
                        callStatus.text = "ACS: Disconnected";
                    break;
            }
        }
    }

    void OnDestroy()
    {
        HandleClickHangUp();
        callClient.Dispose();
#if UNITY_EDITOR
        UnityEditor.EditorApplication.isPlaying = false;
#endif
    }

    public async void HandleClickGroupCall()
    {
        var videoDeviceInfo = deviceManager.Cameras[0];
        var _localVideoStream = new LocalVideoStream[1];
        _localVideoStream[0] = new LocalVideoStream(videoDeviceInfo);

        var groupCallLocator = new GroupCallLocator(Guid.Parse(<GUID形式のGroup ID>));

        var joinCallOptions = new JoinCallOptions();
        joinCallOptions = new JoinCallOptions()
        {
            OutgoingVideoOptions = new OutgoingVideoOptions() { Streams = _localVideoStream }
        };
        call = await callAgent.JoinAsync(groupCallLocator, joinCallOptions);

        // call.RemoteParticipantsUpdated += OnRemoteParticipantsUpdatedAsync;
        call.StateChanged += OnStateChangedAsync;
    }

  
    public async void HandleClickHangUp()
    {
        if (call != null)
        {
            try
            {         
                await call.HangUpAsync(new HangUpOptions() { ForEveryone = false });
            }
            catch (Exception ex)
            {
                Debug.Log(ex.Message);
            }
        }
    }

    private async void OnStateChangedAsync(object sender, PropertyChangedEventArgs args)
    {
        var call = sender as Call;   
        if (call != null)
        {
            var state = call.State;
            switch (state)
            {
                case CallState.Connected:
                    {
                        await call.StartAudioAsync(micStream);
                        break;
                    }
                case CallState.Disconnected:
                    {
                        call.StateChanged -= OnStateChangedAsync;
                        call.Dispose();
                        break;
                    }
                default: break;
            }
        }
    }

    private async void InitCallAgentAndDeviceManagerAsync()
    {
        deviceManager = await callClient.GetDeviceManager();

        var tokenCredential = new CallTokenCredential(<your token>);
        var callAgentOptions = new CallAgentOptions()
        {
            DisplayName = "3DApp",
        };

        callAgent = await callClient.CreateCallAgent(tokenCredential, callAgentOptions);
        // 今回は必要ないのでコメントアウト
        // callAgent.IncomingCallReceived += OnIncomingCallAsync;
    }
}


1. callClintとcallAgentの初期化

ACSを使うためにはユーザーIDに紐づくTokenの認証が必要です。
リソース内の「IDおよびリソースアクセストークン」から作成できるので、作成してください。


Awake関数内で初期化を行います。
ここでHololens2のカメラ情報と音声情報にアクセスしても問題ないかの確認処理をします。
アプリ上にも確認モーダルが出てきます。

IncomingCallReceivedは電話がかかってきた時に実行されるイベントハンドラーですが、今回はこちらからしか電話をかけない想定なので、コメントアウトにしています。

ACSManager.cs
    private Call call;
    private CallClient callClient;
    private CallAgent callAgent;
    private DeviceManager deviceManager;
    private LocalOutgoingAudioStream micStream;
    private LocalVideoStream cameraStream;
    public string CalleeIdentity { get; set; }

    public TMP_Text callStatus;
    public static ACSManager Instance;

    private void Awake()
    {
        callClient = new CallClient();

        InitCallAgentAndDeviceManagerAsync();
    }
    
    private async void InitCallAgentAndDeviceManagerAsync()
    {
        deviceManager = await callClient.GetDeviceManager();

        var tokenCredential = new CallTokenCredential(<your token>);
        var callAgentOptions = new CallAgentOptions()
        {
	       // グループ内のユーザーの識別に使用
            DisplayName = "3DApp",
        };

        callAgent = await callClient.CreateCallAgent(tokenCredential, callAgentOptions);
        // 今回は必要ないのでコメントアウト
        // callAgent.IncomingCallReceived += OnIncomingCallAsync;
    }


2. グループ通話を開始する処理

グループ通話に参加する前に、こちらのカメラ情報を取得して、相手側に送るように設定します。
StateChangedで通話に「参加しようとしている」「参加中」「退出」などの状態を取得することができます。
RemoteParticipantsUpdatedは今回必要ないのでコメントアウトにします。

ACSManager.cs
    public async void HandleClickGroupCall()
    {
        // Hololens2のカメラ情報を取得
        var videoDeviceInfo = deviceManager.Cameras[0];
        var _localVideoStream = new LocalVideoStream[1];
        _localVideoStream[0] = new LocalVideoStream(videoDeviceInfo);

        // グループ通話に使うGUID形式の文字列を用意
        var groupCallLocator = new GroupCallLocator(Guid.Parse(<GUID形式のGroup ID>));

        var joinCallOptions = new JoinCallOptions();
        joinCallOptions = new JoinCallOptions()
        {
	    // 相手側にこちらのカメラ映像を送る
            OutgoingVideoOptions = new OutgoingVideoOptions() { Streams = _localVideoStream }
        };
        call = await callAgent.JoinAsync(groupCallLocator, joinCallOptions);

        // call.RemoteParticipantsUpdated += OnRemoteParticipantsUpdatedAsync;
        call.StateChanged += OnStateChangedAsync;
    }


3. Stateを監視する

通話状態が変わるとこちらの関数がイベントハンドラーとして実行されます。

ACSManager.cs
    private async void OnStateChangedAsync(object sender, PropertyChangedEventArgs args)
    {
        var call = sender as Call;   
        if (call != null)
        {
            var state = call.State;
            switch (state)
            {
                case CallState.Connected:
                    {
                        await call.StartAudioAsync(micStream);
                        break;
                    }
                case CallState.Disconnected:
                    {
                        // call.RemoteParticipantsUpdated -= OnRemoteParticipantsUpdatedAsync;
                        call.StateChanged -= OnStateChangedAsync;
                        call.Dispose();
                        break;
                    }
                default: break;
            }
        }
}


4. 通話を終了する処理

HangUpAsyncメソッドで通話から退出することができます。

ACSManager.cs
    public async void HandleClickHangUp()
    {
        if (call != null)
        {
            try
            {         
                await call.HangUpAsync(new HangUpOptions() { ForEveryone = false });
            }
            catch (Exception ex)
            {
                Debug.Log(ex.Message);
            }
        }
    }


5. UI上に接続状態を表示

ここはなくてもいいです。

ACSManager.cs
    private void Update()
    {       
        if (call != null)
        {
            switch (call.State)
            {
                case CallState.Connected:
                    if (callStatus.text != "ACS: Connected")
                    {
                        callStatus.text = "ACS: Connected";     
                    }
                       
                    break;
                case CallState.Disconnected:
                    if (callStatus.text != "ACS: Disconnected")
                        callStatus.text = "ACS: Disconnected";
                    break;
            }
        }
    }

検証

Hololens2とPC(Web)で通話ができました。
GIFなので映像しか見れませんが、音声も疎通できてます。

今回はWebアプリ上と通話をしました、Teamsの会議にもHololensで参加することもできます。

ヘッドウォータース

Discussion