package tuya import ( "net/http" "github.com/AlexxIT/go2rtc/pkg/core" pionWebrtc "github.com/pion/webrtc/v4" ) type TuyaAPI interface { GetMqtt() *TuyaMqttClient GetStreamType(streamResolution string) int IsHEVC(streamType int) bool GetVideoCodecs() []*core.Codec GetAudioCodecs() []*core.Codec GetStreamUrl(streamUrl string) (string, error) GetICEServers() []pionWebrtc.ICEServer Init() error Close() } type TuyaClient struct { TuyaAPI httpClient *http.Client mqtt *TuyaMqttClient baseUrl string expireTime int64 deviceId string localKey string skill *Skill iceServers []pionWebrtc.ICEServer } type AudioAttributes struct { CallMode []int `json:"call_mode"` // 1 = one way, 2 = two way HardwareCapability []int `json:"hardware_capability"` // 1 = mic, 2 = speaker } type ICEServer struct { Urls string `json:"urls"` Username string `json:"username,omitempty"` Credential string `json:"credential,omitempty"` TTL int `json:"ttl,omitempty"` } type WebICE struct { Urls string `json:"urls"` Username string `json:"username,omitempty"` Credential string `json:"credential,omitempty"` } type P2PConfig struct { Ices []ICEServer `json:"ices"` } type AudioSkill struct { Channels int `json:"channels"` DataBit int `json:"dataBit"` CodecType int `json:"codecType"` SampleRate int `json:"sampleRate"` } type VideoSkill struct { StreamType int `json:"streamType"` // 2 = main stream (HD), 4 = sub stream (SD) CodecType int `json:"codecType"` // 2 = H264, 4 = H265 (HEVC) Width int `json:"width"` Height int `json:"height"` SampleRate int `json:"sampleRate"` ProfileId string `json:"profileId,omitempty"` } type Skill struct { WebRTC int `json:"webrtc"` // Bit flags: bit 4=speaker, bit 5=clarity, bit 6=record LowPower int `json:"lowPower,omitempty"` // 1 = battery-powered camera Audios []AudioSkill `json:"audios"` Videos []VideoSkill `json:"videos"` } type WebRTCConfig struct { AudioAttributes AudioAttributes `json:"audio_attributes"` Auth string `json:"auth"` ID string `json:"id"` LocalKey string `json:"local_key,omitempty"` 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 MQTTConfig struct { Url string `json:"url"` PublishTopic string `json:"publish_topic"` SubscribeTopic string `json:"subscribe_topic"` ClientID string `json:"client_id"` Username string `json:"username"` Password string `json:"password"` } type Allocate struct { URL string `json:"url"` } type AllocateRequest struct { Type string `json:"type"` } type AllocateResponse struct { Success bool `json:"success"` Result Allocate `json:"result"` Msg string `json:"msg,omitempty"` } func (c *TuyaClient) GetICEServers() []pionWebrtc.ICEServer { return c.iceServers } func (c *TuyaClient) GetMqtt() *TuyaMqttClient { return c.mqtt } // GetStreamType returns the Skill StreamType for the requested resolution // Returns Skill values (2 or 4), not MQTT values (0 or 1) // - "hd" → highest resolution streamType (usually 2 = mainStream) // - "sd" → lowest resolution streamType (usually 4 = substream) // // These values must be mapped before sending to MQTT: // - streamType 2 → MQTT stream_type 0 // - streamType 4 → MQTT stream_type 1 func (c *TuyaClient) GetStreamType(streamResolution string) int { // Default streamType if nothing is found defaultStreamType := 1 if c.skill == nil || len(c.skill.Videos) == 0 { return defaultStreamType } // Find the highest and lowest resolution based on pixel count var highestResType = defaultStreamType var highestRes = 0 var lowestResType = defaultStreamType var lowestRes = 0 for _, video := range c.skill.Videos { res := video.Width * video.Height // Highest Resolution if res > highestRes { highestRes = res highestResType = video.StreamType } // Lower Resolution (or first if not set yet) if lowestRes == 0 || res < lowestRes { lowestRes = res lowestResType = video.StreamType } } // Return the streamType based on the selection switch streamResolution { case "hd": return highestResType case "sd": return lowestResType default: return defaultStreamType } } // IsHEVC checks if the given streamType uses H265 (HEVC) codec // HEVC cameras use DataChannel, H264 cameras use RTP tracks // - codecType 4 = H265 (HEVC) → DataChannel mode // - codecType 2 = H264 → Normal RTP mode func (c *TuyaClient) IsHEVC(streamType int) bool { for _, video := range c.skill.Videos { if video.StreamType == streamType { return video.CodecType == 4 // 4 = H265/HEVC } } return false } func (c *TuyaClient) GetVideoCodecs() []*core.Codec { if len(c.skill.Videos) > 0 { codecs := make([]*core.Codec, 0) for _, video := range c.skill.Videos { name := core.CodecH264 if c.IsHEVC(video.StreamType) { name = core.CodecH265 } codec := &core.Codec{ Name: name, ClockRate: uint32(video.SampleRate), } codecs = append(codecs, codec) } if len(codecs) > 0 { return codecs } } return nil } func (c *TuyaClient) GetAudioCodecs() []*core.Codec { if len(c.skill.Audios) > 0 { codecs := make([]*core.Codec, 0) for _, audio := range c.skill.Audios { name := getAudioCodecName(&audio) codec := &core.Codec{ Name: name, ClockRate: uint32(audio.SampleRate), Channels: uint8(audio.Channels), } codecs = append(codecs, codec) } if len(codecs) > 0 { return codecs } } return nil } func (c *TuyaClient) Close() { c.mqtt.Stop() c.httpClient.CloseIdleConnections() } // https://protect-us.ismartlife.me/ func getAudioCodecName(audioSkill *AudioSkill) string { switch audioSkill.CodecType { // case 100: // return "ADPCM" case 101: return core.CodecPCML case 102, 103, 104: return core.CodecAAC case 105: return core.CodecPCMU case 106: return core.CodecPCMA // case 107: // return "G726-32" // case 108: // return "SPEEX" case 109: return core.CodecMP3 default: return core.CodecPCML } }