The official Spaces SDK from Mux. Add real-time video to your apps!
npm i @mux/spaces-web
yarn add @mux/spaces-web
This section will discuss some core concepts in Mux Spaces.
A Mux Space is where participants gather. After joining a Space the LocalParticipant can publish media such as audio and video. As well as subscribe to other participant's published media in the form of tracks. After joining a Space, Space Events get triggered as events occur.
A Participant is someone who has joined a Space. They know about other participants in the Space and can publish media for other participants to subscribe to. In the SDK we differentiate between a LocalParticipant and RemoteParticipant because a LocalParticipant can publish their own tracks, mute themselves and unpublish tracks. Participant Events get triggered as events related to each participant occurs.
A Track is audio or video media that is published and subscribed to. This usually comes in the form of webcam video, microphone audio, or screen share audio and video. Tracks emit Track Events as events occur.
In Mux Spaces events are emitted to give you access to state updates. These events are emitted on 4 main classes. Space, LocalParticipant, RemoteParticipant and Track. This is done to make working with frameworks like React easier. For example, when a participant joins it would make sense to render a component that contains that participant. Then inside that component you can listen to the individual participant events.
Note: That it is possible to listen to the same event mutliple times. Some scenarios of this are outlined below. Take care to ensure that you are listening to these events as you intend.
Space emits SpaceEvent. These are all events that can occur during the lifecycle of a space. It is sufficient to only listen to events on the space. Other classes emit events specific to those classes to give you more fine grained control.
Listen to Space events like so:
space
.on(SpaceEvent.ParticipantJoined, (participant) => {})
.on(SpaceEvent.ParticipantLeft, (participant) => {})
.on(SpaceEvent.ParticipantTrackPublished, (participant, track) => {})
.on(SpaceEvent.ParticipantTrackUnpublished, (participant, track) => {})
...
RemoteParticipant and LocalParticipant emit ParticipantEvent. These are all of the events that are specific to a participant.
Listen to participants events like so:
space.on(SpaceEvent.ParticipantJoined, (participant) => {
participant
.on(ParticipantEvent.TrackMuted, (track) => {})
.on(ParticipantEvent.TrackUnmuted, (track) => {})
.on(ParticipantEvent.TrackPublished, (track) => {})
.on(ParticipantEvent.TrackUnpublished, (track) => {})
...
})
Note: the following scenario:
space
.on(SpaceEvent.ParticipantJoined, (participant) => {
participant.on(ParticipantEvent.TrackMuted, (track) => {});
})
.on(SpaceEvent.ParticipantTrackMuted, (participant, track) => {});
In this case you will be listening for the same event in two places. When a participant mutes a track both of these callbacks will be called. Make sure you handle this appropriately.
TrackEvents get fired on Track objects.
Listen to track events like so:
space.on(SpaceEvent.ParticipantJoined, (participant) => {
participant.on(ParticipantEvent.TrackPublished, (track) => {
track.on(TrackEvent.TrackMuted, () => {});
});
});
Note: the following scenario:
space.on(SpaceEvent.ParticipantJoined, (participant) => {
participant
.on(ParticipantEvent.TrackPublished, (track) => {
track.on(TrackEvent.TrackMuted, () => {});
})
.on(ParticipantEvent.TrackMuted, (track) => {});
});
Similar to the scenario in the Participant Events section you will be listening to the same event twice. When a participant mutes their track both callbacks will be fired.
In Spaces there is the notion of subscriptions. In this context "subscribing" to a remote track
means you will start receiving the media associated with the track. "Unsubscribing" from a track
means you will no longer receive media for that track. It is important to distinguish between when
a track is published and when you subscribe to it. The RemoteTrack.track property will be undefined
until you receive a SpaceEvent.ParticipantTrackSubscribed or ParticipantEvent.TrackSubscribed event
for that track. Here is what that looks like in practice:
space
.on(SpaceEvent.ParticipantTrackPublished, (participant, track) => {
console.log(track.track); //undefined
})
.on(SpaceEvent.ParticipantTrackSubscribed, (participant, track) => {
console.log(track.track); //MediaStreamTrack
});
The SDK uses one of two subscription modes for an instance of a Space: a SubscriptionMode.Automatic or a SubscriptionMode.Manual mode. By default, the SDK operates under the SubscriptionMode.Automatic mode and subscribes you to up to 20 participants based on factors such as who is currently speaking and screen sharing. To learn how to control this behavior, refer to the subsections below.
By default, an instance of a Space operates under the SubscriptionMode.Automatic mode, which works by subscribing to a maximum of 20 participants which have the highest priority server-side. For example, recently speaking and non-muted participants have higher priority than non-speaking and muted participants, and you will automatically be subscribed to such participants if you are unsubscribed.
You have the option to set the automaticParticipantLimit
through SpaceOptionsParams when instantiating a Space:
import { Space, SubscriptionMode } from '@mux/spaces-web';
// Initialize Space using Automatic mode and subscribe to 5 participants
const space = new Space(JWT, {
subscriptionMode: SubscriptionMode.Automatic,
automaticParticipantLimit: 5,
});
If you need custom subscription logic, you may programmatically use the SubscriptionMode.Manual mode to subscribe to a RemoteParticipant using the RemoteParticipant.subscribe and RemoteParticipant.unsubscribe methods:
import { Space, RemoteParticipant, SubscriptionMode, SpaceEvent } from '@mux/spaces-web';
let alice: RemoteParticipant;
const aliceUserId = 'some-uuid';
// Initialize Space using Manual mode
const space = new Space(JWT, {
subscriptionMode: SubscriptionMode.Manual,
});
space.on(SpaceEvent.ParticipantJoined, async (participant) => {
// Manually subscribing to participant
if (participant.id === aliceUserId) {
await participant.subscribe();
alice = participant;
}
// Manually unsubscribing from the participant after another participant joins
if (alice && alice.subscribed) {
await alice.unsubscribe();
}
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Spaces Demo</title>
<script type="module" src="index.js"></script>
</head>
<body>
<h1>Controls</h1>
<button id="join-button">Join Space</button>
<h1>Local Video</h1>
<video id="local-video" autoplay controls muted style="width: 400px"></video>
<h1>Remote Videos</h1>
<div id="remote-videos"></div>
</body>
</html>
import { getUserMedia, Space, SpaceEvent } from '@mux/spaces-web';
const localParticipantEl = document.getElementById('local-video');
const participantsEl = document.getElementById('remote-videos');
// Add a callback for the "Join" button
document.getElementById('join-button').addEventListener('click', (e) => {
e.target.disabled = true;
join();
});
async function join() {
// Get camera and microphone tracks
let localTracks = await getUserMedia({
audio: true,
video: true,
});
// Render local media tracks
localTracks.forEach((track) => {
track.attach(localParticipantEl);
});
// Instantiate our space
let space = new Space('PUT_YOUR_JWT_HERE');
// Setup event listeners for other people joining and leaving
space.on(SpaceEvent.ParticipantTrackSubscribed, addTrack);
space.on(SpaceEvent.ParticipantTrackUnsubscribed, removeTrack);
space.on(SpaceEvent.ParticipantLeft, removeParticipant);
// Join the Space
let localParticipant = await space.join();
// Publish local media tracks
await localParticipant.publishTracks(localTracks);
}
// Creates or updates a <video> element in the page when a participant's track becomes available
async function addTrack(participant, track) {
let remoteVideo = document.getElementById(participant.connectionId);
if (!remoteVideo) {
const el = document.createElement('video');
el.id = participant.connectionId;
el.width = 400;
el.height = 225;
el.autoplay = true;
el.controls = true;
participantsEl.appendChild(el);
remoteVideo = el;
}
track.attach(remoteVideo);
}
// Removes a participant's track when it is no longer available
async function removeTrack(participant, track) {
const remoteVideo = document.getElementById(participant.connectionId);
track.detach(remoteVideo);
}
// Removes the appropriate <video> element from the page when a participant leaves
async function removeParticipant(participant) {
participantsEl.removeChild(document.getElementById(participant.connectionId));
}
Generated using TypeDoc