Using Advanced Features

IceLink 2 had several additional features that, so far, have not been covered by this guide. This section will explain how to convert any code that uses these remaining features.

Peer State

IceLink 2 has the concept of "peer state", which means that you can associate an arbitrary object with a peer. For example, you can use a user profile as the peer state, which means that whenever an event is raised, this data is available from the event handler arguments. You pass the object to the peer when you invoke Link or ReceiveOfferAnswer.

// IceLink 2
conference.OnLinkInit += (FM.IceLink.LinkInitArgs e) =>
{
    Console.WriteLine("Real name is: " + (string)e.PeerState);
};

conference.link("00000000-0000-0000-0000-000000000000", "John Doe");
// IceLink 2
conference.addOnLinkInit(new fm.SingleAction<fm.icelink.linkInitArgs>() {
    public void invoke(fm.icelink.LinkInitArgs e) {
        System.out.println("Real name is: " + (String)e.getPeerState())
    }
});

conference.link("00000000-0000-0000-0000-000000000000", "John Doe");
// IceLink 2
[conference addOnLinkInitBlock: ^(FMIceLinkLinkInitArgs* e) {
    NSLog(@"Real name is: %@", (NSString *)[e peerState]);
}];

[conference linkWithPeerId:@"00000000-0000-0000-0000-000000000000" peerState:@"John Doe"];
// IceLink 2
conference.addOnLinkInitBlock { (e: FMIceLinkLinkInitArgs) in
    NSLog("Real name is: " + e.peerState)
}

conference.link(peerId: "0000000-0000-0000-0000-000000000000" peerState: "John Doe")
// IceLink 2
conference.addOnLinkInit(function(e) {
    console.log("Real name is: " + e.getPeerState().toString());
});

