// 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 not bold"}) 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() }