diff --git a/src/app/Http/Controllers/API/V4/OpenViduController.php b/src/app/Http/Controllers/API/V4/OpenViduController.php --- a/src/app/Http/Controllers/API/V4/OpenViduController.php +++ b/src/app/Http/Controllers/API/V4/OpenViduController.php @@ -429,6 +429,22 @@ break; + case 'language': + // Only the moderator can do it + if (!$this->isModerator($connection->room)) { + return $this->errorResponse(403); + } + + if ($value) { + if (preg_match('/^[a-z]{2}$/', $value)) { + $connection->metadata = ['language' => $value] + $connection->metadata; + } + } else { + $connection->metadata = array_diff_key($connection->metadata, ['language' => 0]); + } + + break; + case 'role': // Only the moderator can do it if (!$this->isModerator($connection->room)) { @@ -452,6 +468,11 @@ $connection->metadata = array_diff_key($connection->metadata, ['hand' => 0]); } + // Non-publisher cannot be a language interpreter + if (!($value & Room::ROLE_PUBLISHER)) { + $connection->metadata = array_diff_key($connection->metadata, ['language' => 0]); + } + $connection->{$key} = $value; break; } diff --git a/src/app/Observers/OpenVidu/ConnectionObserver.php b/src/app/Observers/OpenVidu/ConnectionObserver.php --- a/src/app/Observers/OpenVidu/ConnectionObserver.php +++ b/src/app/Observers/OpenVidu/ConnectionObserver.php @@ -26,12 +26,19 @@ // participant browser to do this. } - // Rised hand state change - $newState = $connection->metadata['hand'] ?? null; - $oldState = $this->getOriginal($connection, 'metadata')['hand'] ?? null; + // Detect metadata changes for specified properties + $keys = [ + 'hand' => 'bool', + 'language' => '', + ]; - if ($newState !== $oldState) { - $params['hand'] = !empty($newState); + foreach ($keys as $key => $type) { + $newState = $connection->metadata[$key] ?? null; + $oldState = $this->getOriginal($connection, 'metadata')[$key] ?? null; + + if ($newState !== $oldState) { + $params[$key] = $type == 'bool' ? !empty($newState) : $newState; + } } // Send the signal to all participants diff --git a/src/app/OpenVidu/Room.php b/src/app/OpenVidu/Room.php --- a/src/app/OpenVidu/Room.php +++ b/src/app/OpenVidu/Room.php @@ -194,6 +194,7 @@ return [ 'role' => $item->role, 'hand' => $item->metadata['hand'] ?? 0, + 'language' => $item->metadata['language'] ?? null, ]; }) // Sort by order in the queue, so UI can re-build the existing queue in order diff --git a/src/resources/js/meet/app.js b/src/resources/js/meet/app.js --- a/src/resources/js/meet/app.js +++ b/src/resources/js/meet/app.js @@ -62,6 +62,7 @@ this.setupSetAudioDevice = setupSetAudioDevice this.setupSetVideoDevice = setupSetVideoDevice this.switchAudio = switchAudio + this.switchChannel = switchChannel this.switchScreen = switchScreen this.switchVideo = switchVideo this.updateSession = updateSession @@ -91,6 +92,8 @@ * 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, * menuElement - DOM element of the room toolbar, * queueElement - DOM element for the Q&A queue (users with a raised hand) @@ -117,6 +120,9 @@ subscribersContainer = $('
').appendTo(container).get(0) } + // TODO: Make sure all supported callbacks exist, so we don't have to check + // their existence everywhere anymore + sessionData = data // Init a session @@ -201,6 +207,8 @@ if (session.connection.connectionId == connectionId) { metadata = sessionData + metadata.audioActive = audioActive + metadata.videoActive = videoActive } if (metadata) { @@ -259,11 +267,19 @@ // Here we expect connections in a proper queue order Object.keys(data.connections || {}).forEach(key => { let conn = data.connections[key] + if (conn.hand) { conn.connectionId = key connectionHandUp(conn) } }) + + sessionData.channels = getChannels(data.connections) + + // Inform the vue component, so it can update some UI controls + if (sessionData.channels.length && sessionData.onSessionDataUpdate) { + sessionData.onSessionDataUpdate(sessionData) + } }) .catch(error => { console.error('There was an error connecting to the session: ', error.message); @@ -710,6 +726,18 @@ } /** + * 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 */ function switchAudio() { @@ -785,7 +813,7 @@ */ 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) { @@ -803,13 +831,6 @@ const isPublisher = sessionData.role & Roles.PUBLISHER const isModerator = sessionData.role & Roles.MODERATOR - // Inform the vue component, so it can update some UI controls - let update = () => { - if (sessionData.onSessionDataUpdate) { - sessionData.onSessionDataUpdate(data) - } - } - // demoted to a subscriber if ('role' in data && isPublisher && !rolePublisher) { session.unpublish(publisher) @@ -823,20 +844,15 @@ handUpdate(sessionData) // merge the changed data into internal session metadata object - Object.keys(data).forEach(key => { sessionData[key] = data[key] }) + 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) { - if ((!isModerator && roleModerator) || (isModerator && !roleModerator)) { - // Update all participants, to enable/disable the popup menu - Object.keys(connections).forEach(key => { - const conn = connections[key] - participantUpdate(conn.element, conn) - }) - } + // Update all participants, to enable/disable the popup menu + refresh = (!isModerator && roleModerator) || (isModerator && !roleModerator) } // Inform the vue component, so it can update some UI controls @@ -846,9 +862,12 @@ if ('role' in data && !isPublisher && rolePublisher) { publisher.createVideoElement(sessionData.element, 'PREPEND') session.publish(publisher).then(() => { - data.audioActive = publisher.stream.audioActive - data.videoActive = publisher.stream.videoActive - update() + sessionData.audioActive = publisher.stream.audioActive + sessionData.videoActive = publisher.stream.videoActive + + if (sessionData.onSessionDataUpdate) { + sessionData.onSessionDataUpdate(sessionData) + } }) // Open the media setup dialog @@ -873,6 +892,24 @@ 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 + if (sessionData.onSessionDataUpdate) { + sessionData.onSessionDataUpdate(sessionData) + } } /** @@ -928,19 +965,22 @@ * 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 params Connection metadata/params + * @param content Optional content to prepend to the element * * @return The element */ - function participantCreate(params) { + function participantCreate(params, content) { let element params.isSelf = params.isSelf || session.connection.connectionId == params.connectionId - if (params.role & Roles.PUBLISHER || params.role & Roles.SCREEN) { - element = publisherCreate(params) + if ((!params.language && params.role & Roles.PUBLISHER) || params.role & Roles.SCREEN) { + // publishers and shared screens + element = publisherCreate(params, content) } else { - element = subscriberCreate(params) + // subscribers and language interpreters + element = subscriberCreate(params, content) } setTimeout(resize, 50); @@ -951,9 +991,10 @@ /** * Create a