Legacy (1.2 and older)

Security

WebRTC in general is very secure and LiveSwitch follows the WebRTC spec. Here we discuss implementation specifics of LiveSwitch's security measures, as well as covering basic network related configuration.

Network Configuration and Required Ports

  • STUN servers provide a WAN address discovery service and listen on port 3478 by default, but any port can be used. Firewall rules must be added to allow inbound UDP traffic on the configured port.
  • TURN servers extend STUN to support relaying around restrictive firewalls. Like STUN, they listen on port 3478 by default, but any port can be used. Firewall rules must be added to allow inbound UDP and/or TCP traffic on the configured port.
  • TURNS servers extend TURN by using TLS (SSL) to secure the underlying TCP connection. Since application data is already encrypted, this simply adds a layer of security on the TURN headers, but is useful to traverse firewalls that only allow TLS/SSL traffic. TURNS servers listen on port 5349 by default, but any port can be used. Port 443 is recommended since it is generally allowed by client networks. Firewall rules must be added to allow inbound TCP traffic on the configured port.
  • Clients are the originator of requests and as such, they require no special firewall rules, provided outbound traffic is allowed.
  • Media Server clustering requires TCP traffic on port 8445. If 8445 is unavailable the next port will be tried incrementally until one is available (i.e. 8446, 8447, 8448, 8449, etc).
  • Media Server client connections require inbound UDP traffic on ports 49152-65535 on public interfaces.
  • The SIP Connector requires TCP/UDP traffic on port 5060 on public interfaces.

Even the most restrictive corporate networks generally allow HTTP traffic on port 80 and TLS/SSL traffic on port 443 for HTTPS. For this reason, we recommend running a TURN server that listens on port 443 for TURNS.

Encryption

All connections are secured using DTLS. Secure X.509 certificates are automatically generated for each connection using ECDSA (default, P-256 curve) or RSA (2048-bit) signing, but custom certificates and key-pairs can be provided. Certificate fingerprints (used to verify the peer during key exchange) are exchanged via signalling in the SDP blobs, so it is imperative to ensure security at the signalling layer.

Connection Security 

  • DTLS 1.2 with support for cipher suites that support perfect forward secrecy (PFS). See below for the specific cipher suites allowed by the key exchange.
  • Certificates use ECDSA signing with keys generated for the P-256 curve.
  • Certificates can use RSA signing with variable key length (default is 2048 bits).
  • Data is encrypted using AES 128-bit encryption with 80-bit message authentication per SRTP standards.

Cipher Suites Used for DTLS

  • TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
  • TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
  • TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
  • TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
  • TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
  • TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
  • TLS_RSA_WITH_AES_128_CBC_SHA,
  • TLS_RSA_WITH_AES_128_GCM_SHA256,
  • TLS_RSA_WITH_AES_128_CBC_SHA256

Signalling

LiveSwitch connections are extremely secure once established, provided the signalling layer is also secure. Since the certificate fingerprints must be signalled in the SDP offer/answer blobs, a weakness in the signalling layer allows the possibility of a man-in-the-middle attack. For LiveSwitch, it is the Gateway that provides the signalling layer, and when installed on a server with a trusted SSL certificate, LiveSwitch's Gateway uses TLS encryption (HTTPS/WSS). The LiveSwitch Gateway also allows you to provide rate limiting, black listing, and white listing of client connections through a token-based registration model. Managing these registration tokens is an application-level security concern. We recommend an authenticated API endpoint on your own authentication server to generate and deliver registration tokens to the client securely at runtime as requested. For DDoS protection, we recommend using a third-party service like CloudFlare.

Again, all signalling connections must be secured via TLS to make security guarantees, so your LiveSwitch Gateway must be running on a server with a trusted SSL certificate, and all traffic to the Gateway must be configured to use secure connections.

TURN Traffic Concerns and Secure TURN (TURNS)

The percentage of time that WebRTC traffic is established over relay (TURN) varies greatly depending on the network environment. For example, TURN may not be required in many home networks, but in more restrictive corporate networks, where symmetric NAT is normal, requiring TURN is far more common. In general, we estimate 20%-30% of connections use TURN.

When connecting to a TURN server, clients are authenticated by username and password. Managing these credentials is an application-level security concern. We recommend an authenticated API endpoint on your own authentication server to deliver these credentials to the client securely at runtime as requested.

For added security, you can force all traffic through TURN, which guarantees that clients cannot learn information about each others' private networks (optionally including their WAN IP).

When TURN is in use, all data flows through the TURN server. Before sending data to the TURN server, clients first encrypt it using the keys exchanged via DTLS and then prepend a TURN header with destination address information. The TURN server can only read the header, as it does not have access to the keys necessary to decrypt the payload. TURNS provides an additional level of layer of security on top of this (for TCP only) by securing the TCP stream with TLS. By doing this, the entire stream is encrypted, which includes the TURN headers (and re-encrypts the payload). The TURN server is still not able to decrypt the payload, but by encrypting the entire stream, the headers are now safe from packet sniffers. This allows the traffic to pass through stateful packet inspection in routers that would otherwise block non-HTTPS traffic.

Server Requirements

LiveSwitch has several server components that have particular hardware requirements. The following specifications describe the requirements of each component, which will help you when you are setting up your application's infrastructure.

Gateway

Firewall

  • Allow inbound TCP traffic on port 8080 (default, configurable) for HTTP
  • Allow inbound TCP traffic on port 8443 (default, configurable) for HTTPS

Minimum Hardware

  • 2x CPU
  • 2GB RAM
  • Moderate network performance

Recommended Hardware

  • 4x CPU
  • 4GB RAM
  • High network performance

High Availability

  • 2+ gateways
  • Load balance the with high-availability guarantees

Media Server

Firewall

  • For clustering, allow TCP traffic on port 8445. If 8445 is unavailable, media servers will try the next port, incrementally, until one is available (i.e. 8446, 8447, 8448, 8449, etc).
  • For client connections, allow inbound UDP traffic on ports 49152-65535 on public interfaces.

Minimum

  • 4x CPU
  • 4GB RAM
  • High network performance

Recommended Hardware

  • 8x CPU
  • 8GB RAM
  • Highest network performance

High Availability

  • 2+ media servers

SIP Connector

Firewall

  • For SIP requests allow UDP and or TCP traffic on port 5060 on public interfaces.

Installing LiveSwitch on Windows

This section describes how to setup LiveSwitch on a Windows host. For information on Linux host setup, skip ahead to the next section on Installing LiveSwitch on Linux.

LiveSwitch has several server components that are necessary for the application to run. To make the installation and configuration of these components easier, an installer is available that streamlines the setup process. You can download this installer from our Downloads page.

Once you have downloaded the installer, locate, extract, and run the FM.LiveSwitch.Installer.msi file. When you start the installer, you should be greeted with a "Welcome to LiveSwitch" splash screen, that will look like the following.

You should run the installer with elevated permissions, as it will need to create several services on your machine. To do so, right click on the .msi and select "Run as Administrator".


Splash Screen

Click "Next" to begin the installation process.

Accepting the License Agreement

The next screen in the installer will prompt you to accept the LiveSwitch licensing agreement. If you do not accept, the installer will exit. To accept, read through the terms, and then click the checkbox indicating your acceptance.

Selecting Features to Install

The LiveSwitch installer is capable of installing several product features. On this screen, you will be prompted to select which features you would like installed:

  • LiveSwitch Gateway: Connects the various parts of LiveSwitch together, required.
  • LiveSwitch Media Server: Manages SFU and MCU sessions, required.
  • LiveSwitch SIP Connector: Allows LiveSwitch to connect to SIP endpoints, optional.
  • Chat Example: Demo of LiveSwitch, optional.

Select which features you would like installed and click the Install button.

At this point, if you did not start the installer with elevated permissions, a UAC prompt will appear asking for them. The prompt will have two options, Yes and No. Click Yes to continue.

Note that although the application will allow you to skip installation of the LiveSwitch Gateway and Media Server components, these components are still required somewhere if you want a full installation of LiveSwitch. The installer allows you to skip these components on a given installation so that you can build up a server cluster by installing individual components on separate servers. In this case, you would run the installer multiple times, once on each server, and each installation would select a single component. See Scaling LiveSwitch for details.

Setting the License Key

Once you have started the installation process, a dialog will appear that prompts you to enter your license key. You can obtain a license key by logging in to your account and going to the downloads page: https://www.frozenmountain.com/downloads

If you enter an invalid license key, the installer will not let you proceed to the next step, so don't worry about entering it incorrectly.

Setting Your Redis Configuration

Redis is a high-performance, in-memory data store that is used as a cache and a message broker. It is commonly used as a backing store for applications that need to propagate data throughout a set of servers. LiveSwitch requires a Redis 3.2+ backend. To configure this, specify your Redis configuration (note you can choose to simply run Redis Locally for development purposes). 

Redis Supported Version

You will have to set up Redis 3.2+ to have your Gateway run properly.

Setting the Initial Gateway Configuration

The next dialog will appear if you have selected to install the LiveSwitch Gateway. You will see two sets of options, labelled Gateway and Admin. These options allow you to configure the HTTP and HTTPS bindings for the LiveSwitch Gateway and the LiveSwitch Gateway's Administration Console. By default, no bindings are selected.

You must, at minimum, select an HTTP port for the Gateway, or other LiveSwitch components will not be able to connect to the LiveSwitch Gateway. If you specify an HTTPS port, you must also specify an X509 certificate at this time.


There are other options that you can specify through the LiveSwitch Gateway's configuration file. Refer to the Configuring the Gateway section for more information.

Setting the Initial Media Server Configuration

The next dialog appears if you have selected to install the LiveSwitch Media Server. There are three main options here.

The Gateway URL is the address that the media server will target. Generally, this URL should match the settings you entered on the previous dialog, or else the URL should correspond to a LiveSwitch Gateway that you have installed on another machine.

The STUN address or public IP are intended to be used if you are installing on a server. If you specify a STUN address, the server will auto-discover it's public IP address at runtime. If you don't want to rely on an external STUN server, you can opt to explicitly set the public IP address of the server.

As with the LiveSwitch Gateway, there are other options that you can specify through the LiveSwitch Media Server's configuration file. Refer to the Configuring the Media Server section for more information.

Setting the Initial SIP Connector Configuration

The following configuration dialog allows you to configure the LiveSwitch SIP Connector if you selected it earlier in the install process. Like the previous dialog, it allows you to set the URL of the LiveSwitch Gateway that you want to use. Again, this URL must match the settings of either a LiveSwitch Gateway that you have installed locally, or a LiveSwitch Gateway that is installed on another machine. You must also specify your SIP registration credentials at this time.

Refer to the the Configuring the SIP Connector section for more configuration options.

Starting the LiveSwitch Web Example

If you have selected to install the Web Example, the installer will copy it to your hard drive and run it now. You will first be prompted to enter your LiveSwitch Gateway URL.

Once that is done, three windows will open. The first is a command prompt window that will display log messages related to the operation of the LiveSwitch services. The second is a browser window of your default browser. This window will open to the Web Example, so that you can try it out. This example is the same as our public demo at https://demo.liveswitch.fm. The final window is the administration console, which will show you who has connected to your LiveSwitch Gateway.

Installation Results

The installation components will be installed to C:\Program Files\Frozen Mountain Software\LiveSwitch. This directory will contain four sub-directories, one for each product feature that you have installed. If you have installed the Gateway, SIP Connector or Media Server product features, a corresponding service for each of these will also be installed. These services are named "LiveSwitch Gateway", "LiveSwitch SipConnector" and "LiveSwitch MediaServer", respectively. They will be started automatically when your machine starts.

Application Default HTTP URL Default HTTPS URL
LiveSwitch Gateway http://localhost:8080/sync

https://localhost:8443/sync

LiveSwitch Media Server n/a (media servers have no http listeners) n/a (media servers have no http listeners)
LiveSwitch Admin http://localhost:9090/admin

https://localhost:9443/admin

LiveSwitch Demo App http://localhost:52008/index.htm n/a (the demo app runs over http only)

To re-run the LiveSwitch demo application if you have closed it, browse to C:\Program Files\Frozen Mountain Software\LiveSwitch\Chat Example and run the command "node server.js". This will launch the demo application server, which can be accessed via the URL noted above.

If you wish to uninstall LiveSwitch, you can do so as you would any other application, from the Programs and Features section of Windows.

Installing LiveSwitch on Linux

This section describes how to setup LiveSwitch on a Linux host. For information on Windows host setup, refer to the previous section, Installing LiveSwitch on Windows.

A typical LiveSwitch Linux install is composed of three services - the "gateway" service, the "media server" service and the "sip connector" service. These services are made available in a tarball, which you can access by downloading the official LiveSwitch distribution from our Downloads page. The compiled Linux services are available in the root of the distribution, in the liveswitch.tar.gz archive.

There is currently no Linux installer for LiveSwitch. You can install it by following the steps described below.

Install .NET Core

LiveSwitch for Linux is built on the .NET Core SDK. To run LiveSwitch on Linux, you must install .NET Core. Instructions for this step vary based on the distribution of your Linux host. Fortunately, Microsoft provides step-by-step instructions for common Linux distributions. To get instructions for your host, visit the following site: https://www.microsoft.com/net/learn/get-started/linux. Once there, select your distribution from the drop down box and follow the steps. Once .NET Core is installed, proceed to the next section.

