Cleanup and update comments
This commit is contained in:
+27
-12
@@ -17,25 +17,28 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
api TuyaAPI
|
api TuyaAPI
|
||||||
conn *webrtc.Conn
|
conn *webrtc.Conn
|
||||||
pc *pion.PeerConnection
|
pc *pion.PeerConnection
|
||||||
|
connected core.Waiter
|
||||||
|
closed bool
|
||||||
|
|
||||||
|
// HEVC only:
|
||||||
dc *pion.DataChannel
|
dc *pion.DataChannel
|
||||||
videoSSRC *uint32
|
videoSSRC *uint32
|
||||||
audioSSRC *uint32
|
audioSSRC *uint32
|
||||||
streamType int
|
streamType int
|
||||||
isHEVC bool
|
isHEVC bool
|
||||||
connected core.Waiter
|
|
||||||
closed bool
|
|
||||||
handlersMu sync.RWMutex
|
handlersMu sync.RWMutex
|
||||||
handlers map[uint32]func(*rtp.Packet)
|
handlers map[uint32]func(*rtp.Packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataChannelMessage struct {
|
type DataChannelMessage struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"` // "codec", "start", "recv", "complete"
|
||||||
Msg string `json:"msg"`
|
Msg string `json:"msg"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RecvMessage contains SSRC values for video/audio streams
|
||||||
type RecvMessage struct {
|
type RecvMessage struct {
|
||||||
Video struct {
|
Video struct {
|
||||||
SSRC uint32 `json:"ssrc"`
|
SSRC uint32 `json:"ssrc"`
|
||||||
@@ -159,7 +162,8 @@ func Dial(rawURL string) (core.Producer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if client.isHEVC {
|
if client.isHEVC {
|
||||||
// Tuya seems to answers always with H264 and PCMU/8000 and PCMA/8000 codecs, replace with real codecs
|
// We need to replace the SDP codecs with the real ones from Skill.
|
||||||
|
// The actual media comes via DataChannel, not RTP tracks.
|
||||||
|
|
||||||
for _, media := range client.conn.Medias {
|
for _, media := range client.conn.Medias {
|
||||||
if media.Kind == core.KindVideo {
|
if media.Kind == core.KindVideo {
|
||||||
@@ -202,9 +206,7 @@ func Dial(rawURL string) (core.Producer, error) {
|
|||||||
client.Close(err)
|
client.Close(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// On HEVC, use DataChannel to receive video/audio
|
|
||||||
if client.isHEVC {
|
if client.isHEVC {
|
||||||
// Create a new DataChannel
|
|
||||||
maxRetransmits := uint16(5)
|
maxRetransmits := uint16(5)
|
||||||
ordered := true
|
ordered := true
|
||||||
client.dc, err = client.pc.CreateDataChannel("fmp4Stream", &pion.DataChannelInit{
|
client.dc, err = client.pc.CreateDataChannel("fmp4Stream", &pion.DataChannelInit{
|
||||||
@@ -212,18 +214,22 @@ func Dial(rawURL string) (core.Producer, error) {
|
|||||||
Ordered: &ordered,
|
Ordered: &ordered,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set up data channel handler
|
// DataChannel receives two types of messages:
|
||||||
|
// 1. String messages: Control messages (codec, recv)
|
||||||
|
// 2. Binary messages: RTP packets with video/audio
|
||||||
client.dc.OnMessage(func(msg pion.DataChannelMessage) {
|
client.dc.OnMessage(func(msg pion.DataChannelMessage) {
|
||||||
if msg.IsString {
|
if msg.IsString {
|
||||||
|
// Handle control messages (codec, recv, etc.)
|
||||||
if connected, err := client.probe(msg); err != nil {
|
if connected, err := client.probe(msg); err != nil {
|
||||||
client.Close(err)
|
client.Close(err)
|
||||||
} else if connected {
|
} else if connected {
|
||||||
client.connected.Done(nil)
|
client.connected.Done(nil)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Handle RTP packets - Route by SSRC retrieved from "recv" message
|
||||||
packet := &rtp.Packet{}
|
packet := &rtp.Packet{}
|
||||||
if err := packet.Unmarshal(msg.Data); err != nil {
|
if err := packet.Unmarshal(msg.Data); err != nil {
|
||||||
// skip
|
// Skip invalid packets
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,6 +345,9 @@ func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Rece
|
|||||||
return errors.New("webrtc: can't get track")
|
return errors.New("webrtc: can't get track")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DISABLED: Speaker Protocol 312 command
|
||||||
|
// JavaScript client doesn't send this on first call either
|
||||||
|
// Only subsequent calls (when speakerChloron is set) send Protocol 312
|
||||||
// mqttClient := c.api.GetMqtt()
|
// mqttClient := c.api.GetMqtt()
|
||||||
// if mqttClient != nil {
|
// if mqttClient != nil {
|
||||||
// _ = mqttClient.SendSpeaker(1)
|
// _ = mqttClient.SendSpeaker(1)
|
||||||
@@ -352,14 +361,16 @@ func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Rece
|
|||||||
_ = localTrack.WriteRTP(payloadType, packet)
|
_ = localTrack.WriteRTP(payloadType, packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tuya cameras require specific frame sizes
|
||||||
|
// See: https://developer.tuya.com/en/docs/iot-device-dev/tuyaos-package-ipc-device?id=Kcn1px33iptn2#title-29
|
||||||
switch track.Codec.Name {
|
switch track.Codec.Name {
|
||||||
case core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecPCML:
|
case core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecPCML:
|
||||||
// https://developer.tuya.com/en/docs/iot-device-dev/tuyaos-package-ipc-device?id=Kcn1px33iptn2#title-29-Why%20can%E2%80%99t%20WebRTC%20play%20audio%3F
|
|
||||||
frameSize := 240
|
frameSize := 240
|
||||||
if track.Codec.Name == core.CodecPCM {
|
if track.Codec.Name == core.CodecPCM {
|
||||||
frameSize = 560
|
frameSize = 560
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Repack to required frame size
|
||||||
sender.Handler = pcm.RepackG711(false, frameSize, sender.Handler)
|
sender.Handler = pcm.RepackG711(false, frameSize, sender.Handler)
|
||||||
sender.Handler = pcm.TranscodeHandler(codec, track.Codec, sender.Handler)
|
sender.Handler = pcm.TranscodeHandler(codec, track.Codec, sender.Handler)
|
||||||
}
|
}
|
||||||
@@ -463,6 +474,7 @@ func (c *Client) probe(msg pion.DataChannelMessage) (bool, error) {
|
|||||||
|
|
||||||
switch message.Type {
|
switch message.Type {
|
||||||
case "codec":
|
case "codec":
|
||||||
|
// Camera responded to our codec request - now request frame start
|
||||||
frameRequest, _ := json.Marshal(DataChannelMessage{
|
frameRequest, _ := json.Marshal(DataChannelMessage{
|
||||||
Type: "start",
|
Type: "start",
|
||||||
Msg: "frame",
|
Msg: "frame",
|
||||||
@@ -474,6 +486,8 @@ func (c *Client) probe(msg pion.DataChannelMessage) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "recv":
|
case "recv":
|
||||||
|
// Camera sends SSRC values for video/audio streams
|
||||||
|
// We need these to route incoming RTP packets correctly
|
||||||
var recvMessage RecvMessage
|
var recvMessage RecvMessage
|
||||||
if err := json.Unmarshal([]byte(message.Msg), &recvMessage); err != nil {
|
if err := json.Unmarshal([]byte(message.Msg), &recvMessage); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -484,6 +498,7 @@ func (c *Client) probe(msg pion.DataChannelMessage) (bool, error) {
|
|||||||
c.videoSSRC = &videoSSRC
|
c.videoSSRC = &videoSSRC
|
||||||
c.audioSSRC = &audioSSRC
|
c.audioSSRC = &audioSSRC
|
||||||
|
|
||||||
|
// Send "complete" to tell camera we're ready to receive RTP packets
|
||||||
completeMsg, _ := json.Marshal(DataChannelMessage{
|
completeMsg, _ := json.Marshal(DataChannelMessage{
|
||||||
Type: "complete",
|
Type: "complete",
|
||||||
Msg: "",
|
Msg: "",
|
||||||
|
|||||||
+19
-7
@@ -66,17 +66,17 @@ type AudioSkill struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type VideoSkill struct {
|
type VideoSkill struct {
|
||||||
StreamType int `json:"streamType"` // 2 = main stream (hd), 4 = sub stream (sd)
|
StreamType int `json:"streamType"` // 2 = main stream (HD), 4 = sub stream (SD)
|
||||||
ProfileId string `json:"profileId,omitempty"`
|
CodecType int `json:"codecType"` // 2 = H264, 4 = H265 (HEVC)
|
||||||
CodecType int `json:"codecType"` // 2 = H264, 4 = H265
|
|
||||||
Width int `json:"width"`
|
Width int `json:"width"`
|
||||||
Height int `json:"height"`
|
Height int `json:"height"`
|
||||||
SampleRate int `json:"sampleRate"`
|
SampleRate int `json:"sampleRate"`
|
||||||
|
ProfileId string `json:"profileId,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Skill struct {
|
type Skill struct {
|
||||||
WebRTC int `json:"webrtc"`
|
WebRTC int `json:"webrtc"` // Bit flags: bit 4=speaker, bit 5=clarity, bit 6=record
|
||||||
LowPower int `json:"lowPower,omitempty"`
|
LowPower int `json:"lowPower,omitempty"` // 1 = battery-powered camera
|
||||||
Audios []AudioSkill `json:"audios"`
|
Audios []AudioSkill `json:"audios"`
|
||||||
Videos []VideoSkill `json:"videos"`
|
Videos []VideoSkill `json:"videos"`
|
||||||
}
|
}
|
||||||
@@ -128,6 +128,14 @@ func (c *TuyaClient) GetMqtt() *TuyaMqttClient {
|
|||||||
return c.mqtt
|
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 {
|
func (c *TuyaClient) GetStreamType(streamResolution string) int {
|
||||||
// Default streamType if nothing is found
|
// Default streamType if nothing is found
|
||||||
defaultStreamType := 1
|
defaultStreamType := 1
|
||||||
@@ -136,7 +144,7 @@ func (c *TuyaClient) GetStreamType(streamResolution string) int {
|
|||||||
return defaultStreamType
|
return defaultStreamType
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the highest and lowest resolution
|
// Find the highest and lowest resolution based on pixel count
|
||||||
var highestResType = defaultStreamType
|
var highestResType = defaultStreamType
|
||||||
var highestRes = 0
|
var highestRes = 0
|
||||||
var lowestResType = defaultStreamType
|
var lowestResType = defaultStreamType
|
||||||
@@ -169,10 +177,14 @@ func (c *TuyaClient) GetStreamType(streamResolution string) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
func (c *TuyaClient) IsHEVC(streamType int) bool {
|
||||||
for _, video := range c.skill.Videos {
|
for _, video := range c.skill.Videos {
|
||||||
if video.StreamType == streamType {
|
if video.StreamType == streamType {
|
||||||
return video.CodecType == 4
|
return video.CodecType == 4 // 4 = H265/HEVC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+27
-19
@@ -52,9 +52,9 @@ type MqttFrame struct {
|
|||||||
type OfferFrame struct {
|
type OfferFrame struct {
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
Sdp string `json:"sdp"`
|
Sdp string `json:"sdp"`
|
||||||
StreamType int `json:"stream_type"`
|
StreamType int `json:"stream_type"` // 0: mainStream(HD), 1: substream(SD)
|
||||||
Auth string `json:"auth"`
|
Auth string `json:"auth"`
|
||||||
DatachannelEnable bool `json:"datachannel_enable"`
|
DatachannelEnable bool `json:"datachannel_enable"` // true for HEVC, false for H264
|
||||||
Token []ICEServer `json:"token"`
|
Token []ICEServer `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,8 +165,11 @@ func (c *TuyaMqttClient) Stop() {
|
|||||||
c.closed = true
|
c.closed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WakeUp sends a wake-up signal to battery-powered cameras (LowPower mode).
|
||||||
|
// The camera wakes up and starts responding immediately - we don't wait for dps[149].
|
||||||
|
// Note: LowPower cameras sleep after ~3 minutes of inactivity.
|
||||||
func (c *TuyaMqttClient) WakeUp(localKey string) error {
|
func (c *TuyaMqttClient) WakeUp(localKey string) error {
|
||||||
// Calculate CRC32 of localKey
|
// Calculate CRC32 of localKey as wake-up payload
|
||||||
crc := crc32.ChecksumIEEE([]byte(localKey))
|
crc := crc32.ChecksumIEEE([]byte(localKey))
|
||||||
|
|
||||||
// Convert to hex string
|
// Convert to hex string
|
||||||
@@ -189,7 +192,8 @@ func (c *TuyaMqttClient) WakeUp(localKey string) error {
|
|||||||
return fmt.Errorf("failed to publish wake-up message: %w", token.Error())
|
return fmt.Errorf("failed to publish wake-up message: %w", token.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to lowPower topic: smart/decrypt/in/{deviceId}
|
// Subscribe to lowPower topic to receive dps[149] status updates
|
||||||
|
// (we don't wait for this signal - camera responds immediately)
|
||||||
lowPowerTopic := fmt.Sprintf("smart/decrypt/in/%s", c.deviceId)
|
lowPowerTopic := fmt.Sprintf("smart/decrypt/in/%s", c.deviceId)
|
||||||
if token := c.client.Subscribe(lowPowerTopic, 1, c.onLowPowerMessage); token.Wait() && token.Error() != nil {
|
if token := c.client.Subscribe(lowPowerTopic, 1, c.onLowPowerMessage); token.Wait() && token.Error() != nil {
|
||||||
return fmt.Errorf("failed to subscribe to lowPower topic: %w", token.Error())
|
return fmt.Errorf("failed to subscribe to lowPower topic: %w", token.Error())
|
||||||
@@ -199,6 +203,7 @@ func (c *TuyaMqttClient) WakeUp(localKey string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TuyaMqttClient) SendOffer(sdp string, streamResolution string, streamType int, isHEVC bool) error {
|
func (c *TuyaMqttClient) SendOffer(sdp string, streamResolution string, streamType int, isHEVC bool) error {
|
||||||
|
// Map Skill StreamType to MQTT stream_type values
|
||||||
// streamType comes from GetStreamType() and uses Skill StreamType values:
|
// streamType comes from GetStreamType() and uses Skill StreamType values:
|
||||||
// - mainStream = 2 (HD)
|
// - mainStream = 2 (HD)
|
||||||
// - substream = 4 (SD)
|
// - substream = 4 (SD)
|
||||||
@@ -220,7 +225,7 @@ func (c *TuyaMqttClient) SendOffer(sdp string, streamResolution string, streamTy
|
|||||||
Sdp: sdp,
|
Sdp: sdp,
|
||||||
StreamType: mqttStreamType,
|
StreamType: mqttStreamType,
|
||||||
Auth: c.auth,
|
Auth: c.auth,
|
||||||
DatachannelEnable: isHEVC,
|
DatachannelEnable: isHEVC, // must be true for HEVC
|
||||||
Token: c.iceServers,
|
Token: c.iceServers,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -233,30 +238,32 @@ func (c *TuyaMqttClient) SendCandidate(candidate string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TuyaMqttClient) SendResolution(resolution int) error {
|
func (c *TuyaMqttClient) SendResolution(resolution int) error {
|
||||||
// isClaritySupperted := (c.webrtcVersion & (1 << 5)) != 0
|
// Check if camera supports clarity switching
|
||||||
// if !isClaritySupperted {
|
isClaritySupported := (c.webrtcVersion & (1 << 5)) != 0
|
||||||
// return nil
|
if !isClaritySupported {
|
||||||
// }
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Protocol 312 is used for clarity
|
|
||||||
return c.sendMqttMessage("resolution", 312, "", ResolutionFrame{
|
return c.sendMqttMessage("resolution", 312, "", ResolutionFrame{
|
||||||
Mode: "webrtc",
|
Mode: "webrtc",
|
||||||
Value: resolution,
|
Value: resolution, // 0: HD, 1: SD
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TuyaMqttClient) SendSpeaker(speaker int) error {
|
func (c *TuyaMqttClient) SendSpeaker(speaker int) error {
|
||||||
// Protocol 312 is used for speaker
|
if err := c.sendMqttMessage("speaker", 312, "", SpeakerFrame{
|
||||||
return c.sendMqttMessage("speaker", 312, "", SpeakerFrame{
|
|
||||||
Mode: "webrtc",
|
Mode: "webrtc",
|
||||||
Value: speaker,
|
Value: speaker, // 0: off, 1: on
|
||||||
})
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// if err := c.speakerWaiter.Wait(); err != nil {
|
// Wait for camera response
|
||||||
// return fmt.Errorf("speaker wait failed: %w", err)
|
if err := c.speakerWaiter.Wait(); err != nil {
|
||||||
// }
|
return fmt.Errorf("speaker wait failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TuyaMqttClient) SendDisconnect() error {
|
func (c *TuyaMqttClient) SendDisconnect() error {
|
||||||
@@ -281,6 +288,7 @@ func (c *TuyaMqttClient) onMessage(client mqtt.Client, msg mqtt.Message) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by session ID to prevent processing messages from other sessions
|
||||||
if rmqtt.Data.Header.SessionID != c.sessionId {
|
if rmqtt.Data.Header.SessionID != c.sessionId {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user