...
- two-way-streaming.html - HTML page
- two-way-streaming.css - HTML page styles
- two-way-streaming.js - main application logic
- config.json - client configuration file, contains streams publishing description
...
Analyzing the code
To analyze the example source code, take the file two-way-streaming.js version available here, which can be downloaded with build 1.0.41
1. Local variables
Local variables declaration to work with constants, SFU SDK, to display video and to work with client configuration
code
Code Block |
---|
|
const constants = SFU.constants;
const sfu = SFU;
let mainConfig;
let localDisplay;
let remoteDisplay;
let publishState;
let playState;
const PUBLISH = "publish";
const PLAY = "play";
const STOP = "stop";
const PRELOADER_URL="../commons/media/silence.mp3" |
...
Default room configuration and stream publishing configuration to use if there is no config.json
file found
code
Code Block |
---|
|
const defaultConfig = {
room: {
url: "wss://127.0.0.1:8888",
name: "ROOM1",
pin: "1234",
nickName: "User1"
},
media: {
audio: {
tracks: [
{
source: "mic",
channels: 1
}
]
},
video: {
tracks: [
{
source: "camera",
width: 640,
height: 360,
codec: "H264",
encodings: [
{ rid: "360p", active: true, maxBitrate: 500000 },
{ rid: "180p", active: true, maxBitrate: 200000, scaleResolutionDownBy: 2 }
]
}
]
}
}
}; |
...
The object should keep Websocket session data, WebRTC connection data and room data, and shoukd form HTML tags ids to access them from code.
code
Code Block |
---|
|
const CurrentState = function(prefix) {
let state = {
prefix: prefix,
pc: null,
session: null,
room: null,
timer: null,
set: function(pc, session, room) {
state.pc = pc;
state.session = session;
state.room = room;
},
clear: function() {
state.stopWaiting();
state.room = null;
state.session = null;
state.pc = null;
},
buttonIdwaitFor: function(div, timeout) {
return state.prefix + "Btn"stopWaiting();
},
state.timer = buttonText: functionsetTimeout(function () {
return if (state.prefix.charAt(0).toUpperCase() + state.prefix.slice(1));div.innerHTML !== "") {
},
inputId: function() {
// Enable stop button
return state.prefix + "Name";
},
statusId: function() { $("#" + state.buttonId()).prop('disabled', false);
return state.prefix + "Status";}
},
else formId: functionif (state.isConnected()) {
return state.prefix + "Form";
setStatus(state.errInfoId(), "No media },
capturing started in " + timeout + errInfoId: function() {
" ms, stopping", "red");
return state.prefix + "ErrorInfo";
onStopClick(state);
},
is: function(value) {
}
return (prefix === value }, timeout);
}
};,
return state;
} |
4. Initialization
init() code
The init() function is called on page load and:
- initializes state objects
- reads
config.json
file or default configuration - initializes input fields
Code Block |
---|
|
const init = stopWaiting: function() {
let configName = getUrlParam("config") || "./config.json"; if (state.timer) {
...
publishState = CurrentState(PUBLISH);
playState = CurrentStateclearTimeout(PLAYstate.timer);
$.getJSON(configName, function(cfg){
mainConfigstate.timer = cfgnull;
onDisconnected(publishState);
onDisconnected(playState);
}).fail(function(e){
//use default config}
console.error("Error reading configuration file " + configName + ": " + e.status + " " + e.statusText)
},
buttonId: function() {
return console.log("Default config will be used")state.prefix + "Btn";
mainConfig = defaultConfig;
},
buttonText: onDisconnectedfunction(publishState); {
onDisconnected(playState);
});
$("#url").val(setURL(return (state.prefix.charAt(0).toUpperCase() + state.prefix.slice(1));
$("#roomName").val("ROOM1-"+createUUID(4));
$("#publishName").val("Publisher1-"+createUUID(4));
$("#playName").val("Player1-"+createUUID(4));
} |
5. Establishing server connection
connect(), SFU.createRoom() code
The connect() function is called by Publish or Play click:
- creates PeerConnection object
- sets up room configuration and creates Websocket session
- subscribes to Websocket session events
Code Block |
---|
|
const connect = function(state},
inputId: function() {
return state.prefix + "Name";
},
statusId: function() {
//create peer connection
pc = new RTCPeerConnection()return state.prefix + "Status";
//get config object for},
room creation
const roomConfig =formId: getRoomConfigfunction(mainConfig); {
roomConfig.pc = pc;
return roomConfigstate.urlprefix =+ $("#urlForm").val();
roomConfig.roomName = $("#roomName").val();
},
roomConfig.nickname = $("#" + state.inputId()).val(); errInfoId: function() {
// clean state display items
return setStatus(state.statusId(),prefix + "ErrorInfo");
setStatus(state.errInfoId(), "");
},
// connect to server and create a room if notis: function(value) {
const session = sfu.createRoom(roomConfig);
return session.on(constants.SFU_EVENT.CONNECTED, function(room) {(prefix === value);
state.set(pc},
session, room);
isActive: onConnectedfunction(state); {
setStatus return (state.statusId(), "ESTABLISHED", "green"room && state.pc);
}).on(constants.SFU_EVENT.DISCONNECTED, ,
isConnected: function() {
return (state.session && state.clearsession.state() == constants.SFU_STATE.CONNECTED);
onDisconnected(state);}
};
setStatus(state.statusId(), "DISCONNECTED", "green");
}).on(constants.SFU_EVENT.FAILED, function(e) {
state.clear(return state;
} |
4. Initialization
init() code
The init() function is called on page load and:
- initializes state objects
- reads
config.json
file or default configuration - initializes input fields
Code Block |
---|
|
const init = function() {
let configName = getUrlParam("config") || "./config.json";
...
publishState = CurrentState(PUBLISH);
playState = onDisconnectedCurrentState(statePLAY);
setStatus(state.statusId(), "FAILED", "red");$.getJSON(configName, function(cfg){
setStatus(state.errInfoId(), e.status + " " + e.statusText, "red"mainConfig = cfg;
onDisconnected(publishState);
onDisconnected(playState);
});
} |
6. Publishing or playback start after session establishing
onConnected() code
The onConnected() function:
- sets up Stop button click actions
- subscribes to room error events
- calls publishing or playback function
Code Block |
---|
|
const onConnected = function(state) {
$("#" + state.buttonId()).text("Stop").off('click').click(function () {
onStopClick(state);
});
...
// Add errors displaying
state.room.on(constants.SFU_ROOM_EVENT.FAILED, function(e) {
setStatus(state.errInfoId(), e, "red");
}).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function (e) {
setStatus(state.errInfoId(), e.operation + " failed: " + e.error, "red");
}.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(publishState);
onDisconnected(playState);
});
$("#url").val(setURL());
$("#roomName").val("ROOM1-"+createUUID(4));
if (state.is(PUBLISH)) {$("#publishName").val("Publisher1-"+createUUID(4));
publishStreams(state);
} else if (state.is(PLAY)) {
playStreams(state);
}
} |
7. Streams publishing
...
$("#playName").val("Player1-"+createUUID(4));
} |
5. Establishing server connection
connect(), SFU.createRoom() code
The publishStreamsconnect() function :
- initializes a basic HTML container tag to display local video
- gets local media access accorging to configuration file
- adds media tracks to WEbRTC connection
- joins the room on server
- starts a timer to wait for successful local video tags initializaton
is called by Publish or Play click:
- creates PeerConnection object
- cleans previous session state displayed
- sets up room configuration and creates Websocket session
- subscribes to Websocket session events
Code Block |
---|
|
const publishStreams = async function(state) {
let timerId;
//create local display item to show local streams
localDisplay = initLocalDisplay(document.getElementById("localVideo"));
try {
connect = function(state) {
//create peer connection
pc = new RTCPeerConnection();
//get config object for room creation
const roomConfig = getRoomConfig(mainConfig);
roomConfig.pc = pc;
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
const session = sfu.createRoom(roomConfig);
session.on(constants.SFU_EVENT.CONNECTED, function(room) {
state.set(pc, session, room);
onConnected(state);
setStatus(state.statusId(), "ESTABLISHED", "green");
}).on(constants.SFU_EVENT.DISCONNECTED, function() {
state.clear();
onDisconnected(state);
setStatus(state.statusId(), "DISCONNECTED", "green");
}).on(constants.SFU_EVENT.FAILED, function(e) {
state.clear();
onDisconnected(state);
setStatus(state.statusId(), "FAILED", "red");
setStatus(state.errInfoId(), e.status + " " + e.statusText, "red");
});
} |
6. Publishing or playback start after session establishing
onConnected() code
The onConnected() function:
- sets up Stop button click actions
- subscribes to room error events
- calls publishing or playback function
Code Block |
---|
|
const onConnected = function(state) {
$("#" + state.buttonId()).text("Stop").off('click').click(function () {
onStopClick(state);
});
...
// Add errors displaying
state.room.on(constants.SFU_ROOM_EVENT.FAILED, function(e) {
setStatus(state.errInfoId(), e, "red");
stopStreaming(state);
}).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function (e) {
setStatus(state.errInfoId(), e.operation + " failed: " + e.error, "red");
stopStreaming(state);
});
startStreaming(state);
} |
7. Streams publishing
publishStreams(), SFURoom.join() code
The publishStreams() function:
- initializes a basic HTML container tag to display local video
- gets local media access accorging to configuration file
- adds media tracks to WEbRTC connection
- joins the room on server
- starts a timer to wait for successful local video tags initializaton
Code Block |
---|
|
const publishStreams = async function(state) {
if (state.isConnected()) {
//create local display item to show local streams
localDisplay = initLocalDisplay(document.getElementById("localVideo"));
try {
//get configured local video streams
let streams = await getVideoStreams(mainConfig);
let audioStreams = await getAudioStreams(mainConfig);
if (state.isConnected() && state.isActive()) {
//combine local video streams with audio streams
streams.push.apply(streams, audioStreams);
let config = {};
//add our local streams to the room (to PeerConnection)
streams.forEach(function (s) {
//add local stream to local display
localDisplay.add(s.stream.id, $("#" + state.inputId()).val(), s.stream);
//add each track to PeerConnection
s.stream.getTracks().forEach((track) => {
if (s.source === "screen") {
//get configured local video streams config[track.id] = s.source;
let streams = await getVideoStreams(mainConfig);
let audioStreams = await getAudioStreams(mainConfig);}
//combine local video streams with audio streams
streams.push.apply(streams, audioStreamsaddTrackToPeerConnection(state.pc, s.stream, track, s.encodings);
let config = {} subscribeTrackToEndedEvent(state.room, track, state.pc);
//add our local streams to the room (to PeerConnection)
streams.forEach(function (s) {
});
//add local stream to local display});
localDisplay.add(s.stream.id, $("#" + state.inputId()).val(), s.streamroom.join(config);
//add each track to PeerConnection
// TODO: Use room state or promises to detect if publishing started s.stream.getTracks().forEach((track) => {to enable stop button
if (s.source === "screen") {state.waitFor(document.getElementById("localVideo"), 3000);
}
config[track.id] = s.source;
} catch(e) {
console.error("Failed to }
capture streams: " + e);
addTrackToPeerConnectionsetStatus(state.pcerrInfoId(), se.streamname, track, s.encodings"red");
subscribeTrackToEndedEvent(state.room, track, state.pcstopWaiting();
if (state.isConnected()) { });
});
state.room.join(configonStopClick(state);
// TODO: Use room state}
or promises to detect if publishing started to enable stop button }
timerId = waitFor(document.getElementById("localVideo"), 3000, state);}
} |
7.1. Media tracks addition to WebRTC connection
addTrackToPeerConnection(), PeerConnection.addTransceiver() code
Code Block |
---|
|
const addTrackToPeerConnection = function(pc, stream, track, encodings) {
} catch(e)pc.addTransceiver(track, {
console.error("Failed to capture streamsdirection: " + e);"sendonly",
setStatus(state.errInfoId(), e.name, "red");streams: [stream],
if (timerId) {
sendEncodings: encodings ? encodings : [] //passing encoding types for video simulcast tracks
clearTimeout(timerId});
} |
7.2. Tracks onended event subscription
subscribeTrackToEndedEvent(), MediaTrack.addEventListener(), SFURoom.updateState() code
Code Block |
---|
|
const subscribeTrackToEndedEvent = function(room, track, pc) {
timerId = null;track.addEventListener("ended", function() {
}//track ended, see if
we need onStopClick(state);
}
} |
7.1. Media tracks addition to WebRTC connection
addTrackToPeerConnection(), PeerConnection.addTransceiver() code
Code Block |
---|
|
const addTrackToPeerConnection = function(pc, stream, track, encodings) {
pc.addTransceiver(track, {
to cleanup
let negotiate = false;
for (const sender of pc.getSenders()) {
if (sender.track === track) {
direction: "sendonly",
streams: [stream],
pc.removeTrack(sender);
sendEncodings: encodings ? encodings : [] //passingtrack encodingfound, typesset forrenegotiation videoflag
simulcast tracks
});
} |
7.2. Tracks onended event subscription
subscribeTrackToEndedEvent(), MediaTrack.addEventListener(), SFURoom.updateState() code
Code Block |
---|
|
const subscribeTrackToEndedEvent = function(room, track, pc) {
track.addEventListener("ended", function() {negotiate = true;
//track ended, see if we need to cleanupbreak;
let negotiate = false;}
}
for (const sender ofif pc.getSenders(negotiate)) {
if (sender.track === track) {
//kickoff renegotiation
room.updateState();
}
pc.removeTrack(sender);
});
}; |
8. Streams playback
playStreams(), SFURoom.join() code
The playStreams() function:
- initializes a base container tag to display incoming media streams
- joins to the room on server
Code Block |
---|
|
const playStreams = function(state) {
if (state.isConnected() && //track found, set renegotiation flagstate.isActive()) {
//create remote display item to show negotiate = true;remote streams
remoteDisplay = breakinitRemoteDisplay(document.getElementById("remoteVideo"), state.room, state.pc);
state.room.join();
}
$("#" + }
if (negotiatestate.buttonId()).prop('disabled', false);
} |
9. Publishing stopping
unPublishStreams(), localDisplay.stop() code
Code Block |
---|
|
const unPublishStreams = function(state) {
if //kickoff renegotiation(localDisplay) {
room.updateStatelocalDisplay.stop();
}
});
}; |
...
10.
...
Playback stopping
playStreamsstopStreams(), SFURoomremoteDisplay.joinstop() codeThe playStreams() function:
- initializes a base container tag to display incoming media streams
- joins to the room on server
Code Block |
---|
|
const playStreamsstopStreams = function(state) {
//create remote display item to show remote streams
if (remoteDisplay) {
remoteDisplay = initRemoteDisplay(document.getElementById("remoteVideo"), state.room, state.pc.stop();
state.room.join();
$("#" + state.buttonId()).prop('disabled', false);
} |
9. Publishing stopping
...
11. Publish/Play click action
onStartClick(), playFirstSound(), connect() code
The onStartClick() function:
- validates input fields
- in Safari browser, calls playFirstSound() before playback to automatically play incoming audio
- calls connect() function
Code Block |
---|
|
const unPublishStreamsonStartClick = function(state(state) {
if (validateForm("connectionForm") && validateForm(state.formId())) {
if (localDisplay) {
$("#" localDisplay.stop(+ state.buttonId()).prop('disabled', true);
}
} |
10. Playback stopping
stopStreams(), remoteDisplay.stop() code
Code Block |
---|
|
const stopStreams = function(state) {
if (remoteDisplay if (state.is(PLAY) && Browser().isSafariWebRTC()) {
remoteDisplay.stop();
}
} |
11. Publish/Play click action
onStartClick(), playFirstSound(), connect() code
The onStartClick() function:
- validates input fields
- in Safari browser, calls playFirstSound() before playback to automatically play incoming audio
- calls connect() function
Code Block |
---|
|
const onStartClick = function(state) playFirstSound(document.getElementById("main"), PRELOADER_URL).then(function () {
connect(state);
});
} else {
if (validateForm("connectionForm") && validateForm(state.formId())) {
connect(state);
}
$("#" + state.buttonId()).prop('disabled', true);
if (state.is(PLAY) && Browser().isSafariWebRTC()) }
} |
12. Stop click actions
onStopClick(), Session.disconnect() code
The onStopClick() function:
- stops playback or publishing
- disconects Websocket session
Code Block |
---|
|
const onStopClick = function(state) {
$("#" playFirstSound(document.getElementById("main"), PRELOADER_URL).then(function () {
+ state.buttonId()).prop('disabled', true);
connectstopStreaming(state);
if (state.isConnected()) {
}state.session.disconnect();
}
} |
13. Websocket session disconnection actions
onDisconnected() code
The onDisconnected() functions:
- sets up Publish/Play click actions
- enables Server url and Room name fields access, if there's no parallel session
Code Block |
---|
|
const onDisconnected } else= function(state) {
$("#" connect(state);
}
}
} |
12. Stop click actions
onStopClick(), Session.disconnect() code
The onStopClick() function:
- stops playback or publishing
- disconects Websocket session
Code Block |
---|
|
const onStopClick = function(state) {+ state.buttonId()).text(state.buttonText()).off('click').click(function () {
onStartClick(state);
}).prop('disabled', false);
$("#" + state.buttonIdinputId()).prop('disabled', true);.prop('disabled', false);
// Check if other session is active
if ((state.is(PUBLISH)) { && playState.session)
|| unPublishStreams(state);.is(PLAY) && publishState.session)) {
} else if (state.is(PLAY)) { return;
}
stopStreams(state$('#url').prop('disabled', false);
}
state.session.disconnect($("#roomName").prop('disabled', false);
} |
13. Websocket session disconnection actions
onDisconnected() code
The onDisconnected() functions:
...
14. Helper functions
14.1. Start publishing or playback
startStreaming() code
Code Block |
---|
|
const onDisconnectedstartStreaming = function(state) {
$("#" + state.buttonId()).textif (state.buttonTextis(PUBLISH)).off('click').click(function () {
onStartClick(state);
}).prop('disabled', false);
$("#" + state.inputId()).prop('disabled', false publishStreams(state);
//} Checkelse if other session is active
(state.is(PLAY)) {
if playStreams((state.is(PUBLISH) && playState.session)
||state);
}
} |
14.2. Stop publishing or playback
stopStreaming() code
Code Block |
---|
|
const stopStreaming = function(state) {
state.stopWaiting();
if (state.is(PLAY) && publishState.session))PUBLISH)) {
returnunPublishStreams(state);
}
else if $('#url').prop('disabled', false);
(state.is(PLAY)) {
$("#roomName").prop('disabled', false); stopStreams(state);
}
} |