Install Dependencies

LiveSwitch for linux requires libfontconfig1. To install libfontconfig1, use the following command:

apt-get update
apt-get install libfontconfig1

Install the LiveSwitch Services

As mentioned above, LiveSwitch is composed of three core services. The default LiveSwitch installation assumes that these services will exist in the /opt/liveswitch directory and that the services will run on systemd.  You can, of course, run the services in any way you choose, but this guide will focus on installing the three core services as systemd service units.

First, unzip the tarball to the /opt directory.

tar -xvzf liveswitch.tar.gz -C /opt

Afterwards, you should see three sub-folders, /opt/liveswitch/gateway/opt/liveswitch/media-server, and /opt/liveswitch/sip-connector. Inside each of these directories is a systemd service unit file specific to the each service. Respectively, they are gateway.servicemedia-server.service and sip-connector.service.

You can use these service units as-is, by symlinking them into the systemd service unit directory.

ln /opt/liveswitch/gateway/gateway.service /etc/systemd/system/gateway.service
ln /opt/liveswitch/media-server/media-server.service /etc/systemd/system/media-server.service
ln /opt/liveswitch/sip-connector/sip-connector.service /etc/systemd/system/sip-connector.service

Once the services have been symlinked, use systemctl to enable them and then reload the systemd daemon.

systemctl enable gateway
systemctl enable media-server
systemctl enable sip-connector
systemctl daemon-reload

If the services have not been started already, you can start them with systemctl. Make sure that you set your license key in the configuration file for each service before you start them.

systemctl start gateway
systemctl start media-server
systemctl start sip-connector

Configuring the Gateway

The LiveSwitch Gateway is a service that allows the various LiveSwitch components to communicate with each other and with your client applications.

Finding the Configuration File

To configure the LiveSwitch Gateway, navigate to the directory with the service executable file. By default, this is C:\Program Files\Frozen Mountain Software\LiveSwitch\Gateway. In this directory, look for a file named FM.LiveSwitch.Gateway.Service.config. You can edit this file to change the LiveSwitch Gateway settings.

The LiveSwitch Gateway configuration file has one top-level configuration section, gateway. This should be specified in the configuration file, as shown below:

<configuration>
  <configSections>
    <section name="gateway" type="FM.LiveSwitch.Gateway.Configurations.GatewayConfigurationSection, FM.LiveSwitch.Gateway" />
  </configSections>
</configuration>

The next sections of this document will describe the configuration options for this element.

Editing the Gateway Section

The gateway section provides configuration for the LiveSwitch Gateway that determines which components and clients are allowed to connect to it and which URIs that these components and clients can use to connect to it. There are several main configuration sections:

  • license sets the license key for the service
  • auth sets the authorization settings
  • redis configures a Redis back-end
  • web sets up HTTP and HTTPS bindings
  • admin sets the web bindings for the administrator UI

Each of these is explored in detail in the following sections.

Setting the License Key

You must specify a single license element. This element has a single key attribute. Its value should contain your LiveSwitch license key.

<gateway>
  <license key="fmeyJpZCI6IjZm...DAwMDAwMH0=" />
</gateway>

Modifying Authorization Settings

One of the main roles of the LiveSwitch Gateway is to authenticate users and other components of the LiveSwitch stack. The auth element specifies how this authorization is performed. At minimum, you must provide a shared secret for each of the three components that can connect to the gateway:

  • Clients: These are end-users that connect to the gateway.
  • Media Servers: These are server side components that manage SFU and MCU sessions.
  • SIP Connectors: These are server side components that handle SIP trunking.

Each of these has an XML element associated with it - clientsconnectors and mediaServers, respectively. To set a shared secret for one of these elements, specify the sharedSecret attribute on the element. The shared secret can be any arbitrary string, but a GUID is common, as shown below.

<gateway>
  <auth>
    <clients sharedSecret="00000000-0000-0000-0000-000000000000" />
    <connectors sharedSecret="11111111-1111-1111-1111-111111111111" />
    <mediaServers sharedSecret="22222222-2222-2222-2222-2222222222222" />
  </auth>
</gateway>

In the above configuration, you specified one shared secret for all clients. If your gateway only runs a single application, this is perfectly fine. If, however, you wish to have multiple applications running on a single LiveSwitch Gateway, you can create separate shared secrets for each application. To do this, instead of adding the sharedSecret attribute to the clients element, specify each application using the add element:

<gateway>
  <auth>
    <clients>
      <add applicationId="first-application" sharedSecret="00000000-0000-0000-0000-00000000000" />
      <add applicationId="second-application" sharedSecret="00000000-1111-1111-1111-00000000000" />
    </clients>
        
    <connectors sharedSecret="11111111-1111-1111-1111-111111111111" />
    <mediaServers sharedSecret="22222222-2222-2222-2222-2222222222222" />
  </auth>
</gateway>

Keep track of these shared secrets, you will need to specify them in the configuration files for your LiveSwitch Media Server and LiveSwitch SIP Connector. To see how to set these, make sure you read the Configuring the Media Server and Configuring the SIP Connector sections.

Setting Web Bindings

The web element defines the HTTP and HTTPS bindings for the LiveSwitch Gateway. The web element itself has a single required attribute, path. This attribute defines the path that LiveSwitch components and clients will use to connect to the gateway. The default path is "sync". If, for example, you had a LiveSwitch Gateway listening on localhost:8080, you would connect to it with the URL: http://localhost:8080/sync. The following snippet shows what this configuration looks like:

<gateway>
  <web path="sync">
  </web>
</gateway>

The previous configuration block alone is not enough to configure the LiveSwitch Gateway bindings. You must also specify which http and https endpoints that the LiveSwitch Gateway should bind to. This is done with the http and https elements. You can specify multiple bindings for each collection. The following configuration sets up a wildcard binding that listens for HTTP requests on port 8080.

<gateway>
  <web path="sync">
    <http>
      <binding host="*" port="8080" />
    </http>
  </web>
</gateway>

If you want the server to only accept requests for a specific host, you can specify the host in the host attribute. The following example only accepts requests made to http://liveswitch.frozenmountain.com:

<gateway>
  <web path="sync">
    <http>
      <binding host="liveswitch.frozenmountain.com" port="8080" />
    </http>
  </web>
</gateway>

Windows HTTPS Bindings

For HTTPS, you specify a binding element the same way you did for HTTP, except you nest it under an https element instead of an http element. On Windows, you must first install the certificate to the Personal certificate store and then specify the the certificate thumbprint in your HTTPS binding element. Use the certifcateHash attribute to specify the thumprint, which should be a SHA-1 hash of the entire certificate. The following example shows how to add an HTTPS binding on Windows.

<gateway>
  <web path="sync">
    <https>
      <binding host="*" port="8443" certificateHash="cb4442bca824b940dd9a17da3cab7250bb1b8ddd" />
    </https>
  </web>
</gateway>

You can combine both of these attributes and provide multiple HTTP and HTTPS bindings for your LiveSwitch Gateway, but make sure you specify a different host and port combination for each binding.

Certificates need to be installed to Personal -> Certificates on Local Computer. The installed certificate must be in .pfx format to contain both the public and private keys.


We have seen an issue where when you copy the thumbprint from the MMC, it may actually include an invisible zero-width space at the beginning and end. If you are getting certficate errors:

  1. Highlight the opening quote and first character of your hash and replace them both.
  2. Highlight the final character of your hash and the closing quote and replace them both.
  3. Restart the Gateway service.

Linux HTTPS Bindings

For HTTPS on Linux, instead of installing to a certificate store, you must instead provide the path to a .pfx file containing both the certificate and private key for the certificate. You can make a .pfx file from a certificate and a private key using openssl, like so:

openssl pkcs12 -export -out my.domain.pfx -inkey my.domain_certificate.pem -in my.domain_certificate.crt

Once you have a pfx file, add a binding element underneath your https element, and specify the certificatePath attribute. Provide the absolute path to your pfx file using this attribute, as shown below:

<gateway>
  <web path="sync">
    <https>
      <binding host="*" port="8443" certificatePath="/etc/ssl/my.domain.pfx" />
    </https>
  </web>
</gateway>

As with Windows, you can specify multiple HTTPS and HTTP bindings, so long as each one binds to a different port.

Configuring the Redis Backend

Redis is a high-performance, in-memory data store that is used as a cache and a message broker. It is commonly used as a backing store for applications that need to propagate data throughout a set of servers. LiveSwitch has full support for using a Redis backend. To configure this, specify a redis element which has one connectionString attribute. The simplest possible connection string that you can have is simply "localhost". The following snippet configures the Redis backend provider to use the Redis server running on localhost.

<gateway>
  <redis connectionString="localhost" />
</gateway>

Note that this is not required. By default, the LiveSwitch Gateway runs in-memory. If you do not have a Redis database up and running, it's perfectly okay to omit this for now. You can always switch over to it painlessly in the future.

More information on connection strings is available at: https://stackexchange.github.io/StackExchange.Redis/Configuration.

Redis for multiple environments

If you are running multiple environments (e.g. production and staging), you will need to run separate Redis backends for each.

Setting Administrator Bindings

The LiveSwitch Gateway also provides a UI for administrators. The UI lists the status of all components that are connected to the LiveSwitch Gateway and allows administrators to deactivate components that might be misbehaving. You configure this administration interface the same way that you configure regular web bindings. The following sample shows you how to configure the administrator UI to be accessible via HTTP on port 9090 or via HTTPS on port 9443.

<gateway>
  <admin path="admin">
    <http>
      <binding host="*" port="9090" />
    </http>
    <https>
      <binding host="*" port="9443" certificateHash="cb4442bca824b940dd9a17da3cab7250bb1b8ddd" />
    </https>    
  </admin>
</gateway>

As with regular web bindings, you can specify as many administrator bindings as you like.

Ensure your admin UI is secure

Access to your adminstrative route must be blocked by your firewall. Prevent unauthorized access to administrative functionality.

Restarting the Service

Whenever you modify the configuration for the LiveSwitch Gateway service, you must restart the service for the changes to be applied. Run the following commands in either the Windows Command Prompt or a PowerShell window with administrator access:

net stop "LiveSwitch Gateway"
net start "LiveSwitch Gateway"

Configuring the Media Server

The LiveSwitch Media Server is a service that manages media data for LiveSwitch media sessions. In a multipoint conference, the LiveSwitch Media Server performs any forwarding or multiplexing required by the session.

Finding the Configuration File

To configure the LiveSwitch Media Server, navigate to the directory with the service executable file. By default, this is C:\Program Files\Frozen Mountain Software\LiveSwitch\Media Server. In this directory, look for a file named FM.LiveSwitch.MediaServer.Service.config. You can edit this file to change the LiveSwitch Media Server settings.

The LiveSwitch Media Server configuration file has one top-level configuration section, mediaServer. This should be specified in the configuration file, as shown below:

<configuration>
  <configSections>
    <section name="mediaServer" type="FM.LiveSwitch.MediaServer.Configurations.MediaServerConfigurationSection, FM.LiveSwitch.MediaServer" />
  </configSections>
</configuration>

The next sections of this document will describe the configuration options for this element.

Editing the Media Server Section

The mediaServer section provides configuration for the LiveSwitch Media Server that determines how the relay server is configured and which codecs are available to clients. There are several main configuration sections:

  • log specifies the logging level and configuration

  • gateway specifies which LiveSwitch Gateway to connect to
  • connection sets the local network settings for connections from the media server
  • codecs specifies which codecs that the media server should use
  • mcu configures settings specific to MCU mode
  • sfu configures settings specific to SFU mode
  • recording enables or disables session recording

Each of these is explored in detail in the following sections.

Specifying the Gateway

You must also specify a single gateway element. This element defines how the LiveSwitch Media Server will connect to your LiveSwitch Gateway. The gateway element has two attributes that you must specify, gatewayUrl and sharedSecret. The gatewayUrl attribute should contain the scheme, domain and path to your LiveSwitch Gateway route. Refer to the section on Configuring the Gateway for more information on how to modify this value. The sharedSecret attribute should contain the shared secret that your gateway has specified for LiveSwitch Media Servers. This should be the same value that you have specified in your LiveSwitch Gateway configuration file. Again, refer to the section on Configuring the Gateway for information on configuring this.

<mediaServer>
  <gateway gatewayUrl="http://localhost:8080/sync" sharedSecret="22222222-2222-2222-2222-222222222222" />
</mediaServer>

Again, make sure the shared secret that you specify here matches what is specified in the mediaServers element in your LiveSwitch Gateway configuration file. If it does not, then the LiveSwitch Media Server will not be able to connect to the gateway.

Specifying Local Network Settings

The connection element lets you configure the public IP address of the media server as it will be visible to clients. This element has two possible attributes, stunAddress and publicIPAddress. If you know the public IP address of your LiveSwitch Media Server, you should specify it here using the publicIPAddress attribute, like so:

<mediaServer>
  <connection publicIPAddress="24.0.0.92" />
</mediaServer>

If you do not know the public IP address of your LiveSwitch Media Server, you can instead specify the address of a STUN server. When the LiveSwitch Media Server starts up, it will use this server to discover its own public IP address. When specifying the address of the STUN server, specify the domain and optionally the port (default is 3478). The following example shows the use of the stunAddress attribute.

<mediaServer>
  <connection stunAddress="stun.liveswitch.fm" />
</mediaServer>

