...
1. Функция-обертка
initLocalDisplay() code
Обертка для кода отображения локального медиа
Code Block | ||||
---|---|---|---|---|
| ||||
const initLocalDisplay = function(localDisplayElement){ |
2. Локальные переменные
Объявление локальных переменных
...
3. Удаление элемента локального отображения
removeLocalDisplay() code
Удаление локального элемента отображения при завершении публикации дорожки
...
4. Поиск видео элемента без аудио
getAudioContainer() code
Поиск видео элемента без аудио дорожки
...
5. Добавление дорожки для локального отображения
add() code
Функция добавляет дорожку для локального отображения
...
Проверка, какая дорожка добавляется. Если аудио, поиск ближайшего видео элемента без аудио, и добавление аудио дорожки к нему
Code Block | ||||
---|---|---|---|---|
| ||||
if (stream.getAudioTracks().length > 0) { let videoElement = getAudioContainer(); if (videoElement) { let track = stream.getAudioTracks()[0]; videoElement.video.srcObject.addTrack(track); videoElement.audioStateDisplay.innerHTML = "Audio state: " + stream.getAudioTracks()[0].enabled; track.addEventListener("ended", function() { videoElement.video.srcObject.removeTrack(track); videoElement.audioStateDisplay.innerHTML = "Audio state: " + false; //check video element has no tracks left for (const [key, vTrack] of Object.entries(videoElement.video.srcObject.getTracks())) { if (vTrack.readyState !== "ended") { return; } } removeLocalDisplay(videoElement.id); }); return; } } |
Создание нового контейнера для отображения
Code Block | ||||
---|---|---|---|---|
| ||||
const coreDisplay = document.createElement('div'); coreDisplay.setAttribute("style","width:200px; height:auto; border: solid; border-width: 1px"); coreDisplay.id = stream.id; |
Создание контейнера для отображения имени
Code Block | ||||
---|---|---|---|---|
| ||||
const streamNameDisplay = document.createElement("div"); streamNameDisplay.innerHTML = "Name: " + name; streamNameDisplay.setAttribute("style","width:auto; height:30px"); coreDisplay.appendChild(streamNameDisplay); |
Создание элемента для отображения состояния аудио. Подписка на событие "click" для отключения/включения аудио.
Code Block | ||||
---|---|---|---|---|
| ||||
const audioStateDisplay = document.createElement("button"); audioStateDisplay.setAttribute("style","width:auto; height:30px"); audioStateDisplay.innerHTML = "Audio state: " + (stream.getAudioTracks().length > 0 ? stream.getAudioTracks()[0].enabled : false); audioStateDisplay.addEventListener('click', function(){ if (stream.getAudioTracks().length > 0) { stream.getAudioTracks()[0].enabled = !(stream.getAudioTracks()[0].enabled); audioStateDisplay.innerHTML = "Audio state: " + stream.getAudioTracks()[0].enabled; } }); coreDisplay.appendChild(audioStateDisplay); |
Создание контейнера для отображения видео потока
Code Block | ||||
---|---|---|---|---|
| ||||
const streamDisplay = document.createElement('div'); streamDisplay.id = id; streamDisplay.setAttribute("style","width:auto; height:auto"); coreDisplay.appendChild(streamDisplay); |
Создание видео элемента и добавление его в контейнер
Code Block | ||||
---|---|---|---|---|
| ||||
const video = document.createElement("video"); streamDisplay.appendChild(video); video.srcObject = stream; video.muted = true; video.onloadedmetadata = function (e) { video.play(); }; |
Подписка на событие "ended" для дорожки. Если дорожка завершилась, и не осталось ни одной активной дорожки, удаление контейнера
Code Block | ||||
---|---|---|---|---|
| ||||
stream.getTracks().forEach(function(track){ track.addEventListener("ended", function() { video.srcObject.removeTrack(track); //check video element has no tracks left for (const [key, vTrack] of Object.entries(video.srcObject.getTracks())) { if (vTrack.readyState !== "ended") { return; } } removeLocalDisplay(id); }); }); |
Подписка на событие 'resize', чтобы масштабировать видео под размеры контейнера.
Code Block | ||||
---|---|---|---|---|
| ||||
video.addEventListener('resize', function (event) { streamNameDisplay.innerHTML = "Name: " + name + " " + video.videoWidth + "x" + video.videoHeight; resizeVideo(event.target); }); |
Сохранение контейнера, обновление отображения и возврат нового контейнера.
Code Block | ||||
---|---|---|---|---|
| ||||
localDisplays[id] = coreDisplay; reassembleLocalLayout(); return coreDisplay; |
6. Обновление локального отображения на странице
reassembleLocalLayout() code
Вспомогательная функция пересчитывает сетку локальных контейнеров и перерисовывает их на странице
...
7. Экспорт функции для использования в основном коде
Code Block | ||||
---|---|---|---|---|
| ||||
return { add: add } |
...
1. Функция-обертка
initRemoteDisplay() code
Обертка для кода отображения получаемого медиа
Code Block | ||||
---|---|---|---|---|
| ||||
const initRemoteDisplay = function(room, mainDiv, peerConnection) { |
2. Локальные переменные
Объявление локальных переменных
...
3. Подписка на события комнаты
Подписка на необходимые события комнаты
...
Поиск участника. Если не найден, создание нового участника
Code Block | ||||
---|---|---|---|---|
| ||||
let participant = remoteParticipants[e.info.nickName]; if (!participant) { participant = {}; participant.nickName = e.info.nickName; participant.tracks = []; participant.displays = []; remoteParticipants[participant.nickName] = participant; } |
Добавление новых дорожек для этого участника
Code Block | ||||
---|---|---|---|---|
| ||||
participant.tracks.push.apply(participant.tracks, e.info.info); |
Создание контейнера для каждой дорожки
Code Block | ||||
---|---|---|---|---|
| ||||
for (const pTrack of e.info.info) { let createDisplay = true; for (let i = 0; i < participant.displays.length; i++) { let display = participant.displays[i]; if (pTrack.type === "VIDEO") { if (display.hasVideo()) { continue; } display.videoMid = pTrack.mid; display.setTrackInfo(pTrack); createDisplay = false; break; } else if (pTrack.type === "AUDIO") { if (display.hasAudio()) { continue; } display.audioMid = pTrack.mid; createDisplay = false; break; } } if (!createDisplay) { continue; } let display = createRemoteDisplay(participant.nickName, participant.nickName, mainDiv); participant.displays.push(display); if (pTrack.type === "VIDEO") { display.videoMid = pTrack.mid; display.setTrackInfo(pTrack); } else if (pTrack.type === "AUDIO") { display.audioMid = pTrack.mid; } } } |
...
Поиск участника. Если не найден, возврат
Code Block | ||||
---|---|---|---|---|
| ||||
const participant = remoteParticipants[e.info.nickName]; if (!participant) { return; } |
Перебор дорожек участника
Code Block | ||||
---|---|---|---|---|
| ||||
for (const rTrack of e.info.info) { |
Поиск и удаление дорожки с таким же mid, какой был получен в событии
Code Block | ||||
---|---|---|---|---|
| ||||
for (let i = 0; i < participant.tracks.length; i++) { if (rTrack.mid === participant.tracks[i].mid) { participant.tracks.splice(i, 1); break; } } |
Поиск контейнера, в котором проигрывается дорожка, и удаление дорожки. Если в контейнере не осталось дорожек, он также удаляется
Code Block | ||||
---|---|---|---|---|
| ||||
for (let i = 0; i < participant.displays.length; i++) { let found = false; const display = participant.displays[i]; if (display.mids.audio.includes(audioMid === rTrack.mid)) { //remove from mids array display.mids.audio.splice(display.mids.audio.indexOf(rTrack.mid), 1); setAudio(null); //stop track and remove stream display.audioStreams[rTrack.mid].getAudioTracks()[0].stop()found = true; delete display.audioStreams[rTrack.mid]; //remove audio element } else if (display.videoMid === rTrack.mid) { display.display.removeChild(display.audioElements[rTrack.mid]setVideo(null); delete display.audioElements[rTrack.mid]; found = true; } else if (display.mids.video === rTrack.mid) { } display.mids.video = undefined; if display.mediaStream.getVideoTracks()[0].stop(); found) { found = true; } if (!display.mids.audio.length === 0hasAudio() && !display.mids.video === undefinedhasVideo()) { const video = display.display.getElementsByTagName("video")[0] videodisplay.pausedispose(); video.srcObject = null; display.display.remove(); participant.displays.splice(i, 1); } if (found) { break; } } |
SFU_ROOM_EVENT.LEFT
Поиск и удаление участника
Code Block | ||||
---|---|---|---|---|
| ||||
let participant = remoteParticipants[e.name]; if (!participant) { return; } participant.displays.forEach(function(display){ display.dispose(); }) delete remoteParticipants[e.name]; |
...
Поиск участника. Если не найден, возврат
Code Block | ||||
---|---|---|---|---|
| ||||
console.log("Received track quality state"); const participant = remoteParticipants[e.info.nickName]; if (!participant) { return; } |
Перебор дорожек участника
Code Block | ||||
---|---|---|---|---|
| ||||
for (const rTrack of e.info.tracks) { |
Поиск соответствующего контейнера и обновление качества
Code Block | ||||
---|---|---|---|---|
| ||||
const mid = rTrack.mid; for (let i = 0; i < participant.displays.length; i++) { const display = participant.displays[i]; if (display.videoMid === mid) { display.updateQualityInfo(rTrack.quality); break; } } |
4. Создание контейнера для отображения принимаемых потоков
createRemoteDisplay() code
Вспомогательная функция создает контейнер на основе данных о потоке и дорожках
...
Удаление контейнера
dispose() code
Code Block | ||||
---|---|---|---|---|
| ||||
dispose: function() { cell.remove(); } |
Скрытие контейнера
hide() code
Code Block | ||||
---|---|---|---|---|
| ||||
hide: function(value) { if (value) { cell.style.display = "none"; } else { cell.style.display = "block"; } } |
Создание аудио элемента
setAudio() code
Code Block | ||||
---|---|---|---|---|
| ||||
setAudio: function(stream) { if (audio) { audio.remove(); } if (!stream) { audio = null; this.audioMid = undefined; return; } audio = document.createElement("audio"); audio.controls = "controls"; cell.appendChild(audio); audio.srcObject = stream; audio.play(); } |
Создание видео элемента
setVideo() code
Code Block | ||||
---|---|---|---|---|
| ||||
setVideo: function(stream) { if (video) { video.remove(); } if (stream == null) { video = null; this.videoMid = undefined; qualityDivs.forEach(function(div) { div.remove(); }); qualityDivs = []; tidDivs.forEach(function(div) { div.remove(); }); tidDivs = []; return; } video = document.createElement("video"); streamDisplay.appendChild(video); video.srcObject = stream; video.onloadedmetadata = function (e) { video.play(); }; video.addEventListener("resize", function (event) { streamNameDisplay.innerHTML = "Name: " + name + " " + video.videoWidth + "x" + video.videoHeight; resizeVideo(event.target); }); } |
Отображение информации о качестве
setTrackInfo() code
Code Block | ||||
---|---|---|---|---|
| ||||
setTrackInfo: function(trackInfo) { if (trackInfo && trackInfo.quality) { for (let i = 0; i < trackInfo.quality.length; i++) { const qualityDiv = document.createElement("button"); qualityDivs.push(qualityDiv); qualityDiv.innerText = trackInfo.quality[i]; qualityDiv.setAttribute("style", "display:inline-block; border: solid; border-width: 1px"); qualityDiv.style.color = "red"; qualityDiv.addEventListener('click', function(){ console.log("Clicked on quality " + trackInfo.quality[i] + " trackId " + trackInfo.id); if (qualityDiv.style.color === "red") { return; } for (let c = 0; c < qualityDivs.length; c++) { if (qualityDivs[c].style.color !== "red") { qualityDivs[c].style.color = "gray"; } } qualityDiv.style.color = "blue"; room.changeQuality(trackInfo.id, trackInfo.quality[i]); }); qualityDisplay.appendChild(qualityDiv); } for (let i = 0; i < 3; i++) { const tidDiv = document.createElement("button"); tidDivs.push(tidDiv); tidDiv.innerText = "TID"+i; tidDiv.setAttribute("style", "display:inline-block; border: solid; border-width: 1px"); tidDiv.style.color = "gray"; tidDiv.addEventListener('click', function(){ console.log("Clicked on TID " + i + " trackId " + trackInfo.id); for (let c = 0; c < tidDivs.length; c++) { tidDivs[c].style.color = "gray"; } tidDiv.style.color = "blue"; room.changeQuality(trackInfo.id, null, i); }); tidDisplay.appendChild(tidDiv); } } } |
Обновление качетсва
updateQualityInfo() code
Code Block | ||||
---|---|---|---|---|
| ||||
updateQualityInfo: function(videoQuality) { for (const qualityInfo of videoQuality) { for (const qualityDiv of qualityDivs) { if (qualityDiv.innerText === qualityInfo.quality){ if (qualityInfo.available === true) { qualityDiv.style.color = "gray"; } else { qualityDiv.style.color = "red"; } break; } } } } |
5. Работа с PeerConnection
Подписка на событие PeerConnection "ontrack".
...
Поиск участника на основе mid дорожки
Code Block | ||||
---|---|---|---|---|
| ||||
let rParticipant; console.log("Attach remote track " + transceiver.receiver.track.id + " kind " + transceiver.receiver.track.kind + " mid " + transceiver.mid); for (const [nickName, participant] of Object.entries(remoteParticipants)) { for (const pTrack of participant.tracks) { console.log("Participant " + participant.nickName + " track " + pTrack.id + " mid " + pTrack.mid); if (pTrack.mid === transceiver.mid) { rParticipant = participant; break; } } if (rParticipant) { break; } } |
Поиск соответствующего контейнера и добавление к нему дорожки
Code Block | ||||
---|---|---|---|---|
| ||||
for (const display of rParticipant.displays) { if (transceiver.receiver.track.kind === "video") { if (display.videoMid === transceiver.mid) { let stream = new MediaStream(); stream.addTrack(transceiver.receiver.track); display.setVideo(stream); break; } } else if (transceiver.receiver.track.kind === "audio") { if (display.audioMid === transceiver.mid) { let stream = new MediaStream(); stream.addTrack(transceiver.receiver.track); display.setAudio(stream); break; } } } |