IceLink 3 Migration Guide

Got a ton of code that uses IceLink 2? No problem! Migrating to IceLink 3 should be quick and easy with this handy migration guide. We recommend reading the entire guide before starting your migration.

JavaScript Class Name Casing

In JavaScript, class names now use Pascal casing instead of camel casing (MyClass instead of myClass) to follow proper JavaScript naming conventions. Namespaces and method names still use standard camel casing.

// IceLink 2
var candidate = new fm.icelink.candidate();

// IceLink 3
var candidate = new fm.icelink.Candidate();

Namespaces

In IceLink 2, classes could be found in the FM, FM.IceLink, or FM.IceLink.WebRTC namespaces.

In IceLink 3, classes can be found in the FM.IceLink namespace. This makes it easier to find the right class and allows different versions of FM products to be used within the same project.

// IceLink 2
fm.log.setProvider(new fm.consoleLogProvider(fm.logLevel.Info));

// IceLink 3
fm.icelink.Log.setProvider(new fm.icelink.ConsoleLogProvider(fm.icelink.LogLevel.Info));
// IceLink 2
FM.Log.Provider = new FM.ConsoleLogProvider(LogLevel.Info);

// IceLink 3
FM.IceLink.Log.Provider = new FM.IceLink.ConsoleLogProvider(LogLevel.Info);
// IceLink 2
fm.Log.setProvider(new fm.ConsoleLogProvider(fm.LogLevel.Info));

// IceLink 3
fm.icelink.Log.setProvider(new fm.icelink.ConsoleLogProvider(fm.icelink.LogLevel.Info));
// IceLink 2
[FMLog setProvider:[FMConsoleLogProvider consoleLogProviderWithLogLevel:FMLogLevelInfo]];

// IceLink 3
[FMIceLinkLog setProvider:[FMIceLinkConsoleLogProvider consoleLogProviderWithLogLevel:FMIceLinkLogLevelInfo]];

Migration

  • Replace all references to FM and FM.IceLink.WebRTC with FM.IceLink.

At the networking layer, this represents the single most significant change in IceLink 3.

In IceLink 2, a Link was an individual connection and a Conference was a template for and a collection of connections. Conference also handled some aspects of signalling (like timeouts).

In IceLink 3, Connection replaces Link and Conference. Each Connection is created by your code individually and can be customized per peer. ConnectionCollection has been added as a simple thread-safe dictionary of Connection instances.

// IceLink 2
var conference = new fm.icelink.conference(...);
...
client.joinConference({
    conferenceChannel: '/my/channel',
    conference: conference,
    ...
});

// IceLink 3
var connections = new fm.icelink.ConnectionCollection(); // optional
client.joinConference({
    conferenceChannel: '/my/channel',
    onRemoteClient: function(remoteClient) {
        var connection = new fm.icelink.Connection(...);
        connections.add(connection); // optional
        ...
        return connection;
    }
    ...
});
// IceLink 2
var conference = new Conference(...);
...
client.JoinConference(new JoinConferenceArgs("/my/channel", conference)
{
    ...
});

// IceLink 3
var connections = new ConnectionCollection(); // optional
client.JoinConference(new JoinConferenceArgs("/my/channel")
{
    OnRemoteClient = (remoteClient) =>
    {
        var connection = new Connection(...);
        connections.Add(connection); // optional
        ...
        return connection;
    }
    ...
});
// IceLink 2
Conference conference = new Conference(...);
...
ClientExtensions.joinConference(client, new JoinConferenceArgs("/my/channel", conference)
{{
    ...
}});

// IceLink 3
final ConnectionCollection connections = new ConnectionCollection(); // optional
ClientExtensions.joinConference(client, new JoinConferenceArgs("/my/channel")
{{
    setOnRemoteClient((remoteClient) ->
    {
        Connection connection = new Connection(...);
        connections.add(connection); // optional
        ...
        return connection;
    });
    ...
}});
// IceLink 2
FMIceLinkConference *conference = [FMIceLinkConference conferenceWith...];
...
FMIceLinkWebSyncJoinConferenceArgs *args = [FMIceLinkWebSyncJoinConferenceArgs
  joinConferenceArgsWithConferenceChannel:@"/my/channel" conference:conference];