Generally, using the stunAddress attribute is more robust, as it automatically handles changes to the public IP address. You should specify only one of these configuration options.

Note

Only specify a stunAddress or a publicIPAddress. If both are specified, connectivity may not be established.

Enabling Bandwidth Adaptation

In your media server configuration, set the bandwidthAdaptationPolicy attribute to enabled on the connection element:

<connection bandwidthAdaptationPolicy="enabled" ... />

Then, in your client code, set BandwidthAdaptationPolicy to Enabled on your VideoStream.

Bandwidth adaptation in browsers

Bandwidth adaptation is enabled by default in WebRTC-capable browser clients (Chrome, Firefox, Edge, and Safari).


Native bandwidth adaptation is experimental

Note that in the native platforms bandwidth adaptation is still experimental and is not recommended for AudioStreams at this time.

Modifying Codec Settings

The codecs element is a collection that contains information on which codecs are supported by the LiveSwitch Media Server. Each codec is represented by its own distinct element - ie: the h264 codec has an h264 element. If a codec element does not appear under this element, then the LiveSwitch Media Server will ignore requests to use it. The available codecs are:

  • VP8: Specified with the vp8 element. You can additionally set the bitrate with the bitrate attribute. This value is in kbps.
  • H264: Enabled with the h264 element. You can additionally set the bitrate with the bitrate attribute. This value is in kbps.
  • Opus: Enabled with the opus element. You can additionally set the bitrate with the bitrate attribute. This value is in kbps.
  • PCMU: Enabled with the pcmu element.
  • PCMA: Enabled with the pcma element.

Please note that bitrate here (in Kbps) is a downstream rate, which means that if you specify 1024, then this is the maximum rate that will be sent in the downstream. In the example below, h264 will sent with a max of 512 Kbps while vp8 will max out at 1024 Kbps.

The sample configuration below enables all available codecs:

<mediaServer>
  <codecs>
    <vp8 bitrate="1024" />
    <h264 bitrate="512" />
    <opus bitrate="32" />
    <pcmu />
    <pcma />
  </codecs>
</mediaServer>

Note that if you disable too many codecs, the LiveSwitch Media Server will be unable to accept connections.

Configuring MCU Sessions

The mcu element allows you to set configuration options for the LiveSwitch Media Server when it is running in MCU mode. Among other things, this allows you to define how you want the output video to be formatted by the multiplexer. To configure this, add a videoOutput element to your mcu element. With this element, you can set both the dimensions and the frame rate of the video that the MCU outputs. The XML snippet below shows how you can configure the MCU to output video at 1280x720 and at 30 frames per second.

<mcu>
  <videoOutput width="1280" height="720" frameRate="30" />
</mcu>

For video resolutions, you should try to stick to common resolutions, as not all combinations are valid for all codecs. For example, don't enter 147x933. You should also keep your frame rate to a reasonable value. Anything beyond 30 frames per second will require a powerful server to keep up in larger video conferences.

Another aspect that you can configure is the maximum bitrate for participant video while in an MCU session. This is useful to limit the amount of bandwidth that the MCU consumes. To do this, add a videoInput element or an audioInput element and set the maxParticipantBitrate attribute. The value of this attribute is in kbps. This is demonstrated below, where a maximum video bitrate of 1024kbps and a maximum bitrate of 32kbps is set.

Please note that bitrate here (in Kbps) is an upstream rate, which means that if you specify 1024, then this is what the Media Server will attempt to negotiate with participants.

<mcu>
  <videoInput maxParticipantBitrate="1024" />
  <audioInput maxParticipantBitrate="32"  />
</mcu>

Configuring SFU Sessions

Similar to the mcu element described above, the sfu element allows you to set configuration options for the LiveSwitch Media Server when it is running in SFU mode. You are not able to set the video or audio output, as the SFU does not actually perform any processing of audio and video streams - it merely forwards them. You can however, still set the maximum bitrate of the participants' input streams. To configure this, again, you use two elements, videoInput and audioInput. For both, set the maxParticipantBitrate attribute. The sample below demonstrates how to set a maximum video bitrate of 1024kbps and a maximum audio bitrate of 32kbps.

<sfu>
  <videoInput maxParticipantBitrate="1024" />
  <audioInput maxParticipantBitrate="32"  />
</sfu>

Enabling Recording

The recording element controls whether or not the LiveSwitch Media Server records any sessions. The recording element has two attributes, which must both be specified to enable recording. To enable recording, the enabled attribute must be set to "true". You must then specify the path attribute, which must be a path to an existing directory. This is demonstrated below:

<mediaServer>
  <recording enabled="true" path="C:\Temp" />
</mediaServer>

Starting with your specified path, recordings are written to path > appId > channelId > userId > deviceId > clientId > connectionId > audio.mkv or path > appId > channelId > userId > deviceId > clientId > connectionId > video.mkv

Note that in a peer to peer session, no recording is performed. This is because in this type of session, no users are connected to the LiveSwitch Media Server itself. If you wish to record a session, you must ensure that it is an SFU or MCU session.

Configuring per-Application and per-Channel Settings

The above configuration options modify the settings for the entire LiveSwitch Media Server. This is useful as a default but you may wish to have settings that differ for each application. For example, if you are running a service with a free tier, you may wish to reserve larger amounts of bandwidth for your paying customers. LiveSwitch accommodates this by allowing you to modify individual configuration options at either the application level or the channel level.

The following configuration options demonstrate how to specify a low resolution MCU output stream for one application and a high resolution MCU stream for another.

<mediaServer>
  <application id="my-free-application">
    <mcu>
      <videoOutput width="640" height="360" frameRate="15" />
    </mcu>
  </application>
  <application id="my-paid-application">
    <mcu>
      <videoOutput width="1280" height="720" frameRate="30" />
    </mcu>
  </application>
</mediaServer>

You can also modify the settings on a per-channel level. The following XML sample disables recording by default but enables it for a single channel.

<mediaServer>
  <application id="my-application">
    <recording enabled="false" />
    <channel id="recording-enabled-channel">
      <recording enabled="true" />
    </channel>
  </application>
</mediaServer>

You can modify any setting under the connectioncodecsmcusfu and recording configuration elements. You cannot specify application-specific settings for the gateway configuration element.

Restarting the Service

Whenever you modify the configuration for the LiveSwitch Media Server service, you must restart the service for the changes to be applied. Run the following commands in either the Windows Command Prompt or a PowerShell window with administrator access:

net stop "LiveSwitch Media Server"
net start "LiveSwitch Media Server"

Configuring the SIP Connector

The LiveSwitch SIP connector is a service that provides SIP trunking, which allows your application to add SIP endpoints to your media sessions.

Finding the Configuration File

To configure the LiveSwitch SIP Connector, navigate to the directory with the service executable file. By default, this is C:\Program Files\Frozen Mountain Software\LiveSwitch\SIP Connector. In this directory, look for a file named FM.LiveSwitch.Connector.Service.exe.config. You can edit this file to change the Connector settings.

The LiveSwitch SIP Connector configuration file has two configuration sections, sip and connector. These should be specified in the configuration file, as shown below:

<configuration>
  <configSections>
    <section name="sip" type="FM.LiveSwitch.SipConnector.Configurations.SipConfigurationSection, FM.LiveSwitch.SipConnector" />
    <section name="connector" type="FM.LiveSwitch.SipConnector.Configurations.ConnectorConfigurationSection, FM.LiveSwitch.SipConnector" />
  </configSections>
</configuration>

The next sections of this document will describe the configuration options for these elements.

Editing the Connector Section

The connector section provides configuration for the LiveSwitch SIP Connector that allows it to communicate with the LiveSwitch Gateway. A typical connector section looks like the following sample:

<connector gatewayUrl="http://localhost/sync" sharedSecret="11111111-1111-1111-1111-111111111111">
</connector>

On the connector element, there are two attributes that you must specify, gatewayUrl and sharedSecret. The gatewayUrl attribute should contain the scheme, domain and path to your LiveSwitch Gateway route. Refer to the section on Configuring the Gateway for more information on how to modify this value. The sharedSecret attribute should contain the shared secret that your gateway has specified for SIP Connectors. This should be the same value that you have specified in your LiveSwitch Gateway configuration file. Again, refer to the section on Configuring the Gateway for information on configuring this.

Editing the SIP Section

The sip element provides additional configuration options that control how SIP calls are handled. The sip element itself has one required attribute, serverPort. Unless you have a specific reason to alter the port, you should leave it as 5060, which is the default port for SIP. Modifying this can cause confusion, as many applications will expect the port to be 5060.

<sip serverPort="5060">
</sip>

There are several additional configuration sections under the sip element:

  • registration sets the SIP registration information
  • dialPlan configures how the SIP connector interprets outbound toUser values entered by clients

Each of these is explored in detail in the following sections.

SIP Connectors require that port 5060 is forwarded if they are behind a firewall.

Registering with the SIP Server

You must have an account with a SIP provider to use the LiveSwitch SIP Connector. There are several providers that provide free trials, which you can use to get started. Once you have a provider, you can use the registration element to tell the LiveSwitch SIP Connector how to register with them. The registration element has three attributes, domainusername and password. The values for these will be provided by your SIP provider. A sample configuration is demonstrated below:

<sip>
  <registration domain="sip.flowroute.com" username="15934744" password="ABzz4455ab3gd" />
</sip>

Again, you must find your own SIP provider and update these settings to the correct values.


Auth Id

Some SIP providers use an auth id in addition to the username and password. If provided, this is just another attribute in the registration xml element.


<registration domain="sip.flowroute.com" username="15934744" authId="yourauthid" password="ABzz4455ab3gd" />

Creating a Dial Plan

A SIP dial plan defines how a SIP call is routed when a client attempts to make a call to a specific user. These settings are contained in the dialPlan element. The dialPlan element has a child mappings element, which is a collection of mappings that determine how SIP calls are routed. Each element that you add must have an applicationId, a channelId and a toUser attribute. If a SIP call is made from a channel matching the channelId attribute and an application matching the applicationId attribute, then the call will be mapped to the user specified in the toUser attribute.

Uniqueness of a mapping

Mappings must be unique according to the toUser field. You should not add more than one mapping per user.


<sip serverPort="5060">
  <dialPlan>
    <mappings>
      <add toUser="john@doe.com" channelId="1585" applicationId="my-other-app-id" />
      <add toUser="16044248331" channelId="3337" applicationId="my-app-id" />
    </mappings>
  </dialPlan>
</sip>

You can also specify a webHookUrl attribute in your dialPlan element. This specifies an HTTP or HTTPS endpoint that will be hit when a call does not match any of the dial plan's mappings. The next snippet shows how to specify this:

<sip serverPort="5060">
  <dialPlan webHookUrl="http://localhost:59763/sip" />
</sip>

The next section describes how to configure this web hook to receive and respond to SIP requests:

Setting up the Web Hook

The SIP connector routes SIP requests that do not match any mappings in the dial plan to the web hook URL that you specify in your configuration, as mentioned above. You must now set up this web hook endpoint to receive and respond these requests. When this endpoint is hit, several query parameters will be available:

  • toUser: the user or phone number that is being called
  • fromUser the user or phone number that initiated the call
  • toHost: the host that is being called
  • fromHost: the host that initiated the call

The HTTP request will be delivered as a POST request, with a Content-Type of application/json. The body of the request itself will contain a JSON object with information on the original SIP INVITE request. This object will contain two top-level keys, headers and sdp.

The headers key is a nested JSON object, where each key represents a header from the INVITE request, and with each value representing the corresponding value of the SIP header. The sdp key will contain the SDP message from the INVITE request. A request will look similar to the following example:

{
  "headers": {
    "From": "\"Frozen Mountain\" <sip:frozenmountain@10.0.0.0>",
    "Content-Type": "application/sdp",
    ...
  },
  "sdp": "v=0\r\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com..."
}

Using the query parameters and the SIP INVITE request, your web hook must issue an HTTP response of type application/json, containing the following parameters:

  • applicationId: the application id to map the request to
  • channelId: the channel id to map the request to
  • userId: the user id to map the request to (optional)

Each of these parameters should be a top-level key of a JSON object. An example JSON response will look similar to the following:

{
  "applicationId": "my-app",
  "channelId": "581233",
  "userId": "john@doe.com"
}

Restarting the Service

Whenever you modify the configuration for the Connector service, you must restart the service for the changes to be applied. Use the following commands in either the Windows Command Prompt or a PowerShell window with administrator access:

net stop "LiveSwitch SipConnector"
net start "LiveSwitch SipConnector"

Configuring Logging

LiveSwitch provides a rich logging framework that supports many different capabilities. You can configure LiveSwitch logging for the Media Server, the Gateway, or the SIP connector with the <log/> element in the config of the given service:

<log level="info">
  <file/>
  <!-- defaults to ProgramData -->
  <event/>
  <syslog/>
  <!-- defaults to 127.0.0.1 -->
  <cloudwatch />
</log>
  • <file/> configures file system logging. You can configure multiple <file/> elements. You can change the default logging location by providing the path attribute to override where the log is written to. The defaults are: %ProgramData%\Frozen Mountain Software\-service-\ for Windows, and /var/log/fm/liveswitch/-service-/ for Linux.
  • <syslog/> configures system message logging. You can configure multiple <syslog/> elements. <syslog/> has a target attribute which is the IP where the syslog is sent. The default is 127.0.0.1
  • <event/> turns on Windows event logging. There is no additional configuration required. If this element is present then event logging is turned on.
  • <cloudwatch /> turns on AWS CloudWatch logging. See the Configuring the CloudWatch Log Provider section below for more information.

