HoloLensのUnity 3DアプリでFirebase Streaming Rest APIを動かすメモ

HoloLensのUnity 3DアプリでFirebase Streaming Rest APIを動かすメモです。2D アプリでのアプローチが上手く行かず、自分の理解を深めながら独自に処理を書いたメモです。

経緯1:HoloLensの2Dアプリでうまくできたライブラリが入らない

悲しいことに、HoloLensの2Dアプリでうまくできた以下のネタ。

FirebaseをHoloLensの2Dアプリで動かすメモ

MQTTの要領で入れればうまくいくと思ったら、なんとUnity 3Dアプリで nuget インストールしようとしたら激しいエラー。

image

Install-Package : NU1107: System.Collections のバージョンの競合が検出されました。この問題を解決するには、System.Collections 4.3.0 をプロジェクト Assembly-CSharp に直接インストールするか、参照します。
Assembly-CSharp -> FirebaseDatabase.net 3.3.3 -> NETStandard.Library 1.6.1 -> System.Collections (>= 4.3.0)
Assembly-CSharp -> Microsoft.NETCore.UniversalWindowsPlatform 5.0.0 -> Microsoft.NETCore.Runtime 1.0.0 -> Microsoft.NETCore.Runtime.CoreCLR-arm 1.0.0 -> System.Collections (= 4.0.10).
発生場所 行:1 文字:1
+ Install-Package FirebaseDatabase.net
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Install-Package], Exception
+ FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand

とか、

Install-Package : NU1203: System.Reflection.Emit.ILGeneration 4.3.0 には System.Reflection.Emit.ILGeneration に対するコンパイル時参照アセンブリ (UAP,Version=v10.0.10240 上) がありますが、win10-arm-aot との互換性を持つランタイム アセンブリが存在しません。
発生場所 行:1 文字:1
+ Install-Package FirebaseDatabase.net
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Install-Package], Exception
+ FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand

などなど、大変そう。

ほかにもツラツラとエラーが出ていて、かなり相性が悪い様子。

こういうインストールに慣れた方なら解消できるかもしれないが、自分としては一旦待避。

経緯2:Unity Editorで動くシンプルなFirebaseライブラリも入らない

では、Unity Editorで動くシンプルなFirebaseライブラリならということで、

dkrprasetya/simple-firebase-unity: Firebase Realtime-Database's REST API Wrapper for Unity in C#

は、Unity Editor上では良かった。良かったけれど、UWPビルド時には

simple-firebase-unity/FirebaseRoot.cs at master · dkrprasetya/simple-firebase-unity

あたりなど、UWPが想定されてない印象で、とにかく、認証が絡みそうな X509Certificate がUWPと書き分けられてないためにビルドできない。

Unity や UWP で不足している .NET API – UWP app developer | Microsoft Docs

このあたりに書かれていることが影響してそう。

仕方ないので自分で組む

  • FirebaseDatabase.net
    • win10-arm-aot との互換性を持つランタイム アセンブリが解消できないかやってみたが断念
  • simple-firebase-unity
    • 今回は認証なしで行くのでエラーが出ているX509Certificate系を外そうとしたが他のエラーが出たので断念

みたいなことをやってみて解消が難しいので、以下の仕様を参考に仕方ないので自分で組むことにしました。

Firebase Database REST API  |  Firebase

これをみていると、

  • 単純なデータのGET/PUT/POST/PATCH/DELETEについては通常HTTPリクエストで行ける
  • 本命のリアルタイム取得 Streaming from the REST API はEventSource / Server-Sent Eventsでがんばる
    • ストリームデータで待つ
    • ヘッダーに “text/event-stream” をいれる などなど

ということが分かったので、UWPというか.NETでストリーム読み取りどうするのと悩んだんですが、紆余曲折は割愛しますが、以下の記事を発見してかなり理解が深まりました。

c# 4.0 – How to close EventSource connection on Firebase server using .NET HttpClient with Firebase REST Streaming API – Stack Overflow

ただ、これは、3年前の記事で、以前のFirebaseの仕様に沿っているので書き換える必要があるけれど、そもそものストリームデータの扱いが見えたのでとても助かりました。

