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は以下になります。一部ログ出力は削除しています。
# 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です。
# 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++ライブラリに渡すアプリを開発します。ソースコードこちらです。
プロジェクトの作成など
- 通常通りUnityのプロジェクトを作成する
- Assetsフォルダ化に、
Scripts
とPlugins
フォルダを作成する -
Plugins
フォルダ化に、rtsp-streamer-clib.so
をコピーします - RenderTextureを生成する
- Assetsフォルダで右クリック
- Create -> Render Texture
- 名前は任意の名前
- フォーマットは、R8G8B8A8_UNORMを設定します。C++でRGBAを指定したことと、各要素が8bitであることを期待しているからです。
- RTSPストリームを出力するためのカメラを生成します
- Hierarchy上で右クリックし、Create -> Cameraを選択します
- 任意の名前をつけます。自分は
RtspStreamer
としました -
Target Texture
に、4.で生成したRender Texture
を設定します
RtspStreamer.csの実装
Unityのカメラ画像をRenderTextureから取り出し、c++ライブラリに渡すソースコードを実装します。
C#からC++を呼び出す方法は、詳しくはこちらを参照ください。
Assets/Scriptsディレクトリ以下にソースコードを置きます。
このコードの中で、wrapper.cpp
の*Initialize()とSetImage()*を呼び出します。
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の設定
- Hierarchyで Create -> Create Empty() を選択し、
RtspStremer
という名前にします -
RtspStreamer
を選択し、RtspStreamer.cs
をInspectorにドラッグ&ドロップします - Inspectorで
RtspStreamer.cs
のパラメータに以下を設定します- RenderTexture
- RenderTextureを設定する
- Fps
- ストリームのFPSを出力する
- Url
- 出力するストリームのURLを設定する
- RenderTexture
残りは通常のUnityアプリと同じです。
Build & Run
Unityアプリの通常の手順でBuild And Run
でビルド、実行します。問題なければアプリが起動します。
RTPSストリームの表示
どんな方法でもいいのですが、自分はVLC media player
を使って確認しました。受信するURLをrtsp://127.0.0.1:8554/test
と指定します。問題なればストリームが表示されます。