Versions Compared

Key

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

...

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

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

Для работы с исходным кодом примера возьмем версию, доступную здесь, которую также можно загрузить в сборке 1.0.1.36

Исходный код разбит на следующие модули:

  • two-way-streaming.html - HTML страница
  • two-way-streaming.css - стили HTML страницы
  • two-way-streaming.js - основная логика приложения
  • config.json - файл конфигурации клиента, содержит описание публикуемых потоков

Анализ исходного кода

Для работы с исходным кодом примера возьмем версию файла two-way-streaming.js, доступную здесь

1. Локальные переменные

Объявление локальных переменных для работы с константами, SFU SDK, для отображения видео и работы с конфигурацией клиента

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"

...

Объявление конфигурации комнаты и публикации потоков по умолчанию, на случай, если нет файла конфигурации config.json

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

...

Хранит данные Websocket сессии, WebRTC соединения и комнаты, формирует идентификаторы элементов на странице для доступа к ним

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;
        },
        buttonId: function() {
            return state.prefix + "Btn"state.pc = null;
        },
        buttonTextwaitFor: function(div, timeout) {
            return (state.prefix.charAt(0).toUpperCase() + state.prefix.slice(1))stopWaiting();
        },
    state.timer = setTimeout(function  inputId: function() () {
            return state.prefix + "Name";
 if (div.innerHTML      },
        statusId: function(!== "") {
            return state.prefix + "Status";
     // Enable stop },button
        formId: function() {
          $("#"  return state.prefix + "Form";
+ state.buttonId()).prop('disabled', false);
           },
     }
   errInfoId: function() {
           else returnif (state.prefix + "ErrorInfo";
isConnected()) {
           },
        is: function(value) {
        setStatus(state.errInfoId(), "No media capturing started in " + timeout + " ms, stopping", "red");
     return (prefix === value);
               onStopClick(state);
                }
            }, timeout);        
    return state;
}

4. Инициализация

init() code

Функция init() вызывается после того, как страница загрузится:

  • инициализирует объекты состояния
  • загружает config.json или конфигурацию по умолчанию
  • инициализирует поля ввода
Code Block
languagejs
themeRDark
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));    inputId: function() {
    $("#playName").val("Player1-"+createUUID(4));
}

5. Соединение с сервером

connect(), SFU.createRoom() code

Функция connect() вызывается по нажатию кнопки Publish или Play:

  • создает объект PeerConnection
  • настраивает конфигурацию комнаты и создает Websocket сессию
  • подписывается на события Websocket сессии
Code Block
languagejs
themeRDark
const 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) {
        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.set(pc, session, roomroom && state.pc);
        onConnected(state);},
        setStatus(state.statusId(), "ESTABLISHED", "green");
