Skip to main content
Version: v1.3.1

Add Multiple Hosts with the Broadcast SDK

The Web broadcast SDK provides the interfaces required to broadcast media from multiple distinct devices on Web. To support multiple hosts, the SDK introduces the following operations:

  • Join a stage
  • Publish media to other participants in the stage
  • Subscribe to media from other participants in the stage
  • Manage and monitor video and audio published to the stage
  • Get WebRTC statistics for each peer connection

In addition to these new operations, the stage package supports all the operations from the Amazon IVS Web Broadcast SDK.

Getting Started with Multiple Hosts

To add the Amazon IVS Web broadcast library with support for multiple hosts to your Web development environment, upgrade your script (Using a Script Tag) or package.json (Using NPM) to the latest version.

Terminology

Throughout this section of the guide, there are some key terms to understand:

  • Stage — The main point of interaction between the application and SDK.
  • Strategy — An easy way to communicate the desired state of the stage.
  • Participant — An entity that can either publish or subscribe to other participants in the stage.
  • Stream — A container for a participant’s audio or video.
  • Subscribe — An action taken by a participant to receive media (audio/video) of another participant.
  • Publish — An action taken by a participant to send media (audio/video) to another participant.

Imports

The building blocks for multiple hosts are located in a different namespace than the root broadcasting modules.

Using a Script Tag

Using the same script imports, the classes and enums defined in the examples below can be found on the global object IVSBroadcastClient:

const { Stage, SubscribeType } = IVSBroadcastClient;

Using NPM

The classes, enums, and types also can be imported from the package module:

import { Stage, SubscribeType } from 'amazon-ivs-web-broadcast'

Concepts

Three core concepts underlie multiple-host functionality: stage, strategy, and events. The design goal is minimizing the amount of client-side logic necessary to build a working product.

Stage

The Stage class is the main point of interaction between the host application and the SDK. It represents the stage itself and is used to join and leave the stage. Creating and joining a stage requires a valid, unexpired token string from the control plane (represented as token). Joining and leaving a stage are simple:

// JS
const stage = new Stage(token, strategy)

try {
await stage.join();
} catch (error) {
// handle join exception
}

stage.leave();

// TS
const stage = new Stage(token: string, strategy: StageStrategy)

try {
await stage.join();
} catch (error: Error) {
// handle join exception
}

stage.leave();

Strategy

The StageStrategy interface provides a way for the host application to communicate the desired state of the stage to the SDK. Three functions need to be implemented: shouldSubscribeToParticipant, shouldPublishParticipant, and stageStreamsToPublish. All are discussed below.

To use a defined strategy, pass it to the Stage constructor. The following is a complete example of an application using a strategy to publish a participant's webcam to the stage and subscribe to all participants. Each required strategy function's purpose is explained in detail in the subsequent sections.

// JS
const devices = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
const myAudioTrack = new LocalStageStream(devices.getAudioTracks()[0]);
const myVideoTrack = new LocalStageStream(devices.getVideoTracks()[0]);

// Define the stage strategy, implementing required functions
const strategy = {
audioTrack: myAudioTrack,
videoTrack: myVideoTrack,

// optional
updateTracks(newAudioTrack, newVideoTrack) {
this.audioTrack = newAudioTrack;
this.videoTrack = newVideoTrack;
},

// required
stageStreamsToPublish() {
return [this.audioTrack, this.videoTrack];
},

// required
shouldPublishParticipant(participant) {
return true;
},

// required
shouldSubscribeToParticipant(participant) {
return SubscribeType.AUDIO_VIDEO;
}
};

// Initialize the stage and start publishing
const stage = new Stage(token, strategy);
await stage.join();


// To update later (e.g. in an onClick event handler)
strategy.updateTracks(myNewAudioTrack, myNewVideoTrack);
stage.refreshStrategy();


// TS
const devices = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
const myAudioTrack = new LocalStageStream(devices.getAudioTracks()[0]);
const myVideoTrack = new LocalStageStream(devices.getVideoTracks()[0]);

