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 offer­answer 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 }