...
[client joinConferenceWithArgs:args];

// IceLink 3
FMIceLinkConnectionCollection *connections = [FMIceLinkConnectionCollection connectionCollection]; // optional
FMIceLinkWebSync4JoinConferenceArgs *args = [FMIceLinkWebSync4JoinConferenceArgs
  joinConferenceArgsWithConferenceChannel:@"/my/channel"];
[args setOnRemoteClientBlock:^(FMIceLinkWebSync4PeerClient* remoteClient) {}
    FMIceLinkConnection* connection = [FMIceLinkConnection connectionWith...];
    [connections add:connection]; // optional
    ...
    return connection;
}];
...
[client joinConferenceWithArgs:args];

Migration

  • Replace references to Link and Conference with Connection and ConnectionCollection.
  • Note that some events have moved:
    • Conference.OnLinkSendRTP and Link.OnSendRTP are now AudioStream/VideoStream.OnProcessFrame.
    • Conference.OnLinkSendRTCP and Link.OnSendRTCP are now AudioStream/VideoStream.OnProcessControlFrame.
    • Conference.OnLinkReceiveRTP and Link.OnReceiveRTP are now AudioStream/VideoStream.OnRaiseFrame.
    • Conference.OnLinkReceiveRTCP and Link.OnReceiveRTCP are now AudioStream/VideoStream.OnRaiseControlFrame.
    • Conference.OnLinkLocalOfferAnswer and Link.OnLocalOfferAnswer are now Connection.OnLocalDescription.
    • Conference.OnLinkLocalCandidate and Link.OnLocalCandidate are now Connection.OnLocalCandidate.
    • Conference.OnLinkRemoteOfferAnswer and Link.OnRemoteOfferAnswer are now Connection.OnRemoteDescription.
    • Conference.OnLinkRemoteCandidate and Link.OnRemoteCandidate are now Connection.OnRemoteCandidate.

WebSync Extension

In IceLink 3, the WebSync extension has been moved from FM.IceLink.WebSync to FM.IceLink.WebSync4. Since Conference is gone, the OnRemoteClient callback allows you to create a new Connection for each new participant.

Migration

  • Replace all references to FM.IceLink.WebSync with FM.IceLink.WebSync4.
  • Add an OnRemoteClient callback that returns a new Connection (or null to opt out of connecting to the remote client).

IceServer (previously ServerAddress, RelayUsername, & RelayPassword)

In IceLink 3, IceServer has replaced ServerAddress(es), RelayUsername(s), and RelayPassword(s). In addition, IceLink 3 now supports the stun: and turn: URI schemes. This makes application code much easier to read/maintain and integrate with third-party STUN/TURN services.

// IceLink 2
var conference = new Conference(new[] { "stun.server.com", "turn.server.com" }, ...);
conference.RelayUsernames = new[] { null, "username" };
conference.RelayPasswords = new[] { null, "password" };

// IceLink 3
var connection = new Connection(...);
connection.IceServers = new[]
{
    new IceServer("stun:stun.server.com"),
    new IceServer("turn:turn.server.com", "username", "password"),
    new IceServer("turn:turn.server.com:443?transport=tcp", "username", "password")
};

Migration

  • Set Connection.IceServers to an IceServer array using your server addresses, relay usernames, and relay passwords.
  • If you were using a single server address for both STUN and TURN, create two IceServer elements - one for STUN and one for TURN - per the above example.

Local and Remote Media

At the media layer, this represents the single most significant change in IceLink 3.

In IceLink 3, the LocalMediaStream, MediaStream and UserMedia APIs have been replaced with RtcLocalMedia<ViewType> and RtcRemoteMedia<ViewType>.

