Skip to content

Commit d5b9c02

Browse files
author
Datanoise
committed
feat: implement experimental WebRTC listener support and separate player
1 parent e1c07fe commit d5b9c02

5 files changed

Lines changed: 387 additions & 2 deletions

File tree

go.mod

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,32 @@ require (
1515
github.com/go-audio/riff v1.0.0 // indirect
1616
github.com/go-audio/wav v1.1.0 // indirect
1717
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab // indirect
18-
github.com/google/uuid v1.5.0 // indirect
18+
github.com/google/uuid v1.6.0 // indirect
1919
github.com/hajimehoshi/go-mp3 v0.3.4 // indirect
2020
github.com/kazzmir/opus-go v1.2.1 // indirect
2121
github.com/mattn/go-isatty v0.0.20 // indirect
22+
github.com/pion/datachannel v1.6.0 // indirect
23+
github.com/pion/dtls/v3 v3.1.2 // indirect
24+
github.com/pion/ice/v4 v4.2.1 // indirect
25+
github.com/pion/interceptor v0.1.44 // indirect
26+
github.com/pion/logging v0.2.4 // indirect
27+
github.com/pion/mdns/v2 v2.1.0 // indirect
28+
github.com/pion/randutil v0.1.0 // indirect
29+
github.com/pion/rtcp v1.2.16 // indirect
30+
github.com/pion/rtp v1.10.1 // indirect
31+
github.com/pion/sctp v1.9.2 // indirect
32+
github.com/pion/sdp/v3 v3.0.18 // indirect
33+
github.com/pion/srtp/v3 v3.0.10 // indirect
34+
github.com/pion/stun/v3 v3.1.1 // indirect
35+
github.com/pion/transport/v4 v4.0.1 // indirect
36+
github.com/pion/turn/v4 v4.1.4 // indirect
37+
github.com/pion/webrtc/v4 v4.2.8 // indirect
2238
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
23-
golang.org/x/net v0.49.0 // indirect
39+
github.com/wlynxg/anet v0.0.5 // indirect
40+
golang.org/x/net v0.50.0 // indirect
2441
golang.org/x/sys v0.41.0 // indirect
2542
golang.org/x/text v0.34.0 // indirect
43+
golang.org/x/time v0.10.0 // indirect
2644
modernc.org/libc v1.37.6 // indirect
2745
modernc.org/mathutil v1.6.0 // indirect
2846
modernc.org/memory v1.7.2 // indirect

go.sum

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,47 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu
1818
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
1919
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
2020
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
21+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
22+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
2123
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=
2224
github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo=
2325
github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
2426
github.com/kazzmir/opus-go v1.2.1 h1:uKHJgA4zlZv60eatfwGBhLdrovlPdRVBVco22MQQvmU=
2527
github.com/kazzmir/opus-go v1.2.1/go.mod h1:oKDlLYxYVpknE4zKDmJyiLPB9RyhKw0n/l7EoPr6000=
2628
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
2729
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
30+
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
31+
github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=
32+
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
33+
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
34+
github.com/pion/ice/v4 v4.2.1 h1:XPRYXaLiFq3LFDG7a7bMrmr3mFr27G/gtXN3v/TVfxY=
35+
github.com/pion/ice/v4 v4.2.1/go.mod h1:2quLV1S5v1tAx3VvAJaH//KGitRXvo4RKlX6D3tnN+c=
36+
github.com/pion/interceptor v0.1.44 h1:sNlZwM8dWXU9JQAkJh8xrarC0Etn8Oolcniukmuy0/I=
37+
github.com/pion/interceptor v0.1.44/go.mod h1:4atVlBkcgXuUP+ykQF0qOCGU2j7pQzX2ofvPRFsY5RY=
38+
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
39+
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
40+
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
41+
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
42+
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
43+
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
44+
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
45+
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
46+
github.com/pion/rtp v1.10.1 h1:xP1prZcCTUuhO2c83XtxyOHJteISg6o8iPsE2acaMtA=
47+
github.com/pion/rtp v1.10.1/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
48+
github.com/pion/sctp v1.9.2 h1:HxsOzEV9pWoeggv7T5kewVkstFNcGvhMPx0GvUOUQXo=
49+
github.com/pion/sctp v1.9.2/go.mod h1:OTOlsQ5EDQ6mQ0z4MUGXt2CgQmKyafBEXhUVqLRB6G8=
50+
github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI=
51+
github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8=
52+
github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ=
53+
github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M=
54+
github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
55+
github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
56+
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
57+
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
58+
github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=
59+
github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=
60+
github.com/pion/webrtc/v4 v4.2.8 h1:zitoHzPcHgdh8o3ndpQ8tQH04d03QD/KeS8y4CtCsd0=
61+
github.com/pion/webrtc/v4 v4.2.8/go.mod h1:9EmLZve0H76eTzf8v2FmchZ6tcBXtDgpfTEu+drW6SY=
2862
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2963
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
3064
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -33,16 +67,22 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w
3367
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
3468
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
3569
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
70+
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
71+
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
3672
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
3773
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
3874
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
3975
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
76+
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
77+
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
4078
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
4179
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
4280
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
4381
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
4482
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
4583
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
84+
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
85+
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
4686
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
4787
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
4888
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=

relay/webrtc.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package relay
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"sync"
9+
"time"
10+
11+
"github.com/pion/webrtc/v4"
12+
"github.com/pion/webrtc/v4/pkg/media"
13+
"github.com/kazzmir/opus-go/ogg"
14+
"github.com/sirupsen/logrus"
15+
)
16+
17+
type WebRTCManager struct {
18+
api *webrtc.API
19+
relay *Relay
20+
mu sync.RWMutex
21+
}
22+
23+
func NewWebRTCManager(r *Relay) *WebRTCManager {
24+
s := webrtc.SettingEngine{}
25+
// Optimize for many listeners
26+
s.SetICETimeouts(10*time.Second, 20*time.Second, 2*time.Second)
27+
28+
api := webrtc.NewAPI(webrtc.WithSettingEngine(s))
29+
return &WebRTCManager{
30+
api: api,
31+
relay: r,
32+
}
33+
}
34+
35+
func (wm *WebRTCManager) HandleOffer(mount string, offer webrtc.SessionDescription) (*webrtc.SessionDescription, error) {
36+
stream, ok := wm.relay.GetStream(mount)
37+
if !ok {
38+
return nil, fmt.Errorf("stream not found")
39+
}
40+
41+
peerConnection, err := wm.api.NewPeerConnection(webrtc.Configuration{
42+
ICEServers: []webrtc.ICEServer{
43+
{URLs: []string{"stun:stun.l.google.com:19302"}},
44+
},
45+
})
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
// Create a track for Opus audio
51+
audioTrack, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "tinyice")
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
if _, err = peerConnection.AddTrack(audioTrack); err != nil {
57+
return nil, err
58+
}
59+
60+
// Set the remote SessionDescription
61+
if err = peerConnection.SetRemoteDescription(offer); err != nil {
62+
return nil, err
63+
}
64+
65+
// Create answer
66+
answer, err := peerConnection.CreateAnswer(nil)
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
// Sets the LocalDescription and starts gathering ICE candidates
72+
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
73+
if err = peerConnection.SetLocalDescription(answer); err != nil {
74+
return nil, err
75+
}
76+
<-gatherComplete
77+
78+
// Start feeding the track
79+
go wm.streamToTrack(peerConnection, audioTrack, stream)
80+
81+
return peerConnection.LocalDescription(), nil
82+
}
83+
84+
func (wm *WebRTCManager) streamToTrack(pc *webrtc.PeerConnection, track *webrtc.TrackLocalStaticSample, stream *Stream) {
85+
ctx, cancel := context.WithCancel(context.Background())
86+
defer cancel()
87+
88+
pc.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
89+
if s == webrtc.PeerConnectionStateClosed || s == webrtc.PeerConnectionStateFailed {
90+
cancel()
91+
}
92+
})
93+
94+
logrus.Infof("WebRTC listener started for %s", stream.MountName)
95+
defer logrus.Infof("WebRTC listener stopped for %s", stream.MountName)
96+
97+
id := fmt.Sprintf("webrtc-%d", time.Now().UnixNano())
98+
offset, signal := stream.Subscribe(id, 64*1024)
99+
defer stream.Unsubscribe(id)
100+
101+
reader := &streamReader{
102+
stream: stream,
103+
offset: offset,
104+
signal: signal,
105+
ctx: ctx,
106+
id: id,
107+
}
108+
109+
opusReader, err := ogg.NewOpusReader(reader)
110+
if err != nil {
111+
logrus.WithError(err).Error("Failed to initialize Ogg/Opus reader for WebRTC")
112+
return
113+
}
114+
115+
for {
116+
packet, err := opusReader.ReadAudioPacket()
117+
if err != nil {
118+
if err != io.EOF && !errors.Is(err, context.Canceled) {
119+
logrus.WithError(err).Error("Error reading Opus packet")
120+
}
121+
return
122+
}
123+
124+
// WebRTC expects 48kHz Opus samples.
125+
// We use a fixed duration of 20ms per packet which is standard.
126+
if err := track.WriteSample(media.Sample{
127+
Data: packet.Data,
128+
Duration: 20 * time.Millisecond,
129+
}); err != nil {
130+
return
131+
}
132+
}
133+
}

