v0.3: WebRTC P2P via Pion (Go) + RTCPeerConnection (browser)
Server: unchanged shape, just added a "signal" message type to the existing /api/send + /api/stream bus. Now carries both "clipboard" (payload) and "signal" (offer/answer/ICE) over the same envelope. Client: -rtc flag turns the Go listener into a Pion peer. Posts an SDP offer at startup, accepts the browser's answer through the signaling bus, exchanges ICE, then receives clipboard text over a DataChannel named "tether". On message: writes to OS clipboard same as SSE path. Web UI: acts as the answerer. Listens for "signal" SSE events, replies to offers, exchanges ICE. When DataChannel opens, the send button uses RTCDataChannel.send() instead of POST /api/send — data no longer traverses the server after pairing. Pill in the header flips sse → negotiating → rtc to make this visible. Toolchain: bumped go.mod to go 1.26, switched to pion/webrtc v4 and prometheus/client_golang v1.23.x. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
199
client/main.go
199
client/main.go
@@ -1,10 +1,17 @@
|
||||
// tether-client: connects to a tether-server, writes incoming messages
|
||||
// to the OS clipboard and prints them to stdout.
|
||||
// tether-client v0.3: SSE listener with optional WebRTC peer.
|
||||
//
|
||||
// Default flow (SSE only): subscribe to /api/stream, write incoming
|
||||
// clipboard messages to the OS clipboard. Works on Win/Linux/macOS.
|
||||
//
|
||||
// With -rtc: also act as a WebRTC peer (Pion). Sends an SDP offer via
|
||||
// the signaling bus, accepts the browser's answer, then receives
|
||||
// clipboard payloads over a DataChannel — true P2P after ICE.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -17,36 +24,51 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// defaultLabel returns "{platform}-{transport}-{role}" so the server can
|
||||
// tell at a glance who's connecting. Transport is "sse" today; "rtc" once
|
||||
// WebRTC lands (v0.3 roadmap).
|
||||
const peerID = "tether-client"
|
||||
|
||||
func defaultLabel(role string) string {
|
||||
return fmt.Sprintf("%s-sse-%s", runtime.GOOS, role)
|
||||
return fmt.Sprintf("%s-%s-%s", runtime.GOOS, "sse", role)
|
||||
}
|
||||
|
||||
// Message envelope (must match server).
|
||||
type Message struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Signal json.RawMessage `json:"signal,omitempty"`
|
||||
From string `json:"from,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
TS int64 `json:"ts"`
|
||||
}
|
||||
|
||||
// Signal payload carried in Message.Signal.
|
||||
type SignalPayload struct {
|
||||
Kind string `json:"kind"` // "offer" | "answer" | "ice"
|
||||
SDP *webrtc.SessionDescription `json:"sdp,omitempty"`
|
||||
Candidate *webrtc.ICECandidateInit `json:"candidate,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
noClipboard bool
|
||||
myLabel string
|
||||
useRTC bool
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Text string `json:"text"`
|
||||
Source string `json:"source,omitempty"`
|
||||
TS int64 `json:"ts"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
server := flag.String("server", "https://tether.pecord.io", "tether-server base URL")
|
||||
label := flag.String("label", "", "X-Tether-Client label (default: <os>-sse-<role>)")
|
||||
sendText := flag.String("send", "", "send this text and exit (otherwise listen)")
|
||||
flag.BoolVar(&noClipboard, "no-clipboard", false, "don't write incoming messages to the OS clipboard")
|
||||
flag.BoolVar(&useRTC, "rtc", false, "enable WebRTC peer (uses signaling bus to negotiate)")
|
||||
flag.Parse()
|
||||
|
||||
if *label == "" {
|
||||
if *sendText != "" {
|
||||
*label = defaultLabel("sender")
|
||||
} else if useRTC {
|
||||
*label = fmt.Sprintf("%s-rtc-listener", runtime.GOOS)
|
||||
} else {
|
||||
*label = defaultLabel("listener")
|
||||
}
|
||||
@@ -54,7 +76,7 @@ func main() {
|
||||
myLabel = *label
|
||||
|
||||
if *sendText != "" {
|
||||
send(*server, *label, *sendText)
|
||||
send(*server, *label, *sendText, "clipboard", nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -63,6 +85,10 @@ func main() {
|
||||
noClipboard = true
|
||||
}
|
||||
|
||||
if useRTC {
|
||||
go runRTCPeer(*server)
|
||||
}
|
||||
|
||||
for {
|
||||
if err := listen(*server, *label); err != nil {
|
||||
log.Printf("stream error: %v — reconnecting in 3s", err)
|
||||
@@ -71,22 +97,29 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func send(server, label, text string) {
|
||||
body, _ := json.Marshal(Message{Text: text, Source: label})
|
||||
func send(server, label, text, msgType string, signal json.RawMessage) {
|
||||
m := Message{Type: msgType, Text: text, Source: label, From: peerID, Signal: signal}
|
||||
body, _ := json.Marshal(m)
|
||||
req, _ := http.NewRequest("POST", server+"/api/send", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Tether-Source", label)
|
||||
r, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Fatalf("send: %v", err)
|
||||
log.Printf("send: %v", err)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
if r.StatusCode >= 300 {
|
||||
log.Fatalf("send: HTTP %d", r.StatusCode)
|
||||
log.Printf("send: HTTP %d", r.StatusCode)
|
||||
return
|
||||
}
|
||||
if msgType != "signal" {
|
||||
fmt.Println("sent.")
|
||||
}
|
||||
fmt.Println("sent.")
|
||||
}
|
||||
|
||||
// listen subscribes to the SSE stream. Clipboard messages → OS clipboard.
|
||||
// Signal messages → forwarded to the WebRTC peer (if enabled).
|
||||
func listen(server, label string) error {
|
||||
req, _ := http.NewRequest("GET", server+"/api/stream", nil)
|
||||
req.Header.Set("X-Tether-Client", label)
|
||||
@@ -98,9 +131,9 @@ func listen(server, label string) error {
|
||||
if r.StatusCode != 200 {
|
||||
return fmt.Errorf("HTTP %d", r.StatusCode)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "tether-client: connected to %s as %q\n", server, label)
|
||||
fmt.Fprintf(os.Stderr, "tether-client: connected to %s as %q (rtc=%v)\n", server, label, useRTC)
|
||||
sc := bufio.NewScanner(r.Body)
|
||||
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||
sc.Buffer(make([]byte, 1<<20), 1<<20)
|
||||
var ev, data string
|
||||
for sc.Scan() {
|
||||
line := sc.Text()
|
||||
@@ -110,24 +143,13 @@ func listen(server, label string) error {
|
||||
case strings.HasPrefix(line, "data: "):
|
||||
data = strings.TrimPrefix(line, "data: ")
|
||||
case line == "":
|
||||
if ev == "clipboard" && data != "" {
|
||||
if data != "" {
|
||||
var m Message
|
||||
if err := json.Unmarshal([]byte(data), &m); err == nil {
|
||||
ts := time.UnixMilli(m.TS).Format("15:04:05")
|
||||
fmt.Printf("\n────── %s from %s ──────\n%s\n", ts, m.Source, m.Text)
|
||||
// Don't echo our own message back into our own clipboard
|
||||
if !noClipboard && m.Source != myLabel {
|
||||
if err := clipboard.WriteAll(m.Text); err != nil {
|
||||
fmt.Fprintf(os.Stderr, " ! clipboard write error: %v\n", err)
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, " → clipboard updated")
|
||||
}
|
||||
}
|
||||
handleMessage(ev, m)
|
||||
}
|
||||
}
|
||||
ev, data = "", ""
|
||||
case strings.HasPrefix(line, ": "):
|
||||
// keepalive — ignore
|
||||
}
|
||||
}
|
||||
if err := sc.Err(); err != nil && err != io.EOF {
|
||||
@@ -135,3 +157,114 @@ func listen(server, label string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMessage(ev string, m Message) {
|
||||
switch m.Type {
|
||||
case "", "clipboard":
|
||||
ts := time.UnixMilli(m.TS).Format("15:04:05")
|
||||
fmt.Printf("\n────── %s from %s ──────\n%s\n", ts, m.Source, m.Text)
|
||||
if !noClipboard && m.Source != myLabel {
|
||||
if err := clipboard.WriteAll(m.Text); err != nil {
|
||||
fmt.Fprintf(os.Stderr, " ! clipboard write error: %v\n", err)
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, " → clipboard updated")
|
||||
}
|
||||
}
|
||||
case "signal":
|
||||
if useRTC && m.From != peerID {
|
||||
incomingSignal <- m
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── WebRTC ────────────────────────────────────────────────────────────────
|
||||
|
||||
var incomingSignal = make(chan Message, 16)
|
||||
|
||||
func runRTCPeer(server string) {
|
||||
api := webrtc.NewAPI()
|
||||
config := webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{URLs: []string{"stun:stun.l.google.com:19302"}},
|
||||
},
|
||||
}
|
||||
pc, err := api.NewPeerConnection(config)
|
||||
if err != nil {
|
||||
log.Printf("rtc: new peer connection: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
dc, err := pc.CreateDataChannel("tether", nil)
|
||||
if err != nil {
|
||||
log.Printf("rtc: create datachannel: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
dc.OnOpen(func() {
|
||||
fmt.Fprintln(os.Stderr, "rtc: DataChannel OPEN — P2P live")
|
||||
})
|
||||
dc.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||
text := string(msg.Data)
|
||||
fmt.Printf("\n[rtc] %s\n", text)
|
||||
if !noClipboard {
|
||||
if err := clipboard.WriteAll(text); err == nil {
|
||||
fmt.Fprintln(os.Stderr, " → clipboard updated (via rtc)")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
pc.OnICECandidate(func(c *webrtc.ICECandidate) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
init := c.ToJSON()
|
||||
payload, _ := json.Marshal(SignalPayload{Kind: "ice", Candidate: &init})
|
||||
send(server, myLabel, "", "signal", payload)
|
||||
})
|
||||
pc.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
|
||||
fmt.Fprintf(os.Stderr, "rtc: state=%s\n", s)
|
||||
})
|
||||
|
||||
// Create offer + post via signaling bus
|
||||
offer, err := pc.CreateOffer(nil)
|
||||
if err != nil {
|
||||
log.Printf("rtc: create offer: %v", err)
|
||||
return
|
||||
}
|
||||
if err := pc.SetLocalDescription(offer); err != nil {
|
||||
log.Printf("rtc: set local desc: %v", err)
|
||||
return
|
||||
}
|
||||
payload, _ := json.Marshal(SignalPayload{Kind: "offer", SDP: &offer})
|
||||
send(server, myLabel, "", "signal", payload)
|
||||
fmt.Fprintln(os.Stderr, "rtc: offer posted, waiting for answer...")
|
||||
|
||||
// Process incoming signals
|
||||
for {
|
||||
select {
|
||||
case msg := <-incomingSignal:
|
||||
var sp SignalPayload
|
||||
if err := json.Unmarshal(msg.Signal, &sp); err != nil {
|
||||
continue
|
||||
}
|
||||
switch sp.Kind {
|
||||
case "answer":
|
||||
if sp.SDP != nil {
|
||||
if err := pc.SetRemoteDescription(*sp.SDP); err != nil {
|
||||
log.Printf("rtc: set remote desc: %v", err)
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "rtc: answer applied")
|
||||
}
|
||||
}
|
||||
case "ice":
|
||||
if sp.Candidate != nil {
|
||||
if err := pc.AddICECandidate(*sp.Candidate); err != nil {
|
||||
log.Printf("rtc: add ice: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
case <-context.Background().Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
59
go.mod
59
go.mod
@@ -1,44 +1,41 @@
|
||||
module github.com/pecord/tether
|
||||
|
||||
go 1.22
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/pion/webrtc/v4 v4.2.12
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pion/datachannel v1.5.8 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||
github.com/pion/ice/v2 v2.3.38 // indirect
|
||||
github.com/pion/interceptor v0.1.29 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/mdns v0.0.12 // indirect
|
||||
github.com/pion/datachannel v1.6.0 // indirect
|
||||
github.com/pion/dtls/v3 v3.1.2 // indirect
|
||||
github.com/pion/ice/v4 v4.2.5 // indirect
|
||||
github.com/pion/interceptor v0.1.44 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/mdns/v2 v2.1.0 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.14 // indirect
|
||||
github.com/pion/rtp v1.8.7 // indirect
|
||||
github.com/pion/sctp v1.8.19 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.9 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.20 // indirect
|
||||
github.com/pion/stun v0.6.1 // indirect
|
||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||
github.com/pion/webrtc/v3 v3.3.6 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.3 // indirect
|
||||
golang.org/x/crypto v0.24.0 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
github.com/pion/rtcp v1.2.16 // indirect
|
||||
github.com/pion/rtp v1.10.1 // indirect
|
||||
github.com/pion/sctp v1.9.5 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.18 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.10 // indirect
|
||||
github.com/pion/stun/v3 v3.1.2 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/pion/turn/v5 v5.0.3 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
)
|
||||
|
||||
201
go.sum
201
go.sum
@@ -4,143 +4,90 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pion/datachannel v1.5.8 h1:ph1P1NsGkazkjrvyMfhRBUAWMxugJjq2HfQifaOoSNo=
|
||||
github.com/pion/datachannel v1.5.8/go.mod h1:PgmdpoaNBLX9HNzNClmdki4DYW5JtI7Yibu8QzbL3tI=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
|
||||
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/ice/v2 v2.3.38 h1:DEpt13igPfvkE2+1Q+6e8mP30dtWnQD3CtMIKoRDRmA=
|
||||
github.com/pion/ice/v2 v2.3.38/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ=
|
||||
github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M=
|
||||
github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
||||
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
|
||||
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
|
||||
github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=
|
||||
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
|
||||
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
|
||||
github.com/pion/ice/v4 v4.2.5 h1:5umUQy4hX6HwMsCnJ0SX337YYCeTWDgC9JWyvUqHIHs=
|
||||
github.com/pion/ice/v4 v4.2.5/go.mod h1:aaABRaykEYnNjccjbiimuYxViaASeuv5mk9BpplUxK0=
|
||||
github.com/pion/interceptor v0.1.44 h1:sNlZwM8dWXU9JQAkJh8xrarC0Etn8Oolcniukmuy0/I=
|
||||
github.com/pion/interceptor v0.1.44/go.mod h1:4atVlBkcgXuUP+ykQF0qOCGU2j7pQzX2ofvPRFsY5RY=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
|
||||
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
||||
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.7 h1:qslKkG8qxvQ7hqaxkmL7Pl0XcUm+/Er7nMnu6Vq+ZxM=
|
||||
github.com/pion/rtp v1.8.7/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.19 h1:2CYuw+SQ5vkQ9t0HdOPccsCz1GQMDuVy5PglLgKVBW8=
|
||||
github.com/pion/sctp v1.8.19/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE=
|
||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
||||
github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk=
|
||||
github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
|
||||
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
|
||||
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.3.6 h1:7XAh4RPtlY1Vul6/GmZrv7z+NnxKA6If0KStXBI2ZLE=
|
||||
github.com/pion/webrtc/v3 v3.3.6/go.mod h1:zyN7th4mZpV27eXybfR/cnUf3J2DRy8zw/mdjD9JTNM=
|
||||
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
||||
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
||||
github.com/pion/rtp v1.10.1 h1:xP1prZcCTUuhO2c83XtxyOHJteISg6o8iPsE2acaMtA=
|
||||
github.com/pion/rtp v1.10.1/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pion/sctp v1.9.5 h1:QoSFB/drmAsmSeSFNQNI3xx010nW4HsycCZckRVWWag=
|
||||
github.com/pion/sctp v1.9.5/go.mod h1:N20Dq6LY+JvJDAh9VVh1JELngb2rQ8dPgds5yBWiPgw=
|
||||
github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI=
|
||||
github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8=
|
||||
github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ=
|
||||
github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M=
|
||||
github.com/pion/stun/v3 v3.1.2 h1:86IhD8wFn6IDW4b1/0QzoQS+f5PeA8OHHRn8UZW5ErY=
|
||||
github.com/pion/stun/v3 v3.1.2/go.mod h1:H7gDic7nNwlUL05pbs6T1dtaBehh/KjupxfWw3ZI7cA=
|
||||
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||
github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=
|
||||
github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=
|
||||
github.com/pion/turn/v5 v5.0.3 h1:I+Nw0fQgdPWF1SXDj0egWDhCkcff7gWiigdQpOK52Ak=
|
||||
github.com/pion/turn/v5 v5.0.3/go.mod h1:fs4SogUh/aRGQzonc4Lx3Jp4EU3j3t0PfNDEd9KcD/w=
|
||||
github.com/pion/webrtc/v4 v4.2.12 h1:ux8i+aJxu0OdhcAcVO39JEeodWugD0wdVJoRDtXk1CY=
|
||||
github.com/pion/webrtc/v4 v4.2.12/go.mod h1:M/DeGZkhdWZVmVgGr34HOD9yUDekVJtz9c9PGO18urQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg=
|
||||
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
128
server/main.go
128
server/main.go
@@ -1,7 +1,10 @@
|
||||
// tether-server: HTTP+SSE relay for phone↔client clipboard sync.
|
||||
// tether-server v0.3: HTTP+SSE relay with extensible message envelope.
|
||||
//
|
||||
// v0.1: SSE-only relay with broadcast bus.
|
||||
// v0.2 (this): /metrics endpoint + signaling stubs (mailbox for WebRTC SDP/ICE).
|
||||
// The same /api/send + /api/stream pipeline carries TWO message kinds:
|
||||
// - "clipboard" — the user-facing payload (text)
|
||||
// - "signal" — WebRTC SDP/ICE for peer negotiation
|
||||
//
|
||||
// Peers filter by .Type on the client side. Server is neutral relay.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -23,10 +26,14 @@ import (
|
||||
//go:embed web/index.html
|
||||
var webFS embed.FS
|
||||
|
||||
// Message envelope. Type defaults to "clipboard" for backward compat.
|
||||
type Message struct {
|
||||
Text string `json:"text"`
|
||||
Source string `json:"source,omitempty"`
|
||||
TS int64 `json:"ts"`
|
||||
Type string `json:"type,omitempty"` // "clipboard" | "signal"
|
||||
Text string `json:"text,omitempty"` // clipboard text
|
||||
Signal json.RawMessage `json:"signal,omitempty"` // {kind:offer|answer|ice, ...}
|
||||
From string `json:"from,omitempty"` // sender peer id (for signal filtering)
|
||||
Source string `json:"source,omitempty"` // human-readable label
|
||||
TS int64 `json:"ts"`
|
||||
}
|
||||
|
||||
type bus struct {
|
||||
@@ -38,13 +45,16 @@ type bus struct {
|
||||
func newBus() *bus { return &bus{clients: map[chan Message]string{}} }
|
||||
|
||||
func (b *bus) subscribe(label string) chan Message {
|
||||
ch := make(chan Message, 16)
|
||||
ch := make(chan Message, 32)
|
||||
b.mu.Lock()
|
||||
b.clients[ch] = label
|
||||
// only replay clipboard messages — signals are time-sensitive
|
||||
for _, m := range b.history {
|
||||
select {
|
||||
case ch <- m:
|
||||
default:
|
||||
if m.Type == "" || m.Type == "clipboard" {
|
||||
select {
|
||||
case ch <- m:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
@@ -63,9 +73,11 @@ func (b *bus) unsubscribe(ch chan Message) {
|
||||
func (b *bus) publish(m Message) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.history = append(b.history, m)
|
||||
if len(b.history) > 10 {
|
||||
b.history = b.history[len(b.history)-10:]
|
||||
if m.Type == "" || m.Type == "clipboard" {
|
||||
b.history = append(b.history, m)
|
||||
if len(b.history) > 10 {
|
||||
b.history = b.history[len(b.history)-10:]
|
||||
}
|
||||
}
|
||||
for ch := range b.clients {
|
||||
select {
|
||||
@@ -75,16 +87,15 @@ func (b *bus) publish(m Message) {
|
||||
}
|
||||
}
|
||||
|
||||
// Prometheus metrics --------------------------------------------------------
|
||||
|
||||
// Prometheus metrics
|
||||
var (
|
||||
messages = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "tether_messages_total",
|
||||
Help: "Total messages published to the broadcast bus, by source label.",
|
||||
}, []string{"source"})
|
||||
Help: "Total messages published to the broadcast bus, by source and type.",
|
||||
}, []string{"source", "type"})
|
||||
bytesIn = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "tether_message_bytes_total",
|
||||
Help: "Total bytes of message text published.",
|
||||
Help: "Total bytes of clipboard text published.",
|
||||
})
|
||||
subscribers = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "tether_active_subscribers",
|
||||
@@ -93,46 +104,15 @@ var (
|
||||
publishLatency = promauto.NewHistogram(prometheus.HistogramOpts{
|
||||
Name: "tether_publish_duration_seconds",
|
||||
Help: "Latency of the publish() fan-out, including channel sends.",
|
||||
Buckets: prometheus.ExponentialBuckets(0.0001, 4, 8), // 0.1ms..1.6s
|
||||
Buckets: prometheus.ExponentialBuckets(0.0001, 4, 8),
|
||||
})
|
||||
)
|
||||
|
||||
// Signaling mailbox (v0.3 WebRTC scaffolding) --------------------------------
|
||||
// Peers POST offers/answers/ICE candidates into a per-room mailbox, peers GET
|
||||
// to drain. Pure relay — no SDP parsing, no peer state on server.
|
||||
|
||||
type signalBox struct {
|
||||
mu sync.Mutex
|
||||
rooms map[string][]json.RawMessage
|
||||
}
|
||||
|
||||
func newSignalBox() *signalBox { return &signalBox{rooms: map[string][]json.RawMessage{}} }
|
||||
|
||||
func (s *signalBox) post(room string, msg json.RawMessage) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.rooms[room] = append(s.rooms[room], msg)
|
||||
if len(s.rooms[room]) > 64 {
|
||||
s.rooms[room] = s.rooms[room][len(s.rooms[room])-64:]
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalBox) drain(room string) []json.RawMessage {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
out := s.rooms[room]
|
||||
delete(s.rooms, room)
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func main() {
|
||||
addr := flag.String("addr", ":8765", "listen address")
|
||||
flag.Parse()
|
||||
|
||||
b := newBus()
|
||||
sig := newSignalBox()
|
||||
|
||||
sub, _ := fs.Sub(webFS, "web")
|
||||
mux := http.NewServeMux()
|
||||
@@ -149,6 +129,9 @@ func main() {
|
||||
http.Error(w, "bad json", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if m.Type == "" {
|
||||
m.Type = "clipboard"
|
||||
}
|
||||
if m.Source == "" {
|
||||
m.Source = r.Header.Get("X-Tether-Source")
|
||||
if m.Source == "" {
|
||||
@@ -160,9 +143,13 @@ func main() {
|
||||
t0 := time.Now()
|
||||
b.publish(m)
|
||||
publishLatency.Observe(time.Since(t0).Seconds())
|
||||
messages.WithLabelValues(m.Source).Inc()
|
||||
bytesIn.Add(float64(len(m.Text)))
|
||||
log.Printf("publish: %s len=%d", m.Source, len(m.Text))
|
||||
messages.WithLabelValues(m.Source, m.Type).Inc()
|
||||
if m.Type == "clipboard" {
|
||||
bytesIn.Add(float64(len(m.Text)))
|
||||
log.Printf("publish clipboard: %s len=%d", m.Source, len(m.Text))
|
||||
} else {
|
||||
log.Printf("publish %s: from=%s", m.Type, m.From)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
|
||||
@@ -195,7 +182,11 @@ func main() {
|
||||
return
|
||||
case m := <-ch:
|
||||
bs, _ := json.Marshal(m)
|
||||
fmt.Fprintf(w, "event: clipboard\ndata: %s\n\n", bs)
|
||||
eventName := m.Type
|
||||
if eventName == "" {
|
||||
eventName = "clipboard"
|
||||
}
|
||||
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", eventName, bs)
|
||||
fl.Flush()
|
||||
case <-ka.C:
|
||||
fmt.Fprintf(w, ": keepalive\n\n")
|
||||
@@ -204,35 +195,6 @@ func main() {
|
||||
}
|
||||
})
|
||||
|
||||
// WebRTC signaling: POST to add a message, GET to drain.
|
||||
// /api/signal/<room> is a dumb relay — peers exchange SDP + ICE via this.
|
||||
mux.HandleFunc("/api/signal/", func(w http.ResponseWriter, r *http.Request) {
|
||||
room := r.URL.Path[len("/api/signal/"):]
|
||||
if room == "" {
|
||||
http.Error(w, "missing room", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
body, _ := func() (json.RawMessage, error) {
|
||||
var raw json.RawMessage
|
||||
err := json.NewDecoder(r.Body).Decode(&raw)
|
||||
return raw, err
|
||||
}()
|
||||
sig.post(room, body)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case http.MethodGet:
|
||||
out := sig.drain(room)
|
||||
if out == nil {
|
||||
out = []json.RawMessage{}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(out)
|
||||
default:
|
||||
http.Error(w, "POST or GET", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
header { display: flex; align-items: baseline; gap: 8px; }
|
||||
h1 { margin: 0; font-size: 22px; font-weight: 600; letter-spacing: -0.5px; }
|
||||
.tag { font-size: 11px; color: #888; letter-spacing: 0.5px; text-transform: uppercase; }
|
||||
.pill {
|
||||
font-size: 10px; padding: 2px 8px; border-radius: 999px;
|
||||
background: #1f1f1f; color: #888; letter-spacing: 0.4px;
|
||||
}
|
||||
.pill.live { background: #052e16; color: #4ade80; }
|
||||
.pill.connecting { background: #1f2937; color: #fbbf24; }
|
||||
.row { display: flex; flex-direction: column; gap: 6px; }
|
||||
label { font-size: 11px; color: #888; letter-spacing: 0.4px; text-transform: uppercase; }
|
||||
textarea {
|
||||
@@ -56,6 +62,7 @@
|
||||
max-height: 200px; overflow: auto;
|
||||
}
|
||||
.meta { font-size: 11px; color: #666; margin-top: 4px; }
|
||||
.meta .rtc-badge { color: #4ade80; }
|
||||
footer {
|
||||
margin-top: auto; padding-top: 8px;
|
||||
font-size: 11px; color: #555; text-align: center;
|
||||
@@ -67,6 +74,7 @@
|
||||
<header>
|
||||
<h1>tether</h1>
|
||||
<span class="tag">phone ↔ laptop</span>
|
||||
<span class="pill" id="rtcPill">sse</span>
|
||||
</header>
|
||||
|
||||
<div class="row">
|
||||
@@ -86,21 +94,41 @@
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<a href="https://gitea.pecord.io/pecord/tether">tether on gitea</a> · v0.1
|
||||
<a href="https://gitea.pecord.io/pecord/tether">tether on gitea</a> · v0.3
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const peerID = "tether-browser-" + Math.random().toString(36).slice(2, 8);
|
||||
let rtcChannel = null; // open RTCDataChannel, when we have one
|
||||
|
||||
const status = (msg, cls) => {
|
||||
const s = $("status");
|
||||
s.textContent = msg;
|
||||
s.className = "status" + (cls ? " " + cls : "");
|
||||
};
|
||||
const setPill = (mode) => {
|
||||
const p = $("rtcPill");
|
||||
p.textContent = mode;
|
||||
p.className = "pill " + (mode === "rtc" ? "live" : mode === "negotiating" ? "connecting" : "");
|
||||
};
|
||||
|
||||
function addIncoming(text, source, ts, viaRTC) {
|
||||
const el = document.createElement("div");
|
||||
el.className = "msg";
|
||||
el.textContent = text;
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "meta";
|
||||
const badge = viaRTC ? '<span class="rtc-badge">via rtc</span> · ' : '';
|
||||
meta.innerHTML = badge + (source || "client") + " @ " + new Date(ts || Date.now()).toLocaleTimeString();
|
||||
el.appendChild(meta);
|
||||
const feed = $("incoming");
|
||||
feed.insertBefore(el, feed.firstChild);
|
||||
}
|
||||
|
||||
$("pasteBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
$("out").value = text;
|
||||
$("out").value = await navigator.clipboard.readText();
|
||||
status("pasted from clipboard", "ok");
|
||||
} catch (e) {
|
||||
status("clipboard read denied — paste manually", "err");
|
||||
@@ -110,38 +138,91 @@
|
||||
$("sendBtn").addEventListener("click", async () => {
|
||||
const text = $("out").value;
|
||||
if (!text) { status("empty", "err"); return; }
|
||||
status("sending…");
|
||||
try {
|
||||
const r = await fetch("/api/send", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text }),
|
||||
});
|
||||
if (r.ok) {
|
||||
status("delivered ✓", "ok");
|
||||
} else {
|
||||
status("server returned " + r.status, "err");
|
||||
}
|
||||
} catch (e) {
|
||||
status("network error", "err");
|
||||
if (rtcChannel && rtcChannel.readyState === "open") {
|
||||
rtcChannel.send(text);
|
||||
status("sent via rtc ✓", "ok");
|
||||
} else {
|
||||
status("sending via http…");
|
||||
try {
|
||||
const r = await fetch("/api/send", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ type: "clipboard", text, source: "phone" }),
|
||||
});
|
||||
status(r.ok ? "delivered ✓" : "server returned " + r.status, r.ok ? "ok" : "err");
|
||||
} catch (e) { status("network error", "err"); }
|
||||
}
|
||||
});
|
||||
|
||||
// SSE: receive incoming messages from clients
|
||||
// ── WebRTC peer ────────────────────────────────────────────────────────
|
||||
// Browser acts as ANSWERER. Listens for offers via SSE, replies via /api/send.
|
||||
async function postSignal(payload) {
|
||||
await fetch("/api/send", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
type: "signal",
|
||||
from: peerID,
|
||||
source: "phone",
|
||||
signal: JSON.stringify(payload),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
let pc = null;
|
||||
async function handleOffer(sdp) {
|
||||
if (pc) try { pc.close(); } catch (_) {}
|
||||
pc = new RTCPeerConnection({
|
||||
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
|
||||
});
|
||||
setPill("negotiating");
|
||||
pc.onicecandidate = (ev) => {
|
||||
if (ev.candidate) postSignal({ kind: "ice", candidate: ev.candidate.toJSON() });
|
||||
};
|
||||
pc.onconnectionstatechange = () => {
|
||||
if (pc.connectionState === "connected") setPill("rtc");
|
||||
else if (pc.connectionState === "failed" || pc.connectionState === "disconnected") setPill("sse");
|
||||
};
|
||||
pc.ondatachannel = (ev) => {
|
||||
const ch = ev.channel;
|
||||
ch.onopen = () => { rtcChannel = ch; setPill("rtc"); };
|
||||
ch.onclose = () => { rtcChannel = null; setPill("sse"); };
|
||||
ch.onmessage = (m) => addIncoming(m.data, "laptop", Date.now(), true);
|
||||
};
|
||||
await pc.setRemoteDescription({ type: "offer", sdp });
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
await postSignal({ kind: "answer", sdp: answer });
|
||||
}
|
||||
|
||||
async function handleIceCandidate(candidate) {
|
||||
if (pc) {
|
||||
try { await pc.addIceCandidate(candidate); } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// ── SSE main feed ──────────────────────────────────────────────────────
|
||||
function connectFeed() {
|
||||
const es = new EventSource("/api/stream");
|
||||
es.addEventListener("clipboard", (ev) => {
|
||||
try {
|
||||
const m = JSON.parse(ev.data);
|
||||
const el = document.createElement("div");
|
||||
el.className = "msg";
|
||||
el.textContent = m.text;
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "meta";
|
||||
meta.textContent = (m.source || "client") + " @ " + new Date(m.ts).toLocaleTimeString();
|
||||
el.appendChild(meta);
|
||||
const feed = $("incoming");
|
||||
feed.insertBefore(el, feed.firstChild);
|
||||
if (m.source !== "phone") {
|
||||
addIncoming(m.text, m.source, m.ts, false);
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
es.addEventListener("signal", (ev) => {
|
||||
try {
|
||||
const m = JSON.parse(ev.data);
|
||||
if (m.from === peerID) return; // ignore our own
|
||||
let payload = m.signal;
|
||||
if (typeof payload === "string") payload = JSON.parse(payload);
|
||||
if (payload.kind === "offer" && payload.sdp) {
|
||||
handleOffer(payload.sdp.sdp);
|
||||
} else if (payload.kind === "ice" && payload.candidate) {
|
||||
handleIceCandidate(payload.candidate);
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
es.onerror = () => setTimeout(connectFeed, 2000);
|
||||
|
||||
Reference in New Issue
Block a user