The new media stack in IceLink 3 is extremely powerful and flexible, allowing you to define the exact flow of media from source(s) through pipes and branches to sinks. While this allows us to address pretty much every use case imaginable, this also increases the complexity of the API. To counter this, we have added RtcLocalMedia<ViewType> and RtcRemoteMedia<ViewType> which do all the heavy lifting to create WebRTC/ORTC-compatible audio and video tracks.

This also allows us to (finally) deliver software-based acoustic echo cancellation in a way that works with any codec - not just Opus - as well as letting you customize the media path for each individual participant.

These classes are abstract, so each example now includes a LocalMedia and RemoteMedia implementation that is custom-tailored to the platform. For example, in the .NET examples, OpenH264 is used for H.264 encoding/decoding, while in iOS, we use the native VideoToolbox API.

// IceLink 2
public class LocalMedia
{
    public LocalMediaStream LocalMediaStream { get; private set; }
    public object LocalVideoControl { get; private set; }

    public void Start(bool disableAudio, bool disableVideo, Action<Exception> callback)
    {
        UserMedia.GetMedia(new GetMediaArgs(!disableAudio, !disableVideo)
        {
            AudioCaptureProvider = new NAudioCaptureProvider(),
            VideoCaptureProvider = new AForgeVideoCaptureProvider(LayoutScale.Contain),
            CreateAudioRenderProvider = (e) =>
            {
                return new NAudioRenderProvider();
            },
            CreateVideoRenderProvider = (e) =>
            {
                return new PictureBoxVideoRenderProvider(LayoutScale.Contain);
            },
            VideoWidth = 640,
            VideoHeight = 480,
            VideoFrameRate = 30,
            OnFailure = (e) =>
            {
                callback(e.Exception);
            },
            OnSuccess = (e) =>
            {
                LocalMediaStream = e.LocalStream;
                LocalVideoControl = e.LocalVideoControl;
                callback(null);
            }
        });
    }

    public void Stop(Action<Exception> callback)
    {
        if (LocalMediaStream != null)
        {
            LocalMediaStream.Stop();
            LocalMediaStream = null;
        }
        callback(null);
    }
}

// IceLink 3
public class LocalMedia : RtcLocalMedia<PictureBoxControl>
{
    public LocalMedia(bool disableAudio, bool disableVideo, AecContext aecContext)
        : base(disableAudio, disableVideo, aecContext)
    {
        Initialize();
    }
    protected override AudioSink CreateAudioRecorder(AudioFormat inputFormat)
    {
        return new Matroska.AudioSink(Id + "-local-audio-" + inputFormat.Name.ToLower() + ".mkv");
    }
    protected override AudioSource CreateAudioSource(AudioConfig config)
    {
        return new NAudio.Source(config);
    }
    protected override VideoEncoder CreateH264Encoder()
    {
        return new OpenH264.Encoder();
    }
    protected override VideoPipe CreateImageConverter(VideoFormat outputFormat)
    {
        return new Yuv.ImageConverter(outputFormat);
    }
    protected override VideoPipe CreateImageScaler()
    {
        return new Yuv.ImageScaler(1.0);
    }
    protected override AudioEncoder CreateOpusEncoder(AudioConfig config)
    {
        return new Opus.Encoder(config);
    }
    protected override VideoSink CreateVideoRecorder(VideoFormat inputFormat)
    {
        return new Matroska.VideoSink(Id + "-local-video-" + inputFormat.Name.ToLower() + ".mkv");
    }
    protected override VideoSource CreateVideoSource()
    {
        return new AForge.CameraSource(new VideoConfig(640, 480, 30));
    }
    protected override ViewSink<PictureBoxControl> CreateViewSink()
    {
        return new PictureBoxSink
        {
            ViewScale = LayoutScale.Contain,
            ViewMirror = true
        };
    }
    protected override VideoEncoder CreateVp8Encoder()
    {
        return new Vp8.Encoder();
    }
    protected override VideoEncoder CreateVp9Encoder()
    {
        return new Vp9.Encoder();
    }
}
public class RemoteMedia : RtcRemoteMedia<PictureBoxControl>
{
    public RemoteMedia(bool disableAudio, bool disableVideo, AecContext aecContext)
        : base(disableAudio, disableVideo, aecContext)
    {
        Initialize();
    }
    protected override AudioSink CreateAudioRecorder(AudioFormat inputFormat)
    {
        return new Matroska.AudioSink(Id + "-remote-audio-" + inputFormat.Name.ToLower() + ".mkv");
    }
    protected override AudioSink CreateAudioSink(AudioConfig config)
    {
        return new NAudio.Sink(config);
    }
    protected override VideoDecoder CreateH264Decoder()
    {
        return new OpenH264.Decoder();
    }
    protected override VideoPipe CreateImageConverter(VideoFormat outputFormat)
    {
        return new Yuv.ImageConverter(outputFormat);
    }
    protected override AudioDecoder CreateOpusDecoder(AudioConfig config)
    {
        return new Opus.Decoder(config);
    }
    protected override VideoSink CreateVideoRecorder(VideoFormat inputFormat)
    {
        return new Matroska.VideoSink(Id + "-remote-video-" + inputFormat.Name.ToLower() + ".mkv");
    }
    protected override ViewSink<PictureBoxControl> CreateViewSink()
    {
        return new PictureBoxSink
        {
            ViewScale = LayoutScale.Contain
        };
    }
    protected override VideoDecoder CreateVp8Decoder()
    {
        return new Vp8.Decoder();
    }
    protected override VideoDecoder CreateVp9Decoder()
    {
        return new Vp9.Decoder();
    }
}

