package telemost import ( "fmt" "sync" "github.com/pion/rtp" "github.com/pion/rtp/codecs" "github.com/pion/webrtc/v4" "github.com/sagernet/sing-box/transport/call/common" "github.com/sagernet/sing-box/transport/call/tunnel" "github.com/sagernet/sing/common/logger" ) type SFURelay struct { pubPC *webrtc.PeerConnection subPC *webrtc.PeerConnection pubRemoteSet bool subRemoteSet bool pubPending []webrtc.ICECandidateInit subPending []webrtc.ICECandidateInit mu sync.Mutex logger logger.ContextLogger sampleTrack *webrtc.TrackLocalStaticSample tun *tunnel.VP8DataTunnel obf *tunnel.TunnelObfuscator OnConnected func(*tunnel.VP8DataTunnel) OnPubReady func() OnPeerRestart func() OnPubICE func(*webrtc.ICECandidate) OnSubICE func(*webrtc.ICECandidate) readBufSize int } func (r *SFURelay) SetObfuscator(o *tunnel.TunnelObfuscator) { r.obf = o } func NewSFURelay(logger logger.ContextLogger) *SFURelay { return &SFURelay{logger: logger} } func (r *SFURelay) Init(iceServers []webrtc.ICEServer) error { config := webrtc.Configuration{ICEServers: iceServers} pubPC, err := NewPeerConnection(config) if err != nil { return err } r.pubPC = pubPC sampleTrack, _ := webrtc.NewTrackLocalStaticSample( webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "tunnel-video", ) r.sampleTrack = sampleTrack audioTrack, _ := webrtc.NewTrackLocalStaticRTP( webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "tunnel-audio", ) pubPC.AddTransceiverFromTrack(audioTrack, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly}) pubPC.AddTransceiverFromTrack(r.sampleTrack, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly}) pubPC.OnICECandidate(func(cand *webrtc.ICECandidate) { if cand == nil || r.OnPubICE == nil { return } r.OnPubICE(cand) }) pubPC.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { r.logger.Debug(fmt.Sprintf("[pub] connection state: %s", state.String())) if state == webrtc.PeerConnectionStateConnected { if r.tun == nil { r.logger.Debug("[relay] starting VP8 publish tunnel on pub PC connected") r.tun = tunnel.NewVP8DataTunnel(r.sampleTrack, r.obf, r.logger) r.tun.Start(0, 0) if r.OnConnected != nil { r.OnConnected(r.tun) } } if r.OnPubReady != nil { r.OnPubReady() } } }) subPC, err := NewPeerConnection(config) if err != nil { pubPC.Close() return err } r.subPC = subPC subPC.OnICECandidate(func(cand *webrtc.ICECandidate) { if cand == nil || r.OnSubICE == nil { return } r.OnSubICE(cand) }) subPC.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { r.logger.Debug(fmt.Sprintf("[sub] connection state: %s", state.String())) }) subPC.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { r.logger.Debug(fmt.Sprintf("[sub] remote track: %s", track.Codec().MimeType)) go r.readTrack(track) }) r.logger.Debug(fmt.Sprintf("[relay] pub+sub PCs created (%d ICE servers)", len(iceServers))) return nil } func (r *SFURelay) CreatePubOffer() (webrtc.SessionDescription, error) { offer, err := r.pubPC.CreateOffer(nil) if err != nil { return offer, err } if err := r.pubPC.SetLocalDescription(offer); err != nil { return offer, err } offer.SDP = MungeSDPAddVideoContent(offer.SDP) return offer, nil } func (r *SFURelay) SetPubAnswer(sdp string) error { r.mu.Lock() defer r.mu.Unlock() err := r.pubPC.SetRemoteDescription(webrtc.SessionDescription{ Type: webrtc.SDPTypeAnswer, SDP: sdp, }) if err != nil { return err } r.pubRemoteSet = true for _, cand := range r.pubPending { r.pubPC.AddICECandidate(cand) } r.pubPending = nil return nil } func (r *SFURelay) SetSubOffer(sdp string) (webrtc.SessionDescription, error) { r.mu.Lock() defer r.mu.Unlock() err := r.subPC.SetRemoteDescription(webrtc.SessionDescription{ Type: webrtc.SDPTypeOffer, SDP: sdp, }) if err != nil { return webrtc.SessionDescription{}, err } r.subRemoteSet = true for _, cand := range r.subPending { r.subPC.AddICECandidate(cand) } r.subPending = nil answer, err := r.subPC.CreateAnswer(nil) if err != nil { return answer, err } r.subPC.SetLocalDescription(answer) return answer, nil } func (r *SFURelay) AddPubICECandidate(cand webrtc.ICECandidateInit) { r.mu.Lock() defer r.mu.Unlock() if !r.pubRemoteSet { r.pubPending = append(r.pubPending, cand) return } r.pubPC.AddICECandidate(cand) } func (r *SFURelay) AddSubICECandidate(cand webrtc.ICECandidateInit) { r.mu.Lock() defer r.mu.Unlock() if !r.subRemoteSet { r.subPending = append(r.subPending, cand) return } r.subPC.AddICECandidate(cand) } func (r *SFURelay) Close() { if r.tun != nil { r.tun.Stop() r.tun = nil } if r.pubPC != nil { r.pubPC.Close() r.pubPC = nil } if r.subPC != nil { r.subPC.Close() r.subPC = nil } } func (r *SFURelay) readTrack(track *webrtc.TrackRemote) { if track.Codec().MimeType != webrtc.MimeTypeVP8 { buf := make([]byte, common.UDPBufSize) for { if _, _, err := track.Read(buf); err != nil { return } } } var vp8Pkt codecs.VP8Packet var pkt rtp.Packet var frameBuf []byte var lastSeq uint16 var haveLastSeq bool frameValid := false var recvCount int bufSz := r.readBufSize if bufSz <= 0 { bufSz = common.RTPBufSize } buf := make([]byte, bufSz) for { n, _, err := track.Read(buf) if err != nil { return } if pkt.Unmarshal(buf[:n]) != nil { continue } if haveLastSeq && pkt.SequenceNumber != lastSeq+1 { frameValid = false frameBuf = frameBuf[:0] } lastSeq = pkt.SequenceNumber haveLastSeq = true vp8Payload, err := vp8Pkt.Unmarshal(pkt.Payload) if err != nil { frameValid = false frameBuf = frameBuf[:0] continue } if vp8Pkt.S == 1 { frameBuf = frameBuf[:0] frameValid = true } if !frameValid { continue } frameBuf = append(frameBuf, vp8Payload...) if !pkt.Marker { continue } recvCount++ if recvCount <= 3 || recvCount%200 == 0 { r.logger.Debug(fmt.Sprintf("[video] recv vp8 frame #%d %d bytes", recvCount, len(frameBuf))) } res := r.obf.Decode(frameBuf) frameBuf = frameBuf[:0] frameValid = false if !res.HasFrame || res.SelfEcho { continue } if res.PeerRestart { r.logger.Info(fmt.Sprintf("[video] peer restart detected, new epoch=0x%08x", res.PeerEpoch)) if r.OnPeerRestart != nil { r.OnPeerRestart() } } if res.Keepalive || len(res.Payload) == 0 { continue } if r.tun != nil && r.tun.OnData != nil { r.tun.OnData(res.Payload) } } }