Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
galene
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
nexedi
galene
Commits
0aa77441
Commit
0aa77441
authored
Sep 20, 2020
by
Juliusz Chroboczek
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Improve typing of javascript code.
We now enable typing of sfu.js.
parent
4e14c29f
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
266 additions
and
118 deletions
+266
-118
static/protocol.js
static/protocol.js
+70
-49
static/sfu.js
static/sfu.js
+188
-67
static/tsconfig.json
static/tsconfig.json
+8
-2
No files found.
static/protocol.js
View file @
0aa77441
...
...
@@ -7,7 +7,8 @@
/**
* toHex formats an array as a hexadecimal string.
* @returns {string}
* @param {number[]|Uint8Array} array - the array to format
* @returns {string} - the hexadecimal representation of array
*/
function
toHex
(
array
)
{
let
a
=
new
Uint8Array
(
array
);
...
...
@@ -37,43 +38,52 @@ function randomid() {
function
ServerConnection
()
{
/**
* The id of this connection.
*
* @type {string}
* @const
*/
this
.
id
=
randomid
();
/**
* The group that we have joined, or nil if we haven't joined yet.
*
* @type {string}
*/
this
.
group
=
null
;
/**
* The underlying websocket.
*
* @type {WebSocket}
*/
this
.
socket
=
null
;
/**
* The set of all up streams, indexed by their id.
* @type {Object.<string,Stream>}
*
* @type {Object<string,Stream>}
*/
this
.
up
=
{};
/**
* The set of all down streams, indexed by their id.
* @type {Object.<string,Stream>}
*
* @type {Object<string,Stream>}
*/
this
.
down
=
{};
/**
* The ICE configuration used by all associated streams.
* @type {Array.<RTCIceServer>}
*
* @type {RTCIceServer[]}
*/
this
.
iceServers
=
null
;
/**
* The permissions granted to this connection.
* @type {Object.<string,boolean>}
*
* @type {Object<string,boolean>}
*/
this
.
permissions
=
{};
/**
* clientdata is a convenient place to attach data to a ServerConnection.
* It is not used by the library.
* @type{Object.<string,any>}
*
* @type{Object<any,any>}
*/
this
.
userdata
=
{};
...
...
@@ -81,46 +91,54 @@ function ServerConnection() {
/**
* onconnected is called when the connection has been established
* @type{function(): any}
*
* @type{(this: ServerConnection) => any}
*/
this
.
onconnected
=
null
;
/**
* onclose is called when the connection is closed
* @type{function(number, string): any}
*
* @type{(this: ServerConnection, code: number, reason: string) => any}
*/
this
.
onclose
=
null
;
/**
* onuser is called whenever a user is added or removed from the group
* @type{function(string, string, string): any}
*
* @type{(this: ServerConnection, id: string, kind: string, username: string) => any}
*/
this
.
onuser
=
null
;
/**
* onpermissions is called whenever the current user's permissions change
* @type{function(Object.<string,boolean>): any}
*
* @type{(this: ServerConnection, permissions: Object<string,boolean>) => any}
*/
this
.
onpermissions
=
null
;
/**
* ondownstream is called whenever a new down stream is added. It
* should set up the stream's callbacks; actually setting up the UI
* should be done in the stream's ondowntrack callback.
* @type{function(Stream): any}
*
* @type{(this: ServerConnection, stream: Stream) => any}
*/
this
.
ondownstream
=
null
;
/**
* onchat is called whenever a new chat message is received.
* @type {function(string, string, string, string): any}
*
* @type {(this: ServerConnection, id: string, username: string, kind: string, message: string) => any}
*/
this
.
onchat
=
null
;
/**
* onclearchat is called whenever the server requests that the chat
* be cleared.
* @type{function(): any}
*
* @type{(this: ServerConnection) => any}
*/
this
.
onclearchat
=
null
;
/**
* onusermessage is called when the server sends an error or warning
* message that should be displayed to the user.
* @type{function(string, string): any}
*
* @type{(this: ServerConnection, kind: string, message: string) => any}
*/
this
.
onusermessage
=
null
;
}
...
...
@@ -132,14 +150,14 @@ function ServerConnection() {
* @property {string} [id]
* @property {string} [username]
* @property {string} [password]
* @property {Object
.
<string,boolean>} [permissions]
* @property {Object<string,boolean>} [permissions]
* @property {string} [group]
* @property {string} [value]
* @property {RTCSessionDescriptionInit} [offer]
* @property {RTCSessionDescriptionInit} [answer]
* @property {RTCIceCandidate} [candidate]
* @property {Object
.
<string,string>} [labels]
* @property {Object
.
<string,(boolean|number)>} [request]
* @property {Object<string,string>} [labels]
* @property {Object<string,(boolean|number)>} [request]
*/
/**
...
...
@@ -157,17 +175,18 @@ ServerConnection.prototype.close = function() {
*/
ServerConnection
.
prototype
.
send
=
function
(
m
)
{
if
(
!
this
.
socket
||
this
.
socket
.
readyState
!==
this
.
socket
.
OPEN
)
{
// send on a closed
connection
doesn't throw
// send on a closed
socket
doesn't throw
throw
(
new
Error
(
'
Connection is not open
'
));
}
return
this
.
socket
.
send
(
JSON
.
stringify
(
m
));
}
/** getIceServers fetches an ICE configuration from the server and
/**
* getIceServers fetches an ICE configuration from the server and
* populates the iceServers field of a ServerConnection. It is called
* lazily by connect.
*
* @returns {Promise<
Array.<Object>
>}
* @returns {Promise<
RTCIceServer[]
>}
* @function
*/
ServerConnection
.
prototype
.
getIceServers
=
async
function
()
{
...
...
@@ -294,8 +313,8 @@ ServerConnection.prototype.connect = async function(url) {
/**
* login authenticates with the server.
*
* @param {string} username
* @param {string} password
* @param {string} username
- the username to login as.
* @param {string} password
- the password.
*/
ServerConnection
.
prototype
.
login
=
function
(
username
,
password
)
{
this
.
send
({
...
...
@@ -324,7 +343,7 @@ ServerConnection.prototype.join = function(group) {
* @param {string} what - One of '', 'audio', 'screenshare' or 'everything'.
*/
ServerConnection
.
prototype
.
request
=
function
(
what
)
{
/** @type {Object
.
<string,boolean>} */
/** @type {Object<string,boolean>} */
let
request
=
{};
switch
(
what
)
{
case
''
:
...
...
@@ -422,7 +441,7 @@ ServerConnection.prototype.chat = function(username, kind, message) {
*
* @param {string} kind - One of "clearchat", "lock", "unlock", "record or
* "unrecord".
* @param {string} [message]
* @param {string} [message]
- An optional user-readable message.
*/
ServerConnection
.
prototype
.
groupAction
=
function
(
kind
,
message
)
{
this
.
send
({
...
...
@@ -436,8 +455,8 @@ ServerConnection.prototype.groupAction = function(kind, message) {
* userAction sends a request to act on a user.
*
* @param {string} kind - One of "op", "unop", "kick", "present", "unpresent".
* @param {string} id
* @param {string} [message]
* @param {string} id
- The id of the user to act upon.
* @param {string} [message]
- An optional user-readable message.
*/
ServerConnection
.
prototype
.
userAction
=
function
(
kind
,
id
,
message
)
{
this
.
send
({
...
...
@@ -452,7 +471,7 @@ ServerConnection.prototype.userAction = function(kind, id, message) {
* Called when we receive an offer from the server. Don't call this.
*
* @param {string} id
* @param {Object
.
<string, string>} labels
* @param {Object<string, string>} labels
* @param {RTCSessionDescriptionInit} offer
* @param {boolean} renegotiate
* @function
...
...
@@ -656,13 +675,15 @@ function Stream(sc, id, pc) {
* The associated ServerConnection.
*
* @type {ServerConnection}
*/
* @const
*/
this
.
sc
=
sc
;
/**
* The id of this stream.
*
* @type {string}
*/
* @const
*/
this
.
id
=
id
;
/**
* For up streams, one of "local" or "screenshare".
...
...
@@ -693,20 +714,20 @@ function Stream(sc, id, pc) {
/**
* Track labels, indexed by track id.
*
* @type {Object
.
<string,string>}
* @type {Object<string,string>}
*/
this
.
labels
=
{};
/**
* Track labels, indexed by mid.
*
* @type {Object
.
<string,string>}
* @type {Object<string,string>}
*/
this
.
labelsByMid
=
{};
/**
* Buffered ICE candidates. This will be flushed by flushIceCandidates
* when the PC becomes stable.
*
* @type {
Array.<RTCIceCandidate>
}
* @type {
RTCIceCandidate[]
}
*/
this
.
iceCandidates
=
[];
/**
...
...
@@ -721,7 +742,7 @@ function Stream(sc, id, pc) {
* a dictionary indexed by track id, with each value a disctionary of
* statistics.
*
* @type {Object
.
<string,any>}
* @type {Object<string,any>}
*/
this
.
stats
=
{};
/**
...
...
@@ -734,7 +755,7 @@ function Stream(sc, id, pc) {
/**
* clientdata is a convenient place to attach data to a Stream.
* It is not used by the library.
* @type{Object
.<string
,any>}
* @type{Object
<any
,any>}
*/
this
.
userdata
=
{};
...
...
@@ -743,21 +764,21 @@ function Stream(sc, id, pc) {
/**
* onclose is called when the stream is closed.
*
* @type{
function():
any}
* @type{
(this: Stream) =>
any}
*/
this
.
onclose
=
null
;
/**
* onerror is called whenever an error occurs. If the error is
* fatal, then onclose will be called afterwards.
*
* @type{
function(any):
any}
* @type{
(this: Stream, error: any) =>
any}
*/
this
.
onerror
=
null
;
/**
* onnegotiationcompleted is called whenever negotiation or
* renegotiation has completed.
*
* @type{
function():
any}
* @type{
(this: Stream) =>
any}
*/
this
.
onnegotiationcompleted
=
null
;
/**
...
...
@@ -765,32 +786,32 @@ function Stream(sc, id, pc) {
* If the stream parameter differs from its previous value, then it
* indicates that the old stream has been discarded.
*
* @type{
function(MediaStreamTrack, RTCRtpTransceiver, string, MediaStream):
any}
* @type{
(this: Stream, track: MediaStreamTrack, transceiver: RTCRtpTransceiver, label: string, stream: MediaStream) =>
any}
*/
this
.
ondowntrack
=
null
;
/**
* onlabel is called whenever the server sets a new label for the stream.
*
* @type{
function(string):
any}
* @type{
(this: Stream, label: string) =>
any}
*/
this
.
onlabel
=
null
;
/**
* onstatus is called whenever the status of the stream changes.
*
* @type{
function(string):
any}
* @type{
(this: Stream, status: string) =>
any}
*/
this
.
onstatus
=
null
;
/**
* onabort is called when the server requested that an up stream be
* closed. It is the resposibility of the client to close the stream.
*
* @type{
function():
any}
* @type{
(this: Stream) =>
any}
*/
this
.
onabort
=
null
;
/**
* onstats is called when we have new statistics about the connection
*
* @type{
function(Object.<string,any>):
any}
* @type{
(this: Stream, stats: Object<any,any>) =>
any}
*/
this
.
onstats
=
null
;
}
...
...
@@ -834,6 +855,7 @@ Stream.prototype.close = function(sendclose) {
* @function
*/
Stream
.
prototype
.
flushIceCandidates
=
async
function
()
{
/** @type {Promise<any>[]} */
let
promises
=
[];
this
.
iceCandidates
.
forEach
(
c
=>
{
promises
.
push
(
this
.
pc
.
addIceCandidate
(
c
).
catch
(
console
.
warn
));
...
...
@@ -845,16 +867,16 @@ Stream.prototype.flushIceCandidates = async function () {
/**
* negotiate negotiates or renegotiates an up stream. It is called
* automatically when required. If the client requires renegotiation, it
* is probably
more effective to call restartIce on the underlying PC
*
rather than invoking this function direct
ly.
* is probably
better to call restartIce which will cause negotiate to be
*
called asynchronous
ly.
*
* @function
* @param {boolean} [restartIce]
* @param {boolean} [restartIce]
- Whether to restart ICE.
*/
Stream
.
prototype
.
negotiate
=
async
function
(
restartIce
)
{
let
c
=
this
;
let
options
=
null
;
let
options
=
{}
;
if
(
restartIce
)
options
=
{
iceRestart
:
true
};
let
offer
=
await
c
.
pc
.
createOffer
(
options
);
...
...
@@ -892,8 +914,7 @@ Stream.prototype.negotiate = async function (restartIce) {
Stream
.
prototype
.
restartIce
=
function
()
{
let
c
=
this
;
/** @ts-ignore */
if
(
typeof
c
.
pc
.
restartIce
===
'
function
'
)
{
if
(
'
restartIce
'
in
c
.
pc
)
{
try
{
/** @ts-ignore */
c
.
pc
.
restartIce
();
...
...
@@ -988,7 +1009,7 @@ Stream.prototype.updateStats = async function() {
* setStatsInterval sets the interval in milliseconds at which the onstats
* handler will be called. This is only useful for up streams.
*
* @param {number} ms
* @param {number} ms
- The interval in milliseconds.
*/
Stream
.
prototype
.
setStatsInterval
=
function
(
ms
)
{
let
c
=
this
;
...
...
static/sfu.js
View file @
0aa77441
...
...
@@ -103,9 +103,8 @@ function storeSettings(settings) {
*
* @returns {settings}
*/
function
getSettings
()
{
/** @type{settings} */
/** @type
{settings} */
let
settings
;
try
{
let
json
=
window
.
sessionStorage
.
getItem
(
'
settings
'
);
...
...
@@ -127,22 +126,50 @@ function updateSettings(settings) {
storeSettings
(
s
);
}
/**
* @param {string} id
*/
function
getSelectElement
(
id
)
{
let
elt
=
document
.
getElementById
(
id
);
if
(
!
elt
||
!
(
elt
instanceof
HTMLSelectElement
))
throw
new
Error
(
`Couldn't find
${
id
}
`
);
return
elt
;
}
/**
* @param {string} id
*/
function
getInputElement
(
id
)
{
let
elt
=
document
.
getElementById
(
id
);
if
(
!
elt
||
!
(
elt
instanceof
HTMLInputElement
))
throw
new
Error
(
`Couldn't find
${
id
}
`
);
return
elt
;
}
/**
* @param {string} id
*/
function
getButtonElement
(
id
)
{
let
elt
=
document
.
getElementById
(
id
);
if
(
!
elt
||
!
(
elt
instanceof
HTMLButtonElement
))
throw
new
Error
(
`Couldn't find
${
id
}
`
);
return
elt
;
}
function
reflectSettings
()
{
let
settings
=
getSettings
();
let
store
=
false
;
setLocalMute
(
settings
.
localMute
);
let
videoselect
=
/** @type {HTMLSelectElement} */
(
document
.
getElementById
(
'
videoselect
'
));
let
videoselect
=
getSelectElement
(
'
videoselect
'
);
if
(
!
settings
.
video
||
!
selectOptionAvailable
(
videoselect
,
settings
.
video
))
{
settings
.
video
=
selectOptionDefault
(
videoselect
);
store
=
true
;
}
videoselect
.
value
=
settings
.
video
;
let
audioselect
=
/** @type {HTMLSelectElement} */
(
document
.
getElementById
(
'
audioselect
'
));
let
audioselect
=
getSelectElement
(
'
audioselect
'
);
if
(
!
settings
.
audio
||
!
selectOptionAvailable
(
audioselect
,
settings
.
audio
))
{
settings
.
audio
=
selectOptionDefault
(
audioselect
);
store
=
true
;
...
...
@@ -150,24 +177,24 @@ function reflectSettings() {
audioselect
.
value
=
settings
.
audio
;
if
(
settings
.
request
)
document
.
getElementById
(
'
requestselect
'
).
value
=
settings
.
request
;
getSelectElement
(
'
requestselect
'
).
value
=
settings
.
request
;
else
{
settings
.
request
=
document
.
getElementById
(
'
requestselect
'
).
value
;
settings
.
request
=
getSelectElement
(
'
requestselect
'
).
value
;
store
=
true
;
}
if
(
settings
.
send
)
document
.
getElementById
(
'
sendselect
'
).
value
=
settings
.
send
;
getSelectElement
(
'
sendselect
'
).
value
=
settings
.
send
;
else
{
settings
.
send
=
document
.
getElementById
(
'
sendselect
'
).
value
;
settings
.
send
=
getSelectElement
(
'
sendselect
'
).
value
;
store
=
true
;
}
document
.
getElementById
(
'
activitybox
'
).
checked
=
settings
.
activityDetection
;
getInputElement
(
'
activitybox
'
).
checked
=
settings
.
activityDetection
;
document
.
getElementById
(
'
blackboardbox
'
).
checked
=
settings
.
blackboardMode
;
getInputElement
(
'
blackboardbox
'
).
checked
=
settings
.
blackboardMode
;
document
.
getElementById
(
'
studiobox
'
).
checked
=
settings
.
studioMode
;
getInputElement
(
'
studiobox
'
).
checked
=
settings
.
studioMode
;
if
(
store
)
storeSettings
(
settings
);
...
...
@@ -218,9 +245,9 @@ function setConnected(connected) {
}
else
{
resetUsers
();
let
userpass
=
getUserPass
();
document
.
getElementById
(
'
username
'
).
value
=
getInputElement
(
'
username
'
).
value
=
userpass
?
userpass
.
username
:
''
;
document
.
getElementById
(
'
password
'
).
value
=
getInputElement
(
'
password
'
).
value
=
userpass
?
userpass
.
password
:
''
;
statspan
.
textContent
=
'
Disconnected
'
;
statspan
.
classList
.
remove
(
'
connected
'
);
...
...
@@ -234,6 +261,7 @@ function setConnected(connected) {
}
}
/** @this {ServerConnection} */
function
gotConnected
()
{
setConnected
(
true
);
let
up
=
getUserPass
();
...
...
@@ -243,6 +271,7 @@ function gotConnected() {
}
/**
* @this {ServerConnection}
* @param {number} code
* @param {string} reason
*/
...
...
@@ -255,6 +284,7 @@ function gotClose(code, reason) {
}
/**
* @this {ServerConnection}
* @param {Stream} c
*/
function
gotDownStream
(
c
)
{
...
...
@@ -291,12 +321,12 @@ setViewportHeight();
addEventListener
(
'
resize
'
,
setViewportHeight
);
addEventListener
(
'
orientationchange
'
,
setViewportHeight
);
document
.
getElementById
(
'
presentbutton
'
).
onclick
=
function
(
e
)
{
getButtonElement
(
'
presentbutton
'
).
onclick
=
function
(
e
)
{
e
.
preventDefault
();
addLocalMedia
();
};
document
.
getElementById
(
'
unpresentbutton
'
).
onclick
=
function
(
e
)
{
getButtonElement
(
'
unpresentbutton
'
).
onclick
=
function
(
e
)
{
e
.
preventDefault
();
delUpMediaKind
(
'
local
'
);
resizePeers
();
...
...
@@ -353,26 +383,34 @@ function setLocalMute(mute) {
}
}
document
.
getElementById
(
'
videoselect
'
).
onchange
=
function
(
e
)
{
getSelectElement
(
'
videoselect
'
).
onchange
=
function
(
e
)
{
e
.
preventDefault
();
if
(
!
(
this
instanceof
HTMLSelectElement
))
throw
new
Error
(
'
Unexpected type for this
'
);
updateSettings
({
video
:
this
.
value
});
changePresentation
();
};
document
.
getElementById
(
'
audioselect
'
).
onchange
=
function
(
e
)
{
getSelectElement
(
'
audioselect
'
).
onchange
=
function
(
e
)
{
e
.
preventDefault
();
if
(
!
(
this
instanceof
HTMLSelectElement
))
throw
new
Error
(
'
Unexpected type for this
'
);
updateSettings
({
audio
:
this
.
value
});
changePresentation
();
};
document
.
getElementById
(
'
blackboardbox
'
).
onchange
=
function
(
e
)
{
getInputElement
(
'
blackboardbox
'
).
onchange
=
function
(
e
)
{
e
.
preventDefault
();
if
(
!
(
this
instanceof
HTMLInputElement
))
throw
new
Error
(
'
Unexpected type for this
'
);
updateSettings
({
blackboardMode
:
this
.
checked
});
changePresentation
();
}
document
.
getElementById
(
'
studiobox
'
).
onchange
=
function
(
e
)
{
getInputElement
(
'
studiobox
'
).
onchange
=
function
(
e
)
{
e
.
preventDefault
();
if
(
!
(
this
instanceof
HTMLInputElement
))
throw
new
Error
(
'
Unexpected type for this
'
);
updateSettings
({
studioMode
:
this
.
checked
});
changePresentation
();
}
...
...
@@ -413,7 +451,9 @@ function getMaxVideoThroughput() {
}
}
document
.
getElementById
(
'
sendselect
'
).
onchange
=
async
function
(
e
)
{
getSelectElement
(
'
sendselect
'
).
onchange
=
async
function
(
e
)
{
if
(
!
(
this
instanceof
HTMLSelectElement
))
throw
new
Error
(
'
Unexpected type for this
'
);
updateSettings
({
send
:
this
.
value
});
let
t
=
getMaxVideoThroughput
();
for
(
let
id
in
serverConnection
.
up
)
{
...
...
@@ -423,8 +463,10 @@ document.getElementById('sendselect').onchange = async function(e) {
}
}
document
.
getElementById
(
'
requestselect
'
).
onchange
=
function
(
e
)
{
getSelectElement
(
'
requestselect
'
).
onchange
=
function
(
e
)
{
e
.
preventDefault
();
if
(
!
(
this
instanceof
HTMLSelectElement
))
throw
new
Error
(
'
Unexpected type for this
'
);
updateSettings
({
request
:
this
.
value
});
serverConnection
.
request
(
this
.
value
);
};
...
...
@@ -433,7 +475,9 @@ const activityDetectionInterval = 200;
const
activityDetectionPeriod
=
700
;
const
activityDetectionThreshold
=
0.2
;
document
.
getElementById
(
'
activitybox
'
).
onchange
=
function
(
e
)
{
getInputElement
(
'
activitybox
'
).
onchange
=
function
(
e
)
{
if
(
!
(
this
instanceof
HTMLInputElement
))
throw
new
Error
(
'
Unexpected type for this
'
);
updateSettings
({
activityDetection
:
this
.
checked
});
for
(
let
id
in
serverConnection
.
down
)
{
let
c
=
serverConnection
.
down
[
id
];
...
...
@@ -446,6 +490,10 @@ document.getElementById('activitybox').onchange = function(e) {
}
}
/**
* @this {Stream}
* @param {Object<string,any>} stats
*/
function
gotUpStats
(
stats
)
{
let
c
=
this
;
...
...
@@ -477,8 +525,12 @@ function setActive(c, value) {
peer
.
classList
.
remove
(
'
peer-active
'
);
}
/**
* @this {Stream}
* @param {Object<string,any>} stats
*/
function
gotDownStats
(
stats
)
{
if
(
!
document
.
getElementById
(
'
activitybox
'
).
checked
)
if
(
!
getInputElement
(
'
activitybox
'
).
checked
)
return
;
let
c
=
this
;
...
...
@@ -514,7 +566,11 @@ function addSelectOption(select, label, value) {
if
(
!
value
)
value
=
label
;
for
(
let
i
=
0
;
i
<
select
.
children
.
length
;
i
++
)
{
let
child
=
/** @type {HTMLOptionElement} */
(
select
.
children
[
i
]);
let
child
=
select
.
children
[
i
];
if
(
!
(
child
instanceof
HTMLOptionElement
))
{
console
.
warn
(
'
Unexpected select child
'
);
continue
;
}
if
(
child
.
value
===
value
)
{
if
(
child
.
label
!==
label
)
{
child
.
label
=
label
;
...
...
@@ -536,10 +592,13 @@ function addSelectOption(select, label, value) {
function
selectOptionAvailable
(
select
,
value
)
{
let
children
=
select
.
children
;
for
(
let
i
=
0
;
i
<
children
.
length
;
i
++
)
{
let
child
=
/** @type {HTMLOptionElement} */
(
select
.
children
[
i
]);
if
(
child
.
value
===
value
)
{
return
true
;
let
child
=
select
.
children
[
i
];
if
(
!
(
child
instanceof
HTMLOptionElement
))
{
console
.
warn
(
'
Unexpected select child
'
);
continue
;
}
if
(
child
.
value
===
value
)
return
true
;
}
return
false
;
}
...
...
@@ -551,7 +610,11 @@ function selectOptionAvailable(select, value) {
function
selectOptionDefault
(
select
)
{
/* First non-empty option. */
for
(
let
i
=
0
;
i
<
select
.
children
.
length
;
i
++
)
{
let
child
=
/** @type {HTMLOptionElement} */
(
select
.
children
[
i
]);
let
child
=
select
.
children
[
i
];
if
(
!
(
child
instanceof
HTMLOptionElement
))
{
console
.
warn
(
'
Unexpected select child
'
);
continue
;
}
if
(
child
.
value
)
return
child
.
value
;
}
...
...
@@ -587,13 +650,13 @@ async function setMediaChoices(done) {
if
(
d
.
kind
===
'
videoinput
'
)
{
if
(
!
label
)
label
=
`Camera
${
cn
}
`
;
addSelectOption
(
document
.
getElementById
(
'
videoselect
'
),
addSelectOption
(
getSelectElement
(
'
videoselect
'
),
label
,
d
.
deviceId
);
cn
++
;
}
else
if
(
d
.
kind
===
'
audioinput
'
)
{
if
(
!
label
)
label
=
`Microphone
${
mn
}
`
;
addSelectOption
(
document
.
getElementById
(
'
audioselect
'
),
addSelectOption
(
getSelectElement
(
'
audioselect
'
),
label
,
d
.
deviceId
);
mn
++
;
}
...
...
@@ -693,6 +756,7 @@ async function addLocalMedia(id) {
stopUpMedia
(
old
);
let
constraints
=
{
audio
:
audio
,
video
:
video
};
/** @type {MediaStream} */
let
stream
=
null
;
try
{
stream
=
await
navigator
.
mediaDevices
.
getUserMedia
(
constraints
);
...
...
@@ -717,11 +781,11 @@ async function addLocalMedia(id) {
t
.
enabled
=
false
;
}
else
if
(
t
.
kind
==
'
video
'
)
{
if
(
settings
.
blackboardMode
)
{
if
(
'
contentHint
'
in
t
)
t
.
contentHint
=
'
detail
'
;
/** @ts-ignore */
t
.
contentHint
=
'
detail
'
;
}
}
let
sender
=
c
.
pc
.
addTrack
(
t
,
stream
);
c
.
pc
.
addTrack
(
t
,
stream
);
});
c
.
onstats
=
gotUpStats
;
...
...
@@ -730,12 +794,16 @@ async function addLocalMedia(id) {
setButtonsVisibility
();
}
async
function
addShareMedia
(
setup
)
{
async
function
addShareMedia
()
{
if
(
!
getUserPass
())
return
;
/** @type {MediaStream} */
let
stream
=
null
;
try
{
if
(
!
(
'
getDisplayMedia
'
in
navigator
.
mediaDevices
))
throw
new
Error
(
'
Your browser does not support screen sharing
'
);
/** @ts-ignore */
stream
=
await
navigator
.
mediaDevices
.
getDisplayMedia
({
video
:
true
});
}
catch
(
e
)
{
console
.
error
(
e
);
...
...
@@ -747,7 +815,7 @@ async function addShareMedia(setup) {
c
.
kind
=
'
screenshare
'
;
c
.
stream
=
stream
;
stream
.
getTracks
().
forEach
(
t
=>
{
let
sender
=
c
.
pc
.
addTrack
(
t
,
stream
);
c
.
pc
.
addTrack
(
t
,
stream
);
t
.
onended
=
e
=>
{
delUpMedia
(
c
);
};
...
...
@@ -807,6 +875,9 @@ function delUpMediaKind(kind) {
hideVideo
();
}
/**
* @param {string} kind
*/
function
findUpMedia
(
kind
)
{
for
(
let
id
in
serverConnection
.
up
)
{
if
(
serverConnection
.
up
[
id
].
kind
===
kind
)
...
...
@@ -815,6 +886,9 @@ function findUpMedia(kind) {
return
null
;
}
/**
* @param {boolean} mute
*/
function
muteLocalTracks
(
mute
)
{
if
(
!
serverConnection
)
return
;
...
...
@@ -846,12 +920,14 @@ function setMedia(c, isUp) {
peersdiv
.
appendChild
(
div
);
}
let
media
=
document
.
getElementById
(
'
media-
'
+
c
.
id
);
let
media
=
/** @type {HTMLVideoElement} */
(
document
.
getElementById
(
'
media-
'
+
c
.
id
));
if
(
!
media
)
{
media
=
document
.
createElement
(
'
video
'
);
media
.
id
=
'
media-
'
+
c
.
id
;
media
.
classList
.
add
(
'
media
'
);
media
.
autoplay
=
true
;
/** @ts-ignore */
media
.
playsinline
=
true
;
media
.
controls
=
true
;
if
(
isUp
)
...
...
@@ -883,7 +959,9 @@ function delMedia(id) {
let
peer
=
document
.
getElementById
(
'
peer-
'
+
id
);
if
(
!
peer
)
throw
new
Error
(
'
Removing unknown media
'
);
let
media
=
document
.
getElementById
(
'
media-
'
+
id
);
let
media
=
/** @type{HTMLVideoElement} */
(
document
.
getElementById
(
'
media-
'
+
id
));
media
.
srcObject
=
null
;
mediadiv
.
removeChild
(
peer
);
...
...
@@ -941,14 +1019,19 @@ function resizePeers() {
if
(
!
count
)
// No video, nothing to resize.
return
;
let
size
=
100
/
columns
;
let
container
=
document
.
getElementById
(
"
video-container
"
)
// Peers div has total padding of 30px, we remove 30 on offsetHeight
let
max_video_height
=
Math
.
trunc
((
peers
.
offsetHeight
-
30
)
/
columns
);
let
media_list
=
document
.
getElementsByClassName
(
"
media
"
);
for
(
let
i
=
0
;
i
<
media_list
.
length
;
i
++
)
media_list
[
i
].
style
[
'
max_height
'
]
=
max_video_height
+
"
px
"
;
for
(
let
i
=
0
;
i
<
media_list
.
length
;
i
++
)
{
let
media
=
media_list
[
i
];
if
(
!
(
media
instanceof
HTMLMediaElement
))
{
console
.
warn
(
'
Unexpected media
'
);
continue
;
}
media
.
style
[
'
max_height
'
]
=
max_video_height
+
"
px
"
;
}
if
(
count
<=
2
&&
container
.
offsetHeight
>
container
.
offsetWidth
)
{
peers
.
style
[
'
grid-template-columns
'
]
=
"
repeat(1, 1fr)
"
;
...
...
@@ -957,7 +1040,7 @@ function resizePeers() {
}
}
/** @type{Object
.
<string,string>} */
/** @type{Object<string,string>} */
let
users
=
{};
/**
...
...
@@ -1051,7 +1134,7 @@ function clearUsername() {
}
/**
* @param {Object
.
<string,boolean>} perms
* @param {Object<string,boolean>} perms
*/
function
gotPermissions
(
perms
)
{
displayUsername
();
...
...
@@ -1064,7 +1147,7 @@ const urlRegexp = /https?:\/\/[-a-zA-Z0-9@:%/._\\+~#=?]+[-a-zA-Z0-9@:%/_\\+~#=]/
/**
* @param {string} line
* @returns {
Array.<Text|HTMLElement>
}
* @returns {
(Text|HTMLElement)[]
}
*/
function
formatLine
(
line
)
{
let
r
=
new
RegExp
(
urlRegexp
);
...
...
@@ -1088,7 +1171,7 @@ function formatLine(line) {
}
/**
* @param {
Array.<string>
} lines
* @param {
string[]
} lines
* @returns {HTMLElement}
*/
function
formatLines
(
lines
)
{
...
...
@@ -1113,6 +1196,12 @@ function formatLines(lines) {
/** @type {lastMessage} */
let
lastMessage
=
{};
/**
* @param {string} peerId
* @param {string} nick
* @param {string} kind
* @param {string} message
*/
function
addToChatbox
(
peerId
,
nick
,
kind
,
message
){
let
userpass
=
getUserPass
();
let
row
=
document
.
createElement
(
'
div
'
);
...
...
@@ -1174,7 +1263,7 @@ function clearChat() {
* part may be quoted and may include backslash escapes.
*
* @param {string} line
* @returns {
Array.<string>
}
* @returns {
string[]
}
*/
function
parseCommand
(
line
)
{
let
i
=
0
;
...
...
@@ -1204,7 +1293,8 @@ function parseCommand(line) {
}
function
handleInput
()
{
let
input
=
document
.
getElementById
(
'
input
'
);
let
input
=
/** @type {HTMLTextAreaElement} */
(
document
.
getElementById
(
'
input
'
));
let
data
=
input
.
value
;
input
.
value
=
''
;
...
...
@@ -1356,39 +1446,66 @@ function chatResizer(e) {
document
.
getElementById
(
'
resizer
'
).
addEventListener
(
'
mousedown
'
,
chatResizer
,
false
);
/** @enum {string} */
const
MessageLevel
=
{
info
:
'
info
'
,
warning
:
'
warning
'
,
error
:
'
error
'
,
}
function
displayError
(
message
,
level
,
position
,
gravity
)
{
var
background
=
"
linear-gradient(to right, #e20a0a, #df2d2d)
"
;
if
(
level
===
"
info
"
)
{
background
=
"
linear-gradient(to right, #529518, #96c93d)
"
;
}
if
(
level
===
"
warning
"
)
{
background
=
"
linear-gradient(to right, #edd800, #c9c200)
"
;
/**
* @param {string} message
* @param {MessageLevel} [level]
*/
function
displayError
(
message
,
level
)
{
if
(
!
level
)
level
=
MessageLevel
.
error
;
var
background
=
'
linear-gradient(to right, #e20a0a, #df2d2d)
'
;
var
position
=
'
center
'
;
var
gravity
=
'
top
'
;
switch
(
level
)
{
case
MessageLevel
.
info
:
background
=
'
linear-gradient(to right, #529518, #96c93d)
'
;
position
=
'
right
'
;
gravity
=
'
bottom
'
;
break
;
case
MessageLevel
.
warning
:
background
=
"
linear-gradient(to right, #edd800, #c9c200)
"
;
break
;
}
/** @ts-ignore */
Toastify
({
text
:
message
,
duration
:
4000
,
close
:
true
,
position
:
position
?
position
:
'
center
'
,
gravity
:
gravity
?
gravity
:
'
top
'
,
backgroundColor
:
background
,
className
:
level
,
text
:
message
,
duration
:
4000
,
close
:
true
,
position
:
position
,
gravity
:
gravity
,
backgroundColor
:
background
,
className
:
level
,
}).
showToast
();
}
/**
* @param {string} message
*/
function
displayWarning
(
message
)
{
let
level
=
"
warning
"
;
return
displayError
(
message
,
level
);
return
displayError
(
message
,
MessageLevel
.
warning
);
}
/**
* @param {string} message
*/
function
displayMessage
(
message
)
{
return
displayError
(
message
,
"
info
"
,
"
right
"
,
"
bottom
"
);
return
displayError
(
message
,
MessageLevel
.
info
);
}
document
.
getElementById
(
'
userform
'
).
onsubmit
=
function
(
e
)
{
e
.
preventDefault
();
let
username
=
document
.
getElementById
(
'
username
'
).
value
.
trim
();
let
password
=
document
.
getElementById
(
'
password
'
).
value
;
let
username
=
getInputElement
(
'
username
'
).
value
.
trim
();
let
password
=
getInputElement
(
'
password
'
).
value
;
storeUserPass
(
username
,
password
);
serverConnect
();
};
...
...
@@ -1438,6 +1555,8 @@ document.getElementById('clodeside').onclick = function(e) {
document
.
getElementById
(
'
collapse-video
'
).
onclick
=
function
(
e
)
{
e
.
preventDefault
();
if
(
!
(
this
instanceof
HTMLElement
))
throw
new
Error
(
'
Unexpected type for this
'
);
let
width
=
window
.
innerWidth
;
if
(
width
<=
768
)
{
let
user_box
=
document
.
getElementById
(
'
userDropdown
'
);
...
...
@@ -1453,6 +1572,8 @@ document.getElementById('collapse-video').onclick = function(e) {
document
.
getElementById
(
'
switch-video
'
).
onclick
=
function
(
e
)
{
e
.
preventDefault
();
if
(
!
(
this
instanceof
HTMLElement
))
throw
new
Error
(
'
Unexpected type for this
'
);
showVideo
();
this
.
style
.
display
=
""
;
document
.
getElementById
(
'
collapse-video
'
).
style
.
display
=
"
block
"
;
...
...
static/tsconfig.json
View file @
0aa77441
...
...
@@ -4,10 +4,16 @@
"allowJs"
:
true
,
"checkJs"
:
true
,
"declaration"
:
true
,
"noImplicitThis"
:
true
,
"emitDeclarationOnly"
:
true
,
"strictBindCallApply"
:
true
"strictFunctionTypes"
:
true
,
"strictBindCallApply"
:
true
,
"noFallthroughCasesInSwitch"
:
true
,
"noImplicitReturns"
:
true
,
"noUnusedLocals"
:
true
},
"files"
:
[
"protocol.js"
"protocol.js"
,
"sfu.js"
]
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment