Changeset View
Changeset View
Standalone View
Standalone View
src/resources/js/meet/app.js
Show All 11 Lines | |||||
function Meet(container) | function Meet(container) | ||||
{ | { | ||||
let OV // OpenVidu object to initialize a session | let OV // OpenVidu object to initialize a session | ||||
let session // Session object where the user will connect | let session // Session object where the user will connect | ||||
let publisher // Publisher object which the user will publish | let publisher // Publisher object which the user will publish | ||||
let audioActive = false // True if the audio track of the publisher is active | let audioActive = false // True if the audio track of the publisher is active | ||||
let videoActive = false // True if the video track of the publisher is active | let videoActive = false // True if the video track of the publisher is active | ||||
let numOfVideos = 0 // Keeps track of the number of videos that are being shown | |||||
let audioSource = '' // Currently selected microphone | let audioSource = '' // Currently selected microphone | ||||
let videoSource = '' // Currently selected camera | let videoSource = '' // Currently selected camera | ||||
let sessionData // Room session metadata | let sessionData // Room session metadata | ||||
let screenOV // OpenVidu object to initialize a screen sharing session | let screenOV // OpenVidu object to initialize a screen sharing session | ||||
let screenSession // Session object where the user will connect for screen sharing | 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 screenPublisher // Publisher object which the user will publish the screen sharing | ||||
▲ Show 20 Lines • Show All 42 Lines • ▼ Show 20 Lines | function Meet(container) | ||||
this.switchAudio = switchAudio | this.switchAudio = switchAudio | ||||
this.switchScreen = switchScreen | this.switchScreen = switchScreen | ||||
this.switchVideo = switchVideo | this.switchVideo = switchVideo | ||||
this.updateSession = updateSession | this.updateSession = updateSession | ||||
/** | /** | ||||
* Join the room session | * Join the room session | ||||
* | * | ||||
* @param data Session metadata and event handlers (session, token, shareToken, nickname, role, | * @param data Session metadata and event handlers (token, shareToken, nickname, role, | ||||
* chatElement, menuElement, onDestroy, onJoinRequest) | * chatElement, menuElement, onDestroy, onJoinRequest, onDismiss, onConnectionChange, | ||||
* onSessionDataUpdate) | |||||
*/ | */ | ||||
function joinRoom(data) { | function joinRoom(data) { | ||||
resize(); | resize(); | ||||
volumeMeterStop() | volumeMeterStop() | ||||
data.params = { | data.params = { | ||||
nickname: data.nickname, // user nickname | nickname: data.nickname, // user nickname | ||||
// avatar: undefined // avatar image | // avatar: undefined // avatar image | ||||
Show All 14 Lines | function joinRoom(data) { | ||||
// Ignore the current user connection | // Ignore the current user connection | ||||
if (event.connection.role) { | if (event.connection.role) { | ||||
return | return | ||||
} | } | ||||
// This is the first event executed when a user joins in. | // This is the first event executed when a user joins in. | ||||
// We'll create the video wrapper here, which can be re-used | // We'll create the video wrapper here, which can be re-used | ||||
// in 'streamCreated' event handler. | // in 'streamCreated' event handler. | ||||
// Note: For a user with a subscriber role 'streamCreated' event | |||||
// is not being dispatched at all | |||||
let metadata = connectionData(event.connection) | let metadata = connectionData(event.connection) | ||||
let connectionId = event.connection.connectionId | |||||
metadata.connId = connectionId | |||||
let element = participantCreate(metadata) | metadata.element = participantCreate(metadata) | ||||
connections[connectionId] = { element } | connections[metadata.connectionId] = metadata | ||||
resize() | |||||
// Send the current user status to the connecting user | // Send the current user status to the connecting user | ||||
// otherwise e.g. nickname might be not up to date | // otherwise e.g. nickname might be not up to date | ||||
signalUserUpdate(event.connection) | signalUserUpdate(event.connection) | ||||
}) | }) | ||||
session.on('connectionDestroyed', event => { | session.on('connectionDestroyed', event => { | ||||
let conn = connections[event.connection.connectionId] | let conn = connections[event.connection.connectionId] | ||||
if (conn) { | if (conn) { | ||||
if ($(conn.element).is('.meet-video')) { | |||||
numOfVideos-- | |||||
} | |||||
$(conn.element).remove() | $(conn.element).remove() | ||||
delete connections[event.connection.connectionId] | delete connections[event.connection.connectionId] | ||||
} | } | ||||
resize() | resize() | ||||
}) | }) | ||||
// On every new Stream received... | // On every new Stream received... | ||||
session.on('streamCreated', event => { | session.on('streamCreated', event => { | ||||
let connection = event.stream.connection | let connectionId = event.stream.connection.connectionId | ||||
let connectionId = connection.connectionId | let metadata = connections[connectionId] | ||||
let metadata = connectionData(connection) | |||||
let wrapper = connections[connectionId].element | |||||
let props = { | let props = { | ||||
// Prepend the video element so it is always before the watermark element | // Prepend the video element so it is always before the watermark element | ||||
insertMode: 'PREPEND' | insertMode: 'PREPEND' | ||||
} | } | ||||
// Subscribe to the Stream to receive it | // Subscribe to the Stream to receive it | ||||
let subscriber = session.subscribe(event.stream, wrapper, props); | let subscriber = session.subscribe(event.stream, metadata.element, props); | ||||
subscriber.on('videoElementCreated', event => { | subscriber.on('videoElementCreated', event => { | ||||
$(event.element).prop({ | $(event.element).prop({ | ||||
tabindex: -1 | tabindex: -1 | ||||
}) | }) | ||||
resize() | resize() | ||||
}) | }) | ||||
/* | |||||
subscriber.on('videoElementDestroyed', event => { | metadata.audioActive = event.stream.audioActive | ||||
}) | metadata.videoActive = event.stream.videoActive | ||||
*/ | |||||
// Update the wrapper controls/status | // Update the wrapper controls/status | ||||
participantUpdate(wrapper, event.stream) | participantUpdate(metadata.element, metadata) | ||||
}) | }) | ||||
/* | |||||
session.on('streamDestroyed', event => { | // Stream properties changes e.g. audio/video muted/unmuted | ||||
session.on('streamPropertyChanged', event => { | |||||
let connectionId = event.stream.connection.connectionId | |||||
let metadata = connections[connectionId] | |||||
if (session.connection.connectionId == connectionId) { | |||||
metadata = sessionData | |||||
} | |||||
if (metadata) { | |||||
metadata[event.changedProperty] = event.newValue | |||||
participantUpdate(metadata.element, metadata) | |||||
} | |||||
}) | }) | ||||
*/ | |||||
// Handle session disconnection events | // Handle session disconnection events | ||||
session.on('sessionDisconnected', event => { | session.on('sessionDisconnected', event => { | ||||
if (data.onDestroy) { | if (data.onDestroy) { | ||||
data.onDestroy(event) | data.onDestroy(event) | ||||
} | } | ||||
resize() | resize() | ||||
}) | }) | ||||
// Handle signals from all participants | // Handle signals from all participants | ||||
session.on('signal', signalEventHandler) | session.on('signal', signalEventHandler) | ||||
// Connect with the token | // Connect with the token | ||||
session.connect(data.token, data.params) | session.connect(data.token, data.params) | ||||
.then(() => { | .then(() => { | ||||
let wrapper | let params = { | ||||
let params = { self: true, role: data.role, audioActive, videoActive } | connectionId: session.connection.connectionId, | ||||
role: data.role, | |||||
audioActive, | |||||
videoActive | |||||
} | |||||
params = Object.assign({}, data.params, params) | params = Object.assign({}, data.params, params) | ||||
publisher.on('videoElementCreated', event => { | publisher.on('videoElementCreated', event => { | ||||
$(event.element).prop({ | $(event.element).prop({ | ||||
muted: true, // Mute local video to avoid feedback | muted: true, // Mute local video to avoid feedback | ||||
disablePictureInPicture: true, // this does not work in Firefox | disablePictureInPicture: true, // this does not work in Firefox | ||||
tabindex: -1 | tabindex: -1 | ||||
}) | }) | ||||
resize() | resize() | ||||
}) | }) | ||||
wrapper = participantCreate(params) | let wrapper = participantCreate(params) | ||||
if (data.role & Roles.PUBLISHER) { | if (data.role & Roles.PUBLISHER) { | ||||
publisher.createVideoElement(wrapper, 'PREPEND') | publisher.createVideoElement(wrapper, 'PREPEND') | ||||
session.publish(publisher) | session.publish(publisher) | ||||
} | } | ||||
resize() | sessionData.element = wrapper | ||||
sessionData.wrapper = wrapper | |||||
}) | }) | ||||
.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); | ||||
}) | }) | ||||
// Prepare the chat | // Prepare the chat | ||||
setupChat() | setupChat() | ||||
} | } | ||||
▲ Show 20 Lines • Show All 210 Lines • ▼ Show 20 Lines | function Meet(container) | ||||
* Signal events handler | * Signal events handler | ||||
*/ | */ | ||||
function signalEventHandler(signal) { | function signalEventHandler(signal) { | ||||
let conn, data | let conn, data | ||||
let connId = signal.from ? signal.from.connectionId : null | let connId = signal.from ? signal.from.connectionId : null | ||||
switch (signal.type) { | switch (signal.type) { | ||||
case 'signal:userChanged': | case 'signal:userChanged': | ||||
// TODO: Use 'signal:connectionUpdate' for nickname updates? | |||||
if (conn = connections[connId]) { | if (conn = connections[connId]) { | ||||
data = JSON.parse(signal.data) | data = JSON.parse(signal.data) | ||||
participantUpdate(conn.element, data) | conn.nickname = data.nickname | ||||
participantUpdate(conn.element, conn) | |||||
nicknameUpdate(data.nickname, connId) | nicknameUpdate(data.nickname, connId) | ||||
} | } | ||||
break | break | ||||
case 'signal:chat': | case 'signal:chat': | ||||
data = JSON.parse(signal.data) | data = JSON.parse(signal.data) | ||||
data.id = connId | data.id = connId | ||||
pushChatMessage(data) | pushChatMessage(data) | ||||
break | break | ||||
case 'signal:joinRequest': | case 'signal:joinRequest': | ||||
if (sessionData.onJoinRequest) { | // accept requests from the server only | ||||
if (!connId && sessionData.onJoinRequest) { | |||||
sessionData.onJoinRequest(JSON.parse(signal.data)) | sessionData.onJoinRequest(JSON.parse(signal.data)) | ||||
} | } | ||||
break; | 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 | * Send the chat message to other participants | ||||
* | * | ||||
* @param message Message string | * @param message Message string | ||||
*/ | */ | ||||
▲ Show 20 Lines • Show All 74 Lines • ▼ Show 20 Lines | function Meet(container) | ||||
/** | /** | ||||
* Send the user properties update signal to other participants | * Send the user properties update signal to other participants | ||||
* | * | ||||
* @param connection Optional connection to which the signal will be sent | * @param connection Optional connection to which the signal will be sent | ||||
* If not specified the signal is sent to all participants | * If not specified the signal is sent to all participants | ||||
*/ | */ | ||||
function signalUserUpdate(connection) { | function signalUserUpdate(connection) { | ||||
let data = { | let data = { | ||||
audioActive, | |||||
videoActive, | |||||
nickname: sessionData.params.nickname | nickname: sessionData.params.nickname | ||||
} | } | ||||
// Note: StreamPropertyChangedEvent might be more standard way | |||||
// to propagate the audio/video state change to other users. | |||||
// It looks there's no other way to propagate nickname changes. | |||||
session.signal({ | session.signal({ | ||||
data: JSON.stringify(data), | data: JSON.stringify(data), | ||||
type: 'userChanged', | type: 'userChanged', | ||||
to: connection ? [connection] : undefined | to: connection ? [connection] : undefined | ||||
}) | }) | ||||
// The same nickname for screen sharing session | // The same nickname for screen sharing session | ||||
if (screenSession) { | if (screenSession) { | ||||
data.audioActive = false | |||||
data.videoActive = true | |||||
screenSession.signal({ | screenSession.signal({ | ||||
data: JSON.stringify(data), | data: JSON.stringify(data), | ||||
type: 'userChanged', | type: 'userChanged', | ||||
to: connection ? [connection] : undefined | to: connection ? [connection] : undefined | ||||
}) | }) | ||||
} | } | ||||
} | } | ||||
/** | /** | ||||
* 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) { | ||||
try { | try { | ||||
publisher.publishAudio(!audioActive) | publisher.publishAudio(!audioActive) | ||||
audioActive = !audioActive | audioActive = !audioActive | ||||
participantUpdate(sessionData.wrapper, { audioActive }) | |||||
signalUserUpdate() | |||||
} catch (e) { | } catch (e) { | ||||
console.error(e) | console.error(e) | ||||
} | } | ||||
} | } | ||||
return audioActive | return audioActive | ||||
} | } | ||||
/** | /** | ||||
* Mute/Unmute video for current session publisher | * Mute/Unmute video for current session publisher | ||||
*/ | */ | ||||
function switchVideo() { | function switchVideo() { | ||||
// 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 (cameras.length) { | if (cameras.length) { | ||||
try { | try { | ||||
publisher.publishVideo(!videoActive) | publisher.publishVideo(!videoActive) | ||||
videoActive = !videoActive | videoActive = !videoActive | ||||
participantUpdate(sessionData.wrapper, { videoActive }) | |||||
signalUserUpdate() | |||||
} catch (e) { | } catch (e) { | ||||
console.error(e) | console.error(e) | ||||
} | } | ||||
} | } | ||||
return videoActive | return videoActive | ||||
} | } | ||||
Show All 21 Lines | function Meet(container) | ||||
/** | /** | ||||
* Detect if screen sharing is supported by the browser | * Detect if screen sharing is supported by the browser | ||||
*/ | */ | ||||
function isScreenSharingSupported() { | function isScreenSharingSupported() { | ||||
return !!OV.checkScreenSharingCapabilities(); | return !!OV.checkScreenSharingCapabilities(); | ||||
} | } | ||||
/** | /** | ||||
* Update participant connection state | |||||
*/ | |||||
function connectionUpdate(data) { | |||||
let conn = connections[data.connectionId] | |||||
// It's me | |||||
if (session.connection.connectionId == data.connectionId) { | |||||
const rolePublisher = data.role && data.role & Roles.PUBLISHER | |||||
const isPublisher = sessionData.role & Roles.PUBLISHER | |||||
// 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) | |||||
// 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) | |||||
} | |||||
// merge the changed data into internal session metadata object | |||||
Object.keys(data).forEach(key => { sessionData[key] = data[key] }) | |||||
// update the participant element | |||||
sessionData.element = participantUpdate(sessionData.element, sessionData) | |||||
// promoted to a publisher | |||||
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() | |||||
}) | |||||
// TODO: Here the user is asked for media permissions again | |||||
// should we rather start the stream without asking the user? | |||||
// Or maybe we want to display the media setup/preview form? | |||||
// Need to find a way to do this. | |||||
} else { | |||||
// Inform the vue component, so it can update some UI controls | |||||
update() | |||||
} | |||||
} else if (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 nickname in chat | * Update nickname in chat | ||||
* | * | ||||
* @param nickname Nickname | * @param nickname Nickname | ||||
* @param connectionId Connection identifier of the user | * @param connectionId Connection identifier of the user | ||||
*/ | */ | ||||
function nicknameUpdate(nickname, connectionId) { | function nicknameUpdate(nickname, connectionId) { | ||||
if (connectionId) { | if (connectionId) { | ||||
$(sessionData.chatElement).find('.chat').find('.message').each(function() { | $(sessionData.chatElement).find('.chat').find('.message').each(function() { | ||||
Show All 10 Lines | function Meet(container) | ||||
* 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 | ||||
* | * | ||||
* @return The element | * @return The element | ||||
*/ | */ | ||||
function participantCreate(params) { | function participantCreate(params) { | ||||
let element | |||||
params.isSelf = params.isSelf || session.connection.connectionId == params.connectionId | |||||
if (params.role & Roles.PUBLISHER || params.role & Roles.SCREEN) { | if (params.role & Roles.PUBLISHER || params.role & Roles.SCREEN) { | ||||
return publisherCreate(params) | element = publisherCreate(params) | ||||
} else { | |||||
element = subscriberCreate(params) | |||||
} | } | ||||
return subscriberCreate(params) | setTimeout(resize, 50); | ||||
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 | ||||
*/ | */ | ||||
function publisherCreate(params) { | function publisherCreate(params) { | ||||
Show All 11 Lines | function publisherCreate(params) { | ||||
+ '<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 (!params.self) { | if (!params.isSelf) { | ||||
// 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') | ||||
}) | }) | ||||
} | } | ||||
Show All 15 Lines | function publisherCreate(params) { | ||||
wrapper.on('fullscreenchange', () => { | wrapper.on('fullscreenchange', () => { | ||||
// const enabled = document.fullscreenElement | // const enabled = document.fullscreenElement | ||||
wrapper.find('.link-fullscreen.closed').toggleClass('hidden') | wrapper.find('.link-fullscreen.closed').toggleClass('hidden') | ||||
wrapper.find('.link-fullscreen.open').toggleClass('hidden') | wrapper.find('.link-fullscreen.open').toggleClass('hidden') | ||||
wrapper.toggleClass('fullscreen') | wrapper.toggleClass('fullscreen') | ||||
}) | }) | ||||
} | } | ||||
numOfVideos++ | |||||
// Remove the subscriber element, if exists | // Remove the subscriber element, if exists | ||||
$('#subscriber-' + params.connId).remove() | $('#subscriber-' + params.connectionId).remove() | ||||
return wrapper[params.self ? 'prependTo' : 'appendTo'](container).get(0) | return wrapper[params.isSelf ? 'prependTo' : 'appendTo'](container) | ||||
.attr('id', 'publisher-' + params.connectionId) | |||||
.get(0) | |||||
} | } | ||||
/** | /** | ||||
* Update the <video> wrapper controls | * Update the <video> wrapper 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) { | ||||
const $element = $(wrapper) | const element = $(wrapper) | ||||
const isModerator = sessionData.role & Roles.MODERATOR | |||||
const isSelf = session.connection.connectionId == params.connectionId | |||||
// Handle publisher-to-subscriber and subscriber-to-publisher change | |||||
if ('role' in params && !(params.role & Roles.SCREEN)) { | |||||
const rolePublisher = params.role & Roles.PUBLISHER | |||||
const isPublisher = element.is('.meet-video') | |||||
if ((rolePublisher && !isPublisher) || (!rolePublisher && isPublisher)) { | |||||
element.remove() | |||||
return participantCreate(params) | |||||
} | |||||
element.find('.action-role-publisher input').prop('checked', params.role & Roles.PUBLISHER) | |||||
} | |||||
if ('audioActive' in params) { | if ('audioActive' in params) { | ||||
$element.find('.status-audio')[params.audioActive ? 'addClass' : 'removeClass']('hidden') | element.find('.status-audio')[params.audioActive ? 'addClass' : 'removeClass']('hidden') | ||||
} | } | ||||
if ('videoActive' in params) { | if ('videoActive' in params) { | ||||
$element.find('.status-video')[params.videoActive ? 'addClass' : 'removeClass']('hidden') | element.find('.status-video')[params.videoActive ? 'addClass' : 'removeClass']('hidden') | ||||
} | } | ||||
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 (params.self) { | if (isSelf) { | ||||
$element.addClass('self') | element.addClass('self') | ||||
} | } | ||||
if (sessionData.role & Roles.MODERATOR) { | if (isModerator) { | ||||
$element.addClass('moderated') | element.addClass('moderated') | ||||
} | } | ||||
element.find('.dropdown-menu')[isSelf || isModerator ? 'removeClass' : 'addClass']('hidden') | |||||
element.find('.permissions')[isModerator ? 'removeClass' : 'addClass']('hidden') | |||||
if ('role' in params && params.role & Roles.SCREEN) { | |||||
element.find('.permissions').addClass('hidden') | |||||
} | |||||
return wrapper | |||||
} | } | ||||
/** | /** | ||||
* 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 | ||||
*/ | */ | ||||
function subscriberCreate(params) { | function subscriberCreate(params) { | ||||
// 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) | participantUpdate(wrapper, params) | ||||
return wrapper[params.self ? 'prependTo' : 'appendTo'](subscribersContainer) | return wrapper[params.isSelf ? 'prependTo' : 'appendTo'](subscribersContainer) | ||||
.attr('id', 'subscriber-' + params.connId) | .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) { | ||||
// Create the element | // Create the element | ||||
let element = $( | let element = $( | ||||
'<div class="dropdown">' | '<div class="dropdown">' | ||||
+ '<a href="#" class="meet-nickname btn" title="Nickname" 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">' + svgIcon('user') + '</span>' | + '<span class="icon">' + svgIcon('user') + '</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-dismiss" href="#">Dismiss</a>' | + '<a class="dropdown-item action-dismiss" href="#">Dismiss</a>' | ||||
+ '<div class="dropdown-divider permissions"></div>' | |||||
+ '<div class="permissions">' | |||||
+ '<h6 class="dropdown-header">Permissions</h6>' | |||||
+ '<label class="dropdown-item action-role-publisher custom-control custom-switch">' | |||||
+ '<input type="checkbox" class="custom-control-input">' | |||||
+ ' <span class="custom-control-label">Audio & Video publishing</span>' | |||||
+ '</label>' | |||||
//+ '<label class="dropdown-item action-role-moderator custom-control custom-switch">' | |||||
// + '<input type="checkbox" class="custom-control-input">' | |||||
// + ' <span class="custom-control-label">Moderation</span>' | |||||
//+ '</label>' | |||||
+ '</div>' | |||||
+ '</div>' | + '</div>' | ||||
+ '</div>' | + '</div>' | ||||
) | ) | ||||
let nickname = element.find('.meet-nickname') | let nickname = element.find('.meet-nickname') | ||||
.addClass('btn btn-outline-' + (params.self ? 'primary' : 'secondary')) | .addClass('btn btn-outline-' + (params.isSelf ? 'primary' : 'secondary')) | ||||
.attr({title: 'Options', 'data-toggle': 'dropdown'}) | |||||
.dropdown({boundary: container}) | |||||
if (params.self) { | if (params.isSelf) { | ||||
// Add events for nickname change | // Add events for nickname change | ||||
let editable = element.find('.content')[0] | let editable = element.find('.content')[0] | ||||
let editableEnable = () => { | let editableEnable = () => { | ||||
editable.contentEditable = true | editable.contentEditable = true | ||||
editable.focus() | editable.focus() | ||||
} | } | ||||
let editableUpdate = () => { | let editableUpdate = () => { | ||||
editable.contentEditable = false | editable.contentEditable = false | ||||
sessionData.params.nickname = editable.innerText | sessionData.params.nickname = editable.innerText | ||||
signalUserUpdate() | signalUserUpdate() | ||||
nicknameUpdate(editable.innerText, session.connection.connectionId) | nicknameUpdate(editable.innerText, session.connection.connectionId) | ||||
} | } | ||||
nickname.on('click', editableEnable) | element.find('.action-nickname').on('click', editableEnable) | ||||
element.find('.action-dismiss').remove() | |||||
$(editable).on('blur', editableUpdate) | $(editable).on('blur', editableUpdate) | ||||
.on('keydown', e => { | .on('keydown', e => { | ||||
// Enter or Esc | // Enter or Esc | ||||
if (e.keyCode == 13 || e.keyCode == 27) { | if (e.keyCode == 13 || e.keyCode == 27) { | ||||
editableUpdate() | editableUpdate() | ||||
return false | return false | ||||
} | } | ||||
}) | }) | ||||
} else if (sessionData.role & Roles.MODERATOR) { | } else { | ||||
nickname.attr({title: 'Options', 'data-toggle': 'dropdown'}) | element.find('.action-nickname').remove() | ||||
.dropdown({boundary: container}) | |||||
element.find('.action-dismiss').on('click', e => { | element.find('.action-dismiss').on('click', e => { | ||||
if (sessionData.onDismiss) { | if (sessionData.onDismiss) { | ||||
sessionData.onDismiss(params.connId) | sessionData.onDismiss(params.connectionId) | ||||
} | } | ||||
}) | }) | ||||
} | } | ||||
// Don't close the menu on permission change | |||||
element.find('.dropdown-menu > label').on('click', e => { e.stopPropagation() }) | |||||
if (sessionData.onConnectionChange) { | |||||
element.find('.action-role-publisher input').on('change', e => { | |||||
const enabled = e.target.checked | |||||
let role = params.role | |||||
if (enabled) { | |||||
role |= Roles.PUBLISHER | |||||
} else { | |||||
role |= Roles.SUBSCRIBER | |||||
if (role & Roles.PUBLISHER) { | |||||
role ^= Roles.PUBLISHER | |||||
} | |||||
} | |||||
sessionData.onConnectionChange(params.connectionId, { role }) | |||||
}) | |||||
element.find('.action-role-moderator input').on('change', e => { | |||||
const enabled = e.target.checked | |||||
let role = params.role | |||||
if (enabled) { | |||||
role |= Roles.MODERATOR | |||||
} else if (role & Roles.MODERATOR) { | |||||
role ^= Roles.MODERATOR | |||||
} | |||||
sessionData.onConnectionChange(params.connectionId, { role }) | |||||
}) | |||||
} | |||||
return element.get(0) | return element.get(0) | ||||
} | } | ||||
/** | /** | ||||
* Window onresize event handler (updates room layout) | * Window onresize event handler (updates room layout) | ||||
*/ | */ | ||||
function resize() { | function resize() { | ||||
containerWidth = container.offsetWidth | containerWidth = container.offsetWidth | ||||
containerHeight = container.offsetHeight | containerHeight = container.offsetHeight | ||||
if (subscribersContainer) { | if (subscribersContainer) { | ||||
containerHeight -= subscribersContainer.offsetHeight | containerHeight -= subscribersContainer.offsetHeight | ||||
} | } | ||||
updateLayout() | updateLayout() | ||||
$(container).parent()[window.screen.width <= 768 ? 'addClass' : 'removeClass']('mobile') | $(container).parent()[window.screen.width <= 768 ? 'addClass' : 'removeClass']('mobile') | ||||
} | } | ||||
/** | /** | ||||
* Update the room "matrix" layout | * Update the room "matrix" layout | ||||
*/ | */ | ||||
function updateLayout() { | function updateLayout() { | ||||
let numOfVideos = $(container).find('.meet-video').length | |||||
if (!numOfVideos) { | if (!numOfVideos) { | ||||
return | return | ||||
} | } | ||||
let css, rows, cols, height | let css, rows, cols, height | ||||
const factor = containerWidth / containerHeight | const factor = containerWidth / containerHeight | ||||
▲ Show 20 Lines • Show All 191 Lines • ▼ Show 20 Lines | |||||
} | } | ||||
function connectionData(connection) { | function connectionData(connection) { | ||||
// Note: we're sending a json from two sources (server-side when | // Note: we're sending a json from two sources (server-side when | ||||
// creating a token/connection, and client-side when joining the session) | // creating a token/connection, and client-side when joining the session) | ||||
// OpenVidu is unable to merge these two objects into one, for it it is only | // OpenVidu is unable to merge these two objects into one, for it it is only | ||||
// 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 | ||||
return JSON.parse(connection.data.replace('}%/%{', ',')) | let data = JSON.parse(connection.data.replace('}%/%{', ',')) | ||||
data.connectionId = connection.connectionId | |||||
return data | |||||
} | } | ||||
} | } | ||||
export { Meet, Roles } | export { Meet, Roles } |