libwebRTCでFakeVideoしてみた

libwebRTCの利用法の勉強がてら、カメラを使わずに自前で作った映像が送信できるか試してみた。

libwebRTC実行には WebRTC Native Client Momo を使用した。試した際のバージョンは19.01.0
https://github.com/shiguredo/momo

MomoのFakeVideoは、Issueを見るとそのうち正式に実装されそう。動作環境はMacで行った。

# 素の状態のビルド

まず、通常状態でのビルド及び実行ができることを確認した。

ドキュメントを参考にビルドを行った。
https://github.com/shiguredo/momo/blob/develop/doc/BUILD.md

自分の環境ではそのままではビルドが行えずboost のエラーが出たため、MakeFileでMOMO_CFLAGSを入れているところで下記オプションを追加した。
-D__ASSERT_MACROS_DEFINE_VERSIONS_WITHOUT_UNDERSCORES=0

./build/Makefile

```Makefile
mac: mac.prepare
  make -C .. MOMO_CFLAGS="-O2 -D__ASSERT_MACROS_DEFINE_VERSIONS_WITHOUT_UNDERSCORES=0" PACKAGE_NAME=mac momo

.PHONY: mac.package
mac.package: mac.prepare
  # momo を package モードでビルドし直す
  rm -f ../_build/mac/momo && make -C .. MOMO_CFLAGS="-O2 -D__ASSERT_MACROS_DEFINE_VERSIONS_WITHOUT_UNDERSCORES=0" PACKAGE_NAME=mac BUILD_MODE=package momo

```

```sh
# cd build
# make mac
```

# 動作確認

まず素の状態でのP2P動作を確認する。

momo起動

```sh
# cd ..
# ./momo p2p
```

ここでもサーバの立て方の問題でクライアント側がそのままではうまく行かなかったため、とりあえず固定値でJavascriptを書き換えた。

./html/webrtc.js

```Javascript
//const wsUrl = wsProtocol + location.host + '/ws';
const wsUrl = wsProtocol + 'localhost:8080/ws';
```

Webサーバ起動

```sh
# cd html
# python -m SimpleHTTPServer
```

Chrome で http://localhost:8000/p2p.html にアクセスすることで
Macのカメラの映像をWebRTC経由で見ることができた。

#FakeVideo

まず、映像をカメラのものから独自画像に差し替えられることを確認するため、自前の黒画像を送ることを試す。

ここでは、cricket::VideoCapturerを継承したCustomVideoCapturerを実装して、既存のVideoCapturerと差し替えることで実装を行う。

まずヘッダファイルを作成する。基本的には関数はoverrideとし、黒画像を作成するRender()関数を追加する。

./src/rtc/customcapturer.h を作成

```c++
#ifndef _CUSTOMVIDEOCAPTURER_H
#define _CUSTOMVIDEOCAPTURER_H

#include "media/base/videocapturer.h"

class CustomVideoCapturer :
        public cricket::VideoCapturer
{
public:
    explicit CustomVideoCapturer();
    virtual ~CustomVideoCapturer();

    // cricket::VideoCapturer implementation.
    virtual cricket::CaptureState Start(const cricket::VideoFormat& capture_format) override;
    virtual void Stop() override;
    virtual bool IsRunning() override;
    virtual bool GetPreferredFourccs(std::vector<uint32_t>* fourccs) override;
    virtual bool GetBestCaptureFormat(const cricket::VideoFormat& desired, cricket::VideoFormat* best_format) override;
    virtual bool IsScreencast() const override;

    void Render();

private:
    bool now_rendering;
};

#endif // _CUSTOMVIDEOCAPTURER_H
```

次にcppを作成する
画面を黒くするのは webrtc::I420Buffer::SetBlack()を呼ぶことで行った。


./src/rtc/customcapturer.cpp

