Opening a Connection

In the last few sections, you've learned how to capture local media, how to display remote media and how to send and receive data using streams. Now you need something to tie all of these concepts together. The IceLink SDK accomplishes this with an object known as a connection. A connection is a high-level component that controls how two participants in a media session will communicate. This section focuses on how to create a connection between two participants and start the process that will allow them to communicate with each other.

Creating a Connection

The first step is to create an FM.IceLink.Connection instance. To create a Connection instance, you invoke its constructor and specify the stream instances that you have previously created. The most common scenario is to specify an audio and a video stream, which is shown below. Note that if you have a data stream, you would specify it here as well.

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

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

fm.icelink.Connection connection = new fm.icelink.Connection(new fm.icelink.Stream[] { audioStream, videoStream });
FMIceLinkAudioStream* audioStream = [FMIceLinkAudioStream audioStreamWith...];
FMIceLinkVideoStream* videoStream = [FMIceLinkVideoStream videoStreamWith...];

NSMutableArray* streams = [NSMutableArray arrayWithObjects: audioStream, videoStream, nil];
FMIceLinkConnection* connection = [FMIceLinkConnection* connectionWithStreams: streams];
var audioStream = FMIceLinkAudioStream(...)
var videoStream = FMIceLinkVideoStream(...)

var connection = FMIceLinkConnection([audioStream, videoStream])
var audioStream = new fm.icelink.AudioStream(...);
var videoStream = new fm.icelink.VideoStream(...);

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

Next, you must specify a set of ICE servers for your Connection instance. ICE is a protocol that is used to determine how two participants can best communicate with each other. ICE servers themselves use two different protocols to help establish a session: STUN and TURN. STUN is a protocol that helps clients that are behind a NAT to discover their public IP address, while TURN is a protocol that allows clients that are behind a restrictive firewall to send all of their data through a relay server. You will want both of these services to be available to your application users.

To specify an ICE server that supports STUN, create an instance of FM.IceLink.IceServer. You must provide a single host parameter, which should include a scheme, domain name and a port. For STUN servers, the scheme should be stun. To specify an ICE server that supports TURN, again create an IceServer instance and provide a host parameter. For TURN, use turn as the scheme instead of stun. In this case, you must also specify a username and a password. TURN services require authentication, as they can consume a lot of bandwidth. Without authentication, anyone could relay their media data through your server and use your resources for free.

Once you have created your IceServer instances, you assign them to the Connection instance using its IceServers property, as shown below.

var connection = new FM.IceLink.Connection(...);

connection.IceServers = new [] {
    new FM.IceLink.IceServer("stun:stun.server.com:3478"),
    new FM.IceLink.IceServer("turn:stun.server.com:3478", "username", "password")
};
fm.icelink.Connection connection = new fm.icelink.Connection(...);

connection.setIceServers(new fm.icelink.IceServer[] {
    new fm.icelink.IceServer("stun:stun.server.com:3478"),
    new fm.icelink.IceServer("turn:turn.server.com:3478", "username", "password")
});
FMIceLinkConnection* connection = [FMIceLinkConnectionWithStreams: ...];

[connection setIceServers: [NSMutableArray arrayWithObjects: 
    [FMIceLinkIceServer iceServerWithUrl: @"stun:stun.server.com:3478"],
    [FMIceLinkIceServer iceServerWithUrl: @"turn:turn.server.com:3478" username: "username" password: "password"],
    nil
]];
var connection = FMIceLinkConnection(...)

connection.setIceServers([
    FMIceLinkIceServer("stun:stun.server.com:3478"),
    FMIceLinkIceServer("turn:turn.server.com:3478", "username", "password")
])
var connection = new fm.icelink.Connection(...);

connection.setIceServers([
    new fm.icelink.IceServer("stun:stun.server.com:3478"),
    new fm.icelink.IceServer("turn:turn.server.com:3478", "username", "password")
]);

Now that you've learned how to create a Connection instance, it's time to join a video conference.

Joining a Video Conference

As mentioned earlier, this guide covers automatic signalling using the IceLink SDK's WebSync extension. If you are using a different framework or want to use manual signalling, refer to the guide on Using Manual Signalling.

