From 1202dbaa06ada9ee8dcdbda3555f3f2392270d24 Mon Sep 17 00:00:00 2001 From: Sean DuBois Date: Tue, 12 Mar 2019 23:54:35 -0700 Subject: [PATCH] Migrate SDP generation to Unified Plan This commit has breaking changes. This API change means we can no longer support an arbitrary number of receivers. For every track you want to receive you MUST call PeerConnection.AddTransceiver We do now support sending an multiple audio/video feeds. You can see this behavior via gstreamer-receive and gstreamer-send currently. Resolves #54 --- examples/gstreamer-receive/README.md | 2 +- examples/gstreamer-receive/jsfiddle/demo.html | 6 +- examples/gstreamer-receive/jsfiddle/demo.js | 23 +- examples/gstreamer-receive/main.go | 9 + examples/gstreamer-send/README.md | 2 +- examples/gstreamer-send/jsfiddle/demo.js | 6 +- examples/gstreamer-send/main.go | 22 +- examples/janus-gateway/streaming/main.go | 7 + examples/save-to-disk/main.go | 7 + examples/sfu-minimal/README.md | 2 +- examples/sfu-minimal/jsfiddle/demo.js | 3 +- examples/sfu-minimal/main.go | 9 +- examples/sfu-ws/room.go | 16 +- examples/sfu-ws/sfu.html | 7 +- mediaengine.go | 13 + peerconnection.go | 336 ++++++++++-------- peerconnection_media_test.go | 20 ++ rtptransceiver.go | 5 +- rtptransceiverinit.go | 8 + track.go | 6 +- 20 files changed, 340 insertions(+), 169 deletions(-) create mode 100644 rtptransceiverinit.go diff --git a/examples/gstreamer-receive/README.md b/examples/gstreamer-receive/README.md index ce8ebf57..cd43bb0b 100644 --- a/examples/gstreamer-receive/README.md +++ b/examples/gstreamer-receive/README.md @@ -14,7 +14,7 @@ go get github.com/pions/webrtc/examples/gstreamer-receive ``` ### Open gstreamer-receive example page -[jsfiddle.net](https://jsfiddle.net/pdm7bqfr/) you should see your Webcam, two text-areas and a 'Start Session' button +[jsfiddle.net](https://jsfiddle.net/8t2g5Lar/) you should see your Webcam, two text-areas and a 'Start Session' button ### Run gstreamer-receive with your browsers SessionDescription as stdin In the jsfiddle the top textarea is your browser, copy that and: diff --git a/examples/gstreamer-receive/jsfiddle/demo.html b/examples/gstreamer-receive/jsfiddle/demo.html index fad2a618..7612e161 100644 --- a/examples/gstreamer-receive/jsfiddle/demo.html +++ b/examples/gstreamer-receive/jsfiddle/demo.html @@ -4,12 +4,14 @@ Browser base64 Session Description
Golang base64 Session Description

-
+ +

Video
-
+

+ Logs
diff --git a/examples/gstreamer-receive/jsfiddle/demo.js b/examples/gstreamer-receive/jsfiddle/demo.js index 8599c763..8e185ec0 100644 --- a/examples/gstreamer-receive/jsfiddle/demo.js +++ b/examples/gstreamer-receive/jsfiddle/demo.js @@ -7,13 +7,24 @@ let pc = new RTCPeerConnection({ } ] }) -var log = msg => { +let log = msg => { document.getElementById('logs').innerHTML += msg + '
' } +let displayVideo = video => { + var el = document.createElement('video') + el.srcObject = video + el.autoplay = true + el.muted = true + el.width = 160 + el.height = 120 + + document.getElementById('localVideos').appendChild(el) + return video +} navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then(stream => { - pc.addStream(document.getElementById('video1').srcObject = stream) + pc.addStream(displayVideo(stream)) pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log) }).catch(log) @@ -36,3 +47,11 @@ window.startSession = () => { alert(e) } } + +window.addDisplayCapture = () => { + navigator.mediaDevices.getDisplayMedia().then(stream => { + document.getElementById('displayCapture').disabled = true + pc.addStream(displayVideo(stream)) + pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log) + }) +} diff --git a/examples/gstreamer-receive/main.go b/examples/gstreamer-receive/main.go index 6b1d13c3..fa920ea4 100644 --- a/examples/gstreamer-receive/main.go +++ b/examples/gstreamer-receive/main.go @@ -32,6 +32,15 @@ func gstreamerReceiveMain() { panic(err) } + // Allow us to receive 1 audio track, and 2 video tracks + if _, err = peerConnection.AddTransceiver(webrtc.RTPCodecTypeAudio); err != nil { + panic(err) + } else if _, err = peerConnection.AddTransceiver(webrtc.RTPCodecTypeVideo); err != nil { + panic(err) + } else if _, err = peerConnection.AddTransceiver(webrtc.RTPCodecTypeVideo); err != nil { + panic(err) + } + // Set a handler for when a new remote track starts, this handler creates a gstreamer pipeline // for the given codec peerConnection.OnTrack(func(track *webrtc.Track, receiver *webrtc.RTPReceiver) { diff --git a/examples/gstreamer-send/README.md b/examples/gstreamer-send/README.md index 894a5c66..edeee01f 100644 --- a/examples/gstreamer-send/README.md +++ b/examples/gstreamer-send/README.md @@ -14,7 +14,7 @@ go get github.com/pions/webrtc/examples/gstreamer-send ``` ### Open gstreamer-send example page -[jsfiddle.net](https://jsfiddle.net/Laf7ujeo/164/) you should see two text-areas and a 'Start Session' button +[jsfiddle.net](https://jsfiddle.net/z7ms3u5r/) you should see two text-areas and a 'Start Session' button ### Run gstreamer-send with your browsers SessionDescription as stdin In the jsfiddle the top textarea is your browser, copy that and: diff --git a/examples/gstreamer-send/jsfiddle/demo.js b/examples/gstreamer-send/jsfiddle/demo.js index 8e0ca2ed..1acb68f3 100644 --- a/examples/gstreamer-send/jsfiddle/demo.js +++ b/examples/gstreamer-send/jsfiddle/demo.js @@ -27,7 +27,11 @@ pc.onicecandidate = event => { } } -pc.createOffer({ offerToReceiveVideo: true, offerToReceiveAudio: true }).then(d => pc.setLocalDescription(d)).catch(log) +// Offer to receive 1 audio, and 2 video tracks +pc.addTransceiver('audio', {'direction': 'recvonly'}) +pc.addTransceiver('video', {'direction': 'recvonly'}) +pc.addTransceiver('video', {'direction': 'recvonly'}) +pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log) window.startSession = () => { let sd = document.getElementById('remoteSessionDescription').value diff --git a/examples/gstreamer-send/main.go b/examples/gstreamer-send/main.go index 8cd760ab..858ebcb6 100644 --- a/examples/gstreamer-send/main.go +++ b/examples/gstreamer-send/main.go @@ -40,21 +40,31 @@ func main() { }) // Create a audio track - opusTrack, err := peerConnection.NewTrack(webrtc.DefaultPayloadTypeOpus, rand.Uint32(), "audio", "pion1") + audioTrack, err := peerConnection.NewTrack(webrtc.DefaultPayloadTypeOpus, rand.Uint32(), "audio", "pion1") if err != nil { panic(err) } - _, err = peerConnection.AddTrack(opusTrack) + _, err = peerConnection.AddTrack(audioTrack) if err != nil { panic(err) } // Create a video track - vp8Track, err := peerConnection.NewTrack(webrtc.DefaultPayloadTypeVP8, rand.Uint32(), "video", "pion2") + firstVideoTrack, err := peerConnection.NewTrack(webrtc.DefaultPayloadTypeVP8, rand.Uint32(), "video", "pion2") if err != nil { panic(err) } - _, err = peerConnection.AddTrack(vp8Track) + _, err = peerConnection.AddTrack(firstVideoTrack) + if err != nil { + panic(err) + } + + // Create a second video track + secondVideoTrack, err := peerConnection.NewTrack(webrtc.DefaultPayloadTypeVP8, rand.Uint32(), "video", "pion3") + if err != nil { + panic(err) + } + _, err = peerConnection.AddTrack(secondVideoTrack) if err != nil { panic(err) } @@ -85,8 +95,8 @@ func main() { fmt.Println(signal.Encode(answer)) // Start pushing buffers on these tracks - gst.CreatePipeline(webrtc.Opus, []*webrtc.Track{opusTrack}, *audioSrc).Start() - gst.CreatePipeline(webrtc.VP8, []*webrtc.Track{vp8Track}, *videoSrc).Start() + gst.CreatePipeline(webrtc.Opus, []*webrtc.Track{audioTrack}, *audioSrc).Start() + gst.CreatePipeline(webrtc.VP8, []*webrtc.Track{firstVideoTrack, secondVideoTrack}, *videoSrc).Start() // Block forever select {} diff --git a/examples/janus-gateway/streaming/main.go b/examples/janus-gateway/streaming/main.go index 11280609..2ce78d0f 100644 --- a/examples/janus-gateway/streaming/main.go +++ b/examples/janus-gateway/streaming/main.go @@ -68,6 +68,13 @@ func main() { panic(err) } + // Allow us to receive 1 audio track, and 1 video track + if _, err = peerConnection.AddTransceiver(webrtc.RTPCodecTypeAudio); err != nil { + panic(err) + } else if _, err = peerConnection.AddTransceiver(webrtc.RTPCodecTypeVideo); err != nil { + panic(err) + } + peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { fmt.Printf("Connection State has changed %s \n", connectionState.String()) }) diff --git a/examples/save-to-disk/main.go b/examples/save-to-disk/main.go index fbdcb6de..08d655e2 100644 --- a/examples/save-to-disk/main.go +++ b/examples/save-to-disk/main.go @@ -61,6 +61,13 @@ func main() { panic(err) } + // Allow us to receive 1 audio track, and 1 video track + if _, err = peerConnection.AddTransceiver(webrtc.RTPCodecTypeAudio); err != nil { + panic(err) + } else if _, err = peerConnection.AddTransceiver(webrtc.RTPCodecTypeVideo); err != nil { + panic(err) + } + opusFile, err := opuswriter.New("output.opus", 48000, 2) if err != nil { panic(err) diff --git a/examples/sfu-minimal/README.md b/examples/sfu-minimal/README.md index 9933f67a..a00cd4d1 100644 --- a/examples/sfu-minimal/README.md +++ b/examples/sfu-minimal/README.md @@ -10,7 +10,7 @@ go get github.com/pions/webrtc/examples/sfu-minimal ``` ### Open sfu-minimal example page -[jsfiddle.net](https://jsfiddle.net/4g03uqrx/) You should see two buttons 'Publish a Broadcast' and 'Join a Broadcast' +[jsfiddle.net](https://jsfiddle.net/zhpya3n9/) You should see two buttons 'Publish a Broadcast' and 'Join a Broadcast' ### Run SFU Minimal #### Linux/macOS diff --git a/examples/sfu-minimal/jsfiddle/demo.js b/examples/sfu-minimal/jsfiddle/demo.js index a38ffa6a..25d96de1 100644 --- a/examples/sfu-minimal/jsfiddle/demo.js +++ b/examples/sfu-minimal/jsfiddle/demo.js @@ -27,7 +27,8 @@ window.createSession = isPublisher => { .catch(log) }).catch(log) } else { - pc.createOffer({ offerToReceiveVideo: true }) + pc.addTransceiver('video', {'direction': 'recvonly'}) + pc.createOffer() .then(d => pc.setLocalDescription(d)) .catch(log) diff --git a/examples/sfu-minimal/main.go b/examples/sfu-minimal/main.go index b1999eb0..32e63edd 100644 --- a/examples/sfu-minimal/main.go +++ b/examples/sfu-minimal/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io" "time" "github.com/pions/rtcp" @@ -46,6 +47,11 @@ func main() { panic(err) } + // Allow us to receive 1 video track + if _, err = peerConnection.AddTransceiver(webrtc.RTPCodecTypeVideo); err != nil { + panic(err) + } + localTrackChan := make(chan *webrtc.Track) // Set a handler for when a new remote track starts, this just distributes all our packets // to connected peers @@ -75,7 +81,8 @@ func main() { panic(readErr) } - if _, err = localTrack.Write(rtpBuf[:i]); err != nil { + // ErrClosedPipe means we don't have any subscribers, this is ok if no peers have connected yet + if _, err = localTrack.Write(rtpBuf[:i]); err != nil && err != io.ErrClosedPipe { panic(err) } } diff --git a/examples/sfu-ws/room.go b/examples/sfu-ws/room.go index 976addf4..f5b8f713 100644 --- a/examples/sfu-ws/room.go +++ b/examples/sfu-ws/room.go @@ -1,6 +1,7 @@ package main import ( + "io" "net/http" "sync" @@ -69,6 +70,12 @@ func room(w http.ResponseWriter, r *http.Request) { pubReceiver, err = api.NewPeerConnection(peerConnectionConfig) checkError(err) + _, err = pubReceiver.AddTransceiver(webrtc.RTPCodecTypeAudio) + checkError(err) + + _, err = pubReceiver.AddTransceiver(webrtc.RTPCodecTypeVideo) + checkError(err) + pubReceiver.OnTrack(func(remoteTrack *webrtc.Track, receiver *webrtc.RTPReceiver) { if remoteTrack.PayloadType() == webrtc.DefaultPayloadTypeVP8 || remoteTrack.PayloadType() == webrtc.DefaultPayloadTypeVP9 || remoteTrack.PayloadType() == webrtc.DefaultPayloadTypeH264 { @@ -94,7 +101,10 @@ func room(w http.ResponseWriter, r *http.Request) { videoTrackLock.RLock() _, err = videoTrack.Write(rtpBuf[:i]) videoTrackLock.RUnlock() - checkError(err) + + if err != io.ErrClosedPipe { + checkError(err) + } } } else { @@ -113,7 +123,9 @@ func room(w http.ResponseWriter, r *http.Request) { audioTrackLock.RLock() _, err = audioTrack.Write(rtpBuf[:i]) audioTrackLock.RUnlock() - checkError(err) + if err != io.ErrClosedPipe { + checkError(err) + } } } }) diff --git a/examples/sfu-ws/sfu.html b/examples/sfu-ws/sfu.html index d8a782ac..a053a4dd 100644 --- a/examples/sfu-ws/sfu.html +++ b/examples/sfu-ws/sfu.html @@ -66,7 +66,7 @@ window.sendMessage = element => { return alert('Message must not be empty') } dataChannel.send(message) - element.value = '' + element.value = '' } } @@ -103,7 +103,10 @@ window.createSession = isPublisher => { document.getElementById('msginput').style = 'display: none' dataChannel = pc.createDataChannel('data') dataChannel.onmessage = e => log(`receive data from '${dataChannel.label}' payload '${e.data}'`) - pc.createOffer({ offerToReceiveVideo: true , offerToReceiveAudio: true}) + pc.addTransceiver('audio', {'direction': 'recvonly'}) + pc.addTransceiver('video', {'direction': 'recvonly'}) + + pc.createOffer() .then(d => pc.setLocalDescription(d)) .catch(log) diff --git a/mediaengine.go b/mediaengine.go index 63d967b8..0082b38a 100644 --- a/mediaengine.go +++ b/mediaengine.go @@ -4,6 +4,7 @@ package webrtc import ( "strconv" + "strings" "github.com/pions/rtp" "github.com/pions/rtp/codecs" @@ -164,6 +165,18 @@ func (t RTPCodecType) String() string { } } +// NewRTPCodecType creates a RTPCodecType from a string +func NewRTPCodecType(r string) RTPCodecType { + switch { + case strings.EqualFold(r, "audio"): + return RTPCodecTypeAudio + case strings.EqualFold(r, "video"): + return RTPCodecTypeVideo + default: + return RTPCodecType(0) + } +} + // RTPCodec represents a codec supported by the PeerConnection type RTPCodec struct { RTPCodecCapability diff --git a/peerconnection.go b/peerconnection.go index ae8a88bb..d7bb6471 100644 --- a/peerconnection.go +++ b/peerconnection.go @@ -7,6 +7,8 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + mathRand "math/rand" + "fmt" "io" "net" @@ -135,7 +137,7 @@ func (api *API) NewPeerConnection(configuration Configuration) (*PeerConnection, pc.iceTransport = iceTransport // Create the DTLS transport - dtlsTransport, err := pc.createDTLSTransport() + dtlsTransport, err := pc.api.NewDTLSTransport(pc.iceTransport, pc.configuration.Certificates) if err != nil { return nil, err } @@ -431,10 +433,6 @@ func (pc *PeerConnection) GetConfiguration() Configuration { return pc.configuration } -// ------------------------------------------------------------------------ -// --- FIXME - BELOW CODE NEEDS REVIEW/CLEANUP -// ------------------------------------------------------------------------ - // CreateOffer starts the PeerConnection and generates the localDescription func (pc *PeerConnection) CreateOffer(options *OfferOptions) (SessionDescription, error) { useIdentity := pc.idpLoginURL != nil @@ -461,16 +459,18 @@ func (pc *PeerConnection) CreateOffer(options *OfferOptions) (SessionDescription } bundleValue := "BUNDLE" - - if pc.addRTPMediaSection(d, RTPCodecTypeAudio, "audio", iceParams, RTPTransceiverDirectionSendrecv, candidates, sdp.ConnectionRoleActpass) { - bundleValue += " audio" - } - if pc.addRTPMediaSection(d, RTPCodecTypeVideo, "video", iceParams, RTPTransceiverDirectionSendrecv, candidates, sdp.ConnectionRoleActpass) { - bundleValue += " video" + bundleCount := 0 + appendBundle := func() { + bundleValue += " " + strconv.Itoa(bundleCount) + bundleCount++ } - pc.addDataMediaSection(d, "data", iceParams, candidates, sdp.ConnectionRoleActpass) - d = d.WithValueAttribute(sdp.AttrKeyGroup, bundleValue+" data") + for _, t := range pc.GetTransceivers() { + pc.addTransceiverSDP(d, t, bundleCount, iceParams, candidates, sdp.ConnectionRoleActpass) + appendBundle() + } + pc.addDataMediaSection(d, bundleCount, iceParams, candidates, sdp.ConnectionRoleActive) + appendBundle() for _, m := range d.MediaDescriptions { m.WithPropertyAttribute("setup:actpass") @@ -535,11 +535,6 @@ func (pc *PeerConnection) createICETransport() *ICETransport { return t } -func (pc *PeerConnection) createDTLSTransport() (*DTLSTransport, error) { - dtlsTransport, err := pc.api.NewDTLSTransport(pc.iceTransport, pc.configuration.Certificates) - return dtlsTransport, err -} - // CreateAnswer starts the PeerConnection and generates the localDescription func (pc *PeerConnection) CreateAnswer(options *AnswerOptions) (SessionDescription, error) { useIdentity := pc.idpLoginURL != nil @@ -567,43 +562,64 @@ func (pc *PeerConnection) CreateAnswer(options *AnswerOptions) (SessionDescripti d := sdp.NewJSEPSessionDescription(useIdentity) pc.addFingerprint(d) - bundleValue := "BUNDLE" - for _, remoteMedia := range pc.RemoteDescription().parsed.MediaDescriptions { - // TODO @trivigy better SDP parser - var peerDirection RTPTransceiverDirection - midValue := "" - for _, a := range remoteMedia.Attributes { + getDirection := func(media *sdp.MediaDescription) RTPTransceiverDirection { + for _, a := range media.Attributes { + if direction := NewRTPTransceiverDirection(a.Key); direction != RTPTransceiverDirection(Unknown) { + return direction + } + } + return RTPTransceiverDirection(Unknown) + } + + localTransceivers := append([]*RTPTransceiver{}, pc.GetTransceivers()...) + satisfyPeerMedia := func(kind RTPCodecType, direction RTPTransceiverDirection) *RTPTransceiver { + for i := range localTransceivers { + t := localTransceivers[i] + switch { - case strings.HasPrefix(*a.String(), "mid"): - midValue = (*a.String())[len("mid:"):] - case strings.HasPrefix(*a.String(), "sendrecv"): - peerDirection = RTPTransceiverDirectionSendrecv - case strings.HasPrefix(*a.String(), "sendonly"): - peerDirection = RTPTransceiverDirectionSendonly - case strings.HasPrefix(*a.String(), "recvonly"): - peerDirection = RTPTransceiverDirectionRecvonly + case t.kind != kind: + continue + case direction == RTPTransceiverDirectionSendrecv && t.Direction != RTPTransceiverDirectionSendrecv: + continue + case direction != RTPTransceiverDirectionSendrecv && direction == t.Direction: + continue + case direction == RTPTransceiverDirectionInactive: + continue } + localTransceivers = append(localTransceivers[:i], localTransceivers[i+1:]...) + return t } - appendBundle := func() { - bundleValue += " " + midValue - } - - switch { - case strings.HasPrefix(*remoteMedia.MediaName.String(), "audio"): - if pc.addRTPMediaSection(d, RTPCodecTypeAudio, midValue, iceParams, peerDirection, candidates, sdp.ConnectionRoleActive) { - appendBundle() - } - case strings.HasPrefix(*remoteMedia.MediaName.String(), "video"): - if pc.addRTPMediaSection(d, RTPCodecTypeVideo, midValue, iceParams, peerDirection, candidates, sdp.ConnectionRoleActive) { - appendBundle() - } - case strings.HasPrefix(*remoteMedia.MediaName.String(), "application"): - pc.addDataMediaSection(d, midValue, iceParams, candidates, sdp.ConnectionRoleActive) - appendBundle() + return &RTPTransceiver{ + kind: kind, + Direction: RTPTransceiverDirectionInactive, } } + bundleValue := "BUNDLE" + bundleCount := 0 + appendBundle := func() { + bundleValue += " " + strconv.Itoa(bundleCount) + bundleCount++ + } + + for _, media := range pc.RemoteDescription().parsed.MediaDescriptions { + if media.MediaName.Media == "application" { + pc.addDataMediaSection(d, bundleCount, iceParams, candidates, sdp.ConnectionRoleActive) + appendBundle() + continue + } + + kind := NewRTPCodecType(media.MediaName.Media) + direction := getDirection(media) + if kind == 0 || direction == RTPTransceiverDirection(Unknown) { + continue + } + + t := satisfyPeerMedia(kind, direction) + pc.addTransceiverSDP(d, t, bundleCount, iceParams, candidates, sdp.ConnectionRoleActive) + appendBundle() + } d = d.WithValueAttribute(sdp.AttrKeyGroup, bundleValue) sdp, err := d.Marshal() @@ -939,13 +955,9 @@ func (pc *PeerConnection) openSRTP() { for _, media := range pc.RemoteDescription().parsed.MediaDescriptions { for _, attr := range media.Attributes { - var codecType RTPCodecType - switch media.MediaName.Media { - case "audio": - codecType = RTPCodecTypeAudio - case "video": - codecType = RTPCodecTypeVideo - default: + + codecType := NewRTPCodecType(media.MediaName.Media) + if codecType == 0 { continue } @@ -957,63 +969,68 @@ func (pc *PeerConnection) openSRTP() { } incomingSSRCes[uint32(ssrc)] = codecType + break } } } - for i := range incomingSSRCes { - go func(ssrc uint32, codecType RTPCodecType) { - receiver, err := pc.api.NewRTPReceiver(codecType, pc.dtlsTransport) - if err != nil { - pc.log.Warnf("Could not create RTPReceiver %s", err) - return + localTransceivers := append([]*RTPTransceiver{}, pc.GetTransceivers()...) + for ssrc := range incomingSSRCes { + for i := range localTransceivers { + t := localTransceivers[i] + switch { + case incomingSSRCes[ssrc] != t.kind: + continue + case t.Direction != RTPTransceiverDirectionRecvonly && t.Direction != RTPTransceiverDirectionSendrecv: + continue + case t.Receiver == nil: + continue } - if err = receiver.Receive(RTPReceiveParameters{ - Encodings: RTPDecodingParameters{ - RTPCodingParameters{SSRC: ssrc}, - }}); err != nil { - pc.log.Warnf("RTPReceiver Receive failed %s", err) - return - } + localTransceivers = append(localTransceivers[:i], localTransceivers[i+1:]...) + go func(ssrc uint32, receiver *RTPReceiver) { + err := receiver.Receive(RTPReceiveParameters{ + Encodings: RTPDecodingParameters{ + RTPCodingParameters{SSRC: ssrc}, + }}) + if err != nil { + pc.log.Warnf("RTPReceiver Receive failed %s", err) + return + } - pc.newRTPTransceiver( - receiver, - nil, - RTPTransceiverDirectionRecvonly, - ) + if err = receiver.Track().determinePayloadType(); err != nil { + pc.log.Warnf("Could not determine PayloadType for SSRC %d", receiver.Track().SSRC()) + return + } - if err = receiver.Track().determinePayloadType(); err != nil { - pc.log.Warnf("Could not determine PayloadType for SSRC %d", receiver.Track().SSRC()) - return - } + pc.mu.RLock() + defer pc.mu.RUnlock() - pc.mu.RLock() - defer pc.mu.RUnlock() + sdpCodec, err := pc.currentLocalDescription.parsed.GetCodecForPayloadType(receiver.Track().PayloadType()) + if err != nil { + pc.log.Warnf("no codec could be found in RemoteDescription for payloadType %d", receiver.Track().PayloadType()) + return + } - sdpCodec, err := pc.currentLocalDescription.parsed.GetCodecForPayloadType(receiver.Track().PayloadType()) - if err != nil { - pc.log.Warnf("no codec could be found in RemoteDescription for payloadType %d", receiver.Track().PayloadType()) - return - } + codec, err := pc.api.mediaEngine.getCodecSDP(sdpCodec) + if err != nil { + pc.log.Warnf("codec %s in not registered", sdpCodec) + return + } - codec, err := pc.api.mediaEngine.getCodecSDP(sdpCodec) - if err != nil { - pc.log.Warnf("codec %s in not registered", sdpCodec) - return - } + receiver.Track().mu.Lock() + receiver.Track().kind = codec.Type + receiver.Track().codec = codec + receiver.Track().mu.Unlock() - receiver.Track().mu.Lock() - receiver.Track().kind = codec.Type - receiver.Track().codec = codec - receiver.Track().mu.Unlock() - - if pc.onTrackHandler != nil { - pc.onTrack(receiver.Track(), receiver) - } else { - pc.log.Warnf("OnTrack unset, unable to handle incoming media streams") - } - }(i, incomingSSRCes[i]) + if pc.onTrackHandler != nil { + pc.onTrack(receiver.Track(), receiver) + } else { + pc.log.Warnf("OnTrack unset, unable to handle incoming media streams") + } + }(ssrc, t.Receiver) + break + } } } @@ -1190,6 +1207,7 @@ func (pc *PeerConnection) AddTrack(track *Track) (*RTPSender, error) { nil, sender, RTPTransceiverDirectionSendonly, + track.Kind(), ) } @@ -1202,9 +1220,60 @@ func (pc *PeerConnection) AddTrack(track *Track) (*RTPSender, error) { // panic("not implemented yet") // FIXME NOT-IMPLEMENTED nolint // } -// func (pc *PeerConnection) AddTransceiver() RTPTransceiver { -// panic("not implemented yet") // FIXME NOT-IMPLEMENTED nolint -// } +// AddTransceiver Create a new RTCRtpTransceiver and add it to the set of transceivers. +func (pc *PeerConnection) AddTransceiver(trackOrKind RTPCodecType, init ...RtpTransceiverInit) (*RTPTransceiver, error) { + direction := RTPTransceiverDirectionSendrecv + if len(init) > 1 { + return nil, fmt.Errorf("AddTransceiver only accepts one RtpTransceiverInit") + } else if len(init) == 1 { + direction = init[0].Direction + } + + switch direction { + case RTPTransceiverDirectionSendrecv: + receiver, err := pc.api.NewRTPReceiver(trackOrKind, pc.dtlsTransport) + if err != nil { + return nil, err + } + + payloadType := DefaultPayloadTypeOpus + if trackOrKind == RTPCodecTypeVideo { + payloadType = DefaultPayloadTypeVP8 + } + + track, err := pc.NewTrack(uint8(payloadType), mathRand.Uint32(), util.RandSeq(trackDefaultIDLength), util.RandSeq(trackDefaultLabelLength)) + if err != nil { + return nil, err + } + + sender, err := pc.api.NewRTPSender(track, pc.dtlsTransport) + if err != nil { + return nil, err + } + + return pc.newRTPTransceiver( + receiver, + sender, + RTPTransceiverDirectionSendrecv, + trackOrKind, + ), nil + + case RTPTransceiverDirectionRecvonly: + receiver, err := pc.api.NewRTPReceiver(trackOrKind, pc.dtlsTransport) + if err != nil { + return nil, err + } + + return pc.newRTPTransceiver( + receiver, + nil, + RTPTransceiverDirectionRecvonly, + trackOrKind, + ), nil + default: + return nil, fmt.Errorf("AddTransceiver currently only suports recvonly and sendrecv") + } +} // CreateDataChannel creates a new DataChannel object with the given label // and optional DataChannelInit used to configure properties of the @@ -1406,20 +1475,6 @@ func (pc *PeerConnection) iceStateChange(newState ICEConnectionState) { pc.onICEConnectionStateChange(newState) } -func localDirection(weSend bool, peerDirection RTPTransceiverDirection) RTPTransceiverDirection { - theySend := (peerDirection == RTPTransceiverDirectionSendrecv || peerDirection == RTPTransceiverDirectionSendonly) - switch { - case weSend && theySend: - return RTPTransceiverDirectionSendrecv - case weSend && !theySend: - return RTPTransceiverDirectionSendonly - case !weSend && theySend: - return RTPTransceiverDirectionRecvonly - } - - return RTPTransceiverDirectionInactive -} - func (pc *PeerConnection) addFingerprint(d *sdp.SessionDescription) { // TODO: Handle multiple certificates for _, fingerprint := range pc.configuration.Certificates[0].GetFingerprints() { @@ -1427,26 +1482,15 @@ func (pc *PeerConnection) addFingerprint(d *sdp.SessionDescription) { } } -func (pc *PeerConnection) addRTPMediaSection(d *sdp.SessionDescription, codecType RTPCodecType, midValue string, iceParams ICEParameters, peerDirection RTPTransceiverDirection, candidates []ICECandidate, dtlsRole sdp.ConnectionRole) bool { - if codecs := pc.api.mediaEngine.getCodecsByKind(codecType); len(codecs) == 0 { - d.WithMedia(&sdp.MediaDescription{ - MediaName: sdp.MediaName{ - Media: codecType.String(), - Port: sdp.RangedPort{Value: 0}, - Protos: []string{"UDP", "TLS", "RTP", "SAVPF"}, - Formats: []string{"0"}, - }, - }) - return false - } - media := sdp.NewJSEPMediaDescription(codecType.String(), []string{}). +func (pc *PeerConnection) addTransceiverSDP(d *sdp.SessionDescription, t *RTPTransceiver, midOffset int, iceParams ICEParameters, candidates []ICECandidate, dtlsRole sdp.ConnectionRole) { + media := sdp.NewJSEPMediaDescription(t.kind.String(), []string{}). WithValueAttribute(sdp.AttrKeyConnectionSetup, dtlsRole.String()). // TODO: Support other connection types - WithValueAttribute(sdp.AttrKeyMID, midValue). + WithValueAttribute(sdp.AttrKeyMID, strconv.Itoa(midOffset)). WithICECredentials(iceParams.UsernameFragment, iceParams.Password). WithPropertyAttribute(sdp.AttrKeyRTCPMux). // TODO: support RTCP fallback WithPropertyAttribute(sdp.AttrKeyRTCPRsize) // TODO: Support Reduced-Size RTCP? - for _, codec := range pc.api.mediaEngine.getCodecsByKind(codecType) { + for _, codec := range pc.api.mediaEngine.getCodecsByKind(t.kind) { media.WithCodec(codec.PayloadType, codec.Name, codec.ClockRate, codec.Channels, codec.SDPFmtpLine) for _, feedback := range codec.RTPCodecCapability.RTCPFeedback { @@ -1454,19 +1498,13 @@ func (pc *PeerConnection) addRTPMediaSection(d *sdp.SessionDescription, codecTyp } } - weSend := false - for _, transceiver := range pc.rtpTransceivers { - if transceiver.Sender == nil || - transceiver.Sender.track == nil || - transceiver.Sender.track.Kind() != codecType { - continue - } - weSend = true - track := transceiver.Sender.track - media = media.WithMediaSource(track.SSRC(), track.Label() /* cname */, track.Label() /* streamLabel */, track.Label()) + if t.Sender != nil && t.Sender.track != nil { + track := t.Sender.track + media = media.WithPropertyAttribute("msid:" + track.Label() + " " + track.ID()) + media = media.WithMediaSource(track.SSRC(), track.Label() /* cname */, track.Label() /* streamLabel */, track.ID()) } - media = media.WithPropertyAttribute(localDirection(weSend, peerDirection).String()) + media = media.WithPropertyAttribute(t.Direction.String()) for _, c := range candidates { sdpCandidate := c.toSDP() sdpCandidate.ExtensionAttributes = append(sdpCandidate.ExtensionAttributes, sdp.ICECandidateAttribute{Key: "generation", Value: "0"}) @@ -1475,12 +1513,14 @@ func (pc *PeerConnection) addRTPMediaSection(d *sdp.SessionDescription, codecTyp sdpCandidate.Component = 2 media.WithICECandidate(sdpCandidate) } - media.WithPropertyAttribute("end-of-candidates") + if len(candidates) != 0 { + media.WithPropertyAttribute("end-of-candidates") + } + d.WithMedia(media) - return true } -func (pc *PeerConnection) addDataMediaSection(d *sdp.SessionDescription, midValue string, iceParams ICEParameters, candidates []ICECandidate, dtlsRole sdp.ConnectionRole) { +func (pc *PeerConnection) addDataMediaSection(d *sdp.SessionDescription, midOffset int, iceParams ICEParameters, candidates []ICECandidate, dtlsRole sdp.ConnectionRole) { media := (&sdp.MediaDescription{ MediaName: sdp.MediaName{ Media: "application", @@ -1497,7 +1537,7 @@ func (pc *PeerConnection) addDataMediaSection(d *sdp.SessionDescription, midValu }, }). WithValueAttribute(sdp.AttrKeyConnectionSetup, dtlsRole.String()). // TODO: Support other connection types - WithValueAttribute(sdp.AttrKeyMID, midValue). + WithValueAttribute(sdp.AttrKeyMID, strconv.Itoa(midOffset)). WithPropertyAttribute(RTPTransceiverDirectionSendrecv.String()). WithPropertyAttribute("sctpmap:5000 webrtc-datachannel 1024"). WithICECredentials(iceParams.UsernameFragment, iceParams.Password) @@ -1531,12 +1571,14 @@ func (pc *PeerConnection) newRTPTransceiver( receiver *RTPReceiver, sender *RTPSender, direction RTPTransceiverDirection, + kind RTPCodecType, ) *RTPTransceiver { t := &RTPTransceiver{ Receiver: receiver, Sender: sender, Direction: direction, + kind: kind, } pc.mu.Lock() defer pc.mu.Unlock() diff --git a/peerconnection_media_test.go b/peerconnection_media_test.go index 89a739c0..d15518ba 100644 --- a/peerconnection_media_test.go +++ b/peerconnection_media_test.go @@ -37,6 +37,11 @@ func TestPeerConnection_Media_Sample(t *testing.T) { t.Fatal(err) } + _, err = pcAnswer.AddTransceiver(RTPCodecTypeVideo) + if err != nil { + t.Fatal(err) + } + awaitRTPRecv := make(chan bool) awaitRTPRecvClosed := make(chan bool) awaitRTPSend := make(chan bool) @@ -191,6 +196,16 @@ func TestPeerConnection_Media_Shutdown(t *testing.T) { t.Fatal(err) } + _, err = pcOffer.AddTransceiver(RTPCodecTypeVideo, RtpTransceiverInit{Direction: RTPTransceiverDirectionRecvonly}) + if err != nil { + t.Fatal(err) + } + + _, err = pcAnswer.AddTransceiver(RTPCodecTypeVideo, RtpTransceiverInit{Direction: RTPTransceiverDirectionRecvonly}) + if err != nil { + t.Fatal(err) + } + opusTrack, err := pcOffer.NewTrack(DefaultPayloadTypeOpus, rand.Uint32(), "audio", "pion1") if err != nil { t.Fatal(err) @@ -366,6 +381,11 @@ func TestPeerConnection_Media_Closed(t *testing.T) { t.Fatal(err) } + _, err = pcAnswer.AddTransceiver(RTPCodecTypeVideo) + if err != nil { + t.Fatal(err) + } + vp8Writer, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, rand.Uint32(), "video", "pion2") if err != nil { t.Fatal(err) diff --git a/rtptransceiver.go b/rtptransceiver.go index 29157bf5..df5fd5c9 100644 --- a/rtptransceiver.go +++ b/rtptransceiver.go @@ -2,7 +2,9 @@ package webrtc -import "fmt" +import ( + "fmt" +) // RTPTransceiver represents a combination of an RTPSender and an RTPReceiver that share a common mid. type RTPTransceiver struct { @@ -14,6 +16,7 @@ type RTPTransceiver struct { // firedDirection RTPTransceiverDirection // receptive bool stopped bool + kind RTPCodecType } func (t *RTPTransceiver) setSendingTrack(track *Track) error { diff --git a/rtptransceiverinit.go b/rtptransceiverinit.go new file mode 100644 index 00000000..19c1769d --- /dev/null +++ b/rtptransceiverinit.go @@ -0,0 +1,8 @@ +package webrtc + +// RtpTransceiverInit dictionary is used when calling the WebRTC function addTransceiver() to provide configuration options for the new transceiver. +type RtpTransceiverInit struct { + Direction RTPTransceiverDirection + SendEncodings []RTPEncodingParameters + // Streams []*Track +} diff --git a/track.go b/track.go index cb026f02..98e8f047 100644 --- a/track.go +++ b/track.go @@ -11,7 +11,11 @@ import ( "github.com/pions/webrtc/pkg/media" ) -const rtpOutboundMTU = 1400 +const ( + rtpOutboundMTU = 1400 + trackDefaultIDLength = 16 + trackDefaultLabelLength = 16 +) // Track represents a single media track type Track struct {