Levels

Logging statements have an associated level of severity. Possible log levels are: verbose, debuginfowarnerrorfatal, and none. The severity level indicates the importance of the message. The levels are, from most severe to least severe:

  • FATAL: The message indicates a fatal application error; the application will need to shut down and no recovery actions are possible.
  • ERROR: The message indicates a normal application error (ie: timeouts, bad user input); this is a normal error state and usually it is possible to proceed.
  • WARN: The message indicates that something abnormal occurred but that the application was not negatively affected; this is used for unexpected values, deprecations or abnormal application state.
  • INFO: This is a trace message; it describes what is going on in the SDK.
  • DEBUG: This is a diagnostic message; it is similar to the INFO level but is used for outputting detailed parameters or for "spammy" messages.
  • VERBOSE: This is a diagnostic message; it is similar to the DEBUG level but is used for outputting extremely low level detailed tracing messages. It is generally not recommended to configure verbose logging.

The level attribute allows you to set the level of logs you want displayed. By default, only INFO and more severe messages are displayed. In production, you may want to display no message, and in development, you may want to display all diagnostic messages. This is accomplished by setting the level attribute. Note that there is one additional level here, NONE. You can set this if you want to suppress all log messages. A level can be configured for the <log> element, and/or any of its child elements. A child element's level overrides the level configured for the <log> element.

Configuring the CloudWatch Log Provider

The CloudWatchLogProvider is a new log provider released with LiveSwitch 1.1.4. This provider dumps your logs directly into a specific AWS CloudWatch log group.

To enable it, add a new <cloudwatch /> element into the <logs></logs> element on any of the LiveSwitch services.

<log level="info">
  ...
  <cloudwatch region="us-west-2" group="myCloudWatchGroup" accessKey="..." secretKey="..." profileName="..." profilePath="..." />
</log>

Required attributes are:

  • region: The CloudWatch region to send the logs.
  • logGroup: The CloudWatch log group to send the logs to.

Basic access configuration requires two attributes:

  • accessKey: Your AWS accesskey.
  • secretKey: Your AWS secret key.

Alternatively you can configure authentication via profile:

  • profileName (required): The profile name in which to try and load from an AWS credential file.
  • profilePath (optional): The path to a AWS credential file. If missing, the default path is used.

Should you choose not to supply any of accessKeysecretKey, or the profile attributes, then AWS auto-detection is turned on if the service is running on EC2 or ECS. For ECS, the task execution role is used, so you must ensure it has CloudWatch permissions. For EC2, the instance profile role is used if available.

Production Environments

Best Practise: For production environments it is highly recommended to set your logging level to INFO or higher to minimize performance issues.

Creating an Auth Server

It is not advisable to create registration tokens in your client applications. This section will go into further detail about why this is a bad idea and will show you how to securely generate authorization tokens.

Why Not Generate Tokens Client Side?

To understand why you should not generate these tokens on the client, first consider how the LiveSwitch Gateway validates an authorization request. A authorization request is valid if the token parameters are valid values and the secret key in the token matches the secret key configured on the LiveSwitch Gateway. This means that if the client knows the secret key, that almost any request they make will be valid - there is no way for your application to stop a client from registering or joining a channel.

What this means is that the security of your application depends on keeping the secret key a secret. You can take this concept further by considering that for each token that you want to generate there are two types of data: things the client knows, and things the server knows. It's not always as black and white as that, but for our purposes here it's a useful distinction. Let's apply this concept to the parameters that are required to generate authorization tokens.

Application ID: As a unique identifier for the application, this value will come from the server. It can be hard-coded or, more commonly, the id may depend on a user's profile. Applications are associated with specific feature sets, so you do not want to allow the user to specify this as it can allow them to access features that they are not entitled to.

User ID: This value will also usually come from the server. Before a server issues an authorization token, it generally requires some form of authentication from the user. Because of this, the server knows who the user is already. You do not want to allow a client to specify this value, both because the server already knows what the value should be and because it may allow a client to impersonate another user.

Device ID: This value will generally come from the client. The device id is a unique identifier associated with a particular device. This is used in the case where a user might be connected on multiple devices, ie: through a web browser and a phone.

Client ID: This value will always come from the client. The client id is unique identifier associated with a specific LiveSwitch instance, so it must be provided by the client.

Roles: Roles will generally come from a combination of both the client or the server. In the case where an application role is associated with a particular application privilege (ie: moderation abilities), then the role will be assigned based by the server based on the user's profile. In the case where a role is not associated with any privileged action, (ie: a presenter in a conference), then client should request this role. Of course, the server should validate all client roles based on their user profile.

Channel Claims: Channel claims usually come from the client, as they represent a claim that a client makes, ie: that they have the right to join the specified channel. The server may also add other channels as required, if there are default channels or specific meta data channels associated with an application.

Shared Secret: This arbitrary string will always come from the server, as mentioned above.

Now that you have a better understanding of the concept of client information and server information, the next step is to set up your server.

Setting Things Up

To set up your auth server, you will need to set up a few things first. Generally, you will already have some sort of server to handle application-specific data such as user profiles. This is a good starting point for your auth server, as the application server will already have access to your user data, which you will need to authorize your client requests.

Generally, you will need to set up at least two additional endpoints on your server - one for generating registration tokens and one for generating channel tokens. This guide will assume a rest-like API, and will refer to these endpoints as "/token/register" and "/token/join"; meaning to register with the server, and to join a channel. In the following sections, some examples of server endpoints are provided using ASP .NET Core for C# and Spring 4 for Java.

Handling Registration Tokens

One of the main roles of an auth server is to authenticate clients and to return registration tokens so that these clients can connect to the LiveSwitch service. Registration tokens are typically generated from a "/token/register" endpoint on a REST-ful web service. In a normal registration request, the application id, user id and shared secret are provided by the server. The only remaining required parameters are the device id and the client id (because roles and channel claims are optional for a registration).

The following code snippet shows platform-specific examples of retrieving device and client ids and then making an http request to the server for a registration token. The device id and client id are provided via query parameters in a POST request.

var applicationId = "...";
var userId = "...";
var deviceId = Environment.MachineName;

var client = new FM.LiveSwitch.Client("https://liveswitch.server:8443/sync", applicationId, userId, deviceId);

var url = "https://app.server/token/register"
    + "?deviceId=" + client.DeviceId
    + "&clientId=" + client.Id;

var httpClient = new HttpClient();
var response = await httpClient.PostAsync(url);
var token = response.Content;
client.Register(token).Then(...);
String applicationId = "...";
String userId = "...";
String deviceId = Secure.getString(getContext().getContentResolver(), Secure.ANDROID_ID);

fm.liveswitch.Client client = new fm.liveswitch.Client("https://liveswitch.server:8443/sync", applicationId, userId, deviceId);

URL url = new URL("https://app.server/token/register"
    + "?deviceId=" + client.getDeviceId()
    + "&clientId=" + client.getId());
    
InputStream inputStream = url.openStream();
BufferedReader buffer = new BufferedReader(new InputStreamReader(input));
String token = buffer.lines.collect(Collectors.joining("\n"));
client.register(token).then(...);
var applicationId = '...';
var userId = '...';
var deviceId = navigator.userAgent;

var client = new fm.liveswitch.Client("https://liveswitch.server:8443/sync", applicationId, userId, deviceId);

var url = 'https://app.server/token/register'
    + '?deviceId=' + client.getDeviceId()
    + '&clientId=' + client.getId();

var xhr = new XMLHttpRequest();
xhr.open('POST', url, true)
xhr.onreadystatechange = function() {
    if (xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) {
        var token = xhr.response;
        client.register(token).then(...);
    }
}
xhr.send(null);

You will have to configure your application server to receive this request. The application server should then respond by generating a client registration token. You can generate this token using the values the client provides, and values available to the server. Pass all these values to the GenerateClientRegisterToken static method of the FM.LiveSwitch.Token class to return a token. This is demonstrated below.

[Route("[controller]")]
public class TokenController : Controller
{
    private String secret;
    private UserManager<AppUser> userManager;
    
    public TokenController(UserManager<AppUser> userManager)
    {
        this.userManager = userManager;
    }

    [HttpPost("register")]
    public ContentResult Register([FromQuery]string deviceId, [FromQuery]string clientId)
    {
        var user = await this.userManager.GetUserAsync(this.User);
        
        return Content(FM.LiveSwitch.Token.GenerateClientRegisterToken(
            user.ApplicationId,
            user.Id,
            deviceId,
            clientId,
            null,
            null
            this.secret
        ));
    }
}
@Controller
@RequestMapping("/token")
public class TokenController {
    private String secret;

    @PostMapping
    @ResponseBody
    public String register(
            @RequestParam(value="deviceId", required=true) String deviceId,
            @RequestParam(value="clientId", required=true) String clientId) {
        UserDetails auth = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        
        applicationId = auth.getApplicationName();
        userId = auth.getUsername();
        
        return fm.liveswitch.Token.generateClientRegisterToken(
            applicationId,
            userId,
            deviceId,
            clientId,
            null,
            null,
            this.secret
        );
    }
}

Note that the details of how these controllers' shared secret property is provided is left up to you. A common way of doing this is through your dependency injection container. Another way of accessing it is to query your global application settings. Whatever way you select, it should not be accessible to any clients.

Handling Channel Tokens

The other main role of an auth server is to return channel tokens so that users who have already registered can join additional channels without needing to re-register. Channel tokens are usually generated from a "/token/join" endpoint on a REST-ful web service.

For a join token, the client must again provide its deviceId and clientId. It must also provide the id of a channel it wishes to join. The code samples below show how to make a REST-ful request with this information embedded in the query string.

var applicationId = "...";
var userId = "...";
var channelId = "...";'
var deviceId = Environment.MachineName;

var client = new FM.LiveSwitch.Client("https://liveswitch.server:8443/sync", applicationId, userId, deviceId);

var url = "https://app.server/token/join"
    + "?deviceId=" + client.DeviceId
    + "&clientId=" + client.Id
    + "&channelId=" + channelId;

var httpClient = new HttpClient();
var response = await httpClient.PostAsync(url);
string token = response.Content;
client.Join(channelId, token).Then(...);
String applicationId = "...";
String userId = "...";
String channelid = "...";
String deviceId = Secure.getString(getContext().getContentResolver(), Secure.ANDROID_ID);

Client client = new fm.liveswitch.Client("https://liveswitch.server:8443/sync", applicationId, userId, deviceId);

URL url = new URL("https://app.server/token/join"
    + "?deviceId=" + client.getDeviceId()
    + "&clientId=" + client.getId())
    + "&channelId=" + channelId;
    
InputStream inputStream = url.openStream();
BufferedReader buffer = new BufferedReader(new InputStreamReader(input));
String token = buffer.lines.collect(Collectors.joining("\n"));
client.join(channelId, token).then(...);
var applicationId = '...';
var userId = '...';
var channelId = '...';
var deviceId = navigator.userAgent;

var client = new fm.liveswitch.Client("https://liveswitch.server:8443/sync", applicationId, userId, deviceId);

var url = 'https://app.server/token/join'
    + '?deviceId=' + client.getDeviceId()
    + '&clientId=' + client.getId()
    + "&channelId=" + channelId;

var xhr = new XMLHttpRequest();
xhr.open('POST', url, true)
xhr.onreadystatechange = function() {
    if (xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) {
        var token = xhr.response;
        client.join(channelId, token);
    }
}
xhr.send(null);

Again, you will have to configure a "/token/join" endpoint to receive this request. The endpoint should respond with a join token that allows the client to join their requested channel. You generate this token using the GenerateClientJoinToken static method of the FM.LiveSwitch.Token class. Note that you must wrap the channel id provided by the client in an instance of FM.LiveSwitch.ChannelClaim.

[Route("[controller]")]
public class TokenController : Controller
{
    private String secret;
    private UserManager<AppUser> userManager;
    
    public TokenController(UserManager<AppUser> userManager)
    {
        this.userManager = userManager;
    }

    [HttpPost("join")]
    public ContentResult Join([FromQuery]string deviceId, [FromQuery]string clientId, [FromQuery]string channelId)
    {
        var user = await this.userManager.GetUserAsync(this.User);
        
        return Content(FM.LiveSwitch.Token.GenerateClientJoinToken(
            user.ApplicationId,
            user.Id,
            deviceId,
            clientId,
            new FM.LiveSwitch.ChannelClaim(channelId),
            this.secret
        ));
    }
}
@Controller
@RequestMapping("/token")
public class TokenController {
    @Value("${secret}")
    private String secret;

    @PostMapping
    @ResponseBody
    public String join(
            @RequestParam(value="deviceId") String deviceId,
            @RequestParam(value="clientId") String clientId,
            @RequestParam(value="channelId") String channelId) {
        UserDetails auth = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        
        applicationId = auth.getApplicationName();
        userId = auth.getUsername();
        
        return fm.liveswitch.Token.generateClientJoinToken(
            applicationId,
            userId,
            deviceId,
            clientId,
            new fm.liveswitch.ChannelClaim(channelId),
            this.secret
        );
    }
}

Upgrading your Server

