Using Manual Signalling

The preferred way to implement IceLink signalling is to use the WebSync extension. This is the simplest option, and most users do not need to know anything else about the signalling internals. There are, however, some advanced use cases that require a custom signalling implementation. This section details some of those use cases and describes how to implement a custom signalling solution.

Prerequisites

Before attempting to implement this solution, ensure that you have a working knowledge of the following topics:

This example implements manual signalling using WebSync. WebSync is ideal for learning the signalling internals as it is well-tested with IceLink. Of course, you can use any signalling library that you want.

Connecting to the Server

The first step is to create a WebSync client and connect it to a WebSync server. This means that you will need access to a WebSync server. Instructions on how to setup your own server are available in the WebSync documentation. As with IceLink, Frozen Mountain provides a community distribution of WebSync that you can use for testing. You can get this from our Downloads page.

You must ensure that all clients have loaded the WebSync Subscribers extension. This extension notifies clients when another client subscribes to the same channel as them. This functionality is used to connect users in a media session. The extension is bundled with all WebSync distributions as a separate library. Refer to the extension documentation for platform-specific details on how to load it. Note that if you are running your own server, there is also a server component to this extension that must be loaded.

The actual connection process is straightforward. To connect to a server, create an instance of an FM.WebSync.Client. Its constructor takes a single parameter, the URL of the WebSync server. Once the client is instantiated, invoke the Connect method of the client.

var client = new FM.WebSync.Client("https://v4.websync.fm/websync.ashx")
client.Connect(new FM.WebSync.ConnectArgs
{
    OnSuccess = (FM.WebSync.ConnectSuccessArgs e) => 
    {
        Console.WriteLine("connected to server");
    }
});
fm.websync.Client client = new fm.websync.Client("https://v4.websync.fm/websync.ashx");

var args = new fm.websync.ConnectArgs();
args.setOnSuccess(new fm.SingleAction<fm.websync.ConnectSuccessArgs>() {
    public void invoke(fm.websync.ConnectSuccessArgs e) {
        System.out.println("connected to server");
    }
});

client.connect(args);
FMWebSyncClient client = [FMWebSyncClient clientWithRequestUrl:@"https://v4.websync.fm/websync.ashx"];

FMWebSyncConnectArgs *args = [FMWebSyncConnectArgs connectArgs];
[args setOnSuccessBlock: ^(FMWebSyncConnectSuccessArgs* e) {
    NSLog(@"connected to server");
}];

[client connectWithConnectArgs: args]
var client = FMWebSyncClient(requestUrl: "https://v4.websync.fm/websync.ashx")

var args = FMWebSyncConnectArgs()
args.setOnSuccessBlock { (e:FMWebSyncConnectSuccessArgs) in
    print("connected to server")
}

client.connect(connectArgs: args)
var client = new fm.websync.client("https://v4.websync.fm/websync.ashx");

client.connect({
    onSuccess: function(e) {
        console.log("connected to server");
    }
});

In the above example, an OnSuccess callback was specified. The callback executes when a connection is established. There are other callbacks that are helpful. In particular, the OnFailure callback is useful for debugging purposes. More information about which callbacks that can be specified is available from the API docs.

Note also that you should only have one WebSync client instance per application instance. One WebSync client is capable of handling multiple media sessions.

Subscribing to the Session Channel

The next step is to subscribe to a session channel. The session channel will serve as a list of which clients are in a media session. When a new client joins a session channel, the existing clients will be notified and can attempt to establish a connection with the new client. Similarly, if a client unsubscribes from the channel, the remaining clients will know to disconnect from the client that left.

Since the channel will serve as a canonical list of clients in a specific session, you will need to establish a naming convention so that each unique session has a unique channel. A common convention for this is to use /session/<id>, where <id> is a unique number or hash sum identifying the session.

To subscribe to the channel, invoke the Subscribe method of the FM.Client class. It takes one parameter, an instance of FM.WebSync.SubscribeArgs. Note that the session id specified below is a placeholder. You should replace it with your own naming convention.

The important part of this snippet is the OnClientSubscribe and OnClientUnsubscribe callbacks, which will be executed whenever a client subscribes or unsubscribes from the channel. The callbacks demonstrate how to retrieve the id of the user that subscribed or unsubscribed from a channel.

