mirror of
https://github.com/pion/webrtc.git
synced 2025-10-19 05:24:50 +08:00
Add sfu example with websocket
sfu example with websocket, support video and audio. easy to use Resolves #507
This commit is contained in:
@@ -92,6 +92,7 @@ Check out the **[contributing wiki](https://github.com/pions/webrtc/wiki/Contrib
|
|||||||
* [frank](https://github.com/feixiao) - *Building examples on OSX*
|
* [frank](https://github.com/feixiao) - *Building examples on OSX*
|
||||||
* [mxmCherry](https://github.com/mxmCherry)
|
* [mxmCherry](https://github.com/mxmCherry)
|
||||||
* [Alex Browne](https://github.com/albrow) - *JavaScript/WASM bindings*
|
* [Alex Browne](https://github.com/albrow) - *JavaScript/WASM bindings*
|
||||||
|
* [adwpc](https://github.com/adwpc) - *SFU example with websocket*
|
||||||
|
|
||||||
### License
|
### License
|
||||||
MIT License - see [LICENSE](LICENSE) for full text
|
MIT License - see [LICENSE](LICENSE) for full text
|
||||||
|
27
examples/sfu-ws/README.md
Normal file
27
examples/sfu-ws/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# sfu-ws
|
||||||
|
sfu-ws 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-ws
|
||||||
|
```
|
||||||
|
go get github.com/pions/webrtc/examples/sfu-ws
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run SFU
|
||||||
|
#### Linux/macOS
|
||||||
|
go build
|
||||||
|
./sfu-ws
|
||||||
|
|
||||||
|
### Start a publisher
|
||||||
|
|
||||||
|
* Click `Publish`
|
||||||
|
|
||||||
|
### Start a Subscriber
|
||||||
|
* Click `Subscribe`
|
||||||
|
|
||||||
|
|
||||||
|
You can start one publisher and many subscriber
|
||||||
|
|
||||||
|
Congrats, you have used pion-WebRTC! Now start building something cool
|
8
examples/sfu-ws/go.mod
Normal file
8
examples/sfu-ws/go.mod
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
module github.com/pions/webrtc/examples/sfu-ws
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/websocket v1.4.0
|
||||||
|
github.com/pions/rtcp v1.1.0
|
||||||
|
github.com/pions/transport v0.4.0 // indirect
|
||||||
|
github.com/pions/webrtc v1.2.1-0.20190311062446-43e0a64ba287
|
||||||
|
)
|
50
examples/sfu-ws/go.sum
Normal file
50
examples/sfu-ws/go.sum
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||||
|
github.com/cloudflare/sidh v0.0.0-20181111220428-fc8e6378752b/go.mod h1:o/DcCuWFr9jFzwO+c3y1hhwqKHHKfJ7HvLhWUwRnqfo=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pions/datachannel v1.2.0/go.mod h1:MKPEKJRwX/a9/tyQvcVTUI9szyf8ZuUyZxSA9AVMSro=
|
||||||
|
github.com/pions/dtls v1.2.1/go.mod h1:OgJcO0SqrDdQzqkCTdAp4xCQlbCmwZtGyhbthbq9zIA=
|
||||||
|
github.com/pions/qtls-vendor-extracted v0.0.0-20190210024908-018998217c65/go.mod h1:tSUehzG/8OAT3JvWvnovveLfRMM8NvgfN1LzwSrBX5s=
|
||||||
|
github.com/pions/quic v0.0.1/go.mod h1:q62rRbOZG6Keu45rWWljWZHXmB3H7fKdeJ1KtNcDrNQ=
|
||||||
|
github.com/pions/quic-go v0.7.1-0.20190211221741-ec20a8498576/go.mod h1:YvOsXPS6wXEfRGJobrsWSOBmlN6dkEIg+cUpnSDLkhc=
|
||||||
|
github.com/pions/rtcp v1.1.0 h1:WJarRpNaGE7aScgTXARaNIObMhIbftcbiPuhhqvDtl4=
|
||||||
|
github.com/pions/rtcp v1.1.0/go.mod h1:Q5twXlqiz775Yn37X0cl4lAsfSk8EiHgeNkte59jBY4=
|
||||||
|
github.com/pions/rtp v1.0.1/go.mod h1:GDIt4UYlSz7za4vfaLqihGJJ+yLvgPshnqrF/lm3vcM=
|
||||||
|
github.com/pions/sctp v1.3.0/go.mod h1:GZTG/xApE7wdUFEQq2Rmzgxl/+YaB/L1k8xUl1D5bmo=
|
||||||
|
github.com/pions/sctp v1.4.1/go.mod h1:dAna+Ct/aIIFiGW45yhGzuQjULWD7ni1vjoKHa9DsyU=
|
||||||
|
github.com/pions/sdp/v2 v2.1.0/go.mod h1:KGRBcHfpkgJXjrzKJz2wj/Jf1KWnsHdoIiqtayQ5QmE=
|
||||||
|
github.com/pions/srtp v1.1.0/go.mod h1:GvEoPJmI8sLqyjdPPdTZaCKNQ9e/+9OCCpck01s9uNQ=
|
||||||
|
github.com/pions/stun v0.2.0/go.mod h1:rMdCIsqqnTLC4MOHJE3LNiFQRfIjUDzI1kzx//7oPOM=
|
||||||
|
github.com/pions/transport v0.0.0-20190110151433-e7cbf7d5f464/go.mod h1:HLhzI7I0k8TyiQ99hfRZNRf84lG76eaFnZHnVy/wFnM=
|
||||||
|
github.com/pions/transport v0.1.0/go.mod h1:HLhzI7I0k8TyiQ99hfRZNRf84lG76eaFnZHnVy/wFnM=
|
||||||
|
github.com/pions/transport v0.2.0/go.mod h1:HLhzI7I0k8TyiQ99hfRZNRf84lG76eaFnZHnVy/wFnM=
|
||||||
|
github.com/pions/transport v0.4.0 h1:1N6fluzmj5W/16eFLDsCB18s/xEkjVTek0K4IJz75FU=
|
||||||
|
github.com/pions/transport v0.4.0/go.mod h1:9gvUd8ZeyU4ZX7dhNuUq97mPoekopkd7eCJEyhKwVO0=
|
||||||
|
github.com/pions/webrtc v1.2.1-0.20190311062446-43e0a64ba287 h1:skL8VgjrCGaoMCL+o2cumZ1uyLdRtK12TuLLM5PT30o=
|
||||||
|
github.com/pions/webrtc v1.2.1-0.20190311062446-43e0a64ba287/go.mod h1:H4lYxyochp11YxW5AGmNam36neRoNDfPcqhSFUItaes=
|
||||||
|
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
47
examples/sfu-ws/main.go
Normal file
47
examples/sfu-ws/main.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/pions/webrtc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkError(err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
|
||||||
|
// Generate pem file for https
|
||||||
|
genPem()
|
||||||
|
|
||||||
|
// Create a MediaEngine object to configure the supported codec
|
||||||
|
m = webrtc.MediaEngine{}
|
||||||
|
|
||||||
|
// Setup the codecs you want to use.
|
||||||
|
m.RegisterCodec(webrtc.NewRTPVP8Codec(webrtc.DefaultPayloadTypeVP8, 90000))
|
||||||
|
m.RegisterCodec(webrtc.NewRTPOpusCodec(webrtc.DefaultPayloadTypeOpus, 48000))
|
||||||
|
|
||||||
|
// Create the API object with the MediaEngine
|
||||||
|
api = webrtc.NewAPI(webrtc.WithMediaEngine(m))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
port := flag.String("p", "8443", "https port")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Websocket handle func
|
||||||
|
http.HandleFunc("/ws", room)
|
||||||
|
|
||||||
|
// Html handle func
|
||||||
|
http.HandleFunc("/", web)
|
||||||
|
|
||||||
|
// Support https, so we can test by lan
|
||||||
|
fmt.Println("Web listening :" + *port)
|
||||||
|
panic(http.ListenAndServeTLS(":"+*port, "cert.pem", "key.pem", nil))
|
||||||
|
}
|
180
examples/sfu-ws/room.go
Normal file
180
examples/sfu-ws/room.go
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/pions/rtcp"
|
||||||
|
"github.com/pions/webrtc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Peer config
|
||||||
|
var peerConnectionConfig = webrtc.Configuration{
|
||||||
|
ICEServers: []webrtc.ICEServer{
|
||||||
|
{
|
||||||
|
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Media engine
|
||||||
|
m webrtc.MediaEngine
|
||||||
|
|
||||||
|
// API object
|
||||||
|
api *webrtc.API
|
||||||
|
|
||||||
|
// Publisher Peer
|
||||||
|
pubCount int32
|
||||||
|
pubReceiver *webrtc.PeerConnection
|
||||||
|
|
||||||
|
// Local track
|
||||||
|
videoTrack *webrtc.Track
|
||||||
|
audioTrack *webrtc.Track
|
||||||
|
videoTrackLock *sync.RWMutex = new(sync.RWMutex)
|
||||||
|
audioTrackLock *sync.RWMutex = new(sync.RWMutex)
|
||||||
|
|
||||||
|
// Websocket upgrader
|
||||||
|
upgrader = websocket.Upgrader{}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rtcpPLIInterval = time.Second * 3
|
||||||
|
)
|
||||||
|
|
||||||
|
func room(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
// Websocket client
|
||||||
|
c, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
checkError(err)
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
// Read sdp from websocket
|
||||||
|
mt, msg, err := c.ReadMessage()
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
if atomic.LoadInt32(&pubCount) == 0 {
|
||||||
|
atomic.AddInt32(&pubCount, 1)
|
||||||
|
|
||||||
|
// Create a new RTCPeerConnection
|
||||||
|
pubReceiver, err = api.NewPeerConnection(peerConnectionConfig)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
pubReceiver.OnTrack(func(remoteTrack *webrtc.Track, receiver *webrtc.RTPReceiver) {
|
||||||
|
if remoteTrack.PayloadType() == webrtc.DefaultPayloadTypeVP8 || remoteTrack.PayloadType() == webrtc.DefaultPayloadTypeVP9 || remoteTrack.PayloadType() == webrtc.DefaultPayloadTypeH264 {
|
||||||
|
|
||||||
|
// Create a local video track, all our SFU clients will be fed via this track
|
||||||
|
var err error
|
||||||
|
videoTrackLock.Lock()
|
||||||
|
videoTrack, err = pubReceiver.NewTrack(remoteTrack.PayloadType(), remoteTrack.SSRC(), "video", "pion")
|
||||||
|
videoTrackLock.Unlock()
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Send a PLI on an interval so that the publisher is pushing a keyframe every rtcpPLIInterval
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(rtcpPLIInterval)
|
||||||
|
for range ticker.C {
|
||||||
|
checkError(pubReceiver.SendRTCP(&rtcp.PictureLossIndication{MediaSSRC: videoTrack.SSRC()}))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
rtpBuf := make([]byte, 1400)
|
||||||
|
for {
|
||||||
|
i, err := remoteTrack.Read(rtpBuf)
|
||||||
|
checkError(err)
|
||||||
|
videoTrackLock.RLock()
|
||||||
|
_, err = videoTrack.Write(rtpBuf[:i])
|
||||||
|
videoTrackLock.RUnlock()
|
||||||
|
checkError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Create a local audio track, all our SFU clients will be fed via this track
|
||||||
|
var err error
|
||||||
|
audioTrackLock.Lock()
|
||||||
|
audioTrack, err = pubReceiver.NewTrack(remoteTrack.PayloadType(), remoteTrack.SSRC(), "audio", "pion")
|
||||||
|
audioTrackLock.Unlock()
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
rtpBuf := make([]byte, 1400)
|
||||||
|
for {
|
||||||
|
i, err := remoteTrack.Read(rtpBuf)
|
||||||
|
checkError(err)
|
||||||
|
audioTrackLock.RLock()
|
||||||
|
_, err = audioTrack.Write(rtpBuf[:i])
|
||||||
|
audioTrackLock.RUnlock()
|
||||||
|
checkError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set the remote SessionDescription
|
||||||
|
checkError(pubReceiver.SetRemoteDescription(
|
||||||
|
webrtc.SessionDescription{
|
||||||
|
SDP: string(msg),
|
||||||
|
Type: webrtc.SDPTypeOffer,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Create answer
|
||||||
|
answer, err := pubReceiver.CreateAnswer(nil)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Sets the LocalDescription, and starts our UDP listeners
|
||||||
|
checkError(pubReceiver.SetLocalDescription(answer))
|
||||||
|
|
||||||
|
// Send server sdp to publisher
|
||||||
|
checkError(c.WriteMessage(mt, []byte(answer.SDP)))
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Create a new PeerConnection
|
||||||
|
subSender, err := api.NewPeerConnection(peerConnectionConfig)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Waiting for publisher track finish
|
||||||
|
for {
|
||||||
|
videoTrackLock.RLock()
|
||||||
|
if videoTrack == nil {
|
||||||
|
videoTrackLock.RUnlock()
|
||||||
|
//if videoTrack == nil, waiting..
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
} else {
|
||||||
|
videoTrackLock.RUnlock()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add local video track
|
||||||
|
videoTrackLock.RLock()
|
||||||
|
_, err = subSender.AddTrack(videoTrack)
|
||||||
|
videoTrackLock.RUnlock()
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Add local audio track
|
||||||
|
audioTrackLock.RLock()
|
||||||
|
_, err = subSender.AddTrack(audioTrack)
|
||||||
|
audioTrackLock.RUnlock()
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Set the remote SessionDescription
|
||||||
|
checkError(subSender.SetRemoteDescription(
|
||||||
|
webrtc.SessionDescription{
|
||||||
|
SDP: string(msg),
|
||||||
|
Type: webrtc.SDPTypeOffer,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Create answer
|
||||||
|
answer, err := subSender.CreateAnswer(nil)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Sets the LocalDescription, and starts our UDP listeners
|
||||||
|
checkError(subSender.SetLocalDescription(answer))
|
||||||
|
|
||||||
|
// Send server sdp to subscriber
|
||||||
|
checkError(c.WriteMessage(mt, []byte(answer.SDP)))
|
||||||
|
}
|
||||||
|
}
|
125
examples/sfu-ws/sfu.html
Normal file
125
examples/sfu-ws/sfu.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>sfu</title>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
.comments {
|
||||||
|
width:100%;/*auto width*/
|
||||||
|
overflow:auto;
|
||||||
|
word-break:break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<video id="video1" width="320" height="240" autoplay muted controls></video> <br />
|
||||||
|
|
||||||
|
|
||||||
|
<button class="sessbtn" onclick="window.createSession(true)">Publish</button>
|
||||||
|
<button class="sessbtn" onclick="window.createSession(false)">Subscribe</button>
|
||||||
|
|
||||||
|
<div id="signalingContainer" style="display: none">
|
||||||
|
Client SDP<textarea class="comments" id="localSDP" readonly="true" rows=10 cols=30 onpropertychange= "this.style.posHeight=this.scrollHeight "></textarea>
|
||||||
|
Server SDP<textarea class="comments" id="remoteSDP" readonly="true" rows=10 cols=30 onpropertychange= "this.style.posHeight=this.scrollHeight "></textarea>
|
||||||
|
<!-- <button onclick="window.startSession()"> Start Session </button> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="logs"></div>
|
||||||
|
<script>
|
||||||
|
var log = msg => {
|
||||||
|
document.getElementById('logs').innerHTML += msg + '<br>'
|
||||||
|
}
|
||||||
|
|
||||||
|
var sock = null;
|
||||||
|
var wsuri = "wss://" + location.host + "/ws";
|
||||||
|
window.onload = function() {
|
||||||
|
sock = new WebSocket(wsuri);
|
||||||
|
sock.onopen = function() {
|
||||||
|
<!-- console.log("websocket connected to " + wsuri); -->
|
||||||
|
}
|
||||||
|
sock.onclose = function(e) {
|
||||||
|
<!-- sock = new WebSocket('wss://' + location.host + '/ws'); -->
|
||||||
|
<!-- console.log("websocket connection closed (" + e.code + ")"); -->
|
||||||
|
}
|
||||||
|
sock.onmessage = function(e) {
|
||||||
|
<!-- console.log("websocket message received: " + e.data); -->
|
||||||
|
document.getElementById('remoteSDP').value = e.data
|
||||||
|
<!-- console.log("startSession"); -->
|
||||||
|
window.startSession()
|
||||||
|
}
|
||||||
|
sock.onerror = function(e) {
|
||||||
|
<!-- console.log("websocket error: " + e.data); -->
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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('localSDP').value = pc.localDescription.sdp;
|
||||||
|
<!-- document.getElementById('localSDP').value = pc.localDescription.sdp; -->
|
||||||
|
sock.send(pc.localDescription.sdp);
|
||||||
|
<!-- console.log("send sdp to server:==============\n" + pc.localDescription.sdp); -->
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPublisher) {
|
||||||
|
navigator.mediaDevices.getUserMedia({ video: true, audio: true})
|
||||||
|
.then(stream => pc.addStream(document.getElementById('video1').srcObject = stream))
|
||||||
|
.catch(log)
|
||||||
|
pc.onnegotiationneeded = e => {
|
||||||
|
<!-- console.log("Publisher createOffer") -->
|
||||||
|
pc.createOffer()
|
||||||
|
.then(d => pc.setLocalDescription(d))
|
||||||
|
.catch(log)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<!-- console.log("Subcriber createOffer") -->
|
||||||
|
pc.createOffer({ offerToReceiveVideo: true , offerToReceiveAudio: true})
|
||||||
|
.then(d => pc.setLocalDescription(d))
|
||||||
|
.catch(log)
|
||||||
|
|
||||||
|
<!-- console.log("Subcriber ontrack") -->
|
||||||
|
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('remoteSDP').value
|
||||||
|
if (sd === '') {
|
||||||
|
return alert('Session Description must not be empty')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
pc.setRemoteDescription(new RTCSessionDescription({type:'answer', sdp:sd}))
|
||||||
|
} catch (e) {
|
||||||
|
alert(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let btns = document.getElementsByClassName('sessbtn')
|
||||||
|
for (let i = 0; i < btns.length; i++) {
|
||||||
|
btns[i].style = 'display: none'
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('signalingContainer').style = 'display: block'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
60
examples/sfu-ws/web.go
Normal file
60
examples/sfu-ws/web.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"html/template"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func genPem() {
|
||||||
|
|
||||||
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
SNLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||||
|
SN, err := rand.Int(rand.Reader, SNLimit)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
template := x509.Certificate{
|
||||||
|
SerialNumber: SN,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
Organization: []string{"test"},
|
||||||
|
},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||||
|
|
||||||
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
}
|
||||||
|
template.DNSNames = append(template.DNSNames, "localhost")
|
||||||
|
template.EmailAddresses = append(template.EmailAddresses, "test@test.com")
|
||||||
|
|
||||||
|
certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
certFile, err := os.Create("cert.pem")
|
||||||
|
checkError(err)
|
||||||
|
pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
|
||||||
|
certFile.Close()
|
||||||
|
|
||||||
|
keyFile, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
|
checkError(err)
|
||||||
|
// pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv.(*rsa.PrivateKey))})
|
||||||
|
pem.Encode(keyFile, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
|
||||||
|
keyFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func web(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == "GET" {
|
||||||
|
t, _ := template.ParseFiles("sfu.html")
|
||||||
|
checkError(t.Execute(w, nil))
|
||||||
|
}
|
||||||
|
}
|
1
go.mod
1
go.mod
@@ -1,6 +1,7 @@
|
|||||||
module github.com/pions/webrtc
|
module github.com/pions/webrtc
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/gorilla/websocket v1.4.0
|
||||||
github.com/pions/datachannel v1.2.0
|
github.com/pions/datachannel v1.2.0
|
||||||
github.com/pions/dtls v1.2.1
|
github.com/pions/dtls v1.2.1
|
||||||
github.com/pions/quic v0.0.1
|
github.com/pions/quic v0.0.1
|
||||||
|
2
go.sum
2
go.sum
@@ -10,6 +10,8 @@ github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
|||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
110
track_test.go
Normal file
110
track_test.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package webrtc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewVideoTrack(t *testing.T) {
|
||||||
|
m := MediaEngine{}
|
||||||
|
m.RegisterCodec(NewRTPVP8Codec(DefaultPayloadTypeVP8, 90000))
|
||||||
|
api := NewAPI(WithMediaEngine(m))
|
||||||
|
peerConfig := Configuration{
|
||||||
|
ICEServers: []ICEServer{
|
||||||
|
{
|
||||||
|
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, _ := api.NewPeerConnection(peerConfig)
|
||||||
|
|
||||||
|
_, err := peer.NewTrack(DefaultPayloadTypeVP8, rand.Uint32(), "video", "pion")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Failed to new video track")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAudioTrack(t *testing.T) {
|
||||||
|
m := MediaEngine{}
|
||||||
|
m.RegisterCodec(NewRTPOpusCodec(DefaultPayloadTypeOpus, 48000))
|
||||||
|
api := NewAPI(WithMediaEngine(m))
|
||||||
|
peerConfig := Configuration{
|
||||||
|
ICEServers: []ICEServer{
|
||||||
|
{
|
||||||
|
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, _ := api.NewPeerConnection(peerConfig)
|
||||||
|
|
||||||
|
_, err := peer.NewTrack(DefaultPayloadTypeOpus, rand.Uint32(), "audio", "pion")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Failed to new audio track")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewTracks(t *testing.T) {
|
||||||
|
m := MediaEngine{}
|
||||||
|
m.RegisterCodec(NewRTPOpusCodec(DefaultPayloadTypeOpus, 48000))
|
||||||
|
m.RegisterCodec(NewRTPVP8Codec(DefaultPayloadTypeVP8, 90000))
|
||||||
|
api := NewAPI(WithMediaEngine(m))
|
||||||
|
peerConfig := Configuration{
|
||||||
|
ICEServers: []ICEServer{
|
||||||
|
{
|
||||||
|
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, _ := api.NewPeerConnection(peerConfig)
|
||||||
|
|
||||||
|
_, err := peer.NewTrack(DefaultPayloadTypeOpus, rand.Uint32(), "audio", "pion")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Failed to new audio track")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = peer.NewTrack(DefaultPayloadTypeVP8, rand.Uint32(), "video", "pion")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Failed to new video track")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewTracksWrite(t *testing.T) {
|
||||||
|
m := MediaEngine{}
|
||||||
|
m.RegisterCodec(NewRTPOpusCodec(DefaultPayloadTypeOpus, 48000))
|
||||||
|
m.RegisterCodec(NewRTPVP8Codec(DefaultPayloadTypeVP8, 90000))
|
||||||
|
api := NewAPI(WithMediaEngine(m))
|
||||||
|
peerConfig := Configuration{
|
||||||
|
ICEServers: []ICEServer{
|
||||||
|
{
|
||||||
|
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, _ := api.NewPeerConnection(peerConfig)
|
||||||
|
|
||||||
|
videoTrack, err := peer.NewTrack(DefaultPayloadTypeOpus, rand.Uint32(), "audio", "pion")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Failed to new video track")
|
||||||
|
}
|
||||||
|
|
||||||
|
audioTrack, err := peer.NewTrack(DefaultPayloadTypeVP8, rand.Uint32(), "video", "pion")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Failed to new audio track")
|
||||||
|
}
|
||||||
|
rtpBuf := make([]byte, 1400)
|
||||||
|
_, err = videoTrack.Write(rtpBuf)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Failed to write to video track")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = audioTrack.Write(rtpBuf)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Failed to write to audio track")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user