Starting a Conference

In IceLink 3, the API for starting a video conference has changed significantly. This section goes through each step of establishing a conference in IceLink 2, and walks you through the equivalent action in IceLink 3.

What Happened to LocalMediaStream?

With IceLink 2, you use the GetMedia static method of the FM.IceLink.WebRTC.UserMedia class to access the user's device. In the OnSuccess callback, you then access a LocalMediaStream instance, which is a wrapper around the user's camera and microphone devices. An example of this is shown below:

// IceLink 2
FM.IceLink.WebRTC.UserMedia.GetMedia(new FM.IceLink.WebRTC.GetMediaArgs(true, true)
{
    OnSuccess = (FM.IceLink.WebRTC.GetMediaSuccessArgs e) =>
    {
        var localMedia = e.LocalStream;
    }
});
// IceLink 2
fm.icelink.webrtc.GetMediaArgs args = new fm.icelink.webrtc.GetMediaArgs(true, true);

args.setOnSuccess(new fm.SingleAction<fm.icelink.webrtc.GetMediaSuccessArgs>() {
    public void invoke(fm.icelink.webrtc.GetMediaSuccessArgs e) {
        fm.icelink.webrtc.LocalMediaStream localMedia = e.getLocalStream();
    }
});

fm.icelink.webrtc.UserMedia.getMedia(args);
// IceLink 2
FMIceLinkWebRTCGetMediaArgs* args = [FMIceLinkWebRTCGetMediaArgs getMediaArgsWithAudio:true video:true];

[args setOnSuccessBlock: ^(FMIceLinkWebRTCGetMediaSuccessArgs* e) {
    FMIceLinkWebRTCLocalMediaStream* localMedia = [e localStream];
}];

[FMIceLinkWebRTCUserMedia getMediaWithGetMediaArgs:args];
// IceLink 2
var args = FMIceLinkWebRTCGetMediaArgs(audio: true, video: true)

args.setOnSuccessBlock { (e:FMIceLinkWebRTCGetMediaSuccessArgs) in
    var localMedia = e.localStream()
}

FMIceLinkWebRTCUserMedia.getMedia(args)
// IceLink 2
fm.icelink.webrtc.userMedia.getMedia({
    audio: true,
    video: true,
    
    onSuccess: function(e) {
        var localMedia = e.getLocalStream();
    }
});

The LocalMediaStream class is mostly a black box. For example, if you want to add a different video codec to your application, you must do this at a global level, like so:

// IceLink 2
FM.IceLink.WebRTC.VideoStream.RegisterCodec("VP8", () =>
{
    return new Vp8Codec();
});
// IceLink 2
fm.icelink.webrtc.VideoStream.registerCodec("VP8", new fm.EmptyFunction<fm.icelink.webrtc.VideoCodec>() {
    public fm.icelink.webrtc.VideoCodec invoke() {
        return new Vp8Codec();
    }
});
// IceLink 2
FMIceLinkWebRTCVideoStream registerCodecWithEncodingName:@"VP8" createCodecBlock: ^() {
    return [Vp8Codec new];
}];
// IceLink 2
FMIceLinkWebRTCVideoStream.registerCodec(encodingName:"VP8", createCodecBlock: { () in
    return Vp8Codec()
})

One of the goals of IceLink 3 was to make this process more open and extensible. In IceLink 3, you now define your own LocalMedia class that inherits from RtcLocalMedia<T>. In this class, you specify the hardware input devices that you want to use, and the codecs that you will support. There is a minimal implementation of a LocalCameraMedia class below that accesses the user's camera and microphone. You can copy-paste this directly into your project and use it as is. It's beyond the scope of this guide to explain this in detail, but you should refer to the section on Capturing Local Media for more information on your available options.

To use this, you'll need to make sure that you include the Opus and Vp8 libraries, as well as libraries for hardware access. This is covered in the introduction of this migration guide, but in case you forget:

For C#, include: - FM.IceLink.NAudio.dll - FM.IceLink.AForge.dll - FM.IceLink.Opus.dll - FM.IceLink.Vpx.dll - FM.IceLink.Yuv.dll - if using WinForms, include FM.IceLink.WinForms.dll - if using WPF, include FM.IceLink.Wpf.dll

For Android and plain Java, include: - fm.icelink.opus.jar - fm.icelink.vpx.jar - fm.icelink.yuv.jar - if using Android, include fm.icelink.android.jar - if using Java, include fm.icelink.java.jar and fm.icelink.java.sarxos.jar

For iOS and macOS, include: - FMIceLinkOpus.h and libFMIceLinkOpus.a - FMIceLinkVpx.h and libFMIceLinkVpx.a - FMIceLinkYuv.h and libFMIceLinkYuv.a - FMIceLinkCocoa.h and libFMIceLinkCocoa.a

For JavaScript, see the next section.

// IceLink 3

// if using WinForms
public class LocalCameraMedia : FM.IceLink.RtcLocalMedia<FM.IceLink.WinForms.PictureBoxControl>

// if using WPF
public class LocalCameraMedia : FM.IceLink.RtcLocalMedia<System.Windows.Controls.Image>
{
    public LocalCameraMedia()
        : base()
    {
        Initialize();
    }
    
    protected override FM.IceLink.AudioSource CreateAudioSource(FM.IceLink.AudioConfig config)
    {
        return new FM.IceLink.NAudio.Source(config);
    }
    
    protected override FM.IceLink.AudioEncoder CreateOpusEncoder(FM.IceLink.AudioConfig config)
    {
        return new FM.IceLink.Opus.Encoder(config);
    }
    
    protected override VideoSource CreateVideoSource()
    {
        return new FM.IceLink.AForge.CameraSource(new FM.IceLink.VideoConfig(640, 480, 30));
    }
  
    protected override FM.IceLink.VideoEncoder CreateVp8Encoder()
    {
        return new FM.IceLink.Vp8.Encoder();
    }
    
    protected override FM.IceLink.VideoPipe CreateImageConverter(FM.IceLink.VideoFormat outputFormat)
    {
        return new FM.IceLink.Yuv.ImageConverter(outputFormat);
    }
    
    protected override FM.IceLink.VideoPipe CreateImageScaler()
    {
        return new FM.IceLink.Yuv.ImageScaler(1.0);
    }
    
    // if using WinForms
    protected override FM.IceLink.ViewSink<FM.IceLink.WinForms.PictureBoxControl> CreateViewSink()
    {
        return new FM.IceLink.WinForms.PictureBoxSink
        {
            ViewScale = LayoutScale.Contain,
            ViewMirror = true
        };
    }
    
    // if using WPF
    protected override FM.IceLink.ViewSink<System.Windows.Controls.Image> CreateViewSink()
    {
        return new FM.IceLink.Wpf.ImageSink
        {
            ViewScale = LayoutScale.Contain,
            ViewMirror = true
        };
    }
}
// IceLink 3

// for Android
public class LocalCameraMedia extends fm.icelink.RtcLocalMedia<android.view.View> {

// for plain Java
public class LocalCameraMedia extends fm.icelink.RtcLocalMedia<fm.iceink.java.VideoComponent> {
    // for Android
    private fm.icelink.android.CameraPreview viewSink;
    
    public LocalCameraMedia(Context context) {
        super();
        
        this.viewSink = new fm.icelink.android.CameraPreview(context, fm.icelink.LayoutScale.Contain);
        
        initialize();
    }
    
    // for Java
    public LocalCameraMedia() {
        super();
        initialize();
    }

    @Override
    protected fm.icelink.AudioSource createAudioSource(fm.icelink.AudioConfig audioConfig) {
        // for Android
        return new fm.icelink.android.AudioRecordSource(audioConfig);
        
        // for plain Java
        return new fm.icelink.java.SoundSource(audioConfig);
    }
    
    @Override
    protected fm.icelink.AudioEncoder createOpusEncoder(fm.icelink.AudioConfig audioConfig) {
        return new fm.icelink.opus.Encoder(audioConfig);
    }

    @Override
    protected fm.icelink.VideoSource createVideoSource() {
        // for Android
        return new fm.icelink.android.CameraSource(this.viewSink, new fm.icelink.VideoConfig(640, 480, 30));
    
        // for plain Java
        return new fm.icelink.java.sarxos.VideoSource(new fm.icelink.VideoConfig(640, 480, 30));
    }

    @Override
    protected fm.icelink.VideoEncoder createVp8Encoder() {
        return new fm.icelink.vp8.Encoder();
    }
    
    @Override
    protected fm.icelink.VideoPipe createImageConverter(VideoFormat videoFormat) {
        return new fm.icelink.yuv.ImageConverter(videoFormat);
    }

    @Override
    protected fm.icelink.VideoPipe createImageScaler() {
        return new fm.icelink.yuv.ImageScaler(1.0);
    }

    // for plain Java
    @Override
    protected fm.icelink.ViewSink<fm.icelink.java.VideoComponent> createViewSink() {
        return new fm.icelink.java.VideoComponentSink();
    }
    
    // for Android
    @Override
    protected fm.icelink.ViewSink<fm.icelink.android.View> createViewSink() {
        return null;
    }
    