Before you can join a video conference, you need an FM.WebSync.Client instance that is connected to a WebSync server. If you need help with this step, refer back to the earlier section on Connecting to the Signalling Server. You must also make sure that you include the FM.IceLink.WebSync4 library in your project. This is available from the standard IceLink SDK distribution, as a library file appropriate for your platform.

To join a video conference, with your Client instance, invoke the JoinConference extension method. For this method, you must specify a JoinConferenceArgs instance, which in turn requires you to specify a session channel. The session channel is a WebSync channel that session participants will use to communicate. You must also specify the OnRemoteClient callback on the JoinConferenceArgs instance. This callback is called each time a new client joins the session channel. The callback must return a new Connection object for each individual client that joins. This is shown below.

client.JoinConference(new FM.IceLink.WebSync4.JoinConferenceArgs("/123456")
{
    OnSuccess = (FM.IceLink.WebSync4.JoinConferenceSuccessArgs e) =>
    {
        Console.WriteLine("joined the conference");
    },
    OnFailure = (FM.IceLink.WebSync4.JoinConferenceFailureArgs e) =>
    {
        Console.WriteLine("failed to join the conference");
    },

    OnRemoteClient = (FM.IceLink.WebSync4.PeerClient remoteClient) =>
    {
        var streams = ...
        var connection = new FM.IceLink.Connection(streams);

        connection.IceServers = ...

        return connection;
    }
}
fm.icelink.websync4.JoinConferenceArgs args = new fm.icelink.websync4.JoinConferenceArgs("/123456");

args.setOnSuccess(new fm.SingleAction<fm.icelink.websync4.JoinConferenceSuccessArgs>() {
    public void invoke(fm.icelink.websync4.JoinConferenceSuccessArgs e) {
        System.out.println("joined the conference");
    }
});
args.setOnFailure(new fm.SingleAction<fm.icelink.websync4.JoinConferenceFailureArgs>() {
    public void invoke(fm.icelink.websync4.JoinConferenceFailureArgs e) {
        System.out.println("failed to join the conference");
    }
});

args.setOnRemoteClient(new fm.SingleAction<fm.icelink.websync4.PeerClient>() {
    public void invoke(fm.icelink.websync4.PeerClient remoteClient) {
        fm.icelink.Stream[] streams = ...
        fm.icelink.Connection connection = new fm.icelink.Connection(streams);

        connection.setIceServers(...);

        return connection;
    }
});

fm.icelink.websync4.ClientExtensions.joinConference(client, args);
FMIceLinkWebSync4JoinConferenceArgs* args = [FMIceLinkWebSync4JoinConferenceArgs joinConferenceArgsWithConferenceChannel:@"/123456"];

[args setOnSuccessBlock: ^(FMIceLinkJoinConferenceSuccessArgs* e) {
    NSLog(@"joined the conference");
}];
[args setOnFAilureBlock: ^(FMIceLinkJoinConferenceFailureArgs* e) {
    NSLog(@"failed to join the conference");
}];

[args setOnRemoteClientBlock: ^ FMIceLinkConnection* (FMIceLinkWebSync4PeerClient* remoteClient) {
    NSMutableArray* streams = ...
    FMIceLinkConnection* connection = [FMIceLinkConnection connectionWithStreams:streams];

    [connection setIceServers: ...];

    return connection;
}];

[client joinConferenceWithArgs: args];
var args = FMIceLinkWebSync4JoinConferenceArgs(conferenceChannel: "/123456")

args.setOnSuccess { (e:FMIceLinkJoinConferenceSuccessArgs) in
    print("joined the conference")
}

args.setOnFailure { (e:FMIceLinkJoinConferenceFailureArgs) in 
    print("failed to join the conference")
}

args.setOnRemoteClient { (remoteClient:FMIceLinkWebSync4PeerClient) -> FMIceLinkConnection in
    var streams = ...
    var connection = FMIceLinkConnection(streams: streams)

    connection.setIceServers(...)

    return connection
}

client.joinConference(args: args)
var joinArgs = new fm.icelink.websync4.JoinConferenceArgs("/123456");
joinArgs.setOnSuccess((args) => {
    fm.icelink.Log.info("joined the conference");
});

joinArgs.setOnFailure((args) => {
    fm.icelink.Log.info("failed to join the conference");
});

joinArgs.setOnRemoteClient((remoteClient) => {
	// set up streams ...
	var connection = new fm.icelink.Connection([... stream array ...]);
    connection.setIceServers(...);
    return connection;
});

fm.icelink.websync4.ClientExtensions.joinConference(this.client, joinArgs);

The above code is missing one important component - layout. If you remember back to the section on Receiving Remote Media, you learned that whenever a new participant joins a session, you would have to create a new remote media object for them, and then add their view to the layout manager. We also have to remove it when the connection ends.

To accomplish this, first add an OnStateChange event handler to the Connection instance that you create in your OnRemoteClient callback. This event is raised whenever the connection's status changes. In the event handler, you can inspect the value of the Connection instance's State property, to see its current status. When the state is Connected, you want to add the remote view object to the layout manager. When the state is Failing or Closing, you want to remove it from the layout manager. This is shown below:

connection.OnStateChange += (FM.IceLink.Connection c) =>
{
    if (c.State == FM.IceLink.ConnectionState.Connected)
    {
        layoutManager.addRemoteView(remoteMedia.Id, remoteMedia.View);
    }
    else if (c.state == FM.IceLink.ConnectionState.Failing || c.state == FM.IceLink.ConnectionState.Closing)
    {
        layoutManager.removeRemoteView(remoteMedia.Id);
    }
};
connection.addOnStateChange((fm.icelink.Connection c) -> {
    if (c.getState() == fm.icelink.ConnectionState.Connected) {
        layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());
    } else if (c.getState() == fm.icelink.ConnectionState.Failing || c.getState() == fm.icelink.ConnectionState.Closing) {
        layoutManager.removeRemoteView(remoteMedia.getId());
    }
});
[connection addOnStateChange: ^(FMIceLinkConnection* c) {
    if ([c state] == FMIceLinkConnectionStateConnected) {
        [layoutManager addRemoteViewWithId:[remoteMedia id] view:[remoteMedia view]];
    } else if ([c state] == FMIceLinkConnectionStateFailing || [c state] == FMIceLinkConnectionStateClosing) {
        [layoutManager removeRemoteViewWithId:[remoteMedia id];
    }
}];
connection.addOnStateChange { (c:FMIceLinkConnection) in
    if c.state() == FMIceLinkConnectionStateConnected) {
        layoutManager.addRemoteView(id: remoteMedia.id(), view: remoteMedia.view())
    } else if c.state() == FMIceLinkConnectionStateFailing || c.state() == FMIceLinkConnectionStateClosing {
        layoutManager.removeRemoteView(id: remoteMedia.id())
    }
}
connection.addOnStateChange(function(c) {
    if (c.getState() == fm.icelink.ConnectionState.Connected) {
        layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());
    } else if (c.getState() == fm.icelink.ConnectionState.Failing || c.getState() == fm.icelink.ConnectionState.Closing) {
        layoutManager.removeRemoteView(remoteMedia.getId());
    }
});

