はつねの日記

Kinect, Windows 10 UWP, Windows Azure, IoT, 電子工作

Azure Communication Servicesをクライアントから使おう

azure.microsoft.com
Microsoft Ignite 2020でプレビュー公開が発表されたAzure Commucication ServicesをWPFアプリ (.NET Core) から使う方法を色々調べています。

Azure Communication Servicesとは

Azure Communication Servicesは、ビデオ、音声、チャット、テキストメッセージングなどで端末間のコミュニケーションを実現するAzure上のサービスです。

Microsoft Teamsと同じ信頼性とセキュリティが考慮されたインフラストラクチャーが採用されていて、クラウド側は特に何もコードを書かずに、アプリにTeamsのコミュニケーション機能のような機能を追加できます。

2020年10月からは、SMSでの通信や電話番号を取得して電話受発信が可能になる予定ですが、現時点でも、チャット(Teamsのチャットを思い出してください)と音声およびビデオ通話(Teamsのビデオ会議を思い出してください)がプレビュー提供されています。

まずは、チャットを試してみる

クラウド側を設定する

Azure Communication Servicesを利用する場合、Azure Portalで「Communication Services」を作成します。
f:id:hatsune_a:20200928231054p:plain
サブスクリプション、リソースグループを指定して、リソース名をつけるだけでサクッと作成できます。
f:id:hatsune_a:20200928231242p:plain
残念ながら、ロケーション(場所)はまだ米国しか選択できませんが、GAされるときには日本リージョンも指定できることを期待したいです。
作成が完了すると、
リソース名.communication.azure.com
という「エンドポイント」とそこにアクセスするための「キー」が生成されます。
アプリからは、エンドポイント+キーでAzure Communication Servicesに接続します。

クライアント側を作成する

クラウド側の準備ができたので、クライアントアプリを作成しましょう。サンプルアプリやサンプルコードなどは、WEBアプリの例ばかりでWindowsアプリだけでどこまでできるのか未知数ですが、やれるところまでやってみましょう。

WPFアプリ(.NET Core)で新規プロジェクタと作成

Visual Studio 2019を起動して、WPFアプリ (.NET Core) のテンプレートから新規アプリを作成します。.NET Coreは3.1がよいでしょう。

.NET Frameworkではなく、.NET Coreにしているのは、await foreachというC# 8.0の非同期ストリームを使いたいというのが大きな理由です。なぜ、使いたいかは後述します。

C# 8.0は、.NET Standard 2.1に対応なので、.NET Frameworkだと.NET Standard 2.1の実装バージョンが存在しないし、.NET Coreも3.0以上である必要があります。

nugetしようぜ!

新規プロジェクトが作成できたら、nugetでSDKを追加します。
f:id:hatsune_a:20200928233503p:plain
[プレビューリリースを含める]チェックボックスにチェックを入れて、次の2つのSDKを追加します。

Azure.Communication.Administration
Azure.Communication.Chat
なお、SDK自体は、C# 8.0でなくても利用可能で、.NET Framework 4.6.2でも動作することは確認済です。

コードを書く(その1) - アクセストークンの取得

Azure Portalからリソース名とキーを取得して、ResourceNameとKeyという名前で格納しておきます。

private async Task GetAccessTokenAsync()
{
    var ConnectionString = $"endpoint=https://{this.ResourceName}.communication.azure.com/;accesskey={this.Key}";
    this.Client = new CommunicationIdentityClient(ConnectionString);
    var userResponse = await this.Client.CreateUserAsync();
    this.User = userResponse.Value;
    var tokenResponse = await this.Client.IssueTokenAsync(this.User, scopes: new[] { CommunicationTokenScope.Chat });
    this.AccessToken = tokenResponse.Value.Token;
    var expiresOn = tokenResponse.Value.ExpiresOn;

    System.Diagnostics.Debug.WriteLine($"Created a user with ID: {this.User.Id}");
    System.Diagnostics.Debug.WriteLine($"Issued a token with 'chat' scope that expires at {expiresOn}:");
    System.Diagnostics.Debug.WriteLine(this.AccessToken);
}