client.Subscribe(new FM.WebSync.SubscribeArgs("/session/000000")
{
    OnClientSubscribe = (FM.WebSync.Subscribers.ClientSubscribeArgs e) =>
    {
        var clientId = e.Client.ClientId;
        // connect to clientId
    },
    OnClientUnsubscribe = (FM.WebSync.Subscribers.ClientUnsubscribeArgs e) =>
    {
        var clientId = e.Client.ClientId;
        // disconnect from clientId
    }
});
var args = new fm.websync.SubscribeArgs("/session/000000");

fm.websync.subscribers.SubscribeArgsExtensions.setOnClientSubscribe(args, new fm.icelink.SingleAction<fm.websync.subscribers.ClientSubscribeArgs>() {
    public void invoke(fm.websync.subscribers.ClientSubscribeArgs e) {
        fm.icelink.Guid = e.getClient().getClientId();
        // connect to clientId
    }
});

fm.websync.subscribers.SubscribeArgsExtensions.setOnClientUnsubscribe(new fm.icelink.SingleAction<fm.websync.subscribers.ClientUnsubscribeArgs>() {
    public void invoke(fm.websync.subscribers.ClientUnsubscribeArgs e) {
        fm.icelink.Guid = e.getClient().getClientId();
        // disconnect from clientId
    }
});

client.subscribe(args);
FMWebSyncSubscribeArgs* args = [FMWebSyncSubscribeArgs subscribeArgsWithChannel:@"/session/000000"];

[args setOnClientSubscribeBlock: ^(FMWebSyncSubscribersClientSubscribeArgs *e) {
    FMIceLinkGuid clientId = [[e client] clientId];
    // connect to clientId
}];
[args setOnClientUnsubscribeBlock: ^(FMWebSyncSubscribersClientUnsubscribeArgs *e) {
    FMIceLinkGuid clientId = [e client] clientId];
    // disconnect from clientId
}];

[client subscribeWithSubscribeArgs: args];
var args = FMWebSyncSubscribeArgs(channel: "/session/000000")

args.setOnClientSubscribe { (e:FMWebSyncSubscribersClientSubscribeArgs) in 
    var clientId = e.client().clientId()
    // connect to clientId
}
args setOnClientUnsubscribeBlock { e:FMWebSyncSubscribersClientUnsubscribeArgs) in
    var clientId = e.client().clientId()
    // disconnect from clientId
}

client.subscribe(subscribeArgs: args)
client.subscribe({
    channel: '/session/000000',

    onClientSubscribe: function(e) {
        var clientId = e.getClient().getClientId();
        // connect to clientId
    },
    onClientUnsubscribe: function(e) {
        var clientId = e.getClient().getClientId();
        // disconnect from clientId
    }
});

This is where it is important to have the Subscribers extension loaded. The OnClientSubscribe and OnClientUnsubscribe callbacks are provided by this extension. If you do not have them loaded, clients will not receive notifications when other clients join the session channel.

Similar to the connect method, the subscribe method has OnSuccess and OnFailure callbacks. It is recommended to always add these callbacks as they help with debugging.

Connecting to Subscribed Clients

When a new client subscribes to a session channel, clients that are already subscribed to the session channel must connect to them. What is meant by "connect" is a series of three actions:

  • create an FM.IceLink.Connection object for the newly subscribed client
  • create and send an offer to the client
  • create and send local candidates to the client

Creating a new connection is covered in the Connection section of the "Getting Started" guide. For completeness, the code below will demonstrate briefly how to create a connection but it will not be explained in detail. The RemoteMedia and LocalMedia classes are derived from RtcLocalMedia and RtcRemoteMedia, respectively. Replace the generic type parameter with the type of the view control that you are using. Refer to the LocalMedia and RemoteMedia sections for more information.

class LocalMedia : RtcLocalMedia<T> { ... }
class RemoteMedia : RtcRemoteMedia<T> { ... }

var iceServers = new IceServer[] {
    new IceServer("stun:turn.icelink.fm:3478"),
    new IceServer("turn:turn.icelink.fm:3478", "test", "pa55w0rd!")
};

