Browse Source

Notify user when browser does not support WebRTC

wip
Trevor Slocum 3 years ago
parent
commit
b55ec21a71
  1. 4
      CHANGELOG
  2. 29
      pkg/web/channel.go
  3. 5
      pkg/web/client.go
  4. 13
      pkg/web/message.go
  5. 206
      pkg/web/public/assets/js/harmony.js
  6. 22
      pkg/web/public/index.html
  7. 159
      pkg/web/web.go

4
CHANGELOG

@ -1,3 +1,7 @@
0.1.2:
- Add channel support
- Notify user when browser does not support WebRTC
0.1.1:
- Fix static asset embedding issue

29
pkg/web/channel.go

@ -1,14 +1,39 @@
package web
import (
"sync"
)
type ChannelType int
const (
ChannelUnknown ChannelType = 0
ChannelAll ChannelType = 1
ChannelText ChannelType = 2
ChannelVoice ChannelType = 3
)
type Channel struct {
ID int
Type ChannelType
Name string
Topic string
Clients map[int]*Client
*sync.Mutex
}
func NewChannel(id int, name string, topic string) *Channel {
c := Channel{ID: id, Name: name, Topic: topic, Clients: make(map[int]*Client)}
func NewChannel(id int, t ChannelType) *Channel {
c := Channel{ID: id, Type: t, Clients: make(map[int]*Client), Mutex: new(sync.Mutex)}
return &c
}
type ChannelListing struct {
ID int
Type ChannelType
Name string
Topic string
}
type ChannelList map[int]*ChannelListing

5
pkg/web/client.go

@ -29,7 +29,8 @@ type Client struct {
AudioTracks map[int]*webrtc.Track
VoiceIn chan []int16
VoiceInLastActive time.Time
VoiceInActive time.Time
VoiceInNotify time.Time
VoiceInTransmitting bool
VoiceInLock *sync.Mutex
@ -38,6 +39,8 @@ type Client struct {
VoiceOutActive []time.Time
VoiceOutLock *sync.Mutex
Channel *Channel
Terminated chan bool
}

13
pkg/web/message.go

@ -22,7 +22,9 @@ const (
MessageTypingStop MessageType = 122
MessageTransmitStart MessageType = 123
MessageTransmitStop MessageType = 124
MessageUsers MessageType = 130
MessageServers MessageType = 130
MessageChannels MessageType = 131
MessageUsers MessageType = 132
)
func (t MessageType) String() string {
@ -57,3 +59,12 @@ func (t MessageType) String() string {
return fmt.Sprintf("%d?", t)
}
}
type Message struct {
S int // Source
N string // Source nickname
PC int // PeerConn
C int // Channel
T MessageType // Type
M []byte // Message
}

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

@ -38,8 +38,13 @@ var muteOnMouseUp = true;
var lastPing = 0;
var userPing = 0;
var allChannels;
var voiceChannel = 0;
var disableVoice = false;
var voiceCompatibilityNotice = "Sorry, your browser does not support WebRTC. This is required join voice channels. Firefox (recommended) and Chrome support WebRTC.";
var channelList = '';
var userList = '';
var userListVoice = '';
var userListStatus = 'Loading...';
var MessageBinary = 2;
@ -59,7 +64,14 @@ var MessageTypingStart = 121;
var MessageTypingStop = 122;
var MessageTransmitStart = 123;
var MessageTransmitStop = 124;
var MessageUsers = 130;
var MessageServers = 130;
var MessageChannels = 131;
var MessageUsers = 132;
var ChannelUnknown = 0;
var ChannelAll = 1;
var ChannelText = 2;
var ChannelVoice = 3;
var tagsToReplace = {
'&': '&',
@ -98,34 +110,13 @@ function HandleInput(e) {
}
$(document).ready(function () {
$("#togglevoice").on("click touchstart", function () {
$("#chatinput").focus();
if (peerConnections.length > 0 || voice) {
// Quit voice chat
voice = false;
w(MessageQuit, "");
peerConnections.forEach(function (pc) {
pc.close();
});
peerConnections = [];
$('#voicepttcontainer').css('display', 'none');
updateStatus();
return;
}
// Join voice chat
// Confirm voice chat support
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia || !window.RTCPeerConnection) {
alert(voiceCompatibilityNotice);
Log(voiceCompatibilityNotice);
var i;
for (i = 0; i < numConnections; i++) {
peerConnections.push(createPeerConnection(i));
}
});
disableVoice = true;
}
$("#voiceptt").on("touchstart", function (e) {
muteOnMouseUp = false;
@ -214,17 +205,16 @@ function createPeerConnection(id) {
el.autoplay = true;
el.srcObject = event.streams[0];
voice = true;
if (id == 0) {
if (!shownPTTHelp) {
shownPTTHelp = true;
if (id == 0 && !shownPTTHelp) {
shownPTTHelp = true;
Log("Note: Push-to-talk is bound to &lt;F8&gt;");
}
Log("Note: Push-to-talk is bound to &lt;F8&gt;");
$('#voicepttcontainer').css('display', 'block');
updateStatus();
}
$('#voicepttcontainer').css('display', 'block');
updateStatus();
};
if (id == 0) {
@ -279,7 +269,6 @@ function onRTCDescription(id) {
function Connect() {
reconnectTimeout = null;
if (webSocketReady() || ReconnectDelay === -1) {
alert('exit');
return;
}
@ -351,17 +340,17 @@ function Connect() {
Log(escapeEntities(p.N) + " connected");
} else if (p.T == MessageJoin) {
if (p.N === undefined) {
if (p.C === undefined || p.N === undefined) {
return;
}
Log(escapeEntities(p.N) + " joined &lobby");
Log(escapeEntities(p.N) + " joined " + allChannels[p.C].Name);
} else if (p.T == MessageQuit) {
if (p.N === undefined) {
if (p.C === undefined || p.N === undefined) {
return;
}
Log(escapeEntities(p.N) + " quit &lobby");
Log(escapeEntities(p.N) + " quit " + allChannels[p.C].Name);
} else if (p.T == MessageDisconnect) {
if (p.N === undefined) {
return;
@ -380,6 +369,10 @@ function Connect() {
}
Log("&lt;" + escapeEntities(p.N) + "&gt; " + p.M);
} else if (p.T == MessageChannels) {
allChannels = JSON.parse(p.M);
updateChannelList();
} else if (p.T == MessageUsers) {
var userListNew = '<ul>';
@ -393,24 +386,36 @@ function Connect() {
userList = userListNew;
var userListVoiceNew = '<ul style="padding-left: 5px;">';
var userListVoiceNew = '';
var u = JSON.parse(p.M);
for (let i in allChannels) {
$("#userscontainer" + i).remove();
}
for (let i = 0; i < u.length; i++) {
if (u[i].C == 0) {
continue;
}
userListVoiceNew += '<li>';
if (voice) {
if ($("#userscontainer" + u[i].C).length == 0) {
$('<li/>')
.attr('id', 'userscontainer' + u[i].C)
.insertAfter($("#channelvoice" + u[i].C));
$('<ul/>')
.attr('id', 'users' + u[i].C)
.appendTo($("#userscontainer" + u[i].C));
}
userListVoiceNew = '<li>';
if (voice && u[i].C == voiceChannel) {
userListVoiceNew += '<span id="voiceindicator' + u[i].ID + '"><div class="voiceinactive">&#128264;</div></span> ';
}
userListVoiceNew += u[i].N + '</li>';
}
userListVoiceNew += '</ul>';
userListVoice = userListVoiceNew;
$("#users" + u[i].C).append(userListVoiceNew);
}
updateUserList();
} else if (p.T == MessageTransmitStart || p.T == MessageTransmitStop) {
@ -511,6 +516,48 @@ function StopPTT() {
updateStatus();
}
function JoinVoice(channelID) {
if (!webSocketReady()) {
return;
}
voiceChannel = parseInt(channelID);
for (let i in allChannels) {
$("#joinvoice" + i).html(i != voiceChannel ? 'Join' : 'Quit');
}
voice = true;
if (peerConnections.length == 0) {
var i;
for (i = 0; i < numConnections; i++) {
peerConnections.push(createPeerConnection(i));
}
}
socket.send(JSON.stringify({T: MessageJoin, C: parseInt(channelID)}));
}
function QuitVoice() {
for (let i in allChannels) {
$("#joinvoice" + i).html('Join');
}
voice = false;
voiceChannel = 0;
w(MessageQuit, "");
peerConnections.forEach(function (pc) {
pc.close();
});
peerConnections = [];
$('#voicepttcontainer').css('display', 'none');
updateStatus();
}
function updateStatus() {
var out = '';
if (userPing > 0) {
@ -530,9 +577,64 @@ function updateStatus() {
$('#togglevoice').html(peerConnections.length > 0 ? 'Quit' : 'Join');
}
function updateUserList() {
$('#userlistvoice1').html(userListVoice);
function updateChannelList() {
var c;
var channelListNew = '<ul>';
channelListNew += '<li style="margin-bottom: 10px;"><div class="headericon">&#128240;</div> Text Channels</li>';
for (let i in allChannels) {
c = allChannels[i];
if (c.Type != ChannelAll && c.Type != ChannelText) {
continue;
}
channelListNew += '<li id="channeltext' + c.ID + '">' + c.Name + '</li>';
}
channelListNew += '<li style="margin-bottom: 10px;">&nbsp;</li><li style="margin-bottom: 10px;"><div class="headericon">&#128266;</div> Voice Channels</li>';
for (let i in allChannels) {
c = allChannels[i];
if (c.Type != ChannelAll && c.Type != ChannelVoice) {
continue;
}
channelListNew += '<li id="channelvoice' + c.ID + '">' + c.Name + ' <a href="#" id="joinvoice' + c.ID + '">' + (parseInt(c.ID) != voiceChannel ? 'Join' : 'Quit') + '</a></li>';
}
channelListNew += '</ul>';
channelList = channelListNew;
$("#sideleft").html(channelList);
for (let i in allChannels) {
c = allChannels[i];
if (c.Type != ChannelAll && c.Type != ChannelVoice) {
continue;
}
$("#joinvoice" + c.ID).click(function (e) {
if (voiceChannel == parseInt($(this).attr('id').substring(9))) {
QuitVoice();
return false;
}
if (disableVoice) {
alert(voiceCompatibilityNotice);
return false;
}
voiceChannel = parseInt($(this).attr('id').substring(9));
JoinVoice(voiceChannel);
return false;
});
}
}
function updateUserList() {
$('#sideright').html(userList);
}
@ -549,7 +651,7 @@ function wpc(pc, t, m) {
return;
}
socket.send(JSON.stringify({PC: pc, T: t, M: btoa(m)}));
socket.send(JSON.stringify({PC: parseInt(pc), T: t, M: btoa(m)}));
}
function escapeEntitiesCallback(tag) {

22
pkg/web/public/index.html

@ -10,32 +10,14 @@
<body>
<div class="wrapper">
<nav class="sideleft" id="sideleft">
<ul>
<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" id="sideright">
</aside>
<div class="header" id="header">
<div style="display: inline-block;float: left;" id="mainheader">#lobby</div>
<div style="display: inline-block;float: right;" id="subheader">harmony demo</div>
<div style="display: inline-block;float: left;" id="mainheader">harmony</div>
<div style="display: inline-block;float: right;" id="subheader">Loading...</div>
</div>
<div class="status">
<div id="userstatus">Loading...</div>

159
pkg/web/web.go

@ -45,14 +45,6 @@ var upgrader = websocket.Upgrader{
EnableCompression: true,
}
type Message struct {
S int // Source
N string // Source nickname
PC int // PeerConn
T MessageType // Type
M []byte // Message
}
type WebInterface struct {
Clients map[int]*Client
ClientsLock *sync.Mutex
@ -98,7 +90,14 @@ func (w *WebInterface) createChannels() {
w.ChannelsLock.Lock()
defer w.ChannelsLock.Unlock()
ch := NewChannel(w.nextChannelID(), "lobby", "harmony demo server")
ch := NewChannel(w.nextChannelID(), ChannelAll)
ch.Name = "lobby"
ch.Topic = "harmony demo server"
w.Channels[ch.ID] = ch
ch = NewChannel(w.nextChannelID(), ChannelAll)
ch.Name = "alt"
ch.Topic = "alt demo channel"
w.Channels[ch.ID] = ch
}
@ -138,6 +137,8 @@ func (w *WebInterface) handleIncomingClients() {
w.ClientsLock.Unlock()
w.sendChannelList(c)
go w.handleRead(c)
}
}
@ -148,7 +149,7 @@ func (w *WebInterface) handleExpireTransmit() {
w.ClientsLock.Lock()
for _, wc := range w.Clients {
wc.VoiceInLock.Lock()
if wc.VoiceInTransmitting && time.Since(wc.VoiceInLastActive) >= 100*time.Millisecond {
if wc.VoiceInTransmitting && time.Since(wc.VoiceInActive) >= 100*time.Millisecond {
wc.VoiceInTransmitting = false
for _, wcc := range w.Clients {
@ -229,24 +230,28 @@ func (w *WebInterface) handleRead(c *Client) {
w.ClientsLock.Unlock()
w.updateUserList()
case MessageConnect, MessageJoin, MessageQuit, MessageDisconnect:
w.ClientsLock.Lock()
case MessageJoin, MessageQuit:
if msg.T == MessageJoin {
w.joinChannel(c, w.Channels[msg.C])
} else { // MessageQuit
w.quitChannel(c)
if msg.T == MessageQuit || msg.T == MessageDisconnect {
c.CloseAudio()
}
if msg.T == MessageDisconnect {
c.Close()
}
w.updateUserList()
case MessageConnect, MessageDisconnect:
w.ClientsLock.Lock()
if msg.T == MessageDisconnect {
w.quitChannel(c)
c.Close()
}
msg.N = c.Name
for _, wc := range w.Clients {
if (msg.T == MessageJoin || msg.T == MessageQuit) && len(wc.AudioTracks) == 0 && wc.ID != c.ID {
continue
}
wc.Out <- msg
}
@ -259,6 +264,60 @@ func (w *WebInterface) handleRead(c *Client) {
}
}
func (w *WebInterface) joinChannel(c *Client, ch *Channel) {
if ch == nil || (c.Channel != nil && c.Channel.ID == ch.ID) {
return
}
w.quitChannel(c)
w.ClientsLock.Lock()
ch.Lock()
ch.Clients[c.ID] = c
c.Channel = ch
for _, wc := range ch.Clients {
if len(wc.AudioTracks) == 0 && wc.ID != c.ID {
continue
}
wc.Out <- &Message{T: MessageJoin, N: c.Name, C: ch.ID}
}
ch.Unlock()
w.ClientsLock.Unlock()
w.updateUserList()
}
func (w *WebInterface) quitChannel(c *Client) {
if c.Channel == nil {
return
}
ch := c.Channel
w.ClientsLock.Lock()
ch.Lock()
for _, wc := range ch.Clients {
if len(wc.AudioTracks) == 0 && wc.ID != c.ID {
continue
}
wc.Out <- &Message{T: MessageQuit, N: c.Name, C: ch.ID}
}
delete(ch.Clients, c.ID)
c.Channel = nil
ch.Unlock()
w.ClientsLock.Unlock()
w.updateUserList()
}
func (w *WebInterface) nextClientID() int {
id := 1
for {
@ -283,6 +342,8 @@ func (w *WebInterface) webSocketHandler(wr http.ResponseWriter, r *http.Request)
<-c.Terminated
w.quitChannel(c)
w.ClientsLock.Lock()
for id := range w.Clients {
if w.Clients[id].Status != -1 {
@ -307,8 +368,8 @@ func (w *WebInterface) updateUserList() {
var userList UserList
for _, wc := range w.Clients {
c := 0
if len(wc.AudioTracks) > 0 {
c = 1
if wc.Channel != nil {
c = wc.Channel.ID
}
userList = append(userList, &User{ID: wc.ID, N: wc.Name, C: c})
@ -329,6 +390,23 @@ func (w *WebInterface) updateUserList() {
w.ClientsLock.Unlock()
}
func (w *WebInterface) sendChannelList(c *Client) {
var channelList = make(ChannelList)
for _, ch := range w.Channels {
channelList[ch.ID] = &ChannelListing{ID: ch.ID, Type: ch.Type, Name: ch.Name, Topic: ch.Topic}
}
msg := Message{T: MessageChannels}
var err error
msg.M, err = json.Marshal(channelList)
if err != nil {
log.Fatal("failed to marshal channel list: ", err)
}
c.Out <- &msg
}
func (w *WebInterface) answerRTC(c *Client, peerConnID int, offerSDP []byte) ([]byte, error) {
c.PeerConnLock.Lock()
defer c.PeerConnLock.Unlock()
@ -427,22 +505,27 @@ func (w *WebInterface) answerRTC(c *Client, peerConnID int, offerSDP []byte) ([]
w.ClientsLock.Lock()
if c.Channel == nil {
continue
}
c.VoiceInLock.Lock()
if !c.VoiceInTransmitting {
if !c.VoiceInTransmitting || time.Since(c.VoiceInNotify) >= 5*time.Second {
c.VoiceInTransmitting = true
c.VoiceInNotify = time.Now()
for _, wc := range w.Clients {
for _, wc := range c.Channel.Clients {
if len(wc.AudioTracks) > 0 {
wc.Out <- &Message{T: MessageTransmitStart, S: c.ID}
}
}
}
c.VoiceInLastActive = time.Now()
c.VoiceInActive = time.Now()
c.VoiceInLock.Unlock()
// TODO trim initial x ms transmitting to remove noise (configurable)
for ci, wc := range w.Clients {
for ci, wc := range c.Channel.Clients {
if ci == c.ID {
continue
}
@ -460,28 +543,10 @@ func (w *WebInterface) answerRTC(c *Client, peerConnID int, offerSDP []byte) ([]
return // Process events from first PeerConn only
}
if connectionState == webrtc.PeerConnectionStateConnected || connectionState == webrtc.PeerConnectionStateDisconnected {
w.ClientsLock.Lock()
if connectionState == webrtc.PeerConnectionStateDisconnected {
c.ClosePeerConn(peerConnID)
log.Printf("closing peerconn, peer disconnected %d", c.ID)
}
for _, wc := range w.Clients {
if len(wc.AudioTracks) == 0 && wc.ID != c.ID {
continue
}
if connectionState == webrtc.PeerConnectionStateDisconnected {
w.quitChannel(c)
if connectionState == webrtc.PeerConnectionStateConnected {
wc.Out <- &Message{T: MessageJoin, N: c.Name, M: []byte(c.Name)}
} else {
wc.Out <- &Message{T: MessageQuit, N: c.Name, M: []byte(c.Name)}
}
}
w.ClientsLock.Unlock()
c.CloseAudio()
w.updateUserList()
}

Loading…
Cancel
Save