Help us understand the problem. What is going on with this article?

Native WebRTCでも手動シグナリングがしたい!

More than 1 year has passed since last update.

これは何?

Linux上にnative webrtcなプログラムをビルドして、オレオレMCUとかOpenCVで動画加工するようなツールを作りたい!
そのためにクライアントとサーバーでwebrtcをループパックするプログラムを作りました。
遅延は0.2-0.3秒くらいです。

特にnative webrtcの情報はほとんどないか古くて使えないので、その辺に絞って解説したいと思います。

プログラム

githubにアップしました。

手動シグナリングがしたい!

native webrtcにはpeer_connection_clientが標準でついていますが、フローがわかりにくい上にhttpsにすら対応していないという感じです。
そこで、native-webrtcを始めるにあたりwebrtc界隈では誰もが通ると思われる手動シグナリングから始めたらいいのではないかと思います。
幸い、c++には標準入力と標準出力があります。いきなりサーバー機能やwebsocketを実装しようとすると気が遠くなりますが、iostreamなら簡単です。

処理の流れ

大まかにいうと、クライアントがカメラを取得してofferを作成し、サーバーに渡します。
するとサーバー側がanswerを返し、クライアントのストリームをそのまま返すofferも返します。

libwebrtcのインストール方法

Google is your friend! 公式ページ

プログラム解説

まず、プログラムはmain.cpp一つです。
使うのは主にPeerConnectionクラスですが、イベントを処理するために PeerConnectionObserverを継承してそこに処理を実装していきます。

ただ、普通にクラスを分けて処理を実装すると処理の流れがわかりにくくなるので、ラムダを使って順番に書いていきます。

class DummySetSessionDescriptionObserver

class DummySetSessionDescriptionObserver
: public webrtc::SetSessionDescriptionObserver {
public:
   static DummySetSessionDescriptionObserver* Create() {
        return new rtc::RefCountedObject<DummySetSessionDescriptionObserver>();
    }
    virtual void OnSuccess() {
        LOG(INFO) << __FUNCTION__;
    }
    virtual void OnFailure(const std::string& error) {
        LOG(INFO) << __FUNCTION__ << " " << error;
    }

    protected:
    DummySetSessionDescriptionObserver() {}
    ~DummySetSessionDescriptionObserver() {}
};

サンプルのクラスそのままですが、これはjsでいうとsetLocalDescription() または pc.setRemoteDescription() のコールバックです。
特に何もする必要はないのでログを出しています。

      pc.setLocalDescription(desc, ここの処理);

class PeerConnectionCallback : public webrtc::PeerConnectionObserver{}

class PeerConnectionCallback : public webrtc::PeerConnectionObserver{
private:
   ... // コンストラクタなど
protected:
  void OnSignalingChange(webrtc::PeerConnectionInterface::SignalingState new_state) override{
  }
  void OnDataChannel(rtc::scoped_refptr<webrtc::DataChannelInterface> channel) override {}
  void OnRenegotiationNeeded() override {
  }
  void OnIceConnectionChange(webrtc::PeerConnectionInterface::IceConnectionState new_state) override{
  };
  void OnIceGatheringChange(webrtc::PeerConnectionInterface::IceGatheringState new_state) override{
  };
  void OnIceConnectionReceivingChange(bool receiving) override {
  }

  void OnIceCandidate(const webrtc::IceCandidateInterface* candidate) override {
     // vanilla ICE を使うのでここでofferを作成
    std::string msg;
    candidate->ToString(&msg);
      LOG(INFO) << __FUNCTION__ << " " << msg;

    std::string sdp;
    pc->local_description()->ToString(&sdp);

        std::cout << "\n\n" << "3 -------------------- send server offer ---------------------" << std::endl;
                std::cout << sdp << std::endl;
        std::cout << "3 -------------------- send server offer ---------------------" << std::endl;
  };
  void OnRemoveStream(rtc::scoped_refptr<webrtc::MediaStreamInterface> stream) override {
  };
  void OnAddStream(rtc::scoped_refptr<webrtc::MediaStreamInterface> stream) override {
    onAddStream(stream);
  };

};

このクラスが主なロジックにあたります。
jsで言う所の pc.onaddstream() や pc.onicecandidate() の処理をこのクラスに実装しています。
ただし、今回はラムダで後から処理を書き換えられるようにしています。

そして、trickle ice (経路候補が見つかり次第 ice candidateを相手に渡す方式) ではなくvanilla ice (ice candidateが一通り揃ってからSDPを投げる)にするため、
OnIceCandidate()でoffer SDPを出力しています。

class CreateSDPCallback : public webrtc::CreateSessionDescriptionObserver {}

jsでいうと

pc.createOffer(これ, これ);

です。
javascriptっぽくコールバックできるように実装してあります。

