🍠 Serve Your Media Without Limits From a "Potato" Computer Hosted in Mom's Basement: Take our Beloved "Series of Tubes" to Full Power
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

222 lines
6.3 KiB

package main
import (
"context"
"encoding/json"
"fmt"
"log"
"math"
"math/rand"
"net/http"
"time"
nat_upnp "git.sequentialread.com/forest/go-libp2p-nat"
webrtc "github.com/pion/webrtc/v3"
"golang.org/x/net/websocket"
)
var externalAddressString string
var nat *nat_upnp.NAT = nil
// websocketServer is called for every new inbound WebSocket
func websocketServer(ws *websocket.Conn) { // nolint:gocognit
// Create a new RTCPeerConnection
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
ICECandidatePoolSize: uint8(7),
})
if err != nil {
panic(err)
}
// When Pion gathers a new ICE Candidate send it to the client. This is how
// ice trickle is implemented. Everytime we have a new candidate available we send
// it as soon as it is ready. We don't wait to emit a Offer/Answer until they are
// all available
attemptedUPnP := false
peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) {
if candidate == nil {
return
}
toLog, _ := json.MarshalIndent(candidate, "", " ")
log.Println(string(toLog))
outbound, err := json.MarshalIndent(candidate.ToJSON(), "", " ")
if err != nil {
log.Printf("serializing ice candidate as json failed: %+v\n", err)
return
}
if _, err = ws.Write(outbound); err != nil {
log.Printf("websocket write error: %+v\n", err)
return
}
if nat != nil && !attemptedUPnP && candidate.Address == externalAddressString {
attemptedUPnP = true
go func() {
mappedPort, err := nat.NewMapping(candidate.Protocol.String(), int(candidate.Port))
if err == nil {
var mappedExternalString string
mappedExternal, err := mappedPort.ExternalAddr()
if err == nil {
mappedExternalString = mappedExternal.String()
} else {
mappedExternalString = fmt.Sprintf("internet:%d", mappedPort.ExternalPort())
}
log.Printf("port map %s --> localhost:%d\n", mappedExternalString, candidate.Port)
upnpCandidate := webrtc.ICECandidate{
Foundation: candidate.Foundation + "1",
Priority: candidate.Priority - 1,
Address: externalAddressString,
Protocol: candidate.Protocol,
Port: uint16(mappedPort.ExternalPort()),
Typ: webrtc.ICECandidateTypeHost,
Component: 1,
}
outbound, err := json.MarshalIndent(upnpCandidate.ToJSON(), "", " ")
if err != nil {
log.Printf("serializing ice candidate as json failed: %+v\n", err)
return
}
// TODO: can the client simply retry the connection instead??
time.Sleep(time.Millisecond * 500)
if _, err = ws.Write(outbound); err != nil {
log.Printf("websocket write error: %+v\n", err)
return
}
} else {
fmt.Printf(
"warning: uPnP AddPortMapping %s %d failed: %+v\n",
candidate.Protocol.String(), int(candidate.Port), err,
)
}
}()
}
})
// Set the handler for ICE connection state
// This will notify you when the peer has connected/disconnected
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
})
// Send the current time via a DataChannel to the remote peer every 3 seconds
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
d.OnMessage(func(message webrtc.DataChannelMessage) {
log.Printf("from DataChannel: %s", string(message.Data))
})
d.OnOpen(func() {
err = d.SendText("datachannel open!")
if err != nil {
log.Printf("DataChannel write error: %+v\n", err)
return
}
// for range time.Tick(time.Second * 3) {
// err = d.SendText(time.Now().String())
// if err != nil {
// log.Printf("DataChannel write error: %+v\n", err)
// return
// }
// }
})
})
buf := make([]byte, 1500)
for {
// Read each inbound WebSocket Message
n, err := ws.Read(buf)
if err != nil {
log.Printf("websocket read error: %+v\n", err)
return
}
// Unmarshal each inbound WebSocket message
var (
candidate webrtc.ICECandidateInit
offer webrtc.SessionDescription
)
switch {
// Attempt to unmarshal as a SessionDescription. If the SDP field is empty
// assume it is not one.
case json.Unmarshal(buf[:n], &offer) == nil && offer.SDP != "":
err = peerConnection.SetRemoteDescription(offer)
if err != nil {
log.Printf("SetRemoteDescription on peer connection failed: %+v\n", err)
return
}
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
log.Printf("peer connection CreateAnswer failed: %+v\n", err)
return
}
err = peerConnection.SetLocalDescription(answer)
if err != nil {
log.Printf("peer connection SetLocalDescription failed: %+v\n", err)
return
}
answerBytes, err := json.Marshal(answer)
if err != nil {
log.Printf("failed to serialize peer connection answer as json: %+v\n", err)
return
}
_, err = ws.Write(answerBytes)
if err != nil {
log.Printf("websocket write error: %+v\n", err)
}
// Attempt to unmarshal as a ICECandidateInit. If the candidate field is empty
// assume it is not one.
case json.Unmarshal(buf[:n], &candidate) == nil && candidate.Candidate != "":
if err = peerConnection.AddICECandidate(candidate); err != nil {
log.Printf("peer connection AddICECandidate failed: %+v\n", err)
}
default:
log.Printf("Unknown message: %s\n", string(buf))
}
}
}
func main() {
go func() {
var err error
nat, err = nat_upnp.DiscoverNAT(context.Background())
if err == nil {
externalAddress, err := nat.GetExternalAddress()
if err == nil {
externalAddressString = externalAddress.String()
log.Printf("My public IP address is '%s'\n", externalAddress)
} else {
log.Printf("uPnP GetExternalAddress failed: %+v\n", err)
}
} else {
log.Printf("WebRTC connections from mobile (LTE/5g) clients will not work because uPnP DiscoverGateway failed: \n%+v\n", err)
}
}()
http.Handle("/", http.FileServer(http.Dir(".")))
http.Handle("/websocket", websocket.Handler(websocketServer))
fmt.Println("Open http://localhost:8080 to access this demo")
panic(http.ListenAndServe(":8080", nil))
}
func randomPort() int {
rand.Seed(time.Now().UnixNano())
return rand.Intn(math.MaxUint16-10000) + 10000
}