LiveSwitch is a core product at Frozen Mountain Software and we are constantly making improvements to it. Because of this, it's important to keep your LiveSwitch server upgraded, so that you can take advantage of the latest features. We make this process as easy as possible. To upgrade to the latest version quickly and painlessly, simply download the latest LiveSwitch SDK version and run the installer. Your pre-existing settings will be saved, so you will not have to re-configure your servers.

Which Component Do I Upgrade First?

If your gateway and media server are on the same host, then you don't need to worry about this. The installer will update them both at the same time. If your services are on different hosts, then it is only a bit more complicated. The golden rule is to always upgrade your gateway service first. The gateway service is backwards compatible with older versions of LiveSwitch. By upgrading the gateway first, you ensure that there are no interruptions for any clients that are using older versions of LiveSwitch. If you upgrade your client applications or media servers before your gateway, then you run the risk of the gateway rejecting your connections because it has not been upgraded yet. The gateway service will never accept connections from newer versions of LiveSwitch clients, because it cannot guarantee that it supports the same feature set expected by the client.

How Much Downtime Can I Expect?

If all of your services are on one host, the downtime will be minimal. The installer will terminate each service, and restart it immediately after the upgrade. If your services are on separate hosts, the upgrade should be painless as well, provided you follow the upgrade order described above. There will be a brief interruption as each service is restarted, and the service will be available again afterwards. The next question addresses scenarios where seamless upgrading is required.

How do I Gracefully Terminate Services In Use?

To ensure minimal downtime for your users and also that your upgrade process does not interrupt their use of your service, you must have at least two gateway servers and at least two media servers. With two media servers, you can upgrade each one individually, ensuring that there are no service interruptions. Through the gateway administration panel you can tell a media server to "deactivate" itself. This is a slow shutdown - the media server will not accept any new connections during this time. Once you have determined that it is safe to do so (no connections remaining) it is up to your Dev-ops team to shut down the media server. By design media servers will not shut down automatically. Once the media server has been shut down you can upgrade it without interrupting any users' session. You can do this for each media server in turn, until all are upgraded.

For a seamless upgrade of gateway services, you must ensure that you have at least two gateway services and that they are both are configured to use a redis backend, rather than the default in-memory backend. The redis backend ensures that existing clients' connection information is propagated to all gateway servers. The extra gateway service ensures that when a gateway service goes down, your clients will seamlessly reconnect to the next available gateway service.

Are There Any Plans to Improve the Upgrade Process

Yes! We love automating things, and we realize that having to manually "drain" each media server before an upgrade can take unnecessary time out of a busy developer's day. In the future, we plan to make this the default upgrade behavior.

Upgrading Linux

The LiveSwitch Linux distribution does not have an installation script, so upgrading requires slightly more manual intervention. Follow the same basic guidelines for a Windows upgrade. However, instead of running the installer, stop each service using systemctl, copy the new files to the LiveSwitch service's directory, and then restart the service.

In the future we will provide both rpm and deb packages for easy installation.

Working with Pipes & Sinks

Pipes and Sinks provide a WebHook mechanism for the Gateway. They allow you to configure URLs (your WebHook callback URL) to handle messages that arrive at your Gateway sent from your clients.

Configure in your Gateway config:

Gateway Configuration
<message>
    <sinks>
        <web url="..." types="message" />
    </sinks>
    <pipes>
        <web url="..." types="message" />
    </pipes>
</message>


  • Sinks are fire and forget, whereas pipes are synchronous.
  • types is a comma separated list of the LiveSwitch message types that will be sent to the hooks. See the MessageType class for possible options. 
  • The '*' type is considered the global and runs first for all messages.
  • Order of operations: Global Sinks, Global pipes, Specific sinks, specific pipes.


Sinks and pipes send the same data to the configured URL (POST), the sink version just doesn't care about the response. The body is a JSON-ified LiveSwitch message in its entirety.

Pipes do different things depending on the response value. If returned 204, the message moves on unmodified. If it returned a 403, the message is considered blocked and an AccessMessageBlocked error is returned to the client. If the response code is 200, then the response body is parsed into a new LiveSwitch message and replaces the old one. If it cannot be parsed, the old message is used.

Currently, the Pipes and Sinks feature is possible for handling messages that originate on the client. It is not possible to use Pipes/Sinks to handle messages that originate from the Gateway at this time, but there is a feature request that you can vote on to show your interest. Please vote here.

Regionality

As of version 1.1.0, LiveSwitch now supports regionality. Regionality is a feature that improves clustering performance by allowing each client to select their preferred region. A region is an identifier that represents a specific geographic area. This allows each client to select a geographic area close to them, which ensures fast connection times and high throughput.

What regions are available?

The region identifiers are specific to your application. If your application has many users that are concentrated in a few areas, you will likely have regions with small geographic areas, such as "chicago" or "new-york". If, on the other hand, you have a global user base, you will instead want to use  regions that represent larger geographic areas, such as "north-america" or "europe".

Where do I set the list of regions?

There is no canonical set of regions. Each media server can select its own region, and the set of available regions is taken from each media server that is connected to the current cluster.

How does region assignment work?

When a client requests a specific region, the gateway service will attempt to assign it to a media server in the same region. If there are no available media servers in the requested region, or all of the media servers in the requested region are over capacity, the gateway will ignore the region and assign the client to an available media server on a round robin basis. 

What this means is that a client's selected region is only a preference and that clients requesting a specific region can potentially be assigned to any media server in the global pool if no media servers for their region are available.

Setting up Regions

There are two steps to setting up regions. First, you must assign a region to each media server that will use the regionality feature. Second, you must add an extra parameter to your logic that generates the client registration token.

Assigning a region to a media server

To assign a region to a media server, set the region property of the mediaServer element in your FM.LiveSwitch.MediaServer.Service.config file. After updating this property, you must restart the media server service for any change to take effect. As mentioned above, there is no canonical set of available regions. The set of available regions is always determined by whichever media servers are connected to the current cluster.

An example of assigning the region north-america to a media server is shown below.

FM.LiveSwitch.MediaServer.Service.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <mediaServer region="north-america">
    ...
  </mediaServer>
</configuration>

Updating the client registration token

Once your pool of media servers have been assigned regions, you must update the client registration logic so that each client requests a specific region. You can review the client documentation for your specific platform to see how to generate tokens and join channels, but to request a specific region you simply specify the optional regionId parameter when you create the client registration token. This parameter comes after the sharedSecret parameter, and indicates the region that the client would like to be assigned to. An example of this is below:

You should never generate the token client side in production. We're doing so for demo purposes ONLY. Refer to the section on Creating an Auth Server for more information.


var client = new FM.LiveSwitch.Client("http://localhost:8080/sync", applicationId, userId, deviceId);

string token = FM.LiveSwitch.Token.GenerateClientRegisterToken(
    "my-application-id",
    client.UserId,
    client.DeviceId,
    client.Id,
    null,
    new[] { new FM.LiveSwitch.ChannelClaim(channelId) },
    "--replaceThisWithYourOwnSharedSecret--",
    "north-america"
);

TURN in the Media Server

Embedded TURN

LiveSwitch now integrates TURN server functionality directly into your Media Server. No longer do you have to maintain separate server infrastructure for your TURN and secure TURN (TURNS) servers. Now, you simply configure <turn> bindings in your Media Server, and the rest takes care of itself.

As an added bonus, this improves the overall efficiency of relay connections, as the latency between the TURN server and the Media Server disappears, and thanks to Regionality, you can easily ensure that your clients are connecting to Media Servers that are nearby.

Configuring TURN Bindings

Media Servers now take a configuration option to specify a TURN binding. Add the following configuration to your <mediaserver> node, and specify:

  • ipAddress - this IP address is what will be bound to locally. It's here in case you have multiple network interfaces and want to dedicate one to external/TURN traffic. A wildcard "*" will bind to all available local network addresses.

  • port - traditionally 3478.

  • Either the publicIPAddress of the Media Server, or in the case that you do not know this you can provide an externalStunAddress to be used to discover the public IP of the Media Server.

  • transport

<turn>
   <binding
      ipAddress="{*|(string)}"
      port="{number}" /* 3478 */
      externalStunAddress="{(empty)|(string)}"
      publicIPAddress="{(empty)|(string)}"
      transport="{udp|tcp}" />
   ...
 </turn>

You can add as many bindings as necessary. For instance you could add a binding for UDP on port 3478 to handle both STUN traffic and TURN traffic over UDP (recall that TURN servers are STUN servers by definition), and you could add a second binding for TCP on port 3478 to handle TURN traffic over TCP if necessary.

But what about secure TURN you ask? Read on to learn how to configure TURNS for your Media Server as well.

Configuring TURNS Bindings

Similarly to the <turn> configuration discussed above, you can now provide a <turns> configuration for your Media Server so that all of your TURNS needs are provided by the Media Server itself, with no reliance on an outside service.

The <turn> and <turns> bindings share some common attributes: ipAddressportpublicIPAddress, and externalStunAddress. Traditionally you would use port 5349 for TURNS. In addition to these shared attributes you must provide your certificate hash and the path to your certificate. This is necessary for securing your relay connection.

Notice also that unlike the <turn> binding you do not need to provide the transport attribute for your <turns> binding. This is because TURNS uses TCP by definition.

<turns>
    <binding
       ipAddress="{*|(string)}"
       port="{number}" /* 5349 */
       externalStunAddress="{(empty)|(string)}"
       publicIPAddress="{(empty)|(string)}"
       publicHostname="(string)"
       certificateHash="(empty)|(string)"
       certificatePath="(empty)|(string)" />
    ...
  </turns>


The final piece of the <turns> binding to understand is the public host name. This is necessary for TURNS, because all traffic to the server must include this host name, which is then compared to the host name that is part of your certificate. Only when they match are TURNS connections possible.

This presents an interesting Operations problem: as you spin up Media Servers to accommodate increasing load, you must also have a mechanism that creates the host name for the embedded TURNS server this new Media Server will use. Your Ops process will have to create the necessary DNS record, and also supply the new host name as part of the TURNS configuration of the new Media Server. Conversely, as you tear down Media Servers you can clean up the DNS records for the TURNS domain names that are no longer required.

How Does it all Work?

No doubt you are well aware that it is typically clients that supply ICE servers, and provide these to a connection, which in turn gathers ICE candidates, etc. So, how is this all working? Obviously the Media Server can make use of its own embedded TURN functionality, but how are clients gaining access when they have not yet opened a connection to the Media Server?

The simple answer is the Gateway. When your Media Server registers with the Gateway, it lets the Gateway know about its TURN configuration. Clients in turn get their ICE server credentials from the Gateway before opening a connection provided (a) you have not already supplied ICE servers to the pending connection, and (b) the connection has the DisableAutomaticIceServers property set to false.

Embedded TURN and P2P Connections

Ok, you’re thinking, this all makes a lot of sense for SFU/MCU connections, but what about my P2P connections, they do not connect with the Media Server. How are P2P connections getting access to the ICE servers they require?

It’s true that P2P connections do not even touch the Media Server, but they do register with the Gateway, and just like SFU/MCU connections, will query the Gateway for ICE servers before opening a connection. A very nice side effect here is that even P2P connections now benefit from your Regionality configuration. If you have configured Regionality properly, then clients will in turn use the region appropriate Media Server for relay (where relay is necessary), which can in turn mitigate latency.

High Availability Architecture

Scaling LiveSwitch

LiveSwitch is built to be highly scalable, both horizontally and vertically. This section explains how to configure these features.

Clustering LiveSwitch

LiveSwitch supports media server clustering, which allows multiple media servers to work together to handle higher traffic volumes than they would be able to alone. Clustering is enabled by default, and all media servers that are connected to a gateway or pool of gateways will automatically attempt to cluster. This default configuration may not be optimal for your application. To get the most out of this feature, read through this brief guide to configuring the clustering feature.

Enabling Cluster Thresholds

Thresholds are configured within the <cluster/> element. For convenience we have provided the <enabled/> element which enables/disables all thresholds for the cluster.

<mediaServer>
  ...
  <cluster>
	<enabled>true</enabled>
	...
  </cluster>
  ...
</mediaServer>

Setting Thresholds

Setting the CPU Threshold

The CPU threshold of a media server is the threshold at which a media server is considered "over capacity". Once a media server is over capacity, the gateway service will attempt to assign clients to a different media server instance. The CPU threshold value is expressed as a percentage of CPU use.  By default this is 80, which means that when a media server begins to use 80% or more of its CPU, then all gateways will stop forwarding it new connections. When the media server drops below the CPU threshold, gateway instances will again start to forward connections to the media server, in a round robin fashion.

it will no longer accept new connections until either its CPU usage drops below 80%, or there are no other media server instances available.

To set the CPU threshold for a media server, add a cpuThreshold element to the cluster element, and use any value between 0 or 100. The example below sets the CPU threshold of a media server to 60%.

<mediaServer>
  ...
  <cluster>
	<enabled>true</enabled>
    <cpuThreshold>60</cpuThreshold> <!-- must be an integer -->
	...
  </cluster>
  ...
</mediaServer>
Setting the Memory Threshold

Similar to the CPU threshold, the memory threshold value of a media server determines when a media server is over capacity.  The memory threshold of a media server is expressed in bytes instead of a percentage.  By default, there is no memory threshold. You can set one yourself by adding a memoryThreshold element to the cluster element.

The example below sets the memory threshold of a media server to 1073741824 bytes, or 1 GB.

