Changeset View
Changeset View
Standalone View
Standalone View
src/resources/js/meet/app.js
Show First 20 Lines • Show All 56 Lines • ▼ Show 20 Lines | function Meet(container) | ||||
this.isScreenSharingSupported = isScreenSharingSupported | this.isScreenSharingSupported = isScreenSharingSupported | ||||
this.joinRoom = joinRoom | this.joinRoom = joinRoom | ||||
this.leaveRoom = leaveRoom | this.leaveRoom = leaveRoom | ||||
this.setupStart = setupStart | this.setupStart = setupStart | ||||
this.setupStop = setupStop | this.setupStop = setupStop | ||||
this.setupSetAudioDevice = setupSetAudioDevice | this.setupSetAudioDevice = setupSetAudioDevice | ||||
this.setupSetVideoDevice = setupSetVideoDevice | this.setupSetVideoDevice = setupSetVideoDevice | ||||
this.switchAudio = switchAudio | this.switchAudio = switchAudio | ||||
this.switchChannel = switchChannel | |||||
this.switchScreen = switchScreen | this.switchScreen = switchScreen | ||||
this.switchVideo = switchVideo | this.switchVideo = switchVideo | ||||
this.updateSession = updateSession | this.updateSession = updateSession | ||||
/** | /** | ||||
* Initialize OpenVidu instance | * Initialize OpenVidu instance | ||||
*/ | */ | ||||
function ovInit() | function ovInit() | ||||
Show All 13 Lines | function Meet(container) | ||||
* Join the room session | * Join the room session | ||||
* | * | ||||
* @param data Session metadata and event handlers: | * @param data Session metadata and event handlers: | ||||
* token - OpenVidu token for the main connection, | * token - OpenVidu token for the main connection, | ||||
* shareToken - OpenVidu token for screen-sharing connection, | * shareToken - OpenVidu token for screen-sharing connection, | ||||
* nickname - Participant name, | * nickname - Participant name, | ||||
* role - connection (participant) role(s), | * role - connection (participant) role(s), | ||||
* connections - Optional metadata for other users connections (current state), | * 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, | * chatElement - DOM element for the chat widget, | ||||
* menuElement - DOM element of the room toolbar, | * menuElement - DOM element of the room toolbar, | ||||
* queueElement - DOM element for the Q&A queue (users with a raised hand) | * queueElement - DOM element for the Q&A queue (users with a raised hand) | ||||
* onSuccess - Callback for session connection (join) success | * onSuccess - Callback for session connection (join) success | ||||
* onError - Callback for session connection (join) error | * onError - Callback for session connection (join) error | ||||
* onDestroy - Callback for session disconnection event, | * onDestroy - Callback for session disconnection event, | ||||
* onDismiss - Callback for Dismiss action, | * onDismiss - Callback for Dismiss action, | ||||
* onJoinRequest - Callback for join request, | * onJoinRequest - Callback for join request, | ||||
Show All 10 Lines | function joinRoom(data) { | ||||
// avatar: undefined // avatar image | // avatar: undefined // avatar image | ||||
} | } | ||||
// Create a container for subscribers | // Create a container for subscribers | ||||
if (!subscribersContainer) { | if (!subscribersContainer) { | ||||
subscribersContainer = $('<div id="meet-subscribers">').appendTo(container).get(0) | subscribersContainer = $('<div id="meet-subscribers">').appendTo(container).get(0) | ||||
} | } | ||||
// TODO: Make sure all supported callbacks exist, so we don't have to check | |||||
// their existence everywhere anymore | |||||
sessionData = data | sessionData = data | ||||
// Init a session | // Init a session | ||||
session = OV.initSession() | session = OV.initSession() | ||||
// Handle connection creation events | // Handle connection creation events | ||||
session.on('connectionCreated', event => { | session.on('connectionCreated', event => { | ||||
// Ignore the current user connection | // Ignore the current user connection | ||||
▲ Show 20 Lines • Show All 68 Lines • ▼ Show 20 Lines | function joinRoom(data) { | ||||
// Stream properties changes e.g. audio/video muted/unmuted | // Stream properties changes e.g. audio/video muted/unmuted | ||||
session.on('streamPropertyChanged', event => { | session.on('streamPropertyChanged', event => { | ||||
let connectionId = event.stream.connection.connectionId | let connectionId = event.stream.connection.connectionId | ||||
let metadata = connections[connectionId] | let metadata = connections[connectionId] | ||||
if (session.connection.connectionId == connectionId) { | if (session.connection.connectionId == connectionId) { | ||||
metadata = sessionData | metadata = sessionData | ||||
metadata.audioActive = audioActive | |||||
metadata.videoActive = videoActive | |||||
} | } | ||||
if (metadata) { | if (metadata) { | ||||
metadata[event.changedProperty] = event.newValue | metadata[event.changedProperty] = event.newValue | ||||
participantUpdate(metadata.element, metadata) | participantUpdate(metadata.element, metadata) | ||||
} | } | ||||
}) | }) | ||||
▲ Show 20 Lines • Show All 42 Lines • ▼ Show 20 Lines | function joinRoom(data) { | ||||
} | } | ||||
sessionData.element = wrapper | sessionData.element = wrapper | ||||
// Create Q&A queue from the existing connections with rised hand. | // Create Q&A queue from the existing connections with rised hand. | ||||
// Here we expect connections in a proper queue order | // Here we expect connections in a proper queue order | ||||
Object.keys(data.connections || {}).forEach(key => { | Object.keys(data.connections || {}).forEach(key => { | ||||
let conn = data.connections[key] | let conn = data.connections[key] | ||||
if (conn.hand) { | if (conn.hand) { | ||||
conn.connectionId = key | conn.connectionId = key | ||||
connectionHandUp(conn) | 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 => { | .catch(error => { | ||||
console.error('There was an error connecting to the session: ', error.message); | console.error('There was an error connecting to the session: ', error.message); | ||||
if (data.onError) { | if (data.onError) { | ||||
data.onError(error) | data.onError(error) | ||||
} | } | ||||
}) | }) | ||||
▲ Show 20 Lines • Show All 430 Lines • ▼ Show 20 Lines | function signalUserUpdate(connection) { | ||||
data: JSON.stringify(data), | data: JSON.stringify(data), | ||||
type: 'userChanged', | type: 'userChanged', | ||||
to: connection ? [connection] : undefined | 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 | * Mute/Unmute audio for current session publisher | ||||
*/ | */ | ||||
function switchAudio() { | function switchAudio() { | ||||
// TODO: If user has no devices or denied access to them in the setup, | // 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 | // the button will just not work. Find a way to make it working | ||||
// after user unlocks his devices. For now he has to refresh | // after user unlocks his devices. For now he has to refresh | ||||
// the page and join the room again. | // the page and join the room again. | ||||
if (microphones.length) { | if (microphones.length) { | ||||
▲ Show 20 Lines • Show All 59 Lines • ▼ Show 20 Lines | function isScreenSharingSupported() { | ||||
return !!OV.checkScreenSharingCapabilities(); | return !!OV.checkScreenSharingCapabilities(); | ||||
} | } | ||||
/** | /** | ||||
* Update participant connection state | * Update participant connection state | ||||
*/ | */ | ||||
function connectionUpdate(data) { | function connectionUpdate(data) { | ||||
let conn = connections[data.connectionId] | let conn = connections[data.connectionId] | ||||
let refresh = false | |||||
let handUpdate = conn => { | let handUpdate = conn => { | ||||
if ('hand' in data && data.hand != conn.hand) { | if ('hand' in data && data.hand != conn.hand) { | ||||
if (data.hand) { | if (data.hand) { | ||||
connectionHandUp(conn) | connectionHandUp(conn) | ||||
} else { | } else { | ||||
connectionHandDown(data.connectionId) | connectionHandDown(data.connectionId) | ||||
} | } | ||||
} | } | ||||
} | } | ||||
// It's me | // It's me | ||||
if (session.connection.connectionId == data.connectionId) { | if (session.connection.connectionId == data.connectionId) { | ||||
const rolePublisher = data.role && data.role & Roles.PUBLISHER | const rolePublisher = data.role && data.role & Roles.PUBLISHER | ||||
const roleModerator = data.role && data.role & Roles.MODERATOR | const roleModerator = data.role && data.role & Roles.MODERATOR | ||||
const isPublisher = sessionData.role & Roles.PUBLISHER | const isPublisher = sessionData.role & Roles.PUBLISHER | ||||
const isModerator = sessionData.role & Roles.MODERATOR | 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 | // demoted to a subscriber | ||||
if ('role' in data && isPublisher && !rolePublisher) { | if ('role' in data && isPublisher && !rolePublisher) { | ||||
session.unpublish(publisher) | session.unpublish(publisher) | ||||
// FIXME: There's a reference in OpenVidu to a video element that should not | // 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 | // 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. | // sequence multiple times in a row. So, we're clearing the reference here. | ||||
let videos = publisher.stream.streamManager.videos | let videos = publisher.stream.streamManager.videos | ||||
publisher.stream.streamManager.videos = videos.filter(video => video.video.parentNode != null) | publisher.stream.streamManager.videos = videos.filter(video => video.video.parentNode != null) | ||||
} | } | ||||
handUpdate(sessionData) | handUpdate(sessionData) | ||||
// merge the changed data into internal session metadata object | // 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 | // update the participant element | ||||
sessionData.element = participantUpdate(sessionData.element, sessionData) | sessionData.element = participantUpdate(sessionData.element, sessionData) | ||||
// promoted/demoted to/from a moderator | // promoted/demoted to/from a moderator | ||||
if ('role' in data) { | if ('role' in data) { | ||||
if ((!isModerator && roleModerator) || (isModerator && !roleModerator)) { | |||||
// Update all participants, to enable/disable the popup menu | // Update all participants, to enable/disable the popup menu | ||||
Object.keys(connections).forEach(key => { | refresh = (!isModerator && roleModerator) || (isModerator && !roleModerator) | ||||
const conn = connections[key] | |||||
participantUpdate(conn.element, conn) | |||||
}) | |||||
} | |||||
} | } | ||||
// Inform the vue component, so it can update some UI controls | // Inform the vue component, so it can update some UI controls | ||||
update() | update() | ||||
// promoted to a publisher | // promoted to a publisher | ||||
if ('role' in data && !isPublisher && rolePublisher) { | if ('role' in data && !isPublisher && rolePublisher) { | ||||
publisher.createVideoElement(sessionData.element, 'PREPEND') | publisher.createVideoElement(sessionData.element, 'PREPEND') | ||||
session.publish(publisher).then(() => { | session.publish(publisher).then(() => { | ||||
data.audioActive = publisher.stream.audioActive | sessionData.audioActive = publisher.stream.audioActive | ||||
data.videoActive = publisher.stream.videoActive | sessionData.videoActive = publisher.stream.videoActive | ||||
update() | |||||
if (sessionData.onSessionDataUpdate) { | |||||
sessionData.onSessionDataUpdate(sessionData) | |||||
} | |||||
}) | }) | ||||
// Open the media setup dialog | // Open the media setup dialog | ||||
// Note: If user didn't give permission to media before joining the room | // 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 | // he will not be able to use them now. Changing permissions requires | ||||
// a page refresh. | // a page refresh. | ||||
// Note: In Firefox I'm always being asked again for media permissions. | // 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. | // It does not happen in Chrome. In Chrome the cam/mic will be just re-used. | ||||
// I.e. streaming starts automatically. | // I.e. streaming starts automatically. | ||||
// It might make sense to not start streaming automatically in any cirmustances, | // 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 | // display the dialog and wait until user closes it, but this would be | ||||
// a bigger refactoring. | // a bigger refactoring. | ||||
if (sessionData.onMediaSetup) { | if (sessionData.onMediaSetup) { | ||||
sessionData.onMediaSetup() | sessionData.onMediaSetup() | ||||
} | } | ||||
} | } | ||||
} else if (conn) { | } else if (conn) { | ||||
handUpdate(conn) | handUpdate(conn) | ||||
// merge the changed data into internal session metadata object | // merge the changed data into internal session metadata object | ||||
Object.keys(data).forEach(key => { conn[key] = data[key] }) | Object.keys(data).forEach(key => { conn[key] = data[key] }) | ||||
conn.element = participantUpdate(conn.element, conn) | 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) | |||||
} | |||||
} | } | ||||
/** | /** | ||||
* Handler for Hand-Up "signal" | * Handler for Hand-Up "signal" | ||||
*/ | */ | ||||
function connectionHandUp(connection) { | function connectionHandUp(connection) { | ||||
connection.isSelf = session.connection.connectionId == connection.connectionId | connection.isSelf = session.connection.connectionId == connection.connectionId | ||||
Show All 39 Lines | function nicknameUpdate(nickname, connectionId) { | ||||
} | } | ||||
} | } | ||||
/** | /** | ||||
* Create a participant element in the matrix. Depending on the connection role | * 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 | * parameter it will be a video element wrapper inside the matrix or a simple | ||||
* tag-like element on the subscribers list. | * 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 | * @return The element | ||||
*/ | */ | ||||
function participantCreate(params) { | function participantCreate(params, content) { | ||||
let element | let element | ||||
params.isSelf = params.isSelf || session.connection.connectionId == params.connectionId | params.isSelf = params.isSelf || session.connection.connectionId == params.connectionId | ||||
if (params.role & Roles.PUBLISHER || params.role & Roles.SCREEN) { | if ((!params.language && params.role & Roles.PUBLISHER) || params.role & Roles.SCREEN) { | ||||
element = publisherCreate(params) | // publishers and shared screens | ||||
element = publisherCreate(params, content) | |||||
} else { | } else { | ||||
element = subscriberCreate(params) | // subscribers and language interpreters | ||||
element = subscriberCreate(params, content) | |||||
} | } | ||||
setTimeout(resize, 50); | setTimeout(resize, 50); | ||||
return element | return element | ||||
} | } | ||||
/** | /** | ||||
* Create a <video> element wrapper with controls | * Create a <video> element wrapper with controls | ||||
* | * | ||||
* @param params Connection metadata/params | * @param params Connection metadata/params | ||||
* @param content Optional content to prepend to the element | |||||
*/ | */ | ||||
function publisherCreate(params) { | function publisherCreate(params, content) { | ||||
// Create the element | // Create the element | ||||
let wrapper = $( | let wrapper = $( | ||||
'<div class="meet-video">' | '<div class="meet-video">' | ||||
+ svgIcon('user', 'fas', 'watermark') | + svgIcon('user', 'fas', 'watermark') | ||||
+ '<div class="controls">' | + '<div class="controls">' | ||||
+ '<button type="button" class="btn btn-link link-setup hidden" title="Media setup">' + svgIcon('cog') + '</button>' | + '<button type="button" class="btn btn-link link-setup hidden" title="Media setup">' + svgIcon('cog') + '</button>' | ||||
+ '<button type="button" class="btn btn-link link-audio hidden" title="Mute audio">' + svgIcon('volume-mute') + '</button>' | + '<button type="button" class="btn btn-link link-audio hidden" title="Mute audio">' + svgIcon('volume-mute') + '</button>' | ||||
+ '<button type="button" class="btn btn-link link-fullscreen closed hidden" title="Full screen">' + svgIcon('expand') + '</button>' | + '<button type="button" class="btn btn-link link-fullscreen closed hidden" title="Full screen">' + svgIcon('expand') + '</button>' | ||||
+ '<button type="button" class="btn btn-link link-fullscreen open hidden" title="Full screen">' + svgIcon('compress') + '</button>' | + '<button type="button" class="btn btn-link link-fullscreen open hidden" title="Full screen">' + svgIcon('compress') + '</button>' | ||||
+ '</div>' | + '</div>' | ||||
+ '<div class="status">' | + '<div class="status">' | ||||
+ '<span class="bg-danger status-audio hidden">' + svgIcon('microphone') + '</span>' | + '<span class="bg-danger status-audio hidden">' + svgIcon('microphone') + '</span>' | ||||
+ '<span class="bg-danger status-video hidden">' + svgIcon('video') + '</span>' | + '<span class="bg-danger status-video hidden">' + svgIcon('video') + '</span>' | ||||
+ '</div>' | + '</div>' | ||||
+ '</div>' | + '</div>' | ||||
) | ) | ||||
// Append the nickname widget | // Append the nickname widget | ||||
wrapper.find('.controls').before(nicknameWidget(params)) | wrapper.find('.controls').before(nicknameWidget(params)) | ||||
if (content) { | |||||
wrapper.prepend(content) | |||||
} | |||||
if (params.isSelf) { | if (params.isSelf) { | ||||
if (sessionData.onMediaSetup) { | if (sessionData.onMediaSetup) { | ||||
wrapper.find('.link-setup').removeClass('hidden') | wrapper.find('.link-setup').removeClass('hidden') | ||||
.click(() => sessionData.onMediaSetup()) | .click(() => sessionData.onMediaSetup()) | ||||
} | } | ||||
} else { | } else { | ||||
// Enable audio mute button | // Enable audio mute button | ||||
wrapper.find('.link-audio').removeClass('hidden') | wrapper.find('.link-audio').removeClass('hidden') | ||||
.on('click', e => { | .on('click', e => { | ||||
let video = wrapper.find('video')[0] | let video = wrapper.find('video')[0] | ||||
video.muted = !video.muted | video.muted = !video.muted | ||||
wrapper.find('.link-audio')[video.muted ? 'addClass' : 'removeClass']('text-danger') | wrapper.find('.link-audio')[video.muted ? 'addClass' : 'removeClass']('text-danger') | ||||
}) | }) | ||||
} | } | ||||
participantUpdate(wrapper, params) | participantUpdate(wrapper, params, true) | ||||
// Fullscreen control | // Fullscreen control | ||||
if (document.fullscreenEnabled) { | if (document.fullscreenEnabled) { | ||||
wrapper.find('.link-fullscreen.closed').removeClass('hidden') | wrapper.find('.link-fullscreen.closed').removeClass('hidden') | ||||
.on('click', () => { | .on('click', () => { | ||||
wrapper.get(0).requestFullscreen() | wrapper.get(0).requestFullscreen() | ||||
}) | }) | ||||
Show All 14 Lines | function publisherCreate(params, content) { | ||||
$('#subscriber-' + params.connectionId).remove() | $('#subscriber-' + params.connectionId).remove() | ||||
return wrapper[params.isSelf ? 'prependTo' : 'appendTo'](container) | return wrapper[params.isSelf ? 'prependTo' : 'appendTo'](container) | ||||
.attr('id', 'publisher-' + params.connectionId) | .attr('id', 'publisher-' + params.connectionId) | ||||
.get(0) | .get(0) | ||||
} | } | ||||
/** | /** | ||||
* Update the <video> wrapper controls | * Update the publisher/subscriber element controls | ||||
* | * | ||||
* @param wrapper The wrapper element | * @param wrapper The wrapper element | ||||
* @param params Connection metadata/params | * @param params Connection metadata/params | ||||
*/ | */ | ||||
function participantUpdate(wrapper, params) { | function participantUpdate(wrapper, params, noupdate) { | ||||
const element = $(wrapper) | const element = $(wrapper) | ||||
const isModerator = sessionData.role & Roles.MODERATOR | const isModerator = sessionData.role & Roles.MODERATOR | ||||
const isSelf = session.connection.connectionId == params.connectionId | const isSelf = session.connection.connectionId == params.connectionId | ||||
const rolePublisher = params.role & Roles.PUBLISHER | const rolePublisher = params.role & Roles.PUBLISHER | ||||
const roleModerator = params.role & Roles.MODERATOR | const roleModerator = params.role & Roles.MODERATOR | ||||
const roleScreen = params.role & Roles.SCREEN | const roleScreen = params.role & Roles.SCREEN | ||||
const roleOwner = params.role & Roles.OWNER | const roleOwner = params.role & Roles.OWNER | ||||
const roleInterpreter = rolePublisher && !!params.language | |||||
// Handle publisher-to-subscriber and subscriber-to-publisher change | if (!noupdate && !roleScreen) { | ||||
if (!roleScreen) { | |||||
const isPublisher = element.is('.meet-video') | const isPublisher = element.is('.meet-video') | ||||
if ((rolePublisher && !isPublisher) || (!rolePublisher && isPublisher)) { | // Publisher-to-interpreter or vice-versa, move element to the subscribers list or vice-versa, | ||||
// but keep the existing video element | |||||
if ( | |||||
!isSelf | |||||
&& element.find('video').length | |||||
&& ((roleInterpreter && isPublisher) || (!roleInterpreter && !isPublisher && rolePublisher)) | |||||
) { | |||||
wrapper = participantCreate(params, element.find('video')) | |||||
element.remove() | |||||
return wrapper | |||||
} | |||||
// Handle publisher-to-subscriber and subscriber-to-publisher change | |||||
if ( | |||||
!roleInterpreter | |||||
&& (rolePublisher && !isPublisher) || (!rolePublisher && isPublisher) | |||||
) { | |||||
element.remove() | element.remove() | ||||
return participantCreate(params) | return participantCreate(params) | ||||
} | } | ||||
} | } | ||||
if ('audioActive' in params) { | let muted = false | ||||
let video = element.find('video')[0] | |||||
// When a channel is selected - mute everyone except the interpreter of the language. | |||||
// When a channel is not selected - mute language interpreters only | |||||
if (sessionData.channel) { | |||||
muted = !(roleInterpreter && params.language == sessionData.channel) | |||||
} else { | |||||
muted = roleInterpreter | |||||
} | |||||
if (muted && !isSelf) { | |||||
element.find('.status-audio').removeClass('hidden') | |||||
element.find('.link-audio').addClass('hidden') | |||||
} else { | |||||
element.find('.status-audio')[params.audioActive ? 'addClass' : 'removeClass']('hidden') | element.find('.status-audio')[params.audioActive ? 'addClass' : 'removeClass']('hidden') | ||||
if (!isSelf) { | |||||
element.find('.link-audio').removeClass('hidden') | |||||
} | |||||
muted = !params.audioActive || isSelf | |||||
} | } | ||||
if ('videoActive' in params) { | |||||
element.find('.status-video')[params.videoActive ? 'addClass' : 'removeClass']('hidden') | element.find('.status-video')[params.videoActive ? 'addClass' : 'removeClass']('hidden') | ||||
if (video) { | |||||
video.muted = muted | |||||
} | } | ||||
if ('nickname' in params) { | if ('nickname' in params) { | ||||
element.find('.meet-nickname > .content').text(params.nickname) | element.find('.meet-nickname > .content').text(params.nickname) | ||||
} | } | ||||
if (isSelf) { | if (isSelf) { | ||||
element.addClass('self') | element.addClass('self') | ||||
} | } | ||||
if (isModerator) { | if (isModerator) { | ||||
element.addClass('moderated') | element.addClass('moderated') | ||||
} | } | ||||
const withPerm = isModerator && !roleScreen && !(roleOwner && !isSelf); | const withPerm = isModerator && !roleScreen && !(roleOwner && !isSelf); | ||||
const withMenu = isSelf || (isModerator && !roleOwner) | const withMenu = isSelf || (isModerator && !roleOwner) | ||||
// TODO: This probably could be better done with css | |||||
let elements = { | let elements = { | ||||
'.dropdown-menu': withMenu, | '.dropdown-menu': withMenu, | ||||
'.permissions': withPerm, | '.permissions': withPerm, | ||||
'.interpreting': withPerm && rolePublisher, | |||||
'svg.moderator': roleModerator, | 'svg.moderator': roleModerator, | ||||
'svg.user': !roleModerator | 'svg.user': !roleModerator && !roleInterpreter, | ||||
'svg.interpreter': !roleModerator && roleInterpreter | |||||
} | } | ||||
Object.keys(elements).forEach(key => { | Object.keys(elements).forEach(key => { | ||||
element.find(key)[elements[key] ? 'removeClass' : 'addClass']('hidden') | element.find(key)[elements[key] ? 'removeClass' : 'addClass']('hidden') | ||||
}) | }) | ||||
element.find('.action-role-publisher input').prop('checked', rolePublisher) | element.find('.action-role-publisher input').prop('checked', rolePublisher) | ||||
element.find('.action-role-moderator input').prop('checked', roleModerator) | element.find('.action-role-moderator input').prop('checked', roleModerator) | ||||
.prop('disabled', roleOwner) | .prop('disabled', roleOwner) | ||||
element.find('.interpreting select').val(roleInterpreter ? params.language : '') | |||||
return wrapper | return wrapper | ||||
} | } | ||||
/** | /** | ||||
* Update/refresh state of all participants' elements | |||||
*/ | |||||
function participantUpdateAll() { | |||||
Object.keys(connections).forEach(key => { | |||||
const conn = connections[key] | |||||
participantUpdate(conn.element, conn) | |||||
}) | |||||
} | |||||
/** | |||||
* Create a tag-like element for a subscriber participant | * Create a tag-like element for a subscriber participant | ||||
* | * | ||||
* @param params Connection metadata/params | * @param params Connection metadata/params | ||||
* @param content Optional content to prepend to the element | |||||
*/ | */ | ||||
function subscriberCreate(params) { | function subscriberCreate(params, content) { | ||||
// Create the element | // Create the element | ||||
let wrapper = $('<div class="meet-subscriber">').append(nicknameWidget(params)) | let wrapper = $('<div class="meet-subscriber">').append(nicknameWidget(params)) | ||||
participantUpdate(wrapper, params) | if (content) { | ||||
wrapper.prepend(content) | |||||
} | |||||
participantUpdate(wrapper, params, true) | |||||
return wrapper[params.isSelf ? 'prependTo' : 'appendTo'](subscribersContainer) | return wrapper[params.isSelf ? 'prependTo' : 'appendTo'](subscribersContainer) | ||||
.attr('id', 'subscriber-' + params.connectionId) | .attr('id', 'subscriber-' + params.connectionId) | ||||
.get(0) | .get(0) | ||||
} | } | ||||
/** | /** | ||||
* Create a tag-like nickname widget | * Create a tag-like nickname widget | ||||
* | * | ||||
* @param object params Connection metadata/params | * @param object params Connection metadata/params | ||||
*/ | */ | ||||
function nicknameWidget(params) { | function nicknameWidget(params) { | ||||
let languages = [] | |||||
// Append languages selection options | |||||
Object.keys(sessionData.languages).forEach(code => { | |||||
languages.push(`<option value="${code}">${sessionData.languages[code]}</option>`) | |||||
}) | |||||
// Create the element | // Create the element | ||||
let element = $( | let element = $( | ||||
'<div class="dropdown">' | '<div class="dropdown">' | ||||
+ '<a href="#" class="meet-nickname btn" aria-haspopup="true" aria-expanded="false" role="button">' | + '<a href="#" class="meet-nickname btn" aria-haspopup="true" aria-expanded="false" role="button">' | ||||
+ '<span class="content"></span>' | + '<span class="content"></span>' | ||||
+ '<span class="icon">' | + '<span class="icon">' | ||||
+ svgIcon('user', null, 'user') | + svgIcon('user', null, 'user') | ||||
+ svgIcon('crown', null, 'moderator hidden') | + svgIcon('crown', null, 'moderator hidden') | ||||
+ svgIcon('headphones', null, 'interpreter hidden') | |||||
+ '</span>' | + '</span>' | ||||
+ '</a>' | + '</a>' | ||||
+ '<div class="dropdown-menu">' | + '<div class="dropdown-menu">' | ||||
+ '<a class="dropdown-item action-nickname" href="#">Nickname</a>' | + '<a class="dropdown-item action-nickname" href="#">Nickname</a>' | ||||
+ '<a class="dropdown-item action-dismiss" href="#">Dismiss</a>' | + '<a class="dropdown-item action-dismiss" href="#">Dismiss</a>' | ||||
+ '<div class="dropdown-divider permissions"></div>' | + '<div class="dropdown-divider permissions"></div>' | ||||
+ '<div class="permissions">' | + '<div class="permissions">' | ||||
+ '<h6 class="dropdown-header">Permissions</h6>' | + '<h6 class="dropdown-header">Permissions</h6>' | ||||
+ '<label class="dropdown-item action-role-publisher custom-control custom-switch">' | + '<label class="dropdown-item action-role-publisher custom-control custom-switch">' | ||||
+ '<input type="checkbox" class="custom-control-input">' | + '<input type="checkbox" class="custom-control-input">' | ||||
+ ' <span class="custom-control-label">Audio & Video publishing</span>' | + ' <span class="custom-control-label">Audio & Video publishing</span>' | ||||
+ '</label>' | + '</label>' | ||||
+ '<label class="dropdown-item action-role-moderator custom-control custom-switch">' | + '<label class="dropdown-item action-role-moderator custom-control custom-switch">' | ||||
+ '<input type="checkbox" class="custom-control-input">' | + '<input type="checkbox" class="custom-control-input">' | ||||
+ ' <span class="custom-control-label">Moderation</span>' | + ' <span class="custom-control-label">Moderation</span>' | ||||
+ '</label>' | + '</label>' | ||||
+ '</div>' | + '</div>' | ||||
+ '<div class="dropdown-divider interpreting"></div>' | |||||
+ '<div class="interpreting">' | |||||
+ '<h6 class="dropdown-header">Language interpreter</h6>' | |||||
+ '<div class="ml-4 mr-4"><select class="custom-select">' | |||||
+ '<option value="">- none -</option>' | |||||
+ languages.join('') | |||||
+ '</select></div>' | |||||
+ '</div>' | |||||
+ '</div>' | + '</div>' | ||||
+ '</div>' | + '</div>' | ||||
) | ) | ||||
let nickname = element.find('.meet-nickname') | let nickname = element.find('.meet-nickname') | ||||
.addClass('btn btn-outline-' + (params.isSelf ? 'primary' : 'secondary')) | .addClass('btn btn-outline-' + (params.isSelf ? 'primary' : 'secondary')) | ||||
.attr({title: 'Options', 'data-toggle': 'dropdown'}) | .attr({title: 'Options', 'data-toggle': 'dropdown'}) | ||||
.dropdown({boundary: container}) | .dropdown({boundary: container}) | ||||
▲ Show 20 Lines • Show All 70 Lines • ▼ Show 20 Lines | function nicknameWidget(params) { | ||||
if (enabled) { | if (enabled) { | ||||
role |= Roles.MODERATOR | role |= Roles.MODERATOR | ||||
} else if (role & Roles.MODERATOR) { | } else if (role & Roles.MODERATOR) { | ||||
role ^= Roles.MODERATOR | role ^= Roles.MODERATOR | ||||
} | } | ||||
sessionData.onConnectionChange(params.connectionId, { role }) | sessionData.onConnectionChange(params.connectionId, { role }) | ||||
}) | }) | ||||
element.find('.interpreting select') | |||||
.on('change', e => { | |||||
const language = $(e.target).val() | |||||
sessionData.onConnectionChange(params.connectionId, { language }) | |||||
element.find('.meet-nickname').dropdown('hide') | |||||
}) | |||||
.on('click', e => { | |||||
// Prevents from closing the dropdown menu on click | |||||
e.stopPropagation() | |||||
}) | |||||
} | } | ||||
return element.get(0) | return element.get(0) | ||||
} | } | ||||
/** | /** | ||||
* Window onresize event handler (updates room layout) | * Window onresize event handler (updates room layout) | ||||
*/ | */ | ||||
▲ Show 20 Lines • Show All 178 Lines • ▼ Show 20 Lines | function svgIcon(name, type, className) { | ||||
return $(`<svg><path fill="currentColor" d="${icon[4]}"></path></svg>`) | return $(`<svg><path fill="currentColor" d="${icon[4]}"></path></svg>`) | ||||
.attr(attrs) | .attr(attrs) | ||||
.get(0).outerHTML | .get(0).outerHTML | ||||
} | } | ||||
/** | /** | ||||
* A way to update some session data, after you joined the room | * A way to update some session data, after you joined the room | ||||
* | * | ||||
* @param data Same input as for joinRoom(), but for now it supports only shareToken | * @param data Same input as for joinRoom() | ||||
*/ | */ | ||||
function updateSession(data) { | function updateSession(data) { | ||||
sessionData.shareToken = data.shareToken | sessionData.shareToken = data.shareToken | ||||
} | } | ||||
/** | /** | ||||
* A handler for volume level change events | * A handler for volume level change events | ||||
*/ | */ | ||||
Show All 38 Lines | function connectionData(connection) { | ||||
// two strings, so it puts a "%/%" separator in between, we'll replace it with comma | // two strings, so it puts a "%/%" separator in between, we'll replace it with comma | ||||
// to get one parseable json object | // to get one parseable json object | ||||
let data = JSON.parse(connection.data.replace('}%/%{', ',')) | let data = JSON.parse(connection.data.replace('}%/%{', ',')) | ||||
data.connectionId = connection.connectionId | data.connectionId = connection.connectionId | ||||
return data | return data | ||||
} | } | ||||
/** | |||||
* Get all existing language interpretation channels | |||||
*/ | |||||
function getChannels(connections) { | |||||
let channels = [] | |||||
Object.keys(connections || {}).forEach(key => { | |||||
let conn = connections[key] | |||||
if ( | |||||
conn.language | |||||
&& !channels.includes(conn.language) | |||||
) { | |||||
channels.push(conn.language) | |||||
} | |||||
}) | |||||
return channels | |||||
} | |||||
} | } | ||||
export { Meet, Roles } | export { Meet, Roles } |