webrtc icon indicating copy to clipboard operation
webrtc copied to clipboard

webrtc client is not sending stun message to keepalive then ice got disconnect

Open http600 opened this issue 3 years ago • 8 comments

Your environment.

  • Version: github.com/pion/webrtc/v3 v3.1.11
  • Browser: NOT RELATED
  • Other Information - ion-sfu c8cea05fa60612ae819594afc9655573d8958a5d

What did you do?

  1. follow here broadcasting-ion-sfu
    package main
    
    import (
        "bytes"
        "encoding/json"
        "flag"
        "fmt"
        "io"
        "log"
        "net/url"
    
        "github.com/google/uuid"
        "github.com/gorilla/websocket"
        "github.com/pion/mediadevices"
        "github.com/pion/mediadevices/pkg/codec/vpx"
        "github.com/pion/mediadevices/pkg/frame"
        "github.com/pion/mediadevices/pkg/prop"
        "github.com/pion/webrtc/v3"
        "github.com/sourcegraph/jsonrpc2"
    
        // Note: If you don't have a camera or microphone or your adapters are not supported,
        //       you can always swap your adapters with our dummy adapters below.
        // _ "github.com/pion/mediadevices/pkg/driver/videotest"
        // _ "github.com/pion/mediadevices/pkg/driver/audiotest"
        _ "github.com/pion/mediadevices/pkg/driver/camera"     // This is required to register camera adapter
        _ "github.com/pion/mediadevices/pkg/driver/microphone" // This is required to register microphone adapter
    )
    
    type Candidate struct {
        Target    int                  `json:"target"`
        Candidate *webrtc.ICECandidate `json:candidate`
    }
    
    type ResponseCandidate struct {
        Target    int                      `json:"target"`
        Candidate *webrtc.ICECandidateInit `json:candidate`
    }
    
    // SendOffer object to send to the sfu over Websockets
    type SendOffer struct {
        SID   string                     `json:sid`
        Offer *webrtc.SessionDescription `json:offer`
    }
    
    // SendAnswer object to send to the sfu over Websockets
    type SendAnswer struct {
        SID    string                     `json:sid`
        Answer *webrtc.SessionDescription `json:answer`
    }
    
    // TrickleResponse received from the sfu server
    type TrickleResponse struct {
        Params ResponseCandidate `json:params`
        Method string            `json:method`
    }
    
    // Response received from the sfu over Websockets
    type Response struct {
        Params *webrtc.SessionDescription `json:params`
        Result *webrtc.SessionDescription `json:result`
        Method string                     `json:method`
        Id     uint64                     `json:id`
    }
    
    var peerConnection *webrtc.PeerConnection
    var connectionID uint64
    var remoteDescription *webrtc.SessionDescription
    
    var addr string
    
    func main() {
        flag.StringVar(&addr, "a", "${HOST}:7000", "address to use")
        flag.Parse()
    
        u := url.URL{Scheme: "ws", Host: addr, Path: "/ws"}
        log.Printf("connecting to %s", u.String())
    
        c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
        if err != nil {
    	    log.Fatal("dial:", err)
        }
        defer c.Close()
    
        config := webrtc.Configuration{
    	    ICEServers: []webrtc.ICEServer{
    		    {
    			    URLs: []string{"stun:stun.l.google.com:19302"},
    		    },
    		    /*{
    			    URLs:       []string{"turn:TURN_IP:3478"},
    			    Username:   "username",
    			    Credential: "password",
    		    },*/
    	    },
    	    SDPSemantics: webrtc.SDPSemanticsUnifiedPlanWithFallback,
        }
    
        // Create a new RTCPeerConnection
        mediaEngine := webrtc.MediaEngine{}
    
        vpxParams, err := vpx.NewVP8Params()
        if err != nil {
    	    panic(err)
        }
        vpxParams.BitRate = 500_000 // 500kbps
    
        codecSelector := mediadevices.NewCodecSelector(
    	    mediadevices.WithVideoEncoders(&vpxParams),
        )
    
        codecSelector.Populate(&mediaEngine)
        api := webrtc.NewAPI(webrtc.WithMediaEngine(&mediaEngine))
        peerConnection, err = api.NewPeerConnection(config)
        if err != nil {
    	    panic(err)
        }
    
        // Read incoming Websocket messages
        done := make(chan struct{})
    
        go readMessage(c, done)
    
        fmt.Println(mediadevices.EnumerateDevices())
    
        s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
    	    Video: func(c *mediadevices.MediaTrackConstraints) {
    		    c.FrameFormat = prop.FrameFormat(frame.FormatYUY2)
    		    c.Width = prop.Int(640)
    		    c.Height = prop.Int(480)
    	    },
    	    Codec: codecSelector,
        })
    
        if err != nil {
    	    panic(err)
        }
    
        for _, track := range s.GetTracks() {
    	    track.OnEnded(func(err error) {
    		    fmt.Printf("Track (ID: %s) ended with error: %v\n",
    			    track.ID(), err)
    	    })
    	    _, err = peerConnection.AddTransceiverFromTrack(track,
    		    webrtc.RtpTransceiverInit{
    			    Direction: webrtc.RTPTransceiverDirectionSendonly,
    		    },
    	    )
    	    if err != nil {
    		    panic(err)
    	    }
        }
    
        // Creating WebRTC offer
        offer, err := peerConnection.CreateOffer(nil)
    
        // Set the remote SessionDescription
        err = peerConnection.SetLocalDescription(offer)
        if err != nil {
    	    panic(err)
        }
    
        // Handling OnICECandidate event
        peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) {
    	    if candidate != nil {
    		    candidateJSON, err := json.Marshal(&Candidate{
    			    Candidate: candidate,
    			    Target:    0,
    		    })
    
    		    params := (*json.RawMessage)(&candidateJSON)
    
    		    if err != nil {
    			    log.Fatal(err)
    		    }
    
    		    message := &jsonrpc2.Request{
    			    Method: "trickle",
    			    Params: params,
    		    }
    
    		    reqBodyBytes := new(bytes.Buffer)
    		    json.NewEncoder(reqBodyBytes).Encode(message)
    
    		    messageBytes := reqBodyBytes.Bytes()
    		    c.WriteMessage(websocket.TextMessage, messageBytes)
    	    }
        })
    
        peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
    	    fmt.Printf("Connection State has changed to %s \n", connectionState.String())
        })
    
        offerJSON, err := json.Marshal(&SendOffer{
    	    Offer: peerConnection.LocalDescription(),
    	    SID:   "test room",
        })
    
        params := (*json.RawMessage)(&offerJSON)
    
        connectionUUID := uuid.New()
        connectionID = uint64(connectionUUID.ID())
    
        offerMessage := &jsonrpc2.Request{
    	    Method: "join",
    	    Params: params,
    	    ID: jsonrpc2.ID{
    		    IsString: false,
    		    Str:      "",
    		    Num:      connectionID,
    	    },
        }
    
        reqBodyBytes := new(bytes.Buffer)
        json.NewEncoder(reqBodyBytes).Encode(offerMessage)
    
        messageBytes := reqBodyBytes.Bytes()
        c.WriteMessage(websocket.TextMessage, messageBytes)
    
        <-done
    }
    
    func readMessage(connection *websocket.Conn, done chan struct{}) {
        defer close(done)
        for {
    	    _, message, err := connection.ReadMessage()
    	    if err != nil || err == io.EOF {
    		    log.Fatal("Error reading: ", err)
    		    break
    	    }
    
    	    fmt.Printf("recv: %s", message)
    
    	    var response Response
    	    json.Unmarshal(message, &response)
    
    	    if response.Id == connectionID {
    		    result := *response.Result
    		    remoteDescription = response.Result
    		    if err := peerConnection.SetRemoteDescription(result); err != nil {
    			    log.Fatal(err)
    		    }
    	    } else if response.Id != 0 && response.Method == "offer" {
    		    peerConnection.SetRemoteDescription(*response.Params)
    		    answer, err := peerConnection.CreateAnswer(nil)
    
    		    if err != nil {
    			    log.Fatal(err)
    		    }
    
    		    peerConnection.SetLocalDescription(answer)
    
    		    connectionUUID := uuid.New()
    		    connectionID = uint64(connectionUUID.ID())
    
    		    offerJSON, err := json.Marshal(&SendAnswer{
    			    Answer: peerConnection.LocalDescription(),
    			    SID:    "test room",
    		    })
    
    		    params := (*json.RawMessage)(&offerJSON)
    
    		    answerMessage := &jsonrpc2.Request{
    			    Method: "answer",
    			    Params: params,
    			    ID: jsonrpc2.ID{
    				    IsString: false,
    				    Str:      "",
    				    Num:      connectionID,
    			    },
    		    }
    
    		    reqBodyBytes := new(bytes.Buffer)
    		    json.NewEncoder(reqBodyBytes).Encode(answerMessage)
    
    		    messageBytes := reqBodyBytes.Bytes()
    		    connection.WriteMessage(websocket.TextMessage, messageBytes)
    	    } else if response.Method == "trickle" {
    		    var trickleResponse TrickleResponse
    
    		    if err := json.Unmarshal(message, &trickleResponse); err != nil {
    			    log.Fatal(err)
    		    }
    
    		    err := peerConnection.AddICECandidate(*trickleResponse.Params.Candidate)
    
    		    if err != nil {
    			    log.Fatal(err)
    		    }
    	    }
        }
    }
    
    
  2. go mod init WebRTCCamera
  3. go mod tidy
    module WebRTCCamera
    
    go 1.17
    
    require (
        github.com/google/uuid v1.3.0
        github.com/gorilla/websocket v1.4.2
        github.com/pion/logging v0.2.2
        github.com/pion/mediadevices v0.3.1
        github.com/pion/webrtc/v3 v3.1.11
        github.com/sourcegraph/jsonrpc2 v0.1.0
    )
    
    require (
        github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539 // indirect
        github.com/gen2brain/malgo v0.10.35 // indirect
        github.com/pion/datachannel v1.5.2 // indirect
        github.com/pion/dtls/v2 v2.0.10 // indirect
        github.com/pion/ice/v2 v2.1.14 // indirect
        github.com/pion/interceptor v0.1.2 // indirect
        github.com/pion/mdns v0.0.5 // indirect
        github.com/pion/randutil v0.1.0 // indirect
        github.com/pion/rtcp v1.2.9 // indirect
        github.com/pion/rtp v1.7.4 // indirect
        github.com/pion/sctp v1.8.0 // indirect
        github.com/pion/sdp/v3 v3.0.4 // indirect
        github.com/pion/srtp/v2 v2.0.5 // indirect
        github.com/pion/stun v0.3.5 // indirect
        github.com/pion/transport v0.12.3 // indirect
        github.com/pion/turn/v2 v2.0.5 // indirect
        github.com/pion/udp v0.1.1 // indirect
        golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
        golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
        golang.org/x/net v0.0.0-20211020060615-d418f374d309 // indirect
        golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect
        golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
    )
    
    
  4. build and run
  5. got disconnected around 60 seconds later