<mediaServer>
  ...
  <cluster>
	<enabled>true</enabled>
    <memoryThreshold>1073741824</memoryThreshold>
	...
  </cluster>
  ...
</mediaServer>
Setting the Bandwidth Threshold

The bandwidth threshold of a media server is another parameter that determines when the server is over capacity. It is expressed in Mbps. By default, there is no bandwidth threshold. You can set one yourself by adding a bandwidthThreshold element to the cluster element. It is important to note that the bandwidth threshold is for both upstream and downstream bandwidth. If either of these exceed the threshold values then the media server wlil be considered to be over capacity.

The example below sets the bandwidth threshold of a media server to 512 Mbps.

<mediaServer>
  ...
  <cluster>
	<enabled>true</enabled>
    <bandwidthThreshold>512</bandwidthThreshold>
	...
  </cluster>
  ...
</mediaServer>
Setting the Failed Connection Threshold

The failed connection threshold is an additional option for specifying media server capacity. You can use this to put your media server over capacity if your failed connections go over the configured value. There are two configurable properties here. The failedConnectionsThreshold specifies the number of failed connections, and the failedConnectionsExpiry specifies the time period to consider. For the example below 20 failed connections in a 10 second period will put the media server over capacity and it will become unavailable.

<mediaServer>
  ...
  <cluster>
	<enabled>true</enabled>
	<failedConnectionsThreshold>20</failedConnectionsThreshold> <!-- The amount of failed connections allowed in a specific time period before server is over capacity --> 
	<failedConnectionsExpiry>10</failedConnectionsExpiry> <!-- The time window (in seconds) for above -->
	...
  </cluster>
  ...
</mediaServer>
Specifying the Over Capacity Delay

The overCapacityDelay configures how long your media server will remain unavailable before becoming available for new connections. You can imagine that without setting this delay you could easily end up with a server thrashing between being over capacity, recovering some resources, accepting more connections, and then going over capacity again. Configuring the overCapacityDelay provides a buffer where the server will remain unavailable even as resources are recovered. This means that when the delay expires, and the server starts accepting new connections, the server is less likely to immediately go over capacity again. The longer the delay, the longer you are giving your server to recover.

<mediaServer>
  ...
  <cluster>
	<overCapacityDelay>60</overCapacityDelay> <!-- minimum time in seconds to be unavailable once triggered -->


	...
  </cluster>
  ...
</mediaServer>
Specifying a Default Configuration

Rather than specifying these thresholds on each individual media server, you can use the same configuration values in your gateway's configuration file. These values will then be used as default values by the media server. If a value exists in both the media server and gateway configuration files, then the value in the media server file will take precedence.

The example below shows how to specify each of these values in your gateway's configuration file.

<gateway>
  ...
  <cluster>
	<enabled>true</enabled>
    <cpuThreshold>60</cpuThreshold>
    <memoryThreshold>1073741824</memoryThreshold>
    <bandwidthThreshold>512</bandwidthThreshold>
	...
  </cluster>
  ...
</gateway>

Controlling the Cluster

LiveSwitch media servers in a cluster communicate with each other over TCP sockets. By default, clustering is performed on port 8445 for the cluster connection. If port 8445, is not available, media servers will incrementally try additional port values until either one is successful or until it is clear that clustering is not possible. For instance, if 8445 is available, a media server will try 8446, 8447, etc. in turn until one is successful. Media servers self-determine which IP addresses they bind to these sockets, and generally will use the first one of their interfaces that work.

This is not always ideal. You can configure the binding behaviour by specifying the cidr attribute on the cluster element. If specified, a media server will only bind to IP addresses in the specified subnet. The following configuration snippet shows how to configure LiveSwitch to bind only to IP addresses in the 10.0.0.0/16 subnet.

<cluster cidr="10.0.0.0/16">
</cluster>

You can also change the default port that a media server will listen on by specifying the port attribute. Note - if you specify a port attribute, a media server will only ever listen on a single port. This means that if the port is unavailable, the media server will not attempt to cluster on a different port.

The example below shows how you might set the clustering port to 8100.

<cluster port="8100">
</cluster>

Limitations of Clustering

A limitation of the clustering feature is that all media servers must exist on the same subnet. This means that if you want to link media servers from different data centres, you must implement bridging logic to connect them yourself, using a VPN or similar feature.

REST API

The REST API is hosted by the LiveSwitch gateway. You can access the REST API provided you have IP-level access to the Admin endpoint (/admin by default). For example:

curl -X GET https://v1.liveswitch.fm:9443/admin/api/v1.0/applications