    // for Android
    @Override
    public android.view.View getView() {
        return this.viewSink.getView();
    }
}
// IceLink 3

// LocalCameraMedia.h
@interface LocalCameraMedia : FMIceLinkRtcLocalMedia

// for iOS
@property (retain) FMIceLinkCocoaAVCapturePreview* preview;

@end

// LocalCameraMedia.m
@implementation LocalCameraMedia
- (instancetype) init {
    self = [super init];
    
    if (self != nil) {
        // for iOS
        self.preview = [FMIceLinkCocoaAVCapturePreview avCapturePreview];
        
        [self initialize];
    }
}

- (FMIceLinkAudioSource*) createAudioSourceWithConfig: (FMIceLinkAudioConfig*)config {
    return [FMIceLinkCocoaAudioUnitSource audioUnitSourceWithConfig:config];
}

- (FMIceLinkAudioEncoder*) createOpusEncoderWithConfig: (FMIceLinkAudioConfig *)config {
    return [FMIceLinkOpusEncoder encoderWithConfig:config];
}

- (FMIceLinkVideoSource*) createVideoSource {
    // for iOS
    return [FMIceLinkCocoaAVCaptureSource avCaptureSourceWithPreview:self.preview config:[FMIceLinkVideoConfig videoConfigWithWidth:640 height:480 frameRate:30]];
    
    // for macOS
    return [FMIceLinkCocoaAVCaptureSource avCaptureSourceWithConfig:[FMIceLinkVideoConfig videoConfigWithWidth:640 height:480 frameRate:30]];
}

- (FMIceLinkVideoEncoder*) createVp8Encoder {
    return [FMIceLinkVp8Encoder encoder];
}

- (FMIceLinkVideoPipe*) createImageConverterWithOutputFormat: (FMIceLinkVideoFormat*)outputFormat {
    return [FMIceLinkYuvImageConverter imageConverterWithOutputFormat:outputFormat];
}

- (FMIceLinkVideoPipe*) createImageScaler {
    return [FMIceLinkYuvImageScaler imageScalerWithScale:1.0];
}

- (FMIceLinkViewSink*) createViewSink {
    // for iOS
    return [FMIceLinkCocoaOpenGLSink openGLSinkWithViewScale:FMIceLinkLayoutScaleContain];
    
    // for macOS
    return [FMIceLinkCocoaImageViewSink imageViewSinkWithView:[[ContextNSView alloc] initWithId:nil]];
}
@end
// IceLink 3

class LocalCameraMedia : LocalMedia {
    // for iOS
    var preview: FMIceLinkCocoaAVCapturePreview

    override init() {
        super.init()
        
        // for iOS
        self.preview = FMICeLinkCocoaAVCapturePreview()
        
        self.initialize()
    }
    
    override func createAudioSource(config:FMIceLinkAudioConfig) -> FMIceLinkAudioSource {
        return FMIceLinkCocoaAudioUnitSource(config: config)
    }
    
    override func createOpusEncoder(config:FMICeLinkAudioConfig) -> FMIceLinkAudioEncoder {
        return FMIceLinkOpusEncoder(config: config)
    }
    
    override func createVideoSource() -> FMIceLinkVideoSource {
        // for iOS
        return FMIceLinkCocoaAVCaptureSource(preview: self.preview, config: FMIceLinkVideoCconfig(width: 640, height: 480, frameRate:30))
        
        // for macOS
        return FMIceLinkCocoaAVCaptureSource(config: FMIceLinkVideoConfig(width: 640, height: 480, frameRate: 30))
    }
    
    override func createVp8Encoder() -> FMIceLinkVideoEncoder {
        return FMIceLinkVp8Encoder()
    }
    
    override func createImageConverter(outputFormat:FMIceLinkVideoFormat) -> FMIceLinkVideoPipe {
        return FMIceLinkYuvImageConverter(outputFormat: outputFormat)
    }
    
    override func createImageScaler() -> FMIceLinkVideoPipe {
        return FMIceLinkYuvImageScaler(scale: 1.0)
    }
    
    override func createViewSink() -> FMIceLinkViewSink {
        // for iOS
        return FMIceLinkCocoaOpenGLSink(viewScale: FMIceLinkLayoutScaleContain)
        
        // for macOS
        return FMIceLinkCocoaImageViewSink()
    }
}


For JavaScript, you don't have to provide an implementation. The browser's codecs and hardware access are fixed, so there would be no point in forcing you to create your own implementation. For JavaScript, you can use the default class we provide, fm.icelink.LocalMedia. When instantiating this class, provide "true" for both parameters to use both audio and video, as shown below:

// IceLink 3
var localMedia = new fm.icelink.LocalMedia(true, true);


Other than this, the JavaScript implementation works exactly like the other platforms.

Migration Steps

  • Add the LocalCameraMedia class to your project.
  • Instead of invoking GetUserMedia, create an instance of your LocalCameraMediaclass.

Replace LocalMedia from the IceLink 2 examples with LocalMedia from the IceLink 3 examples.

More Information

  • For information on the local media options that you can specify, read the section on Capturing Local Media.

Initializing Media Streams

In IceLink 3, there are two important changes to the stream API. The first change is conceptual. With IceLink 2, when you supply a set of FM.IceLink.WebRTC.Stream instances to your FM.IceLink.Conference instance, the conference uses them as templates. Whenever a new link is needed, the conference generates a new set of streams based on the templates. With IceLink 3, the set of streams that you supply are used directly by the SDK. This means that you will create one set of streams every time you want to connect to a new peer.

The other change is practical. With IceLink 2, the FM.IceLink.AudioStream and FM.IceLink.VideoStream classes are instantiated by passing in your FM.IceLink.WebRTC.LocalMediaStream instance as the first parameter. The assumption is always that streams are bi-directional. This is shown below:

// IceLink 2
var audioStream = new FM.IceLink.WebRTC.AudioStream(localMediaStream);
var videoStream = new FM.IceLink.WebRTC.VideoStream(localMediaStream);
// IceLink 2
fm.icelink.webrtc.AudioStream audioStream = new fm.icelink.webrtc.AudioStream(localMediaStream);
fm.icelink.webrtc.VideoStream videoStream = new fm.icelink.webrtc.VideoStream(localMediaStream);
// IceLink 2
FMIceLinkWebRTCAudioStream* audioStream = [FMIceLinkWebRTCAudioStream audioStreamWithLocalMediaStream:localMediaStream];
FMIceLinkWebRTCVideoStream* videoStream = [FMIceLinkWebRTCVideoStream videoStreamWithLocalMediaStream:localMediaStream];
// IceLink 2
var audioStream = FMIceLinkWebRTCAudioStream(localMediaStream: localMediaStream)
var videoStream = FMIceLinkWebRTCVideoStream(localMediaStream: localMediaStream)
// IceLink 2
var audioStream = new fm.icelink.webrtc.audioStream(localMediaStream);
var videoStream = new fm.icelink.webrtc.videoStream(localMediaStream);


With IceLink 3, streams must now explicitly define both endpoints - the inputs and outputs - for audio and video data. This means that in IceLink 3, you need an object to represent the remote endpoint. This new object is known as a remote media instance. When you declare streams in IceLink 3, you must create a new RemoteMedia instance and provide it to the audio and video streams, along with your LocalCameraMedia instance. See the example below:

// IceLink 3
var localMedia = new LocalCameraMedia();
var remoteMedia = new RemoteMedia();

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

var audioStream = new fm.icelink.AudioStream(localMedia, remoteMedia);
var videoStream = new fm.icelink.VideoStream(localMedia, remoteMedia);
// IceLink 3
LocalCameraMedia* localMedia = [LoccalCameraMedia new];
RemoteMedia* remoteMedia = [RemoteMedia new];

FMIceLinkAudioStream* audioStream = [FMIceLinkAudioStream audioStreamWithLocalMedia:localMedia remoteMedia:remoteMedia];
FMIceLinkVideoStream* videoStream = [FMIceLinkVideoStream videoStreamWithLocalMedai:localMedia remoteMedia:remoteMedia];
// IceLink 3
var localMedia = LocalCameraMedia()
var remoteMedia = RemoteMedia()

var audioStream = FMIceLinkAudioStream(localMedia: localMedia, remoteMedia: remoteMedia)
var videoStream = FMIceLinkVideoStream(localMedia: localMedia, remoteMedia, remoteMedia)
// IceLink 3
var localMedia = new fm.icelink.LocalMedia(true, true);
var remoteMedia = new fm.icelink.RemoteMedia();

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

The RemoteMedia class above is not provided by the IceLink 3 SDK. Similar to the LocalCameraMedia class described earlier, you must provide an implementation of this class. Your implementation defines which codecs are available and other properties of the connection. The section on Receiving Remote Media has more information on the available options. For now, you can copy-paste one of the implementations below into your project.

// IceLink 3

// if using WinForms
public class RemoteMedia : FM.IceLink.RtcRemoteMedia<FM.IceLink.WinForms.PictureBoxControl>

// if using WPF
public class RemoteMedia : FM.IceLink.RtcRemoteMedia<System.Windows.Controls.Image>
{
    public RemoteMedia()
        : base(false, false)
    {
        Initialize();
    }
    
    protected override FM.IceLink.AudioSink CreateAudioSink(FM.IceLink.AudioConfig config)
    {
        return new FM.IceLink.NAudio.Sink(config);
    }
    
    protected override FM.IceLink.AudioDecoder CreateOpusDecoder(FM.IceLink.AudioConfig config)
    {
        return new FM.IceLink.Opus.Decoder(config);
    }
    
    protected override FM.IceLink.VideoDecoder CreateVp8Decoder()
    {
        return new FM.IceLink.Vp8.Decoder();
    }
    
    protected override FM.IceLink.VideoPipe CreateImageConverter(FM.IceLink.VideoFormat outputFormat)
    {
        return new FM.IceLink.Yuv.ImageConverter(outputFormat);
    }
    
    // if using WinForms
    protected override FM.IceLink.ViewSink<FM.IceLink.WinForms.PictureBoxControl> CreateViewSink()
    {
        return new FM.IceLink.WinForms.PictureBoxSink
        {
            ViewScale = LayoutScale.Contain
        };
    }
    
