Skip to content

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

@http600

Description

@http600

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions