mirror of
https://github.com/pion/webrtc.git
synced 2025-10-20 21:59:34 +08:00
Implement SFU example
`sfu` example shows how to build a simple broadcast only SFU Resolves #63
This commit is contained in:
@@ -9,6 +9,7 @@ We've build an extensive collection of examples covering common use-cases. Modif
|
|||||||
* [save-to-disk](save-to-disk/README.md): Save video from your Webcam to disk
|
* [save-to-disk](save-to-disk/README.md): Save video from your Webcam to disk
|
||||||
* [data-channels](data-channels/README.md): Use data channels to send text between Pion WebRTC and your browser
|
* [data-channels](data-channels/README.md): Use data channels to send text between Pion WebRTC and your browser
|
||||||
* [data-channels-create](data-channels/README.md): Similar to data channels but now Pion initiates the creation of the data channel.
|
* [data-channels-create](data-channels/README.md): Similar to data channels but now Pion initiates the creation of the data channel.
|
||||||
|
* [sfu](sfu/README.md): Broadcast a video to many peers, while only requiring the broadcaster to upload once
|
||||||
* [WIP] [pion-to-pion](pion-to-pion/README.md): An example of two Pion instances communicating directly.
|
* [WIP] [pion-to-pion](pion-to-pion/README.md): An example of two Pion instances communicating directly.
|
||||||
|
|
||||||
All examples can be executed on your local machine.
|
All examples can be executed on your local machine.
|
||||||
|
@@ -28,5 +28,11 @@
|
|||||||
"link": "save-to-disk",
|
"link": "save-to-disk",
|
||||||
"description": "save-to-disk is a simple application that shows how to record your webcam using pion-WebRTC and save to disk.",
|
"description": "save-to-disk is a simple application that shows how to record your webcam using pion-WebRTC and save to disk.",
|
||||||
"type": "browser"
|
"type": "browser"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "SFU",
|
||||||
|
"link": "sfu",
|
||||||
|
"description": "sfu is a pion-WebRTC application that demonstrates how to broadcast a video to many peers, while only requiring the broadcaster to upload once.",
|
||||||
|
"type": "browser"
|
||||||
}
|
}
|
||||||
]
|
]
|
27
examples/sfu/README.md
Normal file
27
examples/sfu/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# sfu
|
||||||
|
sfu is a pion-WebRTC application that demonstrates how to broadcast a video to many peers, while only requiring the broadcaster to upload once.
|
||||||
|
|
||||||
|
This could serve as the building block to building conferencing software, and other applications where publishers are bandwidth constrained.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
### Download sfu
|
||||||
|
```
|
||||||
|
go get github.com/pions/webrtc/examples/sfu
|
||||||
|
```
|
||||||
|
|
||||||
|
### Open sfu example page
|
||||||
|
[jsfiddle.net](https://jsfiddle.net/5cwx0rns/) You should see two buttons 'Publish a Broadcast' and 'Join a Broadcast'
|
||||||
|
|
||||||
|
### Run SFU
|
||||||
|
#### Linux/macOS
|
||||||
|
Run `sfu` OR run `main.go` in `github.com/pions/webrtc/examples/sfu`
|
||||||
|
|
||||||
|
### Start a publisher
|
||||||
|
Click `Publish a Broadcast` and paste the SDP into your terminal. The `sfu` application will respond with an offer, paste this into the second input field. Then press `Start Session`
|
||||||
|
|
||||||
|
### Join the broadcast
|
||||||
|
Click `Join a Broadcast` and paste the SDP into your terminal. The `sfu` application will respond with an offer, paste this into the second input field. Then press `Start Session`
|
||||||
|
|
||||||
|
You can `Join the broadcast` as many times as you want. The `sfu` Golang application is relaying all traffic, so your browser only has to upload once.
|
||||||
|
|
||||||
|
Congrats, you have used pion-WebRTC! Now start building something cool
|
1
examples/sfu/jsfiddle/demo.css
Normal file
1
examples/sfu/jsfiddle/demo.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
5
examples/sfu/jsfiddle/demo.details
Normal file
5
examples/sfu/jsfiddle/demo.details
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
name: gstreamer-receive
|
||||||
|
description: Example of using pion-WebRTC to play video using GStreamer
|
||||||
|
authors:
|
||||||
|
- Sean DuBois
|
13
examples/sfu/jsfiddle/demo.html
Normal file
13
examples/sfu/jsfiddle/demo.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<video id="video1" width="160" height="120" autoplay muted></video> <br />
|
||||||
|
|
||||||
|
|
||||||
|
<button class="createSessionButton" onclick="window.createSession(true)"> Publish a Broadcast </button>
|
||||||
|
<button class="createSessionButton" onclick="window.createSession(false)"> Join a Broadcast </button>
|
||||||
|
|
||||||
|
<div id="signalingContainer" style="display: none">
|
||||||
|
Browser base64 Session Description <textarea id="localSessionDescription" readonly="true"></textarea> <br />
|
||||||
|
Golang base64 Session Description: <textarea id="remoteSessionDescription"></textarea> <br/>
|
||||||
|
<button onclick="window.startSession()"> Start Session </button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="logs"></div>
|
62
examples/sfu/jsfiddle/demo.js
Normal file
62
examples/sfu/jsfiddle/demo.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/* eslint-env browser */
|
||||||
|
var log = msg => {
|
||||||
|
document.getElementById('logs').innerHTML += msg + '<br>'
|
||||||
|
}
|
||||||
|
|
||||||
|
window.createSession = isPublisher => {
|
||||||
|
let pc = new RTCPeerConnection({
|
||||||
|
iceServers: [
|
||||||
|
{
|
||||||
|
urls: 'stun:stun.l.google.com:19302'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
pc.oniceconnectionstatechange = e => log(pc.iceConnectionState)
|
||||||
|
pc.onicecandidate = event => {
|
||||||
|
if (event.candidate === null) {
|
||||||
|
document.getElementById('localSessionDescription').value = btoa(pc.localDescription.sdp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPublisher) {
|
||||||
|
navigator.mediaDevices.getUserMedia({video: true, audio: false})
|
||||||
|
.then(stream => pc.addStream(document.getElementById('video1').srcObject = stream))
|
||||||
|
.catch(log)
|
||||||
|
pc.onnegotiationneeded = e => {
|
||||||
|
pc.createOffer()
|
||||||
|
.then(d => pc.setLocalDescription(d))
|
||||||
|
.catch(log)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pc.createOffer({offerToReceiveVideo: true})
|
||||||
|
.then(d => pc.setLocalDescription(d))
|
||||||
|
.catch(log)
|
||||||
|
|
||||||
|
pc.ontrack = function (event) {
|
||||||
|
var el = document.getElementById('video1')
|
||||||
|
el.srcObject = event.streams[0]
|
||||||
|
el.autoplay = true
|
||||||
|
el.controls = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.startSession = () => {
|
||||||
|
let sd = document.getElementById('remoteSessionDescription').value
|
||||||
|
if (sd === '') {
|
||||||
|
return alert('Session Description must not be empty')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: atob(sd)}))
|
||||||
|
} catch (e) {
|
||||||
|
alert(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let btns = document.getElementsByClassName('createSessionButton')
|
||||||
|
for (let i = 0; i < btns.length; i++) {
|
||||||
|
btns[i].style = 'display: none'
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('signalingContainer').style = 'display: block'
|
||||||
|
}
|
142
examples/sfu/main.go
Normal file
142
examples/sfu/main.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bufio"
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"github.com/pions/webrtc"
|
||||||
|
"github.com/pions/webrtc/pkg/media"
|
||||||
|
"github.com/pions/webrtc/pkg/media/samplebuilder"
|
||||||
|
"github.com/pions/webrtc/pkg/rtcp"
|
||||||
|
"github.com/pions/webrtc/pkg/rtp/codecs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var peerConnectionConfig = webrtc.RTCConfiguration{
|
||||||
|
IceServers: []webrtc.RTCIceServer{
|
||||||
|
{
|
||||||
|
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func check(err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustReadStdin(reader *bufio.Reader) string {
|
||||||
|
rawSd, err := reader.ReadString('\n')
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
fmt.Println("")
|
||||||
|
sd, err := base64.StdEncoding.DecodeString(rawSd)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
return string(sd)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
rtcpPLIInterval = time.Second * 3
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
sd := mustReadStdin(reader)
|
||||||
|
fmt.Println("")
|
||||||
|
|
||||||
|
/* Everything below is the pion-WebRTC API, thanks for using it! */
|
||||||
|
|
||||||
|
// Only support VP8, this makes our proxying code simpler
|
||||||
|
webrtc.RegisterCodec(webrtc.NewRTCRtpVP8Codec(webrtc.DefaultPayloadTypeVP8, 90000))
|
||||||
|
|
||||||
|
// Create a new RTCPeerConnection
|
||||||
|
peerConnection, err := webrtc.New(peerConnectionConfig)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
outboundSamples := []chan<- media.RTCSample{}
|
||||||
|
var outboundSamplesLock sync.RWMutex
|
||||||
|
// Set a handler for when a new remote track starts, this just distributes all our packets
|
||||||
|
// to connected peers
|
||||||
|
peerConnection.OnTrack = func(track *webrtc.RTCTrack) {
|
||||||
|
// Send a PLI on an interval so that the publisher is pushing a keyframe every rtcpPLIInterval
|
||||||
|
// This is a temporary fix until we implement incoming RTCP events, then we would push a PLI only when a viewer requests it
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(rtcpPLIInterval)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
err := peerConnection.SendRTCP(&rtcp.PictureLossIndication{MediaSSRC: track.Ssrc})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Transform RTP packets into samples, allowing us to distribute incoming packets from the publisher to everyone who has joined the broadcast
|
||||||
|
builder := samplebuilder.New(256, &codecs.VP8Packet{})
|
||||||
|
for {
|
||||||
|
outboundSamplesLock.RLock()
|
||||||
|
builder.Push(<-track.Packets)
|
||||||
|
for s := builder.Pop(); s != nil; s = builder.Pop() {
|
||||||
|
for _, outChan := range outboundSamples {
|
||||||
|
outChan <- *s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outboundSamplesLock.RUnlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the remote SessionDescription
|
||||||
|
check(peerConnection.SetRemoteDescription(webrtc.RTCSessionDescription{
|
||||||
|
Type: webrtc.RTCSdpTypeOffer,
|
||||||
|
Sdp: string(sd),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Sets the LocalDescription, and starts our UDP listeners
|
||||||
|
answer, err := peerConnection.CreateAnswer(nil)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
// Get the LocalDescription and take it to base64 so we can paste in browser
|
||||||
|
fmt.Println(base64.StdEncoding.EncodeToString([]byte(answer.Sdp)))
|
||||||
|
|
||||||
|
for {
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("Paste an SDP to start sendonly peer connection")
|
||||||
|
recvOnlyOffer := mustReadStdin(reader)
|
||||||
|
|
||||||
|
// Create a new RTCPeerConnection
|
||||||
|
peerConnection, err := webrtc.New(peerConnectionConfig)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
// Create a single VP8 Track to send videa
|
||||||
|
vp8Track, err := peerConnection.NewRTCTrack(webrtc.DefaultPayloadTypeVP8, "video", "pion2")
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
_, err = peerConnection.AddTrack(vp8Track)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
outboundSamplesLock.Lock()
|
||||||
|
outboundSamples = append(outboundSamples, vp8Track.Samples)
|
||||||
|
outboundSamplesLock.Unlock()
|
||||||
|
|
||||||
|
// Set the remote SessionDescription
|
||||||
|
check(peerConnection.SetRemoteDescription(webrtc.RTCSessionDescription{
|
||||||
|
Type: webrtc.RTCSdpTypeOffer,
|
||||||
|
Sdp: string(recvOnlyOffer),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Sets the LocalDescription, and starts our UDP listeners
|
||||||
|
answer, err := peerConnection.CreateAnswer(nil)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
// Get the LocalDescription and take it to base64 so we can paste in browser
|
||||||
|
fmt.Println(base64.StdEncoding.EncodeToString([]byte(answer.Sdp)))
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user