HaxeでWebRTCを扱う

この記事では、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が省かれています。

```js:JavaScript
var pc = new RTCPeerConnection(pcOption);
```

```hx:Haxe
var pc = new PeerConnection(pcOption);
```

### ベンダープリフィックスは付かない

コンパイルされて出来るjsファイルは、ベンダープリフィックスの付いていない純粋なものになっています。2015年2月8日時点で、ベンダープリフィックスのないWebRTCコードはいずれのブラウザーでも動作しません。

私は、これを[Initialization Magic](http://old.haxe.org/doc/advanced/magic#initialization-magic)を用いて解決しました。以下はMainクラスに書き加える場合のサンプルコードです。

```hx:Main.hx
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](http://w3c.github.io/webrtc-pc/)の定義と異なります。

```hx:VoidCallback.hx
typedef VoidCallback = Void -> Bool;
```

```hx:ErrorCallback.hx
typedef ErrorCallback = String -> Bool;
```

```hx:SessionDescriptionCallback.hx
typedef SessionDescriptionCallback = SessionDescription -> Bool;
```

試しに、標準ライブラリを書き換えて返り値をVoid型としても動作します。どうしてHaxeでは異なる定義としているのか、私には理解できませんでした。後述するサンプルコードでは、標準ライブラリは書き換えずに、__すべてtrueを返しています__。

## 3. サンプルコード

以下に、簡単なシグナリング処理のサンプルコードを掲載します。なお、クライアント同士のマッチング処理やエラー処理などは省略しているため、これをコピペしても動作はしません。実際に動作する完全なコードは[gitHub](https://github.com/seibe/Haxe_WebRTC)を参照ください。

```hx:Main.hx
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;
	}
}
```

以上です。