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
3e0bb089
Commit
3e0bb089
authored
Aug 11, 2020
by
Juliusz Chroboczek
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Split out the javascript protocol interface.
parent
ec742eac
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
1090 additions
and
613 deletions
+1090
-613
static/protocol.js
static/protocol.js
+918
-0
static/sfu.html
static/sfu.html
+2
-1
static/sfu.js
static/sfu.js
+157
-612
static/tsconfig.json
static/tsconfig.json
+13
-0
No files found.
static/protocol.js
0 → 100644
View file @
3e0bb089
// Copyright (c) 2020 by Juliusz Chroboczek.
// This is not open source software. Copy it, and I'll break into your
// house and tell your three year-old that Santa doesn't exist.
'
use strict
'
;
/**
* toHex formats an array as a hexadecimal string.
* @returns {string}
*/
function
toHex
(
array
)
{
let
a
=
new
Uint8Array
(
array
);
function
hex
(
x
)
{
let
h
=
x
.
toString
(
16
);
if
(
h
.
length
<
2
)
h
=
'
0
'
+
h
;
return
h
;
}
return
a
.
reduce
((
x
,
y
)
=>
x
+
hex
(
y
),
''
);
}
/** randomid returns a random string of 32 hex digits (16 bytes).
* @returns {string}
*/
function
randomid
()
{
let
a
=
new
Uint8Array
(
16
);
crypto
.
getRandomValues
(
a
);
return
toHex
(
a
);
}
/**
* ServerConnection encapsulates a websocket connection to the server and
* all the associated streams.
* @constructor
*/
function
ServerConnection
()
{
/**
* The id of this connection.
* @type {string}
*/
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>}
*/
this
.
up
=
{};
/**
* The set of all down streams, indexed by their id.
* @type {Object.<string,Stream>}
*/
this
.
down
=
{};
/**
* The ICE configuration used by all associated streams.
* @type {Array.<Object>}
*/
this
.
iceServers
=
[];
/**
* The permissions granted to this connection.
* @type {Object.<string,boolean>}
*/
this
.
permissions
=
{};
/* Callbacks */
/**
* onconnected is called when the connection has been established
* @type{function(): any}
*/
this
.
onconnected
=
null
;
/**
* onclose is called when the connection is closed
* @type{function(number, string): any}
*/
this
.
onclose
=
null
;
/**
* onuser is called whenever a user is added or removed from the group
* @type{function(string, string, string): any}
*/
this
.
onuser
=
null
;
/**
* onpermissions is called whenever the current user's permissions change
* @type{function(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}
*/
this
.
ondownstream
=
null
;
/**
* onchat is called whenever a new chat message is received.
* @type {function(string, string, string, string): any}
*/
this
.
onchat
=
null
;
/**
* onclearchat is called whenever the server requests that the chat
* be cleared.
* @type{function(): 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}
*/
this
.
onusermessage
=
null
;
}
/**
* @typedef {Object} message
* @property {string} type
* @property {string} [kind]
* @property {string} [id]
* @property {string} [username]
* @property {string} [password]
* @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]
*/
/**
* close forcibly closes a server connection. The onclose callback will
* be called when the connection is effectively closed.
*/
ServerConnection
.
prototype
.
close
=
function
()
{
this
.
socket
&&
this
.
socket
.
close
(
1000
,
'
Close requested by client
'
);
this
.
socket
=
null
;
}
/**
* send sends a message to the server.
* @param {message} m - the message to send.
*/
ServerConnection
.
prototype
.
send
=
function
(
m
)
{
if
(
this
.
socket
.
readyState
!==
this
.
socket
.
OPEN
)
{
// send on a closed connection 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
* populates the iceServers field of a ServerConnection. It is called
* lazily by connect.
*
* @returns {Promise<Array.<Object>>}
*/
ServerConnection
.
prototype
.
getIceServers
=
async
function
()
{
let
r
=
await
fetch
(
'
/ice-servers.json
'
);
if
(
!
r
.
ok
)
throw
new
Error
(
"
Couldn't fetch ICE servers:
"
+
r
.
status
+
'
'
+
r
.
statusText
);
let
servers
=
await
r
.
json
();
if
(
!
(
servers
instanceof
Array
))
throw
new
Error
(
"
couldn't parse ICE servers
"
);
this
.
iceServers
=
servers
;
return
servers
;
}
/**
* Connect connects to the server.
*
* @param {string} url - The URL to connect to.
* @returns {Promise<ServerConnection>}
*/
ServerConnection
.
prototype
.
connect
=
function
(
url
)
{
let
sc
=
this
;
if
(
sc
.
socket
)
{
sc
.
socket
.
close
(
1000
,
'
Reconnecting
'
);
sc
.
socket
=
null
;
}
if
(
!
sc
.
iceServers
)
{
try
{
sc
.
getIceServers
();
}
catch
(
e
)
{
console
.
error
(
e
);
}
}
try
{
sc
.
socket
=
new
WebSocket
(
`ws
${
location
.
protocol
===
'
https:
'
?
'
s
'
:
''
}
://
${
location
.
host
}
/ws`
,
);
}
catch
(
e
)
{
return
Promise
.
reject
(
e
);
}
return
new
Promise
((
resolve
,
reject
)
=>
{
this
.
socket
.
onerror
=
function
(
e
)
{
reject
(
e
);
};
this
.
socket
.
onopen
=
function
(
e
)
{
if
(
sc
.
onconnected
)
sc
.
onconnected
.
call
(
sc
);
resolve
(
sc
);
};
this
.
socket
.
onclose
=
function
(
e
)
{
sc
.
permissions
=
{};
if
(
sc
.
onpermissions
)
sc
.
onpermissions
.
call
(
sc
,
{});
for
(
let
id
in
sc
.
down
)
{
let
c
=
sc
.
down
[
id
];
delete
(
sc
.
down
[
id
]);
c
.
close
(
false
);
if
(
c
.
onclose
)
c
.
onclose
.
call
(
c
);
}
if
(
sc
.
onclose
)
sc
.
onclose
.
call
(
sc
,
e
.
code
,
e
.
reason
);
reject
(
new
Error
(
'
websocket close
'
+
e
.
code
+
'
'
+
e
.
reason
));
};
this
.
socket
.
onmessage
=
function
(
e
)
{
let
m
=
JSON
.
parse
(
e
.
data
);
switch
(
m
.
type
)
{
case
'
offer
'
:
sc
.
gotOffer
(
m
.
id
,
m
.
labels
,
m
.
offer
,
m
.
kind
===
'
renegotiate
'
);
break
;
case
'
answer
'
:
sc
.
gotAnswer
(
m
.
id
,
m
.
answer
);
break
;
case
'
renegotiate
'
:
sc
.
gotRenegotiate
(
m
.
id
)
break
;
case
'
close
'
:
sc
.
gotClose
(
m
.
id
);
break
;
case
'
abort
'
:
sc
.
gotAbort
(
m
.
id
);
break
;
case
'
ice
'
:
sc
.
gotICE
(
m
.
id
,
m
.
candidate
);
break
;
case
'
label
'
:
sc
.
gotLabel
(
m
.
id
,
m
.
value
);
break
;
case
'
permissions
'
:
sc
.
permissions
=
m
.
permissions
;
if
(
sc
.
onpermissions
)
sc
.
onpermissions
.
call
(
sc
,
m
.
permissions
);
break
;
case
'
user
'
:
if
(
sc
.
onuser
)
sc
.
onuser
.
call
(
sc
,
m
.
id
,
m
.
kind
,
m
.
username
);
break
;
case
'
chat
'
:
if
(
sc
.
onchat
)
sc
.
onchat
.
call
(
sc
,
m
.
id
,
m
.
username
,
m
.
kind
,
m
.
value
);
break
;
case
'
clearchat
'
:
if
(
sc
.
onclearchat
)
sc
.
onclearchat
.
call
(
sc
);
break
;
case
'
ping
'
:
sc
.
send
({
type
:
'
pong
'
,
});
break
;
case
'
pong
'
:
/* nothing */
break
;
case
'
usermessage
'
:
if
(
sc
.
onusermessage
)
sc
.
onusermessage
.
call
(
sc
,
m
.
kind
,
m
.
value
)
break
;
default
:
console
.
warn
(
'
Unexpected server message
'
,
m
.
type
);
return
;
}
};
});
}
/**
* login authenticates with the server.
*
* @param {string} username
* @param {string} password
*/
ServerConnection
.
prototype
.
login
=
function
(
username
,
password
)
{
this
.
send
({
type
:
'
login
'
,
id
:
this
.
id
,
username
:
username
,
password
:
password
,
})
}
/**
* join joins a group.
*
* @param {string} group - The name of the group to join.
*/
ServerConnection
.
prototype
.
join
=
function
(
group
)
{
this
.
send
({
type
:
'
join
'
,
group
:
group
,
})
}
/**
* request sets the list of requested media types.
*
* @param {string} what - One of "audio", "screenshare" or "everything".
*/
ServerConnection
.
prototype
.
request
=
function
(
what
)
{
/** @type {Object.<string,boolean>} */
let
request
=
{};
switch
(
what
)
{
case
'
audio
'
:
request
=
{
audio
:
true
};
break
;
case
'
screenshare
'
:
request
=
{
audio
:
true
,
screenshare
:
true
};
break
;
case
'
everything
'
:
request
=
{
audio
:
true
,
screenshare
:
true
,
video
:
true
};
break
;
default
:
console
.
error
(
`Uknown value
${
what
}
in sendRequest`
);
break
;
}
this
.
send
({
type
:
'
request
'
,
request
:
request
,
});
}
/**
* newUpStream requests the creation of a new up stream.
*
* @param {string} id - The id of the stream to create (optional).
* @returns {Stream}
*/
ServerConnection
.
prototype
.
newUpStream
=
function
(
id
)
{
let
sc
=
this
;
if
(
!
id
)
{
id
=
randomid
();
if
(
sc
.
up
[
id
])
throw
new
Error
(
'
Eek!
'
);
}
let
pc
=
new
RTCPeerConnection
({
iceServers
:
sc
.
iceServers
,
});
if
(
!
pc
)
throw
new
Error
(
"
Couldn't create peer connection
"
);
if
(
sc
.
up
[
id
])
{
sc
.
up
[
id
].
close
(
false
);
}
let
c
=
new
Stream
(
this
,
id
,
pc
);
sc
.
up
[
id
]
=
c
;
pc
.
onnegotiationneeded
=
async
e
=>
{
await
c
.
negotiate
();
}
pc
.
onicecandidate
=
e
=>
{
if
(
!
e
.
candidate
)
return
;
sc
.
send
({
type
:
'
ice
'
,
id
:
id
,
candidate
:
e
.
candidate
,
});
};
pc
.
oniceconnectionstatechange
=
e
=>
{
if
(
c
.
onstatus
)
c
.
onstatus
.
call
(
c
,
pc
.
iceConnectionState
);
if
(
pc
.
iceConnectionState
===
'
failed
'
)
{
try
{
/** @ts-ignore */
pc
.
restartIce
();
}
catch
(
e
)
{
console
.
warn
(
e
);
}
}
}
pc
.
ontrack
=
console
.
error
;
return
c
;
}
/**
* chat sends a chat message to the server. The server will normally echo
* the message back to the client.
*
* @param {string} username - The username of the sending user.
* @param {string} kind - The kind of message, either "" or "me".
* @param {string} message - The text of the message.
*/
ServerConnection
.
prototype
.
chat
=
function
(
username
,
kind
,
message
)
{
this
.
send
({
type
:
'
chat
'
,
id
:
this
.
id
,
username
:
username
,
kind
:
kind
,
value
:
message
,
});
}
/**
* groupAction sends a request to act on the current group.
*
* @param {string} kind - One of "clearchat", "lock", "unlock", "record or
* "unrecord".
*/
ServerConnection
.
prototype
.
groupAction
=
function
(
kind
)
{
this
.
send
({
type
:
'
groupaction
'
,
kind
:
kind
,
});
}
/**
* userAction sends a request to act on a user.
*
* @param {string} kind - One of "op", "unop", "kick", "present", "unpresent".
*/
ServerConnection
.
prototype
.
userAction
=
function
(
kind
,
id
)
{
this
.
send
({
type
:
'
useraction
'
,
kind
:
kind
,
id
:
id
,
});
}
/**
* Called when we receive an offer from the server. Don't call this.
*
* @param {string} id
* @param labels
* @param {RTCSessionDescriptionInit} offer
* @param {boolean} renegotiate
*/
ServerConnection
.
prototype
.
gotOffer
=
async
function
(
id
,
labels
,
offer
,
renegotiate
)
{
let
sc
=
this
;
let
c
=
sc
.
down
[
id
];
if
(
c
&&
!
renegotiate
)
{
// SDP is rather inflexible as to what can be renegotiated.
// Unless the server indicates that this is a renegotiation with
// all parameters unchanged, tear down the existing connection.
delete
(
sc
.
down
[
id
])
c
.
close
(
false
);
c
=
null
;
}
if
(
!
c
)
{
let
pc
=
new
RTCPeerConnection
({
iceServers
:
this
.
iceServers
,
});
c
=
new
Stream
(
this
,
id
,
pc
);
sc
.
down
[
id
]
=
c
;
c
.
pc
.
onicecandidate
=
function
(
e
)
{
if
(
!
e
.
candidate
)
return
;
sc
.
send
({
type
:
'
ice
'
,
id
:
id
,
candidate
:
e
.
candidate
,
});
};
pc
.
oniceconnectionstatechange
=
e
=>
{
if
(
c
.
onstatus
)
c
.
onstatus
.
call
(
c
,
pc
.
iceConnectionState
);
if
(
pc
.
iceConnectionState
===
'
failed
'
)
{
sc
.
send
({
type
:
'
renegotiate
'
,
id
:
id
,
});
}
}
c
.
pc
.
ontrack
=
function
(
e
)
{
let
label
=
e
.
transceiver
&&
c
.
labelsByMid
[
e
.
transceiver
.
mid
];
if
(
label
)
{
c
.
labels
[
e
.
track
.
id
]
=
label
;
}
else
{
console
.
warn
(
"
Couldn't find label for track
"
);
}
if
(
c
.
stream
!==
e
.
streams
[
0
])
{
c
.
stream
=
e
.
streams
[
0
];
let
label
=
e
.
transceiver
&&
c
.
labelsByMid
[
e
.
transceiver
.
mid
];
c
.
labels
[
e
.
track
.
id
]
=
label
;
if
(
c
.
ondowntrack
)
{
c
.
ondowntrack
.
call
(
c
,
e
.
track
,
e
.
transceiver
,
label
,
e
.
streams
[
0
],
);
}
if
(
c
.
onlabel
)
{
c
.
onlabel
.
call
(
c
,
label
);
}
}
};
}
c
.
labelsByMid
=
labels
;
if
(
sc
.
ondownstream
)
sc
.
ondownstream
.
call
(
sc
,
c
);
await
c
.
pc
.
setRemoteDescription
(
offer
);
await
c
.
flushIceCandidates
();
let
answer
=
await
c
.
pc
.
createAnswer
();
if
(
!
answer
)
throw
new
Error
(
"
Didn't create answer
"
);
await
c
.
pc
.
setLocalDescription
(
answer
);
this
.
send
({
type
:
'
answer
'
,
id
:
id
,
answer
:
answer
,
});
}
/**
* Called when we receive a stream label from the server. Don't call this.
*
* @param {string} id
* @param {string} label
*/
ServerConnection
.
prototype
.
gotLabel
=
function
(
id
,
label
)
{
let
c
=
this
.
down
[
id
];
if
(
!
c
)
throw
new
Error
(
'
Got label for unknown id
'
);
c
.
label
=
label
;
if
(
c
.
onlabel
)
c
.
onlabel
.
call
(
c
,
label
);
}
/**
* Called when we receive an answer from the server. Don't call this.
*
* @param {string} id
* @param {RTCSessionDescriptionInit} answer
*/
ServerConnection
.
prototype
.
gotAnswer
=
async
function
(
id
,
answer
)
{
let
c
=
this
.
up
[
id
];
if
(
!
c
)
throw
new
Error
(
'
unknown up stream
'
);
try
{
await
c
.
pc
.
setRemoteDescription
(
answer
);
}
catch
(
e
)
{
if
(
c
.
onerror
)
c
.
onerror
.
call
(
c
,
e
);
return
;
}
await
c
.
flushIceCandidates
();
}
/**
* Called when we receive a renegotiation request from the server. Don't
* call this.
*
* @param {string} id
*/
ServerConnection
.
prototype
.
gotRenegotiate
=
async
function
(
id
)
{
let
c
=
this
.
up
[
id
];
if
(
!
c
)
throw
new
Error
(
'
unknown up stream
'
);
try
{
/** @ts-ignore */
c
.
pc
.
restartIce
();
}
catch
(
e
)
{
console
.
warn
(
e
);
}
}
/**
* Called when we receive a close request from the server. Don't call this.
*
* @param {string} id
*/
ServerConnection
.
prototype
.
gotClose
=
function
(
id
)
{
let
c
=
this
.
down
[
id
];
if
(
!
c
)
throw
new
Error
(
'
unknown down stream
'
);
delete
(
this
.
down
[
id
]);
c
.
close
(
false
);
if
(
c
.
onclose
)
c
.
onclose
.
call
(
c
);
}
/**
* Called when we receive an abort message from the server. Don't call this.
*
* @param {string} id
*/
ServerConnection
.
prototype
.
gotAbort
=
function
(
id
)
{
let
c
=
this
.
down
[
id
];
if
(
!
c
)
throw
new
Error
(
'
unknown up stream
'
);
if
(
c
.
onabort
)
c
.
onabort
.
call
(
c
);
}
/**
* Called when we receive an ICE candidate from the server. Don't call this.
*
* @param {string} id
* @param {RTCIceCandidate} candidate
*/
ServerConnection
.
prototype
.
gotICE
=
async
function
(
id
,
candidate
)
{
let
c
=
this
.
up
[
id
];
if
(
!
c
)
c
=
this
.
down
[
id
];
if
(
!
c
)
throw
new
Error
(
'
unknown stream
'
);
if
(
c
.
pc
.
remoteDescription
)
await
c
.
pc
.
addIceCandidate
(
candidate
).
catch
(
console
.
warn
);
else
c
.
iceCandidates
.
push
(
candidate
);
}
/**
* Stream encapsulates a MediaStream, a set of tracks.
*
* A stream is said to go "up" if it is from the client to the server, and
* "down" otherwise.
*
* @param {ServerConnection} sc
* @param {string} id
* @param {RTCPeerConnection} pc
*
* @constructor
*/
function
Stream
(
sc
,
id
,
pc
)
{
/**
* The associated ServerConnection.
*
* @type {ServerConnection}
*/
this
.
sc
=
sc
;
/**
* The id of this stream.
*
* @type {string}
*/
this
.
id
=
id
;
/**
* For up streams, one of "local" or "screenshare".
*
* @type {string}
*/
this
.
kind
=
null
;
/**
* For down streams, a user-readable label.
*
* @type {string}
*/
this
.
label
=
null
;
/**
* The associated RTCPeerConnectoin. This is null before the stream
* is connected, and may change over time.
*
* @type {RTCPeerConnection}
*/
this
.
pc
=
pc
;
/**
* The associated MediaStream. This is null before the stream is
* connected, and may change over time.
*
* @type {MediaStream}
*/
this
.
stream
=
null
;
/**
* Track labels, indexed by track id.
*
* @type {Object.<string,string>}
*/
this
.
labels
=
{};
/**
* Track labels, indexed by mid.
*
* @type {Object.<string,string>}
*/
this
.
labelsByMid
=
{};
/**
* Buffered ICE candidates. This will be flushed by flushIceCandidates
* when the PC becomes stable.
*
* @type {Array.<RTCIceCandidate>}
*/
this
.
iceCandidates
=
[];
/**
* The statistics last computed by the stats handler. This is
* a dictionary indexed by track id, with each value a disctionary of
* statistics.
*
* @type {Object.<string,any>}
*/
this
.
stats
=
{};
/**
* The id of the periodic handler that computes statistics, as
* returned by setInterval.
*
* @type {number}
*/
this
.
statsHandler
=
null
;
/* Callbacks */
/**
* onclose is called when the stream is closed.
*
* @type{function(): 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}
*/
this
.
onerror
=
null
;
/**
* ondowntrack is called whenever a new track is added to a stream.
* 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}
*/
this
.
ondowntrack
=
null
;
/**
* onlabel is called whenever the server sets a new label for the stream.
*
* @type{function(string): any}
*/
this
.
onlabel
=
null
;
/**
* onstatus is called whenever the status of the stream changes.
*
* @type{function(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}
*/
this
.
onabort
=
null
;
/**
* onstats is called when we have new statistics about the connection
*
* @type{function(Object.<string,any>): any}
*/
this
.
onstats
=
null
;
}
/**
* close closes an up stream. It should not be called for down streams.
* @param {boolean} sendclose - whether to send a close message to the server
*/
Stream
.
prototype
.
close
=
function
(
sendclose
)
{
let
c
=
this
;
if
(
c
.
statsHandler
)
{
clearInterval
(
c
.
statsHandler
);
c
.
statsHandler
=
null
;
}
if
(
c
.
stream
)
{
c
.
stream
.
getTracks
().
forEach
(
t
=>
{
try
{
t
.
stop
();
}
catch
(
e
)
{
}
});
}
c
.
pc
.
close
();
if
(
sendclose
)
{
try
{
c
.
sc
.
send
({
type
:
'
close
'
,
id
:
c
.
id
,
});
}
catch
(
e
)
{
}
}
c
.
sc
=
null
;
};
/**
* flushIceCandidates flushes any buffered ICE candidates. It is called
* automatically when the connection reaches a stable state.
*/
Stream
.
prototype
.
flushIceCandidates
=
async
function
()
{
let
promises
=
[];
this
.
iceCandidates
.
forEach
(
c
=>
{
promises
.
push
(
this
.
pc
.
addIceCandidate
(
c
).
catch
(
console
.
warn
));
});
this
.
iceCandidates
=
[];
return
await
Promise
.
all
(
promises
);
}
/**
* 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 directly.
*/
Stream
.
prototype
.
negotiate
=
async
function
()
{
let
c
=
this
;
let
offer
=
await
c
.
pc
.
createOffer
();
if
(
!
offer
)
throw
(
new
Error
(
"
Didn't create offer
"
));
await
c
.
pc
.
setLocalDescription
(
offer
);
// mids are not known until this point
c
.
pc
.
getTransceivers
().
forEach
(
t
=>
{
if
(
t
.
sender
&&
t
.
sender
.
track
)
{
let
label
=
c
.
labels
[
t
.
sender
.
track
.
id
];
if
(
label
)
c
.
labelsByMid
[
t
.
mid
]
=
label
;
else
console
.
warn
(
"
Couldn't find label for track
"
);
}
});
c
.
sc
.
send
({
type
:
'
offer
'
,
kind
:
'
renegotiate
'
,
id
:
c
.
id
,
labels
:
c
.
labelsByMid
,
offer
:
offer
,
});
}
/**
* updateStats is called periodically, if requested by setStatsInterval,
* in order to recompute stream statistics and invoke the onstats handler.
*
* @returns {Promise<void>}
*/
Stream
.
prototype
.
updateStats
=
async
function
()
{
let
c
=
this
;
let
old
=
c
.
stats
;
let
stats
=
{};
let
transceivers
=
c
.
pc
.
getTransceivers
();
for
(
let
i
=
0
;
i
<
transceivers
.
length
;
i
++
)
{
let
t
=
transceivers
[
i
];
let
tid
=
t
.
sender
.
track
&&
t
.
sender
.
track
.
id
;
if
(
!
tid
)
continue
;
let
report
;
try
{
report
=
await
t
.
sender
.
getStats
();
}
catch
(
e
)
{
continue
;
}
stats
[
tid
]
=
{};
for
(
let
r
of
report
.
values
())
{
if
(
r
.
type
!==
'
outbound-rtp
'
)
continue
;
stats
[
tid
].
timestamp
=
r
.
timestamp
;
stats
[
tid
].
bytesSent
=
r
.
bytesSent
;
if
(
old
[
tid
]
&&
old
[
tid
].
timestamp
)
{
stats
[
tid
].
rate
=
((
r
.
bytesSent
-
old
[
tid
].
bytesSent
)
*
1000
/
(
r
.
timestamp
-
old
[
tid
].
timestamp
))
*
8
;
}
}
}
c
.
stats
=
stats
;
if
(
c
.
onstats
)
c
.
onstats
.
call
(
c
,
c
.
stats
);
}
/**
* setStatsInterval sets the interval in milliseconds at which the onstats
* handler will be called. This is only useful for up streams.
*
* @param {number} ms
*/
Stream
.
prototype
.
setStatsInterval
=
function
(
ms
)
{
let
c
=
this
;
if
(
c
.
statsHandler
)
{
clearInterval
(
c
.
statsHandler
);
c
.
statsHandler
=
null
;
}
if
(
ms
<=
0
)
return
;
c
.
statsHandler
=
setInterval
(()
=>
{
c
.
updateStats
();
},
ms
);
}
static/sfu.html
View file @
3e0bb089
...
...
@@ -76,6 +76,7 @@
<div
id=
"peers"
></div>
</div>
<script
src=
"/protocol.js"
defer
></script>
<script
src=
"/sfu.js"
defer
></script>
</body>
</html>
static/sfu.js
View file @
3e0bb089
...
...
@@ -5,76 +5,8 @@
'
use strict
'
;
let
myid
;
let
group
;
let
socket
;
let
up
=
{},
down
=
{};
let
iceServers
=
[];
let
permissions
=
{};
function
toHex
(
array
)
{
let
a
=
new
Uint8Array
(
array
);
function
hex
(
x
)
{
let
h
=
x
.
toString
(
16
);
if
(
h
.
length
<
2
)
h
=
'
0
'
+
h
;
return
h
;
}
return
a
.
reduce
((
x
,
y
)
=>
x
+
hex
(
y
),
''
);
}
function
randomid
()
{
let
a
=
new
Uint8Array
(
16
);
crypto
.
getRandomValues
(
a
);
return
toHex
(
a
);
}
function
Stream
(
id
,
pc
)
{
this
.
id
=
id
;
this
.
kind
=
null
;
this
.
label
=
null
;
this
.
pc
=
pc
;
this
.
stream
=
null
;
this
.
labels
=
{};
this
.
labelsByMid
=
{};
this
.
iceCandidates
=
[];
this
.
timers
=
[];
this
.
stats
=
{};
}
Stream
.
prototype
.
setInterval
=
function
(
f
,
t
)
{
this
.
timers
.
push
(
setInterval
(
f
,
t
));
};
Stream
.
prototype
.
close
=
function
(
sendit
)
{
while
(
this
.
timers
.
length
>
0
)
clearInterval
(
this
.
timers
.
pop
());
if
(
this
.
stream
)
{
this
.
stream
.
getTracks
().
forEach
(
t
=>
{
try
{
t
.
stop
();
}
catch
(
e
)
{
}
});
}
this
.
pc
.
close
();
if
(
sendit
)
{
try
{
send
({
type
:
'
close
'
,
id
:
this
.
id
,
});
}
catch
(
e
)
{
}
}
};
let
serverConnection
;
function
setUserPass
(
username
,
password
)
{
window
.
sessionStorage
.
setItem
(
...
...
@@ -103,6 +35,8 @@ function setConnected(connected) {
let
disconnectbutton
=
document
.
getElementById
(
'
disconnectbutton
'
);
if
(
connected
)
{
clearError
();
resetUsers
();
clearChat
();
statspan
.
textContent
=
'
Connected
'
;
statspan
.
classList
.
remove
(
'
disconnected
'
);
statspan
.
classList
.
add
(
'
connected
'
);
...
...
@@ -122,8 +56,40 @@ function setConnected(connected) {
userform
.
classList
.
add
(
'
userform
'
);
userform
.
classList
.
remove
(
'
invisible
'
);
disconnectbutton
.
classList
.
add
(
'
invisible
'
);
permissions
=
{};
clearUsername
(
false
);
clearUsername
();
}
}
function
gotConnected
()
{
setConnected
(
true
);
let
up
=
getUserPass
();
this
.
login
(
up
.
username
,
up
.
password
);
this
.
join
(
group
);
this
.
request
(
document
.
getElementById
(
'
requestselect
'
).
value
);
}
function
gotClose
(
code
,
reason
)
{
setConnected
(
false
);
if
(
code
!=
1000
)
console
.
warn
(
'
Socket close
'
,
code
,
reason
);
}
function
gotDownStream
(
c
)
{
c
.
onclose
=
function
()
{
delMedia
(
c
.
id
);
};
c
.
onerror
=
function
(
e
)
{
console
.
error
(
e
);
displayError
(
e
);
}
c
.
ondowntrack
=
function
(
track
,
transceiver
,
label
,
stream
)
{
setMedia
(
c
,
false
);
}
c
.
onlabel
=
function
(
label
)
{
setLabel
(
c
);
}
c
.
onstatus
=
function
(
status
)
{
setMediaStatus
(
c
);
}
}
...
...
@@ -153,6 +119,7 @@ function setVisibility(id, visible) {
}
function
setButtonsVisibility
()
{
let
permissions
=
serverConnection
.
permissions
;
let
local
=
!!
findUpMedia
(
'
local
'
);
let
share
=
!!
findUpMedia
(
'
screenshare
'
)
// don't allow multiple presentations
...
...
@@ -209,59 +176,17 @@ document.getElementById('unsharebutton').onclick = function(e) {
document
.
getElementById
(
'
requestselect
'
).
onchange
=
function
(
e
)
{
e
.
preventDefault
();
se
ndR
equest
(
this
.
value
);
se
rverConnection
.
r
equest
(
this
.
value
);
};
async
function
updateStats
(
conn
,
sender
)
{
let
tid
=
sender
.
track
&&
sender
.
track
.
id
;
if
(
!
tid
)
return
;
let
stats
=
conn
.
stats
[
tid
];
if
(
!
stats
)
{
conn
.
stats
[
tid
]
=
{};
stats
=
conn
.
stats
[
tid
];
}
let
report
;
try
{
report
=
await
sender
.
getStats
();
}
catch
(
e
)
{
delete
(
stats
[
id
].
rate
);
delete
(
stats
.
timestamp
);
delete
(
stats
.
bytesSent
);
return
;
}
for
(
let
r
of
report
.
values
())
{
if
(
r
.
type
!==
'
outbound-rtp
'
)
continue
;
if
(
stats
.
timestamp
)
{
stats
.
rate
=
((
r
.
bytesSent
-
stats
.
bytesSent
)
*
1000
/
(
r
.
timestamp
-
stats
.
timestamp
))
*
8
;
}
else
{
delete
(
stats
.
rate
);
}
stats
.
timestamp
=
r
.
timestamp
;
stats
.
bytesSent
=
r
.
bytesSent
;
return
;
}
}
function
displayStats
(
id
)
{
let
conn
=
up
[
id
];
if
(
!
conn
)
{
setLabel
(
id
);
return
;
}
function
displayStats
(
stats
)
{
let
c
=
this
;
let
text
=
''
;
c
onn
.
pc
.
getSenders
().
forEach
(
s
=>
{
c
.
pc
.
getSenders
().
forEach
(
s
=>
{
let
tid
=
s
.
track
&&
s
.
track
.
id
;
let
stats
=
tid
&&
c
onn
.
stats
[
tid
];
let
stats
=
tid
&&
c
.
stats
[
tid
];
if
(
stats
&&
stats
.
rate
>
0
)
{
if
(
text
)
text
=
text
+
'
+
'
;
...
...
@@ -269,7 +194,7 @@ function displayStats(id) {
}
});
setLabel
(
id
,
text
);
setLabel
(
c
,
text
);
}
function
mapMediaOption
(
value
)
{
...
...
@@ -337,6 +262,22 @@ async function setMediaChoices() {
mediaChoicesDone
=
true
;
}
function
newUpStream
(
id
)
{
let
c
=
serverConnection
.
newUpStream
();
c
.
onstatus
=
function
(
status
)
{
setMediaStatus
(
c
);
}
c
.
onerror
=
function
(
e
)
{
console
.
error
(
e
);
displayError
(
e
);
delUpMedia
(
c
.
id
);
}
c
.
onabort
=
function
()
{
delUpMedia
(
c
.
id
);
}
return
c
;
}
async
function
addLocalMedia
(
id
)
{
if
(
!
getUserPass
())
return
;
...
...
@@ -366,8 +307,7 @@ async function addLocalMedia(id) {
setMediaChoices
();
id
=
await
newUpStream
(
id
);
let
c
=
up
[
id
];
let
c
=
newUpStream
(
id
);
c
.
kind
=
'
local
'
;
c
.
stream
=
stream
;
...
...
@@ -376,14 +316,10 @@ async function addLocalMedia(id) {
if
(
t
.
kind
==
'
audio
'
&&
localMute
)
t
.
enabled
=
false
;
let
sender
=
c
.
pc
.
addTrack
(
t
,
stream
);
c
.
setInterval
(()
=>
{
updateStats
(
c
,
sender
);
},
2000
);
});
c
.
setInterval
(()
=>
{
displayStats
(
id
);
},
2500
);
await
setMedia
(
id
);
c
.
onstats
=
displayStats
;
c
.
setStatsInterval
(
2000
);
await
setMedia
(
c
,
true
);
setButtonsVisibility
()
}
...
...
@@ -399,33 +335,23 @@ async function addShareMedia(setup) {
return
;
}
let
id
=
await
newUpStream
();
let
c
=
up
[
id
];
let
c
=
await
serverConnection
.
newUpStream
();
c
.
kind
=
'
screenshare
'
;
c
.
stream
=
stream
;
stream
.
getTracks
().
forEach
(
t
=>
{
let
sender
=
c
.
pc
.
addTrack
(
t
,
stream
);
t
.
onended
=
e
=>
{
delUpMedia
(
id
);
delUpMedia
(
c
.
id
);
};
c
.
labels
[
t
.
id
]
=
'
screenshare
'
;
c
.
setInterval
(()
=>
{
updateStats
(
c
,
sender
);
},
2000
);
});
c
.
setInterval
(()
=>
{
displayStats
(
id
);
},
2500
);
await
setMedia
(
id
);
c
.
onstats
=
displayStats
;
c
.
setStatsInterval
(
2000
);
await
setMedia
(
c
,
true
);
setButtonsVisibility
()
}
function
stopUpMedia
(
id
)
{
let
c
=
up
[
id
];
if
(
!
c
)
{
console
.
error
(
'
Stopping unknown up media
'
);
return
;
}
function
stopUpMedia
(
c
)
{
if
(
!
c
.
stream
)
return
;
c
.
stream
.
getTracks
().
forEach
(
t
=>
{
...
...
@@ -436,43 +362,40 @@ function stopUpMedia(id) {
});
}
function
delUpMedia
(
id
)
{
let
c
=
up
[
id
];
if
(
!
c
)
{
console
.
error
(
'
Deleting unknown up media
'
);
return
;
}
stopUpMedia
(
id
);
delMedia
(
id
);
function
delUpMedia
(
c
)
{
stopUpMedia
(
c
);
delMedia
(
c
);
c
.
close
(
true
);
delete
(
up
[
id
]);
delete
(
serverConnection
.
up
[
c
.
id
]);
setButtonsVisibility
()
}
function
delUpMediaKind
(
kind
)
{
for
(
let
id
in
up
)
{
let
c
=
up
[
id
];
for
(
let
id
in
serverConnection
.
up
)
{
let
c
=
serverConnection
.
up
[
id
];
if
(
c
.
kind
!=
kind
)
continue
c
.
close
(
true
);
delMedia
(
id
);
delete
(
up
[
id
]);
delete
(
serverConnection
.
up
[
id
]);
}
setButtonsVisibility
()
}
function
findUpMedia
(
kind
)
{
for
(
let
id
in
up
)
{
if
(
up
[
id
].
kind
===
kind
)
for
(
let
id
in
serverConnection
.
up
)
{
if
(
serverConnection
.
up
[
id
].
kind
===
kind
)
return
id
;
}
return
null
;
}
function
muteLocalTracks
(
mute
)
{
for
(
let
id
in
up
)
{
let
c
=
up
[
id
];
if
(
!
serverConnection
)
return
;
for
(
let
id
in
serverConnection
.
up
)
{
let
c
=
serverConnection
.
up
[
id
];
if
(
c
.
kind
===
'
local
'
)
{
let
stream
=
c
.
stream
;
stream
.
getTracks
().
forEach
(
t
=>
{
...
...
@@ -484,50 +407,45 @@ function muteLocalTracks(mute) {
}
}
function
setMedia
(
id
)
{
let
mine
=
true
;
let
c
=
up
[
id
];
if
(
!
c
)
{
c
=
down
[
id
];
mine
=
false
;
}
if
(
!
c
)
throw
new
Error
(
'
Unknown stream
'
);
/**
* @param {Stream} c
* @param {boolean} isUp
*/
function
setMedia
(
c
,
isUp
)
{
let
peersdiv
=
document
.
getElementById
(
'
peers
'
);
let
div
=
document
.
getElementById
(
'
peer-
'
+
id
);
let
div
=
document
.
getElementById
(
'
peer-
'
+
c
.
id
);
if
(
!
div
)
{
div
=
document
.
createElement
(
'
div
'
);
div
.
id
=
'
peer-
'
+
id
;
div
.
id
=
'
peer-
'
+
c
.
id
;
div
.
classList
.
add
(
'
peer
'
);
peersdiv
.
appendChild
(
div
);
}
let
media
=
document
.
getElementById
(
'
media-
'
+
id
);
let
media
=
document
.
getElementById
(
'
media-
'
+
c
.
id
);
if
(
!
media
)
{
media
=
document
.
createElement
(
'
video
'
);
media
.
id
=
'
media-
'
+
id
;
media
.
id
=
'
media-
'
+
c
.
id
;
media
.
classList
.
add
(
'
media
'
);
media
.
autoplay
=
true
;
media
.
playsinline
=
true
;
media
.
controls
=
true
;
if
(
mine
)
if
(
isUp
)
media
.
muted
=
true
;
div
.
appendChild
(
media
);
}
let
label
=
document
.
getElementById
(
'
label-
'
+
id
);
let
label
=
document
.
getElementById
(
'
label-
'
+
c
.
id
);
if
(
!
label
)
{
label
=
document
.
createElement
(
'
div
'
);
label
.
id
=
'
label-
'
+
id
;
label
.
id
=
'
label-
'
+
c
.
id
;
label
.
classList
.
add
(
'
label
'
);
div
.
appendChild
(
label
);
}
media
.
srcObject
=
c
.
stream
;
setLabel
(
id
);
setMediaStatus
(
id
);
setLabel
(
c
);
setMediaStatus
(
c
);
resizePeers
();
}
...
...
@@ -543,12 +461,14 @@ function delMedia(id) {
resizePeers
();
}
function
setMediaStatus
(
id
)
{
let
c
=
up
[
id
]
||
down
[
id
];
/**
* @param {Stream} c
*/
function
setMediaStatus
(
c
)
{
let
state
=
c
&&
c
.
pc
&&
c
.
pc
.
iceConnectionState
;
let
good
=
state
===
'
connected
'
||
state
===
'
completed
'
;
let
media
=
document
.
getElementById
(
'
media-
'
+
id
);
let
media
=
document
.
getElementById
(
'
media-
'
+
c
.
id
);
if
(
!
media
)
{
console
.
warn
(
'
Setting status of unknown media.
'
);
return
;
...
...
@@ -560,11 +480,15 @@ function setMediaStatus(id) {
}
function
setLabel
(
id
,
fallback
)
{
let
label
=
document
.
getElementById
(
'
label-
'
+
id
);
/**
* @param {Stream} c
* @param {string} [fallback]
*/
function
setLabel
(
c
,
fallback
)
{
let
label
=
document
.
getElementById
(
'
label-
'
+
c
.
id
);
if
(
!
label
)
return
;
let
l
=
down
[
id
]
?
down
[
id
].
label
:
nul
l
;
let
l
=
c
.
labe
l
;
if
(
l
)
{
label
.
textContent
=
l
;
label
.
classList
.
remove
(
'
label-fallback
'
);
...
...
@@ -578,294 +502,14 @@ function setLabel(id, fallback) {
}
function
resizePeers
()
{
let
count
=
Object
.
keys
(
up
).
length
+
Object
.
keys
(
down
).
length
;
let
count
=
Object
.
keys
(
serverConnection
.
up
).
length
+
Object
.
keys
(
serverConnection
.
down
).
length
;
let
columns
=
Math
.
ceil
(
Math
.
sqrt
(
count
));
document
.
getElementById
(
'
peers
'
).
style
[
'
grid-template-columns
'
]
=
`repeat(
${
columns
}
, 1fr)`
;
}
function
serverConnect
()
{
if
(
socket
)
{
socket
.
close
(
1000
,
'
Reconnecting
'
);
socket
=
null
;
setConnected
(
false
);
}
try
{
socket
=
new
WebSocket
(
`ws
${
location
.
protocol
===
'
https:
'
?
'
s
'
:
''
}
://
${
location
.
host
}
/ws`
,
);
}
catch
(
e
)
{
console
.
error
(
e
);
setConnected
(
false
);
return
Promise
.
reject
(
e
);
}
return
new
Promise
((
resolve
,
reject
)
=>
{
socket
.
onerror
=
function
(
e
)
{
reject
(
e
.
error
?
e
.
error
:
e
);
};
socket
.
onopen
=
function
(
e
)
{
resetUsers
();
resetChat
();
setConnected
(
true
);
let
up
=
getUserPass
();
try
{
send
({
type
:
'
login
'
,
id
:
myid
,
username
:
up
.
username
,
password
:
up
.
password
,
})
send
({
type
:
'
join
'
,
group
:
group
,
})
sendRequest
(
document
.
getElementById
(
'
requestselect
'
).
value
);
}
catch
(
e
)
{
console
.
error
(
e
);
displayError
(
e
);
reject
(
e
);
return
;
}
resolve
();
};
socket
.
onclose
=
function
(
e
)
{
setConnected
(
false
);
delUpMediaKind
(
'
local
'
);
delUpMediaKind
(
'
screenshare
'
);
for
(
let
id
in
down
)
{
let
c
=
down
[
id
];
delete
(
down
[
id
]);
c
.
close
(
false
);
delMedia
(
id
);
}
reject
(
new
Error
(
'
websocket close
'
+
e
.
code
+
'
'
+
e
.
reason
));
};
socket
.
onmessage
=
function
(
e
)
{
let
m
=
JSON
.
parse
(
e
.
data
);
switch
(
m
.
type
)
{
case
'
offer
'
:
gotOffer
(
m
.
id
,
m
.
labels
,
m
.
offer
,
m
.
kind
===
'
renegotiate
'
);
break
;
case
'
answer
'
:
gotAnswer
(
m
.
id
,
m
.
answer
);
break
;
case
'
renegotiate
'
:
let
c
=
up
[
m
.
id
];
if
(
c
)
{
try
{
c
.
pc
.
restartIce
()
}
catch
(
e
)
{
console
.
error
(
e
);
displayError
(
e
);
}
}
break
;
case
'
close
'
:
gotClose
(
m
.
id
);
break
;
case
'
abort
'
:
gotAbort
(
m
.
id
);
break
;
case
'
ice
'
:
gotICE
(
m
.
id
,
m
.
candidate
);
break
;
case
'
label
'
:
gotLabel
(
m
.
id
,
m
.
value
);
break
;
case
'
permissions
'
:
gotPermissions
(
m
.
permissions
);
break
;
case
'
user
'
:
gotUser
(
m
.
id
,
m
.
kind
,
m
.
username
);
break
;
case
'
chat
'
:
addToChatbox
(
m
.
id
,
m
.
username
,
m
.
kind
,
m
.
value
);
break
;
case
'
clearchat
'
:
resetChat
();
break
;
case
'
ping
'
:
send
({
type
:
'
pong
'
,
});
break
;
case
'
pong
'
:
/* nothing */
break
;
case
'
usermessage
'
:
switch
(
m
.
kind
)
{
case
'
error
'
:
displayError
(
'
The server said:
'
+
m
.
value
);
break
;
default
:
displayWarning
(
'
The server said:
'
+
m
.
value
)
break
;
}
break
;
default
:
console
.
warn
(
'
Unexpected server message
'
,
m
.
type
);
return
;
}
};
});
}
function
sendRequest
(
value
)
{
let
request
=
[];
switch
(
value
)
{
case
'
audio
'
:
request
=
{
audio
:
true
};
break
;
case
'
screenshare
'
:
request
=
{
audio
:
true
,
screenshare
:
true
};
break
;
case
'
everything
'
:
request
=
{
audio
:
true
,
screenshare
:
true
,
video
:
true
};
break
;
default
:
console
.
error
(
`Uknown value
${
value
}
in sendRequest`
);
break
;
}
send
({
type
:
'
request
'
,
request
:
request
,
});
}
async
function
gotOffer
(
id
,
labels
,
offer
,
renegotiate
)
{
let
c
=
down
[
id
];
if
(
c
&&
!
renegotiate
)
{
// SDP is rather inflexible as to what can be renegotiated.
// Unless the server indicates that this is a renegotiation with
// all parameters unchanged, tear down the existing connection.
delete
(
down
[
id
])
c
.
close
(
false
);
c
=
null
;
}
if
(
!
c
)
{
let
pc
=
new
RTCPeerConnection
({
iceServers
:
iceServers
,
});
c
=
new
Stream
(
id
,
pc
);
down
[
id
]
=
c
;
c
.
pc
.
onicecandidate
=
function
(
e
)
{
if
(
!
e
.
candidate
)
return
;
send
({
type
:
'
ice
'
,
id
:
id
,
candidate
:
e
.
candidate
,
});
};
pc
.
oniceconnectionstatechange
=
e
=>
{
setMediaStatus
(
id
);
if
(
pc
.
iceConnectionState
===
'
failed
'
)
{
send
({
type
:
'
renegotiate
'
,
id
:
id
,
});
}
}
c
.
pc
.
ontrack
=
function
(
e
)
{
let
label
=
e
.
transceiver
&&
c
.
labelsByMid
[
e
.
transceiver
.
mid
];
if
(
label
)
{
c
.
labels
[
e
.
track
.
id
]
=
label
;
}
else
{
console
.
warn
(
"
Couldn't find label for track
"
);
}
c
.
stream
=
e
.
streams
[
0
];
setMedia
(
id
);
};
}
c
.
labelsByMid
=
labels
;
await
c
.
pc
.
setRemoteDescription
(
offer
);
await
addIceCandidates
(
c
);
let
answer
=
await
c
.
pc
.
createAnswer
();
if
(
!
answer
)
throw
new
Error
(
"
Didn't create answer
"
);
await
c
.
pc
.
setLocalDescription
(
answer
);
send
({
type
:
'
answer
'
,
id
:
id
,
answer
:
answer
,
});
}
function
gotLabel
(
id
,
label
)
{
let
c
=
down
[
id
];
if
(
!
c
)
throw
new
Error
(
'
Got label for unknown id
'
);
c
.
label
=
label
;
setLabel
(
id
);
}
async
function
gotAnswer
(
id
,
answer
)
{
let
c
=
up
[
id
];
if
(
!
c
)
throw
new
Error
(
'
unknown up stream
'
);
try
{
await
c
.
pc
.
setRemoteDescription
(
answer
);
}
catch
(
e
)
{
console
.
error
(
e
);
displayError
(
e
);
delUpMedia
(
id
);
return
;
}
await
addIceCandidates
(
c
);
}
function
gotClose
(
id
)
{
let
c
=
down
[
id
];
if
(
!
c
)
throw
new
Error
(
'
unknown down stream
'
);
delete
(
down
[
id
]);
c
.
close
(
false
);
delMedia
(
id
);
}
function
gotAbort
(
id
)
{
delUpMedia
(
id
);
}
async
function
gotICE
(
id
,
candidate
)
{
let
conn
=
up
[
id
];
if
(
!
conn
)
conn
=
down
[
id
];
if
(
!
conn
)
throw
new
Error
(
'
unknown stream
'
);
if
(
conn
.
pc
.
remoteDescription
)
await
conn
.
pc
.
addIceCandidate
(
candidate
).
catch
(
console
.
warn
);
else
conn
.
iceCandidates
.
push
(
candidate
);
}
async
function
addIceCandidates
(
conn
)
{
let
promises
=
[];
conn
.
iceCandidates
.
forEach
(
c
=>
{
promises
.
push
(
conn
.
pc
.
addIceCandidate
(
c
).
catch
(
console
.
warn
));
});
conn
.
iceCandidates
=
[];
return
await
Promise
.
all
(
promises
);
}
function
send
(
m
)
{
if
(
!
m
)
throw
(
new
Error
(
'
Sending null message
'
));
if
(
socket
.
readyState
!==
socket
.
OPEN
)
{
// send on a closed connection doesn't throw
throw
(
new
Error
(
'
Stream is not open
'
));
}
return
socket
.
send
(
JSON
.
stringify
(
m
));
}
let
users
=
{};
function
addUser
(
id
,
name
)
{
...
...
@@ -919,11 +563,11 @@ function displayUsername() {
let
text
=
''
;
if
(
userpass
&&
userpass
.
username
)
text
=
'
as
'
+
userpass
.
username
;
if
(
permissions
.
op
&&
permissions
.
present
)
if
(
serverConnection
.
permissions
.
op
&&
serverConnection
.
permissions
.
present
)
text
=
text
+
'
(op, presenter)
'
;
else
if
(
permissions
.
op
)
else
if
(
serverConnection
.
permissions
.
op
)
text
=
text
+
'
(op)
'
;
else
if
(
permissions
.
present
)
else
if
(
serverConnection
.
permissions
.
present
)
text
=
text
+
'
(presenter)
'
;
document
.
getElementById
(
'
userspan
'
).
textContent
=
text
;
}
...
...
@@ -932,8 +576,10 @@ function clearUsername() {
document
.
getElementById
(
'
userspan
'
).
textContent
=
''
;
}
function
gotPermissions
(
perm
)
{
permissions
=
perm
;
/**
* @param {Object.<string,boolean>} perms
*/
function
gotPermissions
(
perms
)
{
displayUsername
();
setButtonsVisibility
();
}
...
...
@@ -1020,13 +666,12 @@ function addToChatbox(peerId, nick, kind, message){
return
message
;
}
function
reset
Chat
()
{
function
clear
Chat
()
{
lastMessage
=
{};
document
.
getElementById
(
'
box
'
).
textContent
=
''
;
}
function
handleInput
()
{
let
username
=
getUsername
();
let
input
=
document
.
getElementById
(
'
input
'
);
let
data
=
input
.
value
;
input
.
value
=
''
;
...
...
@@ -1057,51 +702,42 @@ function handleInput() {
me
=
true
;
break
;
case
'
/leave
'
:
s
ocket
.
close
();
s
erverConnection
.
close
();
return
;
case
'
/clear
'
:
if
(
!
permissions
.
op
)
{
if
(
!
serverConnection
.
permissions
.
op
)
{
displayError
(
"
You're not an operator
"
);
return
;
}
send
({
type
:
'
groupaction
'
,
kind
:
'
clearchat
'
,
});
serverConnection
.
groupAction
(
'
clearchat
'
);
return
;
case
'
/lock
'
:
case
'
/unlock
'
:
if
(
!
permissions
.
op
)
{
if
(
!
serverConnection
.
permissions
.
op
)
{
displayError
(
"
You're not an operator
"
);
return
;
}
send
({
type
:
'
groupaction
'
,
kind
:
cmd
.
slice
(
1
),
});
serverConnection
.
groupAction
(
cmd
.
slice
(
1
));
return
;
case
'
/record
'
:
case
'
/unrecord
'
:
if
(
!
permissions
.
record
)
{
if
(
!
serverConnection
.
permissions
.
record
)
{
displayError
(
"
You're not allowed to record
"
);
return
;
}
send
({
type
:
'
groupaction
'
,
kind
:
cmd
.
slice
(
1
),
});
serverConnection
.
groupAction
(
cmd
.
slice
(
1
));
return
;
case
'
/op
'
:
case
'
/unop
'
:
case
'
/kick
'
:
case
'
/present
'
:
case
'
/unpresent
'
:
{
if
(
!
permissions
.
op
)
{
if
(
!
serverConnection
.
permissions
.
op
)
{
displayError
(
"
You're not an operator
"
);
return
;
}
let
id
;
if
(
id
in
users
)
{
if
(
rest
in
users
)
{
id
=
rest
;
}
else
{
for
(
let
i
in
users
)
{
...
...
@@ -1115,11 +751,7 @@ function handleInput() {
displayError
(
'
Unknown user
'
+
rest
);
return
;
}
send
({
type
:
'
useraction
'
,
kind
:
cmd
.
slice
(
1
),
id
:
id
,
});
serverConnection
.
userAction
(
cmd
.
slice
(
1
),
id
)
return
;
}
default
:
...
...
@@ -1132,19 +764,14 @@ function handleInput() {
me
=
false
;
}
let
username
=
getUsername
();
if
(
!
username
)
{
displayError
(
"
Sorry, you're anonymous, you cannot chat
"
);
return
;
}
try
{
let
a
=
send
({
type
:
'
chat
'
,
id
:
myid
,
username
:
username
,
kind
:
me
?
'
me
'
:
''
,
value
:
message
,
});
serverConnection
.
chat
(
username
,
me
?
'
me
'
:
''
,
message
);
}
catch
(
e
)
{
console
.
error
(
e
);
displayError
(
e
);
...
...
@@ -1198,91 +825,6 @@ function chatResizer(e) {
document
.
getElementById
(
'
resizer
'
).
addEventListener
(
'
mousedown
'
,
chatResizer
,
false
);
async
function
newUpStream
(
id
)
{
if
(
!
id
)
{
id
=
randomid
();
if
(
up
[
id
])
throw
new
Error
(
'
Eek!
'
);
}
let
pc
=
new
RTCPeerConnection
({
iceServers
:
iceServers
,
});
if
(
!
pc
)
throw
new
Error
(
"
Couldn't create peer connection
"
);
if
(
up
[
id
])
{
up
[
id
].
close
(
false
);
}
up
[
id
]
=
new
Stream
(
id
,
pc
);
pc
.
onnegotiationneeded
=
async
e
=>
{
try
{
await
negotiate
(
id
,
false
);
}
catch
(
e
)
{
console
.
error
(
e
);
displayError
(
e
);
delUpMedia
(
id
);
}
}
pc
.
onicecandidate
=
e
=>
{
if
(
!
e
.
candidate
)
return
;
send
({
type
:
'
ice
'
,
id
:
id
,
candidate
:
e
.
candidate
,
});
};
pc
.
oniceconnectionstatechange
=
e
=>
{
setMediaStatus
(
id
);
if
(
pc
.
iceConnectionState
===
'
failed
'
)
{
try
{
pc
.
restartIce
();
}
catch
(
e
)
{
console
.
error
(
e
);
displayError
(
e
);
}
}
}
pc
.
ontrack
=
console
.
error
;
return
id
;
}
async
function
negotiate
(
id
,
restartIce
)
{
let
c
=
up
[
id
];
if
(
!
c
)
throw
new
Error
(
'
unknown stream
'
);
if
(
typeof
(
c
.
pc
.
getTransceivers
)
!==
'
function
'
)
throw
new
Error
(
'
Browser too old, please upgrade
'
);
let
offer
=
await
c
.
pc
.
createOffer
({
iceRestart
:
restartIce
});
if
(
!
offer
)
throw
(
new
Error
(
"
Didn't create offer
"
));
await
c
.
pc
.
setLocalDescription
(
offer
);
// mids are not known until this point
c
.
pc
.
getTransceivers
().
forEach
(
t
=>
{
if
(
t
.
sender
&&
t
.
sender
.
track
)
{
let
label
=
c
.
labels
[
t
.
sender
.
track
.
id
];
if
(
label
)
c
.
labelsByMid
[
t
.
mid
]
=
label
;
else
console
.
warn
(
"
Couldn't find label for track
"
);
}
});
send
({
type
:
'
offer
'
,
kind
:
'
renegotiate
'
,
id
:
id
,
labels
:
c
.
labelsByMid
,
offer
:
offer
,
});
}
let
errorTimeout
=
null
;
function
setErrorTimeout
(
ms
)
{
...
...
@@ -1317,29 +859,36 @@ function clearError() {
setErrorTimeout
(
null
);
}
async
function
getIceServers
()
{
let
r
=
await
fetch
(
'
/ice-servers.json
'
);
if
(
!
r
.
ok
)
throw
new
Error
(
"
Couldn't fetch ICE servers:
"
+
r
.
status
+
'
'
+
r
.
statusText
);
let
servers
=
await
r
.
json
();
if
(
!
(
servers
instanceof
Array
))
throw
new
Error
(
"
couldn't parse ICE servers
"
);
iceServers
=
servers
;
}
document
.
getElementById
(
'
userform
'
).
onsubmit
=
async
function
(
e
)
{
document
.
getElementById
(
'
userform
'
).
onsubmit
=
function
(
e
)
{
e
.
preventDefault
();
let
username
=
document
.
getElementById
(
'
username
'
).
value
.
trim
();
let
password
=
document
.
getElementById
(
'
password
'
).
value
;
setUserPass
(
username
,
password
);
await
serverConnect
();
serverConnect
();
};
document
.
getElementById
(
'
disconnectbutton
'
).
onclick
=
function
(
e
)
{
s
ocket
.
close
();
s
erverConnection
.
close
();
};
function
serverConnect
()
{
serverConnection
=
new
ServerConnection
();
serverConnection
.
onconnected
=
gotConnected
;
serverConnection
.
onclose
=
gotClose
;
serverConnection
.
ondownstream
=
gotDownStream
;
serverConnection
.
onuser
=
gotUser
;
serverConnection
.
onpermissions
=
gotPermissions
;
serverConnection
.
onchat
=
addToChatbox
;
serverConnection
.
onclearchat
=
clearChat
;
serverConnection
.
onusermessage
=
function
(
kind
,
message
)
{
if
(
kind
===
'
error
'
)
displayError
(
`The server said:
${
message
}
`
);
else
displayWarning
(
`The server said:
${
message
}
`
);
}
return
serverConnection
.
connect
(
`ws
${
location
.
protocol
===
'
https:
'
?
'
s
'
:
''
}
://
${
location
.
host
}
/ws`
);
}
function
start
()
{
group
=
decodeURIComponent
(
location
.
pathname
.
replace
(
/^
\/[
a-z
]
*
\/
/
,
''
));
let
title
=
group
.
charAt
(
0
).
toUpperCase
()
+
group
.
slice
(
1
);
...
...
@@ -1350,15 +899,11 @@ function start() {
setLocalMute
(
localMute
);
myid
=
randomid
();
getIceServers
().
catch
(
console
.
error
).
then
(
c
=>
{
document
.
getElementById
(
'
connectbutton
'
).
disabled
=
false
;
}).
then
(
c
=>
{
let
userpass
=
getUserPass
();
if
(
userpass
)
return
serverConnect
();
});
serverConnect
();
}
start
();
static/tsconfig.json
0 → 100644
View file @
3e0bb089
{
"compilerOptions"
:
{
"target"
:
"ES6"
,
"allowJs"
:
true
,
"checkJs"
:
true
,
"declaration"
:
true
,
"emitDeclarationOnly"
:
true
,
"strictBindCallApply"
:
true
},
"files"
:
[
"protocol.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