From 47b740ff350b33e0b567504384cc2adaa8b1dcea Mon Sep 17 00:00:00 2001 From: hsakoh <20980395+hsakoh@users.noreply.github.com> Date: Fri, 6 Sep 2024 17:35:02 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=EF=BB=BFAdd=20client=20for=20SwitchBot=20C?= =?UTF-8?q?amera=20WebRTC=20(supports=20special=20SessionDescription).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 15 ++++++--- internal/webrtc/client.go | 4 ++- internal/webrtc/kinesis.go | 24 +++++++++++--- internal/webrtc/switchbot.go | 62 ++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 internal/webrtc/switchbot.go diff --git a/README.md b/README.md index 65ebf2b7..cec2f247 100644 --- a/README.md +++ b/README.md @@ -682,13 +682,18 @@ Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC proto Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify signalling WebSocket URL with all credentials in query params, `client_id` and `ice_servers` list in [JSON format](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer). +**switchbot** + +Support connection to [SwitchBot](https://us.switch-bot.com/) cameras that are based on Kinesis Video Streams. Specifically, this includes [Pan/Tilt Cam Plus 2K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-2k) and [Pan/Tilt Cam Plus 3K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-3k). (`Outdoor Spotlight Cam 1080P`,`Outdoor Spotlight Cam 2K`, `Pan/Tilt Cam`,`Pan/Tilt Cam 2K`, `Indoor Cam` are based on Tuya, so this feature is not available .) + ```yaml streams: - webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1 - webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1 - webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}] - webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze - webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}] + webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1 + webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1 + webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}] + webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze + webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}] + webrtc-switchbot: webrtc:wss://...amazonaws.com/?...#format=switchbot#resolution=HD#client_id=...#ice_servers=[{...},{...}] ``` **PS.** For `kinesis` sources you can use [echo](#source-echo) to get connection params using `bash`/`python` or any other script language. diff --git a/internal/webrtc/client.go b/internal/webrtc/client.go index a5af8bb6..9f21f4e9 100644 --- a/internal/webrtc/client.go +++ b/internal/webrtc/client.go @@ -41,9 +41,11 @@ func streamsHandler(rawURL string) (core.Producer, error) { // https://aws.amazon.com/kinesis/video-streams/ // https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html // https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc - return kinesisClient(rawURL, query, "webrtc/kinesis") + return kinesisClient(rawURL, query, "webrtc/kinesis", &kinesisClientOpts{}) } else if format == "openipc" { return openIPCClient(rawURL, query) + } else if format == "switchbot" { + return switchbotClient(rawURL, query) } else { return go2rtcClient(rawURL) } diff --git a/internal/webrtc/kinesis.go b/internal/webrtc/kinesis.go index 2ea1cf7a..42f76dce 100644 --- a/internal/webrtc/kinesis.go +++ b/internal/webrtc/kinesis.go @@ -34,7 +34,12 @@ func (k kinesisResponse) String() string { return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload) } -func kinesisClient(rawURL string, query url.Values, format string) (core.Producer, error) { +type kinesisClientOpts struct { + SessionDescriptionModifier func(*pion.SessionDescription) ([]byte, error) + MediaModifier func() ([]*core.Media, error) +} + +func kinesisClient(rawURL string, query url.Values, format string, opts *kinesisClientOpts) (core.Producer, error) { // 1. Connect to signalign server conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil) if err != nil { @@ -112,6 +117,12 @@ func kinesisClient(rawURL string, query url.Values, format string) (core.Produce {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, {Kind: core.KindAudio, Direction: core.DirectionRecvonly}, } + if opts.MediaModifier != nil { + medias, err = opts.MediaModifier() + if err != nil { + return nil, err + } + } // 4. Create offer offer, err := prod.CreateOffer(medias) @@ -121,10 +132,15 @@ func kinesisClient(rawURL string, query url.Values, format string) (core.Produce // 5. Send offer req.Action = "SDP_OFFER" - req.Payload, _ = json.Marshal(pion.SessionDescription{ + sessionDescription := pion.SessionDescription{ Type: pion.SDPTypeOffer, SDP: offer, - }) + } + if opts.SessionDescriptionModifier != nil { + req.Payload, _ = opts.SessionDescriptionModifier(&sessionDescription) + } else { + req.Payload, _ = json.Marshal(sessionDescription) + } if err = conn.WriteJSON(req); err != nil { return nil, err } @@ -218,5 +234,5 @@ func wyzeClient(rawURL string) (core.Producer, error) { "ice_servers": []string{string(kvs.Servers)}, } - return kinesisClient(kvs.URL, query, "webrtc/wyze") + return kinesisClient(kvs.URL, query, "webrtc/wyze", &kinesisClientOpts{}) } diff --git a/internal/webrtc/switchbot.go b/internal/webrtc/switchbot.go new file mode 100644 index 00000000..09d0c5b1 --- /dev/null +++ b/internal/webrtc/switchbot.go @@ -0,0 +1,62 @@ +package webrtc + +import ( + "encoding/json" + "net/url" + "strings" + + "github.com/AlexxIT/go2rtc/pkg/core" + pion "github.com/pion/webrtc/v3" +) + +// SessionDescription is used to expose local and remote session descriptions. +type SwitchBotSessionDescription struct { + Type string `json:"type"` + SDP string `json:"sdp"` + Resolution SwitchBotResolution `json:"resolution"` + PlayType int `json:"play_type"` +} + +func switchbotClient(rawURL string, query url.Values) (core.Producer, error) { + return kinesisClient(rawURL, query, "webrtc/switchbot", &kinesisClientOpts{ + SessionDescriptionModifier: func(sd *pion.SessionDescription) ([]byte, error) { + resolution, ok := parseSwitchBotResolution(query.Get("resolution")) + if !ok { + resolution = SwitchBotResolutionSD + } + json, err := json.Marshal(SwitchBotSessionDescription{ + Type: sd.Type.String(), + SDP: sd.SDP, + Resolution: resolution, + PlayType: 0, + }) + return json, err + }, + MediaModifier: func() ([]*core.Media, error) { + return []*core.Media{ + {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, + //{Kind: core.KindAudio, Direction: core.DirectionRecvonly}, + //{Kind: core.KindAudio, Direction: core.DirectionSendRecv}, + //{Kind: "Data", Direction: core.DirectionSendRecv}, + }, nil + }, + }) +} + +type SwitchBotResolution int + +const ( + SwitchBotResolutionHD SwitchBotResolution = 0 + SwitchBotResolutionSD = 1 +) + +func parseSwitchBotResolution(str string) (SwitchBotResolution, bool) { + var ( + resolutionMap = map[string]SwitchBotResolution{ + "hd": SwitchBotResolutionHD, + "sd": SwitchBotResolutionSD, + } + ) + c, ok := resolutionMap[strings.ToLower(str)] + return c, ok +} From 117d767f0515d346ae53840fcef4307e3411addb Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 9 Mar 2025 18:44:32 +0300 Subject: [PATCH 2/3] Code refactoring for SwitchBot format support #1629 --- internal/webrtc/client.go | 2 +- internal/webrtc/kinesis.go | 57 ++++++++++++------------- internal/webrtc/switchbot.go | 80 +++++++++++++----------------------- 3 files changed, 57 insertions(+), 82 deletions(-) diff --git a/internal/webrtc/client.go b/internal/webrtc/client.go index 9f21f4e9..106b603e 100644 --- a/internal/webrtc/client.go +++ b/internal/webrtc/client.go @@ -41,7 +41,7 @@ func streamsHandler(rawURL string) (core.Producer, error) { // https://aws.amazon.com/kinesis/video-streams/ // https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html // https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc - return kinesisClient(rawURL, query, "webrtc/kinesis", &kinesisClientOpts{}) + return kinesisClient(rawURL, query, "webrtc/kinesis", nil) } else if format == "openipc" { return openIPCClient(rawURL, query) } else if format == "switchbot" { diff --git a/internal/webrtc/kinesis.go b/internal/webrtc/kinesis.go index 42f76dce..b11d1d31 100644 --- a/internal/webrtc/kinesis.go +++ b/internal/webrtc/kinesis.go @@ -34,12 +34,10 @@ func (k kinesisResponse) String() string { return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload) } -type kinesisClientOpts struct { - SessionDescriptionModifier func(*pion.SessionDescription) ([]byte, error) - MediaModifier func() ([]*core.Media, error) -} - -func kinesisClient(rawURL string, query url.Values, format string, opts *kinesisClientOpts) (core.Producer, error) { +func kinesisClient( + rawURL string, query url.Values, format string, + sdpOffer func(prod *webrtc.Conn, query url.Values) (any, error), +) (core.Producer, error) { // 1. Connect to signalign server conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil) if err != nil { @@ -113,34 +111,33 @@ func kinesisClient(rawURL string, query url.Values, format string, opts *kinesis } }) - medias := []*core.Media{ - {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, - {Kind: core.KindAudio, Direction: core.DirectionRecvonly}, - } - if opts.MediaModifier != nil { - medias, err = opts.MediaModifier() - if err != nil { + var payload any + + if sdpOffer == nil { + medias := []*core.Media{ + {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, + {Kind: core.KindAudio, Direction: core.DirectionRecvonly}, + } + + // 4. Create offer + var offer string + if offer, err = prod.CreateOffer(medias); err != nil { + return nil, err + } + + // 5. Send offer + payload = pion.SessionDescription{ + Type: pion.SDPTypeOffer, + SDP: offer, + } + } else { + if payload, err = sdpOffer(prod, query); err != nil { return nil, err } } - // 4. Create offer - offer, err := prod.CreateOffer(medias) - if err != nil { - return nil, err - } - - // 5. Send offer req.Action = "SDP_OFFER" - sessionDescription := pion.SessionDescription{ - Type: pion.SDPTypeOffer, - SDP: offer, - } - if opts.SessionDescriptionModifier != nil { - req.Payload, _ = opts.SessionDescriptionModifier(&sessionDescription) - } else { - req.Payload, _ = json.Marshal(sessionDescription) - } + req.Payload, _ = json.Marshal(payload) if err = conn.WriteJSON(req); err != nil { return nil, err } @@ -234,5 +231,5 @@ func wyzeClient(rawURL string) (core.Producer, error) { "ice_servers": []string{string(kvs.Servers)}, } - return kinesisClient(kvs.URL, query, "webrtc/wyze", &kinesisClientOpts{}) + return kinesisClient(kvs.URL, query, "webrtc/wyze", nil) } diff --git a/internal/webrtc/switchbot.go b/internal/webrtc/switchbot.go index 09d0c5b1..5ece88ae 100644 --- a/internal/webrtc/switchbot.go +++ b/internal/webrtc/switchbot.go @@ -1,62 +1,40 @@ package webrtc import ( - "encoding/json" "net/url" - "strings" "github.com/AlexxIT/go2rtc/pkg/core" - pion "github.com/pion/webrtc/v3" + "github.com/AlexxIT/go2rtc/pkg/webrtc" ) -// SessionDescription is used to expose local and remote session descriptions. -type SwitchBotSessionDescription struct { - Type string `json:"type"` - SDP string `json:"sdp"` - Resolution SwitchBotResolution `json:"resolution"` - PlayType int `json:"play_type"` -} - func switchbotClient(rawURL string, query url.Values) (core.Producer, error) { - return kinesisClient(rawURL, query, "webrtc/switchbot", &kinesisClientOpts{ - SessionDescriptionModifier: func(sd *pion.SessionDescription) ([]byte, error) { - resolution, ok := parseSwitchBotResolution(query.Get("resolution")) - if !ok { - resolution = SwitchBotResolutionSD - } - json, err := json.Marshal(SwitchBotSessionDescription{ - Type: sd.Type.String(), - SDP: sd.SDP, - Resolution: resolution, - PlayType: 0, - }) - return json, err - }, - MediaModifier: func() ([]*core.Media, error) { - return []*core.Media{ - {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, - //{Kind: core.KindAudio, Direction: core.DirectionRecvonly}, - //{Kind: core.KindAudio, Direction: core.DirectionSendRecv}, - //{Kind: "Data", Direction: core.DirectionSendRecv}, - }, nil - }, + return kinesisClient(rawURL, query, "webrtc/switchbot", func(prod *webrtc.Conn, query url.Values) (any, error) { + medias := []*core.Media{ + {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, + } + + offer, err := prod.CreateOffer(medias) + if err != nil { + return nil, err + } + + v := struct { + Type string `json:"type"` + SDP string `json:"sdp"` + Resolution int `json:"resolution"` + PlayType int `json:"play_type"` + }{ + Type: "offer", + SDP: offer, + } + + switch query.Get("resolution") { + case "hd": + v.Resolution = 0 + case "sd": + v.Resolution = 1 + } + + return v, nil }) } - -type SwitchBotResolution int - -const ( - SwitchBotResolutionHD SwitchBotResolution = 0 - SwitchBotResolutionSD = 1 -) - -func parseSwitchBotResolution(str string) (SwitchBotResolution, bool) { - var ( - resolutionMap = map[string]SwitchBotResolution{ - "hd": SwitchBotResolutionHD, - "sd": SwitchBotResolutionSD, - } - ) - c, ok := resolutionMap[strings.ToLower(str)] - return c, ok -} From 761ff7ed5a513a2c1939a089536223ed68f2a5f5 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 9 Mar 2025 18:48:18 +0300 Subject: [PATCH 3/3] Update readme for SwitchBot --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index cec2f247..90a2537f 100644 --- a/README.md +++ b/README.md @@ -684,16 +684,16 @@ Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-str **switchbot** -Support connection to [SwitchBot](https://us.switch-bot.com/) cameras that are based on Kinesis Video Streams. Specifically, this includes [Pan/Tilt Cam Plus 2K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-2k) and [Pan/Tilt Cam Plus 3K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-3k). (`Outdoor Spotlight Cam 1080P`,`Outdoor Spotlight Cam 2K`, `Pan/Tilt Cam`,`Pan/Tilt Cam 2K`, `Indoor Cam` are based on Tuya, so this feature is not available .) +Support connection to [SwitchBot](https://us.switch-bot.com/) cameras that are based on Kinesis Video Streams. Specifically, this includes [Pan/Tilt Cam Plus 2K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-2k) and [Pan/Tilt Cam Plus 3K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-3k). `Outdoor Spotlight Cam 1080P`, `Outdoor Spotlight Cam 2K`, `Pan/Tilt Cam`, `Pan/Tilt Cam 2K`, `Indoor Cam` are based on Tuya, so this feature is not available. ```yaml streams: - webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1 - webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1 - webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}] - webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze - webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}] - webrtc-switchbot: webrtc:wss://...amazonaws.com/?...#format=switchbot#resolution=HD#client_id=...#ice_servers=[{...},{...}] + webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1 + webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1 + webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}] + webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze + webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}] + webrtc-switchbot: webrtc:wss://...amazonaws.com/?...#format=switchbot#resolution=hd#client_id=...#ice_servers=[{...},{...}] ``` **PS.** For `kinesis` sources you can use [echo](#source-echo) to get connection params using `bash`/`python` or any other script language.