Unityのカメラ映像をRTSPストリームで出力する


Unityがシミュレータとして使われることが多いですが、Unityカメラの画像をRTSPストリームで出力できれば便利なのでは？と考え、アプリを開発しました。
ソースコードは[こちら](https://github.com/hiro-han/unity-rtsp-streamer.git)においています。

UnityにはRTSP用のAssetがなかったため、ホストPCにgstreamerをインストールしそれをUnityから使うようにしました。
具体的には以下のような構成になります。

Unityアプリ -> c++ wrapper -> c++ RTSPアプリ -> gstreamer (ホストPC)

- ホストPCにgstreamerをインストール
- gstreamerを使用してRTSPストリームを出力する機能をc++で実装
- Unityから上記機能を呼び出し

# 開発&実行環境
- Ubuntu 16.04
- Unity 2019.4.17f1
- c++14
- GStreamer 1.0
- gst-rtsp-server 1.8.3 

# 環境構築
## ホストPCへgstreamerのinstall
[公式](https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c)の通り`apt`コマンドでインストールをしようとしましたが、`gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5`の３つでエラーのためインストールできませんでした。
最終的にインストールで使用したコマンドは以下です。
* 試していませんが、Ubuntu 18.04 もしくは Ubuntu 20.04 ではエラーでないのではないかと思います。

```
$ sudo apt-get install -y libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-doc gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa  gstreamer1.0-pulseaudio  libgstrtspserver-1.0-dev gstreamer1.0-rtsp
```
## gst-rtsp-serverのインストール
こちらも本来であれば`apt`コマンドでインストールできますがエラーが発生したため、ソースコードからビルドしました。
ビルドに必要なライブラリのみ`apt`コマンドでインストールします。

### ライブラリのインストール
```
$ sudo apt-get -y install autoconf gtk-doc-tools 
```
### ソースコードの取得、ビルド、インストール
- Clone

```
$ git clone https://github.com/GStreamer/gst-rtsp-server.git
```

- Build
gstreamer 1.0に対応するのは、1.8.3のようなので、このバージョンに切り替えます。

```
$ git checkout -b 1.8.3 refs/tags/1.8.3
$ ./autogen.sh
$ ./configure
$ ./make
$ sudo make install
```

# c++ライブラリの実装
gstreamerを使ってRTSPストリームを出力する機能を実装しました。詳しくは[ソースコード](https://github.com/hiro-han/unity-rtsp-streamer/tree/main/RtspLib)を参照してください。

## ディレクトリ構成

```
RtspLib
  ├ lib
  ├ src  
  ├ test
  └ wrapper

```
- lib
    - gst-rtsp-serverとplogが含まれている。plogはログ出力のためのライブラリ
- src
    - 入力された画像からRTSPストリームを出力する機能が実装されている
- test
    - 上記機能のテストコード
    - test/dataディレクトリ下に画像ファイルを置くことで、それらを使ってRTSPストリームを出力することが可能
- wrapper
    - C#からc++で実装された機能を呼び出すためのラッパー
    - C#からc++を呼び出す方法は[[こちら](https://qiita.com/hiro-han/items/c257c94979fce21652e8)]を参照ください

## RtstStreamerクラス
src/rtsp_streamer.cpp、src/rtsp_streamer.hに実装されています。
rtsp_streamer.cppは以下になります。一部ログ出力は削除しています。

```cpp:rtsp_streamer.cpp
#include "rtsp_streamer.h"
#include <gst/gst.h>
#include <gst/rtsp-server/rtsp-server.h>
#include <plog/Initializers/RollingFileInitializer.h>
#include <plog/Log.h>
#include <cstring>
#include <fstream>
#include <memory>

const std::string RtstStreamer::kH264 =
    "( appsrc name=mysrc ! videoconvert ! "
    "x264enc ! speed-preset=ultrafast tune=zerolatency threads=1 ! "
    "rtph264pay name=pay0 pt=96 )";

const std::string RtstStreamer::kJpeg =
    "( appsrc name=mysrc ! videoconvert ! "
    "video/x-raw,format=I420 ! jpegenc ! rtpjpegpay name=pay0 pt=96 )";

RtstStreamer::RtstStreamer(const Encode enc, const ImageFormat format,
                           const std::string &url, const int width,
                           const int height, const int depth, const int fps)
    : enc_(enc),
      format_(format),
      url_(url),
      width_(width),
      height_(height),
      depth_(depth),
      fps_(fps) {}

RtstStreamer::~RtstStreamer() {}

bool RtstStreamer::Initialize() {
  thread_ = std::thread(&RtstStreamer::Run, this);
  return true;
}

void RtstStreamer::Run() {
  PLOG_DEBUG << "Enter Run()";

  gst_init(NULL, NULL);

  loop_ = g_main_loop_new(NULL, false);
  server_ = gst_rtsp_server_new();

  GstRTSPMountPoints *mounts = gst_rtsp_server_get_mount_points(server_);
  GstRTSPMediaFactory *factory = gst_rtsp_media_factory_new();

  switch (enc_) {
    case Encode::kJPEG:
      gst_rtsp_media_factory_set_launch(factory, kJpeg.c_str());
      break;
    case Encode::kH264:
      // No test
      gst_rtsp_media_factory_set_launch(factory, kH264.c_str());
      break;
    default:
      gst_rtsp_media_factory_set_launch(factory, kJpeg.c_str());
      PLOG_WARNING << "Encode is not defined. Encode is set to BGR.";
  }

  g_signal_connect(factory, "media-configure",
                   (GCallback)&RtstStreamer::MediaConfigure, (gpointer) this);
  gst_rtsp_mount_points_add_factory(mounts, url_.c_str(), factory);
  g_object_unref(mounts);

  int id = gst_rtsp_server_attach(server_, NULL);
  if (id < 0) {
    PLOG_ERROR << "Failed gst_rtsp_server_attach(). id = " << id;
  }
  g_main_loop_run(loop_);
}

void RtstStreamer::MediaConfigure(GstRTSPMediaFactory *factory,
                                  GstRTSPMedia *media, gpointer user_data) {
  RtstStreamer *rtsp_streamer = reinterpret_cast<RtstStreamer *>(user_data);

  GstElement *element = gst_rtsp_media_get_element(media);
  GstElement *appsrc =
      gst_bin_get_by_name_recurse_up(GST_BIN(element), "mysrc");

  switch (rtsp_streamer->format_) {
    case ImageFormat::kBGR:
      gst_util_set_object_arg(G_OBJECT(appsrc), "format", "time");
      g_object_set(G_OBJECT(appsrc), "caps",
                   gst_caps_new_simple(
                       "video/x-raw", "format", G_TYPE_STRING, "BGR", "width",
                       G_TYPE_INT, rtsp_streamer->width_, "height", G_TYPE_INT,
                       rtsp_streamer->height_, "framerate", GST_TYPE_FRACTION,
                       rtsp_streamer->fps_, 1, NULL),
                   NULL);
      break;
    case ImageFormat::kBGRA:
      gst_util_set_object_arg(G_OBJECT(appsrc), "format", "time");
      g_object_set(G_OBJECT(appsrc), "caps",
                   gst_caps_new_simple(
                       "video/x-raw", "format", G_TYPE_STRING, "BGRA", "width",
                       G_TYPE_INT, rtsp_streamer->width_, "height", G_TYPE_INT,
                       rtsp_streamer->height_, "framerate", GST_TYPE_FRACTION,
                       rtsp_streamer->fps_, 1, NULL),
                   NULL);
      break;
    case ImageFormat::kRGBA:
      gst_util_set_object_arg(G_OBJECT(appsrc), "format", "time");
      g_object_set(G_OBJECT(appsrc), "caps",
                   gst_caps_new_simple(
                       "video/x-raw", "format", G_TYPE_STRING, "RGBA", "width",
                       G_TYPE_INT, rtsp_streamer->width_, "height", G_TYPE_INT,
                       rtsp_streamer->height_, "framerate", GST_TYPE_FRACTION,
                       rtsp_streamer->fps_, 1, NULL),
                   NULL);
      break;
    default:
      gst_util_set_object_arg(G_OBJECT(appsrc), "format", "time");
      g_object_set(G_OBJECT(appsrc), "caps",
                   gst_caps_new_simple(
                       "video/x-raw", "format", G_TYPE_STRING, "BGR", "width",
                       G_TYPE_INT, rtsp_streamer->width_, "height", G_TYPE_INT,
                       rtsp_streamer->height_, "framerate", GST_TYPE_FRACTION,
                       rtsp_streamer->fps_, 1, NULL),
                   NULL);
      PLOG_WARNING << "Format is not defined. Format is set to BGR.";
  }

  rtsp_streamer->timestamp_ = 0;
  rtsp_streamer->image_size_ =
      rtsp_streamer->width_ * rtsp_streamer->height_ * rtsp_streamer->depth_;

  rtsp_streamer->mutex_.lock();
  rtsp_streamer->buffer_ =
      std::make_unique<uint8_t[]>(rtsp_streamer->image_size_);
  rtsp_streamer->mutex_.unlock();

  g_signal_connect(appsrc, "need-data", (GCallback)&RtstStreamer::NeedData,
                   (gpointer)user_data);
  gst_object_unref(appsrc);
  gst_object_unref(element);
}

void RtstStreamer::NeedData(GstElement *appsrc, guint unused,
                            gpointer user_data) {
  RtstStreamer *rtsp_streamer = reinterpret_cast<RtstStreamer *>(user_data);
  GstBuffer *buffer =
      gst_buffer_new_allocate(NULL, rtsp_streamer->image_size_, NULL);
  if (buffer == NULL) {
    PLOG_ERROR << "Failed  gst_buffer_new_allocate()";
    return;
  }

  rtsp_streamer->mutex_.lock();
  gsize copied_size = gst_buffer_fill(buffer, 0, rtsp_streamer->buffer_.get(),
                                      rtsp_streamer->image_size_);
  rtsp_streamer->mutex_.unlock();

  if (rtsp_streamer->image_size_ != copied_size) {
    PLOG_ERROR << "Failed. data size = " << rtsp_streamer->image_size_
               << ", copied size = " << copied_size;
    return;
  }

  guint64 duration =
      gst_util_uint64_scale_int(1, GST_SECOND, rtsp_streamer->fps_);
  GST_BUFFER_PTS(buffer) = rtsp_streamer->timestamp_;
  GST_BUFFER_DURATION(buffer) = duration;

  rtsp_streamer->timestamp_ = rtsp_streamer->timestamp_ + duration;

  GValue ret;
  g_signal_emit_by_name(appsrc, "push-buffer", buffer, &ret);
  gst_buffer_unref(buffer);
}

void RtstStreamer::setImage(const uint8_t *image, const size_t size) {
  mutex_.lock();
  if (buffer_) {
    PLOG_DEBUG << "Start copy : size = " << size;
    std::memcpy(buffer_.get(), image, size);
    PLOG_DEBUG << "Copied image";
  }
  mutex_.unlock();
}
```

RTSPストリームを出力する処理は別スレッドにしています。
Unityの画像を出力するときはフォーマットに`RGBA`を選択します。Unityでは他の画像フォーマットも使えますが、このフォーマットだと変換することなくそのまま使えます。
フォーマットの設定は以下の処理になります。

```cpp
    case ImageFormat::kRGBA:
      gst_util_set_object_arg(G_OBJECT(appsrc), "format", "time");
      g_object_set(G_OBJECT(appsrc), "caps",
                   gst_caps_new_simple(
                       "video/x-raw", "format", G_TYPE_STRING, "RGBA", "width",
                       G_TYPE_INT, rtsp_streamer->width_, "height", G_TYPE_INT,
                       rtsp_streamer->height_, "framerate", GST_TYPE_FRACTION,
                       rtsp_streamer->fps_, 1, NULL),
                   NULL);
      break;
```

使い方は、`Initialize()`でストリーム出力用のスレッドを生成し、`setImage()`で出力する画像をセットします。

## wrapper
C#からRtspStreamerを使うためのwrapperです。

```cpp:wrapper.cpp

#include "wrapper.h"
#include "rtsp_streamer.h"
#include "type.h"

#include <plog/Initializers/RollingFileInitializer.h>
#include <plog/Log.h>

extern "C" {

RtstStreamer* pRtspStreamer;

void Initialize(int format, char* url, int width, int height, int depth) {
  plog::init(plog::debug, "log.txt");

  pRtspStreamer =
      new RtstStreamer(Encode::kJPEG, static_cast<ImageFormat>(format), url,
                       width, height, depth, 1);
  pRtspStreamer->Initialize();
}

void SetImage(void* pImage, int size) {
  PLOG_DEBUG << "Eenter : SetImage";
  pRtspStreamer->setImage((uint8_t*)pImage, size);
}
```
C#からはここで実装されているInitialize()とSetImage()を呼び出します。

## Build & Run
```
$ cd RtspLib
$ mkdir build
$ cd build
$ cmake ..
$ make
```
ビルド後、`build`ディレクトリ下にライブラリ(.so)やテストコード(rtsp-streamer-test)が出力されます。

test/dataディレクトリ下に`1.jpg`、`2.jpg`、`3.jpg`という名前で画像ファイルを置いて、rtsp-streamer-testを実行することで、RTSPストリームを出力できます。
出力されるストリームは`rtsp://127.0.0.1/test`で受信できます。

Unityから使うには、build/wrapperディレクトリ下に出力される`rtsp-streamer-clib.so`を使います。これをUnityプロジェクトのPluginsフォルダ化にコピーして使います。


# Unityアプリの実装
Unityカメラの画像をc++ライブラリに渡すアプリを開発します。ソースコード[こちら](https://github.com/hiro-han/unity-rtsp-streamer)です。

## プロジェクトの作成など
1. 通常通りUnityのプロジェクトを作成する
1. Assetsフォルダ化に、`Scripts`と`Plugins`フォルダを作成する
1. `Plugins`フォルダ化に、`rtsp-streamer-clib.so`をコピーします
1. RenderTextureを生成する
    1. Assetsフォルダで右クリック
    1. Create -> Render Texture
    1. 名前は任意の名前
    1. フォーマットは、**R8G8B8A8_UNORM**を設定します。C++で*RGBA*を指定したことと、各要素が8bitであることを期待しているからです。
1. RTSPストリームを出力するためのカメラを生成します
    1. Hierarchy上で右クリックし、Create -> Cameraを選択します
    1. 任意の名前をつけます。自分は`RtspStreamer`としました
    1. `Target Texture`に、4.で生成した`Render Texture`を設定します

## RtspStreamer.csの実装
Unityのカメラ画像をRenderTextureから取り出し、c++ライブラリに渡すソースコードを実装します。
C#からC++を呼び出す方法は、詳しくは[こちら](https://qiita.com/hiro-han/items/c257c94979fce21652e8)を参照ください。

Assets/Scriptsディレクトリ以下にソースコードを置きます。
このコードの中で、`wrapper.cpp`の*Initialize()*と*SetImage()*を呼び出します。

```c#:RtspStreamer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// IntPtr型を使用するのに必要
using System;
// Dllの読み込みに必要
using System.Runtime.InteropServices;
using System.IO;

public class RtspStreamer : MonoBehaviour
{

    [DllImport("librtsp-streamer-clib.so")] public static extern void Initialize(int format, string url, int width, int height, int image_size);
    [DllImport("librtsp-streamer-clib.so")] public static extern void SetImage(IntPtr pImage, int size);
    [DllImport("librtsp-streamer-clib.so")] public static extern void Finalize();

    // public Camera camera;
    public RenderTexture _renderTexture;
    public float _fps = 0.5f;
    public string _url = "test";
    private Texture2D _texture;
    private Texture2D _flipped_texture;
    private Rect _rect;
    private int _dataSize;
    private float _timeElapsed = 0.0f;
    private int _width;
    private int _height;


    void Start()
    {
        _width = _renderTexture.width;
        _height = _renderTexture.height;
        int depth = 4;
        _dataSize = _width * _height * depth;

        _texture = new Texture2D(_width, _height, TextureFormat.BGRA32, false);
        _flipped_texture = new Texture2D(_width, _height, TextureFormat.BGRA32, false);
        _rect = new Rect(0, 0, _width, _height);

        Initialize(2, "/" + _url, _width, _height, depth);
    }

    void Update()
    {
        Debug.Log("Enter : Update()");
        _timeElapsed += Time.deltaTime;
        if (_timeElapsed >= _fps) {
            try {
                var currentRenderTexture = RenderTexture.active;
                RenderTexture.active = _renderTexture;
                _texture.ReadPixels(_rect, 0, 0);
                _texture.Apply();
                RenderTexture.active = currentRenderTexture;
                FlipTexture(_texture);
                SetImage(GCHandle.Alloc(_texture.GetPixels32(0), GCHandleType.Pinned).AddrOfPinnedObject(), _dataSize);

                Debug.Log("SetImage()");
            } catch (Exception e) {
                Debug.Log(e.Message);
            }
            _timeElapsed = 0.0f;
        }
    }

    void FlipTexture(Texture2D tex)
    {
        var pixels = tex.GetPixels();

        Color[] newPixels = new Color[pixels.Length];

        for (int x = 0; x < _width; x++)
        {
            for (int y = 0; y < _height; y++)
            {
                newPixels[x + y * _width] = pixels[x + (_height - y -1) * _width];
            }
        }

        tex.SetPixels(newPixels);
        tex.Apply();
    }
}
```

c++ライブラリの`setImage`関数には、画像データの先頭のポインタを渡す必要があります。そこでポインタは以下のように取得しました。

```c#
SetImage(GCHandle.Alloc(_texture.GetPixels32(0), GCHandleType.Pinned).AddrOfPinnedObject(), _dataSize);
```
またC++の画像とUnityでは画像の座標系が異なっています。C++は左上が原点、右下が(x, y) > 0正となります。一方でUnityは左下が原点、右上が(x, y) > 0正となります。
よって、FlipTexture()関数で、垂直方向に反転します。

```c#
    void FlipTexture(Texture2D tex)
    {
        var pixels = tex.GetPixels();
        Color[] newPixels = new Color[pixels.Length];
        for (int x = 0; x < _width; x++)
        {
            for (int y = 0; y < _height; y++)
            {
                newPixels[x + y * _width] = pixels[x + (_height - y -1) * _width];
            }
        }
        tex.SetPixels(newPixels);
        tex.Apply();
    }
```

## RtspStreamerの設定
1. Hierarchyで Create -> Create Empty() を選択し、`RtspStremer`という名前にします
1. `RtspStreamer`を選択し、`RtspStreamer.cs`を*Inspector*にドラッグ&ドロップします
1. Inspectorで`RtspStreamer.cs`のパラメータに以下を設定します
    - RenderTexture
        - RenderTextureを設定する
    - Fps
        - ストリームのFPSを出力する
    - Url
        - 出力するストリームのURLを設定する

残りは通常のUnityアプリと同じです。

## Build & Run
Unityアプリの通常の手順で`Build And Run`でビルド、実行します。問題なければアプリが起動します。

## RTPSストリームの表示
どんな方法でもいいのですが、自分は`VLC media player`を使って確認しました。受信するURLを`rtsp://127.0.0.1:8554/test`と指定します。問題なればストリームが表示されます。

- Unityアプリの表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293692/a80fc3d5-23d9-3c77-18cc-df3e11f1f533.png)

- VLC media playerの表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293692/abde2f2d-357c-4a4f-9b4d-d2a88aaf3d06.png)

# 参考
- [unityアプリ(standalone, androidスマホ, OculusQuest2)でc++ライブラリを使う](https://qiita.com/hiro-han/items/c257c94979fce21652e8)
- [ソースコード](https://github.com/hiro-han/unity-rtsp-streamer)