Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Note that audio tracks are playing in a separate audio tags.

Example source code

To analyze the example source code, take the version available here, which can be downloaded with build 1.0.1.36

The source code consists of the following modules:

  • 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

1. Local variables

Local variables declaration to work with constants, SFU SDK, to display video and to work with client configuration

code

Code Block
languagejs
themeRDark
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
languagejs
themeRDark
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
languagejs
themeRDark
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.room = nullstopWaiting();
            state.sessionroom = null;
            state.pcsession = 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"        $("#" + state.buttonId()).prop('disabled', false);
        },
        statusId: function() {
}
               return else if (state.prefix + "Status";isConnected()) {
        },
        formId: function() {
  setStatus(state.errInfoId(), "No media capturing started in " + timeout + return state.prefix + "Form"" ms, stopping", "red");
        },
           errInfoId: functiononStopClick(state) {;
            return state.prefix + "ErrorInfo";  }
            }, timeout);        
        },
        isstopWaiting: function(value) {
            returnif (prefix === valuestate.timer) {
                clearTimeout(state.timer);
        }
    };
    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
languagejs
themeRDark
const init = function() {
state.timer = null;         let configName = getUrlParam("config") || "./config.json";
   
      ...
    publishState = CurrentState(PUBLISH);
}
      playState = CurrentState(PLAY); },
    $.getJSON(configName,    buttonId: function(cfg) {
          mainConfig = cfg   return state.prefix + "Btn";
        onDisconnected(publishState);},
        onDisconnectedbuttonText: function(playState);
 {
             }return (state.prefix.charAt(0).fail(function(e){
toUpperCase() + state.prefix.slice(1));
         //use default config
},
        inputId: function() {
  console.error("Error reading configuration file " + configName + ": " +return estate.statusprefix + " " + e.statusText)Name";
        },
        console.log("Default config will be used");
statusId: function() {
            return mainConfigstate.prefix =+ defaultConfig"Status";
        onDisconnected(publishState);},
        onDisconnectedformId: function(playState); {
    });
        $("#url").val(setURL())return state.prefix + "Form";
    $("#roomName").val("ROOM1-"+createUUID(4));
    },
        errInfoId: $function("#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
languagejs
themeRDark
const connect = function(state) {
    //create peer connection
    pc = new RTCPeerConnection(        return state.prefix + "ErrorInfo";
        },
        is: function(value) {
            return (prefix === value);
    //get config object for room creation},
    const roomConfig   =isActive: getRoomConfigfunction(mainConfig); {
       roomConfig.pc = pc;
   return roomConfig(state.urlroom = $("#url").val(&& state.pc);
    roomConfig.roomName   = $("#roomName").val(); },
    roomConfig.nickname  = $("#" + state.inputId()).val(); isConnected: function() {
    //  clean state display items
   return setStatus(state.statusId(), "");
    setStatus(state.errInfoId(), ""session && state.session.state() == constants.SFU_STATE.CONNECTED);
    // connect to server and}
 create a room if not};
    const session = sfu.createRoom(roomConfig);
    session.on(constants.SFU_EVENT.CONNECTED, function(roomreturn 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
languagejs
themeRDark
const init = function() {
    let configName =  state.set(pc, session, room)getUrlParam("config") || "./config.json";
    ...
    publishState = onConnectedCurrentState(statePUBLISH);
    playState =   setStatus(state.statusId(), "ESTABLISHED", "green"CurrentState(PLAY);
    })$.on(constants.SFU_EVENT.DISCONNECTEDgetJSON(configName, function(cfg) {
        state.clear()mainConfig = cfg;
        onDisconnected(statepublishState);
        setStatus(state.statusId(), "DISCONNECTED", "green"onDisconnected(playState);
    }).on(constants.SFU_EVENT.FAILED, fail(function(e) {
        state.clear();//use default config
        onDisconnected(state);
        setStatus(state.statusId(), "FAILED", "red");console.error("Error reading configuration file " + configName + ": " + e.status + " " + e.statusText)
        setStatus(stateconsole.errInfoId(), e.status + " " + e.statusText, "red"log("Default config will be used");
        mainConfig = defaultConfig;
        onDisconnected(publishState);
    });
}

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
languagejs
themeRDark
const onConnected = function(state) {    onDisconnected(playState);
    });
    $("#" + state.buttonId#url").val(setURL()).text;
    $("Stop#roomName").off('click').click(function () {val("ROOM1-"+createUUID(4));
        onStopClick(state$("#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
  • cleans previous session state displayed
  • sets up room configuration and creates Websocket session
  • subscribes to Websocket session events
Code Block
languagejs
themeRDark
const connect = function(state) {
    //create Addpeer errorsconnection
  displaying
  pc  state.room.on(constants.SFU_ROOM_EVENT.FAILED, function(e) {
   = new RTCPeerConnection();
    //get config object for room creation
     setStatus(state.errInfoId(), e, "red"const roomConfig = getRoomConfig(mainConfig);
    }).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function (e) {
        setStatus(state.errInfoId(), e.operation + " failed: " + e.error, "red"roomConfig.pc = pc;
    roomConfig.url = $("#url").val();
    roomConfig.roomName = $("#roomName").val();
    });
    if (state.is(PUBLISH)) {roomConfig.nickname = $("#" + state.inputId()).val();
    // clean state  publishStreamsdisplay items
    setStatus(state.statusId(), "");
    } else if setStatus(state.iserrInfoId(PLAY), "") {
        playStreams(state);;
    // connect to server and create a room if not
    }
}

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
languagejs
themeRDark
const publishStreams = async function(state) {
    let timerId;
    //create local display item to show local streams
    localDisplay = initLocalDisplay(document.getElementById("localVideo"));
    try {
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
languagejs
themeRDark
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
languagejs
themeRDark
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") {
                            config[track.id] = s.source;
                        }
                       //get configured local video streams addTrackToPeerConnection(state.pc, s.stream, track, s.encodings);
        let    streams = await getVideoStreams(mainConfig);
        let audioStreams = await getAudioStreams(mainConfig subscribeTrackToEndedEvent(state.room, track, state.pc);
          //combine local video streams with audio streams
    });
    streams.push.apply(streams, audioStreams);
        let config = {});
        //add   our local streams to the state.room (to PeerConnection).join(config);
        streams.forEach(function (s) {
        // TODO: Use room state or promises to detect //addif localpublishing streamstarted to localenable stop displaybutton
            localDisplay.add(s.stream.id, $("#" +    state.inputIdwaitFor())document.valgetElementById("localVideo"), s.stream3000);
            //add each track to PeerConnection}
        } catch(e) {
            sconsole.stream.getTracks().forEach((track) => {
    error("Failed to capture streams: " + e);
            if setStatus(s.source === "screen") {state.errInfoId(), e.name, "red");
            state.stopWaiting();
        config[track.id] = s.source;
      if (state.isConnected()) { 
          }
      onStopClick(state);
          addTrackToPeerConnection(state.pc, s.stream, track, s.encodings); }
        }
        subscribeTrackToEndedEvent(state.room, track, state.pc);
            });}
}

7.1. Media tracks addition to WebRTC connection

addTrackToPeerConnection(), PeerConnection.addTransceiver() code

Code Block
languagejs
themeRDark
const addTrackToPeerConnection = function(pc, stream, track, encodings) {
    pc.addTransceiver(track, {
        });
direction: "sendonly",
         state.room.join(config);streams: [stream],
        // TODOsendEncodings: Use room state or promises to detect if publishing started to enable stop button
        timerId = waitFor(document.getElementById("localVideo"), 3000, state);
    } catch(e) {
        console.error("Failed to capture streams: " + e);
        setStatus(state.errInfoId(), e.name, "red");
        if (timerIdencodings ? encodings : [] //passing encoding types for video simulcast tracks
    });
}

7.2. Tracks onended event subscription

subscribeTrackToEndedEvent(), MediaTrack.addEventListener(), SFURoom.updateState() code

Code Block
languagejs
themeRDark
const subscribeTrackToEndedEvent = function(room, track, pc) {
    track.addEventListener("ended", function() {
        //track ended, see if we need to clearTimeout(timerId);cleanup
        let   negotiate timerId = nullfalse;
        }for (const sender of pc.getSenders()) {
        onStopClick(state);
    }
}

7.1. Media tracks addition to WebRTC connection

addTrackToPeerConnection(), PeerConnection.addTransceiver() code

Code Block
languagejs
themeRDark
const addTrackToPeerConnection = function(pc, stream, track, encodings   if (sender.track === track) {
                pc.addTransceiver(track, {
removeTrack(sender);
                //track found, set renegotiation flag
         direction: "sendonly",       negotiate = true;
        streams: [stream],
       break;
 sendEncodings: encodings ? encodings : [] //passing encoding types for video simulcast}
 tracks
    });
}

7.2. Tracks onended event subscription

subscribeTrackToEndedEvent(), MediaTrack.addEventListener(), SFURoom.updateState() code

Code Block
languagejs
themeRDark
const subscribeTrackToEndedEvent = function(room, track, pc) {
    track.addEventListener("ended", function(   }
        if (negotiate) {
            //track ended, see if we need to cleanupkickoff renegotiation
            room.updateState();
        let}
   negotiate = false;
        for (const sender of pc.getSenders()) {
            if (sender.track === track });
};

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
languagejs
themeRDark
const playStreams = function(state) {
    if (state.isConnected() && state.isActive()) {
        //create remote display item to show remote streams
   pc.removeTrack(sender);
     remoteDisplay = initRemoteDisplay({
         //track found, set renegotiation flagdiv: document.getElementById("remoteVideo"),
            room: state.room,
    negotiate = true;
      peerConnection: state.pc
         break});
        state.room.join(state.pc);
    }
    $("#" +   }
        if (negotiatestate.buttonId()).prop('disabled', false);
}

9. Publishing stopping

unPublishStreams(), localDisplay.stop() code

Code Block
languagejs
themeRDark
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
languagejs
themeRDark
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
languagejs
themeRDark
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
languagejs
themeRDark
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
languagejs
themeRDark
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
languagejs
themeRDark
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
languagejs
themeRDark
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
languagejs
themeRDark
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
languagejs
themeRDark
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
languagejs
themeRDark
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);
    }
}