...
2.15. Сбросить таймер использования текущего качества
2. Обработка событий комнаты
2.1. ADD_TRACKS
initRemoteDisplayabr.stopKeeping() codeЗдесь:
- новый участник добавляется в список
- добавляется информация о качестве потоков
- создается элемент для отображения видео и аудио потоков участника
Code Block |
---|
|
const abrManagerFactory = function room.on(constants.SFU_ROOM_EVENT.ADD_TRACKS, function(e(room, abrOptions) {
console.log("Received ADD_TRACKS");return {
let participant = remoteParticipants[e.info.nickName];
if (!participantcreateAbrManager: function () {
let participantabr = {};
participant.nickName = e..info.nickName;
participant.tracks = [];
stopKeeping: function () {
participant.displays = [];
if (abr.keepGoodTimer) {
remoteParticipants[participant.nickName] = participant;
}
participant.tracks.push.apply(participant.tracks, e.info.infoclearTimeout(abr.keepGoodTimer);
for (const pTrack of e.info.info) {
let createDisplayabr.keepGoodTimer = truenull;
for (let i = 0; i < participant.displays.length; i++) { }
let},
display = participant.displays[i];
...
if (pTrack.type === "VIDEO") {
}
return abr;
if (display.hasVideo()) {
}
}
} |
2.16. Переключиться на более высокое качество на заданное время
abr.tryUpper() code
Code Block |
---|
|
const abrManagerFactory = function (room, abrOptions) {
return {
continue;
createAbrManager: function () {
let abr = }{
display.videoMid = pTrack.mid;...
tryUpper: function display.setTrackInfo(pTrack); {
let createDisplayquality = falseabr.getUpperQuality(abr.currentQualityName);
break;
if (abr.tryUpperTimeout && !abr.tryUpperTimer && quality) {
} else if (pTrack.type === "AUDIO") {
abr.tryUpperTimer if (display.hasAudio())= setTimeout(() => {
continue;
abr.setQualityGood(quality.name, true);
}
abr.stopTrying();
display.audioMid = pTrack.mid;
display.setTrackInfo(pTrack}, abr.tryUpperTimeout);
createDisplay = false;}
break;},
}...
}
if (!createDisplay) {return abr;
}
continue;
}
} |
2.17. Остановить таймер тестирования более высокого качества
abr.stopTrying() code
Code Block |
---|
|
const abrManagerFactory = function (room, abrOptions) {
return {
}createAbrManager: function () {
let displayabr = createRemoteDisplay(participant.nickName, participant.nickName, mainDiv, displayOptions);
{
participant..displays.push(display);
if (pTrack.type === "VIDEO" stopTrying: function () {
display.videoMid = pTrack.mid;
if (abr.tryUpperTimer) {
display.setTrackInfo(pTrack);
} else if (pTrack.type === "AUDIO") {
clearTimeout(abr.tryUpperTimer);
displayabr.audioMidtryUpperTimer = pTrack.midnull;
display.setTrackInfo(pTrack);
}
},
}
...
}); }
return abr;
}
}
} |
2.
...
18. Переключиться на указанное качество
abr.setQuality() code
Здесь:
- удаляются элементы, в которых проигрывались потоки
- информация о потоках удаляется из списка
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];
if (!participant) {
setQuality: async function (name) {
return;
}
for console.log(const"set rTrack of e.info.info) {quality name");
for (let i = 0; i < participant.tracks.length; i++) {
if (rTrack.mid === participant.tracks[i].mid) { // Pause switching until a new quality is received
participantabr.tracks.splice(i, 1pause();
breakabr.currentQualityName = name;
}
abr.track.setPreferredQuality(abr.currentQualityName);
}
}
for (let i = 0; i < participant.displays.length; i++) {}
let found = falsereturn abr;
}
const display = participant.displays[i];
if (display.audioMid === rTrack.mid) {}
} |
3. Создание объекта для управления комнатой
createDefaultMeetingController() code
Code Block |
---|
|
const createDefaultMeetingController = function (room, meetingModel) {
...
return {
stop: stop
display.setAudio(null);
}
} |
3.1. Обработка события PARTICIPANT_LIST
createDefaultMeetingController() code
Code Block |
---|
|
const createDefaultMeetingController = function (room, meetingModel) {
const constants = SFU.constants;
found = true;room.on(constants.SFU_ROOM_EVENT.PARTICIPANT_LIST, async function (e) {
for (const idName } else if (display.videoMid === rTrack.midof e.participants) {
display.setVideo(nullmeetingModel.addParticipant(idName.userId, idName.name);
}
...
found = true});
...
} |
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 if (foundfunction (e) {
...
if (!display.hasAudio() && !display.hasVideo())}).on(constants.SFU_ROOM_EVENT.JOINED, async function (e) {
display.dispose(meetingModel.addParticipant(e.userId, e.name);
...
});
participant.displays.splice(i, 1);
...
} |
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) {
break;...
}).on(constants.SFU_ROOM_EVENT.LEFT, function (e) {
}meetingModel.removeParticipant(e.userId);
}
...
});
...
}); |
2.3. LEFT
...
3.4. Обработка события ADD_TRACKS
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.LEFTADD_TRACKS, async function (e) {
consolemeetingModel.log("Received LEFT"addTracks(e.info.userId, e.info.info);
let participant = remoteParticipants[e.name];...
if (!participant) {});
return;...
} |
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 participant.displays.forEach(function(display)(e) {
...
display.dispose();}).on(constants.SFU_ROOM_EVENT.REMOVE_TRACKS, async function (e) {
})meetingModel.removeTracks(e.info.userId, e.info.info);
delete remoteParticipants[e.name];
...
});
...
}); |
...
3.4. Обработка события TRACK_QUALITY_STATE
initRemoteDisplaycreateDefaultMeetingController() 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_STATE, async function (e) {
consolemeetingModel.log("Received track quality state"updateQualityInfo(e.info.userId, e.info.tracks);
const participant = remoteParticipants[e.info.nickName];...
if (!participant) {});
return;
}
for (const rTrack of e.info.tracks) {
const mid = rTrack.mid...
} |
3.5. Обработка события ENDED
createDefaultMeetingController() code
Code Block |
---|
|
const createDefaultMeetingController = function (room, meetingModel) {
const constants = SFU.constants;
for (let i = 0; i < participant.displays.length; i++) {
const display = participant.displays[i];
if (display.videoMid === mid) {
display.updateQualityInfo(rTrack.quality);
break;
}
}
}
}); |
3. Создание элементов для отображения потоков участников
3.1. Создание контейнера
createRemoteDisplay() code
Здесь:
- настраиваются параметры отображения
- создается контейнер для потоков участника
- создается контейнер для конкретного потока
- создается контейнер для кнопок переключения качества
Code Block |
---|
|
const cell = document.createElement("div");
cell.setAttribute("class", "text-center");
cell.id = id;
mainDiv.appendChild(cell);
let publisherNameDisplay;
let currentQualityDisplay;
let videoTypeDisplay;
let abrQualityCheckPeriod = ABR_QUALITY_CHECK_PERIOD;
let abrKeepOnGoodQuality = ABR_KEEP_ON_QUALITY;
let abrTryForUpperQuality = ABR_TRY_UPPER_QUALITY;
if (displayOptions.abrQualityCheckPeriod !== undefined) {
abrQualityCheckPeriod = displayOptions.abrQualityCheckPeriod;
}
if (displayOptions.abrKeepOnGoodQuality !== undefined) {
abrKeepOnGoodQuality = displayOptions.abrKeepOnGoodQuality;
}
if (displayOptions.abrTryForUpperQuality !== undefined) {
abrTryForUpperQuality = displayOptions.abrTryForUpperQuality;
}
if (!displayOptions.abr) {
abrQualityCheckPeriod = 0;
abrKeepOnGoodQuality = 0;
abrTryForUpperQuality = 0;
}
if (displayOptions.publisher) {
publisherNameDisplay = createInfoDisplay(cell, "Published by: " + name);
}
if (displayOptions.quality) {
currentQualityDisplay = createInfoDisplay(cell, "");
}
if (displayOptions.type) {
videoTypeDisplay = createInfoDisplay(cell, "");
}
const qualitySwitchDisplay = createInfoDisplay(cell, "");
let qualityDivs = [];
let contentType = "";
const rootDisplay = createContainer(cell);
const streamDisplay = createContainer(rootDisplay);
const audioDisplay = createContainer(rootDisplay);
const audioTypeDisplay = createInfoDisplay(audioDisplay);
const audioTrackDisplay = createContainer(audioDisplay);
const audioStateButton = AudioStateButton();
hideItem(streamDisplay);
hideItem(audioDisplay);
hideItem(publisherNameDisplay);
hideItem(currentQualityDisplay);
hideItem(videoTypeDisplay);
hideItem(qualitySwitchDisplay); |
3.2. Инициализация ABR
createRemoteDisplay() code
Здесь задаются параметры качества получения потока от сервера, по которым отображение будет переключаться на более высокое или более низкое качество
Code Block |
---|
|
const abr = ABR(abrQualityCheckPeriod, [
{parameter: "nackCount", maxLeap: 10},
{parameter: "freezeCount", maxLeap: 10},
{parameter: "packetsLost", maxLeap: 10}
], abrKeepOnGoodQuality, abrTryForUpperQuality); |
3.3. Добавление видео элемента
setVideo() code
Code Block |
---|
|
setVideo: function(stream) {
if (video) {
video.remove();
}
if (stream == null) {
video = null;
this.videoMid = undefined;
qualityDivs.forEach(function(div) {
div.remove();
});
qualityDivs = [];
return;
}
showItem(streamDisplay);
video = document.createElement("video");
video.controls = "controls";
video.muted = true;
video.autoplay = true;
if (Browser().isSafariWebRTC()) {
video.setAttribute("playsinline", "");
video.setAttribute("webkit-playsinline", "");
this.setWebkitEventHandlers(video);
} else {
this.setEventHandlers(video);
}
streamDisplay.appendChild(video);
video.srcObject = stream;
this.setResizeHandler(video);
abr.start();
}, |
3.4. Добавление аудио элемента
setAudio() code
Code Block |
---|
|
setAudio: function(stream) {
if (audio) {
audio.remove();
}
if (!stream) {
audio = null;
this.audioMid = undefined;
return;
}
showItem(audioDisplay);
audio = document.createElement("audio");
audio.controls = "controls";
audio.muted = true;
audio.autoplay = true;
if (Browser().isSafariWebRTC()) {
audio.setAttribute("playsinline", "");
audio.setAttribute("webkit-playsinline", "");
this.setWebkitEventHandlers(audio);
} else {
this.setEventHandlers(audio);
}
audioTrackDisplay.appendChild(audio);
audioStateButton.makeButton(audioTypeDisplay, audio);
audio.srcObject = stream;
audio.onloadedmetadata = function (e) {
audio.play().then(function() {
if (Browser().isSafariWebRTC() && Browser().isiOS()) {
console.warn("Audio track should be manually unmuted in iOS Safari");
} else {
audio.muted = false;
audioStateButton.setButtonState();
}
});
};
}, |
3.5. Настройка обработчиков событий для аудио и видео элементов
setResizeHandler(), setEventHandlers(), setWebkitEventHandlers() code
Code Block |
---|
|
setEventHandlers: function(video) {
// Ignore play/pause button
video.addEventListener("pause", function () {
console.log("Media paused by click, continue...");
video.play();
});
},
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;
});
}, |
3.6. Добавление информации о дорожке в ABR
setVideoABRTrack() code
Code Block |
---|
|
setVideoABRTrack: function(track) {
abr.setTrack(track);
}, |
3.7. Настройка переключения качества
setTrackInfo() code
Здесь:
- настраиваются кнопки переключения качества
- информация о заявленных качествах публикации добавляется в ABR
- скрываются или отображаются элементы для показа текущего качества и источника видео/аудио
Code Block |
---|
|
setTrackInfo: function(trackInfo) {
if (trackInfo) {
if (trackInfo.quality) {
showItem(qualitySwitchDisplay);
if (abr.isEnabled()) {
const autoDiv = createQualityButton("Auto", qualityDivs, qualitySwitchDisplay);
autoDiv.style.color = QUALITY_COLORS.SELECTED;
autoDiv.addEventListener('click', function() {
setQualityButtonsColor(qualityDivs);
autoDiv.style.color = QUALITY_COLORS.SELECTED;
abr.setAuto();
});
}
for (let i = 0; i < trackInfo.quality.length; i++) {
abr.addQuality(trackInfo.quality[i]);
const qualityDiv = createQualityButton(trackInfo.quality[i], qualityDivs, qualitySwitchDisplay);
qualityDiv.addEventListener('click', function() {
console.log("Clicked on quality " + trackInfo.quality[i] + " trackId " + trackInfo.id);
if (qualityDiv.style.color === QUALITY_COLORS.UNAVAILABLE) {
return;
}
setQualityButtonsColor(qualityDivs);
qualityDiv.style.color = QUALITY_COLORS.SELECTED;
abr.setManual();
abr.setQuality(trackInfo.quality[i]);
});
}
} else {
hideItem(qualitySwitchDisplay);
}
if (trackInfo.type) {
contentType = trackInfo.contentType || "";
if (trackInfo.type == "VIDEO" && displayOptions.type && contentType !== "") {
showItem(videoTypeDisplay);room.on(constants.SFU_ROOM_EVENT.PARTICIPANT_LIST, async function (e) {
...
}).on(constants.SFU_ROOM_EVENT.ENDED, function (e) {
videoTypeDisplay.innerHTML = contentTypemeetingModel.end();
});
...
} |
3.6. Остановка комнаты
createDefaultMeetingController() code
Code Block |
---|
|
const createDefaultMeetingController = function (room, meetingModel) {
}...
const stop = function () {
meetingModel.end();
};
if (trackInfo.type == "AUDIO") return {
stop: stop
}
} |
4. Создание модели комнаты
createDefaultMeetingModel() code
Code Block |
---|
|
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
...
} |
4.1. Добавление участника
addParticipant() code
Code Block |
---|
|
const createDefaultMeetingModel = audioStateButton.setContentType(contentType);
function (meetingView, participantFactory, displayOptions, abrFactory) {
return {
...
addParticipant: function (userId, participantName) }{
if (this.participants.get(userId)) {
}
return;
}
}, |
3.8. Добавление информации о доступных качествах видео в ABR
updateQualityInfo() code
Code Block |
---|
|
const [participantModel, participantView, participant] = participantFactory.createParticipant(userId, participantName, displayOptions, abrFactory);
updateQualityInfo: function(videoQuality) {this.participants.set(userId, participant);
meetingView.addParticipant(userId, showItem(qualitySwitchDisplayparticipantName, participantView.rootDiv);
},
for (const qualityInfo of videoQuality) {
...
}
} |
4.2. Удаление участника
removeParticipant() code
Code Block |
---|
|
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
return {
let qualityColor = QUALITY_COLORS.UNAVAILABLE; ...
removeParticipant: if (qualityInfo.available === truefunction (userId) {
qualityColorconst participant = QUALITY_COLORS.AVAILABLEthis.participants.get(userId);
if }(participant) {
this.participants.delete(userId);
for (const qualityDiv of qualityDivs) {
meetingView.removeParticipant(userId);
if (qualityDiv.innerText === qualityInfo.quality){participant.dispose();
}
},
qualityDiv..style.color = qualityColor;
}
} |
4.3. Переименование участника
renameParticipant() code
Code Block |
---|
|
const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
return {
...
break;
renameParticipant: function (userId, newNickname) {
const participant }= this.participants.get(userId);
if }(participant) {
abr.setQualityAvailable(qualityInfo.quality, qualityInfo.availableparticipant.setNickname(newNickname);
}
},
...
}
}, |
...
4.
...
4. Добавление треков участника для отображения
addTracks() code
Code Block |
---|
|
const createDefaultMeetingModel = function (meetingView, participantFactory, dispose: function(displayOptions, abrFactory) {
return {
abr.stop();...
addTracks: function (userId, cell.remove();tracks) {
}, |
4. Подписка на добавление дорожек в WebRTC соединение
PeerConnection.ontrack(), setAudio(), setVideo(), setVideoABRTrack() code
Здесь:
- при получении видео или аудио потока, добавляется элемент для его проигрывания
Code Block |
---|
|
const peerConnection.ontrackparticipant = ({transceiver}) =>this.participants.get(userId);
if (!participant) {
let rParticipant return;
console.log("Attach remote track " + transceiver.receiver.track.id + " kind " + transceiver.receiver.track.kind + " mid " + transceiver.mid); }
for (const track of tracks) {
for (const [nickName, participant] of Object.entries(remoteParticipants)) if (track.type === "VIDEO") {
for (const pTrack of participant.tracksaddVideoTrack(track);
{
console.log("Participant " + participant.nickName + " track " + pTrack.id + " mid " + pTrack.mid} else if (track.type === "AUDIO") {
participant.addAudioTrack(track);
if (pTrack.mid === transceiver.mid) {
}
}
},
...
}
} |
4.5. Удаление отображаемых треков участника
removeTracks() code
Code Block |
---|
|
const createDefaultMeetingModel rParticipant = participant;
function (meetingView, participantFactory, displayOptions, abrFactory) {
return {
break;...
removeTracks: function (userId, tracks) {
}
const participant }= this.participants.get(userId);
if (rParticipant!participant) {
breakreturn;
}
}
if (rParticipant) {
for (const displaytrack of rParticipant.displays) {
if (transceiver.receiver.track.kind === "video"tracks) {
if (displaytrack.videoMidtype === transceiver.mid"VIDEO") {
participant.removeVideoTrack(track);
let stream = new MediaStream();
} else if (track.type === "AUDIO") {
streamparticipant.addTrackremoveAudioTrack(transceiver.receiver.track);
}
display.setVideoABRTrack(transceiver.receiver.track);}
},
...
}
} |
4.6. Обновление информации о качестве треков участника
updateQualityInfo() code
Code Block |
---|
|
const createDefaultMeetingModel = function display.setVideo(stream);(meetingView, participantFactory, displayOptions, abrFactory) {
return {
...
updateQualityInfo: function (userId, tracksInfo) break;{
const participant = this.participants.get(userId);
}
if (!participant) {
} else if (transceiver.receiver.track.kind === "audio") { return;
}
if (display.audioMid === transceiverparticipant.mid) {updateQualityInfo(tracksInfo);
},
...
let stream}
} |
4.7. Завершение отображения участников при остановке комнаты
end() code
Code Block |
---|
|
const createDefaultMeetingModel = newfunction MediaStream();
meetingView, participantFactory, displayOptions, abrFactory) {
return {
stream.addTrack(transceiver.receiver.track);
end: function () {
displayconsole.setAudio(stream);
log("Meeting " + this.meetingName + " ended")
breakmeetingView.end();
this.participants.forEach((participant, id) }=> {
}participant.dispose();
});
} else {
console.warn("Failed to find participant for track " + transceiverthis.receiver.track.idparticipants.clear();
},
...
}
} |
5. Остановка воспроизведения
...
4.8. Переименование комнаты
setMeetingName() code
Code Block |
---|
|
const createDefaultMeetingModel = function const stop = function((meetingView, participantFactory, displayOptions, abrFactory) {
return {
for (const [nickName, participant] of Object.entries(remoteParticipants)) {...
setMeetingName: function participant.displays.forEach(function(display)(id) {
this.meetingName display.dispose()= id;
}meetingView.setMeetingName(id);
delete remoteParticipants[nickName];
}
}
} |