mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2026-04-22 23:57:20 +08:00
refactor
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
package tutk
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CalculateAuthKey(enr, mac string) []byte {
|
||||
data := enr + strings.ToUpper(mac)
|
||||
hash := sha256.Sum256([]byte(data))
|
||||
b64 := base64.StdEncoding.EncodeToString(hash[:6])
|
||||
b64 = strings.ReplaceAll(b64, "+", "Z")
|
||||
b64 = strings.ReplaceAll(b64, "/", "9")
|
||||
b64 = strings.ReplaceAll(b64, "=", "A")
|
||||
return []byte(b64)
|
||||
}
|
||||
|
||||
func DerivePSK(enr string) []byte {
|
||||
// DerivePSK derives the DTLS PSK from ENR
|
||||
// TUTK SDK treats the PSK as a NULL-terminated C string, so if SHA256(ENR)
|
||||
// contains a 0x00 byte, the PSK is truncated at that position.
|
||||
hash := sha256.Sum256([]byte(enr))
|
||||
pskLen := 32
|
||||
for i := range 32 {
|
||||
if hash[i] == 0x00 {
|
||||
pskLen = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
psk := make([]byte, 32)
|
||||
copy(psk[:pskLen], hash[:pskLen])
|
||||
return psk
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func computeNonce(iv []byte, epoch uint16, sequenceNumber uint64) []byte {
|
||||
binary.BigEndian.PutUint64(nonce[4:], sequenceNumber)
|
||||
binary.BigEndian.PutUint16(nonce[4:], epoch)
|
||||
|
||||
for i := 0; i < chachaNonceLength; i++ {
|
||||
for i := range chachaNonceLength {
|
||||
nonce[i] ^= iv[i]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package tutk
|
||||
|
||||
// https://github.com/seydx/tutk_wyze#11-codec-reference
|
||||
const (
|
||||
CodecMPEG4 byte = 0x4C
|
||||
CodecH263 byte = 0x4D
|
||||
CodecH264 byte = 0x4E
|
||||
CodecMJPEG byte = 0x4F
|
||||
CodecH265 byte = 0x50
|
||||
)
|
||||
|
||||
const (
|
||||
CodecAACRaw byte = 0x86
|
||||
CodecAACADTS byte = 0x87
|
||||
CodecAACLATM byte = 0x88
|
||||
CodecPCMU byte = 0x89
|
||||
CodecPCMA byte = 0x8A
|
||||
CodecADPCM byte = 0x8B
|
||||
CodecPCML byte = 0x8C
|
||||
CodecSPEEX byte = 0x8D
|
||||
CodecMP3 byte = 0x8E
|
||||
CodecG726 byte = 0x8F
|
||||
CodecAACAlt byte = 0x90
|
||||
CodecOpus byte = 0x92
|
||||
)
|
||||
|
||||
var sampleRates = [9]uint32{8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000}
|
||||
|
||||
func GetSamplesPerFrame(codecID byte) uint32 {
|
||||
switch codecID {
|
||||
case CodecAACRaw, CodecAACADTS, CodecAACLATM, CodecAACAlt:
|
||||
return 1024
|
||||
case CodecPCMU, CodecPCMA, CodecPCML, CodecADPCM, CodecSPEEX, CodecG726:
|
||||
return 160
|
||||
case CodecMP3:
|
||||
return 1152
|
||||
case CodecOpus:
|
||||
return 960
|
||||
default:
|
||||
return 1024
|
||||
}
|
||||
}
|
||||
|
||||
func IsVideoCodec(id byte) bool {
|
||||
return id >= CodecMPEG4 && id <= CodecH265
|
||||
}
|
||||
|
||||
func IsAudioCodec(id byte) bool {
|
||||
return id >= CodecAACRaw && id <= CodecOpus
|
||||
}
|
||||
@@ -3,31 +3,71 @@ 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 (
|
||||
MaxPacketSize = 2048
|
||||
ReadBufferSize = 2 * 1024 * 1024
|
||||
DiscoTimeout = 5000 * time.Millisecond
|
||||
DiscoInterval = 100 * time.Millisecond
|
||||
SessionTimeout = 5000 * time.Millisecond
|
||||
ReadWaitInterval = 50 * time.Millisecond
|
||||
magicCC51 = "\x51\xcc" // (wyze specific?)
|
||||
sdkVersion42 = "\x01\x01\x02\x04" // 4.2.1.1
|
||||
sdkVersion43 = "\x00\x08\x03\x04" // 4.3.8.0
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
const (
|
||||
cmdDiscoReq uint16 = 0x0601
|
||||
cmdDiscoRes uint16 = 0x0602
|
||||
cmdSessionReq uint16 = 0x0402
|
||||
cmdSessionRes uint16 = 0x0404
|
||||
cmdDataTX uint16 = 0x0407
|
||||
cmdDataRX uint16 = 0x0408
|
||||
cmdKeepaliveReq uint16 = 0x0427
|
||||
cmdKeepaliveRes uint16 = 0x0428
|
||||
|
||||
headerSize = 16
|
||||
discoBodySize = 72
|
||||
discoSize = headerSize + discoBodySize
|
||||
sessionBody = 36
|
||||
sessionSize = headerSize + sessionBody
|
||||
)
|
||||
|
||||
const (
|
||||
cmdDiscoCC51 uint16 = 0x1002
|
||||
cmdKeepaliveCC51 uint16 = 0x1202
|
||||
cmdDTLSCC51 uint16 = 0x1502
|
||||
payloadSizeCC51 uint16 = 0x0028
|
||||
packetSizeCC51 = 52
|
||||
headerSizeCC51 = 28
|
||||
authSizeCC51 = 20
|
||||
keepaliveSizeCC51 = 48
|
||||
)
|
||||
|
||||
const (
|
||||
magicAVLoginResp uint16 = 0x2100
|
||||
magicIOCtrl uint16 = 0x7000
|
||||
magicChannelMsg uint16 = 0x1000
|
||||
magicACK uint16 = 0x0009
|
||||
magicAVLogin1 uint16 = 0x0000
|
||||
magicAVLogin2 uint16 = 0x2000
|
||||
)
|
||||
|
||||
const (
|
||||
protoVersion uint16 = 0x000c
|
||||
defaultCaps uint32 = 0x001f07fb
|
||||
)
|
||||
|
||||
const (
|
||||
iotcChannelMain = 0 // Main AV (we = DTLS Client)
|
||||
iotcChannelBack = 1 // Backchannel (we = DTLS Server)
|
||||
)
|
||||
|
||||
type DTLSConn struct {
|
||||
conn *net.UDPConn
|
||||
addr *net.UDPAddr
|
||||
frames *FrameHandler
|
||||
@@ -49,17 +89,15 @@ type Conn struct {
|
||||
uid string
|
||||
authKey string
|
||||
enr string
|
||||
mac string
|
||||
psk []byte
|
||||
rid []byte
|
||||
|
||||
// Session
|
||||
sid []byte
|
||||
ticket uint16
|
||||
avResp *AVLoginResponse
|
||||
sid []byte
|
||||
ticket uint16
|
||||
hasTwoWayStreaming bool
|
||||
|
||||
// Protocol
|
||||
newProto bool
|
||||
isCC51 bool
|
||||
seq uint16
|
||||
seqCmd uint16
|
||||
avSeq uint32
|
||||
@@ -75,34 +113,32 @@ type Conn struct {
|
||||
cmdAck func()
|
||||
}
|
||||
|
||||
func Dial(host string, port int, uid, authKey, enr, mac string, verbose bool) (*Conn, error) {
|
||||
func DialDTLS(host string, port int, uid, authKey, enr string, verbose bool) (*DTLSConn, error) {
|
||||
udp, err := net.ListenUDP("udp", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = udp.SetReadBuffer(ReadBufferSize)
|
||||
_ = udp.SetReadBuffer(2 * 1024 * 1024)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
psk := derivePSK(enr)
|
||||
psk := DerivePSK(enr)
|
||||
|
||||
if port == 0 {
|
||||
port = DefaultPort
|
||||
port = 32761
|
||||
}
|
||||
|
||||
c := &Conn{
|
||||
c := &DTLSConn{
|
||||
conn: udp,
|
||||
addr: &net.UDPAddr{IP: net.ParseIP(host), Port: port},
|
||||
rid: genRandomID(),
|
||||
uid: uid,
|
||||
authKey: authKey,
|
||||
enr: enr,
|
||||
mac: mac,
|
||||
psk: psk,
|
||||
verbose: verbose,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
rxSeqStart: 0xffff, // Initialize RX seq for ACK
|
||||
rxSeqStart: 0xffff,
|
||||
rxSeqEnd: 0xffff,
|
||||
}
|
||||
|
||||
@@ -130,10 +166,10 @@ func Dial(host string, port int, uid, authKey, enr, mac string, verbose bool) (*
|
||||
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)
|
||||
func (c *DTLSConn) AVClientStart(timeout time.Duration) error {
|
||||
randomID := GenSessionID()
|
||||
pkt1 := c.msgAVLogin(magicAVLogin1, 570, 0x0001, randomID)
|
||||
pkt2 := c.msgAVLogin(magicAVLogin2, 572, 0x0000, randomID)
|
||||
pkt2[20]++ // pkt2 has randomID incremented by 1
|
||||
|
||||
if _, err := c.clientConn.Write(pkt1); err != nil {
|
||||
@@ -155,16 +191,13 @@ func (c *Conn) AVClientStart(timeout time.Duration) error {
|
||||
if !ok {
|
||||
return io.EOF
|
||||
}
|
||||
if len(data) >= 32 && binary.LittleEndian.Uint16(data) == MagicAVLoginResp {
|
||||
c.avResp = &AVLoginResponse{
|
||||
ServerType: binary.LittleEndian.Uint32(data[4:]),
|
||||
Resend: int32(data[29]),
|
||||
TwoWayStreaming: int32(data[31]),
|
||||
}
|
||||
if len(data) >= 32 && binary.LittleEndian.Uint16(data) == magicAVLoginResp {
|
||||
c.hasTwoWayStreaming = data[31] == 1
|
||||
|
||||
ack := c.buildACK()
|
||||
ack := c.msgACK()
|
||||
c.clientConn.Write(ack)
|
||||
|
||||
// Start ACK sender for continuous streaming
|
||||
c.wg.Add(1)
|
||||
go func() {
|
||||
defer c.wg.Done()
|
||||
@@ -177,7 +210,7 @@ func (c *Conn) AVClientStart(timeout time.Duration) error {
|
||||
return
|
||||
case <-ackTicker.C:
|
||||
if c.clientConn != nil {
|
||||
ack := c.buildACK()
|
||||
ack := c.msgACK()
|
||||
c.clientConn.Write(ack)
|
||||
}
|
||||
}
|
||||
@@ -192,14 +225,9 @@ func (c *Conn) AVClientStart(timeout time.Duration) error {
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
conn, err := NewDtlsServer(c, IOTCChannelBack, c.psk)
|
||||
func (c *DTLSConn) AVServStart() error {
|
||||
adapter := NewChannelAdapter(c.ctx, iotcChannelBack, c.addr, c.WriteDTLS, c.serverBuf)
|
||||
conn, err := NewDTLSServer(adapter, c.addr, c.psk)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dtls: server handshake failed: %w", err)
|
||||
}
|
||||
@@ -209,7 +237,7 @@ func (c *Conn) AVServStart() error {
|
||||
c.mu.Unlock()
|
||||
|
||||
if c.verbose {
|
||||
fmt.Printf("[DTLS] Server handshake complete on channel %d\n", IOTCChannelBack)
|
||||
fmt.Printf("[DTLS] Server handshake complete on channel %d\n", iotcChannelBack)
|
||||
}
|
||||
|
||||
// Wait for and respond to AV Login request from camera
|
||||
@@ -220,10 +248,11 @@ func (c *Conn) AVServStart() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) AVServStop() error {
|
||||
func (c *DTLSConn) AVServStop() error {
|
||||
c.mu.Lock()
|
||||
serverConn := c.serverConn
|
||||
c.serverConn = nil
|
||||
|
||||
// Reset audio TX state
|
||||
c.audioSeq = 0
|
||||
c.audioFrameNo = 0
|
||||
@@ -238,7 +267,7 @@ func (c *Conn) AVServStop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) AVRecvFrameData() (*Packet, error) {
|
||||
func (c *DTLSConn) AVRecvFrameData() (*Packet, error) {
|
||||
select {
|
||||
case pkt, ok := <-c.frames.Recv():
|
||||
if !ok {
|
||||
@@ -250,7 +279,7 @@ func (c *Conn) AVRecvFrameData() (*Packet, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) AVSendAudioData(codec uint16, payload []byte, timestampUS uint32, sampleRate uint32, channels uint8) error {
|
||||
func (c *DTLSConn) AVSendAudioData(codec byte, payload []byte, timestampUS uint32, sampleRate uint32, channels uint8) error {
|
||||
c.mu.Lock()
|
||||
conn := c.serverConn
|
||||
if conn == nil {
|
||||
@@ -258,7 +287,7 @@ func (c *Conn) AVSendAudioData(codec uint16, payload []byte, timestampUS uint32,
|
||||
return fmt.Errorf("speaker channel not connected")
|
||||
}
|
||||
|
||||
frame := c.buildAudioFrame(payload, timestampUS, codec, sampleRate, channels)
|
||||
frame := c.msgAudioFrame(payload, timestampUS, codec, sampleRate, channels)
|
||||
|
||||
c.mu.Unlock()
|
||||
|
||||
@@ -273,35 +302,27 @@ func (c *Conn) AVSendAudioData(codec uint16, payload []byte, timestampUS uint32,
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) Write(data []byte) error {
|
||||
// if c.verbose {
|
||||
// fmt.Printf("[UDP TX] to=%s, len=%d, data:\n%s", c.addr.String(), len(data), hexDump(data))
|
||||
// }
|
||||
|
||||
if c.newProto {
|
||||
func (c *DTLSConn) Write(data []byte) error {
|
||||
if c.isCC51 {
|
||||
_, err := c.conn.WriteToUDP(data, c.addr)
|
||||
return err
|
||||
}
|
||||
_, err := c.conn.WriteToUDP(crypto.TransCodeBlob(data), c.addr)
|
||||
_, err := c.conn.WriteToUDP(TransCodeBlob(data), c.addr)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) WriteDTLS(payload []byte, channel byte) error {
|
||||
func (c *DTLSConn) WriteDTLS(payload []byte, channel byte) error {
|
||||
var frame []byte
|
||||
if c.newProto {
|
||||
frame = c.buildNewTxData(payload, channel)
|
||||
if c.isCC51 {
|
||||
frame = c.msgTxDataCC51(payload, channel)
|
||||
} else {
|
||||
frame = c.buildTxData(payload, channel)
|
||||
frame = c.msgTxData(payload, channel)
|
||||
}
|
||||
|
||||
// if c.verbose {
|
||||
// fmt.Printf("[DTLS TX] to=%s, len=%d, channel=%d, data:\n%s", c.addr.String(), len(frame), channel, hexDump(frame))
|
||||
// }
|
||||
|
||||
return c.Write(frame)
|
||||
}
|
||||
|
||||
func (c *Conn) WriteAndWait(req []byte, timeout time.Duration, ok func(res []byte) bool) ([]byte, error) {
|
||||
func (c *DTLSConn) WriteAndWait(req []byte, ok func(res []byte) bool) ([]byte, error) {
|
||||
var t *time.Timer
|
||||
t = time.AfterFunc(1, func() {
|
||||
if err := c.Write(req); err == nil && t != nil {
|
||||
@@ -310,10 +331,10 @@ func (c *Conn) WriteAndWait(req []byte, timeout time.Duration, ok func(res []byt
|
||||
})
|
||||
defer t.Stop()
|
||||
|
||||
_ = c.conn.SetDeadline(time.Now().Add(timeout))
|
||||
_ = c.conn.SetDeadline(time.Now().Add(5000 * time.Millisecond))
|
||||
defer c.conn.SetDeadline(time.Time{})
|
||||
|
||||
buf := make([]byte, MaxPacketSize)
|
||||
buf := make([]byte, 2048)
|
||||
for {
|
||||
n, addr, err := c.conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
@@ -324,10 +345,10 @@ func (c *Conn) WriteAndWait(req []byte, timeout time.Duration, ok func(res []byt
|
||||
}
|
||||
|
||||
var res []byte
|
||||
if c.newProto {
|
||||
if c.isCC51 {
|
||||
res = buf[:n]
|
||||
} else {
|
||||
res = crypto.ReverseTransCodeBlob(buf[:n])
|
||||
res = ReverseTransCodeBlob(buf[:n])
|
||||
}
|
||||
|
||||
if ok(res) {
|
||||
@@ -337,8 +358,8 @@ func (c *Conn) WriteAndWait(req []byte, timeout time.Duration, ok func(res []byt
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) WriteAndWaitIOCtrl(cmd uint16, payload []byte, expectCmd uint16, timeout time.Duration) ([]byte, error) {
|
||||
frame := c.buildIOCtrlFrame(payload)
|
||||
func (c *DTLSConn) WriteAndWaitIOCtrl(cmd uint16, payload []byte, expectCmd uint16, timeout time.Duration) ([]byte, error) {
|
||||
frame := c.msgIOCtrl(payload)
|
||||
var t *time.Timer
|
||||
t = time.AfterFunc(1, func() {
|
||||
c.mu.RLock()
|
||||
@@ -362,7 +383,7 @@ func (c *Conn) WriteAndWaitIOCtrl(cmd uint16, payload []byte, expectCmd uint16,
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
ack := c.buildACK()
|
||||
ack := c.msgACK()
|
||||
c.clientConn.Write(ack)
|
||||
|
||||
if len(data) >= 6 {
|
||||
@@ -376,29 +397,29 @@ func (c *Conn) WriteAndWaitIOCtrl(cmd uint16, payload []byte, expectCmd uint16,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) GetAVLoginResponse() *AVLoginResponse {
|
||||
return c.avResp
|
||||
func (c *DTLSConn) HasTwoWayStreaming() bool {
|
||||
return c.hasTwoWayStreaming
|
||||
}
|
||||
|
||||
func (c *Conn) IsBackchannelReady() bool {
|
||||
func (c *DTLSConn) IsBackchannelReady() bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.serverConn != nil
|
||||
}
|
||||
|
||||
func (c *Conn) RemoteAddr() *net.UDPAddr {
|
||||
func (c *DTLSConn) RemoteAddr() *net.UDPAddr {
|
||||
return c.addr
|
||||
}
|
||||
|
||||
func (c *Conn) LocalAddr() *net.UDPAddr {
|
||||
func (c *DTLSConn) LocalAddr() *net.UDPAddr {
|
||||
return c.conn.LocalAddr().(*net.UDPAddr)
|
||||
}
|
||||
|
||||
func (c *Conn) SetDeadline(t time.Time) error {
|
||||
func (c *DTLSConn) SetDeadline(t time.Time) error {
|
||||
return c.conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
func (c *DTLSConn) Close() error {
|
||||
c.cancel()
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -416,27 +437,27 @@ func (c *Conn) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Conn) Error() error {
|
||||
func (c *DTLSConn) Error() error {
|
||||
if c.err != nil {
|
||||
return c.err
|
||||
}
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
func (c *Conn) discovery() error {
|
||||
c.sid = make([]byte, 8)
|
||||
rand.Read(c.sid)
|
||||
func (c *DTLSConn) discovery() error {
|
||||
c.sid = GenSessionID()
|
||||
|
||||
oldPkt := crypto.TransCodeBlob(c.buildDisco(1))
|
||||
newPkt := c.buildNewDisco(0, 0, false)
|
||||
buf := make([]byte, MaxPacketSize)
|
||||
deadline := time.Now().Add(DiscoTimeout)
|
||||
pktIOTC := TransCodeBlob(c.msgDisco(1))
|
||||
pktCC51 := c.msgDiscoCC51(0, 0, false)
|
||||
|
||||
buf := make([]byte, 2048)
|
||||
deadline := time.Now().Add(5000 * time.Millisecond)
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
c.conn.WriteToUDP(oldPkt, c.addr)
|
||||
c.conn.WriteToUDP(newPkt, c.addr)
|
||||
c.conn.WriteToUDP(pktIOTC, c.addr)
|
||||
c.conn.WriteToUDP(pktCC51, c.addr)
|
||||
|
||||
c.conn.SetReadDeadline(time.Now().Add(DiscoInterval))
|
||||
c.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||
n, addr, err := c.conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
continue
|
||||
@@ -445,59 +466,54 @@ func (c *Conn) discovery() error {
|
||||
continue
|
||||
}
|
||||
|
||||
// NEW protocol
|
||||
if n >= NewPacketSize && binary.LittleEndian.Uint16(buf[:2]) == MagicNewProto {
|
||||
if binary.LittleEndian.Uint16(buf[4:]) == CmdNewDisco {
|
||||
c.addr, c.newProto, c.ticket = addr, true, binary.LittleEndian.Uint16(buf[14:])
|
||||
// CC51 protocol
|
||||
if n >= packetSizeCC51 && string(buf[:2]) == magicCC51 {
|
||||
if binary.LittleEndian.Uint16(buf[4:]) == cmdDiscoCC51 {
|
||||
c.addr, c.isCC51, c.ticket = addr, true, binary.LittleEndian.Uint16(buf[14:])
|
||||
if n >= 24 {
|
||||
copy(c.sid, buf[16:24])
|
||||
}
|
||||
return c.newDiscoDone()
|
||||
return c.discoDoneCC51()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// OLD protocol
|
||||
data := crypto.ReverseTransCodeBlob(buf[:n])
|
||||
if len(data) >= 16 && binary.LittleEndian.Uint16(data[8:]) == CmdDiscoRes {
|
||||
c.addr, c.newProto = addr, false
|
||||
return c.oldDiscoDone()
|
||||
// IOTC Protocol (Basis)
|
||||
data := ReverseTransCodeBlob(buf[:n])
|
||||
if len(data) >= 16 && binary.LittleEndian.Uint16(data[8:]) == cmdDiscoRes {
|
||||
c.addr, c.isCC51 = addr, false
|
||||
return c.discoDone()
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("discovery timeout")
|
||||
}
|
||||
|
||||
func (c *Conn) oldDiscoDone() error {
|
||||
c.Write(c.buildDisco(2))
|
||||
func (c *DTLSConn) discoDone() error {
|
||||
c.Write(c.msgDisco(2))
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_, err := c.WriteAndWait(c.buildSession(), SessionTimeout, func(res []byte) bool {
|
||||
return len(res) >= 16 && binary.LittleEndian.Uint16(res[8:]) == CmdSessionRes
|
||||
_, err := c.WriteAndWait(c.msgSession(), func(res []byte) bool {
|
||||
return len(res) >= 16 && binary.LittleEndian.Uint16(res[8:]) == cmdSessionRes
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) newDiscoDone() error {
|
||||
_, err := c.WriteAndWait(c.buildNewDisco(2, c.ticket, false), SessionTimeout, func(res []byte) bool {
|
||||
if len(res) < NewPacketSize || binary.LittleEndian.Uint16(res[:2]) != MagicNewProto {
|
||||
func (c *DTLSConn) discoDoneCC51() error {
|
||||
_, err := c.WriteAndWait(c.msgDiscoCC51(2, c.ticket, false), func(res []byte) bool {
|
||||
if len(res) < packetSizeCC51 || string(res[:2]) != magicCC51 {
|
||||
return false
|
||||
}
|
||||
cmd := binary.LittleEndian.Uint16(res[4:])
|
||||
dir := binary.LittleEndian.Uint16(res[8:])
|
||||
seq := binary.LittleEndian.Uint16(res[12:])
|
||||
return cmd == CmdNewDisco && dir == 0xFFFF && seq == 3
|
||||
return cmd == cmdDiscoCC51 && dir == 0xFFFF && seq == 3
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
conn, err := NewDtlsClient(c, IOTCChannelMain, c.psk)
|
||||
func (c *DTLSConn) connect() error {
|
||||
adapter := NewChannelAdapter(c.ctx, iotcChannelMain, c.addr, c.WriteDTLS, c.clientBuf)
|
||||
conn, err := NewDTLSClient(adapter, c.addr, c.psk)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dtls: client create failed: %w", err)
|
||||
}
|
||||
@@ -507,13 +523,13 @@ func (c *Conn) connect() error {
|
||||
c.mu.Unlock()
|
||||
|
||||
if c.verbose {
|
||||
fmt.Printf("[DTLS] Client created for channel %d\n", IOTCChannelMain)
|
||||
fmt.Printf("[DTLS] Client created for channel %d\n", iotcChannelMain)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) worker() {
|
||||
func (c *DTLSConn) worker() {
|
||||
defer c.wg.Done()
|
||||
|
||||
buf := make([]byte, 2048)
|
||||
@@ -538,15 +554,11 @@ func (c *Conn) worker() {
|
||||
data := buf[:n]
|
||||
magic := binary.LittleEndian.Uint16(data)
|
||||
|
||||
// if c.verbose {
|
||||
// fmt.Printf("[DTLS RX] from=%s, len=%d, data:\n%s", c.addr.String(), n, hexDump(data))
|
||||
// }
|
||||
|
||||
switch magic {
|
||||
case MagicAVLoginResp:
|
||||
case magicAVLoginResp:
|
||||
c.queue(c.rawCmd, data)
|
||||
|
||||
case MagicIOCtrl:
|
||||
case magicIOCtrl:
|
||||
if len(data) >= 32 {
|
||||
for i := 32; i+2 < len(data); i++ {
|
||||
if data[i] == 'H' && data[i+1] == 'L' {
|
||||
@@ -556,7 +568,7 @@ func (c *Conn) worker() {
|
||||
}
|
||||
}
|
||||
|
||||
case MagicChannelMsg:
|
||||
case magicChannelMsg:
|
||||
if len(data) >= 36 && data[16] == 0x00 {
|
||||
for i := 36; i+2 < len(data); i++ {
|
||||
if data[i] == 'H' && data[i+1] == 'L' {
|
||||
@@ -566,7 +578,7 @@ func (c *Conn) worker() {
|
||||
}
|
||||
}
|
||||
|
||||
case ProtoVersion:
|
||||
case protoVersion:
|
||||
if len(data) >= 8 {
|
||||
// Extract seq number at byte 4-5 (uint16 of uint32 AVSeq)
|
||||
seq := binary.LittleEndian.Uint16(data[4:])
|
||||
@@ -589,7 +601,7 @@ func (c *Conn) worker() {
|
||||
}
|
||||
}
|
||||
|
||||
case MagicACK:
|
||||
case magicACK:
|
||||
c.mu.RLock()
|
||||
ack := c.cmdAck
|
||||
c.mu.RUnlock()
|
||||
@@ -606,9 +618,10 @@ func (c *Conn) worker() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) reader() {
|
||||
func (c *DTLSConn) reader() {
|
||||
defer c.wg.Done()
|
||||
buf := make([]byte, MaxPacketSize)
|
||||
|
||||
buf := make([]byte, 2048)
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -626,10 +639,6 @@ func (c *Conn) reader() {
|
||||
return
|
||||
}
|
||||
|
||||
// if c.verbose {
|
||||
// fmt.Printf("[UDP RX] from=%s, len=%d, data:\n%s", addr.String(), n, hexDump(buf[:n]))
|
||||
// }
|
||||
|
||||
if !addr.IP.Equal(c.addr.IP) {
|
||||
if c.verbose {
|
||||
fmt.Printf("Ignored packet from unknown IP: %s\n", addr.IP.String())
|
||||
@@ -640,47 +649,47 @@ func (c *Conn) reader() {
|
||||
c.addr.Port = addr.Port
|
||||
}
|
||||
|
||||
// NEW protocol (0xCC51)
|
||||
if c.newProto && n >= 12 && binary.LittleEndian.Uint16(buf[:2]) == MagicNewProto {
|
||||
// CC51 Protocol
|
||||
if c.isCC51 && n >= 12 && string(buf[:2]) == magicCC51 {
|
||||
cmd := binary.LittleEndian.Uint16(buf[4:])
|
||||
switch cmd {
|
||||
case CmdNewKeepalive:
|
||||
if n >= NewKeepaliveSize {
|
||||
_ = c.Write(c.buildNewKeepalive())
|
||||
case cmdKeepaliveCC51:
|
||||
if n >= keepaliveSizeCC51 {
|
||||
_ = c.Write(c.msgKeepaliveCC51())
|
||||
}
|
||||
case CmdNewDTLS:
|
||||
if n >= NewHeaderSize+NewAuthSize {
|
||||
case cmdDTLSCC51:
|
||||
if n >= headerSizeCC51+authSizeCC51 {
|
||||
ch := byte(binary.LittleEndian.Uint16(buf[12:]) >> 8)
|
||||
dtls := buf[NewHeaderSize : n-NewAuthSize]
|
||||
dtlsData := buf[headerSizeCC51 : n-authSizeCC51]
|
||||
switch ch {
|
||||
case IOTCChannelMain:
|
||||
c.queue(c.clientBuf, dtls)
|
||||
case IOTCChannelBack:
|
||||
c.queue(c.serverBuf, dtls)
|
||||
case iotcChannelMain:
|
||||
c.queue(c.clientBuf, dtlsData)
|
||||
case iotcChannelBack:
|
||||
c.queue(c.serverBuf, dtlsData)
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// OLD protocol (TransCode)
|
||||
data := crypto.ReverseTransCodeBlob(buf[:n])
|
||||
// IOTC Protocol (Basis)
|
||||
data := ReverseTransCodeBlob(buf[:n])
|
||||
if len(data) < 16 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch binary.LittleEndian.Uint16(data[8:]) {
|
||||
case CmdKeepaliveRes:
|
||||
case cmdKeepaliveRes:
|
||||
if len(data) > 24 {
|
||||
_ = c.Write(c.buildKeepAlive(data[16:]))
|
||||
_ = c.Write(c.msgKeepalive(data[16:]))
|
||||
}
|
||||
case CmdDataRX:
|
||||
case cmdDataRX:
|
||||
if len(data) > 28 {
|
||||
ch := data[14]
|
||||
switch ch {
|
||||
case IOTCChannelMain:
|
||||
case iotcChannelMain:
|
||||
c.queue(c.clientBuf, data[28:])
|
||||
case IOTCChannelBack:
|
||||
case iotcChannelBack:
|
||||
c.queue(c.serverBuf, data[28:])
|
||||
}
|
||||
}
|
||||
@@ -688,7 +697,7 @@ func (c *Conn) reader() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) queue(ch chan []byte, data []byte) {
|
||||
func (c *DTLSConn) queue(ch chan []byte, data []byte) {
|
||||
b := make([]byte, len(data))
|
||||
copy(b, data)
|
||||
select {
|
||||
@@ -702,7 +711,7 @@ func (c *Conn) queue(ch chan []byte, data []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) handleSpeakerAVLogin() error {
|
||||
func (c *DTLSConn) handleSpeakerAVLogin() error {
|
||||
if c.verbose {
|
||||
fmt.Printf("[SPEAK] Waiting for AV Login request from camera...\n")
|
||||
}
|
||||
@@ -723,7 +732,7 @@ func (c *Conn) handleSpeakerAVLogin() error {
|
||||
}
|
||||
|
||||
checksum := binary.LittleEndian.Uint32(buf[20:])
|
||||
resp := c.buildAVLoginResponse(checksum)
|
||||
resp := c.msgAVLoginResponse(checksum)
|
||||
|
||||
if c.verbose {
|
||||
fmt.Printf("[SPEAK] Sending AV Login response: %d bytes\n", len(resp))
|
||||
@@ -751,16 +760,16 @@ func (c *Conn) handleSpeakerAVLogin() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) buildDisco(stage byte) []byte {
|
||||
b := make([]byte, OldDiscoSize)
|
||||
copy(b, "\x04\x02\x1a\x02") // marker + mode
|
||||
binary.LittleEndian.PutUint16(b[4:], OldDiscoBodySize) // body size
|
||||
binary.LittleEndian.PutUint16(b[8:], CmdDiscoReq) // 0x0601
|
||||
binary.LittleEndian.PutUint16(b[10:], 0x0021) // flags
|
||||
body := b[OldHeaderSize:]
|
||||
copy(body[:UIDSize], c.uid)
|
||||
copy(body[36:], "\x01\x01\x02\x04") // unknown
|
||||
copy(body[40:], c.rid)
|
||||
func (c *DTLSConn) msgDisco(stage byte) []byte {
|
||||
b := make([]byte, discoSize)
|
||||
copy(b, "\x04\x02\x1a\x02") // marker + mode
|
||||
binary.LittleEndian.PutUint16(b[4:], discoBodySize) // body size
|
||||
binary.LittleEndian.PutUint16(b[8:], cmdDiscoReq) // 0x0601
|
||||
binary.LittleEndian.PutUint16(b[10:], 0x0021) // flags
|
||||
body := b[headerSize:]
|
||||
copy(body[:20], c.uid)
|
||||
copy(body[36:], sdkVersion42) // SDK 4.2.1.1
|
||||
copy(body[40:], c.sid)
|
||||
body[48] = stage
|
||||
if stage == 1 && len(c.authKey) > 0 {
|
||||
copy(body[58:], c.authKey)
|
||||
@@ -768,69 +777,67 @@ func (c *Conn) buildDisco(stage byte) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
func (c *Conn) buildNewDisco(seq, ticket uint16, isResponse bool) []byte {
|
||||
b := make([]byte, NewPacketSize)
|
||||
binary.LittleEndian.PutUint16(b[0:], MagicNewProto) // 0xCC51
|
||||
binary.LittleEndian.PutUint16(b[4:], CmdNewDisco) // 0x1002
|
||||
binary.LittleEndian.PutUint16(b[6:], NewPayloadSize) // 40 bytes
|
||||
func (c *DTLSConn) msgDiscoCC51(seq, ticket uint16, isResponse bool) []byte {
|
||||
b := make([]byte, packetSizeCC51)
|
||||
copy(b[:2], magicCC51)
|
||||
binary.LittleEndian.PutUint16(b[4:], cmdDiscoCC51) // 0x1002
|
||||
binary.LittleEndian.PutUint16(b[6:], payloadSizeCC51) // 40 bytes
|
||||
if isResponse {
|
||||
binary.LittleEndian.PutUint16(b[8:], 0xFFFF) // response
|
||||
}
|
||||
binary.LittleEndian.PutUint16(b[12:], seq)
|
||||
binary.LittleEndian.PutUint16(b[14:], ticket)
|
||||
copy(b[16:24], c.sid)
|
||||
copy(b[24:32], "\x00\x08\x03\x04\x1d\x00\x00\x00") // SDK 4.3.8.0
|
||||
authKey := crypto.CalculateAuthKey(c.enr, c.mac)
|
||||
h := hmac.New(sha1.New, append([]byte(c.uid), authKey...))
|
||||
copy(b[24:28], sdkVersion43) // SDK 4.3.8.0
|
||||
b[28] = 0x1d // unknown field (capability/build flag?)
|
||||
h := hmac.New(sha1.New, append([]byte(c.uid), c.authKey...))
|
||||
h.Write(b[:32])
|
||||
copy(b[32:52], h.Sum(nil))
|
||||
return b
|
||||
}
|
||||
|
||||
func (c *Conn) buildNewKeepalive() []byte {
|
||||
func (c *DTLSConn) msgKeepaliveCC51() []byte {
|
||||
c.kaSeq += 2
|
||||
b := make([]byte, NewKeepaliveSize)
|
||||
binary.LittleEndian.PutUint16(b[0:], MagicNewProto) // 0xCC51
|
||||
binary.LittleEndian.PutUint16(b[4:], CmdNewKeepalive) // 0x1202
|
||||
binary.LittleEndian.PutUint16(b[6:], 0x0024) // 36 bytes payload
|
||||
binary.LittleEndian.PutUint32(b[16:], c.kaSeq) // counter
|
||||
copy(b[20:28], c.sid) // session ID
|
||||
authKey := crypto.CalculateAuthKey(c.enr, c.mac)
|
||||
h := hmac.New(sha1.New, append([]byte(c.uid), authKey...))
|
||||
b := make([]byte, keepaliveSizeCC51)
|
||||
copy(b[:2], magicCC51)
|
||||
binary.LittleEndian.PutUint16(b[4:], cmdKeepaliveCC51) // 0x1202
|
||||
binary.LittleEndian.PutUint16(b[6:], 0x0024) // 36 bytes payload
|
||||
binary.LittleEndian.PutUint32(b[16:], c.kaSeq) // counter
|
||||
copy(b[20:28], c.sid) // session ID
|
||||
h := hmac.New(sha1.New, append([]byte(c.uid), c.authKey...))
|
||||
h.Write(b[:28])
|
||||
copy(b[28:48], h.Sum(nil))
|
||||
return b
|
||||
}
|
||||
|
||||
func (c *Conn) buildSession() []byte {
|
||||
b := make([]byte, OldSessionSize)
|
||||
copy(b, "\x04\x02\x1a\x02") // marker + mode
|
||||
binary.LittleEndian.PutUint16(b[4:], OldSessionBody) // body size
|
||||
binary.LittleEndian.PutUint16(b[8:], CmdSessionReq) // 0x0402
|
||||
binary.LittleEndian.PutUint16(b[10:], 0x0033) // flags
|
||||
body := b[OldHeaderSize:]
|
||||
copy(body[:UIDSize], c.uid)
|
||||
copy(body[UIDSize:], c.rid)
|
||||
func (c *DTLSConn) msgSession() []byte {
|
||||
b := make([]byte, sessionSize)
|
||||
copy(b, "\x04\x02\x1a\x02") // marker + mode
|
||||
binary.LittleEndian.PutUint16(b[4:], sessionBody) // body size
|
||||
binary.LittleEndian.PutUint16(b[8:], cmdSessionReq) // 0x0402
|
||||
binary.LittleEndian.PutUint16(b[10:], 0x0033) // flags
|
||||
body := b[headerSize:]
|
||||
copy(body[:20], c.uid)
|
||||
copy(body[20:], c.sid)
|
||||
binary.LittleEndian.PutUint32(body[32:], uint32(time.Now().Unix()))
|
||||
return b
|
||||
}
|
||||
|
||||
func (c *Conn) buildAVLoginPacket(magic uint16, size int, flags uint16, randomID []byte) []byte {
|
||||
func (c *DTLSConn) msgAVLogin(magic uint16, size int, flags uint16, randomID []byte) []byte {
|
||||
b := make([]byte, size)
|
||||
binary.LittleEndian.PutUint16(b, magic)
|
||||
binary.LittleEndian.PutUint16(b[2:], ProtoVersion)
|
||||
binary.LittleEndian.PutUint16(b[2:], protoVersion)
|
||||
binary.LittleEndian.PutUint16(b[16:], uint16(size-24)) // payload size
|
||||
binary.LittleEndian.PutUint16(b[18:], flags)
|
||||
copy(b[20:], randomID[:4])
|
||||
copy(b[24:], DefaultUser) // username
|
||||
copy(b[280:], c.enr) // password/ENR
|
||||
// binary.LittleEndian.PutUint32(b[536:], 1) // resend enabled
|
||||
copy(b[24:], "admin") // username
|
||||
copy(b[280:], c.enr) // password/ENR
|
||||
binary.LittleEndian.PutUint32(b[540:], 4) // security_mode ?
|
||||
binary.LittleEndian.PutUint32(b[552:], DefaultCaps) // capabilities
|
||||
binary.LittleEndian.PutUint32(b[552:], defaultCaps) // capabilities
|
||||
return b
|
||||
}
|
||||
|
||||
func (c *Conn) buildAVLoginResponse(checksum uint32) []byte {
|
||||
func (c *DTLSConn) msgAVLoginResponse(checksum uint32) []byte {
|
||||
b := make([]byte, 60)
|
||||
binary.LittleEndian.PutUint16(b, 0x2100) // magic
|
||||
binary.LittleEndian.PutUint16(b[2:], 0x000c) // version
|
||||
@@ -840,13 +847,13 @@ func (c *Conn) buildAVLoginResponse(checksum uint32) []byte {
|
||||
b[29] = 0x01 // enable flag
|
||||
b[31] = 0x01 // two-way streaming
|
||||
binary.LittleEndian.PutUint32(b[36:], 0x04) // buffer config
|
||||
binary.LittleEndian.PutUint32(b[40:], DefaultCaps)
|
||||
binary.LittleEndian.PutUint32(b[40:], defaultCaps)
|
||||
binary.LittleEndian.PutUint16(b[54:], 0x0003) // channel info
|
||||
binary.LittleEndian.PutUint16(b[56:], 0x0002)
|
||||
return b
|
||||
}
|
||||
|
||||
func (c *Conn) buildAudioFrame(payload []byte, timestampUS uint32, codec uint16, sampleRate uint32, channels uint8) []byte {
|
||||
func (c *DTLSConn) msgAudioFrame(payload []byte, timestampUS uint32, codec byte, sampleRate uint32, channels uint8) []byte {
|
||||
c.audioSeq++
|
||||
c.audioFrameNo++
|
||||
prevFrame := uint32(0)
|
||||
@@ -860,7 +867,7 @@ func (c *Conn) buildAudioFrame(payload []byte, timestampUS uint32, codec uint16,
|
||||
// Outer header (36 bytes)
|
||||
b[0] = ChannelAudio // 0x03
|
||||
b[1] = FrameTypeStartAlt // 0x09
|
||||
binary.LittleEndian.PutUint16(b[2:], ProtoVersion)
|
||||
binary.LittleEndian.PutUint16(b[2:], protoVersion)
|
||||
binary.LittleEndian.PutUint32(b[4:], c.audioSeq)
|
||||
binary.LittleEndian.PutUint32(b[8:], timestampUS)
|
||||
if c.audioFrameNo == 1 {
|
||||
@@ -880,54 +887,65 @@ func (c *Conn) buildAudioFrame(payload []byte, timestampUS uint32, codec uint16,
|
||||
binary.LittleEndian.PutUint32(b[32:], c.audioFrameNo)
|
||||
copy(b[36:], payload) // Payload + FrameInfo
|
||||
fi := b[36+len(payload):]
|
||||
binary.LittleEndian.PutUint16(fi, codec)
|
||||
fi[2] = BuildAudioFlags(sampleRate, true, channels == 2)
|
||||
fi[0] = codec // Codec ID (low byte)
|
||||
fi[1] = 0 // Codec ID (high byte, unused)
|
||||
// Audio flags: [3:2]=sampleRateIdx [1]=16bit [0]=stereo
|
||||
var srIdx uint8 = 3 // default 16kHz
|
||||
for i, rate := range sampleRates {
|
||||
if rate == sampleRate {
|
||||
srIdx = uint8(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
fi[2] = (srIdx << 2) | 0x02 // 16-bit always set
|
||||
if channels == 2 {
|
||||
fi[2] |= 0x01
|
||||
}
|
||||
fi[4] = 1 // online
|
||||
binary.LittleEndian.PutUint32(fi[12:], (c.audioFrameNo-1)*GetSamplesPerFrame(codec)*1000/sampleRate)
|
||||
return b
|
||||
}
|
||||
|
||||
func (c *Conn) buildTxData(payload []byte, channel byte) []byte {
|
||||
func (c *DTLSConn) msgTxData(payload []byte, channel byte) []byte {
|
||||
bodySize := 12 + len(payload)
|
||||
b := make([]byte, 16+bodySize)
|
||||
copy(b, "\x04\x02\x1a\x0b") // marker + mode=data
|
||||
binary.LittleEndian.PutUint16(b[4:], uint16(bodySize)) // body size
|
||||
binary.LittleEndian.PutUint16(b[6:], c.seq) // sequence
|
||||
c.seq++
|
||||
binary.LittleEndian.PutUint16(b[8:], CmdDataTX) // 0x0407
|
||||
binary.LittleEndian.PutUint16(b[8:], cmdDataTX) // 0x0407
|
||||
binary.LittleEndian.PutUint16(b[10:], 0x0021) // flags
|
||||
copy(b[12:], c.rid[:2]) // rid[0:2]
|
||||
copy(b[12:], c.sid[:2]) // rid[0:2]
|
||||
b[14] = channel // channel
|
||||
b[15] = 0x01 // marker
|
||||
binary.LittleEndian.PutUint32(b[16:], 0x0000000c) // const
|
||||
copy(b[20:], c.rid[:8]) // rid
|
||||
copy(b[20:], c.sid[:8]) // rid
|
||||
copy(b[28:], payload)
|
||||
return b
|
||||
}
|
||||
|
||||
func (c *Conn) buildNewTxData(payload []byte, channel byte) []byte {
|
||||
payloadSize := uint16(16 + len(payload) + NewAuthSize)
|
||||
b := make([]byte, NewHeaderSize+len(payload)+NewAuthSize)
|
||||
binary.LittleEndian.PutUint16(b[0:], MagicNewProto) // 0xCC51
|
||||
binary.LittleEndian.PutUint16(b[4:], CmdNewDTLS) // 0x1502
|
||||
func (c *DTLSConn) msgTxDataCC51(payload []byte, channel byte) []byte {
|
||||
payloadSize := uint16(16 + len(payload) + authSizeCC51)
|
||||
b := make([]byte, headerSizeCC51+len(payload)+authSizeCC51)
|
||||
copy(b[:2], magicCC51)
|
||||
binary.LittleEndian.PutUint16(b[4:], cmdDTLSCC51) // 0x1502
|
||||
binary.LittleEndian.PutUint16(b[6:], payloadSize)
|
||||
binary.LittleEndian.PutUint16(b[12:], uint16(0x0010)|(uint16(channel)<<8)) // channel in high byte
|
||||
binary.LittleEndian.PutUint16(b[14:], c.ticket)
|
||||
copy(b[16:24], c.sid)
|
||||
binary.LittleEndian.PutUint32(b[24:], 1) // const
|
||||
copy(b[NewHeaderSize:], payload)
|
||||
authKey := crypto.CalculateAuthKey(c.enr, c.mac)
|
||||
h := hmac.New(sha1.New, append([]byte(c.uid), authKey...))
|
||||
h.Write(b[:NewHeaderSize])
|
||||
copy(b[NewHeaderSize+len(payload):], h.Sum(nil))
|
||||
copy(b[headerSizeCC51:], payload)
|
||||
h := hmac.New(sha1.New, append([]byte(c.uid), c.authKey...))
|
||||
h.Write(b[:headerSizeCC51])
|
||||
copy(b[headerSizeCC51+len(payload):], h.Sum(nil))
|
||||
return b
|
||||
}
|
||||
|
||||
func (c *Conn) buildACK() []byte {
|
||||
func (c *DTLSConn) msgACK() []byte {
|
||||
c.ackFlags++
|
||||
b := make([]byte, 24)
|
||||
binary.LittleEndian.PutUint16(b[0:], MagicACK) // 0x0009
|
||||
binary.LittleEndian.PutUint16(b[2:], ProtoVersion) // 0x000c
|
||||
binary.LittleEndian.PutUint16(b[0:], magicACK) // 0x0009
|
||||
binary.LittleEndian.PutUint16(b[2:], protoVersion) // 0x000c
|
||||
binary.LittleEndian.PutUint32(b[4:], c.avSeq) // TX seq
|
||||
c.avSeq++
|
||||
binary.LittleEndian.PutUint16(b[8:], c.rxSeqStart) // RX start (last acked)
|
||||
@@ -942,11 +960,11 @@ func (c *Conn) buildACK() []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
func (c *Conn) buildKeepAlive(incoming []byte) []byte {
|
||||
func (c *DTLSConn) msgKeepalive(incoming []byte) []byte {
|
||||
b := make([]byte, 24)
|
||||
copy(b, "\x04\x02\x1a\x0a") // marker + mode
|
||||
binary.LittleEndian.PutUint16(b[4:], 8) // body size
|
||||
binary.LittleEndian.PutUint16(b[8:], CmdKeepaliveReq) // 0x0427
|
||||
binary.LittleEndian.PutUint16(b[8:], cmdKeepaliveReq) // 0x0427
|
||||
binary.LittleEndian.PutUint16(b[10:], 0x0021) // flags
|
||||
if len(incoming) >= 8 {
|
||||
copy(b[16:], incoming[:8]) // echo payload
|
||||
@@ -954,13 +972,13 @@ func (c *Conn) buildKeepAlive(incoming []byte) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
func (c *Conn) buildIOCtrlFrame(payload []byte) []byte {
|
||||
func (c *DTLSConn) msgIOCtrl(payload []byte) []byte {
|
||||
b := make([]byte, 40+len(payload))
|
||||
binary.LittleEndian.PutUint16(b, ProtoVersion) // magic
|
||||
binary.LittleEndian.PutUint16(b[2:], ProtoVersion) // version
|
||||
binary.LittleEndian.PutUint16(b, protoVersion) // magic
|
||||
binary.LittleEndian.PutUint16(b[2:], protoVersion) // version
|
||||
binary.LittleEndian.PutUint32(b[4:], c.avSeq) // av seq
|
||||
c.avSeq++
|
||||
binary.LittleEndian.PutUint16(b[16:], MagicIOCtrl) // 0x7000
|
||||
binary.LittleEndian.PutUint16(b[16:], magicIOCtrl) // 0x7000
|
||||
binary.LittleEndian.PutUint16(b[18:], c.seqCmd) // sub channel
|
||||
binary.LittleEndian.PutUint32(b[20:], 1) // ioctl seq
|
||||
binary.LittleEndian.PutUint32(b[24:], uint32(len(payload)+4)) // payload size
|
||||
@@ -971,30 +989,6 @@ func (c *Conn) buildIOCtrlFrame(payload []byte) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
func derivePSK(enr string) []byte {
|
||||
// TUTK SDK treats the PSK as a NULL-terminated C string, so if SHA256(ENR)
|
||||
// contains a 0x00 byte, the PSK is truncated at that position.
|
||||
// bytes after the first 0x00 are padded with zeros to make a 32-byte key.
|
||||
hash := sha256.Sum256([]byte(enr))
|
||||
pskLen := 32
|
||||
for i := range 32 {
|
||||
if hash[i] == 0x00 {
|
||||
pskLen = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
psk := make([]byte, 32)
|
||||
copy(psk[:pskLen], hash[:pskLen])
|
||||
return psk
|
||||
}
|
||||
|
||||
func genRandomID() []byte {
|
||||
b := make([]byte, 8)
|
||||
_, _ = rand.Read(b)
|
||||
return b
|
||||
}
|
||||
|
||||
func hexDump(data []byte) string {
|
||||
const maxBytes = 650
|
||||
totalLen := len(data)
|
||||
@@ -50,6 +50,34 @@ func ReverseTransCodePartial(dst, src []byte) []byte {
|
||||
return dst
|
||||
}
|
||||
|
||||
func ReverseTransCodeBlob(src []byte) []byte {
|
||||
if len(src) < 16 {
|
||||
return ReverseTransCodePartial(nil, src)
|
||||
}
|
||||
|
||||
dst := make([]byte, len(src))
|
||||
header := ReverseTransCodePartial(nil, src[:16])
|
||||
copy(dst, header)
|
||||
|
||||
if len(src) > 16 {
|
||||
if dst[3]&1 != 0 { // Partial encryption (check decrypted header)
|
||||
remaining := len(src) - 16
|
||||
decryptLen := min(remaining, 48)
|
||||
if decryptLen > 0 {
|
||||
decrypted := ReverseTransCodePartial(nil, src[16:16+decryptLen])
|
||||
copy(dst[16:], decrypted)
|
||||
}
|
||||
if remaining > 48 {
|
||||
copy(dst[64:], src[64:])
|
||||
}
|
||||
} else { // Full decryption
|
||||
decrypted := ReverseTransCodePartial(nil, src[16:])
|
||||
copy(dst[16:], decrypted)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func TransCodePartial(dst, src []byte) []byte {
|
||||
n := len(src)
|
||||
tmp := make([]byte, n)
|
||||
@@ -92,6 +120,34 @@ func TransCodePartial(dst, src []byte) []byte {
|
||||
return dst
|
||||
}
|
||||
|
||||
func TransCodeBlob(src []byte) []byte {
|
||||
if len(src) < 16 {
|
||||
return TransCodePartial(nil, src)
|
||||
}
|
||||
|
||||
dst := make([]byte, len(src))
|
||||
header := TransCodePartial(nil, src[:16])
|
||||
copy(dst, header)
|
||||
|
||||
if len(src) > 16 {
|
||||
if src[3]&1 != 0 { // Partial encryption
|
||||
remaining := len(src) - 16
|
||||
encryptLen := min(remaining, 48)
|
||||
if encryptLen > 0 {
|
||||
encrypted := TransCodePartial(nil, src[16:16+encryptLen])
|
||||
copy(dst[16:], encrypted)
|
||||
}
|
||||
if remaining > 48 {
|
||||
copy(dst[64:], src[64:])
|
||||
}
|
||||
} else { // Full encryption
|
||||
encrypted := TransCodePartial(nil, src[16:])
|
||||
copy(dst[16:], encrypted)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func swap(dst, src []byte, n int) {
|
||||
switch n {
|
||||
case 2:
|
||||
@@ -175,3 +231,49 @@ func XXTEADecrypt(dst, src, key []byte) {
|
||||
dst = dst[4:]
|
||||
}
|
||||
}
|
||||
|
||||
func XXTEADecryptVar(data, key []byte) []byte {
|
||||
if len(data) < 8 || len(key) < 16 {
|
||||
return nil
|
||||
}
|
||||
|
||||
k := make([]uint32, 4)
|
||||
for i := range 4 {
|
||||
k[i] = binary.LittleEndian.Uint32(key[i*4:])
|
||||
}
|
||||
|
||||
n := max(len(data)/4, 2)
|
||||
v := make([]uint32, n)
|
||||
for i := 0; i < len(data)/4; i++ {
|
||||
v[i] = binary.LittleEndian.Uint32(data[i*4:])
|
||||
}
|
||||
|
||||
rounds := 6 + 52/n
|
||||
sum := uint32(rounds) * delta
|
||||
y := v[0]
|
||||
|
||||
for rounds > 0 {
|
||||
e := (sum >> 2) & 3
|
||||
for p := n - 1; p > 0; p-- {
|
||||
z := v[p-1]
|
||||
v[p] -= xxteaMX(sum, y, z, p, e, k)
|
||||
y = v[p]
|
||||
}
|
||||
z := v[n-1]
|
||||
v[0] -= xxteaMX(sum, y, z, 0, e, k)
|
||||
y = v[0]
|
||||
sum -= delta
|
||||
rounds--
|
||||
}
|
||||
|
||||
result := make([]byte, n*4)
|
||||
for i := range n {
|
||||
binary.LittleEndian.PutUint32(result[i*4:], v[i])
|
||||
}
|
||||
|
||||
return result[:len(data)]
|
||||
}
|
||||
|
||||
func xxteaMX(sum, y, z uint32, p int, e uint32, k []uint32) uint32 {
|
||||
return ((z>>5 ^ y<<2) + (y>>3 ^ z<<4)) ^ ((sum ^ y) + (k[(p&3)^int(e)] ^ z))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tutk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -8,22 +9,26 @@ import (
|
||||
"github.com/pion/dtls/v3"
|
||||
)
|
||||
|
||||
func NewDtlsClient(c *Conn, channel uint8, psk []byte) (*dtls.Conn, error) {
|
||||
adapter := &ChannelAdapter{conn: c, channel: channel}
|
||||
return dtls.Client(adapter, c.addr, buildDtlsConfig(psk, false))
|
||||
type DTLSConfig struct {
|
||||
PSK []byte
|
||||
Identity string
|
||||
IsServer bool
|
||||
}
|
||||
|
||||
func NewDtlsServer(c *Conn, channel uint8, psk []byte) (*dtls.Conn, error) {
|
||||
adapter := &ChannelAdapter{conn: c, channel: channel}
|
||||
return dtls.Server(adapter, c.addr, buildDtlsConfig(psk, true))
|
||||
func NewDTLSClient(adapter net.PacketConn, addr net.Addr, psk []byte) (*dtls.Conn, error) {
|
||||
return dtls.Client(adapter, addr, buildDTLSConfig(psk, false))
|
||||
}
|
||||
|
||||
func buildDtlsConfig(psk []byte, isServer bool) *dtls.Config {
|
||||
func NewDTLSServer(adapter net.PacketConn, addr net.Addr, psk []byte) (*dtls.Conn, error) {
|
||||
return dtls.Server(adapter, addr, buildDTLSConfig(psk, true))
|
||||
}
|
||||
|
||||
func buildDTLSConfig(psk []byte, isServer bool) *dtls.Config {
|
||||
config := &dtls.Config{
|
||||
PSK: func(hint []byte) ([]byte, error) {
|
||||
return psk, nil
|
||||
},
|
||||
PSKIdentityHint: []byte(PSKIdentity),
|
||||
PSKIdentityHint: []byte("AUTHPWD_admin"),
|
||||
InsecureSkipVerify: true,
|
||||
InsecureSkipVerifyHello: true,
|
||||
MTU: 1200,
|
||||
@@ -41,21 +46,26 @@ func buildDtlsConfig(psk []byte, isServer bool) *dtls.Config {
|
||||
}
|
||||
|
||||
type ChannelAdapter struct {
|
||||
conn *Conn
|
||||
channel uint8
|
||||
|
||||
ctx context.Context
|
||||
channel uint8
|
||||
writeFn func([]byte, uint8) error
|
||||
readChan chan []byte
|
||||
addr net.Addr
|
||||
mu sync.Mutex
|
||||
readDeadline time.Time
|
||||
}
|
||||
|
||||
func (a *ChannelAdapter) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
var buf chan []byte
|
||||
if a.channel == IOTCChannelMain {
|
||||
buf = a.conn.clientBuf
|
||||
} else {
|
||||
buf = a.conn.serverBuf
|
||||
func NewChannelAdapter(ctx context.Context, channel uint8, addr net.Addr, writeFn func([]byte, uint8) error, readChan chan []byte) *ChannelAdapter {
|
||||
return &ChannelAdapter{
|
||||
ctx: ctx,
|
||||
channel: channel,
|
||||
addr: addr,
|
||||
writeFn: writeFn,
|
||||
readChan: readChan,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ChannelAdapter) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
a.mu.Lock()
|
||||
deadline := a.readDeadline
|
||||
a.mu.Unlock()
|
||||
@@ -70,25 +80,25 @@ func (a *ChannelAdapter) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case data := <-buf:
|
||||
return copy(p, data), a.conn.addr, nil
|
||||
case data := <-a.readChan:
|
||||
return copy(p, data), a.addr, nil
|
||||
case <-timer.C:
|
||||
return 0, nil, &timeoutError{}
|
||||
case <-a.conn.ctx.Done():
|
||||
case <-a.ctx.Done():
|
||||
return 0, nil, net.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case data := <-buf:
|
||||
return copy(p, data), a.conn.addr, nil
|
||||
case <-a.conn.ctx.Done():
|
||||
case data := <-a.readChan:
|
||||
return copy(p, data), a.addr, nil
|
||||
case <-a.ctx.Done():
|
||||
return 0, nil, net.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ChannelAdapter) WriteTo(p []byte, _ net.Addr) (int, error) {
|
||||
if err := a.conn.WriteDTLS(p, a.channel); err != nil {
|
||||
if err := a.writeFn(p, a.channel); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
@@ -96,21 +106,18 @@ func (a *ChannelAdapter) WriteTo(p []byte, _ net.Addr) (int, error) {
|
||||
|
||||
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{}
|
||||
@@ -25,18 +25,7 @@ const (
|
||||
ChannelPVideo uint8 = 0x07
|
||||
)
|
||||
|
||||
const (
|
||||
ResTierLow uint8 = 1 // 360P/SD
|
||||
ResTierHigh uint8 = 4 // HD/2K
|
||||
)
|
||||
|
||||
const (
|
||||
Bitrate360P uint8 = 30
|
||||
BitrateHD uint8 = 100
|
||||
Bitrate2K uint8 = 200
|
||||
)
|
||||
|
||||
const FrameInfoSize = 40
|
||||
const frameInfoSize = 40
|
||||
|
||||
// FrameInfo - Wyze extended FRAMEINFO (40 bytes at end of packet)
|
||||
// Video: 40 bytes, Audio: 16 bytes (uses same struct, fields 16+ are zero)
|
||||
@@ -56,7 +45,7 @@ const FrameInfoSize = 40
|
||||
// 24-35 12 DeviceID - MAC address (ASCII) - video only
|
||||
// 36-39 4 Padding - Always 0 - video only
|
||||
type FrameInfo struct {
|
||||
CodecID uint16 // 0-1
|
||||
CodecID byte // 0 (only low byte used)
|
||||
Flags uint8 // 2
|
||||
CamIndex uint8 // 3
|
||||
OnlineNum uint8 // 4
|
||||
@@ -73,22 +62,12 @@ func (fi *FrameInfo) IsKeyframe() bool {
|
||||
return fi.Flags == 0x01
|
||||
}
|
||||
|
||||
func (fi *FrameInfo) Resolution() string {
|
||||
switch fi.Bitrate {
|
||||
case Bitrate360P:
|
||||
return "360P"
|
||||
case BitrateHD:
|
||||
return "HD"
|
||||
case Bitrate2K:
|
||||
return "2K"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (fi *FrameInfo) SampleRate() uint32 {
|
||||
idx := (fi.Flags >> 2) & 0x0F
|
||||
return uint32(SampleRateValue(idx))
|
||||
if idx < uint8(len(sampleRates)) {
|
||||
return sampleRates[idx]
|
||||
}
|
||||
return 16000
|
||||
}
|
||||
|
||||
func (fi *FrameInfo) Channels() uint8 {
|
||||
@@ -98,24 +77,16 @@ func (fi *FrameInfo) Channels() uint8 {
|
||||
return 1
|
||||
}
|
||||
|
||||
func (fi *FrameInfo) IsVideo() bool {
|
||||
return IsVideoCodec(fi.CodecID)
|
||||
}
|
||||
|
||||
func (fi *FrameInfo) IsAudio() bool {
|
||||
return IsAudioCodec(fi.CodecID)
|
||||
}
|
||||
|
||||
func ParseFrameInfo(data []byte) *FrameInfo {
|
||||
if len(data) < FrameInfoSize {
|
||||
if len(data) < frameInfoSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
offset := len(data) - FrameInfoSize
|
||||
offset := len(data) - frameInfoSize
|
||||
fi := data[offset:]
|
||||
|
||||
return &FrameInfo{
|
||||
CodecID: binary.LittleEndian.Uint16(fi),
|
||||
CodecID: fi[0],
|
||||
Flags: fi[2],
|
||||
CamIndex: fi[3],
|
||||
OnlineNum: fi[4],
|
||||
@@ -131,7 +102,7 @@ func ParseFrameInfo(data []byte) *FrameInfo {
|
||||
|
||||
type Packet struct {
|
||||
Channel uint8
|
||||
Codec uint16
|
||||
Codec byte
|
||||
Timestamp uint32
|
||||
Payload []byte
|
||||
IsKeyframe bool
|
||||
@@ -140,14 +111,6 @@ type Packet struct {
|
||||
Channels uint8
|
||||
}
|
||||
|
||||
func (p *Packet) IsVideo() bool {
|
||||
return p.Channel == ChannelIVideo || p.Channel == ChannelPVideo
|
||||
}
|
||||
|
||||
func (p *Packet) IsAudio() bool {
|
||||
return p.Channel == ChannelAudio
|
||||
}
|
||||
|
||||
type PacketHeader struct {
|
||||
Channel byte
|
||||
FrameType byte
|
||||
@@ -347,7 +310,7 @@ func (h *FrameHandler) extractPayload(data []byte, channel byte) ([]byte, *Frame
|
||||
frameType := data[1]
|
||||
|
||||
headerSize := 28
|
||||
frameInfoSize := 0
|
||||
fiSize := 0
|
||||
|
||||
switch frameType {
|
||||
case FrameTypeStart:
|
||||
@@ -357,17 +320,17 @@ func (h *FrameHandler) extractPayload(data []byte, channel byte) ([]byte, *Frame
|
||||
if len(data) >= 22 {
|
||||
pktTotal := binary.LittleEndian.Uint16(data[20:])
|
||||
if pktTotal == 1 {
|
||||
frameInfoSize = FrameInfoSize
|
||||
fiSize = frameInfoSize
|
||||
}
|
||||
}
|
||||
case FrameTypeCont, FrameTypeContAlt:
|
||||
headerSize = 28
|
||||
case FrameTypeEndSingle, FrameTypeEndMulti:
|
||||
headerSize = 28
|
||||
frameInfoSize = FrameInfoSize
|
||||
fiSize = frameInfoSize
|
||||
case FrameTypeEndExt:
|
||||
headerSize = 36
|
||||
frameInfoSize = FrameInfoSize
|
||||
fiSize = frameInfoSize
|
||||
default:
|
||||
headerSize = 28
|
||||
}
|
||||
@@ -376,11 +339,11 @@ func (h *FrameHandler) extractPayload(data []byte, channel byte) ([]byte, *Frame
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if frameInfoSize == 0 {
|
||||
if fiSize == 0 {
|
||||
return data[headerSize:], nil
|
||||
}
|
||||
|
||||
if len(data) < headerSize+frameInfoSize {
|
||||
if len(data) < headerSize+fiSize {
|
||||
return data[headerSize:], nil
|
||||
}
|
||||
|
||||
@@ -395,7 +358,7 @@ func (h *FrameHandler) extractPayload(data []byte, channel byte) ([]byte, *Frame
|
||||
}
|
||||
|
||||
if validCodec {
|
||||
payload := data[headerSize : len(data)-frameInfoSize]
|
||||
payload := data[headerSize : len(data)-fiSize]
|
||||
return payload, fi
|
||||
}
|
||||
|
||||
@@ -421,7 +384,7 @@ func (h *FrameHandler) handleVideo(channel byte, hdr *PacketHeader, payload []by
|
||||
cs.pktTotal = hdr.PktTotal
|
||||
}
|
||||
|
||||
// Sequential check: if packet index doesn't match expected, reset (data loss)
|
||||
// If packet index doesn't match expected, reset (data loss)
|
||||
if hdr.PktIdx != cs.waitSeq {
|
||||
fmt.Printf("[OOO] ch=0x%02x #%d frameType=0x%02x pktTotal=%d expected pkt %d, got %d - reset\n",
|
||||
channel, hdr.FrameNo, hdr.FrameType, hdr.PktTotal, cs.waitSeq, hdr.PktIdx)
|
||||
@@ -434,7 +397,6 @@ func (h *FrameHandler) handleVideo(channel byte, hdr *PacketHeader, payload []by
|
||||
cs.hasStarted = true
|
||||
}
|
||||
|
||||
// Append payload (simple sequential accumulation)
|
||||
cs.waitData = append(cs.waitData, payload...)
|
||||
cs.waitSeq++
|
||||
|
||||
@@ -444,16 +406,13 @@ func (h *FrameHandler) handleVideo(channel byte, hdr *PacketHeader, payload []by
|
||||
}
|
||||
|
||||
// Check if frame is complete
|
||||
if cs.waitSeq == cs.pktTotal && cs.frameInfo != nil {
|
||||
h.emitVideo(channel, cs)
|
||||
cs.reset()
|
||||
if cs.waitSeq != cs.pktTotal || cs.frameInfo == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *FrameHandler) emitVideo(channel byte, cs *channelState) {
|
||||
fi := cs.frameInfo
|
||||
fi = cs.frameInfo
|
||||
defer cs.reset()
|
||||
|
||||
// Size validation
|
||||
if fi.PayloadSize > 0 && uint32(len(cs.waitData)) != fi.PayloadSize {
|
||||
fmt.Printf("[SIZE] ch=0x%02x #%d mismatch: expected %d, got %d\n",
|
||||
channel, cs.frameNo, fi.PayloadSize, len(cs.waitData))
|
||||
@@ -467,13 +426,9 @@ func (h *FrameHandler) emitVideo(channel byte, cs *channelState) {
|
||||
accumUS := h.videoTS.update(fi.Timestamp)
|
||||
rtpTS := uint32(accumUS * 90000 / 1000000)
|
||||
|
||||
// Copy payload (buffer will be reused)
|
||||
payload := make([]byte, len(cs.waitData))
|
||||
copy(payload, cs.waitData)
|
||||
|
||||
pkt := &Packet{
|
||||
Channel: channel,
|
||||
Payload: payload,
|
||||
Payload: append([]byte{}, cs.waitData...),
|
||||
Codec: fi.CodecID,
|
||||
Timestamp: rtpTS,
|
||||
IsKeyframe: fi.IsKeyframe(),
|
||||
@@ -485,10 +440,10 @@ func (h *FrameHandler) emitVideo(channel byte, cs *channelState) {
|
||||
if fi.IsKeyframe() {
|
||||
frameType = "KEY"
|
||||
}
|
||||
fmt.Printf("[OK] ch=0x%02x #%d %s %s size=%d\n",
|
||||
channel, fi.FrameNo, CodecName(fi.CodecID), frameType, len(payload))
|
||||
fmt.Printf(" [0-1]codec=0x%x(%s) [2]flags=0x%x [3]=%d [4]=%d\n",
|
||||
fi.CodecID, CodecName(fi.CodecID), fi.Flags, fi.CamIndex, fi.OnlineNum)
|
||||
fmt.Printf("[OK] ch=0x%02x #%d codec=0x%02x %s size=%d\n",
|
||||
channel, fi.FrameNo, fi.CodecID, frameType, len(pkt.Payload))
|
||||
fmt.Printf(" [0-1]codec=0x%02x [2]flags=0x%x [3]=%d [4]=%d\n",
|
||||
fi.CodecID, fi.Flags, fi.CamIndex, fi.OnlineNum)
|
||||
fmt.Printf(" [5]=%d [6]=%d [7]=%d [8-11]ts=%d\n",
|
||||
fi.FPS, fi.ResTier, fi.Bitrate, fi.Timestamp)
|
||||
fmt.Printf(" [12-15]=0x%x [16-19]payload=%d [20-23]frameNo=%d\n",
|
||||
@@ -509,7 +464,7 @@ func (h *FrameHandler) handleAudio(payload []byte, fi *FrameInfo) {
|
||||
var channels uint8
|
||||
|
||||
switch fi.CodecID {
|
||||
case AudioCodecAACRaw, AudioCodecAACADTS, AudioCodecAACLATM, AudioCodecAACWyze:
|
||||
case CodecAACRaw, CodecAACADTS, CodecAACLATM, CodecAACAlt:
|
||||
sampleRate, channels = parseAudioParams(payload, fi)
|
||||
default:
|
||||
sampleRate = fi.SampleRate()
|
||||
@@ -537,10 +492,10 @@ func (h *FrameHandler) handleAudio(payload []byte, fi *FrameInfo) {
|
||||
if fi.Flags&0x02 != 0 {
|
||||
bits = 16
|
||||
}
|
||||
fmt.Printf("[OK] Audio #%d %s size=%d\n",
|
||||
fi.FrameNo, AudioCodecName(fi.CodecID), len(payload))
|
||||
fmt.Printf(" [0-1]codec=0x%x(%s) [2]flags=0x%x(%dHz/%dbit/%dch)\n",
|
||||
fi.CodecID, AudioCodecName(fi.CodecID), fi.Flags, sampleRate, bits, channels)
|
||||
fmt.Printf("[OK] Audio #%d codec=0x%02x size=%d\n",
|
||||
fi.FrameNo, fi.CodecID, len(payload))
|
||||
fmt.Printf(" [0-1]codec=0x%02x [2]flags=0x%x(%dHz/%dbit/%dch)\n",
|
||||
fi.CodecID, fi.Flags, sampleRate, bits, channels)
|
||||
fmt.Printf(" [8-11]ts=%d [12-15]=0x%x rtp_ts=%d\n",
|
||||
fi.Timestamp, fi.SessionID, rtpTS)
|
||||
fmt.Printf(" hex: %s\n", dumpHex(fi))
|
||||
@@ -589,8 +544,9 @@ func parseAudioParams(payload []byte, fi *FrameInfo) (sampleRate uint32, channel
|
||||
}
|
||||
|
||||
func dumpHex(fi *FrameInfo) string {
|
||||
b := make([]byte, FrameInfoSize)
|
||||
binary.LittleEndian.PutUint16(b[0:], fi.CodecID)
|
||||
b := make([]byte, frameInfoSize)
|
||||
b[0] = fi.CodecID
|
||||
b[1] = 0 // High byte (unused)
|
||||
b[2] = fi.Flags
|
||||
b[3] = fi.CamIndex
|
||||
b[4] = fi.OnlineNum
|
||||
+43
-9
@@ -1,16 +1,16 @@
|
||||
package tutk
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// https://github.com/seydx/tutk_wyze#11-codec-reference
|
||||
const (
|
||||
CodecH264 = 0x4e
|
||||
CodecH265 = 0x50
|
||||
CodecPCMA = 0x8a
|
||||
CodecPCML = 0x8c
|
||||
CodecAAC = 0x88
|
||||
import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GenSessionID() []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(b, uint64(time.Now().UnixNano()))
|
||||
return b
|
||||
}
|
||||
|
||||
func ICAM(cmd uint32, args ...byte) []byte {
|
||||
// 0 4943414d ICAM
|
||||
// 4 d807ff00 command
|
||||
@@ -26,3 +26,37 @@ func ICAM(cmd uint32, args ...byte) []byte {
|
||||
copy(b[23:], args)
|
||||
return b
|
||||
}
|
||||
|
||||
func HL(cmdID uint16, payload []byte) []byte {
|
||||
// 0-1 "HL" magic
|
||||
// 2 version (typically 5)
|
||||
// 3 reserved
|
||||
// 4-5 cmdID command ID (uint16 LE)
|
||||
// 6-7 payloadLen payload length (uint16 LE)
|
||||
// 8-15 reserved
|
||||
// 16+ payload
|
||||
const headerSize = 16
|
||||
const version = 5
|
||||
|
||||
b := make([]byte, headerSize+len(payload))
|
||||
copy(b, "HL")
|
||||
b[2] = version
|
||||
binary.LittleEndian.PutUint16(b[4:], cmdID)
|
||||
binary.LittleEndian.PutUint16(b[6:], uint16(len(payload)))
|
||||
copy(b[headerSize:], payload)
|
||||
return b
|
||||
}
|
||||
|
||||
func ParseHL(data []byte) (cmdID uint16, payload []byte, ok bool) {
|
||||
if len(data) < 16 || data[0] != 'H' || data[1] != 'L' {
|
||||
return 0, nil, false
|
||||
}
|
||||
cmdID = binary.LittleEndian.Uint16(data[4:])
|
||||
payloadLen := binary.LittleEndian.Uint16(data[6:])
|
||||
if len(data) >= 16+int(payloadLen) {
|
||||
payload = data[16 : 16+payloadLen]
|
||||
} else if len(data) > 16 {
|
||||
payload = data[16:]
|
||||
}
|
||||
return cmdID, payload, true
|
||||
}
|
||||
|
||||
@@ -155,9 +155,3 @@ func ConnectByUID(stage byte, uid string, sid8 []byte) []byte {
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func GenSessionID() []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(b, uint64(time.Now().UnixNano()))
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/wyze/tutk"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tutk"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
|
||||
+50
-23
@@ -12,8 +12,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/wyze/crypto"
|
||||
"github.com/AlexxIT/go2rtc/pkg/wyze/tutk"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tutk"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -29,15 +28,6 @@ const (
|
||||
BitrateSD uint16 = 0x3C
|
||||
)
|
||||
|
||||
const (
|
||||
QualityUnknown = 0
|
||||
QualityMax = 1
|
||||
QualityHigh = 2
|
||||
QualityMiddle = 3
|
||||
QualityLow = 4
|
||||
QualityMin = 5
|
||||
)
|
||||
|
||||
const (
|
||||
MediaTypeVideo = 1
|
||||
MediaTypeAudio = 2
|
||||
@@ -59,7 +49,7 @@ const (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
conn *tutk.Conn
|
||||
conn *tutk.DTLSConn
|
||||
|
||||
host string
|
||||
uid string
|
||||
@@ -76,7 +66,7 @@ type Client struct {
|
||||
hasAudio bool
|
||||
hasIntercom bool
|
||||
|
||||
audioCodecID uint16
|
||||
audioCodecID byte
|
||||
audioSampleRate uint32
|
||||
audioChannels uint8
|
||||
}
|
||||
@@ -107,7 +97,7 @@ func Dial(rawURL string) (*Client, error) {
|
||||
verbose: query.Get("verbose") == "true",
|
||||
}
|
||||
|
||||
c.authKey = string(crypto.CalculateAuthKey(c.enr, c.mac))
|
||||
c.authKey = string(tutk.CalculateAuthKey(c.enr, c.mac))
|
||||
|
||||
if c.verbose {
|
||||
fmt.Printf("[Wyze] Connecting to %s (UID: %s)\n", c.host, c.uid)
|
||||
@@ -143,13 +133,13 @@ func (c *Client) SupportsIntercom() bool {
|
||||
return c.hasIntercom
|
||||
}
|
||||
|
||||
func (c *Client) SetBackchannelCodec(codecID uint16, sampleRate uint32, channels uint8) {
|
||||
func (c *Client) SetBackchannelCodec(codecID byte, sampleRate uint32, channels uint8) {
|
||||
c.audioCodecID = codecID
|
||||
c.audioSampleRate = sampleRate
|
||||
c.audioChannels = channels
|
||||
}
|
||||
|
||||
func (c *Client) GetBackchannelCodec() (codecID uint16, sampleRate uint32, channels uint8) {
|
||||
func (c *Client) GetBackchannelCodec() (codecID byte, sampleRate uint32, channels uint8) {
|
||||
return c.audioCodecID, c.audioSampleRate, c.audioChannels
|
||||
}
|
||||
|
||||
@@ -238,13 +228,13 @@ func (c *Client) ReadPacket() (*tutk.Packet, error) {
|
||||
return c.conn.AVRecvFrameData()
|
||||
}
|
||||
|
||||
func (c *Client) WriteAudio(codec uint16, payload []byte, timestamp uint32, sampleRate uint32, channels uint8) error {
|
||||
func (c *Client) WriteAudio(codec byte, payload []byte, timestamp uint32, sampleRate uint32, channels uint8) error {
|
||||
if !c.conn.IsBackchannelReady() {
|
||||
return fmt.Errorf("speaker channel not connected")
|
||||
}
|
||||
|
||||
if c.verbose {
|
||||
fmt.Printf("[Wyze] WriteAudio: codec=0x%04x, payload=%d bytes, rate=%d, ch=%d\n", codec, len(payload), sampleRate, channels)
|
||||
fmt.Printf("[Wyze] WriteAudio: codec=0x%02x, payload=%d bytes, rate=%d, ch=%d\n", codec, len(payload), sampleRate, channels)
|
||||
}
|
||||
|
||||
return c.conn.AVSendAudioData(codec, payload, timestamp, sampleRate, channels)
|
||||
@@ -305,7 +295,7 @@ func (c *Client) connect() error {
|
||||
host = host[:idx]
|
||||
}
|
||||
|
||||
conn, err := tutk.Dial(host, port, c.uid, c.authKey, c.enr, c.mac, c.verbose)
|
||||
conn, err := tutk.DialDTLS(host, port, c.uid, c.authKey, c.enr, c.verbose)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wyze: connect failed: %w", err)
|
||||
}
|
||||
@@ -386,9 +376,7 @@ func (c *Client) doKAuth() error {
|
||||
fmt.Printf("[Wyze] K10003 auth success\n")
|
||||
}
|
||||
|
||||
if avResp := c.conn.GetAVLoginResponse(); avResp != nil {
|
||||
c.hasIntercom = avResp.TwoWayStreaming == 1
|
||||
}
|
||||
c.hasIntercom = c.conn.HasTwoWayStreaming()
|
||||
|
||||
if c.verbose {
|
||||
fmt.Printf("[Wyze] K-auth complete\n")
|
||||
@@ -409,7 +397,7 @@ func (c *Client) buildK10000() []byte {
|
||||
}
|
||||
|
||||
func (c *Client) buildK10002(challenge []byte, status byte) []byte {
|
||||
resp := crypto.GenerateChallengeResponse(challenge, c.enr, status)
|
||||
resp := generateChallengeResponse(challenge, c.enr, status)
|
||||
sessionID := make([]byte, 4)
|
||||
rand.Read(sessionID)
|
||||
b := make([]byte, 38)
|
||||
@@ -555,3 +543,42 @@ func (c *Client) is2K() bool {
|
||||
func (c *Client) isFloodlight() bool {
|
||||
return c.model == "HL_CFL2"
|
||||
}
|
||||
|
||||
const (
|
||||
statusDefault byte = 1
|
||||
statusENR16 byte = 3
|
||||
statusENR32 byte = 6
|
||||
)
|
||||
|
||||
func generateChallengeResponse(challengeBytes []byte, enr string, status byte) []byte {
|
||||
var secretKey []byte
|
||||
|
||||
switch status {
|
||||
case statusDefault:
|
||||
secretKey = []byte("FFFFFFFFFFFFFFFF")
|
||||
case statusENR16:
|
||||
if len(enr) >= 16 {
|
||||
secretKey = []byte(enr[:16])
|
||||
} else {
|
||||
secretKey = make([]byte, 16)
|
||||
copy(secretKey, enr)
|
||||
}
|
||||
case statusENR32:
|
||||
if len(enr) >= 16 {
|
||||
firstKey := []byte(enr[:16])
|
||||
challengeBytes = tutk.XXTEADecryptVar(challengeBytes, firstKey)
|
||||
}
|
||||
if len(enr) >= 32 {
|
||||
secretKey = []byte(enr[16:32])
|
||||
} else if len(enr) > 16 {
|
||||
secretKey = make([]byte, 16)
|
||||
copy(secretKey, []byte(enr[16:]))
|
||||
} else {
|
||||
secretKey = []byte("FFFFFFFFFFFFFFFF")
|
||||
}
|
||||
default:
|
||||
secretKey = []byte("FFFFFFFFFFFFFFFF")
|
||||
}
|
||||
|
||||
return tutk.XXTEADecryptVar(challengeBytes, secretKey)
|
||||
}
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"math/bits"
|
||||
)
|
||||
|
||||
const charlie = "Charlie is the designer of P2P!!"
|
||||
|
||||
func TransCodePartial(src []byte) []byte {
|
||||
n := len(src)
|
||||
tmp := make([]byte, n)
|
||||
dst := bytes.Clone(src)
|
||||
src16, tmp16, dst16 := src, tmp, dst
|
||||
|
||||
for ; n >= 16; n -= 16 {
|
||||
for i := 0; i < 16; i += 4 {
|
||||
x := binary.LittleEndian.Uint32(src16[i:])
|
||||
binary.LittleEndian.PutUint32(tmp16[i:], bits.RotateLeft32(x, -i-1))
|
||||
}
|
||||
for i := range 16 {
|
||||
dst16[i] = tmp16[i] ^ charlie[i]
|
||||
}
|
||||
swap(dst16, tmp16, 16)
|
||||
for i := 0; i < 16; i += 4 {
|
||||
x := binary.LittleEndian.Uint32(tmp16[i:])
|
||||
binary.LittleEndian.PutUint32(dst16[i:], bits.RotateLeft32(x, -i-3))
|
||||
}
|
||||
tmp16, dst16, src16 = tmp16[16:], dst16[16:], src16[16:]
|
||||
}
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
tmp16[i] = src16[i] ^ charlie[i]
|
||||
}
|
||||
swap(tmp16, dst16, n)
|
||||
return dst
|
||||
}
|
||||
|
||||
func ReverseTransCodePartial(src []byte) []byte {
|
||||
n := len(src)
|
||||
tmp := make([]byte, n)
|
||||
dst := bytes.Clone(src)
|
||||
src16, tmp16, dst16 := src, tmp, dst
|
||||
|
||||
for ; n >= 16; n -= 16 {
|
||||
for i := 0; i < 16; i += 4 {
|
||||
x := binary.LittleEndian.Uint32(src16[i:])
|
||||
binary.LittleEndian.PutUint32(tmp16[i:], bits.RotateLeft32(x, i+3))
|
||||
}
|
||||
swap(tmp16, dst16, 16)
|
||||
for i := range 16 {
|
||||
tmp16[i] = dst16[i] ^ charlie[i]
|
||||
}
|
||||
for i := 0; i < 16; i += 4 {
|
||||
x := binary.LittleEndian.Uint32(tmp16[i:])
|
||||
binary.LittleEndian.PutUint32(dst16[i:], bits.RotateLeft32(x, i+1))
|
||||
}
|
||||
tmp16, dst16, src16 = tmp16[16:], dst16[16:], src16[16:]
|
||||
}
|
||||
|
||||
swap(src16, tmp16, n)
|
||||
for i := 0; i < n; i++ {
|
||||
dst16[i] = tmp16[i] ^ charlie[i]
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func TransCodeBlob(src []byte) []byte {
|
||||
if len(src) < 16 {
|
||||
return TransCodePartial(src)
|
||||
}
|
||||
|
||||
dst := make([]byte, len(src))
|
||||
header := TransCodePartial(src[:16])
|
||||
copy(dst, header)
|
||||
|
||||
if len(src) > 16 {
|
||||
if src[3]&1 != 0 { // Partial encryption
|
||||
remaining := len(src) - 16
|
||||
encryptLen := min(remaining, 48)
|
||||
if encryptLen > 0 {
|
||||
encrypted := TransCodePartial(src[16 : 16+encryptLen])
|
||||
copy(dst[16:], encrypted)
|
||||
}
|
||||
if remaining > 48 {
|
||||
copy(dst[64:], src[64:])
|
||||
}
|
||||
} else { // Full encryption
|
||||
encrypted := TransCodePartial(src[16:])
|
||||
copy(dst[16:], encrypted)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func ReverseTransCodeBlob(src []byte) []byte {
|
||||
if len(src) < 16 {
|
||||
return ReverseTransCodePartial(src)
|
||||
}
|
||||
|
||||
dst := make([]byte, len(src))
|
||||
header := ReverseTransCodePartial(src[:16])
|
||||
copy(dst, header)
|
||||
|
||||
if len(src) > 16 {
|
||||
if dst[3]&1 != 0 { // Partial encryption (check decrypted header)
|
||||
remaining := len(src) - 16
|
||||
decryptLen := min(remaining, 48)
|
||||
if decryptLen > 0 {
|
||||
decrypted := ReverseTransCodePartial(src[16 : 16+decryptLen])
|
||||
copy(dst[16:], decrypted)
|
||||
}
|
||||
if remaining > 48 {
|
||||
copy(dst[64:], src[64:])
|
||||
}
|
||||
} else { // Full decryption
|
||||
decrypted := ReverseTransCodePartial(src[16:])
|
||||
copy(dst[16:], decrypted)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func RandRead(b []byte) {
|
||||
_, _ = rand.Read(b)
|
||||
}
|
||||
|
||||
func swap(src, dst []byte, n int) {
|
||||
switch n {
|
||||
case 8:
|
||||
dst[0], dst[1], dst[2], dst[3] = src[7], src[4], src[3], src[2]
|
||||
dst[4], dst[5], dst[6], dst[7] = src[1], src[6], src[5], src[0]
|
||||
case 16:
|
||||
dst[0], dst[1], dst[2], dst[3] = src[11], src[9], src[8], src[15]
|
||||
dst[4], dst[5], dst[6], dst[7] = src[13], src[10], src[12], src[14]
|
||||
dst[8], dst[9], dst[10], dst[11] = src[2], src[1], src[5], src[0]
|
||||
dst[12], dst[13], dst[14], dst[15] = src[6], src[4], src[7], src[3]
|
||||
default:
|
||||
copy(dst, src[:n])
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const delta = 0x9e3779b9
|
||||
|
||||
const (
|
||||
StatusDefault byte = 1
|
||||
StatusENR16 byte = 3
|
||||
StatusENR32 byte = 6
|
||||
)
|
||||
|
||||
func XXTEADecrypt(data, key []byte) []byte {
|
||||
if len(data) < 8 || len(key) < 16 {
|
||||
return nil
|
||||
}
|
||||
|
||||
k := make([]uint32, 4)
|
||||
for i := range 4 {
|
||||
k[i] = binary.LittleEndian.Uint32(key[i*4:])
|
||||
}
|
||||
|
||||
n := max(len(data)/4, 2)
|
||||
v := make([]uint32, n)
|
||||
for i := 0; i < len(data)/4; i++ {
|
||||
v[i] = binary.LittleEndian.Uint32(data[i*4:])
|
||||
}
|
||||
|
||||
rounds := 6 + 52/n
|
||||
sum := uint32(rounds) * delta
|
||||
y := v[0]
|
||||
|
||||
for rounds > 0 {
|
||||
e := (sum >> 2) & 3
|
||||
for p := n - 1; p > 0; p-- {
|
||||
z := v[p-1]
|
||||
v[p] -= mx(sum, y, z, p, e, k)
|
||||
y = v[p]
|
||||
}
|
||||
z := v[n-1]
|
||||
v[0] -= mx(sum, y, z, 0, e, k)
|
||||
y = v[0]
|
||||
sum -= delta
|
||||
rounds--
|
||||
}
|
||||
|
||||
result := make([]byte, n*4)
|
||||
for i := range n {
|
||||
binary.LittleEndian.PutUint32(result[i*4:], v[i])
|
||||
}
|
||||
|
||||
return result[:len(data)]
|
||||
}
|
||||
|
||||
func XXTEAEncrypt(data, key []byte) []byte {
|
||||
if len(data) < 8 || len(key) < 16 {
|
||||
return nil
|
||||
}
|
||||
|
||||
k := make([]uint32, 4)
|
||||
for i := range 4 {
|
||||
k[i] = binary.LittleEndian.Uint32(key[i*4:])
|
||||
}
|
||||
|
||||
n := max(len(data)/4, 2)
|
||||
v := make([]uint32, n)
|
||||
for i := 0; i < len(data)/4; i++ {
|
||||
v[i] = binary.LittleEndian.Uint32(data[i*4:])
|
||||
}
|
||||
|
||||
rounds := 6 + 52/n
|
||||
var sum uint32
|
||||
z := v[n-1]
|
||||
|
||||
for rounds > 0 {
|
||||
sum += delta
|
||||
e := (sum >> 2) & 3
|
||||
for p := 0; p < n-1; p++ {
|
||||
y := v[p+1]
|
||||
v[p] += mx(sum, y, z, p, e, k)
|
||||
z = v[p]
|
||||
}
|
||||
y := v[0]
|
||||
v[n-1] += mx(sum, y, z, n-1, e, k)
|
||||
z = v[n-1]
|
||||
rounds--
|
||||
}
|
||||
|
||||
result := make([]byte, n*4)
|
||||
for i := range n {
|
||||
binary.LittleEndian.PutUint32(result[i*4:], v[i])
|
||||
}
|
||||
|
||||
return result[:len(data)]
|
||||
}
|
||||
|
||||
func mx(sum, y, z uint32, p int, e uint32, k []uint32) uint32 {
|
||||
return ((z>>5 ^ y<<2) + (y>>3 ^ z<<4)) ^ ((sum ^ y) + (k[(p&3)^int(e)] ^ z))
|
||||
}
|
||||
|
||||
func GenerateChallengeResponse(challengeBytes []byte, enr string, status byte) []byte {
|
||||
var secretKey []byte
|
||||
|
||||
switch status {
|
||||
case StatusDefault:
|
||||
secretKey = []byte("FFFFFFFFFFFFFFFF")
|
||||
case StatusENR16:
|
||||
if len(enr) >= 16 {
|
||||
secretKey = []byte(enr[:16])
|
||||
} else {
|
||||
secretKey = make([]byte, 16)
|
||||
copy(secretKey, enr)
|
||||
}
|
||||
case StatusENR32:
|
||||
if len(enr) >= 16 {
|
||||
firstKey := []byte(enr[:16])
|
||||
challengeBytes = XXTEADecrypt(challengeBytes, firstKey)
|
||||
}
|
||||
if len(enr) >= 32 {
|
||||
secretKey = []byte(enr[16:32])
|
||||
} else if len(enr) > 16 {
|
||||
secretKey = make([]byte, 16)
|
||||
copy(secretKey, []byte(enr[16:]))
|
||||
} else {
|
||||
secretKey = []byte("FFFFFFFFFFFFFFFF")
|
||||
}
|
||||
default:
|
||||
secretKey = []byte("FFFFFFFFFFFFFFFF")
|
||||
}
|
||||
|
||||
return XXTEADecrypt(challengeBytes, secretKey)
|
||||
}
|
||||
|
||||
func CalculateAuthKey(enr, mac string) []byte {
|
||||
data := enr + strings.ToUpper(mac)
|
||||
hash := sha256.Sum256([]byte(data))
|
||||
b64 := base64.StdEncoding.EncodeToString(hash[:6])
|
||||
b64 = strings.ReplaceAll(b64, "+", "Z")
|
||||
b64 = strings.ReplaceAll(b64, "/", "9")
|
||||
b64 = strings.ReplaceAll(b64, "=", "A")
|
||||
return []byte(b64)
|
||||
}
|
||||
+14
-14
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/wyze/tutk"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tutk"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
@@ -96,21 +96,21 @@ func (p *Producer) Start() error {
|
||||
Payload: annexb.EncodeToAVCC(pkt.Payload),
|
||||
}
|
||||
|
||||
case tutk.AudioCodecG711U:
|
||||
case tutk.CodecPCMU:
|
||||
name = core.CodecPCMU
|
||||
pkt2 = &core.Packet{
|
||||
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
|
||||
Payload: pkt.Payload,
|
||||
}
|
||||
|
||||
case tutk.AudioCodecG711A:
|
||||
case tutk.CodecPCMA:
|
||||
name = core.CodecPCMA
|
||||
pkt2 = &core.Packet{
|
||||
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
|
||||
Payload: pkt.Payload,
|
||||
}
|
||||
|
||||
case tutk.AudioCodecAACADTS, tutk.AudioCodecAACWyze, tutk.AudioCodecAACRaw, tutk.AudioCodecAACLATM:
|
||||
case tutk.CodecAACADTS, tutk.CodecAACAlt, tutk.CodecAACRaw, tutk.CodecAACLATM:
|
||||
name = core.CodecAAC
|
||||
payload := pkt.Payload
|
||||
if aac.IsADTS(payload) {
|
||||
@@ -121,21 +121,21 @@ func (p *Producer) Start() error {
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
case tutk.AudioCodecOpus:
|
||||
case tutk.CodecOpus:
|
||||
name = core.CodecOpus
|
||||
pkt2 = &core.Packet{
|
||||
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
|
||||
Payload: pkt.Payload,
|
||||
}
|
||||
|
||||
case tutk.AudioCodecPCM:
|
||||
case tutk.CodecPCML:
|
||||
name = core.CodecPCML
|
||||
pkt2 = &core.Packet{
|
||||
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
|
||||
Payload: pkt.Payload,
|
||||
}
|
||||
|
||||
case tutk.AudioCodecMP3:
|
||||
case tutk.CodecMP3:
|
||||
name = core.CodecMP3
|
||||
pkt2 = &core.Packet{
|
||||
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
|
||||
@@ -167,7 +167,7 @@ func probe(client *Client, quality byte) ([]*core.Media, error) {
|
||||
client.SetDeadline(time.Now().Add(core.ProbeTimeout))
|
||||
|
||||
var vcodec, acodec *core.Codec
|
||||
var tutkAudioCodec uint16
|
||||
var tutkAudioCodec byte
|
||||
|
||||
for {
|
||||
if client.verbose {
|
||||
@@ -197,33 +197,33 @@ func probe(client *Client, quality byte) ([]*core.Media, error) {
|
||||
vcodec = h265.AVCCToCodec(buf)
|
||||
}
|
||||
}
|
||||
case tutk.AudioCodecG711U:
|
||||
case tutk.CodecPCMU:
|
||||
if acodec == nil {
|
||||
acodec = &core.Codec{Name: core.CodecPCMU, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
|
||||
tutkAudioCodec = pkt.Codec
|
||||
}
|
||||
case tutk.AudioCodecG711A:
|
||||
case tutk.CodecPCMA:
|
||||
if acodec == nil {
|
||||
acodec = &core.Codec{Name: core.CodecPCMA, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
|
||||
tutkAudioCodec = pkt.Codec
|
||||
}
|
||||
case tutk.AudioCodecAACWyze, tutk.AudioCodecAACADTS, tutk.AudioCodecAACRaw, tutk.AudioCodecAACLATM:
|
||||
case tutk.CodecAACAlt, tutk.CodecAACADTS, tutk.CodecAACRaw, tutk.CodecAACLATM:
|
||||
if acodec == nil {
|
||||
config := aac.EncodeConfig(aac.TypeAACLC, pkt.SampleRate, pkt.Channels, false)
|
||||
acodec = aac.ConfigToCodec(config)
|
||||
tutkAudioCodec = pkt.Codec
|
||||
}
|
||||
case tutk.AudioCodecOpus:
|
||||
case tutk.CodecOpus:
|
||||
if acodec == nil {
|
||||
acodec = &core.Codec{Name: core.CodecOpus, ClockRate: 48000, Channels: 2}
|
||||
tutkAudioCodec = pkt.Codec
|
||||
}
|
||||
case tutk.AudioCodecPCM:
|
||||
case tutk.CodecPCML:
|
||||
if acodec == nil {
|
||||
acodec = &core.Codec{Name: core.CodecPCML, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
|
||||
tutkAudioCodec = pkt.Codec
|
||||
}
|
||||
case tutk.AudioCodecMP3:
|
||||
case tutk.CodecMP3:
|
||||
if acodec == nil {
|
||||
acodec = &core.Codec{Name: core.CodecMP3, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
|
||||
tutkAudioCodec = pkt.Codec
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,281 +0,0 @@
|
||||
package tutk
|
||||
|
||||
type AVLoginResponse struct {
|
||||
ServerType uint32
|
||||
Resend int32
|
||||
TwoWayStreaming int32
|
||||
SyncRecvData int32
|
||||
SecurityMode uint32
|
||||
VideoOnConnect int32
|
||||
AudioOnConnect int32
|
||||
}
|
||||
|
||||
const (
|
||||
CodecUnknown uint16 = 0x00
|
||||
CodecMPEG4 uint16 = 0x4C // 76
|
||||
CodecH263 uint16 = 0x4D // 77
|
||||
CodecH264 uint16 = 0x4E // 78
|
||||
CodecMJPEG uint16 = 0x4F // 79
|
||||
CodecH265 uint16 = 0x50 // 80
|
||||
)
|
||||
|
||||
const (
|
||||
AudioCodecAACRaw uint16 = 0x86 // 134
|
||||
AudioCodecAACADTS uint16 = 0x87 // 135
|
||||
AudioCodecAACLATM uint16 = 0x88 // 136
|
||||
AudioCodecG711U uint16 = 0x89 // 137
|
||||
AudioCodecG711A uint16 = 0x8A // 138
|
||||
AudioCodecADPCM uint16 = 0x8B // 139
|
||||
AudioCodecPCM uint16 = 0x8C // 140
|
||||
AudioCodecSPEEX uint16 = 0x8D // 141
|
||||
AudioCodecMP3 uint16 = 0x8E // 142
|
||||
AudioCodecG726 uint16 = 0x8F // 143
|
||||
AudioCodecAACWyze uint16 = 0x90 // 144
|
||||
AudioCodecOpus uint16 = 0x92 // 146
|
||||
)
|
||||
|
||||
const (
|
||||
SampleRate8K uint8 = 0x00
|
||||
SampleRate11K uint8 = 0x01
|
||||
SampleRate12K uint8 = 0x02
|
||||
SampleRate16K uint8 = 0x03
|
||||
SampleRate22K uint8 = 0x04
|
||||
SampleRate24K uint8 = 0x05
|
||||
SampleRate32K uint8 = 0x06
|
||||
SampleRate44K uint8 = 0x07
|
||||
SampleRate48K uint8 = 0x08
|
||||
)
|
||||
|
||||
var sampleRates = map[uint8]int{
|
||||
SampleRate8K: 8000,
|
||||
SampleRate11K: 11025,
|
||||
SampleRate12K: 12000,
|
||||
SampleRate16K: 16000,
|
||||
SampleRate22K: 22050,
|
||||
SampleRate24K: 24000,
|
||||
SampleRate32K: 32000,
|
||||
SampleRate44K: 44100,
|
||||
SampleRate48K: 48000,
|
||||
}
|
||||
|
||||
var samplesPerFrame = map[uint16]uint32{
|
||||
AudioCodecAACRaw: 1024,
|
||||
AudioCodecAACADTS: 1024,
|
||||
AudioCodecAACLATM: 1024,
|
||||
AudioCodecAACWyze: 1024,
|
||||
AudioCodecG711U: 160,
|
||||
AudioCodecG711A: 160,
|
||||
AudioCodecPCM: 160,
|
||||
AudioCodecADPCM: 160,
|
||||
AudioCodecSPEEX: 160,
|
||||
AudioCodecMP3: 1152,
|
||||
AudioCodecG726: 160,
|
||||
AudioCodecOpus: 960,
|
||||
}
|
||||
|
||||
const (
|
||||
IOTypeVideoStart = 0x01FF
|
||||
IOTypeVideoStop = 0x02FF
|
||||
IOTypeAudioStart = 0x0300
|
||||
IOTypeAudioStop = 0x0301
|
||||
IOTypeSpeakerStart = 0x0350
|
||||
IOTypeSpeakerStop = 0x0351
|
||||
IOTypeGetAudioOutFormatReq = 0x032A
|
||||
IOTypeGetAudioOutFormatRes = 0x032B
|
||||
IOTypeSetStreamCtrlReq = 0x0320
|
||||
IOTypeSetStreamCtrlRes = 0x0321
|
||||
IOTypeGetStreamCtrlReq = 0x0322
|
||||
IOTypeGetStreamCtrlRes = 0x0323
|
||||
IOTypeDevInfoReq = 0x0340
|
||||
IOTypeDevInfoRes = 0x0341
|
||||
IOTypeGetSupportStreamReq = 0x0344
|
||||
IOTypeGetSupportStreamRes = 0x0345
|
||||
IOTypeSetRecordReq = 0x0310
|
||||
IOTypeSetRecordRes = 0x0311
|
||||
IOTypeGetRecordReq = 0x0312
|
||||
IOTypeGetRecordRes = 0x0313
|
||||
IOTypePTZCommand = 0x1001
|
||||
IOTypeReceiveFirstFrame = 0x1002
|
||||
IOTypeGetEnvironmentReq = 0x030A
|
||||
IOTypeGetEnvironmentRes = 0x030B
|
||||
IOTypeSetVideoModeReq = 0x030C
|
||||
IOTypeSetVideoModeRes = 0x030D
|
||||
IOTypeGetVideoModeReq = 0x030E
|
||||
IOTypeGetVideoModeRes = 0x030F
|
||||
IOTypeSetTimeReq = 0x0316
|
||||
IOTypeSetTimeRes = 0x0317
|
||||
IOTypeGetTimeReq = 0x0318
|
||||
IOTypeGetTimeRes = 0x0319
|
||||
IOTypeSetWifiReq = 0x0102
|
||||
IOTypeSetWifiRes = 0x0103
|
||||
IOTypeGetWifiReq = 0x0104
|
||||
IOTypeGetWifiRes = 0x0105
|
||||
IOTypeListWifiAPReq = 0x0106
|
||||
IOTypeListWifiAPRes = 0x0107
|
||||
IOTypeSetMotionDetectReq = 0x0306
|
||||
IOTypeSetMotionDetectRes = 0x0307
|
||||
IOTypeGetMotionDetectReq = 0x0308
|
||||
IOTypeGetMotionDetectRes = 0x0309
|
||||
)
|
||||
|
||||
// OLD Protocol (IOTC/TransCode)
|
||||
const (
|
||||
CmdDiscoReq uint16 = 0x0601
|
||||
CmdDiscoRes uint16 = 0x0602
|
||||
CmdSessionReq uint16 = 0x0402
|
||||
CmdSessionRes uint16 = 0x0404
|
||||
CmdDataTX uint16 = 0x0407
|
||||
CmdDataRX uint16 = 0x0408
|
||||
CmdKeepaliveReq uint16 = 0x0427
|
||||
CmdKeepaliveRes uint16 = 0x0428
|
||||
|
||||
OldHeaderSize = 16
|
||||
OldDiscoBodySize = 72
|
||||
OldDiscoSize = OldHeaderSize + OldDiscoBodySize
|
||||
OldSessionBody = 36
|
||||
OldSessionSize = OldHeaderSize + OldSessionBody
|
||||
)
|
||||
|
||||
// NEW Protocol (0xCC51)
|
||||
const (
|
||||
MagicNewProto uint16 = 0xCC51
|
||||
CmdNewDisco uint16 = 0x1002
|
||||
CmdNewKeepalive uint16 = 0x1202
|
||||
CmdNewClose uint16 = 0x1302
|
||||
CmdNewDTLS uint16 = 0x1502
|
||||
NewPayloadSize uint16 = 0x0028
|
||||
NewPacketSize = 52
|
||||
NewHeaderSize = 28
|
||||
NewAuthSize = 20
|
||||
NewKeepaliveSize = 48
|
||||
)
|
||||
|
||||
const (
|
||||
UIDSize = 20
|
||||
RandIDSize = 8
|
||||
)
|
||||
|
||||
const (
|
||||
MagicAVLoginResp uint16 = 0x2100
|
||||
MagicIOCtrl uint16 = 0x7000
|
||||
MagicChannelMsg uint16 = 0x1000
|
||||
MagicACK uint16 = 0x0009
|
||||
MagicAVLogin1 uint16 = 0x0000
|
||||
MagicAVLogin2 uint16 = 0x2000
|
||||
)
|
||||
|
||||
const (
|
||||
ProtoVersion uint16 = 0x000c
|
||||
DefaultCaps uint32 = 0x001f07fb
|
||||
)
|
||||
|
||||
const (
|
||||
IOTCChannelMain = 0 // Main AV (we = DTLS Client)
|
||||
IOTCChannelBack = 1 // Backchannel (we = DTLS Server)
|
||||
)
|
||||
|
||||
const (
|
||||
PSKIdentity = "AUTHPWD_admin"
|
||||
DefaultUser = "admin"
|
||||
DefaultPort = 32761
|
||||
)
|
||||
|
||||
func CodecName(id uint16) string {
|
||||
switch id {
|
||||
case CodecH264:
|
||||
return "H264"
|
||||
case CodecH265:
|
||||
return "H265"
|
||||
case CodecMPEG4:
|
||||
return "MPEG4"
|
||||
case CodecH263:
|
||||
return "H263"
|
||||
case CodecMJPEG:
|
||||
return "MJPEG"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func AudioCodecName(id uint16) string {
|
||||
switch id {
|
||||
case AudioCodecG711U:
|
||||
return "PCMU"
|
||||
case AudioCodecG711A:
|
||||
return "PCMA"
|
||||
case AudioCodecPCM:
|
||||
return "PCM"
|
||||
case AudioCodecAACLATM, AudioCodecAACRaw, AudioCodecAACADTS, AudioCodecAACWyze:
|
||||
return "AAC"
|
||||
case AudioCodecOpus:
|
||||
return "Opus"
|
||||
case AudioCodecSPEEX:
|
||||
return "Speex"
|
||||
case AudioCodecMP3:
|
||||
return "MP3"
|
||||
case AudioCodecG726:
|
||||
return "G726"
|
||||
case AudioCodecADPCM:
|
||||
return "ADPCM"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func SampleRateValue(idx uint8) int {
|
||||
if rate, ok := sampleRates[idx]; ok {
|
||||
return rate
|
||||
}
|
||||
return 16000
|
||||
}
|
||||
|
||||
func SampleRateIndex(hz uint32) uint8 {
|
||||
switch hz {
|
||||
case 8000:
|
||||
return SampleRate8K
|
||||
case 11025:
|
||||
return SampleRate11K
|
||||
case 12000:
|
||||
return SampleRate12K
|
||||
case 16000:
|
||||
return SampleRate16K
|
||||
case 22050:
|
||||
return SampleRate22K
|
||||
case 24000:
|
||||
return SampleRate24K
|
||||
case 32000:
|
||||
return SampleRate32K
|
||||
case 44100:
|
||||
return SampleRate44K
|
||||
case 48000:
|
||||
return SampleRate48K
|
||||
default:
|
||||
return SampleRate16K
|
||||
}
|
||||
}
|
||||
|
||||
func BuildAudioFlags(sampleRate uint32, bits16, stereo bool) uint8 {
|
||||
flags := SampleRateIndex(sampleRate) << 2
|
||||
if bits16 {
|
||||
flags |= 0x02
|
||||
}
|
||||
if stereo {
|
||||
flags |= 0x01
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
func IsVideoCodec(id uint16) bool {
|
||||
return id >= CodecMPEG4 && id <= CodecH265
|
||||
}
|
||||
|
||||
func IsAudioCodec(id uint16) bool {
|
||||
return id >= AudioCodecAACRaw && id <= AudioCodecOpus
|
||||
}
|
||||
|
||||
func GetSamplesPerFrame(codecID uint16) uint32 {
|
||||
if samples, ok := samplesPerFrame[codecID]; ok {
|
||||
return samples
|
||||
}
|
||||
return 1024
|
||||
}
|
||||
@@ -107,7 +107,7 @@ func (c *Client) ReadPacket() (hdr, payload []byte, err error) {
|
||||
switch hdr[0] {
|
||||
case tutk.CodecH264, tutk.CodecH265:
|
||||
payload, err = DecodeVideo(payload, c.key)
|
||||
case tutk.CodecAAC:
|
||||
case tutk.CodecAACLATM:
|
||||
payload, err = crypto.Decode(payload, c.key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ func probe(client *Client) ([]*core.Media, error) {
|
||||
if acodec == nil {
|
||||
acodec = &core.Codec{Name: core.CodecPCML, ClockRate: 8000}
|
||||
}
|
||||
case tutk.CodecAAC:
|
||||
case tutk.CodecAACLATM:
|
||||
if acodec == nil {
|
||||
acodec = aac.ADTSToCodec(payload)
|
||||
if acodec != nil {
|
||||
@@ -187,7 +187,7 @@ func (c *Producer) Start() error {
|
||||
audioTS += uint32(n / 2) // because 16bit
|
||||
}
|
||||
|
||||
case tutk.CodecAAC:
|
||||
case tutk.CodecAACLATM:
|
||||
pkt = &core.Packet{
|
||||
Header: rtp.Header{
|
||||
SequenceNumber: audioSeq,
|
||||
|
||||
Reference in New Issue
Block a user