```c++
#include "customcapturer.h"

#include <memory>

#include "api/video/i420_buffer.h"
#include "common_types.h"
#include "common_video/libyuv/include/webrtc_libyuv.h"
#include "base/logging.h"
#include "rtc_base/logging.h"
#include "third_party/libyuv/include/libyuv.h"

#include "opencv2/opencv.hpp"
#include <opencv2/core/core.hpp>

#include <algorithm> // fmod

CustomVideoCapturer::CustomVideoCapturer()
  : now_rendering(false)
{
}

CustomVideoCapturer::~CustomVideoCapturer()
{
}

void CustomVideoCapturer::Render(){
    if (!now_rendering) return;
    int buf_width = 640;
    int buf_height = 480;

    int64_t timems = rtc::TimeMillis();

    rtc::scoped_refptr<webrtc::I420Buffer> buffer = webrtc::I420Buffer::Create(buf_width, buf_height);

    webrtc::I420Buffer::SetBlack(buffer.get());

    webrtc::VideoFrame frame(buffer, 0, timems, webrtc::kVideoRotation_0);
    frame.set_ntp_time_ms(0);

    OnFrame(frame, buf_width, buf_height);
}

cricket::CaptureState CustomVideoCapturer::Start(const cricket::VideoFormat& capture_format)
{
    if (capture_state() == cricket::CS_RUNNING) {
        return capture_state();
    }

    now_rendering = true;
    SetCaptureFormat(&capture_format);

    return cricket::CS_RUNNING;
}

void CustomVideoCapturer::Stop()
{
    now_rendering = false;
    if (capture_state() == cricket::CS_STOPPED) {
        return;
    }

    SetCaptureFormat(NULL);
    SetCaptureState(cricket::CS_STOPPED);
}


bool CustomVideoCapturer::IsRunning()
{
    return capture_state() == cricket::CS_RUNNING;
}

bool CustomVideoCapturer::GetPreferredFourccs(std::vector<uint32_t>* fourccs)
{
    if (!fourccs) return false;
    fourccs->push_back(cricket::FOURCC_I420);
    return true;
}

bool CustomVideoCapturer::GetBestCaptureFormat(const cricket::VideoFormat& desired, cricket::VideoFormat* best_format)
{
    if (!best_format) return false;

    // Use the desired format as the best format.
    best_format->width = desired.width;
    best_format->height = desired.height;
    best_format->fourcc = cricket::FOURCC_I420;
    best_format->interval = desired.interval;
    return true;
}
```

これだけだと、Render()を呼ぶ部分がないため、動作しない。
そこで今回はとりあえずタイマを作ってメインループから定期的に呼ぶこととした。
まずrtc_managerにCustomVideoCapturerのインスタンスを持たせ、
public methodとしてCustomVideoCapturer::Render()を呼ぶrender()を追加する。

./src/rtc/manager.h

```c++
#ifndef RTC_MANAGER_H_
#define RTC_MANAGER_H_
#include "api/peerconnectioninterface.h"

#include "connection.h"
#include "connection_settings.h"

#include "customcapturer.h"

class RTCManager
{
public:
  RTCManager(ConnectionSettings conn_settings, std::unique_ptr<cricket::VideoCapturer> capturer);
  ~RTCManager();
  static std::unique_ptr<cricket::VideoCapturer> createVideoCapturer();
  std::shared_ptr<RTCConnection> createConnection(
          webrtc::PeerConnectionInterface::RTCConfiguration rtc_config,
          RTCMessageSender *sender);

  void render();

private:
  rtc::scoped_refptr<webrtc::PeerConnectionFactoryInterface> _factory;
  rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> _video_source;
  std::unique_ptr<rtc::Thread> _networkThread;
  std::unique_ptr<rtc::Thread> _workerThread;
  std::unique_ptr<rtc::Thread> _signalingThread;
  ConnectionSettings _conn_settings;

  CustomVideoCapturer* _capturer;
};
#endif
```

./src/rtc/manager.cpp

```c++
// 省略

// コンストラクタに変数初期化を追加
RTCManager::RTCManager(ConnectionSettings conn_settings, std::unique_ptr<cricket::VideoCapturer> capturer) : _conn_settings(conn_settings), _capturer(nullptr)
// 省略

// render()実装を追加
void RTCManager::render() {
  if(_capturer) _capturer->Render();
}
// 省略
```

また、RTCManagerコンストラクタ内でVideoCapturerを作っている箇所で、
今回作成したCustomVideoCapturerへの挿げ替えを行う。

./src/rtc/manager.cpp

```c++
//    capturer = createVideoCapturer();
    _capturer = new CustomVideoCapturer();
    std::unique_ptr<cricket::VideoCapturer> capturer(_capturer);
```

main関数では以下のようにrtc_manager変数を関数スコープから出し、
定期動作するtick()関数からrtc_manager->render();を呼ぶよう実装した。

./src/main.cpp