What did you expect?

  1. do not disconnect, keep connected

What happened?

  1. ICE connection got disconnected
  2. It is not sending STUN message any more, with help of wireshark
  3. Supposed to send STUN message here https://github.com/pion/ice/blob/715f2083100814b24cf1ec213d71178d64d65777/agent.go#L694
  4. BUT, selectedPair.Local.LastSent() is not long ago enough, should be longer than 2 seconds
  5. Why LastSent is keep being not long ago, because it is keep updating at here https://github.com/pion/ice/blob/715f2083100814b24cf1ec213d71178d64d65777/candidate_base.go#L306
  6. here siblings https://github.com/pion/ice/blob/715f2083100814b24cf1ec213d71178d64d65777/candidatepair.go#L89 and https://github.com/pion/ice/blob/715f2083100814b24cf1ec213d71178d64d65777/candidatepair.go#L94 they both write message to remote
  7. the problem is CandidatePair.Write is doing too much, candidateBase.lastSent is keep updating to time.Now() then https://github.com/pion/ice/blob/715f2083100814b24cf1ec213d71178d64d65777/agent.go#L701 has no chance to send STUN heartbeat message to keep alive, then got disconnected after timeout
  8. Is there anything I did wrong or how to fix it?

http600 avatar Dec 24 '21 20:12 http600