// Define the stage strategy, implementing required functions
const strategy: StageStrategy = {
audioTrack: myAudioTrack,
videoTrack: myVideoTrack,

// optional
updateTracks(newAudioTrack: LocalStageStream, newVideoTrack: LocalStageStream): void {
this.audioTrack = newAudioTrack;
this.videoTrack = newVideoTrack;
},

// required
stageStreamsToPublish(): LocalStageStream[] {
return [this.audioTrack, this.videoTrack];
},

// required
shouldPublishParticipant(participant: StageParticipantInfo): boolean {
return true;
},

// required
shouldSubscribeToParticipant(participant: StageParticipantInfo): SubscribeType {
return SubscribeType.AUDIO_VIDEO;
}
};

// Initialize the stage and start publishing
const stage = new Stage(token, strategy);
await stage.join();


// To update later (e.g. in an onClick event handler)
strategy.updateTracks(myNewAudioTrack, myNewVideoTrack);
stage.refreshStrategy();

Subscribing to Participants

shouldSubscribeToParticipant(participant: StageParticipantInfo): SubscribeType

When a remote participant joins the stage, the SDK queries the host application about the desired subscription state for that participant. The options are NONE, AUDIO_ONLY, and AUDIO_VIDEO. When returning a value for this function, the host application does not need to worry about the publish state, current subscription state, or stage connection state. If AUDIO_VIDEO is returned, the SDK waits until the remote participant is publishing before it subscribes, and it updates the host application by emitting events throughout the process.

Here is a sample implementation:

// JS
const strategy = {

shouldSubscribeToParticipant: (participant) => {
return SubscribeType.AUDIO_VIDEO;
}

// ... other strategy functions
}

// TS
const strategy: StageStrategy = {

shouldSubscribeToParticipant: (participant: StageParticipantInfo): SubscribeType => {
return SubscribeType.AUDIO_VIDEO;
}

// ... other strategy functions
}

This is the complete implementation of this function for a host application that always wants all participants to see each other; e.g., a video chat application.

More advanced implementations also are possible. Use the userInfo property on ParticipantInfo to selectively subscribe to participants based on server-provided attributes:

// JS
const strategy = {

shouldSubscribeToParticipant(participant) {
switch (participant.info.userInfo) {
case 'moderator':
return SubscribeType.NONE;
case 'guest':
return SubscribeType.AUDIO_VIDEO;
default:
return SubscribeType.NONE;
}
}
// . . . other strategies properties
}


// TS
const strategy: StageStrategy = {

shouldSubscribeToParticipant: (participant: StageParticipantInfo): SubscribeType => {
switch (participant.info.userInfo) {
case 'moderator':
return SubscribeType.NONE;
case 'guest':
return SubscribeType.AUDIO_VIDEO;
default:
return SubscribeType.NONE;
}
}
// ... other strategies properties
}

This can be used to create a stage where moderators can monitor all guests without being seen or heard themselves. The host application could use additional business logic to let moderators see each other but remain invisible to guests.

Publishing

shouldPublishParticipant(participant: StageParticipantInfo): boolean

Once connected to the stage, the SDK queries the host application to see if a particular participant should publish. This is invoked only on local participants that have permission to publish based on the provided token.

Here is a sample implementation:

// JS
const strategy = {

shouldPublishParticipant: (participant) => {
return true;
}

// . . . other strategies properties
}

// TS
const strategy: StageStrategy = {

shouldPublishParticipant: (participant: StageParticipantInfo): boolean => {
return true;
}

// . . . other strategies properties
}

This is for a standard video chat application where users always want to publish. They can mute and unmute their audio and video, to instantly be hidden or seen/heard. (They also can use publish/unpublish, but that is much slower. Mute/unmute is preferable for use cases where changing visibility often is desirable.)

Choosing Streams to Publish

stageStreamsToPublish(): LocalStageStream[];

When publishing, this is used to determine what audio and video streams should be published. This is covered in more detail later in Publish a Media Stream.

Updating the Strategy