var localMedia = new LocalMedia();
var remoteMedia = new RemoteMedia();

var audioStream = new FM.IceLink.AudioStream(localMedia, remoteMedia);
var videoStream = new FM.IceLink.VideoStream(localMedia, remoteMedia);

var connection = new FM.IceLink.Connection(new FM.IceLink.Stream[] { audioStream, videoStream });

connection.Id = clientId;
connection.IceServers = iceServers;
connection.OnStateChange += (FM.IceLink.Connection c) =>
{
    ...
}
class LocalMedia extends RtcLocalMedia<T> { ... }
class RemoteMedia extends RtcRemoteMedia<T> { ... }

fm.icelink.IceServer[] iceServers = {
    new fm.icelink.IceServer("stun:turn.icelink.fm:3478"),
    new fm.icelink.IceServer("turn:turn.icelink.fm:3478", "test", "pa55w0rd!")
};

LocalMedia localMedia = new LocalMedia();
RemoteMedia remoteMedia = new RemoteMedia();

fm.icelink.AudioStream audioStream = new fm.icelink.AudioStream(localMedia, remoteMedia);
fm.icelink.VideoStream videoStream = new fm.icelink.VideoStream(localMedia, remoteMedia);

fm.icelink.Connection = new fm.icelink.Connection(new fm.icelink.Stream[] { audioStream, videoStream});

connection.setExternalId(clientId);
connection.setIceServers(iceServers);
connection.addOnStateChange((fm.icelink.Connection c) ->{
    ...
});
@interface LocalMedia : FMIceLinkRtcLocalMedia
@end
@interface RemoteMedia : FMIceLinkRtcRemoteMedia
@end

NSMutableArray *iceServers = [NSMutableArray arrayWithObjects:
    [FMIceLinkIceServer iceServerWithUrl: @"stun:turn.icelink.fm:3478"],
    [FMIceLinkIceServer iceServerWithUrl: @"turn:turn.icelink.fm:443" username: @"test", password: @"pa55w0rd!"],
    nil];

LocalMedia* localMedia = [LocalMedia new];
RemoteMedia* remoteMedia = [RemoteMedia new];

FMIceLinkAudioStream* audioStream = [FMIceLinkAudioStream audioStreamWithLocalMedia: localMedia remoteMedia: remoteMedia];
FMIceLinkVideoStream* videoStream = [FMIceLinkVideoStream videoStreamWithLocalMedia: localMedia remoteMedia: remoteMedia];

FMIceLinkConnection* connection = [FMIceLinkConnection connectionWithStreams: [NSMutableArray arrayWithObjects: audioStream, videoStream, nil]];

[connection setId: clientId];
[connection setIceServers: iceServers];
[connection addOnStateChangeWithBlock: ^(FMIceLinkConnection* c) {
    ...
}];
class LocalMedia : RtcLocalMedia<T> { ... }
class RemoteMedia : RtcRemoteMedia<T> { ...}

var iceServers = [
    FMIceLinkIceServer(url: "stun:turn.icelink.fm:3478"),
    FMIceLinkIceServer(url: "turn:turn.icelink.fm:443", username: "test", password: "pa55w0rd!")
];

var localMedia = LocalMedia()
var remoteMedia = RemoteMedia()

var audioStream = FMIceLinkAudioStream(localMedia: localMedia, remoteMedia: remoteMedia)
var videoStream = FMIceLinkVideoStream(localMedia: localMedia, remoteMedia: remoteMeida)

var connecation = FMIceLinkConnection(streams: [audioStream, videoStream])

connection.setId(clientId)
connection.setIceServers(iceServers)
connection.addOnStateChanged { (c: FMIceLinkConnection) in
    ...
}
var iceServers = [
    new fm.icelink.IceServer("stun:turn.icelink.fm:3478"),
    new fm.icelink.IceServer("stun:turn.icelink.fm:3478", "test", "pa55w0rd!")
];

var localMedia = new fm.icelink.LocalMedia();
var remoteMedia = new fm.icelink.RemoteMedia();

var audioStream = new fm.icelink.AudioStream(localMedia, remoteMedia);
var videoStream = new fm.icelink.VideoStream(localMedia, remoteMedia);

