Add user list
parent
2443590cde
commit
da3ba57ca9
3
go.mod
3
go.mod
|
@ -8,8 +8,9 @@ require (
|
|||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/lucas-clemente/quic-go v0.14.1 // indirect
|
||||
github.com/omeid/go-resources v0.0.0-20190324090249-46f4269d8abd
|
||||
github.com/pion/ice v0.7.7 // indirect
|
||||
github.com/pion/rtp v1.1.4
|
||||
github.com/pion/webrtc/v2 v2.1.17
|
||||
github.com/pion/webrtc/v2 v2.1.18
|
||||
github.com/pkg/errors v0.8.1
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.4 // indirect
|
||||
|
|
6
go.sum
6
go.sum
|
@ -49,6 +49,8 @@ github.com/pion/dtls/v2 v2.0.0-rc.3 h1:u9utI+EDJOjOWfrkGQsD8WNssPcTwfYIanFB6oI8K
|
|||
github.com/pion/dtls/v2 v2.0.0-rc.3/go.mod h1:x0XH+cN5z+l/+/4nYL8r4sB8g6+0d1Zp2Pfkcoz8BKY=
|
||||
github.com/pion/ice v0.7.6 h1:EARj1MBq5NYaMtXVhYkK03i0RS/meejNHvZS++K5tSY=
|
||||
github.com/pion/ice v0.7.6/go.mod h1:4xCajahEEvc5w0AM+Ujx/Rr2EczON/fKndi3jLyDdh4=
|
||||
github.com/pion/ice v0.7.7 h1:POqtOIISKHwaCd2XqgNyQrylgFl9IZJWZmL9cQQsqkk=
|
||||
github.com/pion/ice v0.7.7/go.mod h1:4xCajahEEvc5w0AM+Ujx/Rr2EczON/fKndi3jLyDdh4=
|
||||
github.com/pion/logging v0.2.1/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
|
@ -75,8 +77,8 @@ github.com/pion/transport v0.8.10 h1:lTiobMEw2PG6BH/mgIVqTV2mBp/mPT+IJLaN8ZxgdHk
|
|||
github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
|
||||
github.com/pion/turn v1.4.0 h1:7NUMRehQz4fIo53Qv9ui1kJ0Kr1CA82I81RHKHCeM80=
|
||||
github.com/pion/turn v1.4.0/go.mod h1:aDSi6hWX/hd1+gKia9cExZOR0MU95O7zX9p3Gw/P2aU=
|
||||
github.com/pion/webrtc/v2 v2.1.17 h1:/z7Ol6Mx93tfkpvb8aofj10mzjtJhzXcn1iYkt3WVoY=
|
||||
github.com/pion/webrtc/v2 v2.1.17/go.mod h1:m0rKlYgLRZWyhmcMWegpF6xtK1ASxmOg8DAR74ttzQY=
|
||||
github.com/pion/webrtc/v2 v2.1.18 h1:g0VN0xfEUSlVNfQmlCD6yOeXy/tMaktESBmHMnBS3bk=
|
||||
github.com/pion/webrtc/v2 v2.1.18/go.mod h1:m0rKlYgLRZWyhmcMWegpF6xtK1ASxmOg8DAR74ttzQY=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
|
|
@ -3,5 +3,7 @@ package audio
|
|||
const (
|
||||
ClockRate = 48 // KHz
|
||||
FrameTime = 20 // ms
|
||||
Samples = ClockRate * FrameTime
|
||||
Channels = 2 // Stereo
|
||||
|
||||
Samples = ClockRate * FrameTime
|
||||
)
|
||||
|
|
|
@ -14,10 +14,11 @@ import (
|
|||
)
|
||||
|
||||
type Client struct {
|
||||
ID int
|
||||
Name string
|
||||
Status int
|
||||
Conn *websocket.Conn
|
||||
ID int
|
||||
Name string
|
||||
Status int
|
||||
Conn *websocket.Conn
|
||||
Connected bool
|
||||
|
||||
PeerConns map[int]*webrtc.PeerConnection
|
||||
PeerConnLock *sync.Mutex
|
||||
|
@ -25,10 +26,12 @@ type Client struct {
|
|||
In chan *Message
|
||||
Out chan *Message
|
||||
|
||||
AudioTracks map[int]*webrtc.Track
|
||||
SequenceNumber uint16
|
||||
AudioTracks map[int]*webrtc.Track
|
||||
|
||||
VoiceIn chan []int16
|
||||
VoiceIn chan []int16
|
||||
VoiceInLastActive time.Time
|
||||
VoiceInTransmitting bool
|
||||
VoiceInLock *sync.Mutex
|
||||
|
||||
VoiceOut []chan *rtp.Packet
|
||||
VoiceOutClient []int
|
||||
|
@ -39,9 +42,17 @@ type Client struct {
|
|||
}
|
||||
|
||||
func NewClient(conn *websocket.Conn) *Client {
|
||||
c := Client{Conn: conn, Name: "Anonymous", PeerConns: make(map[int]*webrtc.PeerConnection), PeerConnLock: new(sync.Mutex), In: make(chan *Message, 10), Out: make(chan *Message, 10), SequenceNumber: 1, AudioTracks: make(map[int]*webrtc.Track), VoiceIn: make(chan []int16, 10), Terminated: make(chan bool)}
|
||||
|
||||
c.VoiceOutLock = new(sync.Mutex)
|
||||
c := Client{Conn: conn,
|
||||
Name: "Anonymous",
|
||||
PeerConns: make(map[int]*webrtc.PeerConnection),
|
||||
PeerConnLock: new(sync.Mutex),
|
||||
In: make(chan *Message, 10),
|
||||
Out: make(chan *Message, 10),
|
||||
AudioTracks: make(map[int]*webrtc.Track),
|
||||
VoiceIn: make(chan []int16, 10),
|
||||
VoiceInLock: new(sync.Mutex),
|
||||
VoiceOutLock: new(sync.Mutex),
|
||||
Terminated: make(chan bool)}
|
||||
|
||||
go c.handleRead()
|
||||
go c.handleWrite()
|
||||
|
|
|
@ -5,20 +5,24 @@ import "fmt"
|
|||
type MessageType int
|
||||
|
||||
const (
|
||||
MessageBinary MessageType = 2
|
||||
MessagePing MessageType = 101
|
||||
MessagePong MessageType = 102
|
||||
MessageCall MessageType = 103
|
||||
MessageAnswer MessageType = 104
|
||||
MessageConnect MessageType = 110
|
||||
MessageJoin MessageType = 111
|
||||
MessageQuit MessageType = 112
|
||||
MessageNick MessageType = 113
|
||||
MessageTopic MessageType = 114
|
||||
MessageAction MessageType = 115
|
||||
MessageDisconnect MessageType = 119
|
||||
MessageChat MessageType = 120
|
||||
MessageUsers MessageType = 121
|
||||
MessageBinary MessageType = 2
|
||||
MessagePing MessageType = 101
|
||||
MessagePong MessageType = 102
|
||||
MessageCall MessageType = 103
|
||||
MessageAnswer MessageType = 104
|
||||
MessageConnect MessageType = 110
|
||||
MessageJoin MessageType = 111
|
||||
MessageQuit MessageType = 112
|
||||
MessageNick MessageType = 113
|
||||
MessageTopic MessageType = 114
|
||||
MessageAction MessageType = 115
|
||||
MessageDisconnect MessageType = 119
|
||||
MessageChat MessageType = 120
|
||||
MessageTypingStart MessageType = 121
|
||||
MessageTypingStop MessageType = 122
|
||||
MessageTransmitStart MessageType = 123
|
||||
MessageTransmitStop MessageType = 124
|
||||
MessageUsers MessageType = 130
|
||||
)
|
||||
|
||||
func (t MessageType) String() string {
|
||||
|
|
|
@ -53,13 +53,6 @@ nav ul, aside ul {
|
|||
font: 1.2em Helvetica, arial, sans-serif;
|
||||
}
|
||||
|
||||
@media (min-height: 500px) {
|
||||
.wrapper {
|
||||
grid-template-rows: min-content 1fr 1fr 2fr min-content min-content;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
.wrapper {
|
||||
grid-template-columns: 1fr 4fr;
|
||||
|
@ -67,21 +60,28 @@ nav ul, aside ul {
|
|||
grid-template-areas: "sideleft content" "sideright content" "status content" "footer footer";
|
||||
}
|
||||
|
||||
nav ul {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
#voiceptt {
|
||||
height: 100px !important;
|
||||
}
|
||||
|
||||
.sideleft, .sideright, .status {
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 700px) {
|
||||
@media (min-width: 750px) {
|
||||
.wrapper {
|
||||
grid-template-columns: 1fr 5fr 1fr;
|
||||
grid-template-rows: 1fr min-content min-content;
|
||||
grid-template-areas: "sideleft content sideright" "status content sideright" "footer footer footer"
|
||||
}
|
||||
|
||||
nav ul {
|
||||
flex-direction: column;
|
||||
#voiceptt {
|
||||
height: 100px !important;
|
||||
}
|
||||
|
||||
.sideleft, .sideright, .status {
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,10 +89,6 @@ nav ul, aside ul {
|
|||
padding: 7px;
|
||||
}
|
||||
|
||||
.sideleft, .sideright, .status {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
|
@ -107,6 +103,15 @@ button, #chathistory, #chatinput {
|
|||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.headericon {
|
||||
display: inline-block;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.widelinks a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#chathistory {
|
||||
overflow-x: auto;
|
||||
overflow-y: scroll;
|
||||
|
@ -123,8 +128,18 @@ button, #chathistory, #chatinput {
|
|||
|
||||
#voiceptt {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
height: 125px;
|
||||
margin-top: 7px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.voiceinactive {
|
||||
display: inline-block;
|
||||
width: 27px;
|
||||
}
|
||||
|
||||
.voiceactive {
|
||||
display: inline-block;
|
||||
width: 27px;
|
||||
padding-left: 4px;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
var printStats = false;
|
||||
var displayStats = false;
|
||||
|
||||
var socket = null;
|
||||
var ReconnectDelay = 0;
|
||||
|
@ -34,6 +34,8 @@ var muteOnMouseUp = true;
|
|||
var lastPing = 0;
|
||||
var userPing = 0;
|
||||
|
||||
var userList = '';
|
||||
var userListVoice = '';
|
||||
var userListStatus = 'Loading...';
|
||||
|
||||
var MessageBinary = 2;
|
||||
|
@ -49,7 +51,11 @@ var MessageTopic = 114;
|
|||
var MessageAction = 115;
|
||||
var MessageDisconnect = 119;
|
||||
var MessageChat = 120;
|
||||
var MessageUsers = 121;
|
||||
var MessageTypingStart = 121;
|
||||
var MessageTypingStop = 122;
|
||||
var MessageTransmitStart = 123;
|
||||
var MessageTransmitStop = 124;
|
||||
var MessageUsers = 130;
|
||||
|
||||
var tagsToReplace = {
|
||||
'&': '&',
|
||||
|
@ -75,7 +81,11 @@ function HandleInput(e) {
|
|||
}
|
||||
|
||||
if ($("#chatinput").val() != "") {
|
||||
w(MessageChat, $("#chatinput").val());
|
||||
if ($("#chatinput").val() == "/debugstats") {
|
||||
enableStats();
|
||||
} else {
|
||||
w(MessageChat, $("#chatinput").val());
|
||||
}
|
||||
}
|
||||
|
||||
$("#chatinput").val('');
|
||||
|
@ -84,11 +94,28 @@ function HandleInput(e) {
|
|||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$("#voiceButtonJoin").on("click touchstart", function () {
|
||||
$("#togglevoice").on("click touchstart", function () {
|
||||
if (peerConnections.length > 0 || voice) {
|
||||
// Quit voice chat
|
||||
|
||||
voice = false;
|
||||
|
||||
w(MessageQuit, "");
|
||||
|
||||
peerConnections.forEach(function (pc) {
|
||||
pc.close();
|
||||
});
|
||||
peerConnections = [];
|
||||
|
||||
$('#voicepttheader').css('display', 'none');
|
||||
$('#voicepttcontainer').css('display', 'none');
|
||||
|
||||
updateStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Join voice chat
|
||||
|
||||
var i;
|
||||
for (i = 0; i < numConnections; i++) {
|
||||
peerConnections.push(createPeerConnection(i));
|
||||
|
@ -127,7 +154,7 @@ $(document).ready(function () {
|
|||
return false;
|
||||
});
|
||||
|
||||
$("#inputheader, #inputfooter").on("click touchstart", function (e) {
|
||||
$("#inputheader, #voicepttheader").on("click touchstart", function (e) {
|
||||
$("#chatinput").focus();
|
||||
|
||||
return false;
|
||||
|
@ -139,29 +166,6 @@ $(document).ready(function () {
|
|||
return false;
|
||||
});
|
||||
|
||||
$("#voiceButtonQuit").on("click touchstart", function () {
|
||||
if (!voice) {
|
||||
return;
|
||||
}
|
||||
voice = false;
|
||||
|
||||
w(MessageQuit, "");
|
||||
|
||||
peerConnections.forEach(function (pc) {
|
||||
pc.close();
|
||||
});
|
||||
peerConnections = [];
|
||||
|
||||
$('#voicepttcontainer').css('display', 'none');
|
||||
$('#voiceinactiveleft').css('display', 'inline-block');
|
||||
$('#voiceactiveleft').css('display', 'none');
|
||||
$('#voiceinactiveright').css('display', 'inline-block');
|
||||
$('#voiceactiveright').css('display', 'none');
|
||||
$('#voiceButton').html('Join voice chat');
|
||||
|
||||
updateUserStatus();
|
||||
});
|
||||
|
||||
window.setInterval(() => {
|
||||
if (!webSocketReady()) {
|
||||
return;
|
||||
|
@ -170,43 +174,6 @@ $(document).ready(function () {
|
|||
pingServer();
|
||||
}, 15000);
|
||||
|
||||
if (printStats) {
|
||||
window.setInterval(() => {
|
||||
// TODO Fix
|
||||
|
||||
if (!pc) {
|
||||
return;
|
||||
}
|
||||
const sender = pc.getSenders()[0];
|
||||
if (sender === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
sender.getStats().then(stats => {
|
||||
let statsOutput = "";
|
||||
|
||||
stats.forEach(report => {
|
||||
if (report.type == "local-candidate" || report.type == "remote-candidate") {
|
||||
return;
|
||||
} else if (report.type == "candidate-pair" && (report.bytesSent == 0 && report.bytesReceived == 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
statsOutput += `<b>Report: ${report.type}</b>\n<strong>ID:</strong> ${report.id}<br>\n` +
|
||||
`<strong>Timestamp:</strong> ${report.timestamp}<br>\n`;
|
||||
|
||||
Object.keys(report).forEach(statName => {
|
||||
if (statName !== "id" && statName !== "timestamp" && statName !== "type") {
|
||||
statsOutput += `<strong>${statName}:</strong> ${report[statName]}<br>\n`;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector("#stats").innerHTML = statsOutput;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
nickname = prompt("What is your name?", nickname);
|
||||
Connect();
|
||||
});
|
||||
|
@ -246,14 +213,10 @@ function createPeerConnection(id) {
|
|||
Log("Note: Push-to-talk is bound to <F8>");
|
||||
}
|
||||
|
||||
$('#voicepttheader').css('display', 'table-row');
|
||||
$('#voicepttcontainer').css('display', 'table-row');
|
||||
$('#voiceinactiveleft').css('display', 'none');
|
||||
$('#voiceactiveleft').css('display', 'inline-block');
|
||||
$('#voiceinactiveright').css('display', 'none');
|
||||
$('#voiceactiveright').css('display', 'inline-block');
|
||||
$('#voiceButton').html('Quit voice chat');
|
||||
|
||||
updateUserStatus();
|
||||
updateStatus();
|
||||
};
|
||||
|
||||
if (id == 0) {
|
||||
|
@ -295,8 +258,8 @@ function onRTCDescription(id) {
|
|||
|
||||
peerConnections[id].setLocalDescription(desc)
|
||||
.then(() => {
|
||||
desc.sdp = maybePreferCodec(desc.sdp, 'audio', 'send', 'opus');
|
||||
desc.sdp = maybePreferCodec(desc.sdp, 'audio', 'receive', 'opus');
|
||||
desc.sdp = maybePreferCodec(desc.sdp, 'audio', 'send', 'opus/48000');
|
||||
desc.sdp = maybePreferCodec(desc.sdp, 'audio', 'receive', 'opus/48000');
|
||||
|
||||
console.log(`PC ${id} onRTCDescription`);
|
||||
|
||||
|
@ -313,7 +276,7 @@ function Connect() {
|
|||
}
|
||||
|
||||
userListStatus = 'Connecting...';
|
||||
updateUserStatus();
|
||||
updateStatus();
|
||||
|
||||
Log("Connecting...");
|
||||
|
||||
|
@ -341,7 +304,7 @@ function Connect() {
|
|||
}
|
||||
|
||||
userListStatus = "";
|
||||
updateUserStatus();
|
||||
updateStatus();
|
||||
|
||||
w(MessageNick, nickname);
|
||||
|
||||
|
@ -363,7 +326,7 @@ function Connect() {
|
|||
if (parseInt(p.M, 10) == lastPing) {
|
||||
userPing = Date.now() - lastPing;
|
||||
|
||||
updateUserStatus();
|
||||
updateStatus();
|
||||
}
|
||||
} else if (p.T == MessageAnswer) {
|
||||
if (p.PC === undefined || p.PC > peerConnections.length) {
|
||||
|
@ -383,19 +346,25 @@ function Connect() {
|
|||
return;
|
||||
}
|
||||
|
||||
Log(escapeEntities(p.N) + " joined #lobby voice chat");
|
||||
Log(escapeEntities(p.N) + " joined &lobby");
|
||||
} else if (p.T == MessageQuit) {
|
||||
if (p.N === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log(escapeEntities(p.N) + " quit #lobby voice chat");
|
||||
Log(escapeEntities(p.N) + " quit &lobby");
|
||||
} else if (p.T == MessageDisconnect) {
|
||||
if (p.N === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log(escapeEntities(p.N) + " disconnected");
|
||||
} else if (p.T == MessageNick) {
|
||||
if (p.N === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log(escapeEntities(p.N) + " is now known as " + escapeEntities(p.M));
|
||||
} else if (p.T == MessageChat) {
|
||||
if (p.N === undefined) {
|
||||
return;
|
||||
|
@ -403,10 +372,44 @@ function Connect() {
|
|||
|
||||
Log("<" + escapeEntities(p.N) + "> " + escapeEntities(p.M));
|
||||
} else if (p.T == MessageUsers) {
|
||||
var userListNew = '<ul>';
|
||||
|
||||
var u = JSON.parse(p.M);
|
||||
userListNew += '<li><b>' + u.length + ' user' + (u.length != 1 ? 's' : '') + '</b></li>';
|
||||
for (let i = 0; i < u.length; i++) {
|
||||
userListNew += '<li>' + u[i].N + '</li>';
|
||||
}
|
||||
|
||||
userListNew += '</ul>';
|
||||
|
||||
userList = userListNew;
|
||||
|
||||
var userListVoiceNew = '<ul style="padding-left: 5px;">';
|
||||
|
||||
var u = JSON.parse(p.M);
|
||||
for (let i = 0; i < u.length; i++) {
|
||||
// TODO: Parse
|
||||
if (u[i].C == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
userListVoiceNew += '<li>';
|
||||
if (voice) {
|
||||
userListVoiceNew += '<span id="voiceindicator' + u[i].ID + '"><div class="voiceinactive">🔈</div></span> ';
|
||||
}
|
||||
userListVoiceNew += u[i].N + '</li>';
|
||||
}
|
||||
|
||||
userListVoiceNew += '</ul>';
|
||||
|
||||
userListVoice = userListVoiceNew;
|
||||
|
||||
updateUserList();
|
||||
} else if (p.T == MessageTransmitStart || p.T == MessageTransmitStop) {
|
||||
if (!voice || p.S === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
$("#voiceindicator" + p.S).html(p.T == MessageTransmitStart ? '<div class="voiceactive">🔊</div>' : '<div class="voiceinactive">🔈</div>');
|
||||
}
|
||||
} else {
|
||||
// TODO Binary data
|
||||
|
@ -457,7 +460,8 @@ function waitForSocketConnection(socket, callback) {
|
|||
|
||||
function Log(msg) {
|
||||
var date = new Date();
|
||||
$('#chathistory').append(chatprefix + date.getHours() + ":" + (date.getMinutes() < 10 ? "0" : "") + date.getMinutes() + " " + msg + "\n");
|
||||
$('#chathistory').append(chatprefix + date.getHours() + ":" + (date.getMinutes() < 10 ? "0" : "") + date.getMinutes() + " " + msg + "\n")
|
||||
$('#chathistory').scrollTop($('#chathistory').prop("scrollHeight"));
|
||||
|
||||
if (chatprefix == "") {
|
||||
chatprefix = "<br>";
|
||||
|
@ -476,7 +480,7 @@ function StartPTT() {
|
|||
var sender = peerConnections[0].getSenders()[0];
|
||||
sender.replaceTrack(audioTrack);
|
||||
|
||||
updateUserStatus();
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
function StopPTT() {
|
||||
|
@ -491,10 +495,10 @@ function StopPTT() {
|
|||
var sender = peerConnections[0].getSenders()[0];
|
||||
sender.replaceTrack(null);
|
||||
|
||||
updateUserStatus();
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
function updateUserStatus() {
|
||||
function updateStatus() {
|
||||
var out = '';
|
||||
if (userPing > 0) {
|
||||
out += userPing + 'ms ping';
|
||||
|
@ -509,6 +513,14 @@ function updateUserStatus() {
|
|||
}
|
||||
|
||||
$('#userstatus').html(out);
|
||||
|
||||
$('#togglevoice').html(peerConnections.length > 0 ? 'Quit' : 'Join');
|
||||
}
|
||||
|
||||
function updateUserList() {
|
||||
$('#userlistvoice1').html(userListVoice);
|
||||
|
||||
$('#sideright').html(userList);
|
||||
}
|
||||
|
||||
function w(t, m) {
|
||||
|
@ -535,6 +547,51 @@ function escapeEntities(str) {
|
|||
return str.replace(/[&<>]/g, escapeEntitiesCallback);
|
||||
}
|
||||
|
||||
function enableStats() {
|
||||
if (displayStats) {
|
||||
return;
|
||||
}
|
||||
|
||||
displayStats = true;
|
||||
|
||||
window.setInterval(() => {
|
||||
var i;
|
||||
for (i = 0; i < peerConnections.length; i++) {
|
||||
var senders = peerConnections[i].getSenders();
|
||||
|
||||
var j;
|
||||
for (j = 0; j < senders.length; j++) {
|
||||
senders[j].getStats().then(stats => {
|
||||
let statsOutput = "";
|
||||
|
||||
stats.forEach(report => {
|
||||
if (report.type == "local-candidate" || report.type == "remote-candidate") {
|
||||
return;
|
||||
} else if (report.type == "candidate-pair" && (report.bytesSent == 0 && report.bytesReceived == 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
statsOutput += `<b>PC${i}S${j} Report: ${report.type}</b>\n<strong>ID:</strong> ${report.id}<br>\n` +
|
||||
`<strong>Timestamp:</strong> ${report.timestamp}<br>\n`;
|
||||
|
||||
Object.keys(report).forEach(statName => {
|
||||
if (statName !== "id" && statName !== "timestamp" && statName !== "type") {
|
||||
statsOutput += `<strong>${statName}:</strong> ${report[statName]}<br>\n`;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (statsOutput != "") {
|
||||
Log(statsOutput);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
Log("Debug stats enabled");
|
||||
}
|
||||
|
||||
// Copied from AppRTC's sdputils.js:
|
||||
|
||||
// Sets |codec| as the default |type| codec if it's present.
|
||||
|
|
|
@ -2,29 +2,30 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>harmony</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="assets/css/harmony.css">
|
||||
<script src="assets/js/jquery.js"></script>
|
||||
<script src="assets/js/harmony.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<nav class="sideleft">
|
||||
<nav class="sideleft" id="sideleft">
|
||||
<ul>
|
||||
<li><a href="">Voice user 1</a></li>
|
||||
<li><a href="">Voice user 2</a></li>
|
||||
<li><a href="">Voice user 3</a></li>
|
||||
<li style="margin-bottom: 10px;"><div class="headericon">📰</div>Text Channels</li>
|
||||
<ul class="widelinks" style="padding-left: 5px;">
|
||||
<li><b>#lobby</b></li>
|
||||
</ul>
|
||||
<li style="margin-bottom: 10px;"> </li>
|
||||
<li style="margin-bottom: 10px;"><div class="headericon">🔊</div> Voice Channels</li>
|
||||
<ul style="padding-left: 5px;">
|
||||
<li style="margin-bottom: 5px;"><b>&lobby</b> <a href="#" id="togglevoice">Join</a></li>
|
||||
<li id="userlistvoice1"></li>
|
||||
</ul>
|
||||
</ul>
|
||||
</nav>
|
||||
<article class="content" id="chathistory">
|
||||
</article>
|
||||
<aside class="sideright">
|
||||
<ul>
|
||||
<li><a href="">User 1</a></li>
|
||||
<li><a href="">User 2</a></li>
|
||||
<li><a href="">User 3</a></li>
|
||||
<li><a href="">User 4</a></li>
|
||||
<li><a href="">User 5</a></li>
|
||||
</ul>
|
||||
<aside class="sideright" id="sideright">
|
||||
</aside>
|
||||
<div class="status">
|
||||
<div id="userstatus">Loading...</div>
|
||||
|
@ -41,9 +42,9 @@
|
|||
<textarea id="chatinput" placeholder="Message #lobby" rows="2"></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="inputfooter">
|
||||
<tr id="voicepttheader" style="display: none;">
|
||||
<td colspan="2">
|
||||
<hr style="color: #777777;">
|
||||
<hr style="color: #eeeeee;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="voicepttcontainer" style="display: none;">
|
||||
|
@ -51,22 +52,6 @@
|
|||
<button id="voiceptt">Push-To-Talk</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div id="voiceinactiveleft">
|
||||
<button id="voiceButtonJoin">Join voice chat</button>
|
||||
</div>
|
||||
<div id="voiceactiveleft" style="display: none;"></div>
|
||||
</td>
|
||||
<td align="right">
|
||||
<div id="voiceinactiveright" style="">
|
||||
|
||||
</div>
|
||||
<div id="voiceactiveright" style="display: none;">
|
||||
<button id="voiceButtonQuit">Quit voice chat</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,22 @@
|
|||
package web
|
||||
|
||||
import "regexp"
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type UserList []*User
|
||||
|
||||
func (u UserList) Len() int { return len(u) }
|
||||
func (u UserList) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
|
||||
func (u UserList) Less(i, j int) bool {
|
||||
return strings.ToLower(u[i].N) < strings.ToLower(u[j].N)
|
||||
}
|
||||
|
||||
type User struct {
|
||||
N string
|
||||
V bool
|
||||
ID int
|
||||
N string // Nickname
|
||||
C int // Channel
|
||||
}
|
||||
|
||||
var nickRegexp = regexp.MustCompile(`[^a-zA-Z0-9_\-!@#$%^&*+=,./?]+`)
|
||||
|
|
|
@ -2,9 +2,11 @@ package web
|
|||
|
||||
import "C"
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -66,6 +68,8 @@ func NewWebInterface(address string, path string) *WebInterface {
|
|||
|
||||
go w.handleIncomingClients()
|
||||
|
||||
go w.handleExpireTransmit()
|
||||
|
||||
addressSplit := strings.Split(address, ",")
|
||||
for _, add := range addressSplit {
|
||||
add := add // Capture
|
||||
|
@ -92,6 +96,8 @@ func (w *WebInterface) handleIncomingClients() {
|
|||
go func(c *Client) {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
c.Connected = true
|
||||
|
||||
w.updateUserList()
|
||||
|
||||
w.ClientsLock.Lock()
|
||||
|
@ -107,6 +113,27 @@ func (w *WebInterface) handleIncomingClients() {
|
|||
}
|
||||
}
|
||||
|
||||
func (w *WebInterface) handleExpireTransmit() {
|
||||
t := time.NewTicker(250 * time.Millisecond)
|
||||
for range t.C {
|
||||
w.ClientsLock.Lock()
|
||||
for _, wc := range w.Clients {
|
||||
wc.VoiceInLock.Lock()
|
||||
if wc.VoiceInTransmitting && time.Since(wc.VoiceInLastActive) >= 100*time.Millisecond {
|
||||
wc.VoiceInTransmitting = false
|
||||
|
||||
for _, wcc := range w.Clients {
|
||||
if len(wcc.AudioTracks) > 0 {
|
||||
wcc.Out <- &Message{T: MessageTransmitStop, S: wc.ID}
|
||||
}
|
||||
}
|
||||
}
|
||||
wc.VoiceInLock.Unlock()
|
||||
}
|
||||
w.ClientsLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebInterface) handleRead(c *Client) {
|
||||
for msg := range c.In {
|
||||
if msg == nil {
|
||||
|
@ -132,10 +159,18 @@ func (w *WebInterface) handleRead(c *Client) {
|
|||
|
||||
c.Out <- &Message{T: MessageAnswer, PC: msg.PC, M: answer}
|
||||
case MessageChat:
|
||||
if bytes.HasPrefix(bytes.ToLower(msg.M), []byte("/nick ")) {
|
||||
go func(mm []byte) {
|
||||
c.In <- &Message{S: c.ID, T: MessageNick, M: mm}
|
||||
}(msg.M[6:])
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
w.ClientsLock.Lock()
|
||||
|
||||
for _, wc := range w.Clients {
|
||||
wc.Out <- &Message{S: c.ID, N: c.Name, T: MessageChat, M: []byte(msg.M)}
|
||||
wc.Out <- &Message{S: c.ID, N: c.Name, T: MessageChat, M: msg.M}
|
||||
}
|
||||
|
||||
w.ClientsLock.Unlock()
|
||||
|
@ -147,7 +182,7 @@ func (w *WebInterface) handleRead(c *Client) {
|
|||
oldNick := c.Name
|
||||
c.Name = Nickname(string(msg.M))
|
||||
|
||||
if oldNick != "Anonymous" {
|
||||
if c.Connected {
|
||||
msg := &Message{S: c.ID, N: oldNick, T: MessageNick, M: []byte(c.Name)}
|
||||
for _, wc := range w.Clients {
|
||||
wc.Out <- msg
|
||||
|
@ -155,6 +190,8 @@ func (w *WebInterface) handleRead(c *Client) {
|
|||
}
|
||||
|
||||
w.ClientsLock.Unlock()
|
||||
|
||||
w.updateUserList()
|
||||
case MessageConnect, MessageJoin, MessageQuit, MessageDisconnect:
|
||||
w.ClientsLock.Lock()
|
||||
|
||||
|
@ -230,11 +267,18 @@ func (w *WebInterface) updateUserList() {
|
|||
|
||||
msg := &Message{T: MessageUsers}
|
||||
|
||||
var userList []*User
|
||||
var userList UserList
|
||||
for _, wc := range w.Clients {
|
||||
userList = append(userList, &User{N: wc.Name, V: len(wc.VoiceOut) > 0})
|
||||
c := 0
|
||||
if len(wc.AudioTracks) > 0 {
|
||||
c = 1
|
||||
}
|
||||
|
||||
userList = append(userList, &User{ID: wc.ID, N: wc.Name, C: c})
|
||||
}
|
||||
|
||||
sort.Sort(userList)
|
||||
|
||||
var err error
|
||||
msg.M, err = json.Marshal(userList)
|
||||
if err != nil {
|
||||
|
@ -248,7 +292,7 @@ func (w *WebInterface) updateUserList() {
|
|||
w.ClientsLock.Unlock()
|
||||
}
|
||||
|
||||
func (w *WebInterface) answerRTC(c *Client, peerConnID int, sdp []byte) ([]byte, error) {
|
||||
func (w *WebInterface) answerRTC(c *Client, peerConnID int, offerSDP []byte) ([]byte, error) {
|
||||
c.PeerConnLock.Lock()
|
||||
defer c.PeerConnLock.Unlock()
|
||||
|
||||
|
@ -259,7 +303,7 @@ func (w *WebInterface) answerRTC(c *Client, peerConnID int, sdp []byte) ([]byte,
|
|||
return nil, errors.New("already have next peerconn")
|
||||
}
|
||||
|
||||
offer := webrtc.SessionDescription{Type: webrtc.SDPTypeOffer, SDP: string(sdp)}
|
||||
offer := webrtc.SessionDescription{Type: webrtc.SDPTypeOffer, SDP: string(offerSDP)}
|
||||
|
||||
m := webrtc.MediaEngine{}
|
||||
|
||||
|
@ -293,7 +337,7 @@ func (w *WebInterface) answerRTC(c *Client, peerConnID int, sdp []byte) ([]byte,
|
|||
|
||||
var payloadType uint8
|
||||
for i := range audioCodecs {
|
||||
if audioCodecs[i].Name == webrtc.Opus {
|
||||
if audioCodecs[i].Name == webrtc.Opus && audioCodecs[i].ClockRate == audio.ClockRate*1000 && audioCodecs[i].Channels == audio.Channels {
|
||||
payloadType = audioCodecs[i].PayloadType
|
||||
break
|
||||
}
|
||||
|
@ -344,9 +388,23 @@ func (w *WebInterface) answerRTC(c *Client, peerConnID int, sdp []byte) ([]byte,
|
|||
return
|
||||
}
|
||||
|
||||
w.ClientsLock.Lock()
|
||||
|
||||
c.VoiceInLock.Lock()
|
||||
if !c.VoiceInTransmitting {
|
||||
c.VoiceInTransmitting = true
|
||||
|
||||
for _, wc := range w.Clients {
|
||||
if len(wc.AudioTracks) > 0 {
|
||||
wc.Out <- &Message{T: MessageTransmitStart, S: c.ID}
|
||||
}
|
||||
}
|
||||
}
|
||||
c.VoiceInLastActive = time.Now()
|
||||
c.VoiceInLock.Unlock()
|
||||
|
||||
// TODO trim initial x ms transmitting to remove noise (configurable)
|
||||
|
||||
w.ClientsLock.Lock()
|
||||
for ci, wc := range w.Clients {
|
||||
if ci == c.ID {
|
||||
continue
|
||||
|
@ -396,7 +454,12 @@ func (w *WebInterface) answerRTC(c *Client, peerConnID int, sdp []byte) ([]byte,
|
|||
log.Printf("%d ice state -> %s\n", c.ID, connectionState)
|
||||
})
|
||||
|
||||
answer, err := pc.CreateAnswer(nil)
|
||||
answerOptions := &webrtc.AnswerOptions{OfferAnswerOptions: webrtc.OfferAnswerOptions{VoiceActivityDetection: false}}
|
||||
|
||||
// TODO webrtc does not yet support AnswerOptions
|
||||
answerOptions = nil
|
||||
|
||||
answer, err := pc.CreateAnswer(answerOptions)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue