LoginSignup
7
6

More than 5 years have passed since last update.

HaxeでWebRTCを扱う

Posted at

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

JavaScript
var pc = new RTCPeerConnection(pcOption);
Haxe
var pc = new PeerConnection(pcOption);

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

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

私は、これをInitialization Magicを用いて解決しました。以下はMainクラスに書き加える場合のサンプルコードです。

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の定義と異なります。

VoidCallback.hx
typedef VoidCallback = Void -> Bool;
ErrorCallback.hx
typedef ErrorCallback = String -> Bool;
SessionDescriptionCallback.hx
typedef SessionDescriptionCallback = SessionDescription -> Bool;

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

3. サンプルコード

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

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;
    }
}

以上です。

7
6
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
7
6