isConnected: function() {
     }).on(constants.SFU_EVENT.DISCONNECTED, function() {
     return (state.session && state.clear(session.state() == constants.SFU_STATE.CONNECTED);
        onDisconnected(state);}
    };
    setStatus(state.statusId(), "DISCONNECTED", "green");
    }).on(constants.SFU_EVENT.FAILED, function(ereturn state;
}

4. Инициализация

init() code

Функция init() вызывается после того, как страница загрузится:

  • инициализирует объекты состояния
  • загружает config.json или конфигурацию по умолчанию
  • инициализирует поля ввода
Code Block
languagejs
themeRDark
const init = function() {
    let configName =  state.clear()getUrlParam("config") || "./config.json";
    ...
    publishState = onDisconnectedCurrentState(statePUBLISH);
    playState = CurrentState(PLAY);
  setStatus(state.statusId(), "FAILED", "red");  $.getJSON(configName, function(cfg){
        setStatus(state.errInfoId(), e.status + " " + e.statusText, "red");
    });
}

6. Запуск публикации или проигрывания при установке соединения

onConnected() code

Функция onConnected():

  • настраивает действия по нажатию кнопки Stop
  • подписывается на события об ошибках комнаты
  • вызывает функцию публикации или проигрывания
Code Block
languagejs
themeRDark
const onConnected = function(state) {
    $("#" + state.buttonId()).text("Stop").off('click').click(function () {mainConfig = cfg;
        onDisconnected(publishState);
        onDisconnected(playState);
    }).fail(function(e){
        //use default config
        onStopClick(state);
    });
    ...
    // Add errors displaying
    state.room.on(constants.SFU_ROOM_EVENT.FAILED, function(e) {console.error("Error reading configuration file " + configName + ": " + e.status + " " + e.statusText)
        setStatus(stateconsole.errInfoId(), e, "red"log("Default config will be used");
    }).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function (e) {  mainConfig = defaultConfig;
        setStatus(state.errInfoId(), e.operation + " failed: " + e.error, "red"onDisconnected(publishState);
        onDisconnected(playState);
    });
    if (state.is(PUBLISH)) {$("#url").val(setURL());
        publishStreams(state$("#roomName").val("ROOM1-"+createUUID(4));
    } else if (state.is(PLAY)) {$("#publishName").val("Publisher1-"+createUUID(4));
        playStreams(state);
    }
}

7. Публикация потоков

...

$("#playName").val("Player1-"+createUUID(4));
}

5. Соединение с сервером

connect(), SFU.createRoom() code

Функция publishStreamsconnect() :

...

вызывается по нажатию кнопки Publish или Play:

  • создает объект PeerConnection
  • очищает отображение статуса предыдущей сессии
  • настраивает конфигурацию комнаты и создает Websocket сессию
  • подписывается на события Websocket сессии
Code Block
languagejs
themeRDark
const publishStreamsconnect = async function(state) 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. Запуск публикации или проигрывания при установке соединения

onConnected() code

Функция onConnected():

  • настраивает действия по нажатию кнопки Stop
  • подписывается на события об ошибках комнаты
  • вызывает функцию публикации или проигрывания
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. Публикация потоков

publishStreams(), SFURoom.join() code

Функция publishStreams():

  • инициализирует базовый элемент для отображения локального видео
  • получает доступ к локальным медиа потокам согласно файлу конфигурации
  • добавляет медиа дорожки в WebRTC соединение
  • входит в комнату на сервере
  • запускает таймер ожидания успешной инициализации локальных видео элементов
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) => {
    let timerId;
        //create local display item to show local streams
    localDisplay =if initLocalDisplay(document.getElementById("localVideo"));
s.source === "screen") {
               try   {
        //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  s.stream.getTracks().forEach((track) => {started 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}
}

7.1. Добавление медиа дорожек в WebRTC соединение

addTrackToPeerConnection(), PeerConnection.addTransceiver() code

Code Block
languagejs
themeRDark
const addTrackToPeerConnection = waitFor(document.getElementById("localVideo"), 3000, state);function(pc, stream, track, encodings) {
    } catch(e)pc.addTransceiver(track, {
        console.error("Failed to capture streamsdirection: "sendonly" + e);
,
         setStatus(state.errInfoId(), e.name, "red");
streams: [stream],
        sendEncodings: encodings if? (timerId) {
        encodings : [] //passing encoding types for video simulcast tracks
    clearTimeout(timerId});
}

7.2. Подписка на событие остановки потока

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

Code Block
languagejs
themeRDark
const subscribeTrackToEndedEvent           timerId = null;
   = function(room, track, pc) {
    track.addEventListener("ended", function() {
     }   //track 
ended, see if we need to   onStopClick(state);
cleanup
     }
}

7.1. Добавление медиа дорожек в WebRTC соединение

addTrackToPeerConnection(), PeerConnection.addTransceiver() code

Code Block
languagejs
themeRDark
const addTrackToPeerConnection = function(pc, stream, track, encodings) {
    pc.addTransceiver(track, {
   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. Подписка на событие остановки потока

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

Code Block
languagejs
themeRDark
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) {
            //kickoff renegotiation
         if (sender.track === track) {
 room.updateState();
        }
    });
};

8. Проигрывание потоков

playStreams(), SFURoom.join() code

Функция playStreams():

  • инициализирует базовый элемент для отображения входящих медиа потоков
  • настраивает
Code Block
languagejs
themeRDark
const playStreams =   pc.removeTrack(sender);function(state) {
    if (state.isConnected() && state.isActive()) {
        //trackcreate found,remote setdisplay renegotiationitem flag
to show remote streams
             negotiateremoteDisplay = true;
                break;initRemoteDisplay({
            }div: document.getElementById("remoteVideo"),
        }
    room: state.room,
   if (negotiate) {
       peerConnection: state.pc
    //kickoff renegotiation
    });
        state.room.updateStatejoin(state.pc);
    }
    }
    }$("#" + state.buttonId()).prop('disabled', false);
};

...

9.

...

Остановка публикации

playStreamsunPublishStreams(), SFURoomlocalDisplay.joinstop() codeФункция playStreams():

  • инициализирует базовый элемент для отображения входящих медиа потоков
  • входит в комнату на сервере
Code Block
languagejs
themeRDark
const playStreamsunPublishStreams = function(state) {
    if (localDisplay) {
    //create  remote display item to show remote streams
    remoteDisplay = initRemoteDisplay(document.getElementById("remoteVideo"), state.room, state.pc);
    state.room.join();
    $("#" + state.buttonId()).prop('disabled', false);
}

9. Остановка публикации

unPublishStreams(), localDisplay.stop() code

 localDisplay.stop();
    }
}

