DMM.comの、一番深くておもしろいトコロ。

VRアプリから直接YouTubeに動画配信できるDMM VR Connect #4

VRアプリから直接YouTubeに動画配信できるDMM VR Connect #4

  • このエントリーをはてなブックマークに追加

Unity製VRアプリからYoutubeに直接映像が飛ばせるって知ってました?

DVRStreamingを使うと、Oculus Quest, Quest 2, Windowsアプリから直接ゲームプレイ映像等をYouTubeやTwitch等へRTMPで配信する機能を追加することができます。 youtu.be

あらゆるVRアプリに好きな姿でダイブできるDMM VR Connect連載 #4

あらゆるVRアプリに好きな姿で飛び込みたい!
そう思ったこと、一度はありますよね?
そんな世界を実現するDMM VR ConnectとそのSDKについて、技術者目線で開発メンバーが全六回の連載でご紹介します。
四回目となる今回は、DVRSDKの大きな特徴である配信機能のDVRStreamingについて、DVRSDK開発チームの長月ゆきながご紹介します。

前回はこちら▼

inside.dmm.com

目次

自己紹介

github.com DVRSDK開発チームの長月ゆきな(@ngtkd)です。DVRSDKのDVRStreamingでネイティブプラグインとUnityの実装を主に担当しています。
2019年にDVRStreamingの開発依頼であきらさんにお声がけいただき、その後2020年6月からDMM VR labにJoinしました。
今回は、私が開発しているDVRStreamingについてお伝えしていきます。

DMM VR ConnectはあらゆるVRアプリに好きな姿で飛び込めるサービス

www.youtube.com connect.vrlab.dmm.com

Windows/Oculus Quest/Quest2からRTMPで動画を配信する話

