271 lines
6.6 KiB
Go
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
|
|
}
|
|
}
|