...
Для анализа исходного кода возьмем версию модуля display.js, которая находится здесь и доступна в сборке 12.0.1.36136
Захват и отображение локального видео
1. Инициализация
initLocalDisplay() code
Функция initLocalDisplay() возвращает объект для работы с HTML5 элементами захвата и отображения локального видео
...
2.1. Добавления аудио дорожки к HTML5 элементу
add() code
Здесь:
- добавляется аудио дорожка к video элементу
- создается обработчик события
onended
для аудио дорожки
...
2.2. Создание контейнера для отображения локального видео
add() code
Здесь:
- создается контейнер для элементов отображения локального видео
- создается элемент для отображения информации о публикуемом видео
...
2.3. Создание кнопки для включения/отключения локального аудио
add() code
Здесь:
- создается кнопка для включения/отключения локального аудио
- добавляется обработчик нажатия этой кнопки
...
2.4. Создание элемента для отображения локального видео
add() code
Здесь:
- создается элемент-контейнер, размеры которого можно менять в зависимости от размеров родительского элемента
- создается HTML5
video
элемент, с учетом публикации в Safari
...
2.5. Создание обработчиков событий video элемента
add() code
Здесь:
- запускается проигрывание локального видео
- настраивается обработчик события
onended
для видео дорожки - настраивается обработчик события
onresize
для локального видео, в котором размеры видео меняются под размеры контейнера
...
2.6. Добавление видео контейнера в элемент HTML страницы
add() code
Code Block | ||||
---|---|---|---|---|
| ||||
localDisplays[id] = coreDisplay; localDisplayDiv.appendChild(coreDisplay); return coreDisplay; |
3. Остановка захвата видео и аудио
stop() code
Code Block | ||||
---|---|---|---|---|
| ||||
const stop = function () { for (const [key, value] of Object.entries(localDisplays)) { removeLocalDisplay(value.id); } } |
...
1. Инициализация
initRemoteDisplay() code
Функция initLocalDisplay() возвращает объект для работы с HTML5 элементами отображения видео и аудио потоков, опубликованных в комнате
Code Block | ||||
---|---|---|---|---|
| ||||
const initRemoteDisplay = function(mainDiv, room, peerConnectionoptions) { const constants = SFU.constants; const remoteParticipants = {}; ... // Validate options first const createRemoteDisplay =if function(id, name, mainDiv(!options.div) { ... } const stop = function() { throw new Error("Main div to place all the media tag is not defined"); } if ...(!options.room) { } peerConnection.ontrackthrow =new ({transceiver}) => { Error("Room is not defined"); } if (!options.peerConnection) { throw new Error("PeerConnection is not defined"); } ... const } createRemoteDisplay = function(id, name, returnmainDiv) { stop: stop... } } |
2. Обработка событий комнаты
...
const stop = function() {
...
}
peerConnection.ontrack = ({transceiver}) => {
...
}
return {
stop: stop
}
} |
2. Обработка событий комнаты
2.1. ADD_TRACKS
initRemoteDisplay() code
Здесь:
- новый участник добавляется в список
- добавляется информация о качестве потоков
- создается элемент для отображения видео и аудио потоков участника
Code Block | ||||
---|---|---|---|---|
| ||||
room.on(constants.SFU_ROOM_EVENT.ADD_TRACKS, function(e) {
console.log("Received ADD_TRACKS");
let participant = remoteParticipants[e.info.nickName];
if (!participant) {
participant = {};
participant.nickName = e.info.nickName;
participant.tracks = [];
participant.displays = [];
remoteParticipants[participant.nickName] = participant;
}
participant.tracks.push.apply(participant.tracks, e.info.info);
for (const pTrack of e.info.info) {
...
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;
}
}
...
}); |
2.2. REMOVE_TRACKS
initRemoteDisplay() code
Здесь:
- удаляются элементы, в которых проигрывались потоки
- информация о потоках удаляется из списка
Code Block | ||||
---|---|---|---|---|
| ||||
}).on(constants.SFU_ROOM_EVENT.REMOVE_TRACKS, function(e) {
console.log("Received REMOVE_TRACKS");
const participant = remoteParticipants[e.info.nickName];
if (!participant) {
return;
}
for (const rTrack of e.info.info) {
for (let i = 0; i < participant.tracks.length; i++) {
if (rTrack.mid === participant.tracks[i].mid) {
participant.tracks.splice(i, 1);
break;
}
}
for (let i = 0; i < participant.displays.length; i++) {
let found = false;
const display = participant.displays[i];
if (display.audioMid === rTrack.mid) {
display.setAudio(null);
found = true;
} else if (display.videoMid === rTrack.mid) {
display.setVideo(null);
found = true;
}
if (found) {
if (!display.hasAudio() && !display.hasVideo()) {
display.dispose();
participant.displays.splice(i, 1);
}
break;
}
}
}
}); |
2.3. LEFT
initRemoteDisplay() code
Здесь:
- участник удаляется из списка
- удаляются элементы, в которых проигрывались потоки
Code Block | ||||
---|---|---|---|---|
| ||||
}).on(constants.SFU_ROOM_EVENT.LEFT, function(e) {
console.log("Received LEFT");
let participant = remoteParticipants[e.name];
if (!participant) {
return;
}
participant.displays.forEach(function(display){
display.dispose();
})
delete remoteParticipants[e.name];
}); |
2.4. TRACK_QUALITY_STATE
initRemoteDisplay() code
Здесь:
- новый участник добавляется в список
- добавляется обновляется информация о качестве качествах потоков
- создается элемент для отображения видео и аудио потоков участника
Code Block | ||||
---|---|---|---|---|
| ||||
room}).on(constants.SFU_ROOM_EVENT.ADDTRACK_QUALITY_TRACKSSTATE, function(e) { console.log("Received ADD_TRACKStrack quality state"); letconst participant = remoteParticipants[e.info.nickName]; if (!participant) { participant) { return; participant = {}; for (const participant.nickName =rTrack of e.info.nickName;tracks) { participant.tracksconst mid = []rTrack.mid; for (let participant.displaysi = []0; i remoteParticipants[participant.nickName] = participant;< participant.displays.length; i++) { } const participant.tracks.push.apply(participant.tracks, e.info.info)display = participant.displays[i]; for (const pTrack of e.info.info) { if (display.videoMid === mid) { ... let display = createRemoteDisplaydisplay.updateQualityInfo(participant.nickName, participant.nickName, mainDivrTrack.quality); participant.displays.push(display); break; if (pTrack.type === "VIDEO") { } display.videoMid = pTrack.mid; } } display.setTrackInfo(pTrack); }); |
3. Создание элементов для отображения потоков участников
3.1. Создание контейнера
createRemoteDisplay() code
Здесь:
- создается контейнер для потоков участника
- создается контейнер для конкретного потока
- создается контейнер для кнопок переключения качества
Code Block | ||||
---|---|---|---|---|
| ||||
const cell = document.createElement("div"); } else if (pTrack.type === "AUDIO") { cell.setAttribute("class", "text-center"); cell.id = id; display.audioMid = pTrack.midmainDiv.appendChild(cell); }let publisherNameDisplay; } ...let currentQualityDisplay; }); |
2.2. REMOVE_TRACKS
initRemoteDisplay() code
Здесь:
- удаляются элементы, в которых проигрывались потоки
- информация о потоках удаляется из списка
Code Block | ||||
---|---|---|---|---|
| ||||
}).on(constants.SFU_ROOM_EVENT.REMOVE_TRACKS, function(eif (displayOptions.publisher) { console.log("Received REMOVE_TRACKS publisherNameDisplay = document.createElement("div"); const participant = remoteParticipants[e.info.nickName] publisherNameDisplay.innerHTML = "Published by: " + name; publisherNameDisplay.setAttribute("style","width:auto; height:30px;"); if (!participant) { publisherNameDisplay.setAttribute("class","text-center"); returncell.appendChild(publisherNameDisplay); } forif (const rTrack of e.info.infodisplayOptions.quality) { currentQualityDisplay for= (let i = 0; i < participant.tracks.length; i++) { document.createElement("div"); currentQualityDisplay.innerHTML = ""; if (rTrack.mid === participant.tracks[i].mid) { currentQualityDisplay.setAttribute("style","width:auto; height:30px;"); currentQualityDisplay.setAttribute("class","text-center"); participantcell.tracks.splice(i, 1appendChild(currentQualityDisplay); } const qualitySwitchDisplay break= document.createElement("div"); }qualitySwitchDisplay.setAttribute("style","width:auto; height:30px;"); qualitySwitchDisplay.setAttribute("class","text-center"); } cell.appendChild(qualitySwitchDisplay); for (let iqualityDivs = 0; i < participant.displays.length; i++) {[]; let foundconst rootDisplay = falsedocument.createElement("div"); rootDisplay.setAttribute("style","width:auto; height:auto;"); const display = participant.displays[i]rootDisplay.setAttribute("class","text-center"); cell.appendChild(rootDisplay); if (display.audioMid ==const streamDisplay = rTrack.mid) {document.createElement("div"); streamDisplay.setAttribute("style","width:auto; height:auto;"); display.setAudio(nullstreamDisplay.setAttribute("class","text-center"); rootDisplay.appendChild(streamDisplay); |
3.2. Добавление видео элемента
setVideo() code
Code Block | ||||
---|---|---|---|---|
| ||||
setVideo: found = true; function(stream) { } else if (display.videoMid === rTrack.midvideo) { displayvideo.setVideoremove(null); found = true; }} if (foundstream == null) { if (!display.hasAudio() && !display.hasVideo()) {video = null; this.videoMid display.dispose()= undefined; qualityDivs.forEach(function(div) { participant.displays.splice(i, 1); }div.remove(); break}); qualityDivs = }[]; } }return; }); |
2.3. LEFT
initRemoteDisplay() code
Здесь:
- участник удаляется из списка
- удаляются элементы, в которых проигрывались потоки
Code Block | ||||
---|---|---|---|---|
| ||||
}).on(constants.SFU_ROOM_EVENT.LEFT, function(e) { } video = consoledocument.logcreateElement("Received LEFTvideo"); let participant = remoteParticipants[e.name]; video.controls = "controls"; if (!participant) { video.muted = returntrue; } participant.displays.forEach(function(display){video.autoplay = true; display.dispose(); if (Browser().isSafariWebRTC()) { }) delete remoteParticipants[e.name]video.setAttribute("playsinline", ""); }); |
2.4. TRACK_QUALITY_STATE
initRemoteDisplay() code
Здесь:
- обновляется информация о качествах потоков
Code Block | ||||
---|---|---|---|---|
| ||||
}).on(constants.SFU_ROOM_EVENT.TRACK_QUALITY_STATE, function(e){ consolevideo.logsetAttribute("webkit-playsinline"Received track quality state, ""); const participant = remoteParticipants[e.info.nickName] this.setWebkitEventHandlers(video); if (!participant) } else { returnthis.setEventHandlers(video); } for (const rTrack of e.info.tracks) { streamDisplay.appendChild(video); const midvideo.srcObject = rTrack.midstream; for (let i = 0; i < participant.displays.length; i++) { this.setResizeHandler(video); }, |
3.3. Добавление аудио элемента
setAudio() code
Code Block | ||||
---|---|---|---|---|
| ||||
const display = participant.displays[i]; setAudio: function(stream) { if (display.videoMid === mid if (audio) { displayaudio.updateQualityInforemove(rTrack.quality); } break; if (!stream) { } } audio = }null; }); |
3. Создание элементов для отображения потоков участников
3.1. Создание контейнера
createRemoteDisplay() code
Здесь:
- создается контейнер для потоков участника
- создается контейнер для конкретного потока
- создается контейнер для кнопок переключения качества
Code Block | ||||
---|---|---|---|---|
| ||||
const cell = document.createElement("div");this.audioMid = undefined; return; } cell.setAttribute("class", "text-center" audio = document.createElement("audio"); cell.id = id; audio.controls mainDiv.appendChild(cell)= "controls"; const streamNameDisplay = document.createElement("div"); audio.muted = true; streamNameDisplay.innerHTML = "Published by: " + name; streamNameDisplay.setAttribute("style","width:auto; height:auto;");audio.autoplay = true; streamNameDisplay.setAttribute("class","text-center"); if cell.appendChild(streamNameDisplay);(Browser().isSafariWebRTC()) { const qualityDisplay = document.createElement("div"); qualityDisplayaudio.setAttribute("styleplaysinline","width:auto; height:auto; ""); qualityDisplayaudio.setAttribute("classwebkit-playsinline", "text-center"); cell.appendChild(qualityDisplay this.setWebkitEventHandlers(audio); let qualityDivs} =else []; { const rootDisplay = documentthis.createElementsetEventHandlers("div"audio); rootDisplay.setAttribute("style","width:auto; height:auto;"); } rootDisplay.setAttribute("class","text-center"); cell.appendChild(rootDisplayaudio); const streamDisplayaudio.srcObject = document.createElement("div")stream; streamDisplay.setAttribute("style","width:auto; height:auto;"); audio.onloadedmetadata = function (e) { streamDisplay.setAttribute("class","text-center"); rootDisplayaudio.appendChildplay(streamDisplay); |
3.2. Добавление видео элемента
setVideo() code
Code Block | ||||
---|---|---|---|---|
| ||||
.then(function() { setVideo: function(streamif (Browser().isSafariWebRTC() && Browser().isiOS()) { if (video) { console.warn("Audio track should be manually unmuted in video.remove(iOS Safari"); } } else { if (stream == null) { videoaudio.muted = nullfalse; this.videoMid = undefined;} qualityDivs.forEach(function(div) { }); }; div.remove(); }, |
3.4. Настройка обработчиков событий для аудио и видео элементов
setResizeHandler(), setEventHandlers(), setWebkitEventHandlers() code
Code Block | ||||
---|---|---|---|---|
| ||||
setResizeHandler: function(video) { }); video.addEventListener("resize", function (event) { qualityDivs = []; if (displayOptions.publisher) { return; } publisherNameDisplay.innerHTML = "Published by: " + name; video = document.createElement("video"); } video.muted = true; if (displayOptions.quality) { if(Browser().isSafariWebRTC()) { currentQualityDisplay.innerHTML = video.setAttribute("playsinline", ""); videoWidth + "x" + video.videoHeight; video.setAttribute("webkit-playsinline", ""); } }resizeVideo(event.target); streamDisplay.appendChild(video}); }, video.srcObject = stream; setEventHandlers: function(video) { video.onloadedmetadata = function (e) { // Ignore play/pause button video.play().then(functionaddEventListener("pause", function () { console.log("Media paused by click, video.muted = falsecontinue..."); }video.play(); }); }, video.addEventListener("resize",setWebkitEventHandlers: function (eventvideo) { let needRestart = false; streamNameDisplay.innerHTML = "Published by: " + namelet + "<br/>Current resolution: " + video.videoWidth + "x" + video.videoHeight; isFullscreen = false; // Use webkitbeginfullscreen event to detect full screen mode in resizeVideo(event.target);iOS Safari }); video.addEventListener("webkitbeginfullscreen", function () { }, |
3.3. Добавление аудио элемента
setAudio() code
Code Block | ||||
---|---|---|---|---|
| ||||
setAudio: function(stream) { isFullscreen = true; }); if (audio) { audio.remove(); video.addEventListener("pause", function () { } if (!streamneedRestart) { audio = null; console.log("Media paused after fullscreen, continue..."); this.audioMid = undefined; video.play(); return; needRestart = false; } audio = document.createElement("audio"); } else { audio.controls = "controls"; console.log("Media paused by click, continue..."); audio.muted = true; audio.autoplay = truevideo.play(); cell.appendChild(audio); } audio.srcObject = stream}); audio.onloadedmetadata =video.addEventListener("webkitendfullscreen", function (e) { audiovideo.play().then(function() {; needRestart audio.muted = falsetrue; })isFullscreen = false; }); }, |
3.
...
5. Настройка переключения качества
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); } } }, |
3.
...
6. Удаление контейнера с видео и аудио
dispose() code
Code Block | ||||
---|---|---|---|---|
| ||||
dispose: function() { cell.remove(); }, |
...
PeerConnection.ontrack(), setAudio(), setVideo() code
Здесь:
- при получении видео или аудио потока, добавляется элемент для его проигрывания
...
5. Остановка воспроизведения
stop() code
Code Block | ||||
---|---|---|---|---|
| ||||
const stop = function() { for (const [nickName, participant] of Object.entries(remoteParticipants)) { participant.displays.forEach(function(display){ display.dispose(); }); delete remoteParticipants[nickName]; } } |