Changeset View
Changeset View
Standalone View
Standalone View
src/resources/js/meet/app.js
Show All 31 Lines | let publisherDefaults = { | ||||
frameRate: 30, // The frame rate of your video | frameRate: 30, // The frame rate of your video | ||||
mirror: true // Whether to mirror your local video or not | mirror: true // Whether to mirror your local video or not | ||||
} | } | ||||
let cameras = [] // List of user video devices | let cameras = [] // List of user video devices | ||||
let microphones = [] // List of user audio devices | let microphones = [] // List of user audio devices | ||||
let connections = {} // Connected users in the session | let connections = {} // Connected users in the session | ||||
let containerWidth | |||||
let containerHeight | |||||
let chatCount = 0 | let chatCount = 0 | ||||
let volumeElement | let volumeElement | ||||
let publishersContainer | let publishersContainer | ||||
let subscribersContainer | let subscribersContainer | ||||
let scrollStop | let scrollStop | ||||
OV = ovInit() | OV = ovInit() | ||||
▲ Show 20 Lines • Show All 66 Lines • ▼ Show 20 Lines | 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 | ||||
} | } | ||||
// TODO: Make sure all supported callbacks exist, so we don't have to check | // Make sure all supported callbacks exist, so we don't have to check | ||||
// their existence everywhere anymore | // 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 | 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 => { | ||||
▲ Show 20 Lines • Show All 89 Lines • ▼ Show 20 Lines | function joinRoom(data) { | ||||
} else { | } else { | ||||
participantUpdate(metadata.element, metadata) | participantUpdate(metadata.element, metadata) | ||||
} | } | ||||
} | } | ||||
}) | }) | ||||
// Handle session disconnection events | // Handle session disconnection events | ||||
session.on('sessionDisconnected', event => { | session.on('sessionDisconnected', event => { | ||||
if (data.onDestroy) { | |||||
data.onDestroy(event) | data.onDestroy(event) | ||||
} | |||||
session = null | session = null | ||||
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(() => { | ||||
if (data.onSuccess) { | |||||
data.onSuccess() | data.onSuccess() | ||||
} | |||||
let params = { | let params = { | ||||
connectionId: session.connection.connectionId, | connectionId: session.connection.connectionId, | ||||
role: data.role, | role: data.role, | ||||
audioActive, | audioActive, | ||||
videoActive | videoActive | ||||
} | } | ||||
Show All 26 Lines | function joinRoom(data) { | ||||
conn.connectionId = key | conn.connectionId = key | ||||
connectionHandUp(conn) | connectionHandUp(conn) | ||||
} | } | ||||
}) | }) | ||||
sessionData.channels = getChannels(data.connections) | sessionData.channels = getChannels(data.connections) | ||||
// Inform the vue component, so it can update some UI controls | // Inform the vue component, so it can update some UI controls | ||||
if (sessionData.channels.length && sessionData.onSessionDataUpdate) { | if (sessionData.channels.length) { | ||||
sessionData.onSessionDataUpdate(sessionData) | 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) { | |||||
data.onError(error) | data.onError(error) | ||||
} | |||||
}) | }) | ||||
// Prepare the chat | // Prepare the chat | ||||
setupChat() | setupChat() | ||||
} | } | ||||
/** | /** | ||||
* Leave the room (disconnect) | * Leave the room (disconnect) | ||||
▲ Show 20 Lines • Show All 293 Lines • ▼ Show 20 Lines | function signalEventHandler(signal) { | ||||
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': | ||||
// accept requests from the server only | // accept requests from the server only | ||||
if (!connId && sessionData.onJoinRequest) { | if (!connId) { | ||||
sessionData.onJoinRequest(JSON.parse(signal.data)) | sessionData.onJoinRequest(JSON.parse(signal.data)) | ||||
} | } | ||||
break | break | ||||
case 'signal:connectionUpdate': | case 'signal:connectionUpdate': | ||||
// accept requests from the server only | // accept requests from the server only | ||||
if (!connId) { | if (!connId) { | ||||
data = JSON.parse(signal.data) | data = JSON.parse(signal.data) | ||||
▲ Show 20 Lines • Show All 247 Lines • ▼ Show 20 Lines | function connectionUpdate(data) { | ||||
// 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(() => { | ||||
sessionData.audioActive = publisher.stream.audioActive | sessionData.audioActive = publisher.stream.audioActive | ||||
sessionData.videoActive = publisher.stream.videoActive | sessionData.videoActive = publisher.stream.videoActive | ||||
if (sessionData.onSessionDataUpdate) { | |||||
sessionData.onSessionDataUpdate(sessionData) | 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) { | |||||
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 | // Update channels list | ||||
sessionData.channels = getChannels(connections) | sessionData.channels = getChannels(connections) | ||||
// The channel user was using has been removed (or rather the participant stopped being an interpreter) | // The channel user was using has been removed (or rather the participant stopped being an interpreter) | ||||
if (sessionData.channel && !sessionData.channels.includes(sessionData.channel)) { | if (sessionData.channel && !sessionData.channels.includes(sessionData.channel)) { | ||||
sessionData.channel = null | sessionData.channel = null | ||||
refresh = true | refresh = true | ||||
} | } | ||||
if (refresh) { | if (refresh) { | ||||
participantUpdateAll() | participantUpdateAll() | ||||
} | } | ||||
// Inform the vue component, so it can update some UI controls | // Inform the vue component, so it can update some UI controls | ||||
if (sessionData.onSessionDataUpdate) { | |||||
sessionData.onSessionDataUpdate(sessionData) | 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 | ||||
let element = $(nicknameWidget(connection)) | let element = $(nicknameWidget(connection)) | ||||
▲ Show 20 Lines • Show All 99 Lines • ▼ Show 20 Lines | function publisherCreate(params, content) { | ||||
wrapper.prepend(content) | wrapper.prepend(content) | ||||
} | } | ||||
if (isScreen) { | if (isScreen) { | ||||
wrapper.addClass('screen') | wrapper.addClass('screen') | ||||
} | } | ||||
if (params.isSelf) { | if (params.isSelf) { | ||||
if (sessionData.onMediaSetup) { | wrapper.find('.link-setup').removeClass('hidden').click(() => sessionData.onMediaSetup()) | ||||
wrapper.find('.link-setup').removeClass('hidden') | |||||
.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') | ||||
}) | }) | ||||
▲ Show 20 Lines • Show All 255 Lines • ▼ Show 20 Lines | function nicknameWidget(params) { | ||||
// Do not propagate the event, so it does not interfere with our | // Do not propagate the event, so it does not interfere with our | ||||
// keyboard shortcuts | // keyboard shortcuts | ||||
e.stopPropagation() | e.stopPropagation() | ||||
}) | }) | ||||
} else { | } else { | ||||
element.find('.action-nickname').remove() | element.find('.action-nickname').remove() | ||||
element.find('.action-dismiss').on('click', e => { | element.find('.action-dismiss').on('click', e => { | ||||
if (sessionData.onDismiss) { | |||||
sessionData.onDismiss(params.connectionId) | sessionData.onDismiss(params.connectionId) | ||||
} | |||||
}) | }) | ||||
} | } | ||||
let connectionRole = () => { | let connectionRole = () => { | ||||
if (params.isSelf) { | if (params.isSelf) { | ||||
return sessionData.role | return sessionData.role | ||||
} | } | ||||
if (params.connectionId in connections) { | if (params.connectionId in connections) { | ||||
return connections[params.connectionId].role | return connections[params.connectionId].role | ||||
} | } | ||||
return 0 | return 0 | ||||
} | } | ||||
// Don't close the menu on permission change | // Don't close the menu on permission change | ||||
element.find('.dropdown-menu > label').on('click', e => { e.stopPropagation() }) | element.find('.dropdown-menu > label').on('click', e => { e.stopPropagation() }) | ||||
if (sessionData.onConnectionChange) { | |||||
element.find('.action-role-publisher input').on('change', e => { | element.find('.action-role-publisher input').on('change', e => { | ||||
const enabled = e.target.checked | const enabled = e.target.checked | ||||
let role = connectionRole() | let role = connectionRole() | ||||
if (enabled) { | if (enabled) { | ||||
role |= Roles.PUBLISHER | role |= Roles.PUBLISHER | ||||
} else { | } else { | ||||
role |= Roles.SUBSCRIBER | role |= Roles.SUBSCRIBER | ||||
if (role & Roles.PUBLISHER) { | if (role & Roles.PUBLISHER) { | ||||
role ^= Roles.PUBLISHER | role ^= Roles.PUBLISHER | ||||
} | } | ||||
} | } | ||||
sessionData.onConnectionChange(params.connectionId, { role }) | sessionData.onConnectionChange(params.connectionId, { role }) | ||||
}) | }) | ||||
element.find('.action-role-moderator input').on('change', e => { | element.find('.action-role-moderator input').on('change', e => { | ||||
const enabled = e.target.checked | const enabled = e.target.checked | ||||
let role = connectionRole() | let role = connectionRole() | ||||
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') | element.find('.interpreting select') | ||||
.on('change', e => { | .on('change', e => { | ||||
const language = $(e.target).val() | const language = $(e.target).val() | ||||
sessionData.onConnectionChange(params.connectionId, { language }) | sessionData.onConnectionChange(params.connectionId, { language }) | ||||
element.find('.meet-nickname').dropdown('hide') | element.find('.meet-nickname').dropdown('hide') | ||||
}) | }) | ||||
.on('click', e => { | .on('click', e => { | ||||
// Prevents from closing the dropdown menu on click | // Prevents from closing the dropdown menu on click | ||||
e.stopPropagation() | e.stopPropagation() | ||||
}) | }) | ||||
} | |||||
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 = publishersContainer.offsetWidth | if (publishersContainer) { | ||||
containerHeight = publishersContainer.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 publishers = $(publishersContainer).find('.meet-video') | let publishers = $(publishersContainer).find('.meet-video') | ||||
let numOfVideos = publishers.length | let numOfVideos = publishers.length | ||||
if (sessionData && sessionData.counterElement) { | if (sessionData && sessionData.counterElement) { | ||||
sessionData.counterElement.innerHTML = Object.keys(connections).length + 1 | sessionData.counterElement.innerHTML = Object.keys(connections).length + 1 | ||||
} | } | ||||
if (!numOfVideos) { | if (!numOfVideos) { | ||||
subscribersContainer.style.minHeight = 'auto' | |||||
return | return | ||||
} | } | ||||
let allHeight = container.offsetHeight | |||||
let scrollHeight = subscribersContainer.scrollHeight | |||||
let containerWidth = publishersContainer.offsetWidth | |||||
let containerHeight = publishersContainer.offsetHeight | |||||
let limit = Math.ceil(allHeight * 0.25) // max subscribers list height | |||||
// Fix subscribers list height | |||||
if (subscribersContainer.offsetHeight <= scrollHeight) { | |||||
limit = Math.min(scrollHeight, limit) | |||||
subscribersContainer.style.minHeight = limit + 'px' | |||||
containerHeight = allHeight - limit | |||||
} else { | |||||
subscribersContainer.style.minHeight = 'auto' | |||||
} | |||||
let css, rows, cols, height, padding = 0 | let css, rows, cols, height, padding = 0 | ||||
// Make the first screen sharing tile big | // Make the first screen sharing tile big | ||||
let screenVideo = publishers.filter('.screen').find('video').get(0) | let screenVideo = publishers.filter('.screen').find('video').get(0) | ||||
if (screenVideo) { | if (screenVideo) { | ||||
let element = screenVideo.parentNode | let element = screenVideo.parentNode | ||||
let connId = element.id.replace(/^publisher-/, '') | let connId = element.id.replace(/^publisher-/, '') | ||||
▲ Show 20 Lines • Show All 265 Lines • Show Last 20 Lines |