package tutk import ( "context" "crypto/hmac" "crypto/rand" "crypto/sha1" "crypto/sha256" "encoding/binary" "encoding/hex" "fmt" "io" "net" "sync" "time" "github.com/AlexxIT/go2rtc/pkg/wyze/crypto" "github.com/pion/dtls/v3" ) const ( PSKIdentity = "AUTHPWD_admin" DefaultUser = "admin" DefaultPort = 32761 // TUTK discovery port MaxPacketSize = 2048 // Max single packet size ReadBufferSize = 2 * 1024 * 1024 // 2MB for video streams DiscoTimeout = 5000 * time.Millisecond // Total timeout for discovery DiscoInterval = 100 * time.Millisecond // Interval between discovery packets SessionTimeout = 5000 * time.Millisecond // Total timeout for session setup ReadWaitInterval = 50 * time.Millisecond // Read wait interval per iteration ) type FrameAssembler struct { frameNo uint32 pktTotal uint16 packets map[uint16][]byte // pkt_idx -> payload frameInfo *FrameInfo } type Conn struct { udpConn *net.UDPConn addr *net.UDPAddr broadcastAddrs []*net.UDPAddr randomID []byte uid string authKey string enr string mac string // MAC address for auth key calculation psk []byte iotcTxSeq uint16 avLoginResp *AVLoginResponse useNewProto bool // true if camera uses NEW protocol newProtoTicket uint16 // ticket from camera response sessionID []byte // 8-byte session ID for NEW protocol // DTLS - Main Channel (we = Client) mainConn *dtls.Conn mainBuf chan []byte // DTLS - Speaker Channel (we = Server) speakerConn *dtls.Conn speakerBuf chan []byte ioctrl chan []byte ackReceived chan struct{} errors chan error frameAssemblers map[byte]*FrameAssembler // channel -> assembler packetQueue chan *Packet avTxSeq uint32 ioctrlSeq uint16 // Audio TX state (for intercom) audioTxSeq uint32 audioTxFrameNo uint32 lastAckCounter uint16 ackFlags uint16 baseTS uint64 ctx context.Context cancel context.CancelFunc wg sync.WaitGroup mu sync.RWMutex done chan struct{} verbose bool } func Dial(host, uid, authKey, enr, mac string, verbose bool) (*Conn, error) { conn, err := net.ListenUDP("udp", nil) if err != nil { return nil, err } _ = conn.SetReadBuffer(ReadBufferSize) ctx, cancel := context.WithCancel(context.Background()) hash := sha256.Sum256([]byte(enr)) psk := hash[:] c := &Conn{ udpConn: conn, addr: &net.UDPAddr{IP: net.ParseIP(host), Port: DefaultPort}, broadcastAddrs: getBroadcastAddrs(DefaultPort, verbose), randomID: genRandomID(), uid: uid, authKey: authKey, enr: enr, mac: mac, psk: psk, verbose: verbose, ctx: ctx, cancel: cancel, mainBuf: make(chan []byte, 64), speakerBuf: make(chan []byte, 64), packetQueue: make(chan *Packet, 128), done: make(chan struct{}), ioctrl: make(chan []byte, 16), ackReceived: make(chan struct{}, 1), errors: make(chan error, 1), } if err = c.discovery(); err != nil { _ = c.Close() return nil, err } // Start IOTC reader goroutine for DTLS routing c.wg.Add(1) go c.iotcReader() // Perform DTLS client handshake on Main channel if err = c.connect(); err != nil { _ = c.Close() return nil, err } // Start AV data worker c.wg.Add(1) go c.worker() return c, nil } func (c *Conn) AVClientStart(timeout time.Duration) error { randomID := genRandomID() pkt1 := c.buildAVLoginPacket(MagicAVLogin1, 570, 0x0001, randomID) pkt2 := c.buildAVLoginPacket(MagicAVLogin2, 572, 0x0000, randomID) pkt2[20]++ // pkt2 has randomID incremented by 1 if _, err := c.mainConn.Write(pkt1); err != nil { return fmt.Errorf("AV login 1 failed: %w", err) } time.Sleep(50 * time.Millisecond) if _, err := c.mainConn.Write(pkt2); err != nil { return fmt.Errorf("AV login 2 failed: %w", err) } // Wait for response deadline := time.Now().Add(timeout) for { remaining := time.Until(deadline) if remaining <= 0 { return context.DeadlineExceeded } select { case data, ok := <-c.ioctrl: if !ok { return io.EOF } if len(data) >= 32 && binary.LittleEndian.Uint16(data) == MagicAVLoginResp { c.avLoginResp = &AVLoginResponse{ ServerType: binary.LittleEndian.Uint32(data[4:]), Resend: int32(data[29]), TwoWayStreaming: int32(data[31]), } if c.verbose { fmt.Printf("[TUTK] AV Login Response: two_way_streaming=%d\n", c.avLoginResp.TwoWayStreaming) } _ = c.sendACK() return nil } case <-c.ctx.Done(): return c.ctx.Err() } } } func (c *Conn) AVServStart() error { if c.verbose { fmt.Printf("[DTLS] Waiting for client handshake on channel %d\n", IOTCChannelBack) fmt.Printf("[DTLS] PSK Identity: %s\n", PSKIdentity) fmt.Printf("[DTLS] PSK Key: %s\n", hex.EncodeToString(c.psk)) } config := c.buildDTLSConfig(true) // Create adapter for speaker channel adapter := &ChannelAdapter{ conn: c, channel: IOTCChannelBack, } conn, err := dtls.Server(adapter, c.addr, config) if err != nil { return fmt.Errorf("dtls: server handshake failed: %w", err) } c.mu.Lock() c.speakerConn = conn c.mu.Unlock() if c.verbose { fmt.Printf("[DTLS] Server handshake complete on channel %d\n", IOTCChannelBack) } // Wait for and respond to AV Login request from camera if err := c.handleSpeakerAVLogin(); err != nil { return fmt.Errorf("speaker AV login failed: %w", err) } return nil } func (c *Conn) AVServStop() error { c.mu.Lock() defer c.mu.Unlock() // Reset audio TX state c.audioTxSeq = 0 c.audioTxFrameNo = 0 if c.speakerConn != nil { err := c.speakerConn.Close() c.speakerConn = nil return err } return nil } func (c *Conn) AVRecvFrameData() (*Packet, error) { select { case pkt, ok := <-c.packetQueue: if !ok { return nil, io.EOF } return pkt, nil case err := <-c.errors: return nil, err case <-c.done: return nil, io.EOF case <-c.ctx.Done(): return nil, io.EOF } } func (c *Conn) AVSendAudioData(codec uint16, payload []byte, timestampUS uint32, sampleRate uint32, channels uint8) error { c.mu.Lock() conn := c.speakerConn if conn == nil { c.mu.Unlock() return fmt.Errorf("speaker channel not connected") } frame := c.buildAudioFrame(payload, timestampUS, codec, sampleRate, channels) if c.verbose { c.logAudioTX(frame, codec, len(payload), timestampUS, sampleRate, channels) } c.mu.Unlock() n, err := conn.Write(frame) if c.verbose { if err != nil { fmt.Printf("[AUDIO TX] DTLS Write ERROR: %v\n", err) } else { fmt.Printf("[AUDIO TX] DTLS Write OK: %d bytes\n", n) } } return err } func (c *Conn) SendIOCtrl(cmdID uint16, payload []byte) error { frame := c.buildIOCtrlFrame(payload) if _, err := c.mainConn.Write(frame); err != nil { return err } select { case <-c.ackReceived: if c.verbose { fmt.Printf("[Conn] SendIOCtrl K%d: ACK received\n", cmdID) } return nil case <-time.After(5 * time.Second): return fmt.Errorf("ACK timeout for K%d", cmdID) case <-c.ctx.Done(): return c.ctx.Err() } } func (c *Conn) RecvIOCtrl(timeout time.Duration) (cmdID uint16, data []byte, err error) { select { case data, ok := <-c.ioctrl: if !ok { return 0, nil, io.EOF } // Parse cmdID from HL header at offset 4-5 if len(data) >= 6 { cmdID = binary.LittleEndian.Uint16(data[4:]) } // Send ACK after receiving _ = c.sendACK() if c.verbose { fmt.Printf("[Conn] RecvIOCtrl: received K%d (%d bytes)\n", cmdID, len(data)) } return cmdID, data, nil case <-time.After(timeout): return 0, nil, context.DeadlineExceeded case <-c.ctx.Done(): return 0, nil, c.ctx.Err() } } func (c *Conn) GetAVLoginResponse() *AVLoginResponse { return c.avLoginResp } func (c *Conn) IsBackchannelReady() bool { c.mu.RLock() defer c.mu.RUnlock() return c.speakerConn != nil } func (c *Conn) RemoteAddr() *net.UDPAddr { return c.addr } func (c *Conn) LocalAddr() *net.UDPAddr { return c.udpConn.LocalAddr().(*net.UDPAddr) } func (c *Conn) SetDeadline(t time.Time) error { return c.udpConn.SetDeadline(t) } func (c *Conn) Close() error { select { case <-c.done: default: close(c.done) } c.mu.Lock() if c.mainConn != nil { c.mainConn.Close() c.mainConn = nil } if c.speakerConn != nil { c.speakerConn.Close() c.speakerConn = nil } c.mu.Unlock() c.cancel() c.wg.Wait() close(c.ioctrl) close(c.errors) return c.udpConn.Close() } func (c *Conn) discovery() error { _ = c.udpConn.SetDeadline(time.Now().Add(10 * time.Second)) // Generate 8-byte session ID for NEW protocol c.sessionID = make([]byte, 8) rand.Read(c.sessionID[:2]) copy(c.sessionID[2:], []byte{0x76, 0x0a, 0x9d, 0x24, 0x88, 0xba}) // Build discovery packets for both protocols oldDiscoPkt := crypto.TransCodeBlob(c.buildDisco(1)) // OLD protocol (TransCode encoded) newDiscoPkt := c.buildNewProtoPacket(0, 0, false) // NEW protocol (0xCC51, cmd=0x1002) if c.verbose { fmt.Printf("[DISCO] Unified discovery: timeout=%v interval=%v broadcasts=%d\n", DiscoTimeout, DiscoInterval, len(c.broadcastAddrs)) fmt.Printf("[DISCO] SessionID=%s\n", hex.EncodeToString(c.sessionID)) fmt.Printf("[OLD] TX Discovery packet (%d bytes):\n%s", len(oldDiscoPkt), hexDump(crypto.ReverseTransCodeBlob(oldDiscoPkt))) fmt.Printf("[NEW] TX Discovery packet (%d bytes):\n%s", len(newDiscoPkt), hexDump(newDiscoPkt)) } deadline := time.Now().Add(DiscoTimeout) lastSend := time.Time{} buf := make([]byte, MaxPacketSize) for time.Now().Before(deadline) { // Send both discovery packets periodically if time.Since(lastSend) >= DiscoInterval { for _, bcast := range c.broadcastAddrs { c.udpConn.WriteToUDP(oldDiscoPkt, bcast) // OLD protocol c.udpConn.WriteToUDP(newDiscoPkt, bcast) // NEW protocol } lastSend = time.Now() } c.udpConn.SetReadDeadline(time.Now().Add(ReadWaitInterval)) n, addr, err := c.udpConn.ReadFromUDP(buf) if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { continue } return err } // Check for NEW protocol response (0xCC51 magic) if n >= 12 && binary.LittleEndian.Uint16(buf[:2]) == MagicNewProto { cmd := binary.LittleEndian.Uint16(buf[4:]) dir := binary.LittleEndian.Uint16(buf[8:]) if c.verbose { fmt.Printf("[NEW] RX %d bytes <- %s (cmd=0x%04x dir=0x%04x)\n", n, addr, cmd, dir) } // Handle cmd=0x1002 seq=1 discovery response if cmd == CmdNewProtoDiscovery && n >= NewProtoPacketSize && dir == 0xFFFF { seq := binary.LittleEndian.Uint16(buf[12:]) ticket := binary.LittleEndian.Uint16(buf[14:]) if seq == 1 { c.addr = addr c.newProtoTicket = ticket c.useNewProto = true if n >= 24 { copy(c.sessionID, buf[16:24]) } if c.verbose { fmt.Printf("[NEW] Camera detected! ticket=0x%04x sessionID=%s\n", ticket, hex.EncodeToString(c.sessionID)) } _ = c.udpConn.SetDeadline(time.Time{}) return c.newProtoComplete() } } continue } // Check for OLD protocol response (TransCode encoded) data := crypto.ReverseTransCodeBlob(buf[:n]) if len(data) >= 16 { cmd := binary.LittleEndian.Uint16(data[8:]) if c.verbose { fmt.Printf("[OLD] RX %d bytes <- %s (cmd=0x%04x)\n%s", n, addr, cmd, hexDump(data)) } if cmd == CmdDiscoRes { c.addr = addr c.useNewProto = false if c.verbose { fmt.Printf("[OLD] Camera detected at %s\n", addr) } _ = c.udpConn.SetDeadline(time.Time{}) return c.oldProtoComplete() } } } _ = c.udpConn.SetDeadline(time.Time{}) return fmt.Errorf("discovery timeout - no camera response") } func (c *Conn) oldProtoComplete() error { // Stage 2 pkt := c.buildDisco(2) if c.verbose { fmt.Printf("[OLD] TX Stage 2 Discovery (%d bytes):\n%s", len(pkt), hexDump(pkt)) } encrypted := crypto.TransCodeBlob(pkt) c.udpConn.WriteToUDP(encrypted, c.addr) time.Sleep(100 * time.Millisecond) // Session setup sessionPkt := c.buildSession() if _, err := c.sendEncrypted(sessionPkt); err != nil { return err } buf := make([]byte, MaxPacketSize) deadline := time.Now().Add(SessionTimeout) for time.Now().Before(deadline) { c.udpConn.SetReadDeadline(time.Now().Add(ReadWaitInterval)) n, addr, err := c.udpConn.ReadFromUDP(buf) if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { continue } return err } data := crypto.ReverseTransCodeBlob(buf[:n]) if len(data) < 16 { continue } cmd := binary.LittleEndian.Uint16(data[8:]) if c.verbose { fmt.Printf("[OLD] RX %d bytes (cmd=0x%04x)\n%s", len(data), cmd, hexDump(data)) } if cmd == CmdSessionRes { c.addr = addr if c.verbose { fmt.Printf("[OLD] Session setup complete!\n") } return nil } } return fmt.Errorf("OLD protocol session timeout") } func (c *Conn) newProtoComplete() error { pkt2 := c.buildNewProtoPacket(2, c.newProtoTicket, false) if c.verbose { fmt.Printf("[NEW] TX seq=2 with ticket=0x%04x (%d bytes):\n%s", c.newProtoTicket, len(pkt2), hexDump(pkt2)) } c.udpConn.WriteToUDP(pkt2, c.addr) buf := make([]byte, MaxPacketSize) deadline := time.Now().Add(SessionTimeout) lastSend := time.Now() for time.Now().Before(deadline) { if time.Since(lastSend) >= DiscoInterval { c.udpConn.WriteToUDP(pkt2, c.addr) lastSend = time.Now() } c.udpConn.SetReadDeadline(time.Now().Add(ReadWaitInterval)) n, addr, err := c.udpConn.ReadFromUDP(buf) if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { continue } return err } if n >= NewProtoPacketSize && binary.LittleEndian.Uint16(buf[:2]) == MagicNewProto { cmd := binary.LittleEndian.Uint16(buf[4:]) dir := binary.LittleEndian.Uint16(buf[8:]) seq := binary.LittleEndian.Uint16(buf[12:]) if cmd == CmdNewProtoDiscovery && dir == 0xFFFF && seq == 3 { if c.verbose { fmt.Printf("[NEW] seq=3 received, discovery complete!\n") } c.addr = addr return nil } } } return fmt.Errorf("NEW protocol handshake timeout waiting for seq=3") } func (c *Conn) connect() error { if c.verbose { fmt.Printf("[DTLS] Starting client handshake on channel %d\n", IOTCChannelMain) fmt.Printf("[DTLS] PSK Identity: %s\n", PSKIdentity) fmt.Printf("[DTLS] PSK Key: %s\n", hex.EncodeToString(c.psk)) } config := c.buildDTLSConfig(false) // Create adapter for main channel adapter := &ChannelAdapter{ conn: c, channel: IOTCChannelMain, } conn, err := dtls.Client(adapter, c.addr, config) if err != nil { return fmt.Errorf("dtls: client create failed: %w", err) } c.mu.Lock() c.mainConn = conn c.mu.Unlock() if c.verbose { fmt.Printf("[DTLS] Client created for channel %d\n", IOTCChannelMain) } return nil } func (c *Conn) iotcReader() { defer c.wg.Done() buf := make([]byte, MaxPacketSize) for { select { case <-c.done: return default: } c.udpConn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) n, addr, err := c.udpConn.ReadFromUDP(buf) if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { continue } return } if addr.Port != c.addr.Port || !addr.IP.Equal(c.addr.IP) { c.addr = addr } // Check for NEW protocol (0xCC51 magic at start) if c.useNewProto && n >= 2 && binary.LittleEndian.Uint16(buf[:2]) == MagicNewProto { c.handleNewProtoPacket(buf[:n]) continue } // OLD protocol: TransCode decode data := crypto.ReverseTransCodeBlob(buf[:n]) if len(data) < 16 { continue } cmd := binary.LittleEndian.Uint16(data[8:]) if cmd == CmdKeepaliveRes && len(data) > 16 { payload := data[16:] if len(payload) >= 8 { keepaliveResp := c.buildKeepaliveResponse(payload) _, _ = c.sendEncrypted(keepaliveResp) if c.verbose { fmt.Printf("[DTLS] Keepalive response sent\n") } } continue } if cmd == CmdDataRX && len(data) > 28 { // Debug: Dump IOTC header to verify structure if c.verbose && len(data) >= 32 { fmt.Printf("[IOTC] RX Header dump (32 bytes):\n") fmt.Printf(" [0-7]: %02x %02x %02x %02x %02x %02x %02x %02x\n", data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]) fmt.Printf(" [8-15]: %02x %02x %02x %02x %02x %02x %02x %02x (cmd@8-9, ch@14)\n", data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15]) fmt.Printf(" [16-23]: %02x %02x %02x %02x %02x %02x %02x %02x\n", data[16], data[17], data[18], data[19], data[20], data[21], data[22], data[23]) fmt.Printf(" [24-31]: %02x %02x %02x %02x %02x %02x %02x %02x (dtls starts @28)\n", data[24], data[25], data[26], data[27], data[28], data[29], data[30], data[31]) } dtlsPayload := data[28:] // Channel byte is at position 14 in IOTC header channel := data[14] if c.verbose { fmt.Printf("[IOTC] RX cmd=0x%04x len=%d ch=%d dtlsLen=%d\n", cmd, len(data), channel, len(dtlsPayload)) if len(dtlsPayload) >= 13 { contentType := dtlsPayload[0] fmt.Printf("[DTLS] ch=%d contentType=%d first8=[%02x %02x %02x %02x %02x %02x %02x %02x]\n", channel, contentType, dtlsPayload[0], dtlsPayload[1], dtlsPayload[2], dtlsPayload[3], dtlsPayload[4], dtlsPayload[5], dtlsPayload[6], dtlsPayload[7]) } } // Copy data since buffer is reused dataCopy := make([]byte, len(dtlsPayload)) copy(dataCopy, dtlsPayload) // Route based on channel var chBuf chan []byte switch channel { case IOTCChannelMain: chBuf = c.mainBuf case IOTCChannelBack: chBuf = c.speakerBuf } if chBuf != nil { select { case chBuf <- dataCopy: default: // Drop oldest if full select { case <-chBuf: default: } chBuf <- dataCopy } } } } } func (c *Conn) handleNewProtoPacket(data []byte) { if len(data) < 16 { return } cmd := binary.LittleEndian.Uint16(data[4:]) seq := binary.LittleEndian.Uint16(data[12:]) ticket := binary.LittleEndian.Uint16(data[14:]) if c.verbose { fmt.Printf("[NEW] RX cmd=0x%04x seq=%d ticket=0x%04x len=%d\n", cmd, seq, ticket, len(data)) fmt.Printf("[NEW] RX full packet:\n%s", hexDump(data)) } // Handle DTLS data (cmd=0x1502) if cmd == CmdNewProtoDTLS && len(data) > NewProtoHeaderSize+NewProtoAuthSize { // Packet structure: [0:28] header, [28:N-20] DTLS payload, [N-20:N] auth bytes // We need to strip the auth bytes at the end dtlsPayload := data[NewProtoHeaderSize : len(data)-NewProtoAuthSize] // Channel is in the 4-byte field at offset 24 (value 1=main, 2=back) channelFlag := binary.LittleEndian.Uint32(data[24:]) var channel byte if channelFlag >= 1 { channel = byte(channelFlag - 1) // Convert back to 0=main, 1=back } if c.verbose && len(dtlsPayload) >= 1 { fmt.Printf("[NEW] DTLS RX ch=%d contentType=%d len=%d (stripped 20 auth bytes)\n%s", channel, dtlsPayload[0], len(dtlsPayload), hexDump(dtlsPayload)) } // Copy data since buffer is reused dataCopy := make([]byte, len(dtlsPayload)) copy(dataCopy, dtlsPayload) // Route based on channel var chBuf chan []byte switch channel { case IOTCChannelMain: chBuf = c.mainBuf case IOTCChannelBack: chBuf = c.speakerBuf } if chBuf != nil { select { case chBuf <- dataCopy: default: // Drop oldest if full select { case <-chBuf: default: } chBuf <- dataCopy } } } } func (c *Conn) worker() { defer c.wg.Done() buf := make([]byte, 2048) for { select { case <-c.ctx.Done(): return default: } n, err := c.mainConn.Read(buf) if err != nil { select { case c.errors <- err: default: } return } if n < 2 { continue } // Debug: dump first bytes to see what we actually receive if c.verbose && n >= 36 { fmt.Printf("[Conn] worker raw: n=%d\n", n) fmt.Printf("[Conn] first16: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15]) fmt.Printf("[Conn] off16-31: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", buf[16], buf[17], buf[18], buf[19], buf[20], buf[21], buf[22], buf[23], buf[24], buf[25], buf[26], buf[27], buf[28], buf[29], buf[30], buf[31]) } else if c.verbose && n >= 8 { fmt.Printf("[Conn] worker raw: n=%d first8=[%02x %02x %02x %02x %02x %02x %02x %02x]\n", n, buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7]) } c.route(buf[:n]) } } func (c *Conn) route(data []byte) { // [channel][frameType][version_lo][version_hi][seq_lo][seq_hi]... // channel: 0x03=Audio, 0x05=I-Video, 0x07=P-Video // frameType: 0x00=cont, 0x05=end, 0x08=I-start, 0x0d=end-44 if len(data) < 2 { return } // Check for control frame magic values first (uint16 LE) magic := binary.LittleEndian.Uint16(data) switch magic { case MagicAVLoginResp: // AV Login Response - send full data for parsing c.queueIOCtrlData(data) return case MagicIOCtrl: // IOCTRL Response Frame (K10001, K10003) if len(data) >= 32 { for i := 32; i+2 < len(data); i++ { if data[i] == 'H' && data[i+1] == 'L' { c.queueIOCtrlData(data[i:]) return } } } return case MagicChannelMsg: // Channel message if len(data) >= 36 { opCode := data[16] if opCode == 0x00 { for i := 36; i+2 < len(data); i++ { if data[i] == 'H' && data[i+1] == 'L' { c.queueIOCtrlData(data[i:]) return } } } } return case MagicACK: // ACK from camera select { case c.ackReceived <- struct{}{}: default: } return } // Check for AV Data packet (channel byte at offset 0) channel := data[0] if channel == ChannelAudio || channel == ChannelIVideo || channel == ChannelPVideo { c.handleAVData(data) return } // Unknown packet type if c.verbose { fmt.Printf("[Conn] Unknown frame: type=0x%02x len=%d\n", data[0], len(data)) } } func (c *Conn) handleSpeakerAVLogin() error { // Read AV Login request from camera (SDK receives 570 bytes) buf := make([]byte, 1024) c.speakerConn.SetReadDeadline(time.Now().Add(5 * time.Second)) n, err := c.speakerConn.Read(buf) if err != nil { return fmt.Errorf("read AV login: %w", err) } if c.verbose { fmt.Printf("[SPEAK] Received AV Login request: %d bytes\n", n) } // Need at least 24 bytes to read the checksum if n < 24 { return fmt.Errorf("AV login too short: %d bytes", n) } // Extract checksum from incoming request (bytes 20-23) checksum := binary.LittleEndian.Uint32(buf[20:]) // Build AV Login response (60 bytes like SDK) resp := c.buildAVLoginResponse(checksum) if c.verbose { fmt.Printf("[SPEAK] Sending AV Login response: %d bytes\n", len(resp)) } _, err = c.speakerConn.Write(resp) if err != nil { return fmt.Errorf("write AV login response: %w", err) } // Camera will resend AV-Login, respond again with AV-LoginResp c.speakerConn.SetReadDeadline(time.Now().Add(500 * time.Millisecond)) n, _ = c.speakerConn.Read(buf) if n > 0 { if c.verbose { fmt.Printf("[SPEAK] Received AV Login resend: %d bytes\n", n) } // Send second AV-LoginResp if c.verbose { fmt.Printf("[SPEAK] Sending second AV Login response: %d bytes\n", len(resp)) } c.speakerConn.Write(resp) } // Clear deadline c.speakerConn.SetReadDeadline(time.Time{}) if c.verbose { fmt.Printf("[SPEAK] AV Login complete, ready for audio\n") } return nil } func (c *Conn) handleAVData(data []byte) { // Parse packet header to get pkt_idx, pkt_total, frame_no hdr := ParsePacketHeader(data) if hdr == nil { fmt.Printf("[Conn] Invalid AV packet header, len=%d\n", len(data)) return } // Debug: Log raw Wire-Header bytes if c.verbose { fmt.Printf("[WIRE] ch=0x%02x type=0x%02x len=%d pkt=%d/%d frame=%d\n", hdr.Channel, hdr.FrameType, len(data), hdr.PktIdx, hdr.PktTotal, hdr.FrameNo) fmt.Printf(" RAW[0..35]: ") for i := 0; i < 36 && i < len(data); i++ { fmt.Printf("%02x ", data[i]) } fmt.Printf("\n") } // Extract payload and try to detect FRAMEINFO payload, fi := c.extractPayload(data, hdr.Channel) if payload == nil { return } if c.verbose { c.logAVPacket(hdr.Channel, hdr.FrameType, payload, fi) } // Route to handler switch hdr.Channel { case ChannelAudio: c.handleAudio(payload, fi) case ChannelIVideo, ChannelPVideo: c.handleVideo(hdr.Channel, hdr, payload, fi) } } func (c *Conn) extractPayload(data []byte, channel byte) ([]byte, *FrameInfo) { if len(data) < 2 { return nil, nil } frameType := data[1] // Determine header size and FrameInfo size based on frameType headerSize := 28 frameInfoSize := 0 // 0 means no FrameInfo switch frameType { case FrameTypeStart: // Extended start packet - 36-byte header, no FrameInfo headerSize = 36 case FrameTypeStartAlt: // StartAlt - 36-byte header // Has FrameInfo only if pkt_total == 1 (single-packet frame) headerSize = 36 if len(data) >= 22 { pktTotal := binary.LittleEndian.Uint16(data[20:]) if pktTotal == 1 { frameInfoSize = FrameInfoSize } } case FrameTypeCont, FrameTypeContAlt: // Continuation packet - standard 28-byte header, no FrameInfo headerSize = 28 case FrameTypeEndSingle, FrameTypeEndMulti: // End packet - standard 28-byte header, 40-byte FrameInfo headerSize = 28 frameInfoSize = FrameInfoSize case FrameTypeEndExt: // Extended end packet - 36-byte header, 40-byte FrameInfo headerSize = 36 frameInfoSize = FrameInfoSize default: // Unknown frame type - use 28-byte header as fallback headerSize = 28 } if len(data) < headerSize { return nil, nil } // If this packet type doesn't have FrameInfo, return payload without it if frameInfoSize == 0 { return data[headerSize:], nil } // End packets have FrameInfo - validate size if len(data) < headerSize+frameInfoSize { return data[headerSize:], nil } fi := ParseFrameInfo(data) // Validate codec matches channel type validCodec := false switch channel { case ChannelIVideo, ChannelPVideo: validCodec = IsVideoCodec(fi.CodecID) case ChannelAudio: validCodec = IsAudioCodec(fi.CodecID) } if validCodec { if c.verbose { fiRaw := data[len(data)-frameInfoSize:] fmt.Printf("[FRAMEINFO RAW %d bytes]:\n", frameInfoSize) fmt.Printf(" [0-15]: ") for i := 0; i < 16 && i < len(fiRaw); i++ { fmt.Printf("%02x ", fiRaw[i]) } fmt.Printf("\n [16-31]: ") for i := 16; i < 32 && i < len(fiRaw); i++ { fmt.Printf("%02x ", fiRaw[i]) } fmt.Printf("\n [32-%d]: ", frameInfoSize-1) for i := 32; i < frameInfoSize && i < len(fiRaw); i++ { fmt.Printf("%02x ", fiRaw[i]) } fmt.Printf("\n") } payload := data[headerSize : len(data)-frameInfoSize] return payload, fi } return data[headerSize:], nil } func (c *Conn) handleVideo(channel byte, hdr *PacketHeader, payload []byte, fi *FrameInfo) { if c.frameAssemblers == nil { c.frameAssemblers = make(map[byte]*FrameAssembler) } asm := c.frameAssemblers[channel] // Frame transition detection: new frame number = previous frame complete if asm != nil && hdr.FrameNo != asm.frameNo { gotAll := uint16(len(asm.packets)) == asm.pktTotal if gotAll && asm.frameInfo != nil { // Perfect: all packets + FrameInfo present c.assembleAndQueueVideo(channel, asm) } else if c.verbose { // Debugging: what exactly is missing? if gotAll && asm.frameInfo == nil { fmt.Printf("[VIDEO] Frame #%d: all %d packets received but End packet lost (no FrameInfo)\n", asm.frameNo, asm.pktTotal) } else { fmt.Printf("[VIDEO] Frame #%d: incomplete %d/%d packets\n", asm.frameNo, len(asm.packets), asm.pktTotal) } } asm = nil } // Create new assembler if needed if asm == nil { asm = &FrameAssembler{ frameNo: hdr.FrameNo, pktTotal: hdr.PktTotal, packets: make(map[uint16][]byte, hdr.PktTotal), } c.frameAssemblers[channel] = asm } // Store packet (with pkt_idx as key!) // IMPORTANT: Always register the packet, even if payload is empty! // End packets may have 0 bytes payload (all data in previous packets) // but still need to be counted for completeness check. // CRITICAL: Must copy payload! The underlying buffer is reused by the worker. payloadCopy := make([]byte, len(payload)) copy(payloadCopy, payload) asm.packets[hdr.PktIdx] = payloadCopy // Store FrameInfo if present if fi != nil { asm.frameInfo = fi } // Check if frame is complete if uint16(len(asm.packets)) == asm.pktTotal && asm.frameInfo != nil { c.assembleAndQueueVideo(channel, asm) delete(c.frameAssemblers, channel) } } func (c *Conn) assembleAndQueueVideo(channel byte, asm *FrameAssembler) { fi := asm.frameInfo // Assemble packets in correct order var payload []byte for i := uint16(0); i < asm.pktTotal; i++ { if pkt, ok := asm.packets[i]; ok { payload = append(payload, pkt...) } } // Size validation if fi.PayloadSize > 0 && len(payload) != int(fi.PayloadSize) { if c.verbose { fmt.Printf("[VIDEO] Frame #%d size mismatch: got=%d expected=%d, discarding\n", asm.frameNo, len(payload), fi.PayloadSize) } return } if len(payload) == 0 { return } // Calculate RTP timestamp (90kHz for video) using relative timestamps // to avoid uint64 overflow (absoluteTS * clockRate exceeds uint64 max) absoluteTS := uint64(fi.Timestamp)*1000000 + uint64(fi.TimestampUS) if c.baseTS == 0 { c.baseTS = absoluteTS } relativeUS := absoluteTS - c.baseTS const clockRate uint64 = 90000 rtpTS := uint32(relativeUS * clockRate / 1000000) pkt := &Packet{ Channel: channel, Payload: payload, Codec: fi.CodecID, Timestamp: rtpTS, IsKeyframe: fi.IsKeyframe(), FrameNo: fi.FrameNo, } if c.verbose { frameType := "P" if fi.IsKeyframe() { frameType = "I" } fmt.Printf("[VIDEO] #%d %s %s size=%d rtp=%d\n", fi.FrameNo, CodecName(fi.CodecID), frameType, len(payload), rtpTS) } c.queuePacket(pkt) } func (c *Conn) handleAudio(payload []byte, fi *FrameInfo) { if len(payload) == 0 || fi == nil { return } var sampleRate uint32 var channels uint8 // Parse ADTS for AAC codecs, use FRAMEINFO for others switch fi.CodecID { case AudioCodecAACRaw, AudioCodecAACADTS, AudioCodecAACLATM, AudioCodecAACWyze: sampleRate, channels = ParseAudioParams(payload, fi) default: sampleRate = fi.SampleRate() channels = fi.Channels() } // Calculate RTP timestamp using relative timestamps to avoid uint64 overflow // Uses shared baseTS with video for proper A/V sync absoluteTS := uint64(fi.Timestamp)*1000000 + uint64(fi.TimestampUS) if c.baseTS == 0 { c.baseTS = absoluteTS } relativeUS := absoluteTS - c.baseTS clockRate := uint64(sampleRate) rtpTS := uint32(relativeUS * clockRate / 1000000) pkt := &Packet{ Channel: ChannelAudio, Payload: payload, Codec: fi.CodecID, Timestamp: rtpTS, SampleRate: sampleRate, Channels: channels, FrameNo: fi.FrameNo, } if c.verbose { fmt.Printf("[AUDIO] #%d %s size=%d rate=%d ch=%d rtp=%d\n", fi.FrameNo, AudioCodecName(fi.CodecID), len(payload), sampleRate, channels, rtpTS) } c.queuePacket(pkt) } func (c *Conn) queuePacket(pkt *Packet) { select { case c.packetQueue <- pkt: default: // Queue full - drop oldest select { case <-c.packetQueue: default: } c.packetQueue <- pkt } } func (c *Conn) queueIOCtrlData(data []byte) { dataCopy := make([]byte, len(data)) copy(dataCopy, data) select { case c.ioctrl <- dataCopy: default: select { case <-c.ioctrl: default: } c.ioctrl <- dataCopy } } func (c *Conn) sendACK() error { ack := c.buildACK() if c.verbose { fmt.Printf("[Conn] SendACK: txSeq=%d flags=0x%04x\n", c.avTxSeq-1, c.ackFlags) } _, err := c.mainConn.Write(ack) return err } func (c *Conn) sendIOTC(payload []byte, channel byte) (int, error) { if c.useNewProto { // NEW Protocol: send DTLS data in 0xCC51 frame with cmd=0x1502 frame := c.buildNewProtoDTLS(payload, channel) if c.verbose { fmt.Printf("\n>>> TX %d bytes (DTLS cmd=0x1502 ch=%d)\n%s", len(frame), channel, hexDump(frame)) } return c.udpConn.WriteToUDP(frame, c.addr) } // OLD Protocol: TransCode encrypted 0x0407 frame frame := c.buildDataTXChannel(payload, channel) return c.sendEncrypted(frame) } func (c *Conn) sendEncrypted(data []byte) (int, error) { if c.verbose { fmt.Printf("[OLD] TX %d bytes\n%s", len(data), hexDump(data)) } encrypted := crypto.TransCodeBlob(data) return c.udpConn.WriteToUDP(encrypted, c.addr) } func (c *Conn) buildNewProtoPacket(seq, ticket uint16, isResponse bool) []byte { pkt := make([]byte, NewProtoPacketSize) // Header [0:12] binary.LittleEndian.PutUint16(pkt[0:], MagicNewProto) // Magic 0xCC51 binary.LittleEndian.PutUint16(pkt[2:], 0x0000) // Flags binary.LittleEndian.PutUint16(pkt[4:], CmdNewProtoDiscovery) // Command 0x1002 binary.LittleEndian.PutUint16(pkt[6:], NewProtoPayloadSize) // Payload size (40 bytes) if isResponse { binary.LittleEndian.PutUint16(pkt[8:], 0xFFFF) // Direction (response) } else { binary.LittleEndian.PutUint16(pkt[8:], 0x0000) // Direction (request) } binary.LittleEndian.PutUint16(pkt[10:], 0x0000) // Reserved binary.LittleEndian.PutUint16(pkt[12:], seq) // Sequence binary.LittleEndian.PutUint16(pkt[14:], ticket) // Ticket // SessionID [16:24] copy(pkt[16:24], c.sessionID) // Capabilities [24:32] - SDK version 4.3.8.0 copy(pkt[24:32], []byte{0x00, 0x08, 0x03, 0x04, 0x1d, 0x00, 0x00, 0x00}) // Auth Bytes [32:52] - HMAC-SHA1(UID+AuthKey, header[0:32]) authKey := crypto.CalculateAuthKey(c.enr, c.mac) key := append([]byte(c.uid), authKey...) h := hmac.New(sha1.New, key) h.Write(pkt[:32]) authBytes := h.Sum(nil) copy(pkt[32:52], authBytes) return pkt } func (c *Conn) buildNewProtoDTLS(payload []byte, channel byte) []byte { payloadSize := uint16(16 + len(payload) + NewProtoAuthSize) pkt := make([]byte, NewProtoHeaderSize+len(payload)+NewProtoAuthSize) if c.verbose { fmt.Printf("[DTLS PKT] payload=%d, payloadSize=%d (0x%04x), pktLen=%d\n", len(payload), payloadSize, payloadSize, len(pkt)) } binary.LittleEndian.PutUint16(pkt[0:], MagicNewProto) binary.LittleEndian.PutUint16(pkt[2:], 0x0000) binary.LittleEndian.PutUint16(pkt[4:], CmdNewProtoDTLS) binary.LittleEndian.PutUint16(pkt[6:], payloadSize) binary.LittleEndian.PutUint16(pkt[8:], 0x0000) // Direction (request) binary.LittleEndian.PutUint16(pkt[10:], 0x0000) // Reserved binary.LittleEndian.PutUint16(pkt[12:], 0x0010) // DTLS uses fixed seq=16 (0x10) binary.LittleEndian.PutUint16(pkt[14:], c.newProtoTicket) copy(pkt[16:24], c.sessionID) binary.LittleEndian.PutUint32(pkt[24:], uint32(channel)+1) // Channel flag (main=1, back=2) copy(pkt[NewProtoHeaderSize:], payload) // Add Auth bytes at the end: HMAC-SHA1(UID+AuthKey, packet_header) authKey := crypto.CalculateAuthKey(c.enr, c.mac) key := append([]byte(c.uid), authKey...) h := hmac.New(sha1.New, key) h.Write(pkt[:NewProtoHeaderSize]) // Hash the header portion authBytes := h.Sum(nil) copy(pkt[NewProtoHeaderSize+len(payload):], authBytes) return pkt } func (c *Conn) buildAudioFrame(payload []byte, timestampUS uint32, codec uint16, sampleRate uint32, channels uint8) []byte { const frameInfoSize = 16 const headerSize = 36 c.audioTxSeq++ c.audioTxFrameNo++ totalPayload := len(payload) + frameInfoSize frame := make([]byte, headerSize+totalPayload) // Calculate prev_frame_no (0 for first frame, otherwise frame_no - 1) prevFrameNo := uint32(0) if c.audioTxFrameNo > 1 { prevFrameNo = c.audioTxFrameNo - 1 } // Type 0x09 "Single" - 36-byte header with full timestamp frame[0] = ChannelAudio // 0x03 frame[1] = FrameTypeStartAlt // 0x09 binary.LittleEndian.PutUint16(frame[2:], ProtocolVersion) // 0x000c binary.LittleEndian.PutUint32(frame[4:], c.audioTxSeq) binary.LittleEndian.PutUint32(frame[8:], timestampUS) // Timestamp in header // Flags at [12-15]: first frame uses 0x00000001, subsequent use 0x00100001 if c.audioTxFrameNo == 1 { binary.LittleEndian.PutUint32(frame[12:], 0x00000001) } else { binary.LittleEndian.PutUint32(frame[12:], 0x00100001) } // Inner header frame[16] = ChannelAudio // 0x03 frame[17] = FrameTypeEndSingle // 0x01 binary.LittleEndian.PutUint16(frame[18:], uint16(prevFrameNo)) // prev_frame_no (16-bit) binary.LittleEndian.PutUint16(frame[20:], 0x0001) // pkt_total = 1 binary.LittleEndian.PutUint16(frame[22:], 0x0010) // flags binary.LittleEndian.PutUint32(frame[24:], uint32(totalPayload)) // payload size binary.LittleEndian.PutUint32(frame[28:], prevFrameNo) // prev_frame_no again (32-bit) binary.LittleEndian.PutUint32(frame[32:], c.audioTxFrameNo) // frame_no // Audio payload copy(frame[headerSize:], payload) // FrameInfo (16 bytes) at end of payload samplesPerFrame := GetSamplesPerFrame(codec) frameDurationMs := samplesPerFrame * 1000 / sampleRate fi := frame[headerSize+len(payload):] binary.LittleEndian.PutUint16(fi[:], codec) // codec_id fi[2] = BuildAudioFlags(sampleRate, true, channels == 2) // flags fi[3] = 0 // cam_index fi[4] = 1 // onlineNum = 1 fi[5] = 0 // tags // fi[6:12] = reserved (already 0) binary.LittleEndian.PutUint32(fi[12:], (c.audioTxFrameNo-1)*frameDurationMs) if c.verbose { fmt.Printf("[AUDIO TX] FrameInfo: codec=0x%04x flags=0x%02x online=%d ts=%d\n", codec, fi[2], fi[4], binary.LittleEndian.Uint32(fi[12:])) } return frame } func (c *Conn) buildDisco(stage byte) []byte { frame := make([]byte, OldProtoDiscoPacketSize) // IOTC Frame Header [0-15] frame[0] = 0x04 // [0] Marker1 frame[1] = 0x02 // [1] Marker2 frame[2] = 0x1a // [2] Marker3 frame[3] = 0x02 // [3] Mode = Disco binary.LittleEndian.PutUint16(frame[4:], OldProtoDiscoBodySize) // [4-5] BodySize binary.LittleEndian.PutUint16(frame[8:], CmdDiscoReq) // [8-9] Command = 0x0601 binary.LittleEndian.PutUint16(frame[10:], 0x0021) // [10-11] Flags // Body [16-87] body := frame[OldProtoHeaderSize:] copy(body[:UIDSize], c.uid) // [0-19] UID (20 bytes) body[36] = 0x01 // [36] Unknown1 body[37] = 0x01 // [37] Unknown2 body[38] = 0x02 // [38] Unknown3 body[39] = 0x04 // [39] Unknown4 copy(body[40:], c.randomID) // [40-47] RandomID body[48] = stage // [48] Stage (1=broadcast, 2=direct) if stage == 1 && len(c.authKey) > 0 { copy(body[58:], c.authKey) // [58-65] AuthKey } return frame } func (c *Conn) buildSession() []byte { frame := make([]byte, OldProtoSessionPacketSize) // IOTC Frame Header [0-15] frame[0] = 0x04 // [0] Marker1 frame[1] = 0x02 // [1] Marker2 frame[2] = 0x1a // [2] Marker3 frame[3] = 0x02 // [3] Mode binary.LittleEndian.PutUint16(frame[4:], OldProtoSessionBodySize) // [4-5] BodySize binary.LittleEndian.PutUint16(frame[8:], CmdSessionReq) // [8-9] Command = 0x0402 binary.LittleEndian.PutUint16(frame[10:], 0x0033) // [10-11] Flags // Body [16-51] body := frame[OldProtoHeaderSize:] copy(body[:UIDSize], c.uid) // [0-19] UID (20 bytes) copy(body[UIDSize:], c.randomID) // [20-27] RandomID ts := uint32(time.Now().Unix()) binary.LittleEndian.PutUint32(body[32:], ts) // [32-35] Timestamp return frame } func (c *Conn) buildDTLSConfig(isServer bool) *dtls.Config { config := &dtls.Config{ PSK: func(hint []byte) ([]byte, error) { if c.verbose { fmt.Printf("[DTLS] PSK callback, hint: %s\n", string(hint)) } return c.psk, nil }, PSKIdentityHint: []byte(PSKIdentity), InsecureSkipVerify: true, InsecureSkipVerifyHello: true, MTU: 1200, FlightInterval: 300 * time.Millisecond, ExtendedMasterSecret: dtls.DisableExtendedMasterSecret, } // Use custom cipher suites for client, standard for server if isServer { config.CipherSuites = []dtls.CipherSuiteID{dtls.TLS_PSK_WITH_AES_128_CBC_SHA256} } else { config.CustomCipherSuites = CustomCipherSuites } if c.verbose { fmt.Printf("[DTLS] Config: isServer=%v, MTU=%d, FlightInterval=%v\n", isServer, config.MTU, config.FlightInterval) } return config } func (c *Conn) buildDataTXChannel(payload []byte, channel byte) []byte { const subHeaderSize = 12 bodySize := subHeaderSize + len(payload) frameSize := 16 + bodySize frame := make([]byte, frameSize) // IOTC Frame Header [0-15] frame[0] = 0x04 // [0] Marker1 frame[1] = 0x02 // [1] Marker2 frame[2] = 0x1a // [2] Marker3 frame[3] = 0x0b // [3] Mode = Data binary.LittleEndian.PutUint16(frame[4:], uint16(bodySize)) // [4-5] BodySize binary.LittleEndian.PutUint16(frame[6:], c.iotcTxSeq) // [6-7] Sequence c.iotcTxSeq++ binary.LittleEndian.PutUint16(frame[8:], CmdDataTX) // [8-9] Command = 0x0407 binary.LittleEndian.PutUint16(frame[10:], 0x0021) // [10-11] Flags copy(frame[12:], c.randomID[:2]) // [12-13] RandomID[0:2] frame[14] = channel // [14] Channel (0=Main, 1=Back) frame[15] = 0x01 // [15] Marker // Sub-Header [16-27] binary.LittleEndian.PutUint32(frame[16:], 0x0000000c) // [16-19] Const copy(frame[20:], c.randomID[:8]) // [20-27] RandomID // Payload [28+] copy(frame[28:], payload) return frame } func (c *Conn) buildACK() []byte { if c.ackFlags == 0 { c.ackFlags = 0x0001 } else if c.ackFlags < 0x0007 { c.ackFlags++ } ack := make([]byte, 24) binary.LittleEndian.PutUint16(ack[0:], MagicACK) // [0-1] Magic = 0x0009 binary.LittleEndian.PutUint16(ack[2:], ProtocolVersion) // [2-3] Version = 0x000C binary.LittleEndian.PutUint32(ack[4:], c.avTxSeq) // [4-7] TxSeq c.avTxSeq++ binary.LittleEndian.PutUint32(ack[8:], 0xffffffff) // [8-11] RxSeq (not used) binary.LittleEndian.PutUint16(ack[12:], c.ackFlags) // [12-13] AckFlags binary.LittleEndian.PutUint32(ack[16:], uint32(c.ackFlags)<<16) // [16-19] AckCounter return ack } func (c *Conn) buildKeepaliveResponse(incomingPayload []byte) []byte { frame := make([]byte, 24) // IOTC Frame Header [0-15] frame[0] = 0x04 // [0] Marker1 frame[1] = 0x02 // [1] Marker2 frame[2] = 0x1a // [2] Marker3 frame[3] = 0x0a // [3] Mode binary.LittleEndian.PutUint16(frame[4:], 8) // [4-5] BodySize = 8 binary.LittleEndian.PutUint16(frame[8:], CmdKeepaliveReq) // [8-9] Command = 0x0427 binary.LittleEndian.PutUint16(frame[10:], 0x0021) // [10-11] Flags // Body [16-23]: Echo back incoming payload if len(incomingPayload) >= 8 { copy(frame[16:], incomingPayload[:8]) // [16-23] EchoPayload } return frame } func (c *Conn) buildAVLoginPacket(magic uint16, size int, flags uint16, randomID []byte) []byte { pkt := make([]byte, size) // Header binary.LittleEndian.PutUint16(pkt, magic) binary.LittleEndian.PutUint16(pkt[2:], ProtocolVersion) // bytes 4-15: reserved (zeros) // Payload info at offset 16 payloadSize := uint16(size - 24) // total - header(16) - random(4) - padding(4) binary.LittleEndian.PutUint16(pkt[16:], payloadSize) binary.LittleEndian.PutUint16(pkt[18:], flags) copy(pkt[20:], randomID[:4]) // Credentials (each field is 256 bytes) copy(pkt[24:], DefaultUser) // username at offset 24 (payload byte 0) copy(pkt[280:], c.enr) // password (ENR) at offset 280 (payload byte 256) // Config section (AVClientStartInConfig) starts at offset 536 (= 24 + 256 + 256) // Layout: resend(4) + security_mode(4) + auth_type(4) + sync_recv_data(4) + ... binary.LittleEndian.PutUint32(pkt[536:], 0) // resend=0 binary.LittleEndian.PutUint32(pkt[540:], 2) // security_mode=2 (AV_SECURITY_AUTO) binary.LittleEndian.PutUint32(pkt[544:], 0) // auth_type=0 (AV_AUTH_PASSWORD) binary.LittleEndian.PutUint32(pkt[548:], 0) // sync_recv_data=0 binary.LittleEndian.PutUint32(pkt[552:], DefaultCapabilities) // capabilities binary.LittleEndian.PutUint16(pkt[556:], 0) // request_video_on_connect=0 binary.LittleEndian.PutUint16(pkt[558:], 0) // request_audio_on_connect=0 return pkt } func (c *Conn) buildAVLoginResponse(checksum uint32) []byte { resp := make([]byte, 60) // Header binary.LittleEndian.PutUint16(resp, 0x2100) // Magic binary.LittleEndian.PutUint16(resp[2:], 0x000c) // Version resp[4] = 0x10 // Response type (success) // Payload info binary.LittleEndian.PutUint32(resp[16:], 0x24) // Payload size = 36 binary.LittleEndian.PutUint32(resp[20:], checksum) // Echo checksum from request! // Payload (36 bytes starting at offset 24) resp[29] = 0x01 // EnableFlag resp[31] = 0x01 // TwoWayStreaming binary.LittleEndian.PutUint32(resp[36:], 0x04) // BufferConfig binary.LittleEndian.PutUint32(resp[40:], 0x001f07fb) // Capabilities binary.LittleEndian.PutUint16(resp[54:], 0x0003) // ChannelInfo1 binary.LittleEndian.PutUint16(resp[56:], 0x0002) // ChannelInfo2 return resp } func (c *Conn) buildIOCtrlFrame(payload []byte) []byte { const headerSize = 40 frame := make([]byte, headerSize+len(payload)) // Magic (same as protocol version for IOCtrl frames) binary.LittleEndian.PutUint16(frame, ProtocolVersion) // Version binary.LittleEndian.PutUint16(frame[2:], ProtocolVersion) // AVSeq (4-7) seq := c.avTxSeq c.avTxSeq++ binary.LittleEndian.PutUint32(frame[4:], seq) // Bytes 8-15: reserved // Channel: MagicIOCtrl (0x7000) for IOCtrl frames binary.LittleEndian.PutUint16(frame[16:], MagicIOCtrl) // SubChannel (18-19): increments with each IOCtrl command sent binary.LittleEndian.PutUint16(frame[18:], c.ioctrlSeq) // IOCTLSeq (20-23): always 1 binary.LittleEndian.PutUint32(frame[20:], 1) // PayloadSize (24-27): payload + 4 bytes padding binary.LittleEndian.PutUint32(frame[24:], uint32(len(payload)+4)) // Flag (28-31): matches subChannel in SDK binary.LittleEndian.PutUint32(frame[28:], uint32(c.ioctrlSeq)) // Bytes 32-36: reserved // Byte 37: 0x01 frame[37] = 0x01 // Bytes 38-39: reserved // Payload at offset 40 copy(frame[headerSize:], payload) c.ioctrlSeq++ return frame } func (c *Conn) logAVPacket(channel, frameType byte, payload []byte, fi *FrameInfo) { fmt.Printf("[Conn] AV: ch=0x%02x type=0x%02x len=%d", channel, frameType, len(payload)) if fi != nil { fmt.Printf(" fi={codec=0x%04x flags=0x%02x ts=%d}", fi.CodecID, fi.Flags, fi.Timestamp) } fmt.Printf("\n") } func (c *Conn) logAudioTX(frame []byte, codec uint16, payloadLen int, timestampUS uint32, sampleRate uint32, channels uint8) { chStr := "mono" if channels == 2 { chStr = "stereo" } // Determine header size based on frame type headerSize := 28 frameType := "P-Start" if len(frame) >= 2 && frame[1] == FrameTypeStartAlt { headerSize = 36 frameType = "Single" } fmt.Printf("[AUDIO TX] %s codec=0x%04x (%s) payload=%d ts=%d rate=%d %s total=%d\n", frameType, codec, AudioCodecName(codec), payloadLen, timestampUS, sampleRate, chStr, len(frame)) // Dump frame header for comparison with SDK if len(frame) >= headerSize { fmt.Printf(" HEADER[0..%d]: ", headerSize-1) for i := 0; i < headerSize; i++ { fmt.Printf("%02x ", frame[i]) } fmt.Printf("\n") } // First few payload bytes (for comparison with SDK) if payloadLen > 0 && len(frame) > headerSize { maxShow := min(16, payloadLen) fmt.Printf(" PAYLOAD[%d..%d]: ", headerSize, headerSize+maxShow-1) for i := 0; i < maxShow; i++ { fmt.Printf("%02x ", frame[headerSize+i]) } if payloadLen > maxShow { fmt.Printf("...") } fmt.Printf("\n") } } func genRandomID() []byte { b := make([]byte, 8) _, _ = rand.Read(b) return b } func getBroadcastAddrs(port int, verbose bool) []*net.UDPAddr { var addrs []*net.UDPAddr ifaces, err := net.Interfaces() if err != nil { if verbose { fmt.Printf("[IOTC] Failed to get interfaces: %v\n", err) } // Fallback to limited broadcast return []*net.UDPAddr{{IP: net.IPv4(255, 255, 255, 255), Port: port}} } for _, iface := range ifaces { // Skip loopback and down interfaces if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 { continue } ifAddrs, err := iface.Addrs() if err != nil { continue } for _, addr := range ifAddrs { ipNet, ok := addr.(*net.IPNet) if !ok { continue } // Only IPv4 ip4 := ipNet.IP.To4() if ip4 == nil { continue } // Calculate broadcast address: IP | ~mask mask := ipNet.Mask if len(mask) != 4 { continue } broadcast := make(net.IP, 4) for i := 0; i < 4; i++ { broadcast[i] = ip4[i] | ^mask[i] } bcastAddr := &net.UDPAddr{IP: broadcast, Port: port} addrs = append(addrs, bcastAddr) if verbose { fmt.Printf("[IOTC] Found broadcast address: %s (iface: %s)\n", bcastAddr, iface.Name) } } } if len(addrs) == 0 { // Fallback to limited broadcast if verbose { fmt.Printf("[IOTC] No broadcast addresses found, using 255.255.255.255\n") } return []*net.UDPAddr{{IP: net.IPv4(255, 255, 255, 255), Port: port}} } return addrs } func hexDump(data []byte) string { var result string for i := 0; i < len(data); i += 16 { end := i + 16 if end > len(data) { end = len(data) } line := fmt.Sprintf(" %04x:", i) for j := i; j < end; j++ { line += fmt.Sprintf(" %02x", data[j]) } result += line + "\n" } return result }