Overview
The PeerConnection is the central component in Pion WebRTC, representing a connection between two peers. It manages the entire lifecycle of a WebRTC session, from ICE candidate gathering to media transmission.
// PeerConnection represents a WebRTC connection that establishes a
// peer-to-peer communications with another PeerConnection instance in a
// browser, or to another endpoint implementing the required protocols.
type PeerConnection struct {
id string
mu sync.RWMutex
sdpOrigin sdp.Origin
// ops is an operations queue which will ensure the enqueued actions are
// executed in order. It is used for asynchronously, but serially processing
// remote and local descriptions
ops *operations
configuration Configuration
currentLocalDescription *SessionDescription
pendingLocalDescription *SessionDescription
currentRemoteDescription *SessionDescription
pendingRemoteDescription *SessionDescription
signalingState SignalingState
iceConnectionState atomic.Value // ICEConnectionState
connectionState atomic.Value // PeerConnectionState
rtpTransceivers []*RTPTransceiver
iceGatherer *ICEGatherer
iceTransport *ICETransport
dtlsTransport *DTLSTransport
sctpTransport *SCTPTransport
api *API
}
Creating a PeerConnection
Using Default Settings
import "github.com/pion/webrtc/v4"
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}
pc, err := webrtc.NewPeerConnection(config)
if err != nil {
panic(err)
}
defer pc.Close()
Using Custom API
The source code shows how NewPeerConnection works:
// NewPeerConnection creates a PeerConnection with the default codecs and interceptors.
func NewPeerConnection(configuration Configuration) (*PeerConnection, error) {
api := NewAPI()
return api.NewPeerConnection(configuration)
}
// NewPeerConnection creates a new PeerConnection with the provided configuration
func (api *API) NewPeerConnection(configuration Configuration) (*PeerConnection, error) {
pc := &PeerConnection{
id: fmt.Sprintf("PeerConnection-%d", time.Now().UnixNano()),
configuration: Configuration{
ICEServers: []ICEServer{},
ICETransportPolicy: ICETransportPolicyAll,
BundlePolicy: BundlePolicyBalanced,
RTCPMuxPolicy: RTCPMuxPolicyRequire,
Certificates: []Certificate{},
ICECandidatePoolSize: 0,
},
signalingState: SignalingStateStable,
api: api,
}
// Initialize transports and complete setup...
return pc, nil
}
Each PeerConnection gets a unique ID based on the current timestamp, useful for debugging and logging.
Signaling States
The PeerConnection goes through different signaling states during negotiation:
Stable
No offer/answer exchange in progress. This is the initial state and the state after a successful negotiation.
Have Local Offer
Local peer has created an offer and called SetLocalDescription().
Have Remote Offer
Remote peer has sent an offer which was set via SetRemoteDescription().
Have Local Pranswer
Local peer has created a provisional answer.
Have Remote Pranswer
Remote peer has sent a provisional answer.
Monitoring Signaling State
pc.OnSignalingStateChange(func(state webrtc.SignalingState) {
fmt.Printf("Signaling state changed: %s\n", state)
})
The implementation from the source:
func (pc *PeerConnection) onSignalingStateChange(newState SignalingState) {
pc.mu.RLock()
handler := pc.onSignalingStateChangeHandler
pc.mu.RUnlock()
pc.log.Infof("signaling state changed to %s", newState)
if handler != nil {
go handler(newState)
}
}
Connection States
The overall connection state is derived from ICE and DTLS transport states:
func (pc *PeerConnection) updateConnectionState(
iceConnectionState ICEConnectionState,
dtlsTransportState DTLSTransportState,
) {
connectionState := PeerConnectionStateNew
switch {
case pc.isClosed.Load():
connectionState = PeerConnectionStateClosed
case iceConnectionState == ICEConnectionStateFailed || dtlsTransportState == DTLSTransportStateFailed:
connectionState = PeerConnectionStateFailed
case iceConnectionState == ICEConnectionStateDisconnected:
connectionState = PeerConnectionStateDisconnected
case (iceConnectionState == ICEConnectionStateConnected ||
iceConnectionState == ICEConnectionStateCompleted) &&
(dtlsTransportState == DTLSTransportStateConnected):
connectionState = PeerConnectionStateConnected
}
if pc.connectionState.Load() == connectionState {
return
}
pc.onConnectionStateChange(connectionState)
}
Connection State Handlers
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
switch state {
case webrtc.PeerConnectionStateConnected:
fmt.Println("Peer connection established")
case webrtc.PeerConnectionStateDisconnected:
fmt.Println("Peer connection lost")
case webrtc.PeerConnectionStateFailed:
fmt.Println("Peer connection failed")
case webrtc.PeerConnectionStateClosed:
fmt.Println("Peer connection closed")
}
})
Creating Offers and Answers
Creating an Offer
// CreateOffer starts the PeerConnection and generates the localDescription
func (pc *PeerConnection) CreateOffer(options *OfferOptions) (SessionDescription, error) {
useIdentity := pc.idpLoginURL != nil
switch {
case useIdentity:
return SessionDescription{}, errIdentityProviderNotImplemented
case pc.isClosed.Load():
return SessionDescription{}, &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
}
if options != nil && options.ICERestart {
if err := pc.iceTransport.restart(); err != nil {
return SessionDescription{}, err
}
}
// Generate SDP and return offer...
}
Example usage:
// Create an offer
offer, err := pc.CreateOffer(nil)
if err != nil {
panic(err)
}
// Set local description
if err = pc.SetLocalDescription(offer); err != nil {
panic(err)
}
// Send offer to remote peer via signaling channel
sendToRemotePeer(offer)
You can trigger an ICE restart by passing &webrtc.OfferOptions{ICERestart: true} to CreateOffer().
Creating an Answer
// CreateAnswer starts the PeerConnection and generates the localDescription
func (pc *PeerConnection) CreateAnswer(options *AnswerOptions) (SessionDescription, error) {
remoteDesc := pc.RemoteDescription()
switch {
case remoteDesc == nil:
return SessionDescription{}, &rtcerr.InvalidStateError{Err: ErrNoRemoteDescription}
case pc.isClosed.Load():
return SessionDescription{}, &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
case pc.signalingState.Get() != SignalingStateHaveRemoteOffer &&
pc.signalingState.Get() != SignalingStateHaveLocalPranswer:
return SessionDescription{}, &rtcerr.InvalidStateError{Err: ErrIncorrectSignalingState}
}
// Generate answer SDP...
}
Example usage:
// Receive offer from remote peer
var offer webrtc.SessionDescription
receiveFromRemotePeer(&offer)
// Set remote description
if err := pc.SetRemoteDescription(offer); err != nil {
panic(err)
}
// Create answer
answer, err := pc.CreateAnswer(nil)
if err != nil {
panic(err)
}
// Set local description
if err = pc.SetLocalDescription(answer); err != nil {
panic(err)
}
// Send answer back to remote peer
sendToRemotePeer(answer)
Setting Descriptions
Setting Local Description
func (pc *PeerConnection) SetLocalDescription(desc SessionDescription) error {
if pc.isClosed.Load() {
return &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
}
haveLocalDescription := pc.currentLocalDescription != nil
// JSEP 5.4 - Allow empty SDP
if desc.SDP == "" {
switch desc.Type {
case SDPTypeAnswer, SDPTypePranswer:
desc.SDP = pc.lastAnswer
case SDPTypeOffer:
desc.SDP = pc.lastOffer
}
}
// Parse and validate SDP...
// Update signaling state...
// Start ICE gathering...
}
Setting Remote Description
func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) error {
if pc.isClosed.Load() {
return &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
}
isRenegotiation := pc.currentRemoteDescription != nil
if _, err := desc.Unmarshal(); err != nil {
return err
}
if err := pc.setDescription(&desc, stateChangeOpSetRemote); err != nil {
return err
}
// Extract ICE candidates from SDP...
// Start transports...
}
Always set the remote description before creating an answer. Attempting to create an answer without a remote offer will result in an error.
Adding Tracks
// Create a video track
videoTrack, err := webrtc.NewTrackLocalStaticSample(
webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8},
"video",
"pion",
)
if err != nil {
panic(err)
}
// Add track to PeerConnection
sender, err := pc.AddTrack(videoTrack)
if err != nil {
panic(err)
}
// Read RTCP packets from sender
go func() {
for {
packets, _, err := sender.ReadRTCP()
if err != nil {
return
}
// Handle RTCP feedback
}
}()
Receiving Tracks
pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
fmt.Printf("Track received: kind=%s, id=%s\n", track.Kind(), track.ID())
// Read RTP packets
go func() {
buf := make([]byte, 1500)
for {
i, _, err := track.Read(buf)
if err != nil {
return
}
// Process RTP packet
processMedia(buf[:i])
}
}()
})
From the source code:
func (pc *PeerConnection) onTrack(t *TrackRemote, r *RTPReceiver) {
pc.mu.RLock()
handler := pc.onTrackHandler
pc.mu.RUnlock()
pc.log.Debugf("got new track: %+v", t)
if t != nil {
if handler != nil {
go handler(t, r)
} else {
pc.log.Warnf("OnTrack unset, unable to handle incoming media streams")
}
}
}
Negotiation Needed
Pion automatically detects when renegotiation is needed:
func (pc *PeerConnection) checkNegotiationNeeded() bool {
pc.mu.Lock()
defer pc.mu.Unlock()
localDesc := pc.currentLocalDescription
remoteDesc := pc.currentRemoteDescription
if localDesc == nil {
return true
}
// Check if we have data channels but no data channel in SDP
pc.sctpTransport.lock.Lock()
lenDataChannel := len(pc.sctpTransport.dataChannels)
pc.sctpTransport.lock.Unlock()
if lenDataChannel != 0 && haveDataChannel(localDesc) == nil {
return true
}
// Check all transceivers...
for _, transceiver := range pc.rtpTransceivers {
// Check if transceiver requires renegotiation
}
return false
}
Handle renegotiation:
pc.OnNegotiationNeeded(func() {
fmt.Println("Renegotiation needed")
// Create new offer
offer, err := pc.CreateOffer(nil)
if err != nil {
return
}
if err = pc.SetLocalDescription(offer); err != nil {
return
}
// Send offer to remote peer
sendToRemotePeer(offer)
})
Configuration Updates
func (pc *PeerConnection) SetConfiguration(configuration Configuration) error {
if pc.isClosed.Load() {
return &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
}
// Validate that immutable fields haven't changed
if len(configuration.Certificates) > 0 {
if len(configuration.Certificates) != len(pc.configuration.Certificates) {
return &rtcerr.InvalidModificationError{Err: ErrModifyingCertificates}
}
}
// Update ICE servers
if pc.iceGatherer != nil {
if err := pc.iceGatherer.updateServers(configuration.ICEServers, pc.configuration.ICETransportPolicy); err != nil {
pc.log.Debugf("Could not update ICE gatherer servers: %v", err)
}
}
pc.configuration.ICEServers = configuration.ICEServers
return nil
}
Most configuration fields cannot be changed after the PeerConnection is created. Only ICE servers can be safely updated.
Closing Connections
// Graceful close
if err := pc.Close(); err != nil {
panic(err)
}
Always close PeerConnections to free resources:
defer func() {
if err := pc.Close(); err != nil {
log.Printf("Failed to close PeerConnection: %v", err)
}
}()
Next Steps
Signaling
Learn about SDP exchange and signaling protocols
ICE & Connectivity
Deep dive into ICE candidates and NAT traversal