Contact Us
Free Trial
New call-to-action
New call-to-action
New call-to-action

LiveSwitch and FreeSWITCH - Part 1

by Anton Venema, on August 24, 2017

LiveSwitch’s powerful client-side API makes it easy to integrate with other media processing libraries and cloud services.

LiveSwitch and FreeSWITCH

This August, we attended ClueCon, hosted by the team behind FreeSWITCH, and did a live coding demo building up a LiveSwitch web application from scratch that demonstrated SFU, MCU, and peer connections simultaneously, all while integrated with FreeSWITCH for VoIP calling, and all self-hosted in a local development box.

In the next couple posts,  we are going to share some of that code and explain in more depth what we’re doing and how it all works.

  • Part 1 covers the construction of the web-based application: demonstrating how to create and manage MCU, SFU, and peer connections, all at the same time, and all in the same app.
  • Part 2 covers the integration of FreeSWITCH with the LiveSwitch SIP connector to bridge in VoIP phones and the traditional telephone network (PSTN/POTS).

Getting Started

To start, you’re going to need LiveSwitch installed on your Windows server or development machine.  If you don’t have a copy, you can download a free trial.

Once you have the LiveSwitch gateway and media server up and running, you’re ready to start coding. The LiveSwitch SIP connector is optional, but can be useful if you want to bridge out to FreeSWITCH or another SIP trunk/PBX.

More information about configuring the SIP connector can be found in the LiveSwitch docs, but integration is a simple as providing your SIP register credentials and setting up a dial-plan, either by static config or dynamic web-hook.

We’re going to create two files - index.htm and index.ts/js. Everything is going to be intentionally minimalistic to make it easier to see what’s happening, and we’re going to link in Bootstrap and jQuery to help with that. Here’s index.htm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<html>
<head>
    <title>LiveSwitch Demo</title>
    <link rel="stylesheet" href="bootstrap.min.css" />
</head>
<body>
    <h1>LiveSwitch Demo</h1>
    <div class="container">
        <div class="row">
            <div class="col-sm-12">
                Name: <input type="text" id="name" value="Anonymous" />
                <button type="button" id="register">Register</button>
            </div>
        </div>
        <div class="row">
            <div class="col-sm-8" id="videos" style="height: 480px">
            </div>
            <div class="col-sm-4">
                <div class="row" id="participants"></div>
                <div class="row" id="sip"></div>
            </div>
        </div>
    </div>
    <script src="jquery.min.js"></script>
    <script src="bootstrap.min.js"></script>
    <script src="fm.liveswitch.js"></script>
    <script src="index.js"></script>
</body>
</html>

 

There’s nothing fancy here. Just a button to kick things off and a couple containers to hold videos and participant information as we get notifications.

The JavaScript/TypeScript is a little more complicated, but we’ll walk through it. Here’s the full index.ts to start:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
let liveswitch = fm.liveswitch;
 
liveswitch.Log.setProvider(new liveswitch.ConsoleLogProvider(liveswitch.LogLevel.Debug));
 
$('#register').click(() => {
 
    let appId = 'my-app-id';
    let client = new liveswitch.Client('http://localhost:8080/sync', appId);
    let layoutManager = new liveswitch.DomLayoutManager($('#videos')[0]);
    let localMedia = new liveswitch.LocalMedia(true, true);
 
    client.setUserAlias($('#name').val());
 
    // WARNING: DON'T DO THIS HERE. THE SHARED SECRET SHOULD
    // BE USED BY YOUR APP SERVER TO GENERATE THIS TOKEN
    let sharedSecret = '--replaceThisWithYourOwnSharedSecret--';
    let channelClaims = [
        new liveswitch.ChannelClaim('333333')
    ];
    let token = liveswitch.Token.generateClientRegisterToken(appId, client.getUserId(), client.getDeviceId(), client.getId(), client.getRoles(), channelClaims, sharedSecret);
 
    // register and join a channel at the same time
    client.register(token).then((channels) => {
        let channel = channels[0];
 
        let mcuButton = $('<button>');
        mcuButton.text('MCU');
        mcuButton.click(() => {
            let remoteMedia = new liveswitch.RemoteMedia();
            let audioStream = new liveswitch.AudioStream(localMedia, remoteMedia);
            let videoStream = new liveswitch.VideoStream(localMedia, remoteMedia);
            let mcuConnection = channel.createMcuConnection(audioStream, videoStream);
            mcuConnection.addOnStateChange((connection) => {
                if (mcuConnection.getState() == liveswitch.ConnectionState.Connected) {
                    layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());
                } else if (mcuConnection.getState() == liveswitch.ConnectionState.Failing ||
                    mcuConnection.getState() == liveswitch.ConnectionState.Closing) {
                    layoutManager.removeRemoteView(remoteMedia.getId());
                }
            });
            mcuConnection.open();
            $(remoteMedia.getView()).dblclick(() => {
                mcuConnection.close();
            });
        });
        $('#participants').append(mcuButton);
 
        let upstreamButton = $('<button>');
        upstreamButton.text('SFU Upstream');
        upstreamButton.click(() => {
            let audioStream = new liveswitch.AudioStream(localMedia);
            let videoStream = new liveswitch.VideoStream(localMedia);
            let sfuUpstreamConnection = channel.createSfuUpstreamConnection(audioStream, videoStream);
            sfuUpstreamConnection.open();
            $(localMedia.getView()).dblclick(() => {
                sfuUpstreamConnection.close();
            });
        });
        $('#participants').append(upstreamButton);
 
        let addRemoteConnection = (remoteConnectionInfo: fm.liveswitch.ConnectionInfo) => {
            var clientDiv = $('#' + remoteConnectionInfo.getClientId());
            if (!clientDiv && remoteConnectionInfo.getClientId().indexOf('sip:') == 0) {
                clientDiv = $('#sip');
            }
            let connectionDiv = $('<div>');
            connectionDiv.attr('id', remoteConnectionInfo.getId());
            connectionDiv.text(remoteConnectionInfo.getId() + ' (' + remoteConnectionInfo.getClientId() + ')');
            let downstreamButton = $('<button>');
            downstreamButton.text('SFU Downstream');
            downstreamButton.click(() => {
                let remoteMedia = new liveswitch.RemoteMedia();
                let audioStream = new liveswitch.AudioStream(localMedia, remoteMedia);
                let videoStream = new liveswitch.VideoStream(localMedia, remoteMedia);
                let sfuDownstreamConnection = channel.createSfuDownstreamConnection(remoteConnectionInfo, audioStream, videoStream);
                sfuDownstreamConnection.addOnStateChange((connection) => {
                    if (sfuDownstreamConnection.getState() == liveswitch.ConnectionState.Connected) {
                        layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());
                    } else if (sfuDownstreamConnection.getState() == liveswitch.ConnectionState.Failing ||
                        sfuDownstreamConnection.getState() == liveswitch.ConnectionState.Closing) {
                        layoutManager.removeRemoteView(remoteMedia.getId());
                    }
                });
                sfuDownstreamConnection.open();
                $(remoteMedia.getView()).dblclick(() => {
                    sfuDownstreamConnection.close();
                });
            });
            connectionDiv.append(downstreamButton);
            $('#participants').append(connectionDiv);
        };
 
        channel.addOnRemoteUpstreamConnectionOpen((remoteConnectionInfo) => {
            addRemoteConnection(remoteConnectionInfo);
        });
        channel.addOnRemoteUpstreamConnectionClose((remoteConnectionInfo) => {
            $('#' + remoteConnectionInfo.getId()).remove();
        });
        for (let remoteConnectionInfo of channel.getRemoteUpstreamConnectionInfos()) {
            addRemoteConnection(remoteConnectionInfo);
        }
 
        let addRemoteClient = (remoteClientInfo: fm.liveswitch.ClientInfo) => {
            let clientDiv = $('<div>');
            clientDiv.attr('id', remoteClientInfo.getId());
            clientDiv.text(remoteClientInfo.getUserAlias());
            let callButton = $('<button>');
            callButton.text('P2P Call');
            callButton.click(() => {
                let remoteMedia = new liveswitch.RemoteMedia();
                let audioStream = new liveswitch.AudioStream(localMedia, remoteMedia);
                let videoStream = new liveswitch.VideoStream(localMedia, remoteMedia);
                let peerConnection = channel.createPeerConnection(remoteClientInfo, audioStream, videoStream);
                peerConnection.addOnStateChange((connection) => {
                    if (peerConnection.getState() == liveswitch.ConnectionState.Connected) {
                        layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());
                    } else if (peerConnection.getState() == liveswitch.ConnectionState.Failing ||
                        peerConnection.getState() == liveswitch.ConnectionState.Closing) {
                        layoutManager.removeRemoteView(remoteMedia.getId());
                    }
                });
                peerConnection.open();
                $(remoteMedia.getView()).dblclick(() => {
                    peerConnection.close();
                });
            });
            clientDiv.append(callButton);
            $('#participants').append(clientDiv);
        };
 
        channel.addOnRemoteClientJoin((remoteClientInfo) => {
            addRemoteClient(remoteClientInfo);
        });
        channel.addOnRemoteClientLeave((remoteClientInfo) => {
            $('#' + remoteClientInfo.getId()).remove();
        });
        for (let remoteClientInfo of channel.getRemoteClientInfos()) {
            addRemoteClient(remoteClientInfo);
        }
 
        channel.addOnPeerConnectionOffer((offer) => {
            if (confirm('Peer-connect to ' + offer.getRemoteClientInfo().getUserAlias() + '?')) {
                let remoteMedia = new liveswitch.RemoteMedia();
                let audioStream = new liveswitch.AudioStream(localMedia, remoteMedia);
                let videoStream = new liveswitch.VideoStream(localMedia, remoteMedia);
                let peerConnection = offer.accept(audioStream, videoStream);
                peerConnection.addOnStateChange((c) => {
                    if (peerConnection.getState() == liveswitch.ConnectionState.Connected) {
                        layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());
                    } else if (peerConnection.getState() == liveswitch.ConnectionState.Failing ||
                        peerConnection.getState() == liveswitch.ConnectionState.Closing) {
                        layoutManager.removeRemoteView(remoteMedia.getId());
                    }
                });
                peerConnection.open();
                $(remoteMedia.getView()).dblclick(() => {
                    peerConnection.close();
                });
            } else {
                offer.reject();
            }
        });
 
        channel.addOnMessage((remoteClientInfo, message) => {
            console.log(message);
        });
    }, (ex) => {
        liveswitch.Log.error('Could not register.', ex);
    });
     
    localMedia.start().then((localMedia) => {
        layoutManager.setLocalView(localMedia.getView());
    }, (ex) => {
        liveswitch.Log.error('Could not start local media.', ex);
    });
     
    $(window).on('beforeunload', () => {
        client.unregister();
        layoutManager.unsetLocalView();
        localMedia.stop();
    });
});

 

