// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package webrtc import ( "errors" "fmt" "io" "math" "sync" "sync/atomic" "time" "github.com/pion/datachannel" "github.com/pion/logging" "github.com/pion/webrtc/v3/pkg/rtcerr" ) const dataChannelBufferSize = math.MaxUint16 // message size limit for Chromium var errSCTPNotEstablished = errors.New("SCTP not established") // DataChannel represents a WebRTC DataChannel // The DataChannel interface represents a network channel // which can be used for bidirectional peer-to-peer transfers of arbitrary data type DataChannel struct { mu sync.RWMutex statsID string label string ordered bool maxPacketLifeTime *uint16 maxRetransmits *uint16 protocol string negotiated bool id *uint16 readyState atomic.Value // DataChannelState bufferedAmountLowThreshold uint64 detachCalled bool // The binaryType represents attribute MUST, on getting, return the value to // which it was last set. On setting, if the new value is either the string // "blob" or the string "arraybuffer", then set the IDL attribute to this // new value. Otherwise, throw a SyntaxError. When an DataChannel object // is created, the binaryType attribute MUST be initialized to the string // "blob". This attribute controls how binary data is exposed to scripts. // binaryType string onMessageHandler func(DataChannelMessage) openHandlerOnce sync.Once onOpenHandler func() dialHandlerOnce sync.Once onDialHandler func() onCloseHandler func() onBufferedAmountLow func() onErrorHandler func(error) sctpTransport *SCTPTransport dataChannel *datachannel.DataChannel // A reference to the associated api object used by this datachannel api *API log logging.LeveledLogger } // NewDataChannel creates a new DataChannel. // This constructor is part of the ORTC API. It is not // meant to be used together with the basic WebRTC API. func (api *API) NewDataChannel(transport *SCTPTransport, params *DataChannelParameters) (*DataChannel, error) { d, err := api.newDataChannel(params, nil, api.settingEngine.LoggerFactory.NewLogger("ortc")) if err != nil { return nil, err } err = d.open(transport) if err != nil { return nil, err } return d, nil } // newDataChannel is an internal constructor for the data channel used to // create the DataChannel object before the networking is set up. func (api *API) newDataChannel(params *DataChannelParameters, sctpTransport *SCTPTransport, log logging.LeveledLogger) (*DataChannel, error) { // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #5) if len(params.Label) > 65535 { return nil, &rtcerr.TypeError{Err: ErrStringSizeLimit} } d := &DataChannel{ sctpTransport: sctpTransport, statsID: fmt.Sprintf("DataChannel-%d", time.Now().UnixNano()), label: params.Label, protocol: params.Protocol, negotiated: params.Negotiated, id: params.ID, ordered: params.Ordered, maxPacketLifeTime: params.MaxPacketLifeTime, maxRetransmits: params.MaxRetransmits, api: api, log: log, } d.setReadyState(DataChannelStateConnecting) return d, nil } // open opens the datachannel over the sctp transport func (d *DataChannel) open(sctpTransport *SCTPTransport) error { association := sctpTransport.association() if association == nil { return errSCTPNotEstablished } d.mu.Lock() if d.sctpTransport != nil { // already open d.mu.Unlock() return nil } d.sctpTransport = sctpTransport var channelType datachannel.ChannelType var reliabilityParameter uint32 switch { case d.maxPacketLifeTime == nil && d.maxRetransmits == nil: if d.ordered { channelType = datachannel.ChannelTypeReliable } else { channelType = datachannel.ChannelTypeReliableUnordered } case d.maxRetransmits != nil: reliabilityParameter = uint32(*d.maxRetransmits) if d.ordered { channelType = datachannel.ChannelTypePartialReliableRexmit } else { channelType = datachannel.ChannelTypePartialReliableRexmitUnordered } default: reliabilityParameter = uint32(*d.maxPacketLifeTime) if d.ordered { channelType = datachannel.ChannelTypePartialReliableTimed } else { channelType = datachannel.ChannelTypePartialReliableTimedUnordered } } cfg := &datachannel.Config{ ChannelType: channelType, Priority: datachannel.ChannelPriorityNormal, ReliabilityParameter: reliabilityParameter, Label: d.label, Protocol: d.protocol, Negotiated: d.negotiated, LoggerFactory: d.api.settingEngine.LoggerFactory, } if d.id == nil { // avoid holding lock when generating ID, since id generation locks d.mu.Unlock() var dcID *uint16 err := d.sctpTransport.generateAndSetDataChannelID(d.sctpTransport.dtlsTransport.role(), &dcID) if err != nil { return err } d.mu.Lock() d.id = dcID } dc, err := datachannel.Dial(association, *d.id, cfg) if err != nil { d.mu.Unlock() return err } // bufferedAmountLowThreshold and onBufferedAmountLow might be set earlier dc.SetBufferedAmountLowThreshold(d.bufferedAmountLowThreshold) dc.OnBufferedAmountLow(d.onBufferedAmountLow) d.mu.Unlock() d.onDial() d.handleOpen(dc, false, d.negotiated) return nil } // Transport returns the SCTPTransport instance the DataChannel is sending over. func (d *DataChannel) Transport() *SCTPTransport { d.mu.RLock() defer d.mu.RUnlock() return d.sctpTransport } // After onOpen is complete check that the user called detach // and provide an error message if the call was missed func (d *DataChannel) checkDetachAfterOpen() { d.mu.RLock() defer d.mu.RUnlock() if d.api.settingEngine.detach.DataChannels && !d.detachCalled { d.log.Warn("webrtc.DetachDataChannels() enabled but didn't Detach, call Detach from OnOpen") } } // OnOpen sets an event handler which is invoked when // the underlying data transport has been established (or re-established). func (d *DataChannel) OnOpen(f func()) { d.mu.Lock() d.openHandlerOnce = sync.Once{} d.onOpenHandler = f d.mu.Unlock() if d.ReadyState() == DataChannelStateOpen { // If the data channel is already open, call the handler immediately. go d.openHandlerOnce.Do(func() { f() d.checkDetachAfterOpen() }) } } func (d *DataChannel) onOpen() { d.mu.RLock() handler := d.onOpenHandler d.mu.RUnlock() if handler != nil { go d.openHandlerOnce.Do(func() { handler() d.checkDetachAfterOpen() }) } } // OnDial sets an event handler which is invoked when the // peer has been dialed, but before said peer has responsed func (d *DataChannel) OnDial(f func()) { d.mu.Lock() d.dialHandlerOnce = sync.Once{} d.onDialHandler = f d.mu.Unlock() if d.ReadyState() == DataChannelStateOpen { // If the data channel is already open, call the handler immediately. go d.dialHandlerOnce.Do(f) } } func (d *DataChannel) onDial() { d.mu.RLock() handler := d.onDialHandler d.mu.RUnlock() if handler != nil { go d.dialHandlerOnce.Do(handler) } } // OnClose sets an event handler which is invoked when // the underlying data transport has been closed. func (d *DataChannel) OnClose(f func()) { d.mu.Lock() defer d.mu.Unlock() d.onCloseHandler = f } func (d *DataChannel) onClose() { d.mu.RLock() handler := d.onCloseHandler d.mu.RUnlock() if handler != nil { go handler() } } // OnMessage sets an event handler which is invoked on a binary // message arrival over the sctp transport from a remote peer. // OnMessage can currently receive messages up to 16384 bytes // in size. Check out the detach API if you want to use larger // message sizes. Note that browser support for larger messages // is also limited. func (d *DataChannel) OnMessage(f func(msg DataChannelMessage)) { d.mu.Lock() defer d.mu.Unlock() d.onMessageHandler = f } func (d *DataChannel) onMessage(msg DataChannelMessage) { d.mu.RLock() handler := d.onMessageHandler d.mu.RUnlock() if handler == nil { return } handler(msg) } func (d *DataChannel) handleOpen(dc *datachannel.DataChannel, isRemote, isAlreadyNegotiated bool) { d.mu.Lock() d.dataChannel = dc d.mu.Unlock() d.setReadyState(DataChannelStateOpen) // Fire the OnOpen handler immediately not using pion/datachannel // * detached datachannels have no read loop, the user needs to read and query themselves // * remote datachannels should fire OnOpened. This isn't spec compliant, but we can't break behavior yet // * already negotiated datachannels should fire OnOpened if d.api.settingEngine.detach.DataChannels || isRemote || isAlreadyNegotiated { // bufferedAmountLowThreshold and onBufferedAmountLow might be set earlier d.dataChannel.SetBufferedAmountLowThreshold(d.bufferedAmountLowThreshold) d.dataChannel.OnBufferedAmountLow(d.onBufferedAmountLow) d.onOpen() } else { dc.OnOpen(func() { d.onOpen() }) } d.mu.Lock() defer d.mu.Unlock() if !d.api.settingEngine.detach.DataChannels { go d.readLoop() } } // OnError sets an event handler which is invoked when // the underlying data transport cannot be read. func (d *DataChannel) OnError(f func(err error)) { d.mu.Lock() defer d.mu.Unlock() d.onErrorHandler = f } func (d *DataChannel) onError(err error) { d.mu.RLock() handler := d.onErrorHandler d.mu.RUnlock() if handler != nil { go handler(err) } } // See https://github.com/pion/webrtc/issues/1516 // nolint:gochecknoglobals var rlBufPool = sync.Pool{New: func() interface{} { return make([]byte, dataChannelBufferSize) }} func (d *DataChannel) readLoop() { for { buffer := rlBufPool.Get().([]byte) //nolint:forcetypeassert n, isString, err := d.dataChannel.ReadDataChannel(buffer) if err != nil { rlBufPool.Put(buffer) // nolint:staticcheck d.setReadyState(DataChannelStateClosed) if !errors.Is(err, io.EOF) { d.onError(err) } d.onClose() return } m := DataChannelMessage{Data: make([]byte, n), IsString: isString} copy(m.Data, buffer[:n]) // The 'staticcheck' pragma is a false positive on the part of the CI linter. rlBufPool.Put(buffer) // nolint:staticcheck // NB: Why was DataChannelMessage not passed as a pointer value? d.onMessage(m) // nolint:staticcheck } } // Send sends the binary message to the DataChannel peer func (d *DataChannel) Send(data []byte) error { err := d.ensureOpen() if err != nil { return err } _, err = d.dataChannel.WriteDataChannel(data, false) return err } // SendText sends the text message to the DataChannel peer func (d *DataChannel) SendText(s string) error { err := d.ensureOpen() if err != nil { return err } _, err = d.dataChannel.WriteDataChannel([]byte(s), true) return err } func (d *DataChannel) ensureOpen() error { d.mu.RLock() defer d.mu.RUnlock() if d.ReadyState() != DataChannelStateOpen { return io.ErrClosedPipe } return nil } // Detach allows you to detach the underlying datachannel. This provides // an idiomatic API to work with, however it disables the OnMessage callback. // Before calling Detach you have to enable this behavior by calling // webrtc.DetachDataChannels(). Combining detached and normal data channels // is not supported. // Please refer to the data-channels-detach example and the // pion/datachannel documentation for the correct way to handle the // resulting DataChannel object. func (d *DataChannel) Detach() (datachannel.ReadWriteCloser, error) { d.mu.Lock() defer d.mu.Unlock() if !d.api.settingEngine.detach.DataChannels { return nil, errDetachNotEnabled } if d.dataChannel == nil { return nil, errDetachBeforeOpened } d.detachCalled = true return d.dataChannel, nil } // Close Closes the DataChannel. It may be called regardless of whether // the DataChannel object was created by this peer or the remote peer. func (d *DataChannel) Close() error { d.mu.Lock() haveSctpTransport := d.dataChannel != nil d.mu.Unlock() if d.ReadyState() == DataChannelStateClosed { return nil } d.setReadyState(DataChannelStateClosing) if !haveSctpTransport { return nil } return d.dataChannel.Close() } // Label represents a label that can be used to distinguish this // DataChannel object from other DataChannel objects. Scripts are // allowed to create multiple DataChannel objects with the same label. func (d *DataChannel) Label() string { d.mu.RLock() defer d.mu.RUnlock() return d.label } // Ordered returns true if the DataChannel is ordered, and false if // out-of-order delivery is allowed. func (d *DataChannel) Ordered() bool { d.mu.RLock() defer d.mu.RUnlock() return d.ordered } // MaxPacketLifeTime represents the length of the time window (msec) during // which transmissions and retransmissions may occur in unreliable mode. func (d *DataChannel) MaxPacketLifeTime() *uint16 { d.mu.RLock() defer d.mu.RUnlock() return d.maxPacketLifeTime } // MaxRetransmits represents the maximum number of retransmissions that are // attempted in unreliable mode. func (d *DataChannel) MaxRetransmits() *uint16 { d.mu.RLock() defer d.mu.RUnlock() return d.maxRetransmits } // Protocol represents the name of the sub-protocol used with this // DataChannel. func (d *DataChannel) Protocol() string { d.mu.RLock() defer d.mu.RUnlock() return d.protocol } // Negotiated represents whether this DataChannel was negotiated by the // application (true), or not (false). func (d *DataChannel) Negotiated() bool { d.mu.RLock() defer d.mu.RUnlock() return d.negotiated } // ID represents the ID for this DataChannel. The value is initially // null, which is what will be returned if the ID was not provided at // channel creation time, and the DTLS role of the SCTP transport has not // yet been negotiated. Otherwise, it will return the ID that was either // selected by the script or generated. After the ID is set to a non-null // value, it will not change. func (d *DataChannel) ID() *uint16 { d.mu.RLock() defer d.mu.RUnlock() return d.id } // ReadyState represents the state of the DataChannel object. func (d *DataChannel) ReadyState() DataChannelState { if v, ok := d.readyState.Load().(DataChannelState); ok { return v } return DataChannelState(0) } // BufferedAmount represents the number of bytes of application data // (UTF-8 text and binary data) that have been queued using send(). Even // though the data transmission can occur in parallel, the returned value // MUST NOT be decreased before the current task yielded back to the event // loop to prevent race conditions. The value does not include framing // overhead incurred by the protocol, or buffering done by the operating // system or network hardware. The value of BufferedAmount slot will only // increase with each call to the send() method as long as the ReadyState is // open; however, BufferedAmount does not reset to zero once the channel // closes. func (d *DataChannel) BufferedAmount() uint64 { d.mu.RLock() defer d.mu.RUnlock() if d.dataChannel == nil { return 0 } return d.dataChannel.BufferedAmount() } // BufferedAmountLowThreshold represents the threshold at which the // bufferedAmount is considered to be low. When the bufferedAmount decreases // from above this threshold to equal or below it, the bufferedamountlow // event fires. BufferedAmountLowThreshold is initially zero on each new // DataChannel, but the application may change its value at any time. // The threshold is set to 0 by default. func (d *DataChannel) BufferedAmountLowThreshold() uint64 { d.mu.RLock() defer d.mu.RUnlock() if d.dataChannel == nil { return d.bufferedAmountLowThreshold } return d.dataChannel.BufferedAmountLowThreshold() } // SetBufferedAmountLowThreshold is used to update the threshold. // See BufferedAmountLowThreshold(). func (d *DataChannel) SetBufferedAmountLowThreshold(th uint64) { d.mu.Lock() defer d.mu.Unlock() d.bufferedAmountLowThreshold = th if d.dataChannel != nil { d.dataChannel.SetBufferedAmountLowThreshold(th) } } // OnBufferedAmountLow sets an event handler which is invoked when // the number of bytes of outgoing data becomes lower than the // BufferedAmountLowThreshold. func (d *DataChannel) OnBufferedAmountLow(f func()) { d.mu.Lock() defer d.mu.Unlock() d.onBufferedAmountLow = f if d.dataChannel != nil { d.dataChannel.OnBufferedAmountLow(f) } } func (d *DataChannel) getStatsID() string { d.mu.Lock() defer d.mu.Unlock() return d.statsID } func (d *DataChannel) collectStats(collector *statsReportCollector) { collector.Collecting() d.mu.Lock() defer d.mu.Unlock() stats := DataChannelStats{ Timestamp: statsTimestampNow(), Type: StatsTypeDataChannel, ID: d.statsID, Label: d.label, Protocol: d.protocol, // TransportID string `json:"transportId"` State: d.ReadyState(), } if d.id != nil { stats.DataChannelIdentifier = int32(*d.id) } if d.dataChannel != nil { stats.MessagesSent = d.dataChannel.MessagesSent() stats.BytesSent = d.dataChannel.BytesSent() stats.MessagesReceived = d.dataChannel.MessagesReceived() stats.BytesReceived = d.dataChannel.BytesReceived() } collector.Collect(stats.ID, stats) } func (d *DataChannel) setReadyState(r DataChannelState) { d.readyState.Store(r) }