この記事では、HaxeにおけるWebRTCの扱い方と注意点について記します。
1. HaxeでWebRTCを扱うには
Haxeは、標準ライブラリでWebRTCに対応しています。関連するクラスは、js.html.rtcパッケージに収められています。
js.html.rtcパッケージ
- DataChannel.hx
- DataChannelEvent.hx
- ErrorCallback.hx
- IceCandidate.hx
- IceCandidateEvent.hx
- LocalMediaStream.hx
- MediaStream.hx
- MediaStreamEvent.hx
- MediaStreamList.hx
- MediaStreamTrack.hx
- MediaStreamTrackEvent.hx
- MediaStreamTrackList.hx
- NavigatorUserMediaError.hx
- NavigatorUserMediaErrorCallback.hx
- NavigatorUserMediaSuccessCallback.hx
- PeerConnection.hx
- SessionDescription.hx
- SessionDescriptionCallback.hx
- StatsCallback.hx
- StatsElement.hx
- StatsReport.hx
- StatsResponse.hx
2. 注意点
JavaScriptでWebRTCを扱ったことのある方であれば、特に突っかかることもなくHaxeに移植可能です。ただし、以下の3点には注意が必要です。
型名にRTCは付かない
Haxeでは、@:native修飾子によって型名からRTCが省かれています。
var pc = new RTCPeerConnection(pcOption);
var pc = new PeerConnection(pcOption);
ベンダープリフィックスは付かない
コンパイルされて出来るjsファイルは、ベンダープリフィックスの付いていない純粋なものになっています。2015年2月8日時点で、ベンダープリフィックスのないWebRTCコードはいずれのブラウザーでも動作しません。
私は、これをInitialization Magicを用いて解決しました。以下はMainクラスに書き加える場合のサンプルコードです。
private static function __init__() : Void untyped {
window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate;
window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription;
}
もしも標準ライブラリの上書きに躊躇が無ければ、該当するクラスにそれぞれ書き加える方が、毎回書く必要がなくなるため便利でしょう。ただ、いずれもあまり美しくはないので、もっと良い方法があればご教示ください。
VoidCallbackの返り値はBool型
後述するサンプルコードで頻出するナントカCallback型は、返り値がBool型で定義されています。これは、返り値はVoid型であるとするWebRTC 1.0 EDの定義と異なります。
typedef VoidCallback = Void -> Bool;
typedef ErrorCallback = String -> Bool;
typedef SessionDescriptionCallback = SessionDescription -> Bool;
試しに、標準ライブラリを書き換えて返り値をVoid型としても動作します。どうしてHaxeでは異なる定義としているのか、私には理解できませんでした。後述するサンプルコードでは、標準ライブラリは書き換えずに、すべてtrueを返しています。
3. サンプルコード
以下に、簡単なシグナリング処理のサンプルコードを掲載します。なお、クライアント同士のマッチング処理やエラー処理などは省略しているため、これをコピペしても動作はしません。実際に動作する完全なコードはgitHubを参照ください。
package ;
import haxe.Json;
import js.Browser;
import js.html.rtc.DataChannel;
import js.html.rtc.IceCandidate;
import js.html.rtc.PeerConnection;
import js.html.rtc.SessionDescription;
import js.html.WebSocket;
import js.Lib;
class Main
{
private var _ws:WebSocket;
private var _wsUrl:String;
private var _pc:PeerConnection;
private var _pcOption:Dynamic;
private var _dc:DataChannel;
private var _dcLabel:String;
private var _dcOption:Dynamic;
static function main()
{
new Main();
}
public function new()
{
Browser.window.onload = init;
}
private function init(evt:Dynamic):Void
{
// 0. オプション設定
_wsUrl = "ws://localhost:8080";
_pcOption = {
"iceServers": [ { "url": "stun:stun.l.google.com:19302" } ]
};
_dcLabel = "dataChannelLabel";
_dcOption = {
ordered: false,
maxRetransmits: 0
};
// 1. RTCPeerConnectionを作成する
_pc = new PeerConnection(_pcOption);
_pc.onicecandidate = onIceCandidate;
_pc.ondatachannel = onDataChannel;
// 2. シグナリングサーバーに接続する
_ws = new WebSocket(_wsUrl);
_ws.onmessage = onMessageWs;
// 送信側であれば
if (isSender) {
// データチャネルを初期化する
_dc = _pc.createDataChannel(_dcLabel, _dcOption);
initDataChannel(_dc);
// セッション情報(オファー)を作成する
_pc.createOffer(onCreateSdp, onFailure);
}
}
private function initDataChannel(dc:DataChannel):Void
{
dc.onopen = function(evt:Dynamic):Void {
trace("open");
dc.send("Hello!");
};
dc.onclose = function(evt:Dynamic):Void {
trace("close");
};
dc.onmessage = function(evt:Dynamic):Void {
trace("message", evt.data);
};
}
private function onMessageWs(evt:Dynamic):Void
{
var msg:Dynamic = Json.parse(evt.data);
switch (msg.type) {
case "sdp":
// セッション情報を受け取る
var sd:SessionDescription = new SessionDescription(msg.data);
_pc.setRemoteDescription(sd, function():Bool {
// 受信側であれば、セッション情報(アンサー)を作成する
if (sd.type == "offer") _pc.createAnswer(onCreateSdp, onFailure);
return true;
}, onFailure);
case "candidate":
// 経路情報を受け取る
var candidate:IceCandidate = new IceCandidate(msg.data);
_pc.addIceCandidate(candidate);
default:
onFailure("予期しないメッセージの受信");
}
}
private function onCreateSdp(sd:SessionDescription):Bool
{
// 生成されたセッション情報を登録する
_pc.setLocalDescription(sd, function():Bool {
// 生成されたセッション情報を シグナリングサーバーを通して転送する
_ws.send( Json.stringify( { type: "sdp", data: sd } ) );
return true;
}, onFailure);
return true;
}
private function onFailure(error:String):Bool
{
trace(error);
return true;
}
private function onIceCandidate(evt:Dynamic):Void
{
if (evt && evt.candidate) {
// 生成された経路情報を シグナリングサーバーを通して転送する
_ws.send( Json.stringify( { type: "candidate", data: evt.candidate } ) );
}
}
private function onDataChannel(evt:Dynamic):Void
{
if (evt && evt.channel) {
// データチャネルを初期化する
_dc = evt.channel;
initDataChannel(_dc);
}
}
private static function __init__() : Void untyped {
window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate;
window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription;
}
}
以上です。