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] =?UTF-8?q?=EF=BB=BFAdd=20client=20for=20SwitchBot=20Camer?= =?UTF-8?q?a=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 +}