ソースコード

GameObjectに仕込んだソースです。

このGameObjectが起動した際にFirebaseのFirebase Streaming Rest APIにつなぎに行きます。

using UnityEngine;
using System.Collections;
using System.Text;
using UnityEngine.Networking;
using MiniJSON;
using System.Collections.Generic;

#if UNITY_UWP
using System.Threading;
using System.Net.Http;
using System.IO;
using System;
using System.Net.Http.Headers;
using System.Threading.Tasks;
#endif

public class SphereCommands : MonoBehaviour
{

#if UNITY_UWP
    CancellationTokenSource cancellationTokenSource;
    HttpResponseMessage httpResponse;
    StreamReader contentStreamReader;
    Stream contentStream;
    Task taskFirebase;
#endif
    // Use this for initialization
    void Start()
    {
#if UNITY_UWP
        // 起動した際にFirebaseのFirebase Streaming Rest APIにつなぎに行きます。
        GetAndProcessFirebaseHttpResponse();
#endif
    }

    // Called by GazeGestureManager when the user performs a Select gesture
    void OnSelect()
    {
        Debug.Log("OnSelect");
    }

    // Called by SpeechManager when the user says the "Reset world" command
    void OnReset()
    {

    }

    // Called by SpeechManager when the user says the "Drop sphere" command
    void OnDrop()
    {
        // Just do the same logic as a Select gesture.
        OnSelect();
    }


#if UNITY_UWP

    public void GetAndProcessFirebaseHttpResponse()
    {
        // while 無限ループで待つと開放されずUnity描画が止まるのでTask化
        taskFirebase = new Task(async () =>
        {

            cancellationTokenSource = new CancellationTokenSource();

            // まず text/event-stream なHTTPリクエストを成立させる
            httpResponse = ListenAsync().Result;

            using (httpResponse)
            {
                using (contentStream = httpResponse.Content.ReadAsStreamAsync().Result)
                {
                    // StreamReaderでストリームデータを待つ
                    using (contentStreamReader = new StreamReader(contentStream))
                    {
                        // whileループで待つ
                        while (true)
                        {

                            if (cancellationTokenSource.IsCancellationRequested)
                            {
                                httpResponse.RequestMessage.Dispose();
                            }

                            cancellationTokenSource.Token.ThrowIfCancellationRequested();

                            // StreamReaderからストリームデータ取得
                            string read = await contentStreamReader.ReadLineAsync();

                            // データ判定
                            if (string.IsNullOrWhiteSpace(read))
                            {
                                // 通常は空のデータがくるので無視
                                // Debug.Log("IsNullOrWhiteSpace");
                            }
                            else
                            {
                                Debug.LogFormat("read : {0}", read);
                                
                                // ストリームデータから
                                string type = read.Split(new[] { ':' }, 2, StringSplitOptions.RemoveEmptyEntries)[0];

                                if (type == "data")
                                {
                                    Debug.Log("type : data");

                                    if (read == "data: null")
                                    {
                                        // 長く変化がないと cancel が来るので除外
                                    } else {
                                        // データがある場合

                                        // data: 以下をデータとして取り出す
                                        string dataStr = read.Split(new[] { ':' }, 2, StringSplitOptions.RemoveEmptyEntries)[1];

                                        // JSONデータ化
                                        Dictionary<string, object> json = Json.Deserialize(dataStr) as Dictionary<string, object>;
                                        Debug.LogFormat("dataStr : {0}", dataStr);

                                        UnityEngine.WSA.Application.InvokeOnAppThread(() => {

                                            RecieveMessage(json);

                                        }, true);
                                    }

                                    
                                }
                                // イベントが発生した
                                if (type == "event")
                                {
                                    Debug.Log("eventType : event");
                                }
                            }

                            // await Task.Delay(100);
                        }


                    }
                }
            }

        });

        taskFirebase.Start();
    }
    
