Create working prototype

This commit is contained in:
Torjus Håkestad 2023-11-30 23:56:33 +01:00
parent c8f076c2ab
commit 86e9024a07
6 changed files with 109 additions and 33 deletions

View File

@ -8,4 +8,6 @@ RUN go build -o ministream main.go
FROM alpine:latest FROM alpine:latest
COPY --from=builder /app/ministream /usr/bin/ministream COPY --from=builder /app/ministream /usr/bin/ministream
EXPOSE 8080
EXPOSE 50000-50050/udp
CMD ["/usr/bin/ministream", "serve"] CMD ["/usr/bin/ministream", "serve"]

17
Taskfile.yaml Normal file
View File

@ -0,0 +1,17 @@
version: '3'
tasks:
default: task -l
build:
desc: "Build image using podman"
cmd: podman build -t git.t-juice.club/torjus/ministream:latest .
push:
desc: "Push image to git.t-juice.club/torjus/ministream"
deps:
- build
cmds:
- cmd: podman push git.t-juice.club/torjus/ministream:latest

View File

@ -9,7 +9,7 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
const Version = "v0.1.0" const Version = "v0.1.1"
func main() { func main() {
app := cli.App{ app := cli.App{

View File

@ -35,7 +35,9 @@ func NewServer(store *UserStore) *Server {
r.Get("/{name}", srv.StaticHandler) r.Get("/{name}", srv.StaticHandler)
r.Post("/whip", http.HandlerFunc(srv.WhipHandler)) r.Post("/whip", http.HandlerFunc(srv.WhipHandler))
r.Get("/whip", srv.ListHandler) r.Get("/whip", srv.ListHandler)
r.Options("/whip", srv.OptionsHandler)
r.Delete("/whip/{streamKey}", srv.DeleteHandler) r.Delete("/whip/{streamKey}", srv.DeleteHandler)
r.Patch("/whip/{streamKey}", srv.PatchHandler)
r.Post("/whip/{streamKey}", srv.PostOfferHandler) r.Post("/whip/{streamKey}", srv.PostOfferHandler)
srv.Handler = r srv.Handler = r
@ -43,6 +45,14 @@ func NewServer(store *UserStore) *Server {
return srv return srv
} }
func (s *Server) OptionsHandler(w http.ResponseWriter, r *http.Request) {
slog.Info("Got OPTIONS")
}
func (s *Server) PatchHandler(w http.ResponseWriter, r *http.Request) {
slog.Info("Got PATCH!")
}
func (s *Server) StaticHandler(w http.ResponseWriter, r *http.Request) { func (s *Server) StaticHandler(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name") name := chi.URLParam(r, "name")
if name == "" { if name == "" {
@ -93,7 +103,7 @@ func (s *Server) PostOfferHandler(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, answer.SDP) io.WriteString(w, answer.SDP)
slog.Info("Got offer.", "stream", stream) slog.Info("Got listener for stream.", "stream_key", streamKey)
} }
func (s *Server) DeleteHandler(w http.ResponseWriter, r *http.Request) { func (s *Server) DeleteHandler(w http.ResponseWriter, r *http.Request) {
@ -140,6 +150,7 @@ func (s *Server) WhipHandler(w http.ResponseWriter, r *http.Request) {
} }
w.Header().Add("Location", fmt.Sprintf("/whip/%s", streamKey)) w.Header().Add("Location", fmt.Sprintf("/whip/%s", streamKey))
w.Header().Add("Link", "stun:stun.l.google.com:19302; rel=\"ice-server\";")
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
if _, err := io.WriteString(w, answer.SDP); err != nil { if _, err := io.WriteString(w, answer.SDP); err != nil {
slog.Error("Error writing response.", "error", err) slog.Error("Error writing response.", "error", err)

View File

@ -4,12 +4,13 @@ const startStream = function (streamKey) {
}) })
pc.oniceconnectionstatechange = e => console.log(e) pc.oniceconnectionstatechange = e => console.log(e)
pc.addTransceiver('video') pc.onicecandidate = e => {
pc.addTransceiver('audio') console.log("Adding ice candidate: " + e.candidate);
pc.createOffer().then(offer => { if (!e.candidate) {
console.log("Done adding candidates. Creating offer.");
fetch("/whip/tjuice", { fetch("/whip/tjuice", {
method: "POST", method: "POST",
body: offer.sdp body: pc.localDescription.sdp
}).then(resp => { }).then(resp => {
resp.text().then(text => { resp.text().then(text => {
var answer = { var answer = {
@ -17,15 +18,21 @@ const startStream = function (streamKey) {
sdp: text sdp: text
} }
try { try {
pc.setLocalDescription(offer).then( console.log("Setting remote description.");
pc.setRemoteDescription(answer) pc.setRemoteDescription(answer)
)
} catch (e) { } catch (e) {
console.log("Error setting remote description: " + e) console.log("Error setting remote description: " + e)
} }
}) })
}); });
}
}
pc.createOffer().then(offer => {
console.log("Setting local description.");
pc.setLocalDescription(offer)
}) })
pc.addTransceiver('video')
pc.addTransceiver('audio')
pc.ontrack = function (event) { pc.ontrack = function (event) {
console.log(event) console.log(event)
@ -33,7 +40,7 @@ const startStream = function (streamKey) {
var ms = new MediaStream() var ms = new MediaStream()
event.streams.forEach(s => { event.streams.forEach(s => {
const tracks = s.getTracks() const tracks = s.getTracks()
tracks.forEach( t => { tracks.forEach(t => {
ms.addTrack(t) ms.addTrack(t)
}) })
}) })
@ -60,7 +67,7 @@ displayStreams = function () {
}) })
} }
window.onload = function ( ev ) { window.onload = function (ev) {
console.log(ev) console.log(ev)
displayStreams() displayStreams()
} }

View File

@ -75,7 +75,22 @@ func (s *Stream) AddListener(sd *webrtc.SessionDescription) (*webrtc.SessionDesc
if err != nil { if err != nil {
return nil, err return nil, err
} }
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
gatherComplete := make(chan struct{})
peerConnection.OnICECandidate(func(i *webrtc.ICECandidate) {
if i == nil {
gatherComplete <- struct{}{}
return
}
slog.Info("Got ICE Candidate for listener.",
"addr", i.Address,
"port", i.Port,
"related_addr", i.RelatedAddress,
"related_port", i.RelatedPort)
if err := peerConnection.AddICECandidate(i.ToJSON()); err != nil {
slog.Info("Error adding ICE Candidate.", "error", err)
}
})
// Sets the LocalDescription, and starts our UDP listeners // Sets the LocalDescription, and starts our UDP listeners
err = peerConnection.SetLocalDescription(answer) err = peerConnection.SetLocalDescription(answer)
@ -126,8 +141,10 @@ func (s *StreamStore) Add(streamKey string, sd *webrtc.SessionDescription) (*web
}, },
} }
se := webrtc.SettingEngine{}
_ = se.SetEphemeralUDPPortRange(50000, 50050)
// Create a new RTCPeerConnection // Create a new RTCPeerConnection
peerConnection, err := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i)).NewPeerConnection(peerConnectionConfig) peerConnection, err := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i), webrtc.WithSettingEngine(se)).NewPeerConnection(peerConnectionConfig)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -146,7 +163,7 @@ func (s *StreamStore) Add(streamKey string, sd *webrtc.SessionDescription) (*web
// to connected peers // to connected peers
peerConnection.OnTrack(func(remoteTrack *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { peerConnection.OnTrack(func(remoteTrack *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
// Create a local track, all our SFU clients will be fed via this track // Create a local track, all our SFU clients will be fed via this track
slog.Info("Got track!", "type", remoteTrack.Codec().MimeType, "id", remoteTrack.ID()) slog.Info("Got track.", "stream_key", streamKey, "type", remoteTrack.Codec().MimeType, "id", remoteTrack.ID())
localTrack, newTrackErr := webrtc.NewTrackLocalStaticRTP(remoteTrack.Codec().RTPCodecCapability, "video", "pion") localTrack, newTrackErr := webrtc.NewTrackLocalStaticRTP(remoteTrack.Codec().RTPCodecCapability, "video", "pion")
if newTrackErr != nil { if newTrackErr != nil {
panic(newTrackErr) panic(newTrackErr)
@ -181,7 +198,6 @@ func (s *StreamStore) Add(streamKey string, sd *webrtc.SessionDescription) (*web
if err != nil { if err != nil {
panic(err) panic(err)
} }
// Create answer // Create answer
answer, err := peerConnection.CreateAnswer(nil) answer, err := peerConnection.CreateAnswer(nil)
if err != nil { if err != nil {
@ -189,7 +205,7 @@ func (s *StreamStore) Add(streamKey string, sd *webrtc.SessionDescription) (*web
} }
// Create channel that is blocked until ICE Gathering is complete // Create channel that is blocked until ICE Gathering is complete
gatherComplete := webrtc.GatheringCompletePromise(peerConnection) // gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
// Sets the LocalDescription, and starts our UDP listeners // Sets the LocalDescription, and starts our UDP listeners
err = peerConnection.SetLocalDescription(answer) err = peerConnection.SetLocalDescription(answer)
@ -200,14 +216,37 @@ func (s *StreamStore) Add(streamKey string, sd *webrtc.SessionDescription) (*web
// Block until ICE Gathering is complete, disabling trickle ICE // Block until ICE Gathering is complete, disabling trickle ICE
// we do this because we only can exchange one signaling message // we do this because we only can exchange one signaling message
// in a production application you should exchange ICE Candidates via OnICECandidate // in a production application you should exchange ICE Candidates via OnICECandidate
<-gatherComplete // answerChan <- &answer
slog.Info("ICE Gathering complete.", "answer", answer)
answerChan <- &answer
peerConnection.OnICEConnectionStateChange(func(is webrtc.ICEConnectionState) {
slog.Info("ICE state changed.", "stream_key", streamKey, "ice_state", is)
})
gatherComplete := make(chan struct{})
peerConnection.OnICECandidate(func(i *webrtc.ICECandidate) {
if i == nil {
gatherComplete <- struct{}{}
return
}
slog.Info("Got ICE Candidate",
"addr", i.Address,
"port", i.Port,
"related_addr", i.RelatedAddress,
"related_port", i.RelatedPort)
if err := peerConnection.AddICECandidate(i.ToJSON()); err != nil {
slog.Info("Error adding ICE Candidate.", "error", err)
}
})
<-gatherComplete
slog.Info("ICE Gathering complete.", "stream_key", streamKey, "ice_state", peerConnection.ICEConnectionState())
answerChan <- peerConnection.CurrentLocalDescription()
s.Streams[streamKey] = stream s.Streams[streamKey] = stream
slog.Info("Added stream.", "stream_key", streamKey) slog.Info("Added stream.", "stream_key", streamKey, "answer", answer)
}() }()
answer := <-answerChan answer := <-answerChan
return answer, nil return answer, nil
} }