The strategy is intended to be dynamic: the values returned from any of the above functions can be changed at any time. For example, if the host application does not want to publish until the end user taps a button, you could return a variable from shouldPublishParticipant (something like hasUserTappedPublishButton). When that variable changes based on an interaction by the end user, call stage.refreshStrategy() to signal to the SDK that it should query the strategy for the latest values, applying only things that have changed. If the SDK observes that the shouldPublishParticipant value has changed, it starts the publish process. If the SDK queries and all functions return the same value as before, the refreshStrategy call does not modify the stage.

If the return value of shouldSubscribeToParticipant changes from AUDIO_VIDEO to AUDIO_ONLY, the video stream is removed for all participants with changed returned values, if a video stream existed previously.

Generally, the stage uses the strategy to most efficiently apply the difference between the previous and current strategies, without the host application needing to worry about all the state required to manage it properly. Because of this, think of calling stage.refreshStrategy() as a cheap operation, because it does nothing unless the strategy changes.

Events

A Stage instance is an event emitter. Using stage.on(), the state of the stage is communicated to the host application. Updates to the host application’s UI usually can be supported entirely by the events. The events are as follows:

// JS
stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_LEFT, (participant) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_PUBLISH_STATE_CHANGED, (participant, state) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_SUBSCRIBE_STATE_CHANGED, (participant, state) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (participant, streams) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_REMOVED, (participant, streams) => {})
stage.on(StageEvents.STAGE_STREAM_MUTE_CHANGED, (participant, stream) => {})

// TS
stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state: StageConnectionState) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant: StageParticipantInfo) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_LEFT, (participant: StageParticipantInfo) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_PUBLISH_STATE_CHANGED, (participant: StageParticipantInfo,state: StageParticipantPublishState) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_SUBSCRIBE_STATE_CHANGED, (participant: StageParticipantInfo,state: StageParticipantSubscribeState) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (participant: StageParticipantInfo,streams: StageStream[]) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_REMOVED, (participant: StageParticipantInfo,streams: StageStream[]) => {})
stage.on(StageEvents.STAGE_STREAM_MUTE_CHANGED, (participant: StageParticipantInfo, stream: StageStream) => {})

For most of these events, the corresponding ParticipantInfo is provided.

It is not expected that the information provided by the events impacts the return values of the strategy. For example, the return value of shouldSubscribeToParticipant is not expected to change when STAGE_PARTICIPANT_PUBLISH_STATE_CHANGED is called. If the host application wants to subscribe to a particular participant, it should return the desired subscription type regardless of that participant’s publish state. The SDK is responsible for ensuring that the desired state of the strategy is acted on at the correct time based on the state of the stage.

Publish a Media Stream

Local devices like microphones and cameras are retrieved using the same steps as outlined above in Retrieve a MediaStream from a Device. In the example we use MediaStream to create a list of LocalStageStream objects used for publishing by the SDK:

// JS
try {
// Get stream using steps outlined in document above
const stream = await getMediaStreamFromDevice();

let streamsToPublish = stream.getTracks().map(track => {
new LocalStageStream(track)
});

// Create stage with strategy, or update existing strategy
const strategy = {
stageStreamsToPublish: () => streamsToPublish
}
}

// TS
try {
// Get stream using steps outlined in document above
const stream = await getMediaStreamFromDevice();

let streamsToPublish = stream.getTracks().map(track => {
new LocalStageStream(track)
});

// Create stage with strategy, or update existing strategy
const strategy: StageStrategy = {
stageStreamsToPublish: () => streamsToPublish
}
}

Publish a Screenshare

Applications often need to publish a screenshare in addition to the user's web camera. Publishing a screenshare necessitates creating an additional Stage with its own unique token.

// JS
// Invoke the following lines to get the screenshare's tracks
const media = await navigator.mediaDevices.getDisplayMedia();
const screenshare = { videoStream: new LocalStageStream(media.getVideoTracks()[0]) };
const screenshareStrategy = {
stageStreamsToPublish: () => {
return [screenshare.videoStream];
},
shouldPublishParticipant: (participant) => {
return true;
},
shouldSubscribeToParticipant: (participant) => {
return SubscribeType.AUDIO_VIDEO;
}
}
const screenshareStage = new Stage(screenshareToken, screenshareStrategy);
await screenshareStage.join();


