...
Для анализа исходного кода возьмем версию модуля display.js, которая находится здесь
Захват и отображение локального видео
1. Инициализация
initLocalDisplay() code
Функция initLocalDisplay() возвращает объект для работы с HTML5 элементами захвата и отображения локального видео
Code Block |
---|
|
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.1. Добавления аудио дорожки к HTML5 элементу
add() code
Здесь:
- добавляется аудио дорожка к video элементу
- создается обработчик события
onended
для видео дорожки - добавляется обработчик нажатия кнопки включения/отключения аудио
Code Block |
---|
|
if (stream.getAudioTracks().length > 0) {
let videoElement = getAudioContainer();
if (videoElement) {
let track = stream.getAudioTracks()[0];
videoElement.video.srcObject.addTrack(track);
videoElement.audioStateDisplay.innerHTML = 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
Здесь:
- создается контейнер для элементов отображения локального видео
- создается элемент для отображения информации о публикуемом видео
...
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
Функция initRemoteDisplay() возвращает объект для работы с HTML5 элементами отображения видео и аудио потоков, опубликованных в комнате
Code Block |
---|
|
const initRemoteDisplay = function(options) {
/*
display options:
autoAbr - choose abr by default
quality const- constantsshow = SFU.constants;
const remoteParticipants = {};quality buttons
showAudio - show audio elements
*/
const initRemoteDisplay = function (room, div, displayOptions, abrOptions, meetingController, meetingModel, meetingView, participantFactory) {
// Validate options first
if (!options.div) {
throw new Error("Main div to place all the media tag is not defined");
}
if (!options.room) {
throw new Error("Room is not defined");
}
if (!options.peerConnection) {
throw new Error("PeerConnection is not defined");
}
let mainDiv = options.div;
let room = options.roomconst dOptions = displayOptions || {quality: true, type: true, showAudio: false};
let abrFactory;
if (abrOptions) {
abrFactory = abrManagerFactory(room, abrOptions);
}
participantFactory.abrFactory = abrFactory;
let peerConnectionparticipantFactory.displayOptions = options.peerConnectiondOptions;
let displayOptions = options.displayOptions || {publisher: true, quality: true, type: true};
...return meetingController(room, meetingModel(meetingView(div), participantFactory));
} |
2. Создание фабрики объектов для управления проигрыванием WebRTC ABR потока
abrManagerFactory() code
Code Block |
---|
|
const abrManagerFactory = function (room, abrOptions) {
return {
const createRemoteDisplay = function(id, name, mainDiv, displayOptions createAbrManager: function () {
...
}
const stop = function() {return abr;
...}
}
peerConnection.ontrack = ({transceiver}) => {
...
}
return {
stop: stop
}
} |
2. Обработка событий комнаты
2.1. ADD_TRACKS
initRemoteDisplay() code
Здесь:
- новый участник добавляется в список
- добавляется информация о качестве потоков
- создается элемент для отображения видео и аудио потоков участника
2.1. Инициализация объекта управления ABR
createAbrManager() code
Code Block |
---|
|
const abrManagerFactory = function room.on(constants.SFU_ROOM_EVENT.ADD_TRACKS, function(e(room, abrOptions) {
return {
console.log("Received ADD_TRACKS");
createAbrManager: function () {
let participant = remoteParticipants[e.info.nickName];
let if (!participant)abr = {
participant = {};
track: null,
participant.nickName = e.info.nickName;
interval: abrOptions.interval,
participant.tracks = [];
thresholds: abrOptions.thresholds,
participant.displays = [];
qualities: remoteParticipants[participant.nickName],
= participant;
}
participant.tracks.push.apply(participant.tracks, e.info.info);currentQualityName: null,
for (const pTrack of e.info.info) {
statTimer: null,
let createDisplay = true;
paused: false,
for (let i = 0; i < participant.displays.length; i++) {
manual: false,
let display =keepGoodTimeout: participant.displays[i];abrOptions.abrKeepOnGoodQuality,
if (pTrack.type === "VIDEO") {keepGoodTimer: null,
if (display.hasVideo()) {
tryUpperTimeout: abrOptions.abrTryForUpperQuality,
tryUpperTimer: null,
continue;
...
}
return abr;
display.videoMid = pTrack.mid;}
}
} |
2.2. Запуск автоматического выбора качества ABR
abr.start() code
Code Block |
---|
|
const abrManagerFactory = function (room, abrOptions) {
return {
display.setTrackInfo(pTrack);
createAbrManager: function () {
let createDisplayabr = false;{
break;...
} else if (pTrack.type === "AUDIO"start: function () {
if (display.hasAudiothis.stop()) {;
console.log("Start abr interval")
continue;
if (abr.interval) }{
display.audioMid = pTrack.mid;
const thresholds = Thresholds();
display.setTrackInfo(pTrack);
for (const threshold of abr.thresholds) {
createDisplay = false;
break;
thresholds.add(threshold.parameter, threshold.maxLeap);
}
}
ifabr.statsTimer = setInterval(!createDisplay() => {
continue;
if (abr.track) }{
let display = createRemoteDisplay(participant.nickName, participant.nickName, mainDiv, displayOptions);
participant.displays.push(display);room.getStats(abr.track.track, constants.SFU_RTC_STATS_TYPE.INBOUND, (stats) => {
if (pTrack.type === "VIDEO") {
display.videoMid =if pTrack.mid;(thresholds.isReached(stats)) {
display.setTrackInfo(pTrack);
} else if (pTrack.type === "AUDIO") {
abr.shiftDown();
display.audioMid = pTrack.mid;
display.setTrackInfo(pTrack);
} else {
}
}
...
}); |
2.2. REMOVE_TRACKS
initRemoteDisplay() code
Здесь:
- удаляются элементы, в которых проигрывались потоки
- информация о потоках удаляется из списка
Code Block |
---|
|
room.on(constants.SFU_ROOM_EVENT.ADD_TRACKS, function(e) {
...
})abr.on(constants.SFU_ROOM_EVENT.REMOVE_TRACKS, function(e) {
useGoodQuality();
console.log("Received REMOVE_TRACKS");
const participant = remoteParticipants[e.info.nickName];
if (!participant) {
return; }
});
}
}, abr.interval);
}
},
...
}
return abr;
}
}
} |
2.3. Остановка автоматического выбора качества ABR
abr.stop() code
Code Block |
---|
|
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
Code Block |
---|
|
const abrManagerFactory = function (room, abrOptions) {
return {
createAbrManager: function () {
let abr = {
...
setTrack: function (track) {
abr.track = track;
},
...
}
return abr;
}
}
} |
2.5. Добавление описания ABR качества в список для выбора
abr.addQuality() code
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
const createDefaultMeetingController = function (room, meetingModel) {
...
return {
stop: stop
}
} |
3.1. Обработка события PARTICIPANT_LIST
createDefaultMeetingController() code
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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.4. Обработка события REMOVE_TRACKS
createDefaultMeetingController() code
Code Block |
---|
|
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.4. Обработка события TRACK_QUALITY_STATE
createDefaultMeetingController() code
Code Block |
---|
|
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.5. Обработка события ENDED
createDefaultMeetingController() code
Code Block |
---|
|
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.6. Остановка комнаты
createDefaultMeetingController() code
Code Block |
---|
|
const createDefaultMeetingController = function (room, meetingModel) {
...
const stop = function () {
meetingModel.end();
};
return {
stop: stop
}
} |
4. Создание модели комнаты
createDefaultMeetingModel() code
Code Block |
---|
|
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
...
} |
4.1. Добавление участника
addParticipant() code
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
return {
...
setMeetingName: function (id) {
this.meetingName = id;
meetingView.setMeetingName(id);
}
}
} |
5. Создание объекта для отображения потоков в комнате
createDefaultMeetingView() code
Code Block |
---|
|
const createDefaultMeetingView = function (entryPoint) {
...
} |
5.1. Инициализация HTML5 элементов
createDefaultMeetingView() code
Code Block |
---|
|
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
Code Block |
---|
|
const createDefaultMeetingView = function (entryPoint) {
...
return {
participantViews: new Map(),
...
}
} |
5.3. Отображение имени комнаты
setMeetingName() code
Code Block |
---|
|
const createDefaultMeetingView = function (entryPoint) {
...
return {
...
setMeetingName: function (id) {
title.innerText = "Meeting: " + id;
},
...
}
} |
5.4. Добавление элементов для отображения участника
addParticipant() code
Code Block |
---|
|
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
Code Block |
---|
|
const createDefaultMeetingView = function (entryPoint) {
...
return {
...
removeParticipant: function (userId) {
const cell = this.participantViews.get(userId);
if (cell) {
this.participantViews.delete(userId);
cell.remove();
}
},
...
}
} |
5.5. Удаление корневого элемента отображения
end() code
Code Block |
---|
|
const createDefaultMeetingView = function (entryPoint) {
...
return {
...
end: function () {
rootDiv.remove();
}
}
} |
6. Создание фабрики объектов участников
createParticipantFactory() code
Здесь содаются объекты модели, управления и отображения участника комнаты
Code Block |
---|
|
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
Объект вызывает соответствующие методы модели участника
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
const createOneToOneParticipantView = function () {
...
return {
...
addVideoTrack: function (track) {
const player = createVideoPlayer(participantDiv);
videoPlayers.set(track.mid, player);
},
...
}
} |
10.2. Удаление видео дорожки
removeVideoTrack() code
Code Block |
---|
|
const createOneToOneParticipantView = function () {
...
return {
...
removeVideoTrack: function (track) {
const player = videoPlayers.get(track.mid);
if (player) {
player.dispose();
}
},
...
}
} |
10.3. Добавление источника к видео элементу
addVideoSource() code
Code Block |
---|
|
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
Code Block |
---|
|
const createOneToOneParticipantView = function () {
...
return {
...
removeVideoSource: function (track) {
const player = videoPlayers.get(track.mid);
if (player) {
player.removeVideoSource();
}
},
...
}
} |
10.5. Показать видео дорожку
showVideoTrack() code
Code Block |
---|
|
const createOneToOneParticipantView = function () {
...
return {
...
showVideoTrack: function (track) {
const player = videoPlayers.get(track.mid);
if (player) {
player.showVideoTrack(track);
}
},
...
}
} |
10.6. Добавление аудио дорожки
addAudioTrack() code
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
const createOneToOneParticipantView = function () {
...
return {
...
setNickname: function (userId, nickname) {
const additionalUserId = userId ? "#" + getShortUserId(userId) : "";
participantNicknameDisplay.innerText = "Name: " + nickname + additionalUserId;
},
...
}
} |
10.9. Обновление информации о качестве
updateQuality() code
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
const createOneToOneParticipantView = function () {
...
return {
...
pickQuality: function (track, qualityName) {
const player = videoPlayers.get(track.mid);
if (player) {
player.pickQuality(qualityName);
}
}
...
}
} |
10.12. Завершение отображения
dispose() code
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
const createOneToManyParticipantView = function () {
...
return {
...
addVideoTrack: function (track, requestVideoTrack) {
player.addVideoTrack(track, async () => {
return requestVideoTrack();
});
},
...
}
} |
11.2. Удаление видео дорожки
removeVideoTrack() code
Code Block |
---|
|
const createOneToManyParticipantView = function () {
...
return {
...
removeVideoTrack: function (track) {
player.removeVideoTrack(track);
},
...
}
} |
11.3. Добавление источника к видео элементу
addVideoSource() code
Code Block |
---|
|
const createOneToManyParticipantView = function () {
...
return {
...
addVideoSource: function (remoteVideoTrack, track, onResize, muteHandler) {
this.currentTrack = track;
player.setVideoSource(remoteVideoTrack, onResize, muteHandler);
},
...
}
} |
11.4. Удаление источника из видео элемента
removeVideoSource() code
Code Block |
---|
|
const createOneToManyParticipantView = function () {
...
return {
...
removeVideoSource: function (track) {
if (this.currentTrack && this.currentTrack.mid === track.mid) {
player.removeVideoSource();
}
},
...
}
} |
11.5. Показать видео дорожку
showVideoTrack() code
Code Block |
---|
|
const createOneToManyParticipantView = function () {
...
return {
...
showVideoTrack: function (track) {
player.showVideoTrack(track);
},
...
}
} |
11.6. Добавление аудио дорожки
addAudioTrack() code
Code Block |
---|
|
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
Code Block |
---|
|
const createOneToManyParticipantView = function () {
...
return {
}...
forremoveAudioTrack: function (const rTrack of e.info.infotrack) {
for (let iconst audioElement = 0; i < participant.tracks.length; i++) {
audioElements.get(track.mid);
if (rTrack.mid === participant.tracks[i].midaudioElement) {
participant.tracks.splice(i, 1audioElement.remove();
breakaudioElements.delete(track.mid);
}
},
...
}
} |
11.8. Отображение имени участника
setNickname() code
Code Block |
---|
|
const createOneToManyParticipantView = forfunction (let) i{
= 0; i < participant..displays.length; i++).
return {
...
setNickname: letfunction found = false;(userId, nickname) {
const displayadditionalUserId = participant.displays[i];
userId ? "#" + getShortUserId(userId) : "";
if (displayparticipantNicknameDisplay.audioMidinnerText === rTrack.mid) {
"Name: " + nickname + additionalUserId;
},
...
display.setAudio(null);
}
} |
11.9. Обновление информации о качестве
updateQuality() code
Code Block |
---|
|
const createOneToManyParticipantView = function () {
...
return {
found = true;...
updateQuality: function (track, qualityName, available) {
} else if (display.videoMid === rTrack.mid) {
player.updateQuality(qualityName, available);
},
...
display.setVideo(null);
}
} |
11.10. Добавление информации о качестве
addQuality() code
Code Block |
---|
|
const createOneToManyParticipantView = function () {
...
return {
found = true;...
addQuality: function (track, qualityName, available, onQualityPick) }{
player.addQuality(qualityName, if (found) {available, onQualityPick);
},
...
if (!display.hasAudio() && !display.hasVideo()}
} |
11.11. Выбор качества
pickQuality() code
Code Block |
---|
|
const createOneToManyParticipantView = function () {
...
return {
display.dispose();...
pickQuality: function (track, qualityName) {
participant.displays.splice(i, 1player.pickQuality(qualityName);
}
...
}
}
} |
11.12. Очистка отображаемого списка качеств
clearQualityState() code
Code Block |
---|
|
const createOneToManyParticipantView = function () {
...
return {
break;
...
clearQualityState: function }(track) {
}player.clearQualityState();
},
...
}
}); |
...
11.
...
13.
...
Завершение отображения
initRemoteDisplaydispose() code
Здесь:
- участник удаляется из списка
- удаляются элементы, в которых проигрывались потоки
Code Block |
---|
| room.on(constants.SFU_ROOM_EVENT.ADD_TRACKS, function(e) {
...
}).on(constants.SFU_ROOM_EVENT.LEFT, function(e |
const createOneToManyParticipantView = function () {
...
console.log("Received LEFT");return {
let participant = remoteParticipants[e.name];
...
dispose: iffunction (!participant) {
returnplayer.dispose();
}
for (const element of participantaudioElements.displays.forEach(function(display){
values()) {
displayelement.disposeremove();
})
delete remoteParticipants[e.name]audioElements.clear();
},
...
}
}); |
2.4. TRACK_QUALITY_STATE
...
12. Создание объекта видео плеера
createVideoPlayer() code
Здесь:
- обновляется информация о качествах потоков
Code Block |
---|
|
const createVideoPlayer = function room.on(constants.SFU_ROOM_EVENT.ADD_TRACKS, function(e) {(participantDiv) {
const streamDisplay = createContainer(participantDiv);
const resolutionLabel = ... createInfoDisplay(streamDisplay, "0x0");
}).on(constants.SFU_ROOM_EVENT.TRACK_QUALITY_STATE, function(e){hideItem(resolutionLabel);
const trackNameDisplay = console.log("Received createInfoDisplay(streamDisplay, "track qualitynot stateset");
hideItem(trackNameDisplay);
const participantvideoMuteDisplay = remoteParticipants[e.info.nickName]createContainer(streamDisplay);
const qualityDisplay = if (!participant) {createContainer(streamDisplay);
const trackDisplay return;= createContainer(streamDisplay);
}let videoElement;
const trackButtons = new for (const rTrack of e.info.tracks) {Map();
const qualityButtons = new Map();
const midlock = rTrack.mid; function () {
...
for}
(let i = 0;const iunlock < participant.displays.length; i++= function () {
...
}
const displaysetWebkitEventHandlers = participant.displays[i]; function (video) {
...
}
if (display.videoMidconst setEventHandlers === mid function (video) {
...
}
const repickQuality = function display.updateQualityInfo(rTrack.quality);(qualityName) {
...
}
return {
break;
rootDiv: streamDisplay,
muteButton: }null,
}autoButton: null,
}
dispose: }); |
3. Создание элементов для отображения потоков участников
3.1. Создание контейнера
createRemoteDisplay() code
Здесь:
- настраиваются параметры отображения
- создается контейнер для потоков участника
- создается контейнер для конкретного потока
- создается контейнер для кнопок переключения качества
Code Block |
---|
|
function () {
const cell = document.createElement("div");...
cell.setAttribute("class", "text-center");
},
clearQualityState: cell.id = id;
function () {
mainDiv.appendChild(cell); ...
let publisherNameDisplay;
},
let currentQualityDisplay;addVideoTrack: function (track, asyncCallback) {
let videoTypeDisplay;
...
let abrQualityCheckPeriod = ABR_QUALITY_CHECK_PERIOD; },
letremoveVideoTrack: abrKeepOnGoodQualityfunction = ABR_KEEP_ON_QUALITY;
(track) {
...
let abrTryForUpperQuality = ABR_TRY_UPPER_QUALITY; },
setVideoSource: iffunction (displayOptions.abrQualityCheckPeriod !== undefinedremoteVideoTrack, onResize, onMute) {
abrQualityCheckPeriod = displayOptions.abrQualityCheckPeriod;...
},
if (displayOptions.abrKeepOnGoodQuality !== undefinedremoveVideoSource: function () {
abrKeepOnGoodQuality = displayOptions.abrKeepOnGoodQuality;...
},
ifshowVideoTrack: function (displayOptions.abrTryForUpperQuality !== undefinedtrack) {
abrTryForUpperQuality = displayOptions.abrTryForUpperQuality;...
},
ifupdateQuality: function (!displayOptions.abrqualityName, available) {
abrQualityCheckPeriod = 0;
...
},
abrKeepOnGoodQuality = 0;
addQuality: function (qualityName, available, onPickQuality) {
abrTryForUpperQuality = 0;...
},
pickQuality: iffunction (displayOptions.publisherqualityName) {
publisherNameDisplay = createInfoDisplay(cell, "Published by: " + name);...
}
}
} |
12.1. Блокировка и разблокировка кнопок плеера для асинхронных операций
lock(), unlock() code
Code Block |
---|
|
const createVideoPlayer = iffunction (displayOptions.quality) {
participantDiv) {
...
const currentQualityDisplaylock = function createInfoDisplay(cell, "");) {
}
for (const btn if (displayOptions.typeof trackButtons.values()) {
videoTypeDisplaybtn.disabled = createInfoDisplay(cell, "")true;
}
for (const qualitySwitchDisplaystate =of createInfoDisplay(cell, "");
qualityButtons.values()) {
let qualityDivsstate.btn.disabled = []true;
let}
contentType = "";}
const rootDisplayunlock = function createContainer(cell); {
for (const streamDisplaybtn =of createContainertrackButtons.values(rootDisplay)); {
const audioDisplay = createContainer(rootDisplay);
const audioTypeDisplay btn.disabled = createInfoDisplay(audioDisplay)false;
const audioTrackDisplay}
= createContainer(audioDisplay);
for (const audioStateButtonstate =of AudioStateButtonqualityButtons.values();) {
hideItem(streamDisplay);
state.btn.disabled hideItem(audioDisplay)= false;
hideItem(publisherNameDisplay);}
}
hideItem(currentQualityDisplay);...
return {
hideItem(videoTypeDisplay);
...
hideItem(qualitySwitchDisplay); |
...
12.2.
...
Настройка обработчиков событий плеера для Safari
setWebkitEventHandlers() code
Здесь задаются параметры качества получения потока от сервера, по которым отображение будет переключаться на более высокое или более низкое качество
Code Block |
---|
|
const createVideoPlayer = function (participantDiv) {
...
const abrsetWebkitEventHandlers = ABR(abrQualityCheckPeriod, [
function (video) {
let needRestart = false;
let isFullscreen = false;
// Use webkitbeginfullscreen event to detect full screen mode in iOS Safari
{parameter: "nackCount video.addEventListener("webkitbeginfullscreen", maxLeap: 10},function () {
{parameter: "freezeCount", maxLeap: 10},
isFullscreen = true;
{parameter: "packetsLost", maxLeap: 10});
]video.addEventListener("pause", abrKeepOnGoodQuality, abrTryForUpperQuality); |
3.3. Добавление видео элемента
setVideo() code
Code Block |
---|
|
function () {
setVideo:if function(streamneedRestart) {
if (video) {
console.log("Media paused after fullscreen, continue...");
video.removeplay();
needRestart = false;
} else {
if (stream == null) {
console.log("Media paused by click, continue...");
video = null.play();
}
this.videoMid = undefined;
});
video.addEventListener("webkitendfullscreen", function () {
qualityDivsvideo.forEach(functionplay(div) {;
needRestart = true;
div.remove();
isFullscreen = false;
});
}
});...
return {
...
}
} |
12.3. Настройка обработчиков событий плеера для других браузеров
setEventHandlers() code
Code Block |
---|
|
const createVideoPlayer = qualityDivsfunction = [];(participantDiv) {
...
const setEventHandlers = function (video) {
return;
// Ignore play/pause button
video.addEventListener("pause", function () }{
console.log("Media paused by showItem(streamDisplayclick, continue...");
video = document.createElementplay("video");
});
}
video.controls = "controls";...
return {
...
video.muted = true;
}
} |
12.4. Перерисовка кнопок переключения качества
repickQuality() code
Code Block |
---|
|
const createVideoPlayer = function (participantDiv) {
...
const repickQuality = video.autoplay = true;function (qualityName) {
for (const [quality, state] if (Browser().isSafariWebRTCof qualityButtons.entries()) {
if (quality === video.setAttribute("playsinline", "");qualityName) {
state.btn.style.color video.setAttribute("webkit-playsinline", "")= QUALITY_COLORS.SELECTED;
} else if (state.btn.style.color this.setWebkitEventHandlers(video);=== QUALITY_COLORS.SELECTED) {
} elseif (state.available) {
state.btn.style.color = this.setEventHandlers(video)QUALITY_COLORS.AVAILABLE;
} else {
streamDisplay.appendChild(video) state.btn.style.color = QUALITY_COLORS.UNAVAILABLE;
video.srcObject}
= stream;
}
this.setResizeHandler(video);
}
}
...
abr.start();return {
...
}
}, |
...
12.
...
5. Удаление кнопок переключения качества
clearQualityState() code
Code Block |
---|
|
const setAudio: function(stream) {
if (audiocreateVideoPlayer = function (participantDiv) {
audio.remove();...
return {
}...
clearQualityState: if (!streamfunction () {
qualityButtons.forEach((state, qName) => {
audio = null;
state.btn.remove();
this.audioMid = undefined });
qualityButtons.clear();
return;},
...
}
} |
12.6. Добавление видео дорожки
addVideoTrack() code
Code Block |
---|
|
const createVideoPlayer = function (participantDiv) }{
...
showItem(audioDisplay);return {
...
audioaddVideoTrack: =function document.createElement("audio");(track, asyncCallback) {
audio.controlsconst trackButton = document.createElement("controlsbutton");
trackButtons.set(track.mid, trackButton);
audio.muted = true;
trackButton.innerText = "Track №" + track.mid + ": " + audio.autoplay = truetrack.contentType;
trackButton.setAttribute("style", "display:inline-block; border: if (Browser().isSafariWebRTC()) {solid; border-width: 1px");
trackButton.style.color = QUALITY_COLORS.AVAILABLE;
audio.setAttribute("playsinline", "");
const self = this;
audiotrackButton.setAttribute("webkit-playsinline", "");
addEventListener('click', async function () {
console.log("Clicked on track button track.mid " + thistrack.setWebkitEventHandlers(audiomid);
if } else(trackButton.style.color === QUALITY_COLORS.SELECTED) {
this.setEventHandlers(audio);return
}
audioTrackDisplay.appendChildlock(audio);
audioStateButton.makeButton(audioTypeDisplay, audio);asyncCallback().then(() => {
audio.srcObject = stream self.showVideoTrack(track);
audio.onloadedmetadata}).finally(() => function (e) {
audio.playunlock().then(function() {
;
});
if (Browser().isSafariWebRTC() && Browser().isiOS()) {});
trackDisplay.appendChild(trackButton);
},
console.warn("Audio track should be manually unmuted in iOS Safari");
...
}
} |
12.7. Удаление видео дорожки
removeVideoTrack() code
Code Block |
---|
|
const createVideoPlayer = function (participantDiv) {
...
return {
} else {
...
removeVideoTrack: function (track) {
const audio.mutedtrackButton = falsetrackButtons.get(track.mid);
if (trackButton) {
audioStateButtontrackButton.setButtonStateremove();
}trackButtons.delete(track.mid);
}
});,
...
};
}, |
3.5. Настройка обработчиков событий для аудио и видео элементов
...
12.8. Добавление видео элемента и назначение видео дорожки как источника проигрывания
setVideoSource() code
Code Block |
---|
|
const createVideoPlayer = function (participantDiv) {
...
setEventHandlers: function(video) return {
...
//setVideoSource: Ignore play/pause button
function (remoteVideoTrack, onResize, onMute) {
video.addEventListener("pause", functionif (!this.muteButton) {
const newVideoMuteBtn = consoledocument.logcreateElement("Media paused by click, continue...button");
this.muteButton video.play()= newVideoMuteBtn;
})newVideoMuteBtn.innerText = "mute";
},
newVideoMuteBtn.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
setWebkitEventHandlers: function(video) {
newVideoMuteBtn.addEventListener('click', async function () {
let needRestart = false;
let isFullscreennewVideoMuteBtn.disabled = falsetrue;
// Use webkitbeginfullscreen event to detect full screen mode in iOS Safari
try {
if video.addEventListener("webkitbeginfullscreen", function () {
(newVideoMuteBtn.innerText === "mute") {
isFullscreen =await onMute(true);
}); newVideoMuteBtn.innerText = "unmute";
} else if video.addEventListener("pause", function ((newVideoMuteBtn.innerText === "unmute") {
if await onMute(needRestartfalse) {;
console.log("Media paused after fullscreen, continue...")newVideoMuteBtn.innerText = "mute";
video.play();}
} finally {
needRestart = false;
}newVideoMuteBtn.disabled else= {false;
}
console.log("Media paused by click, continue...");
});
videovideoMuteDisplay.playappendChild(newVideoMuteBtn);
}
if });(videoElement) {
videovideoElement.addEventListener("webkitendfullscreen", function () {
remove();
videoElement = null;
video.play();
}
if (!remoteVideoTrack) {
needRestart = true;
return;
isFullscreen = false;
}
videoElement }= document.createElement("video");
}, |
3.6. Добавление информации о дорожке в ABR
setVideoABRTrack() code
Code Block |
---|
|
hideItem(videoElement);
setVideoABRTrack: function(track) {videoElement.setAttribute("style", "display:none; border: solid; border-width: 1px");
const stream = new abr.setTrackMediaStream(track);
}, |
3.7. Настройка переключения качества
setTrackInfo() code
Здесь:
- настраиваются кнопки переключения качества
- информация о заявленных качествах публикации добавляется в ABR
- скрываются или отображаются элементы для показа текущего качества и источника видео/аудио
Code Block |
---|
|
streamDisplay.appendChild(videoElement);
videoElement.srcObject = stream;
videoElement.onloadedmetadata setTrackInfo:= function (trackInfoe) {
if videoElement.play(trackInfo);
{
};
ifvideoElement.addEventListener("resize", function (trackInfo.qualityevent) {
showItem(resolutionLabel);
showItem(qualitySwitchDisplay);
if (videoElement) {
if (abr.isEnabled()) {
resolutionLabel.innerText = videoElement.videoWidth + "x" + videoElement.videoHeight;
const autoDiv = createQualityButton("Auto", qualityDivs, qualitySwitchDisplayresizeVideo(event.target);
onResize();
autoDiv.style.color = QUALITY_COLORS.SELECTED;
}
});
autoDivstream.addEventListener('click', function() {addTrack(remoteVideoTrack);
if (Browser().isSafariWebRTC()) {
setQualityButtonsColor(qualityDivsvideoElement.setAttribute("playsinline", "");
videoElement.setAttribute("webkit-playsinline", "");
autoDiv.style.color = QUALITY_COLORS.SELECTED setWebkitEventHandlers(videoElement);
} else {
abr.setAutosetEventHandlers(videoElement);
}
},
...
}
});
|
12.9. Удаление видео элемента
removeVideoSource() code
Code Block |
---|
|
const createVideoPlayer = function (participantDiv) {
...
return {
}...
removeVideoSource: function () {
if for (letvideoElement) i{
= 0; i < trackInfo.quality.length; i++) {
videoElement.remove();
videoElement abr.addQuality(trackInfo.quality[i]);
= null;
}
if const qualityDiv = createQualityButton(trackInfo.quality[i], qualityDivs, qualitySwitchDisplay);
(this.muteButton) {
this.muteButton.remove();
qualityDiv.addEventListener('click', function() {this.muteButton = null;
}
hideItem(resolutionLabel);
console.log("Clicked on quality " + trackInfo.quality[i] +trackNameDisplay.innerText = "track trackIdnot set" + trackInfo.id);
},
...
}
} |
12.10. Отображение видео элемента и информации о видео дорожке
showVideoTrack() code
Code Block |
---|
|
const createVideoPlayer = function (participantDiv) {
if (qualityDiv.style.color === QUALITY_COLORS.UNAVAILABLE) {
return {
...
showVideoTrack: function (track) {
return;
if (videoElement) {
showItem(videoElement);
}
for (const [mid, btn] of trackButtons.entries()) {
setQualityButtonsColor(qualityDivs);
if (mid === track.mid) {
qualityDivbtn.style.color = QUALITY_COLORS.SELECTED;
} else if (btn.style.color === QUALITY_COLORS.SELECTED) {
abr.setManual();
btn.style.color = QUALITY_COLORS.AVAILABLE;
}
abr.setQuality(trackInfo.quality[i]);
}
trackNameDisplay.innerText = "Current video track: " })+ track.mid;
showItem(trackNameDisplay);
},
} ...
}
} |
12.11. Обновление информации о качестве
updateQuality() code
Code Block |
---|
|
const createVideoPlayer = function (participantDiv) {
...
}return else {
...
updateQuality: function (qualityName, hideItem(qualitySwitchDisplay);available) {
const value }= qualityButtons.get(qualityName);
if (trackInfo.typevalue) {
const qualityButton = value.btn;
contentType = trackInfo.contentType || "";
value.available = available;
if (trackInfoqualityButton.style.typecolor == "VIDEO" && displayOptions.type && contentType !== ""= QUALITY_COLORS.SELECTED) {
showItem(videoTypeDisplay)return;
videoTypeDisplay.innerHTML = contentType;}
if }
(available) {
if (trackInfo.type == "AUDIO") {qualityButton.style.color = QUALITY_COLORS.AVAILABLE;
} else {
audioStateButton.setContentType(contentType);
qualityButton.style.color = QUALITY_COLORS.UNAVAILABLE;
}
}
}
},
...
}
}, |
...
12.
...
12. Добавление
...
кнопки выбора качества
addQuality() code
Code Block |
---|
|
const createVideoPlayer = function (participantDiv) {
updateQualityInfo: function(videoQuality)...
return {
...
addQuality: function showItem(qualitySwitchDisplay);
(qualityName, available, onPickQuality) {
const qualityButton = document.createElement("button");
for (const qualityInfo of videoQuality) {qualityButtons.set(qualityName, {btn: qualityButton, available: available});
qualityButton.innerText = qualityName;
let qualityColor = QUALITY_COLORS.UNAVAILABLE;
qualityButton.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
if (qualityInfo.available === true) {
qualityColor qualityButton.style.color = QUALITY_COLORS.AVAILABLE;
} else {
}
qualityButton.style.color = QUALITY_COLORS.UNAVAILABLE;
for (const qualityDiv of qualityDivs) {}
qualityDisplay.appendChild(qualityButton);
if (qualityDiv.innerText === qualityInfo.quality)qualityButton.addEventListener('click', async function () {
console.log("Clicked on quality button " + qualityName);
if (qualityButton.style.color === QUALITY_COLORS.SELECTED || qualityDivqualityButton.style.color == qualityColor;
= QUALITY_COLORS.UNAVAILABLE || !videoElement) {
breakreturn;
}
}
lock();
}
onPickQuality().finally(() => unlock());
abr.setQualityAvailable(qualityInfo.quality, qualityInfo.available});
}},
...
}
}, |
...
12.
...
13. Нажатие на кнопку выбора качества
pickQuality() code
Code Block |
---|
|
const createVideoPlayer = function (participantDiv) {
dispose: function() ...
return {
...
abr.stop();pickQuality: function (qualityName) {
repickQuality(qualityName);
cell.remove();
}
...
}, |
4. Подписка на добавление дорожек в WebRTC соединение
PeerConnection.ontrack(), setAudio(), setVideo(), setVideoABRTrack() code
Здесь:
- при получении видео или аудио потока, добавляется элемент для его проигрывания
12.14. Завершение работы плеера
dispose() code
Code Block |
---|
|
const createVideoPlayer peerConnection.ontrack == function ({transceiver}) =>participantDiv) {
let rParticipant;...
return {
console.log("Attach remote track " + transceiver.receiver.track.id + " kind " + transceiver.receiver.track.kind + " mid " + transceiver.mid);.
dispose: function () {
streamDisplay.remove();
},
for (const [nickName, participant] of Object.entries(remoteParticipants))...
}
} |
13. Получение дорожки из комнаты для отображения
remoteTrackProvider() code
Code Block |
---|
|
const remoteTrackProvider = function (room) {
return {
for (constgetVideoTrack: pTrackasync offunction participant.tracks() {
return await consoleroom.loggetRemoteTrack("Participant VIDEO" + participant.nickName + " track " + pTrack.id + " mid " + pTrack.mid, false);
},
if (pTrack.mid === transceiver.midgetAudioTrack: async function () {
return await room.getRemoteTrack("AUDIO", true);
rParticipant = participant;}
}
} |
14. Вспомогательные функции
14.1. Изменение размера видео под размеры плеера
resizeVideo(), downScaleToFitSize() code
Code Block |
---|
|
const resizeVideo = function (video, width, height) {
// TODO: fix
break;
if (video) {
return;
}
if (!video.parentNode) {
}
return;
}
if (rParticipantvideo instanceof HTMLCanvasElement) {
video.videoWidth = video.width;
break;
video.videoHeight = video.height;
}
const display }= video.parentNode;
const parentSize = {
if (rParticipant) {
w: display.parentNode.clientWidth,
for (consth: display of rParticipant.displays) {.parentNode.clientHeight
};
let newSize;
if (transceiver.receiver.track.kind === "video"width && height) {
newSize = downScaleToFitSize(width, height, parentSize.w, parentSize.h);
if (display.videoMid === transceiver.mid)} else {
newSize = downScaleToFitSize(video.videoWidth, video.videoHeight, parentSize.w, parentSize.h);
}
display.style.width let= streamnewSize.w = new MediaStream()+ "px";
display.style.height = newSize.h + "px";
//vertical align
let margin stream.addTrack(transceiver.receiver.track)= 0;
if (parentSize.h - newSize.h > 1) {
margin = Math.floor((parentSize.h - newSize.h) display.setVideoABRTrack(transceiver.receiver.track/ 2);
}
display.style.margin = margin + "px auto";
display.setVideo(stream);
break;
}
} else if (transceiver.receiver.track.kind === "audio"console.log("Resize from " + video.videoWidth + "x" + video.videoHeight + " to " + display.offsetWidth + "x" + display.offsetHeight);
}
const downScaleToFitSize = function (videoWidth, videoHeight, dstWidth, dstHeight) {
var newWidth, newHeight;
var videoRatio = videoWidth / videoHeight;
var dstRatio = dstWidth / dstHeight;
if (dstRatio > videoRatio) {
newHeight = dstHeight;
newWidth = if (display.audioMid === transceiver.mid) {Math.floor(videoRatio * dstHeight);
} else {
newWidth = dstWidth;
letnewHeight stream = new MediaStream(= Math.floor(dstWidth / videoRatio);
}
return {
w: newWidth,
stream.addTrack(transceiver.receiver.track);
h: newHeight
};
} |
14.2. Создание элемента для отображения текстовой информации
createInfoDisplay() code
Code Block |
---|
|
const createInfoDisplay = function (parent, text) {
const div = displaydocument.setAudiocreateElement(stream"div");
if (text) {
div.innerHTML = text;
}
div.setAttribute("style", break"width:auto; height:30px;");
div.setAttribute("class", "text-center");
if (parent) {
}parent.appendChild(div);
}
return div;
} |
14.3. Создание элемента-контейнера
createContainer() code
Code Block |
---|
|
const createContainer = function (parent) {
}
const div = document.createElement("div");
div.setAttribute("style", }"width:auto; height:auto;");
} else {div.setAttribute("class", "text-center");
if (parent) {
console.warn("Failed to find participant for track " + transceiver.receiver.track.id parent.appendChild(div);
}
return div;
} |
5. Остановка воспроизведения
...
14.4. Скрытие и отображение элемента на странице
showItem(), hideItem() code
Code Block |
---|
|
const stopshowItem = function (tag) {
for (const [nickName, participant] of Object.entries(remoteParticipants)if (tag) {
participant.displays.forEach(function(display){
tag.style.display = "block";
}
}
const hideItem = function (tag) {
if display.dispose(tag);
}); {
tag.style.display delete remoteParticipants[nickName]= "none";
}
} |