var connection = new fm.icelink.Connection([audioStream, videoStream]);

connect.setId(clientId);
connection.setIceServers(iceServers);
connection.addOnStateChange(function(c) {
    ...
});

The above should already be familiar to you. The next step is to generate an SDP offer and send it to the newly subscribed client. If you have not already reviewed the Promise API, now is a good time to do so, as this section makes use of it.

The WebSync Notify API is used here to send the SDP offer to the subscribed client. This API works like a user channel, where a message is delivered only to a single client. Each message can have an optional "tag" associated with it, which is a string that lets the recipient know how to process the message. In this context, the tag will be used to indicate whether a message contains an offer, an answer or a candidate.

To create an offer, invoke the CreateOffer method of the FM.IceLink.Connection class, which returns a promise. When the promise resolves, set the local description of the connection using the SetLocalDescription method. This method also returns a promise. Finally, after the local description promise resolves, forward the offer to the subscribed client using the "offer" tag. The FM.IceLink.SessionDescription class has a convenient ToJson method that you can use to serialize the offer.

The code below omits promise failure handlers, but as with failure callbacks, it is recommended to add them to your application code. Refer to the Promises API section for information on handling failure in promises.

connection.CreateOffer().Then((FM.IceLink.SessionDescription offer) =>
{
    return connection.SetLocalDescription(offer)
}).Then((FM.IceLink.SessionDescription offer) =>
{
    client.Notify(new FM.WebSync.NotifyArgs(clientId, offer.ToJson(), "offer");
});
connection.createOffer().then((fm.icelink.SessionDescription offer) -> {
    return connection.setLocalDescription(offer);
}).then((fm.icelink.SessionDescription offer) -> {
    client.notify(new fm.websync.NotifyArgs(clientId, offer.toJson(), "offer");
});
[[[connection createOffer] thenWithResolveFunctionBlock: ^(FMIceLinkSessionDescription* offer) {
    return [connection setLocalDescription: offer];
}] thenWithResolveFunctionBlock: ^(FMIceLinkSessionDescription* offer) {
    FMWebSyncNotifyArgs *notifyArgs = [FMWebSyncNotifyArgs notifyArgsWithClientId:clientId dataJson:[offer toJson] tag:@"offer"];

    [client notifyWithNotifyArgs: notifyArgs];
}];
connection.createOffer().then(resolveFunctionBlock: { (offer: FMIceLinkSessionDescription) -> FMIceLinkPromise in
    return connection.setLocalDescription(localDescription: offer)
}).then(resolveFunctionBlock: { (offer: FMIceLinkSessionDescription) -> FMIceLinkFuture in
    client.notify(notifyArgs: FMWebSyncNotifyArgs(clientId: clientId, dataJson: offer.toJson, tag: "offer"))
})
connection.createOffer().then(function(offer) {
    return connection.setLocalDescription(offer);
}).then(function(offer) {
    client.notify({
        tag: "offer",
        clientId: clientId,
        dataJson: offer.toJson()
    });
});

After the connection object has been created, and an offer has been forwarded to the subscribed client, the last step is to forward the local candidates to the new client. These are generated automatically. To receive candidates as they are generated, add an event handler for the OnLocalCandidate event. You can expect this event to be raised several times during the initialization of a connection. This event handler is raised on the FM.IceLink.Connection class.

Note that host candidates are included in the SDP offer/answer by default, so they aren’t raised through the LocalCandidate event as this would result in duplicates. Because of that, if you don’t specify any ICE servers, there won’t be any non-host candidates, and you won’t get any LocalCandidate events raised. Specifying ICE servers will ensure that all candidate types are present, and also ensure that your LocalCandidate event is triggered as expected.


In the event handler for the OnLocalCandidate event, use the Notify API to forward the candidate to the newly subscribed candidate. Like the FM.IceLink.SessionDescription class, the FM.IceLink.Candidate class has a convenient ToJson method for serialization. Ensure that you use the "candidate" tag, so that the recipient knows that this message contains a candidate.

connection.OnLocalCandidate += (FM.IceLink.Connection c, FM.IceLink.Candidate candidate) =>
{
    client.Notify(new FM.WebSync.NotifyArgs(clientId, candidate.ToJson(), "candidate");
};
connection.addOnLocalCandidate((fm.icelink.Connection c, fm.icelink.Candidate candidate) -> {
    client.notify(new fm.websync.notifyArgs(clientId, candidate.toJson(), "candidate");
});
[connection addOnLocalCandidateWithBlock: ^(FMIceLinkConnection* c, FMIceLinkCandidate* candidate) {
    FMWebSyncNotifyArgs *notifyArgs = [FMWebSyncNotifyArgs notifyArgsWithClientId:clientId dataJson:[candidate toJson] tag:@"candidate"];

    [client notifyWithNotifyArgs: notifyArgs];
}];
connection.addOnLocalCandidate { (c:FMIceLinkConnection, candidate:FMIceLinkCandidate) in
    client.notify(FMWebSyncNotifyArgs(clientId: clientId, candidate: candidate.toJson(), tag: "candidate")
}
connection.addOnLocalCandidate(function(c, candidate) {
    client.notify({
        tag: "candidate",
        clientId: clientId,
        dataJson: candidate.toJson()
    });
}):

Receiving a Connection Attempt

The above code covers the actions that a client must take when a new client subscribes to a session channel. This is only half the picture. This next section covers how a client that has just joined a session channel must respond to connection attempts from clients that are already in a session channel.

You may extrapolate from the above code that there are two cases that need to be handled:

  • receiving an offer
  • receiving a candidate

First, we'll demonstrate how to add an OnNotify event handler to the client. For .NET platforms, add the handler as you would any other event. For other platforms that don't have the concept of an event, use the addOnNotify event. In this event handler, you will create two branches based on the value of the message's tag - one for handling offers and one for handling answers.

client.OnNotify = (FM.WebSync.NotifyReceiveArgs e) =>
{
    switch (e.Tag)
    {
        case "offer":
            // receive the offer
            break;

        case "candidate":
            // receive the candidate
            break;
    }
}
client.addOnNotify(new fm.SingleAction<fm.websync.notifyReceiveArgs>() {
    public void invoke(fm.websync.notifyReceiveArgs e) {
        switch (e.getTag()) {
            case "offer":
                // receive the offer
                break;

            case "candidate":
                //receive the candidate
                break;
        }
    }
});
[client addOnNotifyBlock: ^(FMWebSyncNotifyReceiveArgs *e) {
    switch ([e tag]) {
        case @"offer":
            // receive the offer
            break;

        case @"candidate":
            // receive the candidate
            break;
    }
}];
client.addOnNotifyBlock { (e:FMWebSyncNOtifyReceiveArgs) in
    switch e.tag() {
    case "offer":
        // receive the offer

    case "candidate":
        // receive the candidate
    }
}
client.addOnNotify(function(e) {
    switch (e.getTag()) {
        case "offer":
            // receive the offer
            break;

        case "candidate":
            // receive the candidate;
            break;
    }
});

When receiving an offer, the receiving client must use the offer to set the remote description and then reply with its own SDP answer.

Assuming the code below is underneath the case statements in the above sample, the first step is to deserialize the offer that was received from the original client. The serialized data of the notify message is available from the FM.WebSync.NotifyReceiveArgs object by accessing the DataJson property. This serialized data can be deserialized to an FM.IceLink.SessionDescription instance using the FromJson static method of the same class.

Because the new client has no knowledge of the connection object that the original client created in its own application space, the new client will have to create its own connection here. This code will be abstracted into a CreateConnection method. Refer to the connection guide for more information on creating connections.

Something important to note here is that the created connection should have an OnLocalCandidate event handler bound, and it should send candidates in exactly the same way described above. To clarify, the code for sending candidates between clients works exactly the same for both the original and the new client and they both need OnLocalCandidate event handlers that send them using the Notify API.

With the connection object created, the next step is to invoke a chain of three promises, each executing after the previous promise has resolved.

  • Use the received offer to set the connection's remote description by invoking the SetRemoteDescription

method. - Create an SDP answer by invoking the CreateAnswer method. - Use the created answer to set the connection's local description by invoking the SetLocalDescription method.

Once the local description has been set, you must send the answer to the original client using the Notify API. Again, use the ToJson method of the FM.IceLink.SessionDescription class and ensure that you use the "answer" tag.

var offer = FM.IceLink.SessionDescription.FromJson(e.DataJson);
var connection = CreateConnection();

connection.SetRemoteDescription(offer).then((FM.IceLink.SessionDescription offer) =>
{
    return connection.CreateAnswer();
}).Then((FM.IceLink.SessionDescription answer) =>
{
    return connection.SetLocalDescription(answer);
}).Then((FM.IceLink.SessionDescription answer) =>
{
    client.Notify(new FM.WebSync.NotifyArgs(e.Client.ClientId, answer.ToJson(), "answer");
});
fm.icelink.SessionDescription offer = fm.icelink.SessionDescription.fromJson(e.getDataJson());
fm.icelink.Connection connection = createConnection();

connection.setRemoteDescription(offer).then((fm.icelink.SessionDescription offer) -> {
    return connecton.createAnswer();
}).then((fm.icelink.SessionDescription answer) -> {
    return connection.setLocalDescription(answer);
}).then((fm.icelink.SessionDescription answer) -> {
    client.notify(new fm.websync.notifyArgs(e.getClient().getClientId(), answer.toJson(), "answer");
});
FMIceLinkSessionDescription offer = [FMIceLinkSessionDescription fromJsonWithSessionDescriptionJson: [e dataJson]];
FMIceLinkConnection connection = [self createConnection];

[[[[connection setRemoteDescriptionWithRemoteDescription: offer] thenWithResolveFunctionBlock: ^(FMIceLinkSessionDescription* offer) {
    return [connection createAnswer];
}] thenWithResolveFunctionBlock: ^(FMIceLinkSessionDescription* answer) {
    return [connection setLocalDescriptionWithLocalDescription: answer];
}] thenWithResolveActionBlock: ^(FMIceLinkSessionDescription* answer) {
    FMWebSyncNotifyArgs *notifyArgs = [FMWebSyncNotifyArgs notifyArgsWithClientId:clientId dataJson:[answer toJson] tag:@"answer"];

    [client notifyWithNotifyArgs: notifyArgs];
}];
var offer = FMIceLinkSessionDescription.fromJson(sessionDescriptionJson: e.dataJson)
var connection = self.createConnection()

connection.setRemoteDescription(remoteDescription: offer).then(resolveFunctionBlock: { (offer: FMIceLinkSessionDescription) -> FMIceLinkFuture in
    return connection.createAnswer();
}).then(resolveFunctionBlock: { (answer: FMIceLinkSessionDescription) -> FMIceLinkFuture in
    return connection.setLocalDescription(localDescription: answer)
}).then(resolveActionBlock: { (answer: FMIceLinkSessionDescription) in
    var notifyArgs = FMWebSyncNotifyArgs(clientId: clientId, dataJson: answer.toJson(), tag: "offer")

    client.notify(notifyArgs: notifyArgs)
})
var offer = fm.icelink.SessionDescription.fromJson(e.getDataJson());
var connection = createConnection();

connection.setRemoteDescription(offer).then(function(offer) {
    return connection.createAnswer();
}).then(function(answer) {
    return connection.setLocalDescription(answer);
}).then(function(answer) {
    client.notify({
        tag: "answer",
        clientId: e.getClient().getClientId(),
        dataJson: answer.toJson()
    });
});

The last step is to receive candidates. This is much simpler than receiving an offer. Under the "candidate" case block in the code above, deserialize the candidate using the FromJson static method of the FM.IceLink.Candidate class. With this deserialized candidate, invoke the AddRemoteCandidate method on the FM.IceLink.Connection class. This method returns a promise but you do not need to wait for it to resolve.

var candidate = fm.icelink.Candidate.fromJson(e.getDataJson());
var connection = connections.getById(e.getClient().getClientId());

connection.AddRemoteCandidate(candidate);
fm.icelink.Candidate candidate = fm.icelink.Candidate.fromJson(e.getDataJson());
fm.icelink.Connection connection = connections.getById(e.getClient().getClientId());

connection.addRemoteCandidate(candidate);
FMIceLinkCandidate candidate = [FMIceLinkCandidate fromJsonWithCandidateJson: [e dataJson]];
FMIcelinkConnection connection = [connections getById: [e getClient] clientId];

[connection addRemoteCandidateWithRemoteCandidate:candidate];
var candidate = FMIceLinkCandidate.fromJson(candidateJson: e.dataJson())
var connection = connections.getById(idValue: e.getClient().clientId)

connection.addRemoteCandidate(remoteCandidate: candidate)
var candidate = fm.icelink.sdp.Candidate.fromJson(e.getDataJson());
var connection = connections.getById(e.getClient().getClientId());

connection.addRemoteCandidate(candidate);

After this, the subscribing client has now completed all of its signaling. The only thing left to do is for the original client to establish the connection by accepting the answer, which is covered in the next section.

One small caveat with the above code. It's possible that the new client receives remote candidates before they receive an offer. This means that when they try to accept a candidate, there may not be a connection to accept it for. To deal with this edge case, you may either cache the candidates until an offer is received, or you can initialize the connection early here. If you use the latter strategy, make sure you do not initialize two different connections, or you won't be able to establish a connection.

Establishing the Connection

The new client will send an answer to the original client with the "answer" tag using the Notify API. To handle this case, a new block needs to be added to the OnNotify event handler. Start by adding an empty "answer" case.

client.OnNotify = (FM.WebSync.NotifyReceiveArgs e) =>
{
    switch (e.Tag)
    {
        case "answer":
            // receive the answer
            break;
    }
}
client.addOnNotify(new SingleAction<fm.websync.notifyReceiveArgs>() {
    public void invoke(fm.websync.notifyReceiveArgs e) {
        switch (e.getTag()) {
            case "answer":
                // receive the answer
                break;
        }
    }
});
[client addOnNotifyBlock: ^(FMWebSyncNotifyReceiveArgs *e) {
    switch ([e tag]) {
        case @"answer":
            // receive the answer
            break;
    }
}];
client.addOnNotify { (e:FMWebSyncNotifyReceiveArgs) in
    switch e.tag() {
    case "answer":
        // receive the answer
    }
}
client.addOnNotify(function(e) {
    switch (e.getTag()) {
        case "answer":
            // receive the answer
            break;
    }
});

Recapping a bit here, there are two clients. The first client, the original client, is the one waiting in the session channel. The second client, the new client, is the one that subscribed to the session channel. Both clients can use the same OnNotify event handler. The original client will use the "candidate" and "answer" case blocks, while the new client will use the "candidate" and "offer" case blocks. If you think about the previous code, this makes perfect sense. The original client only ever sends an offer, and the new client only ever sends an answer.

The last step to completing a connection is to have the original client accept the answer. To do this, invoke the SetRemoteDescription method of the FM.IceLink.Connection class. As with above, you can deserialize the required FM.IceLink.SessionDescription representing the answer by using the FromJson static method of this class.

Note that in this case, even though SetRemoteDescription returns a promise, no action needs to be taken after the promise is resolved. You can, of course, add a resolve handler for debugging purposes.

var answer = FM.IceLink.SessionDescription.FromJson(e.DataJson);

connection.SetRemoteDescription(answer);
fm.icelink.SessionDescription answer = fm.icelink.SessionDescription.fromJson(e.getDataJson());

connection.setRemoteDescription(answer);
FMIceLinkSessionDescription* answer = [FMIceLinkSessionDescription fromJsonWithSessionDescriptionJson: [e dataJson]];

[connection setRemoteDescriptionWithRemoteDescription: answer];
var answer = FMIceLinkSessionDescription.fromJson(sessionDescriptionJson: e.dataJson())

connection.setRemoteDescription(remoteDescription: answer)
var answer = fm.icelink.SessionDescription.fromJson(e.getDataJson());

connection.setRemoteDescription(answer);

Wrapping Up

At this point, the session is established. Both clients should be receiving audio and video data now and no further signalling needs to occur. If you are already using WebSync, you are encouraged to use the WebSync extension, as it is much simpler. Refer to the Connecting to the Signalling Server section for more details.