Migration

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

Note that the new Start and Stop implementations return a Future.

Pausing & Resuming Local Media

In IceLink 2, local audio was paused and resumed using LocalMediaStream.PauseAudio and LocalMediaStream.ResumeAudio, while local video was paused and resumed using LocalMediaStream.PauseVideo and LocalMediaStream.ResumeVideo.

In IceLink 3, local audio is paused and resumed using AudioSource.Stop and AudioSource.Start, while local video is paused and resumed using VideoSource.Stop and VideoSource.Start.

// IceLink 2
localMediaStream.PauseAudio();
localMediaStream.ResumeAudio();
localMediaStream.PauseVideo();
localMediaStream.ResumeVideo();

// IceLink 3
var audioSource = localMedia.AudioTrack.Source;
var videoSource = localMedia.VideoTrack.Source;
audioSource.Stop().Wait();
audioSource.Start().Wait();
videoSource.Stop().Wait();
videoSource.Start().Wait();

Note that Start and Stop return an asynchronous Future, so we call Wait here to wait for the result before continuing to mimic the behaviour of IceLink 2. This is especially important if you are stopping/starting the video source when a mobile app pauses/resumes, since async code may not be allowed.

Migration

  • Replace calls to LocalMediaStream.PauseAudio with LocalMedia.AudioTrack.Source.Stop().Wait().
  • Replace calls to LocalMediaStream.ResumeAudio with LocalMedia.AudioTrack.Source.Start().Wait().
  • Replace calls to LocalMediaStream.PauseVideo with LocalMedia.VideoTrack.Source.Stop().Wait().
  • Replace calls to LocalMediaStream.ResumeVideo with LocalMedia.VideoTrack.Source.Start().Wait().

Muting & Unmuting Local & Remote Media

In IceLink 2, local and remote audio was muted and unmuted using MediaStream.MuteAudio and MediaStream.UnmuteAudio, while local and remote video was muted and unmuted using MediaStream.MuteVideo and MediaStream.UnmuteVideo.

In IceLink 3, local and remote audio is muted and unmuted using AudioTrack.Muted, while local and remote video is muted and unmuted using VideoTrack.Muted.

// IceLink 2
mediaStream.MuteAudio();
mediaStream.UnmuteAudio();
mediaStream.MuteVideo();
mediaStream.UnmuteVideo();

