Versions Compared

Key

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

Table of Contents

Пример демонстрирует публикацию одного или нескольких потоков в нескольких качествах в одном WebRTC соединении, и проигрывание этих потоков. Единицей публикации считается комната, то есть зрители, подключившись к этой комнате, получают все опубликованные в ней потоки.

На скриншотах нижеThe example shows how to publish and 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:

  • Server url - Websocket URL of WCS

...

  • server
  • Room name -

...

  • room name
  • Publisher -

...

  • publisher user name

  • Player - имя пользователя, который играет потоки viewer nickname
  • 180p send, 360p send, 720p send - кнопки переключения принимаемого качества
  • Track - кнопка переключения видео треков, если их несколько

Image Removed

Обратите внимание, что аудио потоки проигрываются в отдельных элементах

Исходный код примера

...

  • quality switch buttons
  • Track - video tracks switch button if there are more than one

Image Added

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

Example source code

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"

2.

...

Default configuration

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",
        failedProbesThreshold: 5,
        pingInterval: 5000
    },
    media: {
        audio: {
            tracks: [
                {
                    source: "mic",
                    channels: 1
                }
            ]
        },
        video: {
            tracks: Array(1).fill({
                source: "camera",
                width: 1280,
                height: 720,
                codec: "H264",
                constraints: {
                    frameRate: 25
                },
                encodings: [
                    {rid: "180p", active: true, maxBitrate: 200000, scaleResolutionDownBy: 4},
                    {rid: "360p", active: true, maxBitrate: 500000, scaleResolutionDownBy: 2},
                    {rid: "720p", active: true, maxBitrate: 900000}
                ],
                type: "cam1"
            })
        }
    }
};

3.

...

Object to store current publishing/playback state

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,
        display: null,
        roomEnded: false,
        starting: 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;
        },
        setStarting: function (value) {
            state.starting = value;
        },
        isStarting: function () {
            return state.starting;
        },
        setDisplay: function (display) {
            state.display = display;
        },
        disposeDisplay: function () {
            if (state.display) {
                state.display.stop();
                state.display = null;
            }
        }
    };
    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 () {
    let configName = getUrlParam("config") || "./config.json";
    $("#publishBtn").prop('disabled', true);
    $("#playBtn").prop('disabled', true);
    $("#url").prop('disabled', true);
    $("#roomName").prop('disabled', true);
    $("#publishName").prop('disabled', true);
    $("#playName").prop('disabled', true);
    publishState = CurrentState(PUBLISH);
    playState = CurrentState(PLAY);
    $.getJSON(configName, function (cfg) {
        mainConfig = cfg;
        onDisconnected(publishState);
        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(publishState);
        onDisconnected(playState);
    });
    $("#url").val(setURL());
    $("#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() вызывается по нажатию кнопки Publish или Play:

...

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 = async function (state) {
    //create peer connection
    let 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");
    }
}

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, 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 () {
        setStatus(state.errInfoId(), "Room " + state.room.name() + " has ended", "red");
        state.setRoomEnded();
        onStopClick(state);
    }).on(constants.SFU_ROOM_EVENT.DROPPED, function () {
        setStatus(state.errInfoId(), "Dropped from the room " + state.room.name() + " due to network issues", "red");
        state.setRoomEnded();
        onStopClick(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
Code Block
languagejs
themeRDark
const publishStreams = async function (state) {
    if (state.isConnected()) {
        //create local display item to show local streams
        const display = initLocalDisplay(document.getElementById("localVideo"));
        state.setDisplay(display);
        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) {
                    let contentType = s.type || s.source;
                    //add local stream to local display
                    display.add(s.stream.id, $("#" + state.inputId()).val(), s.stream, contentType);
                    //add each track to PeerConnection
                    s.stream.getTracks().forEach((track) => {
                        config[track.id] = contentType;
                        addTrackToPeerConnection(state.pc, s.stream, track, s.encodings);
                        subscribeTrackToEndedEvent(state.room, track, state.pc);
                    });
                });
                //start WebRTC negotiation
                await state.room.join(state.pc, null, config);
            }
        } catch (e) {
            if (e.type === constants.SFU_ROOM_EVENT.OPERATION_FAILED) {
                onOperationFailed(state, e);
            } else {
                console.error("Failed to capture streams: " + e);
                setStatus(state.errInfoId(), e.name, "red");
                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) {
    pc.addTransceiver(track, {
        direction: "sendonly",
        streams: [stream],
        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", async function () {
        //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;
                break;
            }
        }
        if (negotiate) {
            //kickoff renegotiation
            await room.updateState();
        }
    });
};

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 = async function (state) {
    if (state.isConnected() && state.isActive()) {
        try {
            //create remote display item to show remote streams
            const display = initDefaultRemoteDisplay(state.room, document.getElementById("remoteVideo"), null, null);
            state.setDisplay(display);
            //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);
            }
        }
    }
}

9.

...

Publishing stopping

state.disposeDisplay() code

Code Block
languagejs
themeRDark
const disposeStateDisplay = function (state) {
    state.disposeDisplay();
}

10.

...

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) {
    if (validateForm("connectionForm", state.errInfoId())
        && validateForm(state.formId(), state.errInfoId())
        && validateName(state, state.errInfoId())) {
        state.setStarting(true);
        let otherState = getOtherState(state);
        $("#" + state.buttonId()).prop('disabled', true);
        // Disable other session button to prevent a simultaneous connections
        if (!otherState.isStarting()) {
            $("#" + otherState.buttonId()).prop('disabled', true);
        }
        if (state.is(PLAY) && Browser().isSafariWebRTC()) {
            playFirstSound(document.getElementById("main"), PRELOADER_URL).then(function () {
                connect(state);
            });
        } else {
            connect(state);
        }
    }
}

11.

...

Stop click actions

onStopClick(), Session.disconnect() codeФункция

The onStopClick() function:

...

  • stops playback or publishing
  • disconects Websocket session
Code Block
languagejs
themeRDark
const onStopClick = async function (state) {
    state.setStarting(false);
    disposeStateDisplay(state);
    if (state.isConnected()) {
        $("#" + state.buttonId()).prop('disabled', true);
        await state.session.disconnect();
        onDisconnected(state);
    }
}

12.

...

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 = function (state) {
    state.clear();
    $("#" + state.buttonId()).text(state.buttonText()).off('click').click(function () {
        onStartClick(state);
    }).prop('disabled', false);
    $("#" + state.inputId()).prop('disabled', false);
    // Enable other session buttons
    let otherState = getOtherState(state);
    if (!otherState.session) {
        $("#" + otherState.buttonId()).prop('disabled', false);
        $("#" + otherState.inputId()).prop('disabled', false);
        $('#url').prop('disabled', false);
        $("#roomName").prop('disabled', false);
    }
}

13.

...

Helper functions

13.1.

...

 Start publishing or playback

startStreaming() code

Code Block
languagejs
themeRDark
const startStreaming = function(state) {
    if (state.is(PUBLISH)) {
        publishStreams(state);
    } else if (state.is(PLAY)) {
        playStreams(state);
    }
}

13.2.

...

 Stop publishing or playback

state.display.stop() code

Code Block
languagejs
themeRDark
const CurrentState = function (prefix) {
    let state = {
        ...
        disposeDisplay: function () {
            if (state.display) {
                state.display.stop();
                state.display = null;
            }
        }
    };
    return state;
}