Changeset View
Changeset View
Standalone View
Standalone View
src/resources/js/meet/app.js
Show All 35 Lines | function Meet(container) | ||||
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 containerWidth | ||||
let containerHeight | let containerHeight | ||||
let chatCount = 0 | let chatCount = 0 | ||||
let volumeElement | let volumeElement | ||||
let publishersContainer | |||||
let subscribersContainer | let subscribersContainer | ||||
let scrollStop | let scrollStop | ||||
OV = ovInit() | OV = ovInit() | ||||
// Disconnect participant when browser's window close | // Disconnect participant when browser's window close | ||||
window.addEventListener('beforeunload', () => { | window.addEventListener('beforeunload', () => { | ||||
leaveRoom() | leaveRoom() | ||||
▲ Show 20 Lines • Show All 50 Lines • ▼ Show 20 Lines | function Meet(container) | ||||
* 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, | ||||
* onConnectionChange - Callback for participant changes, e.g. role update, | * onConnectionChange - Callback for participant changes, e.g. role update, | ||||
* onSessionDataUpdate - Callback for current user connection update, | * onSessionDataUpdate - Callback for current user connection update, | ||||
* onMediaSetup - Called when user clicks the Media setup button | * onMediaSetup - Called when user clicks the Media setup button | ||||
*/ | */ | ||||
function joinRoom(data) { | function joinRoom(data) { | ||||
// Create a container for subscribers and publishers | |||||
publishersContainer = $('<div id="meet-publishers">').appendTo(container).get(0) | |||||
subscribersContainer = $('<div id="meet-subscribers">').appendTo(container).get(0) | |||||
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 | ||||
} | } | ||||
// Create a container for subscribers | |||||
if (!subscribersContainer) { | |||||
subscribersContainer = $('<div id="meet-subscribers">').appendTo(container).get(0) | |||||
} | |||||
// TODO: Make sure all supported callbacks exist, so we don't have to check | // TODO: Make sure all supported callbacks exist, so we don't have to check | ||||
// their existence everywhere anymore | // their existence everywhere anymore | ||||
sessionData = data | sessionData = data | ||||
// Init a session | // Init a session | ||||
session = OV.initSession() | session = OV.initSession() | ||||
▲ Show 20 Lines • Show All 49 Lines • ▼ Show 20 Lines | function joinRoom(data) { | ||||
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, metadata.element, props); | let subscriber = session.subscribe(event.stream, metadata.element, props); | ||||
Object.assign(metadata, { | |||||
audioActive: event.stream.audioActive, | |||||
videoActive: event.stream.videoActive, | |||||
videoDimensions: event.stream.videoDimensions | |||||
}) | |||||
subscriber.on('videoElementCreated', event => { | subscriber.on('videoElementCreated', event => { | ||||
$(event.element).prop({ | $(event.element).prop({ | ||||
tabindex: -1 | tabindex: -1 | ||||
}) | }) | ||||
resize() | resize() | ||||
}) | }) | ||||
metadata.audioActive = event.stream.audioActive | |||||
metadata.videoActive = event.stream.videoActive | |||||
// Update the wrapper controls/status | // Update the wrapper controls/status | ||||
participantUpdate(metadata.element, metadata) | participantUpdate(metadata.element, metadata) | ||||
}) | }) | ||||
// 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.audioActive = audioActive | ||||
metadata.videoActive = videoActive | metadata.videoActive = videoActive | ||||
} | } | ||||
if (metadata) { | if (metadata) { | ||||
metadata[event.changedProperty] = event.newValue | metadata[event.changedProperty] = event.newValue | ||||
if (event.changedProperty == 'videoDimensions') { | |||||
resize() | |||||
} 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) { | if (data.onDestroy) { | ||||
data.onDestroy(event) | data.onDestroy(event) | ||||
} | } | ||||
▲ Show 20 Lines • Show All 760 Lines • ▼ Show 20 Lines | function Meet(container) | ||||
/** | /** | ||||
* 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 | * @param content Optional content to prepend to the element | ||||
*/ | */ | ||||
function publisherCreate(params, content) { | function publisherCreate(params, content) { | ||||
let isScreen = params.role & Roles.SCREEN | |||||
// 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) { | if (content) { | ||||
wrapper.prepend(content) | wrapper.prepend(content) | ||||
} | } | ||||
if (isScreen) { | |||||
wrapper.addClass('screen') | |||||
} | |||||
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') | ||||
Show All 24 Lines | function publisherCreate(params, content) { | ||||
wrapper.find('.link-fullscreen.open').toggleClass('hidden') | wrapper.find('.link-fullscreen.open').toggleClass('hidden') | ||||
wrapper.toggleClass('fullscreen') | wrapper.toggleClass('fullscreen') | ||||
}) | }) | ||||
} | } | ||||
// Remove the subscriber element, if exists | // Remove the subscriber element, if exists | ||||
$('#subscriber-' + params.connectionId).remove() | $('#subscriber-' + params.connectionId).remove() | ||||
return wrapper[params.isSelf ? 'prependTo' : 'appendTo'](container) | let prio = params.isSelf || (isScreen && !$(publishersContainer).children('.screen').length) | ||||
return wrapper[prio ? 'prependTo' : 'appendTo'](publishersContainer) | |||||
.attr('id', 'publisher-' + params.connectionId) | .attr('id', 'publisher-' + params.connectionId) | ||||
.get(0) | .get(0) | ||||
} | } | ||||
/** | /** | ||||
* Update the publisher/subscriber element controls | * Update the publisher/subscriber element controls | ||||
* | * | ||||
* @param wrapper The wrapper element | * @param wrapper The wrapper element | ||||
▲ Show 20 Lines • Show All 285 Lines • ▼ Show 20 Lines | function nicknameWidget(params) { | ||||
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 = publishersContainer.offsetWidth | ||||
containerHeight = container.offsetHeight | containerHeight = publishersContainer.offsetHeight | ||||
if (subscribersContainer) { | |||||
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 | let publishers = $(publishersContainer).find('.meet-video') | ||||
let numOfVideos = publishers.length | |||||
if (!numOfVideos) { | if (!numOfVideos) { | ||||
return | return | ||||
} | } | ||||
let css, rows, cols, height | let css, rows, cols, height, padding = 0 | ||||
// Make the first screen sharing tile big | |||||
let screenVideo = publishers.filter('.screen').find('video').get(0) | |||||
if (screenVideo) { | |||||
let element = screenVideo.parentNode | |||||
let connId = element.id.replace(/^publisher-/, '') | |||||
let connection = connections[connId] | |||||
// We know the shared screen video dimensions, we can calculate | |||||
// width/height of the tile in the matrix | |||||
if (connection && connection.videoDimensions) { | |||||
let screenWidth = connection.videoDimensions.width | |||||
let screenHeight = containerHeight | |||||
// TODO: When the shared window is minimized the width/height is set to 1 (or 2) | |||||
// - at least on my system. We might need to handle this case nicer. Right now | |||||
// it create a 1-2px line on the left of the matrix - not a big issue. | |||||
// TODO: Make the 0.666 factor bigger for wide screen and small number of participants? | |||||
let maxWidth = Math.ceil(containerWidth * 0.666) | |||||
if (screenWidth > maxWidth) { | |||||
screenWidth = maxWidth | |||||
} | |||||
// Set the tile position and size | |||||
$(element).css({ | |||||
width: screenWidth + 'px', | |||||
height: screenHeight + 'px', | |||||
position: 'absolute', | |||||
top: 0, | |||||
left: 0 | |||||
}) | |||||
padding = screenWidth + 'px' | |||||
// Now the estate for the rest of participants is what's left on the right side | |||||
containerWidth -= screenWidth | |||||
publishers = publishers.not(element) | |||||
numOfVideos -= 1 | |||||
} | |||||
} | |||||
// Compensate the shared screen estate with a padding | |||||
$(publishersContainer).css('padding-left', padding) | |||||
const factor = containerWidth / containerHeight | const factor = containerWidth / containerHeight | ||||
if (factor >= 16/9) { | if (factor >= 16/9) { | ||||
if (numOfVideos <= 3) { | if (numOfVideos <= 3) { | ||||
rows = 1 | rows = 1 | ||||
} else if (numOfVideos <= 8) { | } else if (numOfVideos <= 8) { | ||||
rows = 2 | rows = 2 | ||||
Show All 26 Lines | function updateLayout() { | ||||
if (rows < cols && containerWidth < containerHeight) { | if (rows < cols && containerWidth < containerHeight) { | ||||
cols = rows | cols = rows | ||||
rows = Math.ceil(numOfVideos / cols) | rows = Math.ceil(numOfVideos / cols) | ||||
} | } | ||||
} | } | ||||
// console.log('factor=' + factor, 'num=' + numOfVideos, 'cols = '+cols, 'rows=' + rows); | // console.log('factor=' + factor, 'num=' + numOfVideos, 'cols = '+cols, 'rows=' + rows); | ||||
height = containerHeight / rows | // Update all tiles (except the main shared screen) in the matrix | ||||
css = { | publishers.css({ | ||||
width: (100 / cols) + '%', | width: (containerWidth / cols) + 'px', | ||||
// Height must be in pixels to make object-fit:cover working | // Height must be in pixels to make object-fit:cover working | ||||
height: height + 'px' | height: (containerHeight / rows) + 'px' | ||||
} | |||||
// Update the matrix | |||||
$(container).find('.meet-video').css(css) | |||||
/* | |||||
.each((idx, elem) => { | |||||
let video = $(elem).children('video')[0] | |||||
if (video && video.videoWidth && video.videoHeight && video.videoWidth > video.videoHeight) { | |||||
// Set max-width to keep the original aspect ratio in cases | |||||
// when there's enough room to display the element | |||||
let maxWidth = height * video.videoWidth / video.videoHeight | |||||
$(elem).css('max-width', maxWidth) | |||||
} | |||||
}) | }) | ||||
*/ | |||||
} | } | ||||
/** | /** | ||||
* Initialize screen sharing session/publisher | * Initialize screen sharing session/publisher | ||||
*/ | */ | ||||
function screenConnect(callback) { | function screenConnect(callback) { | ||||
if (!sessionData.shareToken) { | if (!sessionData.shareToken) { | ||||
return false | return false | ||||
▲ Show 20 Lines • Show All 171 Lines • Show Last 20 Lines |