In this tutorial, you will learn how to do broadcasting.
LiveSwitch supports massive-scale broadcasting of audio and video data from SFU upstream connections to SFU downstream connections. In broadcasting, there is one Broadcaster and multiple Receivers.
This tutorial requires the SFU connection app or any apps you have built earlier on top of it.
Create Participant
Both the Broadcaster and Receiver follow the same registration process, we can create an abstract class Participant to handle registration and abstract the Broadcaster and the Receiver classes from it.
The Participant class is similar to the HelloWorldLogic class that handles registration, token generation, channel creation, and other shared fields. For broadcasting, we will need to add the following:
Media ID: We use an optional Media ID to identify the Broadcaster's media streams. We must use the same Media ID for Receivers when creating their downstream connection. The media ID replaces the remote connection info.
Disable remote client events: In a normal conference, everyone in the channel is notified when clients come and go, and when upstream connections are opened and closed. In broadcasting, this is unnecessary and negatively impacts the performance of the presentation.
Create the EstablishConnection, Start, and Stop methods for Broadcaster and Receiver to implement.
Note
When creating the Participant class, remember to replace the applicationId and sharedSecret with your own Application ID and Shared Secret, just as you did in the Hello World tutorial.
Paste the following code into the HelloWorld namespace in the Participant.cs file.
public abstract class Participant
{
public string ApplicationId = Config.ApplicationId;
public string ChannelId = Config.ChannelId;
public string GatewayURL = Config.GatewayURL;
public string SharedSecret = Config.SharedSecret;
// Client
private Client _Client;
protected Channel Channel;
// Media and UI
protected FM.LiveSwitch.Wpf.LayoutManager LayoutManager = null;
protected Dispatcher Dispatcher;
// Media Id for Broadcaster/Receiver
protected string PresentationId = "presentation-id";
// Make a registration request
public async Task JoinAsync()
{
_Client = new Client(GatewayURL, ApplicationId);
var claim = new ChannelClaim(ChannelId);
// To improve performance, disable remote client events
claim.DisableRemoteClientEvents = true;
ChannelClaim[] channelClaims = new[] { claim };
string token = Token.GenerateClientRegisterToken(
ApplicationId,
null,
null,
_Client.Id,
_Client.Roles,
channelClaims,
SharedSecret
);
var channels = await _Client.Register(token).AsTask();
OnClientRegistered(channels);
}
// Procedure to run once client is registered
private void OnClientRegistered(Channel[] channels)
{
Channel = channels[0];
EstablishConnection();
}
public async Task LeaveAsync()
{
if (_Client != null)
{
await _Client.Unregister();
Log.Info("Client has successfully unregistered.");
}
}
// EstablishConnection is for Broadcaster and Receiver to implement
public abstract void EstablishConnection();
public abstract Future<object> Start(MainWindow window);
public abstract Future<object> Stop();
}
Paste the following code to the Participant.java file.
public abstract class Participant {
private final String applicationId = Config.applicationId;
private final String channelId = Config.channelId;
private final String gatewayUrl = Config.gatewayUrl;
private final String sharedSecret = Config.sharedSecret;
// Client and channel
protected Client client;
protected Channel channel;
// Media ID, which will be used by both the broadcaster and receivers.
protected String presentationId = "presentation-id";
// Layout Manager
protected LayoutManager layoutManager;
// Context
protected final Context context;
public Participant(Context context) {
this.context = context.getApplicationContext();
}
public Channel getChannel() {
return channel;
}
public Client getClient() {
return client;
}
public Future<Channel[]> joinAsync() {
// Create a client.
client = new Client(gatewayUrl, applicationId);
// To improve performance, disable remote client events.
ChannelClaim claim = new ChannelClaim(channelId);
claim.setDisableRemoteClientEvents(false);
// Generate a token (do this on the server to avoid exposing your shared secret).
String token = Token.generateClientRegisterToken(applicationId, client.getUserId(), client.getDeviceId(), client.getId(), null, new ChannelClaim[]{claim}, sharedSecret);
// Register client with token.
return client.register(token).then(channels -> {
// Store our channel reference.
channel = channels[0];
establishConnection();
}, ex -> Log.error("ERROR: Client unable to register with the gateway.", ex));
}
public Future<Object> leaveAsync() {
if (this.client != null) {
return this.client.unregister().fail(ex -> {
Log.error("ERROR: Unable to unregister client.", ex);
});
}
return null;
}
protected abstract void establishConnection();
public abstract Future<Object> start(final Activity activity, final RelativeLayout container);
public abstract Future<Object> stop();
}
Paste the following code to the Participant.swift file.
class Participant : NSObject {
// Token information
var _applicationId: String = Config.applicationId
var _channelId: String = Config.channelId
var _gatewayUrl: String = Config.gatewayUrl
var _sharedSecret: String = Config.sharedSecret
// Client / Channel
var _client: FMLiveSwitchClient?
var _channel: FMLiveSwitchChannel?
// Media ID
var _presenterId: String = "presentation-id"
var _layoutManager: FMLiveSwitchCocoaLayoutManager?
override init() {
super.init()
}
func joinAsync() -> FMLiveSwitchFuture? {
// Instantiate the client
self._client = FMLiveSwitchClient.init(
gatewayUrl: _gatewayUrl,
applicationId: _applicationId)
let claim: FMLiveSwitchChannelClaim = FMLiveSwitchChannelClaim.init()
claim.setId(_channelId)
// Disable remote client events as it negatively impacts performance
claim.setDisableRemoteClientEvents(false);
let claims: NSMutableArray = []
claims.add(claim)
let token: String = FMLiveSwitchToken.generateClientRegister(
withApplicationId: _applicationId,
userId: (_client?.userId())!,
deviceId: (_client?.deviceId())!,
clientId: _client?.id(),
clientRoles: nil,
channelClaims: claims,
sharedSecret: _sharedSecret)
return _client?.register(withToken: token)?.then(resolveActionBlock: { [weak self] (obj: Any!) -> Void in
self!.onClientRegistered(obj: obj)
})?.fail(rejectActionBlock: { (e: NSException?) in
FMLiveSwitchLog.error(withMessage: "Client failed to register.", ex: e)
})
}
func onClientRegistered(obj: Any!) -> Void {
let channels = obj as! [FMLiveSwitchChannel]
self._channel = channels[0]
establishConnection()
}
func leaveAsync() -> FMLiveSwitchFuture {
if (self._client != nil) {
return (self._client?.unregister().then(resolveActionBlock: {(obj: Any!)-> Void in
FMLiveSwitchLog.info(withMessage: "client successfully unregistered!")
}).fail(rejectActionBlock: { (e: NSException?) in
FMLiveSwitchLog.error(withMessage: "client failed to unregister", ex: e)
}))!
} else {
let promise = FMLiveSwitchPromise()
promise?.resolve(withResult: nil)
return promise!
}
}
// Overridable methods
// https://cocoacasts.com/how-to-create-an-abstract-class-in-swift
func start(container: UIView) -> FMLiveSwitchFuture {
fatalError("Participant.start not implemented.")
}
func stop() -> FMLiveSwitchFuture {
fatalError("Participant.stop not implemented.")
}
func establishConnection() -> Void {
fatalError("Participant.establishConnection not implemented.")
}
}
Paste the following code into the HelloWorld namespace in the Participant.ts file.
export abstract class Participant {
private applicationId: string = Config.applicationId;
private channelId: string = Config.channelId;
private gatewayUrl: string = Config.gatewayUrl;
private sharedSecret: string = Config.sharedSecret;
// Client and channel
public client: fm.liveswitch.Client;
public channel: fm.liveswitch.Channel;
// Media Id, which will be used by both the broadcaster and receivers.
protected presentationId = "presentation-id";
// Layout manager
protected layoutManager = new fm.liveswitch.DomLayoutManager(document.getElementById("my-container"));
constructor() {
// Log to console.
fm.liveswitch.Log.registerProvider(new fm.liveswitch.ConsoleLogProvider(fm.liveswitch.LogLevel.Debug));
}
public joinAsync(): fm.liveswitch.Future<Object> {
const promise = new fm.liveswitch.Promise<Object>();
// Create the client.
this.client = new fm.liveswitch.Client(this.gatewayUrl, this.applicationId);
// Write registration state to log.
this.client.addOnStateChange(() => fm.liveswitch.Log.debug(`Client is ${new fm.liveswitch.ClientStateWrapper(this.client.getState())}.`));
// Generate a token (do this on the server to avoid exposing your shared secret).
const token: string = fm.liveswitch.Token.generateClientRegisterToken(this.applicationId, this.client.getUserId(), this.client.getDeviceId(), this.client.getId(), null, [new fm.liveswitch.ChannelClaim(this.channelId)], this.sharedSecret);
// Register client with token.
this.client.register(token)
.then(channels => {
// Store our channel reference.
this.channel = channels[0];
fm.liveswitch.Log.info(`Client ${this.client.getId()} has successfully connected to channel ${this.channel.getId()}, Hello World!`);
this.establishConnection();
promise.resolve(null);
})
.fail(ex => {
fm.liveswitch.Log.error("Failed to register with Gateway.");
promise.reject(ex);
});
return promise;
}
protected abstract establishConnection(): void;
public abstract start(): fm.liveswitch.Future<Object>;
public abstract stop(): fm.liveswitch.Future<Object>;
}
Create Broadcaster
The Broadcaster class sends an upstream connection to the server without receiving any remote connections. It essentially "broadcasts" its streams to its subscribers. We only need local media to make this class.
We create the class by the following:
Create SFU upstream connections by passing the Media ID into the CreateSfuUptreamConnection channel method.
Start and stop the broadcaster local media using the same method as starting and stopping the camera local media.
Paste the following code into the HelloWorld namespace in the Broadcaster.cs file.
public class Broadcaster : Participant
{
private LocalMedia _Media;
private static Broadcaster _Context;
public static Broadcaster Instance()
{
if (_Context == null)
{
_Context = new Broadcaster();
}
return _Context;
}
public override void EstablishConnection()
{
AudioStream audioStream = (_Media.AudioTrack != null) ? new AudioStream(_Media) : null;
VideoStream videoStream = (_Media.VideoTrack != null) ? new VideoStream(_Media) : null;
SfuUpstreamConnection connection = Channel.CreateSfuUpstreamConnection(audioStream, videoStream, PresentationId);
connection.OnStateChange += (conn) =>
{
Log.Info(string.Format("Upstream connection {0} is currently in a {1} state.", conn.Id, conn.State.ToString()));
if (conn.State == ConnectionState.Closing || conn.State == ConnectionState.Failing)
{
conn = null;
}
else if (conn.State == ConnectionState.Failed)
{
EstablishConnection();
}
};
connection.Open();
}
public override Future<object> Start(MainWindow window)
{
Promise<object> promise = new Promise<object>();
_Media = new CameraLocalMedia(false, false, new AecContext());
LayoutManager = new FM.LiveSwitch.Wpf.LayoutManager(window.videoContainer);
Dispatcher = window.Dispatcher;
_Media.Start().Then((result) =>
{
LayoutManager.SetLocalView(_Media.View);
promise.Resolve(null);
}).Fail((exception) =>
{
promise.Reject(exception);
});
return promise;
}
public override Future<object> Stop()
{
Promise<object> promise = new Promise<object>();
if (_Media != null)
{
_Media.Stop().Then((result) =>
{
if (LayoutManager != null)
{
LayoutManager.UnsetLocalView();
LayoutManager = null;
}
if (_Media != null)
{
_Media.Destroy();
_Media = null;
}
promise.Resolve(null);
}).Fail((exception) =>
{
promise.Reject(exception);
});
}
return promise;
}
}
Paste the following code to the Broadcaster.java file.
public class Broadcaster extends Participant {
private LocalMedia<View> media;
private static Broadcaster instance;
private Broadcaster(Context context) {
super(context);
}
public static Broadcaster getInstance(Context context) {
if (instance == null) {
instance = new Broadcaster(context);
}
return instance;
}
@Override
protected void establishConnection() {
// Create audio and video stream from local media.
AudioStream audioStream = (media.getAudioTrack() != null) ? new AudioStream(media) : null;
VideoStream videoStream = (media.getVideoTrack() != null) ? new VideoStream(media) : null;
// Create a SFU upstream connection with local audio and video and the presentation ID.
SfuUpstreamConnection connection = channel.createSfuUpstreamConnection(audioStream, videoStream, presentationId);
connection.addOnStateChange((ManagedConnection conn) -> {
Log.info(String.format("Upstream connection %s is 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 upstream connection %s.", conn.getId()));
}
} else if (connection.getState() == ConnectionState.Failed) {
// Reconnect if connection failed.
establishConnection();
}
});
connection.open();
}
@Override
public Future<Object> start(final Activity activity, final RelativeLayout container) {
final Promise<Object> promise = new Promise<>();
activity.runOnUiThread(() -> {
// Create a new local media with audio and video enabled.
media = new CameraLocalMedia(context, false, false, new AecContext());
// Set local media in the layout.
layoutManager = new LayoutManager(container);
layoutManager.setLocalView(media.getView());
// Start capturing local media.
media.start().then(result -> {
promise.resolve(null);
}).fail(promise::reject);
});
return promise;
}
@Override
public Future<Object> stop() {
final Promise<Object> promise = new Promise<>();
if (media == null) {
promise.resolve(null);
} else {
// Stop capturing local media.
media.stop().then(result -> {
if (layoutManager != null) {
// Remove views from the layout.
layoutManager.unsetLocalView();
layoutManager = null;
}
if (media != null) {
media.destroy();
media = null;
}
promise.resolve(null);
}).fail(promise::reject);
}
return promise;
}
}
Paste the following code to the Broadcaster.swift file.
class Broadcaster : Participant {
var _media: LocalMedia?
static let instance = Broadcaster()
override init() {
super.init()
}
override func establishConnection() {
var connection: FMLiveSwitchSfuUpstreamConnection?
let audioStream: FMLiveSwitchAudioStream = ((_media!.audioTrack() != nil) ? FMLiveSwitchAudioStream.init(localMedia: _media) : nil)!
let videoStream: FMLiveSwitchVideoStream = ((_media!.videoTrack() != nil) ? FMLiveSwitchVideoStream.init(localMedia: _media) : nil)!
connection = _channel?.createSfuUpstreamConnection(with: audioStream, videoStream: videoStream, mediaId: _presenterId)
connection?.addOnStateChange({[weak self] (obj: Any!) in
let conn = obj as! FMLiveSwitchSfuUpstreamConnection
let state = conn.state()
FMLiveSwitchLog.info(withMessage: "Upstream connection \(String(describing: conn.id()!)) is currently in a \(String(describing: FMLiveSwitchConnectionStateWrapper(value: state).description()!)).")
// Reconnect the stream if the connection has failed
if (state == FMLiveSwitchConnectionState.failed) {
self?.establishConnection()
}
})
connection?.open()
}
override func start(container: UIView) -> FMLiveSwitchFuture {
let promise = FMLiveSwitchPromise()
self._media = CameraLocalMedia(disableAudio: false, disableVideo: false, aecContext: nil)
self._layoutManager = FMLiveSwitchCocoaLayoutManager(container: container)
self._layoutManager!.setLocalView(self._media!.view())
self._media?.start()?.then(resolveActionBlock: {(obj: Any!) in
promise?.resolveAsync(withResult: obj as! NSObject)
}, rejectActionBlock: {(e: NSException?) in
promise?.reject(with: e)
})
return promise!
}
override func stop() -> FMLiveSwitchFuture {
let promise = FMLiveSwitchPromise()
if (self._media != nil) {
self._media?.stop()?.then(resolveActionBlock: {[weak self] (obj: Any?) in
if (self?._layoutManager != nil) {
DispatchQueue.main.async {
self?._layoutManager?.unsetLocalView()
self?._layoutManager = nil
if (self?._media != nil) {
self?._media?.destroy()
self?._media = nil
}
}
}
promise?.resolve(withResult: nil)
}, rejectActionBlock: {(e: NSException?) in
promise!.reject(with: e)
})
} else {
if (self._layoutManager != nil) {
DispatchQueue.main.async {
self._layoutManager?.removeRemoteViews()
self._layoutManager = nil
}
}
promise?.resolve(withResult: nil)
}
return promise!
}
}
Paste the following code into the HelloWorld namespace in the Broadcaster.ts file.
export class Broadcaster extends Participant {
private localMedia: fm.liveswitch.LocalMedia;
protected establishConnection(): void {
// Create a SFU upstream connection with local audio and video and the presentation ID.
const audioStream = new fm.liveswitch.AudioStream(this.localMedia);
const videoStream = new fm.liveswitch.VideoStream(this.localMedia);
const connection: fm.liveswitch.SfuUpstreamConnection = this.channel.createSfuUpstreamConnection(audioStream, videoStream, this.presentationId);
connection.open();
}
public start(): fm.liveswitch.Future<Object> {
const promise = new fm.liveswitch.Promise<Object>();
// Create local media with audio and video enabled.
const audioEnabled = true;
const videoEnabled = true;
this.localMedia = new fm.liveswitch.LocalMedia(audioEnabled, videoEnabled);
// Set local media in the layout.
this.layoutManager.setLocalMedia(this.localMedia);
// Start local media capturing.
this.localMedia.start()
.then(() => {
fm.liveswitch.Log.debug("Media capture started.");
promise.resolve(null);
})
.fail(ex => {
fm.liveswitch.Log.error(ex.message);
promise.reject(ex);
});
return promise;
}
public stop(): fm.liveswitch.Future<Object> {
const promise = new fm.liveswitch.Promise<Object>();
// Stop local media capturing.
this.localMedia.stop()
.then(() => {
fm.liveswitch.Log.debug("Media capture stopped.");
promise.resolve(null);
})
.fail(ex => {
fm.liveswitch.Log.error(ex.message);
promise.reject(ex);
});
return promise;
}
}
Create Receiver
For the Receiver class, we only expect a downstream connection from the server. It acts like a subscriber that "subscribes" to the Broadcaster streams. We only need the remote media class to make the class.
We create the class by the following:
Create SFU downstream connections by passing the Media ID into the CreateSfuDownstreamConnection channel method. This enables users to open their connections prior to receiving the streams. When using Media ID, we don't rely on channel event handlers.
Paste the following code into the HelloWorld namespace in the Receiver.cs file.
public class Receiver : Participant
{
RemoteMedia media;
private static Receiver _Context;
public static Receiver Instance()
{
if(_Context == null)
{
_Context = new Receiver();
}
return _Context;
}
public override void EstablishConnection()
{
AudioStream audioStream = new AudioStream(media);
VideoStream videoStream = new VideoStream(media);
SfuDownstreamConnection connection = Channel.CreateSfuDownstreamConnection(PresentationId, audioStream, videoStream);
LayoutManager.AddRemoteView(media.Id, media.View);
connection.OnStateChange += (conn) =>
{
Log.Info(string.Format("Downstream connection {0} is currently in a {1} state.", conn.Id, conn.State.ToString()));
if (conn.State == ConnectionState.Closing || conn.State == ConnectionState.Failing)
{
var layoutManager = LayoutManager;
if (layoutManager != null)
{
layoutManager.RemoveRemoteView(media.Id);
}
Dispatcher.Invoke(new Action(() =>
{
media.Destroy();
}));
}
else if (conn.State == ConnectionState.Failed)
{
EstablishConnection();
}
};
connection.Open();
}
public override Future<object> Start(MainWindow window)
{
Promise<object> promise = new Promise<object>();
LayoutManager = new FM.LiveSwitch.Wpf.LayoutManager(window.videoContainer);
media = new RemoteMedia(false, false, new AecContext());
Dispatcher = window.Dispatcher;
promise.Resolve(null);
return promise;
}
public override Future<object> Stop()
{
Promise<object> promise = new Promise<object>();
if(LayoutManager != null)
{
LayoutManager.RemoveRemoteViews();
LayoutManager = null;
}
return promise;
}
}
Paste the following code to the Receiver.java file.
public class Receiver extends Participant {
private RemoteMedia media;
private final Handler handler;
private static Receiver instance;
private Receiver(Context context) {
super(context);
this.handler = new Handler(context.getMainLooper());
}
public static Receiver getInstance(Context context) {
if (instance == null) {
instance = new Receiver(context);
}
return instance;
}
@Override
protected void establishConnection() {
// Create remote media.
media = new RemoteMedia(context, false, false, new AecContext());
VideoStream videoStream = new VideoStream(media);
AudioStream audioStream = new AudioStream(media);
// Create a SFU downstream connection with remote audio and video and the presentation ID.
SfuDownstreamConnection connection = channel.createSfuDownstreamConnection(presentationId, audioStream, videoStream);
// Adding remote view to UI.
handler.post(() -> layoutManager.addRemoteView(media.getId(), media.getView()));
connection.addOnStateChange((ManagedConnection conn) -> {
Log.info(String.format("Downstream 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 downstream connection %s.", conn.getId()));
}
handler.post(() -> {
// Remove the remote media from the layout if the remote is closed.
layoutManager.removeRemoteView(media.getId());
media.destroy();
});
} else if (conn.getState() == ConnectionState.Failed) {
// Reconnect if connection failed.
establishConnection();
}
});
connection.open();
}
@Override
public Future<Object> start(final Activity activity, final RelativeLayout container) {
final Promise<Object> promise = new Promise<>();
activity.runOnUiThread(() -> {
layoutManager = new LayoutManager(container);
promise.resolve(null);
});
return promise;
}
@Override
public Future<Object> stop() {
if (layoutManager != null) {
layoutManager.removeRemoteViews();
layoutManager = null;
}
return Promise.resolveNow();
}
}
Paste the following code to the Receiver.swift file.
class Receiver : Participant {
var _media: RemoteMedia?
static let instance = Receiver()
override init() {
super.init()
}
override func establishConnection() {
let audioStream: FMLiveSwitchAudioStream = (_media?.audioTrack() != nil ? FMLiveSwitchAudioStream.init(remoteMedia: _media) : nil)!
let videoStream: FMLiveSwitchVideoStream = (_media?.videoTrack() != nil ? FMLiveSwitchVideoStream.init(remoteMedia: _media) : nil)!
let connection = _channel?.createSfuDownstreamConnection(withRemoteMediaId: _presenterId, audioStream: audioStream, videoStream: videoStream)
DispatchQueue.main.async {
self._layoutManager?.addRemoteView(withId: self._media?.id(), view: self._media?.view())
}
connection?.addOnStateChange({[weak self] (obj: Any!) in
let conn = obj as! FMLiveSwitchSfuDownstreamConnection
let state = conn.state()
FMLiveSwitchLog.info(withMessage: "Downstream 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: "Downstream connection \(String(describing: conn.id()!)) is closed by media server.")
}
if (self?._layoutManager != nil) {
DispatchQueue.main.async {
self?._layoutManager?.removeRemoteView(withId: self?._media?.id())
}
}
// Taking down the audio session from the remote media for our local media
// https://forums.developer.apple.com/thread/22133
// https://trac.pjsip.org/repos/ticket/1697
// Workaround to fix reduced volume issue after the teardown of audio unit.
do {
if #available(iOS 10.0, *) {
try AVAudioSession.sharedInstance().setCategory(.record, mode: .default)
} else {
AVAudioSession.sharedInstance().perform(NSSelectorFromString("setCategory:error:"), with: AVAudioSession.Category.record)
}
} catch {
FMLiveSwitchLog.error(withMessage: "Could not set audio session category for local media.")
}
self?._media!.destroy()
do {
if #available(iOS 10.0, *) {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, (AVAudioSession.CategoryOptions.defaultToSpeaker)])
} else {
AVAudioSession.sharedInstance().perform(NSSelectorFromString("setCategory:withOptions:error:"),with: AVAudioSession.Category.playAndRecord, with: [.allowBluetooth, AVAudioSession.CategoryOptions.defaultToSpeaker])
}
} catch {
FMLiveSwitchLog.error(withMessage: "Could not set audio session category for local media.")
}
}
})
connection?.open()
}
override func start(container: UIView) -> FMLiveSwitchFuture {
let promise = FMLiveSwitchPromise()
self._media = RemoteMedia(disableAudio: false, disableVideo: false, aecContext: nil)
self._layoutManager = FMLiveSwitchCocoaLayoutManager(container: container)
promise?.resolve(withResult: nil)
return promise!
}
override func stop() -> FMLiveSwitchFuture {
let promise = FMLiveSwitchPromise()
if (self._layoutManager != nil) {
DispatchQueue.main.async {
self._layoutManager?.removeRemoteViews()
self._layoutManager = nil
}
}
promise?.resolve(withResult: nil)
return promise!
}
}
Paste the following code into the HelloWorld namespace in the Receiver.ts file.
export class Receiver extends Participant {
private remoteMedia: fm.liveswitch.RemoteMedia;
protected establishConnection(): void {
// Create remote media.
this.remoteMedia = new fm.liveswitch.RemoteMedia();
const audioStream = new fm.liveswitch.AudioStream(this.remoteMedia);
const videoStream = new fm.liveswitch.VideoStream(this.remoteMedia);
// Add remote media to the layout.
this.layoutManager.addRemoteMedia(this.remoteMedia);
// Create a SFU downstream connection with remote audio and video and the presentation ID.
const connection: fm.liveswitch.SfuDownstreamConnection = this.channel.createSfuDownstreamConnection(this.presentationId, audioStream, videoStream);
connection.addOnStateChange(conn => {
// Remove the remote media from the layout if the remote is closed.
if (conn.getRemoteClosed()) {
this.layoutManager.removeRemoteMedia(this.remoteMedia);
}
});
connection.open();
}
// Not needed because receiver only receives media from the broadcaster.
public start(): fm.liveswitch.Future<Object> {
return fm.liveswitch.Promise.resolveNow();
}
// Not needed because receiver only receives media from the broadcaster.
public stop(): fm.liveswitch.Future<Object> {
return fm.liveswitch.Promise.resolveNow();
}
}
Uncomment UI Components
Now go to the files for the UI components and uncomment the codes for broadcasting.