    // if using WPF
    protected override FM.IceLink.ViewSink<System.Windows.Controls.Image> CreateViewSink()
    {
        return new FM.IceLink.Wpf.ImageSink
        {
            ViewScale = LayoutScale.Contain
        };
    }
}
// IceLink 3

// for Android
public class RemoteMedia extends fm.icelink.RtcRemoteMedia<fm.icelink.android.OpenGLSink> {

// for plain Java
public class RemoteMedia extends fm.icelink.RtcRemoteMedia<fm.icelink.java.VideoComponent> {
    public RemoteMedia() {
        super(false, false);
        super.initialize();
    }

    @Override
    protected fm.icelink.AudioSink createAudioSink(fm.icelink.AudioConfig audioConfig) {
        // for Android
        return new fm.icelink.android.AudioTrackSink(audioConfig);
        
        // for plain Java
        return new fm.icelink.java.SoundSink(audioConfig);
    }

    @Override
    protected fm.icelink.AudioDecoder createOpusDecoder(fm.icelink.AudioConfig audioConfig) {
        return new fm.icelink.opus.Decoder(audioConfig);
    }

    @Override
    protected fm.icelink.VideoDecoder createVp8Decoder() {
        return new fm.icelink.vp8.Decoder();
    }
    
    @Override
    protected fm.icelink.VideoPipe createImageConverter(fm.icelink.VideoFormat videoFormat) {
        return new fm.icelink.yuv.ImageConverter(videoFormat);
    }

    // for Android
    @Override
    protected fm.icelink.ViewSink<fm.icelink.adnroid.OpenGLSink> createViewSink() {
        return new fm.icelink.android.OpenGLSink();
    }
    
    // for plain Java
    @Override
    protected fm.icelink.ViewSink<fm.icelink.java.VideoComponent> createViewSink() {
        return new fm.icelink.java.VideoComponentSink();
    }
}
// IceLink 3

// RemoteMedia.h
@interface RemoteMedia : FMIceLinkRtcRemoteMedia {
@end    

// RemoteMedia.m
@implementation RemoteMedia
- (instancetype) init {
    self = [super initWithDisableAudio:false disableVideo:false];
    
    if (self != nil) {
        [self initialize];
    }
    
    return self;
}
    
- (FMIceLinkAudioSink*) createAudioSinkWithConfig: (FMIceLinkAudioConfig*)config {
    return [FMIceLinkCocoaAudioUnitSink audioUnitSinkWithConfig:config];
}

- (FMIceLinkAudioDecoder*) createOpusDecoderWithConfig: (FMIceLinkAudioConfig*)config {
    return [FMIceLinkOpusDecoder decoderWithConfig:config];
}
    
- (FMIceLinkVideoDecoder*) createVp8Decoder {
    return [FMIceLinkVp8Decoder decoder];
}
    
- (FMIceLinkVideoPipe*) createImageConverterWithOutputFormat: (FMIceLinkVideoFormat*)outputFormat {
    return [FMIceLinkYuvImageConverter imageConverterWithOutputFormat:outputFormat];
}
   
- (FMIceLinkViewSink*) createViewSink {
    // for iOS
    return [FMIceLinkCocoaOpenGLSink openGLSinkWithViewScale:FMIceLinkLayoutScaleContain];
    
    // for macOS
    return [FMIceLinkCocoaImageViewSink imageViewSink];
}
@end
// IceLink 3

class RemoteMedia : FMIceLinkRtcRemoteMedia {
    override init() {
        super.init(false, false)
    
        self.iniitalize()
    }
    
    override func createAudioSink(config:FMIceLinkAudioConfig) -> FMIceLinkAudioSink {
        return FMIceLinkCocoaAudioUnitSink(config: config)
    }
    
    override func createOpusDecoder(config:FMIceLinkAudioConfig) -> FMIceLinkAudioDecoder {
        return FMIcelinkOpusDecoder(config: config)
    }
    
    override func createVp8Decoder() -> FMIceLinkVideoDecoder {
        rreturn FMIceLinkVp8Decoder()
    }
    
    override func createImageConverter(outputFormat:FMIceLinkVideoFormat) -> FMIceLinkVideoPipe {
        return FMIceLinkYuvImageConverter(outputFormat: outputFormat)
    }
    
    override func createViewSink() -> FMIceLinkViewSink {
        // for iOS
        return FMIceLinkCocoaOpenGLSink(viewScale: FMIceLinkLayoutScaleContain)
    
        // for macOS
        return FMIceLinkCocoaImageViewSink()
    }
}

Once again, you don't need to provide an implementation for JavaScript, as the browser doesn't allow you to extend the existing functionality. You can use the fm.icelink.Remote media class instead. Note that for every person you connect to, you'll be creating one RemoteMedia instance and one set of stream objects.

Migration Steps

  • Add the RemoteMedia class to your project.
  • When you create an AudioStream or a VideoStream, create a RemoteMedia instance and provide it to the stream instances.

More Information

Initializing Data Streams

With IceLink 2, you can send arbitrary text or binary data to participants in a conference using the ReliableDataStream class. Each ReliableDataStream instance can have one or more ReliableDataChannel instances, each of which is supposed to represent one "type" of data. For example, you might have one data channel for chat messages and one for file transfers.

// IceLink 2
var dataChannel = new FM.IceLink.WebRTC.ReliableDataChannel("label")
{
    OnReceive = (FM.IceLink.WebRTC.DataChannelReceiveArgs e) =>
    {
        ...
    }
};

var dataStream = new FM.IceLink.WebRTC.ReliableDataStream(dataChannel);
// IceLink 2
fm.icelink.WebRTC.ReliableDataChannel dataChannel = new fm.icelink.WebRTC.ReliableDataChannel("label");
dataChannel.setOnReceive(new fm.SingleAction<fm.icelink.webrtc.DataChannelReceiveArgs>() {
    public void invoke(fm.icelink.webrtc.DataChannelReceiveArgs e) {
        ...
    }
});

fm.icelink.webrtc.ReliableDataStream dataStream = new fm.icelink.webrtc.ReliableDataStream(dataChannel);
// IceLink 2
FMIceLinkWebRTCReliableDataChannel* dataChannel = [FMIceLinkWebRTCReliableDataChannelWithLabel:@"label"];
[dataChannel setOnReceiveWithBlock: ^(FMIceLinkWebRTCDataChannelReceiveArgs* e) {
    ...
}];

FMIceLinkReliableDataStream* dataStream = [FMIceLinkReliableDataStreamWithChannel:dataChannel];
// IceLink 2
var dataChannel = FMIceLinkWebRTCReliableDataChannel(label: "label")
dataChannel.setOnReceive { (e:FMIceLinkWebRTCDataChannelReceiveArgs) in
    ...
}

var dataStream = FMIceLinkWebRTCReliableDataStream(channel: dataChannel)
// IceLink 2
var dataChannel = new fm.icelink.webrtc.reliableDataChannel("label");
dataChannel.setOnReceive(function(e) {
    ...
});

var dataStream = new fm.icelink.webrtc.reliableDataStream(dataChannel);

In IceLink 3, this same functionality is available. For brevity, the classes' names have been shortened to DataStream and DataChannel. They are otherwise identical, as shown below.

// IceLink 3

var dataChannel = new DataChannel("label")
{
    OnReceive = (FM.IceLink.DataChannelReceiveArgs e) =>
    {
        ...
    }
};

var dataStream = new FM.IceLink.DataStream(dataChannel);
// IceLink 3
fm.icelink.DataChannel dataChannel = new fm.icelink.DataChannel("label"):
dataChannel.setOnReceive((fm.icelink.DataChannelReceiveArgs e) -> {
    ...
});

fm.icelink.DataStream dataStream = new fm.icelink.DataStream(dataChannel);
// IceLink 3
FMIceLinkDataChannel* dataChannel = [FMIceLinkDataChannelWithLabel:@"label"];
[dataChannel setOnReceiveWithBlock: ^(FMIceLinkDataChannelReceiveArgs* e) {
    ...
}];

FMIceLinkDataStream* dataStream = [FMIceLinkDataStreamWithChannel:dataChannel];
// IceLink 3
var dataChannel = FMIceLinkDataChannel(label: "label")
dataChannel.setOnReceive { (e:FMIceLinkDataChannelReceiveArgs) in
    ...
}

var dataStream = FMIceLinkDataStream(channel: dataChannel)
// IceLink 3
var dataChannel = new fm.icelink.DataChannel("label");
dataChannel.setOnReceive(function(e) {
    ...
});

var dataStream = new fm.icelink.DataStream(dataChannel);


Migration Steps

