Skip to content

whip/whep example #2708

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions examples/whip-whep/README.md
Original file line number Diff line number Diff line change
@@ -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
86 changes: 86 additions & 0 deletions examples/whip-whep/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<html>

<!--
SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
SPDX-License-Identifier: MIT
-->
<head>
<title>whip-whep</title>
</head>

<body>
<button onclick="window.doWHIP()">Publish</button>
<button onclick="window.doWHEP()">Subscribe</button>
<h3> Video </h3>
<video id="videoPlayer" autoplay muted controls style="width: 500"> </video>


<h3> ICE Connection States </h3>
<div id="iceConnectionStates"></div> <br />
</body>

<script>
let peerConnection = new RTCPeerConnection()

peerConnection.oniceconnectionstatechange = () => {
let el = document.createElement('p')
el.appendChild(document.createTextNode(peerConnection.iceConnectionState))

document.getElementById('iceConnectionStates').appendChild(el);
}

window.doWHEP = () => {
peerConnection.addTransceiver('video', { direction: 'recvonly' })

peerConnection.ontrack = function (event) {
document.getElementById('videoPlayer').srcObject = event.streams[0]
}

peerConnection.createOffer().then(offer => {
peerConnection.setLocalDescription(offer)

fetch(`/whep`, {
method: 'POST',
body: offer.sdp,
headers: {
Authorization: `Bearer none`,
'Content-Type': 'application/sdp'
}
}).then(r => r.text())
.then(answer => {
peerConnection.setRemoteDescription({
sdp: answer,
type: 'answer'
})
})
})
}

window.doWHIP = () => {
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
.then(stream => {
document.getElementById('videoPlayer').srcObject = stream
stream.getTracks().forEach(track => peerConnection.addTrack(track, stream))

peerConnection.createOffer().then(offer => {
peerConnection.setLocalDescription(offer)

fetch(`/whip`, {
method: 'POST',
body: offer.sdp,
headers: {
Authorization: `Bearer none`,
'Content-Type': 'application/sdp'
}
}).then(r => r.text())
.then(answer => {
peerConnection.setRemoteDescription({
sdp: answer,
type: 'answer'
})
})
})
})
}
</script>
</html>
197 changes: 197 additions & 0 deletions examples/whip-whep/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

//go:build !js
// +build !js

// whip-whep demonstrates how to use the WHIP/WHEP specifications to exhange 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(w http.ResponseWriter, r *http.Request) {
// Read the offer from HTTP Request
offer, err := io.ReadAll(r.Body)
if err != nil {
panic(err)
}

// Create a MediaEngine object to configure the supported codec
m := &webrtc.MediaEngine{}

// Setup the codecs you want to use.
// We'll only use H264 but you can also define your own
if err = m.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.
i := &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)
}
i.Add(intervalPliFactory)

// Use the default set of Interceptors
if err = webrtc.RegisterDefaultInterceptors(m, i); err != nil {
panic(err)
}

// Create the API object with the MediaEngine
api := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i))

// 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(w, peerConnection, offer, "/whip")
}

func whepHandler(w http.ResponseWriter, r *http.Request) {
// Read the offer from HTTP Request
offer, err := io.ReadAll(r.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(w, peerConnection, offer, "/whep")
}

func writeAnswer(w 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
w.Header().Add("Location", path)
w.WriteHeader(http.StatusCreated)

// Write Answer with Candidates as HTTP Response
fmt.Fprint(w, peerConnection.LocalDescription().SDP)
}
Loading