266 lines
4.9 KiB
Go
266 lines
4.9 KiB
Go
package ring
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
type SessionBody struct {
|
|
DoorbotID int `json:"doorbot_id"`
|
|
SessionID string `json:"session_id"`
|
|
}
|
|
|
|
type AnswerMessage struct {
|
|
Method string `json:"method"` // "sdp"
|
|
Body struct {
|
|
SessionBody
|
|
SDP string `json:"sdp"`
|
|
Type string `json:"type"` // "answer"
|
|
} `json:"body"`
|
|
}
|
|
|
|
type IceCandidateMessage struct {
|
|
Method string `json:"method"` // "ice"
|
|
Body struct {
|
|
SessionBody
|
|
Ice string `json:"ice"`
|
|
MLineIndex int `json:"mlineindex"`
|
|
} `json:"body"`
|
|
}
|
|
|
|
type SessionMessage struct {
|
|
Method string `json:"method"` // "session_created" or "session_started"
|
|
Body SessionBody `json:"body"`
|
|
}
|
|
|
|
type PongMessage struct {
|
|
Method string `json:"method"` // "pong"
|
|
Body SessionBody `json:"body"`
|
|
}
|
|
|
|
type NotificationMessage struct {
|
|
Method string `json:"method"` // "notification"
|
|
Body struct {
|
|
SessionBody
|
|
IsOK bool `json:"is_ok"`
|
|
Text string `json:"text"`
|
|
} `json:"body"`
|
|
}
|
|
|
|
type StreamInfoMessage struct {
|
|
Method string `json:"method"` // "stream_info"
|
|
Body struct {
|
|
SessionBody
|
|
Transcoding bool `json:"transcoding"`
|
|
TranscodingReason string `json:"transcoding_reason"`
|
|
} `json:"body"`
|
|
}
|
|
|
|
type CloseRequest struct {
|
|
Method string `json:"method"` // "close"
|
|
Body struct {
|
|
SessionBody
|
|
Reason struct {
|
|
Code int `json:"code"`
|
|
Text string `json:"text"`
|
|
} `json:"reason"`
|
|
} `json:"body"`
|
|
}
|
|
|
|
type WSMessage struct {
|
|
Method string `json:"method"`
|
|
Body map[string]any `json:"body"`
|
|
}
|
|
|
|
type WSClient struct {
|
|
ws *websocket.Conn
|
|
api *RingApi
|
|
wsMutex sync.Mutex
|
|
cameraID int
|
|
dialogID string
|
|
sessionID string
|
|
|
|
onMessage func(msg WSMessage)
|
|
onError func(err error)
|
|
onClose func()
|
|
|
|
closed chan struct{}
|
|
}
|
|
|
|
const (
|
|
CloseReasonNormalClose = 0
|
|
CloseReasonAuthenticationFailed = 5
|
|
CloseReasonTimeout = 6
|
|
)
|
|
|
|
func StartWebsocket(cameraID int, api *RingApi) (*WSClient, error) {
|
|
client := &WSClient{
|
|
api: api,
|
|
cameraID: cameraID,
|
|
dialogID: uuid.NewString(),
|
|
closed: make(chan struct{}),
|
|
}
|
|
|
|
ticket, err := client.api.GetSocketTicket()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
url := fmt.Sprintf("wss://api.prod.signalling.ring.devices.a2z.com/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-%s&token=%s",
|
|
uuid.NewString(), url.QueryEscape(ticket.Ticket))
|
|
|
|
httpHeader := http.Header{}
|
|
httpHeader.Set("User-Agent", "android:com.ringapp")
|
|
|
|
client.ws, _, err = websocket.DefaultDialer.Dial(url, httpHeader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client.ws.SetCloseHandler(func(code int, text string) error {
|
|
client.onWsClose()
|
|
return nil
|
|
})
|
|
|
|
go client.startPingLoop()
|
|
go client.startMessageLoop()
|
|
|
|
return client, nil
|
|
}
|
|
|
|
func (c *WSClient) Close() error {
|
|
select {
|
|
case <-c.closed:
|
|
return nil
|
|
default:
|
|
close(c.closed)
|
|
}
|
|
|
|
closePayload := map[string]interface{}{
|
|
"reason": map[string]interface{}{
|
|
"code": CloseReasonNormalClose,
|
|
"text": "",
|
|
},
|
|
}
|
|
|
|
_ = c.sendSessionMessage("close", closePayload)
|
|
|
|
return c.ws.Close()
|
|
}
|
|
|
|
func (c *WSClient) startPingLoop() {
|
|
ticker := time.NewTicker(5 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-c.closed:
|
|
return
|
|
case <-ticker.C:
|
|
if err := c.sendSessionMessage("ping", nil); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *WSClient) startMessageLoop() {
|
|
for {
|
|
select {
|
|
case <-c.closed:
|
|
return
|
|
default:
|
|
var res WSMessage
|
|
if err := c.ws.ReadJSON(&res); err != nil {
|
|
select {
|
|
case <-c.closed:
|
|
// Ignore error if closed
|
|
default:
|
|
c.onWsError(err)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
c.onWsMessage(res)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *WSClient) activateSession() error {
|
|
if err := c.sendSessionMessage("activate_session", nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
streamPayload := map[string]interface{}{
|
|
"audio_enabled": true,
|
|
"video_enabled": true,
|
|
}
|
|
|
|
if err := c.sendSessionMessage("stream_options", streamPayload); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *WSClient) sendSessionMessage(method string, payload map[string]interface{}) error {
|
|
select {
|
|
case <-c.closed:
|
|
return nil
|
|
default:
|
|
// continue
|
|
}
|
|
|
|
c.wsMutex.Lock()
|
|
defer c.wsMutex.Unlock()
|
|
|
|
if payload == nil {
|
|
payload = make(map[string]interface{})
|
|
}
|
|
|
|
payload["doorbot_id"] = c.cameraID
|
|
if c.sessionID != "" {
|
|
payload["session_id"] = c.sessionID
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"method": method,
|
|
"dialog_id": c.dialogID,
|
|
"body": payload,
|
|
}
|
|
|
|
// rawMsg, _ := json.Marshal(msg)
|
|
// fmt.Printf("ring: sendSessionMessage: %s: %s\n", method, string(rawMsg))
|
|
|
|
if err := c.ws.WriteJSON(msg); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *WSClient) onWsMessage(msg WSMessage) {
|
|
if c.onMessage != nil {
|
|
c.onMessage(msg)
|
|
}
|
|
}
|
|
|
|
func (c *WSClient) onWsError(err error) {
|
|
if c.onError != nil {
|
|
c.onError(err)
|
|
}
|
|
}
|
|
|
|
func (c *WSClient) onWsClose() {
|
|
if c.onClose != nil {
|
|
c.onClose()
|
|
}
|
|
}
|