mirror of
https://github.com/pion/webrtc.git
synced 2025-10-10 09:30:08 +08:00
api: match WebRTC api more closely
This commit is contained in:
92
errors.go
Normal file
92
errors.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package webrtc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Types of InvalidStateErrors
|
||||||
|
var (
|
||||||
|
ErrConnectionClosed = errors.New("connection closed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// InvalidStateError indicates the object is in an invalid state.
|
||||||
|
type InvalidStateError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *InvalidStateError) Error() string {
|
||||||
|
return fmt.Sprintf("invalid state error: %v", e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types of UnknownErrors
|
||||||
|
var (
|
||||||
|
ErrNoConfig = errors.New("no configuration provided")
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnknownError indicates the operation failed for an unknown transient reason
|
||||||
|
type UnknownError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UnknownError) Error() string {
|
||||||
|
return fmt.Sprintf("unknown error: %v", e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types of InvalidAccessErrors
|
||||||
|
var (
|
||||||
|
ErrCertificateExpired = errors.New("certificate expired")
|
||||||
|
ErrNoTurnCred = errors.New("turn server credentials required")
|
||||||
|
ErrTurnCred = errors.New("invalid turn server credentials")
|
||||||
|
ErrExistingTrack = errors.New("track aready exists")
|
||||||
|
)
|
||||||
|
|
||||||
|
// InvalidAccessError indicates the object does not support the operation or argument.
|
||||||
|
type InvalidAccessError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *InvalidAccessError) Error() string {
|
||||||
|
return fmt.Sprintf("invalid access error: %v", e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types of NotSupportedErrors
|
||||||
|
var ()
|
||||||
|
|
||||||
|
// NotSupportedError indicates the operation is not supported.
|
||||||
|
type NotSupportedError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *NotSupportedError) Error() string {
|
||||||
|
return fmt.Sprintf("not supported error: %v", e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types of InvalidModificationErrors
|
||||||
|
var (
|
||||||
|
ErrModPeerIdentity = errors.New("peer identity cannot be modified")
|
||||||
|
ErrModCertificates = errors.New("certificates cannot be modified")
|
||||||
|
ErrModRtcpMuxPolicy = errors.New("rtcp mux policy cannot be modified")
|
||||||
|
ErrModIceCandidatePoolSize = errors.New("ice candidate pool size cannot be modified")
|
||||||
|
)
|
||||||
|
|
||||||
|
// InvalidModificationError indicates the object can not be modified in this way.
|
||||||
|
type InvalidModificationError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *InvalidModificationError) Error() string {
|
||||||
|
return fmt.Sprintf("invalid modification error: %v", e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types of SyntaxErrors
|
||||||
|
var ()
|
||||||
|
|
||||||
|
// SyntaxError indicates the string did not match the expected pattern.
|
||||||
|
type SyntaxError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SyntaxError) Error() string {
|
||||||
|
return fmt.Sprintf("syntax error: %v", e.Err)
|
||||||
|
}
|
@@ -23,9 +23,9 @@ type Pipeline struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreatePipeline creates a GStreamer Pipeline
|
// CreatePipeline creates a GStreamer Pipeline
|
||||||
func CreatePipeline(codec webrtc.TrackType) *Pipeline {
|
func CreatePipeline(codecName string) *Pipeline {
|
||||||
pipelineStr := "appsrc format=time is-live=true do-timestamp=true name=src ! application/x-rtp"
|
pipelineStr := "appsrc format=time is-live=true do-timestamp=true name=src ! application/x-rtp"
|
||||||
switch codec {
|
switch codecName {
|
||||||
case webrtc.VP8:
|
case webrtc.VP8:
|
||||||
pipelineStr += ", encoding-name=VP8-DRAFT-IETF-01 ! rtpvp8depay ! decodebin ! autovideosink"
|
pipelineStr += ", encoding-name=VP8-DRAFT-IETF-01 ! rtpvp8depay ! decodebin ! autovideosink"
|
||||||
case webrtc.Opus:
|
case webrtc.Opus:
|
||||||
@@ -35,7 +35,7 @@ func CreatePipeline(codec webrtc.TrackType) *Pipeline {
|
|||||||
case webrtc.H264:
|
case webrtc.H264:
|
||||||
pipelineStr += " ! rtph264depay ! decodebin ! autovideosink"
|
pipelineStr += " ! rtph264depay ! decodebin ! autovideosink"
|
||||||
default:
|
default:
|
||||||
panic("Unhandled codec " + codec.String())
|
panic("Unhandled codec " + codecName)
|
||||||
}
|
}
|
||||||
|
|
||||||
pipelineStrUnsafe := C.CString(pipelineStr)
|
pipelineStrUnsafe := C.CString(pipelineStr)
|
||||||
|
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -10,13 +11,12 @@ import (
|
|||||||
"github.com/pions/webrtc"
|
"github.com/pions/webrtc"
|
||||||
"github.com/pions/webrtc/examples/gstreamer-receive/gst"
|
"github.com/pions/webrtc/examples/gstreamer-receive/gst"
|
||||||
"github.com/pions/webrtc/pkg/ice"
|
"github.com/pions/webrtc/pkg/ice"
|
||||||
"github.com/pions/webrtc/pkg/rtp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
rawSd, err := reader.ReadString('\n')
|
rawSd, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil && err != io.EOF {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,20 +28,25 @@ func main() {
|
|||||||
|
|
||||||
/* Everything below is the pion-WebRTC API, thanks for using it! */
|
/* Everything below is the pion-WebRTC API, thanks for using it! */
|
||||||
|
|
||||||
|
// Setup the codecs you want to use.
|
||||||
|
// We'll use the default ones but you can also define your own
|
||||||
|
webrtc.RegisterDefaultCodecs()
|
||||||
|
|
||||||
// Create a new RTCPeerConnection
|
// Create a new RTCPeerConnection
|
||||||
peerConnection, err := webrtc.New(&webrtc.RTCConfiguration{})
|
peerConnection, err := webrtc.New(webrtc.RTCConfiguration{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a handler for when a new remote track starts, this handler creates a gstreamer pipeline
|
// Set a handler for when a new remote track starts, this handler creates a gstreamer pipeline
|
||||||
// for the given codec
|
// for the given codec
|
||||||
peerConnection.Ontrack = func(mediaType webrtc.TrackType, packets <-chan *rtp.Packet) {
|
peerConnection.Ontrack = func(track *webrtc.RTCTrack) {
|
||||||
fmt.Printf("Track has started, of type %s \n", mediaType.String())
|
codec := track.Codec
|
||||||
pipeline := gst.CreatePipeline(mediaType)
|
fmt.Printf("Track has started, of type %d: %s \n", track.PayloadType, codec.Name)
|
||||||
|
pipeline := gst.CreatePipeline(codec.Name)
|
||||||
pipeline.Start()
|
pipeline.Start()
|
||||||
for {
|
for {
|
||||||
p := <-packets
|
p := <-track.Packets
|
||||||
pipeline.Push(p.Raw)
|
pipeline.Push(p.Raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,17 +58,21 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set the remote SessionDescription
|
// Set the remote SessionDescription
|
||||||
if err := peerConnection.SetRemoteDescription(string(sd)); err != nil {
|
offer := webrtc.RTCSessionDescription{
|
||||||
|
Typ: webrtc.RTCSdpTypeOffer,
|
||||||
|
Sdp: string(sd),
|
||||||
|
}
|
||||||
|
if err := peerConnection.SetRemoteDescription(offer); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the LocalDescription, and starts our UDP listeners
|
// Sets the LocalDescription, and starts our UDP listeners
|
||||||
if err := peerConnection.CreateAnswer(); err != nil {
|
answer, err := peerConnection.CreateAnswer(nil)
|
||||||
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the LocalDescription and take it to base64 so we can paste in browser
|
// Get the LocalDescription and take it to base64 so we can paste in browser
|
||||||
localDescriptionStr := peerConnection.LocalDescription.Marshal()
|
fmt.Println(base64.StdEncoding.EncodeToString([]byte(answer.Sdp)))
|
||||||
fmt.Println(base64.StdEncoding.EncodeToString([]byte(localDescriptionStr)))
|
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
|
@@ -24,16 +24,16 @@ type Pipeline struct {
|
|||||||
Pipeline *C.GstElement
|
Pipeline *C.GstElement
|
||||||
in chan<- webrtc.RTCSample
|
in chan<- webrtc.RTCSample
|
||||||
id int
|
id int
|
||||||
codec webrtc.TrackType
|
codecName string
|
||||||
}
|
}
|
||||||
|
|
||||||
var pipelines = make(map[int]*Pipeline)
|
var pipelines = make(map[int]*Pipeline)
|
||||||
var pipelinesLock sync.Mutex
|
var pipelinesLock sync.Mutex
|
||||||
|
|
||||||
// CreatePipeline creates a GStreamer Pipeline
|
// CreatePipeline creates a GStreamer Pipeline
|
||||||
func CreatePipeline(codec webrtc.TrackType, in chan<- webrtc.RTCSample) *Pipeline {
|
func CreatePipeline(codecName string, in chan<- webrtc.RTCSample) *Pipeline {
|
||||||
pipelineStr := "appsink name=appsink"
|
pipelineStr := "appsink name=appsink"
|
||||||
switch codec {
|
switch codecName {
|
||||||
case webrtc.VP8:
|
case webrtc.VP8:
|
||||||
pipelineStr = "videotestsrc ! vp8enc ! " + pipelineStr
|
pipelineStr = "videotestsrc ! vp8enc ! " + pipelineStr
|
||||||
case webrtc.VP9:
|
case webrtc.VP9:
|
||||||
@@ -43,7 +43,7 @@ func CreatePipeline(codec webrtc.TrackType, in chan<- webrtc.RTCSample) *Pipelin
|
|||||||
case webrtc.Opus:
|
case webrtc.Opus:
|
||||||
pipelineStr = "audiotestsrc ! opusenc ! " + pipelineStr
|
pipelineStr = "audiotestsrc ! opusenc ! " + pipelineStr
|
||||||
default:
|
default:
|
||||||
panic("Unhandled codec " + codec.String())
|
panic("Unhandled codec " + codecName)
|
||||||
}
|
}
|
||||||
|
|
||||||
pipelineStrUnsafe := C.CString(pipelineStr)
|
pipelineStrUnsafe := C.CString(pipelineStr)
|
||||||
@@ -56,7 +56,7 @@ func CreatePipeline(codec webrtc.TrackType, in chan<- webrtc.RTCSample) *Pipelin
|
|||||||
Pipeline: C.gstreamer_send_create_pipeline(pipelineStrUnsafe),
|
Pipeline: C.gstreamer_send_create_pipeline(pipelineStrUnsafe),
|
||||||
in: in,
|
in: in,
|
||||||
id: len(pipelines),
|
id: len(pipelines),
|
||||||
codec: codec,
|
codecName: codecName,
|
||||||
}
|
}
|
||||||
|
|
||||||
pipelines[pipeline.id] = pipeline
|
pipelines[pipeline.id] = pipeline
|
||||||
@@ -85,7 +85,7 @@ func goHandlePipelineBuffer(buffer unsafe.Pointer, bufferLen C.int, duration C.i
|
|||||||
|
|
||||||
if pipeline, ok := pipelines[int(pipelineID)]; ok {
|
if pipeline, ok := pipelines[int(pipelineID)]; ok {
|
||||||
var samples uint32
|
var samples uint32
|
||||||
if pipeline.codec == webrtc.Opus {
|
if pipeline.codecName == webrtc.Opus {
|
||||||
samples = uint32(audioClockRate * (float32(duration) / 1000000000))
|
samples = uint32(audioClockRate * (float32(duration) / 1000000000))
|
||||||
} else {
|
} else {
|
||||||
samples = uint32(videoClockRate * (float32(duration) / 1000000000))
|
samples = uint32(videoClockRate * (float32(duration) / 1000000000))
|
||||||
|
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -15,7 +16,7 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
rawSd, err := reader.ReadString('\n')
|
rawSd, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil && err != io.EOF {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,46 +28,62 @@ func main() {
|
|||||||
|
|
||||||
/* Everything below is the pion-WebRTC API, thanks for using it! */
|
/* Everything below is the pion-WebRTC API, thanks for using it! */
|
||||||
|
|
||||||
|
// Setup the codecs you want to use.
|
||||||
|
// We'll use the default ones but you can also define your own
|
||||||
|
webrtc.RegisterDefaultCodecs()
|
||||||
|
|
||||||
// Create a new RTCPeerConnection
|
// Create a new RTCPeerConnection
|
||||||
peerConnection, err := webrtc.New(&webrtc.RTCConfiguration{})
|
peerConnection, err := webrtc.New(webrtc.RTCConfiguration{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a audio track
|
|
||||||
opusIn, err := peerConnection.AddTrack(webrtc.Opus, 48000)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a video track
|
|
||||||
vp8In, err := peerConnection.AddTrack(webrtc.VP8, 90000)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the remote SessionDescription
|
|
||||||
if err := peerConnection.SetRemoteDescription(string(sd)); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sets the LocalDescription, and starts our UDP listeners
|
|
||||||
if err := peerConnection.CreateAnswer(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the handler for ICE connection state
|
// Set the handler for ICE connection state
|
||||||
// This will notify you when the peer has connected/disconnected
|
// This will notify you when the peer has connected/disconnected
|
||||||
peerConnection.OnICEConnectionStateChange = func(connectionState ice.ConnectionState) {
|
peerConnection.OnICEConnectionStateChange = func(connectionState ice.ConnectionState) {
|
||||||
fmt.Printf("Connection State has changed %s \n", connectionState.String())
|
fmt.Printf("Connection State has changed %s \n", connectionState.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a audio track
|
||||||
|
opusTrack, err := peerConnection.NewRTCTrack(webrtc.PayloadTypeOpus, "audio", "pions1")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_, err = peerConnection.AddTrack(opusTrack)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a video track
|
||||||
|
vp8Track, err := peerConnection.NewRTCTrack(webrtc.PayloadTypeVP8, "video", "pions2")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_, err = peerConnection.AddTrack(vp8Track)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the remote SessionDescription
|
||||||
|
offer := webrtc.RTCSessionDescription{
|
||||||
|
Typ: webrtc.RTCSdpTypeOffer,
|
||||||
|
Sdp: string(sd),
|
||||||
|
}
|
||||||
|
if err := peerConnection.SetRemoteDescription(offer); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the LocalDescription, and starts our UDP listeners
|
||||||
|
answer, err := peerConnection.CreateAnswer(nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
// Get the LocalDescription and take it to base64 so we can paste in browser
|
// Get the LocalDescription and take it to base64 so we can paste in browser
|
||||||
localDescriptionStr := peerConnection.LocalDescription.Marshal()
|
fmt.Println(base64.StdEncoding.EncodeToString([]byte(answer.Sdp)))
|
||||||
fmt.Println(base64.StdEncoding.EncodeToString([]byte(localDescriptionStr)))
|
|
||||||
|
|
||||||
// Start pushing buffers on these tracks
|
// Start pushing buffers on these tracks
|
||||||
gst.CreatePipeline(webrtc.Opus, opusIn).Start()
|
gst.CreatePipeline(webrtc.Opus, opusTrack.Samples).Start()
|
||||||
gst.CreatePipeline(webrtc.VP8, vp8In).Start()
|
gst.CreatePipeline(webrtc.VP8, vp8Track.Samples).Start()
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
|
@@ -4,17 +4,17 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/pions/webrtc"
|
"github.com/pions/webrtc"
|
||||||
"github.com/pions/webrtc/pkg/ice"
|
"github.com/pions/webrtc/pkg/ice"
|
||||||
"github.com/pions/webrtc/pkg/rtp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
rawSd, err := reader.ReadString('\n')
|
rawSd, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil && err != io.EOF {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,8 +26,13 @@ func main() {
|
|||||||
|
|
||||||
/* Everything below is the pion-WebRTC API, thanks for using it! */
|
/* Everything below is the pion-WebRTC API, thanks for using it! */
|
||||||
|
|
||||||
|
// Setup the codecs you want to use.
|
||||||
|
// We'll use a VP8 codec but you can also define your own
|
||||||
|
webrtc.RegisterCodec(webrtc.NewRTCRtpOpusCodec(webrtc.PayloadTypeOpus, 48000, 2))
|
||||||
|
webrtc.RegisterCodec(webrtc.NewRTCRtpVP8Codec(webrtc.PayloadTypeVP8, 90000))
|
||||||
|
|
||||||
// Create a new RTCPeerConnection
|
// Create a new RTCPeerConnection
|
||||||
peerConnection, err := webrtc.New(&webrtc.RTCConfiguration{})
|
peerConnection, err := webrtc.New(webrtc.RTCConfiguration{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -35,15 +40,15 @@ func main() {
|
|||||||
// Set a handler for when a new remote track starts, this handler saves buffers to disk as
|
// 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.
|
// 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
|
// In your application this is where you would handle/process video
|
||||||
peerConnection.Ontrack = func(mediaType webrtc.TrackType, packets <-chan *rtp.Packet) {
|
peerConnection.Ontrack = func(track *webrtc.RTCTrack) {
|
||||||
if mediaType == webrtc.VP8 {
|
if track.Codec.Name == webrtc.VP8 {
|
||||||
fmt.Println("Got VP8 track, saving to disk as output.ivf")
|
fmt.Println("Got VP8 track, saving to disk as output.ivf")
|
||||||
i, err := newIVFWriter("output.ivf")
|
i, err := newIVFWriter("output.ivf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
i.addPacket(<-packets)
|
i.addPacket(<-track.Packets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,17 +60,21 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set the remote SessionDescription
|
// Set the remote SessionDescription
|
||||||
if err := peerConnection.SetRemoteDescription(string(sd)); err != nil {
|
offer := webrtc.RTCSessionDescription{
|
||||||
|
Typ: webrtc.RTCSdpTypeOffer,
|
||||||
|
Sdp: string(sd),
|
||||||
|
}
|
||||||
|
if err := peerConnection.SetRemoteDescription(offer); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the LocalDescription, and starts our UDP listeners
|
// Sets the LocalDescription, and starts our UDP listeners
|
||||||
if err := peerConnection.CreateAnswer(); err != nil {
|
answer, err := peerConnection.CreateAnswer(nil)
|
||||||
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the LocalDescription and take it to base64 so we can paste in browser
|
// Get the LocalDescription and take it to base64 so we can paste in browser
|
||||||
localDescriptionStr := peerConnection.LocalDescription.Marshal()
|
fmt.Println(base64.StdEncoding.EncodeToString([]byte(answer.Sdp)))
|
||||||
fmt.Println(base64.StdEncoding.EncodeToString([]byte(localDescriptionStr)))
|
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
|
@@ -4,17 +4,17 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/pions/webrtc"
|
"github.com/pions/webrtc"
|
||||||
"github.com/pions/webrtc/pkg/ice"
|
"github.com/pions/webrtc/pkg/ice"
|
||||||
"github.com/pions/webrtc/pkg/rtp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
rawSd, err := reader.ReadString('\n')
|
rawSd, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil && err != io.EOF {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +24,14 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
peerConnection, err := webrtc.New(&webrtc.RTCConfiguration{
|
/* Everything below is the pion-WebRTC API, thanks for using it! */
|
||||||
|
|
||||||
|
// Setup the codecs you want to use.
|
||||||
|
// We'll use the default ones but you can also define your own
|
||||||
|
webrtc.RegisterDefaultCodecs()
|
||||||
|
|
||||||
|
// Create a new RTCPeerConnection, providing ICE servers
|
||||||
|
peerConnection, err := webrtc.New(webrtc.RTCConfiguration{
|
||||||
ICEServers: []webrtc.RTCICEServer{
|
ICEServers: []webrtc.RTCICEServer{
|
||||||
{
|
{
|
||||||
URLs: []string{"stun:stun.l.google.com:19302"},
|
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||||
@@ -35,23 +42,30 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
peerConnection.Ontrack = func(mediaType webrtc.TrackType, packets <-chan *rtp.Packet) {
|
peerConnection.Ontrack = func(track *webrtc.RTCTrack) {
|
||||||
fmt.Printf("Got a %s track\n", mediaType)
|
fmt.Printf("Got a %s track\n", track.Codec.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
peerConnection.OnICEConnectionStateChange = func(connectionState ice.ConnectionState) {
|
peerConnection.OnICEConnectionStateChange = func(connectionState ice.ConnectionState) {
|
||||||
fmt.Printf("Connection State has changed %s \n", connectionState.String())
|
fmt.Printf("Connection State has changed %s \n", connectionState.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := peerConnection.SetRemoteDescription(string(sd)); err != nil {
|
// Set the remote SessionDescription
|
||||||
|
offer := webrtc.RTCSessionDescription{
|
||||||
|
Typ: webrtc.RTCSdpTypeOffer,
|
||||||
|
Sdp: string(sd),
|
||||||
|
}
|
||||||
|
if err := peerConnection.SetRemoteDescription(offer); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := peerConnection.CreateAnswer(); err != nil {
|
// Sets the LocalDescription, and starts our UDP listeners
|
||||||
|
answer, err := peerConnection.CreateAnswer(nil)
|
||||||
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
localDescriptionStr := peerConnection.LocalDescription.Marshal()
|
// Get the LocalDescription and take it to base64 so we can paste in browser
|
||||||
fmt.Println(base64.StdEncoding.EncodeToString([]byte(localDescriptionStr)))
|
fmt.Println(base64.StdEncoding.EncodeToString([]byte(answer.Sdp)))
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
|
@@ -94,7 +94,7 @@ func (p *Port) handleICE(in *incomingPacket, remoteKey []byte, iceTimer *time.Ti
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
} else {
|
} else {
|
||||||
p.ICEState = ice.Completed
|
p.ICEState = ice.ConnectionStateCompleted
|
||||||
iceTimer.Reset(iceTimeout)
|
iceTimer.Reset(iceTimeout)
|
||||||
iceNotifier(p)
|
iceNotifier(p)
|
||||||
}
|
}
|
||||||
@@ -131,7 +131,7 @@ func (p *Port) networkLoop(remoteKey []byte, tlscfg *dtls.TLSCfg, b BufferTransp
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-iceTimer.C:
|
case <-iceTimer.C:
|
||||||
p.ICEState = ice.Failed
|
p.ICEState = ice.ConnectionStateFailed
|
||||||
iceNotifier(p)
|
iceNotifier(p)
|
||||||
case in, inValid := <-incomingPackets:
|
case in, inValid := <-incomingPackets:
|
||||||
if !inValid {
|
if !inValid {
|
||||||
|
135
internal/sdp/jsep.go
Normal file
135
internal/sdp/jsep.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package sdp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for SDP attributes used in JSEP
|
||||||
|
const (
|
||||||
|
AttrKeyIdentity = "identity"
|
||||||
|
AttrKeyGroup = "group"
|
||||||
|
AttrKeySsrc = "ssrc"
|
||||||
|
AttrKeySsrcGroup = "ssrc-group"
|
||||||
|
AttrKeyMsidSemantic = "msid-semantic"
|
||||||
|
AttrKeyConnectionSetup = "setup"
|
||||||
|
AttrKeyMID = "mid"
|
||||||
|
AttrKeyICELite = "ice-lite"
|
||||||
|
AttrKeyRtcpMux = "rtcp-mux"
|
||||||
|
AttrKeyRtcpRsize = "rtcp-rsize"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for semantic tokens used in JSEP
|
||||||
|
const (
|
||||||
|
SemanticTokenLipSynchronization = "LS"
|
||||||
|
SemanticTokenFlowIdentification = "FID"
|
||||||
|
SemanticTokenForwardErrorCorrection = "FEC"
|
||||||
|
SemanticTokenWebRTCMediaStreams = "WMS"
|
||||||
|
)
|
||||||
|
|
||||||
|
// API to match draft-ietf-rtcweb-jsep
|
||||||
|
// Move to webrtc or its own package?
|
||||||
|
|
||||||
|
// NewJSEPSessionDescription creates a new SessionDescription with
|
||||||
|
// some settings that are required by the JSEP spec.
|
||||||
|
func NewJSEPSessionDescription(fingerprint string, identity bool) *SessionDescription {
|
||||||
|
d := &SessionDescription{
|
||||||
|
ProtocolVersion: 0,
|
||||||
|
Origin: fmt.Sprintf(
|
||||||
|
"- %d %d IN IP4 0.0.0.0",
|
||||||
|
newSessionID(),
|
||||||
|
time.Now().Unix(),
|
||||||
|
),
|
||||||
|
SessionName: "-",
|
||||||
|
Timing: []string{"0 0"},
|
||||||
|
Attributes: []string{
|
||||||
|
// "ice-options:trickle", // TODO: implement trickle ICE
|
||||||
|
"fingerprint:sha-256 " + fingerprint,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if identity {
|
||||||
|
d.WithPropertyAttribute(AttrKeyIdentity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPropertyAttribute adds a property attribute 'a=key' to the session description
|
||||||
|
func (d *SessionDescription) WithPropertyAttribute(key string) *SessionDescription {
|
||||||
|
d.Attributes = append(d.Attributes, key)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithValueAttribute adds a value attribute 'a=key:value' to the session description
|
||||||
|
func (d *SessionDescription) WithValueAttribute(key, value string) *SessionDescription {
|
||||||
|
d.Attributes = append(d.Attributes, fmt.Sprintf("%s:%s", key, value))
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMedia adds a media description to the session description
|
||||||
|
func (d *SessionDescription) WithMedia(md *MediaDescription) *SessionDescription {
|
||||||
|
d.MediaDescriptions = append(d.MediaDescriptions, md)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJSEPMediaDescription creates a new MediaDescription with
|
||||||
|
// some settings that are required by the JSEP spec.
|
||||||
|
func NewJSEPMediaDescription(typ string, codecPrefs []string) *MediaDescription {
|
||||||
|
// TODO: handle codecPrefs
|
||||||
|
d := &MediaDescription{
|
||||||
|
MediaName: fmt.Sprintf("%s 9 UDP/TLS/RTP/SAVPF", typ), // TODO: other transports?
|
||||||
|
ConnectionData: "IN IP4 0.0.0.0",
|
||||||
|
Attributes: []string{},
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPropertyAttribute adds a property attribute 'a=key' to the media description
|
||||||
|
func (d *MediaDescription) WithPropertyAttribute(key string) *MediaDescription {
|
||||||
|
d.Attributes = append(d.Attributes, key)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithValueAttribute adds a value attribute 'a=key:value' to the media description
|
||||||
|
func (d *MediaDescription) WithValueAttribute(key, value string) *MediaDescription {
|
||||||
|
d.Attributes = append(d.Attributes, fmt.Sprintf("%s:%s", key, value))
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithICECredentials adds ICE credentials to the media description
|
||||||
|
func (d *MediaDescription) WithICECredentials(username, password string) *MediaDescription {
|
||||||
|
return d.
|
||||||
|
WithValueAttribute("ice-ufrag", username).
|
||||||
|
WithValueAttribute("ice-pwd", password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCodec adds codec information to the media description
|
||||||
|
func (d *MediaDescription) WithCodec(payloadType uint8, name string, clockrate uint32, channels uint16, fmtp string) *MediaDescription {
|
||||||
|
d.MediaName = fmt.Sprintf("%s %d", d.MediaName, payloadType)
|
||||||
|
rtpmap := fmt.Sprintf("%d %s/%d", payloadType, name, clockrate)
|
||||||
|
if channels > 0 {
|
||||||
|
rtpmap = rtpmap + fmt.Sprintf("/%d", channels)
|
||||||
|
}
|
||||||
|
d.WithValueAttribute("rtpmap", rtpmap)
|
||||||
|
if fmtp != "" {
|
||||||
|
d.WithValueAttribute("fmtp", fmt.Sprintf("%d %s", payloadType, fmtp))
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMediaSource adds media source information to the media description
|
||||||
|
func (d *MediaDescription) WithMediaSource(ssrc uint32, cname, streamLabel, label string) *MediaDescription {
|
||||||
|
return d.
|
||||||
|
WithValueAttribute("ssrc", fmt.Sprintf("%d cname:%s", ssrc, cname)). // Deprecated but not pased out?
|
||||||
|
WithValueAttribute("ssrc", fmt.Sprintf("%d msid:%s %s", ssrc, streamLabel, label)).
|
||||||
|
WithValueAttribute("ssrc", fmt.Sprintf("%d mslabel:%s", ssrc, streamLabel)). // Deprecated but not pased out?
|
||||||
|
WithValueAttribute("ssrc", fmt.Sprintf("%d label:%s", ssrc, label)) // Deprecated but not pased out?
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCandidate adds an ICE candidate to the media description
|
||||||
|
func (d *MediaDescription) WithCandidate(id int, transport string, basePriority uint16, ip string, port int, typ string) *MediaDescription {
|
||||||
|
return d.
|
||||||
|
WithValueAttribute("candidate",
|
||||||
|
fmt.Sprintf("%scandidate %d %s %d %s %d typ %s", transport, id, transport, basePriority, ip, port, typ))
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
package sdp
|
package sdp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -22,102 +23,99 @@ type SessionBuilder struct {
|
|||||||
Tracks []*SessionBuilderTrack
|
Tracks []*SessionBuilderTrack
|
||||||
}
|
}
|
||||||
|
|
||||||
// BaseSessionDescription generates a default SDP response that is ice-lite, initiates the DTLS session and
|
// ConnectionRole indicates which of the end points should initiate the connection establishment
|
||||||
// supports VP8, VP9, H264 and Opus
|
type ConnectionRole int
|
||||||
func BaseSessionDescription(b *SessionBuilder) *SessionDescription {
|
|
||||||
addMediaCandidates := func(m *MediaDescription) *MediaDescription {
|
|
||||||
m.Attributes = append(m.Attributes, b.Candidates...)
|
|
||||||
m.Attributes = append(m.Attributes, "end-of-candidates")
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
audioMediaDescription := &MediaDescription{
|
const (
|
||||||
MediaName: "audio 9 RTP/SAVPF 111",
|
|
||||||
ConnectionData: "IN IP4 127.0.0.1",
|
|
||||||
Attributes: []string{
|
|
||||||
"setup:active",
|
|
||||||
"mid:audio",
|
|
||||||
"sendrecv",
|
|
||||||
"ice-ufrag:" + b.IceUsername,
|
|
||||||
"ice-pwd:" + b.IcePassword,
|
|
||||||
"ice-lite",
|
|
||||||
"fingerprint:sha-256 " + b.Fingerprint,
|
|
||||||
"rtcp-mux",
|
|
||||||
"rtcp-rsize",
|
|
||||||
"rtpmap:111 opus/48000/2",
|
|
||||||
"fmtp:111 minptime=10;useinbandfec=1",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
videoMediaDescription := &MediaDescription{
|
// ConnectionRoleActive indicates the endpoint will initiate an outgoing connection.
|
||||||
MediaName: "video 9 RTP/SAVPF 96 98 100",
|
ConnectionRoleActive ConnectionRole = iota + 1
|
||||||
ConnectionData: "IN IP4 127.0.0.1",
|
|
||||||
Attributes: []string{
|
|
||||||
"setup:active",
|
|
||||||
"mid:video",
|
|
||||||
"sendrecv",
|
|
||||||
"ice-ufrag:" + b.IceUsername,
|
|
||||||
"ice-pwd:" + b.IcePassword,
|
|
||||||
"ice-lite",
|
|
||||||
"fingerprint:sha-256 " + b.Fingerprint,
|
|
||||||
"rtcp-mux",
|
|
||||||
"rtcp-rsize",
|
|
||||||
"rtpmap:96 VP8/90000",
|
|
||||||
"rtpmap:98 VP9/90000",
|
|
||||||
"rtpmap:100 H264/90000",
|
|
||||||
"fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaStreamsAttribute := "msid-semantic: WMS"
|
// ConnectionRolePassive indicates the endpoint will accept an incoming connection.
|
||||||
for i, track := range b.Tracks {
|
ConnectionRolePassive
|
||||||
var attributes *[]string
|
|
||||||
if track.IsAudio {
|
|
||||||
attributes = &audioMediaDescription.Attributes
|
|
||||||
} else {
|
|
||||||
attributes = &videoMediaDescription.Attributes
|
|
||||||
}
|
|
||||||
appendAttr := func(attr string) {
|
|
||||||
*attributes = append(*attributes, attr)
|
|
||||||
}
|
|
||||||
|
|
||||||
appendAttr("ssrc:" + fmt.Sprint(track.SSRC) + " cname:pion" + strconv.Itoa(i))
|
// ConnectionRoleActpass indicates the endpoint is willing to accept an incoming connection or to initiate an outgoing connection.
|
||||||
appendAttr("ssrc:" + fmt.Sprint(track.SSRC) + " msid:pion" + strconv.Itoa(i) + " pion" + strconv.Itoa(i))
|
ConnectionRoleActpass
|
||||||
appendAttr("ssrc:" + fmt.Sprint(track.SSRC) + " mslabel:pion" + strconv.Itoa(i))
|
|
||||||
appendAttr("ssrc:" + fmt.Sprint(track.SSRC) + " label:pion" + strconv.Itoa(i))
|
|
||||||
|
|
||||||
mediaStreamsAttribute += " pion" + strconv.Itoa(i)
|
// ConnectionRoleHoldconn indicates the endpoint does not want the connection to be established for the time being.
|
||||||
}
|
ConnectionRoleHoldconn
|
||||||
|
)
|
||||||
|
|
||||||
sessionID := strconv.FormatUint(uint64(rand.Uint32())<<32+uint64(rand.Uint32()), 10)
|
func (t ConnectionRole) String() string {
|
||||||
return &SessionDescription{
|
switch t {
|
||||||
ProtocolVersion: 0,
|
case ConnectionRoleActive:
|
||||||
Origin: "pion-webrtc " + sessionID + " 2 IN IP4 0.0.0.0",
|
return "active"
|
||||||
SessionName: "-",
|
case ConnectionRolePassive:
|
||||||
Timing: []string{"0 0"},
|
return "passive"
|
||||||
Attributes: []string{
|
case ConnectionRoleActpass:
|
||||||
"group:BUNDLE audio video",
|
return "actpass"
|
||||||
mediaStreamsAttribute,
|
case ConnectionRoleHoldconn:
|
||||||
},
|
return "holdconn"
|
||||||
MediaDescriptions: []*MediaDescription{
|
default:
|
||||||
addMediaCandidates(audioMediaDescription),
|
return "Unknown"
|
||||||
addMediaCandidates(videoMediaDescription),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newSessionID() uint64 {
|
||||||
|
return uint64(rand.Uint32())<<32 + uint64(rand.Uint32())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Codec represents a codec
|
||||||
|
type Codec struct {
|
||||||
|
PayloadType uint8
|
||||||
|
Name string
|
||||||
|
ClockRate uint32
|
||||||
|
EncodingParameters string
|
||||||
|
Fmtp string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Codec) String() string {
|
||||||
|
return fmt.Sprintf("%d %s/%d/%s", c.PayloadType, c.Name, c.ClockRate, c.EncodingParameters)
|
||||||
|
}
|
||||||
|
|
||||||
// GetCodecForPayloadType scans the SessionDescription for the given payloadType and returns the codec
|
// GetCodecForPayloadType scans the SessionDescription for the given payloadType and returns the codec
|
||||||
func GetCodecForPayloadType(payloadType uint8, sd *SessionDescription) (ok bool, codec string) {
|
func (sd *SessionDescription) GetCodecForPayloadType(payloadType uint8) (Codec, error) {
|
||||||
|
codec := Codec{
|
||||||
|
PayloadType: payloadType,
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
payloadTypeString := strconv.Itoa(int(payloadType))
|
||||||
|
rtpmapPrefix := "rtpmap:" + payloadTypeString
|
||||||
|
fmtpPrefix := "fmtp:" + payloadTypeString
|
||||||
|
|
||||||
for _, m := range sd.MediaDescriptions {
|
for _, m := range sd.MediaDescriptions {
|
||||||
for _, a := range m.Attributes {
|
for _, a := range m.Attributes {
|
||||||
if strings.Contains(a, "rtpmap:"+strconv.Itoa(int(payloadType))) {
|
if strings.HasPrefix(a, rtpmapPrefix) {
|
||||||
|
found = true
|
||||||
|
// a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
|
||||||
split := strings.Split(a, " ")
|
split := strings.Split(a, " ")
|
||||||
if len(split) == 2 {
|
if len(split) == 2 {
|
||||||
split := strings.Split(split[1], "/")
|
split := strings.Split(split[1], "/")
|
||||||
return true, split[0]
|
codec.Name = split[0]
|
||||||
|
parts := len(split)
|
||||||
|
if parts > 1 {
|
||||||
|
rate, err := strconv.Atoi(split[1])
|
||||||
|
if err != nil {
|
||||||
|
return codec, err
|
||||||
|
}
|
||||||
|
codec.ClockRate = uint32(rate)
|
||||||
|
}
|
||||||
|
if parts > 2 {
|
||||||
|
codec.EncodingParameters = split[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(a, fmtpPrefix) {
|
||||||
|
// a=fmtp:<format> <format specific parameters>
|
||||||
|
split := strings.Split(a, " ")
|
||||||
|
if len(split) == 2 {
|
||||||
|
codec.Fmtp = split[1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if found {
|
||||||
|
return codec, nil
|
||||||
}
|
}
|
||||||
return false, ""
|
}
|
||||||
|
return codec, errors.New("payload type not found")
|
||||||
}
|
}
|
||||||
|
246
media.go
Normal file
246
media.go
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
package webrtc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
|
"github.com/pions/webrtc/pkg/rtp"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RTCRtpReceiver allows an application to inspect the receipt of a RTCTrack
|
||||||
|
type RTCRtpReceiver struct {
|
||||||
|
Track *RTCTrack
|
||||||
|
// receiverTrack *RTCTrack
|
||||||
|
// receiverTransport
|
||||||
|
// receiverRtcpTransport
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: receiving side
|
||||||
|
// func newRTCRtpReceiver(kind, id string) {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
|
||||||
|
// RTCRtpSender allows an application to control how a given RTCTrack is encoded and transmitted to a remote peer
|
||||||
|
type RTCRtpSender struct {
|
||||||
|
Track *RTCTrack
|
||||||
|
// senderTrack *RTCTrack
|
||||||
|
// senderTransport
|
||||||
|
// senderRtcpTransport
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRTCRtpSender(track *RTCTrack) *RTCRtpSender {
|
||||||
|
s := &RTCRtpSender{
|
||||||
|
Track: track,
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTCRtpTransceiverDirection indicates the direction of the RTCRtpTransceiver
|
||||||
|
type RTCRtpTransceiverDirection int
|
||||||
|
|
||||||
|
const (
|
||||||
|
|
||||||
|
// RTCRtpTransceiverDirectionSendrecv indicates the RTCRtpSender will offer to send RTP and RTCRtpReceiver the will offer to receive RTP
|
||||||
|
RTCRtpTransceiverDirectionSendrecv RTCRtpTransceiverDirection = iota + 1
|
||||||
|
|
||||||
|
// RTCRtpTransceiverDirectionSendonly indicates the RTCRtpSender will offer to send RTP
|
||||||
|
RTCRtpTransceiverDirectionSendonly
|
||||||
|
|
||||||
|
// RTCRtpTransceiverDirectionRecvonly indicates the RTCRtpReceiver the will offer to receive RTP
|
||||||
|
RTCRtpTransceiverDirectionRecvonly
|
||||||
|
|
||||||
|
// RTCRtpTransceiverDirectionInactive indicates the RTCRtpSender won't offer to send RTP and RTCRtpReceiver the won't offer to receive RTP
|
||||||
|
RTCRtpTransceiverDirectionInactive
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t RTCRtpTransceiverDirection) String() string {
|
||||||
|
switch t {
|
||||||
|
case RTCRtpTransceiverDirectionSendrecv:
|
||||||
|
return "sendrecv"
|
||||||
|
case RTCRtpTransceiverDirectionSendonly:
|
||||||
|
return "sendonly"
|
||||||
|
case RTCRtpTransceiverDirectionRecvonly:
|
||||||
|
return "recvonly"
|
||||||
|
case RTCRtpTransceiverDirectionInactive:
|
||||||
|
return "inactive"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTCRtpTransceiver represents a combination of an RTCRtpSender and an RTCRtpReceiver that share a common mid.
|
||||||
|
type RTCRtpTransceiver struct {
|
||||||
|
Mid string
|
||||||
|
Sender *RTCRtpSender
|
||||||
|
Receiver *RTCRtpReceiver
|
||||||
|
Direction RTCRtpTransceiverDirection
|
||||||
|
// currentDirection RTCRtpTransceiverDirection
|
||||||
|
// firedDirection RTCRtpTransceiverDirection
|
||||||
|
// receptive bool
|
||||||
|
stopped bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *RTCRtpTransceiver) setSendingTrack(track *RTCTrack) {
|
||||||
|
t.Sender.Track = track
|
||||||
|
|
||||||
|
switch t.Direction {
|
||||||
|
case RTCRtpTransceiverDirectionRecvonly:
|
||||||
|
t.Direction = RTCRtpTransceiverDirectionSendrecv
|
||||||
|
case RTCRtpTransceiverDirectionInactive:
|
||||||
|
t.Direction = RTCRtpTransceiverDirectionSendonly
|
||||||
|
default:
|
||||||
|
panic("Invalid state change in RTCRtpTransceiver.setSending")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RTCPeerConnection) newRTCRtpTransceiver(
|
||||||
|
receiver *RTCRtpReceiver,
|
||||||
|
sender *RTCRtpSender,
|
||||||
|
direction RTCRtpTransceiverDirection,
|
||||||
|
) *RTCRtpTransceiver {
|
||||||
|
|
||||||
|
t := &RTCRtpTransceiver{
|
||||||
|
Receiver: receiver,
|
||||||
|
Sender: sender,
|
||||||
|
Direction: direction,
|
||||||
|
}
|
||||||
|
r.rtpTransceivers = append(r.rtpTransceivers, t)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop irreversibly stops the RTCRtpTransceiver
|
||||||
|
func (t *RTCRtpTransceiver) Stop() error {
|
||||||
|
panic("TODO")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTCSample contains media, and the amount of samples in it
|
||||||
|
type RTCSample struct {
|
||||||
|
Data []byte
|
||||||
|
Samples uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTCTrack represents a track that is communicated
|
||||||
|
type RTCTrack struct {
|
||||||
|
PayloadType uint8
|
||||||
|
Kind RTCRtpCodecType
|
||||||
|
ID string
|
||||||
|
Label string
|
||||||
|
Ssrc uint32
|
||||||
|
Codec *RTCRtpCodec
|
||||||
|
Packets <-chan *rtp.Packet
|
||||||
|
Samples chan<- RTCSample
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRTCTrack is used to create a new RTCTrack
|
||||||
|
func (r *RTCPeerConnection) NewRTCTrack(payloadType uint8, id, label string) (*RTCTrack, error) {
|
||||||
|
codec, err := rtcMediaEngine.getCodec(payloadType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if codec.Payloader == nil {
|
||||||
|
return nil, errors.New("codec payloader not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInput := make(chan RTCSample, 15) // Is the buffering needed?
|
||||||
|
ssrc := rand.Uint32()
|
||||||
|
go func() {
|
||||||
|
packetizer := rtp.NewPacketizer(
|
||||||
|
1400,
|
||||||
|
payloadType,
|
||||||
|
ssrc,
|
||||||
|
codec.Payloader,
|
||||||
|
rtp.NewRandomSequencer(),
|
||||||
|
codec.ClockRate,
|
||||||
|
)
|
||||||
|
for {
|
||||||
|
in := <-trackInput
|
||||||
|
packets := packetizer.Packetize(in.Data, in.Samples)
|
||||||
|
for _, p := range packets {
|
||||||
|
for _, port := range r.ports {
|
||||||
|
port.Send(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
t := &RTCTrack{
|
||||||
|
PayloadType: payloadType,
|
||||||
|
Kind: codec.Type,
|
||||||
|
ID: id,
|
||||||
|
Label: label,
|
||||||
|
Ssrc: ssrc,
|
||||||
|
Codec: codec,
|
||||||
|
Samples: trackInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTrack adds a RTCTrack to the RTCPeerConnection
|
||||||
|
func (r *RTCPeerConnection) AddTrack(track *RTCTrack) (*RTCRtpSender, error) {
|
||||||
|
if r.IsClosed {
|
||||||
|
return nil, &InvalidStateError{Err: ErrConnectionClosed}
|
||||||
|
}
|
||||||
|
for _, tranceiver := range r.rtpTransceivers {
|
||||||
|
if tranceiver.Sender.Track == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if track.ID == tranceiver.Sender.Track.ID {
|
||||||
|
return nil, &InvalidAccessError{Err: ErrExistingTrack}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var tranciever *RTCRtpTransceiver
|
||||||
|
for _, t := range r.rtpTransceivers {
|
||||||
|
if !t.stopped &&
|
||||||
|
// t.Sender == nil && // TODO: check that the sender has never sent
|
||||||
|
t.Sender.Track == nil &&
|
||||||
|
t.Receiver.Track != nil &&
|
||||||
|
t.Receiver.Track.Kind == track.Kind {
|
||||||
|
tranciever = t
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tranciever != nil {
|
||||||
|
tranciever.setSendingTrack(track)
|
||||||
|
} else {
|
||||||
|
var receiver *RTCRtpReceiver
|
||||||
|
sender := newRTCRtpSender(track)
|
||||||
|
tranciever = r.newRTCRtpTransceiver(
|
||||||
|
receiver,
|
||||||
|
sender,
|
||||||
|
RTCRtpTransceiverDirectionSendonly,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
tranciever.Mid = track.Kind.String() // TODO: Mid generation
|
||||||
|
|
||||||
|
return tranciever.Sender, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSenders returns the RTCRtpSender that are currently attached to this RTCPeerConnection
|
||||||
|
func (r *RTCPeerConnection) GetSenders() []RTCRtpSender {
|
||||||
|
result := make([]RTCRtpSender, len(r.rtpTransceivers))
|
||||||
|
for i, tranceiver := range r.rtpTransceivers {
|
||||||
|
result[i] = *tranceiver.Sender
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReceivers returns the RTCRtpReceivers that are currently attached to this RTCPeerConnection
|
||||||
|
func (r *RTCPeerConnection) GetReceivers() []RTCRtpReceiver {
|
||||||
|
result := make([]RTCRtpReceiver, len(r.rtpTransceivers))
|
||||||
|
for i, tranceiver := range r.rtpTransceivers {
|
||||||
|
result[i] = *tranceiver.Receiver
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransceivers returns the RTCRtpTransceiver that are currently attached to this RTCPeerConnection
|
||||||
|
func (r *RTCPeerConnection) GetTransceivers() []RTCRtpTransceiver {
|
||||||
|
result := make([]RTCRtpTransceiver, len(r.rtpTransceivers))
|
||||||
|
for i, tranceiver := range r.rtpTransceivers {
|
||||||
|
result[i] = *tranceiver
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
210
mediaengine.go
Normal file
210
mediaengine.go
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
package webrtc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/pions/webrtc/internal/sdp"
|
||||||
|
"github.com/pions/webrtc/pkg/rtp"
|
||||||
|
"github.com/pions/webrtc/pkg/rtp/codecs"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PayloadTypes for the default codecs
|
||||||
|
const (
|
||||||
|
PayloadTypeOpus = 111
|
||||||
|
PayloadTypeVP8 = 96
|
||||||
|
PayloadTypeVP9 = 98
|
||||||
|
PayloadTypeH264 = 100
|
||||||
|
)
|
||||||
|
|
||||||
|
// Names for the default codecs
|
||||||
|
const (
|
||||||
|
Opus = "opus"
|
||||||
|
VP8 = "VP8"
|
||||||
|
VP9 = "VP9"
|
||||||
|
H264 = "H264"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rtcMediaEngine = &mediaEngine{}
|
||||||
|
|
||||||
|
// RegisterDefaultCodecs is a helper that registers the default codecs supported by pions-webrtc
|
||||||
|
func RegisterDefaultCodecs() {
|
||||||
|
RegisterCodec(NewRTCRtpOpusCodec(PayloadTypeOpus, 48000, 2))
|
||||||
|
RegisterCodec(NewRTCRtpVP8Codec(PayloadTypeVP8, 90000))
|
||||||
|
RegisterCodec(NewRTCRtpH264Codec(PayloadTypeH264, 90000))
|
||||||
|
RegisterCodec(NewRTCRtpVP9Codec(PayloadTypeVP9, 90000))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterCodec is used to register a codec
|
||||||
|
func RegisterCodec(codec *RTCRtpCodec) {
|
||||||
|
rtcMediaEngine.RegisterCodec(codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mediaEngine struct {
|
||||||
|
codecs []*RTCRtpCodec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mediaEngine) RegisterCodec(codec *RTCRtpCodec) uint8 {
|
||||||
|
// TODO: generate PayloadType if not set
|
||||||
|
m.codecs = append(m.codecs, codec)
|
||||||
|
return codec.PayloadType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mediaEngine) ClearCodecs() {
|
||||||
|
m.codecs = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mediaEngine) getCodec(payloadType uint8) (*RTCRtpCodec, error) {
|
||||||
|
for _, codec := range m.codecs {
|
||||||
|
if codec.PayloadType == payloadType {
|
||||||
|
return codec, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("Codec not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mediaEngine) getCodecSDP(sdpCodec sdp.Codec) (*RTCRtpCodec, error) {
|
||||||
|
for _, codec := range m.codecs {
|
||||||
|
if codec.Name == sdpCodec.Name &&
|
||||||
|
codec.ClockRate == sdpCodec.ClockRate &&
|
||||||
|
(sdpCodec.EncodingParameters == "" ||
|
||||||
|
strconv.Itoa(int(codec.Channels)) == sdpCodec.EncodingParameters) &&
|
||||||
|
codec.SdpFmtpLine == sdpCodec.Fmtp { // TODO: Protocol specific matching?
|
||||||
|
return codec, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("Codec not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mediaEngine) getCodecsByKind(kind RTCRtpCodecType) []*RTCRtpCodec {
|
||||||
|
var codecs []*RTCRtpCodec
|
||||||
|
for _, codec := range rtcMediaEngine.codecs {
|
||||||
|
if codec.Type == kind {
|
||||||
|
codecs = append(codecs, codec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return codecs
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRTCRtpOpusCodec is a helper to create an Opus codec
|
||||||
|
func NewRTCRtpOpusCodec(payloadType uint8, clockrate uint32, channels uint16) *RTCRtpCodec {
|
||||||
|
c := NewRTCRtpCodec(RTCRtpCodecTypeAudio,
|
||||||
|
Opus,
|
||||||
|
clockrate,
|
||||||
|
channels,
|
||||||
|
"minptime=10;useinbandfec=1",
|
||||||
|
payloadType,
|
||||||
|
&codecs.OpusPayloader{})
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRTCRtpVP8Codec is a helper to create an VP8 codec
|
||||||
|
func NewRTCRtpVP8Codec(payloadType uint8, clockrate uint32) *RTCRtpCodec {
|
||||||
|
c := NewRTCRtpCodec(RTCRtpCodecTypeVideo,
|
||||||
|
VP8,
|
||||||
|
clockrate,
|
||||||
|
0,
|
||||||
|
"",
|
||||||
|
payloadType,
|
||||||
|
&codecs.VP8Payloader{})
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRTCRtpVP9Codec is a helper to create an VP9 codec
|
||||||
|
func NewRTCRtpVP9Codec(payloadType uint8, clockrate uint32) *RTCRtpCodec {
|
||||||
|
c := NewRTCRtpCodec(RTCRtpCodecTypeVideo,
|
||||||
|
VP9,
|
||||||
|
clockrate,
|
||||||
|
0,
|
||||||
|
"",
|
||||||
|
payloadType,
|
||||||
|
nil) // TODO
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRTCRtpH264Codec is a helper to create an H264 codec
|
||||||
|
func NewRTCRtpH264Codec(payloadType uint8, clockrate uint32) *RTCRtpCodec {
|
||||||
|
c := NewRTCRtpCodec(RTCRtpCodecTypeVideo,
|
||||||
|
H264,
|
||||||
|
clockrate,
|
||||||
|
0,
|
||||||
|
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
|
||||||
|
payloadType,
|
||||||
|
&codecs.H264Payloader{})
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTCRtpCodecType determines the type of a codec
|
||||||
|
type RTCRtpCodecType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
|
||||||
|
// RTCRtpCodecTypeAudio indicates this is an audio codec
|
||||||
|
RTCRtpCodecTypeAudio RTCRtpCodecType = iota + 1
|
||||||
|
|
||||||
|
// RTCRtpCodecTypeVideo indicates this is a video codec
|
||||||
|
RTCRtpCodecTypeVideo
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t RTCRtpCodecType) String() string {
|
||||||
|
switch t {
|
||||||
|
case RTCRtpCodecTypeAudio:
|
||||||
|
return "audio"
|
||||||
|
case RTCRtpCodecTypeVideo:
|
||||||
|
return "video"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTCRtpCodec represents a codec supported by the PeerConnection
|
||||||
|
type RTCRtpCodec struct {
|
||||||
|
RTCRtpCodecCapability
|
||||||
|
Type RTCRtpCodecType
|
||||||
|
Name string
|
||||||
|
PayloadType uint8
|
||||||
|
Payloader rtp.Payloader
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRTCRtpCodec is used to define a new codec
|
||||||
|
func NewRTCRtpCodec(
|
||||||
|
typ RTCRtpCodecType,
|
||||||
|
name string,
|
||||||
|
clockrate uint32,
|
||||||
|
channels uint16,
|
||||||
|
fmtp string,
|
||||||
|
payloadType uint8,
|
||||||
|
payloader rtp.Payloader,
|
||||||
|
) *RTCRtpCodec {
|
||||||
|
return &RTCRtpCodec{
|
||||||
|
RTCRtpCodecCapability: RTCRtpCodecCapability{
|
||||||
|
MimeType: typ.String() + "/" + name,
|
||||||
|
ClockRate: clockrate,
|
||||||
|
Channels: channels,
|
||||||
|
SdpFmtpLine: fmtp,
|
||||||
|
},
|
||||||
|
PayloadType: payloadType,
|
||||||
|
Payloader: payloader,
|
||||||
|
Type: typ,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTCRtpCodecCapability provides information about codec capabilities.
|
||||||
|
type RTCRtpCodecCapability struct {
|
||||||
|
MimeType string
|
||||||
|
ClockRate uint32
|
||||||
|
Channels uint16
|
||||||
|
SdpFmtpLine string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTCRtpHeaderExtensionCapability is used to define a RFC5285 RTP header extension supported by the codec.
|
||||||
|
type RTCRtpHeaderExtensionCapability struct {
|
||||||
|
URI string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTCRtpCapabilities represents the capabilities of a transceiver
|
||||||
|
type RTCRtpCapabilities struct {
|
||||||
|
Codecs []RTCRtpCodecCapability
|
||||||
|
HeaderExtensions []RTCRtpHeaderExtensionCapability
|
||||||
|
}
|
179
pkg/ice/address.go
Normal file
179
pkg/ice/address.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package ice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: Migrate address parsing to STUN/TURN packages?
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrServerType indicates the server type could not be parsed
|
||||||
|
ErrServerType = errors.New("unknown server type")
|
||||||
|
|
||||||
|
// ErrSTUNQuery indicates query arguments are provided in a STUN URL
|
||||||
|
ErrSTUNQuery = errors.New("queries not supported in stun address")
|
||||||
|
|
||||||
|
// ErrInvalidQuery indicates an unsupported query is provided
|
||||||
|
ErrInvalidQuery = errors.New("invalid query")
|
||||||
|
|
||||||
|
// ErrTransportType indicates an unsupported transport type was provided
|
||||||
|
ErrTransportType = errors.New("invalid transport type")
|
||||||
|
|
||||||
|
// ErrHost indicates the server hostname could not be parsed
|
||||||
|
ErrHost = errors.New("invalid hostname")
|
||||||
|
|
||||||
|
// ErrPort indicates the server port could not be parsed
|
||||||
|
ErrPort = errors.New("invalid port")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServerType indicates the type of server used
|
||||||
|
type ServerType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ServerTypeSTUN indicates the URL represents a STUN server
|
||||||
|
ServerTypeSTUN ServerType = iota + 1
|
||||||
|
|
||||||
|
// ServerTypeTURN indicates the URL represents a TURN server
|
||||||
|
ServerTypeTURN
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t ServerType) String() string {
|
||||||
|
switch t {
|
||||||
|
case ServerTypeSTUN:
|
||||||
|
return "stun"
|
||||||
|
case ServerTypeTURN:
|
||||||
|
return "turn"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransportType indicates the transport that is used
|
||||||
|
type TransportType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TransportUDP indicates the URL uses a UDP transport
|
||||||
|
TransportUDP TransportType = iota + 1
|
||||||
|
|
||||||
|
// TransportTCP indicates the URL uses a TCP transport
|
||||||
|
TransportTCP
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t TransportType) String() string {
|
||||||
|
switch t {
|
||||||
|
case TransportUDP:
|
||||||
|
return "udp"
|
||||||
|
case TransportTCP:
|
||||||
|
return "tcp"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL represents a STUN (rfc7064) or TRUN (rfc7065) URL
|
||||||
|
type URL struct {
|
||||||
|
Type ServerType
|
||||||
|
Secure bool
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
TransportType TransportType
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewURL creates a new URL by parsing a STUN (rfc7064) or TRUN (rfc7065) uri string
|
||||||
|
func NewURL(address string) (URL, error) {
|
||||||
|
var result URL
|
||||||
|
|
||||||
|
var scheme string
|
||||||
|
scheme, address = split(address, ":")
|
||||||
|
|
||||||
|
switch strings.ToLower(scheme) {
|
||||||
|
case "stun":
|
||||||
|
result.Type = ServerTypeSTUN
|
||||||
|
result.Secure = false
|
||||||
|
|
||||||
|
case "stuns":
|
||||||
|
result.Type = ServerTypeSTUN
|
||||||
|
result.Secure = true
|
||||||
|
|
||||||
|
case "turn":
|
||||||
|
result.Type = ServerTypeTURN
|
||||||
|
result.Secure = false
|
||||||
|
|
||||||
|
case "turns":
|
||||||
|
result.Type = ServerTypeTURN
|
||||||
|
result.Secure = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
return result, ErrServerType
|
||||||
|
}
|
||||||
|
|
||||||
|
var query string
|
||||||
|
address, query = split(address, "?")
|
||||||
|
|
||||||
|
if query != "" {
|
||||||
|
if result.Type == ServerTypeSTUN {
|
||||||
|
return result, ErrSTUNQuery
|
||||||
|
}
|
||||||
|
key, value := split(query, "=")
|
||||||
|
if strings.ToLower(key) != "transport" {
|
||||||
|
return result, ErrInvalidQuery
|
||||||
|
}
|
||||||
|
switch strings.ToLower(value) {
|
||||||
|
case "udp":
|
||||||
|
result.TransportType = TransportUDP
|
||||||
|
case "tcp":
|
||||||
|
result.TransportType = TransportTCP
|
||||||
|
default:
|
||||||
|
return result, ErrTransportType
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if result.Secure {
|
||||||
|
result.TransportType = TransportTCP
|
||||||
|
} else {
|
||||||
|
result.TransportType = TransportUDP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var host string
|
||||||
|
var port string
|
||||||
|
colon := strings.IndexByte(address, ':')
|
||||||
|
if colon == -1 {
|
||||||
|
host = address
|
||||||
|
if result.Secure {
|
||||||
|
port = "5349"
|
||||||
|
} else {
|
||||||
|
port = "3478"
|
||||||
|
}
|
||||||
|
} else if i := strings.IndexByte(address, ']'); i != -1 {
|
||||||
|
host = strings.TrimPrefix(address[:i], "[")
|
||||||
|
port = address[i+1+len(":"):]
|
||||||
|
log.Println(port)
|
||||||
|
} else {
|
||||||
|
host = address[:colon]
|
||||||
|
port = address[colon+len(":"):]
|
||||||
|
}
|
||||||
|
if host == "" {
|
||||||
|
return result, ErrHost
|
||||||
|
}
|
||||||
|
result.Host = strings.ToLower(host)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
result.Port, err = strconv.Atoi(port)
|
||||||
|
if err != nil {
|
||||||
|
return result, ErrPort
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func split(s string, c string) (string, string) {
|
||||||
|
i := strings.Index(s, c)
|
||||||
|
if i < 0 {
|
||||||
|
return s, ""
|
||||||
|
}
|
||||||
|
return s[:i], s[i+len(c):]
|
||||||
|
}
|
68
pkg/ice/address_test.go
Normal file
68
pkg/ice/address_test.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package ice
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestNewURL(t *testing.T) {
|
||||||
|
t.Run("Success", func(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
rawURL string
|
||||||
|
expectedType ServerType
|
||||||
|
expectedSecure bool
|
||||||
|
expectedHost string
|
||||||
|
expectedPort int
|
||||||
|
expectedTransportType TransportType
|
||||||
|
}{
|
||||||
|
{"stun:google.de", ServerTypeSTUN, false, "google.de", 3478, TransportUDP},
|
||||||
|
{"stun:google.de:1234", ServerTypeSTUN, false, "google.de", 1234, TransportUDP},
|
||||||
|
{"stuns:google.de", ServerTypeSTUN, true, "google.de", 5349, TransportTCP},
|
||||||
|
{"stun:[::1]:123", ServerTypeSTUN, false, "::1", 123, TransportUDP},
|
||||||
|
{"turn:google.de", ServerTypeTURN, false, "google.de", 3478, TransportUDP},
|
||||||
|
{"turns:google.de", ServerTypeTURN, true, "google.de", 5349, TransportTCP},
|
||||||
|
{"turn:google.de?transport=udp", ServerTypeTURN, false, "google.de", 3478, TransportUDP},
|
||||||
|
{"turn:google.de?transport=tcp", ServerTypeTURN, false, "google.de", 3478, TransportTCP},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
url, err := NewURL(testCase.rawURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: got error: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if url.Type != testCase.expectedType ||
|
||||||
|
url.Secure != testCase.expectedSecure ||
|
||||||
|
url.Host != testCase.expectedHost ||
|
||||||
|
url.Port != testCase.expectedPort ||
|
||||||
|
url.TransportType != testCase.expectedTransportType {
|
||||||
|
t.Errorf("Case %d: got %s %t %s %d %s",
|
||||||
|
i,
|
||||||
|
url.Type,
|
||||||
|
url.Secure,
|
||||||
|
url.Host,
|
||||||
|
url.Port,
|
||||||
|
url.TransportType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Failure", func(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
rawURL string
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{"", ErrServerType},
|
||||||
|
{":::", ErrServerType},
|
||||||
|
{"google.de", ErrServerType},
|
||||||
|
{"stun:", ErrHost},
|
||||||
|
{"stun:google.de:abc", ErrPort},
|
||||||
|
{"stun:google.de?transport=udp", ErrSTUNQuery},
|
||||||
|
{"turn:google.de?trans=udp", ErrInvalidQuery},
|
||||||
|
{"turn:google.de?transport=ip", ErrTransportType},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
if _, err := NewURL(testCase.rawURL); err != testCase.expectedErr {
|
||||||
|
t.Errorf("Case %d: got error '%v' expected '%v'", i, err, testCase.expectedErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
23
pkg/ice/agent.go
Normal file
23
pkg/ice/agent.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package ice
|
||||||
|
|
||||||
|
import "github.com/pions/webrtc/internal/util"
|
||||||
|
|
||||||
|
// Agent represents the ICE agent
|
||||||
|
type Agent struct {
|
||||||
|
Servers [][]URL
|
||||||
|
Ufrag string
|
||||||
|
Pwd string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAgent creates a new Agent
|
||||||
|
func NewAgent() *Agent {
|
||||||
|
return &Agent{
|
||||||
|
Ufrag: util.RandSeq(16),
|
||||||
|
Pwd: util.RandSeq(32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetServers is used to set the ICE servers used by the Agent
|
||||||
|
func (a *Agent) SetServers(urls [][]URL) {
|
||||||
|
a.Servers = urls
|
||||||
|
}
|
@@ -7,49 +7,76 @@ type ConnectionState int
|
|||||||
|
|
||||||
// List of supported States
|
// List of supported States
|
||||||
const (
|
const (
|
||||||
// New ICE agent is gathering addresses
|
// ConnectionStateNew ICE agent is gathering addresses
|
||||||
New = iota + 1
|
ConnectionStateNew = iota + 1
|
||||||
|
|
||||||
// Checking ICE agent has been given local and remote candidates, and is attempting to find a match
|
// ConnectionStateChecking ICE agent has been given local and remote candidates, and is attempting to find a match
|
||||||
Checking
|
ConnectionStateChecking
|
||||||
|
|
||||||
// Connected ICE agent has a pairing, but is still checking other pairs
|
// ConnectionStateConnected ICE agent has a pairing, but is still checking other pairs
|
||||||
Connected
|
ConnectionStateConnected
|
||||||
|
|
||||||
// Completed ICE agent has finished
|
// ConnectionStateCompleted ICE agent has finished
|
||||||
Completed
|
ConnectionStateCompleted
|
||||||
|
|
||||||
// Failed ICE agent never could sucessfully connect
|
// ConnectionStateFailed ICE agent never could sucessfully connect
|
||||||
Failed
|
ConnectionStateFailed
|
||||||
|
|
||||||
// Failed ICE agent connected sucessfully, but has entered a failed state
|
// ConnectionStateDisconnected ICE agent connected sucessfully, but has entered a failed state
|
||||||
Disconnected
|
ConnectionStateDisconnected
|
||||||
|
|
||||||
// Closed ICE agent has finished and is no longer handling requests
|
// ConnectionStateClosed ICE agent has finished and is no longer handling requests
|
||||||
Closed
|
ConnectionStateClosed
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c ConnectionState) String() string {
|
func (c ConnectionState) String() string {
|
||||||
switch c {
|
switch c {
|
||||||
case New:
|
case ConnectionStateNew:
|
||||||
return "New"
|
return "New"
|
||||||
case Checking:
|
case ConnectionStateChecking:
|
||||||
return "Checking"
|
return "Checking"
|
||||||
case Connected:
|
case ConnectionStateConnected:
|
||||||
return "Connected"
|
return "Connected"
|
||||||
case Completed:
|
case ConnectionStateCompleted:
|
||||||
return "Completed"
|
return "Completed"
|
||||||
case Failed:
|
case ConnectionStateFailed:
|
||||||
return "Failed"
|
return "Failed"
|
||||||
case Disconnected:
|
case ConnectionStateDisconnected:
|
||||||
return "Disconnected"
|
return "Disconnected"
|
||||||
case Closed:
|
case ConnectionStateClosed:
|
||||||
return "Closed"
|
return "Closed"
|
||||||
default:
|
default:
|
||||||
return "Invalid"
|
return "Invalid"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GatheringState describes the state of the candidate gathering process
|
||||||
|
type GatheringState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// GatheringStateNew indicates candidate gatering is not yet started
|
||||||
|
GatheringStateNew GatheringState = iota + 1
|
||||||
|
|
||||||
|
// GatheringStateGathering indicates candidate gatering is ongoing
|
||||||
|
GatheringStateGathering
|
||||||
|
|
||||||
|
// GatheringStateComplete indicates candidate gatering has been completed
|
||||||
|
GatheringStateComplete
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t GatheringState) String() string {
|
||||||
|
switch t {
|
||||||
|
case GatheringStateNew:
|
||||||
|
return "new"
|
||||||
|
case GatheringStateGathering:
|
||||||
|
return "gathering"
|
||||||
|
case GatheringStateComplete:
|
||||||
|
return "complete"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// HostInterfaces generates a slice of all the IPs associated with interfaces
|
// HostInterfaces generates a slice of all the IPs associated with interfaces
|
||||||
func HostInterfaces() (ips []string) {
|
func HostInterfaces() (ips []string) {
|
||||||
ifaces, err := net.Interfaces()
|
ifaces, err := net.Interfaces()
|
||||||
|
@@ -1,68 +1,133 @@
|
|||||||
package webrtc
|
package webrtc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"time"
|
||||||
"strings"
|
|
||||||
|
"github.com/pions/webrtc/pkg/ice"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RTCCredentialType specifies the type of credentials provided
|
// RTCICECredentialType indicates the type of credentials used to connect to an ICE server
|
||||||
type RTCCredentialType string
|
type RTCICECredentialType int
|
||||||
|
|
||||||
var (
|
const (
|
||||||
// RTCCredentialTypePassword describes username+pasword based credentials
|
// RTCICECredentialTypePassword describes username+pasword based credentials
|
||||||
RTCCredentialTypePassword RTCCredentialType = "password"
|
RTCICECredentialTypePassword RTCICECredentialType = iota + 1
|
||||||
// RTCCredentialTypeToken describes token based credentials
|
// RTCICECredentialTypeOauth describes token based credentials
|
||||||
RTCCredentialTypeToken RTCCredentialType = "token"
|
RTCICECredentialTypeOauth
|
||||||
)
|
)
|
||||||
|
|
||||||
// RTCServerType is used to identify different ICE server types
|
func (t RTCICECredentialType) String() string {
|
||||||
type RTCServerType string
|
switch t {
|
||||||
|
case RTCICECredentialTypePassword:
|
||||||
|
return "password"
|
||||||
|
case RTCICECredentialTypeOauth:
|
||||||
|
return "oauth"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
// RTCCertificate represents a certificate used to authenticate WebRTC communications.
|
||||||
// RTCServerTypeSTUN is used to identify STUN servers. Prefix is stun:
|
type RTCCertificate struct {
|
||||||
RTCServerTypeSTUN RTCServerType = "STUN"
|
expires time.Time
|
||||||
// RTCServerTypeTURN is used to identify TURN servers. Prefix is turn:
|
// TODO: Finish during DTLS implementation
|
||||||
RTCServerTypeTURN RTCServerType = "TURN"
|
}
|
||||||
// RTCServerTypeUnknown is used when an ICE server can not be identified properly.
|
|
||||||
RTCServerTypeUnknown RTCServerType = "UnknownType"
|
// Equals determines if two certificates are identical
|
||||||
)
|
func (c RTCCertificate) Equals(other RTCCertificate) bool {
|
||||||
|
return c.expires == other.expires
|
||||||
|
}
|
||||||
|
|
||||||
// RTCICEServer describes a single ICE server, as well as required credentials
|
// RTCICEServer describes a single ICE server, as well as required credentials
|
||||||
type RTCICEServer struct {
|
type RTCICEServer struct {
|
||||||
CredentialType RTCCredentialType
|
|
||||||
URLs []string
|
URLs []string
|
||||||
Username string
|
Username string
|
||||||
Credential string
|
Credential RTCICECredential
|
||||||
|
CredentialType RTCICECredentialType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RTCICECredential represents credentials used to connect to an ICE server
|
||||||
|
// Two types of credentials are supported:
|
||||||
|
// - Password (type string)
|
||||||
|
// - Password (type RTCOAuthCredential)
|
||||||
|
type RTCICECredential interface{}
|
||||||
|
|
||||||
|
// RTCOAuthCredential represents OAuth credentials used to connect to an ICE server
|
||||||
|
type RTCOAuthCredential struct {
|
||||||
|
MacKey string
|
||||||
|
AccessToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTCICETransportPolicy defines the ICE candidate policy [JSEP] (section 3.5.3.) used to surface the permitted candidates
|
||||||
|
type RTCICETransportPolicy int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
stunPrefix = "stun:"
|
// RTCICETransportPolicyRelay indicates only media relay candidates such as candidates passing through a TURN server are used
|
||||||
stunsPrefix = "stuns:"
|
RTCICETransportPolicyRelay RTCICETransportPolicy = iota + 1
|
||||||
turnPrefix = "turn:"
|
|
||||||
turnsPrefix = "turns:"
|
// RTCICETransportPolicyAll indicates any type of candidate is used
|
||||||
|
RTCICETransportPolicyAll
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c RTCICEServer) serverType() RTCServerType {
|
func (t RTCICETransportPolicy) String() string {
|
||||||
for _, url := range c.URLs {
|
switch t {
|
||||||
if strings.HasPrefix(url, stunPrefix) || strings.HasPrefix(url, stunsPrefix) {
|
case RTCICETransportPolicyRelay:
|
||||||
return RTCServerTypeSTUN
|
return "relay"
|
||||||
|
case RTCICETransportPolicyAll:
|
||||||
|
return "all"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(url, turnPrefix) || strings.HasPrefix(url, turnsPrefix) {
|
|
||||||
return RTCServerTypeTURN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return RTCServerTypeUnknown
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func protocolAndHost(url string) (string, string, error) {
|
// RTCBundlePolicy affects which media tracks are negotiated if the remote endpoint is not bundle-aware, and what ICE candidates are gathered.
|
||||||
if strings.HasPrefix(url, stunPrefix) {
|
type RTCBundlePolicy int
|
||||||
return "udp", url[len(stunPrefix):], nil
|
|
||||||
|
const (
|
||||||
|
|
||||||
|
// RTCRtcpMuxPolicyBalanced indicates to gather ICE candidates for each media type in use (audio, video, and data).
|
||||||
|
RTCRtcpMuxPolicyBalanced RTCBundlePolicy = iota + 1
|
||||||
|
|
||||||
|
// RTCRtcpMuxPolicyMaxCompat indicates to gather ICE candidates for each track.
|
||||||
|
RTCRtcpMuxPolicyMaxCompat
|
||||||
|
|
||||||
|
// RTCRtcpMuxPolicyMaxBundle indicates to gather ICE candidates for only one track.
|
||||||
|
RTCRtcpMuxPolicyMaxBundle
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t RTCBundlePolicy) String() string {
|
||||||
|
switch t {
|
||||||
|
case RTCRtcpMuxPolicyBalanced:
|
||||||
|
return "balanced"
|
||||||
|
case RTCRtcpMuxPolicyMaxCompat:
|
||||||
|
return "max-compat"
|
||||||
|
case RTCRtcpMuxPolicyMaxBundle:
|
||||||
|
return "max-bundle"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(url, stunsPrefix) {
|
}
|
||||||
return "tcp", url[len(stunsPrefix):], nil
|
|
||||||
|
// RTCRtcpMuxPolicy affects what ICE candidates are gathered to support non-multiplexed RTCP
|
||||||
|
type RTCRtcpMuxPolicy int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RTCRtcpMuxPolicyNegotiate indicates to gather ICE candidates for both RTP and RTCP candidates.
|
||||||
|
RTCRtcpMuxPolicyNegotiate RTCRtcpMuxPolicy = iota + 1
|
||||||
|
|
||||||
|
// RTCRtcpMuxPolicyRequire indicates to gather ICE candidates only for RTP and multiplex RTCP on the RTP candidates
|
||||||
|
RTCRtcpMuxPolicyRequire
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t RTCRtcpMuxPolicy) String() string {
|
||||||
|
switch t {
|
||||||
|
case RTCRtcpMuxPolicyNegotiate:
|
||||||
|
return "negotiate"
|
||||||
|
case RTCRtcpMuxPolicyRequire:
|
||||||
|
return "require"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
}
|
}
|
||||||
// TODO TURN urls
|
|
||||||
return "", "", fmt.Errorf("Unknown protocol in URL %q", url)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RTCConfiguration contains RTCPeerConfiguration options
|
// RTCConfiguration contains RTCPeerConfiguration options
|
||||||
@@ -71,4 +136,152 @@ type RTCConfiguration struct {
|
|||||||
// these are typically STUN and/or TURN servers. If this isn't specified, the ICE agent may choose to use its own ICE servers;
|
// these are typically STUN and/or TURN servers. If this isn't specified, the ICE agent may choose to use its own ICE servers;
|
||||||
// otherwise, the connection attempt will be made with no STUN or TURN server available, which limits the connection to local peers.
|
// otherwise, the connection attempt will be made with no STUN or TURN server available, which limits the connection to local peers.
|
||||||
ICEServers []RTCICEServer
|
ICEServers []RTCICEServer
|
||||||
|
ICETransportPolicy RTCICETransportPolicy
|
||||||
|
BundlePolicy RTCBundlePolicy
|
||||||
|
RtcpMuxPolicy RTCRtcpMuxPolicy
|
||||||
|
PeerIdentity string
|
||||||
|
Certificates []RTCCertificate
|
||||||
|
ICECandidatePoolSize uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfiguration updates the configuration of the RTCPeerConnection
|
||||||
|
func (r *RTCPeerConnection) SetConfiguration(config RTCConfiguration) error {
|
||||||
|
err := r.validatePeerIdentity(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = r.validateCertificates(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = r.validateBundlePolicy(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = r.validateRtcpMuxPolicy(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = r.validateICECandidatePoolSize(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.setICEServers(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.config = config
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RTCPeerConnection) validatePeerIdentity(config RTCConfiguration) error {
|
||||||
|
current := r.config
|
||||||
|
if current.PeerIdentity != "" &&
|
||||||
|
config.PeerIdentity != "" &&
|
||||||
|
config.PeerIdentity != current.PeerIdentity {
|
||||||
|
return &InvalidModificationError{Err: ErrModPeerIdentity}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RTCPeerConnection) validateCertificates(config RTCConfiguration) error {
|
||||||
|
current := r.config
|
||||||
|
if len(current.Certificates) > 0 &&
|
||||||
|
len(config.Certificates) > 0 {
|
||||||
|
if len(config.Certificates) != len(current.Certificates) {
|
||||||
|
return &InvalidModificationError{Err: ErrModCertificates}
|
||||||
|
}
|
||||||
|
for i, cert := range config.Certificates {
|
||||||
|
if !current.Certificates[i].Equals(cert) {
|
||||||
|
return &InvalidModificationError{Err: ErrModCertificates}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for _, cert := range config.Certificates {
|
||||||
|
if now.After(cert.expires) {
|
||||||
|
return &InvalidAccessError{Err: ErrCertificateExpired}
|
||||||
|
}
|
||||||
|
// TODO: Check certificate 'origin'
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RTCPeerConnection) validateBundlePolicy(config RTCConfiguration) error {
|
||||||
|
current := r.config
|
||||||
|
if config.BundlePolicy != current.BundlePolicy {
|
||||||
|
return &InvalidModificationError{Err: ErrModRtcpMuxPolicy}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RTCPeerConnection) validateRtcpMuxPolicy(config RTCConfiguration) error {
|
||||||
|
current := r.config
|
||||||
|
if config.RtcpMuxPolicy != current.RtcpMuxPolicy {
|
||||||
|
return &InvalidModificationError{Err: ErrModRtcpMuxPolicy}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RTCPeerConnection) validateICECandidatePoolSize(config RTCConfiguration) error {
|
||||||
|
current := r.config
|
||||||
|
if r.LocalDescription != nil &&
|
||||||
|
config.ICECandidatePoolSize != current.ICECandidatePoolSize {
|
||||||
|
return &InvalidModificationError{Err: ErrModIceCandidatePoolSize}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RTCPeerConnection) setICEServers(config RTCConfiguration) error {
|
||||||
|
if len(config.ICEServers) > 0 {
|
||||||
|
var servers [][]ice.URL
|
||||||
|
for _, server := range config.ICEServers {
|
||||||
|
var urls []ice.URL
|
||||||
|
for _, rawURL := range server.URLs {
|
||||||
|
url, err := parseICEServer(server, rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
urls = append(urls, url)
|
||||||
|
}
|
||||||
|
if len(urls) > 0 {
|
||||||
|
servers = append(servers, urls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(servers) > 0 {
|
||||||
|
r.iceAgent.SetServers(servers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseICEServer(server RTCICEServer, rawURL string) (ice.URL, error) {
|
||||||
|
iceurl, err := ice.NewURL(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return iceurl, &SyntaxError{Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, isPass := server.Credential.(string)
|
||||||
|
_, isOauth := server.Credential.(RTCOAuthCredential)
|
||||||
|
noPass := !isPass && !isOauth
|
||||||
|
|
||||||
|
if iceurl.Type == ice.ServerTypeTURN {
|
||||||
|
if server.Username == "" ||
|
||||||
|
noPass {
|
||||||
|
return iceurl, &InvalidAccessError{Err: ErrNoTurnCred}
|
||||||
|
}
|
||||||
|
if server.CredentialType == RTCICECredentialTypePassword &&
|
||||||
|
!isPass {
|
||||||
|
return iceurl, &InvalidAccessError{Err: ErrTurnCred}
|
||||||
|
}
|
||||||
|
if server.CredentialType == RTCICECredentialTypeOauth &&
|
||||||
|
!isOauth {
|
||||||
|
return iceurl, &InvalidAccessError{Err: ErrTurnCred}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return iceurl, nil
|
||||||
}
|
}
|
||||||
|
@@ -1,44 +0,0 @@
|
|||||||
package webrtc
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestRTCICEServer_isStun(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
expectedType RTCServerType
|
|
||||||
server RTCICEServer
|
|
||||||
}{
|
|
||||||
{RTCServerTypeSTUN, RTCICEServer{URLs: []string{"stun:google.de"}}},
|
|
||||||
{RTCServerTypeTURN, RTCICEServer{URLs: []string{"turn:google.de"}}},
|
|
||||||
{RTCServerTypeUnknown, RTCICEServer{URLs: []string{"google.de"}}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
if serverType := testCase.server.serverType(); serverType != testCase.expectedType {
|
|
||||||
t.Errorf("Expected %q to be %s, but got %s", testCase.server.URLs, testCase.expectedType, serverType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPortAndHost(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
url string
|
|
||||||
expectedHost string
|
|
||||||
expectedProtocol string
|
|
||||||
}{
|
|
||||||
{"stun:stun.l.google.com:19302", "stun.l.google.com:19302", "udp"},
|
|
||||||
{"stuns:stun.l.google.com:19302", "stun.l.google.com:19302", "tcp"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
proto, host, err := protocolAndHost(testCase.url)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get proto and host: %v", err)
|
|
||||||
}
|
|
||||||
if proto != testCase.expectedProtocol {
|
|
||||||
t.Fatalf("expected %s, got %s", testCase.expectedProtocol, proto)
|
|
||||||
}
|
|
||||||
if host != testCase.expectedHost {
|
|
||||||
t.Fatalf("expected %s, got %s", testCase.expectedHost, host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -3,18 +3,14 @@ package webrtc
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pions/pkg/stun"
|
|
||||||
"github.com/pions/webrtc/internal/dtls"
|
"github.com/pions/webrtc/internal/dtls"
|
||||||
"github.com/pions/webrtc/internal/network"
|
"github.com/pions/webrtc/internal/network"
|
||||||
"github.com/pions/webrtc/internal/sdp"
|
"github.com/pions/webrtc/internal/sdp"
|
||||||
"github.com/pions/webrtc/internal/util"
|
|
||||||
"github.com/pions/webrtc/pkg/ice"
|
"github.com/pions/webrtc/pkg/ice"
|
||||||
"github.com/pions/webrtc/pkg/rtp"
|
"github.com/pions/webrtc/pkg/rtp"
|
||||||
"github.com/pions/webrtc/pkg/rtp/codecs"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@@ -23,212 +19,119 @@ func init() {
|
|||||||
rand.Seed(time.Now().UTC().UnixNano())
|
rand.Seed(time.Now().UTC().UnixNano())
|
||||||
}
|
}
|
||||||
|
|
||||||
// RTCSample contains media, and the amount of samples in it
|
// RTCPeerConnectionState indicates the state of the peer connection
|
||||||
type RTCSample struct {
|
type RTCPeerConnectionState int
|
||||||
Data []byte
|
|
||||||
Samples uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// TrackType determines the type of media we are sending receiving
|
|
||||||
type TrackType int
|
|
||||||
|
|
||||||
// List of supported TrackTypes
|
|
||||||
const (
|
const (
|
||||||
VP8 TrackType = iota + 1
|
|
||||||
VP9
|
// RTCPeerConnectionStateNew indicates some of the ICE or DTLS transports are in status new
|
||||||
H264
|
RTCPeerConnectionStateNew RTCPeerConnectionState = iota + 1
|
||||||
Opus
|
|
||||||
|
// RTCPeerConnectionStateConnecting indicates some of the ICE or DTLS transports are in status connecting or checking
|
||||||
|
RTCPeerConnectionStateConnecting
|
||||||
|
|
||||||
|
// RTCPeerConnectionStateConnected indicates all of the ICE or DTLS transports are in status connected or completed
|
||||||
|
RTCPeerConnectionStateConnected
|
||||||
|
|
||||||
|
// RTCPeerConnectionStateDisconnected indicates some of the ICE or DTLS transports are in status disconnected
|
||||||
|
RTCPeerConnectionStateDisconnected
|
||||||
|
|
||||||
|
// RTCPeerConnectionStateFailed indicates some of the ICE or DTLS transports are in status failed
|
||||||
|
RTCPeerConnectionStateFailed
|
||||||
|
|
||||||
|
// RTCPeerConnectionStateClosed indicates the peer connection is closed
|
||||||
|
RTCPeerConnectionStateClosed
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t TrackType) String() string {
|
func (t RTCPeerConnectionState) String() string {
|
||||||
switch t {
|
switch t {
|
||||||
case VP8:
|
case RTCPeerConnectionStateNew:
|
||||||
return "VP8"
|
return "new"
|
||||||
case VP9:
|
case RTCPeerConnectionStateConnecting:
|
||||||
return "VP9"
|
return "connecting"
|
||||||
case H264:
|
case RTCPeerConnectionStateConnected:
|
||||||
return "H264"
|
return "connected"
|
||||||
case Opus:
|
case RTCPeerConnectionStateDisconnected:
|
||||||
return "Opus"
|
return "disconnected"
|
||||||
|
case RTCPeerConnectionStateFailed:
|
||||||
|
return "failed"
|
||||||
|
case RTCPeerConnectionStateClosed:
|
||||||
|
return "closed"
|
||||||
default:
|
default:
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new RTCPeerConfiguration with the provided configuration
|
|
||||||
func New(config *RTCConfiguration) (*RTCPeerConnection, error) {
|
|
||||||
return &RTCPeerConnection{
|
|
||||||
config: config,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RTCPeerConnection represents a WebRTC connection between itself and a remote peer
|
// RTCPeerConnection represents a WebRTC connection between itself and a remote peer
|
||||||
type RTCPeerConnection struct {
|
type RTCPeerConnection struct {
|
||||||
Ontrack func(mediaType TrackType, buffers <-chan *rtp.Packet)
|
// ICE
|
||||||
LocalDescription *sdp.SessionDescription
|
|
||||||
OnICEConnectionStateChange func(iceConnectionState ice.ConnectionState)
|
OnICEConnectionStateChange func(iceConnectionState ice.ConnectionState)
|
||||||
|
|
||||||
config *RTCConfiguration
|
config RTCConfiguration
|
||||||
tlscfg *dtls.TLSCfg
|
tlscfg *dtls.TLSCfg
|
||||||
|
|
||||||
iceUfrag string
|
// ICE: TODO: Move to ICEAgent
|
||||||
icePwd string
|
iceAgent *ice.Agent
|
||||||
iceState ice.ConnectionState
|
iceState ice.ConnectionState
|
||||||
|
iceGatheringState ice.GatheringState
|
||||||
|
iceConnectionState ice.ConnectionState
|
||||||
|
|
||||||
portsLock sync.RWMutex
|
portsLock sync.RWMutex
|
||||||
ports []*network.Port
|
ports []*network.Port
|
||||||
|
|
||||||
|
// Signaling
|
||||||
|
// pendingLocalDescription *RTCSessionDescription
|
||||||
|
// currentLocalDescription *RTCSessionDescription
|
||||||
|
LocalDescription *sdp.SessionDescription
|
||||||
|
|
||||||
|
// pendingRemoteDescription *RTCSessionDescription
|
||||||
|
currentRemoteDescription *RTCSessionDescription
|
||||||
remoteDescription *sdp.SessionDescription
|
remoteDescription *sdp.SessionDescription
|
||||||
|
|
||||||
localTracks []*sdp.SessionBuilderTrack
|
idpLoginURL *string
|
||||||
|
|
||||||
|
IsClosed bool
|
||||||
|
NegotiationNeeded bool
|
||||||
|
|
||||||
|
// lastOffer string
|
||||||
|
// lastAnswer string
|
||||||
|
|
||||||
|
signalingState RTCSignalingState
|
||||||
|
connectionState RTCPeerConnectionState
|
||||||
|
|
||||||
|
// Media
|
||||||
|
rtpTransceivers []*RTCRtpTransceiver
|
||||||
|
Ontrack func(*RTCTrack)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new RTCPeerConfiguration with the provided configuration
|
||||||
|
func New(config RTCConfiguration) (*RTCPeerConnection, error) {
|
||||||
|
|
||||||
|
r := &RTCPeerConnection{
|
||||||
|
config: config,
|
||||||
|
signalingState: RTCSignalingStateStable,
|
||||||
|
iceAgent: ice.NewAgent(),
|
||||||
|
iceGatheringState: ice.GatheringStateNew,
|
||||||
|
iceConnectionState: ice.ConnectionStateNew,
|
||||||
|
connectionState: RTCPeerConnectionStateNew,
|
||||||
|
}
|
||||||
|
err := r.SetConfiguration(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.tlscfg = dtls.NewTLSCfg()
|
||||||
|
|
||||||
|
// TODO: Initialize ICE Agent
|
||||||
|
|
||||||
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public
|
// Public
|
||||||
|
|
||||||
// SetRemoteDescription sets the SessionDescription of the remote peer
|
// SetIdentityProvider is used to configure an identity provider to generate identity assertions
|
||||||
func (r *RTCPeerConnection) SetRemoteDescription(rawSessionDescription string) error {
|
func (r *RTCPeerConnection) SetIdentityProvider(provider string) error {
|
||||||
if r.remoteDescription != nil {
|
panic("TODO SetIdentityProvider")
|
||||||
return errors.Errorf("remoteDescription is already defined, SetRemoteDescription can only be called once")
|
|
||||||
}
|
|
||||||
|
|
||||||
r.remoteDescription = &sdp.SessionDescription{}
|
|
||||||
return r.remoteDescription.Unmarshal(rawSessionDescription)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateOffer starts the RTCPeerConnection and generates the localDescription
|
|
||||||
func (r *RTCPeerConnection) CreateOffer() error {
|
|
||||||
return errors.Errorf("CreateOffer is not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateAnswer starts the RTCPeerConnection and generates the localDescription
|
|
||||||
func (r *RTCPeerConnection) CreateAnswer() error {
|
|
||||||
if r.tlscfg != nil {
|
|
||||||
return errors.Errorf("tlscfg is already defined, CreateOffer can only be called once")
|
|
||||||
}
|
|
||||||
|
|
||||||
r.tlscfg = dtls.NewTLSCfg()
|
|
||||||
r.iceUfrag = util.RandSeq(16)
|
|
||||||
r.icePwd = util.RandSeq(32)
|
|
||||||
|
|
||||||
r.portsLock.Lock()
|
|
||||||
defer r.portsLock.Unlock()
|
|
||||||
|
|
||||||
candidates := []string{}
|
|
||||||
basePriority := uint16(rand.Uint32() & (1<<16 - 1))
|
|
||||||
for _, c := range ice.HostInterfaces() {
|
|
||||||
port, err := network.NewPort(c+":0", []byte(r.icePwd), r.tlscfg, r.generateChannel, r.iceStateChange)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
candidates = append(candidates, fmt.Sprintf("candidate:udpcandidate 1 udp %d %s %d typ host", basePriority, c, port.ListeningAddr.Port))
|
|
||||||
basePriority = basePriority + 1
|
|
||||||
r.ports = append(r.ports, port)
|
|
||||||
}
|
|
||||||
if r.config != nil {
|
|
||||||
for _, server := range r.config.ICEServers {
|
|
||||||
if server.serverType() != RTCServerTypeSTUN {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, iceURL := range server.URLs {
|
|
||||||
proto, host, err := protocolAndHost(iceURL)
|
|
||||||
// TODO if one of the URLs does not work we should just ignore it.
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "Failed to parse ICE URL")
|
|
||||||
}
|
|
||||||
// TODO Do we want the timeout to be configurable?
|
|
||||||
client, err := stun.NewClient(proto, host, time.Second*5)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "Failed to create STUN client")
|
|
||||||
}
|
|
||||||
localAddr, ok := client.LocalAddr().(*net.UDPAddr)
|
|
||||||
if !ok {
|
|
||||||
return errors.Errorf("Failed to cast STUN client to UDPAddr")
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.Request()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "Failed to make STUN request")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.Close(); err != nil {
|
|
||||||
return errors.Wrapf(err, "Failed to close STUN client")
|
|
||||||
}
|
|
||||||
|
|
||||||
attr, ok := resp.GetOneAttribute(stun.AttrXORMappedAddress)
|
|
||||||
if !ok {
|
|
||||||
return errors.Errorf("Got respond from STUN server that did not contain XORAddress")
|
|
||||||
}
|
|
||||||
|
|
||||||
var addr stun.XorAddress
|
|
||||||
if err := addr.Unpack(resp, attr); err != nil {
|
|
||||||
return errors.Wrapf(err, "Failed to unpack STUN XorAddress response")
|
|
||||||
}
|
|
||||||
|
|
||||||
port, err := network.NewPort(fmt.Sprintf("0.0.0.0:%d", localAddr.Port), []byte(r.icePwd), r.tlscfg, r.generateChannel, r.iceStateChange)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "Failed to build network/port")
|
|
||||||
}
|
|
||||||
candidates = append(candidates, fmt.Sprintf("candidate:%scandidate 1 %s %d %s %d typ srflx", proto, proto, basePriority, addr.IP.String(), localAddr.Port))
|
|
||||||
basePriority = basePriority + 1
|
|
||||||
r.ports = append(r.ports, port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r.LocalDescription = sdp.BaseSessionDescription(&sdp.SessionBuilder{
|
|
||||||
IceUsername: r.iceUfrag,
|
|
||||||
IcePassword: r.icePwd,
|
|
||||||
Fingerprint: r.tlscfg.Fingerprint(),
|
|
||||||
Candidates: candidates,
|
|
||||||
Tracks: r.localTracks,
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddTrack adds a new track to the RTCPeerConnection
|
|
||||||
// This function returns a channel to push buffers on, and an error if the channel can't be added
|
|
||||||
// Closing the channel ends this stream
|
|
||||||
func (r *RTCPeerConnection) AddTrack(mediaType TrackType, clockRate uint32) (samples chan<- RTCSample, err error) {
|
|
||||||
if mediaType != VP8 && mediaType != H264 && mediaType != Opus {
|
|
||||||
panic("TODO Discarding packet, need media parsing")
|
|
||||||
}
|
|
||||||
|
|
||||||
trackInput := make(chan RTCSample, 15)
|
|
||||||
go func() {
|
|
||||||
ssrc := rand.Uint32()
|
|
||||||
sdpTrack := &sdp.SessionBuilderTrack{SSRC: ssrc}
|
|
||||||
var payloader rtp.Payloader
|
|
||||||
var payloadType uint8
|
|
||||||
switch mediaType {
|
|
||||||
case Opus:
|
|
||||||
sdpTrack.IsAudio = true
|
|
||||||
payloader = &codecs.OpusPayloader{}
|
|
||||||
payloadType = 111
|
|
||||||
|
|
||||||
case VP8:
|
|
||||||
payloader = &codecs.VP8Payloader{}
|
|
||||||
payloadType = 96
|
|
||||||
|
|
||||||
case H264:
|
|
||||||
payloader = &codecs.H264Payloader{}
|
|
||||||
payloadType = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
r.localTracks = append(r.localTracks, sdpTrack)
|
|
||||||
packetizer := rtp.NewPacketizer(1400, payloadType, ssrc, payloader, rtp.NewRandomSequencer(), clockRate)
|
|
||||||
for {
|
|
||||||
in := <-trackInput
|
|
||||||
packets := packetizer.Packetize(in.Data, in.Samples)
|
|
||||||
for _, p := range packets {
|
|
||||||
for _, port := range r.ports {
|
|
||||||
port.Send(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return trackInput, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close ends the RTCPeerConnection
|
// Close ends the RTCPeerConnection
|
||||||
@@ -252,29 +155,32 @@ func (r *RTCPeerConnection) generateChannel(ssrc uint32, payloadType uint8) (buf
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var codec TrackType
|
sdpCodec, err := r.remoteDescription.GetCodecForPayloadType(payloadType)
|
||||||
ok, codecStr := sdp.GetCodecForPayloadType(payloadType, r.remoteDescription)
|
if err != nil {
|
||||||
if !ok {
|
|
||||||
fmt.Printf("No codec could be found in RemoteDescription for payloadType %d \n", payloadType)
|
fmt.Printf("No codec could be found in RemoteDescription for payloadType %d \n", payloadType)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch codecStr {
|
codec, err := rtcMediaEngine.getCodecSDP(sdpCodec)
|
||||||
case "VP8":
|
if err != nil {
|
||||||
codec = VP8
|
fmt.Printf("Codec %s in not registered\n", sdpCodec)
|
||||||
case "VP9":
|
|
||||||
codec = VP9
|
|
||||||
case "opus":
|
|
||||||
codec = Opus
|
|
||||||
case "H264":
|
|
||||||
codec = H264
|
|
||||||
default:
|
|
||||||
fmt.Printf("Codec %s in not supported by pion-WebRTC \n", codecStr)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bufferTransport := make(chan *rtp.Packet, 15)
|
bufferTransport := make(chan *rtp.Packet, 15)
|
||||||
go r.Ontrack(codec, bufferTransport)
|
|
||||||
|
track := &RTCTrack{
|
||||||
|
PayloadType: payloadType,
|
||||||
|
Kind: codec.Type,
|
||||||
|
ID: "0", // TODO extract from remoteDescription
|
||||||
|
Label: "", // TODO extract from remoteDescription
|
||||||
|
Ssrc: ssrc,
|
||||||
|
Codec: codec,
|
||||||
|
Packets: bufferTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Register the receiving Track
|
||||||
|
|
||||||
|
go r.Ontrack(track)
|
||||||
return bufferTransport
|
return bufferTransport
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +193,7 @@ func (r *RTCPeerConnection) iceStateChange(p *network.Port) {
|
|||||||
r.iceState = newState
|
r.iceState = newState
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.ICEState == ice.Failed {
|
if p.ICEState == ice.ConnectionStateFailed {
|
||||||
if err := p.Close(); err != nil {
|
if err := p.Close(); err != nil {
|
||||||
fmt.Println(errors.Wrap(err, "Failed to close Port when ICE went to failed"))
|
fmt.Println(errors.Wrap(err, "Failed to close Port when ICE went to failed"))
|
||||||
}
|
}
|
||||||
@@ -301,9 +207,9 @@ func (r *RTCPeerConnection) iceStateChange(p *network.Port) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(r.ports) == 0 {
|
if len(r.ports) == 0 {
|
||||||
updateAndNotify(ice.Disconnected)
|
updateAndNotify(ice.ConnectionStateDisconnected)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateAndNotify(ice.Connected)
|
updateAndNotify(ice.ConnectionStateConnected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
407
signaling.go
Normal file
407
signaling.go
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
package webrtc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pions/pkg/stun"
|
||||||
|
"github.com/pions/webrtc/internal/network"
|
||||||
|
"github.com/pions/webrtc/internal/sdp"
|
||||||
|
"github.com/pions/webrtc/pkg/ice"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
setRemote(OFFER) setLocal(PRANSWER)
|
||||||
|
/-----\ /-----\
|
||||||
|
| | | |
|
||||||
|
v | v |
|
||||||
|
+---------------+ | +---------------+ |
|
||||||
|
| |----/ | |----/
|
||||||
|
| have- | setLocal(PRANSWER) | have- |
|
||||||
|
| remote-offer |------------------- >| local-pranswer|
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
+---------------+ +---------------+
|
||||||
|
^ | |
|
||||||
|
| | setLocal(ANSWER) |
|
||||||
|
setRemote(OFFER) | |
|
||||||
|
| V setLocal(ANSWER) |
|
||||||
|
+---------------+ |
|
||||||
|
| | |
|
||||||
|
| |<---------------------------+
|
||||||
|
| stable |
|
||||||
|
| |<---------------------------+
|
||||||
|
| | |
|
||||||
|
+---------------+ setRemote(ANSWER) |
|
||||||
|
^ | |
|
||||||
|
| | setLocal(OFFER) |
|
||||||
|
setRemote(ANSWER) | |
|
||||||
|
| V |
|
||||||
|
+---------------+ +---------------+
|
||||||
|
| | | |
|
||||||
|
| have- | setRemote(PRANSWER) |have- |
|
||||||
|
| local-offer |------------------- >|remote-pranswer|
|
||||||
|
| | | |
|
||||||
|
| |----\ | |----\
|
||||||
|
+---------------+ | +---------------+ |
|
||||||
|
^ | ^ |
|
||||||
|
| | | |
|
||||||
|
\-----/ \-----/
|
||||||
|
setLocal(OFFER) setRemote(PRANSWER)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// RTCSignalingState indicates the state of the offer/answer process
|
||||||
|
type RTCSignalingState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
|
||||||
|
// RTCSignalingStateStable indicates there is no offeranswer exchange in progress.
|
||||||
|
RTCSignalingStateStable RTCSignalingState = iota + 1
|
||||||
|
|
||||||
|
// RTCSignalingStateHaveLocalOffer indicates A local description, of type "offer", has been successfully applied.
|
||||||
|
RTCSignalingStateHaveLocalOffer
|
||||||
|
|
||||||
|
// RTCSignalingStateHaveRemoteOffer indicates A remote description, of type "offer", has been successfully applied.
|
||||||
|
RTCSignalingStateHaveRemoteOffer
|
||||||
|
|
||||||
|
// RTCSignalingStateHaveLocalPranswer indicates A remote description of type "offer" has been successfully applied and a local description of type "pranswer" has been successfully applied.
|
||||||
|
RTCSignalingStateHaveLocalPranswer
|
||||||
|
|
||||||
|
// RTCSignalingStateHaveRemotePranswer indicates A local description of type "offer" has been successfully applied and a remote description of type "pranswer" has been successfully applied.
|
||||||
|
RTCSignalingStateHaveRemotePranswer
|
||||||
|
|
||||||
|
// RTCSignalingStateClosed indicates The RTCPeerConnection has been closed.
|
||||||
|
RTCSignalingStateClosed
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t RTCSignalingState) String() string {
|
||||||
|
switch t {
|
||||||
|
case RTCSignalingStateStable:
|
||||||
|
return "stable"
|
||||||
|
case RTCSignalingStateHaveLocalOffer:
|
||||||
|
return "have-local-offer"
|
||||||
|
case RTCSignalingStateHaveRemoteOffer:
|
||||||
|
return "have-remote-offer"
|
||||||
|
case RTCSignalingStateHaveLocalPranswer:
|
||||||
|
return "have-local-pranswer"
|
||||||
|
case RTCSignalingStateHaveRemotePranswer:
|
||||||
|
return "have-remote-pranswer"
|
||||||
|
case RTCSignalingStateClosed:
|
||||||
|
return "closed"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTCSdpType describes the type of an RTCSessionDescription
|
||||||
|
type RTCSdpType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
|
||||||
|
// RTCSdpTypeOffer indicates that a description MUST be treated as an SDP offer.
|
||||||
|
RTCSdpTypeOffer RTCSdpType = iota + 1
|
||||||
|
|
||||||
|
// RTCSdpTypePranswer indicates that a description MUST be treated as an SDP answer, but not a final answer.
|
||||||
|
RTCSdpTypePranswer
|
||||||
|
|
||||||
|
// RTCSdpTypeAnswer indicates that a description MUST be treated as an SDP final answer, and the offer-answer exchange MUST be considered complete.
|
||||||
|
RTCSdpTypeAnswer
|
||||||
|
|
||||||
|
// RTCSdpTypeRollback indicates that a description MUST be treated as canceling the current SDP negotiation and moving the SDP offer and answer back to what it was in the previous stable state.
|
||||||
|
RTCSdpTypeRollback
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t RTCSdpType) String() string {
|
||||||
|
switch t {
|
||||||
|
case RTCSdpTypeOffer:
|
||||||
|
return "offer"
|
||||||
|
case RTCSdpTypePranswer:
|
||||||
|
return "pranswer"
|
||||||
|
case RTCSdpTypeAnswer:
|
||||||
|
return "answer"
|
||||||
|
case RTCSdpTypeRollback:
|
||||||
|
return "rollback"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTCSessionDescription is used to expose local and remote session descriptions.
|
||||||
|
type RTCSessionDescription struct {
|
||||||
|
Typ RTCSdpType
|
||||||
|
Sdp string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRemoteDescription sets the SessionDescription of the remote peer
|
||||||
|
func (r *RTCPeerConnection) SetRemoteDescription(desc RTCSessionDescription) error {
|
||||||
|
if r.remoteDescription != nil {
|
||||||
|
return errors.Errorf("remoteDescription is already defined, SetRemoteDescription can only be called once")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.currentRemoteDescription = &desc
|
||||||
|
|
||||||
|
r.remoteDescription = &sdp.SessionDescription{}
|
||||||
|
|
||||||
|
return r.remoteDescription.Unmarshal(desc.Sdp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTCOfferOptions describes the options used to control the offer creation process
|
||||||
|
type RTCOfferOptions struct {
|
||||||
|
VoiceActivityDetection bool
|
||||||
|
IceRestart bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOffer starts the RTCPeerConnection and generates the localDescription
|
||||||
|
func (r *RTCPeerConnection) CreateOffer(options *RTCOfferOptions) (RTCSessionDescription, error) {
|
||||||
|
if options != nil {
|
||||||
|
panic("TODO handle options")
|
||||||
|
}
|
||||||
|
if r.IsClosed {
|
||||||
|
return RTCSessionDescription{}, &InvalidStateError{Err: ErrConnectionClosed}
|
||||||
|
}
|
||||||
|
useIdentity := r.idpLoginURL != nil
|
||||||
|
if useIdentity {
|
||||||
|
panic("TODO handle identity provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
d := sdp.NewJSEPSessionDescription(
|
||||||
|
r.tlscfg.Fingerprint(),
|
||||||
|
useIdentity).
|
||||||
|
WithValueAttribute(sdp.AttrKeyGroup, "BUNDLE audio video") // TODO: Support BUNDLE
|
||||||
|
|
||||||
|
var streamlabels string
|
||||||
|
for _, tranceiver := range r.rtpTransceivers {
|
||||||
|
if tranceiver.Sender == nil ||
|
||||||
|
tranceiver.Sender.Track == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
track := tranceiver.Sender.Track
|
||||||
|
cname := "poins" // TODO: Support RTP streams synchronisation
|
||||||
|
steamlabel := "poins" // TODO: Support steam labels
|
||||||
|
codec, err := rtcMediaEngine.getCodec(track.PayloadType)
|
||||||
|
if err != nil {
|
||||||
|
return RTCSessionDescription{}, err
|
||||||
|
}
|
||||||
|
media := sdp.NewJSEPMediaDescription(track.Kind.String(), []string{}).
|
||||||
|
WithValueAttribute(sdp.AttrKeyConnectionSetup, sdp.ConnectionRoleActive.String()). // TODO: Support other connection types
|
||||||
|
WithValueAttribute(sdp.AttrKeyMID, tranceiver.Mid).
|
||||||
|
WithPropertyAttribute(tranceiver.Direction.String()).
|
||||||
|
WithICECredentials(r.iceAgent.Ufrag, r.iceAgent.Pwd).
|
||||||
|
WithPropertyAttribute(sdp.AttrKeyICELite). // TODO: get ICE type from ICE Agent
|
||||||
|
WithPropertyAttribute(sdp.AttrKeyRtcpMux). // TODO: support RTCP fallback
|
||||||
|
WithPropertyAttribute(sdp.AttrKeyRtcpRsize). // TODO: Support Reduced-Size RTCP?
|
||||||
|
WithCodec(
|
||||||
|
codec.PayloadType,
|
||||||
|
codec.Name,
|
||||||
|
codec.ClockRate,
|
||||||
|
codec.Channels,
|
||||||
|
codec.SdpFmtpLine,
|
||||||
|
).
|
||||||
|
WithMediaSource(track.Ssrc, cname, steamlabel, track.Label)
|
||||||
|
err = r.addICECandidates(media)
|
||||||
|
if err != nil {
|
||||||
|
return RTCSessionDescription{}, err
|
||||||
|
}
|
||||||
|
streamlabels = streamlabels + " " + steamlabel
|
||||||
|
|
||||||
|
d.WithMedia(media)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.WithValueAttribute(sdp.AttrKeyMsidSemantic, " "+sdp.SemanticTokenWebRTCMediaStreams+streamlabels)
|
||||||
|
|
||||||
|
return RTCSessionDescription{
|
||||||
|
Typ: RTCSdpTypeOffer,
|
||||||
|
Sdp: d.Marshal(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTCAnswerOptions describes the options used to control the answer creation process
|
||||||
|
type RTCAnswerOptions struct {
|
||||||
|
VoiceActivityDetection bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAnswer starts the RTCPeerConnection and generates the localDescription
|
||||||
|
func (r *RTCPeerConnection) CreateAnswer(options *RTCOfferOptions) (RTCSessionDescription, error) {
|
||||||
|
if options != nil {
|
||||||
|
panic("TODO handle options")
|
||||||
|
}
|
||||||
|
if r.IsClosed {
|
||||||
|
return RTCSessionDescription{}, &InvalidStateError{Err: ErrConnectionClosed}
|
||||||
|
}
|
||||||
|
useIdentity := r.idpLoginURL != nil
|
||||||
|
if useIdentity {
|
||||||
|
panic("TODO handle identity provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
d := sdp.NewJSEPSessionDescription(
|
||||||
|
r.tlscfg.Fingerprint(),
|
||||||
|
useIdentity).
|
||||||
|
WithValueAttribute(sdp.AttrKeyGroup, "BUNDLE audio video") // TODO: Support BUNDLE
|
||||||
|
|
||||||
|
// TODO: Actually reply based on the offer (#21)
|
||||||
|
// TODO: Media lines should be in the same order as the offer
|
||||||
|
|
||||||
|
audioStreamLabels, err := r.addAnswerMedia(d, RTCRtpCodecTypeAudio)
|
||||||
|
if err != nil {
|
||||||
|
return RTCSessionDescription{}, err
|
||||||
|
}
|
||||||
|
videoStreamLabels, err := r.addAnswerMedia(d, RTCRtpCodecTypeVideo)
|
||||||
|
if err != nil {
|
||||||
|
return RTCSessionDescription{}, err
|
||||||
|
}
|
||||||
|
d.WithValueAttribute(sdp.AttrKeyMsidSemantic, " "+sdp.SemanticTokenWebRTCMediaStreams+audioStreamLabels+videoStreamLabels)
|
||||||
|
|
||||||
|
return RTCSessionDescription{
|
||||||
|
Typ: RTCSdpTypeAnswer,
|
||||||
|
Sdp: d.Marshal(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RTCPeerConnection) addAnswerMedia(d *sdp.SessionDescription, typ RTCRtpCodecType) (string, error) {
|
||||||
|
added := false
|
||||||
|
|
||||||
|
var streamlabels string
|
||||||
|
for _, tranceiver := range r.rtpTransceivers {
|
||||||
|
if tranceiver.Sender == nil ||
|
||||||
|
tranceiver.Sender.Track == nil ||
|
||||||
|
tranceiver.Sender.Track.Kind != typ {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
track := tranceiver.Sender.Track
|
||||||
|
cname := track.Label // TODO: Support RTP streams synchronisation
|
||||||
|
steamlabel := track.Label // TODO: Support steam labels
|
||||||
|
codec, err := rtcMediaEngine.getCodec(track.PayloadType)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
media := sdp.NewJSEPMediaDescription(track.Kind.String(), []string{}).
|
||||||
|
WithValueAttribute(sdp.AttrKeyConnectionSetup, sdp.ConnectionRoleActive.String()). // TODO: Support other connection types
|
||||||
|
WithValueAttribute(sdp.AttrKeyMID, tranceiver.Mid).
|
||||||
|
WithPropertyAttribute(tranceiver.Direction.String()).
|
||||||
|
WithICECredentials(r.iceAgent.Ufrag, r.iceAgent.Pwd).
|
||||||
|
WithPropertyAttribute(sdp.AttrKeyICELite). // TODO: get ICE type from ICE Agent
|
||||||
|
WithPropertyAttribute(sdp.AttrKeyRtcpMux). // TODO: support RTCP fallback
|
||||||
|
WithPropertyAttribute(sdp.AttrKeyRtcpRsize). // TODO: Support Reduced-Size RTCP?
|
||||||
|
WithCodec(
|
||||||
|
codec.PayloadType,
|
||||||
|
codec.Name,
|
||||||
|
codec.ClockRate,
|
||||||
|
codec.Channels,
|
||||||
|
codec.SdpFmtpLine,
|
||||||
|
).
|
||||||
|
WithMediaSource(track.Ssrc, cname, steamlabel, track.Label)
|
||||||
|
|
||||||
|
err = r.addICECandidates(media)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
d.WithMedia(media)
|
||||||
|
streamlabels = streamlabels + " " + steamlabel
|
||||||
|
added = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !added {
|
||||||
|
// Add media line to advertise capabilities
|
||||||
|
media := sdp.NewJSEPMediaDescription(typ.String(), []string{}).
|
||||||
|
WithValueAttribute(sdp.AttrKeyConnectionSetup, sdp.ConnectionRoleActive.String()). // TODO: Support other connection types
|
||||||
|
WithValueAttribute(sdp.AttrKeyMID, typ.String()).
|
||||||
|
WithPropertyAttribute(RTCRtpTransceiverDirectionSendrecv.String()).
|
||||||
|
WithICECredentials(r.iceAgent.Ufrag, r.iceAgent.Pwd). // TODO: get credendials form ICE agent
|
||||||
|
WithPropertyAttribute(sdp.AttrKeyICELite). // TODO: get ICE type from ICE Agent (#23)
|
||||||
|
WithPropertyAttribute(sdp.AttrKeyRtcpMux). // TODO: support RTCP fallback
|
||||||
|
WithPropertyAttribute(sdp.AttrKeyRtcpRsize) // TODO: Support Reduced-Size RTCP?
|
||||||
|
|
||||||
|
for _, codec := range rtcMediaEngine.getCodecsByKind(typ) {
|
||||||
|
media.WithCodec(
|
||||||
|
codec.PayloadType,
|
||||||
|
codec.Name,
|
||||||
|
codec.ClockRate,
|
||||||
|
codec.Channels,
|
||||||
|
codec.SdpFmtpLine,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.addICECandidates(media)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
d.WithMedia(media)
|
||||||
|
}
|
||||||
|
|
||||||
|
return streamlabels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RTCPeerConnection) addICECandidates(d *sdp.MediaDescription) error {
|
||||||
|
r.portsLock.Lock()
|
||||||
|
defer r.portsLock.Unlock()
|
||||||
|
|
||||||
|
// TODO: Move gathering to ice.Agent
|
||||||
|
|
||||||
|
basePriority := uint16(rand.Uint32() & (1<<16 - 1))
|
||||||
|
|
||||||
|
for _, c := range ice.HostInterfaces() {
|
||||||
|
port, err := network.NewPort(c+":0", []byte(r.iceAgent.Pwd), r.tlscfg, r.generateChannel, r.iceStateChange)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.WithCandidate(1, "udp", basePriority, c, port.ListeningAddr.Port, "host")
|
||||||
|
|
||||||
|
basePriority = basePriority + 1
|
||||||
|
r.ports = append(r.ports, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, servers := range r.iceAgent.Servers {
|
||||||
|
for _, server := range servers {
|
||||||
|
if server.Type != ice.ServerTypeSTUN {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// TODO Do we want the timeout to be configurable?
|
||||||
|
proto := server.TransportType.String()
|
||||||
|
client, err := stun.NewClient(proto, fmt.Sprintf("%s:%d", server.Host, server.Port), time.Second*5)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Failed to create STUN client")
|
||||||
|
}
|
||||||
|
localAddr, ok := client.LocalAddr().(*net.UDPAddr)
|
||||||
|
if !ok {
|
||||||
|
return errors.Errorf("Failed to cast STUN client to UDPAddr")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Request()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Failed to make STUN request")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Close(); err != nil {
|
||||||
|
return errors.Wrapf(err, "Failed to close STUN client")
|
||||||
|
}
|
||||||
|
|
||||||
|
attr, ok := resp.GetOneAttribute(stun.AttrXORMappedAddress)
|
||||||
|
if !ok {
|
||||||
|
return errors.Errorf("Got respond from STUN server that did not contain XORAddress")
|
||||||
|
}
|
||||||
|
|
||||||
|
var addr stun.XorAddress
|
||||||
|
if err := addr.Unpack(resp, attr); err != nil {
|
||||||
|
return errors.Wrapf(err, "Failed to unpack STUN XorAddress response")
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := network.NewPort(fmt.Sprintf("0.0.0.0:%d", localAddr.Port), []byte(r.iceAgent.Pwd), r.tlscfg, r.generateChannel, r.iceStateChange)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Failed to build network/port")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.WithCandidate(1, proto, basePriority, addr.IP.String(), localAddr.Port, "srflx")
|
||||||
|
|
||||||
|
basePriority = basePriority + 1
|
||||||
|
r.ports = append(r.ports, port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.WithPropertyAttribute("end-of-candidates") // TODO: Support full trickle-ice
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
26
signaling_test.go
Normal file
26
signaling_test.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package webrtc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pions/webrtc/internal/sdp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetRemoteDescription(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc RTCSessionDescription
|
||||||
|
}{
|
||||||
|
{RTCSessionDescription{RTCSdpTypeOffer, sdp.NewJSEPSessionDescription("", false).Marshal()}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
peerConn, err := New(RTCConfiguration{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: got error: %v", i, err)
|
||||||
|
}
|
||||||
|
err = peerConn.SetRemoteDescription(testCase.desc)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: got error: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user