Repository: pion/interceptor
Pull request #42 (2021-06-24 17:17:42)

Packet Dumper Interceptors
This PR adds interceptors capable of dumping RTP & RTCP packets.

The interceptors directly write to a `io.Writer` which defaults to `os.Stdout` and can be configured by the user.

The RTP and RTCP packets can be dumped based on a filter provided by the user. By default, every packet is dumped.

Hopefully, this is what you had in mind @Sean-Der when writing the [2021 Goals](https://github.com/pion/webrtc/wiki/Goals-2021) 😁

codecov[bot]: # [Codecov](https://codecov.io/gh/pion/interceptor/pull/42?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) Report
> Merging [#42](https://codecov.io/gh/pion/interceptor/pull/42?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) (03eadb9) into [master](https://codecov.io/gh/pion/interceptor/commit/e23b9458bbd9c22b991417ddc564b0c7b7dec7d1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) (e23b945) will **decrease** coverage by `2.94%`.
> The diff coverage is `72.60%`.

[![Impacted file tree graph](https://codecov.io/gh/pion/interceptor/pull/42/graphs/tree.svg?width=650&height=150&src=pr&token=FNH9xUG6AX&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion)](https://codecov.io/gh/pion/interceptor/pull/42?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion)

```diff
@@            Coverage Diff             @@
##           master      #42      +/-   ##
==========================================
- Coverage   79.16%   76.22%   -2.95%     
==========================================
  Files          21       25       +4     
  Lines         600      673      +73     
==========================================
+ Hits          475      513      +38     
- Misses         85      110      +25     
- Partials       40       50      +10     
```

| Flag | Coverage Δ | |
|---|---|---|
| go | `?` | |
| wasm | `76.22% <72.60%> (-0.45%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/pion/interceptor/pull/42?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | Coverage Δ | |
|---|---|---|
| [pkg/packetdump/receiver\_interceptor.go](https://codecov.io/gh/pion/interceptor/pull/42/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGtnL3BhY2tldGR1bXAvcmVjZWl2ZXJfaW50ZXJjZXB0b3IuZ28=) | `58.62% <58.62%> (ø)` | |
| [pkg/packetdump/sender\_interceptor.go](https://codecov.io/gh/pion/interceptor/pull/42/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGtnL3BhY2tldGR1bXAvc2VuZGVyX2ludGVyY2VwdG9yLmdv) | `60.00% <60.00%> (ø)` | |
| [pkg/packetdump/receiver\_option.go](https://codecov.io/gh/pion/interceptor/pull/42/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGtnL3BhY2tldGR1bXAvcmVjZWl2ZXJfb3B0aW9uLmdv) | `100.00% <100.00%> (ø)` | |
| [pkg/packetdump/sender\_option.go](https://codecov.io/gh/pion/interceptor/pull/42/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGtnL3BhY2tldGR1bXAvc2VuZGVyX29wdGlvbi5nbw==) | `100.00% <100.00%> (ø)` | |
| [internal/test/mock\_time.go](https://codecov.io/gh/pion/interceptor/pull/42/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-aW50ZXJuYWwvdGVzdC9tb2NrX3RpbWUuZ28=) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [noop.go](https://codecov.io/gh/pion/interceptor/pull/42/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-bm9vcC5nbw==) | `0.00% <0.00%> (-71.43%)` | :arrow_down: |
| [interceptor.go](https://codecov.io/gh/pion/interceptor/pull/42/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-aW50ZXJjZXB0b3IuZ28=) | `0.00% <0.00%> (-66.67%)` | :arrow_down: |
| ... and [1 more](https://codecov.io/gh/pion/interceptor/pull/42/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pion/interceptor/pull/42?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pion/interceptor/pull/42?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion). Last update [e23b945...03eadb9](https://codecov.io/gh/pion/interceptor/pull/42?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).

Antonito: Concerning the thread-safety, I can either start a "writer's loop" in a goroutine and have a channel forward packet dumps to it, or simply add a mutex over the `Fprintf` calls.

I'm down for either way, the goroutine loop seems more efficient, but as these interceptors are destined for debug use only, maybe we'd be better off with the mutex and simpler code?

On a totally unrelated note, I'm quite concerned about pretty much every interceptor having to unmarshal packets in `BindRemoteStream` and `BindRTCPReader`. It seems every interceptor does this, so maybe there's a way to only do it once?
mengelbart: Hi, I am using something similar to create RTP/RTCP packet dumps and it would be nice if I could replace it by this interceptor. I have a few thoughts about the implementation:

* Regarding repeated parsing in `BindRemoteStream` and `BindRTCPReader`: To me it seems, they mostly parse the whole packet, but then only look at the header. Repeated parsing of the header was also mentioned in [this discussion](https://github.com/pion/interceptor/issues/14) and the proposed solution was to parse it once and then store the header in the attributes. I don't think this has actually been implemented, but I guess we could just add another interceptor?
* I would like to use packet dumps to calculate metrics, so I would prefer a writer's loop in a goroutine instead of a mutex.
* For metrics it would also be nice to have the actual receive time dumped.
* Is it necessary to have two separate interceptors, or could it be combined in one interceptor, which dumps all incoming and outgoing RTP/RTCP packets?
* It would be nice if I could specify a custom format for packets, e.g. something like an option to set a `func(pkt *rtp.Packet) string` (maybe it should take another type, which only contains the packet and some other additional information, like receive time?).
* I would like to pass separate streams for RTP and RTCP, because it would make later analysis easier if I can just parse a CSV file for each stream.
Antonito: Thanks for your feedback @mengelbart !

- Good point, I had missed this discussion. The proposed solution is something I'd be happy with – I don't think this has been implemented yet though (@Sean-Der can you confirm this?)
- Regarding the writer's loop and receive time, I agree – I only suggested the mutex workaround to quickly have a working interceptor that would fit most situations. If people already need/want to do precise metrics computation, yes, we need a dedicated go routine. The receive time can be one of those metrics.
  - However maybe a better design would be to add an additional Interceptor – which only captures additional data (such as the receive time) and have the PacketDumper interceptors able to print these information if present? That way users can easily combine interceptors to match their exact need – without performing unneeded work.
- It seems to me **exposing** two separate interceptors to the users is the way to go (for sake of ease & clarity of use), but we could implement them via a single 'internal' interceptor, to avoid code duplication.
  - However, as of today there's little code duplication and we might be better off with keeping things simple and clear?

Unfortunately I don't have time to work on this – and probably won't for a while. 
If you want to contribute to this PR, I'd be happy to review & discuss changes :)

Patch:
diff --git a/AUTHORS.txt b/AUTHORS.txt
index be40c1b..360b8fa 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -6,6 +6,7 @@
 Adam Kiss <masterada@gmail.com>
 adamroach <adam@nostrum.com>
 aler9 <46489434+aler9@users.noreply.github.com>
+Antoine Baché <antoine@tenten.app>
 Atsushi Watanabe <atsushi.w@ieee.org>
 Jonathan Müller <jonathan@fotokite.com>
 Mathis Engelbart <mathis.engelbart@gmail.com>
diff --git a/README.md b/README.md
index 6385ffc..38384b0 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,7 @@ Check out the **[contributing wiki](https://github.com/pion/webrtc/wiki/Contribu
 * [Atsushi Watanabe](https://github.com/at-wat)
 * [Alessandro Ros](https://github.com/aler9)
 * [Mathis Engelbart](https://github.com/mengelbart)
+* [Antoine Baché](https://github.com/Antonito)
 
 ### License
 MIT License - see [LICENSE](LICENSE) for full text
diff --git a/pkg/packetdump/filter.go b/pkg/packetdump/filter.go
new file mode 100644
index 0000000..14c8f65
--- /dev/null
+++ b/pkg/packetdump/filter.go
@@ -0,0 +1,14 @@
+package packetdump
+
+import (
+	"github.com/pion/rtcp"
+	"github.com/pion/rtp"
+)
+
+// RTPFilterCallback can be used to filter RTP packets to dump.
+// The callback returns whether or not to print dump the packet's content.
+type RTPFilterCallback func(pkt *rtp.Packet) bool
+
+// RTCPFilterCallback can be used to filter RTCP packets to dump.
+// The callback returns whether or not to print dump the packet's content.
+type RTCPFilterCallback func(pkt []rtcp.Packet) bool
diff --git a/pkg/packetdump/format.go b/pkg/packetdump/format.go
new file mode 100644
index 0000000..1ace6fa
--- /dev/null
+++ b/pkg/packetdump/format.go
@@ -0,0 +1,29 @@
+package packetdump
+
+import (
+	"fmt"
+
+	"github.com/pion/interceptor"
+	"github.com/pion/rtcp"
+	"github.com/pion/rtp"
+)
+
+// RTPFormatCallback can be used to apply custom formatting to each dumped RTP
+// packet. If new lines should be added after each packet, they must be included
+// in the returned format.
+type RTPFormatCallback func(*rtp.Packet, interceptor.Attributes) string
+
+// RTCPFormatCallback can be used to apply custom formatting to each dumped RTCP
+// packet. If new lines should be added after each packet, they must be included
+// in the returned format.
+type RTCPFormatCallback func([]rtcp.Packet, interceptor.Attributes) string
+
+// DefaultRTPFormatter returns the default log format for RTP packets
+func DefaultRTPFormatter(pkt *rtp.Packet, _ interceptor.Attributes) string {
+	return fmt.Sprintf("%s\n", pkt)
+}
+
+// DefaultRTCPFormatter returns the default log format for RTCP packets
+func DefaultRTCPFormatter(pkts []rtcp.Packet, _ interceptor.Attributes) string {
+	return fmt.Sprintf("%s\n", pkts)
+}
diff --git a/pkg/packetdump/option.go b/pkg/packetdump/option.go
new file mode 100644
index 0000000..52b2cc4
--- /dev/null
+++ b/pkg/packetdump/option.go
@@ -0,0 +1,66 @@
+package packetdump
+
+import (
+	"io"
+
+	"github.com/pion/logging"
+)
+
+// PacketDumperOption can be used to configure SenderInterceptor
+type PacketDumperOption func(d *PacketDumper) error
+
+// Log sets a logger for the interceptor
+func Log(log logging.LeveledLogger) PacketDumperOption {
+	return func(d *PacketDumper) error {
+		d.log = log
+		return nil
+	}
+}
+
+// RTPWriter sets the io.Writer on which RTP packets will be dumped.
+func RTPWriter(w io.Writer) PacketDumperOption {
+	return func(d *PacketDumper) error {
+		d.rtpStream = w
+		return nil
+	}
+}
+
+// RTCPWriter sets the io.Writer on which RTCP packets will be dumped.
+func RTCPWriter(w io.Writer) PacketDumperOption {
+	return func(d *PacketDumper) error {
+		d.rtcpStream = w
+		return nil
+	}
+}
+
+// RTPFormatter sets the RTP format
+func RTPFormatter(f RTPFormatCallback) PacketDumperOption {
+	return func(d *PacketDumper) error {
+		d.rtpFormat = f
+		return nil
+	}
+}
+
+// RTCPFormatter sets the RTCP format
+func RTCPFormatter(f RTCPFormatCallback) PacketDumperOption {
+	return func(d *PacketDumper) error {
+		d.rtcpFormat = f
+		return nil
+	}
+}
+
+// RTPFilter sets the RTP filter.
+func RTPFilter(callback RTPFilterCallback) PacketDumperOption {
+	return func(d *PacketDumper) error {
+		d.rtpFilter = callback
+		return nil
+	}
+}
+
+// RTCPFilter sets the RTCP filter.
+func RTCPFilter(callback RTCPFilterCallback) PacketDumperOption {
+	return func(d *PacketDumper) error {
+		d.rtcpFilter = callback
+		return nil
+	}
+}
diff --git a/pkg/packetdump/packet_dump.go b/pkg/packetdump/packet_dump.go
new file mode 100644
index 0000000..a74822f
--- /dev/null
+++ b/pkg/packetdump/packet_dump.go
@@ -0,0 +1,18 @@
+// Package packetdump implements RTP & RTCP packet dumpers.
+package packetdump
+
+import (
+	"github.com/pion/interceptor"
+	"github.com/pion/rtcp"
+	"github.com/pion/rtp"
+)
+
+type rtpDump struct {
+	attributes interceptor.Attributes
+	packet     *rtp.Packet
+}
+
+type rtcpDump struct {
+	attributes interceptor.Attributes
+	packets    []rtcp.Packet
+}
diff --git a/pkg/packetdump/packet_dumper.go b/pkg/packetdump/packet_dumper.go
new file mode 100644
index 0000000..df459f8
--- /dev/null
+++ b/pkg/packetdump/packet_dumper.go
@@ -0,0 +1,124 @@
+package packetdump
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"sync"
+
+	"github.com/pion/interceptor"
+	"github.com/pion/logging"
+	"github.com/pion/rtcp"
+	"github.com/pion/rtp"
+)
+
+// PacketDumper dumps packet to a io.Writer
+type PacketDumper struct {
+	log logging.LeveledLogger
+
+	wg    sync.WaitGroup
+	close chan struct{}
+
+	rtpChan  chan *rtpDump
+	rtcpChan chan *rtcpDump
+
+	rtpStream  io.Writer
+	rtcpStream io.Writer
+
+	rtpFormat  RTPFormatCallback
+	rtcpFormat RTCPFormatCallback
+
+	rtpFilter  RTPFilterCallback
+	rtcpFilter RTCPFilterCallback
+}
+
+// NewPacketDumper creates a new PacketDumper
+func NewPacketDumper(opts ...PacketDumperOption) (*PacketDumper, error) {
+	d := &PacketDumper{
+		log:        logging.NewDefaultLoggerFactory().NewLogger("packet_dumper"),
+		wg:         sync.WaitGroup{},
+		close:      make(chan struct{}),
+		rtpChan:    make(chan *rtpDump),
+		rtcpChan:   make(chan *rtcpDump),
+		rtpStream:  os.Stdout,
+		rtcpStream: os.Stdout,
+		rtpFormat:  DefaultRTPFormatter,
+		rtcpFormat: DefaultRTCPFormatter,
+		rtpFilter: func(pkt *rtp.Packet) bool {
+			return true
+		},
+		rtcpFilter: func(pkt []rtcp.Packet) bool {
+			return true
+		},
+	}
+
+	for _, opt := range opts {
+		if err := opt(d); err != nil {
+			return nil, err
+		}
+	}
+
+	d.wg.Add(1)
+	go d.loop()
+
+	return d, nil
+}
+
+func (d *PacketDumper) logRTPPacket(header *rtp.Header, payload []byte, attributes interceptor.Attributes) {
+	d.rtpChan <- &rtpDump{
+		attributes: attributes,
+		packet: &rtp.Packet{
+			Header:  *header,
+			Payload: payload,
+		},
+	}
+}
+
+func (d *PacketDumper) logRTCPPackets(pkts []rtcp.Packet, attributes interceptor.Attributes) {
+	d.rtcpChan <- &rtcpDump{
+		attributes: attributes,
+		packets:    pkts,
+	}
+}
+
+// Close closes the PacketDumper
+func (d *PacketDumper) Close() error {
+	defer d.wg.Wait()
+
+	if !d.isClosed() {
+		close(d.close)
+	}
+	return nil
+}
+
+func (d *PacketDumper) isClosed() bool {
+	select {
+	case <-d.close:
+		return true
+	default:
+		return false
+	}
+}
+
+func (d *PacketDumper) loop() {
+	defer d.wg.Done()
+
+	for {
+		select {
+		case <-d.close:
+			return
+		case dump := <-d.rtpChan:
+			if d.rtpFilter(dump.packet) {
+				if _, err := fmt.Fprint(d.rtpStream, d.rtpFormat(dump.packet, dump.attributes)); err != nil {
+					d.log.Errorf("could not dump RTP packet %v", err)
+				}
+			}
+		case dump := <-d.rtcpChan:
+			if d.rtcpFilter(dump.packets) {
+				if _, err := fmt.Fprint(d.rtcpStream, d.rtcpFormat(dump.packets, dump.attributes)); err != nil {
+					d.log.Errorf("could not dump RTCP packet %v", err)
+				}
+			}
+		}
+	}
+}
diff --git a/pkg/packetdump/receiver_interceptor.go b/pkg/packetdump/receiver_interceptor.go
new file mode 100644
index 0000000..ef17927
--- /dev/null
+++ b/pkg/packetdump/receiver_interceptor.go
@@ -0,0 +1,86 @@
+package packetdump
+
+import (
+	"github.com/pion/interceptor"
+)
+
+// ReceiverInterceptorFactory is a interceptor.Factory for a ReceiverInterceptor
+type ReceiverInterceptorFactory struct {
+	opts []PacketDumperOption
+}
+
+// NewReceiverInterceptor returns a new ReceiverInterceptor
+func NewReceiverInterceptor(opts ...PacketDumperOption) (*ReceiverInterceptorFactory, error) {
+	return &ReceiverInterceptorFactory{
+		opts: opts,
+	}, nil
+}
+
+// NewInterceptor returns a new ReceiverInterceptor interceptor.
+func (r *ReceiverInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) {
+	dumper, err := NewPacketDumper(r.opts...)
+	if err != nil {
+		return nil, err
+	}
+	i := &ReceiverInterceptor{
+		NoOp:         interceptor.NoOp{},
+		PacketDumper: dumper,
+	}
+
+	return i, nil
+}
+
+// ReceiverInterceptor interceptor dumps outgoing RTP packets.
+type ReceiverInterceptor struct {
+	interceptor.NoOp
+	*PacketDumper
+}
+
+// BindRemoteStream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method
+// will be called once per rtp packet.
+func (r *ReceiverInterceptor) BindRemoteStream(info *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader {
+	return interceptor.RTPReaderFunc(func(bytes []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) {
+		i, attr, err := reader.Read(bytes, attributes)
+		if err != nil {
+			return 0, nil, err
+		}
+		if attr == nil {
+			attr = make(interceptor.Attributes)
+		}
+
+		header, err := attr.GetRTPHeader(bytes)
+		if err != nil {
+			return 0, nil, err
+		}
+
+		r.logRTPPacket(header, bytes[header.MarshalSize():i], attr)
+		return i, attr, nil
+	})
+}
+
+// BindRTCPReader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might
+// change in the future. The returned method will be called once per packet batch.
+func (r *ReceiverInterceptor) BindRTCPReader(reader interceptor.RTCPReader) interceptor.RTCPReader {
+	return interceptor.RTCPReaderFunc(func(bytes []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) {
+		i, attr, err := reader.Read(bytes, attributes)
+		if err != nil {
+			return 0, nil, err
+		}
+		if attr == nil {
+			attr = make(interceptor.Attributes)
+		}
+
+		pkts, err := attr.GetRTCPPackets(bytes[:i])
+		if err != nil {
+			return 0, nil, err
+		}
+
+		r.logRTCPPackets(pkts, attr)
+		return i, attr, err
+	})
+}
+
+// Close closes the interceptor
+func (r *ReceiverInterceptor) Close() error {
+	return r.PacketDumper.Close()
+}
diff --git a/pkg/packetdump/sender_interceptor.go b/pkg/packetdump/sender_interceptor.go
new file mode 100644
index 0000000..ae65466
--- /dev/null
+++ b/pkg/packetdump/sender_interceptor.go
@@ -0,0 +1,60 @@
+package packetdump
+
+import (
+	"github.com/pion/interceptor"
+	"github.com/pion/rtcp"
+	"github.com/pion/rtp"
+)
+
+// SenderInterceptorFactory is a interceptor.Factory for a SenderInterceptor
+type SenderInterceptorFactory struct {
+	opts []PacketDumperOption
+}
+
+// NewSenderInterceptor returns a new SenderInterceptorFactory
+func NewSenderInterceptor(opts ...PacketDumperOption) (*SenderInterceptorFactory, error) {
+	return &SenderInterceptorFactory{
+		opts: opts,
+	}, nil
+}
+
+// NewInterceptor returns a new SenderInterceptor interceptor
+func (s *SenderInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) {
+	dumper, err := NewPacketDumper(s.opts...)
+	if err != nil {
+		return nil, err
+	}
+	i := &SenderInterceptor{
+		PacketDumper: dumper,
+	}
+	return i, nil
+}
+
+// SenderInterceptor responds to nack feedback messages
+type SenderInterceptor struct {
+	interceptor.NoOp
+	*PacketDumper
+}
+
+// BindRTCPWriter lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method
+// will be called once per packet batch.
+func (s *SenderInterceptor) BindRTCPWriter(writer interceptor.RTCPWriter) interceptor.RTCPWriter {
+	return interceptor.RTCPWriterFunc(func(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) {
+		s.logRTCPPackets(pkts, attributes)
+		return writer.Write(pkts, attributes)
+	})
+}
+
+// BindLocalStream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method
+// will be called once per rtp packet.
+func (s *SenderInterceptor) BindLocalStream(info *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter {
+	return interceptor.RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) {
+		s.logRTPPacket(header, payload, attributes)
+		return writer.Write(header, payload, attributes)
+	})
+}
+
+// Close closes the interceptor
+func (s *SenderInterceptor) Close() error {
+	return s.PacketDumper.Close()
+}


Test patch:
diff --git a/pkg/packetdump/receiver_interceptor_test.go b/pkg/packetdump/receiver_interceptor_test.go
new file mode 100644
index 0000000..8622d16
--- /dev/null
+++ b/pkg/packetdump/receiver_interceptor_test.go
@@ -0,0 +1,107 @@
+package packetdump
+
+import (
+	"bytes"
+	"testing"
+	"time"
+
+	"github.com/pion/interceptor"
+	"github.com/pion/interceptor/internal/test"
+	"github.com/pion/logging"
+	"github.com/pion/rtcp"
+	"github.com/pion/rtp"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestReceiverFilterEverythingOut(t *testing.T) {
+	buf := bytes.Buffer{}
+
+	factory, err := NewReceiverInterceptor(
+		RTPWriter(&buf),
+		RTCPWriter(&buf),
+		Log(logging.NewDefaultLoggerFactory().NewLogger("test")),
+		RTPFilter(func(pkt *rtp.Packet) bool {
+			return false
+		}),
+		RTCPFilter(func(pkt []rtcp.Packet) bool {
+			return false
+		}),
+	)
+	assert.NoError(t, err)
+
+	i, err := factory.NewInterceptor("")
+	assert.NoError(t, err)
+
+	assert.Zero(t, buf.Len())
+
+	stream := test.NewMockStream(&interceptor.StreamInfo{
+		SSRC:      123456,
+		ClockRate: 90000,
+	}, i)
+	defer func() {
+		assert.NoError(t, stream.Close())
+	}()
+
+	stream.ReceiveRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{
+		SenderSSRC: 123,
+		MediaSSRC:  456,
+	}})
+	stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{
+		SequenceNumber: uint16(0),
+	}})
+
+	// Give time for packets to be handled and stream written to.
+	time.Sleep(50 * time.Millisecond)
+
+	err = i.Close()
+	assert.NoError(t, err)
+
+	// Every packet should have been filtered out – nothing should be written.
+	assert.Zero(t, buf.Len())
+}
+
+func TestReceiverFilterNothing(t *testing.T) {
+	buf := bytes.Buffer{}
+
+	factory, err := NewReceiverInterceptor(
+		RTPWriter(&buf),
+		RTCPWriter(&buf),
+		Log(logging.NewDefaultLoggerFactory().NewLogger("test")),
+		RTPFilter(func(pkt *rtp.Packet) bool {
+			return true
+		}),
+		RTCPFilter(func(pkt []rtcp.Packet) bool {
+			return true
+		}),
+	)
+	assert.NoError(t, err)
+
+	i, err := factory.NewInterceptor("")
+	assert.NoError(t, err)
+
+	assert.EqualValues(t, 0, buf.Len())
+
+	stream := test.NewMockStream(&interceptor.StreamInfo{
+		SSRC:      123456,
+		ClockRate: 90000,
+	}, i)
+	defer func() {
+		assert.NoError(t, stream.Close())
+	}()
+
+	stream.ReceiveRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{
+		SenderSSRC: 123,
+		MediaSSRC:  456,
+	}})
+	stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{
+		SequenceNumber: uint16(0),
+	}})
+
+	// Give time for packets to be handled and stream written to.
+	time.Sleep(50 * time.Millisecond)
+
+	err = i.Close()
+	assert.NoError(t, err)
+
+	assert.NotZero(t, buf.Len())
+}
diff --git a/pkg/packetdump/sender_interceptor_test.go b/pkg/packetdump/sender_interceptor_test.go
new file mode 100644
index 0000000..59a0a77
--- /dev/null
+++ b/pkg/packetdump/sender_interceptor_test.go
@@ -0,0 +1,113 @@
+package packetdump
+
+import (
+	"bytes"
+	"testing"
+	"time"
+
+	"github.com/pion/interceptor"
+	"github.com/pion/interceptor/internal/test"
+	"github.com/pion/logging"
+	"github.com/pion/rtcp"
+	"github.com/pion/rtp"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSenderFilterEverythingOut(t *testing.T) {
+	buf := bytes.Buffer{}
+
+	factory, err := NewSenderInterceptor(
+		RTPWriter(&buf),
+		RTCPWriter(&buf),
+		Log(logging.NewDefaultLoggerFactory().NewLogger("test")),
+		RTPFilter(func(pkt *rtp.Packet) bool {
+			return false
+		}),
+		RTCPFilter(func(pkt []rtcp.Packet) bool {
+			return false
+		}),
+	)
+	assert.NoError(t, err)
+
+	i, err := factory.NewInterceptor("")
+	assert.NoError(t, err)
+
+	assert.Zero(t, buf.Len())
+
+	stream := test.NewMockStream(&interceptor.StreamInfo{
+		SSRC:      123456,
+		ClockRate: 90000,
+	}, i)
+	defer func() {
+		assert.NoError(t, stream.Close())
+	}()
+
+	err = stream.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{
+		SenderSSRC: 123,
+		MediaSSRC:  456,
+	}})
+	assert.NoError(t, err)
+
+	err = stream.WriteRTP(&rtp.Packet{Header: rtp.Header{
+		SequenceNumber: uint16(0),
+	}})
+	assert.NoError(t, err)
+
+	// Give time for packets to be handled and stream written to.
+	time.Sleep(50 * time.Millisecond)
+
+	err = i.Close()
+	assert.NoError(t, err)
+
+	// Every packet should have been filtered out – nothing should be written.
+	assert.Zero(t, buf.Len())
+}
+
+func TestSenderFilterNothing(t *testing.T) {
+	buf := bytes.Buffer{}
+
+	factory, err := NewSenderInterceptor(
+		RTPWriter(&buf),
+		RTCPWriter(&buf),
+		Log(logging.NewDefaultLoggerFactory().NewLogger("test")),
+		RTPFilter(func(pkt *rtp.Packet) bool {
+			return true
+		}),
+		RTCPFilter(func(pkt []rtcp.Packet) bool {
+			return true
+		}),
+	)
+	assert.NoError(t, err)
+
+	i, err := factory.NewInterceptor("")
+	assert.NoError(t, err)
+
+	assert.EqualValues(t, 0, buf.Len())
+
+	stream := test.NewMockStream(&interceptor.StreamInfo{
+		SSRC:      123456,
+		ClockRate: 90000,
+	}, i)
+	defer func() {
+		assert.NoError(t, stream.Close())
+	}()
+
+	err = stream.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{
+		SenderSSRC: 123,
+		MediaSSRC:  456,
+	}})
+	assert.NoError(t, err)
+
+	err = stream.WriteRTP(&rtp.Packet{Header: rtp.Header{
+		SequenceNumber: uint16(0),
+	}})
+	assert.NoError(t, err)
+
+	// Give time for packets to be handled and stream written to.
+	time.Sleep(50 * time.Millisecond)
+
+	err = i.Close()
+	assert.NoError(t, err)
+
+	assert.NotZero(t, buf.Len())
+}