In this tutorial, you'll learn how to create an MCU connection to stream video and audio in a conference.
Prerequisites
This tutorial requires the Handle Media app that you created earlier.
Create a Connection
To create an MCU connection:
Create one bi-directional stream to send and receive audio and video data. To do so, create an AudioStream and a VideoStream, and provide both the LocalMedia and RemoteMedia instances. This returns an FM.LiveSwitch.McuConnection instance.
Manage the layout manager. The local view sent to the Media Server appears in the video feeds received. To prevent delay and duplication of local view, we "float" local view over the video feeds. To do so, add an addOnLayout event handler to the LayoutManager instance. In this event handler, invoke the floatLocalPreview method of the FM.LiveSwitch.LayoutUtility utility class, and provide the following:
The VideoLayout instance, which is the video feeds received.
The Layout instance, which is the local view layout.
The ID of the McuConnection instance, which is used to find the local user's view in the video feeds.
Close the MCU connection by invoking the Closing method of the FM.LiveSwitch.McuConnection instance, and removing the remote view.
Paste the following code into the HelloWorldLogic class.
private McuConnection _McuConnection;
private VideoLayout _VideoLayout;
private McuConnection openMcuConnection()
{
RemoteMedia remoteMedia = null;
_Dispatcher.Invoke(() => remoteMedia = new RemoteMedia(false, false, _AecContext));
_LayoutManager.AddRemoteView(remoteMedia.Id, remoteMedia.View);
var audioStream = (LocalMedia.AudioTrack != null) ? new AudioStream(LocalMedia, remoteMedia) : null;
var videoStream = (LocalMedia.VideoTrack != null) ? new VideoStream(LocalMedia, remoteMedia) : null;
var connection = Channel.CreateMcuConnection(audioStream, videoStream);
connection.OnStateChange += (conn) =>
{
Log.Info($"Mcu connection {conn.Id} is currently in a {conn.State.ToString()} state.");
if (conn.State == ConnectionState.Closing || conn.State == ConnectionState.Failing)
{
_LayoutManager.RemoveRemoteView(remoteMedia.Id);
remoteMedia.Destroy();
}
else if (conn.State == ConnectionState.Failed)
{
_McuConnection = openMcuConnection();
}
};
_LayoutManager.OnLayout += (layout) =>
{
if (_McuConnection != null)
{
LayoutUtility.FloatLocalPreview(layout, _VideoLayout, _McuConnection.Id, remoteMedia.Id, LocalMedia.ViewSink);
}
};
connection.Open();
return connection;
}
private String mcuViewId;
private McuConnection mcuConnection;
private VideoLayout videoLayout = null;
private McuConnection openMcuConnection() {
// Create remote media.
final RemoteMedia remoteMedia = new RemoteMedia(context, false, false, aecContext);
mcuViewId = remoteMedia.getId();
handler.post(() -> {
// Add remote view to the layout.
layoutManager.addRemoteView(mcuViewId, remoteMedia.getView());
});
// Create audio and video streams with local media and remote media.
AudioStream audioStream = (localMedia.getAudioTrack() != null) ? new AudioStream(localMedia, remoteMedia) : null;
VideoStream videoStream = (localMedia.getVideoTrack() != null) ? new VideoStream(localMedia, remoteMedia) : null;
// Create a MCU connection with audio and video stream.
McuConnection connection = channel.createMcuConnection(audioStream, videoStream);
connection.addOnStateChange(conn -> {
Log.info(String.format("MCU connection %s is currently in a %s state.", conn.getId(), conn.getState().toString()));
if (conn.getState() == ConnectionState.Closing || conn.getState() == ConnectionState.Failing) {
if (conn.getRemoteClosed()) {
Log.info(String.format("Media server has closed the MCU connection %s.", conn.getId()));
}
handler.post(() -> {
// Removing remote view from UI.
layoutManager.removeRemoteView(remoteMedia.getId());
remoteMedia.destroy();
});
} else if (conn.getState() == ConnectionState.Failed) {
openMcuConnection();
}
});
/*
MCU connections are bidirectional, so the local media from the client end will be received by the same client. To prevent
duplicate streams a float local preview is needed, which "floats" over the local media presented as remote media for the
client.
*/
layoutManager.addOnLayout(layout -> {
if (mcuConnection != null) {
LayoutUtility.floatLocalPreview(layout, videoLayout, mcuConnection.getId(), mcuViewId, localMedia.getViewSink());
}
});
connection.open();
return connection;
}
func addRemoteView(remoteMedia: RemoteMedia) -> Void {
DispatchQueue.main.async {
self._layoutManager?.addRemoteView(withId: remoteMedia.id(), view: remoteMedia.view())
}
}
func removeRemoteView(remoteMedia: RemoteMedia) -> Void {
DispatchQueue.main.async {
self._layoutManager?.removeRemoteView(withId: remoteMedia.id())
}
}
var _mcuViewId: String?
var _mcuConnection: FMLiveSwitchMcuConnection?
var _videoLayout: FMLiveSwitchVideoLayout?
func OpenMcuConnection() -> FMLiveSwitchMcuConnection {
let connection: FMLiveSwitchMcuConnection?
let remoteMedia: RemoteMedia = RemoteMedia.init(disableAudio: false, disableVideo: false, aecContext: nil)
_mcuViewId = remoteMedia.id()
self.addRemoteView(remoteMedia: remoteMedia)
let audioStream: FMLiveSwitchAudioStream = ((self._localMedia!.audioTrack() != nil) ? FMLiveSwitchAudioStream.init(localMedia: _localMedia, remoteMedia: remoteMedia) : nil)!
let videoStream: FMLiveSwitchVideoStream = ((self._localMedia!.videoTrack() != nil) ? FMLiveSwitchVideoStream.init(localMedia: self._localMedia, remoteMedia: remoteMedia) : nil)!
connection = _channel?.createMcuConnection(with: audioStream, videoStream: videoStream)
connection?.addOnStateChange({[weak self] (obj: Any!) in
let conn = obj as! FMLiveSwitchMcuConnection
let state = conn.state()
FMLiveSwitchLog.info(withMessage: "MCU connection \(String(describing: conn.id()!)) is currently in a \(String(describing: FMLiveSwitchConnectionStateWrapper(value: state).description()!)).")
if (state == FMLiveSwitchConnectionState.closing || state == FMLiveSwitchConnectionState.failing) {
if (conn.remoteClosed()) {
FMLiveSwitchLog.info(withMessage: "MCU connection \(String(describing: conn.id()!)) is closed by media server")
}
}
// Reconnect the stream if the connection has failed
else if (state == FMLiveSwitchConnectionState.failed) {
self?.OpenMcuConnection()
}
})
self._layoutManager?.add(onLayout: FMLiveSwitchAction1.init {(layout) -> Void in
if (self._mcuConnection != nil) {
FMLiveSwitchLayoutUtility.floatLocalPreview(
with: layout as? FMLiveSwitchLayout,
videoLayout: self._videoLayout,
localConnectionId: self._mcuConnection?.id(),
viewId: self._mcuViewId,
localViewSink: self._localMedia?.viewSink() as? NSObjectProtocol & FMLiveSwitchIViewSink)
}
})
connection?.open()
return connection!
}
private mcuConnection: fm.liveswitch.McuConnection;
private videoLayout: fm.liveswitch.VideoLayout;
private openMcuConnection(): fm.liveswitch.McuConnection {
// Create a remote media and add it to the layout.
const remoteMedia = new fm.liveswitch.RemoteMedia(true, true);
this.layoutManager.addRemoteMedia(remoteMedia);
// Create a audio stream and a video stream using local media and remote media.
const audioStream = new fm.liveswitch.AudioStream(this.localMedia, remoteMedia);
const videoStream = new fm.liveswitch.VideoStream(this.localMedia, remoteMedia);
// Create a MCU connection with audio and video streams.
const connection: fm.liveswitch.McuConnection = this.channel.createMcuConnection(audioStream, videoStream);
connection.addOnStateChange(conn => {
if (conn.getState() === fm.liveswitch.ConnectionState.Closing || conn.getState() === fm.liveswitch.ConnectionState.Failing) {
// Remove the remote media from the layout and destroy it when the connection is closing or failing.
this.layoutManager.removeRemoteMedia(remoteMedia);
remoteMedia.destroy();
} else if (conn.getState() === fm.liveswitch.ConnectionState.Failed) {
// Reconnect when the MCU connection failed.
this.mcuConnection = this.openMcuConnection();
}
});
// Overlay the local view on top of the received video layout.
this.layoutManager.addOnLayout(layout => {
if (this.mcuConnection != null) {
fm.liveswitch.LayoutUtility.floatLocalPreview(layout, this.videoLayout, this.mcuConnection.getId(), remoteMedia.getId(), this.localMedia.getViewSink());
}
});
connection.open();
return connection;
}
Handle Stream Connection
If the number of participants in a media session changes, the Media Server automatically changes how it lays out the video feeds in an MCU connection. To handle this, create an addOnMcuVideoLayout event handler to the channel instance. This event handler calls the Layout method of the layout manager and caches the returned VideoLayout instance.
Update the OnClientRegistered function in the HelloWorldLogic class to the following:
private void OnClientRegistered(Channel[] channels)
{
Channel = channels[0];
DisplayMessage($"Client {Client.Id} has successfully connected to channel {Channel.Id}, Hello World!");
Log.Info($"Client {Client.Id} has successfully connected to channel {Channel.Id}, Hello World!");
Channel.OnMcuVideoLayout += (videoLayout) =>
{
_VideoLayout = videoLayout;
if (_LayoutManager != null)
{
_Dispatcher.Invoke(() => _LayoutManager.Layout());
}
};
_McuConnection = openMcuConnection();
}
private void onClientRegistered(Channel[] channels) {
// Store our channel reference.
channel = channels[0];
// Add callback to re-layout based on Media Server Callbacks on layout.
channel.addOnMcuVideoLayout(vidLayout -> {
videoLayout = vidLayout;
if (layoutManager != null) {
handler.post(() -> layoutManager.layout());
}
});
// Open a new MCU connection.
mcuConnection = openMcuConnection();
}
private onClientRegistered(channels: fm.liveswitch.Channel[]): void {
// Store our channel reference.
this.channel = channels[0];
const msg = `Client ${this.client.getId()} has successfully connected to channel ${this.channel.getId()}, Hello World!`;
fm.liveswitch.Log.debug(msg);
this.displayMessage(msg);
// Set the local video layout when a new MCU video layout is received from server.
this.channel.addOnMcuVideoLayout(videoLayout => {
this.videoLayout = videoLayout;
this.layoutManager.layout();
});
// Open a new MCU connection.
this.mcuConnection = this.openMcuConnection();
}
Run Your App
Note
LiveSwitch recommends that you use a phone and not an emulator to run the mobile apps. It's possible to run the mobile apps on an emulator, but the camera doesn't work.
Run your app in your project IDE and click Join to join the conference. You should see video streaming on your app window. For an MCU connection, the Media Server composes local and remote views and sends them back to you. If you run your app on two devices, you should see that the local view and the remote view are displayed side by side. Your app UI should look similar to the following:
Congratulations, you've built a LiveSwitch conference app!
Go to your LiveSwitch Console's homepage, and select the Hello World Application. You should see one client, one channel, and one connection.
Go to your LiveSwitch Cloud's Dashboard page, and select the Hello World Application. You should see one client, one channel, and one connection.
If you run your app on multiple devices, you should see yourself and participants from other devices.