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 onMuteClick = function (button, stream, type) {
...
}
const add = function (id, name, stream, type) {
...
}
const stop = function () {
...
}
const audioStateText = function (stream) {
...
}
return {
add: add,
stop: stop
}
}
2. Добавление элементов для захвата и отображения локального видео/аудио¶
2.1. Добавления аудио дорожки к HTML5 элементу¶
add()
code
Здесь:
- добавляется аудио дорожка к видео элементу
- создается обработчик события
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) + " " + type;
videoElement.audioStateDisplay.addEventListener("click", function () {
onMuteClick(videoElement.audioStateDisplay, stream, type);
});
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 = createContainer(null);
coreDisplay.id = stream.id;
const publisherNameDisplay = createInfoDisplay(coreDisplay, name + " " + type);
2.3. Создание кнопки для включения/отключения локального аудио¶
add()
code
Здесь:
- создается кнопка для включения/отключения локального аудио
const audioStateDisplay = document.createElement("button");
coreDisplay.appendChild(audioStateDisplay);
2.4. Создание элемента для отображения локального видео¶
add()
code
Здесь:
- создается элемент-контейнер, размеры которого можно менять в зависимости от размеров родительского элемента
- создается HTML5
video
элемент, с учетом публикации в Safari
const streamDisplay = createContainer(coreDisplay);
streamDisplay.id = "stream-" + id;
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);
});
});
if (stream.getVideoTracks().length > 0) {
// Resize only if video displayed
video.addEventListener('resize', function (event) {
publisherNameDisplay.innerHTML = name + " " + type + " " + video.videoWidth + "x" + video.videoHeight;
resizeVideo(event.target);
});
} else {
// Hide audio only container
hideItem(streamDisplay);
// Set up mute button for audio only stream
audioStateDisplay.innerHTML = audioStateText(stream) + " " + type;
audioStateDisplay.addEventListener("click", function() {
onMuteClick(audioStateDisplay, stream, type);
});
}
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 элементами отображения видео и аудио потоков, опубликованных в комнате
/*
display options:
autoAbr - choose abr by default
quality - show quality buttons
showAudio - show audio elements
*/
const initRemoteDisplay = function (room, div, displayOptions, abrOptions, meetingController, meetingModel, meetingView, participantFactory) {
// Validate options first
if (!div) {
throw new Error("Main div to place all the media tag is not defined");
}
if (!room) {
throw new Error("Room is not defined");
}
const dOptions = displayOptions || {quality: true, type: true, showAudio: false};
let abrFactory;
if (abrOptions) {
abrFactory = abrManagerFactory(room, abrOptions);
}
participantFactory.abrFactory = abrFactory;
participantFactory.displayOptions = dOptions;
return meetingController(room, meetingModel(meetingView(div), participantFactory));
}
2. Создание фабрики объектов для управления проигрыванием WebRTC ABR потока¶
abrManagerFactory()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
...
return abr;
}
}
}
2.1. Инициализация объекта управления ABR¶
createAbrManager()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
track: null,
interval: abrOptions.interval,
thresholds: abrOptions.thresholds,
qualities: [],
currentQualityName: null,
statTimer: null,
paused: false,
manual: false,
keepGoodTimeout: abrOptions.abrKeepOnGoodQuality,
keepGoodTimer: null,
tryUpperTimeout: abrOptions.abrTryForUpperQuality,
tryUpperTimer: null,
...
}
return abr;
}
}
}
2.2. Запуск автоматического выбора качества ABR¶
abr.start()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
start: function () {
this.stop();
console.log("Start abr interval")
if (abr.interval) {
const thresholds = Thresholds();
for (const threshold of abr.thresholds) {
thresholds.add(threshold.parameter, threshold.maxLeap);
}
abr.statsTimer = setInterval(() => {
if (abr.track) {
room.getStats(abr.track.track, constants.SFU_RTC_STATS_TYPE.INBOUND, (stats) => {
if (thresholds.isReached(stats)) {
abr.shiftDown();
} else {
abr.useGoodQuality();
}
});
}
}, abr.interval);
}
},
...
}
return abr;
}
}
}
2.3. Остановка автоматического выбора качества ABR¶
abr.stop()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
stop: function () {
console.log("Stop abr interval")
abr.stopKeeping();
abr.stopTrying();
if (abr.statsTimer) {
clearInterval(abr.statsTimer);
abr.statsTimer = null;
}
},
...
}
return abr;
}
}
}
2.4. Добавление информации о видеодорожке¶
abr.setTrack()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
setTrack: function (track) {
abr.track = track;
},
...
}
return abr;
}
}
}
2.5. Добавление описания ABR качества в список для выбора¶
abr.addQuality()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
addQuality: function (name) {
abr.qualities.push({name: name, available: false, good: true});
},
...
}
return abr;
}
}
}
2.6. Установка признака доступности качества для проигрывания¶
abr.setQualityAvailable()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
setQualityAvailable: function (name, available) {
for (let i = 0; i < abr.qualities.length; i++) {
if (name === abr.qualities[i].name) {
abr.qualities[i].available = available;
}
}
},
...
}
return abr;
}
}
}
2.7. Установка признака хорошего качества для текущего состояния канала¶
abr.setQualityGood()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
setQualityGood: function (name, good) {
if (name) {
for (let i = 0; i < abr.qualities.length; i++) {
if (name === abr.qualities[i].name) {
abr.qualities[i].good = good;
}
}
}
},
...
}
return abr;
}
}
}
2.8. Получение первого доступного качества¶
abr.getFirstAvailableQuality()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
getFirstAvailableQuality: function () {
for (let i = 0; i < abr.qualities.length; i++) {
if (abr.qualities[i].available) {
return abr.qualities[i];
}
}
return null;
},
...
}
return abr;
}
}
}
2.9. Получение более низкого качества¶
abr.getLowerQuality()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
getLowerQuality: function (name) {
let quality = null;
if (!name) {
// There were no switching yet, return a first available quality
return abr.getFirstAvailableQuality();
}
let currentIndex = abr.qualities.map(item => item.name).indexOf(name);
for (let i = 0; i < currentIndex; i++) {
if (abr.qualities[i].available) {
quality = abr.qualities[i];
}
}
return quality;
},
...
}
return abr;
}
}
}
2.10. Получение более высокого качества¶
abr.getUpperQuality()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
getUpperQuality: function (name) {
let quality = null;
if (!name) {
// There were no switching yet, return a first available quality
return abr.getFirstAvailableQuality();
}
let currentIndex = abr.qualities.map(item => item.name).indexOf(name);
for (let i = currentIndex + 1; i < abr.qualities.length; i++) {
if (abr.qualities[i].available) {
quality = abr.qualities[i];
break;
}
}
return quality;
},
...
}
return abr;
}
}
}
2.11. Переключение качества вниз¶
add.shiftDown()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
shiftDown: function () {
if (!abr.manual && !abr.paused) {
abr.stopKeeping();
abr.setQualityGood(abr.currentQualityName, false);
let quality = abr.getLowerQuality(abr.currentQualityName);
if (quality) {
console.log("Switching down to " + quality.name + " quality");
abr.setQuality(quality.name);
}
}
},
...
}
return abr;
}
}
}
2.12. Переключение качества вверх¶
abr.shiftUp()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
shiftUp: function () {
if (!abr.manual && !abr.paused) {
let quality = abr.getUpperQuality(abr.currentQualityName);
if (quality) {
if (quality.good) {
console.log("Switching up to " + quality.name + " quality");
abr.setQuality(quality.name);
} else {
abr.tryUpper();
}
}
}
},
...
}
return abr;
}
}
}
2.13. Использовать текущее качество как хорошее¶
abr.useGoodQuality()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
useGoodQuality: function () {
if (!abr.manual && !abr.paused) {
if (!abr.currentQualityName) {
let quality = abr.getFirstAvailableQuality();
abr.currentQualityName = quality.name;
}
abr.setQualityGood(abr.currentQualityName, true);
abr.keepGoodQuality();
}
},
...
}
return abr;
}
}
}
2.14. Установить таймер использования текущего качества¶
abr.keepGoodQuality()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
keepGoodQuality: function () {
if (abr.keepGoodTimeout && !abr.keepGoodTimer && abr.getUpperQuality(abr.currentQualityName)) {
console.log("start keepGoodTimer");
abr.keepGoodTimer = setTimeout(() => {
abr.shiftUp();
abr.stopKeeping();
}, abr.keepGoodTimeout);
}
},
...
}
return abr;
}
}
}
2.15. Сбросить таймер использования текущего качества¶
abr.stopKeeping()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
stopKeeping: function () {
if (abr.keepGoodTimer) {
clearTimeout(abr.keepGoodTimer);
abr.keepGoodTimer = null;
}
},
...
}
return abr;
}
}
}
2.16. Переключиться на более высокое качество на заданное время¶
abr.tryUpper()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
tryUpper: function () {
let quality = abr.getUpperQuality(abr.currentQualityName);
if (abr.tryUpperTimeout && !abr.tryUpperTimer && quality) {
abr.tryUpperTimer = setTimeout(() => {
abr.setQualityGood(quality.name, true);
abr.stopTrying();
}, abr.tryUpperTimeout);
}
},
...
}
return abr;
}
}
}
2.17. Остановить таймер тестирования более высокого качества¶
abr.stopTrying()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
stopTrying: function () {
if (abr.tryUpperTimer) {
clearTimeout(abr.tryUpperTimer);
abr.tryUpperTimer = null;
}
},
...
}
return abr;
}
}
}
2.18. Переключиться на указанное качество¶
abr.setQuality()
code
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
setQuality: async function (name) {
console.log("set quality name");
// Pause switching until a new quality is received
abr.pause();
abr.currentQualityName = name;
abr.track.setPreferredQuality(abr.currentQualityName);
}
}
return abr;
}
}
}
3. Создание объекта для управления комнатой¶
createDefaultMeetingController()
code
3.1. Обработка события PARTICIPANT_LIST¶
createDefaultMeetingController()
code
const createDefaultMeetingController = function (room, meetingModel) {
const constants = SFU.constants;
room.on(constants.SFU_ROOM_EVENT.PARTICIPANT_LIST, async function (e) {
for (const idName of e.participants) {
meetingModel.addParticipant(idName.userId, idName.name);
}
...
});
...
}
3.2. Обработка события JOINED¶
createDefaultMeetingController()
code
const createDefaultMeetingController = function (room, meetingModel) {
const constants = SFU.constants;
room.on(constants.SFU_ROOM_EVENT.PARTICIPANT_LIST, async function (e) {
...
}).on(constants.SFU_ROOM_EVENT.JOINED, async function (e) {
meetingModel.addParticipant(e.userId, e.name);
...
});
...
}
3.3. Обработка события LEFT¶
createDefaultMeetingController()
code
const createDefaultMeetingController = function (room, meetingModel) {
const constants = SFU.constants;
room.on(constants.SFU_ROOM_EVENT.PARTICIPANT_LIST, async function (e) {
...
}).on(constants.SFU_ROOM_EVENT.LEFT, function (e) {
meetingModel.removeParticipant(e.userId);
...
});
...
}
3.4. Обработка события ADD_TRACKS¶
createDefaultMeetingController()
code
const createDefaultMeetingController = function (room, meetingModel) {
const constants = SFU.constants;
room.on(constants.SFU_ROOM_EVENT.PARTICIPANT_LIST, async function (e) {
...
}).on(constants.SFU_ROOM_EVENT.ADD_TRACKS, async function (e) {
meetingModel.addTracks(e.info.userId, e.info.info);
...
});
...
}
3.5. Обработка события REMOVE_TRACKS¶
createDefaultMeetingController()
code
const createDefaultMeetingController = function (room, meetingModel) {
const constants = SFU.constants;
room.on(constants.SFU_ROOM_EVENT.PARTICIPANT_LIST, async function (e) {
...
}).on(constants.SFU_ROOM_EVENT.REMOVE_TRACKS, async function (e) {
meetingModel.removeTracks(e.info.userId, e.info.info);
...
});
...
}
3.6. Обработка события TRACK_QUALITY_STATE¶
createDefaultMeetingController()
code
const createDefaultMeetingController = function (room, meetingModel) {
const constants = SFU.constants;
room.on(constants.SFU_ROOM_EVENT.PARTICIPANT_LIST, async function (e) {
...
}).on(constants.SFU_ROOM_EVENT.TRACK_QUALITY_STATE, async function (e) {
meetingModel.updateQualityInfo(e.info.userId, e.info.tracks);
...
});
...
}
3.7. Обработка события ENDED¶
createDefaultMeetingController()
code
const createDefaultMeetingController = function (room, meetingModel) {
const constants = SFU.constants;
room.on(constants.SFU_ROOM_EVENT.PARTICIPANT_LIST, async function (e) {
...
}).on(constants.SFU_ROOM_EVENT.ENDED, function (e) {
meetingModel.end();
});
...
}
3.8. Остановка комнаты¶
createDefaultMeetingController()
code
const createDefaultMeetingController = function (room, meetingModel) {
...
const stop = function () {
meetingModel.end();
};
return {
stop: stop
}
}
4. Создание модели комнаты¶
createDefaultMeetingModel()
code
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
...
}
4.1. Добавление участника¶
addParticipant()
code
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
return {
...
addParticipant: function (userId, participantName) {
if (this.participants.get(userId)) {
return;
}
const [participantModel, participantView, participant] = participantFactory.createParticipant(userId, participantName, displayOptions, abrFactory);
this.participants.set(userId, participant);
meetingView.addParticipant(userId, participantName, participantView.rootDiv);
},
...
}
}
4.2. Удаление участника¶
removeParticipant()
code
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
return {
...
removeParticipant: function (userId) {
const participant = this.participants.get(userId);
if (participant) {
this.participants.delete(userId);
meetingView.removeParticipant(userId);
participant.dispose();
}
},
...
}
}
4.3. Переименование участника¶
renameParticipant()
code
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
return {
...
renameParticipant: function (userId, newNickname) {
const participant = this.participants.get(userId);
if (participant) {
participant.setNickname(newNickname);
}
},
...
}
}
4.4. Добавление треков участника для отображения¶
addTracks()
code
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
return {
...
addTracks: function (userId, tracks) {
const participant = this.participants.get(userId);
if (!participant) {
return;
}
for (const track of tracks) {
if (track.type === "VIDEO") {
participant.addVideoTrack(track);
} else if (track.type === "AUDIO") {
participant.addAudioTrack(track);
}
}
},
...
}
}
4.5. Удаление отображаемых треков участника¶
removeTracks()
code
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
return {
...
removeTracks: function (userId, tracks) {
const participant = this.participants.get(userId);
if (!participant) {
return;
}
for (const track of tracks) {
if (track.type === "VIDEO") {
participant.removeVideoTrack(track);
} else if (track.type === "AUDIO") {
participant.removeAudioTrack(track);
}
}
},
...
}
}
4.6. Обновление информации о качестве треков участника¶
updateQualityInfo()
code
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
return {
...
updateQualityInfo: function (userId, tracksInfo) {
const participant = this.participants.get(userId);
if (!participant) {
return;
}
participant.updateQualityInfo(tracksInfo);
},
...
}
}
4.7. Завершение отображения участников при остановке комнаты¶
end()
code
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
return {
...
end: function () {
console.log("Meeting " + this.meetingName + " ended")
meetingView.end();
this.participants.forEach((participant, id) => {
participant.dispose();
});
this.participants.clear();
},
...
}
}
4.8. Переименование комнаты¶
setMeetingName()
code
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
return {
...
setMeetingName: function (id) {
this.meetingName = id;
meetingView.setMeetingName(id);
}
}
}
5. Создание объекта для отображения потоков в комнате¶
createDefaultMeetingView()
code
5.1. Инициализация HTML5 элементов¶
createDefaultMeetingView()
code
const createDefaultMeetingView = function (entryPoint) {
const rootDiv = document.createElement("div");
rootDiv.setAttribute("class", "grid-item");
entryPoint.appendChild(rootDiv);
const title = document.createElement("label");
title.setAttribute("style", "display:block; border: solid; border-width: 1px");
rootDiv.appendChild(title);
return {
...
}
}
5.2. Инициализация списка элементов для отображения участников¶
participantViews()
code
const createDefaultMeetingView = function (entryPoint) {
...
return {
participantViews: new Map(),
...
}
}
5.3. Отображение имени комнаты¶
setMeetingName()
code
const createDefaultMeetingView = function (entryPoint) {
...
return {
...
setMeetingName: function (id) {
title.innerText = "Meeting: " + id;
},
...
}
}
5.4. Добавление элементов для отображения участника¶
addParticipant()
code
const createDefaultMeetingView = function (entryPoint) {
...
return {
...
addParticipant: function (userId, participantName, cell) {
const participantDiv = createContainer(rootDiv);
participantDiv.appendChild(cell);
this.participantViews.set(userId, participantDiv);
},
...
}
}
5.5. Удаление элементов участника¶
removeParticipant()
code
const createDefaultMeetingView = function (entryPoint) {
...
return {
...
removeParticipant: function (userId) {
const cell = this.participantViews.get(userId);
if (cell) {
this.participantViews.delete(userId);
cell.remove();
}
},
...
}
}
5.6. Удаление корневого элемента отображения¶
end()
code
const createDefaultMeetingView = function (entryPoint) {
...
return {
...
end: function () {
rootDiv.remove();
}
}
}
6. Создание фабрики объектов участников¶
createParticipantFactory()
code
Here a participant model, view and controller objects are created for the certain participant
const createParticipantFactory = function (remoteTrackFactory, createParticipantView, createParticipantModel) {
return {
displayOptions: null,
abrFactory: null,
createParticipant: function (userId, nickname) {
const view = createParticipantView();
const model = createParticipantModel(userId, nickname, view, remoteTrackFactory, this.abrFactory, this.displayOptions);
const controller = createParticipantController(model);
return [model, view, controller];
}
}
}
7. Создание объекта управления участником¶
createParticipantController()
code
The object calls an appropriate participant model methods
const createParticipantController = function (model) {
return {
addVideoTrack: function (track) {
model.addVideoTrack(track);
},
removeVideoTrack: function (track) {
model.removeVideoTrack(track);
},
addAudioTrack: function (track) {
model.addAudioTrack(track);
},
removeAudioTrack: function (track) {
model.removeAudioTrack(track);
},
updateQualityInfo: function (qualityInfo) {
model.updateQualityInfo(qualityInfo);
},
setNickname: function (nickname) {
model.setNickname(nickname);
},
dispose: function () {
model.dispose();
}
}
}
8. Создание объекта модели участника в комнате с двумя участниками¶
createOneToOneParticipantModel()
code
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
const instance = {
userId: userId,
nickname: nickname,
remoteVideoTracks: new Map(),
remoteAudioTracks: new Map(),
audioTracks: new Map(),
videoTracks: new Map(),
abrManagers: new Map(),
disposed: false,
dispose: async function () {
...
},
addVideoTrack: function (track) {
...
},
removeVideoTrack: function (track) {
...
},
addAudioTrack: function (track) {
...
},
removeAudioTrack: function (track) {
...
},
setUserId: function (userId) {
...
},
setNickname: function (nickname) {
...
},
updateQualityInfo: function (remoteTracks) {
...
},
requestVideoTrack: async function (track, remoteTrack) {
...
},
pickQuality: async function (track, qualityName) {
...
},
muteVideo: async function (track) {
...
},
unmuteVideo: async function (track) {
...
}
};
instance.setUserId(userId);
instance.setNickname(nickname);
return instance;
}
8.1. Добавление видео дорожки для отображения¶
addVideoTrack()
code
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
const instance = {
...
addVideoTrack: function (track) {
this.videoTracks.set(track.mid, track);
if (!track.quality) {
track.quality = [];
}
participantView.addVideoTrack(track);
const self = this;
remoteTrackFactory.getVideoTrack().then((remoteTrack) => {
if (remoteTrack) {
if (self.disposed || !self.videoTracks.get(track.mid)) {
remoteTrack.dispose();
return;
}
participantView.addVideoSource(remoteTrack.track, track, () => {
const abrManager = self.abrManagers.get(track.id);
if (!abrManager) {
return;
}
if (abrManager.isAuto()) {
abrManager.resume();
}
}, (mute) => {
if (mute) {
return self.muteVideo(track);
} else {
return self.unmuteVideo(track);
}
});
self.requestVideoTrack(track, remoteTrack).then(() => {
participantView.showVideoTrack(track);
}, (ex) => {
participantView.removeVideoSource(track);
remoteTrack.dispose();
});
}
}, (ex) => {
console.log("Failed to get remote track " + ex);
});
},
...
};
...
return instance;
}
8.2. Удаление отображаемой видео дорожки¶
removeVideoTrack()
code
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
const instance = {
...
removeVideoTrack: function (track) {
if (this.videoTracks.delete(track.mid)) {
const remoteTrack = this.remoteVideoTracks.get(track.mid);
if (remoteTrack) {
this.remoteVideoTracks.delete(track.mid);
remoteTrack.dispose();
}
participantView.removeVideoTrack(track);
const abrManager = this.abrManagers.get(track.id);
if (abrManager) {
this.abrManagers.delete(track.id);
abrManager.clearQualityState();
abrManager.stop();
}
}
},
...
};
...
return instance;
}
8.3. Добавление аудио дорожки для отображения¶
addAudioTrack()
code
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
const instance = {
...
addAudioTrack: function (track) {
this.audioTracks.set(track.mid, track);
const self = this;
remoteTrackFactory.getAudioTrack().then((remoteTrack) => {
if (remoteTrack) {
if (self.disposed || !self.audioTracks.get(track.mid)) {
remoteTrack.dispose();
return;
}
this.remoteAudioTracks.set(track.mid, remoteTrack);
remoteTrack.demandTrack(track.id).then(() => {
if (!self.audioTracks.get(track.mid)) {
remoteTrack.dispose();
self.remoteAudioTracks.delete(track.mid);
return;
}
participantView.addAudioTrack(track, remoteTrack.track, displayOptions.showAudio);
}, (ex) => {
console.log("Failed demand track " + ex);
remoteTrack.dispose();
self.remoteAudioTracks.delete(track.mid);
});
}
}, (ex) => {
console.log("Failed to get audio track " + ex);
});
},
...
};
...
return instance;
}
8.4. Удаление отображаемой аудио дорожки¶
removeAudioTrack()
code
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
const instance = {
...
removeAudioTrack: function (track) {
if (!this.audioTracks.delete(track.mid)) {
return
}
participantView.removeAudioTrack(track);
const remoteTrack = this.remoteAudioTracks.get(track.mid);
if (remoteTrack) {
this.remoteAudioTracks.delete(track.mid);
remoteTrack.dispose();
}
},
...
};
...
return instance;
}
8.5. Запрос видео дорожки для отображения¶
requestVideoTrack()
code
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
const instance = {
...
requestVideoTrack: async function (track, remoteTrack) {
return new Promise((resolve, reject) => {
if (!remoteTrack || !track) {
reject(new Error("Remote and local track must be defined"));
return;
}
const self = this;
remoteTrack.demandTrack(track.id).then(() => {
if (!self.videoTracks.get(track.mid)) {
reject(new Error("Video track already removed from model"));
return;
}
let abrManager = self.abrManagers.get(track.id);
if (abrManager) {
abrManager.clearQualityState();
} else if (abrFactory) {
abrManager = abrFactory.createAbrManager();
self.abrManagers.set(track.id, abrManager);
}
if (abrManager) {
abrManager.setTrack(remoteTrack);
abrManager.stop();
if (track.quality.length > 0) {
participantView.addQuality(track, "Auto", true, async () => {
const manager = self.abrManagers.get(track.id);
if (!manager) {
return;
}
manager.start();
manager.setAuto();
participantView.pickQuality(track, "Auto");
});
if (displayOptions.autoAbr) {
abrManager.setAuto();
abrManager.start();
participantView.pickQuality(track, "Auto");
}
}
}
for (const qualityDescriptor of track.quality) {
if (abrManager) {
abrManager.addQuality(qualityDescriptor.quality);
abrManager.setQualityAvailable(qualityDescriptor.quality, qualityDescriptor.available);
}
if (displayOptions.quality) {
participantView.addQuality(track, qualityDescriptor.quality, qualityDescriptor.available, async () => {
const manager = self.abrManagers.get(track.id);
if (manager) {
manager.setManual();
manager.setQuality(qualityDescriptor.quality);
}
return self.pickQuality(track, qualityDescriptor.quality);
});
}
}
self.remoteVideoTracks.delete(track.mid);
self.remoteVideoTracks.set(track.mid, remoteTrack);
resolve();
}, (ex) => {
reject(ex);
});
});
},
...
};
...
return instance;
}
8.6. Обновление информации о доступных качествах потока¶
updateQualityInfo()
code
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
const instance = {
...
updateQualityInfo: function (remoteTracks) {
for (const remoteTrackQuality of remoteTracks) {
const track = this.videoTracks.get(remoteTrackQuality.mid);
if (!track) {
continue;
}
if (!this.remoteVideoTracks.get(track.mid)) {
// update model and return, view not changed
for (const remoteQualityInfo of remoteTrackQuality.quality) {
const quality = track.quality.find((q) => q.quality === remoteQualityInfo.quality);
if (quality) {
quality.available = remoteQualityInfo.available;
} else {
track.quality.push(remoteQualityInfo);
}
}
return;
}
let abrManager = this.abrManagers.get(track.id);
if (abrManager && track.quality.length === 0 && remoteTrackQuality.quality.length > 0) {
const self = this;
participantView.addQuality(track, "Auto", true, async () => {
const manager = self.abrManagers.get(track.id);
if (!manager) {
return;
}
manager.start();
manager.setAuto();
participantView.pickQuality(track, "Auto");
})
if (displayOptions.autoAbr) {
abrManager.setAuto();
abrManager.start();
participantView.pickQuality(track, "Auto");
}
}
for (const remoteQualityInfo of remoteTrackQuality.quality) {
const localQuality = track.quality.find((q) => q.quality === remoteQualityInfo.quality);
if (localQuality) {
localQuality.available = remoteQualityInfo.available;
if (abrManager) {
abrManager.setQualityAvailable(remoteQualityInfo.quality, remoteQualityInfo.available);
}
if (displayOptions.quality) {
participantView.updateQuality(track, localQuality.quality, localQuality.available);
}
} else {
track.quality.push(remoteQualityInfo);
if (abrManager) {
abrManager.addQuality(remoteQualityInfo.quality);
abrManager.setQualityAvailable(remoteQualityInfo.quality, remoteQualityInfo.available)
}
if (displayOptions.quality) {
const self = this;
participantView.addQuality(track, remoteQualityInfo.quality, remoteQualityInfo.available, async () => {
const manager = self.abrManagers.get(track.id);
if (manager) {
manager.setManual();
manager.setQuality(remoteQualityInfo.quality);
}
return self.pickQuality(track, remoteQualityInfo.quality);
});
}
}
}
}
},
...
};
...
return instance;
}
8.7. Выбор качества для проигрывания¶
pickQuality()
code
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
const instance = {
...
pickQuality: async function (track, qualityName) {
let remoteVideoTrack = this.remoteVideoTracks.get(track.mid);
if (remoteVideoTrack) {
return remoteVideoTrack.setPreferredQuality(qualityName).then(() => {
participantView.pickQuality(track, qualityName);
});
}
},
...
};
...
return instance;
}
8.8. Заглушить видео участника¶
muteVideo()
code
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
const instance = {
...
muteVideo: async function (track) {
const remoteTrack = this.remoteVideoTracks.get(track.mid);
if (remoteTrack) {
return remoteTrack.mute();
} else {
return new Promise((resolve, reject) => {
reject(new Error("Remote track not defined"));
});
}
},
...
};
...
return instance;
}
8.9. Возобновить видео участника¶
unmuteVideo()
code
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
const instance = {
...
unmuteVideo: async function (track) {
const remoteTrack = this.remoteVideoTracks.get(track.mid);
if (remoteTrack) {
return remoteTrack.unmute();
} else {
return new Promise((resolve, reject) => {
reject(new Error("Remote track not defined"));
});
}
}
};
...
return instance;
}
8.10. Завершение работы¶
dispose()
code
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
const instance = {
...
dispose: async function () {
this.disposed = true;
participantView.dispose();
this.remoteVideoTracks.forEach((track, id) => {
track.dispose();
})
this.remoteVideoTracks.clear();
this.remoteAudioTracks.forEach((track, id) => {
track.dispose();
})
this.remoteAudioTracks.clear();
this.abrManagers.forEach((abrManager, id) => {
abrManager.stop();
})
this.abrManagers.clear();
},
};
...
return instance;
}
9. Создание объекта модели участника в комнате со многими участниками¶
createOneToManyParticipantModel()
code
const createOneToManyParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
...
const instance = {
userId: userId,
nickname: nickname,
videoEnabled: false,
currentTrack: null,
remoteVideoTrack: null,
remoteAudioTracks: new Map(),
audioTracks: new Map(),
videoTracks: new Map(),
abr: null,
disposed: false,
dispose: async function () {
...
},
addVideoTrack: function (track) {
...
},
removeVideoTrack: function (track) {
...
},
addAudioTrack: function (track) {
...
},
removeAudioTrack: function (track) {
...
},
setUserId: function (userId) {
...
},
setNickname: function (nickname) {
...
},
updateQualityInfo: function (remoteTracks) {
...
},
requestVideoTrack: async function (track, remoteTrack) {
...
},
pickQuality: async function (track, qualityName) {
...
},
muteVideo: async function (track) {
...
},
unmuteVideo: async function (track) {
...
}
};
instance.setUserId(userId);
instance.setNickname(nickname);
if (abrFactory) {
instance.abr = abrFactory.createAbrManager();
}
return instance;
}
9.1. Добавление видео дорожки для отображения¶
addVideoTrack()
code
const createOneToManyParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
...
const instance = {
...
addVideoTrack: function (track) {
this.videoTracks.set(track.mid, track);
if (!track.quality) {
track.quality = [];
}
const self = this;
participantView.addVideoTrack(track, () => {
if (self.disposed) {
return new Promise((resolve, reject) => {
reject(new Error("Model disposed"));
});
}
if (self.remoteVideoTrack) {
return new Promise((resolve, reject) => {
self.requestVideoTrack(track, self.remoteVideoTrack).then(() => {
resolve();
}, (ex) => {
reject(ex);
});
});
} else {
return new Promise((resolve, reject) => {
reject(new Error("Remote track is null"));
requestTrackAndPick(self, track);
});
}
});
requestTrackAndPick(this, track);
},
...
};
...
return instance;
}
9.2. Удаление отображаемой видео дорожки¶
removeVideoTrack()
code
const createOneToManyParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
...
const instance = {
...
removeVideoTrack: function (track) {
this.videoTracks.delete(track.mid);
participantView.removeVideoTrack(track);
if (this.currentTrack && this.currentTrack.mid === track.mid) {
repickTrack(this, track);
}
},
...
};
...
return instance;
}
9.3. Добавление аудио дорожки для отображения¶
addAudioTrack()
code
const createOneToManyParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
...
const instance = {
...
addAudioTrack: async function (track) {
this.audioTracks.set(track.mid, track);
const self = this;
remoteTrackFactory.getAudioTrack().then((remoteTrack) => {
if (!remoteTrack) {
return;
}
if (self.disposed || !self.audioTracks.get(track.mid)) {
remoteTrack.dispose();
return;
}
this.remoteAudioTracks.set(track.mid, remoteTrack);
remoteTrack.demandTrack(track.id).then(() => {
if (!self.audioTracks.get(track.mid)) {
remoteTrack.dispose();
self.remoteAudioTracks.delete(track.mid);
return;
}
participantView.addAudioTrack(track, remoteTrack.track, displayOptions.showAudio);
}, (ex) => {
console.log("Failed demand track " + ex);
remoteTrack.dispose();
self.remoteAudioTracks.delete(track.mid);
});
}, (ex) => {
console.log("Failed to get audio track " + ex);
});
},
...
};
...
return instance;
}
9.4. Удаление отображаемой аудио дорожки¶
removeAudioTrack()
code
const createOneToManyParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
...
const instance = {
...
removeAudioTrack: function (track) {
if (!this.audioTracks.delete(track.mid)) {
return
}
participantView.removeAudioTrack(track);
const remoteTrack = this.remoteAudioTracks.get(track.mid);
if (remoteTrack) {
this.remoteAudioTracks.delete(track.mid);
remoteTrack.dispose();
}
},
...
};
...
return instance;
}
9.5. Запрос видео дорожки для отображения¶
requestVideoTrack()
code
const createOneToManyParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
...
const instance = {
...
requestVideoTrack: async function (track, remoteTrack) {
return new Promise((resolve, reject) => {
if (!remoteTrack || !track) {
reject(new Error("Remote and local track must be defined"));
return;
}
const self = this;
remoteTrack.demandTrack(track.id).then(() => {
// channels reordering case, must be removed after channels unification
if (!self.videoTracks.get(track.mid)) {
reject(new Error("Video track already removed from model"));
return;
}
self.currentTrack = track;
participantView.clearQualityState(track);
if (self.abr) {
self.abr.stop();
self.abr.clearQualityState();
self.abr.setTrack(remoteTrack);
if (track.quality.length > 0) {
participantView.addQuality(track, "Auto", true, async () => {
if (!self.abr) {
return;
}
self.abr.start();
self.abr.setAuto();
participantView.pickQuality(track, "Auto");
})
}
if (displayOptions.autoAbr) {
self.abr.setAuto();
self.abr.start();
participantView.pickQuality(track, "Auto");
}
}
for (const qualityDescriptor of track.quality) {
if (self.abr) {
self.abr.addQuality(qualityDescriptor.quality);
self.abr.setQualityAvailable(qualityDescriptor.quality, qualityDescriptor.available);
}
if (displayOptions.quality) {
participantView.addQuality(track, qualityDescriptor.quality, qualityDescriptor.available, async () => {
if (self.abr) {
self.abr.setManual();
self.abr.setQuality(qualityDescriptor.quality);
}
return self.pickQuality(track, qualityDescriptor.quality);
});
}
}
resolve();
}, (ex) => reject(ex));
});
},
...
};
...
return instance;
}
9.6. Обновление информации о доступных качествах потока¶
updateQualityInfo()
code
const createOneToManyParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
...
const instance = {
...
updateQualityInfo: function (remoteTracks) {
for (const remoteTrackQuality of remoteTracks) {
const track = this.videoTracks.get(remoteTrackQuality.mid);
if (!track) {
continue;
}
if (!this.currentTrack || this.currentTrack.mid !== track.mid) {
// update model and return, view not changed
for (const remoteQualityInfo of remoteTrackQuality.quality) {
const quality = track.quality.find((q) => q.quality === remoteQualityInfo.quality);
if (quality) {
quality.available = remoteQualityInfo.available;
} else {
track.quality.push(remoteQualityInfo);
}
}
return;
}
if (this.abr && track.quality.length === 0 && remoteTrackQuality.quality.length > 0) {
const self = this;
participantView.addQuality(track, "Auto", true, async () => {
if (!self.abr) {
return;
}
self.abr.start();
self.abr.setAuto();
participantView.pickQuality(track, "Auto");
})
if (displayOptions.autoAbr && this.abr) {
this.abr.setAuto();
this.abr.start();
participantView.pickQuality(track, "Auto");
}
}
for (const remoteQualityInfo of remoteTrackQuality.quality) {
const localQuality = track.quality.find((q) => q.quality === remoteQualityInfo.quality);
if (localQuality) {
localQuality.available = remoteQualityInfo.available;
if (this.abr) {
this.abr.setQualityAvailable(remoteQualityInfo.quality, remoteQualityInfo.available)
}
if (displayOptions.quality) {
participantView.updateQuality(track, localQuality.quality, localQuality.available);
}
} else {
track.quality.push(remoteQualityInfo);
if (this.abr) {
this.abr.addQuality(remoteQualityInfo.quality);
this.abr.setQualityAvailable(remoteQualityInfo.quality, remoteQualityInfo.available)
}
if (displayOptions.quality) {
const self = this;
participantView.addQuality(track, remoteQualityInfo.quality, remoteQualityInfo.available, async () => {
if (self.abr) {
self.abr.setManual();
self.abr.setQuality(remoteQualityInfo.quality);
}
return self.pickQuality(track, remoteQualityInfo.quality);
});
}
}
}
}
},
...
};
...
return instance;
}
9.7. Выбор качества для проигрывания¶
pickQuality()
code
const createOneToManyParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
...
const instance = {
...
pickQuality: async function (track, qualityName) {
if (this.remoteVideoTrack) {
return this.remoteVideoTrack.setPreferredQuality(qualityName).then(() => participantView.pickQuality(track, qualityName));
}
},
...
};
...
return instance;
}
9.8. Заглушить видео участника¶
muteVideo()
code
const createOneToManyParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
...
const instance = {
...
muteVideo: async function (track) {
if (this.remoteVideoTrack) {
return this.remoteVideoTrack.mute();
} else {
return new Promise((resolve, reject) => {
reject(new Error("Remote track not defined"));
});
}
},
...
};
...
return instance;
}
9.9. Возобновить видео участника¶
unmuteVideo()
code
const createOneToManyParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
...
const instance = {
...
unmuteVideo: async function (track) {
if (this.remoteVideoTrack) {
return this.remoteVideoTrack.unmute();
} else {
return new Promise((resolve, reject) => {
reject(new Error("Remote track not defined"));
});
}
}
};
...
return instance;
}
9.10. Завершение работы¶
dispose()
code
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
const instance = {
...
dispose: async function () {
this.disposed = true;
participantView.dispose();
this.remoteVideoTracks.forEach((track, id) => {
track.dispose();
})
this.remoteVideoTracks.clear();
this.remoteAudioTracks.forEach((track, id) => {
track.dispose();
})
this.remoteAudioTracks.clear();
this.abrManagers.forEach((abrManager, id) => {
abrManager.stop();
})
this.abrManagers.clear();
},
};
...
return instance;
}
10. Создание объекта отображения участника в комнате с двумя участникам¶
createOneToOneParticipantView()
code
const createOneToOneParticipantView = function () {
const participantDiv = createContainer(null);
const audioDisplay = createContainer(participantDiv);
const participantNicknameDisplay = createInfoDisplay(participantDiv, "Name: ")
const videoPlayers = new Map();
const audioElements = new Map();
return {
rootDiv: participantDiv,
dispose: function () {
...
},
addVideoTrack: function (track) {
...
},
removeVideoTrack: function (track) {
...
},
addVideoSource: function (remoteVideoTrack, track, onResize, muteHandler) {
...
},
removeVideoSource: function (track) {
...
},
showVideoTrack: function (track) {
...
},
addAudioTrack: function (track, audioTrack, show) {
...
},
removeAudioTrack: function (track) {
...
},
setNickname: function (userId, nickname) {
...
},
updateQuality: function (track, qualityName, available) {
...
},
addQuality: function (track, qualityName, available, onQualityPick) {
...
},
pickQuality: function (track, qualityName) {
...
}
}
}
10.1. Добавление видео дорожки¶
addVideoTrack()
code
const createOneToOneParticipantView = function () {
...
return {
...
addVideoTrack: function (track) {
const player = createVideoPlayer(participantDiv);
videoPlayers.set(track.mid, player);
},
...
}
}
10.2. Удаление видео дорожки¶
removeVideoTrack()
code
const createOneToOneParticipantView = function () {
...
return {
...
removeVideoTrack: function (track) {
const player = videoPlayers.get(track.mid);
if (player) {
player.dispose();
}
},
...
}
}
10.3. Добавление источника к видео элементу¶
addVideoSource()
code
const createOneToOneParticipantView = function () {
...
return {
...
addVideoSource: function (remoteVideoTrack, track, onResize, muteHandler) {
const player = videoPlayers.get(track.mid);
if (player) {
player.setVideoSource(remoteVideoTrack, onResize, muteHandler);
}
},
...
}
}
10.4. Удаление источника из видео элемента¶
removeVideoSource()
code
const createOneToOneParticipantView = function () {
...
return {
...
removeVideoSource: function (track) {
const player = videoPlayers.get(track.mid);
if (player) {
player.removeVideoSource();
}
},
...
}
}
10.5. Показать видео дорожку¶
showVideoTrack()
code
const createOneToOneParticipantView = function () {
...
return {
...
showVideoTrack: function (track) {
const player = videoPlayers.get(track.mid);
if (player) {
player.showVideoTrack(track);
}
},
...
}
}
10.6. Добавление аудио дорожки¶
addAudioTrack()
code
const createOneToOneParticipantView = function () {
...
return {
...
addAudioTrack: function (track, audioTrack, show) {
const stream = new MediaStream();
stream.addTrack(audioTrack);
const audioElement = document.createElement("audio");
if (!show) {
hideItem(audioElement);
}
audioElement.controls = "controls";
audioElement.muted = true;
audioElement.autoplay = true;
audioElement.onloadedmetadata = function (e) {
audioElement.play().then(function () {
if (Browser().isSafariWebRTC() && Browser().isiOS()) {
console.warn("Audio track should be manually unmuted in iOS Safari");
} else {
audioElement.muted = false;
}
});
};
audioElements.set(track.mid, audioElement);
audioDisplay.appendChild(audioElement);
audioElement.srcObject = stream;
},
...
}
}
10.7. Удаление аудио дорожки¶
removeAudioTrack()
code
const createOneToOneParticipantView = function () {
...
return {
...
removeAudioTrack: function (track) {
const audioElement = audioElements.get(track.mid);
if (audioElement) {
audioElement.remove();
audioElements.delete(track.mid);
}
},
...
}
}
10.8. Отображение имени участника¶
setNickname()
code
const createOneToOneParticipantView = function () {
...
return {
...
setNickname: function (userId, nickname) {
const additionalUserId = userId ? "#" + getShortUserId(userId) : "";
participantNicknameDisplay.innerText = "Name: " + nickname + additionalUserId;
},
...
}
}
10.9. Обновление информации о качестве¶
updateQuality()
code
const createOneToOneParticipantView = function () {
...
return {
...
updateQuality: function (track, qualityName, available) {
const player = videoPlayers.get(track.mid);
if (player) {
player.updateQuality(qualityName, available);
}
},
...
}
}
10.10. Добавление информации о качестве¶
addQuality()
code
const createOneToOneParticipantView = function () {
...
return {
...
addQuality: function (track, qualityName, available, onQualityPick) {
const player = videoPlayers.get(track.mid);
if (player) {
player.addQuality(qualityName, available, onQualityPick);
}
},
...
}
}
10.11. Выбор качества¶
pickQuality()
code
const createOneToOneParticipantView = function () {
...
return {
...
pickQuality: function (track, qualityName) {
const player = videoPlayers.get(track.mid);
if (player) {
player.pickQuality(qualityName);
}
}
...
}
}
10.12. Завершение отображения¶
dispose()
code
const createOneToOneParticipantView = function () {
...
return {
...
dispose: function () {
for (const player of videoPlayers.values()) {
player.dispose();
}
videoPlayers.clear();
for (const element of audioElements.values()) {
element.remove();
}
audioElements.clear();
},
...
}
}
11. Создание объекта отображения участника в комнате с несколькими участниками¶
createOneToManyParticipantView()
code
const createOneToManyParticipantView = function () {
const participantDiv = createContainer(null);
const audioDisplay = createContainer(participantDiv);
const participantNicknameDisplay = createInfoDisplay(participantDiv, "Name: ")
const audioElements = new Map();
const player = createVideoPlayer(participantDiv);
return {
rootDiv: participantDiv,
currentTrack: null,
dispose: function () {
...
},
addVideoTrack: function (track) {
...
},
removeVideoTrack: function (track) {
...
},
addVideoSource: function (remoteVideoTrack, track, onResize, muteHandler) {
...
},
removeVideoSource: function (track) {
...
},
showVideoTrack: function (track) {
...
},
addAudioTrack: function (track, audioTrack, show) {
...
},
removeAudioTrack: function (track) {
...
},
setNickname: function (userId, nickname) {
...
},
updateQuality: function (track, qualityName, available) {
...
},
addQuality: function (track, qualityName, available, onQualityPick) {
...
},
clearQualityState: function (track) {
...
},
pickQuality: function (track, qualityName) {
...
}
}
}
11.1. Добавление видео дорожки¶
addVideoTrack()
code
const createOneToManyParticipantView = function () {
...
return {
...
addVideoTrack: function (track, requestVideoTrack) {
player.addVideoTrack(track, async () => {
return requestVideoTrack();
});
},
...
}
}
11.2. Удаление видео дорожки¶
removeVideoTrack()
code
const createOneToManyParticipantView = function () {
...
return {
...
removeVideoTrack: function (track) {
player.removeVideoTrack(track);
},
...
}
}
11.3. Добавление источника к видео элементу¶
addVideoSource()
code
const createOneToManyParticipantView = function () {
...
return {
...
addVideoSource: function (remoteVideoTrack, track, onResize, muteHandler) {
this.currentTrack = track;
player.setVideoSource(remoteVideoTrack, onResize, muteHandler);
},
...
}
}
11.4. Удаление источника из видео элемента¶
removeVideoSource()
code
const createOneToManyParticipantView = function () {
...
return {
...
removeVideoSource: function (track) {
if (this.currentTrack && this.currentTrack.mid === track.mid) {
player.removeVideoSource();
}
},
...
}
}
11.5. Показать видео дорожку¶
showVideoTrack()
code
const createOneToManyParticipantView = function () {
...
return {
...
showVideoTrack: function (track) {
player.showVideoTrack(track);
},
...
}
}
11.6. Добавление аудио дорожки¶
addAudioTrack()
code
const createOneToManyParticipantView = function () {
...
return {
...
addAudioTrack: function (track, audioTrack, show) {
const stream = new MediaStream();
stream.addTrack(audioTrack);
const audioElement = document.createElement("audio");
if (!show) {
hideItem(audioElement);
}
audioElement.controls = "controls";
audioElement.muted = true;
audioElement.autoplay = true;
audioElement.onloadedmetadata = function (e) {
audioElement.play().then(function () {
if (Browser().isSafariWebRTC() && Browser().isiOS()) {
console.warn("Audio track should be manually unmuted in iOS Safari");
} else {
audioElement.muted = false;
}
});
};
audioElements.set(track.mid, audioElement);
audioDisplay.appendChild(audioElement);
audioElement.srcObject = stream;
},
...
}
}
11.7. Удаление аудио дорожки¶
removeAudioTrack()
code
const createOneToManyParticipantView = function () {
...
return {
...
removeAudioTrack: function (track) {
const audioElement = audioElements.get(track.mid);
if (audioElement) {
audioElement.remove();
audioElements.delete(track.mid);
}
},
...
}
}
11.8. Отображение имени участника¶
setNickname()
code
const createOneToManyParticipantView = function () {
...
return {
...
setNickname: function (userId, nickname) {
const additionalUserId = userId ? "#" + getShortUserId(userId) : "";
participantNicknameDisplay.innerText = "Name: " + nickname + additionalUserId;
},
...
}
}
11.9. Обновление информации о качестве¶
updateQuality()
code
const createOneToManyParticipantView = function () {
...
return {
...
updateQuality: function (track, qualityName, available) {
player.updateQuality(qualityName, available);
},
...
}
}
11.10. Добавление информации о качестве¶
addQuality()
code
const createOneToManyParticipantView = function () {
...
return {
...
addQuality: function (track, qualityName, available, onQualityPick) {
player.addQuality(qualityName, available, onQualityPick);
},
...
}
}
11.11. Выбор качества¶
pickQuality()
code
const createOneToManyParticipantView = function () {
...
return {
...
pickQuality: function (track, qualityName) {
player.pickQuality(qualityName);
}
...
}
}
11.12. Очистка отображаемого списка качеств¶
clearQualityState()
code
const createOneToManyParticipantView = function () {
...
return {
...
clearQualityState: function (track) {
player.clearQualityState();
},
...
}
}
11.13. Завершение отображения¶
dispose()
code
const createOneToManyParticipantView = function () {
...
return {
...
dispose: function () {
player.dispose();
for (const element of audioElements.values()) {
element.remove();
}
audioElements.clear();
},
...
}
}
12. Создание объекта видео плеера¶
createVideoPlayer()
code
const createVideoPlayer = function (participantDiv) {
const streamDisplay = createContainer(participantDiv);
const resolutionLabel = createInfoDisplay(streamDisplay, "0x0");
hideItem(resolutionLabel);
const trackNameDisplay = createInfoDisplay(streamDisplay, "track not set");
hideItem(trackNameDisplay);
const videoMuteDisplay = createContainer(streamDisplay);
const qualityDisplay = createContainer(streamDisplay);
const trackDisplay = createContainer(streamDisplay);
let videoElement;
const trackButtons = new Map();
const qualityButtons = new Map();
const lock = function () {
...
}
const unlock = function () {
...
}
const setWebkitEventHandlers = function (video) {
...
}
const setEventHandlers = function (video) {
...
}
const repickQuality = function (qualityName) {
...
}
return {
rootDiv: streamDisplay,
muteButton: null,
autoButton: null,
dispose: function () {
...
},
clearQualityState: function () {
...
},
addVideoTrack: function (track, asyncCallback) {
...
},
removeVideoTrack: function (track) {
...
},
setVideoSource: function (remoteVideoTrack, onResize, onMute) {
...
},
removeVideoSource: function () {
...
},
showVideoTrack: function (track) {
...
},
updateQuality: function (qualityName, available) {
...
},
addQuality: function (qualityName, available, onPickQuality) {
...
},
pickQuality: function (qualityName) {
...
}
}
}
12.1. Блокировка и разблокировка кнопок плеера для асинхронных операций¶
lock()
, unlock()
code
const createVideoPlayer = function (participantDiv) {
...
const lock = function () {
for (const btn of trackButtons.values()) {
btn.disabled = true;
}
for (const state of qualityButtons.values()) {
state.btn.disabled = true;
}
}
const unlock = function () {
for (const btn of trackButtons.values()) {
btn.disabled = false;
}
for (const state of qualityButtons.values()) {
state.btn.disabled = false;
}
}
...
return {
...
}
}
12.2. Настройка обработчиков событий плеера для Safari¶
setWebkitEventHandlers()
code
const createVideoPlayer = function (participantDiv) {
...
const setWebkitEventHandlers = function (video) {
let needRestart = false;
let isFullscreen = false;
// Use webkitbeginfullscreen event to detect full screen mode in iOS Safari
video.addEventListener("webkitbeginfullscreen", function () {
isFullscreen = true;
});
video.addEventListener("pause", function () {
if (needRestart) {
console.log("Media paused after fullscreen, continue...");
video.play();
needRestart = false;
} else {
console.log("Media paused by click, continue...");
video.play();
}
});
video.addEventListener("webkitendfullscreen", function () {
video.play();
needRestart = true;
isFullscreen = false;
});
}
...
return {
...
}
}
12.3. Настройка обработчиков событий плеера для других браузеров¶
setEventHandlers()
code
const createVideoPlayer = function (participantDiv) {
...
const setEventHandlers = function (video) {
// Ignore play/pause button
video.addEventListener("pause", function () {
console.log("Media paused by click, continue...");
video.play();
});
}
...
return {
...
}
}
12.4. Перерисовка кнопок переключения качества¶
repickQuality()
code
const createVideoPlayer = function (participantDiv) {
...
const repickQuality = function (qualityName) {
for (const [quality, state] of qualityButtons.entries()) {
if (quality === qualityName) {
state.btn.style.color = QUALITY_COLORS.SELECTED;
} else if (state.btn.style.color === QUALITY_COLORS.SELECTED) {
if (state.available) {
state.btn.style.color = QUALITY_COLORS.AVAILABLE;
} else {
state.btn.style.color = QUALITY_COLORS.UNAVAILABLE;
}
}
}
}
...
return {
...
}
}
12.5. Удаление кнопок переключения качества¶
clearQualityState()
code
const createVideoPlayer = function (participantDiv) {
...
return {
...
clearQualityState: function () {
qualityButtons.forEach((state, qName) => {
state.btn.remove();
});
qualityButtons.clear();
},
...
}
}
12.6. Добавление видео дорожки¶
addVideoTrack()
code
const createVideoPlayer = function (participantDiv) {
...
return {
...
addVideoTrack: function (track, asyncCallback) {
const trackButton = document.createElement("button");
trackButtons.set(track.mid, trackButton);
trackButton.innerText = "Track №" + track.mid + ": " + track.contentType;
trackButton.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
trackButton.style.color = QUALITY_COLORS.AVAILABLE;
const self = this;
trackButton.addEventListener('click', async function () {
console.log("Clicked on track button track.mid " + track.mid);
if (trackButton.style.color === QUALITY_COLORS.SELECTED) {
return
}
lock();
asyncCallback().then(() => {
self.showVideoTrack(track);
}).finally(() => {
unlock();
});
});
trackDisplay.appendChild(trackButton);
},
...
}
}
12.7. Удаление видео дорожки¶
removeVideoTrack()
code
const createVideoPlayer = function (participantDiv) {
...
return {
...
removeVideoTrack: function (track) {
const trackButton = trackButtons.get(track.mid);
if (trackButton) {
trackButton.remove();
trackButtons.delete(track.mid);
}
},
...
}
}
12.8. Добавление видео элемента и назначение видео дорожки как источника проигрывания¶
setVideoSource()
code
const createVideoPlayer = function (participantDiv) {
...
return {
...
setVideoSource: function (remoteVideoTrack, onResize, onMute) {
if (!this.muteButton) {
const newVideoMuteBtn = document.createElement("button");
this.muteButton = newVideoMuteBtn;
newVideoMuteBtn.innerText = "mute";
newVideoMuteBtn.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
newVideoMuteBtn.addEventListener('click', async function () {
newVideoMuteBtn.disabled = true;
try {
if (newVideoMuteBtn.innerText === "mute") {
await onMute(true);
newVideoMuteBtn.innerText = "unmute";
} else if (newVideoMuteBtn.innerText === "unmute") {
await onMute(false);
newVideoMuteBtn.innerText = "mute";
}
} finally {
newVideoMuteBtn.disabled = false;
}
});
videoMuteDisplay.appendChild(newVideoMuteBtn);
}
if (videoElement) {
videoElement.remove();
videoElement = null;
}
if (!remoteVideoTrack) {
return;
}
videoElement = document.createElement("video");
hideItem(videoElement);
videoElement.setAttribute("style", "display:none; border: solid; border-width: 1px");
const stream = new MediaStream();
streamDisplay.appendChild(videoElement);
videoElement.srcObject = stream;
videoElement.onloadedmetadata = function (e) {
videoElement.play();
};
videoElement.addEventListener("resize", function (event) {
showItem(resolutionLabel);
if (videoElement) {
resolutionLabel.innerText = videoElement.videoWidth + "x" + videoElement.videoHeight;
resizeVideo(event.target);
onResize();
}
});
stream.addTrack(remoteVideoTrack);
if (Browser().isSafariWebRTC()) {
videoElement.setAttribute("playsinline", "");
videoElement.setAttribute("webkit-playsinline", "");
setWebkitEventHandlers(videoElement);
} else {
setEventHandlers(videoElement);
}
},
...
}
}
12.9. Удаление видео элемента¶
removeVideoSource()
code
const createVideoPlayer = function (participantDiv) {
...
return {
...
removeVideoSource: function () {
if (videoElement) {
videoElement.remove();
videoElement = null;
}
if (this.muteButton) {
this.muteButton.remove();
this.muteButton = null;
}
hideItem(resolutionLabel);
trackNameDisplay.innerText = "track not set";
},
...
}
}
12.10. Отображение видео элемента и информации о видео дорожке¶
showVideoTrack()
code
const createVideoPlayer = function (participantDiv) {
...
return {
...
showVideoTrack: function (track) {
if (videoElement) {
showItem(videoElement);
}
for (const [mid, btn] of trackButtons.entries()) {
if (mid === track.mid) {
btn.style.color = QUALITY_COLORS.SELECTED;
} else if (btn.style.color === QUALITY_COLORS.SELECTED) {
btn.style.color = QUALITY_COLORS.AVAILABLE;
}
}
trackNameDisplay.innerText = "Current video track: " + track.mid;
showItem(trackNameDisplay);
},
...
}
}
12.11. Обновление информации о качестве¶
updateQuality()
code
const createVideoPlayer = function (participantDiv) {
...
return {
...
updateQuality: function (qualityName, available) {
const value = qualityButtons.get(qualityName);
if (value) {
const qualityButton = value.btn;
value.available = available;
if (qualityButton.style.color === QUALITY_COLORS.SELECTED) {
return;
}
if (available) {
qualityButton.style.color = QUALITY_COLORS.AVAILABLE;
} else {
qualityButton.style.color = QUALITY_COLORS.UNAVAILABLE;
}
}
},
...
}
}
12.12. Добавление кнопки выбора качества¶
addQuality()
code
const createVideoPlayer = function (participantDiv) {
...
return {
...
addQuality: function (qualityName, available, onPickQuality) {
const qualityButton = document.createElement("button");
qualityButtons.set(qualityName, {btn: qualityButton, available: available});
qualityButton.innerText = qualityName;
qualityButton.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
if (available) {
qualityButton.style.color = QUALITY_COLORS.AVAILABLE;
} else {
qualityButton.style.color = QUALITY_COLORS.UNAVAILABLE;
}
qualityDisplay.appendChild(qualityButton);
qualityButton.addEventListener('click', async function () {
console.log("Clicked on quality button " + qualityName);
if (qualityButton.style.color === QUALITY_COLORS.SELECTED || qualityButton.style.color === QUALITY_COLORS.UNAVAILABLE || !videoElement) {
return;
}
lock();
onPickQuality().finally(() => unlock());
});
},
...
}
}