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
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
# cd build
# make mac
動作確認
まず素の状態でのP2P動作を確認する。
momo起動
# cd ..
# ./momo p2p
ここでもサーバの立て方の問題でクライアント側がそのままではうまく行かなかったため、とりあえず固定値でJavascriptを書き換えた。
./html/webrtc.js
//const wsUrl = wsProtocol + location.host + '/ws';
const wsUrl = wsProtocol + 'localhost:8080/ws';
Webサーバ起動
# cd html
# python -m SimpleHTTPServer
Chrome で http://localhost:8000/p2p.html にアクセスすることで
Macのカメラの映像をWebRTC経由で見ることができた。
#FakeVideo
まず、映像をカメラのものから独自画像に差し替えられることを確認するため、自前の黒画像を送ることを試す。
ここでは、cricket::VideoCapturerを継承したCustomVideoCapturerを実装して、既存のVideoCapturerと差し替えることで実装を行う。
まずヘッダファイルを作成する。基本的には関数はoverrideとし、黒画像を作成するRender()関数を追加する。
./src/rtc/customcapturer.h を作成
#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
#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
#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
// 省略
// コンストラクタに変数初期化を追加
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
// capturer = createVideoCapturer();
_capturer = new CustomVideoCapturer();
std::unique_ptr<cricket::VideoCapturer> capturer(_capturer);
main関数では以下のようにrtc_manager変数を関数スコープから出し、
定期動作するtick()関数からrtc_manager->render();を呼ぶよう実装した。
./src/main.cpp
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を起動する
# ./momo --no-audio p2p
Webブラウザから変更前の場合同様に接続して黒画面が流れることが確認できた
独自画像送信
黒画面ではつまらないのでOpenCVを使ってパックマンを描いて送ってみる。
OpenCVをインストールする。
# brew install opencv
OpenCVを使えるようにするためMakeFileに下記のように追加する。
./Makefile
# OpenCV
CFLAGS += `pkg-config opencv --cflags`
LDFLAGS += `pkg-config opencv --libs`
# ユーザ指定のフラグを追加
CFLAGS += $(MOMO_CFLAGS)
CustomVideoCapturer::Render()を以下のように変更する
./src/rtc/customcapturer.cpp
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);
}
できた