diff --git a/src/resources/js/meet/client.js b/src/resources/js/meet/client.js index a3851e6c..664bcfb7 100644 --- a/src/resources/js/meet/client.js +++ b/src/resources/js/meet/client.js @@ -1,517 +1,575 @@ 'use strict' import { Device, parseScalabilityMode } from 'mediasoup-client' import Config from './config.js' import { Media } from './media.js' import { Socket } from './socket.js' function Client() { let eventHandlers = {} let camProducer let micProducer let screenProducer let consumers = {} let socket let sendTransport let recvTransport let turnServers = [] let nickname = '' let peers = {} let videoSource let audioSource const VIDEO_CONSTRAINTS = { 'low': { width: { ideal: 320 } }, 'medium': { width: { ideal: 640 } }, 'high': { width: { ideal: 1280 } }, 'veryhigh': { width: { ideal: 1920 } }, 'ultra': { width: { ideal: 3840 } } } // Create a device (use browser auto-detection) const device = new Device() // A helper for basic browser media operations const media = new Media() this.media = media navigator.mediaDevices.addEventListener('devicechange', () => { trigger('deviceChange') }) /** * Start a session */ this.startSession = (token, props) => { // Initialize the socket, 'roomReady' request handler will do the rest of the job socket = initSocket(token) nickname = props.nickname videoSource = props.videoSource audioSource = props.audioSource } /** * Close the session (disconnect) */ this.closeSession = () => { if (socket) { socket.close() } // Close mediasoup Transports. if (sendTransport) { sendTransport.close() sendTransport = null } if (recvTransport) { recvTransport.close() recvTransport = null } } this.camMute = async () => { - camProducer.pause() + if (camProducer) { + camProducer.pause() + await socket.sendRequest('pauseProducer', { producerId: camProducer.id }) + trigger('updatePeer', updatePeerState(peers.self)) + } - await socket.sendRequest('pauseProducer', { producerId: camProducer.id }) + return this.camStatus() } this.camUnmute = async () => { if (camProducer) { camProducer.resume() + await socket.sendRequest('resumeProducer', { producerId: camProducer.id }) + trigger('updatePeer', updatePeerState(peers.self)) } - await socket.sendRequest('resumeProducer', { producerId: camProducer.id }) + return this.camStatus() + } + + this.camStatus = () => { + return camProducer && !camProducer.paused && !camProducer.closed } this.micMute = async () => { if (micProducer) { micProducer.pause() + await socket.sendRequest('pauseProducer', { producerId: micProducer.id }) + trigger('updatePeer', updatePeerState(peers.self)) } - await socket.sendRequest('pauseProducer', { producerId: micProducer.id }) + return this.micStatus() } this.micUnmute = async () => { if (micProducer) { micProducer.resume() + await socket.sendRequest('resumeProducer', { producerId: micProducer.id }) + trigger('updatePeer', updatePeerState(peers.self)) } - await socket.sendRequest('resumeProducer', { producerId: micProducer.id }) + return this.micStatus() + } + + this.micStatus = () => { + return micProducer && !micProducer.paused && !micProducer.closed } /** * Register event handlers */ this.on = (eventName, callback) => { eventHandlers[eventName] = callback } /** * Execute an event handler */ const trigger = (...args) => { const eventName = args.shift() if (eventName in eventHandlers) { eventHandlers[eventName].apply(null, args) } } const initSocket = (token) => { // Connect to websocket socket = new Socket(token) socket.on('disconnect', reason => { // this.closeSession() }) socket.on('reconnectFailed', () => { // this.closeSession() }) socket.on('request', async (request, cb) => { switch (request.method) { case 'newConsumer': const { peerId, producerId, id, kind, rtpParameters, type, appData, producerPaused } = request.data const consumer = await recvTransport.consume({ id, producerId, kind, rtpParameters }) consumer.peerId = peerId consumer.on('transportclose', () => { // TODO: What actually else needs to be done here? delete consumers[consumer.id] }) consumers[consumer.id] = consumer // We are ready. Answer the request so the server will // resume this Consumer (which was paused for now). cb(null) let peer = peers[peerId] if (!peer) { return } let tracks = (peer.tracks || []).filter(track => track.kind != kind) tracks.push(consumer.track) setPeerTracks(peer, tracks) peers[peerId] = peer trigger('updatePeer', peer) break default: console.error('Unknow request method: ' + request.method) } }) socket.on('notification', async (notification) => { switch (notification.method) { case 'roomReady': turnServers = notification.data.turnServers joinRoom() return case 'newPeer': peers[notification.data.id] = notification.data trigger('addPeer', notification.data) return case 'peerClosed': const { peerId } = notification.data delete peers[peerId] trigger('removePeer', peerId) return case 'consumerClosed': { const { consumerId } = notification.data const consumer = consumers[consumerId] if (!consumer) { - break + return } consumer.close() delete consumers[consumerId] let peer = peers[consumer.peerId] if (peer) { // TODO: Update peer state, remove track trigger('updatePeer', peer) } return } + case 'consumerPaused': + case 'consumerResumed': { + const { consumerId } = notification.data + const consumer = consumers[consumerId] + + if (!consumer) { + return + } + + consumer[notification.method == 'consumerPaused' : 'pause' : 'resume']() + + let peer = peers[consumer.peerId] + + if (peer) { + trigger('updatePeer', updatePeerState(peer)) + } + + return + } + case 'changeNickname': { const { peerId, nickname } = notification.data const peer = peers[peerId] if (!peer) { - break + return } peer.nickname = nickname trigger('updatePeer', peer) return } default: console.error('Unknow notification method: ' + notification.method) return } trigger('signal', notification.method, notification.data) }) return socket } const joinRoom = async () => { const routerRtpCapabilities = await socket.getRtpCapabilities() routerRtpCapabilities.headerExtensions = routerRtpCapabilities.headerExtensions .filter(ext => ext.uri !== 'urn:3gpp:video-orientation') await device.load({ routerRtpCapabilities }) const iceTransportPolicy = (device.handlerName.toLowerCase().includes('firefox') && turnServers) ? 'relay' : undefined; // Setup 'producer' transport if (videoSource || audioSource) { const transportInfo = await socket.sendRequest('createWebRtcTransport', { forceTcp: false, producing: true, consuming: false }) const { id, iceParameters, iceCandidates, dtlsParameters } = transportInfo sendTransport = device.createSendTransport({ id, iceParameters, iceCandidates, dtlsParameters, iceServers: turnServers, iceTransportPolicy: iceTransportPolicy, proprietaryConstraints: { optional: [{ googDscp: true }] } }) sendTransport.on('connect', ({ dtlsParameters }, callback, errback) => { socket.sendRequest('connectWebRtcTransport', { transportId: sendTransport.id, dtlsParameters }) .then(callback) .catch(errback) }) sendTransport.on('produce', async ({ kind, rtpParameters, appData }, callback, errback) => { try { const { id } = await socket.sendRequest('produce', { transportId: sendTransport.id, kind, rtpParameters, appData }) callback({ id }) } catch (error) { errback(error) } }) } // Setup 'consumer' transport const transportInfo = await socket.sendRequest('createWebRtcTransport', { forceTcp: false, producing: false, consuming: true }) const { id, iceParameters, iceCandidates, dtlsParameters } = transportInfo recvTransport = device.createRecvTransport({ id, iceParameters, iceCandidates, dtlsParameters, iceServers: turnServers, iceTransportPolicy: iceTransportPolicy }) recvTransport.on('connect', ({ dtlsParameters }, callback, errback) => { socket.sendRequest('connectWebRtcTransport', { transportId: recvTransport.id, dtlsParameters }) .then(callback) .catch(errback) }) // Send the "join" request, get room data, participants, etc. const { peers: existing, role, id: peerId } = await socket.sendRequest('join', { nickname: nickname, rtpCapabilities: device.rtpCapabilities }) trigger('joinSuccess') let peer = { id: peerId, role, isSelf: true, nickname, audioActive: !!audioSource, videoActive: !!videoSource } // Start publishing webcam if (videoSource) { await setCamera(videoSource) // Create the video element peer.videoElement = media.createVideoElement([ camProducer.track ], { mirror: true }) } // Start publishing microphone if (audioSource) { setMic(audioSource) // Note: We're not adding this track to the video element } trigger('addPeer', peer) // Add self to the list peers.self = peer console.log(existing) // Trigger addPeer event for all peers already in the room, maintain peers list existing.forEach(peer => { let tracks = [] // We receive newConsumer requests before we add the peer to peers list, // therefore we look here for any consumers that belong to this peer and update // the peer. If we do not do this we have to wait about 20 seconds for repeated // newConsumer requests Object.keys(consumers).forEach(cid => { if (consumers[cid].peerId === peer.id) { tracks.push(consumers[cid].track) } }) if (tracks.length) { setPeerTracks(peer, tracks) - } else { - peer.audioActive = false - peer.videoActive = false } trigger('addPeer', peer) peers[peer.id] = peer }) } const setCamera = async (deviceId) => { if (!device.canProduce('video')) { throw new Error('cannot produce video') } const { aspectRatio, frameRate, resolution } = Config.videoOptions const track = await media.getTrack({ video: { deviceId: { ideal: deviceId }, ...VIDEO_CONSTRAINTS[resolution], frameRate } }) // TODO: Simulcast support? camProducer = await sendTransport.produce({ track, appData: { source : 'webcam' } }) camProducer.on('transportclose', () => { camProducer = null }) camProducer.on('trackended', () => { // disableWebcam() }) } const setMic = async (deviceId) => { if (!device.canProduce('audio')) { throw new Error('cannot produce audio') } const { autoGainControl, echoCancellation, noiseSuppression, sampleRate, channelCount, volume, sampleSize, opusStereo, opusDtx, opusFec, opusPtime, opusMaxPlaybackRate } = Config.audioOptions const track = await media.getTrack({ audio: { sampleRate, channelCount, volume, autoGainControl, echoCancellation, noiseSuppression, sampleSize, deviceId: { ideal: deviceId } } }) micProducer = await sendTransport.produce({ track, codecOptions: { opusStereo, opusDtx, opusFec, opusPtime, opusMaxPlaybackRate }, appData: { source : 'mic' } }) micProducer.on('transportclose', () => { micProducer = null }) micProducer.on('trackended', () => { // disableMic() }) } const setPeerTracks = (peer, tracks) => { if (!peer.videoElement) { peer.videoElement = media.createVideoElement(tracks, {}) } else { const stream = new MediaStream() tracks.forEach(track => stream.addTrack(track)) peer.videoElement.srcObject = stream } - peer.videoActive = true // TODO - peer.audioActive = true // TODO + peer = updatePeerState(peer) + peer.tracks = tracks peers[peer.id] = peer } + + const updatePeerState = (peer) => { + if (peer.isSelf) { + peer.videoActive = this.camStatus() + peer.audioActive = this.micStatus() + peers.self = peer + } else { + peer.videoActive = false + peer.audioActive = false + + Object.keys(consumers).forEach(cid => { + const consumer = consumers[cid] + + if (consumer.peerId == peer.id) { + peer[consumer.kind + 'Active'] = !consumer.paused && !consumer.closed && !consumer.producerPaused + } + }) + + peers[peer.id] = peer + } + + return peer + } } export { Client } diff --git a/src/resources/js/meet/room.js b/src/resources/js/meet/room.js index 28c62479..10060cc5 100644 --- a/src/resources/js/meet/room.js +++ b/src/resources/js/meet/room.js @@ -1,1381 +1,1367 @@ 'use strict' import anchorme from 'anchorme' import { Client } from './client.js' import { Dropdown } from 'bootstrap' import { library } from '@fortawesome/fontawesome-svg-core' class Roles { static get SUBSCRIBER() { return 1 << 0; } static get PUBLISHER() { return 1 << 1; } static get MODERATOR() { return 1 << 2; } static get SCREEN() { return 1 << 3; } static get OWNER() { return 1 << 4; } } function Room(container) { let session // Session object where the user will connect let publisher // Publisher object which the user will publish let sessionData // Room session metadata 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 connections = {} // Connected users in the session let peers = {} let chatCount = 0 let publishersContainer let subscribersContainer let scrollStop let $t const client = new Client() // Disconnect participant when browser's window close window.addEventListener('beforeunload', () => { leaveRoom() }) window.addEventListener('resize', resize) // Public methods this.isScreenSharingSupported = isScreenSharingSupported this.joinRoom = joinRoom this.leaveRoom = leaveRoom this.setupStart = setupStart this.setupStop = setupStop this.setupSetAudioDevice = setupSetAudioDevice this.setupSetVideoDevice = setupSetVideoDevice this.switchAudio = switchAudio this.switchChannel = switchChannel this.switchScreen = switchScreen this.switchVideo = switchVideo this.updateSession = updateSession /** * Join the room session * * @param data Session metadata and event handlers: * token - A token for the main connection, * shareToken - A token for screen-sharing connection, * nickname - Participant name, * role - connection (participant) role(s), * connections - Optional metadata for other users connections (current state), * channel - Selected interpreted language channel (two-letter language code) * languages - Supported languages (code-to-label map) * chatElement - DOM element for the chat widget, * counterElement - DOM element for the participants counter, * menuElement - DOM element of the room toolbar, * queueElement - DOM element for the Q&A queue (users with a raised hand) * onSuccess - Callback for session connection (join) success * onError - Callback for session connection (join) error * onDestroy - Callback for session disconnection event, * onDismiss - Callback for Dismiss action, * onJoinRequest - Callback for join request, * onConnectionChange - Callback for participant changes, e.g. role update, * onSessionDataUpdate - Callback for current user connection update, * onMediaSetup - Called when user clicks the Media setup button * translate - Translation function */ function joinRoom(data) { // Create a container for subscribers and publishers publishersContainer = $('
').appendTo(container).get(0) subscribersContainer = $('
').appendTo(container).get(0) resize(); $t = data.translate // Make sure all supported callbacks exist, so we don't have to check // their existence everywhere anymore let events = ['Success', 'Error', 'Destroy', 'Dismiss', 'JoinRequest', 'ConnectionChange', 'SessionDataUpdate', 'MediaSetup'] events.map(event => 'on' + event).forEach(event => { if (!data[event]) { data[event] = () => {} } }) sessionData = data // Participant added (including self) client.on('addPeer', (event) => { console.log('addPeer', event) event.role = Roles.PUBLISHER // TODO event.element = participantCreate(event) if (event.videoElement) { $(event.element).prepend(event.videoElement) } peers[event.id] = event }) // Participant removed client.on('removePeer', (peerId) => { console.log('removePeer', peerId) let peer = peers[peerId] if (peer) { // Remove elements related to the participant connectionHandDown(peerId) $(peer.element).remove() delete peers[peerId] } resize() }) // Participant properties changed e.g. audio/video muted/unmuted client.on('updatePeer', (event) => { console.log('updatePeer', event) let peer = peers[event.id] if (!peer) { return } if (event.videoElement && event.videoElement.parentNode != peer.element) { $(peer.element).prepend(event.videoElement) } else if (!event.videoElement) { $(peer.element).find('video').remove() } // TODO: update peer properties participantUpdate(peer.element, event) peers[event.id] = peer }) // Handle signals from the server (and other participants) client.on('signal', signalEventHandler) client.on('joinSuccess', () => { data.onSuccess() }) /* // Handle session disconnection events client.on('sessionDisconnected', event => { data.onDestroy(event) client = null resize() }) */ const { audioSource, videoSource } = client.media.setupData() // Start the session client.startSession(data.token, { videoSource, audioSource, nickname: data.nickname }) // Prepare the chat setupChat() } /** * Leave the room (disconnect) */ function leaveRoom() { /* if (publisher) { // Release any media let mediaStream = publisher.stream.getMediaStream() if (mediaStream) { mediaStream.getTracks().forEach(track => track.stop()) } publisher = null } if (session) { session.disconnect(); session = null } if (screenSession) { screenSession.disconnect(); screenSession = null } */ } /** * Sets the audio and video devices for the session. * This will ask user for permission to access media devices. * * @param props Setup properties (videoElement, volumeElement, onSuccess, onError) */ function setupStart(props) { client.media.setupStart(props) } /** * Stop the setup "process", cleanup after it. */ function setupStop() { client.media.setupStop() } /** * Change the publisher audio device * * @param deviceId Device identifier string */ async function setupSetAudioDevice(deviceId) { return await client.media.setupSetAudio(deviceId) } /** * Change the publisher video device * * @param deviceId Device identifier string */ async function setupSetVideoDevice(deviceId) { return await client.media.setupSetVideo(deviceId) } /** * Setup the chat UI */ function setupChat() { // The UI elements are created in the vue template // Here we add a logic for how they work const chat = $(sessionData.chatElement).find('.chat').get(0) const textarea = $(sessionData.chatElement).find('textarea') const button = $(sessionData.menuElement).find('.link-chat') textarea.on('keydown', e => { if (e.keyCode == 13 && !e.shiftKey) { if (textarea.val().length) { signalChat(textarea.val()) textarea.val('') } return false } }) // Add an element for the count of unread messages on the chat button button.append('') .on('click', () => { button.find('.badge').text('') chatCount = 0 // When opening the chat scroll it to the bottom, or we shouldn't? scrollStop = false chat.scrollTop = chat.scrollHeight }) $(chat).on('scroll', event => { // Detect manual scrollbar moves, disable auto-scrolling until // the scrollbar is positioned on the element bottom again scrollStop = chat.scrollTop + chat.offsetHeight < chat.scrollHeight }) } /** * Signal events handler */ function signalEventHandler(signal) { let conn, data let connId = signal.from ? signal.from.connectionId : null switch (signal.type) { case 'signal:userChanged': // TODO: Use 'signal:connectionUpdate' for nickname updates? if (conn = connections[connId]) { data = JSON.parse(signal.data) conn.nickname = data.nickname participantUpdate(conn.element, conn) nicknameUpdate(data.nickname, connId) } break case 'signal:chat': data = JSON.parse(signal.data) data.id = connId pushChatMessage(data) break case 'signal:joinRequest': // accept requests from the server only if (!connId) { sessionData.onJoinRequest(JSON.parse(signal.data)) } break case 'signal:connectionUpdate': // accept requests from the server only if (!connId) { data = JSON.parse(signal.data) connectionUpdate(data) } break } } /** * Send the chat message to other participants * * @param message Message string */ function signalChat(message) { let data = { nickname: sessionData.params.nickname, message } // TODO /* session.signal({ data: JSON.stringify(data), type: 'chat' }) */ } /** * Add a message to the chat * * @param data Object with a message, nickname, id (of the connection, empty for self) */ function pushChatMessage(data) { let message = $('').text(data.message).text() // make the message secure // Format the message, convert emails and urls to links message = anchorme({ input: message, options: { attributes: { target: "_blank" }, // any link above 20 characters will be truncated // to 20 characters and ellipses at the end truncate: 20, // characters will be taken out of the middle middleTruncation: true } // TODO: anchorme is extensible, we could support // github/phabricator's markup e.g. backticks for code samples }) message = message.replace(/\r?\n/, '
') // Display the message let isSelf = false // TODO let chat = $(sessionData.chatElement).find('.chat') let box = chat.find('.message').last() message = $('
').html(message) message.find('a').attr('rel', 'noreferrer') if (box.length && box.data('id') == data.id) { // A message from the same user as the last message, no new box needed message.appendTo(box) } else { box = $('
').data('id', data.id) .append($('
').text(data.nickname || '')) .append(message) .appendTo(chat) if (isSelf) { box.addClass('self') } } // Count unread messages if (!$(sessionData.chatElement).is('.open')) { if (!isSelf) { chatCount++ } } else { chatCount = 0 } $(sessionData.menuElement).find('.link-chat .badge').text(chatCount ? chatCount : '') // Scroll the chat element to the end if (!scrollStop) { chat.get(0).scrollTop = chat.get(0).scrollHeight } } /** * Send the user properties update signal to other participants * * @param connection Optional connection to which the signal will be sent * If not specified the signal is sent to all participants */ function signalUserUpdate(connection) { let data = { nickname: sessionData.params.nickname } session.signal({ data: JSON.stringify(data), type: 'userChanged', to: connection ? [connection] : undefined }) // The same nickname for screen sharing session if (screenSession) { screenSession.signal({ data: JSON.stringify(data), type: 'userChanged', to: connection ? [connection] : undefined }) } } /** * Switch interpreted language channel * * @param channel Two-letter language code */ function switchChannel(channel) { sessionData.channel = channel // Mute/unmute all connections depending on the selected channel participantUpdateAll() } /** * Mute/Unmute audio for current session publisher */ async function switchAudio() { -/* - if (microphones.length) { - if (audioActive) { - await client.micUnmute() - } else { - await client.micMute() - } + const isActive = client.micStatus() - audioActive = !audioActive + if (isActive) { + return await client.micMute() + } else { + return await client.micUnmute() } -*/ - return audioActive } /** * Mute/Unmute video for current session publisher */ async function switchVideo() { - // TODO: If user has no devices or denied access to them in the setup, - // the button will just not work. Find a way to make it working - // after user unlocks his devices. For now he has to refresh - // the page and join the room again. -/* - if (cameras.length) { - if (videoActive) { - await client.camUnmute() - } else { - await client.camMute() - } + const isActive = client.camStatus() - videoActive = !videoActive + if (isActive) { + return await client.camMute() + } else { + return await client.camUnmute() } -*/ - return videoActive } /** * Switch on/off screen sharing */ function switchScreen(callback) { if (screenPublisher) { // Note: This is what the original openvidu-call app does. // It is probably better for performance reasons to close the connection, // than to use unpublish() and keep the connection open. screenSession.disconnect() screenSession = null screenPublisher = null if (callback) { // Note: Disconnecting invalidates the token, we have to inform the vue component // to update UI state (and be prepared to request a new token). callback(false) } return } screenConnect(callback) } /** * Detect if screen sharing is supported by the browser */ function isScreenSharingSupported() { // TODO: Implement detection for screen sharing support in the browser return true; } /** * Update participant connection state */ function connectionUpdate(data) { let conn = connections[data.connectionId] let refresh = false let handUpdate = conn => { if ('hand' in data && data.hand != conn.hand) { if (data.hand) { connectionHandUp(conn) } else { connectionHandDown(data.connectionId) } } } // It's me if (session.connection.connectionId == data.connectionId) { const rolePublisher = data.role && data.role & Roles.PUBLISHER const roleModerator = data.role && data.role & Roles.MODERATOR const isPublisher = sessionData.role & Roles.PUBLISHER const isModerator = sessionData.role & Roles.MODERATOR // demoted to a subscriber if ('role' in data && isPublisher && !rolePublisher) { session.unpublish(publisher) // FIXME: There's a reference in OpenVidu to a video element that should not // exist anymore. It causes issues when we try to do publish/unpublish // sequence multiple times in a row. So, we're clearing the reference here. let videos = publisher.stream.streamManager.videos publisher.stream.streamManager.videos = videos.filter(video => video.video.parentNode != null) } handUpdate(sessionData) // merge the changed data into internal session metadata object sessionData = Object.assign({}, sessionData, data, { audioActive, videoActive }) // update the participant element sessionData.element = participantUpdate(sessionData.element, sessionData) // promoted/demoted to/from a moderator if ('role' in data) { // Update all participants, to enable/disable the popup menu refresh = (!isModerator && roleModerator) || (isModerator && !roleModerator) } // promoted to a publisher if ('role' in data && !isPublisher && rolePublisher) { publisher.createVideoElement(sessionData.element, 'PREPEND') session.publish(publisher).then(() => { sessionData.audioActive = publisher.stream.audioActive sessionData.videoActive = publisher.stream.videoActive sessionData.onSessionDataUpdate(sessionData) }) // Open the media setup dialog // Note: If user didn't give permission to media before joining the room // he will not be able to use them now. Changing permissions requires // a page refresh. // Note: In Firefox I'm always being asked again for media permissions. // It does not happen in Chrome. In Chrome the cam/mic will be just re-used. // I.e. streaming starts automatically. // It might make sense to not start streaming automatically in any cirmustances, // display the dialog and wait until user closes it, but this would be // a bigger refactoring. sessionData.onMediaSetup() } } else if (conn) { handUpdate(conn) // merge the changed data into internal session metadata object Object.keys(data).forEach(key => { conn[key] = data[key] }) conn.element = participantUpdate(conn.element, conn) } // Update channels list sessionData.channels = getChannels(connections) // The channel user was using has been removed (or rather the participant stopped being an interpreter) if (sessionData.channel && !sessionData.channels.includes(sessionData.channel)) { sessionData.channel = null refresh = true } if (refresh) { participantUpdateAll() } // Inform the vue component, so it can update some UI controls sessionData.onSessionDataUpdate(sessionData) } /** * Handler for Hand-Up "signal" */ function connectionHandUp(connection) { connection.isSelf = session.connection.connectionId == connection.connectionId let element = $(nicknameWidget(connection)) participantUpdate(element, connection) element.attr('id', 'qa' + connection.connectionId) .appendTo($(sessionData.queueElement).show()) setTimeout(() => element.addClass('widdle'), 50) } /** * Handler for Hand-Down "signal" */ function connectionHandDown(connectionId) { let list = $(sessionData.queueElement) list.find('#qa' + connectionId).remove(); if (!list.find('.meet-nickname').length) { list.hide(); } } /** * Update participant nickname in the UI * * @param nickname Nickname * @param connectionId Connection identifier of the user */ function nicknameUpdate(nickname, connectionId) { if (connectionId) { $(sessionData.chatElement).find('.chat').find('.message').each(function() { let elem = $(this) if (elem.data('id') == connectionId) { elem.find('.nickname').text(nickname || '') } }) $(sessionData.queueElement).find('#qa' + connectionId + ' .content').text(nickname || '') } } /** * Create a participant element in the matrix. Depending on the connection role * parameter it will be a video element wrapper inside the matrix or a simple * tag-like element on the subscribers list. * * @param params Connection metadata/params * @param content Optional content to prepend to the element * * @return The element */ function participantCreate(params, content) { let element if ((!params.language && params.role & Roles.PUBLISHER) || params.role & Roles.SCREEN) { // publishers and shared screens element = publisherCreate(params, content) } else { // subscribers and language interpreters element = subscriberCreate(params, content) } setTimeout(resize, 50); return element } /** * Create a