...
Обратите внимание, что аудио потоки проигрываются в отдельных элементах
Исходный код примера
Для работы с исходным кодом примера возьмем версию, доступную здесь, которую также можно загрузить в сборке 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.0.41
1. Локальные переменные
Объявление локальных переменных для работы с константами, SFU SDK, для отображения видео и работы с конфигурацией клиента
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" |
...
Объявление конфигурации комнаты и публикации потоков по умолчанию, на случай, если нет файла конфигурации config.json
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 } ] } ] } } }; |
...
Хранит данные Websocket сессии, WebRTC соединения и комнаты, формирует идентификаторы элементов на странице для доступа к ним
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.room stopWaiting(); state.room = null; state.session = null; state.pc = null; }, buttonIdwaitFor: function(div, timeout) { return state.prefix + "Btn"stopWaiting(); }, state.timer = buttonText:setTimeout(function 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. Инициализация
init() code
Функция init() вызывается после того, как страница загрузится:
- инициализирует объекты состояния
- загружает config.json или конфигурацию по умолчанию
- инициализирует поля ввода
Code Block | ||||
---|---|---|---|---|
| ||||
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. Соединение с сервером
connect(), SFU.createRoom() code
Функция connect() вызывается по нажатию кнопки Publish или Play:
- создает объект PeerConnection
- настраивает конфигурацию комнаты и создает Websocket сессию
- подписывается на события Websocket сессии
Code Block | ||||
---|---|---|---|---|
| ||||
const connect = function(state) { //create peer connection pc = new RTCPeerConnection(); //get config object for room creation const roomConfig = getRoomConfig(mainConfig return state.prefix + "ErrorInfo"; }, is: function(value) { return (prefix === value); }, isActive: function() { return (state.room && state.pc); roomConfig.pc = pc; }, roomConfig.url = $("#url").val();isConnected: function() { roomConfig.roomName = $("#roomName").val(); roomConfig.nickname = $("#" +return (state.session && state.inputIdsession.state()).val( == constants.SFU_STATE.CONNECTED); // clean state display items} setStatus(state.statusId(), "")}; return setStatus(state.errInfoId(), ""); // connect to server and create a room if not const session = sfu.createRoom(roomConfig; } |
4. Инициализация
init() code
Функция init() вызывается после того, как страница загрузится:
- инициализирует объекты состояния
- загружает config.json или конфигурацию по умолчанию
- инициализирует поля ввода
Code Block | ||||
---|---|---|---|---|
| ||||
const init = function() { let configName = getUrlParam("config") || "./config.json"; ... publishState = CurrentState(PUBLISH); playState = CurrentState(PLAY); session$.on(constants.SFU_EVENT.CONNECTEDgetJSON(configName, function(roomcfg) { state.set(pc, session, room)mainConfig = cfg; onConnectedonDisconnected(statepublishState); setStatus(state.statusId(), "ESTABLISHED", "green")onDisconnected(playState); }).on(constants.SFU_EVENT.DISCONNECTED, function() fail(function(e){ //use state.clear();default config onDisconnected(state); setStatus(state.statusId(), "DISCONNECTED", "green"); }).on(constants.SFU_EVENT.FAILED, function(e) { console.error("Error reading configuration file " + configName + ": " + e.status + " " + e.statusText) console.log("Default config will be used"); mainConfig = state.clear()defaultConfig; onDisconnected(statepublishState); setStatus(state.statusId(), "FAILED", "red"onDisconnected(playState); }); setStatus(state.errInfoId(), e.status + " " + e.statusText, "red")$("#url").val(setURL()); $("#roomName").val("ROOM1-"+createUUID(4)); $("#publishName").val("Publisher1-"+createUUID(4)); }$("#playName").val("Player1-"+createUUID(4)); } |
6. Запуск публикации или проигрывания при установке соединения
...
5. Соединение с сервером
connect(), SFU.createRoom() code
Функция onConnectedconnect() вызывается по нажатию кнопки Publish или Play:
- настраивает действия по нажатию кнопки Stopсоздает объект PeerConnection
- очищает отображение статуса предыдущей сессии
- настраивает конфигурацию комнаты и создает Websocket сессию
- подписывается на события об ошибках комнатывызывает функцию публикации или проигрыванияWebsocket сессии
Code Block | ||||
---|---|---|---|---|
| ||||
const onConnectedconnect = function(state) { $("#" + state.buttonId()).text("Stop").off('click').click(function () {//create peer connection pc = new RTCPeerConnection(); //get config object onStopClick(state); }for room creation const roomConfig = getRoomConfig(mainConfig); roomConfig... pc = pc; // Add errors displaying state.room.on(constants.SFU_ROOM_EVENT.FAILED, function(e) {roomConfig.url = $("#url").val(); roomConfig.roomName = setStatus(state.errInfoId(), e, "red"$("#roomName").val(); }).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function (e) { setStatus(state.errInfoId(), e.operation + " failed: " + e.error, "redroomConfig.nickname = $("#" + state.inputId()).val(); // clean state display items setStatus(state.statusId(), ""); setStatus(state.errInfoId(), ""); }); if (state.is(PUBLISH)) { // connect to server and create a room if not const publishStreams(statesession = sfu.createRoom(roomConfig); } else if (state.is(PLAY)) { session.on(constants.SFU_EVENT.CONNECTED, function(room) { state.set(pc, session, room); playStreamsonConnected(state); } } |
7. Публикация потоков
publishStreams(), SFURoom.join() code
Функция publishStreams():
- инициализирует базовый элемент для отображения локального видео
- получает доступ к локальным медиа потокам согласно файлу конфигурации
- добавляет медиа дорожки в WebRTC соединение
- входит в комнату на сервере
- запускает таймер ожидания успешной инициализации локальных видео элементов
Code Block | ||||
---|---|---|---|---|
| ||||
const publishStreams = async function(state) { let timerId; //create local display item to show local streams localDisplay = initLocalDisplay(document.getElementById("localVideo")); try { 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 | ||||
---|---|---|---|---|
| ||||
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 | ||||
---|---|---|---|---|
| ||||
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) => { //get configured local video streams if (s.source === "screen") { let streams = await getVideoStreams(mainConfig); let audioStreams = await getAudioStreams(mainConfig); config[track.id] //combine local video streams with audio streams = s.source; streams.push.apply(streams, audioStreams); let config = {}; //add our local streams to the room (to PeerConnection) streams.forEach(function (s) { addTrackToPeerConnection(state.pc, s.stream, track, s.encodings); //add local stream to local display subscribeTrackToEndedEvent(state.room, track, state.pc); localDisplay.add(s.stream.id, $("#" + state.inputId()).val(), s.stream); //add each track to PeerConnection }); s.stream.getTracks().forEach((track) => { }); state.room.join(config); if (s.source === "screen") { // TODO: Use room state or promises to detect if publishing started config[track.id] = s.source;to enable stop button }state.waitFor(document.getElementById("localVideo"), 3000); } addTrackToPeerConnection(state.pc, s.stream, track, s.encodings); } catch(e) { console.error("Failed to capture subscribeTrackToEndedEvent(state.room, track, state.pcstreams: " + e); setStatus(state.errInfoId(), }e.name, "red"); }); state.room.joinstopWaiting(config); // TODO: Use room state or promises to detect if publishing started to enable stop button(state.isConnected()) { timerId = waitFor(document.getElementById("localVideo"), 3000, state); } catchonStopClick(estate); { console.error("Failed to capture} streams: " + e); } setStatus(state.errInfoId} } |
7.1. Добавление медиа дорожек в WebRTC соединение
addTrackToPeerConnection(),
...
PeerConnection.addTransceiver() code
Code Block | ||||
---|---|---|---|---|
| ||||
const addTrackToPeerConnection = function(pc, stream, track, encodings) { pc.addTransceiver(track, { clearTimeout(timerId); direction: "sendonly", timerId = null;streams: [stream], }sendEncodings: encodings ? encodings : [] //passing encoding types for video simulcast onStopClick(state);tracks }); } |
7.
...
2. Подписка на событие остановки потока
subscribeTrackToEndedEvent(), MediaTrack.addEventListener(), PeerConnectionSFURoom.addTransceiverupdateState() code
Code Block | ||||
---|---|---|---|---|
| ||||
const addTrackToPeerConnectionsubscribeTrackToEndedEvent = function(pcroom, stream, track, encodingspc) { pctrack.addTransceiver(track, { direction: "sendonly", streams: [stream], addEventListener("ended", function() { //track ended, sendEncodings:see encodingsif ?we encodingsneed :to [] //passing encoding types for video simulcast tracks }); } |
7.2. Подписка на событие остановки потока
subscribeTrackToEndedEvent(), MediaTrack.addEventListener(), SFURoom.updateState() code
Code Block | ||||
---|---|---|---|---|
| ||||
const subscribeTrackToEndedEvent = function(room, track, pc)cleanup let negotiate = false; for (const sender of pc.getSenders()) { track.addEventListener("ended", function( if (sender.track === track) { //track ended, see if we need to cleanup pc.removeTrack(sender); let negotiate = false; //track found, forset (constrenegotiation senderflag of pc.getSenders()) { if (sender.track === track) {negotiate = true; pc.removeTrack(sender)break; } //track found, set renegotiation flag} if (negotiate) { negotiate = true; //kickoff renegotiation breakroom.updateState(); } }); }; |
8. Проигрывание потоков
playStreams(), SFURoom.join() code
Функция playStreams():
- инициализирует базовый элемент для отображения входящих медиа потоков
- входит в комнату на сервере
Code Block | ||||
---|---|---|---|---|
| ||||
const playStreams = function(state) }{ if (state.isConnected() && }state.isActive()) { if (negotiate) { //create remote display item to show remote streams //kickoff renegotiation remoteDisplay = initRemoteDisplay(document.getElementById("remoteVideo"), state.room, state.pc); state.room.updateStatejoin(); } } }$("#" + state.buttonId()).prop('disabled', false); }; |
...
9.
...
Остановка публикации
playStreamsunPublishStreams(), SFURoomlocalDisplay.joinstop() codeФункция playStreams():
- инициализирует базовый элемент для отображения входящих медиа потоков
- входит в комнату на сервере
Code Block | ||||
---|---|---|---|---|
| ||||
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 | ||||
---|---|---|---|---|
| ||||
const stopStreams = function(state) {
if (remoteDisplay) {
remoteDisplay.stop();
}
} |
11. Действия по нажатию кнопки Publish/Play
onStartClick(), playFirstSound(), connect() code
Функция onStartClick():
- проверяет правильность заполнения полей ввода
- перед стартом воспроизведения, в браузере Safari вызывает функцию playFirstSound() для автоматического проигрывания аудио
- вызывает функцию connect()
Code Block | ||||
---|---|---|---|---|
| ||||
const unPublishStreamsonStartClick = function(state) { if (localDisplayif (validateForm("connectionForm") && validateForm(state.formId())) { localDisplay.stop(); } } |
10. Остановка проигрывания
stopStreams(), remoteDisplay.stop() code
Code Block | ||||
---|---|---|---|---|
| ||||
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 | ||||
---|---|---|---|---|
| ||||
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 | ||||
---|---|---|---|---|
| ||||
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 | ||||
---|---|---|---|---|
| ||||
const onDisconnected } else= function(state) { $("#" connect(state); } } } |
12. Действия по нажатию кнопки Stop
onStopClick(), Session.disconnect() code
Функция onStopClick():
- останавливает публикацию или воспроизведение
- разрывает Websocket сессию
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 сессии
onDisconnected() code
Функция onDisconnected():
...
14. Вспомогательные функции
14.1. Запуск публикации или проигрывания
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. Остановка публикации или проигрывания
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); } } |