10. Остановка проигрывания

stopStreams(), remoteDisplay.stop() code

Code Block
languagejs
themeRDark
const stopStreams = function(state) {
    if (remoteDisplay) {
        remoteDisplay.stop();
    }
}

11. Действия по нажатию кнопки Publish/Play

onStartClick(), playFirstSound(), connect() code

Функция onStartClick():

  • проверяет правильность заполнения полей ввода
  • перед стартом воспроизведения, в браузере Safari  вызывает функцию playFirstSound() для автоматического проигрывания аудио
  • вызывает функцию connect()
Code Block
languagejs
themeRDark
const unPublishStreamsonStartClick = function(state) {
    if (localDisplayif (validateForm("connectionForm") && validateForm(state.formId())) {
        localDisplay.stop();
    }
}

10. Остановка проигрывания

stopStreams(), remoteDisplay.stop() code

Code Block
languagejs
themeRDark
const stopStreams = function(state) {
    if (remoteDisplay$("#" + state.buttonId()).prop('disabled', true);
        if (state.is(PLAY) && Browser().isSafariWebRTC()) {
        remoteDisplay.stop();
    }
}

11. Действия по нажатию кнопки Publish/Play

onStartClick(), playFirstSound(), connect() code

Функция onStartClick():

  • проверяет правильность заполнения полей ввода
  • перед стартом воспроизведения, в браузере Safari  вызывает функцию playFirstSound() для автоматического проигрывания аудио
  • вызывает функцию connect()
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

onStopClick(), Session.disconnect() code

Функция onStopClick():

  • останавливает публикацию или воспроизведение
  • разрывает Websocket сессию
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 сессии

onDisconnected() code

Функция onDisconnected():

  • настраивает действия по нажатию кнопки Publish/Play
  • открывает доступ к полям ввода Server url и Room name, если нет параллельной сессии
Code Block
languagejs
themeRDark
const onDisconnected   } else= function(state) {
    $("#"        connect(state);
        }
    }
}

12. Действия по нажатию кнопки Stop

onStopClick(), Session.dosconnect() code

Функция onStopClick():

  • останавливает публикацию или воспроизведение
  • разрывает Websocket сессию
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 сессии

onDisconnected() code

Функция onDisconnected():

...

14. Вспомогательные функции

14.1. Запуск публикации или проигрывания

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. Остановка публикации или проигрывания

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