// IceLink 3
media.AudioTrack.Muted = true;
media.AudioTrack.Muted = false;
media.VideoTrack.Muted = true;
media.VideoTrack.Muted = false;

In addition, the media sources, media pipes, and media sinks that make up a track can be individually muted and unmuted. Muting and unmuting the track is a shortcut to apply muting to each element in the track.

Migration

  • Replace calls to MediaStream.MuteAudio with Media.AudioTrack.Muted = true.
  • Replace calls to MediaStream.UnmuteAudio with Media.AudioTrack.Muted = false.
  • Replace calls to MediaStream.MuteVideo with Media.VideoTrack.Muted = true.
  • Replace calls to MediaStream.UnmuteVideo with Media.VideoTrack.Muted = false.

Switching Local Media Devices

In IceLink 2, local audio and video devices were switched using LocalMediaStream.SetAudioDeviceNumber and LocalMediaStream.SetVideoDeviceNumber or using LocalMediaStream.UseNextAudioDevice and LocalMediaStream.UseNextVideoDevice. Device names and numbers (indexes) were obtained using LocalMediaStream.GetAudioDeviceNames and LocalMediaStream.GetVideoDeviceNames. The currently selected device numbers (index) were obtained using LocalMediaStream.GetAudioDeviceNumber and LocalMediaStream.GetVideoDeviceNumber. On mobile, shortcuts were provided for switching to the front/rear cameras using LocalMediaStream.UseFrontVideoDevice and LocalMediaStream.UseRearVideoDevice or using LocalMediaStream.GetFrontVideoDeviceNumber and LocalMediaStream.GetRearVideoDeviceNumber.

In IceLink 3, local audio and video devices are switched using AudioSource.ChangeInput and VideoSource.ChangeInput. Available devices are obtained using AudioSource.Inputs and VideoSource.Inputs. The currently selected device is obtained using AudioSource.Input and VideoSource.Input. On mobile, shortcuts are provided for obtaining the front/rear cameras using VideoSource.FrontInput and VideoSource.BackInput.

// IceLink 2
localMediaStream.SetAudioDeviceNumber(0);
localMediaStream.SetVideoDeviceNumber(0);
localMediaStream.UseNextAudioDevice();
localMediaStream.UseNextVideoDevice();
var audioDeviceNames = localMediaStream.GetAudioDeviceNames();
var videoDeviceNames = localMediaStream.GetVideoDeviceNames();
localMediaStream.UseFrontVideoDevice();
localMediaStream.UseRearVideoDevice();

// IceLink 3
var audioSource = localMedia.AudioTrack.Source;
var videoSource = localMedia.VideoTrack.Source;
audioSource.ChangeInput(audioSource.Inputs[0]).Wait();
videoSource.ChangeInput(videoSource.Inputs[0]).Wait();
var audioInputs = audioSource.Inputs; // each input has an Id and a Name
var videoInputs = videoSource.Inputs; // each input has an Id and a Name
videoSource.ChangeInput(videoSource.FrontInput).Wait();
videoSource.ChangeInput(videoSource.BackInput).Wait();

Note that ChangeInput returns an asynchronous Future, so we call Wait here to wait for the result before continuing to mimic the behaviour of IceLink 2.

Migration

  • Replace calls to LocalMediaStream.SetAudioDeviceNumber(...) with AudioSource.ChangeInput(...).Wait().
  • Replace calls to LocalMediaStream.SetVideoDeviceNumber(...) with VideoSource.ChangeInput(...).Wait().
  • Replace calls to LocalMediaStream.GetAudioDeviceNames() with AudioSource.Inputs.
  • Replace calls to LocalMediaStream.GetVideoDeviceNames() with VideoSource.Inputs.
  • Replace calls to LocalMediaStream.UseFrontVideoDevice() with VideoSource.ChangeInput(VideoSource.FrontInput).Wait().
  • Replace calls to LocalMediaStream.UseRearVideoDevice() with VideoSource.ChangeInput(VideoSource.BackInput).Wait().

Stream

In IceLink 2, a Stream was a template used by a Conference for each connection.