// TS
const media = await navigator.mediaDevices.getDisplayMedia();
const screenshare = { videoStream: new LocalStageStream(media.getVideoTracks()[0]) };
const screenshareStrategy: StageStrategy = {
stageStreamsToPublish: () => {
return [screenshare.videoStream];
},
shouldPublishParticipant: (participant: StageParticipantInfo) => {
return true;
},
shouldSubscribeToParticipant: (participant: StageParticipantInfo) => {
return SubscribeType.AUDIO_VIDEO;
}
}
const screenshareStage = new Stage(screenshareToken, screenshareStrategy);
await screenshareStage.join();

Display and Remove Participants

After subscribing is completed, you receive an array of StageStream objects through the STAGE_PARTICIPANT_STREAMS_ADDED event. The event also gives you participant info to help when displaying media streams:

// JS
stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (participant, streams) => {
const streamsToDisplay = streams;

if (participant.isLocal) {
// Ensure to exclude local audio streams, otherwise echo will occur
streamsToDisplay = streams.filter(stream => stream.streamType === StreamType.VIDEO)
}

// Create or find video element already available in your application
const videoEl = getParticipantVideoElement(participant.id);

// Attach the participants streams
videoEl.srcObject = new MediaStream();
streamsToDisplay.forEach(stream => videoEl.srcObject.addTrack(stream.mediaStreamTrack));
})

// TS
stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (participant: StageParticipantInfo, streams: StageStream[]) => {
const streamsToDisplay = streams;

if (participant.isLocal) {
// Ensure to exclude local audio streams, otherwise echo will occur
streamsToDisplay = streams.filter(stream => stream.streamType === StreamType.VIDEO)
}

// Create or find video element already available in your application
const videoEl: HTMLVideoElement = getParticipantVideoElement(participant.id);

// Attach the participants streams
videoEl.srcObject = new MediaStream();
streamsToDisplay.forEach(stream => videoEl.srcObject.addTrack(stream.mediaStreamTrack));
})

When a participant stops publishing or is unsubscribed from a stream, the STAGE_PARTICIPANT_STREAMS_REMOVED function is called with the streams that were removed. Host applications should use this as a signal to remove the participant’s video stream from the DOM.

STAGE_PARTICIPANT_STREAMS_REMOVED is invoked for all scenarios in which a stream might be removed, including:

  • The remote participant stops publishing.
  • A local device unsubscribes or changes subscription from AUDIO_VIDEO to AUDIO_ONLY.
  • The remote participant leaves the stage.
  • The local participant leaves the stage.

Because STAGE_PARTICIPANT_STREAMS_REMOVED is invoked for all scenarios, no custom business logic is required around removing participants from the UI during remote or local leave operations.

Broadcast the Stage

To broadcast a stage, create a separate IVSBroadcastClient session and then follow the usual instructions for broadcasting with the SDK, described above. The list of StageStream exposed via STAGE_PARTICIPANT_STREAMS_ADDED can be used to retrieve the participant media streams which can be applied to the broadcast stream composition, as follows:

// JS
// Setup client with preferred settings
const broadcastClient = getIvsBroadcastClient();

stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (participant, streams) => {
streams.forEach(stream => {
const inputStream = new MediaStream([stream.mediaStreamTrack]);
switch (stream.streamType) {
case StreamType.VIDEO:
broadcastClient.addVideoInputDevice(inputStream, `video-${participantInfo.id}`, {
index: DESIRED_LAYER,
width: MAX_WIDTH,
height: MAX_HEIGHT
});
break;
case StreamType.AUDIO:
broadcastClient.addAudioInputDevice(inputStream, `audio-${participantInfo.id}`);
break;
}
})

})

// TS
// Setup client with preferred settings
const broadcastClient = getIvsBroadcastClient();

stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (participant: StageParticipantInfo, streams: StageStream[]) => {
streams.forEach(stream => {
const inputStream = new MediaStream([stream.mediaStreamTrack]);
switch (stream.streamType) {
case StreamType.VIDEO:
broadcastClient.addVideoInputDevice(inputStream, `video-${participantInfo.id}`, {
index: DESIRED_LAYER,
width: MAX_WIDTH,
height: MAX_HEIGHT
});
break;
case StreamType.AUDIO:
broadcastClient.addAudioInputDevice(inputStream, `audio-${participantInfo.id}`);
break;
}
})
})

Mute and Unmute Media Streams

LocalStageStream objects have a setMuted function that controls whether the stream is muted. This function can be called on the stream before or after it is returned from the stageStreamsToPublish strategy function.

Important: If a new LocalStageStream object instance is returned by stageStreamsToPublish after a call to refreshStrategy, the mute state of the new stream object is applied to the stage. Be careful when creating new LocalStageStream instances to make sure the expected mute state is maintained.

Monitor Remote Participant Media Mute State

When participants change the mute state of their video or audio, the STAGE_STREAM_MUTE_CHANGED event is triggered with a list of streams that have changed. Use the getMuted method on StageStream to update your UI accordingly:

// JS
stage.on(StageEvents.STAGE_STREAM_MUTE_CHANGED, (participant, stream) => {
const muted = stream.getMuted();

// handle UI changes
})

// TS
stage.on(StageEvents.STAGE_STREAM_MUTE_CHANGED, (participant: StageParticipantInfo, stream: StageStream) => {
const muted = stream.getMuted();

// handle UI changes
})

Get WebRTC Statistics

To get the latest WebRTC statistics for a publishing stream or subscribing stream, use getStats on StageStream. This is an asynchronous method with which you can retrieve statistics either via await or by chaining a promise. The result is an RTCStatsReport which is a dictionary containing all standard statistics.

// JS
try {
const stats = await stream.getStats();
} catch (error) {
// Unable to retrieve stats
}

// TS
try {
const stats = await stream.getStats();
} catch (error: Error) {
// Unable to retrieve stats
}

Optimizing Media

It's recommended to limit getUserMedia and getDisplayMedia calls to the following constraints for the best performance:

const CONSTRAINTS = {
maxResolution: {
width: 1280, // Note: flip width and height values if portrait is desired
height: 720,
},
maxFramerate: 30,
maxBitrate: 2500,
};

Get Participant Attributes

If you specify attributes in the CreateParticipantToken endpoint request, you can see the attributes in StageParticipantInfo properties:

// JS
stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant) => {
console.log(`Participant ${participant.id} info:`, participant.attributes);
})

// TS
stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant: StageParticipantInfo) => {
console.log(`Participant ${participant.id} info:`, participant.attributes);
})

Handling Network Issues

When the local device’s network connection is lost, the SDK internally tries to reconnect without any user action. In some cases, the SDK is not successful and user action is needed.

Broadly the state of the stage can be handled via the STAGE_CONNECTION_STATE_CHANGED event:

// JS
stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state) => {
switch (state) {
case StageConnectionState.DISCONNECTED:
// handle disconnected UI
break;
case StageConnectionState.CONNECTING:
// handle establishing connection UI
break;
case StageConnectionState.CONNECTED:
// SDK is connected to the Stage
break;
case StageConnectionState.ERRORED:
// unrecoverable error detected, please re-instantiate
Break;
})

// TS
stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state: StageConnectionState) => {
switch (state) {
case StageConnectionState.DISCONNECTED:
// handle disconnected UI
break;
case StageConnectionState.CONNECTING:
// handle establishing connection UI
break;
case StageConnectionState.CONNECTED:
// SDK is connected to the Stage
break;
case StageConnectionState.ERRORED:
// unrecoverable error detected, please re-instantiate
Break;
})

In general, encountering errors after successfully joining a stage indicates that the SDK lost the connection and was unsuccessful in reestablishing a connection. Create a new Stage object and try to join when network conditions improve.