├── .gitignore ├── config ├── coturn.service └── engine.example.toml ├── engine ├── boot.go ├── util.go ├── config.go ├── turn.go ├── error.go ├── engine.go ├── peer.go ├── router.go └── rpc.go ├── main.go ├── go.mod ├── README.md ├── go.sum └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | kraken 18 | -------------------------------------------------------------------------------- /config/coturn.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Coturn Daemon 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | ExecStart=/opt/coturn/bin/turnserver -a -f --syslog --no-stun --listening-port 443 --static-auth-secret=812ecb0604d9b90c4aa43a0e3fd1ba85 -r kraken -v 8 | Restart=on-failure 9 | LimitNOFILE=65536 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /engine/boot.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import "github.com/MixinNetwork/mixin/logger" 4 | 5 | func Boot(cp, version string) { 6 | conf, err := Setup(cp) 7 | if err != nil { 8 | panic(err) 9 | } 10 | logger.SetLevel(conf.Engine.LogLevel) 11 | 12 | engine, err := BuildEngine(conf) 13 | if err != nil { 14 | panic(err) 15 | } 16 | 17 | go engine.Loop(version) 18 | err = ServeRPC(engine, conf) 19 | if err != nil { 20 | panic(err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /config/engine.example.toml: -------------------------------------------------------------------------------- 1 | [engine] 2 | # the network interface to bind 3 | interface = "eth0" 4 | # the IP address to bind, empty allows the engine to get it from interface 5 | address = "" 6 | log-level = 10 7 | # the UDP port range, leave them to 0 for default strategy 8 | port-min = 0 9 | port-max = 0 10 | 11 | [turn] 12 | host = "turn:turn.kraken.fm:443" 13 | # must be identical to coturn static auth secret 14 | secret = "812ecb0604d9b90c4aa43a0e3fd1ba85" 15 | 16 | [rpc] 17 | port = 7000 18 | -------------------------------------------------------------------------------- /engine/util.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/pion/webrtc/v4" 8 | ) 9 | 10 | func lockRunWithTimeout(run func() error, duration time.Duration) error { 11 | timer := time.NewTimer(duration) 12 | defer timer.Stop() 13 | 14 | r := make(chan error) 15 | go func() { r <- run() }() 16 | 17 | select { 18 | case err := <-r: 19 | return err 20 | case <-timer.C: 21 | return buildError(ErrorServerTimeout, fmt.Errorf("timeout after %s", duration)) 22 | } 23 | } 24 | 25 | func setLocalDescription(pc *webrtc.PeerConnection, desc webrtc.SessionDescription) error { 26 | gatherComplete := webrtc.GatheringCompletePromise(pc) 27 | err := pc.SetLocalDescription(desc) 28 | if err != nil { 29 | return err 30 | } 31 | <-gatherComplete 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | _ "net/http/pprof" 8 | "os/user" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/MixinNetwork/kraken/engine" 13 | "github.com/MixinNetwork/mixin/logger" 14 | ) 15 | 16 | const Version = "0.3.3" 17 | 18 | func main() { 19 | cp := flag.String("c", "~/.kraken/engine.toml", "configuration file path") 20 | flag.Parse() 21 | 22 | args := flag.Args() 23 | if len(args) > 0 { 24 | fmt.Println(Version) 25 | return 26 | } 27 | 28 | if strings.HasPrefix(*cp, "~/") { 29 | usr, _ := user.Current() 30 | *cp = filepath.Join(usr.HomeDir, (*cp)[2:]) 31 | } 32 | 33 | logger.SetLevel(logger.VERBOSE) 34 | 35 | go func() { 36 | err := http.ListenAndServe(":9000", http.DefaultServeMux) 37 | if err != nil { 38 | panic(err) 39 | } 40 | }() 41 | 42 | engine.Boot(*cp, Version) 43 | } 44 | -------------------------------------------------------------------------------- /engine/config.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "github.com/MixinNetwork/mixin/logger" 7 | "github.com/pelletier/go-toml" 8 | ) 9 | 10 | type Configuration struct { 11 | Engine struct { 12 | Interface string `toml:"interface"` 13 | Address string `toml:"address"` 14 | LogLevel int `toml:"log-level"` 15 | PortMin uint16 `toml:"port-min"` 16 | PortMax uint16 `toml:"port-max"` 17 | } `toml:"engine"` 18 | Turn struct { 19 | Host string `toml:"host"` 20 | Secret string `toml:"secret"` 21 | } `toml:"turn"` 22 | RPC struct { 23 | Port int `toml:"port"` 24 | } `toml:"rpc"` 25 | } 26 | 27 | func Setup(path string) (*Configuration, error) { 28 | logger.Printf("Setup(%s)\n", path) 29 | f, err := ioutil.ReadFile(path) 30 | if err != nil { 31 | return nil, err 32 | } 33 | var conf Configuration 34 | err = toml.Unmarshal(f, &conf) 35 | return &conf, err 36 | } 37 | -------------------------------------------------------------------------------- /engine/turn.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "encoding/base64" 7 | "fmt" 8 | "time" 9 | ) 10 | 11 | type NTS struct { 12 | URLs string `json:"urls"` 13 | Credential string `json:"credential"` 14 | Username string `json:"username"` 15 | } 16 | 17 | func turn(conf *Configuration, uid string) ([]*NTS, error) { 18 | timestamp := time.Now().Add(1 * time.Hour).Unix() 19 | username := fmt.Sprintf("%d:%s", timestamp, uid) 20 | mac := hmac.New(sha1.New, []byte(conf.Turn.Secret)) 21 | if _, err := mac.Write([]byte(username)); err != nil { 22 | return nil, err 23 | } 24 | credential := base64.StdEncoding.EncodeToString(mac.Sum(nil)) 25 | url := conf.Turn.Host 26 | ownUDP := &NTS{ 27 | URLs: url + "?transport=udp", 28 | Username: username, 29 | Credential: credential, 30 | } 31 | ownTCP := &NTS{ 32 | URLs: url + "?transport=tcp", 33 | Username: username, 34 | Credential: credential, 35 | } 36 | return []*NTS{ownUDP, ownTCP}, nil 37 | } 38 | -------------------------------------------------------------------------------- /engine/error.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | const ( 9 | ErrorInvalidParams = 5001000 10 | ErrorInvalidSDP = 5001001 11 | ErrorInvalidCandidate = 5001002 12 | ErrorRoomFull = 5002000 13 | ErrorPeerNotFound = 5002001 14 | ErrorPeerClosed = 5002002 15 | ErrorTrackNotFound = 5002003 16 | ErrorServerNewPeerConnection = 5003000 17 | ErrorServerCreateOffer = 5003001 18 | ErrorServerSetLocalOffer = 5003002 19 | ErrorServerNewTrack = 5003003 20 | ErrorServerAddTransceiver = 5003004 21 | ErrorServerSetRemoteOffer = 5003005 22 | ErrorServerCreateAnswer = 5003006 23 | ErrorServerSetLocalAnswer = 5003007 24 | ErrorServerSetRemoteAnswer = 5003008 25 | ErrorServerTimeout = 5003999 26 | ) 27 | 28 | type Error struct { 29 | Status int `json:"status"` 30 | Code int `json:"code"` 31 | Description string `json:"description"` 32 | } 33 | 34 | func (e Error) Error() string { 35 | b, _ := json.Marshal(e) 36 | return string(b) 37 | } 38 | 39 | func buildError(code int, err error) error { 40 | status := http.StatusAccepted 41 | if code >= ErrorServerNewPeerConnection && code <= ErrorServerTimeout { 42 | status = http.StatusInternalServerError 43 | } 44 | return Error{ 45 | Status: status, 46 | Code: code, 47 | Description: err.Error(), 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/MixinNetwork/kraken 2 | 3 | go 1.25.1 4 | 5 | require ( 6 | github.com/MixinNetwork/mixin v0.18.27 7 | github.com/dimfeld/httptreemux/v5 v5.5.0 8 | github.com/gofrs/uuid/v5 v5.3.2 9 | github.com/gorilla/handlers v1.5.2 10 | github.com/pelletier/go-toml v1.9.5 11 | github.com/pion/interceptor v0.1.40 12 | github.com/pion/rtp v1.8.22 13 | github.com/pion/sdp/v2 v2.4.0 14 | github.com/pion/webrtc/v4 v4.1.4 15 | github.com/unrolled/render v1.7.0 16 | ) 17 | 18 | require ( 19 | github.com/felixge/httpsnoop v1.0.4 // indirect 20 | github.com/fsnotify/fsnotify v1.9.0 // indirect 21 | github.com/google/uuid v1.6.0 // indirect 22 | github.com/pion/datachannel v1.5.10 // indirect 23 | github.com/pion/dtls/v3 v3.0.7 // indirect 24 | github.com/pion/ice/v4 v4.0.10 // indirect 25 | github.com/pion/logging v0.2.4 // indirect 26 | github.com/pion/mdns/v2 v2.0.7 // indirect 27 | github.com/pion/randutil v0.1.0 // indirect 28 | github.com/pion/rtcp v1.2.15 // indirect 29 | github.com/pion/sctp v1.8.39 // indirect 30 | github.com/pion/sdp/v3 v3.0.16 // indirect 31 | github.com/pion/srtp/v3 v3.0.7 // indirect 32 | github.com/pion/stun/v3 v3.0.0 // indirect 33 | github.com/pion/transport/v3 v3.0.7 // indirect 34 | github.com/pion/turn/v4 v4.1.1 // indirect 35 | github.com/wlynxg/anet v0.0.5 // indirect 36 | golang.org/x/crypto v0.41.0 // indirect 37 | golang.org/x/net v0.43.0 // indirect 38 | golang.org/x/sys v0.36.0 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kraken 2 | 3 | 🐙 High performance WebRTC audio SFU implemented with pure Go. 4 | 5 | ## Architecture 6 | 7 | Kraken SFU only supports simple group audio conferencing, more features may be added easily. 8 | 9 | Both Unified Plan and RTCP-MUX supported, so that only one UDP port per participant despite the number of participants in a room. 10 | 11 | ### monitor [WIP] 12 | 13 | This is the daemon that load balance all engine instances according to their system load, and it will direct all peers in a room to the same engine instance. 14 | 15 | ### engine 16 | 17 | The engine handles rooms, all peers in a room should connect to the same engine instance. No need to create rooms, a room is just an ID to distribute streams. 18 | 19 | Access the engine with HTTP JSON-RPC, some pseudocode to demonstrate the full procedure. 20 | 21 | ```javascript 22 | var roomId = getUrlQueryParameter('room'); 23 | var userId = uuidv4(); 24 | var trackId; 25 | 26 | var pc = new RTCPeerConnection(configuration); 27 | 28 | // send ICE candidate to engine 29 | pc.onicecandidate = ({candidate}) => { 30 | rpc('trickle', [roomId, userId, trackId, JSON.stringify(candidate)]); 31 | }; 32 | 33 | // play the audio stream when available 34 | pc.ontrack = (event) => { 35 | el = document.createElement(event.track.kind) 36 | el.id = aid; 37 | el.srcObject = stream; 38 | el.autoplay = true; 39 | document.getElementById('peers').appendChild(el) 40 | }; 41 | 42 | // setup local audio stream from microphone 43 | const stream = await navigator.mediaDevices.getUserMedia(constraints); 44 | stream.getTracks().forEach((track) => { 45 | pc.addTrack(track, stream); 46 | }); 47 | await pc.setLocalDescription(await pc.createOffer()); 48 | 49 | // RPC publish to roomId, with SDP offer 50 | var res = await rpc('publish', [roomId, userId, JSON.stringify(pc.localDescription)]); 51 | // publish should respond an SDP answer 52 | var jsep = JSON.parse(res.data.jsep); 53 | if (jsep.type == 'answer') { 54 | await pc.setRemoteDescription(jsep); 55 | trackId = res.data.track; 56 | subscribe(pc); 57 | } 58 | 59 | // RPC subscribe to roomId periodically 60 | async function subscribe(pc) { 61 | var res = await rpc('subscribe', [roomId, userId, trackId]); 62 | var jsep = JSON.parse(res.data.jsep); 63 | if (jsep.type == 'offer') { 64 | await pc.setRemoteDescription(jsep); 65 | var sdp = await pc.createAnswer(); 66 | await pc.setLocalDescription(sdp); 67 | // RPC anwser the subscribe offer 68 | await rpc('answer', [roomId, userId, trackId, JSON.stringify(sdp)]); 69 | } 70 | setTimeout(function () { 71 | subscribe(pc); 72 | }, 3000); 73 | } 74 | 75 | async function rpc(method, params = []) { 76 | const response = await fetch('http://localhost:7000', { 77 | method: 'POST', 78 | mode: 'cors', 79 | headers: { 80 | 'Content-Type': 'application/json' 81 | }, 82 | body: JSON.stringify({id: uuidv4(), method: method, params: params}) 83 | }); 84 | return response.json(); 85 | } 86 | ``` 87 | 88 | ## Quick Start 89 | 90 | Setup Golang development environment at first. 91 | 92 | ``` 93 | git clone https://github.com/MixinNetwork/kraken 94 | cd kraken && go build 95 | 96 | cp config/engine.example.toml config/engine.toml 97 | ip address # get your network interface name, edit config/engine.toml 98 | 99 | ./kraken -c config/engine.toml -s engine 100 | ``` 101 | 102 | Get the source code of either [kraken.fm](https://github.com/MixinNetwork/kraken.fm) or [Mornin](https://github.com/fox-one/mornin.fm), follow their guides to use your local kraken API. 103 | 104 | ## Community 105 | 106 | Kraken is built with [Pion](https://github.com/pion/webrtc), we have discussions over their Slack. 107 | -------------------------------------------------------------------------------- /engine/engine.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "sync" 7 | "time" 8 | 9 | "github.com/MixinNetwork/mixin/logger" 10 | ) 11 | 12 | const ( 13 | engineStateLoopPeriod = 60 * time.Second 14 | ) 15 | 16 | type State struct { 17 | Version string `json:"version"` 18 | BootedAt time.Time `json:"booted_at"` 19 | UpdatedAt time.Time `json:"updated_at"` 20 | ActivePeers int `json:"active_peers"` 21 | ClosedPeers int `json:"closed_peers"` 22 | PeakPeers int `json:"peak_peers"` 23 | ActiveRooms int `json:"active_rooms"` 24 | ClosedRooms int `json:"closed_rooms"` 25 | PeakRooms int `json:"peak_rooms"` 26 | } 27 | 28 | type Engine struct { 29 | IP string 30 | Interface string 31 | PortMin uint16 32 | PortMax uint16 33 | 34 | peakPeers int 35 | peakRooms int 36 | state *State 37 | rooms *rmap 38 | } 39 | 40 | func BuildEngine(conf *Configuration) (*Engine, error) { 41 | ip, err := getIPFromInterface(conf.Engine.Interface, conf.Engine.Address) 42 | if err != nil { 43 | return nil, err 44 | } 45 | engine := &Engine{ 46 | IP: ip, 47 | Interface: conf.Engine.Interface, 48 | PortMin: conf.Engine.PortMin, 49 | PortMax: conf.Engine.PortMax, 50 | rooms: rmapAllocate(), 51 | } 52 | logger.Printf("BuildEngine(IP: %s, Interface: %s, Ports: %d-%d)\n", engine.IP, engine.Interface, engine.PortMin, engine.PortMax) 53 | return engine, nil 54 | } 55 | 56 | func (engine *Engine) Loop(version string) { 57 | bootedAt := time.Now() 58 | 59 | for { 60 | engine.rooms.RLock() 61 | rooms := make(map[string]*pmap, len(engine.rooms.m)) 62 | for k, v := range engine.rooms.m { 63 | rooms[k] = v 64 | } 65 | engine.rooms.RUnlock() 66 | 67 | state := &State{ 68 | Version: version, 69 | BootedAt: bootedAt, 70 | UpdatedAt: time.Now(), 71 | } 72 | for _, pm := range rooms { 73 | peers := pm.PeersCopy() 74 | ap, cp := 0, 0 75 | for _, p := range peers { 76 | if p.cid == peerTrackClosedId { 77 | cp += 1 78 | } else { 79 | ap += 1 80 | } 81 | } 82 | state.ActivePeers += ap 83 | state.ClosedPeers += cp 84 | if ap > 0 { 85 | state.ActiveRooms += 1 86 | logger.Printf("room#%s with %d active and %d closed peers", pm.id, ap, cp) 87 | } else { 88 | state.ClosedRooms += 1 89 | } 90 | } 91 | if state.ActiveRooms > engine.peakRooms { 92 | engine.peakRooms = state.ActiveRooms 93 | } 94 | if state.ActivePeers > engine.peakPeers { 95 | engine.peakPeers = state.ActivePeers 96 | } 97 | state.PeakPeers = engine.peakPeers 98 | state.PeakRooms = engine.peakRooms 99 | engine.state = state 100 | 101 | time.Sleep(engineStateLoopPeriod) 102 | } 103 | } 104 | 105 | func getIPFromInterface(iname string, addr string) (string, error) { 106 | if addr != "" { 107 | return addr, nil 108 | } 109 | 110 | ifaces, err := net.Interfaces() 111 | if err != nil { 112 | return "", err 113 | } 114 | for _, i := range ifaces { 115 | if i.Name != iname { 116 | continue 117 | } 118 | addrs, err := i.Addrs() 119 | if err != nil { 120 | return "", err 121 | } 122 | for _, addr := range addrs { 123 | switch v := addr.(type) { 124 | case *net.IPNet: 125 | return v.IP.String(), nil 126 | case *net.IPAddr: 127 | return v.IP.String(), nil 128 | } 129 | } 130 | } 131 | 132 | return "", fmt.Errorf("no address for interface %s", iname) 133 | } 134 | 135 | type pmap struct { 136 | sync.RWMutex 137 | id string 138 | m map[string]*Peer 139 | } 140 | 141 | func pmapAllocate(id string) *pmap { 142 | pm := new(pmap) 143 | pm.id = id 144 | pm.m = make(map[string]*Peer) 145 | return pm 146 | } 147 | 148 | type rmap struct { 149 | sync.RWMutex 150 | m map[string]*pmap 151 | } 152 | 153 | func rmapAllocate() *rmap { 154 | rm := new(rmap) 155 | rm.m = make(map[string]*pmap) 156 | return rm 157 | } 158 | 159 | func (engine *Engine) getRoom(rid string) *pmap { 160 | rm := engine.rooms 161 | rm.RLock() 162 | defer rm.RUnlock() 163 | 164 | return rm.m[rid] 165 | } 166 | 167 | func (engine *Engine) GetRoom(rid string) *pmap { 168 | pm := engine.getRoom(rid) 169 | if pm != nil { 170 | return pm 171 | } 172 | 173 | rm := engine.rooms 174 | rm.Lock() 175 | defer rm.Unlock() 176 | if rm.m[rid] == nil { 177 | rm.m[rid] = pmapAllocate(rid) 178 | } 179 | return rm.m[rid] 180 | } 181 | 182 | func (room *pmap) PeersCopy() map[string]*Peer { 183 | room.RLock() 184 | defer room.RUnlock() 185 | 186 | peers := make(map[string]*Peer, len(room.m)) 187 | for k, v := range room.m { 188 | peers[k] = v 189 | } 190 | return peers 191 | } 192 | 193 | func (room *pmap) GetPeer(uid, cid string) (*Peer, error) { 194 | room.RLock() 195 | defer room.RUnlock() 196 | 197 | return room.getPeer(uid, cid) 198 | } 199 | 200 | func (room *pmap) getPeer(uid, cid string) (*Peer, error) { 201 | peer := room.m[uid] 202 | if peer == nil { 203 | return nil, buildError(ErrorPeerNotFound, fmt.Errorf("peer %s not found in %s", uid, room.id)) 204 | } 205 | if peer.cid == peerTrackClosedId { 206 | return nil, buildError(ErrorPeerClosed, fmt.Errorf("peer %s closed in %s", uid, room.id)) 207 | } 208 | if peer.cid != cid { 209 | return nil, buildError(ErrorTrackNotFound, fmt.Errorf("peer %s track not match %s %s in %s", uid, cid, peer.cid, room.id)) 210 | } 211 | return peer, nil 212 | } 213 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/MixinNetwork/mixin v0.18.27 h1:dF5WdyenUH0FbuwEO0WkloxTrKJ6Y4h2SNMtTnMsgdw= 2 | github.com/MixinNetwork/mixin v0.18.27/go.mod h1:4SpfCfGb2MICD4suiSHPJ2E0lAoqwY2PFuBC6OZQLwE= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/dimfeld/httptreemux/v5 v5.5.0 h1:p8jkiMrCuZ0CmhwYLcbNbl7DDo21fozhKHQ2PccwOFQ= 5 | github.com/dimfeld/httptreemux/v5 v5.5.0/go.mod h1:QeEylH57C0v3VO0tkKraVz9oD3Uu93CKPnTLbsidvSw= 6 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 7 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 8 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 9 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 10 | github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0= 11 | github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= 12 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 13 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 14 | github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= 15 | github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= 16 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 17 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 18 | github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= 19 | github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= 20 | github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q= 21 | github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8= 22 | github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= 23 | github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= 24 | github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= 25 | github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= 26 | github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= 27 | github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= 28 | github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= 29 | github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= 30 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 31 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 32 | github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= 33 | github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= 34 | github.com/pion/rtp v1.8.22 h1:8NCVDDF+uSJmMUkjLJVnIr/HX7gPesyMV1xFt5xozXc= 35 | github.com/pion/rtp v1.8.22/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= 36 | github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= 37 | github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= 38 | github.com/pion/sdp/v2 v2.4.0 h1:luUtaETR5x2KNNpvEMv/r4Y+/kzImzbz4Lm1z8eQNQI= 39 | github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E= 40 | github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo= 41 | github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo= 42 | github.com/pion/srtp/v3 v3.0.7 h1:QUElw0A/FUg3MP8/KNMZB3i0m8F9XeMnTum86F7S4bs= 43 | github.com/pion/srtp/v3 v3.0.7/go.mod h1:qvnHeqbhT7kDdB+OGB05KA/P067G3mm7XBfLaLiaNF0= 44 | github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= 45 | github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= 46 | github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= 47 | github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= 48 | github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc= 49 | github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8= 50 | github.com/pion/webrtc/v4 v4.1.4 h1:/gK1ACGHXQmtyVVbJFQDxNoODg4eSRiFLB7t9r9pg8M= 51 | github.com/pion/webrtc/v4 v4.1.4/go.mod h1:Oab9npu1iZtQRMic3K3toYq5zFPvToe/QBw7dMI2ok4= 52 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 53 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 54 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 55 | github.com/unrolled/render v1.7.0 h1:1yke01/tZiZpiXfUG+zqB+6fq3G4I+KDmnh0EhPq7So= 56 | github.com/unrolled/render v1.7.0/go.mod h1:LwQSeDhjml8NLjIO9GJO1/1qpFJxtfVIpzxXKjfVkoI= 57 | github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= 58 | github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= 59 | golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= 60 | golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= 61 | golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= 62 | golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 63 | golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 64 | golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 65 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 66 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 67 | -------------------------------------------------------------------------------- /engine/peer.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "sync" 10 | "time" 11 | 12 | "github.com/MixinNetwork/mixin/logger" 13 | "github.com/gofrs/uuid/v5" 14 | "github.com/pion/rtp" 15 | "github.com/pion/webrtc/v4" 16 | ) 17 | 18 | const ( 19 | peerTrackClosedId = "CLOSED" 20 | peerTrackConnectionTimeout = 20 * time.Second 21 | peerTrackReadTimeout = 5 * time.Second 22 | ) 23 | 24 | var clbkClient *http.Client 25 | 26 | func init() { 27 | clbkClient = &http.Client{ 28 | Timeout: 5 * time.Second, 29 | } 30 | } 31 | 32 | type Sender struct { 33 | id string 34 | rtp *webrtc.RTPSender 35 | } 36 | 37 | type Peer struct { 38 | sync.RWMutex 39 | rid string 40 | uid string 41 | cid string 42 | callback string 43 | listenOnly bool 44 | pc *webrtc.PeerConnection 45 | track *webrtc.TrackLocalStaticRTP 46 | publishers map[string]*Sender 47 | queue chan *rtp.Packet 48 | connected chan bool 49 | } 50 | 51 | func BuildPeer(rid, uid string, pc *webrtc.PeerConnection, callback string, listenOnly bool) *Peer { 52 | cid, err := uuid.NewV4() 53 | if err != nil { 54 | panic(err) 55 | } 56 | peer := new(Peer) 57 | peer.rid = rid 58 | peer.uid = uid 59 | peer.cid = cid.String() 60 | peer.pc = pc 61 | peer.callback = callback 62 | peer.listenOnly = listenOnly 63 | peer.connected = make(chan bool, 1) 64 | peer.queue = make(chan *rtp.Packet, 8) 65 | peer.publishers = make(map[string]*Sender) 66 | peer.handle() 67 | return peer 68 | } 69 | 70 | func (p *Peer) id() string { 71 | return fmt.Sprintf("%s:%s:%s", p.rid, p.uid, p.cid) 72 | } 73 | 74 | func (p *Peer) CloseWithTimeout() error { 75 | logger.Printf("PeerClose(%s) now\n", p.id()) 76 | p.Lock() 77 | defer p.Unlock() 78 | 79 | err := lockRunWithTimeout(func() error { 80 | return p.close() 81 | }, peerTrackReadTimeout) 82 | logger.Printf("PeerClose(%s) with %v\n", p.id(), err) 83 | return err 84 | } 85 | 86 | func (p *Peer) close() error { 87 | if p.cid == peerTrackClosedId { 88 | return nil 89 | } 90 | 91 | p.track = nil 92 | p.cid = peerTrackClosedId 93 | return p.pc.Close() 94 | } 95 | 96 | func (peer *Peer) handle() { 97 | go func() { 98 | timer := time.NewTimer(peerTrackConnectionTimeout) 99 | defer timer.Stop() 100 | 101 | select { 102 | case <-peer.connected: 103 | case <-timer.C: 104 | logger.Printf("HandlePeer(%s) OnTrackTimeout()\n", peer.id()) 105 | _ = peer.CloseWithTimeout() 106 | } 107 | }() 108 | 109 | peer.pc.OnSignalingStateChange(func(state webrtc.SignalingState) { 110 | logger.Printf("HandlePeer(%s) OnSignalingStateChange(%s)\n", peer.id(), state) 111 | }) 112 | peer.pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { 113 | logger.Printf("HandlePeer(%s) OnConnectionStateChange(%s)\n", peer.id(), state) 114 | }) 115 | peer.pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) { 116 | logger.Printf("HandlePeer(%s) OnICEConnectionStateChange(%s)\n", peer.id(), state) 117 | }) 118 | peer.pc.OnTrack(func(rt *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { 119 | logger.Printf("HandlePeer(%s) OnTrack(%s, %d, %d)\n", peer.id(), rt.ID(), rt.PayloadType(), rt.SSRC()) 120 | added, err := peer.addTrackFromRemote(rt) 121 | if err != nil { 122 | panic(err) 123 | } 124 | if !added { 125 | return 126 | } 127 | peer.connected <- true 128 | 129 | err = peer.callbackOnTrack() 130 | if err != nil { 131 | logger.Printf("HandlePeer(%s) OnTrack(%d, %d) callback error %v\n", peer.id(), rt.PayloadType(), rt.SSRC(), err) 132 | } else { 133 | err = peer.copyTrack(rt) 134 | logger.Printf("HandlePeer(%s) OnTrack(%d, %d) end with %v\n", peer.id(), rt.PayloadType(), rt.SSRC(), err) 135 | } 136 | err = peer.CloseWithTimeout() 137 | logger.Printf("HandlePeer(%s) OnTrack(%d, %d) DONE %v\n", peer.id(), rt.PayloadType(), rt.SSRC(), err) 138 | }) 139 | } 140 | 141 | func (peer *Peer) addTrackFromRemote(rt *webrtc.TrackRemote) (bool, error) { 142 | peer.Lock() 143 | defer peer.Unlock() 144 | 145 | if peer.cid == peerTrackClosedId { 146 | return false, nil 147 | } 148 | 149 | rpt := rt.PayloadType() 150 | if peer.track != nil || (rpt != 111 && rpt != 109) { 151 | return false, nil 152 | } 153 | lt, err := webrtc.NewTrackLocalStaticRTP(rt.Codec().RTPCodecCapability, peer.cid, peer.uid) 154 | if err != nil { 155 | return false, err 156 | } 157 | peer.track = lt 158 | return true, nil 159 | } 160 | 161 | func (peer *Peer) callbackOnTrack() error { 162 | if peer.callback == "" { 163 | return nil 164 | } 165 | 166 | body, _ := json.Marshal(map[string]string{ 167 | "rid": peer.rid, 168 | "uid": peer.uid, 169 | "cid": peer.cid, 170 | "action": "ontrack", 171 | }) 172 | req, err := http.NewRequest("POST", peer.callback, bytes.NewReader(body)) 173 | if err != nil { 174 | return err 175 | } 176 | req.Header.Set("Content-Type", "application/json") 177 | resp, err := clbkClient.Do(req) 178 | if err != nil { 179 | return err 180 | } 181 | defer resp.Body.Close() 182 | 183 | if resp.StatusCode != 200 { 184 | return fmt.Errorf("status: %d", resp.StatusCode) 185 | } 186 | return nil 187 | } 188 | 189 | func (peer *Peer) copyTrack(src *webrtc.TrackRemote) error { 190 | go func() { 191 | defer close(peer.queue) 192 | 193 | for { 194 | pkt, _, err := src.ReadRTP() 195 | if err == io.EOF { 196 | logger.Verbosef("copyTrack(%s) EOF\n", peer.id()) 197 | return 198 | } 199 | if err != nil { 200 | logger.Verbosef("copyTrack(%s) error %s\n", peer.id(), err.Error()) 201 | return 202 | } 203 | peer.queue <- pkt 204 | } 205 | }() 206 | 207 | for { 208 | err := peer.consumeQueue() 209 | if err != nil { 210 | return err 211 | } 212 | } 213 | } 214 | 215 | func (peer *Peer) consumeQueue() error { 216 | timer := time.NewTimer(peerTrackReadTimeout) 217 | defer timer.Stop() 218 | 219 | select { 220 | case pkt, ok := <-peer.queue: 221 | if !ok { 222 | return fmt.Errorf("peer %s queue closed", peer.uid) 223 | } 224 | track := peer.track 225 | if track == nil { 226 | return fmt.Errorf("peer %s closed", peer.uid) 227 | } 228 | if peer.listenOnly { 229 | // FIXME make real silent opus packet 230 | pkt.Payload = make([]byte, len(pkt.Payload)) 231 | } 232 | err := track.WriteRTP(pkt) 233 | if err != nil { 234 | return fmt.Errorf("peer %s track write %v", peer.uid, err) 235 | } 236 | case <-timer.C: 237 | return fmt.Errorf("peer %s track read timeout", peer.uid) 238 | } 239 | 240 | return nil 241 | } 242 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /engine/router.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "time" 8 | 9 | "github.com/MixinNetwork/mixin/logger" 10 | "github.com/gofrs/uuid/v5" 11 | "github.com/pion/interceptor" 12 | "github.com/pion/sdp/v2" 13 | "github.com/pion/webrtc/v4" 14 | ) 15 | 16 | type Router struct { 17 | engine *Engine 18 | } 19 | 20 | func NewRouter(engine *Engine) *Router { 21 | return &Router{engine: engine} 22 | } 23 | 24 | func (r *Router) info() any { 25 | r.engine.rooms.RLock() 26 | defer r.engine.rooms.RUnlock() 27 | 28 | return r.engine.state 29 | } 30 | 31 | func (r *Router) list(rid string) ([]map[string]any, error) { 32 | room := r.engine.GetRoom(rid) 33 | peers := room.PeersCopy() 34 | list := make([]map[string]any, 0) 35 | for _, p := range peers { 36 | cid := uuid.FromStringOrNil(p.cid) 37 | if cid.String() == uuid.Nil.String() { 38 | continue 39 | } 40 | list = append(list, map[string]any{ 41 | "id": p.uid, 42 | "track": cid.String(), 43 | "mute": p.listenOnly, 44 | }) 45 | } 46 | return list, nil 47 | } 48 | 49 | func (r *Router) mute(rid, uid string) map[string]any { 50 | room := r.engine.GetRoom(rid) 51 | peers := room.PeersCopy() 52 | for _, p := range peers { 53 | if p.uid != uid { 54 | continue 55 | } 56 | cid := uuid.FromStringOrNil(p.cid) 57 | if cid.String() == uuid.Nil.String() { 58 | continue 59 | } 60 | p.listenOnly = !p.listenOnly 61 | return map[string]any{ 62 | "id": p.uid, 63 | "track": cid.String(), 64 | "mute": p.listenOnly, 65 | } 66 | } 67 | return nil 68 | } 69 | 70 | func (r *Router) create(rid, uid, callback string, listenOnly bool, offer webrtc.SessionDescription) (*Peer, error) { 71 | se := webrtc.SettingEngine{} 72 | se.SetLite(true) 73 | se.EnableSCTPZeroChecksum(true) 74 | se.SetInterfaceFilter(func(in string) bool { return in == r.engine.Interface }) 75 | se.SetNAT1To1IPs([]string{r.engine.IP}, webrtc.ICECandidateTypeHost) 76 | se.SetICETimeouts(10*time.Second, 20*time.Second, 2*time.Second) 77 | se.SetDTLSInsecureSkipHelloVerify(true) 78 | se.SetReceiveMTU(8192) 79 | err := se.SetEphemeralUDPPortRange(r.engine.PortMin, r.engine.PortMax) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | me := &webrtc.MediaEngine{} 85 | opusChrome := webrtc.RTPCodecParameters{ 86 | RTPCodecCapability: webrtc.RTPCodecCapability{ 87 | MimeType: webrtc.MimeTypeOpus, 88 | ClockRate: 48000, 89 | Channels: 2, 90 | SDPFmtpLine: "minptime=10;useinbandfec=1", 91 | RTCPFeedback: nil, 92 | }, 93 | PayloadType: 111, 94 | } 95 | opusFirefox := webrtc.RTPCodecParameters{ 96 | RTPCodecCapability: webrtc.RTPCodecCapability{ 97 | MimeType: webrtc.MimeTypeOpus, 98 | ClockRate: 48000, 99 | Channels: 2, 100 | SDPFmtpLine: "minptime=10;useinbandfec=1", 101 | RTCPFeedback: nil, 102 | }, 103 | PayloadType: 109, 104 | } 105 | err = me.RegisterCodec(opusChrome, webrtc.RTPCodecTypeAudio) 106 | if err != nil { 107 | return nil, err 108 | } 109 | err = me.RegisterCodec(opusFirefox, webrtc.RTPCodecTypeAudio) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | ir := &interceptor.Registry{} 115 | err = webrtc.RegisterDefaultInterceptors(me, ir) 116 | if err != nil { 117 | panic(err) 118 | } 119 | 120 | api := webrtc.NewAPI(webrtc.WithMediaEngine(me), webrtc.WithSettingEngine(se), webrtc.WithInterceptorRegistry(ir)) 121 | 122 | pcConfig := webrtc.Configuration{ 123 | BundlePolicy: webrtc.BundlePolicyMaxBundle, 124 | RTCPMuxPolicy: webrtc.RTCPMuxPolicyRequire, 125 | } 126 | pc, err := api.NewPeerConnection(pcConfig) 127 | if err != nil { 128 | return nil, buildError(ErrorServerNewPeerConnection, err) 129 | } 130 | 131 | err = pc.SetRemoteDescription(offer) 132 | if err != nil { 133 | pc.Close() 134 | return nil, buildError(ErrorServerSetRemoteOffer, err) 135 | } 136 | answer, err := pc.CreateAnswer(nil) 137 | if err != nil { 138 | pc.Close() 139 | return nil, buildError(ErrorServerCreateAnswer, err) 140 | } 141 | err = setLocalDescription(pc, answer) 142 | if err != nil { 143 | pc.Close() 144 | return nil, buildError(ErrorServerSetLocalAnswer, err) 145 | } 146 | 147 | peer := BuildPeer(rid, uid, pc, callback, listenOnly) 148 | return peer, nil 149 | } 150 | 151 | func (r *Router) publish(rid, uid string, jsep string, limit int, callback string, listenOnly bool) (string, *webrtc.SessionDescription, error) { 152 | if err := validateId(rid); err != nil { 153 | return "", nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid rid format %s %s", rid, err.Error())) 154 | } 155 | if err := validateId(uid); err != nil { 156 | return "", nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid uid format %s %s", uid, err.Error())) 157 | } 158 | var offer webrtc.SessionDescription 159 | err := json.Unmarshal([]byte(jsep), &offer) 160 | if err != nil { 161 | return "", nil, buildError(ErrorInvalidSDP, err) 162 | } 163 | if offer.Type != webrtc.SDPTypeOffer { 164 | return "", nil, buildError(ErrorInvalidSDP, fmt.Errorf("invalid sdp type %s", offer.Type)) 165 | } 166 | 167 | parser := sdp.SessionDescription{} 168 | err = parser.Unmarshal([]byte(offer.SDP)) 169 | if err != nil { 170 | return "", nil, buildError(ErrorInvalidSDP, err) 171 | } 172 | 173 | room := r.engine.GetRoom(rid) 174 | if limit > 0 { 175 | peers := room.PeersCopy() 176 | for i, p := range peers { 177 | cid := uuid.FromStringOrNil(p.cid) 178 | if cid.String() == uuid.Nil.String() || uid == i { 179 | continue 180 | } 181 | limit-- 182 | } 183 | if limit <= 0 { 184 | return "", nil, buildError(ErrorRoomFull, fmt.Errorf("room full %d", limit)) 185 | } 186 | } 187 | 188 | room.Lock() 189 | defer room.Unlock() 190 | 191 | var peer *Peer 192 | err = lockRunWithTimeout(func() error { 193 | pub, err := r.create(rid, uid, callback, listenOnly, offer) 194 | peer = pub 195 | return err 196 | }, peerTrackConnectionTimeout) 197 | if err != nil { 198 | return "", nil, err 199 | } 200 | 201 | old := room.m[peer.uid] 202 | if old != nil { 203 | _ = old.CloseWithTimeout() 204 | } 205 | room.m[peer.uid] = peer 206 | return peer.cid, peer.pc.LocalDescription(), nil 207 | } 208 | 209 | func (r *Router) restart(rid, uid, cid string, jsep string) (*webrtc.SessionDescription, error) { 210 | room := r.engine.GetRoom(rid) 211 | peer, err := room.GetPeer(uid, cid) 212 | if err != nil { 213 | return nil, err 214 | } 215 | 216 | var offer webrtc.SessionDescription 217 | err = json.Unmarshal([]byte(jsep), &offer) 218 | if err != nil { 219 | return nil, buildError(ErrorInvalidSDP, err) 220 | } 221 | if offer.Type != webrtc.SDPTypeOffer { 222 | return nil, buildError(ErrorInvalidSDP, fmt.Errorf("invalid sdp type %s", offer.Type)) 223 | } 224 | parser := sdp.SessionDescription{} 225 | err = parser.Unmarshal([]byte(offer.SDP)) 226 | if err != nil { 227 | return nil, buildError(ErrorInvalidSDP, err) 228 | } 229 | 230 | peer.Lock() 231 | defer peer.Unlock() 232 | 233 | err = lockRunWithTimeout(func() error { 234 | err := peer.pc.SetRemoteDescription(offer) 235 | if err != nil { 236 | return buildError(ErrorServerSetRemoteOffer, err) 237 | } 238 | answer, err := peer.pc.CreateAnswer(nil) 239 | if err != nil { 240 | return buildError(ErrorServerCreateAnswer, err) 241 | } 242 | err = setLocalDescription(peer.pc, answer) 243 | if err != nil { 244 | return buildError(ErrorServerSetLocalAnswer, err) 245 | } 246 | return nil 247 | }, peerTrackConnectionTimeout) 248 | 249 | if err != nil { 250 | _ = lockRunWithTimeout(func() error { 251 | return peer.close() 252 | }, peerTrackReadTimeout) 253 | return nil, err 254 | } 255 | return peer.pc.LocalDescription(), nil 256 | } 257 | 258 | func (r *Router) end(rid, uid, cid string) error { 259 | room := r.engine.GetRoom(rid) 260 | peer, err := room.GetPeer(uid, cid) 261 | if err != nil { 262 | return err 263 | } 264 | 265 | return peer.CloseWithTimeout() 266 | } 267 | 268 | func (r *Router) trickle(rid, uid, cid string, candi string) error { 269 | var ici webrtc.ICECandidateInit 270 | err := json.Unmarshal([]byte(candi), &ici) 271 | if err != nil { 272 | return buildError(ErrorInvalidCandidate, err) 273 | } 274 | if ici.Candidate == "" { 275 | return nil 276 | } 277 | 278 | room := r.engine.GetRoom(rid) 279 | peer, err := room.GetPeer(uid, cid) 280 | if err != nil { 281 | return err 282 | } 283 | peer.Lock() 284 | defer peer.Unlock() 285 | 286 | return lockRunWithTimeout(func() error { 287 | return peer.pc.AddICECandidate(ici) 288 | }, peerTrackReadTimeout) 289 | } 290 | 291 | func (r *Router) subscribe(rid, uid, cid string) (*webrtc.SessionDescription, error) { 292 | room := r.engine.GetRoom(rid) 293 | room.Lock() 294 | defer room.Unlock() 295 | 296 | peer, err := room.getPeer(uid, cid) 297 | if err != nil { 298 | return nil, err 299 | } 300 | 301 | err = lockRunWithTimeout(func() error { 302 | err := peer.doSubscribe(room.m) 303 | logger.Printf("peer.doSubscribe(%s, %s, %s) => %v", rid, uid, cid, err) 304 | if err != nil { 305 | _ = peer.close() 306 | return err 307 | } 308 | return nil 309 | }, peerTrackConnectionTimeout) 310 | 311 | if err != nil { 312 | return nil, err 313 | } 314 | return peer.pc.LocalDescription(), nil 315 | } 316 | 317 | func (peer *Peer) doSubscribe(peers map[string]*Peer) error { 318 | peer.Lock() 319 | defer peer.Unlock() 320 | 321 | return lockRunWithTimeout(func() error { 322 | var renegotiate bool 323 | for _, pub := range peers { 324 | if pub.uid == peer.uid { 325 | continue 326 | } 327 | 328 | res, err := peer.connectPublisher(pub) 329 | if err != nil { 330 | return err 331 | } 332 | renegotiate = renegotiate || res 333 | } 334 | if renegotiate { 335 | offer, err := peer.pc.CreateOffer(nil) 336 | if err != nil { 337 | return buildError(ErrorServerCreateOffer, err) 338 | } 339 | err = setLocalDescription(peer.pc, offer) 340 | if err != nil { 341 | return buildError(ErrorServerSetLocalOffer, err) 342 | } 343 | } 344 | return nil 345 | }, peerTrackReadTimeout) 346 | } 347 | 348 | func (sub *Peer) connectPublisher(pub *Peer) (bool, error) { 349 | pub.RLock() 350 | defer pub.RUnlock() 351 | 352 | var renegotiate bool 353 | err := lockRunWithTimeout(func() error { 354 | old := sub.publishers[pub.uid] 355 | if old != nil && (pub.track == nil || old.id != pub.cid) { 356 | err := sub.pc.RemoveTrack(old.rtp) 357 | if err != nil { 358 | return fmt.Errorf("pc.RemoveTrack(%s, %s) => %v", pub.id(), sub.id(), err) 359 | } 360 | delete(sub.publishers, pub.uid) 361 | renegotiate = true 362 | } 363 | if pub.track != nil && (old == nil || old.id != pub.cid) { 364 | sender, err := sub.pc.AddTrack(pub.track) 365 | logger.Printf("pc.AddTrack(%s, %s) => %v %v", sub.id(), pub.id(), sender, err) 366 | if err != nil { 367 | return fmt.Errorf("pc.AddTrack(%s, %s) => %v", sub.id(), pub.id(), err) 368 | } 369 | if id := sender.Track().ID(); id != pub.cid { 370 | return fmt.Errorf("malformed peer and track id %s %s", pub.cid, id) 371 | } 372 | sub.publishers[pub.uid] = &Sender{id: pub.cid, rtp: sender} 373 | renegotiate = true 374 | } 375 | return nil 376 | }, peerTrackReadTimeout) 377 | return renegotiate, err 378 | } 379 | 380 | func (r *Router) answer(rid, uid, cid string, jsep string) error { 381 | var answer webrtc.SessionDescription 382 | err := json.Unmarshal([]byte(jsep), &answer) 383 | if err != nil { 384 | return buildError(ErrorInvalidSDP, err) 385 | } 386 | if answer.Type != webrtc.SDPTypeAnswer { 387 | return buildError(ErrorInvalidSDP, fmt.Errorf("invalid sdp type %s", answer.Type)) 388 | } 389 | 390 | parser := sdp.SessionDescription{} 391 | err = parser.Unmarshal([]byte(answer.SDP)) 392 | if err != nil { 393 | return buildError(ErrorInvalidSDP, err) 394 | } 395 | 396 | room := r.engine.GetRoom(rid) 397 | peer, err := room.GetPeer(uid, cid) 398 | if err != nil { 399 | return err 400 | } 401 | 402 | peer.Lock() 403 | defer peer.Unlock() 404 | 405 | return lockRunWithTimeout(func() error { 406 | if peer.pc.SignalingState() == webrtc.SignalingStateStable { 407 | return nil 408 | } 409 | err := peer.pc.SetRemoteDescription(answer) 410 | logger.Printf("pc.SetRemoteDescription(%s, %s, %s) => %v", rid, uid, cid, err) 411 | if err != nil { 412 | return buildError(ErrorServerSetRemoteAnswer, err) 413 | } 414 | return nil 415 | }, peerTrackReadTimeout) 416 | } 417 | 418 | func validateId(id string) error { 419 | if len(id) > 256 { 420 | return fmt.Errorf("id %s too long, the maximum is %d", id, 256) 421 | } 422 | uid, err := url.QueryUnescape(id) 423 | if err != nil { 424 | return err 425 | } 426 | if eid := url.QueryEscape(uid); eid != id { 427 | return fmt.Errorf("unmatch %s %s", id, eid) 428 | } 429 | return nil 430 | } 431 | -------------------------------------------------------------------------------- /engine/rpc.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/MixinNetwork/mixin/logger" 12 | "github.com/dimfeld/httptreemux/v5" 13 | "github.com/gofrs/uuid/v5" 14 | "github.com/gorilla/handlers" 15 | "github.com/pion/webrtc/v4" 16 | "github.com/unrolled/render" 17 | ) 18 | 19 | type R struct { 20 | router *Router 21 | conf *Configuration 22 | } 23 | 24 | type Call struct { 25 | Id string `json:"id"` 26 | Method string `json:"method"` 27 | Params []any `json:"params"` 28 | } 29 | 30 | type Render struct { 31 | w http.ResponseWriter 32 | impl *render.Render 33 | call *Call 34 | startAt time.Time 35 | } 36 | 37 | func NewRender(w http.ResponseWriter, c *Call) *Render { 38 | r := &Render{ 39 | w: w, 40 | impl: render.New(), 41 | call: c, 42 | startAt: time.Now(), 43 | } 44 | return r 45 | } 46 | 47 | func (r *Render) RenderData(data any) { 48 | body := map[string]any{"data": data} 49 | if r.call != nil { 50 | body["id"] = r.call.Id 51 | } 52 | rerr := r.impl.JSON(r.w, http.StatusOK, body) 53 | if rerr != nil { 54 | panic(rerr) 55 | } 56 | logger.Printf("RPC.handle(id: %s, method: %s, time: %f) OK\n", 57 | r.call.Id, r.call.Method, time.Since(r.startAt).Seconds()) 58 | } 59 | 60 | func (r *Render) RenderError(err error) { 61 | body := map[string]any{"error": err} 62 | if r.call != nil { 63 | body["id"] = r.call.Id 64 | } 65 | rerr := r.impl.JSON(r.w, http.StatusOK, body) 66 | if rerr != nil { 67 | panic(err) 68 | } 69 | logger.Printf("RPC.handle(id: %s, method: %s, time: %f) ERROR %s\n", 70 | r.call.Id, r.call.Method, time.Since(r.startAt).Seconds(), err.Error()) 71 | } 72 | 73 | func (impl *R) root(w http.ResponseWriter, r *http.Request, _ map[string]string) { 74 | info := impl.info() 75 | renderer := NewRender(w, &Call{Id: uuid.Must(uuid.NewV4()).String(), Method: "root"}) 76 | renderer.RenderData(info) 77 | } 78 | 79 | func (impl *R) handle(w http.ResponseWriter, r *http.Request, _ map[string]string) { 80 | var call Call 81 | d := json.NewDecoder(r.Body) 82 | d.UseNumber() 83 | if err := d.Decode(&call); err != nil { 84 | renderJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) 85 | return 86 | } 87 | renderer := NewRender(w, &call) 88 | logger.Printf("RPC.handle(id: %s, method: %s, params: %v)\n", call.Id, call.Method, call.Params) 89 | switch call.Method { 90 | case "turn": 91 | servers, err := impl.turn(call.Params) 92 | if err != nil { 93 | renderer.RenderError(err) 94 | } else { 95 | renderer.RenderData(servers) 96 | } 97 | case "info": 98 | info := impl.info() 99 | renderer.RenderData(info) 100 | case "list": 101 | peers, err := impl.list(call.Params) 102 | if err != nil { 103 | renderer.RenderError(err) 104 | } else { 105 | renderer.RenderData(map[string]any{"peers": peers}) 106 | } 107 | case "mute": 108 | peer, err := impl.mute(call.Params) 109 | if err != nil { 110 | renderer.RenderError(err) 111 | } else { 112 | renderer.RenderData(map[string]any{"peer": peer}) 113 | } 114 | case "publish": 115 | cid, answer, err := impl.publish(call.Params) 116 | if err != nil { 117 | renderer.RenderError(err) 118 | } else { 119 | jsep, _ := json.Marshal(answer) 120 | renderer.RenderData(map[string]any{"track": cid, "sdp": answer, "jsep": string(jsep)}) 121 | } 122 | case "restart": 123 | answer, err := impl.restart(call.Params) 124 | if err != nil { 125 | renderer.RenderError(err) 126 | } else { 127 | jsep, _ := json.Marshal(answer) 128 | renderer.RenderData(map[string]any{"jsep": string(jsep)}) 129 | } 130 | case "end": 131 | err := impl.end(call.Params) 132 | if err != nil { 133 | renderer.RenderError(err) 134 | } else { 135 | renderer.RenderData(map[string]string{}) 136 | } 137 | case "trickle": 138 | err := impl.trickle(call.Params) 139 | if err != nil { 140 | renderer.RenderError(err) 141 | } else { 142 | renderer.RenderData(map[string]string{}) 143 | } 144 | case "subscribe": 145 | offer, err := impl.subscribe(call.Params) 146 | if err != nil { 147 | renderer.RenderError(err) 148 | } else { 149 | jsep, _ := json.Marshal(offer) 150 | renderer.RenderData(map[string]any{"type": offer.Type, "sdp": offer.SDP, "jsep": string(jsep)}) 151 | } 152 | case "answer": 153 | err := impl.answer(call.Params) 154 | if err != nil { 155 | renderer.RenderError(err) 156 | } else { 157 | renderer.RenderData(map[string]string{}) 158 | } 159 | default: 160 | renderer.RenderError(fmt.Errorf("invalid method %s", call.Method)) 161 | } 162 | } 163 | 164 | func (r *R) turn(params []any) (any, error) { 165 | if len(params) != 1 { 166 | return nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid params count %d", len(params))) 167 | } 168 | uid, ok := params[0].(string) 169 | if !ok { 170 | return nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid uid type %s", params[0])) 171 | } 172 | return turn(r.conf, uid) 173 | } 174 | 175 | func (r *R) info() any { 176 | return r.router.info() 177 | } 178 | 179 | func (r *R) list(params []any) ([]map[string]any, error) { 180 | if len(params) != 1 { 181 | return nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid params count %d", len(params))) 182 | } 183 | rid, ok := params[0].(string) 184 | if !ok { 185 | return nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid rid type %s", params[0])) 186 | } 187 | return r.router.list(rid) 188 | } 189 | 190 | func (r *R) mute(params []any) (map[string]any, error) { 191 | if len(params) != 2 { 192 | return nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid params count %d", len(params))) 193 | } 194 | rid, ok := params[0].(string) 195 | if !ok { 196 | return nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid rid type %s", params[0])) 197 | } 198 | uid, ok := params[1].(string) 199 | if !ok { 200 | return nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid uid type %s", params[1])) 201 | } 202 | peer := r.router.mute(rid, uid) 203 | if peer == nil { 204 | return nil, buildError(http.StatusNotFound, fmt.Errorf("peer not found %s", params[1])) 205 | } 206 | return peer, nil 207 | } 208 | 209 | func (r *R) publish(params []any) (string, *webrtc.SessionDescription, error) { 210 | if len(params) < 3 { 211 | return "", nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid params count %d", len(params))) 212 | } 213 | rid, ok := params[0].(string) 214 | if !ok { 215 | return "", nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid rid type %v", params[0])) 216 | } 217 | uid, ok := params[1].(string) 218 | if !ok { 219 | return "", nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid uid type %v", params[1])) 220 | } 221 | sdp, ok := params[2].(string) 222 | if !ok { 223 | return "", nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid sdp type %v", params[2])) 224 | } 225 | var limit int 226 | var callback string 227 | if len(params) == 5 { 228 | i, err := strconv.ParseInt(fmt.Sprint(params[3]), 10, 32) 229 | if err != nil { 230 | return "", nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid limit type %v %v", params[3], err)) 231 | } 232 | limit = int(i) 233 | cbk, ok := params[4].(string) 234 | if !ok { 235 | return "", nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid callback type %v", params[4])) 236 | } 237 | if !strings.HasPrefix(cbk, "https://") { 238 | return "", nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid callback value %s", cbk)) 239 | } 240 | callback = cbk 241 | } 242 | var listenOnly bool 243 | if len(params) == 6 { 244 | listenOnly, _ = strconv.ParseBool(fmt.Sprint(params[5])) 245 | } 246 | return r.router.publish(rid, uid, sdp, limit, callback, listenOnly) 247 | } 248 | 249 | func (r *R) restart(params []any) (*webrtc.SessionDescription, error) { 250 | if len(params) != 4 { 251 | return nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid params count %d", len(params))) 252 | } 253 | ids, err := r.parseId(params) 254 | if err != nil { 255 | return nil, buildError(ErrorInvalidParams, err) 256 | } 257 | jsep, ok := params[3].(string) 258 | if !ok { 259 | return nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid jsep type %s", params[3])) 260 | } 261 | return r.router.restart(ids[0], ids[1], ids[2], jsep) 262 | } 263 | 264 | func (r *R) end(params []any) error { 265 | if len(params) != 3 { 266 | return buildError(ErrorInvalidParams, fmt.Errorf("invalid params count %d", len(params))) 267 | } 268 | ids, err := r.parseId(params) 269 | if err != nil { 270 | return buildError(ErrorInvalidParams, err) 271 | } 272 | return r.router.end(ids[0], ids[1], ids[2]) 273 | } 274 | 275 | func (r *R) trickle(params []any) error { 276 | if len(params) != 4 { 277 | return buildError(ErrorInvalidParams, fmt.Errorf("invalid params count %d", len(params))) 278 | } 279 | ids, err := r.parseId(params) 280 | if err != nil { 281 | return buildError(ErrorInvalidParams, err) 282 | } 283 | candi, ok := params[3].(string) 284 | if !ok { 285 | return buildError(ErrorInvalidParams, fmt.Errorf("invalid candi type %s", params[3])) 286 | } 287 | return r.router.trickle(ids[0], ids[1], ids[2], candi) 288 | } 289 | 290 | func (r *R) subscribe(params []any) (*webrtc.SessionDescription, error) { 291 | if len(params) != 3 { 292 | return nil, buildError(ErrorInvalidParams, fmt.Errorf("invalid params count %d", len(params))) 293 | } 294 | ids, err := r.parseId(params) 295 | if err != nil { 296 | return nil, buildError(ErrorInvalidParams, err) 297 | } 298 | return r.router.subscribe(ids[0], ids[1], ids[2]) 299 | } 300 | 301 | func (r *R) answer(params []any) error { 302 | if len(params) != 4 { 303 | return buildError(ErrorInvalidParams, fmt.Errorf("invalid params count %d", len(params))) 304 | } 305 | ids, err := r.parseId(params) 306 | if err != nil { 307 | return buildError(ErrorInvalidParams, err) 308 | } 309 | sdp, ok := params[3].(string) 310 | if !ok { 311 | return buildError(ErrorInvalidParams, fmt.Errorf("invalid sdp type %s", params[3])) 312 | } 313 | return r.router.answer(ids[0], ids[1], ids[2], sdp) 314 | } 315 | 316 | func (r *R) parseId(params []any) ([]string, error) { 317 | rid, ok := params[0].(string) 318 | if !ok { 319 | return nil, fmt.Errorf("invalid rid type %s", params[0]) 320 | } 321 | uid, ok := params[1].(string) 322 | if !ok { 323 | return nil, fmt.Errorf("invalid uid type %s", params[1]) 324 | } 325 | cid, ok := params[2].(string) 326 | if !ok { 327 | return nil, fmt.Errorf("invalid cid type %s", params[2]) 328 | } 329 | return []string{rid, uid, cid}, nil 330 | } 331 | 332 | func registerHandlers(router *httptreemux.TreeMux) { 333 | router.MethodNotAllowedHandler = func(w http.ResponseWriter, r *http.Request, _ map[string]httptreemux.HandlerFunc) { 334 | renderJSON(w, http.StatusNotFound, map[string]any{"error": "not found"}) 335 | } 336 | router.NotFoundHandler = func(w http.ResponseWriter, r *http.Request) { 337 | renderJSON(w, http.StatusNotFound, map[string]any{"error": "not found"}) 338 | } 339 | router.PanicHandler = func(w http.ResponseWriter, r *http.Request, rcv any) { 340 | logger.Println(rcv) 341 | renderJSON(w, http.StatusInternalServerError, map[string]any{"error": "server error"}) 342 | } 343 | } 344 | 345 | func handleCORS(handler http.Handler) http.Handler { 346 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 347 | origin := r.Header.Get("Origin") 348 | if origin == "" { 349 | handler.ServeHTTP(w, r) 350 | return 351 | } 352 | w.Header().Set("Access-Control-Allow-Origin", origin) 353 | w.Header().Add("Access-Control-Allow-Headers", "Content-Type,Authorization,Mixin-Conversation-ID") 354 | w.Header().Set("Access-Control-Allow-Methods", "OPTIONS,GET,POST,DELETE") 355 | w.Header().Set("Access-Control-Max-Age", "600") 356 | if r.Method == "OPTIONS" { 357 | renderJSON(w, http.StatusOK, map[string]any{}) 358 | } else { 359 | handler.ServeHTTP(w, r) 360 | } 361 | }) 362 | } 363 | 364 | func renderJSON(w http.ResponseWriter, status int, data any) { 365 | err := render.New().JSON(w, status, data) 366 | if err != nil { 367 | panic(err) 368 | } 369 | } 370 | 371 | func ServeRPC(engine *Engine, conf *Configuration) error { 372 | logger.Printf("ServeRPC(:%d)\n", conf.RPC.Port) 373 | impl := &R{router: NewRouter(engine), conf: conf} 374 | router := httptreemux.New() 375 | router.GET("/", impl.root) 376 | router.POST("/", impl.handle) 377 | registerHandlers(router) 378 | handler := handleCORS(router) 379 | handler = handlers.ProxyHeaders(handler) 380 | 381 | server := &http.Server{ 382 | Addr: fmt.Sprintf(":%d", conf.RPC.Port), 383 | Handler: handler, 384 | ReadTimeout: 10 * time.Second, 385 | WriteTimeout: 10 * time.Second, 386 | IdleTimeout: 120 * time.Second, 387 | } 388 | return server.ListenAndServe() 389 | } 390 | --------------------------------------------------------------------------------