With that out of the way, let’s explain one chunk at a time. First, our basic framework:

1
2
3
4
5
6
7
let liveswitch = fm.liveswitch;
 
liveswitch.Log.setProvider(new liveswitch.ConsoleLogProvider(liveswitch.LogLevel.Debug));
 
$('#register').click(() => {
    
});

 

LiveSwitch and IceLink both have very detailed client-side logging to help you diagnose issues when developing your application. A good first step is to set this up. Once we’ve done that, we use jQuery to wire up a button click event so we have our trigger to kick things off.

Inside the click handler, things start to get interesting:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let appId = 'my-app-id';
let client = new liveswitch.Client('http://localhost:8080/sync', appId);
let layoutManager = new liveswitch.DomLayoutManager($('#videos')[0]);
let localMedia = new liveswitch.LocalMedia(true, true);
 
client.setUserAlias($('#name').val());
 
// WARNING: DON'T DO THIS HERE. THE SHARED SECRET SHOULD
// BE USED BY YOUR APP SERVER TO GENERATE THIS TOKEN
let sharedSecret = '--replaceThisWithYourOwnSharedSecret--';
let channelClaims = [
    new liveswitch.ChannelClaim('333333')
];
let token = liveswitch.Token.generateClientRegisterToken(appId, client.getUserId(), client.getDeviceId(), client.getId(), client.getRoles(), channelClaims, sharedSecret);
 
// register/join
client.register(token).then((channels) => {
    ...
}, (ex) => {
    liveswitch.Log.error('Could not register.', ex);
});
     
localMedia.start().then((localMedia) => {
    layoutManager.setLocalView(localMedia.getView());
}, (ex) => {
    liveswitch.Log.error('Could not start local media.', ex);
});
     
$(window).on('beforeunload', () => {
    client.unregister();
    layoutManager.unsetLocalView();
    localMedia.stop();
});

 

There are three key pieces we initialize here:

  1. Our LiveSwitch client.
  2. Our LiveSwitch layout manager (optional).
  3. Our LiveSwitch local media.

The client drives all our signalling and is used to kick off new connections (the app ID is used to sandbox different applications using the same server hardware). The layout manager is optional, but helps make it easier to place videos into the layout with sensible size and positioning. The local media gives access to the device camera and microphone (or the device screen if screen-sharing).

Once we have all three, we’re just about ready to kick things off by registering the client with the LiveSwitch gateway.

First, though, we assign the client a user alias (device alias is possible as well) so our remote peers get a friendly name to display when they get notifications about our activities. This is an optional step, but a pretty common one.

The only required step before registering is to obtain an authorization token. This should be generated securely by your auth server after is has authenticated the requesting endpoint successfully, but for the sake of simplicity, we’re generating it inline here. Don’t do this in your production application!

The register token can optionally include channel information so the client is automatically joined to an initial set of channels in the process of registering. You can also join channels after registering, but combining the two operations makes things more efficient when possible.

With registration token in hand, we can register. Next, we start our local media, and if successful, add the local preview to our layout.

Finally, we add a bit of cleanup when the window unloads by wiring an event handler to reverse our actions thus far.

Once we’ve registered and joined our desired channel, we can set up notification handlers and start creating streaming connections!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let channel = channels[0];
 
let mcuButton = $('<button>');
mcuButton.text('MCU');
mcuButton.click(() => {
    let remoteMedia = new liveswitch.RemoteMedia();
    let audioStream = new liveswitch.AudioStream(localMedia, remoteMedia);
    let videoStream = new liveswitch.VideoStream(localMedia, remoteMedia);
    let mcuConnection = channel.createMcuConnection(audioStream, videoStream);
    mcuConnection.addOnStateChange((connection) => {
        if (mcuConnection.getState() == liveswitch.ConnectionState.Connected) {
            layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());
        } else if (mcuConnection.getState() == liveswitch.ConnectionState.Failing ||
            mcuConnection.getState() == liveswitch.ConnectionState.Closing) {
            layoutManager.removeRemoteView(remoteMedia.getId());
        }
    });
    mcuConnection.open();
    $(remoteMedia.getView()).dblclick(() => {
        mcuConnection.close();
    });
});
$('#participants').append(mcuButton);

 MCU Connection Set-UP

The simplest type of connection to set up is an MCU connection. All we’re doing here is adding a button to the page that, when clicked, does a few things:

  1. Creates a remote media instance to manage the incoming media.
  2. Creates an audio and video stream to send and receive media.
  3. Creates an MCU connection to manage the above streams.
  4. Monitors the MCU connection for state change so we can add/remove from the video view from the layout (not needed if the connection is audio-only).
  5. Opens the connection.
  6. Sets up a UI hook (double-clicking the video) to close the connection later.

We’re going to use this same pattern, with slight variations, for all the connection types - MCU, SFU upstream, SFU downstream, and peer.

At this point, you should be able to run the application and register to create an MCU connection!

SFU Connection Set-UP

Next up, we’re going to add SFU connections into the mix. SFU connections are little more complicated, since you have both upstream and downstream components. Creating an SFU upstream connection, for example, doesn’t actually show anything visibly until an SFU downstream connection is created to receive the media.

1
2
3
4
5
6
7
8
9
10
11
12
let upstreamButton = $('<button>');
upstreamButton.text('SFU Upstream');
upstreamButton.click(() => {
    let audioStream = new liveswitch.AudioStream(localMedia);
    let videoStream = new liveswitch.VideoStream(localMedia);
    let sfuUpstreamConnection = channel.createSfuUpstreamConnection(audioStream, videoStream);
    sfuUpstreamConnection.open();
    $(localMedia.getView()).dblclick(() => {
        sfuUpstreamConnection.close();
    });
});
$('#participants').append(upstreamButton);

 

This should look pretty familiar. Creating an SFU upstream connection is just like creating an MCU connection, but without the remote media.

Likewise, creating an SFU downstream connection is like creating an MCU connection, but without the local media, and with the additional requirement that we need to know what which upstream connection we are targeting.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
let addRemoteConnection = (remoteConnectionInfo: fm.liveswitch.ConnectionInfo) => {
    var clientDiv = $('#' + remoteConnectionInfo.getClientId());
    if (!clientDiv && remoteConnectionInfo.getClientId().indexOf('sip:') == 0) {
        clientDiv = $('#sip');
    }
    let connectionDiv = $('<div>');
    connectionDiv.attr('id', remoteConnectionInfo.getId());
    connectionDiv.text(remoteConnectionInfo.getId() + ' (' + remoteConnectionInfo.getClientId() + ')');
    let downstreamButton = $('<button>');
    downstreamButton.text('SFU Downstream');
    downstreamButton.click(() => {
        let remoteMedia = new liveswitch.RemoteMedia();
        let audioStream = new liveswitch.AudioStream(null, remoteMedia);
        let videoStream = new liveswitch.VideoStream(null, remoteMedia);
        let sfuDownstreamConnection = channel.createSfuDownstreamConnection(remoteConnectionInfo, audioStream, videoStream);
        sfuDownstreamConnection.addOnStateChange((connection) => {
            if (sfuDownstreamConnection.getState() == liveswitch.ConnectionState.Connected) {
                layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());
            } else if (sfuDownstreamConnection.getState() == liveswitch.ConnectionState.Failing ||
                sfuDownstreamConnection.getState() == liveswitch.ConnectionState.Closing) {
                layoutManager.removeRemoteView(remoteMedia.getId());
            }
        });
        sfuDownstreamConnection.open();
        $(remoteMedia.getView()).dblclick(() => {
            sfuDownstreamConnection.close();
        });
    });
    connectionDiv.append(downstreamButton);
    $('#participants').append(connectionDiv);
};

 

So how do we know what connection to target? This is where notifications come into play:

1
2
3
4
5
6
7
8
9
channel.addOnRemoteUpstreamConnectionOpen((remoteConnectionInfo) => {
    addRemoteConnection(remoteConnectionInfo);
});
channel.addOnRemoteUpstreamConnectionClose((remoteConnectionInfo) => {
    $('#' + remoteConnectionInfo.getId()).remove();
});
for (let remoteConnectionInfo of channel.getRemoteUpstreamConnectionInfos()) {
    addRemoteConnection(remoteConnectionInfo);
}

 

When we get a notification that a remote upstream connection has been created (noting that this could be an SFU upstream connection or an MCU connection), we add a button to the UI that, when clicked, creates an SFU downstream connection to receive the media. Likewise, when the remote upstream connection is closed, we remove that button.

Finally, there could be a number of remote upstream connections already running when we join the channel, so we process our initial set of remote upstream connection details as if we received notifications for each of them.

At this point, you should be able to run the application and register to create MCU, SFU upstream, and/or SFU downstream connections!

Peer-to-Peer Connection Set-Up

Next up, we’re going to look at peer connections.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let addRemoteClient = (remoteClientInfo: fm.liveswitch.ClientInfo) => {
    let clientDiv = $('<div>');
    clientDiv.attr('id', remoteClientInfo.getId());
    clientDiv.text(remoteClientInfo.getUserAlias());
    let callButton = $('<button>');
    callButton.text('P2P Call');
    callButton.click(() => {
        let remoteMedia = new liveswitch.RemoteMedia();
        let audioStream = new liveswitch.AudioStream(localMedia, remoteMedia);
        let videoStream = new liveswitch.VideoStream(localMedia, remoteMedia);
        let peerConnection = channel.createPeerConnection(remoteClientInfo, audioStream, videoStream);
        peerConnection.addOnStateChange((connection) => {
            if (peerConnection.getState() == liveswitch.ConnectionState.Connected) {
                layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());
            } else if (peerConnection.getState() == liveswitch.ConnectionState.Failing ||
                peerConnection.getState() == liveswitch.ConnectionState.Closing) {
                layoutManager.removeRemoteView(remoteMedia.getId());
            }
        });
        peerConnection.open();
        $(remoteMedia.getView()).dblclick(() => {
            peerConnection.close();
        });
    });
    clientDiv.append(callButton);
    $('#participants').append(clientDiv);
};

 

As with SFU downstream connections, we need a target for a peer connection, but instead of information about a remote connection, we need information about a remote client.

Once we have that, creating the peer connection is just like creating an MCU connection.

1
2
3
4
5
6
7
8
9
channel.addOnRemoteClientJoin((remoteClientInfo) => {
    addRemoteClient(remoteClientInfo);
});
channel.addOnRemoteClientLeave((remoteClientInfo) => {
    $('#' + remoteClientInfo.getId()).remove();
});
for (let remoteClientInfo of channel.getRemoteClientInfos()) {
    addRemoteClient(remoteClientInfo);
}

 

When we get a notification that a remote client has joined, we add a button to the UI that, when clicked, creates a peer connection. Likewise, when the remote client leaves, we remove that button.

Finally, as with remote upstream connections, there could be a number of remote clients already joined when we join the channel, so we process our initial set of remote client details as if we received notifications for each of them.

Peer connections are unique in that they require acceptance on behalf of the answering client. You can make this acceptance automatic so that the called client doesn’t have a choice, or you can prompt them to accept the connection, which is what we’re demonstrating here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
channel.addOnPeerConnectionOffer((offer) => {
    if (confirm('Peer-connect to ' + offer.getRemoteClientInfo().getUserAlias() + '?')) {
        let remoteMedia = new liveswitch.RemoteMedia();
        let audioStream = new liveswitch.AudioStream(localMedia, remoteMedia);
        let videoStream = new liveswitch.VideoStream(localMedia, remoteMedia);
        let peerConnection = offer.accept(audioStream, videoStream);
        peerConnection.addOnStateChange((c) => {
            if (peerConnection.getState() == liveswitch.ConnectionState.Connected) {
                layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());
            } else if (peerConnection.getState() == liveswitch.ConnectionState.Failing ||
                peerConnection.getState() == liveswitch.ConnectionState.Closing) {
                layoutManager.removeRemoteView(remoteMedia.getId());
            }
        });
        peerConnection.open();
        $(remoteMedia.getView()).dblclick(() => {
            peerConnection.close();
        });
    } else {
        offer.reject();
    }
});

 

If the user declines the call, we reject the offer - nice and simple. LiveSwitch will handle the signalling back to the offering client. If the user accepts the call, we create a peer connection just like the calling side, but by accepting the offer (noted in bold above).

At this point, you should be able to run the application and register to create MCU, SFU upstream, SFU downstream, and/or peer connections!

As always, if you have any questions or comments, we would love to hear from you.

LiveSwitch Whitepaper-2

 

 

Topics:LiveSwitch

Anton Venema

As Frozen Mountain’s CTO, Anton is one of the world’s foremost experts on RTC solutions, as well as the technical visionary and prime architect of our products, IceLink and WebSync, and our custom solutions. Anton is responsible for ensuring that Frozen Mountain’s products exceed the needs of today and predict the needs of tomorrow.