{"swagger":"2.0","basePath":"/admin","paths":{"/api/v{version}/applications/{applicationId}":{"get":{"tags":["Applications"],"summary":"Gets high level snapshot of application information.","operationId":"GetApplicationSnapshot","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Application"}}}}},"/api/v{version}/applications":{"get":{"tags":["Applications"],"summary":"Gets a list of application ids currently in use.","operationId":"GetApplications","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/ApplicationCollectionItem"}}}}}},"/api/v{version}/applications/{applicationId}/channels":{"get":{"tags":["Channels"],"summary":"Gets of list of active channels.","operationId":"GetActiveChannels","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/ChannelCollectionItem"}}},"404":{"description":"Not Found"}}}},"/api/v{version}/applications/{applicationId}/channels/{channelId}":{"get":{"tags":["Channels"],"summary":"Gets a high level snapshot of channel information.","operationId":"GetChannel","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel Id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Channel"}},"404":{"description":"Not Found"}}},"put":{"tags":["Channels"],"summary":"Update channel config.","operationId":"UpdateChannelConfig","consumes":["application/json-patch+json","application/json","text/json","application/*+json"],"produces":[],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"config","in":"body","description":"The new channel config.","required":false,"schema":{"$ref":"#/definitions/ChannelConfig"}},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"204":{"description":"successful operation"},"404":{"description":"channel does not exist."}}},"delete":{"tags":["Channels"],"summary":"Kick everyone out of a channel.","operationId":"KickChannelClients","consumes":[],"produces":[],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"204":{"description":"successful operation"},"404":{"description":"channel does not exist."}}}},"/api/v{version}/applications/{applicationId}/users/{userId}/devices/{deviceId}/clients/{clientId}/channels":{"get":{"tags":["Channels"],"summary":"Gets a list of all channels that a specific client has joined.","operationId":"GetClientChannels","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"deviceId","in":"path","description":"The device id.","required":true,"type":"string"},{"name":"clientId","in":"path","description":"The client id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/ClientChannelCollectionItem"}}},"404":{"description":"Not Found"}}}},"/api/v{version}/applications/{applicationId}/channels/{channelId}/clients":{"get":{"tags":["Clients"],"summary":"Gets a list of all clients in a channel.","operationId":"GetChannelClients","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/ChannelClientCollectionItem"}}},"404":{"description":"Not Found"}}}},"/api/v{version}/applications/{applicationId}/users/{userId}/devices/{deviceId}/clients/{clientId}":{"get":{"tags":["Clients"],"summary":"Gets information about a specific client owned by a specific user.","operationId":"GetUserClient","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"deviceId","in":"path","description":"The device id.","required":true,"type":"string"},{"name":"clientId","in":"path","description":"The client id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Client"}}}},"put":{"tags":["Clients"],"summary":"Updates the client config and sends out notifications to update other clients.","operationId":"UpdateClientRecords","consumes":["application/json-patch+json","application/json","text/json","application/*+json"],"produces":[],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"deviceId","in":"path","description":"The device id.","required":true,"type":"string"},{"name":"clientId","in":"path","description":"The client id.","required":true,"type":"string"},{"name":"newConfig","in":"body","description":"The new config to apply to the client.","required":false,"schema":{"$ref":"#/definitions/ClientConfig"}},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"204":{"description":"successful operation"},"404":{"description":"client not found."}}}},"/api/v{version}/applications/{applicationId}/channels/{channelId}/users/{userId}/devices/{deviceId}/clients/{clientId}":{"get":{"tags":["Clients"],"summary":"Gets a high level snapshot of a client in a channel owned by a specific device.","operationId":"GetChannelDeviceClient","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"deviceId","in":"path","description":"The device id.","required":true,"type":"string"},{"name":"clientId","in":"path","description":"The client id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/ChannelClient"}},"404":{"description":"Not Found"}}},"put":{"tags":["Clients"],"summary":"Updates the channel claim or other settings.","operationId":"UpdateChannelInformation","consumes":["application/json-patch+json","application/json","text/json","application/*+json"],"produces":[],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"deviceId","in":"path","description":"The device id.","required":true,"type":"string"},{"name":"clientId","in":"path","description":"The client id.","required":true,"type":"string"},{"name":"channelSettings","in":"body","description":"The new claims and other information to apply to the client.","required":false,"schema":{"$ref":"#/definitions/ChannelClientSettings"}},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"204":{"description":"successful operation"},"404":{"description":"client not found."}}},"delete":{"tags":["Clients"],"summary":"Kick a client out of a channel.","operationId":"KickChannelClient","consumes":[],"produces":[],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"deviceId","in":"path","description":"The device id.","required":true,"type":"string"},{"name":"clientId","in":"path","description":"The client id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"204":{"description":"successful operation"},"404":{"description":"client was not in channel."}}}},"/api/v{version}/applications/{applicationId}/channels/{channelId}/users/{userId}/devices/{deviceId}/clients":{"get":{"tags":["Clients"],"summary":"Gets a list of clients in a channel owned by a specific device.","operationId":"GetChannelDeviceClients","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"deviceId","in":"path","description":"The device id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/ChannelClientCollectionItem"}}},"404":{"description":"Not Found"}}}},"/api/v{version}/applications/{applicationId}/users/{userId}/devices/{deviceId}/clients":{"get":{"tags":["Clients"],"summary":"Gets a list of all clients owned by a device.","operationId":"GetUserDeviceClients","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"deviceId","in":"path","description":"The device id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/ClientCollectionItem"}}}}}},"/api/v{version}/applications/{applicationId}/channels/{channelId}/connections":{"get":{"tags":["Connections"],"summary":"Gets a list of all connections in a channel.","operationId":"GetChannelConnections","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/ChannelConnectionCollectionItem"}}}}}},"/api/v{version}/applications/{applicationId}/channels/{channelId}/users/{userId}/devices/{deviceId}/clients/{clientId}/connections/{connectionId}":{"get":{"tags":["Connections"],"summary":"Queries the reports to get the connection info for a connection in a channel owned by a specific client.","operationId":"GetChannelClientConnection","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"deviceId","in":"path","description":"The device id.","required":true,"type":"string"},{"name":"clientId","in":"path","description":"The client id.","required":true,"type":"string"},{"name":"connectionId","in":"path","description":"The connection id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/ConnectionInfo"}}}},"put":{"tags":["Connections"],"summary":"Update connection config.","operationId":"UpdateConnectionConfig","consumes":["application/json-patch+json","application/json","text/json","application/*+json"],"produces":[],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"deviceId","in":"path","description":"The device id.","required":true,"type":"string"},{"name":"clientId","in":"path","description":"The client id.","required":true,"type":"string"},{"name":"connectionId","in":"path","description":"The connection id.","required":true,"type":"string"},{"name":"config","in":"body","description":"The new connection config.","required":false,"schema":{"$ref":"#/definitions/ChannelConfig"}},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"204":{"description":"successful operation"},"404":{"description":"user, device, client, connection, or channel does not exist."}}}},"/api/v{version}/applications/{applicationId}/channels/{channelId}/users/{userId}/devices/{deviceId}/clients/{clientId}/connections":{"get":{"tags":["Connections"],"summary":"Gets a list of connections in a channel owned by a specific client.","operationId":"GetChannelClientConnections","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"deviceId","in":"path","description":"The device id.","required":true,"type":"string"},{"name":"clientId","in":"path","description":"The client id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/ChannelConnectionCollectionItem"}}}}}},"/api/v{version}/connectors":{"get":{"tags":["Connector"],"summary":"Get a list of all registered connectors.","description":"","operationId":"GetConnectors","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/Connector"}}}}}},"/api/v{version}/applications/{applicationId}/channels/{channelId}/devices":{"get":{"tags":["Devices"],"summary":"Gets a list of all devices in a channel.","operationId":"GetChannelDevices","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/ChannelDeviceCollectionItem"}}}}}},"/api/v{version}/applications/{applicationId}/channels/{channelId}/users/{userId}/devices/{deviceId}":{"get":{"tags":["Devices"],"summary":"Gets a high level snapshot of a device in a channel owned by a specific user.","operationId":"GetChannelUserDevice","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"deviceId","in":"path","description":"The device id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/ChannelDevice"}},"404":{"description":"device not found in this channel"}}},"put":{"tags":["Devices"],"summary":"Update device config settings.","operationId":"UpdateDeviceConfig","consumes":["application/json-patch+json","application/json","text/json","application/*+json"],"produces":[],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"deviceId","in":"path","description":"The device id.","required":true,"type":"string"},{"name":"config","in":"body","description":"The new device config config.","required":false,"schema":{"$ref":"#/definitions/DeviceConfig"}},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"204":{"description":"successful operation"},"404":{"description":"user, device, or channel does not exist."}}},"delete":{"tags":["Devices"],"summary":"Kick a device out of a channel.","operationId":"KickChannelDevice","consumes":[],"produces":[],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"deviceId","in":"path","description":"The device id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"204":{"description":"successful operation"},"404":{"description":"device was not in channel."}}}},"/api/v{version}/applications/{applicationId}/channels/{channelId}/users/{userId}/devices":{"get":{"tags":["Devices"],"summary":"Gets a list of devices in a channel owned by a specific user.","operationId":"GetChannelUserDevices","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/ChannelDeviceCollectionItem"}}},"404":{"description":"device not found in this channel"}}}},"/api/v{version}/applications/{applicationId}/users/{userId}/devices/{deviceId}":{"get":{"tags":["Devices"],"summary":"Gets information about a specific device owned by a specific user.","operationId":"GetUserDevice","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"deviceId","in":"path","description":"The device id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Device"}}}}},"/api/v{version}/applications/{applicationId}/users/{userId}/devices":{"get":{"tags":["Devices"],"summary":"Gets a list of all devices owned by a user.","operationId":"GetUserDevices","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/DeviceCollectionItem"}}}}}},"/api/v{version}/datastore/dump":{"get":{"tags":["Diagnostics"],"summary":"Gets a dump of the raw datastore data.","operationId":"DumpDatastore","consumes":[],"produces":[],"parameters":[{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation"}}}},"/api/v{version}/mediaservers/{mediaServerId}":{"get":{"tags":["MediaServers"],"summary":"Get the connection information for a specific Media Server.","operationId":"GetMediaServer","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"mediaServerId","in":"path","description":"ID of media server to return","required":true,"type":"string","format":"uuid"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/MediaServerReport"}},"404":{"description":"Media Server not found"}}},"put":{"tags":["MediaServers"],"summary":"Update a Media Server by it's ID","operationId":"UpdateMediaServer","consumes":["application/json-patch+json","application/json","text/json","application/*+json"],"produces":[],"parameters":[{"name":"mediaServerId","in":"path","description":"ID of media server to update","required":true,"type":"string","format":"uuid"},{"name":"body","in":"body","description":"","required":false,"schema":{"$ref":"#/definitions/MediaServerUpdate"}},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation"},"404":{"description":"media server not found."}}}},"/api/v{version}/mediaservers":{"get":{"tags":["MediaServers"],"summary":"Get a list of all active and inactive media servers.","description":"","operationId":"GetMediaServers","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/MediaServer"}}}}}},"/api/v{version}/mediaservers/stats/{mediaServerId}":{"get":{"tags":["MediaServers"],"summary":"Get the connection information for a specific Media Server.","operationId":"GetMediaServerConnectionStats","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"mediaServerId","in":"path","description":"ID of media server to return","required":true,"type":"string","format":"uuid"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/MediaServerStats"}},"404":{"description":"Media Server not found"}}}},"/api/v{version}/mediaservers/stats":{"get":{"tags":["MediaServers"],"summary":"Get the connection information for all Media Servers.","operationId":"GetAllMediaServerConnectionStats","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/MediaServerStats"}}}}}},"/api/v{version}/applications/{applicationId}/channels/{channelId}/users":{"get":{"tags":["Users"],"summary":"Gets a list of users in the channel.","operationId":"GetChannelUsers","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/ChannelUserCollectionItem"}}}}}},"/api/v{version}/applications/{applicationId}/channels/{channelId}/users/{userId}":{"get":{"tags":["Users"],"summary":"Gets a high level snapshot of a user in the channel.","operationId":"GetChannelUser","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/ChannelUser"}}}},"put":{"tags":["Users"],"summary":"Update user config.","operationId":"UpdateUserConfig","consumes":["application/json-patch+json","application/json","text/json","application/*+json"],"produces":[],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"config","in":"body","description":"The new user recording config.","required":false,"schema":{"$ref":"#/definitions/UserConfig"}},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"204":{"description":"successful operation"},"404":{"description":"user or channel does not exist."}}},"delete":{"tags":["Users"],"summary":"Kick a user out of a channel.","operationId":"KickChannelUser","consumes":[],"produces":[],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"channelId","in":"path","description":"The channel id.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The userid.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"204":{"description":"successful operation"},"404":{"description":"user was not in channel."}}}},"/api/v{version}/applications/{applicationId}/users":{"get":{"tags":["Users"],"summary":"Gets a list of all users for this application.","operationId":"GetApplicationUsers","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/UserCollectionItem"}}}}}},"/api/v{version}/applications/{applicationId}/users/{userId}":{"get":{"tags":["Users"],"summary":"Gets information about a specific user.","operationId":"GetApplicationUser","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"applicationId","in":"path","description":"Id of the application.","required":true,"type":"string"},{"name":"userId","in":"path","description":"The user id.","required":true,"type":"string"},{"name":"version","in":"path","description":"The requested API version","required":true,"type":"string","default":"1.0"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/User"}}}}}},"definitions":{"Application":{"description":"","type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"},"channelCount":{"format":"int32","description":"Gets or Sets ChannelCount","type":"integer"},"userCount":{"format":"int32","description":"Gets or Sets UserCount","type":"integer"},"deviceCount":{"format":"int32","description":"Gets or Sets DeviceCount","type":"integer"},"clientCount":{"format":"int32","description":"Gets or Sets ClientCount","type":"integer"},"connectionCount":{"format":"int32","description":"Gets or Sets ConnectionCount","type":"integer"}}},"ApplicationCollectionItem":{"description":"","type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"}}},"ChannelCollectionItem":{"description":"","type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"},"userCount":{"format":"int32","description":"Gets or Sets UserCount","type":"integer"},"deviceCount":{"format":"int32","description":"Gets or Sets DeviceCount","type":"integer"},"clientCount":{"format":"int32","description":"Gets or Sets ClientCount","type":"integer"},"connectionCount":{"format":"int32","description":"Gets or Sets ConnectionCount","type":"integer"}}},"ChannelConfig":{"type":"object","properties":{"recording":{"description":"True if to enable recording, false if to disable.","type":"boolean"}}},"Channel":{"type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"},"userCount":{"format":"int32","description":"Gets or Sets UserCount","type":"integer"},"deviceCount":{"format":"int32","description":"Gets or Sets DeviceCount","type":"integer"},"clientCount":{"format":"int32","description":"Gets or Sets ClientCount","type":"integer"},"connectionCount":{"format":"int32","description":"Gets or Sets ConnectionCount","type":"integer"}}},"ClientChannelCollectionItem":{"type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"}}},"ChannelClientCollectionItem":{"description":"","type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"},"userId":{"description":"Gets or Sets UserId","type":"string"},"deviceId":{"description":"Gets or Sets DeviceId","type":"string"},"connectionCount":{"format":"int32","description":"Gets or Sets ConnectionCount","type":"integer"}}},"ClientConfig":{"description":"Client configuration.","type":"object","properties":{"userAlias":{"description":"Gets or sets the User Alias.","type":"string"},"deviceAlias":{"description":"Gets or sets the Device Alias.","type":"string"},"tag":{"description":"Gets or sets the Tag.","type":"string"},"roles":{"description":"Gets or sets the Roles.","type":"array","items":{"type":"string"}}}},"Client":{"description":"","type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"},"userId":{"description":"Gets or Sets UserId","type":"string"},"deviceId":{"description":"Gets or Sets DeviceId","type":"string"},"roles":{"description":"Gets or Sets Roles","type":"array","items":{"type":"string"}},"channelCount":{"format":"int32","description":"Gets or Sets ChannelCount","type":"integer"}}},"ChannelClientSettings":{"type":"object","properties":{"claim":{"$ref":"#/definitions/ChannelClaim","description":"Gets or sets the channel claim."},"recording":{"description":"Gets or sets whether to enable recording.","type":"boolean"}}},"ChannelClaim":{"description":"A channel claim.","type":"object","properties":{"id":{"description":"Gets or sets the channel identifier.","type":"string"},"broadcast":{"description":"Gets or sets if this is a broadcast channel or not.","type":"boolean"},"disableSendMessage":{"description":"Gets or sets whether the user is NOT authorized to send messages.","type":"boolean"},"disablePeer":{"description":"Gets or sets whether the user is NOT authorized to create peer connections.","type":"boolean"},"disableSfu":{"description":"Gets or sets whether the user is NOT authorized to create SFU connections.","type":"boolean"},"disableMcu":{"description":"Gets or sets whether the user is NOT authorized to create MCU connections.","type":"boolean"},"disableSendAudio":{"description":"Gets or sets whether the user is NOT authorized to send audio.","type":"boolean"},"disableSendVideo":{"description":"Gets or sets whether the user is NOT authorized to send video.","type":"boolean"},"disableSendData":{"description":"Gets or sets whether the user is NOT authorized to send data.","type":"boolean"},"disableRemoteClientEvents":{"description":"Gets or sets whether to disable the NotifyJoin, NotifyLeave, and NotifyUpdate messages and events.","type":"boolean"},"audioReceiveWhitelist":{"description":"Gets or sets a whitelist of roles whose member's audio can be received from. Null disables the whitelist whereas an empty list\r\ndisables all receiving.","type":"array","items":{"type":"string"}},"videoReceiveWhitelist":{"description":"Gets or sets a whitelist of roles whose member's video can be received from. Null disables the whitelist whereas an empty list\r\ndisables all receiving.","type":"array","items":{"type":"string"}},"dataReceiveWhitelist":{"description":"Gets or sets a whitelist of roles whose member's data can be received from. Null disables the whitelist whereas an empty list\r\ndisables all receiving.","type":"array","items":{"type":"string"}}}},"ChannelClient":{"description":"","type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"},"userId":{"description":"Gets or Sets UserId","type":"string"},"deviceId":{"description":"Gets or Sets DeviceId","type":"string"},"roles":{"description":"Gets or Sets Roles","type":"array","items":{"type":"string"}},"connectionCount":{"format":"int32","description":"Gets or Sets ConnectionCount","type":"integer"}}},"ClientCollectionItem":{"description":"","type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"},"userId":{"description":"Gets or Sets UserId","type":"string"},"deviceId":{"description":"Gets or Sets DeviceId","type":"string"},"channelCount":{"format":"int32","description":"Gets or Sets ChannelCount","type":"integer"}}},"ChannelConnectionCollectionItem":{"description":"","type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"},"userId":{"description":"Gets or Sets UserId","type":"string"},"deviceId":{"description":"Gets or Sets DeviceId","type":"string"},"clientId":{"description":"Gets or Sets ClientId","type":"string"}}},"ConnectionInfo":{"description":"Information about a connection.","type":"object","properties":{"userId":{"description":"Gets the user identifier.","type":"string"},"userAlias":{"description":"Gets the user alias.","type":"string"},"deviceId":{"description":"Gets the device identifier.","type":"string"},"deviceAlias":{"description":"Gets the device alias.","type":"string"},"clientId":{"description":"Gets the client identifier.","type":"string"},"clientTag":{"description":"Gets the client tag.","type":"string"},"clientRoles":{"description":"Gets the client roles.","type":"array","items":{"type":"string"}},"localAudioMuted":{"description":"Gets or sets if the local audio is muted.","type":"boolean"},"localVideoMuted":{"description":"Gets or sets if the local video is muted.","type":"boolean"},"audioDirection":{"description":"Gets or sets the audio stream direction.","type":"string"},"videoDirection":{"description":"Gets or sets the video stream direction.","type":"string"},"id":{"description":"Gets the connection identifier.","type":"string"},"tag":{"description":"Gets the connection tag.","type":"string"},"type":{"description":"Gets the connection type.","type":"string"},"audioFormats":{"description":"Gets the audio formats.","type":"array","items":{"$ref":"#/definitions/FormatInfo"}},"videoFormats":{"description":"Gets the video formats.","type":"array","items":{"$ref":"#/definitions/FormatInfo"}}}},"FormatInfo":{"description":"An serializable object that contains info on a video or audio format.","type":"object","properties":{"name":{"description":"Gets or sets the codec name.","type":"string"},"clockRate":{"format":"int32","description":"Gets or sets the clock rate.","type":"integer"},"channels":{"format":"int32","description":"Gets or sets the channel count if available. Unused for video codecs.","type":"integer"}}},"Connector":{"type":"object","properties":{"id":{"format":"uuid","type":"string","readOnly":true},"registeredProtocols":{"type":"array","items":{"type":"string"},"readOnly":true}}},"ChannelDeviceCollectionItem":{"description":"","type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"},"userId":{"description":"Gets or Sets UserId","type":"string"},"clientCount":{"format":"int32","description":"Gets or Sets ClientCount","type":"integer"},"connectionCount":{"format":"int32","description":"Gets or Sets ConnectionCount","type":"integer"}}},"DeviceConfig":{"type":"object","properties":{"recording":{"description":"True if to enable recording, false if to disable.","type":"boolean"}}},"ChannelDevice":{"description":"","type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"},"userId":{"description":"Gets or Sets UserId","type":"string"},"alias":{"description":"Gets or Sets Alias","type":"string"},"clientCount":{"format":"int32","description":"Gets or Sets ClientCount","type":"integer"},"connectionCount":{"format":"int32","description":"Gets or Sets ConnectionCount","type":"integer"}}},"Device":{"description":"","type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"},"userId":{"description":"Gets or Sets UserId","type":"string"},"alias":{"description":"Gets or Sets Alias","type":"string"},"clientCount":{"format":"int32","description":"Gets or Sets ClientCount","type":"integer"},"channelCount":{"format":"int32","description":"Gets or Sets ChannelCount","type":"integer"}}},"DeviceCollectionItem":{"description":"","type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"},"userId":{"description":"Gets or Sets UserId","type":"string"},"clientCount":{"format":"int32","description":"Gets or Sets ClientCount","type":"integer"},"channelCount":{"format":"int32","description":"Gets or Sets ChannelCount","type":"integer"}}},"MediaServerReport":{"type":"object","properties":{"applications":{"type":"array","items":{"$ref":"#/definitions/ApplicationReport"}},"computer":{"$ref":"#/definitions/ComputerReport"},"connectionStats":{"$ref":"#/definitions/ConnectionStatsReport"},"id":{"type":"string"}}},"ApplicationReport":{"type":"object","properties":{"channels":{"type":"array","items":{"$ref":"#/definitions/ChannelReport"}},"id":{"type":"string"}}},"ComputerReport":{"type":"object","properties":{"cpuUsage":{"format":"double","type":"number"},"memoryUsage":{"format":"int64","type":"integer"},"inboundBandwidth":{"format":"double","type":"number"},"outboundBandwidth":{"format":"double","type":"number"},"id":{"type":"string"}}},"ConnectionStatsReport":{"type":"object","properties":{"pendingConnections":{"format":"int32","description":"Gets or sets the number of connections that are currently in the process of connecting.","type":"integer"},"failedConnections":{"format":"int32","description":"Gets or sets the number of connections that had connected previously but now have failed.","type":"integer"},"failedConnectionAttempts":{"format":"int32","description":"Gets or sets the number of connections that failed during the connecting process.","type":"integer"},"activeConnections":{"format":"int32","description":"Gets or sets the current number of active connections on this media server.","type":"integer"},"closedConnections":{"format":"int32","description":"Gets or sets the current number of closed connections on this media server.","type":"integer"},"closedPendingConnections":{"format":"int32","description":"Gets or sets the current number of connections that were closed before they completed.","type":"integer"},"id":{"type":"string"}}},"ChannelReport":{"type":"object","properties":{"users":{"type":"array","items":{"$ref":"#/definitions/UserReport"}},"recording":{"$ref":"#/definitions/RecordingInfo"},"id":{"type":"string"}}},"UserReport":{"type":"object","properties":{"devices":{"type":"array","items":{"$ref":"#/definitions/DeviceReport"}},"recording":{"$ref":"#/definitions/RecordingInfo"},"id":{"type":"string"}}},"RecordingInfo":{"type":"object","properties":{"enabled":{"type":"boolean"}}},"DeviceReport":{"type":"object","properties":{"clients":{"type":"array","items":{"$ref":"#/definitions/ClientReport"}},"recording":{"$ref":"#/definitions/RecordingInfo"},"id":{"type":"string"}}},"ClientReport":{"type":"object","properties":{"connections":{"type":"array","items":{"$ref":"#/definitions/ConnectionReport"}},"recording":{"$ref":"#/definitions/RecordingInfo"},"id":{"type":"string"}}},"ConnectionReport":{"type":"object","properties":{"state":{"type":"string"},"isProxy":{"type":"boolean"},"connectionInfo":{"$ref":"#/definitions/ConnectionInfo"},"remoteConnectionInfo":{"$ref":"#/definitions/ConnectionInfo"},"recording":{"$ref":"#/definitions/RecordingInfo"},"id":{"type":"string"}}},"MediaServerUpdate":{"description":"","type":"object","properties":{"active":{"description":"True to enable the media server; false to disable.","type":"boolean"}}},"MediaServer":{"description":"The model holding data for a media server.","type":"object","properties":{"id":{"format":"uuid","description":"The media server id.","type":"string"},"available":{"description":"True if the media server is available for mapping.","type":"boolean","readOnly":true},"active":{"description":"True if the media server hasn't been manually deactivated.","type":"boolean"},"overCapacity":{"description":"True if the media server reached its capacity thresholds.","type":"boolean"},"lastStateChange":{"format":"date-time","description":"The time that state was last changed.","type":"string"},"ipAddresses":{"description":"The list of ip addresses associated with a media server.","type":"array","items":{"type":"string"}},"region":{"description":"The region for this media server.","type":"string"},"thresholds":{"$ref":"#/definitions/ThresholdsConfiguration","description":"The thresholds for this specific media server."}}},"ThresholdsConfiguration":{"type":"object","properties":{"enabled":{"type":"boolean"},"cpu":{"format":"int32","type":"integer"},"memory":{"format":"int64","type":"integer"},"bandwidth":{"format":"int32","type":"integer"},"delay":{"format":"int32","type":"integer"},"pendingConnections":{"format":"int32","type":"integer"},"failedConnections":{"format":"int32","type":"integer"},"failedConnectionsExpiry":{"format":"int32","type":"integer"}}},"MediaServerStats":{"type":"object","properties":{"mediaServerId":{"format":"uuid","description":"The id of the media server these stats belong to.","type":"string"},"pendingConnections":{"format":"int32","description":"The total number of pending connections on a media server.","type":"integer"},"activeConnections":{"format":"int32","description":"The total amount of active connections on a media server.","type":"integer"},"totalClosedConnections":{"format":"int32","description":"The total amount of connections that were closed on a media server.","type":"integer"},"totalFailedConnections":{"format":"int32","description":"The total number of failed connections on a media server.","type":"integer"},"durationFailedConnections":{"format":"int32","description":"The number of failed connections within a specific duration.","type":"integer"},"durationSeconds":{"format":"int32","description":"How long the duration is for DurationFailedConnections in seconds.","type":"integer"}}},"ChannelUserCollectionItem":{"description":"","type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"},"deviceCount":{"format":"int32","description":"Gets or Sets DeviceCount","type":"integer"},"clientCount":{"format":"int32","description":"Gets or Sets ClientCount","type":"integer"},"connectionCount":{"format":"int32","description":"Gets or Sets ConnectionCount","type":"integer"}}},"UserConfig":{"type":"object","properties":{"recording":{"description":"True if to enable recording, false if to disable.","type":"boolean"}}},"ChannelUser":{"description":"","type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"},"alias":{"description":"Gets or Sets Alias","type":"string"},"deviceCount":{"format":"int32","description":"Gets or Sets DeviceCount","type":"integer"},"clientCount":{"format":"int32","description":"Gets or Sets ClientCount","type":"integer"},"connectionCount":{"format":"int32","description":"Gets or Sets ConnectionCount","type":"integer"}}},"UserCollectionItem":{"description":"","type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"},"deviceCount":{"format":"int32","description":"Gets or Sets DeviceCount","type":"integer"},"clientCount":{"format":"int32","description":"Gets or Sets ClientCount","type":"integer"},"channelCount":{"format":"int32","description":"Gets or Sets ChannelCount","type":"integer"}}},"User":{"description":"","type":"object","properties":{"id":{"description":"Gets or Sets Id","type":"string"},"alias":{"description":"Gets or Sets Alias","type":"string"},"deviceCount":{"format":"int32","description":"Gets or Sets DeviceCount","type":"integer"},"clientCount":{"format":"int32","description":"Gets or Sets ClientCount","type":"integer"},"channelCount":{"format":"int32","description":"Gets or Sets ChannelCount","type":"integer"}}}},"securityDefinitions":{}}

FAQ

Do I need a STUN/TURN server with LiveSwitch?

In general, LiveSwitch does not require STUN or TURN for SFU/MCU connections. LiveSwitch does require STUN/TURN for guaranteed peer connections.

There are several use cases for STUN and/or TURN:

  1. STUN/TURN: When both endpoints in a streaming connection reside behind a NAT/router. In this case, STUN and/or TURN assists with NAT traversal.
  2. TURNS: When an ISP actively blocks real-time media using packet inspection. In this case, using the TLS-encrypted TURN transport bypasses prying eyes.
  3. STUN: When you want to know your own public IP address. In this case, the STUN server is used to simply echo back the public IP address of the client that made the request.

Use case 1 is why LiveSwitch requires STUN/TURN for peer connections. It also explains why STUN/TURN is generally not required for SFU/MCU connections. Since a publicly-accessible LiveSwitch media server is one of the endpoints in SFU/MCU connections, NAT traversal is not required. Note that if all endpoints are on the same local subnet, then STUN/TURN is generally not required.

Use case 2 is why we say that "in general" LiveSwitch doesn't require STUN or TURN for SFU/MCU connections. An Internet service provider can throw a wrench into the middle of things by actively blocking media packets. This happens more often on networks where a anti-competitive practices are in place to ensure that customers use a pre-selected media provider (e.g. on hotel networks). Adding a TURNS server (and configuring clients to use it in their ICE server array), ensures a safe fallback.

Use case 3 only applies to the media server, and is entirely optional. When deploying a LiveSwitch media server to the public Internet, there are two possible options for IP addressing:

  1. The server can bind directly to a private IP address, which is 1:1 port-mapped to a public IP address.
  2. The server can bind directly to a public IP address.

The LiveSwitch media server needs to More information on clustering streaming routes with clients. If you are using option 1, which is typical of a cloud compute hosting provider, then the media server needs to either be configured manually with its public IP address, or given the address of an external STUN server that can be used to auto-discover it. If you are using option 2, then the LiveSwitch media server can read the public IP address directly from the operating system - no special configuration is required.

To summarize:

  1. Clients require STUN/TURN(S) when creating peer connections (unless all peers belong to the same local subnet).
  2. Clients may require TURN(S) when creating SFU/MCU connections if their Internet service provider actively blocks real-time media.
  3. Media servers can optionally use STUN to auto-discover its public IP address, if the server is configured to bind to a private IP address and manual configuration of the public IP address is not desired.

We want to run our STUN/TURN server behind a proxy. How do we accomplish this?

You don't! STUN/TURN cannot go behind a proxy. The entire purpose of TURN is to inspect the IP traffic directly. TURN requires access to the unmodified original IP headers to forward traffic between peers. This is not possible where a proxy is involved.

How many server instances will I need to support my user base?

Estimating exactly the number of servers you need depends very heavily on expected concurrency. It doesn't matter how many users you have - it matters how many users you expect to have active at the same time.

As a general best practice, we estimate concurrency using the 100:10:1 rule, which states that for every 1,000 named users, 100 are quite active, and about 10 are concurrent at any given point in time. Using 32,000 as a starting point, that means we need to plan for 3,200 active users and 320 concurrent at any moment in time.

In general you can expect one Media Server, on the equivalent of an AWS-EC2 c4.large (2xCPU, 4GB RAM) instance, to handle ~100 concurrent connections in SFU mode with a standard 480p stream. So, to handle the load described here you would require at least four Media Servers in a clustered environment. More information on clustering is available here.

A single Gateway can handle a lot of traffic, far more than our example of 320 concurrent connections. We run our demo environment Gateway on the equivalent of an AWS-EC2 c5.xlarge (4xCPU, 8GB RAM) and it can handle thousands of concurrent clients. That said, we recommend running two Gateways behind a standard load balancer for redundancy purposes.

P2P Connections

For MCU and SFU connections relay is generally not required. See the FAQ question Do I Need a STUN/TURN Server with LiveSwitch? for an in depth discussion on the reasons for this. That said, if you do use P2P connections then you can expect that some portion of these will be over relay. We estimate that about 20-30% of P2P connections will require TURN, so with the example numbers discussed here we need to plan for 96 concurrent TURN users. For this example load the recommended server is also the equivalent of an AWS-EC2 c5.xlarge. TURN is very demanding on bandwidth. A typical standard-definition audio/video stream is about 1mbps, so with 96 relayed connections, you can get away with a single server that has 100Mbps of available upstream/downstream bandwidth. For redundancy, you will need 2 servers. No load balancer is required - round-robin DNS to spread the load across the two is generally considered to be best practice.

I've set up a Gateway. Now how do I tell if it's running properly?

Your Gateway must be available over HTTPS. The simplest, debugging 101, test to see if your Gateway is available is to browse to it in a browser. Let's say you have a Gateway served from mydomain.liveswitch.com, if you browse to https://mydomain.liveswitch.com/sync then you should see a page that says "LiveSwitch", the version number, and nothing else. If you do not, then your Gateway is not being served properly. If you do see the message but still cannot get a connection, then something else is wrong and you should contact support@frozenmountain.com.

You can also start up your Gateway in interactive mode from the command line on the server where the Gateway service is installed. If you fire it up, it'll popup a console window that shows the Media Server registration(s) that come in along with the client requests from client app(s). This is really useful for debugging. To do so you may need to stop the Gateway Windows service first since it's installed as a Windows service. Then, go to Program Files/Frozen Mountain Software/LiveSwitch/Gateway/FM.LiveSwitch.Gateway.Service.exe.

How do I enable bandwidth adaptation?

In your media server configuration, set the bandwidthAdaptationPolicy attribute to enabled on the connection element:

<connection bandwidthAdaptationPolicy="enabled" ... />


Native bandwidth adaptation is experimental

Note that in the native platforms bandwidth adaptation is still experimental and is not recommended for AudioStreams at this time.

I've set up a Media Server. Now how do I tell if it's running properly?

The Media Server can be started up from the command line in interactive mode on the server where the Media Server service is installed. If you fire it up, it'll popup a console window that shows it's registering, then registered, (along with other info) then reporting thread started. To do so you may need to stop the Media Server Windows service first since it's installed as a Windows service. Then, run the following - Program Files/Frozen Mountain Software/LiveSwitch/Media Server/MediaServer.exe