class CreateSDPCallback : public webrtc::CreateSessionDescriptionObserver {
    private:
                // コールバック
        std::function<void(webrtc::SessionDescriptionInterface*)> success;
        std::function<void(const std::string&)> failure;
    public:
                // コンストラクタでコールバックをセットする
        CreateSDPCallback(std::function<void(webrtc::SessionDescriptionInterface*)> s, std::function<void(const std::string&)> f)
      : success(s), failure(f) {
        };
                // あとは呼ぶだけー
        void OnSuccess(webrtc::SessionDescriptionInterface* desc) {
                success(desc);
        }
        void OnFailure(const std::string& error) {
            if (failure) {
                failure(error);
            } else {
                LOG(LERROR) << error;
            }
        }
};

rtc::scoped_refptrwebrtc::PeerConnectionInterface CreatePeerConnection(webrtc::PeerConnectionObserver* observer) {}

Factoryパターンを使ってPeerConnectionを生成しています。
後々、キャプチャークラスを実装したりするときに触ります。

stun turnにつなぐ場合はここで設定します。
ちなみにjanusやmediasoupと接続するのにturnが必須な環境から接続する場合でもturn動かさなくて繋がったします。謎。

  webrtc::PeerConnectionInterface::RTCConfiguration config;
  webrtc::PeerConnectionInterface::IceServer server;

  /*
  server.uri = "stun.l.google.com:19302";
  config.servers.push_back(server);
*/

  server.uri = "turn:localhost:3478?transport=udp";
  //server.uri = "turn:xxx.xxx.xxx.xxx:3478?transport=udp";
  server.username = "xxx";
  server.password = "xxx";
  config.servers.push_back(server);

  return peer_connection_factory->CreatePeerConnection(
      config, &constraints, NULL, NULL, observer);

main()

やっとmain()ですね。
ここにjavascriptっぽくロジックを書きます。
流れはjsとほとんど変わりません。

要点だけ書きます。

int main() {

  // PeerConnection作成
  rtc::scoped_refptr<webrtc::PeerConnectionInterface> peer_connection = CreatePeerConnection(peer_connection_callback);

  // receive offer sdp
  {
            std::string input;
            do {
              std::getline(std::cin, input);
              if (input != "") {
                sdp += input + '\n';
              }
            } while (input != "");

        webrtc::SessionDescriptionInterface* session_description(webrtc::CreateSessionDescription("offer", sdp, &error));

    // おおよそここでonaddstream()が走ります。
    peer_connection_callback->SetOnAddStream(
      [&](rtc::scoped_refptr<webrtc::MediaStreamInterface> stream){
        peer_connection->AddStream(stream);
    });

    peer_connection->SetRemoteDescription( DummySetSessionDescriptionObserver::Create(), session_description);
  }


  // create anser
  peer_connection->CreateAnswer(new rtc::RefCountedObject<CreateSDPCallback>(
    [&](webrtc::SessionDescriptionInterface* desc) {
      peer_connection->SetLocalDescription( DummySetSessionDescriptionObserver::Create(), desc);
      std::cout << sdp << std::endl;

      // 続いてループバック用のofferを作ります。
      peer_connection->CreateOffer(new rtc::RefCountedObject<CreateSDPCallback>(
      [&](webrtc::SessionDescriptionInterface* desc){
        peer_connection->SetLocalDescription( DummySetSessionDescriptionObserver::Create(), desc);

      // ここでSDPを出力してしまう ice candidate がありません。trickle iceにする場合は良いのですが、今回はvanillaにするので、上記で設定したOnIceCandidate()を待ちましょう。

      // 続いてanswerの待ち受けに入ります。
      {
        std::string sdp;
        {
          std::string input;
          do {
            std::getline(std::cin, input);
            if (input != "") {
              sdp += input + '\n';
            }
          } while (input != "");
        }
        webrtc::SessionDescriptionInterface* session_description(webrtc::CreateSessionDescription("answer", sdp, &error));
        peer_connection->SetRemoteDescription( DummySetSessionDescriptionObserver::Create(), session_description);

      // これでpeer connectionが繋がります!
      }
    }
  }

  // あれ? このまま終了しても良いの??

  return;
}

こうして抜粋してみるとそこそこ短いですね。
最後にマルチスレッド系では必須のメッセージ処理です。
windowsなら GetMessage()なんかをwhile()しますが、サンプル見てもよくわかりません。
もちろん、nodejsよろしくそのままreturnしてしまうとプログラムは終了します。

で、仕方がないので webrtc/base/thread.cc を見ていくと Run() があり、これを呼べば良さそうです。

  (rtc::Thread::Current())->Run();
    return 0;

これで完成です!
クライアントから送った映像と音声がサーバーからループバックされるだけのシンプルなプログラムができました。

さて、次回は手動シグナリングを自動シグナリングにします。
websocketpp0.7.0を使いました。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした