mirror of
https://github.com/pion/webrtc.git
synced 2025-10-25 16:20:38 +08:00
1729 lines
52 KiB
Go
1729 lines
52 KiB
Go
// Package webrtc implements the WebRTC 1.0 as defined in W3C WebRTC specification document.
|
|
package webrtc
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/pions/sdp"
|
|
"github.com/pions/webrtc/internal/mux"
|
|
"github.com/pions/webrtc/internal/srtp"
|
|
"github.com/pions/webrtc/pkg/ice"
|
|
"github.com/pions/webrtc/pkg/logging"
|
|
"github.com/pions/webrtc/pkg/media"
|
|
"github.com/pions/webrtc/pkg/rtcerr"
|
|
"github.com/pions/webrtc/pkg/rtcp"
|
|
"github.com/pions/webrtc/pkg/rtp"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
var pcLog = logging.NewScopedLogger("pc")
|
|
|
|
const (
|
|
// Unknown defines default public constant to use for "enum" like struct
|
|
// comparisons when no value was defined.
|
|
Unknown = iota
|
|
unknownStr = "unknown"
|
|
|
|
receiveMTU = 8192
|
|
|
|
srtpMasterKeyLen = 16
|
|
srtpMasterKeySaltLen = 14
|
|
)
|
|
|
|
// RTCPeerConnection represents a WebRTC connection that establishes a
|
|
// peer-to-peer communications with another RTCPeerConnection instance in a
|
|
// browser, or to another endpoint implementing the required protocols.
|
|
type RTCPeerConnection struct {
|
|
sync.RWMutex
|
|
|
|
configuration RTCConfiguration
|
|
|
|
// CurrentLocalDescription represents the local description that was
|
|
// successfully negotiated the last time the RTCPeerConnection transitioned
|
|
// into the stable state plus any local candidates that have been generated
|
|
// by the IceAgent since the offer or answer was created.
|
|
CurrentLocalDescription *RTCSessionDescription
|
|
|
|
// PendingLocalDescription represents a local description that is in the
|
|
// process of being negotiated plus any local candidates that have been
|
|
// generated by the IceAgent since the offer or answer was created. If the
|
|
// RTCPeerConnection is in the stable state, the value is null.
|
|
PendingLocalDescription *RTCSessionDescription
|
|
|
|
// CurrentRemoteDescription represents the last remote description that was
|
|
// successfully negotiated the last time the RTCPeerConnection transitioned
|
|
// into the stable state plus any remote candidates that have been supplied
|
|
// via AddIceCandidate() since the offer or answer was created.
|
|
CurrentRemoteDescription *RTCSessionDescription
|
|
|
|
// PendingRemoteDescription represents a remote description that is in the
|
|
// process of being negotiated, complete with any remote candidates that
|
|
// have been supplied via AddIceCandidate() since the offer or answer was
|
|
// created. If the RTCPeerConnection is in the stable state, the value is
|
|
// null.
|
|
PendingRemoteDescription *RTCSessionDescription
|
|
|
|
// SignalingState attribute returns the signaling state of the
|
|
// RTCPeerConnection instance.
|
|
SignalingState RTCSignalingState
|
|
|
|
// IceGatheringState attribute returns the ICE gathering state of the
|
|
// RTCPeerConnection instance.
|
|
IceGatheringState RTCIceGatheringState // FIXME NOT-USED
|
|
|
|
// IceConnectionState attribute returns the ICE connection state of the
|
|
// RTCPeerConnection instance.
|
|
// IceConnectionState RTCIceConnectionState // FIXME SWAP-FOR-THIS
|
|
IceConnectionState ice.ConnectionState // FIXME REMOVE
|
|
|
|
// ConnectionState attribute returns the connection state of the
|
|
// RTCPeerConnection instance.
|
|
ConnectionState RTCPeerConnectionState
|
|
|
|
idpLoginURL *string
|
|
|
|
isClosed bool
|
|
negotiationNeeded bool
|
|
|
|
lastOffer string
|
|
lastAnswer string
|
|
|
|
rtpTransceivers []*RTCRtpTransceiver
|
|
|
|
// DataChannels
|
|
dataChannels map[uint16]*RTCDataChannel
|
|
|
|
// OnNegotiationNeeded func() // FIXME NOT-USED
|
|
// OnIceCandidate func() // FIXME NOT-USED
|
|
// OnIceCandidateError func() // FIXME NOT-USED
|
|
|
|
// OnIceGatheringStateChange func() // FIXME NOT-USED
|
|
// OnConnectionStateChange func() // FIXME NOT-USED
|
|
|
|
onSignalingStateChangeHandler func(RTCSignalingState)
|
|
onICEConnectionStateChangeHandler func(ice.ConnectionState)
|
|
onTrackHandler func(*RTCTrack)
|
|
onDataChannelHandler func(*RTCDataChannel)
|
|
|
|
iceGatherer *RTCIceGatherer
|
|
iceTransport *RTCIceTransport
|
|
dtlsTransport *RTCDtlsTransport
|
|
sctpTransport *RTCSctpTransport
|
|
|
|
srtpSession *srtp.SessionSRTP
|
|
srtpEndpoint *mux.Endpoint
|
|
|
|
srtcpSession *srtp.SessionSRTCP
|
|
srtcpEndpoint *mux.Endpoint
|
|
|
|
// A reference to the associated API state used by this connection
|
|
api *API
|
|
}
|
|
|
|
// New creates a new RTCPeerConfiguration with the provided configuration against the received API object
|
|
func (api *API) New(configuration RTCConfiguration) (*RTCPeerConnection, error) {
|
|
// https://w3c.github.io/webrtc-pc/#constructor (Step #2)
|
|
// Some variables defined explicitly despite their implicit zero values to
|
|
// allow better readability to understand what is happening.
|
|
pc := RTCPeerConnection{
|
|
configuration: RTCConfiguration{
|
|
IceServers: []RTCIceServer{},
|
|
IceTransportPolicy: RTCIceTransportPolicyAll,
|
|
BundlePolicy: RTCBundlePolicyBalanced,
|
|
RtcpMuxPolicy: RTCRtcpMuxPolicyRequire,
|
|
Certificates: []RTCCertificate{},
|
|
IceCandidatePoolSize: 0,
|
|
},
|
|
isClosed: false,
|
|
negotiationNeeded: false,
|
|
lastOffer: "",
|
|
lastAnswer: "",
|
|
SignalingState: RTCSignalingStateStable,
|
|
// IceConnectionState: RTCIceConnectionStateNew, // FIXME SWAP-FOR-THIS
|
|
IceConnectionState: ice.ConnectionStateNew, // FIXME REMOVE
|
|
IceGatheringState: RTCIceGatheringStateNew,
|
|
ConnectionState: RTCPeerConnectionStateNew,
|
|
dataChannels: make(map[uint16]*RTCDataChannel),
|
|
|
|
srtpSession: srtp.CreateSessionSRTP(),
|
|
srtcpSession: srtp.CreateSessionSRTCP(),
|
|
|
|
api: api,
|
|
}
|
|
|
|
var err error
|
|
if err = pc.initConfiguration(configuration); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// For now we eagerly allocate and start the gatherer
|
|
gatherer, err := pc.createIceGatherer()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pc.iceGatherer = gatherer
|
|
|
|
err = pc.gather()
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &pc, nil
|
|
}
|
|
|
|
// New creates a new RTCPeerConfiguration with the provided configuration
|
|
func New(configuration RTCConfiguration) (*RTCPeerConnection, error) {
|
|
return defaultAPI.New(configuration)
|
|
}
|
|
|
|
// initConfiguration defines validation of the specified RTCConfiguration and
|
|
// its assignment to the internal configuration variable. This function differs
|
|
// from its SetConfiguration counterpart because most of the checks do not
|
|
// include verification statements related to the existing state. Thus the
|
|
// function describes only minor verification of some the struct variables.
|
|
func (pc *RTCPeerConnection) initConfiguration(configuration RTCConfiguration) error {
|
|
if configuration.PeerIdentity != "" {
|
|
pc.configuration.PeerIdentity = configuration.PeerIdentity
|
|
}
|
|
|
|
// https://www.w3.org/TR/webrtc/#constructor (step #3)
|
|
if len(configuration.Certificates) > 0 {
|
|
now := time.Now()
|
|
for _, x509Cert := range configuration.Certificates {
|
|
if !x509Cert.Expires().IsZero() && now.After(x509Cert.Expires()) {
|
|
return &rtcerr.InvalidAccessError{Err: ErrCertificateExpired}
|
|
}
|
|
pc.configuration.Certificates = append(pc.configuration.Certificates, x509Cert)
|
|
}
|
|
} else {
|
|
sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return &rtcerr.UnknownError{Err: err}
|
|
}
|
|
certificate, err := GenerateCertificate(sk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pc.configuration.Certificates = []RTCCertificate{*certificate}
|
|
}
|
|
|
|
if configuration.BundlePolicy != RTCBundlePolicy(Unknown) {
|
|
pc.configuration.BundlePolicy = configuration.BundlePolicy
|
|
}
|
|
|
|
if configuration.RtcpMuxPolicy != RTCRtcpMuxPolicy(Unknown) {
|
|
pc.configuration.RtcpMuxPolicy = configuration.RtcpMuxPolicy
|
|
}
|
|
|
|
if configuration.IceCandidatePoolSize != 0 {
|
|
pc.configuration.IceCandidatePoolSize = configuration.IceCandidatePoolSize
|
|
}
|
|
|
|
if configuration.IceTransportPolicy != RTCIceTransportPolicy(Unknown) {
|
|
pc.configuration.IceTransportPolicy = configuration.IceTransportPolicy
|
|
}
|
|
|
|
if len(configuration.IceServers) > 0 {
|
|
for _, server := range configuration.IceServers {
|
|
if _, err := server.validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
pc.configuration.IceServers = configuration.IceServers
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// OnSignalingStateChange sets an event handler which is invoked when the
|
|
// peer connection's signaling state changes
|
|
func (pc *RTCPeerConnection) OnSignalingStateChange(f func(RTCSignalingState)) {
|
|
pc.Lock()
|
|
defer pc.Unlock()
|
|
pc.onSignalingStateChangeHandler = f
|
|
}
|
|
|
|
func (pc *RTCPeerConnection) onSignalingStateChange(newState RTCSignalingState) (done chan struct{}) {
|
|
pc.RLock()
|
|
hdlr := pc.onSignalingStateChangeHandler
|
|
pc.RUnlock()
|
|
|
|
pcLog.Infof("signaling state changed to %s", newState)
|
|
done = make(chan struct{})
|
|
if hdlr == nil {
|
|
close(done)
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
hdlr(newState)
|
|
close(done)
|
|
}()
|
|
|
|
return
|
|
}
|
|
|
|
// OnDataChannel sets an event handler which is invoked when a data
|
|
// channel message arrives from a remote peer.
|
|
func (pc *RTCPeerConnection) OnDataChannel(f func(*RTCDataChannel)) {
|
|
pc.Lock()
|
|
defer pc.Unlock()
|
|
pc.onDataChannelHandler = f
|
|
}
|
|
|
|
// OnTrack sets an event handler which is called when remote track
|
|
// arrives from a remote peer.
|
|
func (pc *RTCPeerConnection) OnTrack(f func(*RTCTrack)) {
|
|
pc.Lock()
|
|
defer pc.Unlock()
|
|
pc.onTrackHandler = f
|
|
}
|
|
|
|
func (pc *RTCPeerConnection) onTrack(t *RTCTrack) (done chan struct{}) {
|
|
pc.RLock()
|
|
hdlr := pc.onTrackHandler
|
|
pc.RUnlock()
|
|
|
|
pcLog.Debugf("got new track: %+v", t)
|
|
done = make(chan struct{})
|
|
if hdlr == nil || t == nil {
|
|
close(done)
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
hdlr(t)
|
|
close(done)
|
|
}()
|
|
|
|
return
|
|
}
|
|
|
|
// OnICEConnectionStateChange sets an event handler which is called
|
|
// when an ICE connection state is changed.
|
|
func (pc *RTCPeerConnection) OnICEConnectionStateChange(f func(ice.ConnectionState)) {
|
|
pc.Lock()
|
|
defer pc.Unlock()
|
|
pc.onICEConnectionStateChangeHandler = f
|
|
}
|
|
|
|
func (pc *RTCPeerConnection) onICEConnectionStateChange(cs ice.ConnectionState) (done chan struct{}) {
|
|
pc.RLock()
|
|
hdlr := pc.onICEConnectionStateChangeHandler
|
|
pc.RUnlock()
|
|
|
|
pcLog.Infof("ICE connection state changed: %s", cs)
|
|
done = make(chan struct{})
|
|
if hdlr == nil {
|
|
close(done)
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
hdlr(cs)
|
|
close(done)
|
|
}()
|
|
|
|
return
|
|
}
|
|
|
|
// SetConfiguration updates the configuration of this RTCPeerConnection object.
|
|
func (pc *RTCPeerConnection) SetConfiguration(configuration RTCConfiguration) error {
|
|
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-setconfiguration (step #2)
|
|
if pc.isClosed {
|
|
return &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
|
|
}
|
|
|
|
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #3)
|
|
if configuration.PeerIdentity != "" {
|
|
if configuration.PeerIdentity != pc.configuration.PeerIdentity {
|
|
return &rtcerr.InvalidModificationError{Err: ErrModifyingPeerIdentity}
|
|
}
|
|
pc.configuration.PeerIdentity = configuration.PeerIdentity
|
|
}
|
|
|
|
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #4)
|
|
if len(configuration.Certificates) > 0 {
|
|
if len(configuration.Certificates) != len(pc.configuration.Certificates) {
|
|
return &rtcerr.InvalidModificationError{Err: ErrModifyingCertificates}
|
|
}
|
|
|
|
for i, certificate := range configuration.Certificates {
|
|
if !pc.configuration.Certificates[i].Equals(certificate) {
|
|
return &rtcerr.InvalidModificationError{Err: ErrModifyingCertificates}
|
|
}
|
|
}
|
|
pc.configuration.Certificates = configuration.Certificates
|
|
}
|
|
|
|
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #5)
|
|
if configuration.BundlePolicy != RTCBundlePolicy(Unknown) {
|
|
if configuration.BundlePolicy != pc.configuration.BundlePolicy {
|
|
return &rtcerr.InvalidModificationError{Err: ErrModifyingBundlePolicy}
|
|
}
|
|
pc.configuration.BundlePolicy = configuration.BundlePolicy
|
|
}
|
|
|
|
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #6)
|
|
if configuration.RtcpMuxPolicy != RTCRtcpMuxPolicy(Unknown) {
|
|
if configuration.RtcpMuxPolicy != pc.configuration.RtcpMuxPolicy {
|
|
return &rtcerr.InvalidModificationError{Err: ErrModifyingRtcpMuxPolicy}
|
|
}
|
|
pc.configuration.RtcpMuxPolicy = configuration.RtcpMuxPolicy
|
|
}
|
|
|
|
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #7)
|
|
if configuration.IceCandidatePoolSize != 0 {
|
|
if pc.configuration.IceCandidatePoolSize != configuration.IceCandidatePoolSize &&
|
|
pc.LocalDescription() != nil {
|
|
return &rtcerr.InvalidModificationError{Err: ErrModifyingIceCandidatePoolSize}
|
|
}
|
|
pc.configuration.IceCandidatePoolSize = configuration.IceCandidatePoolSize
|
|
}
|
|
|
|
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #8)
|
|
if configuration.IceTransportPolicy != RTCIceTransportPolicy(Unknown) {
|
|
pc.configuration.IceTransportPolicy = configuration.IceTransportPolicy
|
|
}
|
|
|
|
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #11)
|
|
if len(configuration.IceServers) > 0 {
|
|
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3)
|
|
for _, server := range configuration.IceServers {
|
|
if _, err := server.validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
pc.configuration.IceServers = configuration.IceServers
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetConfiguration returns an RTCConfiguration object representing the current
|
|
// configuration of this RTCPeerConnection object. The returned object is a
|
|
// copy and direct mutation on it will not take affect until SetConfiguration
|
|
// has been called with RTCConfiguration passed as its only argument.
|
|
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-getconfiguration
|
|
func (pc *RTCPeerConnection) GetConfiguration() RTCConfiguration {
|
|
return pc.configuration
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// --- FIXME - BELOW CODE NEEDS REVIEW/CLEANUP
|
|
// ------------------------------------------------------------------------
|
|
|
|
// CreateOffer starts the RTCPeerConnection and generates the localDescription
|
|
func (pc *RTCPeerConnection) CreateOffer(options *RTCOfferOptions) (RTCSessionDescription, error) {
|
|
useIdentity := pc.idpLoginURL != nil
|
|
if options != nil {
|
|
return RTCSessionDescription{}, errors.Errorf("TODO handle options")
|
|
} else if useIdentity {
|
|
return RTCSessionDescription{}, errors.Errorf("TODO handle identity provider")
|
|
} else if pc.isClosed {
|
|
return RTCSessionDescription{}, &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
|
|
}
|
|
|
|
d := sdp.NewJSEPSessionDescription(useIdentity)
|
|
pc.addFingerprint(d)
|
|
|
|
iceParams, err := pc.iceGatherer.GetLocalParameters()
|
|
if err != nil {
|
|
return RTCSessionDescription{}, err
|
|
}
|
|
|
|
candidates, err := pc.iceGatherer.GetLocalCandidates()
|
|
if err != nil {
|
|
return RTCSessionDescription{}, err
|
|
}
|
|
|
|
bundleValue := "BUNDLE"
|
|
|
|
if pc.addRTPMediaSection(d, RTCRtpCodecTypeAudio, "audio", iceParams, RTCRtpTransceiverDirectionSendrecv, candidates, sdp.ConnectionRoleActpass) {
|
|
bundleValue += " audio"
|
|
}
|
|
if pc.addRTPMediaSection(d, RTCRtpCodecTypeVideo, "video", iceParams, RTCRtpTransceiverDirectionSendrecv, candidates, sdp.ConnectionRoleActpass) {
|
|
bundleValue += " video"
|
|
}
|
|
|
|
pc.addDataMediaSection(d, "data", iceParams, candidates, sdp.ConnectionRoleActpass)
|
|
d = d.WithValueAttribute(sdp.AttrKeyGroup, bundleValue+" data")
|
|
|
|
for _, m := range d.MediaDescriptions {
|
|
m.WithPropertyAttribute("setup:actpass")
|
|
}
|
|
|
|
desc := RTCSessionDescription{
|
|
Type: RTCSdpTypeOffer,
|
|
Sdp: d.Marshal(),
|
|
parsed: d,
|
|
}
|
|
pc.lastOffer = desc.Sdp
|
|
|
|
// FIXME: This doesn't follow the JS API spec, but removing it
|
|
// would mean our examples and existing code have to change
|
|
if err := pc.SetLocalDescription(desc); err != nil {
|
|
return RTCSessionDescription{}, err
|
|
}
|
|
return desc, nil
|
|
}
|
|
|
|
func (pc *RTCPeerConnection) createIceGatherer() (*RTCIceGatherer, error) {
|
|
g, err := NewRTCIceGatherer(RTCIceGatherOptions{
|
|
ICEServers: pc.configuration.IceServers,
|
|
// TODO: GatherPolicy
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return g, nil
|
|
}
|
|
|
|
func (pc *RTCPeerConnection) gather() error {
|
|
return pc.iceGatherer.Gather()
|
|
}
|
|
|
|
func (pc *RTCPeerConnection) createICETransport() *RTCIceTransport {
|
|
t := NewRTCIceTransport(pc.iceGatherer)
|
|
|
|
t.OnConnectionStateChange(func(state RTCIceTransportState) {
|
|
// We convert the state back to the ICE state to not brake the
|
|
// existing public API at this point.
|
|
iceState := state.toICE()
|
|
pc.iceStateChange(iceState)
|
|
})
|
|
|
|
return t
|
|
}
|
|
|
|
func (pc *RTCPeerConnection) createDTLSTransport() (*RTCDtlsTransport, error) {
|
|
dtlsTransport, err := NewRTCDtlsTransport(pc.iceTransport, pc.configuration.Certificates)
|
|
return dtlsTransport, err
|
|
}
|
|
|
|
// CreateAnswer starts the RTCPeerConnection and generates the localDescription
|
|
func (pc *RTCPeerConnection) CreateAnswer(options *RTCAnswerOptions) (RTCSessionDescription, error) {
|
|
useIdentity := pc.idpLoginURL != nil
|
|
if options != nil {
|
|
return RTCSessionDescription{}, errors.Errorf("TODO handle options")
|
|
} else if useIdentity {
|
|
return RTCSessionDescription{}, errors.Errorf("TODO handle identity provider")
|
|
} else if pc.isClosed {
|
|
return RTCSessionDescription{}, &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
|
|
}
|
|
|
|
iceParams, err := pc.iceGatherer.GetLocalParameters()
|
|
if err != nil {
|
|
return RTCSessionDescription{}, err
|
|
}
|
|
|
|
candidates, err := pc.iceGatherer.GetLocalCandidates()
|
|
if err != nil {
|
|
return RTCSessionDescription{}, err
|
|
}
|
|
|
|
d := sdp.NewJSEPSessionDescription(useIdentity)
|
|
pc.addFingerprint(d)
|
|
|
|
bundleValue := "BUNDLE"
|
|
for _, remoteMedia := range pc.RemoteDescription().parsed.MediaDescriptions {
|
|
// TODO @trivigy better SDP parser
|
|
var peerDirection RTCRtpTransceiverDirection
|
|
midValue := ""
|
|
for _, a := range remoteMedia.Attributes {
|
|
if strings.HasPrefix(*a.String(), "mid") {
|
|
midValue = (*a.String())[len("mid:"):]
|
|
} else if strings.HasPrefix(*a.String(), "sendrecv") {
|
|
peerDirection = RTCRtpTransceiverDirectionSendrecv
|
|
} else if strings.HasPrefix(*a.String(), "sendonly") {
|
|
peerDirection = RTCRtpTransceiverDirectionSendonly
|
|
} else if strings.HasPrefix(*a.String(), "recvonly") {
|
|
peerDirection = RTCRtpTransceiverDirectionRecvonly
|
|
}
|
|
}
|
|
|
|
appendBundle := func() {
|
|
bundleValue += " " + midValue
|
|
}
|
|
|
|
if strings.HasPrefix(*remoteMedia.MediaName.String(), "audio") {
|
|
if pc.addRTPMediaSection(d, RTCRtpCodecTypeAudio, midValue, iceParams, peerDirection, candidates, sdp.ConnectionRoleActive) {
|
|
appendBundle()
|
|
}
|
|
} else if strings.HasPrefix(*remoteMedia.MediaName.String(), "video") {
|
|
if pc.addRTPMediaSection(d, RTCRtpCodecTypeVideo, midValue, iceParams, peerDirection, candidates, sdp.ConnectionRoleActive) {
|
|
appendBundle()
|
|
}
|
|
} else if strings.HasPrefix(*remoteMedia.MediaName.String(), "application") {
|
|
pc.addDataMediaSection(d, midValue, iceParams, candidates, sdp.ConnectionRoleActive)
|
|
appendBundle()
|
|
}
|
|
}
|
|
|
|
d = d.WithValueAttribute(sdp.AttrKeyGroup, bundleValue)
|
|
|
|
desc := RTCSessionDescription{
|
|
Type: RTCSdpTypeAnswer,
|
|
Sdp: d.Marshal(),
|
|
parsed: d,
|
|
}
|
|
pc.lastAnswer = desc.Sdp
|
|
|
|
// FIXME: This doesn't follow the JS API spec, but removing it
|
|
// would mean our examples and existing code have to change
|
|
if err := pc.SetLocalDescription(desc); err != nil {
|
|
return RTCSessionDescription{}, err
|
|
}
|
|
return desc, nil
|
|
}
|
|
|
|
// 4.4.1.6 Set the RTCSessionDescription
|
|
func (pc *RTCPeerConnection) setDescription(sd *RTCSessionDescription, op rtcStateChangeOp) error {
|
|
if pc.isClosed {
|
|
return &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
|
|
}
|
|
|
|
cur := pc.SignalingState
|
|
setLocal := rtcStateChangeOpSetLocal
|
|
setRemote := rtcStateChangeOpSetRemote
|
|
newSdpDoesNotMatchOffer := &rtcerr.InvalidModificationError{Err: errors.New("New sdp does not match previous offer")}
|
|
newSdpDoesNotMatchAnswer := &rtcerr.InvalidModificationError{Err: errors.New("New sdp does not match previous answer")}
|
|
|
|
var nextState RTCSignalingState
|
|
var err error
|
|
switch op {
|
|
case setLocal:
|
|
switch sd.Type {
|
|
// stable->SetLocal(offer)->have-local-offer
|
|
case RTCSdpTypeOffer:
|
|
if sd.Sdp != pc.lastOffer {
|
|
return newSdpDoesNotMatchOffer
|
|
}
|
|
nextState, err = checkNextSignalingState(cur, RTCSignalingStateHaveLocalOffer, setLocal, sd.Type)
|
|
if err == nil {
|
|
pc.PendingLocalDescription = sd
|
|
}
|
|
// have-remote-offer->SetLocal(answer)->stable
|
|
// have-local-pranswer->SetLocal(answer)->stable
|
|
case RTCSdpTypeAnswer:
|
|
if sd.Sdp != pc.lastAnswer {
|
|
return newSdpDoesNotMatchAnswer
|
|
}
|
|
nextState, err = checkNextSignalingState(cur, RTCSignalingStateStable, setLocal, sd.Type)
|
|
if err == nil {
|
|
pc.CurrentLocalDescription = sd
|
|
pc.CurrentRemoteDescription = pc.PendingRemoteDescription
|
|
pc.PendingRemoteDescription = nil
|
|
pc.PendingLocalDescription = nil
|
|
}
|
|
case RTCSdpTypeRollback:
|
|
nextState, err = checkNextSignalingState(cur, RTCSignalingStateStable, setLocal, sd.Type)
|
|
if err == nil {
|
|
pc.PendingLocalDescription = nil
|
|
}
|
|
// have-remote-offer->SetLocal(pranswer)->have-local-pranswer
|
|
case RTCSdpTypePranswer:
|
|
if sd.Sdp != pc.lastAnswer {
|
|
return newSdpDoesNotMatchAnswer
|
|
}
|
|
nextState, err = checkNextSignalingState(cur, RTCSignalingStateHaveLocalPranswer, setLocal, sd.Type)
|
|
if err == nil {
|
|
pc.PendingLocalDescription = sd
|
|
}
|
|
default:
|
|
return &rtcerr.OperationError{Err: fmt.Errorf("Invalid state change op: %s(%s)", op, sd.Type)}
|
|
}
|
|
case setRemote:
|
|
switch sd.Type {
|
|
// stable->SetRemote(offer)->have-remote-offer
|
|
case RTCSdpTypeOffer:
|
|
nextState, err = checkNextSignalingState(cur, RTCSignalingStateHaveRemoteOffer, setRemote, sd.Type)
|
|
if err == nil {
|
|
pc.PendingRemoteDescription = sd
|
|
}
|
|
// have-local-offer->SetRemote(answer)->stable
|
|
// have-remote-pranswer->SetRemote(answer)->stable
|
|
case RTCSdpTypeAnswer:
|
|
nextState, err = checkNextSignalingState(cur, RTCSignalingStateStable, setRemote, sd.Type)
|
|
if err == nil {
|
|
pc.CurrentRemoteDescription = sd
|
|
pc.CurrentLocalDescription = pc.PendingLocalDescription
|
|
pc.PendingRemoteDescription = nil
|
|
pc.PendingLocalDescription = nil
|
|
}
|
|
case RTCSdpTypeRollback:
|
|
nextState, err = checkNextSignalingState(cur, RTCSignalingStateStable, setRemote, sd.Type)
|
|
if err == nil {
|
|
pc.PendingRemoteDescription = nil
|
|
}
|
|
// have-local-offer->SetRemote(pranswer)->have-remote-pranswer
|
|
case RTCSdpTypePranswer:
|
|
nextState, err = checkNextSignalingState(cur, RTCSignalingStateHaveRemotePranswer, setRemote, sd.Type)
|
|
if err == nil {
|
|
pc.PendingRemoteDescription = sd
|
|
}
|
|
default:
|
|
return &rtcerr.OperationError{Err: fmt.Errorf("Invalid state change op: %s(%s)", op, sd.Type)}
|
|
}
|
|
default:
|
|
return &rtcerr.OperationError{Err: fmt.Errorf("Unhandled state change op: %q", op)}
|
|
}
|
|
|
|
if err == nil {
|
|
pc.SignalingState = nextState
|
|
pc.onSignalingStateChange(nextState)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// SetLocalDescription sets the SessionDescription of the local peer
|
|
func (pc *RTCPeerConnection) SetLocalDescription(desc RTCSessionDescription) error {
|
|
if pc.isClosed {
|
|
return &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
|
|
}
|
|
|
|
// JSEP 5.4
|
|
if desc.Sdp == "" {
|
|
switch desc.Type {
|
|
case RTCSdpTypeAnswer, RTCSdpTypePranswer:
|
|
desc.Sdp = pc.lastAnswer
|
|
case RTCSdpTypeOffer:
|
|
desc.Sdp = pc.lastOffer
|
|
default:
|
|
return &rtcerr.InvalidModificationError{
|
|
Err: fmt.Errorf("Invalid SDP type supplied to SetLocalDescription(): %s", desc.Type),
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Initiate ICE candidate gathering?
|
|
|
|
desc.parsed = &sdp.SessionDescription{}
|
|
if err := desc.parsed.Unmarshal(desc.Sdp); err != nil {
|
|
return err
|
|
}
|
|
return pc.setDescription(&desc, rtcStateChangeOpSetLocal)
|
|
}
|
|
|
|
// LocalDescription returns PendingLocalDescription if it is not null and
|
|
// otherwise it returns CurrentLocalDescription. This property is used to
|
|
// determine if setLocalDescription has already been called.
|
|
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-localdescription
|
|
func (pc *RTCPeerConnection) LocalDescription() *RTCSessionDescription {
|
|
if pc.PendingLocalDescription != nil {
|
|
return pc.PendingLocalDescription
|
|
}
|
|
return pc.CurrentLocalDescription
|
|
}
|
|
|
|
// SetRemoteDescription sets the SessionDescription of the remote peer
|
|
func (pc *RTCPeerConnection) SetRemoteDescription(desc RTCSessionDescription) error {
|
|
// FIXME: Remove this when renegotiation is supported
|
|
if pc.CurrentRemoteDescription != nil {
|
|
return errors.Errorf("remoteDescription is already defined, SetRemoteDescription can only be called once")
|
|
}
|
|
if pc.isClosed {
|
|
return &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
|
|
}
|
|
|
|
desc.parsed = &sdp.SessionDescription{}
|
|
if err := desc.parsed.Unmarshal(desc.Sdp); err != nil {
|
|
return err
|
|
}
|
|
if err := pc.setDescription(&desc, rtcStateChangeOpSetRemote); err != nil {
|
|
return err
|
|
}
|
|
|
|
weOffer := true
|
|
remoteUfrag := ""
|
|
remotePwd := ""
|
|
if desc.Type == RTCSdpTypeOffer {
|
|
weOffer = false
|
|
}
|
|
|
|
// Create the ice transport
|
|
iceTransport := pc.createICETransport()
|
|
pc.iceTransport = iceTransport
|
|
|
|
for _, m := range pc.RemoteDescription().parsed.MediaDescriptions {
|
|
for _, a := range m.Attributes {
|
|
if a.IsICECandidate() {
|
|
sdpCandidate, err := a.ToICECandidate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
candidate, err := newRTCIceCandidateFromSDP(sdpCandidate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = pc.iceTransport.AddRemoteCandidate(candidate); err != nil {
|
|
return err
|
|
}
|
|
} else if strings.HasPrefix(*a.String(), "ice-ufrag") {
|
|
remoteUfrag = (*a.String())[len("ice-ufrag:"):]
|
|
} else if strings.HasPrefix(*a.String(), "ice-pwd") {
|
|
remotePwd = (*a.String())[len("ice-pwd:"):]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create the DTLS transport
|
|
dtlsTransport, err := pc.createDTLSTransport()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pc.dtlsTransport = dtlsTransport
|
|
|
|
fingerprint, ok := desc.parsed.Attribute("fingerprint")
|
|
if !ok {
|
|
fingerprint, ok = desc.parsed.MediaDescriptions[0].Attribute("fingerprint")
|
|
if !ok {
|
|
return errors.New("could not find fingerprint")
|
|
}
|
|
}
|
|
var fingerprintHash string
|
|
parts := strings.Split(fingerprint, " ")
|
|
if len(parts) != 2 {
|
|
return errors.New("invalid fingerprint")
|
|
}
|
|
fingerprint = parts[1]
|
|
fingerprintHash = parts[0]
|
|
|
|
// Create the SCTP transport
|
|
sctp := NewRTCSctpTransport(pc.dtlsTransport)
|
|
pc.sctpTransport = sctp
|
|
|
|
// Wire up the on datachannel handler
|
|
sctp.OnDataChannel(func(d *RTCDataChannel) {
|
|
pc.RLock()
|
|
hdlr := pc.onDataChannelHandler
|
|
pc.RUnlock()
|
|
if hdlr != nil {
|
|
hdlr(d)
|
|
}
|
|
})
|
|
|
|
go func() {
|
|
// Star the networking in a new routine since it will block until
|
|
// the connection is actually established.
|
|
|
|
// Start the ice transport
|
|
iceRole := RTCIceRoleControlled
|
|
if weOffer {
|
|
iceRole = RTCIceRoleControlling
|
|
}
|
|
err := pc.iceTransport.Start(
|
|
pc.iceGatherer,
|
|
RTCIceParameters{
|
|
UsernameFragment: remoteUfrag,
|
|
Password: remotePwd,
|
|
IceLite: false,
|
|
},
|
|
&iceRole,
|
|
)
|
|
|
|
if err != nil {
|
|
// TODO: Handle error
|
|
pcLog.Warnf("Failed to start manager: %s", err)
|
|
return
|
|
}
|
|
|
|
// Start the dtls transport
|
|
err = pc.dtlsTransport.Start(RTCDtlsParameters{
|
|
Role: RTCDtlsRoleAuto,
|
|
Fingerprints: []RTCDtlsFingerprint{{Algorithm: fingerprintHash, Value: fingerprint}},
|
|
})
|
|
if err != nil {
|
|
// TODO: Handle error
|
|
pcLog.Warnf("Failed to start manager: %s", err)
|
|
return
|
|
}
|
|
|
|
pc.srtpEndpoint = pc.iceTransport.mux.NewEndpoint(mux.MatchSRTP)
|
|
pc.srtcpEndpoint = pc.iceTransport.mux.NewEndpoint(mux.MatchSRTCP)
|
|
|
|
err = pc.startSRTP(weOffer)
|
|
if err != nil {
|
|
// TODO: Handle error
|
|
pcLog.Warnf("Failed to start RTP: %s", err)
|
|
return
|
|
}
|
|
|
|
go pc.acceptSRTP()
|
|
go pc.drainSRTCP()
|
|
|
|
// Start sctp
|
|
err = pc.sctpTransport.Start(RTCSctpCapabilities{
|
|
MaxMessageSize: 0,
|
|
})
|
|
if err != nil {
|
|
// TODO: Handle error
|
|
pcLog.Warnf("Failed to start SCTP: %s", err)
|
|
return
|
|
}
|
|
|
|
// Open data channels that where created before signaling
|
|
pc.openDataChannels()
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// openDataChannels opens the existing data channels
|
|
func (pc *RTCPeerConnection) openDataChannels() {
|
|
for _, d := range pc.dataChannels {
|
|
err := d.open(pc.sctpTransport)
|
|
if err != nil {
|
|
pcLog.Warnf("failed to open data channel: %s", err)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// startSRTP initializes all the cryptographic context needed for encrypted RTP
|
|
func (pc *RTCPeerConnection) startSRTP(isOffer bool) error {
|
|
keyingMaterial, err := pc.dtlsTransport.conn.ExportKeyingMaterial([]byte("EXTRACTOR-dtls_srtp"), nil, (srtpMasterKeyLen*2)+(srtpMasterKeySaltLen*2))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
offset := 0
|
|
clientWriteKey := append([]byte{}, keyingMaterial[offset:offset+srtpMasterKeyLen]...)
|
|
offset += srtpMasterKeyLen
|
|
|
|
serverWriteKey := append([]byte{}, keyingMaterial[offset:offset+srtpMasterKeyLen]...)
|
|
offset += srtpMasterKeyLen
|
|
|
|
clientWriteKey = append(clientWriteKey, keyingMaterial[offset:offset+srtpMasterKeySaltLen]...)
|
|
offset += srtpMasterKeySaltLen
|
|
|
|
serverWriteKey = append(serverWriteKey, keyingMaterial[offset:offset+srtpMasterKeySaltLen]...)
|
|
|
|
if isOffer {
|
|
err = pc.srtpSession.Start(
|
|
serverWriteKey[0:16], serverWriteKey[16:],
|
|
clientWriteKey[0:16], clientWriteKey[16:],
|
|
srtp.ProtectionProfileAes128CmHmacSha1_80, pc.srtpEndpoint,
|
|
)
|
|
|
|
if err == nil {
|
|
err = pc.srtcpSession.Start(
|
|
serverWriteKey[0:16], serverWriteKey[16:],
|
|
clientWriteKey[0:16], clientWriteKey[16:],
|
|
srtp.ProtectionProfileAes128CmHmacSha1_80, pc.srtcpEndpoint,
|
|
)
|
|
}
|
|
} else {
|
|
err = pc.srtpSession.Start(
|
|
clientWriteKey[0:16], clientWriteKey[16:],
|
|
serverWriteKey[0:16], serverWriteKey[16:],
|
|
srtp.ProtectionProfileAes128CmHmacSha1_80, pc.srtpEndpoint,
|
|
)
|
|
|
|
if err == nil {
|
|
err = pc.srtcpSession.Start(
|
|
clientWriteKey[0:16], clientWriteKey[16:],
|
|
serverWriteKey[0:16], serverWriteKey[16:],
|
|
srtp.ProtectionProfileAes128CmHmacSha1_80, pc.srtcpEndpoint,
|
|
)
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// drainSRTCP pulls and discards RTCP packets that don't match any SRTP
|
|
// These could be sent to the user, but right now we don't provide an API
|
|
// to distribute orphaned RTCP messages. This is needed to make sure we don't block
|
|
// and provides useful debugging messages
|
|
func (pc *RTCPeerConnection) drainSRTCP() {
|
|
for {
|
|
r, ssrc, err := pc.srtcpSession.AcceptStream()
|
|
if err != nil {
|
|
pcLog.Warnf("Failed to accept RTCP %v \n", err)
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
var rtcpPacket rtcp.Packet
|
|
|
|
for {
|
|
rtcpBuf := make([]byte, receiveMTU)
|
|
i, err := r.Read(rtcpBuf)
|
|
if err != nil {
|
|
pcLog.Warnf("Failed to read, RTCTrack done for: %v %d \n", err, ssrc)
|
|
return
|
|
}
|
|
|
|
rtcpPacket, _, err = rtcp.Unmarshal(rtcpBuf[:i])
|
|
if err != nil {
|
|
pcLog.Warnf("Failed to unmarshal RTCP packet, discarding: %v \n", err)
|
|
continue
|
|
}
|
|
pcLog.Debugf("got RTCP: %+v", rtcpPacket)
|
|
}
|
|
}()
|
|
|
|
}
|
|
}
|
|
|
|
// TODO: Move to RTCRTpSender?
|
|
func (pc *RTCPeerConnection) acceptSRTP() {
|
|
for {
|
|
r, ssrc, err := pc.srtpSession.AcceptStream()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
_, h, err := r.ReadRTP(make([]byte, receiveMTU))
|
|
if err != nil {
|
|
pcLog.Warnf("Failed to read, ignoring AcceptStream: %v \n", err)
|
|
continue
|
|
}
|
|
|
|
rtpChannel, rtcpChannel, err := pc.generateChannel(h)
|
|
if err != nil {
|
|
pcLog.Warnf("Failed to create output channels, ignoring AcceptStream: %v \n", err)
|
|
continue
|
|
}
|
|
|
|
// RTP
|
|
go func() {
|
|
for {
|
|
rtpBuf := make([]byte, receiveMTU)
|
|
rtpLen, h, err := r.ReadRTP(rtpBuf)
|
|
if err != nil {
|
|
pcLog.Warnf("Failed to read, RTCTrack done for: %v %d \n", err, ssrc)
|
|
return
|
|
}
|
|
|
|
select {
|
|
case rtpChannel <- &rtp.Packet{Header: *h, Raw: rtpBuf[:rtpLen], Payload: rtpBuf[h.PayloadOffset:rtpLen]}:
|
|
default:
|
|
}
|
|
}
|
|
}()
|
|
|
|
// RTCP
|
|
go func() {
|
|
readStream, err := pc.srtcpSession.OpenReadStream(ssrc)
|
|
if err != nil {
|
|
pcLog.Warnf("Failed to open RTCP ReadStream, RTCTrack done for: %v %d \n", err, ssrc)
|
|
return
|
|
}
|
|
|
|
for {
|
|
var (
|
|
rtcpPacket rtcp.Packet
|
|
rtcpLen int
|
|
)
|
|
rtcpBuf := make([]byte, receiveMTU)
|
|
rtcpLen, err = readStream.Read(rtcpBuf)
|
|
if err != nil {
|
|
pcLog.Warnf("Failed to read, RTCTrack done for: %v %d \n", err, ssrc)
|
|
return
|
|
}
|
|
|
|
rtcpPacket, _, err = rtcp.Unmarshal(rtcpBuf[:rtcpLen])
|
|
if err != nil {
|
|
pcLog.Warnf("Failed to unmarshal RTCP packet, discarding: %v \n", err)
|
|
continue
|
|
}
|
|
select {
|
|
case rtcpChannel <- rtcpPacket:
|
|
default:
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// RemoteDescription returns PendingRemoteDescription if it is not null and
|
|
// otherwise it returns CurrentRemoteDescription. This property is used to
|
|
// determine if setRemoteDescription has already been called.
|
|
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-remotedescription
|
|
func (pc *RTCPeerConnection) RemoteDescription() *RTCSessionDescription {
|
|
if pc.PendingRemoteDescription != nil {
|
|
return pc.PendingRemoteDescription
|
|
}
|
|
return pc.CurrentRemoteDescription
|
|
}
|
|
|
|
// AddIceCandidate accepts an ICE candidate string and adds it
|
|
// to the existing set of candidates
|
|
func (pc *RTCPeerConnection) AddIceCandidate(s string) error {
|
|
// TODO: AddIceCandidate should take RTCIceCandidateInit
|
|
s = strings.TrimPrefix(s, "candidate:")
|
|
|
|
attribute := sdp.NewAttribute("candidate", s)
|
|
sdpCandidate, err := attribute.ToICECandidate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
candidate, err := newRTCIceCandidateFromSDP(sdpCandidate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return pc.iceTransport.AddRemoteCandidate(candidate)
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// --- FIXME - BELOW CODE NEEDS RE-ORGANIZATION - https://w3c.github.io/webrtc-pc/#rtp-media-api
|
|
// ------------------------------------------------------------------------
|
|
|
|
// GetSenders returns the RTCRtpSender that are currently attached to this RTCPeerConnection
|
|
func (pc *RTCPeerConnection) GetSenders() []RTCRtpSender {
|
|
result := make([]RTCRtpSender, len(pc.rtpTransceivers))
|
|
for i, tranceiver := range pc.rtpTransceivers {
|
|
result[i] = *tranceiver.Sender
|
|
}
|
|
return result
|
|
}
|
|
|
|
// GetReceivers returns the RTCRtpReceivers that are currently attached to this RTCPeerConnection
|
|
func (pc *RTCPeerConnection) GetReceivers() []RTCRtpReceiver {
|
|
result := make([]RTCRtpReceiver, len(pc.rtpTransceivers))
|
|
for i, tranceiver := range pc.rtpTransceivers {
|
|
result[i] = *tranceiver.Receiver
|
|
}
|
|
return result
|
|
}
|
|
|
|
// GetTransceivers returns the RTCRtpTransceiver that are currently attached to this RTCPeerConnection
|
|
func (pc *RTCPeerConnection) GetTransceivers() []RTCRtpTransceiver {
|
|
result := make([]RTCRtpTransceiver, len(pc.rtpTransceivers))
|
|
for i, tranceiver := range pc.rtpTransceivers {
|
|
result[i] = *tranceiver
|
|
}
|
|
return result
|
|
}
|
|
|
|
// AddTrack adds a RTCTrack to the RTCPeerConnection
|
|
func (pc *RTCPeerConnection) AddTrack(track *RTCTrack) (*RTCRtpSender, error) {
|
|
if pc.isClosed {
|
|
return nil, &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
|
|
}
|
|
for _, transceiver := range pc.rtpTransceivers {
|
|
if transceiver.Sender.Track == nil {
|
|
continue
|
|
}
|
|
if track.ID == transceiver.Sender.Track.ID {
|
|
return nil, &rtcerr.InvalidAccessError{Err: ErrExistingTrack}
|
|
}
|
|
}
|
|
var transceiver *RTCRtpTransceiver
|
|
for _, t := range pc.rtpTransceivers {
|
|
if !t.stopped &&
|
|
// t.Sender == nil && // TODO: check that the sender has never sent
|
|
t.Sender.Track == nil &&
|
|
t.Receiver.Track != nil &&
|
|
t.Receiver.Track.Kind == track.Kind {
|
|
transceiver = t
|
|
break
|
|
}
|
|
}
|
|
if transceiver != nil {
|
|
if err := transceiver.setSendingTrack(track); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
var receiver *RTCRtpReceiver
|
|
sender := newRTCRtpSender(track)
|
|
transceiver = pc.newRTCRtpTransceiver(
|
|
receiver,
|
|
sender,
|
|
RTCRtpTransceiverDirectionSendonly,
|
|
)
|
|
}
|
|
|
|
transceiver.Mid = track.Kind.String() // TODO: Mid generation
|
|
|
|
return transceiver.Sender, nil
|
|
}
|
|
|
|
// func (pc *RTCPeerConnection) RemoveTrack() {
|
|
// panic("not implemented yet") // FIXME NOT-IMPLEMENTED nolint
|
|
// }
|
|
|
|
// func (pc *RTCPeerConnection) AddTransceiver() RTCRtpTransceiver {
|
|
// panic("not implemented yet") // FIXME NOT-IMPLEMENTED nolint
|
|
// }
|
|
|
|
// ------------------------------------------------------------------------
|
|
// --- FIXME - BELOW CODE NEEDS RE-ORGANIZATION - https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api
|
|
// ------------------------------------------------------------------------
|
|
|
|
// CreateDataChannel creates a new RTCDataChannel object with the given label
|
|
// and optional RTCDataChannelInit used to configure properties of the
|
|
// underlying channel such as data reliability.
|
|
func (pc *RTCPeerConnection) CreateDataChannel(label string, options *RTCDataChannelInit) (*RTCDataChannel, error) {
|
|
// https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #2)
|
|
if pc.isClosed {
|
|
return nil, &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
|
|
}
|
|
|
|
// TODO: Add additional options once implemented. RTCDataChannelInit
|
|
// implements all options. RTCDataChannelParameters implements the
|
|
// options that actually have an effect at this point.
|
|
params := &RTCDataChannelParameters{
|
|
Label: label,
|
|
}
|
|
|
|
// https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #19)
|
|
if options != nil {
|
|
if options.ID == nil {
|
|
var err error
|
|
if params.ID, err = pc.generateDataChannelID(true); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
params.ID = *options.ID
|
|
}
|
|
}
|
|
|
|
// TODO: Re-enable validation of the parameters once they are implemented.
|
|
/*
|
|
// https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #3)
|
|
// Some variables defined explicitly despite their implicit zero values to
|
|
// allow better readability to understand what is happening. Additionally,
|
|
// some members are set to a non zero value default due to the default
|
|
// definitions in https://w3c.github.io/webrtc-pc/#dom-rtcdatachannelinit
|
|
// which are later overwriten by the options if any were specified.
|
|
channel := RTCDataChannel{
|
|
rtcPeerConnection: pc,
|
|
// https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #4)
|
|
Label: label,
|
|
Ordered: true,
|
|
MaxPacketLifeTime: nil,
|
|
MaxRetransmits: nil,
|
|
Protocol: "",
|
|
Negotiated: false,
|
|
ID: nil,
|
|
Priority: RTCPriorityTypeLow,
|
|
// https://w3c.github.io/webrtc-pc/#dfn-create-an-rtcdatachannel (Step #3)
|
|
BufferedAmount: 0,
|
|
}
|
|
|
|
if options != nil {
|
|
// https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #7)
|
|
if options.MaxPacketLifeTime != nil {
|
|
channel.MaxPacketLifeTime = options.MaxPacketLifeTime
|
|
}
|
|
|
|
// https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #8)
|
|
if options.MaxRetransmits != nil {
|
|
channel.MaxRetransmits = options.MaxRetransmits
|
|
}
|
|
|
|
// https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #9)
|
|
if options.Ordered != nil {
|
|
channel.Ordered = *options.Ordered
|
|
}
|
|
|
|
// https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #10)
|
|
if options.Protocol != nil {
|
|
channel.Protocol = *options.Protocol
|
|
}
|
|
|
|
// https://w3c.github.io/webrtc-pc/#peer-to-peer-da ta-api (Step #12)
|
|
if options.Negotiated != nil {
|
|
channel.Negotiated = *options.Negotiated
|
|
}
|
|
|
|
// https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #13)
|
|
if options.ID != nil && channel.Negotiated {
|
|
channel.ID = options.ID
|
|
}
|
|
|
|
// https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #15)
|
|
if options.Priority != nil {
|
|
channel.Priority = *options.Priority
|
|
}
|
|
}
|
|
|
|
// https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #11)
|
|
if len(channel.Protocol) > 65535 {
|
|
return nil, &rtcerr.TypeError{Err: ErrStringSizeLimit}
|
|
}
|
|
|
|
// https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #14)
|
|
if channel.Negotiated && channel.ID == nil {
|
|
return nil, &rtcerr.TypeError{Err: ErrNegotiatedWithoutID}
|
|
}
|
|
|
|
// https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #16)
|
|
if channel.MaxPacketLifeTime != nil && channel.MaxRetransmits != nil {
|
|
return nil, &rtcerr.TypeError{Err: ErrRetransmitsOrPacketLifeTime}
|
|
}
|
|
|
|
// FIXME https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-createdatachannel (Step #17)
|
|
|
|
|
|
// // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #18)
|
|
if *channel.ID > 65534 {
|
|
return nil, &rtcerr.TypeError{Err: ErrMaxDataChannelID}
|
|
}
|
|
|
|
if pc.sctpTransport.State == RTCSctpTransportStateConnected &&
|
|
*channel.ID >= *pc.sctpTransport.MaxChannels {
|
|
return nil, &rtcerr.OperationError{Err: ErrMaxDataChannelID}
|
|
}
|
|
*/
|
|
|
|
d, err := pc.api.newRTCDataChannel(params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Remember datachannel
|
|
pc.dataChannels[params.ID] = d
|
|
|
|
// Open if networking already started
|
|
if pc.sctpTransport != nil {
|
|
err = d.open(pc.sctpTransport)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return d, nil
|
|
}
|
|
|
|
func (pc *RTCPeerConnection) generateDataChannelID(client bool) (uint16, error) {
|
|
var id uint16
|
|
if !client {
|
|
id++
|
|
}
|
|
|
|
max := sctpMaxChannels
|
|
if pc.sctpTransport != nil {
|
|
max = *pc.sctpTransport.MaxChannels
|
|
}
|
|
|
|
for ; id < max-1; id += 2 {
|
|
_, ok := pc.dataChannels[id]
|
|
if !ok {
|
|
return id, nil
|
|
}
|
|
}
|
|
return 0, &rtcerr.OperationError{Err: ErrMaxDataChannelID}
|
|
}
|
|
|
|
// SetIdentityProvider is used to configure an identity provider to generate identity assertions
|
|
func (pc *RTCPeerConnection) SetIdentityProvider(provider string) error {
|
|
return errors.Errorf("TODO SetIdentityProvider")
|
|
}
|
|
|
|
// SendRTCP sends a user provided RTCP packet to the connected peer
|
|
// If no peer is connected the packet is discarded
|
|
func (pc *RTCPeerConnection) SendRTCP(pkt rtcp.Packet) error {
|
|
raw, err := pkt.Marshal()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
writeStream, err := pc.srtcpSession.OpenWriteStream()
|
|
if err != nil {
|
|
return fmt.Errorf("SendRTCP failed to open WriteStream: %v", err)
|
|
}
|
|
|
|
if _, err := writeStream.Write(raw); err != nil {
|
|
return fmt.Errorf("SendRTCP failed to write: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Close ends the RTCPeerConnection
|
|
func (pc *RTCPeerConnection) Close() error {
|
|
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #2)
|
|
if pc.isClosed {
|
|
return nil
|
|
}
|
|
|
|
for _, t := range pc.rtpTransceivers {
|
|
if track := t.Sender.Track; track != nil {
|
|
if track.isRawRTP {
|
|
close(track.RawRTP)
|
|
} else {
|
|
close(track.Samples)
|
|
}
|
|
}
|
|
}
|
|
|
|
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #3)
|
|
pc.isClosed = true
|
|
|
|
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #4)
|
|
pc.SignalingState = RTCSignalingStateClosed
|
|
|
|
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #11)
|
|
// pc.IceConnectionState = RTCIceConnectionStateClosed
|
|
pc.iceStateChange(ice.ConnectionStateClosed) // FIXME REMOVE
|
|
|
|
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #12)
|
|
pc.ConnectionState = RTCPeerConnectionStateClosed
|
|
|
|
// Try closing everything and collect the errors
|
|
var closeErrs []error
|
|
|
|
// Shutdown strategy:
|
|
// 1. All Conn close by closing their underlying Conn.
|
|
// 2. A Mux stops this chain. It won't close the underlying
|
|
// Conn if one of the endpoints is closed down. To
|
|
// continue the chain the Mux has to be closed.
|
|
|
|
if err := pc.srtpSession.Close(); err != nil {
|
|
closeErrs = append(closeErrs, err)
|
|
}
|
|
|
|
if err := pc.srtcpSession.Close(); err != nil {
|
|
closeErrs = append(closeErrs, err)
|
|
}
|
|
|
|
if pc.sctpTransport != nil {
|
|
if err := pc.sctpTransport.Stop(); err != nil {
|
|
closeErrs = append(closeErrs, err)
|
|
}
|
|
}
|
|
|
|
// TODO: Close DTLS?
|
|
|
|
if pc.iceTransport != nil {
|
|
if err := pc.iceTransport.Stop(); err != nil {
|
|
closeErrs = append(closeErrs, err)
|
|
}
|
|
}
|
|
|
|
// TODO: Figure out stopping ICE transport & Gatherer independently.
|
|
// pc.iceGatherer()
|
|
|
|
return flattenErrs(closeErrs)
|
|
}
|
|
|
|
func flattenErrs(errs []error) error {
|
|
var errstrings []string
|
|
|
|
for _, err := range errs {
|
|
if err != nil {
|
|
errstrings = append(errstrings, err.Error())
|
|
}
|
|
}
|
|
|
|
if len(errstrings) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf(strings.Join(errstrings, "\n"))
|
|
}
|
|
|
|
/* Everything below is private */
|
|
func (pc *RTCPeerConnection) generateChannel(h *rtp.Header) (chan *rtp.Packet, chan rtcp.Packet, error) {
|
|
pc.RLock()
|
|
if pc.onTrackHandler == nil {
|
|
pc.RUnlock()
|
|
return nil, nil, fmt.Errorf("OnTrack unset, unable to handle incoming")
|
|
}
|
|
pc.RUnlock()
|
|
|
|
sdpCodec, err := pc.CurrentLocalDescription.parsed.GetCodecForPayloadType(h.PayloadType)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("no codec could be found in RemoteDescription for payloadType %d", h.PayloadType)
|
|
}
|
|
|
|
codec, err := pc.api.mediaEngine.getCodecSDP(sdpCodec)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("codec %s in not registered", sdpCodec)
|
|
}
|
|
|
|
rtpTransport := make(chan *rtp.Packet, 15)
|
|
rtcpTransport := make(chan rtcp.Packet, 15)
|
|
|
|
track := &RTCTrack{
|
|
PayloadType: h.PayloadType,
|
|
Kind: codec.Type,
|
|
ID: "0", // TODO extract from remoteDescription
|
|
Label: "", // TODO extract from remoteDescription
|
|
Ssrc: h.SSRC,
|
|
Codec: codec,
|
|
Packets: rtpTransport,
|
|
RTCPPackets: rtcpTransport,
|
|
}
|
|
|
|
// TODO: Register the receiving Track
|
|
pc.onTrack(track)
|
|
return rtpTransport, rtcpTransport, nil
|
|
}
|
|
|
|
func (pc *RTCPeerConnection) iceStateChange(newState ice.ConnectionState) {
|
|
pc.Lock()
|
|
pc.IceConnectionState = newState
|
|
pc.Unlock()
|
|
|
|
pc.onICEConnectionStateChange(newState)
|
|
}
|
|
|
|
func localDirection(weSend bool, peerDirection RTCRtpTransceiverDirection) RTCRtpTransceiverDirection {
|
|
theySend := (peerDirection == RTCRtpTransceiverDirectionSendrecv || peerDirection == RTCRtpTransceiverDirectionSendonly)
|
|
if weSend && theySend {
|
|
return RTCRtpTransceiverDirectionSendrecv
|
|
} else if weSend && !theySend {
|
|
return RTCRtpTransceiverDirectionSendonly
|
|
} else if !weSend && theySend {
|
|
return RTCRtpTransceiverDirectionRecvonly
|
|
}
|
|
|
|
return RTCRtpTransceiverDirectionInactive
|
|
}
|
|
|
|
func (pc *RTCPeerConnection) addFingerprint(d *sdp.SessionDescription) {
|
|
// TODO: Handle multiple certificates
|
|
for _, fingerprint := range pc.configuration.Certificates[0].GetFingerprints() {
|
|
d.WithFingerprint(fingerprint.Algorithm, strings.ToUpper(fingerprint.Value))
|
|
}
|
|
}
|
|
|
|
func (pc *RTCPeerConnection) addRTPMediaSection(d *sdp.SessionDescription, codecType RTCRtpCodecType, midValue string, iceParams RTCIceParameters, peerDirection RTCRtpTransceiverDirection, candidates []RTCIceCandidate, dtlsRole sdp.ConnectionRole) bool {
|
|
if codecs := pc.api.mediaEngine.getCodecsByKind(codecType); len(codecs) == 0 {
|
|
return false
|
|
}
|
|
media := sdp.NewJSEPMediaDescription(codecType.String(), []string{}).
|
|
WithValueAttribute(sdp.AttrKeyConnectionSetup, dtlsRole.String()). // TODO: Support other connection types
|
|
WithValueAttribute(sdp.AttrKeyMID, midValue).
|
|
WithICECredentials(iceParams.UsernameFragment, iceParams.Password).
|
|
WithPropertyAttribute(sdp.AttrKeyRtcpMux). // TODO: support RTCP fallback
|
|
WithPropertyAttribute(sdp.AttrKeyRtcpRsize) // TODO: Support Reduced-Size RTCP?
|
|
|
|
for _, codec := range pc.api.mediaEngine.getCodecsByKind(codecType) {
|
|
media.WithCodec(codec.PayloadType, codec.Name, codec.ClockRate, codec.Channels, codec.SdpFmtpLine)
|
|
}
|
|
|
|
weSend := false
|
|
for _, transceiver := range pc.rtpTransceivers {
|
|
if transceiver.Sender == nil ||
|
|
transceiver.Sender.Track == nil ||
|
|
transceiver.Sender.Track.Kind != codecType {
|
|
continue
|
|
}
|
|
weSend = true
|
|
track := transceiver.Sender.Track
|
|
media = media.WithMediaSource(track.Ssrc, track.Label /* cname */, track.Label /* streamLabel */, track.Label)
|
|
}
|
|
media = media.WithPropertyAttribute(localDirection(weSend, peerDirection).String())
|
|
|
|
for _, c := range candidates {
|
|
sdpCandidate := c.toSDP()
|
|
sdpCandidate.ExtensionAttributes = append(sdpCandidate.ExtensionAttributes, sdp.ICECandidateAttribute{Key: "generation", Value: "0"})
|
|
sdpCandidate.Component = 1
|
|
media.WithICECandidate(sdpCandidate)
|
|
sdpCandidate.Component = 2
|
|
media.WithICECandidate(sdpCandidate)
|
|
}
|
|
media.WithPropertyAttribute("end-of-candidates")
|
|
d.WithMedia(media)
|
|
return true
|
|
}
|
|
|
|
func (pc *RTCPeerConnection) addDataMediaSection(d *sdp.SessionDescription, midValue string, iceParams RTCIceParameters, candidates []RTCIceCandidate, dtlsRole sdp.ConnectionRole) {
|
|
media := (&sdp.MediaDescription{
|
|
MediaName: sdp.MediaName{
|
|
Media: "application",
|
|
Port: sdp.RangedPort{Value: 9},
|
|
Protos: []string{"DTLS", "SCTP"},
|
|
Formats: []string{"5000"},
|
|
},
|
|
ConnectionInformation: &sdp.ConnectionInformation{
|
|
NetworkType: "IN",
|
|
AddressType: "IP4",
|
|
Address: &sdp.Address{
|
|
IP: net.ParseIP("0.0.0.0"),
|
|
},
|
|
},
|
|
}).
|
|
WithValueAttribute(sdp.AttrKeyConnectionSetup, dtlsRole.String()). // TODO: Support other connection types
|
|
WithValueAttribute(sdp.AttrKeyMID, midValue).
|
|
WithPropertyAttribute(RTCRtpTransceiverDirectionSendrecv.String()).
|
|
WithPropertyAttribute("sctpmap:5000 webrtc-datachannel 1024").
|
|
WithICECredentials(iceParams.UsernameFragment, iceParams.Password)
|
|
|
|
for _, c := range candidates {
|
|
sdpCandidate := c.toSDP()
|
|
sdpCandidate.ExtensionAttributes = append(sdpCandidate.ExtensionAttributes, sdp.ICECandidateAttribute{Key: "generation", Value: "0"})
|
|
sdpCandidate.Component = 1
|
|
media.WithICECandidate(sdpCandidate)
|
|
sdpCandidate.Component = 2
|
|
media.WithICECandidate(sdpCandidate)
|
|
}
|
|
media.WithPropertyAttribute("end-of-candidates")
|
|
|
|
d.WithMedia(media)
|
|
}
|
|
|
|
// TODO RTCRtpSender
|
|
func (pc *RTCPeerConnection) sendRTP(packet *rtp.Packet) {
|
|
writeStream, err := pc.srtpSession.OpenWriteStream()
|
|
if err != nil {
|
|
pcLog.Warnf("SendRTP failed to open WriteStream: %v", err)
|
|
return
|
|
}
|
|
|
|
if _, err := writeStream.WriteRTP(&packet.Header, packet.Payload); err != nil {
|
|
pcLog.Warnf("SendRTP failed to write: %v", err)
|
|
}
|
|
}
|
|
|
|
func (pc *RTCPeerConnection) newRTCTrack(payloadType uint8, ssrc uint32, id, label string) (*RTCTrack, error) {
|
|
codec, err := pc.api.mediaEngine.getCodec(payloadType)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if codec.Payloader == nil {
|
|
return nil, errors.New("codec payloader not set")
|
|
}
|
|
|
|
trackInput := make(chan media.RTCSample, 15) // Is the buffering needed?
|
|
rawPackets := make(chan *rtp.Packet, 15) // Is the buffering needed?
|
|
rtcpPackets := make(chan rtcp.Packet, 15) // Is the buffering needed?
|
|
isRawRTP := false
|
|
|
|
if ssrc == 0 {
|
|
buf := make([]byte, 4)
|
|
_, err = rand.Read(buf)
|
|
if err != nil {
|
|
return nil, errors.New("failed to generate random value")
|
|
}
|
|
ssrc = binary.LittleEndian.Uint32(buf)
|
|
|
|
go func() {
|
|
packetizer := rtp.NewPacketizer(
|
|
1400,
|
|
payloadType,
|
|
ssrc,
|
|
codec.Payloader,
|
|
rtp.NewRandomSequencer(),
|
|
codec.ClockRate,
|
|
)
|
|
|
|
for {
|
|
in, ok := <-trackInput
|
|
if !ok {
|
|
return
|
|
}
|
|
packets := packetizer.Packetize(in.Data, in.Samples)
|
|
for _, p := range packets {
|
|
pc.sendRTP(p)
|
|
}
|
|
}
|
|
}()
|
|
close(rawPackets)
|
|
} else {
|
|
// If SSRC is not 0, then we are working with an established RTP stream
|
|
// and need to accept raw RTP packets for forwarding.
|
|
isRawRTP = true
|
|
go func() {
|
|
for {
|
|
p, ok := <-rawPackets
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
pc.sendRTP(p)
|
|
}
|
|
}()
|
|
close(trackInput)
|
|
}
|
|
|
|
t := &RTCTrack{
|
|
isRawRTP: isRawRTP,
|
|
PayloadType: payloadType,
|
|
Kind: codec.Type,
|
|
ID: id,
|
|
Label: label,
|
|
Ssrc: ssrc,
|
|
Codec: codec,
|
|
RTCPPackets: rtcpPackets,
|
|
Samples: trackInput,
|
|
RawRTP: rawPackets,
|
|
}
|
|
|
|
// Inbound RTCP
|
|
go func() {
|
|
readStream, err := pc.srtcpSession.OpenReadStream(ssrc)
|
|
if err != nil {
|
|
pcLog.Warnf("Failed to open RTCP ReadStream, RTCTrack done for: %v %d \n", err, ssrc)
|
|
return
|
|
}
|
|
|
|
var rtcpPacket rtcp.Packet
|
|
|
|
for {
|
|
rtcpBuf := make([]byte, receiveMTU)
|
|
i, err := readStream.Read(rtcpBuf)
|
|
if err != nil {
|
|
pcLog.Warnf("Failed to read, RTCTrack done for: %v %d \n", err, ssrc)
|
|
return
|
|
}
|
|
|
|
rtcpPacket, _, err = rtcp.Unmarshal(rtcpBuf[:i])
|
|
if err != nil {
|
|
pcLog.Warnf("Failed to unmarshal RTCP packet, discarding: %v \n", err)
|
|
continue
|
|
}
|
|
select {
|
|
case rtcpPackets <- rtcpPacket:
|
|
default:
|
|
}
|
|
}
|
|
}()
|
|
|
|
return t, nil
|
|
}
|
|
|
|
// NewRawRTPTrack initializes a new *RTCTrack configured to accept raw *rtp.Packet
|
|
//
|
|
// NB: If the source RTP stream is being broadcast to multiple tracks, each track
|
|
// must receive its own copies of the source packets in order to avoid packet corruption.
|
|
func (pc *RTCPeerConnection) NewRawRTPTrack(payloadType uint8, ssrc uint32, id, label string) (*RTCTrack, error) {
|
|
if ssrc == 0 {
|
|
return nil, errors.New("SSRC supplied to NewRawRTPTrack() must be non-zero")
|
|
}
|
|
return pc.newRTCTrack(payloadType, ssrc, id, label)
|
|
}
|
|
|
|
// NewRTCSampleTrack initializes a new *RTCTrack configured to accept media.RTCSample
|
|
func (pc *RTCPeerConnection) NewRTCSampleTrack(payloadType uint8, id, label string) (*RTCTrack, error) {
|
|
return pc.newRTCTrack(payloadType, 0, id, label)
|
|
}
|
|
|
|
// NewRTCTrack is used to create a new RTCTrack
|
|
//
|
|
// Deprecated: Use NewRTCSampleTrack() instead
|
|
func (pc *RTCPeerConnection) NewRTCTrack(payloadType uint8, id, label string) (*RTCTrack, error) {
|
|
return pc.NewRTCSampleTrack(payloadType, id, label)
|
|
}
|
|
|
|
func (pc *RTCPeerConnection) newRTCRtpTransceiver(
|
|
receiver *RTCRtpReceiver,
|
|
sender *RTCRtpSender,
|
|
direction RTCRtpTransceiverDirection,
|
|
) *RTCRtpTransceiver {
|
|
|
|
t := &RTCRtpTransceiver{
|
|
Receiver: receiver,
|
|
Sender: sender,
|
|
Direction: direction,
|
|
}
|
|
pc.rtpTransceivers = append(pc.rtpTransceivers, t)
|
|
return t
|
|
}
|