接続文字列を指定して「Azure.Communication.Administration.CommunicationIdentityClient」をnewします。
次に「CreateUserAsync」メソッドを実行して接続ユーザーを作成します。
そして、「IssueTokenAsync」メソッドに接続ユーザーとチャットを指定してアクセストークを取得します。

コードを書く(その2) - スレッドの生成
var chatClient = new ChatClient(new Uri($"https://{this.ResourceName}.communication.azure.com"), new CommunicationUserCredential(this.AccessToken));
var chatThreadMember = new ChatThreadMember(this.User)
{
    DisplayName = "UserDisplayName",
    ShareHistoryTime = DateTime.UtcNow,
};
this.Thread = await chatClient.CreateChatThreadAsync(groupId, new[] { chatThreadMember });
this.ThreadId = this.Thread.Id;

アクセストークンが取得できたならば、そのアクセストークンとAzure.Connection.Servicesのエンドポイントを指定して「Azure.Communication.Chat.ChatClient」をnewします。
そして、アクセストークンを作ったときのユーザーを指定して「Azure.Communication.Chat.ChatThreadMember」をnewします。
最後にCreateChatThreadAsyncメソッドを実行すれば、会話スレッドが新規作成できます。

コードを書く(その2´) - スレッドの取得

すでにスレッドがある場合は、スレッドIDを指定してスレッドを取得することも可能です。

var item = await chatClient.GetChatThreadAsync(this.ThreadId);
コードを書く(その3) - チャットに送信

チャットスレッドのSendMessageAsyncメソッドを実行すると、そのチャットスレッドにメッセージを送信できます。

internal async Task SendAsync(string content)
{
    var priority = ChatMessagePriority.Normal;
    var senderDisplayName = "sender name";

    var sendChatMessageResult = await this.Thread.SendMessageAsync(content, priority, senderDisplayName);
    System.Diagnostics.Debug.WriteLine($"Send Message: {content}");
}
コードを書く(その4) - チャットから受信

チャットからのメッセージ受信は少々手こずっています。
現在試行錯誤しており、次のように別スレッドで受信ループを回しています。

this.TokenSource = new CancellationTokenSource();
Task.Factory.StartNew(async () =>
{
    DateTimeOffset? startTime = DateTimeOffset.MinValue;
    while (!this.TokenSource.IsCancellationRequested)
    {
        try
        {
            var allMessages = this.Thread.GetMessagesAsync(startTime, this.TokenSource.Token);
            await foreach (var message in allMessages)
            {
                System.Diagnostics.Debug.WriteLine($"{message.Id}:{message.Sender.Id}:{message.Content}");
                startTime = (startTime < message.CreatedOn ? message.CreatedOn : startTime).GetValueOrDefault().AddSeconds(1);
            }
        }
        catch { }
    }
});

スレッドに対するGetMessagesAsyncメソッドの実行を行うと、メッセージを受信することができます。
結果は非同期ストリーミングとなるので「await foreach (var message in allMessages)」というC# 8.0の書き方で受信したメッセージを取り出します。
なお、GetMessagesAsyncメソッド実行時の第一パラメータ―の指定値が非常に重要になります。
第一パラメータにどの時刻以降のメッセージを取得するかを指定できますが、あくまでも時刻指定なので、前回受信メッセージの次のメッセージからというような明確な指定ができません。しかも、前回取得したときの最新時刻を指定すると前回メッセージを取得していまいます。
そこで、次のようにして前回+1秒後を指定するようにしています。

startTime = (startTime < message.CreatedOn ? message.CreatedOn : startTime).GetValueOrDefault().AddSeconds(1);

しかし前回と+1秒の間にメッセージが届いたときには取りこぼしが発生しそうな危険性があります。

ここまでで分かったこと(わからないこと)

チャットのメッセージを受信するときに前回以降の差分を取得する方法が分からない(存在しない)
クライアントアプリ間で同じスレッドにメッセージを送受信するには、アプリ同士で何を指定すればよいかがまだわからない
Azure.Communication.Servicesの動作フローを見るとクライアントアプリでもAppServiceなどを使ってアプリ間で同じスレッドを指定するための情報を共有しないといけないような図となっています。
f:id:hatsune_a:20200929083246p:plain
もしかしたら、クラウド側をAzure.Communication.Servicesだけで実現するのは難しいかもしれないのですが、もう少し、調べてみようと思います。