Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F120826519
room.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
38 KB
Referenced Files
None
Subscribers
None
room.js
View Options
'use strict'
import
{
Client
}
from
'./client.js'
import
{
Roles
}
from
'./constants.js'
import
{
Dropdown
}
from
'bootstrap'
import
{
library
}
from
'@fortawesome/fontawesome-svg-core'
import
linkifyStr
from
'linkify-string'
function
Room
(
container
)
{
let
sessionData
// Room session metadata
let
peers
=
{}
// Participants in the session (including self)
let
publishersContainer
// Container element for publishers
let
subscribersContainer
// Container element for subscribers
let
selfId
// peer Id of the current user
let
chatCount
=
0
let
scrollStop
let
$t
let
$toast
const
client
=
new
Client
()
// Disconnect participant when browser's window close
window
.
addEventListener
(
'beforeunload'
,
()
=>
{
leaveRoom
()
})
window
.
addEventListener
(
'resize'
,
resize
)
// Public methods
this
.
isScreenSharingSupported
=
isScreenSharingSupported
this
.
joinRoom
=
joinRoom
this
.
leaveRoom
=
leaveRoom
this
.
raiseHand
=
raiseHand
this
.
setupStart
=
setupStart
this
.
setupStop
=
setupStop
this
.
setupSetAudioDevice
=
setupSetAudioDevice
this
.
setupSetVideoDevice
=
setupSetVideoDevice
this
.
switchAudio
=
switchAudio
this
.
switchChannel
=
switchChannel
this
.
switchScreen
=
switchScreen
this
.
switchVideo
=
switchVideo
this
.
getStats
=
getStats
/**
* Join the room session
*
* @param data Session metadata and event handlers:
* token - A token for the main connection,
* nickname - Participant name,
* languages - Supported languages (code-to-label map)
* chatElement - DOM element for the chat widget,
* counterElement - DOM element for the participants counter,
* menuElement - DOM element of the room toolbar,
* queueElement - DOM element for the Q&A queue (users with a raised hand)
* onSuccess - Callback for session connection (join) success
* onError - Callback for session connection (join) error
* onDestroy - Callback for session disconnection event,
* onMediaSetup - Called when user clicks the Media setup button
* onUpdate - Callback for current user/session update,
* toast - Toast widget
* translate - Translation function
*/
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
()
$t
=
data
.
translate
$toast
=
data
.
toast
// Make sure all supported callbacks exist, so we don't have to check
// their existence everywhere anymore
let
events
=
[
'Success'
,
'Error'
,
'Destroy'
,
'Update'
,
'MediaSetup'
]
events
.
map
(
event
=>
'on'
+
event
).
forEach
(
event
=>
{
if
(
!
data
[
event
])
{
data
[
event
]
=
()
=>
{}
}
})
sessionData
=
data
// Handle new participants (including self)
client
.
on
(
'addPeer'
,
(
event
)
=>
{
if
(
event
.
isSelf
)
{
selfId
=
event
.
id
}
peers
[
event
.
id
]
=
event
event
.
element
=
participantCreate
(
event
,
event
.
videoElement
)
if
(
event
.
screenVideoElement
&&
!
event
.
screen
)
{
const
screen
=
{
id
:
event
.
id
,
role
:
event
.
role
|
Roles
.
SCREEN
,
nickname
:
event
.
nickname
}
event
.
screen
=
participantCreate
(
screen
,
event
.
screenVideoElement
)
}
if
(
event
.
raisedHand
)
{
peerHandUp
(
event
)
}
})
// Handle removed participants
client
.
on
(
'removePeer'
,
(
peerId
)
=>
{
let
peer
=
peers
[
peerId
]
if
(
peer
)
{
// Remove elements related to the participant
peerHandDown
(
peer
)
$
(
peer
.
element
).
remove
()
if
(
peer
.
screen
)
{
$
(
peer
.
screen
).
remove
()
}
delete
peers
[
peerId
]
}
resize
()
})
// Participant properties changed e.g. audio/video muted/unmuted
client
.
on
(
'updatePeer'
,
(
event
,
changed
)
=>
{
let
peer
=
peers
[
event
.
id
]
if
(
!
peer
)
{
return
}
event
.
element
=
peer
.
element
event
.
screen
=
peer
.
screen
// Video element added or removed
if
(
event
.
videoElement
&&
event
.
videoElement
.
parentNode
!=
event
.
element
)
{
$
(
event
.
element
).
prepend
(
event
.
videoElement
)
}
else
if
(
!
event
.
videoElement
)
{
$
(
event
.
element
).
find
(
'video'
).
remove
()
}
// Video element of the shared screen added or removed
if
(
event
.
screenVideoElement
&&
!
event
.
screen
)
{
const
screen
=
{
id
:
event
.
id
,
role
:
event
.
role
|
Roles
.
SCREEN
,
nickname
:
event
.
nickname
}
event
.
screen
=
participantCreate
(
screen
,
event
.
screenVideoElement
)
}
else
if
(
!
event
.
screenVideoElement
&&
event
.
screen
)
{
$
(
event
.
screen
).
remove
()
event
.
screen
=
null
resize
()
}
peers
[
event
.
id
]
=
event
if
(
changed
&&
changed
.
length
)
{
if
(
changed
&&
changed
.
includes
(
'nickname'
))
{
nicknameUpdate
(
event
.
nickname
,
event
.
id
)
}
if
(
changed
.
includes
(
'raisedHand'
))
{
if
(
event
.
raisedHand
)
{
peerHandUp
(
event
)
}
else
{
peerHandDown
(
event
)
}
}
if
(
changed
&&
changed
.
includes
(
'screenWidth'
))
{
resize
()
return
}
}
if
(
changed
&&
changed
.
includes
(
'interpreterRole'
)
&&
!
event
.
isSelf
&&
$
(
event
.
element
).
find
(
'video'
).
length
)
{
// Publisher-to-interpreter or vice-versa, move element to the subscribers list or vice-versa,
// but keep the existing video element
let
wrapper
=
participantCreate
(
event
,
$
(
event
.
element
).
find
(
'video'
))
event
.
element
.
remove
()
event
.
element
=
wrapper
}
else
if
(
changed
&&
changed
.
includes
(
'publisherRole'
)
&&
!
event
.
language
)
{
// Handle publisher-to-subscriber and subscriber-to-publisher change
event
.
element
.
remove
()
event
.
element
=
participantCreate
(
event
,
event
.
videoElement
)
}
else
{
participantUpdate
(
event
.
element
,
event
)
}
// It's me, got publisher role
if
(
event
.
isSelf
&&
(
event
.
role
&
Roles
.
PUBLISHER
)
&&
changed
&&
changed
.
includes
(
'publisherRole'
))
{
// Open the media setup dialog
sessionData
.
onMediaSetup
()
}
if
(
changed
&&
changed
.
includes
(
'moderatorRole'
))
{
participantUpdateAll
()
}
})
// Handle successful connection to the room
client
.
on
(
'joinSuccess'
,
()
=>
{
data
.
onSuccess
()
client
.
media
.
setupStop
()
})
// Handle join requests from other users (knocking to the room)
client
.
on
(
'joinRequest'
,
event
=>
{
joinRequest
(
event
)
})
// Handle session disconnection events
client
.
on
(
'closeSession'
,
event
=>
{
// Notify the UI
data
.
onDestroy
(
event
)
// Remove all participant elements
Object
.
keys
(
peers
).
forEach
(
peerId
=>
{
$
(
peers
[
peerId
].
element
).
remove
()
})
peers
=
{}
// refresh the matrix
resize
()
})
// Handle session update events (e.g. channel, channels list changes)
client
.
on
(
'updateSession'
,
event
=>
{
// Inform the vue component, so it can update some UI controls
sessionData
.
onUpdate
(
event
)
})
const
{
audioSource
,
videoSource
}
=
client
.
media
.
setupData
()
// Start the session
client
.
joinSession
(
data
.
token
,
{
videoSource
,
audioSource
,
nickname
:
data
.
nickname
})
// Prepare the chat
initChat
()
}
async
function
getStats
()
{
return
await
client
.
getStats
()
}
/**
* Leave the room (disconnect)
*/
function
leaveRoom
(
forced
)
{
client
.
closeSession
(
forced
)
peers
=
{}
}
/**
* Handler for an event received by the moderator when a participant
* is asking for a permission to join the room
*/
function
joinRequest
(
data
)
{
const
id
=
data
.
requestId
// The toast for this user request already exists, ignore
// It's not really needed as we do this on server-side already
if
(
$
(
'#i'
+
id
).
length
)
{
return
}
const
body
=
$
(
`<div>`
+
`<div class="picture"><img src="
${
data
.
picture
}
"></div>`
+
`<div class="content">`
+
`<p class="mb-2"></p>`
+
`<div class="text-end">`
+
`<button type="button" class="btn btn-sm btn-success accept">
${
$t
(
'btn.accept'
)
}
</button>`
+
`<button type="button" class="btn btn-sm btn-danger deny ms-2">
${
$t
(
'btn.deny'
)
}
</button>`
)
$toast
.
message
({
className
:
'join-request'
,
icon
:
'user'
,
timeout
:
0
,
title
:
$t
(
'meet.join-request'
),
// titleClassName: '',
body
:
body
.
html
(),
onShow
:
element
=>
{
$
(
element
).
find
(
'p'
).
text
(
$t
(
'meet.join-requested'
,
{
user
:
data
.
nickname
||
''
}))
// add id attribute, so we can identify it
$
(
element
).
attr
(
'id'
,
'i'
+
id
)
// add action to the buttons
.
find
(
'button.accept,button.deny'
).
on
(
'click'
,
e
=>
{
const
action
=
$
(
e
.
target
).
is
(
'.accept'
)
?
'Accept'
:
'Deny'
client
[
'joinRequest'
+
action
](
id
)
$
(
'#i'
+
id
).
remove
()
})
}
})
}
/**
* Raise or lower the hand
*
* @param status Hand raised or not
*/
async
function
raiseHand
(
status
)
{
return
await
client
.
raiseHand
(
status
)
}
/**
* Sets the audio and video devices for the session.
* This will ask user for permission to access media devices.
*
* @param props Setup properties (videoElement, volumeElement, onSuccess, onError)
*/
async
function
setupStart
(
props
)
{
client
.
media
.
setupStart
(
props
)
// When setting up devices while the session is ongoing we have to
// disable currently selected devices (temporarily) otherwise e.g.
// changing a mic or camera to another device will not be possible.
if
(
client
.
isJoined
())
{
client
.
setMic
(
''
)
client
.
setCamera
(
''
)
}
}
/**
* Stop the setup "process", cleanup after it.
*/
async
function
setupStop
()
{
client
.
media
.
setupStop
()
// Apply device changes to the client
const
{
audioSource
,
videoSource
}
=
client
.
media
.
setupData
()
await
client
.
setMic
(
audioSource
)
await
client
.
setCamera
(
videoSource
)
}
/**
* Change the publisher audio device
*
* @param deviceId Device identifier string
*/
async
function
setupSetAudioDevice
(
deviceId
)
{
return
await
client
.
media
.
setupSetAudio
(
deviceId
)
}
/**
* Change the publisher video device
*
* @param deviceId Device identifier string
*/
async
function
setupSetVideoDevice
(
deviceId
)
{
return
await
client
.
media
.
setupSetVideo
(
deviceId
)
}
/**
* Setup the chat UI
*/
function
initChat
()
{
// Handle arriving chat messages
client
.
on
(
'chatMessage'
,
pushChatMessage
)
// The UI elements are created in the vue template
// Here we add a logic for how they work
const
chat
=
$
(
sessionData
.
chatElement
).
find
(
'.chat'
).
get
(
0
)
const
textarea
=
$
(
sessionData
.
chatElement
).
find
(
'textarea'
)
const
button
=
$
(
sessionData
.
menuElement
).
find
(
'.link-chat'
)
textarea
.
on
(
'keydown'
,
e
=>
{
if
(
e
.
keyCode
==
13
&&
!
e
.
shiftKey
)
{
if
(
textarea
.
val
().
length
)
{
client
.
chatMessage
(
textarea
.
val
())
textarea
.
val
(
''
)
}
return
false
}
})
// Add an element for the count of unread messages on the chat button
button
.
append
(
'<span class="badge bg-dark blinker">'
)
.
on
(
'click'
,
()
=>
{
button
.
find
(
'.badge'
).
text
(
''
)
chatCount
=
0
// When opening the chat scroll it to the bottom, or we shouldn't?
scrollStop
=
false
chat
.
scrollTop
=
chat
.
scrollHeight
})
$
(
chat
).
on
(
'scroll'
,
event
=>
{
// Detect manual scrollbar moves, disable auto-scrolling until
// the scrollbar is positioned on the element bottom again
scrollStop
=
chat
.
scrollTop
+
chat
.
offsetHeight
<
chat
.
scrollHeight
})
}
/**
* Add a message to the chat
*
* @param data Object with a message, nickname, id (of the connection, empty for self)
*/
function
pushChatMessage
(
data
)
{
let
message
=
$
(
'<span>'
).
text
(
data
.
message
).
text
()
// make the message secure
// Format the message, convert emails and urls to links
message
=
linkifyStr
(
message
,
{
nl2br
:
true
,
rel
:
'noreferrer'
,
target
:
'_blank'
,
truncate
:
25
,
// Skip links that don't begin with a protocol
// e.g., "http://github.com" will be linkified, but "github.com" will not.
validate
:
{
url
:
value
=>
/^
https
?:
\
/
\
//.test(value)
}
})
// Display the message
let
chat
=
$
(
sessionData
.
chatElement
).
find
(
'.chat'
)
let
box
=
chat
.
find
(
'.message'
).
last
()
message
=
$
(
'<div>'
).
html
(
message
)
if
(
box
.
length
&&
box
.
data
(
'id'
)
==
data
.
peerId
)
{
// A message from the same user as the last message, no new box needed
message
.
appendTo
(
box
)
}
else
{
box
=
$
(
'<div class="message">'
).
data
(
'id'
,
data
.
peerId
)
.
append
(
$
(
'<div class="nickname">'
).
text
(
data
.
nickname
||
''
))
.
append
(
message
)
.
appendTo
(
chat
)
if
(
data
.
isSelf
)
{
box
.
addClass
(
'self'
)
}
}
// Count unread messages
if
(
!
$
(
sessionData
.
chatElement
).
is
(
'.open'
))
{
if
(
!
data
.
isSelf
)
{
chatCount
++
}
}
else
{
chatCount
=
0
}
$
(
sessionData
.
menuElement
).
find
(
'.link-chat .badge'
).
text
(
chatCount
?
chatCount
:
''
)
// Scroll the chat element to the end
if
(
!
scrollStop
)
{
chat
.
get
(
0
).
scrollTop
=
chat
.
get
(
0
).
scrollHeight
}
}
/**
* Switch interpreted language channel
*
* @param channel Two-letter language code
*/
function
switchChannel
(
channel
)
{
client
.
setLanguageChannel
(
channel
)
}
/**
* Mute/Unmute audio for current session publisher
*/
async
function
switchAudio
()
{
const
isActive
=
client
.
micStatus
()
if
(
isActive
)
{
return
await
client
.
micMute
()
}
else
{
return
await
client
.
micUnmute
()
}
}
/**
* Mute/Unmute video for current session publisher
*/
async
function
switchVideo
()
{
const
isActive
=
client
.
camStatus
()
if
(
isActive
)
{
return
await
client
.
camMute
()
}
else
{
return
await
client
.
camUnmute
()
}
}
/**
* Switch on/off screen sharing
*/
async
function
switchScreen
()
{
const
isActive
=
client
.
screenStatus
()
if
(
isActive
)
{
return
await
client
.
screenUnshare
()
}
else
{
return
await
client
.
screenShare
()
}
}
/**
* Detect if screen sharing is supported by the browser
*/
function
isScreenSharingSupported
()
{
return
!!
(
navigator
.
mediaDevices
&&
navigator
.
mediaDevices
.
getDisplayMedia
)
}
/**
* Handler for Hand-Up "signal"
*/
function
peerHandUp
(
peer
)
{
let
element
=
$
(
nicknameWidget
(
peer
))
participantUpdate
(
element
,
peer
)
element
.
attr
(
'id'
,
'qa'
+
peer
.
id
).
appendTo
(
$
(
sessionData
.
queueElement
).
show
())
setTimeout
(()
=>
element
.
addClass
(
'wiggle'
),
50
)
}
/**
* Handler for Hand-Down "signal"
*/
function
peerHandDown
(
peer
)
{
let
list
=
$
(
sessionData
.
queueElement
)
list
.
find
(
'#qa'
+
peer
.
id
).
remove
()
if
(
!
list
.
find
(
'.meet-nickname'
).
length
)
{
list
.
hide
()
}
}
/**
* Update participant nickname in the UI
*
* @param nickname Nickname
* @param peerId Connection identifier of the user
*/
function
nicknameUpdate
(
nickname
,
peerId
)
{
if
(
peerId
)
{
$
(
sessionData
.
chatElement
).
find
(
'.chat'
).
find
(
'.message'
).
each
(
function
()
{
let
elem
=
$
(
this
)
if
(
elem
.
data
(
'id'
)
==
peerId
)
{
elem
.
find
(
'.nickname'
).
text
(
nickname
||
''
)
}
})
$
(
sessionData
.
queueElement
).
find
(
'#qa'
+
peerId
+
' .content'
).
text
(
nickname
||
''
)
// Also update the nickname for the shared screen as we do not call
// participantUpdate() for this element
$
(
'#screen-'
+
peerId
).
find
(
'.meet-nickname .content'
).
text
(
nickname
||
''
)
}
}
/**
* Create a participant element in the matrix. Depending on the peer role
* parameter it will be a video element wrapper inside the matrix or a simple
* tag-like element on the subscribers list.
*
* @param params Peer metadata/params
* @param content Optional content to prepend to the element, e.g. video element
*
* @return The element
*/
function
participantCreate
(
params
,
content
)
{
let
element
if
((
!
params
.
language
&&
params
.
role
&
Roles
.
PUBLISHER
)
||
params
.
role
&
Roles
.
SCREEN
)
{
// publishers and shared screens
element
=
publisherCreate
(
params
,
content
)
}
else
{
// subscribers and language interpreters
element
=
subscriberCreate
(
params
,
content
)
}
setTimeout
(
resize
,
50
)
return
element
}
/**
* Create a <video> element wrapper with controls
*
* @param params Connection metadata/params
* @param content Optional content to prepend to the element
*/
function
publisherCreate
(
params
,
content
)
{
let
isScreen
=
params
.
role
&
Roles
.
SCREEN
// Create the element
let
wrapper
=
$
(
'<div class="meet-video">'
+
svgIcon
(
'user'
,
'fas'
,
'watermark'
)
+
'<div class="controls">'
+
'<button type="button" class="btn btn-link link-setup hidden" title="'
+
$t
(
'meet.media-setup'
)
+
'">'
+
svgIcon
(
'gear'
)
+
'</button>'
+
'<div class="volume hidden"><input type="range" min="0" max="1" step="0.1" /></div>'
+
'<button type="button" class="btn btn-link link-audio hidden" title="'
+
$t
(
'meet.menu-audio-mute'
)
+
'">'
+
svgIcon
(
'volume-xmark'
)
+
'</button>'
+
'<button type="button" class="btn btn-link link-fullscreen closed hidden" title="'
+
$t
(
'meet.menu-fullscreen'
)
+
'">'
+
svgIcon
(
'expand'
)
+
'</button>'
+
'<button type="button" class="btn btn-link link-fullscreen open hidden" title="'
+
$t
(
'meet.menu-fullscreen'
)
+
'">'
+
svgIcon
(
'compress'
)
+
'</button>'
+
'</div>'
+
'<div class="status">'
+
'<span class="bg-warning status-audio hidden">'
+
svgIcon
(
'microphone-slash'
)
+
'</span>'
+
'<span class="bg-warning status-video hidden">'
+
svgIcon
(
'video-slash'
)
+
'</span>'
+
'</div>'
+
'</div>'
)
// Append the nickname widget
wrapper
.
find
(
'.controls'
).
before
(
nicknameWidget
(
params
))
if
(
content
)
{
wrapper
.
prepend
(
content
)
}
if
(
isScreen
)
{
wrapper
.
addClass
(
'screen'
)
}
if
(
params
.
isSelf
)
{
wrapper
.
find
(
'.link-setup'
).
removeClass
(
'hidden'
).
on
(
'click'
,
()
=>
sessionData
.
onMediaSetup
())
}
else
if
(
!
isScreen
)
{
let
volumeInput
=
wrapper
.
find
(
'.volume input'
)
let
audioButton
=
wrapper
.
find
(
'.link-audio'
)
let
inVolume
=
false
let
hideVolumeTimeout
let
hideVolume
=
()
=>
{
if
(
inVolume
)
{
hideVolumeTimeout
=
setTimeout
(
hideVolume
,
1000
)
}
else
{
volumeInput
.
parent
().
addClass
(
'hidden'
)
}
}
let
setVolume
=
(
video
,
volume
)
=>
{
video
.
volume
=
volume
video
.
muted
=
volume
==
0
audioButton
[
video
.
muted
?
'addClass'
:
'removeClass'
](
'text-danger'
)
client
[
video
.
muted
?
'peerMicMute'
:
'peerMicUnmute'
](
params
.
id
)
}
// Enable and set up the audio mute button
audioButton
.
removeClass
(
'hidden'
)
.
on
(
'click'
,
e
=>
{
let
video
=
wrapper
.
find
(
'video'
)[
0
]
setVolume
(
video
,
!
video
.
muted
?
0
:
1
)
volumeInput
.
val
(
video
.
volume
)
})
// Show the volume slider when mouse is over the audio mute/unmute button
.
on
(
'mouseenter'
,
()
=>
{
let
video
=
wrapper
.
find
(
'video'
)[
0
]
clearTimeout
(
hideVolumeTimeout
)
volumeInput
.
parent
().
removeClass
(
'hidden'
)
volumeInput
.
val
(
video
.
volume
)
})
.
on
(
'mouseleave'
,
()
=>
{
hideVolumeTimeout
=
setTimeout
(
hideVolume
,
1000
)
})
// Set up the audio volume control
volumeInput
.
on
(
'mouseenter'
,
()
=>
{
inVolume
=
true
})
.
on
(
'mouseleave'
,
()
=>
{
inVolume
=
false
})
.
on
(
'change input'
,
()
=>
{
setVolume
(
wrapper
.
find
(
'video'
)[
0
],
volumeInput
.
val
())
})
}
participantUpdate
(
wrapper
,
params
)
// Fullscreen control
if
(
document
.
fullscreenEnabled
)
{
wrapper
.
find
(
'.link-fullscreen.closed'
).
removeClass
(
'hidden'
)
.
on
(
'click'
,
()
=>
{
wrapper
.
get
(
0
).
requestFullscreen
()
})
wrapper
.
find
(
'.link-fullscreen.open'
)
.
on
(
'click'
,
()
=>
{
document
.
exitFullscreen
()
})
wrapper
.
on
(
'fullscreenchange'
,
()
=>
{
// const enabled = document.fullscreenElement
wrapper
.
find
(
'.link-fullscreen'
).
toggleClass
(
'hidden'
)
})
}
// Remove the subscriber element, if exists
$
(
'#subscriber-'
+
params
.
id
).
remove
()
let
prio
=
params
.
isSelf
||
(
isScreen
&&
!
$
(
publishersContainer
).
children
(
'.screen'
).
length
)
return
wrapper
[
prio
?
'prependTo'
:
'appendTo'
](
publishersContainer
)
.
attr
(
'id'
,
(
isScreen
?
'screen-'
:
'publisher-'
)
+
params
.
id
)
.
get
(
0
)
}
/**
* Update the publisher/subscriber element controls
*
* @param wrapper The wrapper element
* @param params Peer metadata/params
*/
function
participantUpdate
(
wrapper
,
params
)
{
const
element
=
$
(
wrapper
)
const
isSelf
=
params
.
isSelf
const
rolePublisher
=
params
.
role
&
Roles
.
PUBLISHER
const
roleModerator
=
params
.
role
&
Roles
.
MODERATOR
const
roleScreen
=
params
.
role
&
Roles
.
SCREEN
const
roleOwner
=
params
.
role
&
Roles
.
OWNER
const
roleInterpreter
=
rolePublisher
&&
!!
params
.
language
const
audioActive
=
roleScreen
?
true
:
params
.
audioActive
const
videoActive
=
roleScreen
?
true
:
params
.
videoActive
element
.
find
(
'.status-audio'
)[
audioActive
?
'addClass'
:
'removeClass'
](
'hidden'
)
element
.
find
(
'.status-video'
)[
videoActive
?
'addClass'
:
'removeClass'
](
'hidden'
)
element
.
find
(
'.meet-nickname > .content'
).
text
(
params
.
nickname
||
''
)
if
(
isSelf
)
{
element
.
addClass
(
'self'
)
}
else
if
(
!
roleScreen
)
{
element
.
find
(
'.link-audio'
).
removeClass
(
'hidden'
)
}
const
isModerator
=
peers
[
selfId
]
&&
peers
[
selfId
].
role
&
Roles
.
MODERATOR
const
withPerm
=
isModerator
&&
!
roleScreen
&&
!
(
roleOwner
&&
!
isSelf
)
const
withMenu
=
isSelf
||
(
isModerator
&&
!
roleOwner
)
if
(
isModerator
)
{
element
.
addClass
(
'moderated'
)
}
// TODO: This probably could be better done with css
let
elements
=
{
'.permissions'
:
withPerm
,
'.interpreting'
:
withPerm
&&
rolePublisher
,
'svg.moderator'
:
roleModerator
,
'svg.user'
:
!
roleModerator
&&
!
roleInterpreter
,
'svg.interpreter'
:
!
roleModerator
&&
roleInterpreter
}
Object
.
keys
(
elements
).
forEach
(
key
=>
{
element
.
find
(
key
)[
elements
[
key
]
?
'removeClass'
:
'addClass'
](
'hidden'
)
})
element
.
find
(
'.action-role-publisher input'
).
prop
(
'checked'
,
rolePublisher
)
element
.
find
(
'.action-role-moderator input'
).
prop
(
'checked'
,
roleModerator
)
.
prop
(
'disabled'
,
roleOwner
)
element
.
find
(
'.interpreting select'
).
val
(
roleInterpreter
?
params
.
language
:
''
)
}
/**
* Update/refresh state of all participants' elements
*/
function
participantUpdateAll
()
{
Object
.
keys
(
peers
).
forEach
(
peerId
=>
{
const
peer
=
peers
[
peerId
]
participantUpdate
(
peer
.
element
,
peer
)
})
}
/**
* Create a tag-like element for a subscriber participant
*
* @param params Connection metadata/params
* @param content Optional content to prepend to the element
*/
function
subscriberCreate
(
params
,
content
)
{
// Create the element
let
wrapper
=
$
(
'<div class="meet-subscriber">'
).
append
(
nicknameWidget
(
params
))
if
(
content
)
{
wrapper
.
prepend
(
content
)
}
participantUpdate
(
wrapper
,
params
)
// Two event handlers below fix the dropdown in the subscribers list.
// Normally it gets hidden because of the overflow/height of the container.
// FIXME: I think it wasn't needed in BS4, but it's a problem with BS5.
const
nickname
=
wrapper
.
children
(
'.dropdown'
)[
0
]
nickname
.
addEventListener
(
'show.bs.dropdown'
,
()
=>
{
$
(
subscribersContainer
).
css
({
'overflow-y'
:
'unset'
,
height
:
$
(
subscribersContainer
).
height
()
+
'px'
})
})
nickname
.
addEventListener
(
'hide.bs.dropdown'
,
()
=>
{
$
(
subscribersContainer
).
css
({
'overflow-y'
:
'auto'
,
height
:
'unset'
})
})
return
wrapper
[
params
.
isSelf
?
'prependTo'
:
'appendTo'
](
subscribersContainer
)
.
attr
(
'id'
,
'subscriber-'
+
params
.
id
)
.
get
(
0
)
}
/**
* Create a tag-like nickname widget
*
* @param object params Peer metadata/params
*/
function
nicknameWidget
(
params
)
{
let
languages
=
[]
// Append languages selection options
Object
.
keys
(
sessionData
.
languages
).
forEach
(
code
=>
{
languages
.
push
(
`<option value="
${
code
}
">
${
$t
(
sessionData
.
languages
[
code
])
}
</option>`
)
})
// Create the element
let
element
=
$
(
'<div class="dropdown">'
+
'<a href="#" class="meet-nickname btn" aria-haspopup="true" aria-expanded="false" role="button">'
+
'<span class="content"></span>'
+
'<span class="icon">'
+
svgIcon
(
'user'
,
null
,
'user'
)
+
svgIcon
(
'crown'
,
null
,
'moderator hidden'
)
+
svgIcon
(
'headphones'
,
null
,
'interpreter hidden'
)
+
'</span>'
+
'</a>'
+
'<div class="dropdown-menu">'
+
'<a class="dropdown-item action-nickname" href="#">Nickname</a>'
+
'<a class="dropdown-item action-dismiss" href="#">Dismiss</a>'
+
'<div class="dropdown-divider permissions"></div>'
+
'<div class="permissions">'
+
'<h6 class="dropdown-header">'
+
$t
(
'meet.perm'
)
+
'</h6>'
+
'<label class="dropdown-item action-role-publisher form-check form-switch">'
+
'<input type="checkbox" class="form-check-input">'
+
' <span class="form-check-label">'
+
$t
(
'meet.perm-av'
)
+
'</span>'
+
'</label>'
+
'<label class="dropdown-item action-role-moderator form-check form-switch">'
+
'<input type="checkbox" class="form-check-input">'
+
' <span class="form-check-label">'
+
$t
(
'meet.perm-mod'
)
+
'</span>'
+
'</label>'
+
'</div>'
+
'<div class="dropdown-divider interpreting"></div>'
+
'<div class="interpreting">'
+
'<h6 class="dropdown-header">'
+
$t
(
'meet.lang-int'
)
+
'</h6>'
+
'<div class="ps-3 pe-3"><select class="form-select">'
+
'<option value="">- '
+
$t
(
'form.none'
)
+
' -</option>'
+
languages
.
join
(
''
)
+
'</select></div>'
+
'</div>'
+
'</div>'
+
'</div>'
)
let
nickname
=
element
.
find
(
'.meet-nickname'
)
.
addClass
(
'btn btn-outline-'
+
(
params
.
isSelf
?
'primary'
:
'secondary'
))
.
attr
({
title
:
$t
(
'meet.menu-options'
),
'data-bs-toggle'
:
'dropdown'
})
const
dropdown
=
new
Dropdown
(
nickname
[
0
],
{
boundary
:
container
.
parentNode
})
if
(
params
.
isSelf
)
{
// Add events for nickname change
let
editable
=
element
.
find
(
'.content'
)[
0
]
let
editableEnable
=
()
=>
{
editable
.
contentEditable
=
true
editable
.
focus
()
}
let
editableUpdate
=
()
=>
{
// Skip redundant update on blur, if it was already updated
if
(
editable
.
contentEditable
!==
'false'
)
{
editable
.
contentEditable
=
false
client
.
setNickname
(
editable
.
innerText
)
}
}
element
.
find
(
'.action-nickname'
).
on
(
'click'
,
editableEnable
)
element
.
find
(
'.action-dismiss'
).
remove
()
$
(
editable
).
on
(
'blur'
,
editableUpdate
)
.
on
(
'keydown'
,
e
=>
{
// Enter or Esc
if
(
e
.
keyCode
==
13
||
e
.
keyCode
==
27
)
{
editableUpdate
()
return
false
}
// Do not propagate the event, so it does not interfere with our
// keyboard shortcuts
e
.
stopPropagation
()
})
}
else
{
element
.
find
(
'.action-nickname'
).
remove
()
element
.
find
(
'.action-dismiss'
).
on
(
'click'
,
()
=>
{
client
.
kickPeer
(
params
.
id
)
})
}
// Don't close the menu on permission change
element
.
find
(
'.dropdown-menu > label'
).
on
(
'click'
,
e
=>
{
e
.
stopPropagation
()
})
element
.
find
(
'.action-role-publisher input'
).
on
(
'change'
,
e
=>
{
client
[
e
.
target
.
checked
?
'addRole'
:
'removeRole'
](
params
.
id
,
Roles
.
PUBLISHER
)
})
element
.
find
(
'.action-role-moderator input'
).
on
(
'change'
,
e
=>
{
client
[
e
.
target
.
checked
?
'addRole'
:
'removeRole'
](
params
.
id
,
Roles
.
MODERATOR
)
})
element
.
find
(
'.interpreting select'
)
.
on
(
'change'
,
e
=>
{
const
language
=
$
(
e
.
target
).
val
()
client
.
setLanguage
(
params
.
id
,
language
)
dropdown
.
hide
()
})
.
on
(
'click'
,
e
=>
{
// Prevents from closing the dropdown menu on click
e
.
stopPropagation
()
})
return
element
.
get
(
0
)
}
/**
* Window onresize event handler (updates room layout)
*/
function
resize
()
{
if
(
publishersContainer
)
{
updateLayout
()
}
$
(
container
).
parent
()[
window
.
screen
.
width
<=
768
?
'addClass'
:
'removeClass'
](
'mobile'
)
}
/**
* Update the room "matrix" layout
*/
function
updateLayout
()
{
let
publishers
=
$
(
publishersContainer
).
find
(
'.meet-video'
)
let
numOfVideos
=
publishers
.
length
if
(
sessionData
&&
sessionData
.
counterElement
)
{
sessionData
.
counterElement
.
innerHTML
=
Object
.
keys
(
peers
).
length
}
if
(
!
numOfVideos
)
{
subscribersContainer
.
style
.
minHeight
=
'auto'
return
}
// Note: offsetHeight/offsetWidth return rounded values, but for proper matrix
// calculations we need more precision, therefore we use getBoundingClientRect()
let
allHeight
=
container
.
offsetHeight
let
scrollHeight
=
subscribersContainer
.
scrollHeight
let
bcr
=
publishersContainer
.
getBoundingClientRect
()
let
containerWidth
=
bcr
.
width
let
containerHeight
=
bcr
.
height
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
// Make the first screen sharing tile big
let
screenVideo
=
publishers
.
filter
(
'.screen'
).
find
(
'video'
).
get
(
0
)
if
(
screenVideo
)
{
const
element
=
screenVideo
.
parentNode
const
peerId
=
element
.
id
.
replace
(
/^screen-/
,
''
)
const
peer
=
peers
[
peerId
]
// We know the shared screen video dimensions, we can calculate
// width/height of the tile in the matrix
if
(
peer
&&
peer
.
screenWidth
)
{
let
screenWidth
=
peer
.
screenWidth
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
if
(
factor
>=
16
/
9
)
{
if
(
numOfVideos
<=
3
)
{
rows
=
1
}
else
if
(
numOfVideos
<=
8
)
{
rows
=
2
}
else
if
(
numOfVideos
<=
15
)
{
rows
=
3
}
else
if
(
numOfVideos
<=
20
)
{
rows
=
4
}
else
{
rows
=
5
}
cols
=
Math
.
ceil
(
numOfVideos
/
rows
)
}
else
{
if
(
numOfVideos
==
1
)
{
cols
=
1
}
else
if
(
numOfVideos
<=
4
)
{
cols
=
2
}
else
if
(
numOfVideos
<=
9
)
{
cols
=
3
}
else
if
(
numOfVideos
<=
16
)
{
cols
=
4
}
else
if
(
numOfVideos
<=
25
)
{
cols
=
5
}
else
{
cols
=
6
}
rows
=
Math
.
ceil
(
numOfVideos
/
cols
)
if
(
rows
<
cols
&&
containerWidth
<
containerHeight
)
{
cols
=
rows
rows
=
Math
.
ceil
(
numOfVideos
/
cols
)
}
}
// console.log('factor=' + factor, 'num=' + numOfVideos, 'cols = '+cols, 'rows=' + rows)
// Update all tiles (except the main shared screen) in the matrix
publishers
.
css
({
width
:
(
containerWidth
/
cols
)
+
'px'
,
// Height must be in pixels to make object-fit:cover working
height
:
(
containerHeight
/
rows
)
+
'px'
})
}
/**
* Create an svg element (string) for a FontAwesome icon
*
* @todo Find if there's a "official" way to do this
*/
function
svgIcon
(
name
,
type
,
className
)
{
// Note: the library will contain definitions for all icons registered elswhere
const
icon
=
library
.
definitions
[
type
||
'fas'
][
name
]
let
attrs
=
{
'class'
:
'svg-inline--fa'
,
'aria-hidden'
:
true
,
focusable
:
false
,
role
:
'img'
,
xmlns
:
'http://www.w3.org/2000/svg'
,
viewBox
:
`0 0
${
icon
[
0
]
}
${
icon
[
1
]
}
`
}
if
(
className
)
{
attrs
[
'class'
]
+=
' '
+
className
}
return
$
(
`<svg><path fill="currentColor" d="
${
icon
[
4
]
}
"></path></svg>`
)
.
attr
(
attrs
)
.
get
(
0
).
outerHTML
}
}
export
{
Room
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 24, 10:56 AM (1 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18870729
Default Alt Text
room.js (38 KB)
Attached To
Mode
rK kolab
Attached
Detach File
Event Timeline