char/live.umm.gay
git clone https://git.t4t.associates/char/live.umm.gay
acb190d
main
1package main 2 3import ( 4"errors" 5"fmt" 6"io" 7"log" 8"net" 9"net/http" 10"os" 11"strconv" 12"strings" 13"sync" 14 15"database/sql" 16 17"github.com/joho/godotenv" 18"github.com/pion/ice/v2" 19"github.com/pion/interceptor" 20"github.com/pion/webrtc/v3" 21 22 _"github.com/mattn/go-sqlite3" 23) 24 25type WebRTCStream struct { 26audioTrack ,videoTrack * webrtc.TrackLocalStaticRTP 27} 28 29var ( 30runningStreams map [string ]WebRTCStream 31runningStreamsLock sync.Mutex 32api * webrtc.API 33db * sql.DB 34) 35 36func main () { 37if err := godotenv .Load (".env" );err != nil { 38log .Fatal (err ) 39 } 40 41db = setupDatabase () 42defer db .Close () 43 44api = setupWebRTC () 45runningStreams = map [string ]WebRTCStream {} 46 47mux := http .NewServeMux () 48mux .HandleFunc ("/api/wish-server/whip" ,withCors (HandleWHIP )) 49mux .HandleFunc ("/api/wish-server/whep" ,withCors (HandleWHEP )) 50 51log .Fatal ((& http.Server { 52Handler :mux , 53Addr :os .Getenv ("HTTP_ADDRESS" ), 54 }).ListenAndServe ()) 55} 56 57func setupDatabase ()* sql.DB { 58db ,err := sql .Open ("sqlite3" ,os .Getenv ("DATABASE_URL" )) 59if err != nil { 60log .Fatal (err ) 61 } 62 63return db 64} 65 66func setupWebRTC ()* webrtc.API { 67mediaEngine := & webrtc.MediaEngine {} 68if err := mediaEngine .RegisterDefaultCodecs ();err != nil { 69log .Fatal (err ) 70 } 71 72interceptorRegistry := & interceptor.Registry {} 73if err := webrtc .RegisterDefaultInterceptors (mediaEngine ,interceptorRegistry );err != nil { 74log .Fatal (err ) 75 } 76 77settingEngine := webrtc.SettingEngine {} 78setupICE (& settingEngine ) 79 80return webrtc .NewAPI ( 81webrtc .WithMediaEngine (mediaEngine ), 82webrtc .WithInterceptorRegistry (interceptorRegistry ), 83webrtc .WithSettingEngine (settingEngine ), 84 ) 85} 86 87func setupICE (settingEngine * webrtc.SettingEngine ) { 88settingEngine .SetNetworkTypes ([]webrtc.NetworkType { 89webrtc .NetworkTypeUDP4 ,webrtc .NetworkTypeUDP6 , 90webrtc .NetworkTypeTCP4 ,webrtc .NetworkTypeTCP6 , 91 }) 92 93if udpPort := os .Getenv ("UDP_MUX_PORT" );udpPort != "" { 94port ,err := strconv .Atoi (udpPort ) 95if err != nil { 96log .Fatal (err ) 97 } 98mux ,err := ice .NewMultiUDPMuxFromPort (port ) 99if err != nil { 100log .Fatal (err ) 101 } 102settingEngine .SetICEUDPMux (mux ) 103 } 104 105if tcpAddr := os .Getenv ("TCP_MUX_ADDR" );tcpAddr != "" { 106addr ,err := net .ResolveTCPAddr ("tcp" ,tcpAddr ) 107if err != nil { 108log .Fatal (err ) 109 } 110 111listener ,err := net .ListenTCP ("tcp" ,addr ) 112if err != nil { 113log .Fatal (err ) 114 } 115 116mux := webrtc .NewICETCPMux (nil ,listener ,8 ) 117settingEngine .SetICETCPMux (mux ) 118 } 119} 120 121func withCors (next func (w http.ResponseWriter ,r * http.Request )) http.HandlerFunc { 122return func (res http.ResponseWriter ,req * http.Request ) { 123res .Header ().Set ("Access-Control-Allow-Origin" ,"*" ) 124res .Header ().Set ("Access-Control-Allow-Methods" ,"*" ) 125res .Header ().Set ("Access-Control-Allow-Headers" ,"*" ) 126 127if req .Method != http .MethodOptions { 128next (res ,req ) 129 } 130 } 131} 132 133func logHTTPError (w http.ResponseWriter ,err string ,code int ) { 134log .Println (err ) 135http .Error (w ,err ,code ) 136} 137 138func getTracksForStream (streamName string ) ( 139* webrtc.TrackLocalStaticRTP , 140* webrtc.TrackLocalStaticRTP , 141error , 142) { 143runningStreamsLock .Lock () 144defer runningStreamsLock .Unlock () 145 146foundStream ,ok := runningStreams [streamName ] 147if ! ok { 148videoTrack ,err := webrtc .NewTrackLocalStaticRTP (webrtc.RTPCodecCapability {MimeType :webrtc .MimeTypeH264 },"video" ,"pion" ) 149if err != nil { 150return nil ,nil ,err 151 } 152 153audioTrack ,err := webrtc .NewTrackLocalStaticRTP (webrtc.RTPCodecCapability {MimeType :webrtc .MimeTypeOpus },"audio" ,"pion" ) 154if err != nil { 155return nil ,nil ,err 156 } 157 158foundStream = WebRTCStream { 159audioTrack :audioTrack , 160videoTrack :videoTrack , 161 } 162runningStreams [streamName ]= foundStream 163 } 164 165return foundStream .audioTrack ,foundStream .videoTrack ,nil 166} 167 168func HandleWHIP (res http.ResponseWriter ,req * http.Request ) { 169authorization := req .Header .Get ("Authorization" ) 170if authorization == "" { 171logHTTPError (res ,"Authorization was not set" ,http .StatusBadRequest ) 172return 173 } 174streamName ,streamPassword ,_ := strings .Cut (authorization ,":" ) 175streamName ,_ = strings .CutPrefix (strings .ToLower (streamName ),"bearer " ) 176 177var qN string 178var qP string 179if err := db .QueryRow ("SELECT * FROM streams WHERE stream = ? AND password = ?" ,streamName ,streamPassword ).Scan (& qN ,& qP );err != nil { 180logHTTPError (res ,"Invalid stream authorization for: " + streamName + " - " + err .Error (),http .StatusUnauthorized ) 181return 182 } 183 184offer ,err := io .ReadAll (req .Body ) 185if err != nil { 186logHTTPError (res ,err .Error (),http .StatusInternalServerError ) 187return 188 } 189 190peerConnection ,err := api .NewPeerConnection (webrtc.Configuration { 191ICEServers : []webrtc.ICEServer { 192 { 193URLs : []string {"stun:stun.cloudflare.com:3478" }, 194 }, 195 { 196URLs : []string {"stun:stun.l.google.com:19302" }, 197 }, 198 }, 199 }) 200if err != nil { 201logHTTPError (res ,err .Error (),http .StatusInternalServerError ) 202return 203 } 204 205audioTrack ,videoTrack ,err := getTracksForStream (streamName ) 206if err != nil { 207logHTTPError (res ,err .Error (),http .StatusInternalServerError ) 208return 209 } 210 211peerConnection .OnTrack (func (track * webrtc.TrackRemote ,_recv * webrtc.RTPReceiver ) { 212var localTrack * webrtc.TrackLocalStaticRTP 213if strings .HasPrefix (track .Codec ().RTPCodecCapability .MimeType ,"audio/" ) { 214localTrack = audioTrack 215 }else { 216localTrack = videoTrack 217 } 218 219rtpBuf := make ([]byte ,1500 ) 220for { 221rtpRead ,_ ,readErr := track .Read (rtpBuf ) 222switch { 223case errors .Is (readErr ,io .EOF ): 224return 225case readErr != nil : 226log .Println (readErr ) 227return 228 } 229 230if _ ,writeErr := localTrack .Write (rtpBuf [:rtpRead ]);writeErr != nil && ! errors .Is (writeErr ,io .ErrClosedPipe ) { 231log .Println (writeErr ) 232return 233 } 234 } 235 }) 236 237peerConnection .OnICEConnectionStateChange (func (state webrtc.ICEConnectionState ) { 238if state == webrtc .ICEConnectionStateFailed { 239if err := peerConnection .Close ();err != nil { 240log .Println (err ) 241return 242 } 243 } 244 }) 245 246if err := peerConnection .SetRemoteDescription (webrtc.SessionDescription { 247SDP :string (offer ), 248Type :webrtc .SDPTypeOffer , 249 });err != nil { 250logHTTPError (res ,err .Error (),http .StatusBadRequest ) 251return 252 } 253 254gatheringComplete := webrtc .GatheringCompletePromise (peerConnection ) 255 256answer ,err := peerConnection .CreateAnswer (nil ) 257if err != nil { 258logHTTPError (res ,err .Error (),http .StatusInternalServerError ) 259return 260 }else if err = peerConnection .SetLocalDescription (answer );err != nil { 261logHTTPError (res ,err .Error (),http .StatusInternalServerError ) 262return 263 } 264 265<- gatheringComplete 266 267fmt .Fprint (res ,peerConnection .LocalDescription ().SDP ) 268} 269 270func HandleWHEP (res http.ResponseWriter ,req * http.Request ) { 271streamName := req .Header .Get ("Authorization" ) 272streamName ,_ = strings .CutPrefix (strings .ToLower (streamName ),"bearer " ) 273if streamName == "" { 274logHTTPError (res ,"Stream name was not set" ,http .StatusBadRequest ) 275return 276 } 277 278offer ,err := io .ReadAll (req .Body ) 279if err != nil { 280logHTTPError (res ,err .Error (),http .StatusInternalServerError ) 281return 282 } 283 284peerConnection ,err := api .NewPeerConnection (webrtc.Configuration {}) 285if err != nil { 286logHTTPError (res ,err .Error (),http .StatusInternalServerError ) 287return 288 } 289 290audioTrack ,videoTrack ,err := getTracksForStream (streamName ) 291if err != nil { 292logHTTPError (res ,err .Error (),http .StatusInternalServerError ) 293return 294 } 295 296if _ ,err = peerConnection .AddTrack (audioTrack );err != nil { 297logHTTPError (res ,err .Error (),http .StatusInternalServerError ) 298return 299 } 300 301if _ ,err = peerConnection .AddTrack (videoTrack );err != nil { 302logHTTPError (res ,err .Error (),http .StatusInternalServerError ) 303return 304 } 305 306if err := peerConnection .SetRemoteDescription (webrtc.SessionDescription { 307SDP :string (offer ), 308Type :webrtc .SDPTypeOffer , 309 });err != nil { 310logHTTPError (res ,err .Error (),http .StatusInternalServerError ) 311return 312 } 313 314gatheringComplete := webrtc .GatheringCompletePromise (peerConnection ) 315 316answer ,err := peerConnection .CreateAnswer (nil ) 317if err != nil { 318logHTTPError (res ,err .Error (),http .StatusInternalServerError ) 319return 320 }else if err = peerConnection .SetLocalDescription (answer );err != nil { 321logHTTPError (res ,err .Error (),http .StatusInternalServerError ) 322return 323 } 324 325<- gatheringComplete 326 327fmt .Fprint (res ,peerConnection .LocalDescription ().SDP ) 328}