The example shows how to play a number of streams in one WebRTC connection with simulcast. A room is considered to be a publishing unit, that is, viewers who connect to this room receive all the streams published in it.
On the screenshots below:
Note that audio tracks are playing in a separate audio
tags.
The source code consists of the following modules:
To analyze the example source code, take the file player.js version available here
Local variables declaration to work with constants, SFU SDK, to display video and to work with client configuration
const constants = SFU.constants; const sfu = SFU; let mainConfig; let remoteDisplay; let playState; const PLAY = "play"; const STOP = "stop"; const PRELOADER_URL="../commons/media/silence.mp3" |
Default room configuration to use if there is no config.json
file found
const defaultConfig = { room: { url: "ws://127.0.0.1:8080", name: "ROOM1", pin: "1234", nickName: "User1" } }; |
The object should keep Websocket session data, WebRTC connection data and room data, and shoukd form HTML tags ids to access them from code.
const CurrentState = function(prefix) { let state = { prefix: prefix, pc: null, session: null, room: null, roomEnded: false, set: function(pc, session, room) { state.pc = pc; state.session = session; state.room = room; state.roomEnded = false; }, clear: function() { state.room = null; state.session = null; state.pc = null; state.roomEnded = false; }, setRoomEnded: function() { state.roomEnded = true; }, buttonId: function() { return state.prefix + "Btn"; }, buttonText: function() { return (state.prefix.charAt(0).toUpperCase() + state.prefix.slice(1)); }, inputId: function() { return state.prefix + "Name"; }, statusId: function() { return state.prefix + "Status"; }, formId: function() { return state.prefix + "Form"; }, errInfoId: function() { return state.prefix + "ErrorInfo"; }, is: function(value) { return (prefix === value); }, isActive: function() { return (state.room && !state.roomEnded && state.pc); }, isConnected: function() { return (state.session && state.session.state() === constants.SFU_STATE.CONNECTED); }, isRoomEnded: function() { return state.roomEnded; } }; return state; } |
init() code
The init() function is called on page load and:
config.json
file or default configurationconst init = function() { let configName = getUrlParam("config") || "./config.json"; ... playState = CurrentState(PLAY); $.getJSON(configName, function(cfg){ mainConfig = cfg; onDisconnected(playState); }).fail(function(e){ //use default config console.error("Error reading configuration file " + configName + ": " + e.status + " " + e.statusText) console.log("Default config will be used"); mainConfig = defaultConfig; onDisconnected(playState); }); $("#url").val(setURL()); $("#roomName").val("ROOM1-"+createUUID(4)); $("#playName").val("Player1-"+createUUID(4)); } |
connect(), SFU.createRoom() code
The connect() function is called by Publish or Play click:
const connect = async function(state) { //create peer connection const pc = new RTCPeerConnection(); //get config object for room creation const roomConfig = getRoomConfig(mainConfig); roomConfig.url = $("#url").val(); roomConfig.roomName = $("#roomName").val(); roomConfig.nickname = $("#" + state.inputId()).val(); // clean state display items setStatus(state.statusId(), ""); setStatus(state.errInfoId(), ""); // connect to server and create a room if not try { const session = await sfu.createRoom(roomConfig); // Set up session ending events session.on(constants.SFU_EVENT.DISCONNECTED, function() { onStopClick(state); onDisconnected(state); setStatus(state.statusId(), "DISCONNECTED", "green"); }).on(constants.SFU_EVENT.FAILED, function(e) { onStopClick(state); onDisconnected(state); setStatus(state.statusId(), "FAILED", "red"); if (e.status && e.statusText) { setStatus(state.errInfoId(), e.status + " " + e.statusText, "red"); } else if (e.type && e.info) { setStatus(state.errInfoId(), e.type + ": " + e.info, "red"); } }); // Connected successfully onConnected(state, pc, session); setStatus(state.statusId(), "ESTABLISHED", "green"); } catch(e) { onDisconnected(state); setStatus(state.statusId(), "FAILED", "red"); setStatus(state.errInfoId(), e, "red"); } } |
onConnected() code
The onConnected() function:
const onConnected = async function(state, pc, session) { state.set(pc, session, session.room()); $("#" + state.buttonId()).text("Stop").off('click').click(function () { onStopClick(state); }); $('#url').prop('disabled', true); $("#roomName").prop('disabled', true); $("#" + state.inputId()).prop('disabled', true); // Add errors displaying state.room.on(constants.SFU_ROOM_EVENT.FAILED, function(e) { setStatus(state.errInfoId(), e, "red"); state.setRoomEnded(); onStopClick(state); }).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function (e) { onOperationFailed(state, e); }).on(constants.SFU_ROOM_EVENT.ENDED, function (e) { setStatus(state.errInfoId(), "Room "+state.room.name()+" has ended", "red"); state.setRoomEnded(); onStopClick(state); }).on(constants.SFU_ROOM_EVENT.DROPPED, function (e) { setStatus(state.errInfoId(), "Dropped from the room "+state.room.name()+" due to network issues", "red"); state.setRoomEnded(); onStopClick(state); }); await playStreams(state); // Enable button after starting playback #WCS-3635 $("#" + state.buttonId()).prop('disabled', false); } |
playStreams(), SFURoom.join(), initRemoteDisplay() code
The playStreams() function:
const playStreams = async function(state) { //create remote display item to show remote streams try { remoteDisplay = initDefaultRemoteDisplay(state.room, document.getElementById("remoteVideo"), {quality: true}); // Start WebRTC negotiation await state.room.join(state.pc, null, null, 1); } catch(e) { if (e.type === constants.SFU_ROOM_EVENT.OPERATION_FAILED) { onOperationFailed(state, e); } else { console.error("Failed to play streams: " + e); setStatus(state.errInfoId(), e.name, "red"); onStopClick(state); } } } |
stopStreams(), remoteDisplay.stop() code
const stopStreams = function(state) { if (remoteDisplay) { remoteDisplay.stop(); } } |
onStartClick(), playFirstSound(), connect() code
The onStartClick() function:
const onStartClick = function(state) { if (validateForm("connectionForm") && validateForm(state.formId())) { $("#" + state.buttonId()).prop('disabled', true); if (state.is(PLAY) && Browser().isSafariWebRTC()) { playFirstSound(document.getElementById("main"), PRELOADER_URL).then(function () { connect(state); }); } else { connect(state); } } } |
onStopClick(), Session.disconnect() code
The onStopClick() function:
const onStopClick = async function(state) { stopStreams(state); if (state.isConnected()) { $("#" + state.buttonId()).prop('disabled', true); await state.session.disconnect(); onDisconnected(state); } } |
onDisconnected() code
The onDisconnected() functions:
const onDisconnected = function(state) { state.clear(); $("#" + state.buttonId()).text(state.buttonText()).off('click').click(function () { onStartClick(state); }).prop('disabled', false); $('#url').prop('disabled', false); $("#roomName").prop('disabled', false); $("#" + state.inputId()).prop('disabled', false); } |