    private async Task<HttpResponseMessage> ListenAsync()
    {
        // Create HTTP Client which will allow auto redirect as required by Firebase
        HttpClientHandler httpClientHandler = new HttpClientHandler();
        httpClientHandler.AllowAutoRedirect = true;

        // 自分のFirebaseのURL入れる
        // この場合は childpath /sample を取得
        var _firebasePath = "https://          .firebaseio.com/sample.json";

        HttpClient httpClient = new HttpClient(httpClientHandler, true);
        httpClient.BaseAddress = new Uri(_firebasePath);
        httpClient.Timeout = TimeSpan.FromSeconds(60);

        string requestUrl = _firebasePath;
        Uri requestUri = new Uri(requestUrl);

        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri);
        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));

        // HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
        HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
        response.EnsureSuccessStatusCode();

        return response;
    }


    private void RecieveMessage(Dictionary<string, object> json)
    {
        Debug.Log("RecieveMessage");

        // Firebaseから来た内容
        // 例: {"path":"/","data":"ddddd"}
        Debug.LogFormat("json : {0}", json["data"]);  // ddddd
    }

   
#endif
}

StreamReaderでストリームデータを待つところのwhileループをどう非同期にやりくりしたかが突破口でした。

  • using (contentStreamReader = new StreamReader(contentStream)) 以下の小さい括りにTaskで非同期を回したがStreamReaderがDispose
    • Task回した瞬間にwhile回す前に処理が抜けてしまいロスト
  • Coroutineで非同期回そうとしたらうまくいかない
    • メインスレッドで処理を複数フレームに分けて実行するアプローチなので今回のwhile回しっぱなしでは描画が止まるので合わない(ようだ)
  • Unity内で使えるThread回そうとしたがUWPでうまくいかなかった
    • UWPで使える記法と違う方法のようで、断念。

上記のように紆余曲折ありましたが、しかるべき処理範囲をTaskで回したらうまく行きまして、今回の処理には合っているようです。

動かしてみる

実際にHoloLensでアプリを起動します。

image

        // 自分のFirebaseのURL入れる
        // この場合は childpath /sample を取得
        var _firebasePath = "https://          .firebaseio.com/sample.json";

今回は、FirebaseのURL / sample でchildpath /sample のデータ取得します。

image

最初に起動した時に、このように現状のデータをつなぎに行きます。

キャンセルが来る時がある

これも仕様なんですが、長く待っているとキャンセルが来ることがあります。

image

キャンセル時はデータで null が来るので除外してます。文字列?

データを変更してみる

image

Firebase コンソールで、データを変更します。

image

すぐに反映されます。

ひとまずうまくいった

ということで、HoloLensのUnity 3DアプリでFirebase Streaming Rest APIを動かすことはできました。

何かしらのFirebaseライブラリに頼りたかったはずが、Firebaseの仕様とHoloLensというかUnity UWPの深淵に触れた気がしますがこんな状況です。

  • Firebaseで一番使いたかったFirebase Streaming Rest APIが使うことが一応出来た
  • いまはリアルタイム受信しか出来ないので、本来Firebaseライブラリができるようなことをやりたい
  • Firebaseのほかの認証込みでいずれはつないでみたい
  • Unity UWPでのマルチスレッド(Task)の使い方が少しわかった気がする
  • Unity UWPのナレッジが狭間すぎてうまく探せないが脳汁は出て楽しい。ただし使いこなせているわけではない。
  • Unityでのスレッド、.Netでのスレッド、などなどマルチスレッドをしれたのは良かった
  • Unityのコルーチンも少しだけ挙動が分かってよかった、非同期を割り振りたい気持ちもわかる
  • あらためてUniRxといったObserverなのは面白いと思った。こういうのに強い。
  • while(true) している間は描画は進まずデータは待てる、描画を優先するとデータは待てないといった悩ましさを体験できた
  • Firebaseは賢いので、ツリー階層の中の変更された差分だけ返答するところの対応ができてないので「ある値1つを狙って変化を検出」を対応。

ほかにもいろいろ思いがあった気がしますが、ひとまずいい経験が積めました。

Firebaseをまた使うことがあれば、引き続き、データ変更のほうや、最適なライブラリの捜索など行いたいと思います。

それでは、よきHoloLens & Firebase Lifeを!