Repository: pion/interceptor
Pull request #67 (2021-09-16 18:07:06)

Add InterceptorFactory
Interceptors are being accidentally misused by users.
The issue is that an Interceptor can be re-used between
multiple PeerConnections. Interceptors were designed to only be
single PeerConnection aware, so state is being corrupted.

Instead we are now going to provide InterceptorFactories. The default
API of pion/webrtc will now be safe to use between PeerConnections.

Relates to pion/webrtc#1956

codecov[bot]: # [Codecov](https://codecov.io/gh/pion/interceptor/pull/67?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) Report
> Merging [#67](https://codecov.io/gh/pion/interceptor/pull/67?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) (4ceb5b9) into [master](https://codecov.io/gh/pion/interceptor/commit/5043c14b3efb6d83ef0bc215ce19f7e10ebb8220?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) (5043c14) will **decrease** coverage by `0.94%`.
> The diff coverage is `84.61%`.

[![Impacted file tree graph](https://codecov.io/gh/pion/interceptor/pull/67/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/67?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion)

```diff
@@            Coverage Diff             @@
##           master      #67      +/-   ##
==========================================
- Coverage   81.21%   80.27%   -0.95%     
==========================================
  Files          24       24              
  Lines         809      811       +2     
==========================================
- Hits          657      651       -6     
- Misses        101      108       +7     
- Partials       51       52       +1     
```

| Flag | Coverage Δ | |
|---|---|---|
| go | `80.27% <84.61%> (-0.95%)` | :arrow_down: |
| wasm | `?` | |

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/67?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | Coverage Δ | |
|---|---|---|
| [interceptor.go](https://codecov.io/gh/pion/interceptor/pull/67/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-aW50ZXJjZXB0b3IuZ28=) | `66.66% <ø> (ø)` | |
| [internal/test/mock\_stream.go](https://codecov.io/gh/pion/interceptor/pull/67/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-aW50ZXJuYWwvdGVzdC9tb2NrX3N0cmVhbS5nbw==) | `74.68% <ø> (ø)` | |
| [pkg/twcc/sender\_interceptor.go](https://codecov.io/gh/pion/interceptor/pull/67/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGtnL3R3Y2Mvc2VuZGVyX2ludGVyY2VwdG9yLmdv) | `80.00% <81.81%> (+0.28%)` | :arrow_up: |
| [pkg/twcc/header\_extension\_interceptor.go](https://codecov.io/gh/pion/interceptor/pull/67/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGtnL3R3Y2MvaGVhZGVyX2V4dGVuc2lvbl9pbnRlcmNlcHRvci5nbw==) | `64.70% <100.00%> (ø)` | |
| [internal/test/mock\_time.go](https://codecov.io/gh/pion/interceptor/pull/67/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: |
| [pkg/nack/send\_buffer.go](https://codecov.io/gh/pion/interceptor/pull/67/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGtnL25hY2svc2VuZF9idWZmZXIuZ28=) | `85.71% <0.00%> (-5.72%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pion/interceptor/pull/67?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/67?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion). Last update [5043c14...4ceb5b9](https://codecov.io/gh/pion/interceptor/pull/67?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).

Sean-Der: Do people prefer that `Option` gets executed once per factory or interceptor?

I opted for once per Interceptor
* You can customize each one
* You could track interceptor creation

It could be wasteful resource wise, but I don't believe enough to matter.

Patch:
diff --git a/examples/nack/main.go b/examples/nack/main.go
index 45e892a..430d3d4 100644
--- a/examples/nack/main.go
+++ b/examples/nack/main.go
@@ -35,7 +35,12 @@ func receiveRoutine() {
 	}
 
 	// Create NACK Generator
-	generator, err := nack.NewGeneratorInterceptor()
+	generatorFactory, err := nack.NewGeneratorInterceptor()
+	if err != nil {
+		panic(err)
+	}
+
+	generator, err := generatorFactory.NewInterceptor("")
 	if err != nil {
 		panic(err)
 	}
@@ -92,7 +97,12 @@ func sendRoutine() {
 	}
 
 	// Create NACK Responder
-	responder, err := nack.NewResponderInterceptor()
+	responderFactory, err := nack.NewResponderInterceptor()
+	if err != nil {
+		panic(err)
+	}
+
+	responder, err := responderFactory.NewInterceptor("")
 	if err != nil {
 		panic(err)
 	}
diff --git a/go.mod b/go.mod
index 6aaa8c1..e3e0cd9 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.15
 
 require (
 	github.com/pion/logging v0.2.2
-	github.com/pion/rtcp v1.2.7
+	github.com/pion/rtcp v1.2.8
 	github.com/pion/rtp v1.7.2
 	github.com/stretchr/testify v1.7.0
 )
diff --git a/go.sum b/go.sum
index c86a3e4..10da544 100644
--- a/go.sum
+++ b/go.sum
@@ -4,8 +4,8 @@ github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
 github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
 github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
 github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
-github.com/pion/rtcp v1.2.7 h1:cPeOJu9sHMTLTWmxzLH8/wcF8giondpLgvXDPfauUBY=
-github.com/pion/rtcp v1.2.7/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
+github.com/pion/rtcp v1.2.8 h1:Cys8X6r0xxU65ESTmXkqr8eU1Q1Wx+lNkoZCUH4zD7E=
+github.com/pion/rtcp v1.2.8/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
 github.com/pion/rtp v1.7.2 h1:HCDKDCixh7PVjkQTsqHAbk1lg+bx059EHxcnyl42dYs=
 github.com/pion/rtp v1.7.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
diff --git a/interceptor.go b/interceptor.go
index 2d1b97e..88313e0 100644
--- a/interceptor.go
+++ b/interceptor.go
@@ -9,6 +9,11 @@ import (
 	"github.com/pion/rtp"
 )
 
+// Factory provides an interface for constructing interceptors
+type Factory interface {
+	NewInterceptor(id string) (Interceptor, error)
+}
+
 // Interceptor can be used to add functionality to you PeerConnections by modifying any incoming/outgoing rtp/rtcp
 // packets, or sending your own packets as needed.
 type Interceptor interface {
diff --git a/pkg/mock/factory.go b/pkg/mock/factory.go
new file mode 100644
index 0000000..6ff72a1
--- /dev/null
+++ b/pkg/mock/factory.go
@@ -0,0 +1,13 @@
+package mock
+
+import "github.com/pion/interceptor"
+
+// Factory is a mock Factory for testing.
+type Factory struct {
+	NewInterceptorFn func(id string) (interceptor.Interceptor, error)
+}
+
+// NewInterceptor implements Interceptor
+func (f *Factory) NewInterceptor(id string) (interceptor.Interceptor, error) {
+	return f.NewInterceptorFn(id)
+}
diff --git a/pkg/nack/generator_interceptor.go b/pkg/nack/generator_interceptor.go
index 7b805df..6110309 100644
--- a/pkg/nack/generator_interceptor.go
+++ b/pkg/nack/generator_interceptor.go
@@ -11,24 +11,14 @@ import (
 	"github.com/pion/rtp"
 )
 
-// GeneratorInterceptor interceptor generates nack feedback messages.
-type GeneratorInterceptor struct {
-	interceptor.NoOp
-	size      uint16
-	skipLastN uint16
-	interval  time.Duration
-	m         sync.Mutex
-	wg        sync.WaitGroup
-	close     chan struct{}
-	log       logging.LeveledLogger
-
-	receiveLogs   map[uint32]*receiveLog
-	receiveLogsMu sync.Mutex
+// GeneratorInterceptorFactory is a interceptor.Factory for a GeneratorInterceptor
+type GeneratorInterceptorFactory struct {
+	opts []GeneratorOption
 }
 
-// NewGeneratorInterceptor returns a new GeneratorInterceptor interceptor
-func NewGeneratorInterceptor(opts ...GeneratorOption) (*GeneratorInterceptor, error) {
-	r := &GeneratorInterceptor{
+// NewInterceptor constructs a new ReceiverInterceptor
+func (g *GeneratorInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) {
+	i := &GeneratorInterceptor{
 		size:        8192,
 		skipLastN:   0,
 		interval:    time.Millisecond * 100,
@@ -37,17 +27,37 @@ func NewGeneratorInterceptor(opts ...GeneratorOption) (*GeneratorInterceptor, er
 		log:         logging.NewDefaultLoggerFactory().NewLogger("nack_generator"),
 	}
 
-	for _, opt := range opts {
-		if err := opt(r); err != nil {
+	for _, opt := range g.opts {
+		if err := opt(i); err != nil {
 			return nil, err
 		}
 	}
 
-	if _, err := newReceiveLog(r.size); err != nil {
+	if _, err := newReceiveLog(i.size); err != nil {
 		return nil, err
 	}
 
-	return r, nil
+	return i, nil
+}
+
+// GeneratorInterceptor interceptor generates nack feedback messages.
+type GeneratorInterceptor struct {
+	interceptor.NoOp
+	size      uint16
+	skipLastN uint16
+	interval  time.Duration
+	m         sync.Mutex
+	wg        sync.WaitGroup
+	close     chan struct{}
+	log       logging.LeveledLogger
+
+	receiveLogs   map[uint32]*receiveLog
+	receiveLogsMu sync.Mutex
+}
+
+// NewGeneratorInterceptor returns a new GeneratorInterceptorFactory
+func NewGeneratorInterceptor(opts ...GeneratorOption) (*GeneratorInterceptorFactory, error) {
+	return &GeneratorInterceptorFactory{opts}, nil
 }
 
 // BindRTCPWriter lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method
diff --git a/pkg/nack/responder_interceptor.go b/pkg/nack/responder_interceptor.go
index e9707f3..0a2b850 100644
--- a/pkg/nack/responder_interceptor.go
+++ b/pkg/nack/responder_interceptor.go
@@ -9,6 +9,32 @@ import (
 	"github.com/pion/rtp"
 )
 
+// ResponderInterceptorFactory is a interceptor.Factory for a ResponderInterceptor
+type ResponderInterceptorFactory struct {
+	opts []ResponderOption
+}
+
+// NewInterceptor constructs a new ResponderInterceptor
+func (r *ResponderInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) {
+	i := &ResponderInterceptor{
+		size:    8192,
+		log:     logging.NewDefaultLoggerFactory().NewLogger("nack_responder"),
+		streams: map[uint32]*localStream{},
+	}
+
+	for _, opt := range r.opts {
+		if err := opt(i); err != nil {
+			return nil, err
+		}
+	}
+
+	if _, err := newSendBuffer(i.size); err != nil {
+		return nil, err
+	}
+
+	return i, nil
+}
+
 // ResponderInterceptor responds to nack feedback messages
 type ResponderInterceptor struct {
 	interceptor.NoOp
@@ -24,25 +50,9 @@ type localStream struct {
 	rtpWriter  interceptor.RTPWriter
 }
 
-// NewResponderInterceptor returns a new ResponderInterceptor interceptor
-func NewResponderInterceptor(opts ...ResponderOption) (*ResponderInterceptor, error) {
-	r := &ResponderInterceptor{
-		size:    8192,
-		log:     logging.NewDefaultLoggerFactory().NewLogger("nack_responder"),
-		streams: map[uint32]*localStream{},
-	}
-
-	for _, opt := range opts {
-		if err := opt(r); err != nil {
-			return nil, err
-		}
-	}
-
-	if _, err := newSendBuffer(r.size); err != nil {
-		return nil, err
-	}
-
-	return r, nil
+// NewResponderInterceptor returns a new ResponderInterceptorFactor
+func NewResponderInterceptor(opts ...ResponderOption) (*ResponderInterceptorFactory, error) {
+	return &ResponderInterceptorFactory{opts}, nil
 }
 
 // BindRTCPReader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might
diff --git a/pkg/report/receiver_interceptor.go b/pkg/report/receiver_interceptor.go
index 40a4677..18f6206 100644
--- a/pkg/report/receiver_interceptor.go
+++ b/pkg/report/receiver_interceptor.go
@@ -10,34 +10,44 @@ import (
 	"github.com/pion/rtp"
 )
 
-// ReceiverInterceptor interceptor generates receiver reports.
-type ReceiverInterceptor struct {
-	interceptor.NoOp
-	interval time.Duration
-	now      func() time.Time
-	streams  sync.Map
-	log      logging.LeveledLogger
-	m        sync.Mutex
-	wg       sync.WaitGroup
-	close    chan struct{}
+// ReceiverInterceptorFactory is a interceptor.Factory for a ReceiverInterceptor
+type ReceiverInterceptorFactory struct {
+	opts []ReceiverOption
 }
 
-// NewReceiverInterceptor returns a new ReceiverInterceptor interceptor.
-func NewReceiverInterceptor(opts ...ReceiverOption) (*ReceiverInterceptor, error) {
-	r := &ReceiverInterceptor{
+// NewInterceptor constructs a new ReceiverInterceptor
+func (r *ReceiverInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) {
+	i := &ReceiverInterceptor{
 		interval: 1 * time.Second,
 		now:      time.Now,
 		log:      logging.NewDefaultLoggerFactory().NewLogger("receiver_interceptor"),
 		close:    make(chan struct{}),
 	}
 
-	for _, opt := range opts {
-		if err := opt(r); err != nil {
+	for _, opt := range r.opts {
+		if err := opt(i); err != nil {
 			return nil, err
 		}
 	}
 
-	return r, nil
+	return i, nil
+}
+
+// NewReceiverInterceptor returns a new ReceiverInterceptorFactory
+func NewReceiverInterceptor(opts ...ReceiverOption) (*ReceiverInterceptorFactory, error) {
+	return &ReceiverInterceptorFactory{opts}, nil
+}
+
+// ReceiverInterceptor interceptor generates receiver reports.
+type ReceiverInterceptor struct {
+	interceptor.NoOp
+	interval time.Duration
+	now      func() time.Time
+	streams  sync.Map
+	log      logging.LeveledLogger
+	m        sync.Mutex
+	wg       sync.WaitGroup
+	close    chan struct{}
 }
 
 func (r *ReceiverInterceptor) isClosed() bool {
diff --git a/pkg/report/sender_interceptor.go b/pkg/report/sender_interceptor.go
index 661640a..0c18e45 100644
--- a/pkg/report/sender_interceptor.go
+++ b/pkg/report/sender_interceptor.go
@@ -10,14 +10,32 @@ import (
 	"github.com/pion/rtp"
 )
 
-func ntpTime(t time.Time) uint64 {
-	// seconds since 1st January 1900
-	s := (float64(t.UnixNano()) / 1000000000) + 2208988800
+// SenderInterceptorFactory is a interceptor.Factory for a SenderInterceptor
+type SenderInterceptorFactory struct {
+	opts []SenderOption
+}
 
-	// higher 32 bits are the integer part, lower 32 bits are the fractional part
-	integerPart := uint32(s)
-	fractionalPart := uint32((s - float64(integerPart)) * 0xFFFFFFFF)
-	return uint64(integerPart)<<32 | uint64(fractionalPart)
+// NewInterceptor constructs a new SenderInterceptor
+func (s *SenderInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) {
+	i := &SenderInterceptor{
+		interval: 1 * time.Second,
+		now:      time.Now,
+		log:      logging.NewDefaultLoggerFactory().NewLogger("sender_interceptor"),
+		close:    make(chan struct{}),
+	}
+
+	for _, opt := range s.opts {
+		if err := opt(i); err != nil {
+			return nil, err
+		}
+	}
+
+	return i, nil
+}
+
+// NewSenderInterceptor returns a new SenderInterceptorFactory
+func NewSenderInterceptor(opts ...SenderOption) (*SenderInterceptorFactory, error) {
+	return &SenderInterceptorFactory{opts}, nil
 }
 
 // SenderInterceptor interceptor generates sender reports.
@@ -32,24 +50,6 @@ type SenderInterceptor struct {
 	close    chan struct{}
 }
 
-// NewSenderInterceptor returns a new SenderInterceptor interceptor.
-func NewSenderInterceptor(opts ...SenderOption) (*SenderInterceptor, error) {
-	s := &SenderInterceptor{
-		interval: 1 * time.Second,
-		now:      time.Now,
-		log:      logging.NewDefaultLoggerFactory().NewLogger("sender_interceptor"),
-		close:    make(chan struct{}),
-	}
-
-	for _, opt := range opts {
-		if err := opt(s); err != nil {
-			return nil, err
-		}
-	}
-
-	return s, nil
-}
-
 func (s *SenderInterceptor) isClosed() bool {
 	select {
 	case <-s.close:
@@ -138,3 +138,13 @@ func (s *SenderInterceptor) BindLocalStream(info *interceptor.StreamInfo, writer
 		return writer.Write(header, payload, a)
 	})
 }
+
+func ntpTime(t time.Time) uint64 {
+	// seconds since 1st January 1900
+	s := (float64(t.UnixNano()) / 1000000000) + 2208988800
+
+	// higher 32 bits are the integer part, lower 32 bits are the fractional part
+	integerPart := uint32(s)
+	fractionalPart := uint32((s - float64(integerPart)) * 0xFFFFFFFF)
+	return uint64(integerPart)<<32 | uint64(fractionalPart)
+}
diff --git a/pkg/twcc/header_extension.go b/pkg/twcc/header_extension_interceptor.go
similarity index 79%
rename from pkg/twcc/header_extension.go
rename to pkg/twcc/header_extension_interceptor.go
index 7db444d..305efc2 100644
--- a/pkg/twcc/header_extension.go
+++ b/pkg/twcc/header_extension_interceptor.go
@@ -7,17 +7,26 @@ import (
 	"github.com/pion/rtp"
 )
 
+// HeaderExtensionInterceptorFactory is a interceptor.Factory for a HeaderExtensionInterceptor
+type HeaderExtensionInterceptorFactory struct {
+}
+
+// NewInterceptor constructs a new HeaderExtensionInterceptor
+func (h *HeaderExtensionInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) {
+	return &HeaderExtensionInterceptor{}, nil
+}
+
+// NewHeaderExtensionInterceptor returns a HeaderExtensionInterceptorFactory
+func NewHeaderExtensionInterceptor() (*HeaderExtensionInterceptorFactory, error) {
+	return &HeaderExtensionInterceptorFactory{}, nil
+}
+
 // HeaderExtensionInterceptor adds transport wide sequence numbers as header extension to each RTP packet
 type HeaderExtensionInterceptor struct {
 	interceptor.NoOp
 	nextSequenceNr uint32
 }
 
-// NewHeaderExtensionInterceptor returns a HeaderExtensionInterceptor
-func NewHeaderExtensionInterceptor() (*HeaderExtensionInterceptor, error) {
-	return &HeaderExtensionInterceptor{}, nil
-}
-
 const transportCCURI = "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"
 
 // BindLocalStream returns a writer that adds a rtp.TransportCCExtension
diff --git a/pkg/twcc/sender_interceptor.go b/pkg/twcc/sender_interceptor.go
index cfd68af..9189d98 100644
--- a/pkg/twcc/sender_interceptor.go
+++ b/pkg/twcc/sender_interceptor.go
@@ -10,6 +10,36 @@ import (
 	"github.com/pion/rtp"
 )
 
+// SenderInterceptorFactory is a interceptor.Factory for a SenderInterceptor
+type SenderInterceptorFactory struct {
+	opts []Option
+}
+
+// NewInterceptor constructs a new SenderInterceptor
+func (s *SenderInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) {
+	i := &SenderInterceptor{
+		log:        logging.NewDefaultLoggerFactory().NewLogger("twcc_sender_interceptor"),
+		packetChan: make(chan packet),
+		close:      make(chan struct{}),
+		interval:   100 * time.Millisecond,
+		startTime:  time.Now(),
+	}
+
+	for _, opt := range s.opts {
+		err := opt(i)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return i, nil
+}
+
+// NewSenderInterceptor returns a new SenderInterceptorFactory configured with the given options.
+func NewSenderInterceptor(opts ...Option) (*SenderInterceptorFactory, error) {
+	return &SenderInterceptorFactory{opts: opts}, nil
+}
+
 // SenderInterceptor sends transport wide congestion control reports as specified in:
 // https://datatracker.ietf.org/doc/html/draft-holmer-rmcat-transport-wide-cc-extensions-01
 type SenderInterceptor struct {
@@ -40,26 +70,6 @@ func SendInterval(interval time.Duration) Option {
 	}
 }
 
-// NewSenderInterceptor returns a new SenderInterceptor configured with the given options.
-func NewSenderInterceptor(opts ...Option) (*SenderInterceptor, error) {
-	i := &SenderInterceptor{
-		log:        logging.NewDefaultLoggerFactory().NewLogger("twcc_sender_interceptor"),
-		packetChan: make(chan packet),
-		close:      make(chan struct{}),
-		interval:   100 * time.Millisecond,
-		startTime:  time.Now(),
-	}
-
-	for _, opt := range opts {
-		err := opt(i)
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	return i, nil
-}
-
 // 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 {
diff --git a/registry.go b/registry.go
index d3eed3b..1347193 100644
--- a/registry.go
+++ b/registry.go
@@ -2,19 +2,29 @@ package interceptor
 
 // Registry is a collector for interceptors.
 type Registry struct {
-	interceptors []Interceptor
+	factories []Factory
 }
 
 // Add adds a new Interceptor to the registry.
-func (i *Registry) Add(icpr Interceptor) {
-	i.interceptors = append(i.interceptors, icpr)
+func (r *Registry) Add(f Factory) {
+	r.factories = append(r.factories, f)
 }
 
 // Build constructs a single Interceptor from a InterceptorRegistry
-func (i *Registry) Build() Interceptor {
-	if len(i.interceptors) == 0 {
-		return &NoOp{}
+func (r *Registry) Build(id string) (Interceptor, error) {
+	if len(r.factories) == 0 {
+		return &NoOp{}, nil
 	}
 
-	return NewChain(i.interceptors)
+	interceptors := []Interceptor{}
+	for _, f := range r.factories {
+		i, err := f.NewInterceptor(id)
+		if err != nil {
+			return nil, err
+		}
+
+		interceptors = append(interceptors, i)
+	}
+
+	return NewChain(interceptors), nil
 }


Test patch:
diff --git a/pkg/nack/generator_interceptor_test.go b/pkg/nack/generator_interceptor_test.go
index aa9cbc7..5ef4f0d 100644
--- a/pkg/nack/generator_interceptor_test.go
+++ b/pkg/nack/generator_interceptor_test.go
@@ -14,7 +14,7 @@ import (
 
 func TestGeneratorInterceptor(t *testing.T) {
 	const interval = time.Millisecond * 10
-	i, err := NewGeneratorInterceptor(
+	f, err := NewGeneratorInterceptor(
 		GeneratorSize(64),
 		GeneratorSkipLastN(2),
 		GeneratorInterval(interval),
@@ -22,6 +22,9 @@ func TestGeneratorInterceptor(t *testing.T) {
 	)
 	assert.NoError(t, err)
 
+	i, err := f.NewInterceptor("")
+	assert.NoError(t, err)
+
 	stream := test.NewMockStream(&interceptor.StreamInfo{
 		SSRC:         1,
 		RTCPFeedback: []interceptor.RTCPFeedback{{Type: "nack"}},
@@ -65,6 +68,8 @@ func TestGeneratorInterceptor(t *testing.T) {
 }
 
 func TestGeneratorInterceptor_InvalidSize(t *testing.T) {
-	_, err := NewGeneratorInterceptor(GeneratorSize(5))
+	f, _ := NewGeneratorInterceptor(GeneratorSize(5))
+
+	_, err := f.NewInterceptor("")
 	assert.Error(t, err, ErrInvalidSize)
 }
diff --git a/pkg/nack/responder_interceptor_test.go b/pkg/nack/responder_interceptor_test.go
index 08f7024..42f125b 100644
--- a/pkg/nack/responder_interceptor_test.go
+++ b/pkg/nack/responder_interceptor_test.go
@@ -13,12 +13,15 @@ import (
 )
 
 func TestResponderInterceptor(t *testing.T) {
-	i, err := NewResponderInterceptor(
+	f, err := NewResponderInterceptor(
 		ResponderSize(8),
 		ResponderLog(logging.NewDefaultLoggerFactory().NewLogger("test")),
 	)
 	assert.NoError(t, err)
 
+	i, err := f.NewInterceptor("")
+	assert.NoError(t, err)
+
 	stream := test.NewMockStream(&interceptor.StreamInfo{
 		SSRC:         1,
 		RTCPFeedback: []interceptor.RTCPFeedback{{Type: "nack"}},
@@ -66,7 +69,9 @@ func TestResponderInterceptor(t *testing.T) {
 }
 
 func TestResponderInterceptor_InvalidSize(t *testing.T) {
-	_, err := NewResponderInterceptor(ResponderSize(5))
+	f, _ := NewResponderInterceptor(ResponderSize(5))
+
+	_, err := f.NewInterceptor("")
 	assert.Error(t, err, ErrInvalidSize)
 }
 
@@ -74,12 +79,15 @@ func TestResponderInterceptor_InvalidSize(t *testing.T) {
 //
 //     go test -race ./pkg/nack/
 func TestResponderInterceptor_Race(t *testing.T) {
-	i, err := NewResponderInterceptor(
+	f, err := NewResponderInterceptor(
 		ResponderSize(32768),
 		ResponderLog(logging.NewDefaultLoggerFactory().NewLogger("test")),
 	)
 	assert.NoError(t, err)
 
+	i, err := f.NewInterceptor("")
+	assert.NoError(t, err)
+
 	stream := test.NewMockStream(&interceptor.StreamInfo{
 		SSRC:         1,
 		RTCPFeedback: []interceptor.RTCPFeedback{{Type: "nack"}},
diff --git a/pkg/report/receiver_interceptor_test.go b/pkg/report/receiver_interceptor_test.go
index 412a016..2056689 100644
--- a/pkg/report/receiver_interceptor_test.go
+++ b/pkg/report/receiver_interceptor_test.go
@@ -15,13 +15,16 @@ import (
 func TestReceiverInterceptor(t *testing.T) {
 	t.Run("before any packet", func(t *testing.T) {
 		mt := test.MockTime{}
-		i, err := NewReceiverInterceptor(
+		f, err := NewReceiverInterceptor(
 			ReceiverInterval(time.Millisecond*50),
 			ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")),
 			ReceiverNow(mt.Now),
 		)
 		assert.NoError(t, err)
 
+		i, err := f.NewInterceptor("")
+		assert.NoError(t, err)
+
 		stream := test.NewMockStream(&interceptor.StreamInfo{
 			SSRC:      123456,
 			ClockRate: 90000,
@@ -50,13 +53,16 @@ func TestReceiverInterceptor(t *testing.T) {
 
 	t.Run("after RTP packets", func(t *testing.T) {
 		mt := test.MockTime{}
-		i, err := NewReceiverInterceptor(
+		f, err := NewReceiverInterceptor(
 			ReceiverInterval(time.Millisecond*50),
 			ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")),
 			ReceiverNow(mt.Now),
 		)
 		assert.NoError(t, err)
 
+		i, err := f.NewInterceptor("")
+		assert.NoError(t, err)
+
 		stream := test.NewMockStream(&interceptor.StreamInfo{
 			SSRC:      123456,
 			ClockRate: 90000,
@@ -89,13 +95,16 @@ func TestReceiverInterceptor(t *testing.T) {
 
 	t.Run("after RTP and RTCP packets", func(t *testing.T) {
 		mt := test.MockTime{}
-		i, err := NewReceiverInterceptor(
+		f, err := NewReceiverInterceptor(
 			ReceiverInterval(time.Millisecond*50),
 			ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")),
 			ReceiverNow(mt.Now),
 		)
 		assert.NoError(t, err)
 
+		i, err := f.NewInterceptor("")
+		assert.NoError(t, err)
+
 		stream := test.NewMockStream(&interceptor.StreamInfo{
 			SSRC:      123456,
 			ClockRate: 90000,
@@ -139,13 +148,16 @@ func TestReceiverInterceptor(t *testing.T) {
 
 	t.Run("overflow", func(t *testing.T) {
 		mt := test.MockTime{}
-		i, err := NewReceiverInterceptor(
+		f, err := NewReceiverInterceptor(
 			ReceiverInterval(time.Millisecond*50),
 			ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")),
 			ReceiverNow(mt.Now),
 		)
 		assert.NoError(t, err)
 
+		i, err := f.NewInterceptor("")
+		assert.NoError(t, err)
+
 		stream := test.NewMockStream(&interceptor.StreamInfo{
 			SSRC:      123456,
 			ClockRate: 90000,
@@ -180,13 +192,16 @@ func TestReceiverInterceptor(t *testing.T) {
 
 	t.Run("packet loss", func(t *testing.T) {
 		mt := test.MockTime{}
-		i, err := NewReceiverInterceptor(
+		f, err := NewReceiverInterceptor(
 			ReceiverInterval(time.Millisecond*50),
 			ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")),
 			ReceiverNow(mt.Now),
 		)
 		assert.NoError(t, err)
 
+		i, err := f.NewInterceptor("")
+		assert.NoError(t, err)
+
 		stream := test.NewMockStream(&interceptor.StreamInfo{
 			SSRC:      123456,
 			ClockRate: 90000,
@@ -247,13 +262,16 @@ func TestReceiverInterceptor(t *testing.T) {
 
 	t.Run("overflow and packet loss", func(t *testing.T) {
 		mt := test.MockTime{}
-		i, err := NewReceiverInterceptor(
+		f, err := NewReceiverInterceptor(
 			ReceiverInterval(time.Millisecond*50),
 			ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")),
 			ReceiverNow(mt.Now),
 		)
 		assert.NoError(t, err)
 
+		i, err := f.NewInterceptor("")
+		assert.NoError(t, err)
+
 		stream := test.NewMockStream(&interceptor.StreamInfo{
 			SSRC:      123456,
 			ClockRate: 90000,
@@ -288,13 +306,16 @@ func TestReceiverInterceptor(t *testing.T) {
 
 	t.Run("reordered packets", func(t *testing.T) {
 		mt := test.MockTime{}
-		i, err := NewReceiverInterceptor(
+		f, err := NewReceiverInterceptor(
 			ReceiverInterval(time.Millisecond*50),
 			ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")),
 			ReceiverNow(mt.Now),
 		)
 		assert.NoError(t, err)
 
+		i, err := f.NewInterceptor("")
+		assert.NoError(t, err)
+
 		stream := test.NewMockStream(&interceptor.StreamInfo{
 			SSRC:      123456,
 			ClockRate: 90000,
@@ -327,13 +348,16 @@ func TestReceiverInterceptor(t *testing.T) {
 
 	t.Run("jitter", func(t *testing.T) {
 		mt := test.MockTime{}
-		i, err := NewReceiverInterceptor(
+		f, err := NewReceiverInterceptor(
 			ReceiverInterval(time.Millisecond*50),
 			ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")),
 			ReceiverNow(mt.Now),
 		)
 		assert.NoError(t, err)
 
+		i, err := f.NewInterceptor("")
+		assert.NoError(t, err)
+
 		stream := test.NewMockStream(&interceptor.StreamInfo{
 			SSRC:      123456,
 			ClockRate: 90000,
@@ -373,13 +397,16 @@ func TestReceiverInterceptor(t *testing.T) {
 
 	t.Run("delay", func(t *testing.T) {
 		mt := test.MockTime{}
-		i, err := NewReceiverInterceptor(
+		f, err := NewReceiverInterceptor(
 			ReceiverInterval(time.Millisecond*50),
 			ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")),
 			ReceiverNow(mt.Now),
 		)
 		assert.NoError(t, err)
 
+		i, err := f.NewInterceptor("")
+		assert.NoError(t, err)
+
 		stream := test.NewMockStream(&interceptor.StreamInfo{
 			SSRC:      123456,
 			ClockRate: 90000,
diff --git a/pkg/report/sender_interceptor_test.go b/pkg/report/sender_interceptor_test.go
index 5a53491..6c891a8 100644
--- a/pkg/report/sender_interceptor_test.go
+++ b/pkg/report/sender_interceptor_test.go
@@ -15,13 +15,16 @@ import (
 func TestSenderInterceptor(t *testing.T) {
 	t.Run("before any packet", func(t *testing.T) {
 		mt := &test.MockTime{}
-		i, err := NewSenderInterceptor(
+		f, err := NewSenderInterceptor(
 			SenderInterval(time.Millisecond*50),
 			SenderLog(logging.NewDefaultLoggerFactory().NewLogger("test")),
 			SenderNow(mt.Now),
 		)
 		assert.NoError(t, err)
 
+		i, err := f.NewInterceptor("")
+		assert.NoError(t, err)
+
 		stream := test.NewMockStream(&interceptor.StreamInfo{
 			SSRC:      123456,
 			ClockRate: 90000,
@@ -46,13 +49,16 @@ func TestSenderInterceptor(t *testing.T) {
 
 	t.Run("after RTP packets", func(t *testing.T) {
 		mt := &test.MockTime{}
-		i, err := NewSenderInterceptor(
+		f, err := NewSenderInterceptor(
 			SenderInterval(time.Millisecond*50),
 			SenderLog(logging.NewDefaultLoggerFactory().NewLogger("test")),
 			SenderNow(mt.Now),
 		)
 		assert.NoError(t, err)
 
+		i, err := f.NewInterceptor("")
+		assert.NoError(t, err)
+
 		stream := test.NewMockStream(&interceptor.StreamInfo{
 			SSRC:      123456,
 			ClockRate: 90000,
diff --git a/pkg/twcc/header_extension_test.go b/pkg/twcc/header_extension_interceptor_test.go
similarity index 93%
rename from pkg/twcc/header_extension_test.go
rename to pkg/twcc/header_extension_interceptor_test.go
index aea0a53..f28ac72 100644
--- a/pkg/twcc/header_extension_test.go
+++ b/pkg/twcc/header_extension_interceptor_test.go
@@ -13,7 +13,10 @@ import (
 
 func TestHeaderExtensionInterceptor(t *testing.T) {
 	t.Run("add transport wide cc to each packet", func(t *testing.T) {
-		inter, err := NewHeaderExtensionInterceptor()
+		factory, err := NewHeaderExtensionInterceptor()
+		assert.NoError(t, err)
+
+		inter, err := factory.NewInterceptor("")
 		assert.NoError(t, err)
 
 		pChan := make(chan *rtp.Packet, 10*5)
diff --git a/pkg/twcc/sender_interceptor_test.go b/pkg/twcc/sender_interceptor_test.go
index 503e1d8..4293b21 100644
--- a/pkg/twcc/sender_interceptor_test.go
+++ b/pkg/twcc/sender_interceptor_test.go
@@ -13,7 +13,10 @@ import (
 
 func TestSenderInterceptor(t *testing.T) {
 	t.Run("before any packets", func(t *testing.T) {
-		i, err := NewSenderInterceptor()
+		f, err := NewSenderInterceptor()
+		assert.NoError(t, err)
+
+		i, err := f.NewInterceptor("")
 		assert.NoError(t, err)
 
 		stream := test.NewMockStream(&interceptor.StreamInfo{SSRC: 1, RTPHeaderExtensions: []interceptor.RTPHeaderExtension{
@@ -40,7 +43,10 @@ func TestSenderInterceptor(t *testing.T) {
 	})
 
 	t.Run("after RTP packets", func(t *testing.T) {
-		i, err := NewSenderInterceptor()
+		f, err := NewSenderInterceptor()
+		assert.NoError(t, err)
+
+		i, err := f.NewInterceptor("")
 		assert.NoError(t, err)
 
 		stream := test.NewMockStream(&interceptor.StreamInfo{SSRC: 1, RTPHeaderExtensions: []interceptor.RTPHeaderExtension{
@@ -77,9 +83,10 @@ func TestSenderInterceptor(t *testing.T) {
 	})
 
 	t.Run("different delays between RTP packets", func(t *testing.T) {
-		i, err := NewSenderInterceptor(
-			SendInterval(500 * time.Millisecond),
-		)
+		f, err := NewSenderInterceptor(SendInterval(500 * time.Millisecond))
+		assert.NoError(t, err)
+
+		i, err := f.NewInterceptor("")
 		assert.NoError(t, err)
 
 		stream := test.NewMockStream(&interceptor.StreamInfo{RTPHeaderExtensions: []interceptor.RTPHeaderExtension{
@@ -123,9 +130,10 @@ func TestSenderInterceptor(t *testing.T) {
 	})
 
 	t.Run("packet loss", func(t *testing.T) {
-		i, err := NewSenderInterceptor(
-			SendInterval(2 * time.Second),
-		)
+		f, err := NewSenderInterceptor(SendInterval(2 * time.Second))
+		assert.NoError(t, err)
+
+		i, err := f.NewInterceptor("")
 		assert.NoError(t, err)
 
 		stream := test.NewMockStream(&interceptor.StreamInfo{RTPHeaderExtensions: []interceptor.RTPHeaderExtension{
@@ -201,9 +209,10 @@ func TestSenderInterceptor(t *testing.T) {
 	})
 
 	t.Run("overflow", func(t *testing.T) {
-		i, err := NewSenderInterceptor(
-			SendInterval(2 * time.Second),
-		)
+		f, err := NewSenderInterceptor(SendInterval(2 * time.Second))
+		assert.NoError(t, err)
+
+		i, err := f.NewInterceptor("")
 		assert.NoError(t, err)
 
 		stream := test.NewMockStream(&interceptor.StreamInfo{RTPHeaderExtensions: []interceptor.RTPHeaderExtension{