Understanding the core Interceptor interface and its lifecycle methods
The Interceptor interface is the foundation of the Pion Interceptor library. It defines methods for processing RTP and RTCP packets in both directions, along with lifecycle hooks for stream management.
Here’s the complete interface from interceptor.go:
// 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 { // 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. BindRTCPReader(reader RTCPReader) RTCPReader // BindRTCPWriter lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method // will be called once per packet batch. BindRTCPWriter(writer RTCPWriter) RTCPWriter // 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. BindLocalStream(info *StreamInfo, writer RTPWriter) RTPWriter // UnbindLocalStream is called when the Stream is removed. It can be used to clean up any data related to that track. UnbindLocalStream(info *StreamInfo) // 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. BindRemoteStream(info *StreamInfo, reader RTPReader) RTPReader // UnbindRemoteStream is called when the Stream is removed. It can be used to clean up any data related to that track. UnbindRemoteStream(info *StreamInfo) io.Closer}
The library provides a NoOp interceptor that passes all packets through unchanged. This is useful as a base for implementing interceptors that only need to override specific methods:
// NoOp is an Interceptor that does not modify any packets. It can embedded in other interceptors, so it's// possible to implement only a subset of the methods.type NoOp struct{}func (i *NoOp) BindRTCPReader(reader RTCPReader) RTCPReader { return reader}func (i *NoOp) BindRTCPWriter(writer RTCPWriter) RTCPWriter { return writer}func (i *NoOp) BindLocalStream(_ *StreamInfo, writer RTPWriter) RTPWriter { return writer}func (i *NoOp) UnbindLocalStream(_ *StreamInfo) {}func (i *NoOp) BindRemoteStream(_ *StreamInfo, reader RTPReader) RTPReader { return reader}func (i *NoOp) UnbindRemoteStream(_ *StreamInfo) {}func (i *NoOp) Close() error { return nil}
When implementing bind methods, always call the provided reader/writer in your returned implementation. This ensures the packet flow continues through the chain.
// Goodfunc (i *MyInterceptor) BindLocalStream(info *StreamInfo, writer RTPWriter) RTPWriter { return RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes Attributes) (int, error) { // Your logic here return writer.Write(header, payload, attributes) // Pass it along })}// Bad - breaks the chain!func (i *MyInterceptor) BindLocalStream(info *StreamInfo, writer RTPWriter) RTPWriter { return RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes Attributes) (int, error) { // Your logic here return len(payload), nil // Doesn't call writer.Write! })}
Use StreamInfo for stream-specific logic
The StreamInfo parameter contains important stream metadata. Use it to differentiate between streams and access codec information.
func (i *MyInterceptor) BindRemoteStream(info *StreamInfo, reader RTPReader) RTPReader { // Different logic for audio vs video if strings.HasPrefix(info.MimeType, "video/") { return i.createVideoReader(info, reader) } return i.createAudioReader(info, reader)}
Clean up resources in unbind methods
Always implement the unbind methods to properly clean up stream-specific resources. Don’t wait for Close() to clean up per-stream resources.
func (i *MyInterceptor) UnbindLocalStream(info *StreamInfo) { i.mu.Lock() defer i.mu.Unlock() if state, ok := i.streams[info.SSRC]; ok { state.Close() delete(i.streams, info.SSRC) }}
Handle errors gracefully
If you encounter an error while processing a packet, decide whether to drop the packet or pass it through. Consider logging errors for debugging.
return RTPReaderFunc(func(b []byte, a Attributes) (int, Attributes, error) { if err := i.processPacket(b); err != nil { log.Printf("Error processing packet: %v", err) // Still pass packet through } return reader.Read(b, a)})
Use Attributes for inter-interceptor communication
The Attributes map allows interceptors to share data. Use custom keys to avoid conflicts.
type myKeyType intconst myDataKey myKeyType = 0// In one interceptorattributes.Set(myDataKey, myData)// In another interceptorif val := attributes.Get(myDataKey); val != nil { if data, ok := val.(*MyData); ok { // Use the data }}