func (p *CandidatePair) Write(b []byte) (int, error) {
	fmt.Printf("CandidatePair Write to %s for %x\n", p.Remote.String(), b)
	return p.Local.writeTo(b, p.Remote)
}

func (a *Agent) sendSTUN(msg *stun.Message, local, remote Candidate) {
	a.log.Tracef("send STUN message: %s from %s to %s", string(msg.Raw), local.String(), remote.String())
	_, err := local.writeTo(msg.Raw, remote)
	if err != nil {
		a.log.Tracef("failed to send STUN message: %s", err)
	}
}

just can't get it, they are both writing by the same Candidate, holding the same candidateBase.lastSent, CandidatePair is running Write constantly, how can Agent to sendSTUN

http600 avatar Dec 25 '21 09:12 http600

Hi @http600

If you remove the lastReceived/LastSent checks https://github.com/pion/ice/blob/master/agent.go#L701-L702 does it reconnect?

Do you always get disconnected? What causes the disconnection?

I believe things are correct today. If you are sending RTP/RTCP/DTLS traffic then ICE doesn't need to send keepAlives. We consider any inbound traffic to update timing information here

Sean-Der avatar Dec 25 '21 19:12 Sean-Der

Hi @http600

If you remove the lastReceived/LastSent checks https://github.com/pion/ice/blob/master/agent.go#L701-L702 does it reconnect?

