Repository: pions/webrtc
Pull request #328 (2018-12-29 10:31:29)

Move SRTP to Transport API
Move SRTP to transport based API

coveralls: ## Pull Request Test Coverage Report for [Build 1586](https://coveralls.io/builds/20835222)

* **23** of **42**   **(54.76%)**  changed or added relevant lines in **3** files are covered.
* **1** unchanged line in **1** file lost coverage.
* Overall coverage increased (+**0.7%**) to **73.522%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [internal/network/manager.go](https://coveralls.io/builds/20835222/source?filename=internal%2Fnetwork%2Fmanager.go#L282) | 0 | 6 | 0.0%
| [internal/network/srtp.go](https://coveralls.io/builds/20835222/source?filename=internal%2Fnetwork%2Fsrtp.go#L130) | 0 | 6 | 0.0%
| [internal/srtp/srtp.go](https://coveralls.io/builds/20835222/source?filename=internal%2Fsrtp%2Fsrtp.go#L15) | 23 | 30 | 76.67%
<!-- | **Total:** | **23** | **42** | **54.76%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [internal/network/manager.go](https://coveralls.io/builds/20835222/source?filename=internal%2Fnetwork%2Fmanager.go#L283) | 1 | 62.05% |
<!-- | **Total:** | **1** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/20835222/badge)](https://coveralls.io/builds/20835222) |
| :-- | --: |
| Change from base [Build 1583](https://coveralls.io/builds/20831598): |  0.7% |
| Covered Lines: | 3732 |
| Relevant Lines: | 5076 |

---
##### 💛  - [Coveralls](https://coveralls.io)

backkem: Possible [optimizations ](https://github.com/pions/webrtc/wiki/TransportDesign#optimizations)
Sean-Der: @backkem All the tests pass and send/recv RTP+RTCP works, mind taking a look over and suggesting improvements?

I will push hopefully tomorrow and get the code coverage back up, I have to rewrite one test and hopefully that gets up back in a better spot!
backkem: @Sean-Der Here's what I came up with for Raw RTC: [Raw RTC](https://github.com/pions/webrtc/wiki/TransportDesign#raw-rtp).
I also think we should probably take a look at the [RTC Track API](https://github.com/pions/webrtc/wiki/TransportDesign#rtctrack-api). Maybe we should keep it for another PR thought.

Patch:
diff --git a/internal/mux/muxfunc.go b/internal/mux/muxfunc.go
index 99e991ba..1571a887 100644
--- a/internal/mux/muxfunc.go
+++ b/internal/mux/muxfunc.go
@@ -1,5 +1,10 @@
 package mux
 
+import (
+	"bytes"
+	"encoding/binary"
+)
+
 // MatchFunc allows custom logic for mapping packets to an Endpoint
 type MatchFunc func([]byte) bool
 
@@ -44,6 +49,33 @@ var MatchDTLS = MatchRange(20, 63)
 // as defied in RFC7983
 var MatchTURN = MatchRange(64, 79)
 
-// MatchSRTP is a MatchFunc that accepts packets with the first byte in [128..191]
+// MatchSRTPOrSRTCP is a MatchFunc that accepts packets with the first byte in [128..191]
 // as defied in RFC7983
-var MatchSRTP = MatchRange(128, 191)
+var MatchSRTPOrSRTCP = MatchRange(128, 191)
+
+func isRTCP(buf []byte) bool {
+	// Not long enough to determine RTP/RTCP
+	if len(buf) < 4 {
+		return false
+	}
+
+	var rtcpPacketType uint8
+	r := bytes.NewReader([]byte{buf[1]})
+	if err := binary.Read(r, binary.BigEndian, &rtcpPacketType); err != nil {
+		return false
+	} else if rtcpPacketType >= 192 && rtcpPacketType <= 223 {
+		return true
+	}
+
+	return false
+}
+
+// MatchSRTP is a MatchFunc that only matches SRTP and not SRTCP
+var MatchSRTP = func(buf []byte) bool {
+	return MatchSRTPOrSRTCP(buf) && !isRTCP(buf)
+}
+
+// MatchSRTCP is a MatchFunc that only matches SRTCP and not SRTP
+var MatchSRTCP = func(buf []byte) bool {
+	return MatchSRTPOrSRTCP(buf) && isRTCP(buf)
+}
diff --git a/internal/network/manager.go b/internal/network/manager.go
index 2ab3eb98..bb9c44a6 100644
--- a/internal/network/manager.go
+++ b/internal/network/manager.go
@@ -14,18 +14,13 @@ import (
 	"github.com/pions/webrtc/internal/mux"
 	"github.com/pions/webrtc/internal/srtp"
 	"github.com/pions/webrtc/pkg/ice"
-	"github.com/pions/webrtc/pkg/rtcp"
-	"github.com/pions/webrtc/pkg/rtp"
 )
 
-const receiveMTU = 8192
-
-// TransportPair allows the application to be notified about both Rtp
-// and Rtcp messages incoming from the remote host
-type TransportPair struct {
-	RTP  chan<- *rtp.Packet
-	RTCP chan<- rtcp.Packet
-}
+const (
+	srtpMasterKeyLen     = 16
+	srtpMasterKeySaltLen = 14
+	receiveMTU           = 8192
+)
 
 // Manager contains all network state (DTLS, SRTP) that is shared between ports
 // It is also used to perform operations that involve multiple ports
@@ -34,39 +29,23 @@ type Manager struct {
 	iceConn  *ice.Conn
 	isOffer  bool
 
-	mux *mux.Mux
+	SrtpSession  *srtp.SessionSRTP
+	SrtcpSession *srtp.SessionSRTCP
 
-	dtlsEndpoint *mux.Endpoint
-	srtpEndpoint *mux.Endpoint
-
-	srtpInboundContextLock  sync.RWMutex
-	srtpInboundContext      *srtp.Context
-	srtpOutboundContextLock sync.RWMutex
-	srtpOutboundContext     *srtp.Context
+	mux *mux.Mux
 
-	bufferTransportGenerator BufferTransportGenerator
-	pairsLock                sync.RWMutex
-	bufferTransportPairs     map[uint32]*TransportPair
+	dtlsEndpoint  *mux.Endpoint
+	srtpEndpoint  *mux.Endpoint
+	srtcpEndpoint *mux.Endpoint
 
 	dtlsConn *dtls.Conn
 
-	sctpAssociation *sctp.Association
-}
-
-//AddTransportPair notifies the network manager that an RTCTrack has
-//been created externally, and packets may be incoming with this ssrc
-func (m *Manager) AddTransportPair(ssrc uint32, Rtp chan<- *rtp.Packet, Rtcp chan<- rtcp.Packet) {
-	m.pairsLock.Lock()
-	defer m.pairsLock.Unlock()
-	bufferTransport := m.bufferTransportPairs[ssrc]
-	if bufferTransport == nil {
-		bufferTransport = &TransportPair{Rtp, Rtcp}
-		m.bufferTransportPairs[ssrc] = bufferTransport
-	}
+	sctpAssociationMutex sync.RWMutex
+	sctpAssociation      *sctp.Association
 }
 
 // NewManager creates a new network.Manager
-func NewManager(urls []*ice.URL, btg BufferTransportGenerator, ntf ICENotifier, minport, maxport uint16) (*Manager, error) {
+func NewManager(urls []*ice.URL, ntf ICENotifier, minport, maxport uint16) (*Manager, error) {
 	config := &ice.AgentConfig{Urls: urls, Notifier: ntf, PortMin: minport, PortMax: maxport}
 	iceAgent, err := ice.NewAgent(config)
 
@@ -75,30 +54,12 @@ func NewManager(urls []*ice.URL, btg BufferTransportGenerator, ntf ICENotifier,
 	}
 
 	return &Manager{
-		IceAgent:                 iceAgent,
-		bufferTransportPairs:     make(map[uint32]*TransportPair),
-		bufferTransportGenerator: btg,
+		IceAgent:     iceAgent,
+		SrtpSession:  srtp.CreateSessionSRTP(),
+		SrtcpSession: srtp.CreateSessionSRTCP(),
 	}, nil
 }
 
-func (m *Manager) getBufferTransports(ssrc uint32) *TransportPair {
-	m.pairsLock.RLock()
-	defer m.pairsLock.RUnlock()
-	return m.bufferTransportPairs[ssrc]
-}
-
-func (m *Manager) getOrCreateBufferTransports(ssrc uint32, payloadtype uint8) *TransportPair {
-	m.pairsLock.Lock()
-	defer m.pairsLock.Unlock()
-	bufferTransport := m.bufferTransportPairs[ssrc]
-	if bufferTransport == nil {
-		bufferTransport = m.bufferTransportGenerator(ssrc, payloadtype)
-		m.bufferTransportPairs[ssrc] = bufferTransport
-	}
-
-	return bufferTransport
-}
-
 // Start allocates the network stack
 // TODO: Turn into the ORTC constructors
 func (m *Manager) Start(isOffer bool,
@@ -114,18 +75,13 @@ func (m *Manager) Start(isOffer bool,
 	m.mux = mux.NewMux(m.iceConn, receiveMTU)
 	m.dtlsEndpoint = m.mux.NewEndpoint(mux.MatchDTLS)
 	m.srtpEndpoint = m.mux.NewEndpoint(mux.MatchSRTP)
-
-	m.startSRTP()
+	m.srtcpEndpoint = m.mux.NewEndpoint(mux.MatchSRTCP)
 
 	if err := m.startDTLS(isOffer, dtlsCert, dtlsPrivKey, fingerprint, fingerprintHash); err != nil {
 		return err
 	}
 
-	if err := m.createContextSRTP(isOffer); err != nil {
-		return err
-	}
-
-	return m.startSCTP(isOffer)
+	return m.startSRTP(isOffer)
 }
 
 func (m *Manager) startICE(isOffer bool, remoteUfrag, remotePwd string) error {
@@ -145,27 +101,55 @@ func (m *Manager) startICE(isOffer bool, remoteUfrag, remotePwd string) error {
 	return nil
 }
 
-func (m *Manager) startSRTP() {
-	// Glue code until SRTP is a Conn.
-	go func() {
-		buf := make([]byte, receiveMTU)
-		for {
-			n, err := m.srtpEndpoint.Read(buf)
-			if err != nil {
-				return
-			}
-			m.handleSRTP(buf[:n])
-		}
-	}()
-}
-
-func (m *Manager) createContextSRTP(isOffer bool) error {
+func (m *Manager) startSRTP(isOffer bool) error {
 	keyingMaterial, err := m.dtlsConn.ExportKeyingMaterial([]byte("EXTRACTOR-dtls_srtp"), nil, (srtpMasterKeyLen*2)+(srtpMasterKeySaltLen*2))
 	if err != nil {
 		return err
 	}
 
-	return m.CreateContextSRTP(keyingMaterial, isOffer)
+	offset := 0
+	clientWriteKey := append([]byte{}, keyingMaterial[offset:offset+srtpMasterKeyLen]...)
+	offset += srtpMasterKeyLen
+
+	serverWriteKey := append([]byte{}, keyingMaterial[offset:offset+srtpMasterKeyLen]...)
+	offset += srtpMasterKeyLen
+
+	clientWriteKey = append(clientWriteKey, keyingMaterial[offset:offset+srtpMasterKeySaltLen]...)
+	offset += srtpMasterKeySaltLen
+
+	serverWriteKey = append(serverWriteKey, keyingMaterial[offset:offset+srtpMasterKeySaltLen]...)
+
+	if isOffer {
+		err = m.SrtpSession.Start(
+			serverWriteKey[0:16], serverWriteKey[16:],
+			clientWriteKey[0:16], clientWriteKey[16:],
+			srtp.ProtectionProfileAes128CmHmacSha1_80, m.srtpEndpoint,
+		)
+
+		if err == nil {
+			err = m.SrtcpSession.Start(
+				serverWriteKey[0:16], serverWriteKey[16:],
+				clientWriteKey[0:16], clientWriteKey[16:],
+				srtp.ProtectionProfileAes128CmHmacSha1_80, m.srtcpEndpoint,
+			)
+		}
+	} else {
+		err = m.SrtpSession.Start(
+			clientWriteKey[0:16], clientWriteKey[16:],
+			serverWriteKey[0:16], serverWriteKey[16:],
+			srtp.ProtectionProfileAes128CmHmacSha1_80, m.srtpEndpoint,
+		)
+
+		if err == nil {
+			err = m.SrtcpSession.Start(
+				clientWriteKey[0:16], clientWriteKey[16:],
+				serverWriteKey[0:16], serverWriteKey[16:],
+				srtp.ProtectionProfileAes128CmHmacSha1_80, m.srtcpEndpoint,
+			)
+		}
+	}
+
+	return err
 }
 
 func (m *Manager) startDTLS(isOffer bool, dtlsCert *x509.Certificate, dtlsPrivKey crypto.PrivateKey, fingerprint, fingerprintHash string) error {
@@ -209,19 +193,26 @@ func (m *Manager) startDTLS(isOffer bool, dtlsCert *x509.Certificate, dtlsPrivKe
 	return nil
 }
 
-func (m *Manager) startSCTP(isOffer bool) error {
+// StartSCTP starts the SCTP association
+func (m *Manager) StartSCTP(isOffer bool) error {
 	if isOffer {
 		sctpAssociation, err := sctp.Client(m.dtlsConn)
 		if err != nil {
 			return err
 		}
+
+		m.sctpAssociationMutex.Lock()
 		m.sctpAssociation = sctpAssociation
+		m.sctpAssociationMutex.Unlock()
 	} else {
 		sctpAssociation, err := sctp.Server(m.dtlsConn)
 		if err != nil {
 			return err
 		}
+
+		m.sctpAssociationMutex.Lock()
 		m.sctpAssociation = sctpAssociation
+		m.sctpAssociationMutex.Unlock()
 	}
 	return nil
 }
@@ -247,10 +238,16 @@ func (m *Manager) Close() error {
 	//    continue the chain the Mux has to be closed.
 
 	// Close SCTP. This should close the data channels, SCTP, and DTLS
-	var errSCTP, errMux error
+	var errSCTP, errMux, errSRTP, errSRTCP error
+
+	m.sctpAssociationMutex.RLock()
 	if m.sctpAssociation != nil {
 		errSCTP = m.sctpAssociation.Close()
 	}
+	m.sctpAssociationMutex.RUnlock()
+
+	errSRTP = m.SrtpSession.Close()
+	errSRTCP = m.SrtcpSession.Close()
 
 	// Close the Mux. This should close the Mux and ICE.
 	if m.mux != nil {
@@ -259,56 +256,11 @@ func (m *Manager) Close() error {
 
 	// TODO: better way to combine/handle errors?
 	if errSCTP != nil ||
-		errMux != nil {
-		return fmt.Errorf("Failed to close: %v, %v", errSCTP, errMux)
+		errMux != nil ||
+		errSRTP != nil ||
+		errSRTCP != nil {
+		return fmt.Errorf("Failed to close: %v, %v, %v, %v", errSCTP, errMux, errSRTP, errSRTCP)
 	}
 
 	return nil
 }
-
-// SendRTP finds a connected port and sends the passed RTP packet
-func (m *Manager) SendRTP(packet *rtp.Packet) {
-	m.srtpOutboundContextLock.Lock()
-	defer m.srtpOutboundContextLock.Unlock()
-	if m.srtpOutboundContext == nil {
-		// TODO log-level
-		// fmt.Printf("Tried to send RTP packet but no SRTP Context to handle it \n")
-		return
-	}
-
-	if ok := m.srtpOutboundContext.EncryptRTP(packet); !ok {
-		fmt.Println("SendRTP failed to encrypt packet")
-		return
-	}
-
-	raw, err := packet.Marshal()
-	if err != nil {
-		fmt.Printf("SendRTP failed to marshal packet: %s \n", err.Error())
-	}
-
-	_, err = m.iceConn.Write(raw)
-	if err != nil {
-		fmt.Println("SendRTP failed to write:", err)
-	}
-}
-
-// SendRTCP finds a connected port and sends the passed RTCP packet
-func (m *Manager) SendRTCP(pkt []byte) {
-	m.srtpOutboundContextLock.Lock()
-	defer m.srtpOutboundContextLock.Unlock()
-	if m.srtpOutboundContext == nil {
-		fmt.Printf("Tried to send RTCP packet but no SRTP Context to handle it \n")
-		return
-	}
-
-	encrypted, err := m.srtpOutboundContext.EncryptRTCP(pkt)
-	if err != nil {
-		fmt.Println("SendRTCP failed to encrypt packet:", err)
-		return
-	}
-
-	_, err = m.iceConn.Write(encrypted)
-	if err != nil {
-		fmt.Println("SendRTCP failed to write:", err)
-	}
-}
diff --git a/internal/network/network.go b/internal/network/network.go
index f860d0ab..5265c568 100644
--- a/internal/network/network.go
+++ b/internal/network/network.go
@@ -5,10 +5,6 @@ import (
 	"github.com/pions/webrtc/pkg/ice"
 )
 
-// BufferTransportGenerator generates a new channel for the associated SSRC
-// This channel is used to send RTP and RTCP packets to users of pion-WebRTC
-type BufferTransportGenerator func(uint32, uint8) *TransportPair
-
 // ICENotifier notifies the RTCPeerConnection if ICE state has changed
 type ICENotifier func(ice.ConnectionState)
 
diff --git a/internal/network/srtp.go b/internal/network/srtp.go
deleted file mode 100644
index f31be952..00000000
--- a/internal/network/srtp.go
+++ /dev/null
@@ -1,149 +0,0 @@
-package network
-
-import (
-	"bytes"
-	"encoding/binary"
-	"fmt"
-	"io"
-
-	"github.com/pions/webrtc/internal/srtp"
-	"github.com/pions/webrtc/pkg/rtcp"
-	"github.com/pions/webrtc/pkg/rtp"
-	"github.com/pkg/errors"
-)
-
-const (
-	srtpMasterKeyLen     = 16
-	srtpMasterKeySaltLen = 14
-)
-
-// TODO: Migrate to srtp.Conn
-
-// CreateContextSRTP takes the exported keying material from DTLS and creates Client/Server contexts
-func (m *Manager) CreateContextSRTP(keyingMaterial []byte, isOffer bool) error {
-	offset := 0
-
-	clientWriteKey := append([]byte{}, keyingMaterial[offset:offset+srtpMasterKeyLen]...)
-	offset += srtpMasterKeyLen
-
-	serverWriteKey := append([]byte{}, keyingMaterial[offset:offset+srtpMasterKeyLen]...)
-	offset += srtpMasterKeyLen
-
-	clientWriteKey = append(clientWriteKey, keyingMaterial[offset:offset+srtpMasterKeySaltLen]...)
-	offset += srtpMasterKeySaltLen
-
-	serverWriteKey = append(serverWriteKey, keyingMaterial[offset:offset+srtpMasterKeySaltLen]...)
-
-	var err error
-	m.srtpInboundContextLock.Lock()
-	if isOffer {
-		m.srtpInboundContext, err = srtp.CreateContext(clientWriteKey[0:16], clientWriteKey[16:] /* Profile */, "")
-	} else {
-		m.srtpInboundContext, err = srtp.CreateContext(serverWriteKey[0:16], serverWriteKey[16:] /* Profile */, "")
-	}
-	m.srtpInboundContextLock.Unlock()
-	if err != nil {
-		return errors.New("failed to build inbound SRTP context")
-	}
-
-	m.srtpOutboundContextLock.Lock()
-	if isOffer {
-		m.srtpOutboundContext, err = srtp.CreateContext(serverWriteKey[0:16], serverWriteKey[16:] /* Profile */, "")
-	} else {
-		m.srtpOutboundContext, err = srtp.CreateContext(clientWriteKey[0:16], clientWriteKey[16:] /* Profile */, "")
-	}
-	m.srtpOutboundContextLock.Unlock()
-	if err != nil {
-		return errors.New("failed to build outbound SRTP context")
-	}
-
-	return nil
-}
-
-func handleRTCP(getBufferTransports func(uint32) *TransportPair, buffer []byte) {
-	//decrypted packets can also be compound packets, so we have to nest our reader loop here.
-	compoundPacket := rtcp.NewReader(bytes.NewReader(buffer))
-	for {
-		_, rawrtcp, err := compoundPacket.ReadPacket()
-
-		if err != nil {
-			if err == io.EOF {
-				break
-			}
-			fmt.Println(err)
-			return
-		}
-
-		var report rtcp.Packet
-		report, _, err = rtcp.Unmarshal(rawrtcp)
-		if err != nil {
-			fmt.Println(err)
-			return
-		}
-
-		f := func(ssrc uint32) {
-			bufferTransport := getBufferTransports(ssrc)
-			if bufferTransport != nil && bufferTransport.RTCP != nil {
-				select {
-				case bufferTransport.RTCP <- report:
-				default:
-				}
-			}
-		}
-
-		for _, ssrc := range report.DestinationSSRC() {
-			f(ssrc)
-		}
-	}
-}
-
-func (m *Manager) handleSRTP(buffer []byte) {
-	m.srtpInboundContextLock.Lock()
-	defer m.srtpInboundContextLock.Unlock()
-	if m.srtpInboundContext == nil {
-		fmt.Printf("Got RTP packet but no SRTP Context to handle it \n")
-		return
-	}
-
-	if len(buffer) > 4 {
-		var rtcpPacketType uint8
-
-		r := bytes.NewReader([]byte{buffer[1]})
-		if err := binary.Read(r, binary.BigEndian, &rtcpPacketType); err != nil {
-			fmt.Println("Failed to check packet for RTCP")
-			return
-		}
-
-		if rtcpPacketType >= 192 && rtcpPacketType <= 223 {
-			decrypted, err := m.srtpInboundContext.DecryptRTCP(buffer)
-			if err != nil {
-				fmt.Println(err)
-				fmt.Println(decrypted)
-				return
-			}
-
-			handleRTCP(m.getBufferTransports, decrypted)
-			return
-		}
-	}
-
-	packet := &rtp.Packet{}
-	if err := packet.Unmarshal(buffer); err != nil {
-		fmt.Println("Failed to unmarshal RTP packet")
-		return
-	}
-
-	if ok := m.srtpInboundContext.DecryptRTP(packet); !ok {
-		fmt.Println("Failed to decrypt packet")
-		return
-	}
-
-	bufferTransport := m.getOrCreateBufferTransports(packet.SSRC, packet.PayloadType)
-	if bufferTransport != nil && bufferTransport.RTP != nil {
-		select {
-		case bufferTransport.RTP <- packet:
-		default:
-		}
-	}
-
-}
diff --git a/internal/srtp/context.go b/internal/srtp/context.go
index 8f5e6fd1..228a58fb 100644
--- a/internal/srtp/context.go
+++ b/internal/srtp/context.go
@@ -11,6 +11,14 @@ import (
 	"github.com/pkg/errors"
 )
 
+// ProtectionProfile specifies Cipher and AuthTag details, similar to TLS cipher suite
+type ProtectionProfile uint16
+
+// Supported protection profiles
+const (
+	ProtectionProfileAes128CmHmacSha1_80 ProtectionProfile = 0x0001
+)
+
 const (
 	labelSRTPEncryption        = 0x00
 	labelSRTPAuthenticationTag = 0x01
@@ -59,7 +67,7 @@ type Context struct {
 }
 
 // CreateContext creates a new SRTP Context
-func CreateContext(masterKey, masterSalt []byte, profile string) (c *Context, err error) {
+func CreateContext(masterKey, masterSalt []byte, profile ProtectionProfile) (c *Context, err error) {
 	if masterKeyLen := len(masterKey); masterKeyLen != keyLen {
 		return c, errors.Errorf("SRTP Master Key must be len %d, got %d", masterKey, keyLen)
 	} else if masterSaltLen := len(masterSalt); masterSaltLen != saltLen {
diff --git a/internal/srtp/session.go b/internal/srtp/session.go
new file mode 100644
index 00000000..4f1441b3
--- /dev/null
+++ b/internal/srtp/session.go
@@ -0,0 +1,289 @@
+package srtp
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"net"
+	"sync"
+
+	"github.com/pions/webrtc/pkg/rtcp"
+	"github.com/pions/webrtc/pkg/rtp"
+)
+
+// SessionSRTP implements io.ReadWriteCloser and provides a bi-directional SRTP session
+// SRTP itself does not have a design like this, but it is common in most applications
+// for local/remote to each have their own keying material. This provides those patterns
+// instead of making everyone re-implement
+type SessionSRTP struct {
+	session
+	writeStream *WriteStream
+}
+
+// CreateSessionSRTP creates a new SessionSRTP
+func CreateSessionSRTP() *SessionSRTP {
+	s := &SessionSRTP{}
+	s.writeStream = &WriteStream{s}
+	s.session.initalize()
+	return s
+}
+
+// Start initializes any crypto context and allows reading/writing to begin
+func (s *SessionSRTP) Start(localMasterKey, localMasterSalt, remoteMasterKey, remoteMasterSalt []byte, profile ProtectionProfile, nextConn net.Conn) error {
+	s.session.nextConn = nextConn
+	return s.session.start(localMasterKey, localMasterSalt, remoteMasterKey, remoteMasterSalt, profile, s)
+}
+
+// OpenWriteStream returns the global write stream for the Session
+func (s *SessionSRTP) OpenWriteStream() (*WriteStream, error) {
+	return s.writeStream, nil
+}
+
+// OpenReadStream opens a read stream for the given SSRC, it can be used
+// if you want a certain SSRC, but don't want to wait for AcceptStream
+func (s *SessionSRTP) OpenReadStream(SSRC uint32) (*ReadStream, error) {
+	r, _ := s.session.getOrCreateReadStream(SSRC, s)
+	return r, nil
+}
+
+// AcceptStream returns a stream to handle RTCP for a single SSRC
+func (s *SessionSRTP) AcceptStream() (*ReadStream, uint32, error) {
+	stream, ok := <-s.newStream
+	if !ok {
+		return nil, 0, fmt.Errorf("SessionSRTP has been closed")
+	}
+
+	return stream, stream.GetSSRC(), nil
+}
+
+// Close ends the session
+func (s *SessionSRTP) Close() error {
+	return nil
+}
+
+func (s *SessionSRTP) write(buf []byte) (int, error) {
+	_, ok := <-s.session.started
+	if ok {
+		return 0, fmt.Errorf("started channel used incorrectly, should only be closed")
+	}
+
+	s.session.localContextMutex.Lock()
+	defer s.session.localContextMutex.Unlock()
+
+	encrypted, err := s.localContext.EncryptRTP(buf)
+	if err != nil {
+		return 0, err
+	}
+	return s.session.nextConn.Write(encrypted)
+}
+
+func (s *SessionSRTP) decrypt(buf []byte) error {
+	decrypted, err := s.remoteContext.DecryptRTP(buf)
+	if err != nil {
+		return err
+	}
+
+	p := &rtp.Packet{}
+	if err := p.Unmarshal(decrypted); err != nil {
+		return err
+	}
+
+	r, isNew := s.session.getOrCreateReadStream(p.SSRC, s)
+	if r == nil {
+		return nil // Session has been closed
+	} else if isNew {
+		s.session.newStream <- r // Notify AcceptStream
+	}
+
+	r.decrypted <- decrypted
+	return nil
+}
+
+// SessionSRTCP implements io.ReadWriteCloser and provides a bi-directional SRTP session
+// SRTP itself does not have a design like this, but it is common in most applications
+// for local/remote to each have their own keying material. This provides those patterns
+// instead of making everyone re-implement
+type SessionSRTCP struct {
+	session
+	writeStream *WriteStream
+}
+
+// CreateSessionSRTCP creates a new SessionSRTCP
+func CreateSessionSRTCP() *SessionSRTCP {
+	s := &SessionSRTCP{}
+	s.writeStream = &WriteStream{s}
+	s.session.initalize()
+	return s
+}
+
+// Start initializes any crypto context and allows reading/writing to begin
+func (s *SessionSRTCP) Start(localMasterKey, localMasterSalt, remoteMasterKey, remoteMasterSalt []byte, profile ProtectionProfile, nextConn net.Conn) error {
+	s.session.nextConn = nextConn
+	return s.session.start(localMasterKey, localMasterSalt, remoteMasterKey, remoteMasterSalt, profile, s)
+}
+
+// OpenWriteStream returns the global write stream for the Session
+func (s *SessionSRTCP) OpenWriteStream() (*WriteStream, error) {
+	return s.writeStream, nil
+}
+
+// OpenReadStream opens a read stream for the given SSRC, it can be used
+// if you want a certain SSRC, but don't want to wait for AcceptStream
+func (s *SessionSRTCP) OpenReadStream(SSRC uint32) (*ReadStream, error) {
+	r, _ := s.session.getOrCreateReadStream(SSRC, s)
+	return r, nil
+}
+
+// AcceptStream returns a stream to handle RTCP for a single SSRC
+func (s *SessionSRTCP) AcceptStream() (*ReadStream, uint32, error) {
+	stream, ok := <-s.newStream
+	if !ok {
+		return nil, 0, fmt.Errorf("SessionSRTP has been closed")
+	}
+
+	return stream, stream.GetSSRC(), nil
+}
+
+// Close ends the session
+func (s *SessionSRTCP) Close() error {
+	return nil
+}
+
+func (s *SessionSRTCP) write(buf []byte) (int, error) {
+	_, ok := <-s.session.started
+	if ok {
+		return 0, fmt.Errorf("started channel used incorrectly, should only be closed")
+	}
+
+	s.session.localContextMutex.Lock()
+	defer s.session.localContextMutex.Unlock()
+
+	encrypted, err := s.localContext.EncryptRTCP(buf)
+	if err != nil {
+		return 0, err
+	}
+	return s.session.nextConn.Write(encrypted)
+}
+
+func (s *SessionSRTCP) decrypt(buf []byte) error {
+	decrypted, err := s.remoteContext.DecryptRTCP(buf)
+	if err != nil {
+		return err
+	}
+
+	compoundPacket := rtcp.NewReader(bytes.NewReader(decrypted))
+	for {
+		_, rawrtcp, err := compoundPacket.ReadPacket()
+		if err != nil {
+			if err == io.EOF {
+				return nil
+			}
+			return err
+		}
+
+		var report rtcp.Packet
+		report, _, err = rtcp.Unmarshal(rawrtcp)
+		if err != nil {
+			return err
+		}
+
+		for _, ssrc := range report.DestinationSSRC() {
+			r, isNew := s.session.getOrCreateReadStream(ssrc, s)
+			if r == nil {
+				return nil // Session has been closed
+			} else if isNew {
+				s.session.newStream <- r // Notify AcceptStream
+			}
+
+			r.decrypted <- decrypted
+		}
+	}
+}
+
+/*
+	Private
+*/
+type session struct {
+	localContextMutex           sync.Mutex
+	localContext, remoteContext *Context
+
+	newStream chan *ReadStream
+	started   chan interface{}
+
+	readStreamsClosed bool
+	readStreams       map[uint32]*ReadStream
+	readStreamsLock   sync.Mutex
+
+	nextConn net.Conn
+}
+
+func (s *session) getOrCreateReadStream(ssrc uint32, child streamSession) (*ReadStream, bool) {
+	s.readStreamsLock.Lock()
+	defer s.readStreamsLock.Unlock()
+
+	if s.readStreamsClosed {
+		return nil, false
+	}
+
+	isNew := false
+	r, ok := s.readStreams[ssrc]
+	if !ok {
+		r = &ReadStream{s: child, decrypted: make(chan []byte), ssrc: ssrc}
+		s.readStreams[ssrc] = r
+
+		isNew = true
+	}
+
+	return r, isNew
+}
+
+func (s *session) initalize() {
+	s.readStreams = map[uint32]*ReadStream{}
+	s.newStream = make(chan *ReadStream)
+	s.started = make(chan interface{})
+}
+
+func (s *session) start(localMasterKey, localMasterSalt, remoteMasterKey, remoteMasterSalt []byte, profile ProtectionProfile, child streamSession) error {
+	var err error
+	s.localContext, err = CreateContext(localMasterKey, localMasterSalt, profile)
+	if err != nil {
+		return err
+	}
+
+	s.remoteContext, err = CreateContext(remoteMasterKey, remoteMasterSalt, profile)
+	if err != nil {
+		return err
+	}
+
+	go func() {
+		defer func() {
+			close(s.newStream)
+
+			s.readStreamsLock.Lock()
+			s.readStreamsClosed = true
+			for _, r := range s.readStreams {
+				close(r.decrypted)
+			}
+			s.readStreamsLock.Unlock()
+		}()
+
+		b := make([]byte, 8192)
+		for {
+			var i int
+			i, err = s.nextConn.Read(b)
+			if err != nil {
+				fmt.Println(err)
+				return
+			}
+
+			if err = child.decrypt(b[:i]); err != nil {
+				fmt.Println(err)
+				return
+			}
+		}
+	}()
+
+	close(s.started)
+
+	return nil
+}
diff --git a/internal/srtp/srtcp.go b/internal/srtp/srtcp.go
index 197caa4c..ebceab03 100644
--- a/internal/srtp/srtcp.go
+++ b/internal/srtp/srtcp.go
@@ -6,7 +6,6 @@ import (
 )
 
 // DecryptRTCP decrypts a buffer that contains a RTCP packet
-// We can't pass *rtcp.Packet as the encrypt will obscure significant fields
 func (c *Context) DecryptRTCP(encrypted []byte) ([]byte, error) {
 	tailOffset := len(encrypted) - (authTagSize + srtcpIndexSize)
 	out := append([]byte{}, encrypted[0:tailOffset]...)
diff --git a/internal/srtp/srtp.go b/internal/srtp/srtp.go
index 0e14be7c..f47d2263 100644
--- a/internal/srtp/srtp.go
+++ b/internal/srtp/srtp.go
@@ -5,47 +5,57 @@ import (
 	"encoding/binary"
 
 	"github.com/pions/webrtc/pkg/rtp"
+	"github.com/pkg/errors"
 )
 
 // DecryptRTP decrypts a RTP packet with an encrypted payload
-func (c *Context) DecryptRTP(packet *rtp.Packet) bool {
-	s := c.getSSRCState(packet.SSRC)
+func (c *Context) DecryptRTP(encrypted []byte) ([]byte, error) {
+	p := &rtp.Packet{}
+	if err := p.Unmarshal(append([]byte{}, encrypted...)); err != nil {
+		return nil, err
+	}
+
+	s := c.getSSRCState(p.SSRC)
 
-	c.updateRolloverCount(packet.SequenceNumber, s)
+	c.updateRolloverCount(p.SequenceNumber, s)
 
-	pktWithROC := append([]byte{}, packet.Raw[:len(packet.Raw)-authTagSize]...)
+	pktWithROC := append([]byte{}, p.Raw[:len(p.Raw)-authTagSize]...)
 	pktWithROC = append(pktWithROC, make([]byte, 4)...)
 	binary.BigEndian.PutUint32(pktWithROC[len(pktWithROC)-4:], s.rolloverCounter)
 
-	actualAuthTag := packet.Payload[len(packet.Payload)-authTagSize:]
+	actualAuthTag := p.Payload[len(p.Payload)-authTagSize:]
 	verified, err := c.verifyAuthTag(pktWithROC, actualAuthTag)
-	if err != nil || !verified {
-		return false
+	if err != nil {
+		return nil, err
+	} else if !verified {
+		return nil, errors.Errorf("Failed to verify auth tag")
 	}
 
-	packet.Payload = packet.Payload[:len(packet.Payload)-authTagSize]
-
-	stream := cipher.NewCTR(c.srtpBlock, c.generateCounter(packet.SequenceNumber, s.rolloverCounter, s.ssrc, c.srtpSessionSalt))
-	stream.XORKeyStream(packet.Payload, packet.Payload)
+	p.Payload = p.Payload[:len(p.Payload)-authTagSize]
 
-	// Replace payload with decrypted
-	packet.Raw = append(packet.Raw[0:packet.PayloadOffset], packet.Payload...)
+	stream := cipher.NewCTR(c.srtpBlock, c.generateCounter(p.SequenceNumber, s.rolloverCounter, s.ssrc, c.srtpSessionSalt))
+	stream.XORKeyStream(p.Payload, p.Payload)
 
-	return true
+	return append(p.Raw[0:p.PayloadOffset], p.Payload...), nil
 }
 
 // EncryptRTP Encrypts a SRTP packet in place
-func (c *Context) EncryptRTP(packet *rtp.Packet) bool {
-	s := c.getSSRCState(packet.SSRC)
+func (c *Context) EncryptRTP(encrypted []byte) ([]byte, error) {
+	p := &rtp.Packet{}
+	if err := p.Unmarshal(append([]byte{}, encrypted...)); err != nil {
+		return nil, err
+	}
+
+	s := c.getSSRCState(p.SSRC)
 
-	c.updateRolloverCount(packet.SequenceNumber, s)
+	c.updateRolloverCount(p.SequenceNumber, s)
 
-	stream := cipher.NewCTR(c.srtpBlock, c.generateCounter(packet.SequenceNumber, s.rolloverCounter, s.ssrc, c.srtpSessionSalt))
-	stream.XORKeyStream(packet.Payload, packet.Payload)
+	stream := cipher.NewCTR(c.srtpBlock, c.generateCounter(p.SequenceNumber, s.rolloverCounter, s.ssrc, c.srtpSessionSalt))
+	stream.XORKeyStream(p.Payload, p.Payload)
 
-	fullPkt, err := packet.Marshal()
+	fullPkt, err := p.Marshal()
 	if err != nil {
-		return false
+		return nil, err
 	}
 
 	fullPkt = append(fullPkt, make([]byte, 4)...)
@@ -53,13 +63,10 @@ func (c *Context) EncryptRTP(packet *rtp.Packet) bool {
 
 	authTag, err := c.generateAuthTag(fullPkt, c.srtpSessionAuthTag)
 	if err != nil {
-		return false
+		return nil, err
 	}
 
-	packet.Payload = append(packet.Payload, authTag...)
-	packet.Raw = append(packet.Raw[0:packet.PayloadOffset], packet.Payload...)
-
-	return true
+	return append(p.Raw[0:p.PayloadOffset], append(p.Payload, authTag...)...), nil
 }
 
 // https://tools.ietf.org/html/rfc3550#appendix-A.1
diff --git a/internal/srtp/stream.go b/internal/srtp/stream.go
new file mode 100644
index 00000000..9ec5c86d
--- /dev/null
+++ b/internal/srtp/stream.go
@@ -0,0 +1,46 @@
+package srtp
+
+import "fmt"
+
+type streamSession interface {
+	Close() error
+	write([]byte) (int, error)
+	decrypt([]byte) error
+}
+
+// ReadStream handles decryption for a single SSRC
+type ReadStream struct {
+	s streamSession
+
+	decrypted chan []byte
+	ssrc      uint32
+}
+
+// GetSSRC returns the SSRC this ReadStream gets data for
+func (r *ReadStream) GetSSRC() uint32 {
+	return r.ssrc
+}
+
+// Read reads decrypted packets from the stream
+func (r *ReadStream) Read(buf []byte) (int, error) {
+	decrypted, ok := <-r.decrypted
+	if !ok {
+		return 0, fmt.Errorf("Stream has been closed")
+	} else if len(decrypted) > len(buf) {
+		return 0, fmt.Errorf("Buffer is to small to copy")
+	}
+
+	copy(buf, decrypted)
+	return len(decrypted), nil
+}
+
+// WriteStream is stream for a single Session that is used to encrypt
+// RTP or RTCP
+type WriteStream struct {
+	session streamSession
+}
+
+// Write encrypts the passed RTP/RTCP buffer and writes to the session
+func (w *WriteStream) Write(buf []byte) (int, error) {
+	return w.session.write(buf)
+}
diff --git a/media.go b/media.go
index 5a43d560..e33f0aff 100644
--- a/media.go
+++ b/media.go
@@ -18,4 +18,5 @@ type RTCTrack struct {
 	RTCPPackets <-chan rtcp.Packet
 	Samples     chan<- media.RTCSample
 	RawRTP      chan<- *rtp.Packet
+	isRawRTP    bool
 }
diff --git a/pkg/ice/agent.go b/pkg/ice/agent.go
index 313c556b..00d47684 100644
--- a/pkg/ice/agent.go
+++ b/pkg/ice/agent.go
@@ -163,14 +163,14 @@ func (a *Agent) gatherCandidatesLocal() {
 		for _, network := range supportedNetworks {
 			conn, err := a.listenUDP(network, &net.UDPAddr{IP: ip, Port: 0})
 			if err != nil {
-				iceLog.Warningf("could not listen %s %s\n", network, ip)
+				iceLog.Warnf("could not listen %s %s\n", network, ip)
 				continue
 			}
 
 			port := conn.LocalAddr().(*net.UDPAddr).Port
 			c, err := NewCandidateHost(network, ip, port)
 			if err != nil {
-				iceLog.Warningf("Failed to create host candidate: %s %s %d: %v\n", network, ip, port, err)
+				iceLog.Warnf("Failed to create host candidate: %s %s %d: %v\n", network, ip, port, err)
 				continue
 			}
 
@@ -192,12 +192,12 @@ func (a *Agent) gatherCandidatesReflective(urls []*URL) {
 			case SchemeTypeSTUN:
 				laddr, xoraddr, err := allocateUDP(network, url)
 				if err != nil {
-					iceLog.Warningf("could not allocate %s %s: %v\n", network, url, err)
+					iceLog.Warnf("could not allocate %s %s: %v\n", network, url, err)
 					continue
 				}
 				conn, err := net.ListenUDP(network, laddr)
 				if err != nil {
-					iceLog.Warningf("could not listen %s %s: %v\n", network, laddr, err)
+					iceLog.Warnf("could not listen %s %s: %v\n", network, laddr, err)
 				}
 
 				ip := xoraddr.IP
@@ -206,7 +206,7 @@ func (a *Agent) gatherCandidatesReflective(urls []*URL) {
 				relPort := laddr.Port
 				c, err := NewCandidateServerReflexive(network, ip, port, relIP, relPort)
 				if err != nil {
-					iceLog.Warningf("Failed to create server reflexive candidate: %s %s %d: %v\n", network, ip, port, err)
+					iceLog.Warnf("Failed to create server reflexive candidate: %s %s %d: %v\n", network, ip, port, err)
 					continue
 				}
 
@@ -218,7 +218,7 @@ func (a *Agent) gatherCandidatesReflective(urls []*URL) {
 				c.start(a, conn)
 
 			default:
-				iceLog.Warningf("scheme %s is not implemented\n", url.Scheme)
+				iceLog.Warnf("scheme %s is not implemented\n", url.Scheme)
 				continue
 			}
 		}
@@ -494,7 +494,7 @@ func (a *Agent) Close() error {
 			for _, c := range cs {
 				err := c.close()
 				if err != nil {
-					iceLog.Warningf("Failed to close candidate %s: %v", c, err)
+					iceLog.Warnf("Failed to close candidate %s: %v", c, err)
 				}
 			}
 			delete(agent.localCandidates, net)
@@ -503,7 +503,7 @@ func (a *Agent) Close() error {
 			for _, c := range cs {
 				err := c.close()
 				if err != nil {
-					iceLog.Warningf("Failed to close candidate %s: %v", c, err)
+					iceLog.Warnf("Failed to close candidate %s: %v", c, err)
 				}
 			}
 			delete(agent.remoteCandidates, net)
@@ -530,7 +530,7 @@ func (a *Agent) findRemoteCandidate(networkType NetworkType, addr net.Addr) *Can
 		ip = a.IP
 		port = a.Port
 	default:
-		iceLog.Warningf("unsupported address type %T", a)
+		iceLog.Warnf("unsupported address type %T", a)
 		return nil
 	}
 
@@ -559,7 +559,7 @@ func (a *Agent) sendBindingSuccess(m *stun.Message, local, remote *Candidate) {
 		},
 		&stun.Fingerprint{},
 	); err != nil {
-		iceLog.Warningf("Failed to handle inbound ICE from: %s to: %s error: %s", local, remote, err)
+		iceLog.Warnf("Failed to handle inbound ICE from: %s to: %s error: %s", local, remote, err)
 	} else {
 		a.sendSTUN(out, local, remote)
 	}
diff --git a/pkg/ice/candidate.go b/pkg/ice/candidate.go
index f647a45a..f42abbff 100644
--- a/pkg/ice/candidate.go
+++ b/pkg/ice/candidate.go
@@ -89,14 +89,14 @@ func (c *Candidate) recvLoop() {
 		if stun.IsSTUN(buffer[:n]) {
 			m, err := stun.NewMessage(buffer[:n])
 			if err != nil {
-				iceLog.Warningf("Failed to handle decode ICE from %s to %s: %v", c.addr(), srcAddr, err)
+				iceLog.Warnf("Failed to handle decode ICE from %s to %s: %v", c.addr(), srcAddr, err)
 				continue
 			}
 			err = c.agent.run(func(agent *Agent) {
 				agent.handleInbound(m, c, srcAddr)
 			})
 			if err != nil {
-				iceLog.Warningf("Failed to handle message: %v", err)
+				iceLog.Warnf("Failed to handle message: %v", err)
 			}
 
 			continue
diff --git a/pkg/ice/candidatepair.go b/pkg/ice/candidatepair.go
index c274d9da..a3d244a6 100644
--- a/pkg/ice/candidatepair.go
+++ b/pkg/ice/candidatepair.go
@@ -32,7 +32,7 @@ func (a *Agent) keepaliveCandidate(local, remote *Candidate) {
 	)
 
 	if err != nil {
-		iceLog.Warning(err.Error())
+		iceLog.Warn(err.Error())
 		return
 	}
 
@@ -42,6 +42,6 @@ func (a *Agent) keepaliveCandidate(local, remote *Candidate) {
 func (a *Agent) sendSTUN(msg *stun.Message, local, remote *Candidate) {
 	_, err := local.writeTo(msg.Pack(), remote)
 	if err != nil {
-		iceLog.Warningf("failed to send STUN message: %s", err)
+		iceLog.Warnf("failed to send STUN message: %s", err)
 	}
 }
diff --git a/pkg/logging/leveled.go b/pkg/logging/leveled.go
index 377a3e67..0b751795 100644
--- a/pkg/logging/leveled.go
+++ b/pkg/logging/leveled.go
@@ -28,8 +28,8 @@ const (
 	// LogLevelError is for fatal errors which should be handled by user code,
 	// but are logged to ensure that they are seen
 	LogLevelError
-	// LogLevelWarning is for logging abnormal, but non-fatal library operation
-	LogLevelWarning
+	// LogLevelWarn is for logging abnormal, but non-fatal library operation
+	LogLevelWarn
 	// LogLevelInfo is for logging normal library operation (e.g. state transitions, etc.)
 	LogLevelInfo
 	// LogLevelDebug is for logging low-level library information (e.g. internal operations)
@@ -102,9 +102,9 @@ func (ll *LeveledLogger) WithInfoLogger(log *log.Logger) *LeveledLogger {
 	return ll
 }
 
-// WithWarningLogger is a chainable configuration function which sets the
-// Warning-level logger
-func (ll *LeveledLogger) WithWarningLogger(log *log.Logger) *LeveledLogger {
+// WithWarnLogger is a chainable configuration function which sets the
+// Warn-level logger
+func (ll *LeveledLogger) WithWarnLogger(log *log.Logger) *LeveledLogger {
 	ll.warn = log
 	return ll
 }
@@ -177,14 +177,14 @@ func (ll *LeveledLogger) Infof(format string, args ...interface{}) {
 	ll.logf(ll.info, LogLevelInfo, format, args...)
 }
 
-// Warning emits the preformatted message if the logger is at or below LogLevelWarning
-func (ll *LeveledLogger) Warning(msg string) {
-	ll.logf(ll.warn, LogLevelWarning, msg)
+// Warn emits the preformatted message if the logger is at or below LogLevelWarn
+func (ll *LeveledLogger) Warn(msg string) {
+	ll.logf(ll.warn, LogLevelWarn, msg)
 }
 
-// Warningf formats and emits a message if the logger is at or below LogLevelWarning
-func (ll *LeveledLogger) Warningf(format string, args ...interface{}) {
-	ll.logf(ll.warn, LogLevelWarning, format, args...)
+// Warnf formats and emits a message if the logger is at or below LogLevelWarn
+func (ll *LeveledLogger) Warnf(format string, args ...interface{}) {
+	ll.logf(ll.warn, LogLevelWarn, format, args...)
 }
 
 // Error emits the preformatted message if the logger is at or below LogLevelError
@@ -214,6 +214,6 @@ func NewLeveledLoggerForScope(scope string) *LeveledLogger {
 		WithTraceLogger(log.New(logger.writer, fmt.Sprintf("%s TRACE: ", scope), log.Lmicroseconds|log.Lshortfile)).
 		WithDebugLogger(log.New(logger.writer, fmt.Sprintf("%s DEBUG: ", scope), log.Lmicroseconds|log.Lshortfile)).
 		WithInfoLogger(log.New(logger.writer, fmt.Sprintf("%s INFO: ", scope), log.LstdFlags)).
-		WithWarningLogger(log.New(logger.writer, fmt.Sprintf("%s WARNING: ", scope), log.LstdFlags)).
+		WithWarnLogger(log.New(logger.writer, fmt.Sprintf("%s WARNING: ", scope), log.LstdFlags)).
 		WithErrorLogger(log.New(logger.writer, fmt.Sprintf("%s ERROR: ", scope), log.LstdFlags))
 }
diff --git a/pkg/logging/scoped.go b/pkg/logging/scoped.go
index 797c4171..44be5493 100644
--- a/pkg/logging/scoped.go
+++ b/pkg/logging/scoped.go
@@ -73,11 +73,11 @@ func NewScopedLogger(scope string) *LeveledLogger {
 
 func init() {
 	logLevels := map[string]LogLevel{
-		"ERROR":   LogLevelError,
-		"WARNING": LogLevelWarning,
-		"INFO":    LogLevelInfo,
-		"DEBUG":   LogLevelDebug,
-		"TRACE":   LogLevelTrace,
+		"ERROR": LogLevelError,
+		"WARN":  LogLevelWarn,
+		"INFO":  LogLevelInfo,
+		"DEBUG": LogLevelDebug,
+		"TRACE": LogLevelTrace,
 	}
 
 	for name, level := range logLevels {
diff --git a/rtcpeerconnection.go b/rtcpeerconnection.go
index 23bbc5dc..6dd54872 100644
--- a/rtcpeerconnection.go
+++ b/rtcpeerconnection.go
@@ -5,14 +5,13 @@ import (
 	"crypto/ecdsa"
 	"crypto/elliptic"
 	"crypto/rand"
+	"encoding/binary"
 	"fmt"
 	"net"
 	"strings"
 	"sync"
 	"time"
 
-	"encoding/binary"
-
 	"github.com/pions/datachannel"
 	"github.com/pions/sdp"
 	"github.com/pions/webrtc/internal/network"
@@ -161,7 +160,7 @@ func New(configuration RTCConfiguration) (*RTCPeerConnection, error) {
 		}
 	}
 
-	pc.networkManager, err = network.NewManager(urls, pc.generateChannel, pc.iceStateChange, defaultSettingEngine.EphemeralUDP.PortMin, defaultSettingEngine.EphemeralUDP.PortMax)
+	pc.networkManager, err = network.NewManager(urls, pc.iceStateChange, defaultSettingEngine.EphemeralUDP.PortMin, defaultSettingEngine.EphemeralUDP.PortMax)
 	if err != nil {
 		return nil, err
 	}
@@ -749,12 +748,20 @@ func (pc *RTCPeerConnection) SetRemoteDescription(desc RTCSessionDescription) er
 			remoteUfrag, remotePwd,
 			cert.x509Cert, cert.privateKey, fingerprint, fingerprintHash)
 		if err != nil {
-			pcLog.Warningf("Failed to start manager: %s", err)
+			pcLog.Warnf("Failed to start manager: %s", err)
 		}
 
+		// Temporary SRTP glue
+		go pc.acceptSRTP()
+		go pc.acceptSRTCP()
+
 		// Temporary data channel glue
+		if err = pc.networkManager.StartSCTP(weOffer); err != nil {
+			pcLog.Warnf("Failed to start SCTP: %s", err)
+		}
 		pc.openDataChannels()
 		pc.acceptDataChannels()
+
 	}()
 
 	return nil
@@ -775,7 +782,7 @@ func (pc *RTCPeerConnection) openDataChannels() {
 				Label:                rtcDC.Label,
 			})
 		if err != nil {
-			pcLog.Warningf("failed to open data channel: %s", err)
+			pcLog.Warnf("failed to open data channel: %s", err)
 			continue
 		}
 		rtcDC.ReadyState = RTCDataChannelStateOpen
@@ -789,7 +796,7 @@ func (pc *RTCPeerConnection) acceptDataChannels() {
 	for {
 		dc, err := pc.networkManager.AcceptDataChannel()
 		if err != nil {
-			pcLog.Warningf("Failed to accept data channel: %s", err)
+			pcLog.Warnf("Failed to accept data channel: %s", err)
 			// TODO: Kill DataChannel/PeerConnection?
 			return
 		}
@@ -811,6 +818,114 @@ func (pc *RTCPeerConnection) acceptDataChannels() {
 	}
 }
 
+// TODO: Move to RTCRTpSender?
+func (pc *RTCPeerConnection) acceptSRTCP() {
+	for {
+		r, ssrc, err := pc.networkManager.SrtcpSession.AcceptStream()
+		if err != nil {
+			pcLog.Warnf("Failed to accept RTCP %v \n", err)
+			return
+		}
+
+		go func() {
+			rtcpBuf := make([]byte, receiveMTU)
+			var rtcpPacket rtcp.Packet
+
+			for {
+				i, err := r.Read(rtcpBuf)
+				if err != nil {
+					pcLog.Warnf("Failed to read, RTCTrack done for: %v %d \n", err, ssrc)
+					return
+				}
+
+				rtcpPacket, _, err = rtcp.Unmarshal(rtcpBuf[:i])
+				if err != nil {
+					pcLog.Warnf("Failed to unmarshal RTCP packet, discarding: %v \n", err)
+					continue
+				}
+				pcLog.Debugf("got RTCP: %+v", rtcpPacket)
+			}
+		}()
+
+	}
+}
+
+// TODO: Move to RTCRTpSender?
+func (pc *RTCPeerConnection) acceptSRTP() {
+	for {
+		r, ssrc, err := pc.networkManager.SrtpSession.AcceptStream()
+		if err != nil {
+			return
+		}
+
+		rtpBuf := make([]byte, receiveMTU)
+		rtpLen, err := r.Read(rtpBuf)
+		if err != nil {
+			pcLog.Warnf("Failed to read, ignoring AcceptStream: %v \n", err)
+			continue
+		}
+
+		rtpChannel, rtcpChannel, err := pc.generateChannel(rtpBuf)
+		if err != nil {
+			pcLog.Warnf("Failed to create output channels, ignoring AcceptStream: %v \n", err)
+			continue
+		}
+
+		// RTP
+		go func() {
+			for {
+				rtpPacket := &rtp.Packet{}
+				rtpLen, err = r.Read(rtpBuf)
+				if err != nil {
+					pcLog.Warnf("Failed to read, RTCTrack done for: %v %d \n", err, ssrc)
+					return
+				} else if err := rtpPacket.Unmarshal(append([]byte{}, rtpBuf[:rtpLen]...)); err != nil {
+					pcLog.Warnf("Failed to unmarshal RTP packet, discarding: %v \n", err)
+					continue
+				}
+
+				select {
+				case rtpChannel <- rtpPacket:
+				default:
+				}
+			}
+		}()
+
+		// RTCP
+		go func() {
+			readStream, err := pc.networkManager.SrtcpSession.OpenReadStream(ssrc)
+			if err != nil {
+				pcLog.Warnf("Failed to open RTCP ReadStream, RTCTrack done for: %v %d \n", err, ssrc)
+				return
+			}
+
+			rtcpBuf := make([]byte, receiveMTU)
+
+			for {
+				var (
+					rtcpPacket rtcp.Packet
+					rtcpLen    int
+				)
+				rtcpLen, err = readStream.Read(rtcpBuf)
+				if err != nil {
+					pcLog.Warnf("Failed to read, RTCTrack done for: %v %d \n", err, ssrc)
+					return
+				}
+
+				rtcpPacket, _, err = rtcp.Unmarshal(rtcpBuf[:rtcpLen])
+				if err != nil {
+					pcLog.Warnf("Failed to unmarshal RTCP packet, discarding: %v \n", err)
+					continue
+				}
+				select {
+				case rtcpChannel <- rtcpPacket:
+				default:
+				}
+			}
+		}()
+	}
+}
+
 // RemoteDescription returns PendingRemoteDescription if it is not null and
 // otherwise it returns CurrentRemoteDescription. This property is used to
 // determine if setRemoteDescription has already been called.
@@ -1076,7 +1191,15 @@ func (pc *RTCPeerConnection) SendRTCP(pkt rtcp.Packet) error {
 	if err != nil {
 		return err
 	}
-	pc.networkManager.SendRTCP(raw)
+
+	writeStream, err := pc.networkManager.SrtcpSession.OpenWriteStream()
+	if err != nil {
+		return fmt.Errorf("SendRTCP failed to open WriteStream: %v", err)
+	}
+
+	if _, err := writeStream.Write(raw); err != nil {
+		return fmt.Errorf("SendRTCP failed to write: %v", err)
+	}
 	return nil
 }
 
@@ -1087,6 +1210,16 @@ func (pc *RTCPeerConnection) Close() error {
 		return nil
 	}
 
+	for _, t := range pc.rtpTransceivers {
+		if track := t.Sender.Track; track != nil {
+			if track.isRawRTP {
+				close(track.RawRTP)
+			} else {
+				close(track.Samples)
+			}
+		}
+	}
+
 	err := pc.networkManager.Close()
 
 	// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #3)
@@ -1097,7 +1230,7 @@ func (pc *RTCPeerConnection) Close() error {
 
 	// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #11)
 	// pc.IceConnectionState = RTCIceConnectionStateClosed
-	pc.IceConnectionState = ice.ConnectionStateClosed // FIXME REMOVE
+	pc.iceStateChange(ice.ConnectionStateClosed) // FIXME REMOVE
 
 	// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #12)
 	pc.ConnectionState = RTCPeerConnectionStateClosed
@@ -1106,44 +1239,46 @@ func (pc *RTCPeerConnection) Close() error {
 }
 
 /* Everything below is private */
-func (pc *RTCPeerConnection) generateChannel(ssrc uint32, payloadType uint8) (tpair *network.TransportPair) {
+func (pc *RTCPeerConnection) generateChannel(rawRTP []byte) (chan *rtp.Packet, chan rtcp.Packet, error) {
 	pc.RLock()
 	if pc.onTrackHandler == nil {
 		pc.RUnlock()
-		return nil
+		return nil, nil, fmt.Errorf("OnTrack unset, unable to handle incoming")
 	}
 	pc.RUnlock()
 
-	sdpCodec, err := pc.CurrentLocalDescription.parsed.GetCodecForPayloadType(payloadType)
+	rtpPacket := &rtp.Packet{}
+	if err := rtpPacket.Unmarshal(rawRTP); err != nil {
+		return nil, nil, fmt.Errorf("failed to unmarshal RTP packet, discarding: %v", err)
+	}
+
+	sdpCodec, err := pc.CurrentLocalDescription.parsed.GetCodecForPayloadType(rtpPacket.PayloadType)
 	if err != nil {
-		pcLog.Warningf("No codec could be found in RemoteDescription for payloadType %d \n", payloadType)
-		return nil
+		return nil, nil, fmt.Errorf("no codec could be found in RemoteDescription for payloadType %d", rtpPacket.PayloadType)
 	}
 
 	codec, err := pc.mediaEngine.getCodecSDP(sdpCodec)
 	if err != nil {
-		pcLog.Warningf("Codec %s in not registered\n", sdpCodec)
-		return nil
+		return nil, nil, fmt.Errorf("codec %s in not registered", sdpCodec)
 	}
 
 	rtpTransport := make(chan *rtp.Packet, 15)
 	rtcpTransport := make(chan rtcp.Packet, 15)
 
 	track := &RTCTrack{
-		PayloadType: payloadType,
+		PayloadType: rtpPacket.PayloadType,
 		Kind:        codec.Type,
 		ID:          "0", // TODO extract from remoteDescription
 		Label:       "",  // TODO extract from remoteDescription
-		Ssrc:        ssrc,
+		Ssrc:        rtpPacket.SSRC,
 		Codec:       codec,
 		Packets:     rtpTransport,
 		RTCPPackets: rtcpTransport,
 	}
 
 	// TODO: Register the receiving Track
-
 	pc.onTrack(track)
-	return &network.TransportPair{RTP: rtpTransport, RTCP: rtcpTransport}
+	return rtpTransport, rtcpTransport, nil
 }
 
 func (pc *RTCPeerConnection) iceStateChange(newState ice.ConnectionState) {
@@ -1253,19 +1388,38 @@ func (pc *RTCPeerConnection) addDataMediaSection(d *sdp.SessionDescription, midV
 	d.WithMedia(media)
 }
 
+// TODO RTCRtpSender
+func (pc *RTCPeerConnection) sendRTP(packet *rtp.Packet) {
+	raw, err := packet.Marshal()
+	if err != nil {
+		pcLog.Warnf("SendRTP failed to marshal packet: %s \n", err.Error())
+		return
+	}
+
+	writeStream, err := pc.networkManager.SrtpSession.OpenWriteStream()
+	if err != nil {
+		pcLog.Warnf("SendRTP failed to open WriteStream: %v", err)
+		return
+	}
+
+	if _, err := writeStream.Write(raw); err != nil {
+		pcLog.Warnf("SendRTP failed to write: %v", err)
+	}
+}
+
 func (pc *RTCPeerConnection) newRTCTrack(payloadType uint8, ssrc uint32, id, label string) (*RTCTrack, error) {
 	codec, err := pc.mediaEngine.getCodec(payloadType)
 	if err != nil {
 		return nil, err
-	}
-
-	if codec.Payloader == nil {
+	} else if codec.Payloader == nil {
 		return nil, errors.New("codec payloader not set")
 	}
 
 	trackInput := make(chan media.RTCSample, 15) // Is the buffering needed?
 	rawPackets := make(chan *rtp.Packet)
 	rtcpPackets := make(chan rtcp.Packet)
+	isRawRTP := false
+
 	if ssrc == 0 {
 		buf := make([]byte, 4)
 		_, err = rand.Read(buf)
@@ -1285,10 +1439,13 @@ func (pc *RTCPeerConnection) newRTCTrack(payloadType uint8, ssrc uint32, id, lab
 			)
 
 			for {
-				in := <-trackInput
+				in, ok := <-trackInput
+				if !ok {
+					return
+				}
 				packets := packetizer.Packetize(in.Data, in.Samples)
 				for _, p := range packets {
-					pc.networkManager.SendRTP(p)
+					pc.sendRTP(p)
 				}
 			}
 		}()
@@ -1296,16 +1453,22 @@ func (pc *RTCPeerConnection) newRTCTrack(payloadType uint8, ssrc uint32, id, lab
 	} else {
 		// If SSRC is not 0, then we are working with an established RTP stream
 		// and need to accept raw RTP packets for forwarding.
+		isRawRTP = true
 		go func() {
 			for {
-				p := <-rawPackets
-				pc.networkManager.SendRTP(p)
+				p, ok := <-rawPackets
+				if !ok {
+					return
+				}
+
+				pc.sendRTP(p)
 			}
 		}()
 		close(trackInput)
 	}
 
 	t := &RTCTrack{
+		isRawRTP:    isRawRTP,
 		PayloadType: payloadType,
 		Kind:        codec.Type,
 		ID:          id,
@@ -1317,7 +1480,35 @@ func (pc *RTCPeerConnection) newRTCTrack(payloadType uint8, ssrc uint32, id, lab
 		RawRTP:      rawPackets,
 	}
 
-	pc.networkManager.AddTransportPair(ssrc, nil, rtcpPackets)
+	// Inbound RTCP
+	go func() {
+		readStream, err := pc.networkManager.SrtcpSession.OpenReadStream(ssrc)
+		if err != nil {
+			pcLog.Warnf("Failed to open RTCP ReadStream, RTCTrack done for: %v %d \n", err, ssrc)
+			return
+		}
+
+		rtcpBuf := make([]byte, receiveMTU)
+		var rtcpPacket rtcp.Packet
+
+		for {
+			i, err := readStream.Read(rtcpBuf)
+			if err != nil {
+				pcLog.Warnf("Failed to read, RTCTrack done for: %v %d \n", err, ssrc)
+				return
+			}
+
+			rtcpPacket, _, err = rtcp.Unmarshal(rtcpBuf[:i])
+			if err != nil {
+				pcLog.Warnf("Failed to unmarshal RTCP packet, discarding: %v \n", err)
+				continue
+			}
+			select {
+			case rtcpPackets <- rtcpPacket:
+			default:
+			}
+		}
+	}()
 
 	return t, nil
 }


Test patch:
diff --git a/internal/network/port_test.go b/internal/network/port_test.go
deleted file mode 100644
index 521c7369..00000000
--- a/internal/network/port_test.go
+++ /dev/null
@@ -1,115 +0,0 @@
-package network
-
-import (
-	"testing"
-
-	"github.com/pions/webrtc/pkg/rtcp"
-	"github.com/stretchr/testify/assert"
-)
-
-// An RTCP packet from a packet dump
-var realPacket = []byte{
-	// Receiver Report (offset=0)
-	// v=2, p=0, count=1, RR, len=7
-	0x81, 0xc9, 0x0, 0x7,
-	// ssrc=0x902f9e2e
-	0x90, 0x2f, 0x9e, 0x2e,
-	// ssrc=0xbc5e9a40
-	0xbc, 0x5e, 0x9a, 0x40,
-	// fracLost=0, totalLost=0
-	0x0, 0x0, 0x0, 0x0,
-	// lastSeq=0x46e1
-	0x0, 0x0, 0x46, 0xe1,
-	// jitter=273
-	0x0, 0x0, 0x1, 0x11,
-	// lsr=0x9f36432
-	0x9, 0xf3, 0x64, 0x32,
-	// delay=150137
-	0x0, 0x2, 0x4a, 0x79,
-
-	// Source Description (offset=32)
-	// v=2, p=0, count=1, SDES, len=12
-	0x81, 0xca, 0x0, 0xc,
-	// ssrc=0x902f9e2e
-	0x90, 0x2f, 0x9e, 0x2e,
-	// CNAME, len=38
-	0x1, 0x26,
-	// text="{9c00eb92-1afb-9d49-a47d-91f64eee69f5}"
-	0x7b, 0x39, 0x63, 0x30,
-	0x30, 0x65, 0x62, 0x39,
-	0x32, 0x2d, 0x31, 0x61,
-	0x66, 0x62, 0x2d, 0x39,
-	0x64, 0x34, 0x39, 0x2d,
-	0x61, 0x34, 0x37, 0x64,
-	0x2d, 0x39, 0x31, 0x66,
-	0x36, 0x34, 0x65, 0x65,
-	0x65, 0x36, 0x39, 0x66,
-	0x35, 0x7d,
-	// END + padding
-	0x0, 0x0, 0x0, 0x0,
-
-	// Goodbye (offset=84)
-	// v=2, p=0, count=1, BYE, len=1
-	0x81, 0xcb, 0x0, 0x1,
-	// source=0x902f9e2e
-	0x90, 0x2f, 0x9e, 0x2e,
-
-	// Picture Loss Indication (offset=92)
-	0x81, 0xce, 0x0, 0x2,
-	// sender=0x902f9e2e
-	0x90, 0x2f, 0x9e, 0x2e,
-	// media=0x902f9e2e
-	0x90, 0x2f, 0x9e, 0x2e,
-
-	// RapidResynchronizationRequest (offset=104)
-	0x85, 0xcd, 0x0, 0x2,
-	// sender=0x902f9e2e
-	0x90, 0x2f, 0x9e, 0x2e,
-	// media=0x902f9e2e
-	0x90, 0x2f, 0x9e, 0x2e,
-}
-
-func TestHandleRtcp(t *testing.T) {
-	pktChannel := make(chan rtcp.Packet, 6)
-	getBufStub := func(uint32) *TransportPair {
-		return &TransportPair{nil, pktChannel}
-	}
-
-	handleRTCP(getBufStub, realPacket)
-
-	select {
-	case parsed := <-pktChannel:
-		assert.IsType(t, parsed, (*rtcp.ReceiverReport)(nil), "Unmarshalled to incorrect type")
-	default:
-		t.Fatalf("Not enough packets parsed from channel")
-	}
-	select {
-	case parsed := <-pktChannel:
-		assert.IsType(t, parsed, (*rtcp.SourceDescription)(nil), "Unmarshalled to incorrect type")
-	default:
-		t.Fatalf("Not enough packets parsed from channel")
-	}
-	select {
-	case parsed := <-pktChannel:
-		assert.IsType(t, parsed, (*rtcp.Goodbye)(nil), "Unmarshalled to incorrect type")
-	default:
-		t.Fatalf("Not enough packets parsed from channel")
-	}
-	select {
-	case parsed := <-pktChannel:
-		assert.IsType(t, parsed, (*rtcp.PictureLossIndication)(nil), "Unmarshalled to incorrect type")
-	default:
-		t.Fatalf("Not enough packets parsed from channel")
-	}
-	select {
-	case parsed := <-pktChannel:
-		assert.IsType(t, parsed, (*rtcp.RapidResynchronizationRequest)(nil), "Unmarshalled to incorrect type")
-	default:
-		t.Fatalf("Not enough packets parsed from channel")
-	}
-	select {
-	case <-pktChannel:
-		t.Fatalf("Too many packets parsed from channel")
-	default:
-	}
-}
diff --git a/internal/srtp/srtp_test.go b/internal/srtp/srtp_test.go
index 4c8f8c1d..04d656bd 100644
--- a/internal/srtp/srtp_test.go
+++ b/internal/srtp/srtp_test.go
@@ -9,7 +9,7 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-const cipherContextAlgo = "SRTP_AES128_CM_SHA1_80"
+const cipherContextAlgo = ProtectionProfileAes128CmHmacSha1_80
 const defaultSsrc = 0
 
 type rtpTestCase struct {
@@ -166,23 +166,44 @@ func TestRTPLifecyle(t *testing.T) {
 	}
 
 	for _, testCase := range testCases {
-		pkt := &rtp.Packet{Payload: append([]byte{}, decrypted...), SequenceNumber: testCase.sequenceNumber}
-		if !encryptContext.EncryptRTP(pkt) {
-			t.Errorf("Failed to encrypt RTP packet with SeqNum: %d", testCase.sequenceNumber)
+		decryptedPkt := &rtp.Packet{Payload: append([]byte{}, decrypted...), SequenceNumber: testCase.sequenceNumber}
+		decryptedRaw, err := decryptedPkt.Marshal()
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		encryptedPkt := &rtp.Packet{Payload: append([]byte{}, testCase.encrypted...), SequenceNumber: testCase.sequenceNumber}
+		encryptedRaw, err := encryptedPkt.Marshal()
+		if err != nil {
+			t.Fatal(err)
 		}
-		assert.Equalf(pkt.Payload, testCase.encrypted, "RTP packet with SeqNum invalid encryption: %d", testCase.sequenceNumber)
 
-		if !decryptContext.DecryptRTP(pkt) {
-			t.Errorf("Failed to decrypt RTP packet with SeqNum: %d", testCase.sequenceNumber)
+		actualEncrypted, err := decryptContext.EncryptRTP(decryptedRaw)
+		if err != nil {
+			t.Fatal(err)
 		}
-		assert.Equalf(pkt.Payload, decrypted, "RTP packet with SeqNum invalid decryption: %d", testCase.sequenceNumber)
+		assert.Equalf(actualEncrypted, encryptedRaw, "RTP packet with SeqNum invalid encryption: %d", testCase.sequenceNumber)
 
+		actualDecrypted, err := encryptContext.DecryptRTP(encryptedRaw)
+		if err != nil {
+			t.Fatal(err)
+		}
+		assert.Equalf(actualDecrypted, decryptedRaw, "RTP packet with SeqNum invalid decryption: %d", testCase.sequenceNumber)
 	}
 
 	for _, testCase := range testCases {
 		pkt := &rtp.Packet{Payload: append([]byte{}, decrypted...), SequenceNumber: testCase.sequenceNumber}
-		encryptContext.EncryptRTP(pkt)
-		if invalidContext.DecryptRTP(pkt) {
+		pktRaw, err := pkt.Marshal()
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		out, err := encryptContext.EncryptRTP(pktRaw)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if _, err := invalidContext.DecryptRTP(out); err == nil {
 			t.Errorf("Managed to decrypt with incorrect salt for packet with SeqNum: %d", testCase.sequenceNumber)
 		}
 	}
diff --git a/pkg/logging/logging_test.go b/pkg/logging/logging_test.go
index 8a5541ef..7578377b 100644
--- a/pkg/logging/logging_test.go
+++ b/pkg/logging/logging_test.go
@@ -12,7 +12,7 @@ func TestScopedLogger(t *testing.T) {
 	var outBuf bytes.Buffer
 	logger := logging.NewScopedLogger("test1").
 		WithOutput(&outBuf).
-		WithLogLevel(logging.LogLevelWarning)
+		WithLogLevel(logging.LogLevelWarn)
 
 	logger.Debug("this shouldn't be logged")
 	if outBuf.Len() > 0 {
@@ -20,7 +20,7 @@ func TestScopedLogger(t *testing.T) {
 	}
 
 	warnMsg := "this is a warning message"
-	logger.Warning(warnMsg)
+	logger.Warn(warnMsg)
 	if !strings.Contains(outBuf.String(), warnMsg) {
 		t.Errorf("Expected to find %q in %q, but didn't", warnMsg, outBuf.String())
 	}
diff --git a/rtcpeerconnection_close_test.go b/rtcpeerconnection_close_test.go
index fbf4b0a2..f85024dd 100644
--- a/rtcpeerconnection_close_test.go
+++ b/rtcpeerconnection_close_test.go
@@ -50,41 +50,3 @@ func TestRTCPeerConnection_Close(t *testing.T) {
 		t.Fatal(err)
 	}
 }
-
-func newPair() (pcOffer *RTCPeerConnection, pcAnswer *RTCPeerConnection, err error) {
-	pca, err := New(RTCConfiguration{})
-	if err != nil {
-		return nil, nil, err
-	}
-
-	pcb, err := New(RTCConfiguration{})
-	if err != nil {
-		return nil, nil, err
-	}
-
-	return pca, pcb, nil
-}
-
-func signalPair(pcOffer *RTCPeerConnection, pcAnswer *RTCPeerConnection) error {
-	offer, err := pcOffer.CreateOffer(nil)
-	if err != nil {
-		return err
-	}
-
-	err = pcAnswer.SetRemoteDescription(offer)
-	if err != nil {
-		return err
-	}
-
-	answer, err := pcAnswer.CreateAnswer(nil)
-	if err != nil {
-		return err
-	}
-
-	err = pcOffer.SetRemoteDescription(answer)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
diff --git a/rtcpeerconnection_media_test.go b/rtcpeerconnection_media_test.go
new file mode 100644
index 00000000..4bd36c8b
--- /dev/null
+++ b/rtcpeerconnection_media_test.go
@@ -0,0 +1,139 @@
+package webrtc
+
+import (
+	"bytes"
+	"testing"
+	"time"
+
+	"github.com/pions/transport/test"
+	"github.com/pions/webrtc/pkg/media"
+	"github.com/pions/webrtc/pkg/rtcp"
+)
+
+func TestRTCPeerConnection_Media_Sample(t *testing.T) {
+	lim := test.TimeOut(time.Second * 30)
+	defer lim.Stop()
+
+	report := test.CheckRoutines(t)
+	defer report()
+
+	RegisterDefaultCodecs()
+	pcOffer, pcAnswer, err := newPair()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	awaitRTPRecv := make(chan bool)
+	awaitRTPSend := make(chan bool)
+
+	awaitRTCPSenderRecv := make(chan bool)
+	awaitRTCPSenderSend := make(chan error)
+
+	awaitRTCPRecieverRecv := make(chan bool)
+	awaitRTCPRecieverSend := make(chan error)
+
+	pcAnswer.OnTrack(func(track *RTCTrack) {
+		go func() {
+			for {
+				time.Sleep(time.Millisecond * 100)
+				if routineErr := pcAnswer.SendRTCP(&rtcp.PictureLossIndication{SenderSSRC: track.Ssrc, MediaSSRC: track.Ssrc}); routineErr != nil {
+					awaitRTCPRecieverSend <- routineErr
+					return
+				}
+
+				select {
+				case <-awaitRTCPRecieverRecv:
+					close(awaitRTCPRecieverSend)
+					return
+				default:
+				}
+			}
+		}()
+
+		go func() {
+			<-track.RTCPPackets
+			close(awaitRTCPRecieverRecv)
+		}()
+
+		for {
+			p := <-track.Packets
+			if bytes.Equal(p.Payload, []byte{0x10, 0x00}) {
+				close(awaitRTPRecv)
+				return
+			}
+		}
+	})
+
+	vp8Track, err := pcOffer.NewRTCSampleTrack(DefaultPayloadTypeVP8, "video", "pion")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if _, err = pcOffer.AddTrack(vp8Track); err != nil {
+		t.Fatal(err)
+	}
+
+	go func() {
+		for {
+			time.Sleep(time.Millisecond * 100)
+			vp8Track.Samples <- media.RTCSample{Data: []byte{0x00}, Samples: 1}
+
+			select {
+			case <-awaitRTPRecv:
+				close(awaitRTPSend)
+				return
+			default:
+			}
+		}
+	}()
+
+	go func() {
+		for {
+			time.Sleep(time.Millisecond * 100)
+			if routineErr := pcOffer.SendRTCP(&rtcp.PictureLossIndication{SenderSSRC: vp8Track.Ssrc, MediaSSRC: vp8Track.Ssrc}); routineErr != nil {
+				awaitRTCPSenderSend <- routineErr
+			}
+
+			select {
+			case <-awaitRTCPRecieverRecv:
+				close(awaitRTCPSenderSend)
+				return
+			default:
+			}
+		}
+	}()
+
+	go func() {
+		<-vp8Track.RTCPPackets
+		close(awaitRTCPSenderRecv)
+	}()
+
+	err = signalPair(pcOffer, pcAnswer)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	<-awaitRTPRecv
+	<-awaitRTPSend
+
+	<-awaitRTCPSenderRecv
+	err, ok := <-awaitRTCPSenderSend
+	if ok {
+		t.Fatal(err)
+	}
+
+	<-awaitRTCPRecieverRecv
+	err, ok = <-awaitRTCPRecieverSend
+	if ok {
+		t.Fatal(err)
+	}
+
+	err = pcOffer.Close()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = pcAnswer.Close()
+	if err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/rtcpeerconnection_test.go b/rtcpeerconnection_test.go
index 0486a36b..bb7fc200 100644
--- a/rtcpeerconnection_test.go
+++ b/rtcpeerconnection_test.go
@@ -18,6 +18,44 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+func newPair() (pcOffer *RTCPeerConnection, pcAnswer *RTCPeerConnection, err error) {
+	pca, err := New(RTCConfiguration{})
+	if err != nil {
+		return nil, nil, err
+	}
+
+	pcb, err := New(RTCConfiguration{})
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return pca, pcb, nil
+}
+
+func signalPair(pcOffer *RTCPeerConnection, pcAnswer *RTCPeerConnection) error {
+	offer, err := pcOffer.CreateOffer(nil)
+	if err != nil {
+		return err
+	}
+
+	err = pcAnswer.SetRemoteDescription(offer)
+	if err != nil {
+		return err
+	}
+
+	answer, err := pcAnswer.CreateAnswer(nil)
+	if err != nil {
+		return err
+	}
+
+	err = pcOffer.SetRemoteDescription(answer)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
 func TestNew(t *testing.T) {
 	t.Run("Success", func(t *testing.T) {
 		secretKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)