WebRTCを用いた遠隔作業支援システムを作ります。 前回 はシグナリング処理からスマートグラスのカメラ映像をリアルタイムで監視端末に表示する部分までを解説しました。最終回の今回は、DataConnectionを用いたテキストメッセージや画像の転送処理、及び接続終了処理について解説し、遠隔作業支援システムとして完成させます。
DataConnectionによるデータ転送の処理フロー
前回 までで、信頼性の有る状態でDataConnectionのP2P接続が確立しました。では実際に、監視端末からスマートグラスへデータを転送します。
遠隔作業支援システムではDataConnectionを通じて以下3種類のデータを転送しますが、P2Pの通信経路を通すためにはデータをシリアライズしなければなりません。
- イベント通知
- テキストメッセージの転送
- 画像の転送
転送データのシリアライズ
PeerJSはDataConnectionを確立する際のオプションとして、データのシリアライズ方式を選択できます。デフォルトはBinaryPack形式でpack/unpackを行う binary で、下記のような複雑なJavaScriptオブジェクトもバイナリシリアライズして送信することができます。
// http://peerjs.com/docs/#apiを参照 conn.send({ strings: 'hi!', numbers: 150, arrays: [1,2,3], evenBinary: new Blob([1,2,3]), andMore: {bool: true} });
binary 以外にも、UTF8文字列をバイナリ変換してpack/unpackすることで文字化けを防ぐ(が遅くなる) binary-utf8 や、JSONとしてシリアライズする json 、及び何もしない none が選択できます。今回の検証ではデフォルトのbinaryで問題ありませんでしたが、転送したデータが壊れてしまったり文字化けしてしまった場合には、シリアライズ方式もチェックすべきでしょう。
転送データの形式
今回の遠隔作業支援システムでは、以下のようなオブジェクトを転送することにします。
type は転送されるデータの種類、 payload は転送する実際のデータです。
{type:<転送されるデータの種類>, payload:<転送する実際のデータ>}
type
以下のような列挙型のようなオブジェクトによって、転送されるデータの種類を示します。
(JavaScriptには列挙型が無いため、実際は普通のStringオブジェクトが転送されるだけですが)
# DataConnection経由で転送されるデータの種類 TYPE = event: "event" message:"message" image: "image"
payload
転送するデータ自身です。テキストメッセージの場合は文字列そのものですし、画像データの場合はBase64でエンコードされたDataURIスキーム文字列を格納します。イベントの場合は、以下のような列挙型もどきのオブジェクトによって定義された文字列です。
# DataConnection経由で指示されるイベント EVENT = mic: on: "mic-on" off: "mic-off"
今回の遠隔作業支援システムでは「マイクのON/OFF」というイベントしか定義していません。
監視端末:データ送信処理
DataConnectionが確立していますので、 DataConnection.send(data) メソッドを用いればデータを送信できます。
実際のデータ送信処理は MonitorClass.__send(type, payload) メソッドにて実装されており、マイクON/OFFのイベント通知( MonitorClass.toggleMIC() )やテキストメッセージの転送( MonitorClass.sendMessage(message) )、画像の転送( MonitorClass.sendImage(image) )は__sendを適切に呼び出すだけです。
入力フィールドから転送すべきテキストメッセージを取得する機能や、videoタグに表示されるリアルタイム映像をcanvasにキャプチャしマウスで指示を描く機能、及びその画像データをDataURIスキームで文字列化する機能はmonitor.coffeeで実装しています。
ただしWebRTCには依存しない部分ですので、本稿ではその部分の解説を省略します。詳細な実装は monitor.coffeeのコード を参照ください。
class MonitorClass extends BaseClass ... toggleMIC: -> # マイクON/OFFのイベント送信 state = @ls.getAudioTracks()[0].enabled console.log "toggleMIC state:#{state}" @ls.getAudioTracks()[0].enabled = !state if state @__send TYPE.event, EVENT.mic.off else @__send TYPE.event, EVENT.mic.on sendMessage: (message) -> # テキストメッセージの送信 console.log "sendMessage: #{message}" @__send TYPE.message, message sendImage: (image) -> # 画像の送信 console.log "sendImage: #{image}" @__send TYPE.image, image __send: (type, payload) -> # 送信処理 if @edc? and @edc.open # DataConnectionが確立し利用可能な場合はデータを送信 data = {type:type, payload:payload} # 転送オブジェクト組み立て @edc.send data # データ送信 console.log "sent object:#{JSON.stringify data}" else # 何らかの理由でDataConnectionが利用できない場合はエラー発生 console.log "dataConnection is lost" @eh "dataConnection is lost" if @eh?
スマートグラス:データ受信処理
前回 解説しましたが、DataConnection経由でデータを受信すると DataConnection.on('data', callback) イベントが発生します。そのイベントハンドラ内で、受信したデータの type によって処理を分岐します。
イベント受信時の処理は DeviceClassに定義された DeviceClass.__eventHandler(event) メソッドに委譲していますが、テキストメッセージ受信時は、device.coffeeから渡される messageHandler(data.payload) へ処理を委譲します。同様に画像受信時も、device.coffeeから渡される imageHandler(data.payload) へ処理を委譲します。
テキストメッセージ受信時に画面にそのメッセージを表示する機能や、DataURIスキームで文字列化された画像データを画面に表示する機能はdevice.coffeeで実装しています。
ただしWebRTCには依存しない部分ですので、本稿ではその部分の解説を省略します。詳細な実装は device.coffeeのコード を参照ください。
class DeviceClass extends BaseClass ... onConnection: (messageHandler = null, imageHandler = null)-> ... @peer.on 'connection', (dataConnection) => ... dataConnection.on 'data', (data) => # 接続相手先からデータを受信した際の処理 console.log "dataConnection.on 'data' #{JSON.stringify data}" switch data.type when TYPE.event # イベント受信時は、__eventHandlerに処理を委譲 console.log "event received:#{data.payload}" @__eventHandler data.payload when TYPE.message # テキストメッセージ受信時は、device.coffeeから渡されるmessageHandlerへ処理を委譲 console.log "message received:#{data.payload}" messageHandler data.payload if messageHandler when TYPE.image # 画像受信時は、device.coffeeから渡されるimageHandlerへ処理を委譲 console.log "image received:#{data.payload}" imageHandler data.payload if imageHandler else # 上記以外のtypeの場合は何もしない console.log "unknown data type" ... __eventHandler: (event) -> # イベント受信時の処理 console.log "__eventHandler event:#{event}" switch event when EVENT.mic.on # マイクONイベントを受信 console.log "event: mic-on" @ls.getAudioTracks()[0].enabled = true when EVENT.mic.off # マイクOFFイベントを受信 console.log "event: mic-off" @ls.getAudioTracks()[0].enabled = false else # 上記以外のイベントを受信した場合は何もしない console.log "event: unknown"
スマートグラスへ音声・テキストメッセージ・画像で指示を出す
このようにDataConnectionを活用することにより、トラブル発生時に音声やテキストメッセージ、画像によって監視端末から指示を出すことができるようになります。
監視端末の「MIC ON」ボタンをクリックすることで、スマートグラスと監視端末双方のマイクがONになり、作業者と支援者が音声で会話をすることが可能となります。
監視端末の入力フォームに指示メッセージを入力し「SEND MESSAGE」ボタンをクリックすることで、スマートグラスにはdevice画面④(下図)のようにメッセージが表示されます。
また監視端末でリアルタイム映像をキャプチャして静止画にし、monitor画面⑤(下図右)のようにマウスで指示を描き「SEND IMAGE」ボタンをクリックすることで、スマートグラスにはdevice画面⑤(下図左)のような指示画像が表示されます。
作業終了時の処理フロー
最後に作業終了時の処理です。
P2P接続経路の破棄
スマートデバイスと監視端末のどちらでもかまわないので「END CALL」をクリックすると、DiveceClassやMonitorClassの closeCall() メソッドが実行されます(下図はスマートデバイスから「END CALL」した場合です)。
$('#end-call').click -> dc.closeCall() waiting()
$('#end-call').click -> mc.closeCall() waiting()
DiveceClassやMonitorClassの closeCall() メソッドでは、MediaConnectionとDataConnectionをcloseします。
MediaConnectionやDataConnectionはどちらかの側からcloseされると、双方で MediaConnection.on('close', callback) イベントや DataConnection.on('close', callback) イベントが発生するため、 前回 説明したように逆側の MediaConnection.close() と DataConnection.close() も実行されて通信経路が破棄されることになります。
class BaseClass ... closeCall: -> console.log "closeCall" @emc.close() if @emc? @edc.close() if @edc? ...
シグナリングサーバとの接続破棄+MediaStreamの破棄
最後にスマートデバイスと監視端末双方で「TERMINATE」をクリックすると、DeviceClassやMonitorClassの terminate() メソッドが起動します。
$('#terminate').click -> dc.terminate() window.open('about:blank', '_self').close()
$('#terminate').click -> mc.terminate() window.open('about:blank', '_self').close()
DeviceClassやMonitorClassの terminate() メソッドを呼び出した後、自身のHTML自体を閉じることで、initialize時に接続したMediaStreamが破棄されます。
class BaseClass ... terminate: -> console.log "terminate" @emc.close() if @emc? @edc.close() if @edc? @peer.destroy() if @peer? ...
DeviceClassやMonitorClassの terminate() メソッドでは、もしまだP2P接続が確立していたら切断した後、 @peer.destroy() を実行します。これにより、Peer初期化時にpeerjs-serverに登録されたユニークIDが破棄され、WebSocket接続も閉じられます。
これらの終了処理を適切に実行することで、クライアント側・サーバ側共にリソースの再利用が可能となります。
最後に
全5回( 第1回 第2回 第3回 第4回 今回 )に渡った「WebRTC(PeerJS)で遠隔作業支援システムを作る」シリーズも、これで最終回となりました。
P2P通信を旨とするWebRTCを用いることで、これまで扱うことが困難だったリアルタイムデータの送受信が簡単に実現できます。これまでのWebの常識にとらわれないエキサイティングなシステムの実現に向け、本稿が一助になれば幸いです。