Do you always get disconnected? What causes the disconnection?

I believe things are correct today. If you are sending RTP/RTCP/DTLS traffic then ICE doesn't need to send keepAlives. We consider any inbound traffic to update timing information here

Hi @Sean-Der appreciate it very much, We consider any inbound traffic to update timing information, that's great, RTP is sending constantly, I can see it in wireshark, also I can play it in on web page as mentioned in broadcasting-ion-sfu, so the function of forward is working fine, I will check why constant RTP sending is not keeping alive in the environment, thanks.

http600 avatar Dec 26 '21 03:12 http600

client has received DTLSv1.2 with Content Type: Alert (21)

http600 avatar Dec 26 '21 15:12 http600

Hi @http600

If you remove the lastReceived/LastSent checks https://github.com/pion/ice/blob/master/agent.go#L701-L702 does it reconnect?

still got disconnected even sending/receiving stun smoothly

Do you always get disconnected? What causes the disconnection?

I'm not sure currently, but the last packet was DTLSV1.2 alert message

I believe things are correct today. If you are sending RTP/RTCP/DTLS traffic then ICE doesn't need to send keepAlives. We consider any inbound traffic to update timing information here

I'm sure it is working, just can not figure it out what is the cause of this problem

http600 avatar Dec 26 '21 16:12 http600

Is there any solution yet? Facing same problem here, connection got disconnected in 2 minutes after sending big file data in data channels.

dafapro avatar Jun 23 '22 16:06 dafapro

@Sean-Der

dafapro avatar Jun 23 '22 16:06 dafapro

@dafapro / @http600 - did either of you ever figure this out? I'm experiencing a similar issue, connecting a libjuice remote from Pion.

adriancable avatar May 08 '24 18:05 adriancable