refactor
This commit is contained in:
+279
-201
@@ -22,151 +22,165 @@ type TuyaClient struct {
|
||||
apiURL string
|
||||
rtspURL string
|
||||
hlsURL string
|
||||
sessionID string
|
||||
clientID string
|
||||
deviceID string
|
||||
sessionId string
|
||||
clientId string
|
||||
clientSecret string
|
||||
deviceId string
|
||||
accessToken string
|
||||
refreshToken string
|
||||
secret string
|
||||
expireTime int64
|
||||
uid string
|
||||
motoID string
|
||||
motoId string
|
||||
auth string
|
||||
skill *Skill
|
||||
iceServers []pionWebrtc.ICEServer
|
||||
medias []*core.Media
|
||||
hasBackchannel bool
|
||||
}
|
||||
|
||||
type Token struct {
|
||||
UID string `json:"uid"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpireTime int64 `json:"expire_time"`
|
||||
}
|
||||
|
||||
type AllocateRequest struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type AllocateResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Result struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"result"`
|
||||
UID string `json:"uid"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpireTime int64 `json:"expire_time"`
|
||||
}
|
||||
|
||||
type AudioAttributes struct {
|
||||
CallMode []int `json:"call_mode"` // 1 = one way, 2 = two way
|
||||
HardwareCapability []int `json:"hardware_capability"` // 1 = mic, 2 = speaker
|
||||
CallMode []int `json:"call_mode"` // 1 = one way, 2 = two way
|
||||
HardwareCapability []int `json:"hardware_capability"` // 1 = mic, 2 = speaker
|
||||
}
|
||||
|
||||
type OpenApiICE struct {
|
||||
Urls string `json:"urls"`
|
||||
Username string `json:"username"`
|
||||
Credential string `json:"credential"`
|
||||
TTL int `json:"ttl"`
|
||||
Urls string `json:"urls"`
|
||||
Username string `json:"username"`
|
||||
Credential string `json:"credential"`
|
||||
TTL int `json:"ttl"`
|
||||
}
|
||||
|
||||
type WebICE struct {
|
||||
Urls string `json:"urls"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Credential string `json:"credential,omitempty"`
|
||||
Urls string `json:"urls"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Credential string `json:"credential,omitempty"`
|
||||
}
|
||||
|
||||
type P2PConfig struct {
|
||||
Ices []OpenApiICE `json:"ices"`
|
||||
Ices []OpenApiICE `json:"ices"`
|
||||
}
|
||||
|
||||
type Skill struct {
|
||||
WebRTC int `json:"webrtc"`
|
||||
WebRTC int `json:"webrtc"`
|
||||
Audios []struct {
|
||||
Channels int `json:"channels"`
|
||||
DataBit int `json:"dataBit"`
|
||||
CodecType int `json:"codecType"`
|
||||
SampleRate int `json:"sampleRate"`
|
||||
Channels int `json:"channels"`
|
||||
DataBit int `json:"dataBit"`
|
||||
CodecType int `json:"codecType"`
|
||||
SampleRate int `json:"sampleRate"`
|
||||
} `json:"audios"`
|
||||
Videos []struct {
|
||||
StreamType int `json:"streamType"` // streamType = 2 => H265 and streamType = 4 => H264
|
||||
ProfileId string `json:"profileId"`
|
||||
Width int `json:"width"`
|
||||
CodecType int `json:"codecType"`
|
||||
SampleRate int `json:"sampleRate"`
|
||||
Height int `json:"height"`
|
||||
StreamType int `json:"streamType"` // streamType = 2 => main stream - streamType = 4 => sub stream
|
||||
ProfileId string `json:"profileId"`
|
||||
Width int `json:"width"`
|
||||
CodecType int `json:"codecType"`
|
||||
SampleRate int `json:"sampleRate"`
|
||||
Height int `json:"height"`
|
||||
} `json:"videos"`
|
||||
}
|
||||
|
||||
type WebRTConfig struct {
|
||||
AudioAttributes AudioAttributes `json:"audio_attributes"`
|
||||
Auth string `json:"auth"`
|
||||
ID string `json:"id"`
|
||||
MotoID string `json:"moto_id"`
|
||||
P2PConfig P2PConfig `json:"p2p_config"`
|
||||
Skill string `json:"skill"`
|
||||
SupportsWebRTC bool `json:"supports_webrtc"`
|
||||
VideoClaritiy int `json:"video_clarity"`
|
||||
}
|
||||
|
||||
type WebRTCConfigResponse struct {
|
||||
Result WebRTConfig `json:"result"`
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
Result Token `json:"result"`
|
||||
}
|
||||
|
||||
type OpenIoTHubConfigRequest struct {
|
||||
UID string `json:"uid"`
|
||||
UniqueID string `json:"unique_id"`
|
||||
LinkType string `json:"link_type"`
|
||||
Topics string `json:"topics"`
|
||||
}
|
||||
|
||||
type OpenIoTHubConfigResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Result OpenIoTHubConfig `json:"result"`
|
||||
AudioAttributes AudioAttributes `json:"audio_attributes"`
|
||||
Auth string `json:"auth"`
|
||||
ID string `json:"id"`
|
||||
MotoID string `json:"moto_id"`
|
||||
P2PConfig P2PConfig `json:"p2p_config"`
|
||||
ProtocolVersion string `json:"protocol_version"`
|
||||
Skill string `json:"skill"`
|
||||
SupportsWebRTCRecord bool `json:"supports_webrtc_record"`
|
||||
SupportsWebRTC bool `json:"supports_webrtc"`
|
||||
VedioClaritiy int `json:"vedio_clarity"`
|
||||
VideoClaritiy int `json:"video_clarity"`
|
||||
VideoClarities []int `json:"video_clarities"`
|
||||
}
|
||||
|
||||
type OpenIoTHubConfig struct {
|
||||
Url string `json:"url"`
|
||||
ClientID string `json:"client_id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
|
||||
Url string `json:"url"`
|
||||
ClientID string `json:"client_id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
SinkTopic struct {
|
||||
IPC string `json:"ipc"`
|
||||
} `json:"sink_topic"`
|
||||
|
||||
SourceSink struct {
|
||||
IPC string `json:"ipc"`
|
||||
} `json:"source_topic"`
|
||||
ExpireTime int `json:"expire_time"`
|
||||
}
|
||||
|
||||
ExpireTime int `json:"expire_time"`
|
||||
type WebRTCConfigResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Result WebRTConfig `json:"result"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Code int `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Result Token `json:"result"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Code int `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
type AllocateRequest struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type AllocateResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Result struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"result"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Code int `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
type OpenIoTHubConfigRequest struct {
|
||||
UID string `json:"uid"`
|
||||
UniqueID string `json:"unique_id"`
|
||||
LinkType string `json:"link_type"`
|
||||
Topics string `json:"topics"`
|
||||
}
|
||||
|
||||
type OpenIoTHubConfigResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Result OpenIoTHubConfig `json:"result"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Code int `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
defaultTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
func NewTuyaClient(openAPIURL string, deviceID string, uid string, clientID string, secret string, streamType string) (*TuyaClient, error) {
|
||||
func NewTuyaClient(openAPIURL string, deviceId string, uid string, clientId string, clientSecret string, streamMode string) (*TuyaClient, error) {
|
||||
client := &TuyaClient{
|
||||
httpClient: &http.Client{Timeout: defaultTimeout},
|
||||
mqtt: &TuyaMQTT{waiter: core.Waiter{}},
|
||||
apiURL: openAPIURL,
|
||||
sessionID: core.RandString(6, 62),
|
||||
clientID: clientID,
|
||||
deviceID: deviceID,
|
||||
secret: secret,
|
||||
sessionId: core.RandString(6, 62),
|
||||
clientId: clientId,
|
||||
deviceId: deviceId,
|
||||
clientSecret: clientSecret,
|
||||
uid: uid,
|
||||
hasBackchannel: false,
|
||||
}
|
||||
|
||||
if err := client.InitToken(); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize token: %w", err)
|
||||
}
|
||||
|
||||
if streamType == "rtsp" {
|
||||
if streamMode == "rtsp" {
|
||||
if err := client.GetStreamUrl("rtsp"); err != nil {
|
||||
return nil, fmt.Errorf("failed to get RTSP URL: %w", err)
|
||||
}
|
||||
} else if streamType == "hls" {
|
||||
} else if streamMode == "hls" {
|
||||
if err := client.GetStreamUrl("hls"); err != nil {
|
||||
return nil, fmt.Errorf("failed to get HLS URL: %w", err)
|
||||
}
|
||||
@@ -193,14 +207,14 @@ func(c *TuyaClient) Request(method string, url string, body any) ([]byte, error)
|
||||
if body != nil {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
bodyReader = bytes.NewReader(jsonBody)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, bodyReader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ts := time.Now().UnixNano() / 1000000
|
||||
@@ -212,24 +226,24 @@ func(c *TuyaClient) Request(method string, url string, body any) ([]byte, error)
|
||||
req.Header.Set("Access-Control-Allow-Methods", "*")
|
||||
req.Header.Set("Access-Control-Allow-Headers", "*")
|
||||
req.Header.Set("mode", "no-cors")
|
||||
req.Header.Set("client_id", c.clientID)
|
||||
req.Header.Set("client_id", c.clientId)
|
||||
req.Header.Set("access_token", c.accessToken)
|
||||
req.Header.Set("sign", sign)
|
||||
req.Header.Set("t", strconv.FormatInt(ts, 10))
|
||||
|
||||
response, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
res, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("request failed with status code %d: %s", response.StatusCode, string(res))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
@@ -243,13 +257,17 @@ func(c *TuyaClient) InitToken() (err error) {
|
||||
|
||||
body, err := c.Request("GET", url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get token: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var tokenResponse TokenResponse
|
||||
err = json.Unmarshal(body, &tokenResponse)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal token response: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !tokenResponse.Success {
|
||||
return fmt.Errorf("error: %s", tokenResponse.Msg)
|
||||
}
|
||||
|
||||
c.accessToken = tokenResponse.Result.AccessToken
|
||||
@@ -260,119 +278,139 @@ func(c *TuyaClient) InitToken() (err error) {
|
||||
}
|
||||
|
||||
func(c *TuyaClient) InitDevice() (err error) {
|
||||
url := fmt.Sprintf("https://%s/v1.0/users/%s/devices/%s/webrtc-configs", c.apiURL, c.uid, c.deviceID)
|
||||
url := fmt.Sprintf("https://%s/v1.0/users/%s/devices/%s/webrtc-configs", c.apiURL, c.uid, c.deviceId)
|
||||
|
||||
body, err := c.Request("GET", url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get webrtc-configs: %w", err)
|
||||
}
|
||||
|
||||
var webRTCConfigResponse WebRTCConfigResponse
|
||||
err = json.Unmarshal(body, &webRTCConfigResponse)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal webrtc-configs response: %w", err)
|
||||
}
|
||||
|
||||
c.motoID = webRTCConfigResponse.Result.MotoID
|
||||
c.auth = webRTCConfigResponse.Result.Auth
|
||||
|
||||
var skill Skill
|
||||
err = json.Unmarshal([]byte(webRTCConfigResponse.Result.Skill), &skill)
|
||||
body, err := c.Request("GET", url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal skill: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var audioDirection string
|
||||
if contains(webRTCConfigResponse.Result.AudioAttributes.CallMode, 2) && contains(webRTCConfigResponse.Result.AudioAttributes.HardwareCapability, 1) {
|
||||
audioDirection = core.DirectionSendRecv
|
||||
} else {
|
||||
audioDirection = core.DirectionRecvonly
|
||||
var webRTCConfigResponse WebRTCConfigResponse
|
||||
err = json.Unmarshal(body, &webRTCConfigResponse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !webRTCConfigResponse.Success {
|
||||
return fmt.Errorf("error: %s", webRTCConfigResponse.Msg)
|
||||
}
|
||||
|
||||
c.medias = make([]*core.Media, 0)
|
||||
if len(skill.Audios) > 0 {
|
||||
for _, audio := range skill.Audios {
|
||||
c.medias = append(c.medias, &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: audioDirection,
|
||||
Codecs: []*core.Codec{
|
||||
{
|
||||
Name: "PCMU",
|
||||
ClockRate: uint32(audio.SampleRate),
|
||||
Channels: uint8(audio.Channels),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
c.medias = append(c.medias, &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{
|
||||
|
||||
c.motoId = webRTCConfigResponse.Result.MotoID
|
||||
c.auth = webRTCConfigResponse.Result.Auth
|
||||
|
||||
c.skill = &Skill{
|
||||
Audios: []struct {
|
||||
Channels int `json:"channels"`
|
||||
DataBit int `json:"dataBit"`
|
||||
CodecType int `json:"codecType"`
|
||||
SampleRate int `json:"sampleRate"`
|
||||
}{},
|
||||
Videos: []struct {
|
||||
StreamType int `json:"streamType"`
|
||||
ProfileId string `json:"profileId"`
|
||||
Width int `json:"width"`
|
||||
CodecType int `json:"codecType"`
|
||||
SampleRate int `json:"sampleRate"`
|
||||
Height int `json:"height"`
|
||||
}{},
|
||||
}
|
||||
|
||||
if webRTCConfigResponse.Result.Skill != "" {
|
||||
_ = json.Unmarshal([]byte(webRTCConfigResponse.Result.Skill), c.skill)
|
||||
}
|
||||
|
||||
var audioDirection string
|
||||
if contains(webRTCConfigResponse.Result.AudioAttributes.CallMode, 2) &&
|
||||
contains(webRTCConfigResponse.Result.AudioAttributes.HardwareCapability, 1) {
|
||||
audioDirection = core.DirectionSendRecv
|
||||
c.hasBackchannel = true
|
||||
} else {
|
||||
audioDirection = core.DirectionRecvonly
|
||||
c.hasBackchannel = false
|
||||
}
|
||||
|
||||
c.medias = make([]*core.Media, 0)
|
||||
|
||||
if len(c.skill.Audios) > 0 {
|
||||
// Use the first Audio-Codec
|
||||
audio := c.skill.Audios[0]
|
||||
|
||||
c.medias = append(c.medias, &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: audioDirection,
|
||||
Codecs: []*core.Codec{
|
||||
{
|
||||
Name: "PCMU",
|
||||
ClockRate: uint32(audio.SampleRate),
|
||||
Channels: uint8(audio.Channels),
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Use default values for Audio
|
||||
c.medias = append(c.medias, &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{
|
||||
{
|
||||
Name: "PCMU",
|
||||
ClockRate: uint32(8000),
|
||||
Channels: uint8(1),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if len(c.skill.Videos) > 0 {
|
||||
// Use the first Video-Codec
|
||||
video := c.skill.Videos[0]
|
||||
|
||||
c.medias = append(c.medias, &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{
|
||||
{
|
||||
Name: core.CodecH265,
|
||||
ClockRate: uint32(video.SampleRate),
|
||||
PayloadType: 96,
|
||||
},
|
||||
{
|
||||
Name: "PCMU",
|
||||
ClockRate: uint32(8000),
|
||||
Channels: uint8(1),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
Name: core.CodecH264,
|
||||
ClockRate: uint32(video.SampleRate),
|
||||
PayloadType: 96,
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Use default values for Video
|
||||
c.medias = append(c.medias, &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{
|
||||
{
|
||||
Name: core.CodecH264,
|
||||
ClockRate: uint32(90000),
|
||||
PayloadType: 96,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if len(skill.Videos) > 0 {
|
||||
// take only the first video codec
|
||||
video := skill.Videos[0]
|
||||
|
||||
var name string
|
||||
switch video.CodecType {
|
||||
case 4:
|
||||
name = core.CodecH265
|
||||
case 2:
|
||||
name = core.CodecH264
|
||||
default:
|
||||
name = core.CodecH264
|
||||
}
|
||||
|
||||
c.medias = append(c.medias, &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{
|
||||
{
|
||||
Name: name,
|
||||
ClockRate: uint32(video.SampleRate),
|
||||
PayloadType: 96,
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
c.medias = append(c.medias, &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{
|
||||
{
|
||||
Name: core.CodecH264,
|
||||
ClockRate: uint32(90000),
|
||||
PayloadType: 96,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
iceServersBytes, err := json.Marshal(&webRTCConfigResponse.Result.P2PConfig.Ices)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
iceServersBytes, err := json.Marshal(&webRTCConfigResponse.Result.P2PConfig.Ices)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal ICE servers: %w", err)
|
||||
}
|
||||
c.iceServers, err = webrtc.UnmarshalICEServers([]byte(iceServersBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
c.iceServers, err = webrtc.UnmarshalICEServers([]byte(iceServersBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal ICE servers: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func(c *TuyaClient) GetStreamUrl(streamType string) (err error) {
|
||||
url := fmt.Sprintf("https://%s/v1.0/devices/%s/stream/actions/allocate", c.apiURL, c.deviceID)
|
||||
url := fmt.Sprintf("https://%s/v1.0/devices/%s/stream/actions/allocate", c.apiURL, c.deviceId)
|
||||
|
||||
request := &AllocateRequest{
|
||||
Type: streamType,
|
||||
@@ -380,26 +418,24 @@ func(c *TuyaClient) GetStreamUrl(streamType string) (err error) {
|
||||
|
||||
body, err := c.Request("POST", url, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get rtsp url: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var allosResponse AllocateResponse
|
||||
err = json.Unmarshal(body, &allosResponse)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal stream response: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !allosResponse.Success {
|
||||
return fmt.Errorf("failed to get stream url: %s", string(body))
|
||||
return fmt.Errorf("error: %s", allosResponse.Msg)
|
||||
}
|
||||
|
||||
switch streamType {
|
||||
case "rtsp":
|
||||
c.rtspURL = allosResponse.Result.URL
|
||||
fmt.Printf("RTSP URL: %s\n", c.rtspURL)
|
||||
case "hls":
|
||||
c.hlsURL = allosResponse.Result.URL
|
||||
fmt.Printf("HLS URL: %s\n", c.hlsURL)
|
||||
default:
|
||||
return fmt.Errorf("unsupported stream type: %s", streamType)
|
||||
}
|
||||
@@ -419,24 +455,66 @@ func(c *TuyaClient) LoadHubConfig() (config *OpenIoTHubConfig, err error) {
|
||||
|
||||
body, err := c.Request("POST", url, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get OpenIoTHub config: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var openIoTHubConfigResponse OpenIoTHubConfigResponse
|
||||
err = json.Unmarshal(body, &openIoTHubConfigResponse)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal OpenIoTHub config response: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !openIoTHubConfigResponse.Success {
|
||||
return nil, fmt.Errorf("failed to get OpenIoTHub config: %s", string(body))
|
||||
return nil, fmt.Errorf("error: %s", openIoTHubConfigResponse.Msg)
|
||||
}
|
||||
|
||||
return &openIoTHubConfigResponse.Result, nil
|
||||
}
|
||||
|
||||
// Search the streamType based on the selection "main" or "sub"
|
||||
func (c *TuyaClient) getStreamType(streamChoice string) uint32 {
|
||||
// Default streamType if nothing is found
|
||||
defaultStreamType := uint32(1)
|
||||
|
||||
if c.skill == nil || len(c.skill.Videos) == 0 {
|
||||
return defaultStreamType
|
||||
}
|
||||
|
||||
// Find the highest and lowest resolution
|
||||
var highestResType uint32 = defaultStreamType
|
||||
var highestRes int = 0
|
||||
var lowestResType uint32 = defaultStreamType
|
||||
var lowestRes int = 0
|
||||
|
||||
for _, video := range c.skill.Videos {
|
||||
res := video.Width * video.Height
|
||||
|
||||
// Highest Resolution
|
||||
if res > highestRes {
|
||||
highestRes = res
|
||||
highestResType = uint32(video.StreamType)
|
||||
}
|
||||
|
||||
// Lower Resolution (or first if not set yet)
|
||||
if lowestRes == 0 || res < lowestRes {
|
||||
lowestRes = res
|
||||
lowestResType = uint32(video.StreamType)
|
||||
}
|
||||
}
|
||||
|
||||
// Return the streamType based on the selection
|
||||
switch streamChoice {
|
||||
case "main":
|
||||
return highestResType
|
||||
case "sub":
|
||||
return lowestResType
|
||||
default:
|
||||
return defaultStreamType
|
||||
}
|
||||
}
|
||||
|
||||
func(c *TuyaClient) calBusinessSign(ts int64) string {
|
||||
data := fmt.Sprintf("%s%s%s%d", c.clientID, c.accessToken, c.secret, ts)
|
||||
data := fmt.Sprintf("%s%s%s%d", c.clientId, c.accessToken, c.clientSecret, ts)
|
||||
val := md5.Sum([]byte(data))
|
||||
res := fmt.Sprintf("%X", val)
|
||||
return res
|
||||
|
||||
+26
-25
@@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"regexp"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
api *TuyaClient
|
||||
prod core.Producer
|
||||
done chan struct{}
|
||||
api *TuyaClient
|
||||
prod core.Producer
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -38,13 +38,13 @@ func Dial(rawURL string) (core.Producer, error) {
|
||||
query := u.Query()
|
||||
deviceID := query.Get("device_id")
|
||||
uid := query.Get("uid")
|
||||
clientID := query.Get("client_id")
|
||||
secret := query.Get("secret")
|
||||
resolution := query.Get("resolution")
|
||||
clientId := query.Get("client_id")
|
||||
clientSecret := query.Get("client_secret")
|
||||
streamType := query.Get("type")
|
||||
useRTSP := streamType == "rtsp"
|
||||
useHLS := streamType == "hls"
|
||||
useWebRTC := streamType == "webrtc" || streamType == ""
|
||||
streamMode := query.Get("mode")
|
||||
useRTSP := streamMode == "rtsp"
|
||||
useHLS := streamMode == "hls"
|
||||
useWebRTC := streamMode == "webrtc" || streamMode == ""
|
||||
|
||||
// check if host is correct
|
||||
switch u.Hostname() {
|
||||
@@ -58,8 +58,12 @@ func Dial(rawURL string) (core.Producer, error) {
|
||||
return nil, fmt.Errorf("tuya: wrong host %s", u.Hostname())
|
||||
}
|
||||
|
||||
if deviceID == "" || uid == "" || clientID == "" || secret == "" {
|
||||
return nil, errors.New("tuya: wrong query")
|
||||
if deviceID == "" || clientId == "" || clientSecret == "" {
|
||||
return nil, errors.New("tuya: no device_id, client_id or client_secret")
|
||||
}
|
||||
|
||||
if useWebRTC && uid == "" {
|
||||
return nil, errors.New("tuya: no uid")
|
||||
}
|
||||
|
||||
if !useRTSP && !useHLS && !useWebRTC {
|
||||
@@ -67,7 +71,7 @@ func Dial(rawURL string) (core.Producer, error) {
|
||||
}
|
||||
|
||||
// Initialize Tuya API client
|
||||
tuyaAPI, err := NewTuyaClient(u.Hostname(), deviceID, uid, clientID, secret, streamType)
|
||||
tuyaAPI, err := NewTuyaClient(u.Hostname(), deviceID, uid, clientId, clientSecret, streamType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -157,7 +161,7 @@ func Dial(rawURL string) (core.Producer, error) {
|
||||
}
|
||||
|
||||
client.api.mqtt.handleError = func(err error) {
|
||||
fmt.Printf("Tuya error: %s\n", err.Error())
|
||||
// fmt.Printf("tuya: error: %s\n", err.Error())
|
||||
client.Stop()
|
||||
}
|
||||
|
||||
@@ -188,21 +192,18 @@ func Dial(rawURL string) (core.Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// horter sdp, remove a=extmap... line, device ONLY allow 8KB json payload
|
||||
re := regexp.MustCompile(`\r\na=extmap[^\r\n]*`)
|
||||
offer = re.ReplaceAllString(offer, "")
|
||||
|
||||
// Send offer
|
||||
client.api.sendOffer(offer)
|
||||
client.api.sendOffer(offer, tuyaAPI.getStreamType(streamType))
|
||||
sendOffer.Done(nil)
|
||||
|
||||
if err = connState.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resolution != "" {
|
||||
value, err := strconv.Atoi(resolution)
|
||||
if err == nil {
|
||||
client.api.sendResolution(value)
|
||||
}
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
}
|
||||
@@ -216,11 +217,11 @@ func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver,
|
||||
}
|
||||
|
||||
func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
if webrtcProd, ok := c.prod.(*webrtc.Conn); ok {
|
||||
return webrtcProd.AddTrack(media, codec, track)
|
||||
if prod, ok := c.prod.(*webrtc.Conn); ok {
|
||||
return prod.AddTrack(media, codec, track)
|
||||
}
|
||||
|
||||
return fmt.Errorf("add track not supported")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
|
||||
+34
-34
@@ -24,64 +24,64 @@ type TuyaMQTT struct {
|
||||
}
|
||||
|
||||
type MqttFrameHeader struct {
|
||||
Type string `json:"type"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
SubDevID string `json:"sub_dev_id"`
|
||||
SessionID string `json:"sessionid"`
|
||||
MotoID string `json:"moto_id"`
|
||||
TransactionID string `json:"tid"`
|
||||
Type string `json:"type"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
SubDevID string `json:"sub_dev_id"`
|
||||
SessionID string `json:"sessionid"`
|
||||
MotoID string `json:"moto_id"`
|
||||
TransactionID string `json:"tid"`
|
||||
}
|
||||
|
||||
type MqttFrame struct {
|
||||
Header MqttFrameHeader `json:"header"`
|
||||
Message json.RawMessage `json:"msg"`
|
||||
Header MqttFrameHeader `json:"header"`
|
||||
Message json.RawMessage `json:"msg"`
|
||||
}
|
||||
|
||||
type OfferFrame struct {
|
||||
Mode string `json:"mode"`
|
||||
Sdp string `json:"sdp"`
|
||||
StreamType uint32 `json:"stream_type"`
|
||||
Auth string `json:"auth"`
|
||||
Mode string `json:"mode"`
|
||||
Sdp string `json:"sdp"`
|
||||
StreamType uint32 `json:"stream_type"`
|
||||
Auth string `json:"auth"`
|
||||
}
|
||||
|
||||
type AnswerFrame struct {
|
||||
Mode string `json:"mode"`
|
||||
Sdp string `json:"sdp"`
|
||||
Mode string `json:"mode"`
|
||||
Sdp string `json:"sdp"`
|
||||
}
|
||||
|
||||
type CandidateFrame struct {
|
||||
Mode string `json:"mode"`
|
||||
Candidate string `json:"candidate"`
|
||||
Mode string `json:"mode"`
|
||||
Candidate string `json:"candidate"`
|
||||
}
|
||||
|
||||
type ResolutionFrame struct {
|
||||
Mode string `json:"mode"`
|
||||
Value int `json:"value"`
|
||||
Mode string `json:"mode"`
|
||||
Value int `json:"value"`
|
||||
}
|
||||
|
||||
type DisconnectFrame struct {
|
||||
Mode string `json:"mode"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
type MqttMessage struct {
|
||||
Protocol int `json:"protocol"`
|
||||
Pv string `json:"pv"`
|
||||
T int64 `json:"t"`
|
||||
Data MqttFrame `json:"data"`
|
||||
Protocol int `json:"protocol"`
|
||||
Pv string `json:"pv"`
|
||||
T int64 `json:"t"`
|
||||
Data MqttFrame `json:"data"`
|
||||
}
|
||||
|
||||
func(c *TuyaClient) StartMQTT() error {
|
||||
hubConfig, err := c.LoadHubConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load hub config: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.mqtt.publishTopic = hubConfig.SinkTopic.IPC
|
||||
c.mqtt.subscribeTopic = hubConfig.SourceSink.IPC
|
||||
|
||||
c.mqtt.publishTopic = strings.Replace(c.mqtt.publishTopic, "moto_id", c.motoID, 1)
|
||||
c.mqtt.publishTopic = strings.Replace(c.mqtt.publishTopic, "{device_id}", c.deviceID, 1)
|
||||
c.mqtt.publishTopic = strings.Replace(c.mqtt.publishTopic, "moto_id", c.motoId, 1)
|
||||
c.mqtt.publishTopic = strings.Replace(c.mqtt.publishTopic, "{device_id}", c.deviceId, 1)
|
||||
|
||||
parts := strings.Split(c.mqtt.subscribeTopic, "/")
|
||||
c.mqtt.uid = parts[3]
|
||||
@@ -96,7 +96,7 @@ func(c *TuyaClient) StartMQTT() error {
|
||||
c.mqtt.client = mqtt.NewClient(opts)
|
||||
|
||||
if token := c.mqtt.client.Connect(); token.Wait() && token.Error() != nil {
|
||||
return fmt.Errorf("failed to connect to MQTT broker: %w", token.Error())
|
||||
return token.Error()
|
||||
}
|
||||
|
||||
if err := c.mqtt.waiter.Wait(); err != nil {
|
||||
@@ -129,7 +129,7 @@ func(c *TuyaClient) consume(client mqtt.Client, msg mqtt.Message) {
|
||||
return
|
||||
}
|
||||
|
||||
if rmqtt.Data.Header.SessionID != c.sessionID {
|
||||
if rmqtt.Data.Header.SessionID != c.sessionId {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -202,11 +202,11 @@ func(c *TuyaMQTT) onError(err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TuyaClient) sendOffer(sdp string) {
|
||||
func (c *TuyaClient) sendOffer(sdp string, streamType uint32) {
|
||||
c.sendMqttMessage("offer", 302, "", OfferFrame{
|
||||
Mode: "webrtc",
|
||||
Sdp: sdp,
|
||||
StreamType: 1,
|
||||
StreamType: streamType,
|
||||
Auth: c.auth,
|
||||
})
|
||||
}
|
||||
@@ -251,9 +251,9 @@ func (c *TuyaClient) sendMqttMessage(messageType string, protocol int, transacti
|
||||
Header: MqttFrameHeader{
|
||||
Type: messageType,
|
||||
From: c.mqtt.uid,
|
||||
To: c.deviceID,
|
||||
SessionID: c.sessionID,
|
||||
MotoID: c.motoID,
|
||||
To: c.deviceId,
|
||||
SessionID: c.sessionId,
|
||||
MotoID: c.motoId,
|
||||
TransactionID: transactionID,
|
||||
},
|
||||
Message: jsonMessage,
|
||||
|
||||
Reference in New Issue
Block a user