  • Rename ReliableDataChannel to DataChannel and ReliableDataStream to DataStream.

More Information

Creating a Conference

The main classes in IceLink 2 are the FM.IceLink.Link, which represents an individual connection between two peers - and a FM.IceLink.Conference - which represents a collection of links participating in a single video conference. Normally, you control the conference as a whole, using the Conference instance. You create a conference by instantiating it with a server address and a set of streams, as you can see below.

// IceLink 2
var conference = new FM.IceLink.Conference("127.0.0.1", streams);
// IceLink 2
fm.icelink.Conference conference = new fm.icelink.Conference("127.0.0.1", streams);
// IceLink 2
FMIceLinkConference* conference = [FMIceLinkConference conferenceWithServerAddress:@"127.0.0.1" streams:streams];
// IceLink 2
var conference = FMIceLinkConference(serverAddress: "127.0.0.1" streams: streams)
// IceLink 2
var conference = new fm.icelink.conference('127.0.0.1', streams);


With IceLink 3, there is no Conference equivalent. Instead, you work with individual FM.IceLink.Connection instances, which are roughly equivalent to the Link class from IceLink 2. There are no more "session-level" settings - each Connection instance has its own configuration. To instantiate a Connection, you provide it with your set of streams, like you would with a conference. You do not specify the server here; this is covered in the next section on STUN / TURN servers.

You can keep track of your connections with the new FM.IceLink.ConnectionCollection class. This is, as advertised, a collection that holds Connection instances. It has no functionality other than to hold connections. The code below shows an example of creating a Connection instance and storing it in a ConnectionCollection.

The following section has several snippets describing the API changes. If you are using the WebSync extension, skip ahead to the final section, where it is described in detail. The next code snippet briefly shows the instantiation of a Connection object versus a Conference. You may notice we don't pass a STUN/TURN server URL to the Connection instance - this will be covered later.

// IceLink 3
var connections = new ConnectionCollection();
var connection = new Connection(streams)
{
    Id = "12345"
};

connections.Add(connection);
// IceLink 3

fm.icelink.ConnectionCollection connections = new fm.icelink.ConnectionCollection();
fm.icelink.Connection connection = new fm.icelink.Connection(streams);

connection.setId("12345");
connections.add(connection);
// IceLink 3
FMIceLinkConnectionCollection* connections = [FMIceLinkConnectionCollection connectionCollection];
FMIceLinkConnection* connection = [FMIceLinkConnection connectionWithStreams:streams];

[connection setId:@"12345"];
[connections addWithConnection:connection];
// IceLink 3
var connections = FMIceLinkConnectionCollection()
var connection = FMIceLinkConnection(streams: streams)

connection.setId("12345")
connections.add(connection: connection)
// IceLink 3
var connections = new fm.icelink.ConnectionCollection();
var connection = new fm.icelink.Connection(streams);

connection.setId("12345");
connections.add(connection);

The situations where you would create a Connection are covered a bit further on in this guide. For now, you can replace all calls to the Link method of your Conference instance by creating a new Connection instance instead.

Migration Steps

  • Whenever you create a Conference, instead create a ConnectionCollection.
  • Whenever you create a Link, instead create a Connection.
  • Instead of invoking the Link method of your Conference instance, create a new Connection instance and add it to a ConnectionCollection.

More Information

Specifying Relay Servers

In IceLink 2, when you create a Conference instance, you must specify a server address. This server address is the address of an ICE server. An ICE server helps establish connections between peers using either the STUN protocol or the TURN protocol. STUN stands for Session Traversal Utilities for NAT and helps peers discover their public IP addresses. TURN stands for Traversal Using Relays around NAT and forwards peers's audio and video data through a relay server. IceLink 2 makes no difference between these two and refers to them both as relay servers. The IceLink 2 SDK allows you to specify usernames and passwords for TURN authentication through the RelayUsernames and RelayPasswords properties of your Conference instance.

The example below shows how to instantiate a conference object with two servers, a STUN server and a TURN server, and how to specify the username and password for a TURN server.

// IceLink 2
var conference = new FM.IceLink.Conference(new string[] { "stun.server.com", "turn.server.com" }, streams);
conference.RelayUsernames = new string[] { null, "username" };
conference.RelayPasswords = new string[] { null, "password" };
// IceLink 2
fm.icelink.Conference conference = new fm.icelink.Conference(new String[] {"stun.server.com", "turn.server.com"], streams);
conference.setRelayUsernames(new String[] { null, "username"});
conference.setRelayPasswords(new String[] { null, "password"});
// IceLink 2
NSMutableArray* serverAddresses = [NSMutableArray arrayWithObjects:@"stun.server.com", @"turn.server.com"];
NSMutableArray* relayUsernames = [NSMutableArray arrayWithObject:nil, @"username"];
NSMutableArray* relayPasswords = [NSMutableArray arrayWithObject:nil, @"password"];

FMIceLinkConference* conference = [FMIceLinkConference conferenceWithServerAddresses:serverAddresses];
[conference setRelayUsernames:relayUsernames];
[conference setRelayPasswords:relayPasswords];
// IceLink 2
var serverAddresses = ["stun.server.com", "turn.server.com"]
var relayUsernames = [nil, "username"]
var relayPasswords = [nil, "password"]

var conference = FMIceLinkConference(serverAddresses)
conference.setRelayUsernames(relayUsernames);
conference.setRelayPasswords(relayPasswords);
// IceLink 2
var conference = new fm.icelink.conference(["stun.server.com", "turn.server.com"], streams);
conference.setRelayUsernames([null, "username"]);
conference.setRelayPasswords([null, "password"]);

IceLink 3 is much more precise when specifying ICE servers. Each Connection instance has an IceServers property, which takes an array of FM.IceLink.IceServer instances. The IceServer class wraps information about a particular ICE server, including any TURN credentials that are required. This approach also makes integrating with third-party STUN/TURN services easier, as they will often provide you with URIs containing the stun: and turn: schemes. An example below shows how to add ICE servers to a Connection instance.

// IceLink 3
var connection = new FM.IceLink.Connection(streams);
connection.IceServers = new[]
{
    new FM.IceLink.IceServer("stun:stun.server.com"),
    new FM.IceLink.IceServer("turn:turn.server.com", "username", "password")
};
// IceLink 3
fm.icelink.Connection connection = new fm.icelink.Connection(streams);
connection.setIceServers(new fm.icelink.IceServer[] {
    new fm.icelink.IceServer("stun:stun.server.com"),
    new fm.icelink.IceServer("turn:turn.server.com", "username", "password")
});
// IceLink 3
FMIceLinkConnection* connection = [FMIceLinkConnection connectionWithStreams:streams];
[connection setIceServers: [NSMutableArray arrayWithObjects:
    [FMIceLinkIceServer iceServerWithUrl:@"stun:stun.server.com"],
    [FMIceLinkIceServer iceServerWithUrl:@"turn:turn.server.com", "username", "password"],
    nil
]];
// IceLink 3
var connection = FMIceLinkConnection(streams: streams)

connection.setIceServers([
    FMIceLinkIceServer(url: "stun:stun.server.com"),
    FMIceLinkIceServer(url: "turn:turn.server.com", "username", "password")
])
// IceLink 3
var connection = new fm.icelink.Connection(streams);
connection.setIceServers([
    new fm.icelink.IceServer("stun:stun.server.com"),
    new fm.icelink.IceServer("turn:turn.server.com", "username", "password")
]);

Note that these servers are validated more strictly than in IceLink 2. If you specify the turn: scheme, you must provide a username and password. Likewise, if you specify the stun:, you must not provide either a username or a password.

Migration Steps

  • Instead of specifying the serverAddress parameter of your Conference instance, set the IceServers property of the Connection instance to an array of IceServer instances using the address of your STUN or TURN server. Remember to specify either a stun: or turn: URI scheme.
  • If you were using a single server address for both STUN and TURN, create two IceServer instances - one for STUN and one for TURN. Refer to the code sample above.
  • If you were using the RelayUsername or RelayPassword properties, specify them when you create the IceServer instance instead.

More Information

