概要
前回はSkyWayサンプルプログラムを使ってP2Pのビデオ通話システムを構築しました。
ですが、その時はNoodlを簡易Webサーバとして利用しただけで、Noodl本来のビジュアルプログラミング部分は活用できていませんでした。
今回はSkyWayのサンプルプログラムをNoodl上のJavascriptノードに実装することができたので、その内容をまとめておきます。
1. 完成画面とサンプルデータ
今回は下記のような画面を作ります。
今回の完成画面イメージ
ほぼサンプルと同じデザインです。画面下のメッセージボックスはMQTTを使ったシンプルなメッセンジャとなっています。
左の送信BOXに入力した内容をパブリッシュ。右の受信BOXでサブスクライブしています。
ビデオ通話を開始する前に相手と確認を行ったり、どちらかのPeerIDを送る用途で使えます。
サンプルデータ
プロジェクトファイルはこちらからCloneできます。デモサイトも用意しています。
※デモサイトはAPIキー、MQTTサーバの設定をしていないためそのまま使用することはできません。
プロジェクトファイルを自身の環境で使う場合には、Cloneしたフォルダのにある/Noodl2_WebRTCDemo
のフォルダをImportしてください。
また、JavascriptノードのSKYWAY_KEYにSkyWayのサイトで取得したAPIキーを入力してください。
2. SkyWayのセットアップ
まずはSkyWayのサンプルを作るための事前準備を行います。SkyWayを利用するためには使用するドメインの設定とAPIキーの取得が必要です。下記手順でAPIキーの取得まで実施してください。
- SkyWayのサイトでアカウントを作成
- アプリケーションの作成:アプリを利用可能とするドメイン追加
- APIキーの取得
詳しくは前回の記事を参照ください。
3. Noodlで画面を作る
Noodlを起動して2つのコンポーネントを作ります。今回は2つ作りました。
- Button :ボタンのコンポーネント
- Main :今回作成した画面コンポーネント
開発方針としては、UI部分はNoodlを使い。WebCamの操作含めビデオ通話にかかわる部分は丸ごと1つのJavascriptノードに入れています。流用するときはこのJavascriptノードだけをコピペすれば使えるイメージです。
Buttonコンポーネント
画面で使うボタン部品を作成します。ボタンを押すと色が変わりclick信号を発火する機能を持っています。
各ノードの設定はサンプルプロジェクトを確認してください
Noodlでノード設定後、下図のようにつなぎます。
Mainコンポーネント
メインとなる画面コンポーネントを作成します。
各ノードの設定はサンプルプロジェクトを確認してください。
ここでは通信にかかわるMessageノードとJavascriptノードを説明します。
Messageノードの設定
MQTTメッセンジャを作るためSend Messageノード、Recive MessageノードをText Inputノードに接続します。
送信したメッセージが受取れるよう、MessageノードのTopix:p2pMedia(任意)を合わせ、PAYLOADにポートを追加します。今回はmsg(任意)を追加しました。
また、Send MessageはSend On Changeのチェックを付けて、 ポート変更時に送信するようにしています。
MQTTサーバを設定します。丸と丸がつながったようなアイコンがMESSAGE BROKERSの設定となります。デフォルトではNoodl上の簡易MQTTサーバが設定されています。Noodlで実験する場合はこのままでOKです。
外部のMQTTサーバを使うときにはパネル下部にあるCREATE NEW BROKERボタンから外部MQTTサーバの説明、URLを設定してください。
現状のNoodl2.0はWorkSpaceを共有しているため、作成したMQTTサーバの設定まで共有してしまいます。
最終的にDeployする直前にだけMQTTサーバの設定を行って。Deploy後は設定を削除しておきましょう。
Javascriptノードの設定
ビデオ通話部分のJavascriptノードには下記スクリプトをコピペします
define({
inputs: {
mySignal: 'signal',
SKYWAY_KEY: 'string',
callTrigger: 'signal',
closeTrigger: 'signal',
localVideo: 'domelement',
remoteVideo: 'domelement',
remoteId: 'string',
},
outputs: {
localId: 'string',
},
//const mediaConnection;
closeTrigger: function (inputs, outputs) {
mediaCon.close(true);
},
callTrigger: function (inputs, outputs) {
console.log("call");
if (!peer.open) {
return;
}
const mediaConnection = peer.call(inputs.remoteId, localStr);
mediaCon = mediaConnection;
mediaConnection.on('stream', async stream => {
// Render remote stream for caller
inputs.remoteVideo.srcObjec = stream;
inputs.remoteVideo.playsInline = true;
await inputs.remoteVideo.play().catch(console.error);
console.log("--StartPlay");
});
mediaConnection.once('close', () => {
inputs.remoteVideo.srcObject.getTracks().forEach(track => track.stop());
inputs.remoteVideo.srcObject = null;
});
},
// All signal inputs need their own function with the corresponding name that
// will be run when a signal is received on the input.
mySignal: function (inputs, outputs) {
let Peer = window.Peer;
let self = this;
(async function main() {
const localStream = await navigator.mediaDevices
.getUserMedia({
audio: true,
video: true,
})
.catch(console.error);
localStr=localStream;
// Render local stream
inputs.localVideo.muted = true;
inputs.localVideo.srcObject = localStream;
inputs.localVideo.playsInline = true;
await inputs.localVideo.play().catch(console.error);
const peer = (window.peer = new Peer({
key: inputs.SKYWAY_KEY,
debug: 3,
}));
pee=peer;
// Register callee handler
peer.once('open', id => {
outputs.localId = id;
self.flagOutputDirty("localId");
//self.runNextFrame();
});
// Register caller handler
peer.on('call', mediaConnection => {
mediaCon = mediaConnection;
mediaConnection.answer(localStream);
mediaConnection.on('stream', async stream => {
// Render remote stream for callee
inputs.remoteVideo.srcObject = stream;
inputs.remoteVideo.playsInline = true;
inputs.remoteVideo.playsInline = true;
await inputs.remoteVideo.play().catch(console.error);
});
mediaConnection.once('close', () => {
inputs.remoteVideo.srcObject.getTracks().forEach(track => track.stop());
inputs.remoteVideo.srcObject = null;
});
});
peer.on('error', console.error);
})();
},
// This function will be called when any of the inputs have changed
change: function (inputs, outputs) {
// ...
}
})
let mediaCon;
let localStr;
Javascriptノードの解説
基本的にはSkyWayのサンプルコードのままです。
Noodl向けに変えたところは
- Javascriptノードの入出力を作っているところ。
- ボタンイベントで
addEventListener('click'
となっていたところをsignal
に変更 - 非同期で変更された出力をNoodlに反映させるために
flagOutputDirty("outputName")
の追加
ノードの接続
Noodlでノードを設定しつなぎます。
上からVideo部分、PeerID表示、接続・切断ボタン部分、MQTTチャット部分となっています。
ビデオ通話に関連するJavascriptノードの入出力は下記のようになっています。
Javascriptノードの入力:
- RemoteVideo:Dom Element を Javascript:remoteVideoに接続
- LocalVideo:Dom Element を Javascript:localvideoに接続
- 相手のPeerIDが入るTextInput:TextをJavascript:remoteIdに接続
- CallボタンButton:clickをJavascript:callTriggerに接続
- closeボタンButton:clickをJavascript:closeTriggerに接続
- 外部ライブラリ読込完了を通知するためScriptDlonloader:LoadedをJavascript:mySignalに接続
Javascriptノードの出力:
- 自分のPeerID表示用にJavascript:localIdをTextInput:Textに接続
最後に、JavascriptのSKYWAY_KEYにはSkyWayで取得したAPIキーを入力しておきます。これで完成です。
4. 実行
最後に動作チェックをします。
まずはNoodlの簡易Webサーバを使って確認しましょう。
Webcamはセキュリティの問題でlocalhostへのHTTP通信かHTTPS通信のサイトでなければ実行できません。
そのためURLはlocalhostと書き換えてアクセスしてください。http://localhost:8574/external/viewer/index.html?device=Main
初めてサイトにアクセスすると、カメラ・マイクへのアクセス許可確認のダイアログが表示されます。許可にして完了ボタンを押せばカメラの映像が表示されるはずです。
まとめ
これでSkyWayをNoodl上で使い、P2Pのビデオ通信を行うことができました。
・・・と言いたいのですが、実は上記内容は不具合があります。相手からの受信は問題なくできるのですが。こちらから呼び出しをした場合、相手側のWebCamの映像を受取れないのです。相手にはこちらの画像は見えていますが、こちらの画面には相手が見えません。
非同期処理の書き方でどこかが悪いのですが、理解できていないところがあり修正できませんでした。解決策わかりましたら続報します。
さらに、SkyWayにはSkyWay WebRTC Gatewayと言うものもありました。
こちらではブラウザ上での通信だけでなく、IoT機器との通信をサポートしているそうです。