Commit f5a518a4 authored by Juliusz Chroboczek's avatar Juliusz Chroboczek

Initial commit.

parents
This diff is collapsed.
module sfu
go 1.13
require (
github.com/gorilla/websocket v1.4.2
github.com/pion/rtcp v1.2.1
github.com/pion/sdp v1.3.0
github.com/pion/webrtc/v2 v2.2.5
)
This diff is collapsed.
// 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.
package main
import (
"log"
"sync"
"github.com/pion/webrtc/v2"
)
type trackPair struct {
remote, local *webrtc.Track
}
type upConnection struct {
id string
label string
pc *webrtc.PeerConnection
maxAudioBitrate uint32
maxVideoBitrate uint32
streamCount int
pairs []trackPair
}
type downConnection struct {
id string
pc *webrtc.PeerConnection
remote *upConnection
maxBitrate uint32
}
type client struct {
group *group
id string
username string
done chan struct{}
writeCh chan interface{}
writerDone chan struct{}
actionCh chan interface{}
down map[string]*downConnection
up map[string]*upConnection
}
type group struct {
name string
public bool
mu sync.Mutex
clients []*client
}
type delPCAction struct {
id string
}
type addTrackAction struct {
id string
track *webrtc.Track
remote *upConnection
done bool
}
type addLabelAction struct {
id string
label string
}
type getUpAction struct {
ch chan<- string
}
type pushTracksAction struct {
c *client
}
var groups struct {
mu sync.Mutex
groups map[string]*group
api *webrtc.API
}
func addGroup(name string) (*group, error) {
groups.mu.Lock()
defer groups.mu.Unlock()
if groups.groups == nil {
groups.groups = make(map[string]*group)
m := webrtc.MediaEngine{}
m.RegisterCodec(webrtc.NewRTPVP8Codec(
webrtc.DefaultPayloadTypeVP8, 90000))
m.RegisterCodec(webrtc.NewRTPOpusCodec(
webrtc.DefaultPayloadTypeOpus, 48000))
groups.api = webrtc.NewAPI(
webrtc.WithMediaEngine(m),
)
}
g := groups.groups[name]
if g == nil {
g = &group{
name: name,
}
groups.groups[name] = g
}
return g, nil
}
func delGroup(name string) bool {
groups.mu.Lock()
defer groups.mu.Unlock()
g := groups.groups[name]
if g == nil {
return true
}
if len(g.clients) != 0 {
return false
}
delete(groups.groups, name)
return true
}
type userid struct {
id string
username string
}
func addClient(name string, client *client) (*group, []userid, error) {
g, err := addGroup(name)
if err != nil {
return nil, nil, err
}
var users []userid
g.mu.Lock()
defer g.mu.Unlock()
for _, c := range g.clients {
users = append(users, userid{c.id, c.username})
}
g.clients = append(g.clients, client)
return g, users, nil
}
func delClient(c *client) {
c.group.mu.Lock()
defer c.group.mu.Unlock()
g := c.group
for i, cc := range g.clients {
if cc == c {
g.clients =
append(g.clients[:i], g.clients[i+1:]...)
c.group = nil
return
}
}
log.Printf("Deleting unknown client")
c.group = nil
}
func (g *group) getClients(except *client) []*client {
g.mu.Lock()
defer g.mu.Unlock()
clients := make([]*client, 0, len(g.clients))
for _, c := range g.clients {
if c != except {
clients = append(clients, c)
}
}
return clients
}
func (g *group) Range(f func(c *client) bool) {
g.mu.Lock()
defer g.mu.Unlock()
for _, c := range g.clients {
ok := f(c)
if(!ok){
break;
}
}
}
type writerDeadError int
func (err writerDeadError) Error() string {
return "client writer died"
}
func (c *client) write(m clientMessage) error {
select {
case c.writeCh <- m:
return nil
case <-c.writerDone:
return writerDeadError(0)
}
}
type clientDeadError int
func (err clientDeadError) Error() string {
return "client dead"
}
func (c *client) action(m interface{}) error {
select {
case c.actionCh <- m:
return nil
case <-c.done:
return clientDeadError(0)
}
}
type publicGroup struct {
Name string `json:"name"`
ClientCount int `json:"clientCount"`
}
func getPublicGroups() []publicGroup {
gs := make([]publicGroup, 0)
groups.mu.Lock()
defer groups.mu.Unlock()
for _, g := range groups.groups {
if g.public {
gs = append(gs, publicGroup{
Name: g.name,
ClientCount: len(g.clients),
})
}
}
return gs
}
// 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.
package main
import (
"encoding/json"
"flag"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"github.com/gorilla/websocket"
)
var httpAddr string
var staticRoot string
var dataDir string
var iceFilename string
func main() {
flag.StringVar(&httpAddr, "http", ":8443", "web server `address`")
flag.StringVar(&staticRoot, "static", "./static/",
"web server root `directory`")
flag.StringVar(&dataDir, "data", "./data/",
"data `directory`")
flag.Parse()
iceFilename = filepath.Join(staticRoot, "ice-servers.json")
http.Handle("/", mungeHandler{http.FileServer(http.Dir(staticRoot))})
http.HandleFunc("/group/",
func(w http.ResponseWriter, r *http.Request) {
mungeHeader(w)
http.ServeFile(w, r, staticRoot+"/sfu.html")
})
http.HandleFunc("/ws", wsHandler)
http.HandleFunc("/public-groups.json", publicHandler)
go func() {
server := &http.Server{
Addr: httpAddr,
ReadTimeout: 60 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
var err error
log.Printf("Listening on %v", httpAddr)
err = server.ListenAndServeTLS(
filepath.Join(dataDir, "cert.pem"),
filepath.Join(dataDir, "key.pem"),
)
log.Fatalf("ListenAndServeTLS: %v", err)
}()
terminate := make(chan os.Signal, 1)
signal.Notify(terminate, syscall.SIGINT)
<-terminate
}
func mungeHeader(w http.ResponseWriter) {
w.Header().Add("Content-Security-Policy",
"connect-src ws: wss: 'self'; img-src data: 'self'; default-src 'self'")
}
type mungeHandler struct {
h http.Handler
}
func (h mungeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
mungeHeader(w)
h.h.ServeHTTP(w, r)
}
func publicHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
w.Header().Set("cache-control", "no-cache")
if r.Method == "HEAD" {
return
}
g := getPublicGroups()
e := json.NewEncoder(w)
e.Encode(g)
return
}
var upgrader websocket.Upgrader
func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Websocket upgrade: %v", err)
return
}
go func() {
err := startClient(conn)
if err != nil {
log.Printf("client: %v", err)
}
}()
}
body {
font: 14px "Lato", Arial, sans-serif;
}
h1 {
font-size: 160%;
}
.signature {
border-top: solid;
margin-top: 2em;
padding-top: 0em;
border-width: thin;
clear: both;
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>SFU</title>
<link rel="stylesheet" href="/common.css">
<link rel="stylesheet" href="/mainpage.css">
<link rel="author" href="https://www.irif.fr/~jch/"/>
</head>
<body>
<h1>SFU</h1>
<form id="groupform">
<label for="group">Group:</label>
<input id="group" type="text" name="group"/>
<input type="submit" value="Join"/><br/>
</form>
<div id="public-groups" class="groups">
<h2>Public groups</h2>
<table id="public-groups-table"></table>
</div>
<footer class="signature"><p><a href="https://www.irif.fr/~jch/software/sfu/">Unnamed SFU</a> by <a href="https://www.irif.fr/~jch/" rel="author">Juliusz Chroboczek</a></footer>
<script src="/mainpage.js" defer></script>
</body>
</html>
.groups {
}
.nogroups {
display: none;
}
// Copyright (c) 2019-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';
document.getElementById('groupform').onsubmit = function(e) {
e.preventDefault();
let group = document.getElementById('group').value.trim();
location.href = '/group/' + group;
}
async function listPublicGroups() {
let div = document.getElementById('public-groups');
let table = document.getElementById('public-groups-table');
let l;
try {
l = await (await fetch('/public-groups.json')).json();
} catch(e) {
console.error(e);
l = [];
}
if (l.length === 0) {
table.textContent = '(No groups found.)';
div.classList.remove('groups');
div.classList.add('nogroups');
return;
}
div.classList.remove('nogroups');
div.classList.add('groups');
for(let i = 0; i < l.length; i++) {
let group = l[i];
let tr = document.createElement('tr');
let td = document.createElement('td');
let a = document.createElement('a');
a.textContent = group.name;
a.href = '/group/' + encodeURIComponent(group.name);
td.appendChild(a);
tr.appendChild(td);
let td2 = document.createElement('td');
td2.textContent = `(${group.clientCount} clients)`;
tr.appendChild(td2);
table.appendChild(tr);
}
}
listPublicGroups();
#title {
text-align: center;
}
h1 {
white-space: nowrap;
}
#header {
margin-left: 2%;
}
#statdiv {
white-space: nowrap;
margin-bottom: 2pt;
}
#errspan {
margin-left: 1em;
}
.connected {
color: green;
}
.disconnected {
background-color: red;
font-weight: bold;
}
.userform {
display: inline
}
.userform-invisible {
display: none;
}
.disconnect-invisible {
display: none;
}
.error {
color: red;
font-weight: bold;
}
.noerror {
display: none;
}
#main {
display: flex;
}
#users {
width: 5%;
margin-left: 2%;
border: 1px solid;
}
#anonymous-users {
white-space: nowrap;
}
#chatbox {
width: 100%;
}
#chat {
display: flex;
width: 20%;
margin-left: 0.3em;
border: 1px solid;
height: 85vh;
}
#inputform {
display: flex;
}
#box {
height: 95%;
overflow: auto;
}
.message, message-me {
margin: 0 0.5em 0 0.5em;
}
.message-user {
font-weight: bold;
}
.message-content {
line-height: 1.5;
margin-left: 1em;
}
.message-me-asterisk {
margin-right: 0.5em;
}
.message-me-user {
font-weight: bold;
margin-right: 0.5em;
}
.message-me-content {
}
#input {
width: 100%;
border: none;
resize: none;
overflow-y: hidden;
}
#input:focus {
outline: none;
}
#inputbutton {
background-color: white;
border: none;
margin-right: 0.2em;
font-size: 1.5em;
}
#inputbutton:focus {
outline: none;
}
#resizer {
width: 8px;
margin-left: -8px;
z-index: 1000;
}
#resizer:hover {
cursor: ew-resize;
}
#peers {
margin-left: 1%;
margin-right: 1%;
white-space: nowrap;
display: flex;
flex-wrap: wrap;
margin-bottom: 4px;
}
.peer {
display: flex;
flex-direction: column;
margin-right: 5px;
margin-left: 5px;
margin-bottom: 10px;
max-height: 50%;
}
.media {
height: 400px;
margin: auto;
min-width: 4em;
}
.label {
text-align: center;
height: 2em;
margin-top: 5px;
}
#inputform {
width: 100%;
}
#input {
width: 85%;
border: 1px solid;
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>SFU</title>
<link rel="stylesheet" type="text/css" href="/common.css"/>
<link rel="stylesheet" type="text/css" href="/sfu.css"/>
<link rel="author" href="https://www.irif.fr/~jch/"/>
</head>
<body>
<h1 id="title">SFU</h1>
<div id="header">
<div id="statdiv">
<span id="statspan"></span>
<form id="userform" class="userform">
<label for="username">Username:</label>
<input id="username" type="text" name="username"
autocomplete="username"/>
<label for="password">Password:</label>
<input id="password" type="password" name="password"
autocomplete="current-password"/>
<input id="connectbutton" type="submit" value="Connect" disabled/>
</form>
<input id="disconnectbutton" class="disconnect-invisible"
type="submit" value="Disconnect"/>
<span id="errspan"></span>
</div>
<div id="optionsdiv">
<label for="presenterbox">Present:</label>
<input id="presenterbox" type="checkbox"/>
<label for="sharebox">Share screen:</label>
<input id="sharebox" type="checkbox"/>
</div>
</div>
<div id="main">
<div id="users"></div>
<div id="chat">
<div id="chatbox">
<div id="box"></div>
<form id="inputform">
<textarea id="input"></textarea>
<input id="inputbutton" type="submit" value="&#10148;"/>
</form>
</div>
<div id="resizer">
</div>
</div>
<div id="peers"></div>
</div>
<script src="/sfu.js" defer></script>
</body>
</html>
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment