mirror of
				https://github.com/pion/webrtc.git
				synced 2025-10-31 02:36:46 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			408 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			408 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package webrtc
 | ||
| 
 | ||
| import (
 | ||
| 	"fmt"
 | ||
| 	"math/rand"
 | ||
| 	"net"
 | ||
| 	"time"
 | ||
| 
 | ||
| 	"github.com/pions/pkg/stun"
 | ||
| 	"github.com/pions/webrtc/internal/network"
 | ||
| 	"github.com/pions/webrtc/internal/sdp"
 | ||
| 	"github.com/pions/webrtc/pkg/ice"
 | ||
| 	"github.com/pkg/errors"
 | ||
| )
 | ||
| 
 | ||
| /*
 | ||
|                       setRemote(OFFER)               setLocal(PRANSWER)
 | ||
|                           /-----\                               /-----\
 | ||
|                           |     |                               |     |
 | ||
|                           v     |                               v     |
 | ||
|            +---------------+    |                +---------------+    |
 | ||
|            |               |----/                |               |----/
 | ||
|            |  have-        | setLocal(PRANSWER)  | have-         |
 | ||
|            |  remote-offer |------------------- >| local-pranswer|
 | ||
|            |               |                     |               |
 | ||
|            |               |                     |               |
 | ||
|            +---------------+                     +---------------+
 | ||
|                 ^   |                                   |
 | ||
|                 |   | setLocal(ANSWER)                  |
 | ||
|   setRemote(OFFER)  |                                   |
 | ||
|                 |   V                  setLocal(ANSWER) |
 | ||
|            +---------------+                            |
 | ||
|            |               |                            |
 | ||
|            |               |<---------------------------+
 | ||
|            |    stable     |
 | ||
|            |               |<---------------------------+
 | ||
|            |               |                            |
 | ||
|            +---------------+          setRemote(ANSWER) |
 | ||
|                 ^   |                                   |
 | ||
|                 |   | setLocal(OFFER)                   |
 | ||
|   setRemote(ANSWER) |                                   |
 | ||
|                 |   V                                   |
 | ||
|            +---------------+                     +---------------+
 | ||
|            |               |                     |               |
 | ||
|            |  have-        | setRemote(PRANSWER) |have-          |
 | ||
|            |  local-offer  |------------------- >|remote-pranswer|
 | ||
|            |               |                     |               |
 | ||
|            |               |----\                |               |----\
 | ||
|            +---------------+    |                +---------------+    |
 | ||
|                           ^     |                               ^     |
 | ||
|                           |     |                               |     |
 | ||
|                           \-----/                               \-----/
 | ||
|                       setLocal(OFFER)               setRemote(PRANSWER)
 | ||
| */
 | ||
| 
 | ||
| // RTCSignalingState indicates the state of the offer/answer process
 | ||
| type RTCSignalingState int
 | ||
| 
 | ||
| const (
 | ||
| 
 | ||
| 	// RTCSignalingStateStable indicates there is no offeranswer exchange in progress.
 | ||
| 	RTCSignalingStateStable RTCSignalingState = iota + 1
 | ||
| 
 | ||
| 	// RTCSignalingStateHaveLocalOffer indicates A local description, of type "offer", has been successfully applied.
 | ||
| 	RTCSignalingStateHaveLocalOffer
 | ||
| 
 | ||
| 	// RTCSignalingStateHaveRemoteOffer indicates A remote description, of type "offer", has been successfully applied.
 | ||
| 	RTCSignalingStateHaveRemoteOffer
 | ||
| 
 | ||
| 	// RTCSignalingStateHaveLocalPranswer indicates A remote description of type "offer" has been successfully applied and a local description of type "pranswer" has been successfully applied.
 | ||
| 	RTCSignalingStateHaveLocalPranswer
 | ||
| 
 | ||
| 	// RTCSignalingStateHaveRemotePranswer indicates A local description of type "offer" has been successfully applied and a remote description of type "pranswer" has been successfully applied.
 | ||
| 	RTCSignalingStateHaveRemotePranswer
 | ||
| 
 | ||
| 	// RTCSignalingStateClosed indicates The RTCPeerConnection has been closed.
 | ||
| 	RTCSignalingStateClosed
 | ||
| )
 | ||
| 
 | ||
