Browse Source

Add user connected/disconnected events

wip
Trevor Slocum 3 years ago
parent
commit
c54b7c799e
  1. 24
      pkg/web/client.go
  2. 49
      pkg/web/message.go
  3. 25
      pkg/web/public/assets/css/harmony.css
  4. 129
      pkg/web/public/assets/js/harmony.js
  5. 34
      pkg/web/public/index.html
  6. 94
      pkg/web/web.go

24
pkg/web/client.go

@ -3,7 +3,7 @@ package web
import (
"encoding/json"
"log"
"sync"
"strconv"
"time"
"github.com/gorilla/websocket"
@ -13,20 +13,20 @@ import (
type Client struct {
ID int
Name string
Status int
Conn *websocket.Conn
PeerConn *webrtc.PeerConnection
In chan *Message
Out chan *Message
AudioTrack *webrtc.Track
AudioTrackLock *sync.RWMutex
AudioTrack *webrtc.Track
Terminated chan bool
}
func NewClient(conn *websocket.Conn) *Client {
c := Client{Conn: conn, In: make(chan *Message, 10), Out: make(chan *Message, 10), Terminated: make(chan bool), AudioTrackLock: new(sync.RWMutex)}
c := Client{Conn: conn, Name: "Anonymous", In: make(chan *Message, 10), Out: make(chan *Message, 10), Terminated: make(chan bool)}
go c.handleRead()
go c.handleWrite()
@ -36,17 +36,19 @@ func NewClient(conn *websocket.Conn) *Client {
func (c *Client) handleRead() {
var (
messageType int
message []byte
err error
messageTypeInt int
messageType MessageType
message []byte
err error
)
for {
c.Conn.SetReadDeadline(time.Now().Add(1 * time.Minute))
messageType, message, err = c.Conn.ReadMessage()
messageTypeInt, message, err = c.Conn.ReadMessage()
if err != nil || c.Status == -1 {
c.Close()
return
}
messageType = MessageType(messageTypeInt)
in := Message{}
if messageType == 2 {
@ -82,7 +84,7 @@ func (c *Client) handleWrite() {
out, err = json.Marshal(msg)
if err != nil {
//c.Close()
c.Close()
return
}
@ -95,6 +97,7 @@ func (c *Client) Close() {
return
}
c.Status = -1
c.AudioTrack = nil
if c.PeerConn != nil {
c.PeerConn.Close()
@ -106,7 +109,8 @@ func (c *Client) Close() {
c.In <- nil
c.Out <- nil
log.Printf("closing %d: %+v", c.ID, errors.New("test"))
// TODO Place behind debug/verbose flag
log.Printf("%+v", errors.New("Closing client #"+strconv.Itoa(c.ID)))
go func() {
c.Terminated <- true

49
pkg/web/message.go

@ -1,8 +1,49 @@
package web
type MessageType int
const (
MessageBinary = 2
MessagePing = 100
MessageCall = 101
MessageAnswer = 102
MessageBinary MessageType = 2
MessagePing MessageType = 100
MessageCall MessageType = 101
MessageAnswer MessageType = 102
MessageConnect MessageType = 110
MessageJoin MessageType = 111
MessageQuit MessageType = 112
MessageNick MessageType = 113
MessageTopic MessageType = 114
MessageAction MessageType = 115
MessageDisconnect MessageType = 119
MessageChat MessageType = 120
)
func (t MessageType) String() string {
switch t {
case MessageBinary:
return "Binary"
case MessagePing:
return "Ping"
case MessageCall:
return "Call"
case MessageAnswer:
return "Answer"
case MessageConnect:
return "Connect"
case MessageJoin:
return "Join"
case MessageQuit:
return "Quit"
case MessageNick:
return "Nick"
case MessageTopic:
return "Topic"
case MessageAction:
return "Action"
case MessageDisconnect:
return "Disconnect"
case MessageChat:
return "Chat"
default:
return "Unknown"
}
}

25
pkg/web/public/assets/css/harmony.css

@ -10,6 +10,7 @@ body {
textarea {
margin: 0;
resize: none;
}
* {
@ -20,6 +21,10 @@ td {
vertical-align: top;
}
#chatcontainer {
height: 100%;
}
#chathistory {
overflow-x: auto;
overflow-y: scroll;
@ -27,6 +32,12 @@ td {
width: 100%;
max-height: 100%;
font-family: monospace;
}
#inputheader, #inputfooter {
height: 15px;
}
#inputcontainer {
@ -37,14 +48,14 @@ td {
#chatinput {
width: 100%;
height: 100%;
border: 0;
}
#voiceinactive {
display: inline-block;
#voicepttcontainer {
height: 70px;
}
#voicestatus {
display: inline-block;
width: 50px;
white-space: pre;
}
#voiceptt {
width: 100%;
margin-top: 7px;
}

129
pkg/web/public/assets/js/harmony.js

@ -17,6 +17,28 @@ var RTCOfferOptions = {
var RTCICEServers = [{urls: 'stun:stun.l.google.com:19302'}];
var audioTrack;
var shownPTTHelp = false;
var muteOnMouseUp = true;
var MessageBinary = 2;
var MessagePing = 100;
var MessageCall = 101;
var MessageAnswer = 102;
var MessageConnect = 110;
var MessageJoin = 111;
var MessageQuit = 112;
var MessageNick = 113;
var MessageTopic = 114;
var MessageAction = 115;
var MessageDisconnect = 119;
var MessageChat = 120;
var tagsToReplace = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
};
$(document).keydown(HandleInput);
$(document).keyup(HandleInput);
@ -35,7 +57,7 @@ function HandleInput(e) {
}
if ($("#chatinput").val() != "") {
Log("&lt;tee&gt; " + $("#chatinput").val());
w(MessageChat, $("#chatinput").val());
}
$("#chatinput").val('');
@ -44,7 +66,7 @@ function HandleInput(e) {
}
$(document).ready(function () {
$("#voiceButtonJoin").on("click", function () {
$("#voiceButtonJoin").on("click touchstart", function () {
pc = new RTCPeerConnection({
iceServers: RTCICEServers
});
@ -76,11 +98,17 @@ $(document).ready(function () {
voice = true;
Log("* tee has joined voice chat");
Log("* Push to talk is bound to F8");
$('#voiceinactive').css('display', 'none');
$('#voiceactive').css('display', 'inline-block');
$('#voiceactiveside').css('display', 'inline-block');
if (!shownPTTHelp) {
shownPTTHelp = true;
Log("* Note: Push-to-talk is bound to &lt;F8&gt;");
}
$('#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');
updateVoiceStatus();
@ -91,22 +119,54 @@ $(document).ready(function () {
}).catch(Log);
});
$("#pushtotalk").on("mousedown", function (e) {
$("#voiceptt").on("touchstart", function (e) {
muteOnMouseUp = false;
StartPTT();
return false;
});
$("#voiceptt").on("mousedown", function (e) {
muteOnMouseUp = true;
StartPTT();
return false;
});
$(document).on("mouseup", function (e) {
if (!muteOnMouseUp) {
return;
}
StopPTT();
return false;
});
$(document).on("touchend", function (e) {
StopPTT();
return false;
});
$("#inputheader, #inputcontainer, #inputfooter").on("click", function (e) {
$("#chatinput").focus();
return false;
});
$("#voiceButtonQuit").on("click", function () {
$("#voiceButtonQuit").on("click touchstart", function () {
pc.close();
w(MessageQuit, "");
voice = false;
Log("* tee has quit voice chat");
$('#voiceactive').css('display', 'none');
$('#voiceactiveside').css('display', 'none');
$('#voiceinactive').css('display', 'inline-block');
$('#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');
updateVoiceStatus();
@ -119,7 +179,7 @@ $(document).ready(function () {
return;
}
w(100, "ping");
w(MessagePing, "ping");
}, 15000);
if (printStats) {
@ -172,7 +232,7 @@ function onRTCDescription(desc) {
console.log("onRTCDescription");
console.log(desc);
w(101, desc.sdp);
w(MessageCall, desc.sdp);
}, onRTCDescriptionError);
}
@ -200,7 +260,7 @@ function Connect() {
socket = new WebSocket(wsurl);
socket.onerror = function (e) {
Log(e);
Log("* Connection error");
console.log(e);
};
socket.onopen = function (e) {
@ -208,7 +268,6 @@ function Connect() {
clearTimeout(reconnectTimeout);
}
Log("* Connected");
updateVoiceStatus();
};
socket.onmessage = function (e) {
@ -219,9 +278,22 @@ function Connect() {
try {
if (typeof e.data === "string") {
var p = JSON.parse(e.data);
if (p.M !== undefined) {
p.M = atob(p.M);
}
if (p.T == 102) {
pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: atob(p.M)}));
if (p.T == MessageAnswer) {
pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: p.M}));
} else if (p.T == MessageConnect) {
Log("* " + p.M + " connected");
} else if (p.T == MessageJoin) {
Log("* " + p.M + " joined #lobby voice chat");
} else if (p.T == MessageQuit) {
Log("* " + p.M + " quit #lobby voice chat");
} else if (p.T == MessageDisconnect) {
Log("* " + p.M + " disconnected");
} else if (p.T == MessageChat) {
Log("&lt;Anonymous&gt; " + escapeEntities(p.M));
}
} else {
// TODO Binary data
@ -265,7 +337,8 @@ function waitForSocketConnection(socket, callback) {
}
function Log(msg) {
$('#chathistory').append(chatprefix + msg + "\n");
var date = new Date();
$('#chathistory').append(chatprefix + date.getHours() + ":" + (date.getMinutes() < 10 ? "0" : "") + date.getMinutes() + " " + msg + "\n");
if (chatprefix == "") {
chatprefix = "<br>";
@ -300,13 +373,9 @@ function StopPTT() {
function updateVoiceStatus() {
if (ptt) {
$('#voicestatus').html('<b>PTT Active</b>');
$('#voiceactiveleft').html('<b>Transmitting</b>');
} else {
if (voice) {
$('#voicestatus').html('1 user');
} else {
$('#voicestatus').html('0 users');
}
$('#voiceactiveleft').html('# users voice chatting');
}
}
@ -318,6 +387,14 @@ function w(t, m) {
socket.send(JSON.stringify({T: t, M: btoa(m)}));
}
function escapeEntitiesCallback(tag) {
return tagsToReplace[tag] || tag;
}
function escapeEntities(str) {
return str.replace(/[&<>]/g, escapeEntitiesCallback);
}
// Copied from AppRTC's sdputils.js:
// Sets |codec| as the default |type| codec if it's present.

34
pkg/web/public/index.html

@ -9,30 +9,40 @@
<body>
<table style="width: 100%; height: 100%;">
<tr>
<td id="chatcontainer" colspan="2" style="height: 100%;">
<td id="chatcontainer" colspan="2">
<div id="chathistory"></div>
</td>
</tr>
<tr style="height: 35px;">
<tr id="inputheader">
<td colspan="2">
<hr>
<hr style="color: #eeeeee;">
</td>
</tr>
<tr style="height: 100px;">
<td id="inputcontainer" colspan="2"><textarea id="chatinput" placeholder="Message #lobby"></textarea></td>
<tr style="height: 65px;">
<td id="inputcontainer" colspan="2"><textarea id="chatinput" placeholder="Message #lobby" rows="1"></textarea></td>
</tr>
<tr style="height: 55px;">
<tr id="inputfooter">
<td colspan="2">
<hr style="color: #777777;margin: 0; padding: 0;">
</td>
</tr>
<tr id="voicepttcontainer" style="display: none;">
<td colspan="2">
<button id="voiceptt">Push-To-Talk</button>
</td>
</tr>
<tr style="height: 60px;">
<td>
<div id="voiceinactive">
<div id="voiceinactiveleft">
<button id="voiceButtonJoin">Join voice chat</button>
</div>
<div id="voiceactive" style="display: none;">
<button id="pushtotalk">Push to talk</button>
</div> &nbsp;
<div id="voicestatus"></div>
<div id="voiceactiveleft" style="display: none;"># users voice chatting</div>
</td>
<td align="right">
<div id="voiceactiveside" style="display: none;">
<div id="voiceinactiveright" style="">
# users voice chatting
</div>
<div id="voiceactiveright" style="display: none;">
<button id="voiceButtonQuit">Quit voice chat</button>
</div>
</td>

94
pkg/web/web.go

@ -36,9 +36,9 @@ var upgrader = websocket.Upgrader{
}
type Message struct {
S int // Source
T int // Type
M []byte // Message
S int // Source
T MessageType // Type
M []byte // Message
}
type WebInterface struct {
@ -73,6 +73,11 @@ func (w *WebInterface) handleIncomingClients() {
id := w.nextClientID()
c.ID = id
w.Clients[id] = c
for _, wc := range w.Clients {
wc.Out <- &Message{T: MessageConnect, M: []byte(c.Name)}
}
w.ClientsLock.Unlock()
go w.handleRead(c)
@ -81,12 +86,19 @@ func (w *WebInterface) handleIncomingClients() {
func (w *WebInterface) handleTerminatedClients() {
for {
time.Sleep(1 * time.Second)
time.Sleep(15 * time.Second)
w.ClientsLock.Lock()
for id := range w.Clients {
if w.Clients[id].Status == -1 {
delete(w.Clients, id)
if w.Clients[id].Status != -1 {
continue
}
name := w.Clients[id].Name
delete(w.Clients, id)
for _, wc := range w.Clients {
wc.Out <- &Message{T: MessageDisconnect, M: []byte(name)}
}
}
w.ClientsLock.Unlock()
@ -99,7 +111,9 @@ func (w *WebInterface) handleRead(c *Client) {
return
}
log.Printf("%d -> %d %d", msg.S, msg.T, len(msg.M))
if msg.T != MessagePing {
log.Printf("%d -> %s %d", msg.S, msg.T, len(msg.M))
}
switch msg.T {
case MessageBinary:
@ -110,10 +124,48 @@ func (w *WebInterface) handleRead(c *Client) {
case MessageCall:
answer, err := w.answerRTC(c, msg.M)
if err != nil {
log.Printf("failed to answer call: %s", err)
continue
}
c.Out <- &Message{T: MessageAnswer, M: answer}
case MessageChat:
w.ClientsLock.Lock()
for _, wc := range w.Clients {
wc.Out <- &Message{S: c.ID, T: MessageChat, M: []byte(msg.M)}
}
w.ClientsLock.Unlock()
log.Printf("<%s> %s", c.Name, msg.M)
case MessageConnect, MessageJoin, MessageNick, MessageQuit, MessageDisconnect:
w.ClientsLock.Lock()
if msg.T == MessageQuit || msg.T == MessageDisconnect {
c.AudioTrack = nil
if c.PeerConn != nil {
c.PeerConn.Close()
c.PeerConn = nil
}
if msg.T == MessageDisconnect {
c.Close()
}
}
msg.M = []byte(c.Name)
for _, wc := range w.Clients {
if (msg.T == MessageJoin || msg.T == MessageQuit) && wc.AudioTrack == nil && wc.ID != c.ID {
continue
}
wc.Out <- msg
}
w.ClientsLock.Unlock()
default:
log.Printf("Unhandled message %d %s", msg.T, msg.M)
}
@ -209,7 +261,8 @@ func (w *WebInterface) answerRTC(c *Client, sdp []byte) ([]byte, error) {
for {
p, err = remoteTrack.ReadRTP()
if err != nil {
c.Close()
c.AudioTrack = nil
c.PeerConn.Close()
return
}
@ -233,7 +286,30 @@ func (w *WebInterface) answerRTC(c *Client, sdp []byte) ([]byte, error) {
pc.OnConnectionStateChange(func(connectionState webrtc.PeerConnectionState) {
log.Printf("%d conn state -> %s\n", c.ID, connectionState)
// TODO User has quit voice chat
if connectionState == webrtc.PeerConnectionStateConnected || connectionState == webrtc.PeerConnectionStateDisconnected {
w.ClientsLock.Lock()
if connectionState == webrtc.PeerConnectionStateDisconnected {
c.AudioTrack = nil
c.PeerConn.Close()
c.PeerConn = nil
}
for _, wc := range w.Clients {
if wc.AudioTrack == nil && wc.ID != c.ID {
continue
}
if connectionState == webrtc.PeerConnectionStateConnected {
wc.Out <- &Message{T: MessageJoin, M: []byte(c.Name)}
} else {
wc.Out <- &Message{T: MessageQuit, M: []byte(c.Name)}
}
}
w.ClientsLock.Unlock()
}
})
pc.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {

Loading…
Cancel
Save