add snapshot producer
This commit is contained in:
@@ -84,10 +84,18 @@ func apiRing(w http.ResponseWriter, r *http.Request) {
|
|||||||
var items []*api.Source
|
var items []*api.Source
|
||||||
for _, camera := range devices.AllCameras {
|
for _, camera := range devices.AllCameras {
|
||||||
cleanQuery.Set("device_id", camera.DeviceID)
|
cleanQuery.Set("device_id", camera.DeviceID)
|
||||||
|
|
||||||
|
// Stream source
|
||||||
items = append(items, &api.Source{
|
items = append(items, &api.Source{
|
||||||
Name: camera.Description,
|
Name: camera.Description,
|
||||||
URL: "ring:?" + cleanQuery.Encode(),
|
URL: "ring:?" + cleanQuery.Encode(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Snapshot source
|
||||||
|
items = append(items, &api.Source{
|
||||||
|
Name: camera.Description + " Snapshot",
|
||||||
|
URL: "ring:?" + cleanQuery.Encode() + "&snapshot",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
api.ResponseSources(w, items)
|
api.ResponseSources(w, items)
|
||||||
|
|||||||
+70
-49
@@ -18,7 +18,7 @@ import (
|
|||||||
type Client struct {
|
type Client struct {
|
||||||
api *RingRestClient
|
api *RingRestClient
|
||||||
ws *websocket.Conn
|
ws *websocket.Conn
|
||||||
prod *webrtc.Conn
|
prod core.Producer
|
||||||
camera *CameraData
|
camera *CameraData
|
||||||
dialogID string
|
dialogID string
|
||||||
sessionID string
|
sessionID string
|
||||||
@@ -101,7 +101,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Dial(rawURL string) (*Client, error) {
|
func Dial(rawURL string) (*Client, error) {
|
||||||
// 1. Create Ring Rest API client
|
// 1. Parse URL and validate basic params
|
||||||
u, err := url.Parse(rawURL)
|
u, err := url.Parse(rawURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -110,6 +110,7 @@ func Dial(rawURL string) (*Client, error) {
|
|||||||
query := u.Query()
|
query := u.Query()
|
||||||
encodedToken := query.Get("refresh_token")
|
encodedToken := query.Get("refresh_token")
|
||||||
deviceID := query.Get("device_id")
|
deviceID := query.Get("device_id")
|
||||||
|
_, isSnapshot := query["snapshot"]
|
||||||
|
|
||||||
if encodedToken == "" || deviceID == "" {
|
if encodedToken == "" || deviceID == "" {
|
||||||
return nil, errors.New("ring: wrong query")
|
return nil, errors.New("ring: wrong query")
|
||||||
@@ -144,7 +145,21 @@ func Dial(rawURL string) (*Client, error) {
|
|||||||
return nil, errors.New("ring: camera not found")
|
return nil, errors.New("ring: camera not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Connect to signaling server
|
// Create base client
|
||||||
|
client := &Client{
|
||||||
|
api: ringAPI,
|
||||||
|
camera: camera,
|
||||||
|
dialogID: uuid.NewString(),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if snapshot request
|
||||||
|
if isSnapshot {
|
||||||
|
client.prod = NewSnapshotProducer(ringAPI, camera)
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not snapshot, continue with WebRTC setup
|
||||||
ticket, err := ringAPI.GetSocketTicket()
|
ticket, err := ringAPI.GetSocketTicket()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -154,14 +169,14 @@ func Dial(rawURL string) (*Client, error) {
|
|||||||
wsURL := 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",
|
wsURL := 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))
|
uuid.NewString(), url.QueryEscape(ticket.Ticket))
|
||||||
|
|
||||||
ws, _, err := websocket.DefaultDialer.Dial(wsURL, map[string][]string{
|
client.ws, _, err = websocket.DefaultDialer.Dial(wsURL, map[string][]string{
|
||||||
"User-Agent": {"android:com.ringapp"},
|
"User-Agent": {"android:com.ringapp"},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Create Peer Connection
|
// Create Peer Connection
|
||||||
conf := pion.Configuration{
|
conf := pion.Configuration{
|
||||||
ICEServers: []pion.ICEServer{
|
ICEServers: []pion.ICEServer{
|
||||||
{URLs: []string{
|
{URLs: []string{
|
||||||
@@ -181,13 +196,13 @@ func Dial(rawURL string) (*Client, error) {
|
|||||||
|
|
||||||
api, err := webrtc.NewAPI()
|
api, err := webrtc.NewAPI()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ws.Close()
|
client.ws.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pc, err := api.NewPeerConnection(conf)
|
pc, err := api.NewPeerConnection(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ws.Close()
|
client.ws.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,14 +221,7 @@ func Dial(rawURL string) (*Client, error) {
|
|||||||
prod.Protocol = "ws"
|
prod.Protocol = "ws"
|
||||||
prod.URL = rawURL
|
prod.URL = rawURL
|
||||||
|
|
||||||
client := &Client{
|
client.prod = prod
|
||||||
api: ringAPI,
|
|
||||||
ws: ws,
|
|
||||||
prod: prod,
|
|
||||||
camera: camera,
|
|
||||||
dialogID: uuid.NewString(),
|
|
||||||
done: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
prod.Listen(func(msg any) {
|
prod.Listen(func(msg any) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
@@ -273,14 +281,14 @@ func Dial(rawURL string) (*Client, error) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Create offer
|
// Create offer
|
||||||
offer, err := prod.CreateOffer(medias)
|
offer, err := prod.CreateOffer(medias)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
client.Stop()
|
client.Stop()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Send offer
|
// Send offer
|
||||||
offerPayload := map[string]interface{}{
|
offerPayload := map[string]interface{}{
|
||||||
"stream_options": map[string]bool{
|
"stream_options": map[string]bool{
|
||||||
"audio_enabled": true,
|
"audio_enabled": true,
|
||||||
@@ -297,25 +305,35 @@ func Dial(rawURL string) (*Client, error) {
|
|||||||
sendOffer.Done(nil)
|
sendOffer.Done(nil)
|
||||||
|
|
||||||
// Ring expects a ping message every 5 seconds
|
// Ring expects a ping message every 5 seconds
|
||||||
go func() {
|
go client.startPingLoop(pc)
|
||||||
|
go client.startMessageLoop(&connState)
|
||||||
|
|
||||||
|
if err = connState.Wait(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) startPingLoop(pc *pion.PeerConnection) {
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-client.done:
|
case <-c.done:
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
if pc.ConnectionState() == pion.PeerConnectionStateConnected {
|
if pc.ConnectionState() == pion.PeerConnectionStateConnected {
|
||||||
if err := client.sendSessionMessage("ping", nil); err != nil {
|
if err := c.sendSessionMessage("ping", nil); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
|
|
||||||
go func() {
|
func (c *Client) startMessageLoop(connState *core.Waiter) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// will be closed when conn will be closed
|
// will be closed when conn will be closed
|
||||||
@@ -325,18 +343,18 @@ func Dial(rawURL string) (*Client, error) {
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-client.done:
|
case <-c.done:
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
var res BaseMessage
|
var res BaseMessage
|
||||||
if err = ws.ReadJSON(&res); err != nil {
|
if err = c.ws.ReadJSON(&res); err != nil {
|
||||||
select {
|
select {
|
||||||
case <-client.done:
|
case <-c.done:
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
client.Stop()
|
c.Stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,19 +365,19 @@ func Dial(rawURL string) (*Client, error) {
|
|||||||
|
|
||||||
// check if the message is from the correct doorbot
|
// check if the message is from the correct doorbot
|
||||||
doorbotID := res.Body["doorbot_id"].(float64)
|
doorbotID := res.Body["doorbot_id"].(float64)
|
||||||
if doorbotID != float64(client.camera.ID) {
|
if doorbotID != float64(c.camera.ID) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the message is from the correct session
|
// check if the message is from the correct session
|
||||||
if res.Method == "session_created" || res.Method == "session_started" {
|
if res.Method == "session_created" || res.Method == "session_started" {
|
||||||
if _, ok := res.Body["session_id"]; ok && client.sessionID == "" {
|
if _, ok := res.Body["session_id"]; ok && c.sessionID == "" {
|
||||||
client.sessionID = res.Body["session_id"].(string)
|
c.sessionID = res.Body["session_id"].(string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := res.Body["session_id"]; ok {
|
if _, ok := res.Body["session_id"]; ok {
|
||||||
if res.Body["session_id"].(string) != client.sessionID {
|
if res.Body["session_id"].(string) != c.sessionID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,23 +386,26 @@ func Dial(rawURL string) (*Client, error) {
|
|||||||
|
|
||||||
switch res.Method {
|
switch res.Method {
|
||||||
case "sdp":
|
case "sdp":
|
||||||
// 6. Get answer
|
if prod, ok := c.prod.(*webrtc.Conn); ok {
|
||||||
|
// Get answer
|
||||||
var msg AnswerMessage
|
var msg AnswerMessage
|
||||||
if err = json.Unmarshal(rawMsg, &msg); err != nil {
|
if err = json.Unmarshal(rawMsg, &msg); err != nil {
|
||||||
client.Stop()
|
c.Stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = prod.SetAnswer(msg.Body.SDP); err != nil {
|
if err = prod.SetAnswer(msg.Body.SDP); err != nil {
|
||||||
client.Stop()
|
c.Stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = client.activateSession(); err != nil {
|
if err = c.activateSession(); err != nil {
|
||||||
client.Stop()
|
c.Stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case "ice":
|
case "ice":
|
||||||
// 7. Continue to receiving candidates
|
if prod, ok := c.prod.(*webrtc.Conn); ok {
|
||||||
|
// Continue to receiving candidates
|
||||||
var msg IceCandidateMessage
|
var msg IceCandidateMessage
|
||||||
if err = json.Unmarshal(rawMsg, &msg); err != nil {
|
if err = json.Unmarshal(rawMsg, &msg); err != nil {
|
||||||
break
|
break
|
||||||
@@ -396,12 +417,13 @@ func Dial(rawURL string) (*Client, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = prod.AddCandidate(msg.Body.Ice); err != nil {
|
if err = prod.AddCandidate(msg.Body.Ice); err != nil {
|
||||||
client.Stop()
|
c.Stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case "close":
|
case "close":
|
||||||
client.Stop()
|
c.Stop()
|
||||||
return
|
return
|
||||||
|
|
||||||
case "pong":
|
case "pong":
|
||||||
@@ -410,13 +432,6 @@ func Dial(rawURL string) (*Client, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
if err = connState.Wait(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) activateSession() error {
|
func (c *Client) activateSession() error {
|
||||||
@@ -471,16 +486,18 @@ func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||||
|
if webrtcProd, ok := c.prod.(*webrtc.Conn); ok {
|
||||||
if media.Kind == core.KindAudio {
|
if media.Kind == core.KindAudio {
|
||||||
// Enable speaker
|
// Enable speaker
|
||||||
speakerPayload := map[string]interface{}{
|
speakerPayload := map[string]interface{}{
|
||||||
"stealth_mode": false,
|
"stealth_mode": false,
|
||||||
}
|
}
|
||||||
|
_ = c.sendSessionMessage("camera_options", speakerPayload)
|
||||||
_ = c.sendSessionMessage("camera_options", speakerPayload);
|
}
|
||||||
|
return webrtcProd.AddTrack(media, codec, track)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.prod.AddTrack(media, codec, track)
|
return fmt.Errorf("add track not supported for snapshot")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Start() error {
|
func (c *Client) Start() error {
|
||||||
@@ -517,5 +534,9 @@ func (c *Client) Stop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||||
return c.prod.MarshalJSON()
|
if webrtcProd, ok := c.prod.(*webrtc.Conn); ok {
|
||||||
|
return webrtcProd.MarshalJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("ring: can't marshal")
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package ring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SnapshotProducer struct {
|
||||||
|
core.Connection
|
||||||
|
|
||||||
|
client *RingRestClient
|
||||||
|
camera *CameraData
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSnapshotProducer(client *RingRestClient, camera *CameraData) *SnapshotProducer {
|
||||||
|
return &SnapshotProducer{
|
||||||
|
Connection: core.Connection{
|
||||||
|
ID: core.NewID(),
|
||||||
|
FormatName: "ring/snapshot",
|
||||||
|
Protocol: "https",
|
||||||
|
Medias: []*core.Media{
|
||||||
|
{
|
||||||
|
Kind: core.KindVideo,
|
||||||
|
Direction: core.DirectionRecvonly,
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
{
|
||||||
|
Name: core.CodecJPEG,
|
||||||
|
ClockRate: 90000,
|
||||||
|
PayloadType: core.PayloadTypeRAW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
client: client,
|
||||||
|
camera: camera,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SnapshotProducer) Start() error {
|
||||||
|
// Fetch snapshot
|
||||||
|
response, err := p.client.Request("GET", fmt.Sprintf("https://app-snaps.ring.com/snapshots/next/%d", int(p.camera.ID)), nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get snapshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt := &rtp.Packet{
|
||||||
|
Header: rtp.Header{Timestamp: core.Now90000()},
|
||||||
|
Payload: response,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to all receivers
|
||||||
|
for _, receiver := range p.Receivers {
|
||||||
|
receiver.WriteRTP(pkt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SnapshotProducer) Stop() error {
|
||||||
|
return p.Connection.Stop()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user