| func (t RTCSignalingState) String() string {
 | ||
| 	switch t {
 | ||
| 	case RTCSignalingStateStable:
 | ||
| 		return "stable"
 | ||
| 	case RTCSignalingStateHaveLocalOffer:
 | ||
| 		return "have-local-offer"
 | ||
| 	case RTCSignalingStateHaveRemoteOffer:
 | ||
| 		return "have-remote-offer"
 | ||
| 	case RTCSignalingStateHaveLocalPranswer:
 | ||
| 		return "have-local-pranswer"
 | ||
| 	case RTCSignalingStateHaveRemotePranswer:
 | ||
| 		return "have-remote-pranswer"
 | ||
| 	case RTCSignalingStateClosed:
 | ||
| 		return "closed"
 | ||
| 	default:
 | ||
| 		return "Unknown"
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // RTCSdpType describes the type of an RTCSessionDescription
 | ||
| type RTCSdpType int
 | ||
| 
 | ||
| const (
 | ||
| 
 | ||
| 	// RTCSdpTypeOffer indicates that a description MUST be treated as an SDP offer.
 | ||
| 	RTCSdpTypeOffer RTCSdpType = iota + 1
 | ||
| 
 | ||
| 	// RTCSdpTypePranswer indicates that a description MUST be treated as an SDP answer, but not a final answer.
 | ||
| 	RTCSdpTypePranswer
 | ||
| 
 | ||
| 	// RTCSdpTypeAnswer indicates that a description MUST be treated as an SDP final answer, and the offer-answer exchange MUST be considered complete.
 | ||
| 	RTCSdpTypeAnswer
 | ||
| 
 | ||
| 	// RTCSdpTypeRollback indicates that a description MUST be treated as canceling the current SDP negotiation and moving the SDP offer and answer back to what it was in the previous stable state.
 | ||
| 	RTCSdpTypeRollback
 | ||
| )
 | ||
| 
 | ||
| func (t RTCSdpType) String() string {
 | ||
| 	switch t {
 | ||
| 	case RTCSdpTypeOffer:
 | ||
| 		return "offer"
 | ||
| 	case RTCSdpTypePranswer:
 | ||
| 		return "pranswer"
 | ||
| 	case RTCSdpTypeAnswer:
 | ||
| 		return "answer"
 | ||
| 	case RTCSdpTypeRollback:
 | ||
| 		return "rollback"
 | ||
| 	default:
 | ||
| 		return "Unknown"
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // RTCSessionDescription is used to expose local and remote session descriptions.
 | ||
| type RTCSessionDescription struct {
 | ||
| 	Type RTCSdpType
 | ||
| 	Sdp  string
 | ||
| }
 | ||
| 
 | ||
| // SetRemoteDescription sets the SessionDescription of the remote peer
 | ||
| func (r *RTCPeerConnection) SetRemoteDescription(desc RTCSessionDescription) error {
 | ||
| 	if r.remoteDescription != nil {
 | ||
| 		return errors.Errorf("remoteDescription is already defined, SetRemoteDescription can only be called once")
 | ||
| 	}
 | ||
| 
 | ||
| 	r.currentRemoteDescription = &desc
 | ||
| 
 | ||
| 	r.remoteDescription = &sdp.SessionDescription{}
 | ||
| 
 | ||
| 	return r.remoteDescription.Unmarshal(desc.Sdp)
 | ||
| }
 | ||
| 
 | ||
| // RTCOfferOptions describes the options used to control the offer creation process
 | ||
| type RTCOfferOptions struct {
 | ||
| 	VoiceActivityDetection bool
 | ||
| 	ICERestart             bool
 | ||
| }
 | ||
| 
 | ||
| // CreateOffer starts the RTCPeerConnection and generates the localDescription
 | ||
| func (r *RTCPeerConnection) CreateOffer(options *RTCOfferOptions) (RTCSessionDescription, error) {
 | ||
| 	if options != nil {
 | ||
| 		panic("TODO handle options")
 | ||
| 	}
 | ||
| 	if r.IsClosed {
 | ||
| 		return RTCSessionDescription{}, &InvalidStateError{Err: ErrConnectionClosed}
 | ||
| 	}
 | ||
| 	useIdentity := r.idpLoginURL != nil
 | ||
| 	if useIdentity {
 | ||
| 		panic("TODO handle identity provider")
 | ||
| 	}
 | ||
| 
 | ||
| 	d := sdp.NewJSEPSessionDescription(
 | ||
| 		r.tlscfg.Fingerprint(),
 | ||
| 		useIdentity).
 | ||
| 		WithValueAttribute(sdp.AttrKeyGroup, "BUNDLE audio video") // TODO: Support BUNDLE
 | ||
| 
 | ||
| 	var streamlabels string
 | ||
| 	for _, tranceiver := range r.rtpTransceivers {
 | ||
| 		if tranceiver.Sender == nil ||
 | ||
| 			tranceiver.Sender.Track == nil {
 | ||
| 			continue
 | ||
| 		}
 | ||
| 		track := tranceiver.Sender.Track
 | ||
| 		cname := "pion"      // TODO: Support RTP streams synchronisation
 | ||
| 		steamlabel := "pion" // TODO: Support steam labels
 | ||
| 		codec, err := r.mediaEngine.getCodec(track.PayloadType)
 | ||
| 		if err != nil {
 | ||
| 			return RTCSessionDescription{}, err
 | ||
| 		}
 | ||
| 		media := sdp.NewJSEPMediaDescription(track.Kind.String(), []string{}).
 | ||
| 			WithValueAttribute(sdp.AttrKeyConnectionSetup, sdp.ConnectionRoleActive.String()). // TODO: Support other connection types
 | ||
| 			WithValueAttribute(sdp.AttrKeyMID, tranceiver.Mid).
 | ||
| 			WithPropertyAttribute(tranceiver.Direction.String()).
 | ||
| 			WithICECredentials(r.iceAgent.Ufrag, r.iceAgent.Pwd).
 | ||
| 			WithPropertyAttribute(sdp.AttrKeyICELite).   // TODO: get ICE type from ICE Agent
 | ||
| 			WithPropertyAttribute(sdp.AttrKeyRtcpMux).   // TODO: support RTCP fallback
 | ||
| 			WithPropertyAttribute(sdp.AttrKeyRtcpRsize). // TODO: Support Reduced-Size RTCP?
 | ||
| 			WithCodec(
 | ||
| 				codec.PayloadType,
 | ||
| 				codec.Name,
 | ||
| 				codec.ClockRate,
 | ||
| 				codec.Channels,
 | ||
| 				codec.SdpFmtpLine,
 | ||
| 			).
 | ||
| 			WithMediaSource(track.Ssrc, cname, steamlabel, track.Label)
 | ||
| 		err = r.addICECandidates(media)
 | ||
| 		if err != nil {
 | ||
| 			return RTCSessionDescription{}, err
 | ||
| 		}
 | ||
| 		streamlabels = streamlabels + " " + steamlabel
 | ||
| 
 | ||
| 		d.WithMedia(media)
 | ||
| 	}
 | ||
| 
 | ||
| 	d.WithValueAttribute(sdp.AttrKeyMsidSemantic, " "+sdp.SemanticTokenWebRTCMediaStreams+streamlabels)
 | ||
| 
 | ||
| 	return RTCSessionDescription{
 | ||
| 		Type: RTCSdpTypeOffer,
 | ||
| 		Sdp:  d.Marshal(),
 | ||
| 	}, nil
 | ||
| }
 | ||
| 
 | ||
| // RTCAnswerOptions describes the options used to control the answer creation process
 | ||
| type RTCAnswerOptions struct {
 | ||
| 	VoiceActivityDetection bool
 | ||
| }
 | ||
| 
 | ||
| // CreateAnswer starts the RTCPeerConnection and generates the localDescription
 | ||
| func (r *RTCPeerConnection) CreateAnswer(options *RTCOfferOptions) (RTCSessionDescription, error) {
 | ||
| 	if options != nil {
 | ||
| 		panic("TODO handle options")
 | ||
| 	}
 | ||
| 	if r.IsClosed {
 | ||
| 		return RTCSessionDescription{}, &InvalidStateError{Err: ErrConnectionClosed}
 | ||
| 	}
 | ||
| 	useIdentity := r.idpLoginURL != nil
 | ||
| 	if useIdentity {
 | ||
| 		panic("TODO handle identity provider")
 | ||
| 	}
 | ||
| 
 | ||
| 	d := sdp.NewJSEPSessionDescription(
 | ||
| 		r.tlscfg.Fingerprint(),
 | ||
| 		useIdentity).
 | ||
| 		WithValueAttribute(sdp.AttrKeyGroup, "BUNDLE audio video") // TODO: Support BUNDLE
 | ||
| 
 | ||
| 	// TODO: Actually reply based on the offer (#21)
 | ||
| 	// TODO: Media lines should be in the same order as the offer
 | ||
| 
 | ||
| 	audioStreamLabels, err := r.addAnswerMedia(d, RTCRtpCodecTypeAudio)
 | ||
| 	if err != nil {
 | ||
| 		return RTCSessionDescription{}, err
 | ||
| 	}
 | ||
| 	videoStreamLabels, err := r.addAnswerMedia(d, RTCRtpCodecTypeVideo)
 | ||
| 	if err != nil {
 | ||
| 		return RTCSessionDescription{}, err
 | ||
| 	}
 | ||
| 	d.WithValueAttribute(sdp.AttrKeyMsidSemantic, " "+sdp.SemanticTokenWebRTCMediaStreams+audioStreamLabels+videoStreamLabels)
 | ||
| 
 | ||
| 	return RTCSessionDescription{
 | ||
| 		Type: RTCSdpTypeAnswer,
 | ||
| 		Sdp:  d.Marshal(),
 | ||
| 	}, nil
 | ||
| }
 | ||
| 
 | ||
| func (r *RTCPeerConnection) addAnswerMedia(d *sdp.SessionDescription, codecType RTCRtpCodecType) (string, error) {
 | ||
| 	added := false
 | ||
| 
 | ||
| 	var streamlabels string
 | ||
| 	for _, tranceiver := range r.rtpTransceivers {
 | ||
| 		if tranceiver.Sender == nil ||
 | ||
| 			tranceiver.Sender.Track == nil ||
 | ||
| 			tranceiver.Sender.Track.Kind != codecType {
 | ||
| 			continue
 | ||
| 		}
 | ||
| 		track := tranceiver.Sender.Track
 | ||
| 		cname := track.Label      // TODO: Support RTP streams synchronisation
 | ||
| 		steamlabel := track.Label // TODO: Support steam labels
 | ||
| 		codec, err := r.mediaEngine.getCodec(track.PayloadType)
 | ||
| 		if err != nil {
 | ||
| 			return "", err
 | ||
| 		}
 | ||
| 		media := sdp.NewJSEPMediaDescription(track.Kind.String(), []string{}).
 | ||
| 			WithValueAttribute(sdp.AttrKeyConnectionSetup, sdp.ConnectionRoleActive.String()). // TODO: Support other connection types
 | ||
| 			WithValueAttribute(sdp.AttrKeyMID, tranceiver.Mid).
 | ||
| 			WithPropertyAttribute(tranceiver.Direction.String()).
 | ||
| 			WithICECredentials(r.iceAgent.Ufrag, r.iceAgent.Pwd).
 | ||
| 			WithPropertyAttribute(sdp.AttrKeyICELite).   // TODO: get ICE type from ICE Agent
 | ||
| 			WithPropertyAttribute(sdp.AttrKeyRtcpMux).   // TODO: support RTCP fallback
 | ||
| 			WithPropertyAttribute(sdp.AttrKeyRtcpRsize). // TODO: Support Reduced-Size RTCP?
 | ||
| 			WithCodec(
 | ||
| 				codec.PayloadType,
 | ||
| 				codec.Name,
 | ||
| 				codec.ClockRate,
 | ||
| 				codec.Channels,
 | ||
| 				codec.SdpFmtpLine,
 | ||
| 			).
 | ||
| 			WithMediaSource(track.Ssrc, cname, steamlabel, track.Label)
 | ||
| 
 | ||
| 		err = r.addICECandidates(media)
 | ||
| 		if err != nil {
 | ||
| 			return "", err
 | ||
| 		}
 | ||
| 		d.WithMedia(media)
 | ||
| 		streamlabels = streamlabels + " " + steamlabel
 | ||
| 		added = true
 | ||
| 	}
 | ||
| 
 | ||
| 	if !added {
 | ||
| 		// Add media line to advertise capabilities
 | ||
| 		media := sdp.NewJSEPMediaDescription(codecType.String(), []string{}).
 | ||
| 			WithValueAttribute(sdp.AttrKeyConnectionSetup, sdp.ConnectionRoleActive.String()). // TODO: Support other connection types
 | ||
| 			WithValueAttribute(sdp.AttrKeyMID, codecType.String()).
 | ||
| 			WithPropertyAttribute(RTCRtpTransceiverDirectionSendrecv.String()).
 | ||
| 			WithICECredentials(r.iceAgent.Ufrag, r.iceAgent.Pwd). // TODO: get credendials form ICE agent
 | ||
| 			WithPropertyAttribute(sdp.AttrKeyICELite).            // TODO: get ICE type from ICE Agent (#23)
 | ||
| 			WithPropertyAttribute(sdp.AttrKeyRtcpMux).            // TODO: support RTCP fallback
 | ||
| 			WithPropertyAttribute(sdp.AttrKeyRtcpRsize)           // TODO: Support Reduced-Size RTCP?
 | ||
| 
 | ||
| 		for _, codec := range r.mediaEngine.getCodecsByKind(codecType) {
 | ||
| 			media.WithCodec(
 | ||
| 				codec.PayloadType,
 | ||
| 				codec.Name,
 | ||
| 				codec.ClockRate,
 | ||
| 				codec.Channels,
 | ||
| 				codec.SdpFmtpLine,
 | ||
| 			)
 | ||
| 		}
 | ||
| 
 | ||
| 		err := r.addICECandidates(media)
 | ||
| 		if err != nil {
 | ||
| 			return "", err
 | ||
| 		}
 | ||
| 		d.WithMedia(media)
 | ||
| 	}
 | ||
| 
 | ||
| 	return streamlabels, nil
 | ||
| }
 | ||
| 
 | ||
| func (r *RTCPeerConnection) addICECandidates(d *sdp.MediaDescription) error {
 | ||
| 	r.portsLock.Lock()
 | ||
| 	defer r.portsLock.Unlock()
 | ||
| 
 | ||
| 	// TODO: Move gathering to ice.Agent
 | ||
| 
 | ||
| 	basePriority := uint16(rand.Uint32() & (1<<16 - 1))
 | ||
| 
 | ||
| 	for _, c := range ice.HostInterfaces() {
 | ||
| 		port, err := network.NewPort(c+":0", []byte(r.iceAgent.Pwd), r.tlscfg, r.generateChannel, r.iceStateChange)
 | ||
| 		if err != nil {
 | ||
| 			return err
 | ||
| 		}
 | ||
| 
 | ||
| 		d.WithCandidate(1, "udp", basePriority, c, port.ListeningAddr.Port, "host")
 | ||
| 
 | ||
| 		basePriority = basePriority + 1
 | ||
| 		r.ports = append(r.ports, port)
 | ||
| 	}
 | ||
| 
 | ||
| 	for _, servers := range r.iceAgent.Servers {
 | ||
| 		for _, server := range servers {
 | ||
| 			if server.Type != ice.ServerTypeSTUN {
 | ||
| 				continue
 | ||
| 			}
 | ||
| 			// TODO Do we want the timeout to be configurable?
 | ||
| 			proto := server.TransportType.String()
 | ||
| 			client, err := stun.NewClient(proto, fmt.Sprintf("%s:%d", server.Host, server.Port), time.Second*5)
 | ||
| 			if err != nil {
 | ||
| 				return errors.Wrapf(err, "Failed to create STUN client")
 | ||
| 			}
 | ||
| 			localAddr, ok := client.LocalAddr().(*net.UDPAddr)
 | ||
| 			if !ok {
 | ||
| 				return errors.Errorf("Failed to cast STUN client to UDPAddr")
 | ||
| 			}
 | ||
| 
 | ||
| 			resp, err := client.Request()
 | ||
| 			if err != nil {
 | ||
| 				return errors.Wrapf(err, "Failed to make STUN request")
 | ||
| 			}
 | ||
| 
 | ||
| 			if err := client.Close(); err != nil {
 | ||
| 				return errors.Wrapf(err, "Failed to close STUN client")
 | ||
| 			}
 | ||
| 
 | ||
| 			attr, ok := resp.GetOneAttribute(stun.AttrXORMappedAddress)
 | ||
| 			if !ok {
 | ||
| 				return errors.Errorf("Got respond from STUN server that did not contain XORAddress")
 | ||
| 			}
 | ||
| 
 | ||
| 			var addr stun.XorAddress
 | ||
| 			if err := addr.Unpack(resp, attr); err != nil {
 | ||
| 				return errors.Wrapf(err, "Failed to unpack STUN XorAddress response")
 | ||
| 			}
 | ||
| 
 | ||
| 			port, err := network.NewPort(fmt.Sprintf("0.0.0.0:%d", localAddr.Port), []byte(r.iceAgent.Pwd), r.tlscfg, r.generateChannel, r.iceStateChange)
 | ||
| 			if err != nil {
 | ||
| 				return errors.Wrapf(err, "Failed to build network/port")
 | ||
| 			}
 | ||
| 
 | ||
| 			d.WithCandidate(1, proto, basePriority, addr.IP.String(), localAddr.Port, "srflx")
 | ||
| 
 | ||
| 			basePriority = basePriority + 1
 | ||
| 			r.ports = append(r.ports, port)
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	d.WithPropertyAttribute("end-of-candidates") // TODO: Support full trickle-ice
 | ||
| 
 | ||
| 	return nil
 | ||
| }
 | 
