// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package webrtc import ( "context" "fmt" "sync" "sync/atomic" "time" "github.com/pion/ice/v2" "github.com/pion/logging" "github.com/pion/webrtc/v3/internal/mux" ) // ICETransport allows an application access to information about the ICE // transport over which packets are sent and received. type ICETransport struct { lock sync.RWMutex role ICERole onConnectionStateChangeHandler atomic.Value // func(ICETransportState) internalOnConnectionStateChangeHandler atomic.Value // func(ICETransportState) onSelectedCandidatePairChangeHandler atomic.Value // func(*ICECandidatePair) state atomic.Value // ICETransportState gatherer *ICEGatherer conn *ice.Conn mux *mux.Mux ctx context.Context ctxCancel func() loggerFactory logging.LoggerFactory log logging.LeveledLogger } // GetSelectedCandidatePair returns the selected candidate pair on which packets are sent // if there is no selected pair nil is returned func (t *ICETransport) GetSelectedCandidatePair() (*ICECandidatePair, error) { agent := t.gatherer.getAgent() if agent == nil { return nil, nil //nolint:nilnil } icePair, err := agent.GetSelectedCandidatePair() if icePair == nil || err != nil { return nil, err } local, err := newICECandidateFromICE(icePair.Local) if err != nil { return nil, err } remote, err := newICECandidateFromICE(icePair.Remote) if err != nil { return nil, err } return &ICECandidatePair{Local: &local, Remote: &remote}, nil } // NewICETransport creates a new NewICETransport. func NewICETransport(gatherer *ICEGatherer, loggerFactory logging.LoggerFactory) *ICETransport { iceTransport := &ICETransport{ gatherer: gatherer, loggerFactory: loggerFactory, log: loggerFactory.NewLogger("ortc"), } iceTransport.setState(ICETransportStateNew) return iceTransport } // Start incoming connectivity checks based on its configured role. func (t *ICETransport) Start(gatherer *ICEGatherer, params ICEParameters, role *ICERole) error { t.lock.Lock() defer t.lock.Unlock() if t.State() != ICETransportStateNew { return errICETransportNotInNew } if gatherer != nil { t.gatherer = gatherer } if err := t.ensureGatherer(); err != nil { return err } agent := t.gatherer.getAgent() if agent == nil { return fmt.Errorf("%w: unable to start ICETransport", errICEAgentNotExist) } if err := agent.OnConnectionStateChange(func(iceState ice.ConnectionState) { state := newICETransportStateFromICE(iceState) t.setState(state) t.onConnectionStateChange(state) }); err != nil { return err } if err := agent.OnSelectedCandidatePairChange(func(local, remote ice.Candidate) { candidates, err := newICECandidatesFromICE([]ice.Candidate{local, remote}) if err != nil { t.log.Warnf("%w: %s", errICECandiatesCoversionFailed, err) return } t.onSelectedCandidatePairChange(NewICECandidatePair(&candidates[0], &candidates[1])) }); err != nil { return err } if role == nil { controlled := ICERoleControlled role = &controlled } t.role = *role t.ctx, t.ctxCancel = context.WithCancel(context.Background()) // Drop the lock here to allow ICE candidates to be // added so that the agent can complete a connection t.lock.Unlock() var iceConn *ice.Conn var err error switch *role { case ICERoleControlling: iceConn, err = agent.Dial(t.ctx, params.UsernameFragment, params.Password) case ICERoleControlled: iceConn, err = agent.Accept(t.ctx, params.UsernameFragment, params.Password) default: err = errICERoleUnknown } // Reacquire the lock to set the connection/mux t.lock.Lock() if err != nil { return err } t.conn = iceConn config := mux.Config{ Conn: t.conn, BufferSize: int(t.gatherer.api.settingEngine.getReceiveMTU()), LoggerFactory: t.loggerFactory, } t.mux = mux.NewMux(config) return nil } // restart is not exposed currently because ORTC has users create a whole new ICETransport // so for now lets keep it private so we don't cause ORTC users to depend on non-standard APIs func (t *ICETransport) restart() error { t.lock.Lock() defer t.lock.Unlock() agent := t.gatherer.getAgent() if agent == nil { return fmt.Errorf("%w: unable to restart ICETransport", errICEAgentNotExist) } if err := agent.Restart(t.gatherer.api.settingEngine.candidates.UsernameFragment, t.gatherer.api.settingEngine.candidates.Password); err != nil { return err } return t.gatherer.Gather() } // Stop irreversibly stops the ICETransport. func (t *ICETransport) Stop() error { t.lock.Lock() defer t.lock.Unlock() t.setState(ICETransportStateClosed) if t.ctxCancel != nil { t.ctxCancel() } if t.mux != nil { return t.mux.Close() } else if t.gatherer != nil { return t.gatherer.Close() } return nil } // OnSelectedCandidatePairChange sets a handler that is invoked when a new // ICE candidate pair is selected func (t *ICETransport) OnSelectedCandidatePairChange(f func(*ICECandidatePair)) { t.onSelectedCandidatePairChangeHandler.Store(f) } func (t *ICETransport) onSelectedCandidatePairChange(pair *ICECandidatePair) { if handler, ok := t.onSelectedCandidatePairChangeHandler.Load().(func(*ICECandidatePair)); ok { handler(pair) } } // OnConnectionStateChange sets a handler that is fired when the ICE // connection state changes. func (t *ICETransport) OnConnectionStateChange(f func(ICETransportState)) { t.onConnectionStateChangeHandler.Store(f) } func (t *ICETransport) onConnectionStateChange(state ICETransportState) { if handler, ok := t.onConnectionStateChangeHandler.Load().(func(ICETransportState)); ok { handler(state) } if handler, ok := t.internalOnConnectionStateChangeHandler.Load().(func(ICETransportState)); ok { handler(state) } } // Role indicates the current role of the ICE transport. func (t *ICETransport) Role() ICERole { t.lock.RLock() defer t.lock.RUnlock() return t.role } // SetRemoteCandidates sets the sequence of candidates associated with the remote ICETransport. func (t *ICETransport) SetRemoteCandidates(remoteCandidates []ICECandidate) error { t.lock.RLock() defer t.lock.RUnlock() if err := t.ensureGatherer(); err != nil { return err } agent := t.gatherer.getAgent() if agent == nil { return fmt.Errorf("%w: unable to set remote candidates", errICEAgentNotExist) } for _, c := range remoteCandidates { i, err := c.toICE() if err != nil { return err } if err = agent.AddRemoteCandidate(i); err != nil { return err } } return nil } // AddRemoteCandidate adds a candidate associated with the remote ICETransport. func (t *ICETransport) AddRemoteCandidate(remoteCandidate *ICECandidate) error { t.lock.RLock() defer t.lock.RUnlock() var ( c ice.Candidate err error ) if err = t.ensureGatherer(); err != nil { return err } if remoteCandidate != nil { if c, err = remoteCandidate.toICE(); err != nil { return err } } agent := t.gatherer.getAgent() if agent == nil { return fmt.Errorf("%w: unable to add remote candidates", errICEAgentNotExist) } return agent.AddRemoteCandidate(c) } // State returns the current ice transport state. func (t *ICETransport) State() ICETransportState { if v, ok := t.state.Load().(ICETransportState); ok { return v } return ICETransportState(0) } func (t *ICETransport) setState(i ICETransportState) { t.state.Store(i) } func (t *ICETransport) newEndpoint(f mux.MatchFunc) *mux.Endpoint { t.lock.Lock() defer t.lock.Unlock() return t.mux.NewEndpoint(f) } func (t *ICETransport) ensureGatherer() error { if t.gatherer == nil { return errICEGathererNotStarted } else if t.gatherer.getAgent() == nil { if err := t.gatherer.createAgent(); err != nil { return err } } return nil } func (t *ICETransport) collectStats(collector *statsReportCollector) { t.lock.Lock() conn := t.conn t.lock.Unlock() collector.Collecting() stats := TransportStats{ Timestamp: statsTimestampFrom(time.Now()), Type: StatsTypeTransport, ID: "iceTransport", } if conn != nil { stats.BytesSent = conn.BytesSent() stats.BytesReceived = conn.BytesReceived() } collector.Collect(stats.ID, stats) } func (t *ICETransport) haveRemoteCredentialsChange(newUfrag, newPwd string) bool { t.lock.Lock() defer t.lock.Unlock() agent := t.gatherer.getAgent() if agent == nil { return false } uFrag, uPwd, err := agent.GetRemoteUserCredentials() if err != nil { return false } return uFrag != newUfrag || uPwd != newPwd } func (t *ICETransport) setRemoteCredentials(newUfrag, newPwd string) error { t.lock.Lock() defer t.lock.Unlock() agent := t.gatherer.getAgent() if agent == nil { return fmt.Errorf("%w: unable to SetRemoteCredentials", errICEAgentNotExist) } return agent.SetRemoteCredentials(newUfrag, newPwd) }