feat(nest): add retry logic for 429 and 409 errors with exponential backoff

This commit is contained in:
hnws
2025-03-21 23:10:16 -04:00
parent 47f32a5f55
commit fe10a7e55f
2 changed files with 187 additions and 76 deletions
+90 -43
View File
@@ -4,13 +4,17 @@ import (
"errors"
"net/url"
"strings"
"time"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
)
var log = app.GetLogger("nest")
type WebRTCClient struct {
conn *webrtc.Conn
api *API
@@ -38,9 +42,32 @@ func Dial(rawURL string) (core.Producer, error) {
return nil, errors.New("nest: wrong query")
}
nestAPI, err := NewAPI(cliendID, cliendSecret, refreshToken)
if err != nil {
return nil, err
maxRetries := 3
retryDelay := time.Second * 30
var nestAPI *API
var lastErr error
for attempt := 0; attempt < maxRetries; attempt++ {
nestAPI, err = NewAPI(cliendID, cliendSecret, refreshToken)
if err == nil {
break
}
lastErr = err
if attempt < maxRetries-1 {
log.Info().
Float64("delay", retryDelay.Seconds()).
Int("attempt", attempt+1).
Int("max_retries", maxRetries-1).
Err(err).
Msg("API initialization failed, retrying")
time.Sleep(retryDelay)
retryDelay *= 2 // exponential backoff
}
}
if nestAPI == nil {
return nil, lastErr
}
protocols := strings.Split(query.Get("protocols"), ",")
@@ -79,48 +106,68 @@ func (c *WebRTCClient) MarshalJSON() ([]byte, error) {
}
func rtcConn(nestAPI *API, rawURL, projectID, deviceID string) (*WebRTCClient, error) {
rtcAPI, err := webrtc.NewAPI()
if err != nil {
return nil, err
maxRetries := 3
retryDelay := time.Second * 30
var lastErr error
for attempt := 0; attempt < maxRetries; attempt++ {
rtcAPI, err := webrtc.NewAPI()
if err != nil {
return nil, err
}
conf := pion.Configuration{}
pc, err := rtcAPI.NewPeerConnection(conf)
if err != nil {
return nil, err
}
conn := webrtc.NewConn(pc)
conn.FormatName = "nest/webrtc"
conn.Mode = core.ModeActiveProducer
conn.Protocol = "http"
conn.URL = rawURL
// https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields
medias := []*core.Media{
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
{Kind: "app"}, // important for Nest
}
// 3. Create offer with candidates
offer, err := conn.CreateCompleteOffer(medias)
if err != nil {
return nil, err
}
// 4. Exchange SDP via Hass
answer, err := nestAPI.ExchangeSDP(projectID, deviceID, offer)
if err != nil {
lastErr = err
if attempt < maxRetries-1 {
log.Info().
Float64("delay", retryDelay.Seconds()).
Int("attempt", attempt+1).
Int("max_retries", maxRetries-1).
Err(err).
Msg("WebRTC connection setup failed, retrying")
time.Sleep(retryDelay)
retryDelay *= 2
continue
}
return nil, err
}
// 5. Set answer with remote medias
if err = conn.SetAnswer(answer); err != nil {
return nil, err
}
return &WebRTCClient{conn: conn, api: nestAPI}, nil
}
conf := pion.Configuration{}
pc, err := rtcAPI.NewPeerConnection(conf)
if err != nil {
return nil, err
}
conn := webrtc.NewConn(pc)
conn.FormatName = "nest/webrtc"
conn.Mode = core.ModeActiveProducer
conn.Protocol = "http"
conn.URL = rawURL
// https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields
medias := []*core.Media{
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
{Kind: "app"}, // important for Nest
}
// 3. Create offer with candidates
offer, err := conn.CreateCompleteOffer(medias)
if err != nil {
return nil, err
}
// 4. Exchange SDP via Hass
answer, err := nestAPI.ExchangeSDP(projectID, deviceID, offer)
if err != nil {
return nil, err
}
// 5. Set answer with remote medias
if err = conn.SetAnswer(answer); err != nil {
return nil, err
}
return &WebRTCClient{conn: conn, api: nestAPI}, nil
return nil, lastErr
}
func rtspConn(nestAPI *API, rawURL, projectID, deviceID string) (*RTSPClient, error) {