display.js - захват и отображение видео и аудио¶
В модуль display.js
вынесены функции создания и удаления HTML5 элементов для захвата и отображения видео
Исходный код модуля¶
Для анализа исходного кода возьмем версию модуля display.js
, которая находится здесь
Захват и отображение локального видео¶
1. Инициализация¶
initLocalDisplay()
code
Функция initLocalDisplay()
возвращает объект для работы с HTML5 элементами захвата и отображения локального видео
const initLocalDisplay = function(localDisplayElement){
const localDisplayDiv = localDisplayElement;
const localDisplays = {};
const removeLocalDisplay = function(id) {
...
}
const getAudioContainer = function() {
...
};
const add = function(id, name, stream) {
...
}
const stop = function () {
...
}
const audioStateText = function (stream) {
...
}
return {
add: add,
stop: stop
}
}
2. Добавление элементов для захвата и отображения локального видео/аудио¶
2.1. Добавления аудио дорожки к HTML5 элементу¶
add()
code
Здесь:
- добавляется аудио дорожка к video элементу
- создается обработчик события
onended
для аудио дорожки
if (stream.getAudioTracks().length > 0) {
let videoElement = getAudioContainer();
if (videoElement) {
let track = stream.getAudioTracks()[0];
videoElement.video.srcObject.addTrack(track);
videoElement.audioStateDisplay.innerHTML = audioStateText(stream);
track.addEventListener("ended", function() {
videoElement.video.srcObject.removeTrack(track);
videoElement.audioStateDisplay.innerHTML = "No audio";
//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;
}
}
2.2. Создание контейнера для отображения локального видео¶
add()
code
Здесь:
- создается контейнер для элементов отображения локального видео
- создается элемент для отображения информации о публикуемом видео
const coreDisplay = document.createElement('div');
coreDisplay.setAttribute("class","text-center");
coreDisplay.setAttribute("style","width: auto; height: auto;");
coreDisplay.id = stream.id;
const streamNameDisplay = document.createElement("div");
streamNameDisplay.innerHTML = "Name: " + name;
streamNameDisplay.setAttribute("class","text-center");
streamNameDisplay.setAttribute("style","width: auto; height: auto;");
coreDisplay.appendChild(streamNameDisplay);
2.3. Создание кнопки для включения/отключения локального аудио¶
add()
code
Здесь:
- создается кнопка для включения/отключения локального аудио
- добавляется обработчик нажатия этой кнопки
const audioStateDisplay = document.createElement("button");
audioStateDisplay.innerHTML = audioStateText(stream);
audioStateDisplay.addEventListener('click', function(){
if (stream.getAudioTracks().length > 0) {
stream.getAudioTracks()[0].enabled = !(stream.getAudioTracks()[0].enabled);
audioStateDisplay.innerHTML = audioStateText(stream);
}
});
coreDisplay.appendChild(audioStateDisplay);
2.4. Создание элемента для отображения локального видео¶
add()
code
Здесь:
- создается элемент-контейнер, размеры которого можно менять в зависимости от размеров родительского элемента
- создается HTML5
video
элемент, с учетом публикации в Safari
const streamDisplay = document.createElement('div');
streamDisplay.id = "stream-" + id;
streamDisplay.setAttribute("class","text-center");
streamDisplay.setAttribute("style","width: auto; height: auto;");
coreDisplay.appendChild(streamDisplay);
const video = document.createElement("video");
video.muted = true;
if(Browser().isSafariWebRTC()) {
video.setAttribute("playsinline", "");
video.setAttribute("webkit-playsinline", "");
}
streamDisplay.appendChild(video);
video.srcObject = stream;
2.5. Создание обработчиков событий video элемента¶
add()
code
Здесь:
- запускается проигрывание локального видео
- настраивается обработчик события
onended
для видео дорожки - настраивается обработчик события
onresize
для локального видео, в котором размеры видео меняются под размеры контейнера
video.onloadedmetadata = function (e) {
video.play();
};
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);
});
});
video.addEventListener('resize', function (event) {
streamNameDisplay.innerHTML = "Name: " + name + "<br/>Max.resolution: " + video.videoWidth + "x" + video.videoHeight;
resizeVideo(event.target);
});
2.6. Добавление видео контейнера в элемент HTML страницы¶
add()
code
3. Остановка захвата видео и аудио¶
stop()
code
const stop = function () {
for (const [key, value] of Object.entries(localDisplays)) {
removeLocalDisplay(value.id);
}
}
Отображение потоков, опубликованных в комнате¶
1. Инициализация¶
initRemoteDisplay()
code
Функция initRemoteDisplay()
возвращает объект для работы с HTML5 элементами отображения видео и аудио потоков, опубликованных в комнате
const initRemoteDisplay = function(mainDiv, room, peerConnection) {
const constants = SFU.constants;
const remoteParticipants = {};
...
const createRemoteDisplay = function(id, name, mainDiv) {
...
}
const stop = function() {
...
}
peerConnection.ontrack = ({transceiver}) => {
...
}
return {
stop: stop
}
}
2. Обработка событий комнаты¶
2.1. ADD_TRACKS¶
initRemoteDisplay()
code
Здесь:
- новый участник добавляется в список
- добавляется информация о качестве потоков
- создается элемент для отображения видео и аудио потоков участника
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
Здесь:
- удаляются элементы, в которых проигрывались потоки
- информация о потоках удаляется из списка
}).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
Здесь:
- участник удаляется из списка
- удаляются элементы, в которых проигрывались потоки
}).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
Здесь:
- обновляется информация о качествах потоков
}).on(constants.SFU_ROOM_EVENT.TRACK_QUALITY_STATE, function(e){
console.log("Received track quality state");
const participant = remoteParticipants[e.info.nickName];
if (!participant) {
return;
}
for (const rTrack of e.info.tracks) {
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;
}
}
}
});
3. Создание элементов для отображения потоков участников¶
3.1. Создание контейнера¶
createRemoteDisplay()
code
Здесь:
- создается контейнер для потоков участника
- создается контейнер для конкретного потока
- создается контейнер для кнопок переключения качества
const cell = document.createElement("div");
cell.setAttribute("class", "text-center");
cell.id = id;
mainDiv.appendChild(cell);
const streamNameDisplay = document.createElement("div");
streamNameDisplay.innerHTML = "Published by: " + name;
streamNameDisplay.setAttribute("style","width:auto; height:auto;");
streamNameDisplay.setAttribute("class","text-center");
cell.appendChild(streamNameDisplay);
const qualityDisplay = document.createElement("div");
qualityDisplay.setAttribute("style","width:auto; height:auto;");
qualityDisplay.setAttribute("class","text-center");
cell.appendChild(qualityDisplay);
let qualityDivs = [];
const rootDisplay = document.createElement("div");
rootDisplay.setAttribute("style","width:auto; height:auto;");
rootDisplay.setAttribute("class","text-center");
cell.appendChild(rootDisplay);
const streamDisplay = document.createElement("div");
streamDisplay.setAttribute("style","width:auto; height:auto;");
streamDisplay.setAttribute("class","text-center");
rootDisplay.appendChild(streamDisplay);
3.2. Добавление видео элемента¶
setVideo()
code
setVideo: function(stream) {
if (video) {
video.remove();
}
if (stream == null) {
video = null;
this.videoMid = undefined;
qualityDivs.forEach(function(div) {
div.remove();
});
qualityDivs = [];
return;
}
video = document.createElement("video");
video.muted = true;
if(Browser().isSafariWebRTC()) {
video.setAttribute("playsinline", "");
video.setAttribute("webkit-playsinline", "");
}
streamDisplay.appendChild(video);
video.srcObject = stream;
video.onloadedmetadata = function (e) {
video.play().then(function() {
video.muted = false;
});
};
video.addEventListener("resize", function (event) {
streamNameDisplay.innerHTML = "Published by: " + name + "<br/>Current resolution: " + video.videoWidth + "x" + video.videoHeight;
resizeVideo(event.target);
});
},
3.3. Добавление аудио элемента¶
setAudio()
code
setAudio: function(stream) {
if (audio) {
audio.remove();
}
if (!stream) {
audio = null;
this.audioMid = undefined;
return;
}
audio = document.createElement("audio");
audio.controls = "controls";
audio.muted = true;
audio.autoplay = true;
cell.appendChild(audio);
audio.srcObject = stream;
audio.onloadedmetadata = function (e) {
audio.play().then(function() {
audio.muted = false;
});
};
},
3.4. Настройка переключения качества¶
setTrackInfo()
code
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.5. Удаление контейнера с видео и аудио¶
dispose()
code
4. Подписка на добавление дорожек в WebRTC соединение¶
PeerConnection.ontrack()
, setAudio()
, setVideo()
code
Здесь:
- при получении видео или аудио потока, добавляется элемент для его проигрывания
peerConnection.ontrack = ({transceiver}) => {
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;
}
}
if (rParticipant) {
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;
}
}
}
} else {
console.warn("Failed to find participant for track " + transceiver.receiver.track.id);
}
}
5. Остановка воспроизведения¶
stop()
code