install go2rtc on bob
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
const MimeSDP = "application/sdp"
|
||||
|
||||
var sessions = map[string]*webrtc.Conn{}
|
||||
|
||||
func syncHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
query := r.URL.Query()
|
||||
if query.Get("src") != "" {
|
||||
// WHEP or JSON SDP or raw SDP exchange
|
||||
outputWebRTC(w, r)
|
||||
} else if query.Get("dst") != "" {
|
||||
// WHIP SDP exchange
|
||||
inputWebRTC(w, r)
|
||||
} else {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
case "PATCH":
|
||||
// TODO: WHEP/WHIP
|
||||
http.Error(w, "", http.StatusMethodNotAllowed)
|
||||
|
||||
case "DELETE":
|
||||
if id := r.URL.Query().Get("id"); id != "" {
|
||||
if conn, ok := sessions[id]; ok {
|
||||
delete(sessions, id)
|
||||
_ = conn.Close()
|
||||
} else {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
case "OPTIONS":
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
|
||||
default:
|
||||
http.Error(w, "", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// outputWebRTC support API depending on Content-Type:
|
||||
// 1. application/json - receive {"type":"offer","sdp":"v=0\r\n..."} and response {"type":"answer","sdp":"v=0\r\n..."}
|
||||
// 2. application/sdp - receive/response SDP via WebRTC-HTTP Egress Protocol (WHEP)
|
||||
// 3. other - receive/response raw SDP
|
||||
func outputWebRTC(w http.ResponseWriter, r *http.Request) {
|
||||
u := r.URL.Query().Get("src")
|
||||
stream := streams.Get(u)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
mediaType := r.Header.Get("Content-Type")
|
||||
if mediaType != "" {
|
||||
mediaType, _, _ = strings.Cut(mediaType, ";")
|
||||
mediaType = strings.ToLower(strings.TrimSpace(mediaType))
|
||||
}
|
||||
|
||||
var offer string
|
||||
|
||||
switch mediaType {
|
||||
case "application/json":
|
||||
var desc pion.SessionDescription
|
||||
if err := json.NewDecoder(r.Body).Decode(&desc); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
offer = desc.SDP
|
||||
|
||||
case "application/x-www-form-urlencoded":
|
||||
if err := r.ParseForm(); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
offerB64 := r.Form.Get("data")
|
||||
b, err := base64.StdEncoding.DecodeString(offerB64)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
offer = string(b)
|
||||
|
||||
default:
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
offer = string(body)
|
||||
}
|
||||
|
||||
var desc string
|
||||
|
||||
switch mediaType {
|
||||
case "application/json":
|
||||
desc = "webrtc/json"
|
||||
case MimeSDP:
|
||||
desc = "webrtc/whep"
|
||||
default:
|
||||
desc = "webrtc/post"
|
||||
}
|
||||
|
||||
answer, err := ExchangeSDP(stream, offer, desc, r.UserAgent())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
switch mediaType {
|
||||
case "application/json":
|
||||
w.Header().Set("Content-Type", mediaType)
|
||||
|
||||
v := pion.SessionDescription{
|
||||
Type: pion.SDPTypeAnswer, SDP: answer,
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(v)
|
||||
|
||||
case "application/x-www-form-urlencoded":
|
||||
w.Header().Set("Content-Type", mediaType)
|
||||
answerB64 := base64.StdEncoding.EncodeToString([]byte(answer))
|
||||
_, err = w.Write([]byte(answerB64))
|
||||
|
||||
case MimeSDP:
|
||||
w.Header().Set("Content-Type", mediaType)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
|
||||
_, err = w.Write([]byte(answer))
|
||||
|
||||
default:
|
||||
w.Header().Set("Content-Type", mediaType)
|
||||
|
||||
_, err = w.Write([]byte(answer))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func inputWebRTC(w http.ResponseWriter, r *http.Request) {
|
||||
dst := r.URL.Query().Get("dst")
|
||||
stream := streams.Get(dst)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Get offer
|
||||
offer, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[webrtc] WHIP offer\n%s", offer)
|
||||
|
||||
pc, err := PeerConnection(false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// create new webrtc instance
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Mode = core.ModePassiveProducer
|
||||
prod.Protocol = "http"
|
||||
prod.UserAgent = r.UserAgent()
|
||||
|
||||
if err = prod.SetOffer(string(offer)); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
answer, err := prod.GetCompleteAnswer(GetCandidates(), FilterCandidate)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[webrtc] WHIP answer\n%s", answer)
|
||||
|
||||
id := strconv.FormatInt(time.Now().UnixNano(), 36)
|
||||
sessions[id] = prod
|
||||
|
||||
prod.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case pion.PeerConnectionState:
|
||||
if msg == pion.PeerConnectionStateClosed {
|
||||
stream.RemoveProducer(prod)
|
||||
delete(sessions, id)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
stream.AddProducer(prod)
|
||||
|
||||
w.Header().Set("Content-Type", MimeSDP)
|
||||
w.Header().Set("Location", "webrtc?id="+id)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
|
||||
if _, err = w.Write([]byte(answer)); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user