```c++
const size_t kDefaultMaxLogFileSize = 10 * 1024 * 1024;

boost::asio::io_context ioc{1};

boost::posix_time::seconds interval(1);
boost::asio::deadline_timer timer(ioc, interval);

std::unique_ptr<RTCManager> rtc_manager;

void tick(const boost::system::error_code&) {
  if(rtc_manager) rtc_manager->render();
  timer.expires_at(timer.expires_at() + interval);
  timer.async_wait(tick);
}

int main(int argc, char* argv[]) {
  // 省略
  //  std::unique_ptr<RTCManager> rtc_manager(new RTCManager(cs, std::move(capturer)));
  rtc_manager.reset(new RTCManager(cs, std::move(capturer)));
  {
      boost::asio::signal_set signals(ioc, SIGINT, SIGTERM);
      signals.async_wait([&](const boost::system::error_code&, int) {
          ioc.stop();
      });

      timer.async_wait(tick);
  // 省略
  }
}

```
# 黒画面動作確認
ビルドののち下記のコマンドでmomoを起動する

```sh
# ./momo --no-audio p2p
```

Webブラウザから変更前の場合同様に接続して黒画面が流れることが確認できた

# 独自画像送信

黒画面ではつまらないのでOpenCVを使ってパックマンを描いて送ってみる。
OpenCVをインストールする。

```sh
# brew install opencv
```

OpenCVを使えるようにするためMakeFileに下記のように追加する。

./Makefile

```Makefile
# OpenCV
CFLAGS += `pkg-config opencv --cflags`
LDFLAGS += `pkg-config opencv --libs`

# ユーザ指定のフラグを追加
CFLAGS += $(MOMO_CFLAGS)
```

CustomVideoCapturer::Render()を以下のように変更する

./src/rtc/customcapturer.cpp

```c++
void CustomVideoCapturer::Render(){
    if (!now_rendering) return;
    int buf_width = 640;
    int buf_height = 480;

    int64_t timems = rtc::TimeMillis();

    rtc::scoped_refptr<webrtc::I420Buffer> buffer = webrtc::I420Buffer::Create(buf_width, buf_height, buf_width, (buf_width + 1) / 2, (buf_width + 1) / 2);

    cv::Mat cvbuffer = cv::Mat(cv::Size(buf_width, buf_height), CV_8UC4, cv::Scalar(5));
    cv::rectangle(cvbuffer, cv::Point(0,0), cv::Point(buf_width,buf_height), cv::Scalar(0,140,0, 255), CV_FILLED, 8, 0);

    const float end_angle = fmod(60 * (timems / 1000), 361);
    const int radius = std::min(buf_width, buf_height) / 3;

    cv::ellipse(cvbuffer, cv::Point(buf_width / 2, buf_height / 2), cv::Size(radius, radius), 0, 0, end_angle, cv::Scalar(0,244,0, 255), CV_FILLED, 8, 0);

    std::ios::fmtflags cflag = std::cout.flags();
    std::ostringstream ss;
    ss << std::setw(2) << std::setfill('0') << (timems / 1000) % 60;
    std::string s(ss.str());
    std::ostringstream mm;
    mm << std::setw(2) << std::setfill('0') << (timems / 1000 / 60) % 60;
    std::string m(mm.str());
    std::ostringstream hh;
    hh << std::setw(2) << std::setfill('0') << (timems / 1000 / 60 / 60);
    std::string h(hh.str());
    std::cout.flags(cflag);

    const std::string time_string = h + ":" +  m + ":" + s + ":" + std::to_string(timems % 1000);

    cv::putText(cvbuffer, time_string, cv::Point(10,50), cv::FONT_HERSHEY_SIMPLEX, 1.2, cv::Scalar(0,244,0, 255), 2, CV_AA);

    webrtc::I420Buffer* dst_buffer = buffer.get();
    libyuv::ConvertToI420(
        cvbuffer.ptr(),CalcBufferSize(webrtc::VideoType::kARGB, buf_width, buf_height),
        dst_buffer->MutableDataY(), dst_buffer->StrideY(),
        dst_buffer->MutableDataU(), dst_buffer->StrideU(),
        dst_buffer->MutableDataV(), dst_buffer->StrideV(),
        0, 0,
        buf_width, buf_height,
        dst_buffer->width(),dst_buffer->height(),
        libyuv::kRotate0,
        libyuv::FOURCC_ARGB);

    webrtc::VideoFrame frame(buffer, 0, timems, webrtc::kVideoRotation_0);
    frame.set_ntp_time_ms(0);

    OnFrame(frame, buf_width, buf_height);

}
```

できた

![pacman.png](https://qiita-image-store.s3.amazonaws.com/0/120507/9196b0bc-c780-9b30-5d74-c69188a7168e.png)