main.js¶
1. Local variables¶
Declare local variables for constants, SFU SDK, local display and controls
const constants = SFU.constants;
const sfu = SFU;
const quality = window.ConnectionQualityBadge;
const CONNECTION_STATUS = quality.CONNECTION_STATUS;
let localDisplay;
let cControls;
2. Default configuration¶
Declare default room and publishing configuration which will be used if there was no config.json file available
With this config client will be preconfigured to connect to localhost over WSS, enter room "ROOM1" with pin "1234" and nickname "Alice". Media section directs client to publish audio and video tracks. Video will have two sub-tracks - high (h) and medium (m).
const defaultConfig = {
room: {
url: "wss://127.0.0.1:8888",
name: "ROOM1",
pin: "1234",
nickName: "Alice",
turnServer: "",
forceRelay: false
},
media: {
audio: {
tracks: [
{
source: "mic",
channels: 1
}
]
},
video: {
tracks: [
{
source: "camera",
width: 1280,
height: 720,
codec: "H264",
encodings: [
{rid: "m", active: true, maxBitrate: 300000, scaleResolutionDownBy: 2},
{rid: "h", active: true, maxBitrate: 900000}
]
}
]
}
},
initPoolParticipantsCount: 10,
idleTransceiverTimeoutMs: 60000
};
3. Initialization¶
init() code
Init function is called when page is finished loading. The function will load config.json or default config and open entrance modal window.
const init = function () {
$.getJSON("config.json", function (config) {
cControls = createControls(config);
}).fail(function () {
//use default config
cControls = createControls(defaultConfig);
});
// insert transport values in entrance modal
const transportSelect = document.getElementById('transport');
Object.values(constants.SFU_TRANSPORT_TYPE).forEach(function(value) {
const option = document.createElement('option');
option.value = value;
option.textContent = value;
transportSelect.appendChild(option);
});
// insert participant view types in entrance modal
const participantViewTypeSelect = document.getElementById("participantViewType");
Object.values(PARTICIPANT_VIEW_TYPE).forEach(function(value) {
const option = document.createElement('option');
option.value = value;
option.textContent = value;
participantViewTypeSelect.appendChild(option);
})
//open entrance modal
$('#entranceModal').modal('show');
}
4. Connect to the server and create or enter to the room¶
connect() code
Connect function that is called once user clicks Enter in entrance modal window.
async function connect() {
// hide modal
$('#entranceModal').modal('hide');
// disable controls
cControls.muteInput();
//get config object for room creation
const roomConfig = cControls.roomConfig();
//kick off connect to server and local room creation
try {
const rtcConfiguration = buildRtcConfiguration(roomConfig);
const pc = rtcConfiguration ? new RTCPeerConnection(rtcConfiguration) : new RTCPeerConnection();
const session = await sfu.createRoom(roomConfig);
// Now we connected to the server (if no exception was thrown)
session.on(constants.SFU_EVENT.FAILED, function (e) {
if (e.status && e.statusText) {
displayError("CONNECTION FAILED: " + e.status + " " + e.statusText);
} else if (e.type && e.info) {
displayError("CONNECTION FAILED: " + e.info);
} else {
displayError("CONNECTION FAILED: " + e);
}
}).on(constants.SFU_EVENT.DISCONNECTED, function (e) {
displayError("DISCONNECTED. Refresh the page to enter the room again");
});
const room = session.room();
room.on(constants.SFU_ROOM_EVENT.FAILED, function (e) {
displayError(e);
}).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function (e) {
displayError(e.operation + " failed: " + e.error);
})
// create local display to show local streams
localDisplay = initLocalDisplay(document.getElementById("localDisplay"));
// display audio and video control tables
await cControls.displayTables();
cControls.onTrack(async function (s) {
await publishNewTrack(room, pc, s);
});
//create and bind chat to the new room
const chatDiv = document.getElementById('messages');
const chatInput = document.getElementById('localMessage');
const chatButton = document.getElementById('sendMessage');
createChat(room, chatDiv, chatInput, chatButton);
//setup remote display for showing remote audio/video tracks
const remoteDisplay = document.getElementById("display");
const displayOptions = {
quality: true,
autoAbr: false
};
const abrOptions = {
thresholds: [
{parameter: "nackCount", maxLeap: 10},
{parameter: "freezeCount", maxLeap: 10},
{parameter: "packetsLost", maxLeap: 10}
],
abrKeepOnGoodQuality: ABR_KEEP_ON_QUALITY,
abrTryForUpperQuality: ABR_TRY_UPPER_QUALITY,
interval: ABR_QUALITY_CHECK_PERIOD
};
initDefaultRemoteDisplay(room, remoteDisplay, displayOptions, abrOptions, roomConfig.participantViewType);
bindTrafficWidget(room);
//get configured local video streams
let streams = cControls.getVideoStreams();
//combine local video streams with audio streams
streams.push.apply(streams, cControls.getAudioStreams());
// Publish preconfigured streams
publishPreconfiguredStreams(room, pc, streams);
} catch (e) {
console.error(e);
displayError(formatError(e));
}
}
5. connect() function details¶
Hide modal as we don't need it anymore and mute controls before connection is established
async function connect() {
// hide modal
$('#entranceModal').modal('hide');
// disable controls
cControls.muteInput();
...
}
Create PeerConnection and prepare the room config for the creation of session and room
async function connect() {
...
//get config object for room creation
const roomConfig = cControls.roomConfig();
//kick off connect to server and local room creation
try {
const rtcConfiguration = buildRtcConfiguration(roomConfig);
const pc = rtcConfiguration ? new RTCPeerConnection(rtcConfiguration) : new RTCPeerConnection();
...
} catch (e) {
console.error(e);
displayError(e);
}
...
}
Create session (which will automatically connect to the server)
async function connect() {
...
//kick off connect to server and local room creation
try {
...
const session = await sfu.createRoom(roomConfig);
...
} catch (e) {
console.error(e);
displayError(e);
}
}
Subscribe to session's events
async function connect() {
...
//kick off connect to server and local room creation
try {
...
// Now we connected to the server (if no exception was thrown)
session.on(constants.SFU_EVENT.FAILED, function (e) {
if (e.status && e.statusText) {
displayError("CONNECTION FAILED: " + e.status + " " + e.statusText);
} else if (e.type && e.info) {
displayError("CONNECTION FAILED: " + e.info);
} else {
displayError("CONNECTION FAILED: " + e);
}
}).on(constants.SFU_EVENT.DISCONNECTED, function (e) {
displayError("DISCONNECTED. Refresh the page to enter the room again");
});
...
} catch (e) {
console.error(e);
displayError(e);
}
}
Create a room object and subscribe to error events
async function connect() {
...
//kick off connect to server and local room creation
try {
...
const room = session.room();
room.on(constants.SFU_ROOM_EVENT.FAILED, function (e) {
displayError(e);
}).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function (e) {
displayError(e.operation + " failed: " + e.error);
})
...
} catch (e) {
console.error(e);
displayError(e);
}
}
Create an object to display local video
async function connect() {
...
//kick off connect to server and local room creation
try {
...
// create local display to show local streams
localDisplay = initLocalDisplay(document.getElementById("localDisplay"));
// display audio and video control tables
await cControls.displayTables();
cControls.onTrack(async function (s) {
await publishNewTrack(room, pc, s);
});
...
} catch (e) {
console.error(e);
displayError(e);
}
}
Initialize chat window
async function connect() {
...
//kick off connect to server and local room creation
try {
...
//create and bind chat to the new room
const chatDiv = document.getElementById('messages');
const chatInput = document.getElementById('localMessage');
const chatButton = document.getElementById('sendMessage');
createChat(room, chatDiv, chatInput, chatButton);
...
} catch (e) {
console.error(e);
displayError(e);
}
}
Initialize an object to display other participants tracks
async function connect() {
...
//kick off connect to server and local room creation
try {
...
//setup remote display for showing remote audio/video tracks
const remoteDisplay = document.getElementById("display");
const displayOptions = {
quality: true,
autoAbr: false
};
const abrOptions = {
thresholds: [
{parameter: "nackCount", maxLeap: 10},
{parameter: "freezeCount", maxLeap: 10},
{parameter: "packetsLost", maxLeap: 10}
],
abrKeepOnGoodQuality: ABR_KEEP_ON_QUALITY,
abrTryForUpperQuality: ABR_TRY_UPPER_QUALITY,
interval: ABR_QUALITY_CHECK_PERIOD
};
initDefaultRemoteDisplay(room, remoteDisplay, displayOptions, abrOptions, roomConfig.participantViewType);
...
} catch (e) {
console.error(e);
displayError(e);
}
}
Bind traffic display widget to the room data and start to display the channel state
async function connect() {
...
//kick off connect to server and local room creation
try {
...
bindTrafficWidget(room);
...
} catch (e) {
console.error(e);
displayError(e);
}
}
Get preconfigured local media parameters and publish local media streams
async function connect() {
...
//kick off connect to server and local room creation
try {
...
//get configured local video streams
let streams = cControls.getVideoStreams();
//combine local video streams with audio streams
streams.push.apply(streams, cControls.getAudioStreams());
// Publish preconfigured streams
publishPreconfiguredStreams(room, pc, streams);
} catch (e) {
console.error(e);
displayError(e);
}
}
6. Enter the room and publish a local media tracks¶
publishPreconfiguredStreams(), Room.join() code
const publishPreconfiguredStreams = async function (room, pc, streams) {
try {
const config = {};
//add our local streams to the room (to PeerConnection)
streams.forEach(function (s) {
let contentType = s.type || s.source;
//add each track to PeerConnection
s.stream.getTracks().forEach((track) => {
config[track.id] = contentType;
addTrackToPeerConnection(pc, s.stream, track, s.encodings);
subscribeTrackToEndedEvent(room, track, pc);
});
localDisplay.add(s.stream.id, "local", s.stream, contentType);
});
//join room
const transportType = cControls.roomConfig().transport;
await room.join(pc, null, config, cControls.initPoolParticipantsCount(), cControls.idleTransceiverTimeoutMs(), transportType);
// Enable Delete button for each preconfigured stream #WCS-3689
streams.forEach(function (s) {
$('#' + s.stream.id + "-button").prop('disabled', false);
});
...
} catch (e) {
onOperationFailed("Failed to publish a preconfigured streams", e);
// Enable Delete button for each preconfigured stream #WCS-3689
streams.forEach(function (s) {
$('#' + s.stream.id + "-button").prop('disabled', false);
});
}
}
7. Publish an additional local tracks¶
publishNewTrack(), Room.updateState() code
const publishNewTrack = async function (room, pc, media) {
try {
let config = {};
//add local stream to local display
let contentType = media.type || media.source;
localDisplay.add(media.stream.id, "local", media.stream, contentType);
//add each track to PeerConnection
media.stream.getTracks().forEach((track) => {
config[track.id] = contentType;
addTrackToPeerConnection(pc, media.stream, track, media.encodings);
subscribeTrackToEndedEvent(room, track, pc);
});
// Clean error message
displayError("");
//kickoff renegotiation
await room.updateState(config);
// Enable Delete button for a new stream #WCS-3689
$('#' + media.stream.id + "-button").prop('disabled', false);
} catch (e) {
onOperationFailed("Failed to publish a new track", e);
// Enable Delete button for a new stream #WCS-3689
$('#' + media.stream.id + "-button").prop('disabled', false);
}
}
8. Finalizing local track¶
subscribeTrackToEndedEvent() code
This is a helper function that subscribes new local track to "ended" event. Once event fired we remove track from peer connection and kickoff renegotiation.
const subscribeTrackToEndedEvent = function (room, track, pc) {
track.addEventListener("ended", async function () {
try {
//track ended, see if we need to cleanup
let negotiate = false;
for (const sender of pc.getSenders()) {
if (sender.track === track) {
pc.removeTrack(sender);
//track found, set renegotiation flag
negotiate = true;
if (sender.track) {
sender.track.stop();
sender.track.active = false;
}
break;
}
}
// Clean error message
displayError("");
if (negotiate) {
//kickoff renegotiation
await room.updateState();
pc.restartIce();
}
} catch (e) {
onOperationFailed("Failed to update room state", e);
}
});
}
9. Add new local track to peer connection¶
addTrackToPeerConnection() code
This is a helper function which adds new local track to peer connection.
const addTrackToPeerConnection = function (pc, stream, track, encodings) {
if (encodings) {
for (const encoding of encodings) {
if (encoding.scalabilityMode === "") {
delete encoding.scalabilityMode;
}
}
}
pc.addTransceiver(track, {
direction: "sendonly",
streams: [stream],
sendEncodings: encodings ? encodings : [] //passing encoding types for video simulcast tracks
});
}
10. Display the channel state¶
bindTrafficWidget code
const bindTrafficWidget = function (room) {
const SERVER_TRAFFIC_TTL_MS = 3000;
let lastClientTraffic = null;
let lastServerTraffic = null;
let lastServerTrafficAt = 0;
let lastKnownPing = 0;
const isServerTrafficFresh = function () {
return lastServerTraffic && Date.now() - lastServerTrafficAt <= SERVER_TRAFFIC_TTL_MS;
};
const resolvePing = function (clientTraffic, serverTraffic) {
if (serverTraffic && typeof serverTraffic.ping === "number" && !Number.isNaN(serverTraffic.ping) && serverTraffic.ping > 0) {
return serverTraffic.ping;
}
if (clientTraffic && typeof clientTraffic.ping === "number" && !Number.isNaN(clientTraffic.ping) && clientTraffic.ping > 0) {
return clientTraffic.ping;
}
return lastKnownPing;
};
const hasActiveRoute = function (traffic) {
const links = Array.isArray(traffic && traffic.badges && traffic.badges.links) ? traffic.badges.links : [];
if (links.length === 0) {
return false;
}
return links.some(function (link) {
return link.active === true || link.connected === true || link.status === CONNECTION_STATUS.GREEN || link.status === CONNECTION_STATUS.YELLOW;
});
};
const updatePingMetric = function (clientTraffic, serverTraffic) {
const ping = resolvePing(clientTraffic, serverTraffic);
if (ping > 0) {
lastKnownPing = ping;
}
const routeActive = hasActiveRoute(serverTraffic) || hasActiveRoute(clientTraffic);
const pingQuality = quality.evaluateConnectionBadgeQuality({
connected: routeActive || lastKnownPing > 0,
ping: ping
});
document.getElementById("stat-ping").innerText = ping > 0 ? Math.round(ping) + " ms" : (routeActive ? "Connected" : "—");
updateStatusCircle("ping-status", ping > 0 ? pingQuality.status : routeActive);
};
const updateClientTraffic = function (traffic) {
if (!traffic) {
return;
}
lastClientTraffic = traffic;
const serverTraffic = isServerTrafficFresh() ? lastServerTraffic : null;
const pingTraffic = serverTraffic || lastServerTraffic;
const topologyBadges = quality.resolveTopologyBadges(traffic, serverTraffic);
document.getElementById("stat-outbound").innerText = quality.formatSpeed(traffic.outboundBitrate);
document.getElementById("stat-inbound").innerText = quality.formatSpeed(traffic.inboundBitrate);
updatePingMetric(traffic, pingTraffic);
updateStatusCircle("outbound-status", traffic.outboundBitrate > 0);
updateStatusCircle("inbound-status", traffic.inboundBitrate > 0);
document.getElementById("stat-participants").innerText = topologyBadges.participants.length;
updateStatusCircle("participants-status", topologyBadges.participants.length > 0);
updateRouteIndicator(topologyBadges);
renderConnectionBadges(quality.createParticipantBadgeModel(topologyBadges, traffic, serverTraffic));
};
const updateServerTraffic = function (serverTraffic) {
if (!serverTraffic) {
return;
}
lastServerTraffic = serverTraffic;
lastServerTrafficAt = Date.now();
const traffic = lastClientTraffic;
if (traffic) {
document.getElementById("stat-outbound").innerText = quality.formatSpeed(traffic.outboundBitrate);
document.getElementById("stat-inbound").innerText = quality.formatSpeed(traffic.inboundBitrate);
updateStatusCircle("outbound-status", traffic.outboundBitrate > 0);
updateStatusCircle("inbound-status", traffic.inboundBitrate > 0);
}
updatePingMetric(traffic, serverTraffic);
const topologyBadges = quality.resolveTopologyBadges(lastClientTraffic, serverTraffic);
document.getElementById("stat-participants").innerText = topologyBadges.participants.length;
updateStatusCircle("participants-status", topologyBadges.participants.length > 0);
updateRouteIndicator(topologyBadges);
const badgeModel = quality.createParticipantBadgeModel(topologyBadges, lastClientTraffic, serverTraffic);
renderConnectionBadges(badgeModel);
};
const clientListener = function (traffic) {
updateClientTraffic(traffic);
};
room.addTrafficListener(clientListener);
room.getTraffic().then(function (traffic) {
updateClientTraffic(traffic);
});
room.addServerTrafficListener(updateServerTraffic);
const cleanup = function () {
room.removeTrafficListener(clientListener);
room.removeServerTrafficListener(updateServerTraffic);
};
room.on(constants.SFU_ROOM_EVENT.LEFT, function (participant) {
if (participant && participant.userId === room.userId()) {
cleanup();
}
});
room.on(constants.SFU_ROOM_EVENT.ENDED, cleanup);
};
11. bindTrafficWidget() function details¶
Check if traffic data from server are fresh
const isServerTrafficFresh = function () {
return lastServerTraffic && Date.now() - lastServerTrafficAt <= SERVER_TRAFFIC_TTL_MS;
};
Return actual ping metric value
const resolvePing = function (clientTraffic, serverTraffic) {
if (serverTraffic && typeof serverTraffic.ping === "number" && !Number.isNaN(serverTraffic.ping) && serverTraffic.ping > 0) {
return serverTraffic.ping;
}
if (clientTraffic && typeof clientTraffic.ping === "number" && !Number.isNaN(clientTraffic.ping) && clientTraffic.ping > 0) {
return clientTraffic.ping;
}
return lastKnownPing;
};
Check if there is a media traffic route between client and server
const hasActiveRoute = function (traffic) {
const links = Array.isArray(traffic && traffic.badges && traffic.badges.links) ? traffic.badges.links : [];
if (links.length === 0) {
return false;
}
return links.some(function (link) {
return link.active === true || link.connected === true || link.status === CONNECTION_STATUS.GREEN || link.status === CONNECTION_STATUS.YELLOW;
});
};
Update ping metric displayed
const updatePingMetric = function (clientTraffic, serverTraffic) {
const ping = resolvePing(clientTraffic, serverTraffic);
if (ping > 0) {
lastKnownPing = ping;
}
const routeActive = hasActiveRoute(serverTraffic) || hasActiveRoute(clientTraffic);
const pingQuality = quality.evaluateConnectionBadgeQuality({
connected: routeActive || lastKnownPing > 0,
ping: ping
});
document.getElementById("stat-ping").innerText = ping > 0 ? Math.round(ping) + " ms" : (routeActive ? "Connected" : "—");
updateStatusCircle("ping-status", ping > 0 ? pingQuality.status : routeActive);
};
Update a locally measured client traffic value
const updateClientTraffic = function (traffic) {
if (!traffic) {
return;
}
lastClientTraffic = traffic;
const serverTraffic = isServerTrafficFresh() ? lastServerTraffic : null;
const pingTraffic = serverTraffic || lastServerTraffic;
const topologyBadges = quality.resolveTopologyBadges(traffic, serverTraffic);
document.getElementById("stat-outbound").innerText = quality.formatSpeed(traffic.outboundBitrate);
document.getElementById("stat-inbound").innerText = quality.formatSpeed(traffic.inboundBitrate);
updatePingMetric(traffic, pingTraffic);
updateStatusCircle("outbound-status", traffic.outboundBitrate > 0);
updateStatusCircle("inbound-status", traffic.inboundBitrate > 0);
document.getElementById("stat-participants").innerText = topologyBadges.participants.length;
updateStatusCircle("participants-status", topologyBadges.participants.length > 0);
updateRouteIndicator(topologyBadges);
renderConnectionBadges(quality.createParticipantBadgeModel(topologyBadges, traffic, serverTraffic));
};
Update a server traffic value received from the server
const updateServerTraffic = function (serverTraffic) {
if (!serverTraffic) {
return;
}
lastServerTraffic = serverTraffic;
lastServerTrafficAt = Date.now();
const traffic = lastClientTraffic;
if (traffic) {
document.getElementById("stat-outbound").innerText = quality.formatSpeed(traffic.outboundBitrate);
document.getElementById("stat-inbound").innerText = quality.formatSpeed(traffic.inboundBitrate);
updateStatusCircle("outbound-status", traffic.outboundBitrate > 0);
updateStatusCircle("inbound-status", traffic.inboundBitrate > 0);
}
updatePingMetric(traffic, serverTraffic);
const topologyBadges = quality.resolveTopologyBadges(lastClientTraffic, serverTraffic);
document.getElementById("stat-participants").innerText = topologyBadges.participants.length;
updateStatusCircle("participants-status", topologyBadges.participants.length > 0);
updateRouteIndicator(topologyBadges);
const badgeModel = quality.createParticipantBadgeModel(topologyBadges, lastClientTraffic, serverTraffic);
renderConnectionBadges(badgeModel);
};
Room local traffic listener function
Add traffic listeners and updaters to the room
room.addTrafficListener(clientListener);
room.getTraffic().then(function (traffic) {
updateClientTraffic(traffic);
});
room.addServerTrafficListener(updateServerTraffic);
const cleanup = function () {
room.removeTrafficListener(clientListener);
room.removeServerTrafficListener(updateServerTraffic);
};
room.on(constants.SFU_ROOM_EVENT.LEFT, function (participant) {
if (participant && participant.userId === room.userId()) {
cleanup();
}
});
room.on(constants.SFU_ROOM_EVENT.ENDED, cleanup);
12. Build RTC configuration for RTCPeerConnection¶
buildRtcConfiguration code
const buildRtcConfiguration = function (roomConfig) {
const iceServers = (roomConfig.turnServer || "")
.split(/[\n,]+/)
.map(parseIceServerEntry)
.filter(Boolean);
const hasTurnServer = iceServers.some(function (server) {
const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
return urls.some(function (url) {
return /^turns?:/i.test(url);
});
});
if (iceServers.length === 0 && !roomConfig.forceRelay) {
return undefined;
}
if (roomConfig.forceRelay && !hasTurnServer) {
throw new Error("Force relay requires a TURN server. Fill TURN Server, for example turn:user:password@turn.example.com:3478?transport=udp.");
}
const rtcConfiguration = {
iceServers: iceServers
};
if (roomConfig.forceRelay) {
rtcConfiguration.iceTransportPolicy = "relay";
}
return rtcConfiguration;
};
12. iceServers parser helper function¶
parseIceServerEntry code
const parseIceServerEntry = function (value) {
const trimmedValue = value.trim();
if (!trimmedValue) {
return null;
}
let urls = trimmedValue;
if (!/^(stun|turn|turns):/i.test(urls)) {
urls = "turn:" + urls;
}
const credentialMatch = urls.match(/^(stun|turn|turns):([^@]+)@(.+)$/i);
if (!credentialMatch) {
return {urls: urls};
}
const server = {
urls: credentialMatch[1] + ":" + credentialMatch[3]
};
const authParts = credentialMatch[2].split(":");
const username = authParts.shift() || "";
const credential = authParts.join(":");
if (username) {
server.username = username;
}
if (credential) {
server.credential = credential;
server.credentialType = "password";
}
return server;
};