diff --git a/src/resources/js/meet/app.js b/src/resources/js/meet/app.js index 49e6f4aa..b08c088a 100644 --- a/src/resources/js/meet/app.js +++ b/src/resources/js/meet/app.js @@ -1,336 +1,408 @@ import { OpenVidu } from 'openvidu-browser' function Meet(container) { let OV // OpenVidu object to initialize a session let session // Session object where the user will connect let publisher // Publisher object which the user will publish let sessionId // Unique identifier of the session let audioEnabled = true // True if the audio track of publisher is active let videoEnabled = true // True if the video track of publisher is active let numOfVideos = 0 // Keeps track of the number of videos that are being shown let audioSource = '' // Currently selected microphone let videoSource = '' // Currently selected camera - let sessionData + let sessionData // Room session metadata let screenOV // OpenVidu object to initialize a screen sharing session let screenSession // Session object where the user will connect for screen sharing let screenPublisher // Publisher object which the user will publish the screen sharing let publisherDefaults = { publishAudio: true, // Whether to start publishing with your audio unmuted or not publishVideo: true, // Whether to start publishing with your video enabled or not resolution: '640x480', // The resolution of your video frameRate: 30, // The frame rate of your video mirror: true // Whether to mirror your local video or not } let cameras = [] // List of user video devices let microphones = [] // List of user audio devices + let containerWidth + let containerHeight + OV = new OpenVidu() screenOV = new OpenVidu() // if there's anything to do, do it here. //OV.setAdvancedConfiguration(config) // Disconnect participant on browser's window closed -/* window.addEventListener('beforeunload', () => { - if (session) session.disconnect(); + leaveRoom() }) -*/ + + window.addEventListener('resize', resize) // Public methods this.isScreenSharingSupported = isScreenSharingSupported this.joinRoom = joinRoom this.leaveRoom = leaveRoom this.setup = setup this.setupSetAudioDevice = setupSetAudioDevice this.setupSetVideoDevice = setupSetVideoDevice this.switchAudio = switchAudio this.switchScreen = switchScreen this.switchVideo = switchVideo function setup(videoElement, success_callback, error_callback) { publisher = OV.initPublisher(null, publisherDefaults) publisher.once('accessDenied', error => { error_callback(error) }) publisher.once('accessAllowed', async () => { let mediaStream = publisher.stream.getMediaStream() let videoStream = mediaStream.getVideoTracks()[0] let audioStream = mediaStream.getAudioTracks()[0] audioEnabled = !!audioStream videoEnabled = !!videoStream publisher.addVideoElement(videoElement) const devices = await OV.getDevices() devices.forEach(device => { // device's props: deviceId, kind, label if (device.kind == 'videoinput') { cameras.push(device) if (videoStream && videoStream.label == device.label) { videoSource = device.deviceId } } else if (device.kind == 'audioinput') { microphones.push(device) if (audioStream && audioStream.label == device.label) { audioSource = device.deviceId } } }) success_callback({ microphones, cameras, audioSource, videoSource, audioEnabled, videoEnabled }) }) } async function setupSetAudioDevice(deviceId) { if (!deviceId) { publisher.publishAudio(false) audioEnabled = false } else if (deviceId == audioSource) { publisher.publishAudio(true) audioEnabled = true } else { /* let mediaStream = publisher.stream.getMediaStream() let audioStream = mediaStream.getAudioTracks()[0] audioStream.stop() publisher = OV.initPublisher(null, properties); publisher.addVideoElement(videoElement) */ // FIXME: None of this is working let properties = Object.assign({}, publisherDefaults, { publishAudio: true, publishVideo: videoEnabled, audioSource: deviceId, videoSource: videoSource }) await OV.getUserMedia(properties) .then(async (mediaStream) => { const track = mediaStream.getAudioTracks()[0] await publisher.replaceTrack(track) audioEnabled = true }) } return audioEnabled } function setupSetVideoDevice(deviceId) { if (!deviceId) { publisher.publishVideo(false) videoEnabled = false } else if (deviceId == videoSource) { publisher.publishVideo(true) videoEnabled = true } else { // TODO } return videoEnabled } /** * Join the room session * * @param data Session metadata (session, token, shareToken) */ function joinRoom(data) { + resize(); + // TODO data.params = { clientData: 'Test', // user nickname avatar: undefined // avatar image } sessionData = data sessionId = data.session // Init a session session = OV.initSession() // On every new Stream received... session.on('streamCreated', function (event) { // Subscribe to the Stream to receive it let subscriber = session.subscribe(event.stream, addVideoWrapper(container)); - // When the new video is added to DOM, update the page layout to fit one more participant + // When the new video is added to DOM, update the page layout subscriber.on('videoElementCreated', (event) => { numOfVideos++ updateLayout() }) - }) + // When a video is removed from DOM, update the page layout + subscriber.on('videoElementDestroyed', (event) => { + numOfVideos-- + updateLayout() + }) + }) +/* // On every new Stream destroyed... session.on('streamDestroyed', (event) => { // Update the page layout numOfVideos-- updateLayout() }) - +*/ // Connect with the token session.connect(data.token, data.params) .then(() => { - publisher.createVideoElement(addVideoWrapper(container), 'PREPEND') - // When our HTML video has been added to DOM... + let wrapper = addVideoWrapper(container, 'publisher') + publisher.on('videoElementCreated', (event) => { - $(event.element).addClass('publisher') - .prop('muted', true) // Mute local video to avoid feedback + $(event.element).prop('muted', true) // Mute local video to avoid feedback // When your own video is added to DOM, update the page layout to fit it numOfVideos++ updateLayout() }) + publisher.createVideoElement(wrapper, 'PREPEND') + // Publish the stream session.publish(publisher) }) .catch(error => { console.error('There was an error connecting to the session:', error.code, error.message); }) } /** * Leave the room (disconnect) */ function leaveRoom() { if (session) { session.disconnect(); } if (screenSession) { screenSession.disconnect(); } } /** * Mute/Unmute audio for current session publisher */ function switchAudio() { audioEnabled = !audioEnabled publisher.publishAudio(audioEnabled) return audioEnabled } /** * Mute/Unmute video for current session publisher */ function switchVideo() { videoEnabled = !videoEnabled publisher.publishVideo(videoEnabled) return videoEnabled } /** * Switch on/off screen sharing */ function switchScreen(callback) { if (screenPublisher) { screenSession.unpublish(screenPublisher) screenPublisher = null if (callback) { callback(false) } return } screenConnect(callback) } function isScreenSharingSupported() { return !!OV.checkScreenSharingCapabilities(); } - function updateLayout() { - // update the "matrix" layout + function addVideoWrapper(container, className) { + return $('
').addClass(className || '') + .appendTo(container).get(0) + } + + /** + * Window onresize event handler (updates room layout) + */ + function resize() { + containerWidth = container.offsetWidth + containerHeight = container.offsetHeight + updateLayout() } - function addVideoWrapper(container) { - return $('
').appendTo(container).get(0) + /** + * Update the room "matrix" layout + */ + function updateLayout() { + if (!numOfVideos) { + return + } + + let css, rows, cols, height + + if (numOfVideos == 1) { + cols = 1 + } else if (numOfVideos <= 4) { + cols = 2 + } else if (numOfVideos <= 9) { + cols = 3 + } else if (numOfVideos <= 16) { + cols = 4 + } else if (numOfVideos <= 25) { + cols = 5 + } else { + cols = 6 + } + + rows = Math.ceil(numOfVideos / cols) + + if (rows < cols && containerWidth < containerHeight) { + cols = rows + rows = Math.ceil(numOfVideos / cols) + } + + height = containerHeight / rows + css = { + width: (100 / cols) + '%', + // Height must be in pixels to make object-fit:cover working + height: height + 'px' + } + + // Update the matrix + $(container).find('.meet-video').css(css) + .each((idx, elem) => { + let video = $(elem).children('video')[0] + if (!video) { + // Remove orphaned video wrappers (after video has been removed) + $(elem).remove() + } else if (video.videoWidth && video.videoHeight && video.videoWidth > video.videoHeight) { + // Set max-width to keep the original aspect ratio in cases + // when there's enough room to display the element + let maxWidth = height * video.videoWidth / video.videoHeight + $(elem).css('max-width', maxWidth) + } + }) } /** * Initialize screen sharing session/publisher */ function screenConnect(callback) { if (!sessionData.shareToken) { return false } let gotSession = !!screenSession // Init screen sharing session if (!gotSession) { screenSession = screenOV.initSession(); } let successFunc = function() { screenSession.publish(screenPublisher) if (callback) { callback(true) } } let errorFunc = function() { screenPublisher = null if (callback) { callback(false) } } // Init the publisher let params = { videoSource: 'screen', publishAudio: false } screenPublisher = screenOV.initPublisher(null, params) screenPublisher.once('accessAllowed', (event) => { if (gotSession) { successFunc() } else { screenSession.connect(sessionData.shareToken, sessionData.params) .then(() => { successFunc() }) .catch(error => { console.error('There was an error connecting to the session:', error.code, error.message); errorFunc() }) } }) screenPublisher.once('accessDenied', () => { console.info('ScreenShare: Access Denied') errorFunc() }) } } export default Meet diff --git a/src/resources/sass/meet.scss b/src/resources/sass/meet.scss index 9ae1c129..66a5ce60 100644 --- a/src/resources/sass/meet.scss +++ b/src/resources/sass/meet.scss @@ -1,52 +1,62 @@ +#app.meet { + height: 100%; + + #meet-component { + overflow: hidden; + } +} + #meet-component { flex-grow: 1; + display: flex; + flex-direction: column; & + .filler { display: none; } } #meet-session-toolbar { display: flex; justify-content: center; } #meet-session-menu { button { font-size: 1.3rem; padding: 0.5rem 1rem; } } #meet-session { display: flex; justify-content: center; flex-wrap: wrap; + flex: 1; + + overflow: hidden; } .meet-video { position: relative; - max-hwight: 480px; - max-width: 640px; - // Note: To make object-fit:cover working we have to set - // height in px on the wrapper element - video { + // To make object-fit:cover working we have to set the height in pixels + // on the wrapper element. This is what javascript method will do. object-fit: cover; width: 100%; height: 100%; } } #meet-setup { } #setup-preview { display: flex; video { width: 100%; transform: rotateY(180deg); background: black; } } diff --git a/src/resources/vue/Meet/Room.vue b/src/resources/vue/Meet/Room.vue index 1d3618c0..b02abdcf 100644 --- a/src/resources/vue/Meet/Room.vue +++ b/src/resources/vue/Meet/Room.vue @@ -1,155 +1,156 @@