In IceLink 3, a Stream is used directly by a Connection.

Data Streams

In IceLink 3, DataStream has replaced ReliableDataStream, and DataChannel.

// IceLink 2
var dataChannel = new ReliableDataChannel("label")
{
    OnReceive = (e) =>
    {
        if (e.DataString != null)
        {
            // string
        }
        else if (e.DataBytes != null)
        {
            // bytes
        }
    }
};
var dataStream = new ReliableDataStream(new[] { dataChannel });
var connection = new Connection(new Stream[] { dataStream });

// IceLink 3
var dataChannel = new DataChannel("label")
{
    OnReceive = (e) =>
    {
        if (e.DataString != null)
        {
            // string
        }
        else if (e.DataBytes != null)
        {
            // bytes
        }
    }
};
var dataStream = new DataStream(new[] { dataChannel });
var connection = new Connection(new Stream[] { dataStream });

Migration

  • Rename ReliableDataChannel to DataChannel and ReliableDataStream to DataStream.

Media Streams

In IceLink 2, AudioStream and VideoStream took a LocalMediaStream as input.

In IceLink 3, AudioStream and VideoStream take one or more sources as input and one or more sinks as output.

// IceLink 2
var audioStream = new AudioStream(LocalMedia.LocalMediaStream);
var videoStream = new VideoStream(LocalMedia.LocalMediaStream);

// IceLink 3
var remoteMedia = new RemoteMedia(...);
var audioStream = new AudioStream(LocalMedia.AudioOutputs, remoteMedia.AudioInputs);
var videoStream = new VideoStream(LocalMedia.VideoOutputs, remoteMedia.VideoInputs);

Migration

  • Replace LocalMedia.LocalMediaStream with LocalMedia.Audio/VideoOutputs, LocalMedia.Audio/VideoInputs.

LayoutManager

In IceLink 3, "video controls" are now "views". The LayoutManager API has been updated to reflect this.

Migration

  • Replace LayoutManager.SetLocalVideoControl with LayoutManager.SetLocalView.
  • Replace LayoutManager.AddRemoteVideoControl with LayoutManager.AddRemoteView.
  • Replace LayoutManager.UnsetLocalVideoControl with LayoutManager.UnsetLocalView.
  • Replace LayoutManager.RemoveRemoteVideoControl with LayoutManager.RemoveRemoteView.
  • Replace LayoutManager.RemoveRemoteVideoControls with LayoutManager.RemoveRemoteViews.

Custom LayoutManager

In IceLink 3, LayoutManager<T> has replaced BaseLayoutManager if you have implemented your own layout management.

// IceLink 2
public class MyLayoutManager : BaseLayoutManager
{
    public abstract void AddToContainer(object control) { ... }
    public abstract void RemoveFromContainer(object control) { ... }
    public abstract void RunOnUIThread(DoubleAction<object, object> action, object arg1, object arg2) { ... }
    public abstract void ApplyLayout() { ... }
}

// IceLink 3
public class MyLayoutManager : LayoutManager<T>
{
    protected abstract void AddView(T view) { ... }
    protected abstract void RemoveView(T view) { ... }
    protected abstract void DispatchToMainThread(Action2<object, object> action, object arg1, object arg2) { ... }
    protected abstract void Layout() { ... }
}

Migration

  • Replace AddToContainer with AddView.
  • Replace RemoveFromContainer with RemoveView.
  • Replace RunOnUIThread with DispatchToMainThread.
  • Replace ApplyLayout with Layout.

SessionDescription (previously OfferAnswer)

In IceLink 3, OfferAnswer has been renamed to SessionDescription to more closely model WebRTC/ORTC conventions. OfferAnswer.SdpMessage is now an Sdp.Message instead of a string to make parsing and modification as simple as possible.

// IceLink 2
var message = SDPMessage.Parse(offerAnswer.SdpMessage);

// IceLink 3
var message = sessionDescription.SdpMessage;

