From c03cd9f1561eedb6933b01dc78498c1050ec81ba Mon Sep 17 00:00:00 2001 From: seydx Date: Sat, 17 Jan 2026 12:09:38 +0100 Subject: [PATCH] minor improvements --- pkg/wyze/client.go | 19 +++++++++++- pkg/wyze/tutk/README.md | 68 +++++++++++++++++++++-------------------- pkg/wyze/tutk/conn.go | 29 ++++++++++-------- pkg/wyze/tutk/dtls.go | 54 +++++++++++++++++++++++++++++--- pkg/wyze/tutk/frame.go | 23 +++++++++++++- 5 files changed, 141 insertions(+), 52 deletions(-) diff --git a/pkg/wyze/client.go b/pkg/wyze/client.go index ab8f7d4e..e047cfd5 100644 --- a/pkg/wyze/client.go +++ b/pkg/wyze/client.go @@ -211,7 +211,7 @@ func (c *Client) StartAudio() error { } func (c *Client) StartIntercom() error { - if c.conn.IsBackchannelReady() { + if c.conn == nil || !c.conn.IsBackchannelReady() { return nil } @@ -223,6 +223,17 @@ func (c *Client) StartIntercom() error { return c.conn.AVServStart() } +func (c *Client) StopIntercom() error { + if c.conn == nil || !c.conn.IsBackchannelReady() { + return nil + } + + k10010 := c.buildK10010(MediaTypeReturnAudio, false) + c.conn.WriteAndWaitIOCtrl(KCmdControlChannel, k10010, KCmdControlChannelResp, 5*time.Second) + + return c.conn.AVServStop() +} + func (c *Client) ReadPacket() (*tutk.Packet, error) { return c.conn.AVRecvFrameData() } @@ -270,10 +281,16 @@ func (c *Client) Close() error { fmt.Printf("[Wyze] Closing connection\n") } + c.StopIntercom() + if c.conn != nil { c.conn.Close() } + if c.verbose { + fmt.Printf("[Wyze] Connection closed\n") + } + return nil } diff --git a/pkg/wyze/tutk/README.md b/pkg/wyze/tutk/README.md index ed98a857..36fa4728 100644 --- a/pkg/wyze/tutk/README.md +++ b/pkg/wyze/tutk/README.md @@ -443,38 +443,10 @@ Offset Size Field Description [31] 1 TwoWayAudio 0x01 if intercom supported [32-35] 4 Reserved [36-39] 4 BufferConfig 0x00000004 -[40-43] 4 Capabilities 0x001F07FB (see below) +[40-43] 4 Capabilities 0x001F07FB [44-57] 14 Reserved ``` -### Capabilities Bitmask (0x001F07FB) - -``` -Bit Hex Name Description -────────────────────────────────────────────────────────────── -0 0x00000001 CYCLIC_FRAME_NUMBERING Frame numbers wrap around -1 0x00000002 CLEAN_BUF_ON_RESET Clear buffer on stream reset -3 0x00000008 TIMESTAMP_IN_FRAMEINFO Timestamps in FRAMEINFO struct -4 0x00000010 MULTI_CHANNEL Multiple AV channels supported -5 0x00000020 EXTENDED_FRAMEINFO 40-byte FRAMEINFO (vs 16-byte SDK) -6 0x00000040 RESEND_TIMEOUT Packet resend with timeout -7 0x00000080 DTLS_SUPPORT DTLS encryption supported -8 0x00000100 SPEAKER_CHANNEL Two-way audio / intercom -9 0x00000200 PTZ_CHANNEL PTZ control channel -10 0x00000400 PLAYBACK_CHANNEL SD card playback channel -16 0x00010000 AV_SECURITY_ENABLED Encrypted AV stream -17 0x00020000 RESEND_ENABLED Packet resend mechanism -18 0x00040000 DTLS_PSK DTLS with Pre-Shared Key -19 0x00080000 DTLS_ECDHE DTLS with ECDHE key exchange -20 0x00100000 CHACHA20_POLY1305 ChaCha20-Poly1305 cipher support -``` - -**0x001F07FB breakdown:** -``` -0x001F07FB = 0b0000_0000_0001_1111_0000_0111_1111_1011 - = Bits: 0,1,3,4,5,6,7,8,9,10,16,17,18,19,20 -``` - --- ## 7. K-Command Authentication @@ -515,9 +487,21 @@ Offset Size Field Description [16+] var Payload Command-specific data ``` -### K10000 - Auth Request (16 bytes) +### K10000 - Auth Request (16 + JSON bytes) -Header only, no payload. Initiates authentication. +``` +Offset Size Field Description +────────────────────────────────────────────────────────────── +[0-15] 16 HLHeader CommandID = 10000, PayloadLen = len(JSON) +[16+] var JSONPayload Audio codec preferences +``` + +**JSON Payload:** +```json +{"cameraInfo":{"audioEncoderList":[137,138,140]}} +``` + +Where audioEncoderList contains supported codec IDs: 137=PCMU, 138=PCMA, 140=PCM. ### K10001 - Challenge (33+ bytes) @@ -543,7 +527,7 @@ Offset Size Field Description ────────────────────────────────────────────────────────────── [0-15] 16 HLHeader CommandID = 10002, PayloadLen = 22 [16-31] 16 Response XXTEA-decrypted challenge -[32-35] 4 UIDPrefix First 4 bytes of UID +[32-35] 4 SessionID Random 4-byte session identifier [36] 1 VideoFlag 1 = enable video stream [37] 1 AudioFlag 1 = enable audio stream ``` @@ -620,6 +604,22 @@ Offset Size Field Description | 0xF0 (240) | Maximum | | 0x3C (60) | SD quality | +### K10052 - Set Resolution Doorbell (22 bytes) + +Used by doorbell models (WYZEDB3, WVOD1, HL_WCO2, WYZEC1) instead of K10056: + +``` +Offset Size Field Description +────────────────────────────────────────────────────────────── +[0-15] 16 HLHeader CommandID = 10052, PayloadLen = 6 +[16-17] 2 Bitrate KB/s value (LE) +[18] 1 FrameSize Resolution + 1 (see table above) +[19] 1 FPS Frames per second, 0 = auto +[20-21] 2 Reserved Zero-filled +``` + +**Note:** K10052 has a different field order than K10056 (bitrate before frameSize). + --- ## 9. AV Frame Structure @@ -1155,12 +1155,14 @@ authkey = b64.replace('+', 'Z').replace('/', '9').replace('=', 'A') | Command | ID | Description | |---------|-----|-------------| -| KCmdAuth | 10000 | Auth request | +| KCmdAuth | 10000 | Auth request (with JSON) | | KCmdChallenge | 10001 | Challenge from camera | | KCmdChallengeResp | 10002 | Challenge response | | KCmdAuthResult | 10003 | Auth result (JSON) | | KCmdControlChannel | 10010 | Start/stop media | | KCmdControlChannelResp | 10011 | Control response | +| KCmdSetResolutionDB | 10052 | Set resolution (doorbell) | +| KCmdSetResolutionDBResp | 10053 | Resolution response (doorbell) | | KCmdSetResolution | 10056 | Set resolution/bitrate | | KCmdSetResolutionResp | 10057 | Resolution response | diff --git a/pkg/wyze/tutk/conn.go b/pkg/wyze/tutk/conn.go index 22b72afd..fc16da27 100644 --- a/pkg/wyze/tutk/conn.go +++ b/pkg/wyze/tutk/conn.go @@ -222,17 +222,19 @@ func (c *Conn) AVServStart() error { func (c *Conn) AVServStop() error { c.mu.Lock() - defer c.mu.Unlock() - + serverConn := c.serverConn + c.serverConn = nil // Reset audio TX state c.audioSeq = 0 c.audioFrameNo = 0 + c.mu.Unlock() - if c.serverConn != nil { - err := c.serverConn.Close() - c.serverConn = nil - return err + if serverConn == nil { + return nil } + + go serverConn.Close() + return nil } @@ -339,8 +341,13 @@ func (c *Conn) WriteAndWaitIOCtrl(cmd uint16, payload []byte, expectCmd uint16, frame := c.buildIOCtrlFrame(payload) var t *time.Timer t = time.AfterFunc(1, func() { - if _, err := c.clientConn.Write(frame); err == nil && t != nil { - t.Reset(time.Second) + c.mu.RLock() + conn := c.clientConn + c.mu.RUnlock() + if conn != nil { + if _, err := conn.Write(frame); err == nil && t != nil { + t.Reset(time.Second) + } } }) defer t.Stop() @@ -399,10 +406,6 @@ func (c *Conn) Close() error { c.clientConn.Close() c.clientConn = nil } - if c.serverConn != nil { - c.serverConn.Close() - c.serverConn = nil - } if c.frames != nil { c.frames.Close() } @@ -705,7 +708,7 @@ func (c *Conn) handleSpeakerAVLogin() error { } buf := make([]byte, 1024) - c.serverConn.SetReadDeadline(time.Now().Add(5 * time.Second)) + c.serverConn.SetReadDeadline(time.Now().Add(2 * time.Second)) n, err := c.serverConn.Read(buf) if err != nil { return fmt.Errorf("read av login: %w", err) diff --git a/pkg/wyze/tutk/dtls.go b/pkg/wyze/tutk/dtls.go index e4e2b3ea..c51b7762 100644 --- a/pkg/wyze/tutk/dtls.go +++ b/pkg/wyze/tutk/dtls.go @@ -2,6 +2,7 @@ package tutk import ( "net" + "sync" "time" "github.com/pion/dtls/v3" @@ -42,6 +43,9 @@ func buildDtlsConfig(psk []byte, isServer bool) *dtls.Config { type ChannelAdapter struct { conn *Conn channel uint8 + + mu sync.Mutex + readDeadline time.Time } func (a *ChannelAdapter) ReadFrom(p []byte) (n int, addr net.Addr, err error) { @@ -52,6 +56,29 @@ func (a *ChannelAdapter) ReadFrom(p []byte) (n int, addr net.Addr, err error) { buf = a.conn.serverBuf } + a.mu.Lock() + deadline := a.readDeadline + a.mu.Unlock() + + if !deadline.IsZero() { + timeout := time.Until(deadline) + if timeout <= 0 { + return 0, nil, &timeoutError{} + } + + timer := time.NewTimer(timeout) + defer timer.Stop() + + select { + case data := <-buf: + return copy(p, data), a.conn.addr, nil + case <-timer.C: + return 0, nil, &timeoutError{} + case <-a.conn.ctx.Done(): + return 0, nil, net.ErrClosed + } + } + select { case data := <-buf: return copy(p, data), a.conn.addr, nil @@ -67,8 +94,27 @@ func (a *ChannelAdapter) WriteTo(p []byte, _ net.Addr) (int, error) { return len(p), nil } -func (a *ChannelAdapter) Close() error { return nil } -func (a *ChannelAdapter) LocalAddr() net.Addr { return &net.UDPAddr{} } -func (a *ChannelAdapter) SetDeadline(time.Time) error { return nil } -func (a *ChannelAdapter) SetReadDeadline(time.Time) error { return nil } +func (a *ChannelAdapter) Close() error { return nil } +func (a *ChannelAdapter) LocalAddr() net.Addr { return &net.UDPAddr{} } + +func (a *ChannelAdapter) SetDeadline(t time.Time) error { + a.mu.Lock() + a.readDeadline = t + a.mu.Unlock() + return nil +} + +func (a *ChannelAdapter) SetReadDeadline(t time.Time) error { + a.mu.Lock() + a.readDeadline = t + a.mu.Unlock() + return nil +} + func (a *ChannelAdapter) SetWriteDeadline(time.Time) error { return nil } + +type timeoutError struct{} + +func (e *timeoutError) Error() string { return "i/o timeout" } +func (e *timeoutError) Timeout() bool { return true } +func (e *timeoutError) Temporary() bool { return true } diff --git a/pkg/wyze/tutk/frame.go b/pkg/wyze/tutk/frame.go index cebdc825..ee673181 100644 --- a/pkg/wyze/tutk/frame.go +++ b/pkg/wyze/tutk/frame.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "encoding/hex" "fmt" + "sync" "github.com/AlexxIT/go2rtc/pkg/aac" ) @@ -282,6 +283,8 @@ type FrameHandler struct { audioTS tsTracker output chan *Packet verbose bool + closed bool + closeMu sync.Mutex } func NewFrameHandler(verbose bool) *FrameHandler { @@ -297,6 +300,13 @@ func (h *FrameHandler) Recv() <-chan *Packet { } func (h *FrameHandler) Close() { + h.closeMu.Lock() + defer h.closeMu.Unlock() + + if h.closed { + return + } + h.closed = true close(h.output) } @@ -540,6 +550,13 @@ func (h *FrameHandler) handleAudio(payload []byte, fi *FrameInfo) { } func (h *FrameHandler) queue(pkt *Packet) { + h.closeMu.Lock() + defer h.closeMu.Unlock() + + if h.closed { + return + } + select { case h.output <- pkt: default: @@ -548,7 +565,11 @@ func (h *FrameHandler) queue(pkt *Packet) { case <-h.output: default: } - h.output <- pkt + select { + case h.output <- pkt: + default: + // Queue still full, drop this packet + } } }