guestlink v1: self-hosted guest line (Cleanfeed replacement)
Go + Pion + libopus. One guest, live audio both ways, anonymous chat. README for this version was not preserved; see the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
b0e3b805b1
13 files changed
+1839
No files matched your search
@@ -0,0 +1,173 @@
|
||||
// glprobe: headless test guest for guestlink. Creates (or uses) an invite via the host
|
||||
// console, joins as a guest, sends a quiet tone, chats, and reports what it receives.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/pion/webrtc/v4/pkg/media"
|
||||
"gopkg.in/hraban/opus.v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
hostWS := flag.String("hostws", "ws://127.0.0.1:8791/ws", "host console websocket (to create a link)")
|
||||
guestBase := flag.String("guest", "ws://127.0.0.1:8790", "guest base (ws:// or wss://)")
|
||||
token := flag.String("token", "", "use this token instead of creating a new link")
|
||||
secs := flag.Int("secs", 8, "seconds to stay connected")
|
||||
toneDB := flag.Float64("tone", -40, "test tone level dBFS")
|
||||
flag.Parse()
|
||||
|
||||
if *token == "" {
|
||||
h, _, err := websocket.DefaultDialer.Dial(*hostWS, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("host ws: %v", err)
|
||||
}
|
||||
h.WriteJSON(map[string]string{"t": "newlink"})
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
var m map[string]any
|
||||
h.SetReadDeadline(deadline)
|
||||
if err := h.ReadJSON(&m); err != nil {
|
||||
break
|
||||
}
|
||||
if m["t"] == "state" {
|
||||
if l, _ := m["link"].(string); l != "" {
|
||||
*token = l[strings.LastIndex(l, "/")+1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
h.Close()
|
||||
if *token == "" {
|
||||
log.Fatal("no link created")
|
||||
}
|
||||
fmt.Println("token created")
|
||||
}
|
||||
|
||||
hdr := http.Header{}
|
||||
u, _ := url.Parse(*guestBase)
|
||||
scheme := "http"
|
||||
if u.Scheme == "wss" {
|
||||
scheme = "https"
|
||||
}
|
||||
hdr.Set("Origin", scheme+"://"+u.Host)
|
||||
ws, resp, err := websocket.DefaultDialer.Dial(*guestBase+"/ws?t="+url.QueryEscape(*token), hdr)
|
||||
if err != nil {
|
||||
code := 0
|
||||
if resp != nil {
|
||||
code = resp.StatusCode
|
||||
}
|
||||
log.Fatalf("guest ws: %v (HTTP %d)", err, code)
|
||||
}
|
||||
ws.WriteJSON(map[string]string{"t": "name", "name": "glprobe"})
|
||||
ws.WriteJSON(map[string]string{"t": "chat", "text": "hello from glprobe <b>not bold</b>"})
|
||||
|
||||
pc, _ := webrtc.NewPeerConnection(webrtc.Configuration{})
|
||||
track, _ := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 2}, "a", "probe")
|
||||
pc.AddTransceiverFromTrack(track, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv})
|
||||
var rxPeak atomic.Uint64
|
||||
var rxPkts atomic.Int64
|
||||
pc.OnTrack(func(t *webrtc.TrackRemote, _ *webrtc.RTPReceiver) {
|
||||
dec, _ := opus.NewDecoder(48000, 2)
|
||||
pcm := make([]int16, 5760*2)
|
||||
for {
|
||||
p, _, err := t.ReadRTP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rxPkts.Add(1)
|
||||
n, err := dec.Decode(p.Payload, pcm)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, s := range pcm[:n*2] {
|
||||
v := uint64(math.Abs(float64(s)))
|
||||
if v > rxPeak.Load() {
|
||||
rxPeak.Store(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
connected := make(chan struct{}, 1)
|
||||
pc.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
|
||||
fmt.Println("pc:", s)
|
||||
if s == webrtc.PeerConnectionStateConnected {
|
||||
connected <- struct{}{}
|
||||
}
|
||||
})
|
||||
pc.OnICECandidate(func(c *webrtc.ICECandidate) {
|
||||
if c != nil {
|
||||
ci := c.ToJSON()
|
||||
ws.WriteJSON(map[string]any{"t": "candidate", "candidate": ci})
|
||||
}
|
||||
})
|
||||
offer, _ := pc.CreateOffer(nil)
|
||||
pc.SetLocalDescription(offer)
|
||||
ws.WriteJSON(map[string]string{"t": "offer", "sdp": offer.SDP})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
var m map[string]any
|
||||
if err := ws.ReadJSON(&m); err != nil {
|
||||
return
|
||||
}
|
||||
switch m["t"] {
|
||||
case "answer":
|
||||
sdp := m["sdp"].(string)
|
||||
for _, l := range strings.Split(sdp, "\n") {
|
||||
if strings.HasPrefix(l, "a=candidate") || strings.HasPrefix(l, "a=fmtp") {
|
||||
fmt.Println(" answer:", strings.TrimSpace(l))
|
||||
}
|
||||
}
|
||||
pc.SetRemoteDescription(webrtc.SessionDescription{Type: webrtc.SDPTypeAnswer, SDP: sdp})
|
||||
case "chat":
|
||||
b, _ := json.Marshal(m["msg"])
|
||||
fmt.Println("chat:", string(b))
|
||||
default:
|
||||
b, _ := json.Marshal(m)
|
||||
if len(b) > 200 {
|
||||
b = b[:200]
|
||||
}
|
||||
fmt.Println("ws:", string(b))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-connected:
|
||||
case <-time.After(10 * time.Second):
|
||||
log.Fatal("did not connect")
|
||||
}
|
||||
enc, _ := opus.NewEncoder(48000, 2, opus.AppAudio)
|
||||
amp := math.Pow(10, *toneDB/20) * 32767
|
||||
pcm := make([]int16, 960*2)
|
||||
pkt := make([]byte, 4000)
|
||||
ph := 0.0
|
||||
tick := time.NewTicker(20 * time.Millisecond)
|
||||
end := time.Now().Add(time.Duration(*secs) * time.Second)
|
||||
for time.Now().Before(end) {
|
||||
<-tick.C
|
||||
for i := 0; i < 960; i++ {
|
||||
v := int16(amp * math.Sin(ph))
|
||||
ph += 2 * math.Pi * 440 / 48000
|
||||
pcm[2*i], pcm[2*i+1] = v, v
|
||||
}
|
||||
n, _ := enc.Encode(pcm, pkt)
|
||||
track.WriteSample(media.Sample{Data: append([]byte(nil), pkt[:n]...), Duration: 20 * time.Millisecond})
|
||||
}
|
||||
pk := float64(rxPeak.Load()) / 32768
|
||||
fmt.Printf("received %d packets from server, peak %.1f dBFS\n", rxPkts.Load(), 20*math.Log10(pk+1e-9))
|
||||
ws.WriteJSON(map[string]string{"t": "hangup"})
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
pc.Close()
|
||||
}
|
||||
Reference in new issue
Block a user