Search Results for

    Show / Hide Table of Contents

    P2P Connection

    In addition to SFU and MCU connections, LiveSwitch also supports a peer-to-peer (P2P) connections. This allows for users to be connected to each other in a mesh topology, where each user is connected to every other user directly. This type of connection consumes the least amount of server resources but is also the most bandwidth and CPU-intensive type of connection for end-users. The amount of bandwidth required to maintain the session increases by a constant factor as each additional user joins. This makes peer-to-peer connections preferable for small video conferences, as users' connections can generally not support more than five or six people in a conference.

    Create a P2P Connection

    Start by registering with the LiveSwitch gateway and obtaining an FM.LiveSwitch.Channel instance. This is covered in the previous Registering a Client section. You should also review the Handling Local Media, Handling Remote Media, and Creating Streams and Connections sections, which review the media components and streams that are used in this guide.

    When establishing a peer-to-peer connection, there are two roles to consider. The first role is the offerer. The offerer is a user who is already in a channel. When a new user joins this channel, it is the offerer's responsibility to send this new user a message that indicates that they wish to connect to them. This leads to the second role - the answerer. The answerer is a user who has just joined a channel. They are responsible for answering any connection messages that they receive. This section focuses first on defining the offerer's role and then the answerer's role.

    Note

    About the code examples on this page:

    • For .NET MAUI and Unity, use the C# code.
    • For macOS, use the iOS code.

    Define the Offerer

    To send an offer to clients who have just joined, you first need to be notified when a client joins. This is accomplished by adding an OnRemoteClientJoin event handler to your Channel instance. As indicated by the event name, this event is raised whenever a remote client joins the channel. This event handler must do two things:

    • update the UI
    • create and open a peer-to-peer connection

    Begin by updating the UI. Create a new instance of your RemoteMedia class. Next, add the RemoteMedia instance's associated view to your layout manager by invoking its AddRemoteView method. This method requires an id parameter and a view object, both of which can be accessed through properties on the RemoteMedia instance. Next, you must create an FM.LiveSwitch.PeerConnection instance. You do this by invoking the CreatePeerConnection method of your Channel instance. This method expects an instance of FM.LiveSwitch.ClientInfo as its first parameter, which is provided to you as one of the event handler parameters. Create the PeerConnection instance, assign it the required ICE servers and kick off the connection process by invoking its Open method. This sends an offer to the user who has just connected, identified by the information in the ClientInfo instance. Remember to inspect the promise returned from the Open method.

    Once complete, your app is able to send offers to new users who join the channel. The next step is to have these users respond to the offers and establish a connection.

    • CSharp
    • Android
    • iOS
    • JavaScript
    channel.OnRemoteClientJoin += (FM.LiveSwitch.ClientInfo remoteClientInfo) =>
    {
        var remoteMedia = new RemoteMedia();
        var audioStream = new FM.LiveSwitch.AudioStream(localMedia, remoteMedia);
        var videoStream = new FM.LiveSwitch.VideoStream(localMedia, remoteMedia);
        var connection = channel.CreatePeerConnection(remoteClientInfo, audioStream, videoStream);
        layoutManager.AddRemoteView(remoteMedia.Id, remoteMedia.View);
        connection.OnStateChange += (FM.LiveSwitch.ManagedConnection c) =>
        {
            if (c.State == FM.LiveSwitch.ConnectionState.Closing || c.State == FM.LiveSwitch.ConnectionState.Failing)
            {
                layoutManager.RemoveRemoteView(remoteMedia.Id);
            }
        };
        connection.IceServers = ...
        connection.Open().Then((result) =>
        {
            Console.WriteLine("offerer's connection established");
        }).Fail((ex) =>
        {
            Console.WriteLine("an error occurred");
        });
    };
    
    channel.addOnRemoteClientJoin((fm.liveswitch.ClientInfo remoteClientInfo) -> {
        RemoteMedia remoteMedia = new RemoteMedia();
        fm.liveswitch.AudioStream audioStream = new fm.liveswitch.AudioStream(localMedia, remoteMedia);
        fm.liveswitch.VideoStream videoStream = new fm.liveswitch.VideoStream(localMedia, remoteMedia);
        fm.liveswitch.PeerConnection connection = channel.createPeerConnection(remoteClientInfo, audioStream, videoStream);
        layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());
        connection.addOnStateChange((fm.liveswitch.ManagedConnection c) -> {
            if (c.getState() == fm.liveswitch.ConnectionState.Closing || c.getState() == fm.liveswitch.ConnectionState.Failing) {
                layoutManager.removeRemoteView(remoteMedia.getId());
            }
        });
        connection.setIceServers(...);
        connection.open().then((Object result) -> {
            System.out.println("offerer's connection established");
        }).fail((Exception ex) -> {
            System.out.println("an error occurred");
        });
    });
    
    channel.addOnRemoteClientJoin { (remoteClientInfo:FMLiveSwitchClientInfo) in
        var remoteMedia:RemoteMedia = RemoteMedia()
        var audioStream = FMLiveSwitchAudioStream(localMedia: localMedia, remoteMedia: remoteMedia)
        var videoStream = FMLiveSwitchVideoStream(localMedia: localMedia, remoteMedia: remoteMedia)
        var connection = channel.createPeerConnection(remoteClientInfo: remoteClientInfo, audioStream: audioStream, videoStream: videoStream)
        layoutManager.addRemoteView(id: remoteMedia.id(), view: remoteMedia.view())
        connection.addOnStateChange { (c:FMLiveSwitchManagedConnection) in
            if c.state() == FMLiveSwitchConnectionStateClosing || c.state() == FMLiveSwitchConnectionStateFailing {
                layoutManager.removeRemoteView(id: remoteMedia.id())
            }
        }
        connection.setIceServers(...)
        connection.open().then(resolveAction: { (result:NSObject) in
            print("offerer's connection established")
        }).fail(rejectAction: { (ex:NSException) in
            print("an error occurred")
        })
    }
    
    channel.addOnRemoteClientJoin(function(remoteClientInfo) {
        var remoteMedia = new fm.liveswitch.RemoteMedia();
        var audioStream = new fm.liveswitch.AudioStream(localMedia, remoteMedia);
        var videoStream = new fm.liveswitch.VideoStream(localMedia, remoteMedia);
        var connection = channel.createPeerConnection(remoteClientInfo, audioStream, videoStream);
        layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());
        connection.addOnStateChange(function(c) {
            if (c.getState() == fm.liveswitch.ConnectionState.Closing || c.getState() == fm.liveswitch.ConnectionState.Failing) {
                layoutManager.removeRemoteView(remoteMedia.getId());
            }
        });
        connection.setIceServers(...);
        connection.open().then(function(result) {
            console.log("offerer's connection established");
        }).fail(function(ex) {
            console.log("an error occurred");
        });
    });
    

    Define the Answerer

    The code for responding to an offer is almost identical to the code for sending one. The key difference is that instead of adding an OnRemoteClientJoin event handler, you add an OnPeerConnectionOffer event handler. Similar to above, this event handler has two key responsibilities:

    • update the UI
    • create and open a peer-to-peer connection

    Proceed as you did above when writing this event handler. Create a RemoteMedia instance, and add the new view to the layout. Once again, invoke CreatePeerConnection, but this time you pass it an instance of FM.LiveSwitch.PeerConnectionOffer. You can obtain this instance from the parameters of the event handler. The CreatePeerConnection method recognizes that because you have provided it with an offer, that it must now respond with an answer. Invoke the Open method of the created FM.LiveSwitch.PeerConnection instance and an answer is generated and sent back to the original peer. As usual, the Open method returns a promise wrapping the result of the Open invocation.

    Once you have worked through the example below you will have now defined both the offerer and answerer role, and two or more users should be able to connect to each other when they join a channel. The next sections focus on what to do when users leave and how to properly end a peer-to-peer session.

    • CSharp
    • Android
    • iOS
    • JavaScript
    channel.OnPeerConnectionOffer += (peerConnectionOffer) =>
    {
        var remoteMedia = new RemoteMedia();
        var audioStream = new FM.LiveSwitch.AudioStream(localMedia, remoteMedia);
        var videoStream = new FM.LiveSwitch.VideoStream(localMedia, remoteMedia);
        var connection = channel.CreatePeerConnection(peerConnectionOffer, audioStream, videoStream);
        layoutManager.AddRemoteView(remoteMedia.Id, remoteMedia.View);
        connection.OnStateChange += (FM.LiveSwitch.ManagedConnection c) =>
        {
            if (c.State == FM.LiveSwitch.ConnectionState.Closing || c.State == FM.LiveSwitch.ConnectionState.Failing)
            {
                layoutManager.RemoveRemoteView(remoteMedia.Id);
            }
        };
        connection.IceServers = ...
        connection.Open().Then((object result) =>
        {
            Console.WriteLine("answerer's connection established");
        }).Fail((Exception ex) =>
        {
            Console.WriteLine("an error occurred");
        });
    };
    
    channel.addOnPeerConnectionOffer((fm.liveswitch.PeerConnectionOffer peerConnectionOffer) -> {
        RemoteMedia remoteMedia = new RemoteMedia();
        fm.liveswitch.AudioStream audioStream = new fm.liveswitch.AudioStream(localMedia, remoteMedia);
        fm.liveswitch.VideoStream videoStream = new fm.liveswitch.VideoStream(localMedia, remoteMedia);
        fm.liveswitch.PeerConnection connection = channel.createPeerConnection(peerConnectionOffer, audioStream, viedoStream);
        layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());
        connection.addOnStateChange((fm.liveswitch.ManagedConnection c) -> {
            if (c.getState() == fm.liveswitch.ConnectionState.Closing || c.getState() == fm.liveswitch.ConnectionState.Failing) {
                layoutManager.removeRemoteView(remoteMedia.getId());
            }
        });
        connection.setIceServers(...);
        connection.open().then((Object result) -> {
            System.out.println("answerer's connection established");
        }).fail((Exception ex) -> {
            System.out.println("an error occurred");
        });
    });
    
    channel.addOnPeerConnectionOffer { (peerConnectionOffer:FMLiveSwitchPeerConnectionOffer) in
        var remoteMedia:RemoteMedia = RemoteMedia()
        var audioStream = FMLiveSwitchAudioStream(localMedia: localMedia, remoteMedia: remoteMedia)
        var videoStream = FMLiveSwitchVideoStream(localMedia: localMedia, remoteMedia: remoteMedia)
        var connection = channel.createPeerConnection(peerConnectionOffer: peerConnectionOffer, audioStream: audioStream, videoStream: videoStream)
        layoutManager.addRemoteView(id: remoteMedia.id(), view: remoteMedia.view())
        connection.addOnStateChange { (c:FMLiveSwitchManagedConnection) in
            if c.state() == FMLiveSwitchConnectionStateClosing || c.state() == FMLiveSwitchConnectionStateFailing {
                layoutManager.removeRemoteView(id: remoteMedia.id())
            }
        }
        connection.setIceServers(...)
        connection.open().then(resolveAction: { (result:NSObject) in
            print("answerer's connection established")
        }).fail(rejectAction: { (ex:NSException) in
            print("an error occurred")
        })
    }
    
    channel.addOnPeerConnectionOffer(function(peerConnectionOffer) {
        var remoteMedia = new fm.liveswitch.RemoteMedia();
        var audioStream = new fm.liveswitch.AudioStream(localMedia, remoteMedia);
        var videoStream = new fm.liveswitch.VideoStream(localMedia, remoteMedia);
        var connection = channel.createPeerConnection(peerConnectionOffer, audioStream, videoStream);
        layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());
        connection.addOnStateChange(function(c) {
            if (c.getState() == fm.liveswitch.ConnectionState.Closing || c.getState() == fm.liveswitch.ConnectionState.Failing) {
                layoutManager.removeRemoteView(remoteMedia.getId());
            }
        });
        connection.setIceServers(...);
        connection.open().then(function(result) {
            console.log("answerer's connection established");
        }).fail(function(ex) {
            console.log("an error occurred");
        });
    });
    

    Close a Connection

    If you need to close a connection manually, you can do so by invoking the Close method of the PeerConnection instance. The Close method returns a promise, the result of which you can inspect to ensure the connection has been closed properly.

    Closing a connection is straightforward - note though, that the result object in this promise is not used, which is why it is assigned the generic object type.

    • CSharp
    • Android
    • iOS
    • JavaScript
    connection.Close().Then((object result) =>
    {
        Console.WriteLine("connection closed");
    }).Fail((Exception ex) =>
    {
        Console.WriteLine("an error occurred");
    });
    
    connection.close().then((Object result) -> {
        System.out.println("connection closed");
    }).fail((Exception ex) -> {
        System.out.println("an error occurred");
    });
    
    connection.close().then(resolveBlock: { (result:NSObject) in
        print("connection closed")
    }).fail(rejectBlock: { (ex:NSException) in
        print("an error occurred")
    })
    
    connection.close().then(function(result) {
        console.log("connection closed");
    }).fail(function(ex) {
        console.log("an error occurred");
    });
    

    You now know how to properly establish and tear down a peer-to-peer session. Once again, peer-to-peer sessions work best with small amounts of participants, as the required bandwidth escalates quickly as more people join the session. You should still try to use them whenever possible, however, as they can provide substantial savings on server bandwidth.

    In This Article
    Back to top Copyright © LiveSwitch Inc. All Rights Reserved.Documentation for LiveSwitch Version 1.24.2