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 {