diff --git a/README.md b/README.md index 65ebf2b7..90a2537f 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..106b603e 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", nil) } 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..b11d1d31 100644 --- a/internal/webrtc/kinesis.go +++ b/internal/webrtc/kinesis.go @@ -34,7 +34,10 @@ 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) { +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 { @@ -108,23 +111,33 @@ func kinesisClient(rawURL string, query url.Values, format string) (core.Produce } }) - medias := []*core.Media{ - {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, - {Kind: core.KindAudio, Direction: core.DirectionRecvonly}, + 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" - req.Payload, _ = json.Marshal(pion.SessionDescription{ - Type: pion.SDPTypeOffer, - SDP: offer, - }) + req.Payload, _ = json.Marshal(payload) if err = conn.WriteJSON(req); err != nil { return nil, err } @@ -218,5 +231,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", nil) } diff --git a/internal/webrtc/switchbot.go b/internal/webrtc/switchbot.go new file mode 100644 index 00000000..5ece88ae --- /dev/null +++ b/internal/webrtc/switchbot.go @@ -0,0 +1,40 @@ +package webrtc + +import ( + "net/url" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/webrtc" +) + +func switchbotClient(rawURL string, query url.Values) (core.Producer, error) { + 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 + }) +}