Add user list

This commit is contained in:
Trevor Slocum 2019-12-11 18:31:23 -08:00
parent 2443590cde
commit da3ba57ca9
10 changed files with 323 additions and 171 deletions

3
go.mod
View File

@ -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
View File

@ -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=

View File

@ -3,5 +3,7 @@ package audio
const (
ClockRate = 48 // KHz
FrameTime = 20 // ms
Samples = ClockRate * FrameTime
Channels = 2 // Stereo
Samples = ClockRate * FrameTime
)

View File

@ -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()

View File

@ -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 {

View File

@ -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;
}

View File

@ -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 &lt;F8&gt;");
}
$('#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("&lt;" + escapeEntities(p.N) + "&gt; " + 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">&#128264;</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">&#128266;</div>' : '<div class="voiceinactive">&#128264;</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.

View File

@ -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">&#128240;</div>Text Channels</li>
<ul class="widelinks" style="padding-left: 5px;">
<li><b>#lobby</b></li>
</ul>
<li style="margin-bottom: 10px;">&nbsp;</li>
<li style="margin-bottom: 10px;"><div class="headericon">&#128266;</div> Voice Channels</li>
<ul style="padding-left: 5px;">
<li style="margin-bottom: 5px;"><b>&lobby</b> &nbsp; <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="">
&nbsp;
</div>
<div id="voiceactiveright" style="display: none;">
<button id="voiceButtonQuit">Quit voice chat</button>
</div>
</td>
</tr>
</table>
</footer>
</div>

View File

@ -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_\-!@#$%^&*+=,./?]+`)

View File

@ -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)
}