Skip to content

Commit 6365968

Browse files
authored
Merge pull request #1253 from jamal/nest-rtsp
nest: add support for RTSP cameras
2 parents 33f4bb4 + 1abb3c8 commit 6365968

File tree

3 files changed

+226
-33
lines changed

3 files changed

+226
-33
lines changed

internal/nest/init.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package nest
22

33
import (
44
"net/http"
5+
"strings"
56

67
"github.com/AlexxIT/go2rtc/internal/api"
78
"github.com/AlexxIT/go2rtc/internal/streams"
@@ -38,11 +39,12 @@ func apiNest(w http.ResponseWriter, r *http.Request) {
3839

3940
var items []*api.Source
4041

41-
for name, deviceID := range devices {
42-
query.Set("device_id", deviceID)
42+
for _, device := range devices {
43+
query.Set("device_id", device.DeviceID)
44+
query.Set("protocols", strings.Join(device.Protocols, ","))
4345

4446
items = append(items, &api.Source{
45-
Name: name, URL: "nest:?" + query.Encode(),
47+
Name: device.Name, URL: "nest:?" + query.Encode(),
4648
})
4749
}
4850

pkg/nest/api.go

+151-18
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,28 @@ type API struct {
1717

1818
StreamProjectID string
1919
StreamDeviceID string
20-
StreamSessionID string
2120
StreamExpiresAt time.Time
2221

22+
// WebRTC
23+
StreamSessionID string
24+
25+
// RTSP
26+
StreamToken string
27+
StreamExtensionToken string
28+
2329
extendTimer *time.Timer
2430
}
2531

2632
type Auth struct {
2733
AccessToken string
2834
}
2935

36+
type DeviceInfo struct {
37+
Name string
38+
DeviceID string
39+
Protocols []string
40+
}
41+
3042
var cache = map[string]*API{}
3143
var cacheMu sync.Mutex
3244

@@ -80,7 +92,7 @@ func NewAPI(clientID, clientSecret, refreshToken string) (*API, error) {
8092
return api, nil
8193
}
8294

83-
func (a *API) GetDevices(projectID string) (map[string]string, error) {
95+
func (a *API) GetDevices(projectID string) ([]DeviceInfo, error) {
8496
uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectID + "/devices"
8597
req, err := http.NewRequest("GET", uri, nil)
8698
if err != nil {
@@ -108,24 +120,30 @@ func (a *API) GetDevices(projectID string) (map[string]string, error) {
108120
return nil, err
109121
}
110122

111-
devices := map[string]string{}
123+
devices := make([]DeviceInfo, 0, len(resv.Devices))
112124

113125
for _, device := range resv.Devices {
126+
// only RTSP and WEB_RTC available (both supported)
114127
if len(device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols) == 0 {
115128
continue
116129
}
117130

118-
if device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols[0] != "WEB_RTC" {
119-
continue
120-
}
121-
122131
i := strings.LastIndexByte(device.Name, '/')
123132
if i <= 0 {
124133
continue
125134
}
126135

127136
name := device.Traits.SdmDevicesTraitsInfo.CustomName
128-
devices[name] = device.Name[i+1:]
137+
// Devices configured through the Nest app use the container/room name as opposed to the customName trait
138+
if name == "" && len(device.ParentRelations) > 0 {
139+
name = device.ParentRelations[0].DisplayName
140+
}
141+
142+
devices = append(devices, DeviceInfo{
143+
Name: name,
144+
DeviceID: device.Name[i+1:],
145+
Protocols: device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols,
146+
})
129147
}
130148

131149
return devices, nil
@@ -190,11 +208,20 @@ func (a *API) ExtendStream() error {
190208
var reqv struct {
191209
Command string `json:"command"`
192210
Params struct {
193-
MediaSessionID string `json:"mediaSessionId"`
211+
MediaSessionID string `json:"mediaSessionId,omitempty"`
212+
StreamExtensionToken string `json:"streamExtensionToken,omitempty"`
194213
} `json:"params"`
195214
}
196-
reqv.Command = "sdm.devices.commands.CameraLiveStream.ExtendWebRtcStream"
197-
reqv.Params.MediaSessionID = a.StreamSessionID
215+
216+
if a.StreamToken != "" {
217+
// RTSP
218+
reqv.Command = "sdm.devices.commands.CameraLiveStream.ExtendRtspStream"
219+
reqv.Params.StreamExtensionToken = a.StreamExtensionToken
220+
} else {
221+
// WebRTC
222+
reqv.Command = "sdm.devices.commands.CameraLiveStream.ExtendWebRtcStream"
223+
reqv.Params.MediaSessionID = a.StreamSessionID
224+
}
198225

199226
b, err := json.Marshal(reqv)
200227
if err != nil {
@@ -223,8 +250,10 @@ func (a *API) ExtendStream() error {
223250

224251
var resv struct {
225252
Results struct {
226-
ExpiresAt time.Time `json:"expiresAt"`
227-
MediaSessionID string `json:"mediaSessionId"`
253+
ExpiresAt time.Time `json:"expiresAt"`
254+
MediaSessionID string `json:"mediaSessionId"`
255+
StreamExtensionToken string `json:"streamExtensionToken"`
256+
StreamToken string `json:"streamToken"`
228257
} `json:"results"`
229258
}
230259

@@ -234,6 +263,111 @@ func (a *API) ExtendStream() error {
234263

235264
a.StreamSessionID = resv.Results.MediaSessionID
236265
a.StreamExpiresAt = resv.Results.ExpiresAt
266+
a.StreamExtensionToken = resv.Results.StreamExtensionToken
267+
a.StreamToken = resv.Results.StreamToken
268+
269+
return nil
270+
}
271+
272+
func (a *API) GenerateRtspStream(projectID, deviceID string) (string, error) {
273+
var reqv struct {
274+
Command string `json:"command"`
275+
Params struct{} `json:"params"`
276+
}
277+
reqv.Command = "sdm.devices.commands.CameraLiveStream.GenerateRtspStream"
278+
279+
b, err := json.Marshal(reqv)
280+
if err != nil {
281+
return "", err
282+
}
283+
284+
uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" +
285+
projectID + "/devices/" + deviceID + ":executeCommand"
286+
req, err := http.NewRequest("POST", uri, bytes.NewReader(b))
287+
if err != nil {
288+
return "", err
289+
}
290+
291+
req.Header.Set("Authorization", "Bearer "+a.Token)
292+
293+
client := &http.Client{Timeout: time.Second * 5000}
294+
res, err := client.Do(req)
295+
if err != nil {
296+
return "", err
297+
}
298+
299+
if res.StatusCode != 200 {
300+
return "", errors.New("nest: wrong status: " + res.Status)
301+
}
302+
303+
var resv struct {
304+
Results struct {
305+
StreamURLs map[string]string `json:"streamUrls"`
306+
StreamExtensionToken string `json:"streamExtensionToken"`
307+
StreamToken string `json:"streamToken"`
308+
ExpiresAt time.Time `json:"expiresAt"`
309+
} `json:"results"`
310+
}
311+
312+
if err = json.NewDecoder(res.Body).Decode(&resv); err != nil {
313+
return "", err
314+
}
315+
316+
if _, ok := resv.Results.StreamURLs["rtspUrl"]; !ok {
317+
return "", errors.New("nest: failed to generate rtsp url")
318+
}
319+
320+
a.StreamProjectID = projectID
321+
a.StreamDeviceID = deviceID
322+
a.StreamToken = resv.Results.StreamToken
323+
a.StreamExtensionToken = resv.Results.StreamExtensionToken
324+
a.StreamExpiresAt = resv.Results.ExpiresAt
325+
326+
return resv.Results.StreamURLs["rtspUrl"], nil
327+
}
328+
329+
func (a *API) StopRTSPStream() error {
330+
if a.StreamProjectID == "" || a.StreamDeviceID == "" {
331+
return errors.New("nest: tried to stop rtsp stream without a project or device ID")
332+
}
333+
334+
var reqv struct {
335+
Command string `json:"command"`
336+
Params struct {
337+
StreamExtensionToken string `json:"streamExtensionToken"`
338+
} `json:"params"`
339+
}
340+
reqv.Command = "sdm.devices.commands.CameraLiveStream.StopRtspStream"
341+
reqv.Params.StreamExtensionToken = a.StreamExtensionToken
342+
343+
b, err := json.Marshal(reqv)
344+
if err != nil {
345+
return err
346+
}
347+
348+
uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" +
349+
a.StreamProjectID + "/devices/" + a.StreamDeviceID + ":executeCommand"
350+
req, err := http.NewRequest("POST", uri, bytes.NewReader(b))
351+
if err != nil {
352+
return err
353+
}
354+
355+
req.Header.Set("Authorization", "Bearer "+a.Token)
356+
357+
client := &http.Client{Timeout: time.Second * 5000}
358+
res, err := client.Do(req)
359+
if err != nil {
360+
return err
361+
}
362+
363+
if res.StatusCode != 200 {
364+
return errors.New("nest: wrong status: " + res.Status)
365+
}
366+
367+
a.StreamProjectID = ""
368+
a.StreamDeviceID = ""
369+
a.StreamExtensionToken = ""
370+
a.StreamToken = ""
237371

238372
return nil
239373
}
@@ -266,10 +400,10 @@ type Device struct {
266400
//SdmDevicesTraitsCameraClipPreview struct {
267401
//} `json:"sdm.devices.traits.CameraClipPreview"`
268402
} `json:"traits"`
269-
//ParentRelations []struct {
270-
// Parent string `json:"parent"`
271-
// DisplayName string `json:"displayName"`
272-
//} `json:"parentRelations"`
403+
ParentRelations []struct {
404+
Parent string `json:"parent"`
405+
DisplayName string `json:"displayName"`
406+
} `json:"parentRelations"`
273407
}
274408

275409
func (a *API) StartExtendStreamTimer() {
@@ -282,7 +416,6 @@ func (a *API) StartExtendStreamTimer() {
282416
duration = time.Until(a.StreamExpiresAt.Add(-30 * time.Second))
283417
a.extendTimer.Reset(duration)
284418
})
285-
286419
}
287420

288421
func (a *API) StopExtendStreamTimer() {

pkg/nest/client.go

+70-12
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,25 @@ package nest
33
import (
44
"errors"
55
"net/url"
6+
"strings"
67

78
"github.com/AlexxIT/go2rtc/pkg/core"
9+
"github.com/AlexxIT/go2rtc/pkg/rtsp"
810
"github.com/AlexxIT/go2rtc/pkg/webrtc"
911
pion "github.com/pion/webrtc/v3"
1012
)
1113

12-
type Client struct {
14+
type WebRTCClient struct {
1315
conn *webrtc.Conn
1416
api *API
1517
}
1618

17-
func Dial(rawURL string) (*Client, error) {
19+
type RTSPClient struct {
20+
conn *rtsp.Conn
21+
api *API
22+
}
23+
24+
func Dial(rawURL string) (core.Producer, error) {
1825
u, err := url.Parse(rawURL)
1926
if err != nil {
2027
return nil, err
@@ -36,6 +43,42 @@ func Dial(rawURL string) (*Client, error) {
3643
return nil, err
3744
}
3845

46+
protocols := strings.Split(query.Get("protocols"), ",")
47+
if len(protocols) > 0 && protocols[0] == "RTSP" {
48+
return rtspConn(nestAPI, rawURL, projectID, deviceID)
49+
}
50+
51+
// Default to WEB_RTC for backwards compataiility
52+
return rtcConn(nestAPI, rawURL, projectID, deviceID)
53+
}
54+
55+
func (c *WebRTCClient) GetMedias() []*core.Media {
56+
return c.conn.GetMedias()
57+
}
58+
59+
func (c *WebRTCClient) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
60+
return c.conn.GetTrack(media, codec)
61+
}
62+
63+
func (c *WebRTCClient) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
64+
return c.conn.AddTrack(media, codec, track)
65+
}
66+
67+
func (c *WebRTCClient) Start() error {
68+
c.api.StartExtendStreamTimer()
69+
return c.conn.Start()
70+
}
71+
72+
func (c *WebRTCClient) Stop() error {
73+
c.api.StopExtendStreamTimer()
74+
return c.conn.Stop()
75+
}
76+
77+
func (c *WebRTCClient) MarshalJSON() ([]byte, error) {
78+
return c.conn.MarshalJSON()
79+
}
80+
81+
func rtcConn(nestAPI *API, rawURL, projectID, deviceID string) (*WebRTCClient, error) {
3982
rtcAPI, err := webrtc.NewAPI()
4083
if err != nil {
4184
return nil, err
@@ -77,31 +120,46 @@ func Dial(rawURL string) (*Client, error) {
77120
return nil, err
78121
}
79122

80-
return &Client{conn: conn, api: nestAPI}, nil
123+
return &WebRTCClient{conn: conn, api: nestAPI}, nil
81124
}
82125

83-
func (c *Client) GetMedias() []*core.Media {
84-
return c.conn.GetMedias()
126+
func rtspConn(nestAPI *API, rawURL, projectID, deviceID string) (*RTSPClient, error) {
127+
rtspURL, err := nestAPI.GenerateRtspStream(projectID, deviceID)
128+
if err != nil {
129+
return nil, err
130+
}
131+
132+
rtspClient := rtsp.NewClient(rtspURL)
133+
if err := rtspClient.Dial(); err != nil {
134+
return nil, err
135+
}
136+
if err := rtspClient.Describe(); err != nil {
137+
return nil, err
138+
}
139+
140+
return &RTSPClient{conn: rtspClient, api: nestAPI}, nil
85141
}
86142

87-
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
88-
return c.conn.GetTrack(media, codec)
143+
func (c *RTSPClient) GetMedias() []*core.Media {
144+
result := c.conn.GetMedias()
145+
return result
89146
}
90147

91-
func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
92-
return c.conn.AddTrack(media, codec, track)
148+
func (c *RTSPClient) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
149+
return c.conn.GetTrack(media, codec)
93150
}
94151

95-
func (c *Client) Start() error {
152+
func (c *RTSPClient) Start() error {
96153
c.api.StartExtendStreamTimer()
97154
return c.conn.Start()
98155
}
99156

100-
func (c *Client) Stop() error {
157+
func (c *RTSPClient) Stop() error {
158+
c.api.StopRTSPStream()
101159
c.api.StopExtendStreamTimer()
102160
return c.conn.Stop()
103161
}
104162

105-
func (c *Client) MarshalJSON() ([]byte, error) {
163+
func (c *RTSPClient) MarshalJSON() ([]byte, error) {
106164
return c.conn.MarshalJSON()
107165
}

0 commit comments

Comments
 (0)