-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create WHIP/WHEP example works with OBS or browser Resolves #2499
- Loading branch information
1 parent
836184c
commit 923541c
Showing
3 changed files
with
326 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |