diff --git a/README.md b/README.md index 70ad4712..d5695262 100644 --- a/README.md +++ b/README.md @@ -679,16 +679,23 @@ 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). +**xtend_tuya** + +Supports [Home Assistant eXtended Tuya](https://github.com/azerty9971/xtend_tuya), using WebRTC WHEP protocol. You need to specify the base URL of your Home Assistant instance, the device ID, the channel (low, high or an integer) and the auth_token that you generate in HA in your user profile (long term access token) +Documentation is available at https://github.com/azerty9971/xtend_tuya/blob/main/docs/configure_go2rtc.md + ```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-xtend_tuya: webrtc:http://...ip_or_host_of_ha#format=xtend_tuya#device_id=...#channel=high#auth_token=... ``` **PS.** For `kinesis` sources you can use [echo](#source-echo) to get connection params using `bash`/`python` or any other script language. +**PS2** For `xtend_tuya` the high quality stream is not yet retrieved, the low quality stream is returned instead, this will be fixed soon in xtend_tuya #### Source: WebTorrent diff --git a/internal/webrtc/client.go b/internal/webrtc/client.go index d42c51dd..7a468430 100644 --- a/internal/webrtc/client.go +++ b/internal/webrtc/client.go @@ -54,6 +54,8 @@ func streamsHandler(rawURL string) (core.Producer, error) { } else if format == "wyze" { // https://github.com/mrlt8/docker-wyze-bridge return wyzeClient(rawURL) + } else if format == "xtend_tuya" { + return xtendTuyaWhepClient(rawURL, query) } else { return whepClient(rawURL) } diff --git a/internal/webrtc/xtend_tuya.go b/internal/webrtc/xtend_tuya.go new file mode 100644 index 00000000..abd403b3 --- /dev/null +++ b/internal/webrtc/xtend_tuya.go @@ -0,0 +1,125 @@ +package webrtc + +import ( + "io" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/webrtc" + pion "github.com/pion/webrtc/v3" +) + +// whepClient - support WebRTC-HTTP Egress Protocol (WHEP) +// ex: http://localhost:1984/api/webrtc?src=camera1 +func xtendTuyaWhepClient(url string, query url.Values) (core.Producer, error) { + // 1. Prepare variables + api_path := "/api/xtend_tuya/" + api_service_sdp_exchange := "webrtc_sdp_exchange" + api_service_get_ice_servers := "webrtc_get_ice_servers" + device_id := query.Get("device_id") + auth_token := query.Get("auth_token") + channel := query.Get("channel") + session_id := device_id + strconv.FormatInt(time.Now().UTC().Unix(), 10) + + // 2. Get ICE servers from HA + conf := pion.Configuration{} + var err error + + completeUrl := url + api_path + api_service_get_ice_servers + "?device_id=" + device_id + "&session_id=" + session_id + "&format=GO2RTC" + req, err := http.NewRequest("GET", completeUrl, nil) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+auth_token) + + client := http.Client{Timeout: time.Second * 5000} + defer client.CloseIdleConnections() + + res, err := client.Do(req) + if err != nil { + return nil, err + } + ice_servers, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + conf.ICEServers, err = webrtc.UnmarshalICEServers([]byte(ice_servers)) + if err != nil { + log.Warn().Err(err).Caller().Send() + } + + // 3. Create Peer Connection + api, err := webrtc.NewAPI() + if err != nil { + return nil, err + } + + pc, err := api.NewPeerConnection(conf) + if err != nil { + return nil, err + } + + prod := webrtc.NewConn(pc) + prod.FormatName = "webrtc/xtend_tuya" + prod.Mode = core.ModeActiveProducer + prod.Protocol = "http" + prod.URL = url + + medias := []*core.Media{ + {Kind: core.KindAudio, Direction: core.DirectionSendRecv}, + {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, + } + + // 4. Create offer + offer, err := prod.CreateCompleteOffer(medias) + if err != nil { + return nil, err + } + + // shorter sdp, remove a=extmap... line, device ONLY allow 8KB json payload + re := regexp.MustCompile(`\r\na=extmap[^\r\n]*`) + offer = re.ReplaceAllString(offer, "") + + // 5. Send offer + completeUrl = url + api_path + api_service_sdp_exchange + "?device_id=" + device_id + "&session_id=" + session_id + "&channel=" + channel + req, err = http.NewRequest("POST", completeUrl, strings.NewReader(offer)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", MimeSDP) + req.Header.Set("Authorization", "Bearer "+auth_token) + + client = http.Client{Timeout: time.Second * 5000} + defer client.CloseIdleConnections() + + res, err = client.Do(req) + if err != nil { + return nil, err + } + + // 6. Get answer + answer, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + desc := pion.SessionDescription{ + Type: pion.SDPTypePranswer, + SDP: string(answer), + } + if err = pc.SetRemoteDescription(desc); err != nil { + return nil, err + } + + if err = prod.SetAnswer(string(answer)); err != nil { + return nil, err + } + + return prod, nil +}