...
2.15. Сбросить таймер использования текущего качества
2. Обработка событий комнаты
2.1. ADD_TRACKS
initRemoteDisplay() code
Здесь:
...
abr.stopKeeping() 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 participantabr = remoteParticipants[e.info.nickName];
{
if (!participant) { ...
participant = {};
stopKeeping: function () {
participant.nickName = e.info.nickName;
if participant(abr.tracks = [];keepGoodTimer) {
participant.displays = [];
clearTimeout(abr.keepGoodTimer);
remoteParticipants[participant.nickName] = participant;
}
participant.tracks.push.apply(participant.tracks, e.info.info);
abr.keepGoodTimer = null;
for (const pTrack of e.info.info) { }
let createDisplay = true;},
for (let i = 0; i < participant.displays.length; i++) {
...
}
let display = participant.displays[i]return abr;
}
}
} |
2.16. Переключиться на более высокое качество на заданное время
abr.tryUpper() code
Code Block |
---|
|
const abrManagerFactory = function if (pTrack.type === "VIDEO"(room, abrOptions) {
return {
createAbrManager: function () {
if (display.hasVideo()) {
let abr = {
continue;...
tryUpper: function () }{
display.videoMidlet quality = pTrack.midabr.getUpperQuality(abr.currentQualityName);
if (abr.tryUpperTimeout && display.setTrackInfo(pTrack);!abr.tryUpperTimer && quality) {
createDisplay = false;
abr.tryUpperTimer = setTimeout(() => {
break;
abr.setQualityGood(quality.name, true);
} else if (pTrack.type === "AUDIO") {
if (displayabr.hasAudiostopTrying()) {;
continue}, abr.tryUpperTimeout);
}
display.audioMid = pTrack.mid;},
...
display.setTrackInfo(pTrack);
}
createDisplayreturn = falseabr;
}
}
} |
2.17. Остановить таймер тестирования более высокого качества
abr.stopTrying() code
Code Block |
---|
|
const abrManagerFactory = function (room, break;abrOptions) {
return {
createAbrManager: function () }{
let abr = }{
if (!createDisplay) {
...
continue;
stopTrying: function () {
}
let display =if createRemoteDisplay(participantabr.nickName, participant.nickName, mainDiv, displayOptions);tryUpperTimer) {
participant.displays.push(display);
if clearTimeout(pTrack.type === "VIDEO") {abr.tryUpperTimer);
display.videoMid = pTrack.mid;
abr.tryUpperTimer = null;
display.setTrackInfo(pTrack);
} else if (pTrack.type === "AUDIO") {
display.audioMid = pTrack.mid;},
display.setTrackInfo(pTrack);...
}
return }abr;
...}
});
} |
2.
...
18. Переключиться на указанное качество
abr.setQuality() code
Здесь:
- удаляются элементы, в которых проигрывались потоки
- информация о потоках удаляется из списка
Code Block |
---|
language | Code Block |
---|
|
const abrManagerFactory = function room.on(constants.SFU_ROOM_EVENT.ADD_TRACKS, function(e(room, abrOptions) {
return {
...
}).on(constants.SFU_ROOM_EVENT.REMOVE_TRACKS,createAbrManager: function (e) {
console.log("Received REMOVE_TRACKS"); let abr = {
const participant = remoteParticipants[e..info.nickName];
ifsetQuality: async function (!participantname) {
return;
}
console.log("set quality name");
for (const rTrack of e.info.info) {
// Pause switching foruntil (leta inew =quality 0;is ireceived
< participant.tracks.length; i++) {
if (rTrack.mid === participant.tracks[i].mid) {abr.pause();
participant.tracks.splice(i, 1)abr.currentQualityName = name;
breakabr.track.setPreferredQuality(abr.currentQualityName);
}
}
for (let i = 0; i < participant.displays.length; i++) {return abr;
}
}
} |
3. Создание объекта для управления комнатой
createDefaultMeetingController() code
Code Block |
---|
|
const createDefaultMeetingController = function (room, meetingModel) {
...
let found =return false;{
stop: stop
const display = participant.displays[i];
}
} |
3.1. Обработка события PARTICIPANT_LIST
createDefaultMeetingController() code
Code Block |
---|
|
const createDefaultMeetingController = function (room, meetingModel) {
const constants = SFU.constants;
if (display.audioMid === rTrack.midroom.on(constants.SFU_ROOM_EVENT.PARTICIPANT_LIST, async function (e) {
for (const idName of e.participants) {
displaymeetingModel.setAudio(nulladdParticipant(idName.userId, idName.name);
}
...
found = true});
...
} |
3.2. Обработка события JOINED
createDefaultMeetingController() code
Code Block |
---|
|
const createDefaultMeetingController = function (room, meetingModel) {
} else if (display.videoMid === rTrack.mid) {const constants = SFU.constants;
room.on(constants.SFU_ROOM_EVENT.PARTICIPANT_LIST, async function (e) {
...
display}).setVideo(null);
on(constants.SFU_ROOM_EVENT.JOINED, async function (e) {
found = truemeetingModel.addParticipant(e.userId, e.name);
...
});
...
} |
3.3. Обработка события LEFT
createDefaultMeetingController() code
Code Block |
---|
|
const createDefaultMeetingController = function if (found(room, meetingModel) {
const constants = SFU.constants;
if (!display.hasAudio() && !display.hasVideo())room.on(constants.SFU_ROOM_EVENT.PARTICIPANT_LIST, async function (e) {
...
}).on(constants.SFU_ROOM_EVENT.LEFT, function (e) {
displaymeetingModel.disposeremoveParticipant(e.userId);
...
});
participant.displays.splice(i, 1);
...
} |
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 break;(e) {
}meetingModel.addTracks(e.info.userId, e.info.info);
}
...
});
...
}); |
...
3.
...
4. Обработка события REMOVE_TRACKS
createDefaultMeetingController() codeЗдесь:
- участник удаляется из списка
- удаляются элементы, в которых проигрывались потоки
Code Block |
---|
|
const createDefaultMeetingController = function room.on((room, meetingModel) {
const constants = SFU.constants;
room.on(constants.SFU_ROOM_EVENT.ADDPARTICIPANT_TRACKSLIST, async function (e) {
...
}).on(constants.SFU_ROOM_EVENT.LEFTREMOVE_TRACKS, async function (e) {
consolemeetingModel.log("Received LEFT"removeTracks(e.info.userId, e.info.info);
...
let participant = remoteParticipants[e.name] });
...
} |
3.4. Обработка события TRACK_QUALITY_STATE
createDefaultMeetingController() code
Code Block |
---|
|
const createDefaultMeetingController = function if (!participant(room, meetingModel) {
const constants return= SFU.constants;
room.on(constants.SFU_ROOM_EVENT.PARTICIPANT_LIST, async function (e) }{
participant..displays.forEach(function(display){
}).on(constants.SFU_ROOM_EVENT.TRACK_QUALITY_STATE, async display.dispose();function (e) {
}) meetingModel.updateQualityInfo(e.info.userId, e.info.tracks);
delete remoteParticipants[e.name];...
});
...
}); |
...
3.
...
5. Обработка события ENDED
createDefaultMeetingController() code
Здесь:
...
Code Block |
---|
|
const createDefaultMeetingController = function (room, meetingModel) {
const constants = SFU.constants;
room.on(constants.SFU_ROOM_EVENT.ADDPARTICIPANT_TRACKSLIST, async function (e) {
...
}).on(constants.SFU_ROOM_EVENT.TRACK_QUALITY_STATEENDED, function (e) {
consolemeetingModel.log("Received track quality state"end();
});
...
} |
3.6. Остановка комнаты
createDefaultMeetingController() code
Code Block |
---|
|
const createDefaultMeetingController = function (room, meetingModel) {
const participant = remoteParticipants[e..info.nickName];
const stop = iffunction (!participant) {
meetingModel.end();
return};
return {
}
stop: stop
for (const rTrack of e.info.tracks }
} |
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) {
const mid = rTrack.mid;return {
...
for (let i = 0;addParticipant: ifunction < participant.displays.length; i++(userId, participantName) {
if (this.participants.get(userId)) {
const display = participant.displays[i];
return;
if (display.videoMid === mid) { }
const [participantModel, participantView, participant] = participantFactory.createParticipant(userId, participantName, display.updateQualityInfo(rTrack.qualitydisplayOptions, abrFactory);
breakthis.participants.set(userId, participant);
meetingView.addParticipant(userId, }participantName, participantView.rootDiv);
},
}...
}
}); |
3. Создание элементов для отображения потоков участников
3.1. Создание контейнера
createRemoteDisplay() code
Здесь:
- настраиваются параметры отображения
- создается контейнер для потоков участника
- создается контейнер для конкретного потока
- создается контейнер для кнопок переключения качества
4.2. Удаление участника
removeParticipant() code
Code Block |
---|
|
const cellcreateDefaultMeetingModel = function document.createElement("div");
(meetingView, participantFactory, displayOptions, abrFactory) {
cell.setAttribute("class", "text-center");return {
cell.id = id;...
mainDiv.appendChild(cell);
removeParticipant: function (userId) {
const participant let publisherNameDisplay= this.participants.get(userId);
let currentQualityDisplay;
if (participant) {
let videoTypeDisplay;
let abrQualityCheckPeriod = ABR_QUALITY_CHECK_PERIOD;
this.participants.delete(userId);
let abrKeepOnGoodQuality = ABR_KEEP_ON_QUALITY meetingView.removeParticipant(userId);
let abrTryForUpperQuality = ABR_TRY_UPPER_QUALITY;
participant.dispose();
if (displayOptions.abrQualityCheckPeriod !== undefined) {
}
abrQualityCheckPeriod = displayOptions.abrQualityCheckPeriod; },
}...
}
} |
4.3. Переименование участника
renameParticipant() code
Code Block |
---|
|
const createDefaultMeetingModel = function (meetingView, ifparticipantFactory, (displayOptions.abrKeepOnGoodQuality !== undefined, abrFactory) {
return {
abrKeepOnGoodQuality = displayOptions.abrKeepOnGoodQuality;...
}
if (displayOptions.abrTryForUpperQuality !== undefinedrenameParticipant: function (userId, newNickname) {
const abrTryForUpperQualityparticipant = displayOptions.abrTryForUpperQualitythis.participants.get(userId);
}
if (!displayOptions.abrparticipant) {
abrQualityCheckPeriod = 0 participant.setNickname(newNickname);
abrKeepOnGoodQuality}
= 0;
},
abrTryForUpperQuality = 0;...
}
} |
4.4. Добавление треков участника для отображения
addTracks() code
Code Block |
---|
|
const createDefaultMeetingModel = function }
(meetingView, participantFactory, displayOptions, abrFactory) {
if (displayOptions.publisher)return {
...
publisherNameDisplay = createInfoDisplay(cell, "Published byaddTracks: "function +(userId, nametracks); {
}
const participant if (displayOptions.quality) {= this.participants.get(userId);
currentQualityDisplayif = createInfoDisplay(cell, "");(!participant) {
}
if (displayOptions.type) {return;
videoTypeDisplay = createInfoDisplay(cell, "");
}
}
for (const qualitySwitchDisplaytrack = createInfoDisplay(cell, "");
of tracks) {
let qualityDivs = [];
if let contentType (track.type === "VIDEO";
) {
const rootDisplay = createContainer(cell);
const streamDisplay = createContainer(rootDisplay participant.addVideoTrack(track);
const audioDisplay = createContainer(rootDisplay);
} else if const audioTypeDisplay = createInfoDisplay(audioDisplay);(track.type === "AUDIO") {
const audioTrackDisplay = createContainer(audioDisplay);
const audioStateButton = AudioStateButton( participant.addAudioTrack(track);
hideItem(streamDisplay);
hideItem(audioDisplay);}
hideItem(publisherNameDisplay);
}
hideItem(currentQualityDisplay);
},
hideItem(videoTypeDisplay);
...
hideItem(qualitySwitchDisplay); |
3.2. Инициализация ABR
createRemoteDisplay() code
...
4.5. Удаление отображаемых треков участника
removeTracks() code
Code Block |
---|
|
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
const abr = ABR(abrQualityCheckPeriod, [
return {
...
{parameterremoveTracks: "nackCount" function (userId, maxLeap: 10},tracks) {
{parameter: "freezeCount", maxLeap: 10},const participant = this.participants.get(userId);
if {parameter: "packetsLost", maxLeap: 10}
(!participant) {
], abrKeepOnGoodQuality, abrTryForUpperQuality); |
3.3. Добавление видео элемента
setVideo() code
Code Block |
---|
|
return;
setVideo: function(stream) {}
for (const track of if (videotracks) {
if video.remove();
(track.type === "VIDEO") {
}
participant.removeVideoTrack(track);
} else if (streamtrack.type === null"AUDIO") {
video = null;
participant.removeAudioTrack(track);
}
this.videoMid = undefined;}
},
...
qualityDivs.forEach(function(div}
} |
4.6. Обновление информации о качестве треков участника
updateQualityInfo() code
Code Block |
---|
|
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
return {
...
updateQualityInfo: function div.remove();(userId, tracksInfo) {
const participant }= this.participants.get(userId);
if (!participant) {
qualityDivs = [];
return;
return;}
participant.updateQualityInfo(tracksInfo);
}
},
...
showItem(streamDisplay);
}
} |
4.7. Завершение отображения участников при остановке комнаты
end() code
Code Block |
---|
|
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
return {
video = document.createElement("video");...
end: function () {
video.controls = "controls";
console.log("Meeting " + this.meetingName + " ended")
video.muted = true;
meetingView.end();
video.autoplay this.participants.forEach((participant, id) => true;{
if (Browser().isSafariWebRTC()) {
participant.dispose();
});
videothis.participants.setAttribute("playsinline", ""clear();
},
...
video.setAttribute("webkit-playsinline", "");
}
} |
4.8. Переименование комнаты
setMeetingName() code
Code Block |
---|
|
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
return {
this.setWebkitEventHandlers(video);...
setMeetingName: function (id) {
} else {
this.meetingName = id;
thismeetingView.setEventHandlerssetMeetingName(videoid);
}
}
} |
5. Создание объекта для отображения потоков в комнате
createDefaultMeetingView() code
Code Block |
---|
|
const createDefaultMeetingView = function (entryPoint) }{
...
} |
5.1. Инициализация HTML5 элементов
createDefaultMeetingView() code
Code Block |
---|
|
const createDefaultMeetingView = function (entryPoint) {
const rootDiv = streamDisplaydocument.appendChildcreateElement(video"div");
rootDiv.setAttribute("class", "grid-item");
entryPoint.appendChild(rootDiv);
const video.srcObjecttitle = streamdocument.createElement("label");
title.setAttribute("style", "display:block; border: solid; border-width: 1px");
this.setResizeHandler(videorootDiv.appendChild(title);
return {
abr.start();...
}, |
3.4. Добавление аудио элемента
...
5.2. Инициализация списка элементов для отображения участников
participantViews() code
Code Block |
---|
|
const createDefaultMeetingView = function (entryPoint) {
...
setAudio: function(stream) return {
participantViews: if (audio) {
new Map(),
...
}
} |
5.3. Отображение имени комнаты
setMeetingName() code
Code Block |
---|
|
const createDefaultMeetingView = function (entryPoint) {
audio.remove();...
return {
}...
if (!streamsetMeetingName: function (id) {
title.innerText = "Meeting: " audio = null+ id;
},
...
this.audioMid}
} |
5.4. Добавление элементов для отображения участника
addParticipant() code
Code Block |
---|
|
const createDefaultMeetingView = undefined;
function (entryPoint) {
...
return {
return;...
addParticipant: function (userId, participantName, }cell) {
const participantDiv = showItemcreateContainer(audioDisplayrootDiv);
audio = document.createElement("audio"participantDiv.appendChild(cell);
audio.controls = "controls"this.participantViews.set(userId, participantDiv);
},
audio.muted = true;...
audio.autoplay = true;
if (Browser().isSafariWebRTC())}
} |
5.5. Удаление элементов участника
removeParticipant() code
Code Block |
---|
|
const createDefaultMeetingView = function (entryPoint) {
...
return {
audio.setAttribute("playsinline", "");...
audio.setAttribute("webkit-playsinline", "");removeParticipant: function (userId) {
const cell = this.participantViews.setWebkitEventHandlersget(audiouserId);
if } else (cell) {
this.setEventHandlersparticipantViews.delete(audiouserId);
}
audioTrackDisplay.appendChild(audiocell.remove();
audioStateButton.makeButton(audioTypeDisplay, audio);}
},
audio.srcObject = stream;...
}
} |
5.5. Удаление корневого элемента отображения
end() code
Code Block |
---|
|
const createDefaultMeetingView = function (entryPoint) {
audio.onloadedmetadata = function (e)...
return {
...
end: audio.play().then(functionfunction () {
rootDiv.remove();
}
if (Browser().isSafariWebRTC() && Browser().isiOS())}
} |
6. Создание фабрики объектов участников
createParticipantFactory() code
Здесь содаются объекты модели, управления и отображения участника комнаты
Code Block |
---|
|
const createParticipantFactory = function (remoteTrackFactory, createParticipantView, createParticipantModel) {
return {
displayOptions: null,
abrFactory: null,
console.warn("Audio track should becreateParticipant: manuallyfunction unmuted in iOS Safari");(userId, nickname) {
const view = createParticipantView();
} else {
const model = createParticipantModel(userId, nickname, view, remoteTrackFactory, this.abrFactory, this.displayOptions);
audio.mutedconst controller = falsecreateParticipantController(model);
return [model, view, controller];
}
audioStateButton.setButtonState();
}
} |
7. Создание объекта управления участником
createParticipantController() code
Объект вызывает соответствующие методы модели участника
Code Block |
---|
|
const createParticipantController = function (model) {
return {
addVideoTrack: function }(track) {
model.addVideoTrack(track);
});,
removeVideoTrack: function (track) {
};
model.removeVideoTrack(track);
}, |
3.5. Настройка обработчиков событий для аудио и видео элементов
setResizeHandler(), setEventHandlers(), setWebkitEventHandlers() code
Code Block |
---|
|
},
setEventHandlersaddAudioTrack: function (videotrack) {
// Ignore play/pause buttonmodel.addAudioTrack(track);
},
video.addEventListener("pause",removeAudioTrack: function (track) {
model.removeAudioTrack(track);
console.log("Media paused by click, continue...");
},
updateQualityInfo: function (qualityInfo) {
videomodel.playupdateQualityInfo(qualityInfo);
},
setNickname: function }(nickname); {
},
model.setNickname(nickname);
},
setWebkitEventHandlersdispose: function (video) {
model.dispose();
let needRestart = false;}
}
} |
8. Создание объекта модели участника в комнате с двумя участниками
createOneToOneParticipantModel() code
Code Block |
---|
|
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
letconst isFullscreeninstance = false;{
userId: userId,
nickname: nickname,
// Use webkitbeginfullscreen event to detect fullremoteVideoTracks: screen mode in iOS Safari
new Map(),
remoteAudioTracks: new Map(),
video.addEventListener("webkitbeginfullscreen", function () {audioTracks: new Map(),
videoTracks: new Map(),
abrManagers: new isFullscreenMap(),
= true;
disposed: false,
dispose: });async function () {
...
},
video.addEventListener("pause",addVideoTrack: function (track) {
...
if (needRestart) {},
removeVideoTrack: function (track) {
console.log("Media paused after fullscreen, continue...");
},
addAudioTrack: function (track) {
video.play();
...
},
removeAudioTrack: needRestartfunction = false;(track) {
...
},
else {
setUserId: function (userId) {
...
console.log("Media paused by click, continue...");},
setNickname: function (nickname) {
video.play();...
},
updateQualityInfo: function (remoteTracks) }{
...
});
},
requestVideoTrack: async function video.addEventListener("webkitendfullscreen"(track, function (remoteTrack) {
...
video.play();
},
pickQuality: async function (track, qualityName) {
needRestart = true;...
},
muteVideo: async function isFullscreen = false;
(track) {
...
});,
unmuteVideo: async }, |
3.6. Добавление информации о дорожке в ABR
setVideoABRTrack() code
Code Block |
---|
|
function (track) {
setVideoABRTrack: function(track) { ...
}
};
abrinstance.setTracksetUserId(trackuserId);
instance.setNickname(nickname);
}, |
3.7. Настройка переключения качества
setTrackInfo() code
Здесь:
- настраиваются кнопки переключения качества
- информация о заявленных качествах публикации добавляется в ABR
- скрываются или отображаются элементы для показа текущего качества и источника видео/аудио
8.1. Добавление видео дорожки для отображения
addVideoTrack() code
Code Block |
---|
|
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
const setTrackInfo: function(trackInfo)instance = {
...
addVideoTrack: iffunction (trackInfotrack) {
this.videoTracks.set(track.mid, track);
if (trackInfo!track.quality) {
track.quality showItem(qualitySwitchDisplay)= [];
}
if (abr.isEnabled()) {participantView.addVideoTrack(track);
const self = this;
const autoDiv = createQualityButton("Auto", qualityDivs, qualitySwitchDisplay);
remoteTrackFactory.getVideoTrack().then((remoteTrack) => {
if (remoteTrack) {
autoDiv.style.color = QUALITY_COLORS.SELECTED;
if (self.disposed || !self.videoTracks.get(track.mid)) {
autoDivremoteTrack.addEventListener('click', function() {dispose();
return;
setQualityButtonsColor(qualityDivs);
}
autoDivparticipantView.style.coloraddVideoSource(remoteTrack.track, track, () = QUALITY_COLORS.SELECTED;> {
const abrManager abr.setAuto(= self.abrManagers.get(track.id);
if (!abrManager) {
});
}return;
}
for (let i = 0; i < trackInfo.quality.length; i++) {
if (abrManager.isAuto()) {
abr.addQuality(trackInfo.quality[i]);
abrManager.resume();
const qualityDiv = createQualityButton(trackInfo.quality[i], qualityDivs, qualitySwitchDisplay);
}
qualityDiv.addEventListener('click'}, function(mute) => {
if (mute) {
console.log("Clicked on quality " + trackInfo.quality[i] + " trackId " + trackInfo.id);
return self.muteVideo(track);
if (qualityDiv.style.color === QUALITY_COLORS.UNAVAILABLE) } else {
return self.unmuteVideo(track);
return;
}
}
});
self.requestVideoTrack(track, remoteTrack).then(() => {
setQualityButtonsColor(qualityDivs);
participantView.showVideoTrack(track);
qualityDiv.style.color = QUALITY_COLORS.SELECTED;
}, (ex) => {
abrparticipantView.setManualremoveVideoSource(track);
abr.setQuality(trackInfo.quality[i]remoteTrack.dispose();
});
}
}
}, (ex) => {
} else {
console.log("Failed to get remote track " + ex);
hideItem(qualitySwitchDisplay});
},
...
};
...
return instance;
} |
8.2. Удаление отображаемой видео дорожки
removeVideoTrack() code
Code Block |
---|
|
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, if (trackInfo.typeabrFactory, displayOptions) {
const instance = {
...
removeVideoTrack: function contentType = trackInfo.contentType || "";
(track) {
if (this.videoTracks.delete(track.mid)) {
if (trackInfo.type == "VIDEO" && displayOptions.type && contentType !== "") {
const remoteTrack = this.remoteVideoTracks.get(track.mid);
if (remoteTrack) {
showItem(videoTypeDisplaythis.remoteVideoTracks.delete(track.mid);
remoteTrack.dispose();
videoTypeDisplay.innerHTML = contentType;
}
participantView.removeVideoTrack(track);
}
const abrManager = this.abrManagers.get(track.id);
if (trackInfo.type == "AUDIO") {
if (abrManager) {
audioStateButtonthis.abrManagers.setContentTypedelete(contentTypetrack.id);
}abrManager.clearQualityState();
}abrManager.stop();
}
}, |
3.8. Добавление информации о доступных качествах видео в ABR
updateQualityInfo() code
Code Block |
---|
|
},
updateQualityInfo: function(videoQuality) {
...
};
...
showItem(qualitySwitchDisplay);
for (const qualityInfo of videoQuality) return instance;
} |
8.3. Добавление аудио дорожки для отображения
addAudioTrack() code
Code Block |
---|
|
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
const instance = {
...
addAudioTrack: function let qualityColor = QUALITY_COLORS.UNAVAILABLE;(track) {
if (qualityInfo.available === true) {this.audioTracks.set(track.mid, track);
const self = this;
qualityColor = QUALITY_COLORS.AVAILABLE;
remoteTrackFactory.getAudioTrack().then((remoteTrack) => {
if (remoteTrack) }{
forif (const qualityDiv of qualityDivsself.disposed || !self.audioTracks.get(track.mid)) {
if (qualityDiv.innerText === qualityInfo.quality){
remoteTrack.dispose();
return;
qualityDiv.style.color = qualityColor;
}
breakthis.remoteAudioTracks.set(track.mid, remoteTrack);
remoteTrack.demandTrack(track.id).then(() => {
}
}
if (!self.audioTracks.get(track.mid)) {
abr.setQualityAvailable(qualityInfo.quality, qualityInfo.available remoteTrack.dispose();
}
}, |
3.9. Удаление контейнера с видео и аудио
dispose() code
Code Block |
---|
|
self.remoteAudioTracks.delete(track.mid);
dispose: function() {return;
abr.stop();
}
cell.remove();
}, |
4. Подписка на добавление дорожек в WebRTC соединение
PeerConnection.ontrack(), setAudio(), setVideo(), setVideoABRTrack() code
Здесь:
- при получении видео или аудио потока, добавляется элемент для его проигрывания
Code Block |
---|
|
peerConnection.ontrack = ({transceiver}participantView.addAudioTrack(track, remoteTrack.track, displayOptions.showAudio);
}, (ex) => {
let rParticipant;
console.log("AttachFailed remotedemand track " + transceiver.receiver.track.id + " kind " + transceiver.receiver.track.kind + " mid " + transceiver.mid);
ex);
for (const [nickName, participant] of ObjectremoteTrack.entriesdispose(remoteParticipants)) {;
for (const pTrack of participant.tracks) {
self.remoteAudioTracks.delete(track.mid);
console.log("Participant " + participant.nickName + " track " + pTrack.id + " mid " + pTrack.mid });
if (pTrack.mid === transceiver.mid) {}
}, (ex) => {
rParticipant = participant;
console.log("Failed to get audio track " + ex);
break;
});
},
...
};
...
return instance;
} |
8.4. Удаление отображаемой аудио дорожки
removeAudioTrack() code
Code Block |
---|
|
const createOneToOneParticipantModel = iffunction (rParticipantuserId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
const instance = {
break;...
removeAudioTrack: function (track) }
{
}
if (rParticipant!this.audioTracks.delete(track.mid)) {
for (const display of rParticipant.displays) { return
}
if (transceiver.receiver.track.kind === "video") {
participantView.removeAudioTrack(track);
const remoteTrack if (display.videoMid === transceiver= this.remoteAudioTracks.get(track.mid);
{
if (remoteTrack) {
let stream = new MediaStream(this.remoteAudioTracks.delete(track.mid);
stream.addTrack(transceiver.receiver.trackremoteTrack.dispose();
}
},
display .setVideoABRTrack(transceiver.receiver.track);
};
...
return instance;
} |
8.5. Запрос видео дорожки для отображения
requestVideoTrack() code
Code Block |
---|
|
const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, display.setVideo(stream);displayOptions) {
const instance = {
...
requestVideoTrack: break;
async function (track, remoteTrack) {
return new }
Promise((resolve, reject) => {
} else if (transceiver.receiver.track.kind === "audio"!remoteTrack || !track) {
ifreject(new (display.audioMid === transceiver.mid) {
Error("Remote and local track must be defined"));
return;
let stream = new MediaStream();}
const self = this;
streamremoteTrack.addTrackdemandTrack(transceivertrack.receiver.track);
id).then(() => {
display.setAudio(stream);
if (!self.videoTracks.get(track.mid)) {
reject(new Error("Video track already removed from breakmodel"));
} return;
}
}
let }abrManager else {
console.warn("Failed to find participant for track " + transceiver.receiver.track.id);
}
} |
5. Остановка воспроизведения
...
= 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 {
...
removeAudioTrack: function (track) {
const audioElement = audioElements.get(track.mid);
if (audioElement) {
audioElement.remove();
audioElements.delete(track.mid);
}
},
...
}
} |
11.8. Отображение имени участника
setNickname() code
Code Block |
---|
|
const createOneToManyParticipantView = function () {
...
return {
...
setNickname: function (userId, nickname) {
const additionalUserId = userId ? "#" + getShortUserId(userId) : "";
participantNicknameDisplay.innerText = "Name: " + nickname + additionalUserId;
},
...
}
} |
11.9. Обновление информации о качестве
updateQuality() code
Code Block |
---|
|
const createOneToManyParticipantView = function () {
...
return {
...
updateQuality: function (track, qualityName, available) {
player.updateQuality(qualityName, available);
},
...
}
} |
11.10. Добавление информации о качестве
addQuality() code
Code Block |
---|
|
const createOneToManyParticipantView = function () {
...
return {
...
addQuality: function (track, qualityName, available, onQualityPick) {
player.addQuality(qualityName, available, onQualityPick);
},
...
}
} |
11.11. Выбор качества
pickQuality() code
Code Block |
---|
|
const createOneToManyParticipantView = function () {
...
return {
...
pickQuality: function (track, qualityName) {
player.pickQuality(qualityName);
}
...
}
} |
11.12. Очистка отображаемого списка качеств
clearQualityState() code
Code Block |
---|
|
const createOneToManyParticipantView = function () {
...
return {
...
clearQualityState: function (track) {
player.clearQualityState();
},
...
}
} |
11.13. Завершение отображения
dispose() code
Code Block |
---|
|
const createOneToManyParticipantView = function () {
...
return {
...
dispose: function () {
player.dispose();
for (const element of audioElements.values()) {
element.remove();
}
audioElements.clear();
},
...
}
} |
12. Создание объекта видео плеера
createVideoPlayer() code
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
const createVideoPlayer = function (participantDiv) {
...
return {
...
clearQualityState: function () {
qualityButtons.forEach((state, qName) => {
state.btn.remove();
});
qualityButtons.clear();
},
...
}
} |
12.6. Добавление видео дорожки
addVideoTrack() code
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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
Code Block |
---|
|
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());
});
},
...
}
} |
12.13. Нажатие на кнопку выбора качества
pickQuality() code
Code Block |
---|
|
const createVideoPlayer = function (participantDiv) {
...
return {
...
pickQuality: function (qualityName) {
repickQuality(qualityName);
}
...
}
} |
12.14. Завершение работы плеера
dispose() code
Code Block |
---|
|
const createVideoPlayer = function (participantDiv) {
...
return {
...
dispose: function () {
streamDisplay.remove();
},
...
}
} |
13. Получение дорожки из комнаты для отображения
remoteTrackProvider() code
Code Block |
---|
|
const remoteTrackProvider = function (room) {
return {
getVideoTrack: async function () {
return await room.getRemoteTrack("VIDEO", false);
},
getAudioTrack: async function () {
return await room.getRemoteTrack("AUDIO", true);
}
}
} |
14. Вспомогательные функции
14.1. Изменение размера видео под размеры плеера
resizeVideo(), downScaleToFitSize() code
Code Block |
---|
|
const resizeVideo = function (video, width, height) {
// TODO: fix
if (video) {
return;
}
if (!video.parentNode) {
return;
}
if (video instanceof HTMLCanvasElement) {
video.videoWidth = video.width;
video.videoHeight = video.height;
}
const display = video.parentNode;
const parentSize = {
w: display.parentNode.clientWidth,
h: display.parentNode.clientHeight
};
let newSize;
if (width && height) {
newSize = downScaleToFitSize(width, height, parentSize.w, parentSize.h);
} else {
newSize = downScaleToFitSize(video.videoWidth, video.videoHeight, parentSize.w, parentSize.h);
}
display.style.width = newSize.w + "px";
display.style.height = newSize.h + "px";
//vertical align
let margin = 0;
if (parentSize.h - newSize.h > 1) {
margin = Math.floor((parentSize.h - newSize.h) / 2);
}
display.style.margin = margin + "px auto";
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 = Math.floor(videoRatio * dstHeight);
} else {
newWidth = dstWidth;
newHeight = Math.floor(dstWidth / videoRatio);
}
return {
w: newWidth,
h: newHeight
};
} |
14.2. Создание элемента для отображения текстовой информации
createInfoDisplay() code
Code Block |
---|
|
const createInfoDisplay = function (parent, text) {
const div = document.createElement("div");
if (text) {
div.innerHTML = text;
}
div.setAttribute("style", "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;");
div.setAttribute("class", "text-center");
if (parent) {
parent.appendChild(div);
}
return div;
} |
14.4. Скрытие и отображение элемента на странице
showItem(), hideItem() code
Code Block |
---|
|
const stopshowItem = function (tag) {
if for (const [nickName, participant] of Object.entries(remoteParticipants)(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";
}
} |