12
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

Unityがシミュレータとして使われることが多いですが、Unityカメラの画像をRTSPストリームで出力できれば便利なのでは?と考え、アプリを開発しました。
ソースコードはこちらにおいています。

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

公式の通りaptコマンドでインストールをしようとしましたが、gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5の3つでエラーのためインストールできませんでした。
最終的にインストールで使用したコマンドは以下です。

  • 試していませんが、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ストリームを出力する機能を実装しました。詳しくはソースコードを参照してください。

ディレクトリ構成

RtspLib
  ├ lib
  ├ src  
  ├ test
  └ wrapper

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

RtstStreamerクラス

src/rtsp_streamer.cpp、src/rtsp_streamer.hに実装されています。
rtsp_streamer.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では他の画像フォーマットも使えますが、このフォーマットだと変換することなくそのまま使えます。
フォーマットの設定は以下の処理になります。

    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です。

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.jpg2.jpg3.jpgという名前で画像ファイルを置いて、rtsp-streamer-testを実行することで、RTSPストリームを出力できます。
出力されるストリームはrtsp://127.0.0.1/testで受信できます。

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

Unityアプリの実装

Unityカメラの画像をc++ライブラリに渡すアプリを開発します。ソースコードこちらです。

プロジェクトの作成など

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

RtspStreamer.csの実装

Unityのカメラ画像をRenderTextureから取り出し、c++ライブラリに渡すソースコードを実装します。
C#からC++を呼び出す方法は、詳しくはこちらを参照ください。

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

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関数には、画像データの先頭のポインタを渡す必要があります。そこでポインタは以下のように取得しました。

SetImage(GCHandle.Alloc(_texture.GetPixels32(0), GCHandleType.Pinned).AddrOfPinnedObject(), _dataSize);

またC++の画像とUnityでは画像の座標系が異なっています。C++は左上が原点、右下が(x, y) > 0正となります。一方でUnityは左下が原点、右上が(x, y) > 0正となります。
よって、FlipTexture()関数で、垂直方向に反転します。

    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という名前にします
  2. RtspStreamerを選択し、RtspStreamer.csInspectorにドラッグ&ドロップします
  3. 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

  • VLC media playerの表示
    image.png

参考

12
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?