Now that you can join a video conference, it's time to learn how to leave one.

Leaving a Video Conference

Whenever you are done with a video conference, you should leave it by invoking the LeaveConference method of your Client instance. This ensures that all media objects are cleaned up properly. The LeaveConference method, as with the JoinConference method, supports OnSuccess and OnFailure callbacks, as shown in the examples below.

client.LeaveConference(new FM.IceLink.WebSync4.LeaveConferenceArgs("/123456")
{
    OnSuccess = (FM.IceLink.WebSync4.LeaveConferenceSuccessArgs e) =>
    {
        Console.WriteLine("left the conference");
    },
    OnFailure = (FM.IceLink.WebSync4.LeaveConferenceFailureArgs e) =>
    {
        Console.WriteLine("failed to leave the conference");
    }
}
fm.icelink.websync4.LeaveConferenceArgs args = new fm.icelink.websync4.LeaveConferenceArgs("/123456");

args.setOnSuccess(new fm.SingleAction<fm.icelink.websync4.LeaveConferenceSuccessArgs>() {
    public void invoke(fm.icelink.websync4.LeaveConferenceSuccessArgs e) {
        System.out.println("left the conference");
    }
});
args.setOnFailure(new fm.SingleAction<fm.icelink.websync4.LeaveConferenceFailureArgs>() {
    public void invoke(fm.icelink.websync4.LeaveConferenceFailureArgs e) {
        System.out.println("failed to leave the conference");
    }
});

fm.icelink.websync4.ClientExtensions.leaveConference(client, args);
FMIceLinkWebSync4LeaveConferenceArgs* args = [FMIceLinkWebSync4LeaveConferenceArgs leaveConferenceArgsWithConferenceChannel:@"/123456"];

[args setOnSuccessBlock: ^(FMIceLinkLeaveConferenceSuccessArgs* e) {
    NSLog(@"left the conference");
}];
[args setOnFailureBlock: ^(FMIceLinkLeaveConferenceFailureArgs* e) {
    NSLog(@"failed to leave the conference");
}];

[client leaveConferenceWithArgs: args];
var args = FMIceLinkWebSync4LeaveConferenceArgs(conferenceChannel: "/123456")

args.setOnSuccess { (e:FMIceLinkLeaveConferenceSuccessArgs) in
    print("left the conference")
}
args.setOnFailure { (e:FMIceLinkLeaveConferenceFailureArgs) in 
    print("failed to leave the conference")
}

client.leaveConference(args: args)
client.leaveConference({
    conferenceChannel: "/123456",

    onSuccess: function(e) {
        console.log("left the conference");
    },
    onFailure: function(e) {
        console.log("failed to leave the conference");
    }
});

The LeaveConference method will automatically close any open connections and teardown any active streams. It will not destroy the layout manager object, nor will it stop local media - ensure that you clean up those objects yourself.

Disabling Trickle ICE

By default, the IceLink SDK uses trickle ICE. Trickle ICE is a variant of the ICE protocol, where instead of sending all network candidates at once, the candidates are sent as they are discovered. A candidate in this sense, is a specific term from the ICE specification that refers to a single method by which a peer can be communicated with. For example, you may have a private candidate associated with a participant's local network.

Trickle ICE is the default setting because it allows you to establish a connection faster. You should not disable it unless your application interacts with a component that specifically does not support it. To disable trickle ICE, set the TrickleIcePolicy property of your Connection instance to NotSupport. This is shown below.

// enable trickle ICE (default)
connection.TrickleIcePolicy = FM.IceLink.TrickleIcePolicy.FullTrickle;

// disable trickle ICE
connection.TrickleIcePolicy = FM.IceLink.TrickleIcePolicy.NotSupported;
// enable trickle ICE (default)
connection.setTrickleIcePolicy(fm.icelink.TrickleIcePolicy.FullTrickle);

// disable trickle ICE
connection.setTrickleIcePolicy(fm.icelink.TrickleIcePolicy.NotSupported);
// enable trickle ICE (default)
[connection setTrickleIcePolicy: FMIceLinkTrickleIcePolicyFullTrickle];

// disable trickle ICE
[connection setTrickleIcePolicy: FMIceLinkTrickleIcePolicyNotSupported];
// enable trickle ICE (default)
connection.setTrickleIcePolicy(FMIceLinkTrickleIcePolicyFullTrickle)

// disable trickle ICE
connection.setTrickleIcePolicy(FMIceLinkTrickleIcePolicyNotSupported)
// enable trickle ICE (default)
connection.setTrickleIcePolicy(fm.icelink.TrickleIcePolicy.FullTrickle);

// disable trickle ICE
connection.setTrickleIcePolicy(fm.icelink.TrickleIcePolicy.NotSupported);

Wrapping Up

By now, you should have your video conference up and running. The next sections cover a few miscellaneous features of the SDK, such as acoustic echo cancellation. Read on for more information.