Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for xtend_tuya #1412

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions internal/webrtc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
125 changes: 125 additions & 0 deletions internal/webrtc/xtend_tuya.go
Original file line number Diff line number Diff line change
@@ -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
}