  • For more information on opening connections and ICE servers, see the Opening a Connection section.

Using the Layout Manager

With IceLink 2, you create an FM.IceLink.WebRTC.LayoutManager instance to manage the layout of the video feeds in a conference. Normally, this is done immediately after a successful call to GetMedia. In the callback, you retrieve the user's local video control and then assign it to the layout manager with the SetLocalVideoControl method. When you are finished with a session, you can remove the video control with the UnsetLocalVideoControl method. This is shown below:

// IceLink 2
FM.IceLink.WebRTC.UserMedia.GetMedia(new FM.IceLink.WebRTC.GetMediaArgs(true, true)
{
    OnSuccess = (FM.IceLink.WebRTC.GetMediaSuccessArgs e) =>
    {
        var layoutManager = new FM.IceLink.WebRTC.LayoutManager(...);
        
        layoutManager.SetLocalVideoControl = e.LocalVideoControl
        
        ...
        
        layoutManager.UnsetLocalVideoControl();
    }
});
// IceLink 2
var args = new fm.icelink.webrtc.GetMediaArgs(true, true);

args.setOnSuccess(new fm.SingleAction<fm.icelink.webrtc.GetMediaSuccessArgs>() {
    public void invoke(fm.icelink.webrtc.GetMediaSuccessArgs e) {
        fm.icelink.webrtc.LayoutManager layoutManager = new fm.icelink.webrtc.LayoutManager(...);
        
        layoutManager.setLocalVideoControl(e.getLocalVideoControl());
        
        ...
        
        layoutManager.unsetLocalVideoControl();
    }
});

fm.icelink.webrtc.UserMedia.getMedia(args);
// IceLink 2
FMIceLinkWebRTCGetMediaArgs* args = [FMIceLinkWebRTCGetMediaArgs getMediaArgsWithAudio:true video:true];

[args setOnSuccessBlock: ^(FMIceLinkWebRTCGetMediaSuccessArgs* e) {
    FMIceLinkWebRTCLayoutManager* layoutManager = [FMIceLinkWebRTCLayoutManager layoutManagerWithContainer:...];
    
    [layoutManager setLocalVideoControlWithLocalVideoControl:[e localVideoControl]];
    
    ...
    
    [layoutManager unsetLocalVideoControl]
}];

[FMIceLinkWebRTCUserMedia getMediaWithGetMediaArgs:args];
// IceLink 2
var args = FMIceLinkWebRTCGetMediaArgs(audio: true, video: true)

args.setOnSuccessBlock { (e:FMIceLinkWebRTCGetMediaSuccessArgs) in
    var layoutManager = FMIceLinkWebRTCLayoutManager(container: ...)
    
    layoutManager.setLocalVideoControl(localVideoControl: e.localVideoControl())
    
    ...
    
    layoutManager.unsetLocalVideoControl();
}

FMIceLinkWebRTCUserMedia.getMedia(getMediaArgs: args)
// IceLink 2
fm.icelink.webrtc.userMedia.getMedia({
    audio: true,
    video: true,
    
    onSuccess: function(e) {
        var layoutManager = new fm.icelink.webrtc.layoutManager(...);
        
        layoutManager.setLocalVideoControl(e.getLocalVideoControl());
        
        ...
        
        layoutManager.unsetLocalVideoControl();
    }
});

With IceLink 3, this process has a few differences. The main difference is that instead of a single layout manager class, there are now multiple strongly-typed layout manager implementations. For example, instead of creating anFM.IceLink.WebRTC.LayoutManager, you would create either an FM.IceLink.Wpf.LayoutManager or an FM.IceLink.WinForms.LayoutManager instance. The other key differences are that SetLocalVideoControl has been renamed to SetLocalView and the local media view is now accessed with the View property. This is part of a larger change, where video controls from IceLink 2 are now referred to as views.

One last thing worth mentioning is that with IceLink 2, it is natural to instantiate your layout manager after the GetMedia call is successful. With IceLink 3, since there is no more GetMedia, it makes sense to instantiate the layout manager when you create your LocalCameraMedia instance.

// IceLink 3
var localMedia = new LocalCameraMedia();

// if using WinForms
var layoutManager = new FM.IceLink.WinForms.LayoutManager(...);
// if using WPF
var layoutManager = new FM.IceLink.Wpf.LayoutManager(...);

layoutManager.SetLocalView(localMedia.View);

...

layoutManager.UnsetLocalView();
// IceLink 3
LocalCameraMedia localMedia = new LocalCameraMedia();

// for Android
var layoutManager = new fm.icelink.android.LayoutManager(...);
// for Java
var layoutManager = new fm.icelink.java.LayoutManager(...);

layoutManager.setLocalView(localMedia.getView();

...

layoutManager.unsetLocalView();
LocalCameraMedia* localMedia = [LocalCameraMedia new];
FMIceLinkCocoaLayoutManager* layoutManager = [FMIceLinkCocoaLayoutManager layoutManagerWithContainer:...];

[layoutManager setLocalViewWithView: [localMedia view]];
// IceLink 3
var localMedia = LocalCameraMedia()
var layoutManager = FMIceLinkCocoaLayoutManager(...)

layoutManager.setLocalView(localMedia.view())

...

layoutManager.unsetLocalView()
// IceLink 3
var localMedia = new fm.icelink.LocalMedia(true, true);
var layoutManager = new fm.icelink.DomLayoutManager(...);

layoutManager.setLocalView(localMedia.getView());

...

layoutManager.unsetLocalView()


With IceLink 2, you also have to manage remote video controls. When a party joins a video conference, you must add a remote video control for them and when they leave, you must remove it. This is accomplished using the AddRemoteVideoControl and RemoveRemoteVideoControl methods and is normally done by adding OnLinkInit and OnLinkDown event handlers to your VideoStream instance as shown below:

// IceLink 2
var videoStream = new FM.IceLink.WebRTC.VideoStream(localMediaStream);

videoStream.OnLinkInit += (FM.IceLink.LinkInitArgs e) =>
{
    layoutManager.AddRemoteVideoControl(e.PeerId, e.Link.GetRemoteVideoControl());
}

videoStream.OnLinkDown += (FM.IceLink.LinkDownArgs e) =>
{
    layoutMAnager.RemoveRemoteVideoControl(e.PeerId);
}
// IceLink 2
fm.icelink.webrtc.VideoStream videoStream = new fm.icelink.webrtc.VideoStream(localMediaStream);

videoStream.addOnLinkInit(new fm.SingleAction<fm.icelink.LinkInitArgs>() {
    public void invoke(fm.icelink.LinkInitArgs e) {
        layoutManager.addRemoteVideoControl(e.getPeerId(), e.getLink().getRemoteVideoControl());
    }
});

videoStream.addOnLinkDown(new fm.SingleAction<fm.icelink.LinkDownArgs>() {
    public void invoke(fm.icelink.LinkDownArgs e) {
        layoutManager.removeRemoteVideoControl(e.getPeerId());
    }
});
// IceLink 2
FMIceLinkWebRTCVideoStream* videoStream = [FMIceLinkWebRTCVideoStream videoStreamWithLocalStream:localMediaStream];

[videoStream addOnLinkInitBlock: ^(FMIceLinkLinkInitArgs* e) {
    [layoutManager addRemoteVideoControlWithPeerId:[e peerId] remoteVideoControl:[[e link] getRemoteVideoControl]];
}];

[videoStream addOnLinkDownBlock: ^(FMIceLinkLinkDownArgs* e) {
    [layoutManager removeRemoteVideoControlWithPeerId:[e peerId]];
}];
// IceLink 2
var videoStream = FMIceLinkWebRTCVideoStream(localStream: localMediaStream)

videoStream.addOnLinkInitBlock: { (e:FMIceLinkLinkInitArgs) in
    layoutManager.addRemoteVideoControl(peerId: e.peerId(), remoteVideoControl: e.link().getRemoteVideoControl())
}

videoStream.addOnLinkDownBlock: { (e:FMIceLinkLinkDownArgs) in
    layoutManager.removeRemoteVideoControl(peerId: e.peerId())
}
// IceLink 2
var videoStream = new fm.icelink.webrtc.videoStream(localMediaStream);

videoStream.addOnLinkInit(function(e) {
    layoutManager.addRemoteVideoControl(e.getPeerId(), e.getLink().getRemoteVideoControl());
});

videoStream.addOnLinkDown(function(e) {
    layoutManager.removeRemoteVideoControl(e.getPeerId());
});

With IceLink 3, the methods AddRemoteVideoControl and RemoveRemoteVideoControl have been renamed to AddRemoteView and RemoveRemoteView. Also, as mentioned earlier, there are no more OnLink event handlers. Instead, you should now perform these actions in the OnStateChange event handler of the Connection class. You will need to have created a RemoteMedia instance for the connection by now, as you need to access its view so that you can add it to your layout manager. This is shown below.

// IceLink 3
var remoteMedia = new RemoteMedia();

connection.OnStateChange += (FM.IceLink.Connection c) =>
{
    if (c.State == FM.IceLink.ConnectionState.Initializing)
    {
        layoutManager.AddRemoteView(remoteMedia.Id, remoteMedia.View);
    }
    
    if (c.State == FM.IceLink.ConnectionState.Closing || c.State == FM.IceLink.ConnectionState.Failing)
    {
        layoutManager.RemoveRemoteView(remoteMedia.Id);
    }
};
// IceLink 3
final RemoteMedia remoteMedia = new RemoteMedia();

connection.addOnStateChange((fm.icelink.Connection c) -> {
    if (c.getState() == fm.icelink.ConnectionState.Initalizing) {
        layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());
    }
    
    if (c.getState() == fm.icelink.ConnectionState.Closing || c.getState() == fm.icelink.ConnectionState.Failing) {
        layoutManager.removeRemoteView(remoteMedia.getId());
    }
});
// IceLink 3
RemoteMedia* remoteMedia = [RemoteMedia new];

[connection addOnStateChangeWithBlock: ^(FMIceLinkConnection* c) {
    if ([c state] == FMIceLinkConnectionStateInitializing) {
        [layoutManager addRemoteViewWithId:[remoteMedia id] view:[remoteMedia view]];
    }
    
    if ([c state] == FMIceLinkConnectionStateClosing || [c state] == FMIceLinkConnectionStateFailing) {
        [layoutManager removeRemoteViewWithId:[remoteMedia id];
    }
}];
// IceLink 3
var remoteMedia = RemoteMedia()

connection.addOnStateChange { (c:FMIceLinkConnection) in
    if c.state() == FMIceLinkConnectionStateInitializing {
        layoutManager.addRemoteView(id: remoteMedia.id(), view: remoteMedia.view())
    }
    
    if c.state() == FMIceLinkConnectionStateClosing || c.state() == FMIceLinkConnectionStateFailing {
        layoutManager.removeRemoteView(id: remoteMedia.id())
    }
}
// IceLink 3
var remoteMedia = new fm.icelink.RemoteMedia();

connection.addOnStateChange(function(c) {
    if (c.getState() == fm.icelink.ConnectionStateInitializing) {
        layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());
    }
    
    if (c.getState() == fm.iceilnk.ConnectionStateClosing || c.getState() == fm.icelink.ConnectionStateFailing) {
        layoutManager.removeRemoteView(remoteMedia.getId());
    }
});


Migration

