pion / webrtc

Pure Go implementation of the WebRTC API
https://pion.ly
MIT License
13.37k stars 1.63k forks source link

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

Open http600 opened 2 years ago

http600 commented 2 years ago

Your environment.

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?

  2. ICE connection got disconnected
  3. It is not sending STUN message any more, with help of wireshark
  4. Supposed to send STUN message here https://github.com/pion/ice/blob/715f2083100814b24cf1ec213d71178d64d65777/agent.go#L694
  5. BUT, selectedPair.Local.LastSent() is not long ago enough, should be longer than 2 seconds
  6. 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
  7. 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
  8. 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
  9. Is there anything I did wrong or how to fix it?
http600 commented 2 years ago
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

Sean-Der commented 2 years ago

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

http600 commented 2 years ago

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 commented 2 years ago

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

http600 commented 2 years ago

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

dafapro commented 2 years ago

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

dafapro commented 2 years ago

@Sean-Der

adriancable commented 4 months ago

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