From 8decbcb86f5cb30e9b66c50759e3f134258af970 Mon Sep 17 00:00:00 2001 From: Srayan Jana Date: Sun, 27 Apr 2025 22:01:49 -0700 Subject: [PATCH 1/2] start! --- examples/whip-whep-data-channels/README.md | 43 +++++ examples/whip-whep-data-channels/index.html | 86 +++++++++ examples/whip-whep-data-channels/main.go | 204 ++++++++++++++++++++ 3 files changed, 333 insertions(+) create mode 100644 examples/whip-whep-data-channels/README.md create mode 100644 examples/whip-whep-data-channels/index.html create mode 100644 examples/whip-whep-data-channels/main.go diff --git a/examples/whip-whep-data-channels/README.md b/examples/whip-whep-data-channels/README.md new file mode 100644 index 00000000000..6014b3469bd --- /dev/null +++ b/examples/whip-whep-data-channels/README.md @@ -0,0 +1,43 @@ +# whip-whep +whip-whep demonstrates using WHIP and WHEP with Pion. Since WHIP+WHEP is standardized signaling you can publish via tools like OBS and GStreamer. +You can then watch it in sub-second time from your browser, or pull the video back into OBS and GStreamer via WHEP. + +Further details about the why and how of WHIP+WHEP are below the instructions. + +## Instructions + +### Download whip-whep + +This example requires you to clone the repo since it is serving static HTML. + +``` +git clone https://github.com/pion/webrtc.git +cd webrtc/examples/whip-whep +``` + +### Run whip-whep +Execute `go run *.go` + +### Publish + +You can publish via an tool that supports WHIP or via your browser. To publish via your browser open [http://localhost:8080](http://localhost:8080), and press publish. + +To publish via OBS set `Service` to `WHIP` and `Server` to `http://localhost:8080/whip`. The `Bearer Token` can be whatever value you like. + + +### Subscribe + +Once you have started publishing open [http://localhost:8080](http://localhost:8080) and press the subscribe button. You can now view your video you published via +OBS or your browser. + +Congrats, you have used Pion WebRTC! Now start building something cool + +## Why WHIP/WHEP? + +WHIP/WHEP mandates that a Offer is uploaded via HTTP. The server responds with a Answer. With this strong API contract WebRTC support can be added to tools like OBS. + +For more info on WHIP/WHEP specification, feel free to read some of these great resources: +- https://webrtchacks.com/webrtc-cracks-the-whip-on-obs/ +- https://datatracker.ietf.org/doc/draft-ietf-wish-whip/ +- https://datatracker.ietf.org/doc/draft-ietf-wish-whep/ +- https://bloggeek.me/whip-whep-webrtc-live-streaming diff --git a/examples/whip-whep-data-channels/index.html b/examples/whip-whep-data-channels/index.html new file mode 100644 index 00000000000..8944ad8caee --- /dev/null +++ b/examples/whip-whep-data-channels/index.html @@ -0,0 +1,86 @@ + + + + + whip-whep + + + + + +

Video

+ + + +

ICE Connection States

+

+ + + + diff --git a/examples/whip-whep-data-channels/main.go b/examples/whip-whep-data-channels/main.go new file mode 100644 index 00000000000..fc65060d32f --- /dev/null +++ b/examples/whip-whep-data-channels/main.go @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +//go:build !js +// +build !js + +// whip-whep demonstrates how to use the WHIP/WHEP specifications to exchange SPD descriptions +// and stream media to a WebRTC client in the browser or OBS. +package main + +import ( + "fmt" + "io" + "net/http" + + "github.com/pion/interceptor" + "github.com/pion/interceptor/pkg/intervalpli" + "github.com/pion/webrtc/v4" +) + +// nolint: gochecknoglobals +var ( + videoTrack *webrtc.TrackLocalStaticRTP + + peerConnectionConfiguration = webrtc.Configuration{ + ICEServers: []webrtc.ICEServer{ + { + URLs: []string{"stun:stun.l.google.com:19302"}, + }, + }, + } +) + +// nolint:gocognit +func main() { + // Everything below is the Pion WebRTC API! Thanks for using it ❤️. + var err error + if videoTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypeH264, + }, "video", "pion"); err != nil { + panic(err) + } + + http.Handle("/", http.FileServer(http.Dir("."))) + http.HandleFunc("/whep", whepHandler) + http.HandleFunc("/whip", whipHandler) + + fmt.Println("Open http://localhost:8080 to access this demo") + panic(http.ListenAndServe(":8080", nil)) // nolint: gosec +} + +func whipHandler(res http.ResponseWriter, req *http.Request) { + // Read the offer from HTTP Request + offer, err := io.ReadAll(req.Body) + if err != nil { + panic(err) + } + + // Create a MediaEngine object to configure the supported codec + mediaEngine := &webrtc.MediaEngine{} + + // Setup the codecs you want to use. + // We'll only use H264 but you can also define your own + if err = mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypeH264, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil, + }, + PayloadType: 96, + }, webrtc.RTPCodecTypeVideo); err != nil { + panic(err) + } + + // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. + // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` + // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry + // for each PeerConnection. + interceptorRegistry := &interceptor.Registry{} + + // Register a intervalpli factory + // This interceptor sends a PLI every 3 seconds. A PLI causes a video keyframe to be generated by the sender. + // This makes our video seekable and more error resilent, but at a cost of lower picture quality and higher bitrates + // A real world application should process incoming RTCP packets from viewers and forward them to senders + intervalPliFactory, err := intervalpli.NewReceiverInterceptor() + if err != nil { + panic(err) + } + interceptorRegistry.Add(intervalPliFactory) + + // Use the default set of Interceptors + if err = webrtc.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry); err != nil { + panic(err) + } + + // Create the API object with the MediaEngine + api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine), webrtc.WithInterceptorRegistry(interceptorRegistry)) + + // Prepare the configuration + + // Create a new RTCPeerConnection + peerConnection, err := api.NewPeerConnection(peerConnectionConfiguration) + if err != nil { + panic(err) + } + + // Allow us to receive 1 video trac + if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo); err != nil { + panic(err) + } + + // Set a handler for when a new remote track starts, this handler saves buffers to disk as + // an ivf file, since we could have multiple video tracks we provide a counter. + // In your application this is where you would handle/process video + peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { //nolint: revive + for { + pkt, _, err := track.ReadRTP() + if err != nil { + panic(err) + } + + if err = videoTrack.WriteRTP(pkt); err != nil { + panic(err) + } + } + }) + + // Send answer via HTTP Response + writeAnswer(res, peerConnection, offer, "/whip") +} + +func whepHandler(res http.ResponseWriter, req *http.Request) { + // Read the offer from HTTP Request + offer, err := io.ReadAll(req.Body) + if err != nil { + panic(err) + } + + // Create a new RTCPeerConnection + peerConnection, err := webrtc.NewPeerConnection(peerConnectionConfiguration) + if err != nil { + panic(err) + } + + // Add Video Track that is being written to from WHIP Session + rtpSender, err := peerConnection.AddTrack(videoTrack) + if err != nil { + panic(err) + } + + // Read incoming RTCP packets + // Before these packets are returned they are processed by interceptors. For things + // like NACK this needs to be called. + go func() { + rtcpBuf := make([]byte, 1500) + for { + if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { + return + } + } + }() + + // Send answer via HTTP Response + writeAnswer(res, peerConnection, offer, "/whep") +} + +func writeAnswer(res http.ResponseWriter, peerConnection *webrtc.PeerConnection, offer []byte, path string) { + // 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()) + + if connectionState == webrtc.ICEConnectionStateFailed { + _ = peerConnection.Close() + } + }) + + if err := peerConnection.SetRemoteDescription(webrtc.SessionDescription{ + Type: webrtc.SDPTypeOffer, SDP: string(offer), + }); err != nil { + panic(err) + } + + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + + // Create answer + answer, err := peerConnection.CreateAnswer(nil) + if err != nil { + panic(err) + } else if err = peerConnection.SetLocalDescription(answer); err != nil { + panic(err) + } + + // Block until ICE Gathering is complete, disabling trickle ICE + // we do this because we only can exchange one signaling message + // in a production application you should exchange ICE Candidates via OnICECandidate + <-gatherComplete + + // WHIP+WHEP expects a Location header and a HTTP Status Code of 201 + res.Header().Add("Location", path) + res.WriteHeader(http.StatusCreated) + + // Write Answer with Candidates as HTTP Response + fmt.Fprint(res, peerConnection.LocalDescription().SDP) //nolint: errcheck +} From dae2163ee96aef55a83b30bf33aec3cb3cf2bb9d Mon Sep 17 00:00:00 2001 From: Srayan Jana Date: Sun, 27 Apr 2025 22:19:33 -0700 Subject: [PATCH 2/2] slowly hacking my way into this --- examples/whip-whep-data-channels/index.html | 37 ++++++--- examples/whip-whep-data-channels/main.go | 92 +-------------------- 2 files changed, 25 insertions(+), 104 deletions(-) diff --git a/examples/whip-whep-data-channels/index.html b/examples/whip-whep-data-channels/index.html index 8944ad8caee..148de5bd9b2 100644 --- a/examples/whip-whep-data-channels/index.html +++ b/examples/whip-whep-data-channels/index.html @@ -11,8 +11,12 @@ -

Video

- +
+Message
+
+
+

Logs

+

ICE Connection States

@@ -20,8 +24,17 @@

ICE Connection States

diff --git a/examples/whip-whep-data-channels/main.go b/examples/whip-whep-data-channels/main.go index fc65060d32f..c33f11a5513 100644 --- a/examples/whip-whep-data-channels/main.go +++ b/examples/whip-whep-data-channels/main.go @@ -13,15 +13,11 @@ import ( "io" "net/http" - "github.com/pion/interceptor" - "github.com/pion/interceptor/pkg/intervalpli" "github.com/pion/webrtc/v4" ) // nolint: gochecknoglobals var ( - videoTrack *webrtc.TrackLocalStaticRTP - peerConnectionConfiguration = webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { @@ -34,13 +30,6 @@ var ( // nolint:gocognit func main() { // Everything below is the Pion WebRTC API! Thanks for using it ❤️. - var err error - if videoTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypeH264, - }, "video", "pion"); err != nil { - panic(err) - } - http.Handle("/", http.FileServer(http.Dir("."))) http.HandleFunc("/whep", whepHandler) http.HandleFunc("/whip", whipHandler) @@ -56,73 +45,12 @@ func whipHandler(res http.ResponseWriter, req *http.Request) { panic(err) } - // Create a MediaEngine object to configure the supported codec - mediaEngine := &webrtc.MediaEngine{} - - // Setup the codecs you want to use. - // We'll only use H264 but you can also define your own - if err = mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{ - RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypeH264, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil, - }, - PayloadType: 96, - }, webrtc.RTPCodecTypeVideo); err != nil { - panic(err) - } - - // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. - // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` - // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry - // for each PeerConnection. - interceptorRegistry := &interceptor.Registry{} - - // Register a intervalpli factory - // This interceptor sends a PLI every 3 seconds. A PLI causes a video keyframe to be generated by the sender. - // This makes our video seekable and more error resilent, but at a cost of lower picture quality and higher bitrates - // A real world application should process incoming RTCP packets from viewers and forward them to senders - intervalPliFactory, err := intervalpli.NewReceiverInterceptor() - if err != nil { - panic(err) - } - interceptorRegistry.Add(intervalPliFactory) - - // Use the default set of Interceptors - if err = webrtc.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry); err != nil { - panic(err) - } - - // Create the API object with the MediaEngine - api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine), webrtc.WithInterceptorRegistry(interceptorRegistry)) - - // Prepare the configuration - // Create a new RTCPeerConnection - peerConnection, err := api.NewPeerConnection(peerConnectionConfiguration) + peerConnection, err := webrtc.NewPeerConnection(peerConnectionConfiguration) if err != nil { panic(err) } - // Allow us to receive 1 video trac - if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo); err != nil { - panic(err) - } - - // Set a handler for when a new remote track starts, this handler saves buffers to disk as - // an ivf file, since we could have multiple video tracks we provide a counter. - // In your application this is where you would handle/process video - peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { //nolint: revive - for { - pkt, _, err := track.ReadRTP() - if err != nil { - panic(err) - } - - if err = videoTrack.WriteRTP(pkt); err != nil { - panic(err) - } - } - }) - // Send answer via HTTP Response writeAnswer(res, peerConnection, offer, "/whip") } @@ -140,24 +68,6 @@ func whepHandler(res http.ResponseWriter, req *http.Request) { panic(err) } - // Add Video Track that is being written to from WHIP Session - rtpSender, err := peerConnection.AddTrack(videoTrack) - if err != nil { - panic(err) - } - - // Read incoming RTCP packets - // Before these packets are returned they are processed by interceptors. For things - // like NACK this needs to be called. - go func() { - rtcpBuf := make([]byte, 1500) - for { - if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { - return - } - } - }() - // Send answer via HTTP Response writeAnswer(res, peerConnection, offer, "/whep") }