LoginSignup
10
4

More than 5 years have passed since last update.

SkyWay WebRTC Gatewayハンズオン Chapter3

Last updated at Posted at 2018-09-18

ハンズオン のChapter 3

Chapter 3 - DataChannel

この章ではDataのやり取りについて実装します。
最初の図の通り、今回はブラウザからWebRTC Gatewayへのデータ送信のケースです。

SkyWayではconnectメソッドを実行すると自動的にDataChannelの確立が行われます。
したがってデータに関してはやることは少ないです。

完成したソースコードはgithubを確認して下さい。

JavaScript側

connect

HTML
<input type="text" id="chat_box"></input><button id="chat_button">send message</button>

メッセージを送るboxと送信ボタンを足しました。

JavaScript
        // (1)
        const connection = peer.connect(target_id, {
            serialization: "none"
        });
        // (2)
        connection.on('data', (data)=> {
            console.log(data);
        });
        // (3)
        document.getElementById("chat_button").onclick = ()=> {
            const message = document.getElementById("chat_box").value;
            connection.send(message);
        };

相手側とDataChannelを確立するためにconnectを実行します。
ここではserializeはnoneを選択するのが手軽でしょう。(1)
理由については後述します。

相手側からのデータの受信処理です。
今回とくに利用しないので、logだけ吐いておきます。 (2)

相手側へのデータ送信処理です。
sendボタンが押されたときに発火し、テキストボックスの中身の文字列をそのまま送っています。 (3)

ブラウザ側はこれでおしまいです。

WebRTC Gateway側

Mainの処理

util.rb, peer.rb, media.rbに関しては2章から変更はありません。

WebRTC Gateway側では、実は特になにもしなくても接続自体は確立されます。
接続が確立されたことを検知し、データを最終的に処理するエンドユーザプログラムへの転送設定を行います。
またこちらからデータを送り返したい場合に備えて、返信の設定もします。

では手順を見ていきましょう。

webrtc_control.rbの一部

def on_open(peer_id, peer_token)
  (video_id, video_ip, video_port) = create_media(true)
  #(1)
  (data_id, data_ip, data_port) = create_data

  p_id = ""
  mc_id = ""
  th_call = listen_call_event(peer_id, peer_token) {|media_connection_id|
    mc_id = media_connection_id
    answer(media_connection_id, video_id)
    cmd = "gst-launch-1.0 -e rpicamsrc ! video/x-raw,width=640,height=480,framerate=30/1 ! videoconvert ! vp8enc deadline=1  ! rtpvp8pay pt=96 ! udpsink port=#{video_port} host=#{video_ip} sync=false"
    p_id = Process.spawn(cmd)
  }

  #=====================本章追加部分  start==================
  #(2)
  dc_id = ""
  th_connect = listen_connect_event(peer_id, peer_token) {|data_connection_id|
    dc_id = data_connection_id
    #(3)
    set_data_redirect(data_connection_id, data_id, "127.0.0.1", 10000)
  }
  #=====================本章追加部分  end==================

  th_call.join
  th_connect.join

  #(4)
  [mc_id, dc_id, p_id]
end

まず相手側へ送り返すデータをエンドユーザから受け取る設定をします。
ここで生成したdata_ip, data_portに対して、エンドユーザがUDPで送信したデータを相手側へ転送します。(1)

相手側からDataChannelの確立要求があったことを検知します。
MediaStreamのイベント検知の邪魔にならないように、非同期処理で待受を行います。
MediaStream側も非同期処理で待ち受けるようにしたので、順番を入れ替えても問題ありません。(2)

相手側から送られてきたデータの転送先を設定します。
この例ではエンドユーザプログラムが127.0.0.1、ポート5000番でデータを取得できます。(3)

終了処理のためdata_connection_idを返しておきます(4)
(media_connection_idとpeer_idも一緒に返しています)

以上で一連の流れは終了です。
以下詳細について記載します。

Data系

data.rbの一部

def create_data
  #open datasocket for sending data
  res = request(:post, "/data", '{}')
  if res.is_a?(Net::HTTPCreated)
    json = JSON.parse(res.body)
    data_id = json["data_id"]
    ip_v4 = json["ip_v4"]
    port = json["port"]
    [data_id, ip_v4, port]
  else
    # 異常動作の場合は終了する
    p res
    exit(1)
  end
end

