Skip to content

main.js

1. Local variables

Declare local variables for constants, SFU SDK, local display and controls

code

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

code

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

code

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

code

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)

code

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

code

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

code

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

code

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

code

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

code

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

code

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

code

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

code

    const isServerTrafficFresh = function () {
        return lastServerTraffic && Date.now() - lastServerTrafficAt <= SERVER_TRAFFIC_TTL_MS;
    };

Return actual ping metric value

code

    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

code

    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

code

    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

code

    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

code

    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

code

    const clientListener = function (traffic) {
        updateClientTraffic(traffic);
    };

Add traffic listeners and updaters to the room

code

    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;
};