Migration

  • Replace references to OfferAnswer with SessionDescription.
  • Remove references to SDPMessage.Parse.

Candidate

In IceLink 3, Candidate.SdpCandidateAttribute is now an Sdp.Ice.CandidateAttribute instead of a string to make parsing and modification as simple as possible.

// IceLink 2
var attribute = (SDPCandidateAttribute)SDPAttribute.Parse(candidate.SdpCandidateAttribute);

// IceLink 3
var attribute = candidate.SdpCandidateAttribute;

Migration

  • Remove references to SDPAttribute.Parse.

Custom Signalling

IceLink 3 signalling follows the WebRTC/ORTC model more closely. Once a Connection has been created, a session description can be generated at any time using CreateOffer or CreateAnswer. Once created, the local offer/answer should be set as the local description using SetLocalDescription. When the remote offer/answer is available, it should be set as the remote description using SetRemoteDescription.

Once both SetLocalDescription and SetRemoteDescription have been called, the connection process will begin. Local candidates are raised to the OnLocalCandidate event, and remote candidates should be set using SetRemoteCandidate as they become available.

CreateOffer, CreateAnswer, SetLocalDescription, and SetRemoteDescription are all asynchronous and return a Future<SessionDescription>, so while this model is more verbose than IceLink 2, it is also more explicit and much easier to manipulate.

// A initiates a call by creating an offer,
// setting it as the local description, and
// then signalling it to B.
connectionA.CreateOffer().Then((offer) =>
{
    return connectionA.SetLocalDescription(offer);
}).Then((offer) =>
{
    // signal offer to remote peer
}).Fail((exception) =>
{
    // uh oh
});

// B accepts the call by setting A's offer
// as the remote description, creating an
// answer, setting it as the local description,
// and then signalling it to A.
connectionB.SetRemoteDescription(offer).Then((offer) =>
{
    return connectionB.CreateAnswer();
}).Then((answer) =>
{
    return connectionB.SetLocalDescription(answer);
}).Then((answer) =>
{
    // signal answer to remote peer
}).Fail((exception) =>
{
    // uh oh
});

// A completes the call negotiation by setting
// B's answer as the remote description
connectionA.SetRemoteDescription(answer).Fail((exception) =>
{
    // uh oh
});

Migration

  • Replace calls to Conference.Link with Connection.CreateOffer followed by Connection.SetLocalDescription.
  • Replace calls to Conference.ReceiveOfferAnswer with Connection.SetRemoteDescription. If the session description is an offer, also call Connection.CreateAnswer followed by Connection.SetLocalDescription.
  • Replace calls to Conference.ReceiveCandidate with Connection.AddRemoteCandidate.
  • Replace references to Conference.OnLinkCandidate with Connection.OnLocalCandidate.
  • Move code from Conference.OnLinkOfferAnswer into the resolved future handler for SetLocalDescription.
  • Replace calls to Conference.Unlink with Connection.Close.

TurnServer and StunServer (previously Server)

In IceLink 3, TurnServer and StunServer have replaced Server. The servers can be started on multiple addresses, with optional public addresses specified for each. Separate addresses can be used for UDP and TCP.

// IceLink 2
var server = new Server();
server.EnableRelay((e) =>
{
    return RelayAuthenticateResult.FromLongTermKeyBytes(...); // RelayAuthenticateResult.FromPassword(...);
});
server.PublicIPAddress = "public IP";
server.Start("private IP", 3478);