conference.link("00000000-0000-0000-0000-00000000000, "John Doe");

This feature has been removed from IceLink 3, essentially because there are better options. The benefit to this feature in IceLink 2 was that it provided you the ability to associate arbitrary data with an FM.IceLink.Link, when most functions dealt with an FM.IceLink.Conference. Since the focus in IceLink 3 is now on individual connections, you can achieve the same functionality by taking advantage of the fact that the FM.IceLink.Connection object inherits from FM.IceLink.Dynamic.

The Dynamic class is a cross-platform way of doing exactly what is mentioned above - associating arbitrary data with an object instance. Most FM classes inherit from it, and the Connection class is no exception. To associate or access data, you call the GetDynamicValue and SetDynamicValue methods of the Connection instance. There are a number of advantages to this, namely in that you can assign values to the Connection at any point in its lifestyle. The code below demonstrates this use.

// IceLink 3
connection.OnStateChange += (FM.IceLink.Connection c) =>
{
    Console.WriteLine("Real name is: " + (string)c.getDynamicValue("realName"));
};

connection.setDynamicValue("realName", "John Doe");
// IceLink 3
connection.addOnStateChange((fm.icelink.Connection c) -> {
    System.out.println("Real name is: " + (String)c.getDynamicValue("realName"));
});

connection.setDynamicValue("realName", "John Doe");
// IceLink 3
[connection addOnStateChangeBlock: ^(FMIceLinkConnection* c) {
    NSLog(@"Real name is: %@", (NSString *)[c getDynamicValueWithKey:@"realName"]);
}];

[connection setDynamicValueWithKey:@"realName" value:@"John Doe"];
// IceLink 3
connection.addOnStateChangeBlock { (c: FMIceLinkConnection) in
    NSLog("Real name is: " + c.getDynamic(key: "realName"))
}

connection.setDynamicValue(realName: "realName", value: "John Doe")
// IceLink 3
connection.addOnStateChange(function(c) {
    console.log("Real name is: " + c.getDynamicValue("realName"));
});

connection.setDynamicValue("realName", "John Doe");


Migration Steps

  • Instead of peer state, use SetDynamicValue and GetDynamicValue.

Non-Trickle ICE

The default setting in IceLink 2 is to generate candidates as they are available, and raise the OnLinkCandidate event handler each time a new candidate is generated. This is known as Trickle ICE, because the ICE candidates "trickle in". IceLink 2 refers to this as late candidate gathering. IceLink 2 also supports non-Trickle ICE, otherwise referred to as early candidate gathering. In early gathering, the candidates are appended to the SDP offer. You can enable this setting by changing the value of the CandidateMode property of the FM.IceLink.Conference object to CandidateMode.Early.

// IceLink 2
conference.CandidateMode = FM.IceLink.CandidateMode.Early;
// IceLink 2
conference.setCandidateMode(fm.icelink.CandidateMode.Early);
// IceLink 2
conference.candidateMode = FMIceLinkCandidateModeEarly;
// IceLink 2
conference.candidateMode = FMIceLinkCandidateModeEarly
// IceLink 2
conference.setCandidateMode(fm.icelink.CandidateMode.Early);

IceLink 3 also supports this feature, though the naming has changed to conform to convention. You can modify the trickle ICE policy by setting the TrickleIcePolicy property of the Connection object. There are two possible values, FullTrickle and NotSupported, which map to Late and Early from IceLink 2. The default is FullTrickle.

// IceLink 3
connection.TrickleIcePolicy = FM.IceLink.TrickleIcePolicy.NotSupported;
// IceLink 3
connection.setTrickleIcePolicy(fm.icelink.TrickleIcePolicy.NotSuppored);
// IceLink 3
connection.trickleIcePolicy = FMIceLinkTrickleIcePolicyNotSupported;
// IceLink 3
connection.trickleIcePolicy = FMIceLinkTrickleIcePolicyNotSupported
// IceLink 3
connection.setTrickleIcePolicy(fm.icelink.TrickleIcePolicy.Early);


Migration Steps

  • Replace all references to CandidateMode with TrickleIcePolicy.

Working With SDP

In IceLink 2, candidates and SDP messages are treated primarily as strings, wrapped with instances of FM.IceLink.OfferAnswer and FM.IceLink.Candidate. To modify them, you change the value of the SdpMessage or SdpCandidateAttribute properties.

// IceLink 2
FM.IceLink.OfferAnswer offerAnswer;
FM.IceLink.Candidate candidate;


string sdpMessage = offerAnswer.SdpMessage;
string sdpMessageChanged = sdpMessage.Replace(...);
offerAnswer.SdpMessage = sdpMessageChanged;

string sdpCandidate = candidate.SdpCandidateAttribute;
string sdpCandidateChanged = sdpCandidate.Replace(...);
candidate.SdpCandidateAttribute = sdpCandidateChanged;
// IceLink 2
fm.icelink.OfferAnswer offerAnswer;
fm.icelink.Candidate candidate;

String sdpMessage = offerAnswer.getSdpMessage();
String sdpMessageChanged = sdpMessage.replace(...);
offerAnswer.setSdpMessage(sdpMessageChanged);

String sdpCandidate = candidate.getSdpCandidateAttribute();
String sdpCandidateChanged = sdpCandidate.replace(...);
candidate.setSdpCandidateAttribute(sdpCandidateChanged);
// IceLink 2
FMIceLinkOfferAnswer* offerAnswer;
FMIceLinkCandidate* candidate;

NSString* sdpMessage = offerAnswer.sdpMessage;
NSString* sdpMessageChanged = [sdpMessage stringByReplacingOccurrencesOfString: ... withString: ...];
[offerAnswer setSdpMessage: sdpMessageChanged];

NSString* sdpCandidate = candidate.sdpCandidateAttribute;
NSString* sdpCandidateChanged = [sdpCandidate stringByReplacingOccurrencesOfString: ... withString: ...];
[candidate setSdpCandidateAttribute: sdpCandidateChanged];
// IceLink 2
let offerAnswer:FMIceLinkOfferAnswer
let candidate:FMIceLinkCandidate

let sdpMessage:String = offerAnswer.sdpMessage
let sdpMessageChanged:String = sdpMessage.replacingOccurrences(...)
offerAnswer.setSdpMessage(value: sdpMessageChanged)

let sdpCandidate:String = candidate.sdpCandidateAttribute
let sdpCandidateChanged:String = sdpCandidate.replacingOccurrences(...)
candidate.setSdpMessage(value: sdpCandidateChanged)
// IceLink 2
var offerAnswer;
var candidate;

var sdpMessage = offerAnswer.getSdpMessage();
var sdpMessageChanged = sdpMessage.replace(...);
offerAnswer.setSdpMessage(sdpMessageChanged);

var sdpCandidate = candidate.getSdpCandidateAttribute();
var sdpCandidateChanged = sdpCandidate.replace(...);
sdpCandidate.setSdpMessage(sdpCandidateChanged);

For IceLink 3, these are instead represented by an object-oriented hierarchy. The OfferAnswer class has been renamed to FM.IceLink.SessionDescription, to conform to convention. The recommended way to modify SDP messages now is to use the API. If you need to work with a string, however, you can retrieve the string value by invoking the ToString method of the objects returned from the SdpMessage and SdpCandidateAttribute properties. To convert them back into objects of the appropriate type, use the FM.IceLink.Sdp.Message.Parse and FM.IceLink.Sdp.Ice.CandidateAttribute.Parse static methods.

// IceLink 3
FM.IceLink.SessionDescription sessionDescription;
FM.IceLink.Candidate candidate;

string sdpMessage = sessionDescription.SdpMessage.ToString();
string sdpMessageChanged = sdpMessage.Replace(...);
sessionDescription.SdpMessage = FM.IceLink.Sdp.Message.Parse(sdpMessageChanged);

string sdpCandidate = candidate.SdpCandidateAttribute.ToString();
string sdpCandidateChanged = sdpCandidate.Replace(...);
candidate.SdpCandidateAttribute = FM.IceLink.Sdp.Ice.CandidateAttribute.Parse(sdpCandidateChanged);
// IceLink 3
fm.icelink.SessionDescription sessionDescription;
fm.icelink.Candidate candidate;

String sdpMessage = sessionDescription.getSdpMessage().toString();
String sdpMessageChanged = sdpMessage.replace(...);
sessionDescription.setSdpMessage(fm.icelink.sdp.Message.parse(sdpMessageChanged));

String sdpCandidate = candidate.getSdpCandidateAttribute().toString();
String sdpCandidateChanged = sdpCandidate.replace(...);
candidate.setSdpCandidateAttribute(fm.icelink.sdp.ice.CandidateAttribute.parse(sdpCandidateChanged));
// IceLink 3
FMIceLinkSessionDescription* sessionDescription;
FMIceLinkCandidate* candidate;

NSString* sdpMessage = [sessionDescription.sdpMessage toString];
NSString* sdpMessageChanged = [sdpMessage stringByReplacingOccurrencesOfString: ... withString: ...];
[sessionDescription setSdpMessage:[FMIceLinkSdpMessage parseWithS:sdpMessageChanged]];

NSString* sdpCandidate = [candidate.sdpCandidateAttribute toString];
NSString* sdpCandidateChanged = [sdpMessage stringByReplacingOccurrencesOfString: ... withString: ...];
[sdpCandidate setSdpCandidateAttribute:[FMIceLinkSdpIceCandidateAttribute parseWithS:sdpCandidateChanged]];
// IceLink 3
FMIceLinkSessionDescription* sessionDescription;
FMIceLinkCandidate* candidate;

NSString* sdpMessage = [sessionDescription.sdpMessage toString];
NSString* sdpMessageChanged = [sdpMessage stringByReplacingOccurrencesOfString: ... withString: ...];
[sessionDescription setSdpMessage:[FMIceLinkSdpMessage parseWithS:sdpMessageChanged]];

NSString* sdpCandidate = [candidate.sdpCandidateAttribute toString];
NSString* sdpCandidateChanged = [sdpMessage stringByReplacingOccurrencesOfString: ... withString: ...];
[sdpCandidate setSdpCandidateAttribute:[FMIceLinkSdpIceCandidateAttribute parseWithS:sdpCandidateChanged]];
// IceLink 3
var sessionDescription;
var candidate;

var sdpMessage = sessionDescription.getSdpMessage().toString();
var sdpMessageChanged = sdpMessage.replace(...);
sessionDescription.setSdpMessage(fm.icelink.sdp.Message.parse(sdpMessageChanged));

var sdpCandidate = candidate.getSdpCandidateAttribute().toString();
var sdpCandidateChanged = sdpCandidate.replace(...);
candidate.setSdpMessage(fm.icelink.sdp.ice.CandidateAttribute.parse(sdpCandidateChanged));


Migration Steps

  • Replace references to FM.IceLink.OfferAnswer with FM.IceLink.SessionDescription.
  • If modifying an SDP message, invoke FM.IceLink.Sdp.Message.Parse or FM.IceLink.Sdp.Ice.CandidateAttribute.Parse on the modified string.

ActiveX Control

IceLink 2 offers an ActiveX control for Internet Explorer users. This allows these users to connect seamlessly with others who are using more modern browsers. To use the plugin, you must upload the associated cab files (available in the IceLink distribution) to your site and invoke the fm.icelink.webrtc.setActiveX method in your application code. The plugin will then be loaded when you attempt to connect.

// IceLink 2
fm.icelink.webrtc.setActiveX({
    path_x86: 'FM.IceLink.WebRTC.ActiveX.x86.cab',
    path_x64: 'FM.IceLink.WebRTC.ActiveX.x64.cab'
});

This plugin has been extended in IceLink 3 and the syntax has changed. There are no longer two separate cab files for x86 and x64 architecture. Before obtaining the local media of a user, you must define an fm.icelink.PluginConfig instance and specify the ActiveX control's location using the setActiveX path method. You then call the install method of the plugin, which returns a promise. When this promise is resolved, the plugin is installed and you can access the user's local media as you normally would.

The install method does nothing if the browser does not require it. You can safely wrap your code with this method and your application will still work in Chrome, FireFox and Edge.

// IceLink 3
var pluginConfig = new fm.icelink.PluginConfig();
pluginConfig.setActiveXPath("FM.IceLink.ActiveX.cab");

fm.icelink.Plugin.install(pluginConfig).then(result => 
    var localMedia = new fm.icelink.localMedia(true, true);
    localMedia.start();
    
    ...
);


Migration Steps

  • Instead of using the setActiveX method, create a new instance of PluginConfig and use the setActiveXPath method to set the path to the ActiveX control.
  • Invoke the install method using your plugin configuration and wrap your session start up code inside the callback.

Checking ICE Servers with LocalNetwork

In IceLink 3, the IceServerTest class has replaced and expanded on the functionality previously offered by LocalNetwork.CheckServer.

// IceLink 2
LocalNetwork.CheckServer(new CheckServerArgs("my-server-ip", 3478)
{
    RelayUsername = "username",
    RelayPassword = "password",
    OnSuccess = (e) =>
    {
        var myPublicIPAddress = e.PublicIPAddress;
        var myPublicPort = e.PublicPort;
    },
    OnFailure = (e) =>
    {
        Log.Error("Check failed.", e.Exception);
    }
});

// IceLink 3
var iceServerTest = new IceServerTest(new IceServer("my-server:3478", "username", "password"));
iceServerTest.Run().Then((e) =>
{
    // all candidates (host, srflx, relay) are returned here
    foreach (var candidate in e.ServerReflexiveCandidates)
    {
        var myPublicIPAddress = candidate.SdpCandidateAttribute.ConnectionAddress;
        var myPublicPort = candidate.SdpCandidateAttribute.Port;
    }
}, (ex) =>
{
    Log.Error("Check failed.", ex);
});

Working With the Logging API

IceLink 2 has a simple logging API that is used by the SDK. It allows you to hook into the API to write your own log messages or to handle log messages generated by the SDK. To do so, you specify an FM.LogProvider and pass it to the static FM.Log instance's Provider property.

// IceLink 2
FM.Log.Provider = new FM.ConsoleLogProvider(FM.LogLevel.Debug);

FM.Log.DebugFormat("This is a log {}", "message");
// IceLink 2
fm.Log.setProvider(new fm.ConsoleLogProvider(fm.LogLevel.Debug);
fm.Log.debugFormat("This is a log {}", "message");
// IceLink 2

[FMLog setProvider:[FMNSLogProvider nsLogProviderWithLogLevel:FMLogLevelDebug];

[FMLog debugFormatWithFormat:@"This is a log {}", @"message"];
// IceLink 2
FMLog.setProvider(provider: FMNSLogProvider(logLevel: FMLogLevelDebug))

FMLog.debugFormat(format: "This is a log {}", "message")
// IceLink 2
fm.Log.setProvider(new fm.consoleLogProvider(fm.logLevel.Debug));

fm.Log.debugFormat("This is a log {}", "message");

The logging API has changed slightly with IceLink 3. The various format methods, such as infoFormat, have been removed, as they were duplicating a functionality already provided by the various languages's string.Format equivalents. Additionally, the API has been extended to support multiple LogProviders. You can now use the RegisterProvider and RemoveProvider methods to manage the available log providers.

Note that because multiple log providers are now supported, there are two places in which a log severity level is specified. Each provider has an individual severity level but there is also a global severity level. If the global severity level is less severe than the severity of a log message, the message will not be output. You should also be aware that because the logging API is now bundled with IceLink, that it is completely separate from the logging API in other Frozen Mountain Projects. Specifically, if you are using WebSync for signalling, you will have to define separate log providers for WebSync.

// IceLink 3
FM.IceLink.Log.Level = FM.IceLink.LogLevel.Debug;
FM.IceLink.Log.RegisterProvider(new FM.IceLink.ConsoleLogProvider(FM.IceLink.LogLevel.Debug));

FM.IceLink.Log.Debug(string.Format("This is a log {}", "message"));
// IceLink 3
fm.icelink.Log.setLevel(fm.icelink.LogLevel.Debug);
fm.icelink.Log.registerProvider(new fm.icelink.ConsoleLogProvider(fm.icelink.LogLevel.Debug));

fm.icelink.Log.debug(String.format("This is a log %s", "message"));
// IceLink 3
[FMIceLinkLog setLevel:FMIceLinkLogLevelDebug];
[FMIceLinkLog registerProvider:[FMIceLinkNSLogProvider nsLogProviderWithLogLevel:FMIceLinkLogLevelDebug]];

[FMIceLinkLog debugWithMessage:[NSString stringWithFormat:@"This is a log %@", @"message"]];
// IceLink 3
FMIceLinkLog.setLevel(value: FMIceLinkLogLevelDebug)
FMIceLinkLog.registerProvider(provider: FMIceLinkNSLogProvider(logLevel: FMLogLevelDebug))

FMIceLinkLog.debug(String(format: "This is a log %@", "message"))
// IceLink 3
fm.icelink.Log.setLevel(fm.icelink.LogLevel.Debug);
fm.icelink.Log.registerProvider(fm.icelink.ConsoleLogProvider(fm.LogLevel.Debug));

fm.icelink.Log.debug(fm.icelink.StringExtensions.format("This is a log {}", "message"));


JavaScript does not have a native string formatting method, so the code snippet above uses our internal string formatting method. You can optionally add this to the prototype of the String class, to make it a bit cleaner.

String.prototype.format = function() {
    var args = Array.prototype.slice.call(arguments, 0);
    args.unshift(this);
    
    return fm.icelink.StringExtensions.format.apply(null, args);
}

fm.icelink.Log.debug("This is a log {0}".format(message));


Migration Steps

  • Remove any references to xFormat methods, and replace them with the native string.Format equivalent.

Wrapping Up

After this, your application should be fully migrated. If there is something we missed, please reach out at support@frozenmountain.com and https://support.frozenmountain.com.