Files
2026-04-04 19:36:14 +02:00

271 lines
6.6 KiB
Go

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
}
}