server/server.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/gomarkdown/markdown"
2929
"github.com/gomarkdown/markdown/html"
3030
"github.com/gomarkdown/markdown/parser"
31+
"github.com/pion/webrtc/v4"
3132
"github.com/sirupsen/logrus"
3233
"golang.org/x/crypto/acme"
3334
"golang.org/x/crypto/acme/autocert"
@@ -44,6 +45,7 @@ type Server struct {
4445
Relay *relay.Relay
4546
RelayM *relay.RelayManager
4647
TranscoderM *relay.TranscoderManager
48+
WebRTCM *relay.WebRTCManager
4749
tmpl *template.Template
4850
httpServers []*http.Server
4951
startTime time.Time
@@ -154,6 +156,7 @@ func NewServer(cfg *config.Config, authLog *logrus.Logger) *Server {
154156
Relay: r,
155157
RelayM: relay.NewRelayManager(r),
156158
TranscoderM: relay.NewTranscoderManager(r),
159+
WebRTCM: relay.NewWebRTCManager(r),
157160
tmpl: tmpl,
158161
startTime: time.Now(),
159162
AuthLog: authLog,
@@ -194,7 +197,9 @@ func (s *Server) setupRoutes() *http.ServeMux {
194197
mux.HandleFunc("/login", s.handleLogin)
195198
mux.HandleFunc("/logout", s.handleLogout)
196199
mux.HandleFunc("/explore", s.handleExplore)
200+
mux.HandleFunc("/webrtc/offer", s.handleWebRTCOffer)
197201
mux.HandleFunc("/player/", s.handlePlayer)
202+
mux.HandleFunc("/player-webrtc/", s.handleWebRTCPlayer)
198203
mux.HandleFunc("/embed/", s.handleEmbed)
199204
mux.HandleFunc("/", s.handleRoot)
200205
mux.HandleFunc("/events", s.handlePublicEvents)
@@ -1799,6 +1804,34 @@ func (s *Server) handleGetStats(w http.ResponseWriter, r *http.Request) {
17991804
})
18001805
}
18011806

1807+
func (s *Server) handleWebRTCOffer(w http.ResponseWriter, r *http.Request) {
1808+
if r.Method != http.MethodPost {
1809+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
1810+
return
1811+
}
1812+
1813+
mount := r.URL.Query().Get("mount")
1814+
if mount == "" {
1815+
http.Error(w, "mount query param required", http.StatusBadRequest)
1816+
return
1817+
}
1818+
1819+
var offer webrtc.SessionDescription
1820+
if err := json.NewDecoder(r.Body).Decode(&offer); err != nil {
1821+
http.Error(w, err.Error(), http.StatusBadRequest)
1822+
return
1823+
}
1824+
1825+
answer, err := s.WebRTCM.HandleOffer(mount, offer)
1826+
if err != nil {
1827+
http.Error(w, err.Error(), http.StatusInternalServerError)
1828+
return
1829+
}
1830+
1831+
w.Header().Set("Content-Type", "application/json")
1832+
json.NewEncoder(w).Encode(answer)
1833+
}
1834+
18021835
func (s *Server) handlePlayer(w http.ResponseWriter, r *http.Request) {
18031836
mount := strings.TrimPrefix(r.URL.Path, "/player")
18041837
if mount == "" || mount == "/" {
@@ -1831,6 +1864,28 @@ func (s *Server) handlePlayer(w http.ResponseWriter, r *http.Request) {
18311864
}
18321865
}
18331866

1867+
func (s *Server) handleWebRTCPlayer(w http.ResponseWriter, r *http.Request) {
1868+
mount := strings.TrimPrefix(r.URL.Path, "/player-webrtc")
1869+
if mount == "" || mount == "/" {
1870+
http.Redirect(w, r, "/explore", http.StatusSeeOther)
1871+
return
1872+
}
1873+
1874+
stream, ok := s.Relay.GetStream(mount)
1875+
if !ok {
1876+
http.Error(w, "Stream not found", http.StatusNotFound)
1877+
return
1878+
}
1879+
1880+
data := map[string]interface{}{
1881+
"Stream": stream,
1882+
"Config": s.Config,
1883+
}
1884+
if err := s.tmpl.ExecuteTemplate(w, "webrtc_player.html", data); err != nil {
1885+
logrus.WithError(err).Error("Template error")
1886+
}
1887+
}
1888+
18341889
func (s *Server) handleEmbed(w http.ResponseWriter, r *http.Request) {
18351890
mount := strings.TrimPrefix(r.URL.Path, "/embed")
18361891
if mount == "" || mount == "/" {

0 commit comments

Comments
 (0)