Quick Start
This guide will walk you through creating a complete working example using Pion Interceptor. You’ll build a simple RTP packet logger that demonstrates the core concepts.
What You’ll Build
You’ll create a custom interceptor that:
Logs all outgoing RTP packets from local streams
Counts packets per stream
Demonstrates the interceptor lifecycle
Prerequisites
Make sure you have installed Pion Interceptor before continuing.
Understanding the Core Types
Before writing code, let’s understand the key types you’ll work with:
Interceptor Interface
The main interface that all interceptors must implement:
type Interceptor interface {
BindRTCPReader ( reader RTCPReader ) RTCPReader
BindRTCPWriter ( writer RTCPWriter ) RTCPWriter
BindLocalStream ( info * StreamInfo , writer RTPWriter ) RTPWriter
BindRemoteStream ( info * StreamInfo , reader RTPReader ) RTPReader
UnbindLocalStream ( info * StreamInfo )
UnbindRemoteStream ( info * StreamInfo )
Close () error
}
StreamInfo
Contains metadata about an RTP stream:
type StreamInfo struct {
ID string
SSRC uint32
PayloadType uint8
MimeType string
ClockRate uint32
Channels uint16
RTCPFeedback [] RTCPFeedback
RTPHeaderExtensions [] RTPHeaderExtension
// ... additional fields
}
Attributes
A key-value store for passing metadata between interceptors:
type Attributes map [ any ] any
func ( a Attributes ) Get ( key any ) any
func ( a Attributes ) Set ( key any , val any )
func ( a Attributes ) GetRTPHeader ( raw [] byte ) ( * rtp . Header , error )
Step 1: Create a Custom Interceptor
Create a new file logger_interceptor.go:
package main
import (
" fmt "
" sync "
" github.com/pion/interceptor "
" github.com/pion/rtp "
)
// LoggerInterceptor logs RTP packets for each stream
type LoggerInterceptor struct {
interceptor . NoOp
mu sync . Mutex
packetCounts map [ uint32 ] int
}
// NewLoggerInterceptor creates a new logger interceptor
func NewLoggerInterceptor () * LoggerInterceptor {
return & LoggerInterceptor {
packetCounts : make ( map [ uint32 ] int ),
}
}
// BindLocalStream is called when a new local stream is created
func ( l * LoggerInterceptor ) BindLocalStream ( info * interceptor . StreamInfo , writer interceptor . RTPWriter ) interceptor . RTPWriter {
fmt . Printf ( "[Logger] New local stream bound - SSRC: %d , PayloadType: %d \n " ,
info . SSRC , info . PayloadType )
// Return a writer that logs each packet
return interceptor . RTPWriterFunc ( func ( header * rtp . Header , payload [] byte , attributes interceptor . Attributes ) ( int , error ) {
// Increment packet count
l . mu . Lock ()
l . packetCounts [ header . SSRC ] ++
count := l . packetCounts [ header . SSRC ]
l . mu . Unlock ()
// Log packet information
fmt . Printf ( "[Logger] SSRC: %d , Seq: %d , Timestamp: %d , Payload: %d bytes, Count: %d \n " ,
header . SSRC , header . SequenceNumber , header . Timestamp , len ( payload ), count )
// Pass the packet to the next writer in the chain
return writer . Write ( header , payload , attributes )
})
}
// UnbindLocalStream is called when a stream is removed
func ( l * LoggerInterceptor ) UnbindLocalStream ( info * interceptor . StreamInfo ) {
l . mu . Lock ()
defer l . mu . Unlock ()
count := l . packetCounts [ info . SSRC ]
delete ( l . packetCounts , info . SSRC )
fmt . Printf ( "[Logger] Stream unbound - SSRC: %d , Total packets: %d \n " ,
info . SSRC , count )
}
// Close cleans up the interceptor
func ( l * LoggerInterceptor ) Close () error {
fmt . Println ( "[Logger] Interceptor closed" )
return nil
}
Notice how we embed interceptor.NoOp - this provides default implementations for all methods we don’t need to override.
Step 2: Create a Test Application
Now create a main.go that uses your interceptor:
package main
import (
" fmt "
" time "
" github.com/pion/interceptor "
" github.com/pion/rtp "
)
func main () {
fmt . Println ( "Starting Pion Interceptor example... \n " )
// Create our custom logger interceptor
logger := NewLoggerInterceptor ()
// Create an interceptor chain
chain := interceptor . NewChain ([] interceptor . Interceptor { logger })
defer chain . Close ()
// Simulate a stream being bound
streamInfo := & interceptor . StreamInfo {
SSRC : 12345 ,
PayloadType : 96 ,
MimeType : "video/VP8" ,
ClockRate : 90000 ,
}
// Bind the local stream with a writer that simulates sending packets
writer := chain . BindLocalStream ( streamInfo , interceptor . RTPWriterFunc (
func ( header * rtp . Header , payload [] byte , attributes interceptor . Attributes ) ( int , error ) {
// This is the final writer - in a real app, this would send to the network
return len ( payload ), nil
},
))
// Simulate sending 10 RTP packets
fmt . Println ( " \n Sending packets... \n " )
for i := uint16 ( 0 ); i < 10 ; i ++ {
header := & rtp . Header {
Version : 2 ,
SSRC : 12345 ,
SequenceNumber : i ,
Timestamp : uint32 ( i * 3000 ),
PayloadType : 96 ,
}
payload := [] byte ( fmt . Sprintf ( "Packet %d " , i ))
if _ , err := writer . Write ( header , payload , nil ); err != nil {
fmt . Printf ( "Error writing packet: %v \n " , err )
return
}
// Small delay between packets
time . Sleep ( 100 * time . Millisecond )
}
// Unbind the stream
fmt . Println ( " \n Unbinding stream... \n " )
chain . UnbindLocalStream ( streamInfo )
fmt . Println ( " \n Example completed successfully!" )
}
Step 3: Run Your Example
Initialize the module
go mod init github.com/yourusername/interceptor-example
go mod tidy
You should see output like:
Starting Pion Interceptor example...
[Logger] New local stream bound - SSRC: 12345, PayloadType: 96
Sending packets...
[Logger] SSRC: 12345, Seq: 0, Timestamp: 0, Payload: 8 bytes, Count: 1
[Logger] SSRC: 12345, Seq: 1, Timestamp: 3000, Payload: 8 bytes, Count: 2
[Logger] SSRC: 12345, Seq: 2, Timestamp: 6000, Payload: 8 bytes, Count: 3
...
[Logger] SSRC: 12345, Seq: 9, Timestamp: 27000, Payload: 8 bytes, Count: 10
Unbinding stream...
[Logger] Stream unbound - SSRC: 12345, Total packets: 10
[Logger] Interceptor closed
Example completed successfully!
Step 4: Using Built-in Interceptors
Let’s extend the example to use the built-in NACK interceptor:
package main
import (
" fmt "
" time "
" github.com/pion/interceptor "
" github.com/pion/interceptor/pkg/nack "
" github.com/pion/rtcp "
" github.com/pion/rtp "
)
func nackExample () {
// Create a NACK responder factory
responderFactory , err := nack . NewResponderInterceptor ()
if err != nil {
panic ( err )
}
// Build the interceptor
responder , err := responderFactory . NewInterceptor ( "" )
if err != nil {
panic ( err )
}
// Create a chain with the NACK responder
chain := interceptor . NewChain ([] interceptor . Interceptor { responder })
defer chain . Close ()
// Bind RTCP reader to receive NACK packets
rtcpReader := chain . BindRTCPReader (
interceptor . RTCPReaderFunc ( func ( b [] byte , a interceptor . Attributes ) ( int , interceptor . Attributes , error ) {
return len ( b ), nil , nil
}),
)
// Bind local stream
streamInfo := & interceptor . StreamInfo {
SSRC : 5000 ,
RTCPFeedback : [] interceptor . RTCPFeedback {{ Type : "nack" , Parameter : "" }},
}
streamWriter := chain . BindLocalStream ( streamInfo ,
interceptor . RTPWriterFunc ( func ( header * rtp . Header , payload [] byte , attributes interceptor . Attributes ) ( int , error ) {
fmt . Printf ( "Sending packet: Seq= %d \n " , header . SequenceNumber )
return len ( payload ), nil
}),
)
// Send some packets
for i := uint16 ( 0 ); i < 5 ; i ++ {
header := & rtp . Header {
Version : 2 ,
SSRC : 5000 ,
SequenceNumber : i ,
Timestamp : uint32 ( i * 3000 ),
}
streamWriter . Write ( header , [] byte { 0x 01 , 0x 02 , 0x 03 }, nil )
time . Sleep ( 50 * time . Millisecond )
}
// Simulate receiving a NACK for packet 2
fmt . Println ( " \n Simulating NACK for packet 2..." )
nackPacket := & rtcp . TransportLayerNack {
MediaSSRC : 5000 ,
Nacks : [] rtcp . NackPair {{ PacketID : 2 }},
}
nackBytes , _ := nackPacket . Marshal ()
rtcpReader . Read ( nackBytes , nil )
time . Sleep ( 100 * time . Millisecond )
fmt . Println ( " \n NACK example completed!" )
}
Understanding the Chain
The Chain type allows you to combine multiple interceptors:
// Create multiple interceptors
logger := NewLoggerInterceptor ()
nackResponder , _ := nack . NewResponderInterceptor ()
nackInterceptor , _ := nackResponder . NewInterceptor ( "" )
// Chain them together - they execute in order
chain := interceptor . NewChain ([] interceptor . Interceptor {
logger , // Logs first
nackInterceptor , // Then handles retransmission
})
Packets flow through interceptors in the order they appear in the chain. Place logging/monitoring interceptors first, and modifying interceptors later.
Using the Registry
For production applications, use the Registry to manage interceptor factories:
import (
" github.com/pion/interceptor "
" github.com/pion/interceptor/pkg/nack "
" github.com/pion/interceptor/pkg/report "
)
func setupRegistry () ( * interceptor . Registry , error ) {
registry := & interceptor . Registry {}
// Add NACK generator
nackGen , err := nack . NewGeneratorInterceptor ()
if err != nil {
return nil , err
}
registry . Add ( nackGen )
// Add sender/receiver reports
reportFactory , err := report . NewInterceptor ()
if err != nil {
return nil , err
}
registry . Add ( reportFactory )
return registry , nil
}
func main () {
registry , _ := setupRegistry ()
// Build the chain from registry
chain , _ := registry . Build ( "" )
defer chain . Close ()
// Use the chain...
}
Key Concepts Review
NoOp Embedding Embed interceptor.NoOp to only implement the methods you need
Bind Methods BindLocalStream and BindRemoteStream are called when streams are created
Unbind Methods UnbindLocalStream and UnbindRemoteStream clean up stream-specific resources
Chain Execution Interceptors in a chain execute sequentially in the order they’re added
Common Patterns
Pattern 1: Packet Inspection
func ( i * MyInterceptor ) BindLocalStream ( info * interceptor . StreamInfo , writer interceptor . RTPWriter ) interceptor . RTPWriter {
return interceptor . RTPWriterFunc ( func ( header * rtp . Header , payload [] byte , attributes interceptor . Attributes ) ( int , error ) {
// Inspect the packet
fmt . Printf ( "Packet SSRC: %d , Seq: %d \n " , header . SSRC , header . SequenceNumber )
// Pass through unchanged
return writer . Write ( header , payload , attributes )
})
}
Pattern 2: Packet Modification
func ( i * MyInterceptor ) BindLocalStream ( info * interceptor . StreamInfo , writer interceptor . RTPWriter ) interceptor . RTPWriter {
return interceptor . RTPWriterFunc ( func ( header * rtp . Header , payload [] byte , attributes interceptor . Attributes ) ( int , error ) {
// Modify the header
header . Marker = true
// Modify the payload
newPayload := append ([] byte { 0x FF }, payload ... )
// Write modified packet
return writer . Write ( header , newPayload , attributes )
})
}
Pattern 3: Using Attributes
func ( i * MyInterceptor ) BindRemoteStream ( info * interceptor . StreamInfo , reader interceptor . RTPReader ) interceptor . RTPReader {
return interceptor . RTPReaderFunc ( func ( b [] byte , a interceptor . Attributes ) ( int , interceptor . Attributes , error ) {
n , attr , err := reader . Read ( b , a )
if err != nil {
return n , attr , err
}
// Get the RTP header from attributes (cached)
header , err := attr . GetRTPHeader ( b [: n ])
if err != nil {
return n , attr , err
}
// Store custom metadata
attr . Set ( "arrival_time" , time . Now ())
return n , attr , nil
})
}
Next Steps
Explore Built-in Interceptors Learn from production-ready implementations like NACK, GCC, and TWCC
API Reference Browse the complete API documentation
Examples View more complete examples in the repository
Pion WebRTC Integration Use interceptors with Pion WebRTC for full WebRTC applications
Remember to always call Close() on your interceptor chain to clean up resources when you’re done.