さて、前回(#3)までの連載で、VRMアバターを三人称視点で映せるようになることがお分かりいただけたと思います。
アバターを三人称視点で映せるようになった後はやっぱり、配信できるようにしたいですよね。
ところが用意するほうとしてはこれ、結構ツライんです。
OBS等の配信ソフトウェアをインストールして音声のルーティングを設定してetc…
Questではさらにツライ。
スタンドアロンが売りのプラットフォームなのに配信のためのPCが必要。
さらにQuestからPCへ映像と音声を送る方法がいくつもあってわけが分からない。
これらの問題をなんとかするのがDVRStreamingです。

そもそもDVRStreamingとは? 何ができるのか?

Unityのカメラの映像と音声を直接YouTube、Twitch等のRTMP対応配信サービスへ送信します。
OBS等の配信用ソフトウェアは必要ありませんし、Quest版ではQuest単体で完結します。

使い方

  1. CameraにVideoStreamingSourceコンポーネントを追加する
  2. Audio ListenerにAudioStreamingSourceコンポーネントを追加する
  3. DVRStreamingプレハブをシーンに設置する
  4. スクリプトからServerUrlを設定してStartStreamingを呼び出す

基本はこれだけです。
DVRSDKにサンプルシーンが入っているので、すぐに試すことができます。
ユーザーはConnectのマイページでストリームキー(配信先)を設定することができます。

DVRStreamingが内部で何をしているのか

  • Unityのカメラから映像を取得
  • Unityのオーディオから音声を取得
  • 映像のエンコード
  • 音声のエンコード
  • Muxer(映像と音声の結合)
  • RTMPで配信

UnityからYouTube等に配信を行うには以上の処理を全て行う必要があります。
今回はそれぞれ具体的にどのように実装されているのかを詳細に解説します!

Unityでカメラから映像のフレームを取得

        private IEnumerator CallPluginAtEndOfFrames()
        {
            RenderTexture renderTexture = spectatorCamera.targetTexture;
            NativeMethods.SetTextureFromUnity(renderTexture.GetNativeTexturePtr(), renderTexture.width, renderTexture.height);

            while (true)
            {
                yield return new WaitForEndOfFrame();

                GL.IssuePluginEvent(NativeMethods.GetRenderEventFunc(), 1);
            }
        }

コルーチンでレンダリングが終了しているEnd of frameまで待ってGL.IssuePluginEventを実行すると、レンダリングスレッドでプラグインが呼び出されます。
GLESのコンテキストはスレッドに結び付けられていて他のスレッドからは利用できないので、これ重要です。  

Unityでオーディオから音声のデータを取得

        private void OnAudioFilterRead(float[] sampleData, int channels)
        {
            GCHandle pinnedSampleData = GCHandle.Alloc(sampleData, GCHandleType.Pinned);
            NativeMethods.AddAudioSamples(pinnedSampleData.AddrOfPinnedObject(), sampleData.Length, channels);
            pinnedSampleData.Free();
        }

Audio Listenerと同じGameObjectにOnAudioFilterReadを定義すると、一定時間ごとに呼び出されます。
本来、sampleDataを変更してエフェクトをかけるために使われるのですが、ここではプラグインにそのまま渡します。

これで映像と音声の生データが取得できたのでエンコードをしていきます。

映像と音声のエンコード(Android)

Oculus QuestはAndroidなので、Android用のエンコーダを用意します。スペック的にCPUエンコードでは処理が間に合わないので、H.264のハードウェアエンコーダを直接叩きます。

  • 初期化

MediaCodecを使うのですが、NDKのネイティブインターフェースでは機能が足りないので泣きながらJNI経由でJava版を使うことになります。 MediaCodec#createInputSurfaceで入力サーフェースを作成し、そこにGLESでUnityから渡されたテクスチャをレンダリングします。

    EGLint ConfigAttributeList[] = {
        EGL_RED_SIZE, 8,
        EGL_GREEN_SIZE, 8,
        EGL_BLUE_SIZE, 8,
        EGL_ALPHA_SIZE, 8,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
        EGL_RECORDABLE_ANDROID, 1,
        EGL_NONE,
    };
    eglChooseConfig(Display, ConfigAttributeList, &Config, 1, &NumConfig);

GLES(EGL)の初期化処理の一部分ですが、EGL_RECORDABLE_ANDROIDという謎の属性が指定されています。
これがないと、何のエラーもないけどなぜかMediaCodecが動かないという問題に苦しむことになるのですが、MediaCodecのドキュメントには一切書かれていない。つらい。

  • 映像のフレームを渡す

GL.IssuePluginEventから呼び出された関数でサーフェスにテクスチャをレンダリングします。 eglSwapBuffersを呼び出すと、エンコードが始まります。

  • 音声のデータを渡す

Unityから渡されたバッファをMediaCodecに渡してエンコードするだけ…と言いたいところですが、そうはいきませんでした。 Unityのサンプリングレートは48kHz固定となっていますが、RTMPでサポートされているのは44.1kHzまでです。 また、32bit浮動小数点数を16bit整数へ変換する必要があります(Quest 2対策)。 DVRStreamingではlibsamplerateを使用して変換しています。

映像と音声のエンコード(Windows)

Windows版でもパフォーマンスのためにハードウェアエンコーダのNVENCを使用する目的で、MediaFoundationを使用します。

  • 初期化

公式のBasic MFT Processing ModelAsynchronous MFTsを参考に実装していったわけですが、謎の現象に悩まされまくります。

Unlocking Asynchronous MFTsには

Until the client unlocks the MFT, all IMFTransform methods should return MF_E_TRANSFORM_ASYNC_LOCKED, with the following exceptions:
- IMFTransform::GetAttributes (all asynchronous MFTs)
- IMFTransform::GetInputAvailableType (all asynchronous MFTs)
- IMFTransform::GetOutputCurrentType (encoders only)
- IMFTransform::SetOutputType (encoders only)
- IMFTransform::GetStreamCount (all asynchronous MFTs)
- IMFTransform::GetStreamIDs (all asynchronous MFTs)

と書かれていますが、NVIDIAのMFTでUnlock前にGetStreamIDsを呼ぶとMF_E_TRANSFORM_ASYNC_LOCKEDが帰ってきます。なんで?

IntelのMFTでIMFTransform::GetInputAvailableTypeで帰ってきたフォーマットをIMFTransform::SetInputTypeに渡してもMF_E_INVALIDMEDIATYPEが帰ってきて動作しない。もうやだ。

  • 映像のフレームを渡す
    Microsoft::WRL::ComPtr<IMFSample> Sample;
    MFCreateSample(&Sample);
    Sample->AddBuffer(MediaBuffer.Get());
    Sample->SetSampleTime(TimestampUs * 10);

    EncoderMFT->ProcessInput(InputStreamID, Sample.Get(), 0);

基本はこんな感じです。 実際には映像が上下反転していたり、色が青と赤が逆になったりと謎現象が起きるので、シェーダーで補正しています。

  • 音声のデータを渡す

Android版と同様に44.1kHzにリサンプリングする必要があるのですが、標準でAudio Resampler DSPが用意されているので、それを使っています。

Muxer(映像と音声の結合)してRTMPで配信

DVRStreamingのRTMP実装はsrs-librtmpベースに改造したものを使用しています。

ここまで全て実装すると、やっとUnityからYouTubeやTwitchに直接配信が可能になります。

実装していてしんどかったポイント

  • ネイティブプラグインの開発しんどい
    ひたすらUnity Editorが落ちます。落ちまくります。 また、プラグインのDLLをUnity Editorが掴みっぱなしにするのでエディタを終了しないと更新もできません。*1

  • Texture2D.ReadPixelsが死ぬほど遅い 1280x720の画像で28msとかかかります。72fpsで13msなので無理。

  • 音関係がOculus Questでは動いていたのにQuest2で動かなかった
    KEY_PCM_ENCODINGでENCODING_PCM_FLOATを指定してUnityから渡されたfloatをそのまま渡していたのですが、Quest 2のAACエンコーダでは動きません。 16bit整数に変換してからエンコーダに渡して回避しました。 Quest 2対応まで時間がかかった原因がこれです。

まとめ

Unityから直接RTMP配信するにはかなりたくさんの難所を超える必要がありました。
特にスタンドアローン機のOculus Quest2でも動くのが売りです。
DVRSDKにはこれら難しいことすべてが実装済みなのでぜひ使ってみてください!
Oculus Quest単体でアバター配信するところまで実装できちゃいます。

ぜひ皆さんもデベロッパー登録してアプリを作ってみてください! devs.connect.vrlab.dmm.com

もし実装で困った時は、DMM VR lab公式DiscordサーバーのDMM VR lab Communityで気軽にお問い合わせください! discord.gg

*1:これはPatchLibaryで改善できそうです。