// IceLink 3
var server = new TurnServer((e) =>
{
    return TurnAuthResult.FromLongTermKeyBytes(...); // 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 });

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.
  • Update the arguments passed into Server.Start to use ServerAddress.

Custom AudioSource (previously AudioCaptureProvider)

In IceLink 3, AudioSource has replaced AudioCaptureProvider.

// IceLink 2
public class MyAudioCaptureProvider : AudioCaptureProvider
{
    public override string[] GetDeviceNames() { ... }
    public override string GetLabel() { ... }

    public override void Initialize(AudioCaptureInitializeArgs captureArgs) { ... }
    public override void Destroy() { ... }

    public override bool Start() { ... }
    public override void Stop() { ... }
}

// IceLink 3
public class MyAudioSource : AudioSource
{
    public override SourceInput[] Inputs { get { ... } } // optional
    public override string Label { get { ... } }

    public MyAudioSource(AudioFormat outputFormat) : base(format) { ... }
    public override DoDestroy() { ... } // optional

    protected override Future<object> DoStart() { ... }
    protected override Future<object> DoStop() { ... }
}

Migration

  • Replace GetLabel with the Label property.
  • Replace GetDeviceNames with the Inputs property (or remove it entirely).
  • Replace Start and Stop with DoStart and DoStop, noting that they must return a Future, even if the operation is resolved synchronously.
  • Remove Initialize and Destroy, moving code into the constructor and DoDestroy methods.

Custom VideoSource (previously VideoCaptureProvider)

In IceLink 3, VideoSource has replaced VideoCaptureProvider.

// IceLink 2
public class MyVideoCaptureProvider : VideoCaptureProvider
{
    public override string[] GetDeviceNames() { ... }
    public override string GetLabel() { ... }

    public override void Initialize(VideoCaptureInitializeArgs captureArgs) { ... }
    public override void Destroy() { ... }

    public override bool Start() { ... }
    public override void Stop() { ... }
}

// IceLink 3
public class MyVideoSource : VideoSource
{
    public override SourceInput[] Inputs { get { ... } } // optional
    public override string Label { get { ... } }

    public MyVideoSource(VideoFormat outputFormat) : base(format) { ... }
    public override DoDestroy() { ... } // optional

    protected override Future<object> DoStart() { ... }
    protected override Future<object> DoStop() { ... }
}

Migration

  • Replace GetLabel with the Label property.
  • Replace GetDeviceNames with the Inputs property (or remove it entirely).
  • Replace Start and Stop with DoStart and DoStop, noting that they must return a Future, even if the operation is resolved synchronously.
  • Remove Initialize and Destroy, moving code into the constructor and DoDestroy methods.

Custom AudioSink (previously AudioRenderProvider)

In IceLink 3, AudioSink has replaced AudioRenderProvider.

// IceLink 2
public class MyAudioRenderProvider : AudioRenderProvider
{
    public override void Initialize(AudioRenderInitializeArgs renderArgs) { ... }
    public override void Destroy() { ... }

    public override void Render(AudioBuffer buffer) { ... }
}

// IceLink 3
public class MyAudioSink : AudioSink
{
    public override string Label { get { ... } }
    protected override void DoDestroy() { ... }
    protected override void DoProcessFrame(AudioFrame frame, AudioBuffer inputBuffer) { ... }
}

Migration

  • Add the new Label property.
  • Remove Initialize and Destroy, moving code into the constructor and DoDestroy methods.

Custom VideoSink (previously VideoRenderProvider)

In IceLink 3, VideoSink has replaced VideoRenderProvider.

// IceLink 2
public class MyVideoRenderProvider : VideoRenderProvider
{
    public override void Initialize(VideoRenderInitializeArgs renderArgs) { ... }
    public override void Destroy() { ... }

    public override void Render(VideoBuffer buffer) { ... }
}

// IceLink 3
public class MyVideoSink : VideoSink
{
    public override string Label { get { ... } }
    protected override void DoDestroy() { ... }
    protected override void DoProcessFrame(VideoFrame frame, VideoBuffer inputBuffer) { ... }
}

Migration

  • Add the new Label property.
  • Remove Initialize and Destroy, moving code into the constructor and DoDestroy methods.

IceServerTest (previously LocalNetwork.CheckServer)

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

Migration

  • Add the new Label property.
  • Remove Initialize and Destroy, moving code into the constructor and DoDestroy methods.

Wrap Up

If we missed something, or if you have a suggestion on a way to improve this guide, please let us know by contacting support@frozenmountain.com or visiting support.frozenmountain.com.

Thank you, and happy coding!