  • Instead of creating an instance of FM.IceLink.WebRTC.LayoutManager, use one of the implementations described above, as appropriate for your platform.
  • Replace calls to SetLocalVideoControl with SetLocalView.
  • Replace calls to UnsetLocalVideoControl with UnsetLocalView.
  • Replace calls to AddRemoteVideoControl with AddRemoteView.
  • Replace calls to RemoveRemoteVideoControl with RemoveRemoteView.

More Information

Linking to Peers

In IceLink 2, you invoke the Link method of an FM.IceLink.Conference instance to connect to a remote party. This creates an FM.IceLink.Link object and begins the offer/answer process. The link creates an FM.IceLink.OfferAnswer, which is retrieved by adding an OnLinkOfferAnswer event handler to the `Conference instance. In this event handler, you then forward the offer to the remote party. An example of this using WebSync is shown below.

// IceLink 2
conference.Link("peer-id");

conference.OnLinkOfferAnswer += (FM.IceLink.LinkOfferAnswerArgs e) =>
{
    client.Notify(new FM.WebSync.NotifyArgs(e.PeerId, e.OfferAnswer.ToJson(), "offer");
};
// IceLink 2
conference.link("peer-id");

conference.addOnLinkOfferAnswer(new fm.SingleAction<fm.icelink.LinkOfferAnswerArgs>() {
    public void invoke(fm.icelink.LinkOfferAnswerArgs e) {
        client.notify(new fm.websync.NotifyArgs(e.getPeerId(), e.getOfferAnswer().toJson(), "offer");
    }
});
// IceLink 2
[conference linkWithPeerId:@"peer-id"];

[conference addOnLinkOfferAnswerWithValueBlock: ^(FMIceLinkLinkOfferAnswerArgs* e) {
    [client notifyWithNotifyArgs: [FMWebSyncNotifyArgs notifyArgsWithClientId:[e peerId] dataJson:[[e.offerAnswer] toJson] tag:@"offer"]];
}];
// IceLink 2
conference.link(peerId: "peer-id")

conference.addOnLinkOfferAnswer { (e:FMIceLinkLinkOfferAnswerArgs) in
    client.notify(FMWebSyncNotifyArgs(clientId: e.peerId(), dataJson: e.offerAnswer().toJson(), tag: "offer"))
}
// IceLink 2
connection.link("peer-id");

connection.addOnLinkOfferAnswer(function(e) {
    client.notify({
        clientId: e.getPeerId(),
        dataJson: e.getOfferAnswer().toJson(),
        tag: "offer"
    });
});


In IceLink 2, the Link method performed three operations:

  • associates an id with the link
  • generates an SDP offer for the link
  • sets the local SDP description for the link

In IceLink 3 these operations are handled separately. You first assign an id to your Connection instance using its Id property. Next, you create an offer using the CreateOffer method of your Connection instance. Finally you use the offer to set the local description by invoking the SetLocalDescription method. When the local description has been set, you can then forward the offer to the remote party. An example of this is shown below.

Note that this API is now promise-based. You can probably get the gist of it from looking at the code below, but if you need a quick explanation, visit the section on Working with Promises.

// IceLink 3
connection.ExternalId = "peer-id";

connection.CreateOffer().Then((FM.IceLink.SessionDescription offer) =>
{
    return connection.SetLocalDescription(offer)
}).Then((FM.IceLink.SessionDescription offer) =>
{
    client.Notify(new FM.WebSync.NotifyArgs(connection.ExternalId, offer.ToJson(), "offer");
});
// IceLink 3
connection.setExternalId("peer-id");

connection.createOffer().then((fm.icelink.SessionDescription offer) -> {
    return connection.setLocalDescription(offer);
}).then((fm.icelink.SessionDescription offer) -> {
    client.notify(new fm.websync.NotifyArgs(connection.getExternalId(), offer.toJson(), "offer");
});
// IceLink 3
[connection setExternalId:@"peer-id"];

[[[connection createOffer] thenWithResolveFunctionBlock: ^(FMIceLinkSessionDescription* offer) {
    return [connection setLocalDescriptionWithLocalDescription: offer];
}] thenWithResolveFunctionBlock: ^(FMIceLinkSessionDescription* offer) {
    [client notifyWithNotifyArgs: [FMWebSyncNotifyArgs notifyArgsWithClientId:[connection externalId] dataJson:[offer toJson] tag:@"offer"]];
}];
// IceLink 3
connection.setExternalId("peer-id")

connection.createOffer().then(resolveFunctionBlock: { (offer:FMIceLinkSessionDescription) -> FMIceLinkPromise in
    return connection.setLocalDescription(localDescription: offer)
}).then(resolveFunctionBlock: { (offer:FMIceLinkSessionDescription) -> FMIceLinkPromise in
    client.notify(notifyArgs: FMWebSyncNotifyArgs(clientId: connection.getExternalId(), dataJson: offer.toJson(), tag: "offer"))
})
// IceLink 3
connection.setExternalId("peer-id");

connection.createOffer().then(function(offer) {
    return connection.setLocalDescription(offer);
}).then(function(offer) {
    client.notify({
        clientId: connection.getExternalId(),
        dataJson: offer.toJson(),
        tag: "offer"
    });
});


Migration Steps

  • Instead of invoking the Link method of a Conference object, create a new Connection instance and call CreateOffer and SetLocalDescription.
  • Instead of passing the peer id to the Link method, set it using the Connection instance's ExternalId property.
  • Move any signalling logic in the OnLinkOfferAnswer event handler into the resolved handler of the promise returned by SetLocalDescription .

Connection.ExternalId vs Connection.Id

Note that the 3.2.3.666 release of June 2018 made the Connection.Id property private, and added the Connection.ExternalId to the public API. For prior IceLink 3 releases Connection.Id may be used, but from 3.2.3.666 onwards you must use Connection.ExternalId as demonstrated in this guide.


More Information

Receiving an Offer or an Answer

In IceLink 2, when a peer receives an SDP offer, they accepts the offer using the ReceiveOfferAnswer method of the FM.IceLink.Conference instance. An answer is then generated which, is available in an OnLinkOfferAnswer event handler. The generated answer is then forwarded as a reply to the peer who initially sent the offer. An example of this follows.

// IceLink 2
conference.ReceiveOfferAnswer(offer, "peer-id");

conference.OnLinkOfferAnswer += (FM.IceLink.LinkOfferAnswerArgs e) =>
{
    client.Notify(new FM.WebSync.NotifyArgs(e.PeerId, e.OfferAnswer.ToJson(), "answer");
};
// IceLink 2
conference.receiveOfferAnswer(offer, "peer-id");

conference.addOnLinkOfferAnswer(new fm.SingleAction<fm.icelink.LinkOfferAnswerArgs>() {
    public void invoke(fm.icelink.LinkOfferAnswerArgs e) {
        client.notify(new fm.websync.notifyArgs(e.getPeerId(), e.getOfferAnswer().toJson(), "answer");
    }
});
// IceLink 2
[conference receiveOfferAnswerWithOfferAnswer:offer peerId:@"peer-id"];

[conference addOnLinkOfferAnswerWithValueBlock: ^(FMIceLinkLinkOfferAnswerArgs* e) {
    [client notifyWithNotifyArgs: [FMWebSyncNotifyArgs notifyArgsWithClientId:[e peerId] dataJson:[[e.offerAnswer] toJson] tag:@"answer"]];
}];
// IceLink 2
conference.receiveOfferAnswer(offerAnswer: offer, peerId: "peer-id")

conference.addOnLinkOfferAnswer { (e:FMIceLinkLinkOfferAnswerArgs)
    client.notify(notifyArgs: FMWebSyncNotifyArgs(clientId: e.peerId(), dataJson: e.offerAnswer().toJson(), tag: "answer")
}
// IceLink 2
connection.receiveOfferAnswer(offer, "peer-id");

connection.addOnLinkOfferAnswer(function(e) {
    client.notify({
        clientId: e.getPeerId(),
        dataJson: e.getOfferAnswer().toJson(),
        tag: "answer"
    });
});


For IceLink 3, this operation is again split into several steps. In order, you must:

  • set the remote description using the SDP offer
  • generate an SDP answer
  • set the local description using the SDP answer

Again, this API is heavily promise based, so if you haven't you may wish to review Working with Promises. To set the remote description, first invoke the SetRemoteDescription method of your Connection instance. After the remote description is set, create an answer using its CreateAnswer method. Finally, use the answer and the SetLocalDescription method to set the local description, and then forward it to the other party using your signalling client.

// IceLink 3
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(connection.ExternalId, answer.ToJson(), "answer");
});
// IceLink 3
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(connection.getExternalId(), answer.toJson(), "answer");
});
// IceLink 3
[[[[connection setRemoteDescriptionWithRemoteDescription: offer] thenWithResolveFunctionBlock: ^(FMIceLinkSessionDescription* offer) {
    return [connection createAnswer];
}] thenWithResolveFunctionBlock: ^(FMIceLinkSessionDescription* answer) {
    return [connection setLocalDescriptionWithLocalDescription: answer];
}] thenWithResolveFunctionBlock: ^(FMIceLinkSessionDescription* answer) {
    [client notifyWithNotifyArgs: [FMWebSyncNotifyArgs notifyArgsWithClientId:[connection externalId] dataJson:[answer toJson] tag:@"answer"]];
}];
// IceLink 3
conference.setRemoteDescription(offer: offer).then(resolveFunctionBlock: { (offer: FMIceLinkSessionDescription) -> FMIceLinkPromise! in (
    return connection.createAnswer()
}).then(resolveFunctionBlock: { (answer: FMIceLinkSessionDescription) -> FMIceLinkPromise in
    return connection.setLocalDescription(localDescription: offer)
}).then(resolveFunctionBlock: { (answer: FMIceLinkSessionDescription) -> FMIceLinkPromise in
    client.notify(notifyArgs: FMWebSyncNotifyArgs(clientId: connection.externalId(), dataJson: answer.toJson(), tag: "answer")
})
// IceLink 3
connection.setRemoteDescription(offer).then(function(offer) {
    return connection.createAnswer();
}).then(function(answer) {
    return connection.setLocalDescription(answer);
}).then(function(answer) {
    client.notify({
        clientId: connection.getExternalId(),
        dataJson: answer.toJson(),
        tag: "answer"
    });
});


Migration Steps

  • Replace calls to Conference.ReceiveOfferAnswer with Connection.SetRemoteDescription. If the description is an offer, also invoke Connection.CreateAnswer and Connection.SetLocalDescription.

Migration Steps

  • Instead of invoking the ReceiveOfferAnswer method of a Conference object, call the SetRemoteDescription, CreateAnswer and SetLocalDescription methods.
  • Move any signalling logic in the OnLinkOfferAnswer event handler into the resolved handler of the promise returned by SetLocalDescription .

More Information

Exchanging Candidates

With IceLink 2, you add an OnLinkCandidate event handler to your Conference instance to receive candidates as they are generated. You then forward these candidates to the remote party using your signalling client. The remote party accepts them using the ReceiveCandidate method. An example of this is shown below.

// IceLink 2

// local peer
conference.OnLinkCandidate += (FM.IceLink.LinkCandidateArgs e) =>
{
    client.notify(new FM.WebSync.NotifyArgs(e.PeerId, e.Candidate.ToJson(), "candidate"));
};

// remote peer
conference.ReceiveCandidate(candidate, "peer-id");
// IceLink 2

// local peer
conference.addOnLinkCandidate(new fm.SingleAction<fm.icelink.LinkCandidateArgs>() {
    public void invoke(fm.icelink.LinkCandidateArgs e) {
        client.notify(new fm.websync.NotifyArgs(e.getPeerId(), e.getCandidate().toJson(), "candidate");
    }
});

// remote peer
conference.receiveCandidate(candidate, "peer-id");
// IceLink 2

// local peer
[conference addOnLinkCandidateWithValueBlock: ^(FMIceLinkLinkCandidateArgs* e) {
    [client notifyWithNotifyArgs: [FMWebSyncNotifyArgs notifyArgsWithClientId:[e peerId] dataJson:[[e candidate] toJson] tag:@"candidate"]];
}];

// remote peer
[conference receiveCandidateWithCandidate:candidate peerId:@"peer-id"];
// IceLink 2

// local peer
conference.addOnLinkCandidateBlock { (e: FMIceLinkLinkCandateArgs) in
    client.notify(notifyArgs: FMWebSyncNotifyArgs(clientId: e.peerId(), dataJson: e.candidate().toJson(), tag: "candidate"))
}

// remote peer
conference.receiveCandidate(candidate: candidate, "peer-id")
// IceLink 2

// local peer
conference.addOnLinkCandidate(function(e) {
    client.notify({
        clientId: e.getPeerId(),
        dataJson: e.getCandidate().toJson(),
        tag: "candidate"
    });
});

// remote peer
conference.receiveCandidate(candidate, "peer-id");


With IceLink 3, you instead add an OnLocalCandidate event handler to your Connection instance to handle generated candidates and use the AddRemoteCandidate method to receive them, as shown below.

// IceLink 3

// local peer
connection.OnLocalCandidate += (FM.IceLink.Connection c, FM.IceLink.Candidate candidate) =>
{
    client.notify(new FM.WebSync.NotifyArgs(c.ExternalId, candidate.ToJson(), "candidate"));
};

// remote peer
connection.addRemoteCandidate(candidate);
// IceLink 3

// local peer
connection.addOnLocalCandidate((fm.icelink.Connection c, fm.icelink.Candidate candidate) -> {
    client.notify(new fm.websync.NotifyArgs(c.getExternalId(), candate.toJson(), "candidate");
});

// remote peer
connection.addRemoteCandidate(candidate);
// IceLink 3

// local peer
[connection addOnLocalCandidateBlock: ^(FMIceLinkConnection* c, FMIceLinkCandidate* candidate) {
    [client notifyWithNotifyArgs: [FMWebSyncNotifyArgs notifyArgsWithClientId:[c externalId] dataJson:[candidate toJson] tag:@"candidate"]];
}];

// remote peer
[connection addRemoteCandidateWithCandidate:candidate];
// IceLink 3

// local peer
connection.addOnLocalCandidateBlock { (c:FMIceLinkConnection, candidate:FMIceLinkCandidate) in
    client.notify(notifyArgs: FMWebSyncNotifyArgs(clientId: c.externalId(), dataJson: candidate.toJson(), tag: "candidate")
}

// remote peer
connection.addRemoteCandidate(candidate: candidate)
// IceLink 3

// local peer
connection.addOnLocalCandidate(function(c, candidate) {
    client.notify({
        clientId: c.getExternalId(),
        dataJson: candidate.toJson(),
        tag: "candidate"
    });
});

// remote party
connection.addRemoteCandidate(candidate);


Migration Steps

  • Replace calls to ReceiveCandidate with AddRemoteCandidate.
  • Rename the OnLinkCandidate event handler to Connection.OnLocalCandidate.

More Information

Link State Handlers

IceLink 2 notifies you of link-related events - such as when a link finishes connecting or when it disconnects from a peer - through a series of event handlers. For example, the OnLinkUp event handler is raised whenever a link successfully connects. The code snippet below shows the most common link event handers.

// IceLink 2
conference.OnLinkInit = (FM.IceLink.LinkInitArgs e) =>
{
    Console.WriteLine("link initializing");
};

conference.OnLinkUp = (FM.IceLink.LinkUpArgs e) =>
{
    Console.WriteLine("link established");
};

conference.OnLinkDown = (FM.IceLink.LinkDownArgs e) =>
{
    if (e.Exception == null)
    {
        Console.WriteLine("link closed");
    }
    else
    {
        Console.WriteLine("link failed");
    }
};
// IceLink 2
conference.addOnLinkInit(new fm.SingleAction<fm.icelink.LinkInitArgs>() {
    public void invoke(fm.icelink.LinkInitArgs e) {
        System.out.println("link initializing");
    }
});

conference.addOnLinkUp(new fm.SingleAction<fm.icelink.LinkUpArgs>() {
    public void invoke(fm.icelink.LinkUpArgs e) {
        System.out.println("link established");
    }
});

conference.addOnLinkDown(new fm.SingleAction<fm.icelink.LinkDownArgs>() {
    public void invoke(fm.icelink.LinkDownArgs e) {
        if (e.getException() == null) {
            System.out.println("link closed");
        } else {
            System.out.println("link failed");
        }
    }
});
// IceLink 2
[conference addOnLinkInitWithValueBlock: ^(FMIceLinkLinkInitArgs* e) {
    NSLog(@"link initializing");
}];

[conference addOnLinkUpWithValueBlock: ^(FMIceLinkLinkUpArgs* e) {
    NSLog(@"link established");
}];

[conference addOnLinkDownWithValueBlock: ^(FMIceLinkLinkDownArgs* e) {
    if ([e exception] == null) {
        NSLog(@"link closed");
    } else {
        NSLog(@"link failed");
    }
}];
// IceLink 2
conference.addOnLinkInit { (e:FMIceLinkLinkInitArgs)
    print("link initializing")
}

conference.addOnLinkUp { (e:FMIceLinkLinkUpArgs)
    print("link established")
}

conference.addOnLinkDown { (e:FMIceLinkLinkDownArgs)
    if e.exception() == nil {
        print("link closed")
    } else {
        print("link failed")
    }
}
// IceLink 2
conference.addOnLinkInit(function(e) {
    console.log("link initializing");
});

conference.addOnLinkUp(function(e) {
    console.log("link established");
});

conference.addOnLinkDown(function(e) {
    if (e.getException() == null) {
        console.log("link closed");
    } else {
        console.log("link failed");
    }
});


In IceLink 3, these handlers have been removed. To access the state of a Connection instance, you access its State property. You can add an OnStateChange event handler to be notified when the state of a Connection instance changes. The code below demonstrates adding an event handler that is equivalent to the IceLink 2 state changes above.

// IceLink 3
connection.OnStateChange = (FM.IceLink.Connection c) =>
{
    if (c.State == ConnectionState.Initializing) 
    {
        Console.WriteLine("link initializing");
    }
    
    if (c.State == ConnectionState.Connected)
    {
        Console.WriteLine("link established");
    }
    
    if (c.State == ConnectionState.Closed)
    {
        Console.WriteLine("link closed");
    }
    
    if (c.State == ConnectionState.Failed)
    {
        Console.WriteLine("link failed");
    }
};
// IceLink 3
connection.addOnStateChange((fm.icelink.Connection c) -> [
    if (c.getState() == fm.icelink.ConnectionState.Initializing) {
        System.out.println("link initializing");
    }
    
    if (c.getState() == fm.icelink.ConnectionState.Connected) {
        System.out.println("link established");
    }
    
    if (c.getState() == fm.icelink.ConnectionState.Closed) {
        System.out.println("link closed");
    }
    
    if (c.getState() == fm.icelink.ConnectionState.Failed) {
        System.out.println("link failed");
    }
});
// IceLink 3
[connection addOnStateChangeWithBlock: ^(FMIceLinkConnection* c) {
    if ([c state] == FMIceLinkConnectionStateInitializing) {
        NSLog(@"link initializing");
    }
    
    if ([c state] == FMIceLinkConnectionStateConnected) {
        NSLog(@"link established");
    }
    
    if ([c state] == FMIceLinkConnectionStateClosed) {
        NSLog(@"link closed");
    }
    
    if ([c state] == FMIceLinkConnectionStateFailed) {
        NSLog(@"link failed");
    }
}];
// IceLink 3
connection.addOnStateChangeBlock { (c:FMIceLinkConnection) in
    if c.state() == FMIceLinkConnectionStateInitializing {
        print("link initializing")
    }
    
    if c.state() == FMIceLinkConnectionStateConnected {
        print("link established")
    }
    
    if c.state() == FMIceLinkConnectionStateClosed {
        print("link closed")
    }
    
    if c.state() == FMIceLinkConnectionStateFailed {
        print("link failed")
    }
}
// IceLink 3
connection.addOnStateChange(function(c) {
    if (c.getState() === fm.icelink.ConnectionState.Initializing) {
        console.log("link initializing");
    }
    
    if (c.getState() === fm.icelink.ConnectionState.Connected) {
        console.log("link established");
    }
    
    if (c.getState() === fm.icelink.ConnectionState.Closed) {
        console.log("link closed");
    }
    
    if (c.getState() === fm.icelink.ConnectionState.Failed) {
        console.log("link failed");
    }
});


Note that the IceLink 3 SDK defines several states that aren't available in IceLink 2. You can explore these by investigating the values in the ConnectionState enumeration.

Migration Steps

  • Move code from all OnLinkX event handlers to into an OnStateChange event handler.

More Information

  • For more information about connections and connection state, see the section on Opening a Connection.

Unlinking a Conference

In IceLink 2, you end a conference by invoking the Unlink method of your Conference instance.

// IceLink 2
conference.Unlink();
// IceLink 2
conference.unlink();
// IceLink 2
[conference unlink];
// IceLink 2
conference.unlink()
// IceLink 2
conference.unlink();


In IceLink 3, you instead invoke the Close method of each Connection instance. You can iterate over all of the connections in ConnectionCollection to close each connection.

// IceLink 3
for (var i = 0; i < connectionCollection.Count; i++)
{
    connectionCollection.ValueAt(i).Close();
}
// IceLink 3
for (int i = 0; i < connectionCollection.getCount(); i++) {
    connectionCollection.valueAt(i).close();
}
// IceLink 3
for (int i = 0; i < [connectionCollection count]; i++) {
  [[connectionCollection valueAtWithIndex:i] close];
}
// IceLink 3
for i in 0..connectionCollection.count() {
    connectionCollection.valueAt(index: i).close()
}
 


Migration Steps

  • Instead of invoking the Unlink method of a Conference instance, iterate over each connection in a ConnectionCollection instance and invoke each one's Close method.

More Information

Joining a Conference With the WebSync Extension

IceLink 2 includes a WebSync extension that simplifies the process of creating a video conference. To use the extension you create an FM.IceLink.Conference object and then invoke the JoinConference method of your FM.WebSync.Client instance, passing it the conference object as a parameter. The extension takes care of everything else. An example of this is shown below.

// IceLink 2
var conference = new FM.IceLink.Conference(...);

client.JoinConference(new FM.IceLink.WebSync.JoinConferenceArgs("/123456", conference));
// IceLink 2
Conference conference = new fm.icelink.Conference(...);
fm.icelink.websync.JoinConferenceArgs args = new fm.icelink.websync.JoinConferenceArgs("/123456", conference);

fm.icelink.websync.ClientExtensions.joinConference(client, args);
// IceLink 2

FMIceLinkConference* conference = [FMIceLinkConference conferenceWith...];
FMIceLinkWebSyncJoinConferenceArgs* args = [FMIceLinkWebSyncJoinConferenceArgs joinConferenceArgsWithConferenceChannel:@"/123456" conference:conference];
  
[client joinConferenceWithJoinConferenceArgs:args];
// IceLink 2
var conference = FMIceLinkConference(...)
var args = FMIceLinkWebSyncJoinConferenceArgs(conferenceChannel: "/123456", conference: conference)

client.joinConference(joinConferenceArgs: args)
// IceLink 2
var conference = new fm.icelink.conference(...);

client.joinConference({
    conferenceChannel: '/123456',
    conference: conference
});


IceLink 3 also includes a WebSync extension, but the API has changed slightly. Because the Conference class no longer exists and there is no equivalent object, you must provide a callback to create an FM.IceLink.Connection instance for each person who joins the session. This is done by specifying the OnRemoteClient callback.

Additionally, the WebSync extension has been moved from the FM.IceLink.WebSync namespace to FM.IceLink.WebSync4. This is to future-proof the extension for use with later versions of WebSync.

// IceLink 3
client.JoinConference(new FM.IceLink.WebSync4.JoinConferenceArgs("/123456")
{
    OnRemoteClient = (FM.IceLink.PeerClient remoteClient) =>
    {
        return new FM.IceLink.Connection(...);
    }
});
// IceLink 3
fm.icelink.websync4.JoinConferenceArgs args = new fm.icelink.websync4.JoinConferenceArgs("/123456");
args.setOnRemoteClient((fm.icelink.websync4.PeerClient remoteClient) -> {
    return new fm.icelink.Connection(...);
});

fm.icelink.websync4.ClientExtensions.joinConference(client, args);
// IceLink 3
FMIceLinkWebSync4JoinConferenceArgs *args = [FMIceLinkWebSync4JoinConferenceArgs joinConferenceArgsWithConferenceChannel:@"/123456"];
[args setOnRemoteClientBlock:^(FMIceLinkWebSync4PeerClient* remoteClient) {}
    return [FMIceLinkConnection connectionWith...];
}];

[client joinConferenceWithJoinConferenceArgs:args];
// IceLink 3
var args = FMIceLinkWebSync4JoinConferenceArgs(conferenceChannel:"/123456")
args.setOnREmoteClientBlock { (remoteClient:FMIceLinkWebSync4PeerClient) in
    return FMIceLinkConnection()
}

client.joinConference(joinConferenceArgs: args)
// IceLink 3
client.joinConference({
    conferenceChannel: '/123456',
    onRemoteClient: function(remoteClient) {
        return new fm.icelink.Connection(...);
    }
});


Migration Steps

  • Replace all references to FM.IceLink.WebSync with FM.IceLink.WebSync4.
  • Add an OnRemoteClient callback to any JoinConference invocations. The callback should return a Connection instance.

More Information

STUN / TURN Servers

IceLink 2 features a combined STUN / TURN server for .NET. To run it, you create an instance of FM.IceLink.Server and invoke the Start method. You can enable TURN authentication by invoking the EnableRelay method of the the Server instance. The server is only available for .NET.

// IceLink 2
var server = new Server();
server.EnableRelay((FM.IceLink.RelayAuthenticateArgs e) =>
{
    return RelayAuthenticateResult.FromLongTermKeyBytes(...);
    // or
    return RelayAuthenticateResult.FromPassword(...);
});

server.PublicIPAddress = "public IP";
server.Start("private IP", 3478);

In IceLink 3, the Server class has been split into two separate classes: FM.IceLink.StunServer and FM.IceLink.TurnServer. Note that the TurnServer class supports STUN as well, so you only ever need to use one of these classes. In version 3, all properties that specify an IP address must now be wrapped in an FM.IceLink.ServerAddress instance, for validation.

IceLink 3 also adds Java and Objective-C versions of these classes, which were not available in IceLink 2. There is no JavaScript version of these APIs.

// IceLink 3
var server = new TurnServer((FM.IceLink.TurnAuthArgs e) =>
{
    return TurnAuthResult.FromLongTermKeyBytes(...);
    // or
    return TurnAuthResult.FromPassword(...);
});

var udpAddress = new ServerAddress("private IP", 3478, "public IP");
var tcpAddress = new ServerAddress("private IP", 443, "public IP");

server.Start(new[] { udpAddress }, new[] { tcpAddress });
// IceLink 3
fm.icelink.TurnServer server = new fm.icelink.TurnServer((fm.icelink.TurnAuthArgs e) -> {
    return fm.icelink.TurnAuthResult.fromLongTermKeyBytes(...);
    // or
    return fm.icelink.TurnAuthResult.fromPassword(...);
});

fm.icelink.ServerAddress udpAddress = new fm.icelink.ServerAddress("private IP", 3478, "public IP");
fm.icelink.ServerAddress tcpAddress = new fm.icelink.ServerAddress("private IP', 443, "public IP");

server.start(new fm.icelink.ServerAddress[] { udpAddress }, new fm.icelink.ServerAddress{} { tcpAddress });
// IceLink 3
FMIceLinkTurnServer* server = [FMIceLinkTurnServer turnServerWithAuthCallback: ^(FMIceLinkTurnAuthArgs* e) {
    return [FMIceLinkTurnAuthResult fromLongTermKeyBytes(...);
    // or
    return [FMIceLinkTurnAuthResult fromPassword(...);
}];

FMIceLinkServerAddress* udpAddress = [FMIceLinkServerAddress serverAddressWithIPAddress:@"private IP", port:3478, publicIPAddress:@"public IP"];
FMIceLinkServerAddress* tcpAddress = [FMIceLinkServerAddress serverAddressWithIPAddress:@"private IP", port:443, publicIPAddress:@"public IP"];

[server startWithUdpAddresses:[NSMutableArray arrayWithObjects:udpAddress, nil] tcpAddresses:[NSMutableArray arrayWithObjects:tcpAddress, nil]];
// IceLink 3
let server:FMIceLinkTurnServer server = FMIceLinkTurnServer() { (e:FMIceLinkTurnAuthArgs) -> FMIceLinkTurnAuthResult in
    return FMIceLinkTurnAuthResult.fromLongTermKeyBytes(...)
    // or
    return FMIceLinkTurnAuthResult.fromPassword(...)
}

let udpAddress:FMIceLinkServerAddress = FMIceLinkServerAddress(ipAddress: "private IP", port: 3478, publicIpAddress: "public IP")
let tcpAddress:FMIceLinkServerAddress = FMIceLinkServerAddress(ipAddress: "private IP", port: 443, publicIpAddress: "public IP")

server.start(udpAddresses: [udpAddress], tcpAddresses: tcpAddress])


Migration

  • If using STUN only, replace references to Server with StunServer.
  • If using TURN, replace references to Server with TurnServer. Pass your EnableRelay callback into the TurnServer constructor.
    • Replace references to RelayAuthenticateArgs with TurnAuthArgs.
    • Replace references to RelayAuthenticateResult with TurnAuthResult.
    • Replace references to RelayOperation with TurnAuthOperation.
  • If specifying private or public IP addresses, wrap the previous IP Address strings in ServerAddress instances.

More Information

Wrapping Up

After reading this section, you should be able to migrate the core functionality of your application to version 3. You should be able to connect users to each other in a video conference. The next sections go deeper and focus on converting more specific parts of the SDK.