POST /dataにアクセスすることでデータの待受ポートを開放します。
実際に開放されたIPアドレスとポート番号、識別のためのdata_idを受け取ります。

data.rbの一部

def listen_connect_event(peer_id, peer_token, &callback)
  async_get_event("/peers/#{peer_id}/events?token=#{peer_token}", "CONNECTION") { |e|
    data_connection_id = e["data_params"]["data_connection_id"]
    callback.call(data_connection_id)
  }
end

相手側からDataChannelの確立要求があったことは/peers/{peer_id}/eventsを監視することで確認できます。
確立されたDataChannelにはdata_connection_idが割り当てられるので取得します。

data.rbの一部

def set_data_redirect(data_connection_id, data_id, redirect_addr, redirect_port)
  params = {
      #for sending data
      "feed_params": {
          "data_id": data_id,
      },
      #for receiving data
      "redirect_params": {
          "ip_v4": redirect_addr,
          "port": redirect_port,
      },
  }

  res = request(:put, "/data/connections/#{data_connection_id}", JSON.generate(params))
  p res
end

データの転送設定です。送り返すデータもここで与えます。
どのDataChannelに関する話なのか特定するためにdata_connection_idを受取、相手側へ転送するデータを特定するためdata_idを受け取っています。
あとはredirect先のアドレスとポート番号を指定し、PUT /data/connections/{data_connection_id}を叩いています。

以上で終了です。
最後に開放処理を記載します。

data.rbの一部

def close_data(data_connection_id)
  res = request(:delete, "/data/connections/#{data_connection_id}")
  if res.is_a?(Net::HTTPNoContent)
    # 正常動作の場合NoContentが帰る
  else
    # 異常動作の場合は終了する
    p res
    exit(1)
  end
end

実行

0章で作成したLチカアプリケーションを起こしておいて、

$ sudo ruby rasbpi.rb 

まずWebRTC Gatewayを動かします

$ ./gateway_linux_arm

次にコントローラーを動かします。API_KEYを忘れずに設定して下さい。

$ export API_KEY=YOUR_API_KEY
$ ruby webrtc_control.rb PEER_ID

index.htmlとJavaScriptのあるディレクトリでWebサーバを立ち上げます。サーバプログラムはなんでもいいですが今回はpythonで動かしています

$python -m SimpleHTTPServer 9000

あとはブラウザで http://localhost:9000/index.html?key=YOUR_API_KEY を開き、ラズパイ側のpeer_idを入れてcallボタンを押して下さい
Chapter 0
で実装したLチカアプリケーションの待受ポートに対して転送を行ってください。
(本ハンズオンガイドでは別々のプログラムとして動かしていますが、実用の際は同じアプリケーションとしてまとめることになると思います。)

まず画面が出ます。
その後onと入力してsend messageボタンを押して下さい。
LEDが光りましたね!

962196b4-217a-be41-02a0-2b95bcb11344.png

これで全世界から自由にあなたのLEDを点灯できるようになりました。
今回はデバイス側が非常に簡単ですが、面白い回路を組めば組むほど遠隔操作の価値も高まります。いろんなものを作ってみて下さい。

備考-serializeについて

(この項目については理解できなくても大丈夫です。シリアライズをnoneを利用するようにして貰えれば利用可能です。発展的な利用をする方のために以下記載します。)

上記の手順で、serializeはnoneにするように記載しました。
SkyWayでは、DataChannelで送信するデータは特定のフォーマットでシリアライズして転送することができ、利用できるのはBinaryPackとJSONです。
これらを利用する場合、JavaScript版ではデータを自動的に変換して送ってくれますが、WebRTC Gatewayでは変換は行いません。

JavaScript版はJS内で生成したデータのため、データのフォーマットを知っています。例えば

var obj = {
 hoge: "moge"
 len: 10
};
con.send(obj);

のような送り方ができますが、これはJavaScriptがobjのフォーマットを理解している(フィールドにhogeとlenを持っていることを把握している)から変換が行なえます。

WebRTC Gatewayの場合エンドユーザが外部から送信してきたUDPデータを単なるバイナリ列として受信するため、データのフォーマットがわからず、従って変換が行なえません。
シリアライズをbinarypackにする場合は、エンドユーザプログラム内でBinaryPackやJSONでエンコードして送る必要があります。
逆に言えば、送信側エンドユーザプログラム内できちんとエンコード&デコードすればserializeは何でも構いません。JavaScriptとも相互通信可能です。

10
4
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
10
4