2023-02-23 11:19:57 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"log"
|
2023-02-23 20:35:08 +00:00
|
|
|
"net"
|
2023-02-23 11:19:57 +00:00
|
|
|
"net/http"
|
|
|
|
"os"
|
2023-02-23 20:35:08 +00:00
|
|
|
"strconv"
|
2023-02-23 11:19:57 +00:00
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
|
2023-02-23 16:35:15 +00:00
|
|
|
"database/sql"
|
|
|
|
|
2023-02-23 11:19:57 +00:00
|
|
|
"github.com/joho/godotenv"
|
2023-02-23 20:35:08 +00:00
|
|
|
"github.com/pion/ice/v2"
|
2023-02-23 11:19:57 +00:00
|
|
|
"github.com/pion/interceptor"
|
|
|
|
"github.com/pion/webrtc/v3"
|
2023-02-23 16:35:15 +00:00
|
|
|
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
2023-02-23 11:19:57 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type WebRTCStream struct {
|
|
|
|
audioTrack, videoTrack *webrtc.TrackLocalStaticRTP
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
runningStreams map[string]WebRTCStream
|
|
|
|
runningStreamsLock sync.Mutex
|
|
|
|
api *webrtc.API
|
2023-02-23 16:35:15 +00:00
|
|
|
db *sql.DB
|
2023-02-23 11:19:57 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
if err := godotenv.Load(".env"); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2023-02-23 16:35:15 +00:00
|
|
|
db = setupDatabase()
|
|
|
|
defer db.Close()
|
|
|
|
|
|
|
|
api = setupWebRTC()
|
|
|
|
runningStreams = map[string]WebRTCStream{}
|
|
|
|
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
mux.HandleFunc("/api/wish-server/whip", withCors(HandleWHIP))
|
|
|
|
mux.HandleFunc("/api/wish-server/whep", withCors(HandleWHEP))
|
|
|
|
|
|
|
|
log.Fatal((&http.Server{
|
|
|
|
Handler: mux,
|
|
|
|
Addr: os.Getenv("HTTP_ADDRESS"),
|
|
|
|
}).ListenAndServe())
|
|
|
|
}
|
|
|
|
|
|
|
|
func setupDatabase() *sql.DB {
|
|
|
|
db, err := sql.Open("sqlite3", os.Getenv("DATABASE_URL"))
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return db
|
|
|
|
}
|
|
|
|
|
|
|
|
func setupWebRTC() *webrtc.API {
|
2023-02-23 11:19:57 +00:00
|
|
|
mediaEngine := &webrtc.MediaEngine{}
|
|
|
|
if err := mediaEngine.RegisterDefaultCodecs(); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
interceptorRegistry := &interceptor.Registry{}
|
|
|
|
if err := webrtc.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
settingEngine := webrtc.SettingEngine{}
|
2023-02-23 20:35:08 +00:00
|
|
|
setupICE(&settingEngine)
|
2023-02-23 11:19:57 +00:00
|
|
|
|
2023-02-23 16:35:15 +00:00
|
|
|
return webrtc.NewAPI(
|
2023-02-23 11:19:57 +00:00
|
|
|
webrtc.WithMediaEngine(mediaEngine),
|
|
|
|
webrtc.WithInterceptorRegistry(interceptorRegistry),
|
|
|
|
webrtc.WithSettingEngine(settingEngine),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-02-23 20:35:08 +00:00
|
|
|
func setupICE(settingEngine *webrtc.SettingEngine) {
|
|
|
|
settingEngine.SetNetworkTypes([]webrtc.NetworkType{
|
2023-02-24 05:29:18 +00:00
|
|
|
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,
|
|
|
|
webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,
|
2023-02-23 20:35:08 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
if udpPort := os.Getenv("UDP_MUX_PORT"); udpPort != "" {
|
|
|
|
port, err := strconv.Atoi(udpPort)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
mux, err := ice.NewMultiUDPMuxFromPort(port)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
settingEngine.SetICEUDPMux(mux)
|
|
|
|
}
|
|
|
|
|
|
|
|
if tcpAddr := os.Getenv("TCP_MUX_ADDR"); tcpAddr != "" {
|
|
|
|
addr, err := net.ResolveTCPAddr("tcp", tcpAddr)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
listener, err := net.ListenTCP("tcp", addr)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
mux := webrtc.NewICETCPMux(nil, listener, 8)
|
|
|
|
settingEngine.SetICETCPMux(mux)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-23 11:19:57 +00:00
|
|
|
func withCors(next func(w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
|
|
|
|
return func(res http.ResponseWriter, req *http.Request) {
|
|
|
|
res.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
|
res.Header().Set("Access-Control-Allow-Methods", "*")
|
|
|
|
res.Header().Set("Access-Control-Allow-Headers", "*")
|
|
|
|
|
|
|
|
if req.Method != http.MethodOptions {
|
|
|
|
next(res, req)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func logHTTPError(w http.ResponseWriter, err string, code int) {
|
|
|
|
log.Println(err)
|
|
|
|
http.Error(w, err, code)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getTracksForStream(streamName string) (
|
|
|
|
*webrtc.TrackLocalStaticRTP,
|
|
|
|
*webrtc.TrackLocalStaticRTP,
|
|
|
|
error,
|
|
|
|
) {
|
|
|
|
runningStreamsLock.Lock()
|
|
|
|
defer runningStreamsLock.Unlock()
|
|
|
|
|
|
|
|
foundStream, ok := runningStreams[streamName]
|
|
|
|
if !ok {
|
|
|
|
videoTrack, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264}, "video", "pion")
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
audioTrack, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "pion")
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
foundStream = WebRTCStream{
|
|
|
|
audioTrack: audioTrack,
|
|
|
|
videoTrack: videoTrack,
|
|
|
|
}
|
|
|
|
runningStreams[streamName] = foundStream
|
|
|
|
}
|
|
|
|
|
|
|
|
return foundStream.audioTrack, foundStream.videoTrack, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func HandleWHIP(res http.ResponseWriter, req *http.Request) {
|
2023-02-23 16:35:15 +00:00
|
|
|
authorization := req.Header.Get("Authorization")
|
|
|
|
if authorization == "" {
|
|
|
|
logHTTPError(res, "Authorization was not set", http.StatusBadRequest)
|
2023-02-23 11:19:57 +00:00
|
|
|
return
|
|
|
|
}
|
2023-02-23 16:35:15 +00:00
|
|
|
streamName, streamPassword, _ := strings.Cut(authorization, ":")
|
2023-02-23 16:55:06 +00:00
|
|
|
streamName, _ = strings.CutPrefix(strings.ToLower(streamName), "bearer ")
|
|
|
|
|
|
|
|
var qN string
|
|
|
|
var qP string
|
|
|
|
if err := db.QueryRow("SELECT * FROM streams WHERE stream = ? AND password = ?", streamName, streamPassword).Scan(&qN, &qP); err != nil {
|
|
|
|
logHTTPError(res, "Invalid stream authorization for: "+streamName+" - "+err.Error(), http.StatusUnauthorized)
|
2023-02-23 16:46:22 +00:00
|
|
|
return
|
2023-02-23 16:35:15 +00:00
|
|
|
}
|
|
|
|
|
2023-02-23 11:21:33 +00:00
|
|
|
offer, err := io.ReadAll(req.Body)
|
2023-02-23 11:19:57 +00:00
|
|
|
if err != nil {
|
|
|
|
logHTTPError(res, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-02-23 20:35:08 +00:00
|
|
|
peerConnection, err := api.NewPeerConnection(webrtc.Configuration{
|
|
|
|
ICEServers: []webrtc.ICEServer{
|
|
|
|
{
|
|
|
|
URLs: []string{"stun:stun.cloudflare.com:3478"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
URLs: []string{"stun:stun.l.google.com:19302"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
2023-02-23 11:19:57 +00:00
|
|
|
if err != nil {
|
|
|
|
logHTTPError(res, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
audioTrack, videoTrack, err := getTracksForStream(streamName)
|
|
|
|
if err != nil {
|
|
|
|
logHTTPError(res, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
peerConnection.OnTrack(func(track *webrtc.TrackRemote, _recv *webrtc.RTPReceiver) {
|
|
|
|
var localTrack *webrtc.TrackLocalStaticRTP
|
|
|
|
if strings.HasPrefix(track.Codec().RTPCodecCapability.MimeType, "audio/") {
|
|
|
|
localTrack = audioTrack
|
|
|
|
} else {
|
|
|
|
localTrack = videoTrack
|
|
|
|
}
|
|
|
|
|
|
|
|
rtpBuf := make([]byte, 1500)
|
|
|
|
for {
|
|
|
|
rtpRead, _, readErr := track.Read(rtpBuf)
|
|
|
|
switch {
|
|
|
|
case errors.Is(readErr, io.EOF):
|
|
|
|
return
|
|
|
|
case readErr != nil:
|
|
|
|
log.Println(readErr)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, writeErr := localTrack.Write(rtpBuf[:rtpRead]); writeErr != nil && !errors.Is(writeErr, io.ErrClosedPipe) {
|
|
|
|
log.Println(writeErr)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
peerConnection.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
|
|
|
if state == webrtc.ICEConnectionStateFailed {
|
|
|
|
if err := peerConnection.Close(); err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if err := peerConnection.SetRemoteDescription(webrtc.SessionDescription{
|
|
|
|
SDP: string(offer),
|
|
|
|
Type: webrtc.SDPTypeOffer,
|
|
|
|
}); err != nil {
|
|
|
|
logHTTPError(res, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
gatheringComplete := webrtc.GatheringCompletePromise(peerConnection)
|
|
|
|
|
|
|
|
answer, err := peerConnection.CreateAnswer(nil)
|
|
|
|
if err != nil {
|
|
|
|
logHTTPError(res, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
} else if err = peerConnection.SetLocalDescription(answer); err != nil {
|
|
|
|
logHTTPError(res, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
<-gatheringComplete
|
|
|
|
|
|
|
|
fmt.Fprint(res, peerConnection.LocalDescription().SDP)
|
|
|
|
}
|
|
|
|
|
|
|
|
func HandleWHEP(res http.ResponseWriter, req *http.Request) {
|
|
|
|
streamName := req.Header.Get("Authorization")
|
2023-02-23 16:57:07 +00:00
|
|
|
streamName, _ = strings.CutPrefix(strings.ToLower(streamName), "bearer ")
|
2023-02-23 11:19:57 +00:00
|
|
|
if streamName == "" {
|
|
|
|
logHTTPError(res, "Stream name was not set", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-02-23 11:21:33 +00:00
|
|
|
offer, err := io.ReadAll(req.Body)
|
2023-02-23 11:19:57 +00:00
|
|
|
if err != nil {
|
|
|
|
logHTTPError(res, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
peerConnection, err := api.NewPeerConnection(webrtc.Configuration{})
|
|
|
|
if err != nil {
|
|
|
|
logHTTPError(res, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
audioTrack, videoTrack, err := getTracksForStream(streamName)
|
|
|
|
if err != nil {
|
|
|
|
logHTTPError(res, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err = peerConnection.AddTrack(audioTrack); err != nil {
|
|
|
|
logHTTPError(res, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err = peerConnection.AddTrack(videoTrack); err != nil {
|
|
|
|
logHTTPError(res, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := peerConnection.SetRemoteDescription(webrtc.SessionDescription{
|
|
|
|
SDP: string(offer),
|
|
|
|
Type: webrtc.SDPTypeOffer,
|
|
|
|
}); err != nil {
|
|
|
|
logHTTPError(res, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
gatheringComplete := webrtc.GatheringCompletePromise(peerConnection)
|
|
|
|
|
|
|
|
answer, err := peerConnection.CreateAnswer(nil)
|
|
|
|
if err != nil {
|
|
|
|
logHTTPError(res, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
} else if err = peerConnection.SetLocalDescription(answer); err != nil {
|
|
|
|
logHTTPError(res, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
<-gatheringComplete
|
|
|
|
|
|
|
|
fmt.Fprint(res, peerConnection.LocalDescription().SDP)
|
|
|
|
}
|