...
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.
...
Participant view object creation for one-to-many meeting room
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.
...
Adding video track to play
addVideoTrack() code
Code Block | ||||
---|---|---|---|---|
| ||||
const createOneToManyParticipantView = function () { ... return { ... addVideoTrack: function (track, requestVideoTrack) { player.addVideoTrack(track, async () => { return requestVideoTrack(); }); }, ... } } |
11.2.
...
Removing video track playing
removeVideoTrack() code
Code Block | ||||
---|---|---|---|---|
| ||||
const createOneToManyParticipantView = function () { ... return { ... removeVideoTrack: function (track) { player.removeVideoTrack(track); }, ... } } |
11.3.
...
Adding media source to video tag
addVideoSource() code
Code Block | ||||
---|---|---|---|---|
| ||||
const createOneToManyParticipantView = function () { ... return { ... addVideoSource: function (remoteVideoTrack, track, onResize, muteHandler) { this.currentTrack = track; player.setVideoSource(remoteVideoTrack, onResize, muteHandler); }, ... } } |
11.4.
...
Removing media source from video tag
removeVideoSource() code
Code Block | ||||
---|---|---|---|---|
| ||||
const createOneToManyParticipantView = function () { ... return { ... removeVideoSource: function (track) { if (this.currentTrack && this.currentTrack.mid === track.mid) { player.removeVideoSource(); } }, ... } } |
11.5.
...
Show video track
showVideoTrack() code
Code Block | ||||
---|---|---|---|---|
| ||||
const createOneToManyParticipantView = function () { ... return { ... showVideoTrack: function (track) { player.showVideoTrack(track); }, ... } } |
11.6.
...
Adding audio track
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.
...
Removing audio track
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.
...
Set participant nickname to display
setNickname() code
Code Block | ||||
---|---|---|---|---|
| ||||
const createOneToManyParticipantView = function () { ... return { ... setNickname: function (userId, nickname) { const additionalUserId = userId ? "#" + getShortUserId(userId) : ""; participantNicknameDisplay.innerText = "Name: " + nickname + additionalUserId; }, ... } } |
11.9.
...
Update track quality info
updateQuality() code
Code Block | ||||
---|---|---|---|---|
| ||||
const createOneToManyParticipantView = function () { ... return { ... updateQuality: function (track, qualityName, available) { player.updateQuality(qualityName, available); }, ... } } |
11.10.
...
Add track quality info
addQuality() code
Code Block | ||||
---|---|---|---|---|
| ||||
const createOneToManyParticipantView = function () { ... return { ... addQuality: function (track, qualityName, available, onQualityPick) { player.addQuality(qualityName, available, onQualityPick); }, ... } } |
11.11.
...
Pick a quality to display
pickQuality() code
Code Block | ||||
---|---|---|---|---|
| ||||
const createOneToManyParticipantView = function () { ... return { ... pickQuality: function (track, qualityName) { player.pickQuality(qualityName); } ... } } |
11.12.
...
Clear quality state displayed
clearQualityState() code
Code Block | ||||
---|---|---|---|---|
| ||||
const createOneToManyParticipantView = function () { ... return { ... clearQualityState: function (track) { player.clearQualityState(); }, ... } } |
11.13.
...
Dispose the object
dispose() code
Code Block | ||||
---|---|---|---|---|
| ||||
const createOneToManyParticipantView = function () { ... return { ... dispose: function () { player.dispose(); for (const element of audioElements.values()) { element.remove(); } audioElements.clear(); }, ... } } |
12.
...
Video player object creation
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 and unlock player buttons to wait for asynchronous operations
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 event handles set up
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.
...
Other browsers event handlers set up
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.
...
Change quality buttons colors when picking a quality
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.
...
Remove quality buttons
clearQualityState() code
Code Block | ||||
---|---|---|---|---|
| ||||
const createVideoPlayer = function (participantDiv) { ... return { ... clearQualityState: function () { qualityButtons.forEach((state, qName) => { state.btn.remove(); }); qualityButtons.clear(); }, ... } } |
12.6.
...
Adding a video track to play
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.
...
Removing video track played
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.
...
Adding a video tag and setting video track as a source
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.
...
Removing the video tag
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.
...
Video tag and track info displaying
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.
...
Update available auality info
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.
...
Add quality button
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.
...
Handle a quality button click
pickQuality() code
Code Block | ||||
---|---|---|---|---|
| ||||
const createVideoPlayer = function (participantDiv) { ... return { ... pickQuality: function (qualityName) { repickQuality(qualityName); } ... } } |
12.14.
...
Dispose the object
dispose() code
Code Block | ||||
---|---|---|---|---|
| ||||
const createVideoPlayer = function (participantDiv) { ... return { ... dispose: function () { streamDisplay.remove(); }, ... } } |
13.
...
Get a track to display from the meeting room
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.
...
Helper functions
14.1.
...
Re-scale video to player video tag size
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.
...
Create a tag to display a textual info
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.
...
Create a container tag
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.
...
Show and hide the tag on the page
showItem(), hideItem() code
...