Merge pull request #2011 from seydx/wyze

Native Wyze camera support
This commit is contained in:
Alex X
2026-01-18 08:39:37 +03:00
committed by GitHub
23 changed files with 3865 additions and 22 deletions
+7
View File
@@ -10,6 +10,13 @@ import (
const ADTSHeaderSize = 7
func ADTSHeaderLen(b []byte) int {
if HasCRC(b) {
return 9 // 7 bytes header + 2 bytes CRC
}
return ADTSHeaderSize
}
func IsADTS(b []byte) bool {
// AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)
// A 12 Syncword, all bits must be set to 1.
+59
View File
@@ -0,0 +1,59 @@
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 GetSampleRateIndex(sampleRate uint32) uint8 {
for i, rate := range sampleRates {
if rate == sampleRate {
return uint8(i)
}
}
return 3 // default 16kHz
}
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
}
+102
View File
@@ -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))
}
+35
View File
@@ -0,0 +1,35 @@
package dtls
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
}
+218
View File
@@ -0,0 +1,218 @@
package dtls
import (
"crypto/cipher"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"hash"
"sync/atomic"
"github.com/pion/dtls/v3"
"github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
"github.com/pion/dtls/v3/pkg/crypto/prf"
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
"golang.org/x/crypto/chacha20poly1305"
)
const CipherSuiteID_CCAC dtls.CipherSuiteID = 0xCCAC
const (
chachaTagLength = 16
chachaNonceLength = 12
)
var (
errDecryptPacket = &protocol.TemporaryError{Err: errors.New("failed to decrypt packet")}
errCipherSuiteNotInit = &protocol.TemporaryError{Err: errors.New("CipherSuite not initialized")}
)
type ChaCha20Poly1305Cipher struct {
localCipher, remoteCipher cipher.AEAD
localWriteIV, remoteWriteIV []byte
}
func NewChaCha20Poly1305Cipher(localKey, localWriteIV, remoteKey, remoteWriteIV []byte) (*ChaCha20Poly1305Cipher, error) {
localCipher, err := chacha20poly1305.New(localKey)
if err != nil {
return nil, err
}
remoteCipher, err := chacha20poly1305.New(remoteKey)
if err != nil {
return nil, err
}
return &ChaCha20Poly1305Cipher{
localCipher: localCipher,
localWriteIV: localWriteIV,
remoteCipher: remoteCipher,
remoteWriteIV: remoteWriteIV,
}, nil
}
func generateAEADAdditionalData(h *recordlayer.Header, payloadLen int) []byte {
var additionalData [13]byte
binary.BigEndian.PutUint64(additionalData[:], h.SequenceNumber)
binary.BigEndian.PutUint16(additionalData[:], h.Epoch)
additionalData[8] = byte(h.ContentType)
additionalData[9] = h.Version.Major
additionalData[10] = h.Version.Minor
binary.BigEndian.PutUint16(additionalData[11:], uint16(payloadLen))
return additionalData[:]
}
func computeNonce(iv []byte, epoch uint16, sequenceNumber uint64) []byte {
nonce := make([]byte, chachaNonceLength)
binary.BigEndian.PutUint64(nonce[4:], sequenceNumber)
binary.BigEndian.PutUint16(nonce[4:], epoch)
for i := range chachaNonceLength {
nonce[i] ^= iv[i]
}
return nonce
}
func (c *ChaCha20Poly1305Cipher) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error) {
payload := raw[pkt.Header.Size():]
raw = raw[:pkt.Header.Size()]
nonce := computeNonce(c.localWriteIV, pkt.Header.Epoch, pkt.Header.SequenceNumber)
additionalData := generateAEADAdditionalData(&pkt.Header, len(payload))
encryptedPayload := c.localCipher.Seal(nil, nonce, payload, additionalData)
r := make([]byte, len(raw)+len(encryptedPayload))
copy(r, raw)
copy(r[len(raw):], encryptedPayload)
binary.BigEndian.PutUint16(r[pkt.Header.Size()-2:], uint16(len(r)-pkt.Header.Size()))
return r, nil
}
func (c *ChaCha20Poly1305Cipher) Decrypt(header recordlayer.Header, in []byte) ([]byte, error) {
err := header.Unmarshal(in)
switch {
case err != nil:
return nil, err
case header.ContentType == protocol.ContentTypeChangeCipherSpec:
return in, nil
case len(in) <= header.Size()+chachaTagLength:
return nil, fmt.Errorf("ciphertext too short: %d <= %d", len(in), header.Size()+chachaTagLength)
}
nonce := computeNonce(c.remoteWriteIV, header.Epoch, header.SequenceNumber)
out := in[header.Size():]
additionalData := generateAEADAdditionalData(&header, len(out)-chachaTagLength)
out, err = c.remoteCipher.Open(out[:0], nonce, out, additionalData)
if err != nil {
return nil, fmt.Errorf("%w: %v", errDecryptPacket, err)
}
return append(in[:header.Size()], out...), nil
}
type TLSEcdhePskWithChacha20Poly1305Sha256 struct {
aead atomic.Value
}
func NewTLSEcdhePskWithChacha20Poly1305Sha256() *TLSEcdhePskWithChacha20Poly1305Sha256 {
return &TLSEcdhePskWithChacha20Poly1305Sha256{}
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) CertificateType() clientcertificate.Type {
return clientcertificate.Type(0)
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) KeyExchangeAlgorithm() dtls.CipherSuiteKeyExchangeAlgorithm {
return dtls.CipherSuiteKeyExchangeAlgorithmPsk | dtls.CipherSuiteKeyExchangeAlgorithmEcdhe
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) ECC() bool {
return true
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) ID() dtls.CipherSuiteID {
return CipherSuiteID_CCAC
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) String() string {
return "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256"
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) HashFunc() func() hash.Hash {
return sha256.New
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) AuthenticationType() dtls.CipherSuiteAuthenticationType {
return dtls.CipherSuiteAuthenticationTypePreSharedKey
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) IsInitialized() bool {
return c.aead.Load() != nil
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) Init(masterSecret, clientRandom, serverRandom []byte, isClient bool) error {
const (
prfMacLen = 0
prfKeyLen = 32
prfIvLen = 12
)
keys, err := prf.GenerateEncryptionKeys(
masterSecret, clientRandom, serverRandom,
prfMacLen, prfKeyLen, prfIvLen,
c.HashFunc(),
)
if err != nil {
return err
}
var aead *ChaCha20Poly1305Cipher
if isClient {
aead, err = NewChaCha20Poly1305Cipher(
keys.ClientWriteKey, keys.ClientWriteIV,
keys.ServerWriteKey, keys.ServerWriteIV,
)
} else {
aead, err = NewChaCha20Poly1305Cipher(
keys.ServerWriteKey, keys.ServerWriteIV,
keys.ClientWriteKey, keys.ClientWriteIV,
)
}
if err != nil {
return err
}
c.aead.Store(aead)
return nil
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error) {
aead, ok := c.aead.Load().(*ChaCha20Poly1305Cipher)
if !ok {
return nil, fmt.Errorf("%w: unable to encrypt", errCipherSuiteNotInit)
}
return aead.Encrypt(pkt, raw)
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) Decrypt(h recordlayer.Header, raw []byte) ([]byte, error) {
aead, ok := c.aead.Load().(*ChaCha20Poly1305Cipher)
if !ok {
return nil, fmt.Errorf("%w: unable to decrypt", errCipherSuiteNotInit)
}
return aead.Decrypt(h, raw)
}
func CustomCipherSuites() []dtls.CipherSuite {
return []dtls.CipherSuite{
NewTLSEcdhePskWithChacha20Poly1305Sha256(),
}
}
+987
View File
@@ -0,0 +1,987 @@
package dtls
import (
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/binary"
"fmt"
"io"
"net"
"sync"
"time"
"github.com/AlexxIT/go2rtc/pkg/tutk"
"github.com/pion/dtls/v3"
)
const (
magicCC51 = "\x51\xcc" // (wyze specific?)
sdkVersion42 = "\x01\x01\x02\x04" // 4.2.1.1
sdkVersion43 = "\x00\x08\x03\x04" // 4.3.8.0
)
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 *tutk.FrameHandler
err error
verbose bool
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
mu sync.RWMutex
// DTLS
clientConn *dtls.Conn
serverConn *dtls.Conn
clientBuf chan []byte
serverBuf chan []byte
rawCmd chan []byte
// Identity
uid string
authKey string
enr string
psk []byte
// Session
sid []byte
ticket uint16
hasTwoWayStreaming bool
// Protocol
isCC51 bool
seq uint16
seqCmd uint16
avSeq uint32
kaSeq uint32
audioSeq uint32
audioFrameNo uint32
// Ack
ackFlags uint16
rxSeqStart uint16
rxSeqEnd uint16
rxSeqInit bool
cmdAck func()
}
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(2 * 1024 * 1024)
ctx, cancel := context.WithCancel(context.Background())
psk := DerivePSK(enr)
if port == 0 {
port = 32761
}
c := &DTLSConn{
conn: udp,
addr: &net.UDPAddr{IP: net.ParseIP(host), Port: port},
uid: uid,
authKey: authKey,
enr: enr,
psk: psk,
verbose: verbose,
ctx: ctx,
cancel: cancel,
rxSeqStart: 0xffff,
rxSeqEnd: 0xffff,
}
if err = c.discovery(); err != nil {
_ = c.Close()
return nil, err
}
c.clientBuf = make(chan []byte, 64)
c.serverBuf = make(chan []byte, 64)
c.rawCmd = make(chan []byte, 16)
c.frames = tutk.NewFrameHandler(c.verbose)
c.wg.Add(1)
go c.reader()
if err = c.connect(); err != nil {
_ = c.Close()
return nil, err
}
c.wg.Add(1)
go c.worker()
return c, nil
}
func (c *DTLSConn) AVClientStart(timeout time.Duration) error {
randomID := tutk.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 {
return fmt.Errorf("av login 1 failed: %w", err)
}
time.Sleep(10 * time.Millisecond)
if _, err := c.clientConn.Write(pkt2); err != nil {
return fmt.Errorf("av login 2 failed: %w", err)
}
// Wait for response
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case data, ok := <-c.rawCmd:
if !ok {
return io.EOF
}
if len(data) >= 32 && binary.LittleEndian.Uint16(data) == magicAVLoginResp {
c.hasTwoWayStreaming = data[31] == 1
ack := c.msgACK()
c.clientConn.Write(ack)
// Start ACK sender for continuous streaming
c.wg.Add(1)
go func() {
defer c.wg.Done()
ackTicker := time.NewTicker(100 * time.Millisecond)
defer ackTicker.Stop()
for {
select {
case <-c.ctx.Done():
return
case <-ackTicker.C:
if c.clientConn != nil {
ack := c.msgACK()
c.clientConn.Write(ack)
}
}
}
}()
return nil
}
case <-timer.C:
return context.DeadlineExceeded
}
}
}
func (c *DTLSConn) AVServStart() error {
conn, err := NewDTLSServer(c.ctx, iotcChannelBack, c.addr, c.WriteDTLS, c.serverBuf, c.psk)
if err != nil {
return fmt.Errorf("dtls: server handshake failed: %w", err)
}
if c.verbose {
fmt.Printf("[DTLS] Server handshake complete on channel %d\n", iotcChannelBack)
fmt.Printf("[SERVER] Waiting for AV Login request from camera...\n")
}
// Wait for AV Login request from camera
buf := make([]byte, 1024)
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
n, err := conn.Read(buf)
if err != nil {
go conn.Close()
return fmt.Errorf("read av login: %w", err)
}
if c.verbose {
fmt.Printf("[SERVER] AV Login request len=%d data:\n%s", n, hexDump(buf[:n]))
}
if n < 24 {
go conn.Close()
return fmt.Errorf("av login too short: %d bytes", n)
}
checksum := binary.LittleEndian.Uint32(buf[20:])
resp := c.msgAVLoginResponse(checksum)
if c.verbose {
fmt.Printf("[SERVER] Sending AV Login response: %d bytes\n", len(resp))
}
if _, err = conn.Write(resp); err != nil {
go conn.Close()
return fmt.Errorf("write av login response: %w", err)
}
if c.verbose {
fmt.Printf("[SERVER] AV Login response sent, waiting for possible resend...\n")
}
// Camera may resend, respond again
conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
if n, _ = conn.Read(buf); n > 0 {
if c.verbose {
fmt.Printf("[SERVER] Received AV Login resend: %d bytes\n", n)
}
conn.Write(resp)
}
conn.SetReadDeadline(time.Time{})
if c.verbose {
fmt.Printf("[SERVER] AV Login complete, ready for two way streaming\n")
}
c.mu.Lock()
c.serverConn = conn
c.mu.Unlock()
return nil
}
func (c *DTLSConn) AVServStop() error {
c.mu.Lock()
serverConn := c.serverConn
c.serverConn = nil
// Reset audio TX state
c.audioSeq = 0
c.audioFrameNo = 0
c.mu.Unlock()
if serverConn == nil {
return nil
}
go serverConn.Close()
return nil
}
func (c *DTLSConn) AVRecvFrameData() (*tutk.Packet, error) {
select {
case pkt, ok := <-c.frames.Recv():
if !ok {
return nil, c.Error()
}
return pkt, nil
case <-c.ctx.Done():
return nil, c.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 {
c.mu.Unlock()
return fmt.Errorf("av server not ready")
}
frame := c.msgAudioFrame(payload, timestampUS, codec, sampleRate, channels)
c.mu.Unlock()
n, err := conn.Write(frame)
if c.verbose {
if err != nil {
fmt.Printf("[SERVER TX] DTLS Write ERROR: %v\n", err)
} else {
fmt.Printf("[SERVER TX] len=%d, data:\n%s", n, hexDump(frame))
}
}
return err
}
func (c *DTLSConn) Write(data []byte) error {
if c.isCC51 {
_, err := c.conn.WriteToUDP(data, c.addr)
return err
}
_, err := c.conn.WriteToUDP(tutk.TransCodeBlob(data), c.addr)
return err
}
func (c *DTLSConn) WriteDTLS(payload []byte, channel byte) error {
var frame []byte
if c.isCC51 {
frame = c.msgTxDataCC51(payload, channel)
} else {
frame = c.msgTxData(payload, channel)
}
return c.Write(frame)
}
func (c *DTLSConn) WriteIOCtrl(payload []byte) error {
_, err := c.conn.Write(c.msgIOCtrl(payload))
return err
}
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 {
t.Reset(time.Second)
}
})
defer t.Stop()
_ = c.conn.SetDeadline(time.Now().Add(5 * time.Second))
defer c.conn.SetDeadline(time.Time{})
buf := make([]byte, 2048)
for {
n, addr, err := c.conn.ReadFromUDP(buf)
if err != nil {
return nil, err
}
if string(addr.IP) != string(c.addr.IP) || n < 16 {
continue
}
var res []byte
if c.isCC51 {
res = buf[:n]
} else {
res = tutk.ReverseTransCodeBlob(buf[:n])
}
if ok(res) {
c.addr.Port = addr.Port
return res, nil
}
}
}
func (c *DTLSConn) WriteAndWaitIOCtrl(payload []byte, match func([]byte) bool, timeout time.Duration) ([]byte, error) {
frame := c.msgIOCtrl(payload)
var t *time.Timer
t = time.AfterFunc(1, func() {
c.mu.RLock()
conn := c.clientConn
c.mu.RUnlock()
if conn != nil {
if _, err := conn.Write(frame); err == nil && t != nil {
t.Reset(time.Second)
}
}
})
defer t.Stop()
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case data, ok := <-c.rawCmd:
if !ok {
return nil, io.EOF
}
ack := c.msgACK()
c.clientConn.Write(ack)
if match(data) {
return data, nil
}
case <-timer.C:
return nil, fmt.Errorf("timeout waiting for response")
}
}
}
func (c *DTLSConn) HasTwoWayStreaming() bool {
return c.hasTwoWayStreaming
}
func (c *DTLSConn) IsBackchannelReady() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.serverConn != nil
}
func (c *DTLSConn) RemoteAddr() *net.UDPAddr {
return c.addr
}
func (c *DTLSConn) LocalAddr() *net.UDPAddr {
return c.conn.LocalAddr().(*net.UDPAddr)
}
func (c *DTLSConn) SetDeadline(t time.Time) error {
return c.conn.SetDeadline(t)
}
func (c *DTLSConn) Close() error {
c.cancel()
c.mu.Lock()
if conn := c.serverConn; conn != nil {
c.serverConn = nil
go conn.Close()
}
if conn := c.clientConn; conn != nil {
c.clientConn = nil
go conn.Close()
}
if c.frames != nil {
c.frames.Close()
}
c.mu.Unlock()
c.wg.Wait()
return c.conn.Close()
}
func (c *DTLSConn) Error() error {
if c.err != nil {
return c.err
}
return io.EOF
}
func (c *DTLSConn) discovery() error {
c.sid = tutk.GenSessionID()
pktIOTC := tutk.TransCodeBlob(c.msgDisco(1))
pktCC51 := c.msgDiscoCC51(0, 0, false)
buf := make([]byte, 2048)
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
c.conn.WriteToUDP(pktIOTC, c.addr)
c.conn.WriteToUDP(pktCC51, c.addr)
c.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
n, addr, err := c.conn.ReadFromUDP(buf)
if err != nil {
continue
}
if !addr.IP.Equal(c.addr.IP) {
continue
}
// 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.discoDoneCC51()
}
continue
}
// IOTC Protocol (Basis)
data := tutk.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 *DTLSConn) discoDone() error {
c.Write(c.msgDisco(2))
time.Sleep(100 * time.Millisecond)
_, err := c.WriteAndWait(c.msgSession(), func(res []byte) bool {
return len(res) >= 16 && binary.LittleEndian.Uint16(res[8:]) == cmdSessionRes
})
return err
}
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 == cmdDiscoCC51 && dir == 0xFFFF && seq == 3
})
return err
}
func (c *DTLSConn) connect() error {
conn, err := NewDTLSClient(c.ctx, iotcChannelMain, c.addr, c.WriteDTLS, c.clientBuf, c.psk)
if err != nil {
return fmt.Errorf("dtls: client handshake failed: %w", err)
}
c.mu.Lock()
c.clientConn = conn
c.mu.Unlock()
if c.verbose {
fmt.Printf("[DTLS] Client handshake complete on channel %d\n", iotcChannelMain)
}
return nil
}
func (c *DTLSConn) worker() {
defer c.wg.Done()
buf := make([]byte, 2048)
for {
select {
case <-c.ctx.Done():
return
default:
}
n, err := c.clientConn.Read(buf)
if err != nil {
c.err = err
return
}
if n < 2 {
continue
}
data := buf[:n]
magic := binary.LittleEndian.Uint16(data)
if c.verbose {
fmt.Printf("[DTLS RX] magic=0x%04x len=%d\n", magic, n)
}
switch magic {
case magicAVLoginResp:
c.queue(c.rawCmd, data)
case magicIOCtrl, magicChannelMsg:
c.queue(c.rawCmd, data)
case protoVersion:
// Seq-Tracking
if len(data) >= 8 {
seq := binary.LittleEndian.Uint16(data[4:])
if !c.rxSeqInit {
c.rxSeqInit = true
}
if seq > c.rxSeqEnd || c.rxSeqEnd == 0xffff {
c.rxSeqEnd = seq
}
}
c.queue(c.rawCmd, data)
case magicACK:
c.mu.RLock()
ack := c.cmdAck
c.mu.RUnlock()
if ack != nil {
ack()
}
default:
channel := data[0]
if channel == tutk.ChannelAudio || channel == tutk.ChannelIVideo || channel == tutk.ChannelPVideo {
c.frames.Handle(data)
}
}
}
}
func (c *DTLSConn) reader() {
defer c.wg.Done()
buf := make([]byte, 2048)
for {
select {
case <-c.ctx.Done():
return
default:
}
c.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
n, addr, err := c.conn.ReadFromUDP(buf)
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
continue
}
return
}
if !addr.IP.Equal(c.addr.IP) {
if c.verbose {
fmt.Printf("Ignored packet from unknown IP: %s\n", addr.IP.String())
}
continue
}
if addr.Port != c.addr.Port {
c.addr.Port = addr.Port
}
// CC51 Protocol
if c.isCC51 && n >= 12 && string(buf[:2]) == magicCC51 {
cmd := binary.LittleEndian.Uint16(buf[4:])
switch cmd {
case cmdKeepaliveCC51:
if n >= keepaliveSizeCC51 {
_ = c.Write(c.msgKeepaliveCC51())
}
case cmdDTLSCC51:
if n >= headerSizeCC51+authSizeCC51 {
ch := byte(binary.LittleEndian.Uint16(buf[12:]) >> 8)
dtlsData := buf[headerSizeCC51 : n-authSizeCC51]
switch ch {
case iotcChannelMain:
c.queue(c.clientBuf, dtlsData)
case iotcChannelBack:
c.queue(c.serverBuf, dtlsData)
}
}
}
continue
}
// IOTC Protocol (Basis)
data := tutk.ReverseTransCodeBlob(buf[:n])
if len(data) < 16 {
continue
}
switch binary.LittleEndian.Uint16(data[8:]) {
case cmdKeepaliveRes:
if len(data) > 24 {
_ = c.Write(c.msgKeepalive(data[16:]))
}
case cmdDataRX:
if len(data) > 28 {
ch := data[14]
switch ch {
case iotcChannelMain:
c.queue(c.clientBuf, data[28:])
case iotcChannelBack:
c.queue(c.serverBuf, data[28:])
}
}
}
}
}
func (c *DTLSConn) queue(ch chan []byte, data []byte) {
b := make([]byte, len(data))
copy(b, data)
select {
case ch <- b:
default:
select {
case <-ch:
default:
}
ch <- b
}
}
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)
}
return b
}
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: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 *DTLSConn) msgKeepaliveCC51() []byte {
c.kaSeq += 2
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 *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 *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[16:], uint16(size-24)) // payload size
binary.LittleEndian.PutUint16(b[18:], flags)
copy(b[20:], randomID[:4])
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
return b
}
func (c *DTLSConn) msgAVLoginResponse(checksum uint32) []byte {
b := make([]byte, 60)
binary.LittleEndian.PutUint16(b, 0x2100) // magic
binary.LittleEndian.PutUint16(b[2:], 0x000c) // version
b[4] = 0x10 // success
binary.LittleEndian.PutUint32(b[16:], 0x24) // payload size
binary.LittleEndian.PutUint32(b[20:], checksum) // echo checksum
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.PutUint16(b[54:], 0x0003) // channel info
binary.LittleEndian.PutUint16(b[56:], 0x0002)
return b
}
func (c *DTLSConn) msgAudioFrame(payload []byte, timestampUS uint32, codec byte, sampleRate uint32, channels uint8) []byte {
c.audioSeq++
c.audioFrameNo++
prevFrame := uint32(0)
if c.audioFrameNo > 1 {
prevFrame = c.audioFrameNo - 1
}
totalPayload := len(payload) + 16 // payload + frameinfo
b := make([]byte, 36+totalPayload)
// Outer header (36 bytes)
b[0] = tutk.ChannelAudio // 0x03
b[1] = tutk.FrameTypeStartAlt // 0x09
binary.LittleEndian.PutUint16(b[2:], protoVersion)
binary.LittleEndian.PutUint32(b[4:], c.audioSeq)
binary.LittleEndian.PutUint32(b[8:], timestampUS)
if c.audioFrameNo == 1 {
binary.LittleEndian.PutUint32(b[12:], 0x00000001)
} else {
binary.LittleEndian.PutUint32(b[12:], 0x00100001)
}
// Inner header
b[16] = tutk.ChannelAudio
b[17] = tutk.FrameTypeEndSingle
binary.LittleEndian.PutUint16(b[18:], uint16(prevFrame))
binary.LittleEndian.PutUint16(b[20:], 0x0001) // pkt_total
binary.LittleEndian.PutUint16(b[22:], 0x0010) // flags
binary.LittleEndian.PutUint32(b[24:], uint32(totalPayload))
binary.LittleEndian.PutUint32(b[28:], prevFrame)
binary.LittleEndian.PutUint32(b[32:], c.audioFrameNo)
copy(b[36:], payload) // Payload + FrameInfo
fi := b[36+len(payload):]
fi[0] = codec // Codec ID (low byte)
fi[1] = 0 // Codec ID (high byte, unused)
// Audio flags: [3:2]=sampleRateIdx [1]=16bit [0]=stereo
srIdx := tutk.GetSampleRateIndex(sampleRate)
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)*tutk.GetSamplesPerFrame(codec)*1000/sampleRate)
return b
}
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[10:], 0x0021) // flags
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.sid[:8]) // rid
copy(b[28:], payload)
return b
}
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[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 *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.PutUint32(b[4:], c.avSeq) // TX seq
c.avSeq++
binary.LittleEndian.PutUint16(b[8:], c.rxSeqStart) // RX start (last acked)
binary.LittleEndian.PutUint16(b[10:], c.rxSeqEnd) // RX end (highest received)
if c.rxSeqInit {
c.rxSeqStart = c.rxSeqEnd
}
binary.LittleEndian.PutUint16(b[12:], c.ackFlags) // AckFlags
binary.LittleEndian.PutUint32(b[16:], uint32(c.ackFlags)<<16) // AckCounter
ts := uint32(time.Now().UnixMilli() & 0xFFFF)
binary.LittleEndian.PutUint16(b[20:], uint16(ts)) // Timestamp
return b
}
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[10:], 0x0021) // flags
if len(incoming) >= 8 {
copy(b[16:], incoming[:8]) // echo payload
}
return b
}
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.PutUint32(b[4:], c.avSeq) // av seq
c.avSeq++
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
binary.LittleEndian.PutUint32(b[28:], uint32(c.seqCmd)) // flag
b[37] = 0x01
copy(b[40:], payload)
c.seqCmd++
return b
}
func hexDump(data []byte) string {
const maxBytes = 650
totalLen := len(data)
truncated := totalLen > maxBytes
if truncated {
data = data[:maxBytes]
}
var result string
for i := 0; i < len(data); i += 16 {
end := min(i+16, len(data))
line := fmt.Sprintf(" %04x:", i)
for j := i; j < end; j++ {
line += fmt.Sprintf(" %02x", data[j])
}
result += line + "\n"
}
if truncated {
result += fmt.Sprintf(" ... (truncated, showing %d of %d bytes)\n", maxBytes, totalLen)
}
return result
}
+146
View File
@@ -0,0 +1,146 @@
package dtls
import (
"context"
"net"
"sync"
"time"
"github.com/pion/dtls/v3"
)
func NewDTLSClient(ctx context.Context, channel uint8, addr net.Addr, writeFn func([]byte, uint8) error, readChan chan []byte, psk []byte) (*dtls.Conn, error) {
return dialDTLS(ctx, channel, addr, writeFn, readChan, psk, false)
}
func NewDTLSServer(ctx context.Context, channel uint8, addr net.Addr, writeFn func([]byte, uint8) error, readChan chan []byte, psk []byte) (*dtls.Conn, error) {
return dialDTLS(ctx, channel, addr, writeFn, readChan, psk, true)
}
func dialDTLS(ctx context.Context, channel uint8, addr net.Addr, writeFn func([]byte, uint8) error, readChan chan []byte, psk []byte, isServer bool) (*dtls.Conn, error) {
adapter := &channelAdapter{
ctx: ctx,
channel: channel,
addr: addr,
writeFn: writeFn,
readChan: readChan,
}
var conn *dtls.Conn
var err error
if isServer {
conn, err = dtls.Server(adapter, addr, buildDTLSConfig(psk, true))
} else {
conn, err = dtls.Client(adapter, addr, buildDTLSConfig(psk, false))
}
if err != nil {
return nil, err
}
timeout := 5 * time.Second
adapter.SetReadDeadline(time.Now().Add(timeout))
hsCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
if err := conn.HandshakeContext(hsCtx); err != nil {
go conn.Close()
return nil, err
}
adapter.SetReadDeadline(time.Time{})
return conn, nil
}
func buildDTLSConfig(psk []byte, isServer bool) *dtls.Config {
config := &dtls.Config{
PSK: func(hint []byte) ([]byte, error) {
return psk, nil
},
PSKIdentityHint: []byte("AUTHPWD_admin"),
InsecureSkipVerify: true,
InsecureSkipVerifyHello: true,
MTU: 1200,
FlightInterval: 300 * time.Millisecond,
ExtendedMasterSecret: dtls.DisableExtendedMasterSecret,
}
if isServer {
config.CipherSuites = []dtls.CipherSuiteID{dtls.TLS_PSK_WITH_AES_128_CBC_SHA256}
} else {
config.CustomCipherSuites = CustomCipherSuites
}
return config
}
type channelAdapter struct {
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) {
a.mu.Lock()
deadline := a.readDeadline
a.mu.Unlock()
if !deadline.IsZero() {
timeout := time.Until(deadline)
if timeout <= 0 {
return 0, nil, &timeoutError{}
}
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case data := <-a.readChan:
return copy(p, data), a.addr, nil
case <-timer.C:
return 0, nil, &timeoutError{}
case <-a.ctx.Done():
return 0, nil, net.ErrClosed
}
}
select {
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.writeFn(p, a.channel); err != nil {
return 0, err
}
return len(p), nil
}
func (a *channelAdapter) Close() error { return nil }
func (a *channelAdapter) LocalAddr() net.Addr { return &net.UDPAddr{} }
func (a *channelAdapter) SetDeadline(t time.Time) error {
a.mu.Lock()
a.readDeadline = t
a.mu.Unlock()
return nil
}
func (a *channelAdapter) SetReadDeadline(t time.Time) error {
a.mu.Lock()
a.readDeadline = t
a.mu.Unlock()
return nil
}
func (a *channelAdapter) SetWriteDeadline(time.Time) error { return nil }
type timeoutError struct{}
func (e *timeoutError) Error() string { return "i/o timeout" }
func (e *timeoutError) Timeout() bool { return true }
func (e *timeoutError) Temporary() bool { return true }
+571
View File
@@ -0,0 +1,571 @@
package tutk
import (
"encoding/binary"
"encoding/hex"
"fmt"
"sync"
"github.com/AlexxIT/go2rtc/pkg/aac"
)
const (
FrameTypeStart uint8 = 0x08 // Extended start (36-byte header)
FrameTypeStartAlt uint8 = 0x09 // StartAlt (36-byte header)
FrameTypeCont uint8 = 0x00 // Continuation (28-byte header)
FrameTypeContAlt uint8 = 0x04 // Continuation alt
FrameTypeEndSingle uint8 = 0x01 // Single-packet frame (28-byte)
FrameTypeEndMulti uint8 = 0x05 // Multi-packet end (28-byte)
FrameTypeEndExt uint8 = 0x0d // Extended end (36-byte)
)
const (
ChannelIVideo uint8 = 0x05
ChannelAudio uint8 = 0x03
ChannelPVideo uint8 = 0x07
)
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)
//
// Offset Size Field
// 0-1 2 CodecID - 0x4E=H264, 0x7B=H265, 0x90=AAC_WYZE
// 2 1 Flags - Video: 1=Keyframe, 0=P-frame | Audio: sample rate/bits/channels
// 3 1 CamIndex - Camera index
// 4 1 OnlineNum - Online number
// 5 1 FPS - Framerate (e.g. 20)
// 6 1 ResTier - Video: 1=Low(360P), 4=High(HD/2K) | Audio: 0
// 7 1 Bitrate - Video: 30=360P, 100=HD, 200=2K | Audio: 1
// 8-11 4 Timestamp - Timestamp (increases ~50000/frame for 20fps video)
// 12-15 4 SessionID - Session marker (constant per stream)
// 16-19 4 PayloadSize - Frame payload size in bytes
// 20-23 4 FrameNo - Global frame number
// 24-35 12 DeviceID - MAC address (ASCII) - video only
// 36-39 4 Padding - Always 0 - video only
type FrameInfo struct {
CodecID byte // 0 (only low byte used)
Flags uint8 // 2
CamIndex uint8 // 3
OnlineNum uint8 // 4
FPS uint8 // 5: Framerate
ResTier uint8 // 6: Resolution tier (1=Low, 4=High)
Bitrate uint8 // 7: Bitrate index (30=360P, 100=HD, 200=2K)
Timestamp uint32 // 8-11: Timestamp
SessionID uint32 // 12-15: Session marker (constant)
PayloadSize uint32 // 16-19: Payload size
FrameNo uint32 // 20-23: Frame number
}
func (fi *FrameInfo) IsKeyframe() bool {
return fi.Flags == 0x01
}
func (fi *FrameInfo) SampleRate() uint32 {
idx := (fi.Flags >> 2) & 0x0F
if idx < uint8(len(sampleRates)) {
return sampleRates[idx]
}
return 16000
}
func (fi *FrameInfo) Channels() uint8 {
if fi.Flags&0x01 == 1 {
return 2
}
return 1
}
func ParseFrameInfo(data []byte) *FrameInfo {
if len(data) < frameInfoSize {
return nil
}
offset := len(data) - frameInfoSize
fi := data[offset:]
return &FrameInfo{
CodecID: fi[0],
Flags: fi[2],
CamIndex: fi[3],
OnlineNum: fi[4],
FPS: fi[5],
ResTier: fi[6],
Bitrate: fi[7],
Timestamp: binary.LittleEndian.Uint32(fi[8:]),
SessionID: binary.LittleEndian.Uint32(fi[12:]),
PayloadSize: binary.LittleEndian.Uint32(fi[16:]),
FrameNo: binary.LittleEndian.Uint32(fi[20:]),
}
}
type Packet struct {
Channel uint8
Codec byte
Timestamp uint32
Payload []byte
IsKeyframe bool
FrameNo uint32
SampleRate uint32
Channels uint8
}
type PacketHeader struct {
Channel byte
FrameType byte
HeaderSize int
FrameNo uint32
PktIdx uint16
PktTotal uint16
PayloadSize uint16
HasFrameInfo bool
}
func ParsePacketHeader(data []byte) *PacketHeader {
if len(data) < 28 {
return nil
}
frameType := data[1]
hdr := &PacketHeader{
Channel: data[0],
FrameType: frameType,
}
switch frameType {
case FrameTypeStart, FrameTypeStartAlt, FrameTypeEndExt:
hdr.HeaderSize = 36
default:
hdr.HeaderSize = 28
}
if len(data) < hdr.HeaderSize {
return nil
}
if hdr.HeaderSize == 28 {
hdr.PktTotal = binary.LittleEndian.Uint16(data[12:])
pktIdxOrMarker := binary.LittleEndian.Uint16(data[14:])
hdr.PayloadSize = binary.LittleEndian.Uint16(data[16:])
hdr.FrameNo = binary.LittleEndian.Uint32(data[24:])
if pktIdxOrMarker == 0x0028 && (IsEndFrame(frameType) || hdr.PktTotal == 1) {
hdr.HasFrameInfo = true
if hdr.PktTotal > 0 {
hdr.PktIdx = hdr.PktTotal - 1
}
} else {
hdr.PktIdx = pktIdxOrMarker
}
} else {
hdr.PktTotal = binary.LittleEndian.Uint16(data[20:])
pktIdxOrMarker := binary.LittleEndian.Uint16(data[22:])
hdr.PayloadSize = binary.LittleEndian.Uint16(data[24:])
hdr.FrameNo = binary.LittleEndian.Uint32(data[32:])
if pktIdxOrMarker == 0x0028 && (IsEndFrame(frameType) || hdr.PktTotal == 1) {
hdr.HasFrameInfo = true
if hdr.PktTotal > 0 {
hdr.PktIdx = hdr.PktTotal - 1
}
} else {
hdr.PktIdx = pktIdxOrMarker
}
}
return hdr
}
func IsStartFrame(frameType uint8) bool {
return frameType == FrameTypeStart || frameType == FrameTypeStartAlt
}
func IsEndFrame(frameType uint8) bool {
return frameType == FrameTypeEndSingle ||
frameType == FrameTypeEndMulti ||
frameType == FrameTypeEndExt
}
func IsContinuationFrame(frameType uint8) bool {
return frameType == FrameTypeCont || frameType == FrameTypeContAlt
}
type channelState struct {
frameNo uint32 // current frame being assembled
pktTotal uint16 // expected total packets
waitSeq uint16 // next expected packet index (0, 1, 2, ...)
waitData []byte // accumulated payload data
frameInfo *FrameInfo // frame info (from end packet)
hasStarted bool // received first packet of frame
lastPktIdx uint16 // last received packet index (for OOO detection)
}
func (cs *channelState) reset() {
cs.frameNo = 0
cs.pktTotal = 0
cs.waitSeq = 0
cs.waitData = cs.waitData[:0]
cs.frameInfo = nil
cs.hasStarted = false
cs.lastPktIdx = 0
}
const tsWrapPeriod uint32 = 1000000
type tsTracker struct {
lastRawTS uint32
accumUS uint64
firstTS bool
}
func (t *tsTracker) update(rawTS uint32) uint64 {
if !t.firstTS {
t.firstTS = true
t.lastRawTS = rawTS
return 0
}
var delta uint32
if rawTS >= t.lastRawTS {
delta = rawTS - t.lastRawTS
} else {
// Wrapped: delta = (wrap - last) + new
delta = (tsWrapPeriod - t.lastRawTS) + rawTS
}
t.accumUS += uint64(delta)
t.lastRawTS = rawTS
return t.accumUS
}
type FrameHandler struct {
channels map[byte]*channelState
videoTS tsTracker
audioTS tsTracker
output chan *Packet
verbose bool
closed bool
closeMu sync.Mutex
}
func NewFrameHandler(verbose bool) *FrameHandler {
return &FrameHandler{
channels: make(map[byte]*channelState),
output: make(chan *Packet, 128),
verbose: verbose,
}
}
func (h *FrameHandler) Recv() <-chan *Packet {
return h.output
}
func (h *FrameHandler) Close() {
h.closeMu.Lock()
defer h.closeMu.Unlock()
if h.closed {
return
}
h.closed = true
close(h.output)
}
func (h *FrameHandler) Handle(data []byte) {
hdr := ParsePacketHeader(data)
if hdr == nil {
return
}
payload, fi := h.extractPayload(data, hdr.Channel)
if payload == nil {
return
}
if h.verbose {
fiStr := ""
if hdr.HasFrameInfo {
fiStr = " +FI"
}
fmt.Printf("[RX] ch=0x%02x type=0x%02x #%d pkt=%d/%d data=%dB%s\n",
hdr.Channel, hdr.FrameType,
hdr.FrameNo, hdr.PktIdx, hdr.PktTotal, len(payload), fiStr)
}
switch hdr.Channel {
case ChannelAudio:
h.handleAudio(payload, fi)
case ChannelIVideo, ChannelPVideo:
h.handleVideo(hdr.Channel, hdr, payload, fi)
}
}
func (h *FrameHandler) extractPayload(data []byte, channel byte) ([]byte, *FrameInfo) {
if len(data) < 2 {
return nil, nil
}
frameType := data[1]
headerSize := 28
fiSize := 0
switch frameType {
case FrameTypeStart:
headerSize = 36
case FrameTypeStartAlt:
headerSize = 36
if len(data) >= 22 {
pktTotal := binary.LittleEndian.Uint16(data[20:])
if pktTotal == 1 {
fiSize = frameInfoSize
}
}
case FrameTypeCont, FrameTypeContAlt:
headerSize = 28
case FrameTypeEndSingle, FrameTypeEndMulti:
headerSize = 28
fiSize = frameInfoSize
case FrameTypeEndExt:
headerSize = 36
fiSize = frameInfoSize
default:
headerSize = 28
}
if len(data) < headerSize {
return nil, nil
}
if fiSize == 0 {
return data[headerSize:], nil
}
if len(data) < headerSize+fiSize {
return data[headerSize:], nil
}
fi := ParseFrameInfo(data)
validCodec := false
switch channel {
case ChannelIVideo, ChannelPVideo:
validCodec = IsVideoCodec(fi.CodecID)
case ChannelAudio:
validCodec = IsAudioCodec(fi.CodecID)
}
if validCodec {
payload := data[headerSize : len(data)-fiSize]
return payload, fi
}
return data[headerSize:], nil
}
func (h *FrameHandler) handleVideo(channel byte, hdr *PacketHeader, payload []byte, fi *FrameInfo) {
cs := h.channels[channel]
if cs == nil {
cs = &channelState{}
h.channels[channel] = cs
}
// New frame number - reset and start fresh
if hdr.FrameNo != cs.frameNo {
// Check if previous frame was incomplete
if cs.hasStarted && cs.waitSeq < cs.pktTotal {
fmt.Printf("[DROP] ch=0x%02x #%d INCOMPLETE: got %d/%d pkts\n",
channel, cs.frameNo, cs.waitSeq, cs.pktTotal)
}
cs.reset()
cs.frameNo = hdr.FrameNo
cs.pktTotal = hdr.PktTotal
}
// 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)
cs.reset()
return
}
// First packet - mark as started
if cs.waitSeq == 0 {
cs.hasStarted = true
}
cs.waitData = append(cs.waitData, payload...)
cs.waitSeq++
// Store frame info if present
if fi != nil {
cs.frameInfo = fi
}
// Check if frame is complete
if cs.waitSeq != cs.pktTotal || cs.frameInfo == nil {
return
}
fi = cs.frameInfo
defer cs.reset()
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))
return
}
if len(cs.waitData) == 0 {
return
}
accumUS := h.videoTS.update(fi.Timestamp)
rtpTS := uint32(accumUS * 90000 / 1000000)
pkt := &Packet{
Channel: channel,
Payload: append([]byte{}, cs.waitData...),
Codec: fi.CodecID,
Timestamp: rtpTS,
IsKeyframe: fi.IsKeyframe(),
FrameNo: fi.FrameNo,
}
if h.verbose {
frameType := "P"
if fi.IsKeyframe() {
frameType = "KEY"
}
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",
fi.SessionID, fi.PayloadSize, fi.FrameNo)
fmt.Printf(" rtp_ts=%d accum_us=%d\n", rtpTS, accumUS)
fmt.Printf(" hex: %s\n", dumpHex(fi))
}
h.queue(pkt)
}
func (h *FrameHandler) handleAudio(payload []byte, fi *FrameInfo) {
if len(payload) == 0 || fi == nil {
return
}
var sampleRate uint32
var channels uint8
switch fi.CodecID {
case CodecAACRaw, CodecAACADTS, CodecAACLATM, CodecAACAlt:
sampleRate, channels = parseAudioParams(payload, fi)
default:
sampleRate = fi.SampleRate()
channels = fi.Channels()
}
accumUS := h.audioTS.update(fi.Timestamp)
rtpTS := uint32(accumUS * uint64(sampleRate) / 1000000)
payloadCopy := make([]byte, len(payload))
copy(payloadCopy, payload)
pkt := &Packet{
Channel: ChannelAudio,
Payload: payloadCopy,
Codec: fi.CodecID,
Timestamp: rtpTS,
SampleRate: sampleRate,
Channels: channels,
FrameNo: fi.FrameNo,
}
if h.verbose {
bits := 8
if fi.Flags&0x02 != 0 {
bits = 16
}
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))
}
h.queue(pkt)
}
func (h *FrameHandler) queue(pkt *Packet) {
h.closeMu.Lock()
defer h.closeMu.Unlock()
if h.closed {
return
}
select {
case h.output <- pkt:
default:
// Queue full - drop oldest
select {
case <-h.output:
default:
}
select {
case h.output <- pkt:
default:
// Queue still full, drop this packet
}
}
}
func parseAudioParams(payload []byte, fi *FrameInfo) (sampleRate uint32, channels uint8) {
if aac.IsADTS(payload) {
codec := aac.ADTSToCodec(payload)
if codec != nil {
return codec.ClockRate, codec.Channels
}
}
if fi != nil {
return fi.SampleRate(), fi.Channels()
}
return 16000, 1
}
func dumpHex(fi *FrameInfo) string {
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
b[5] = fi.FPS
b[6] = fi.ResTier
b[7] = fi.Bitrate
binary.LittleEndian.PutUint32(b[8:], fi.Timestamp)
binary.LittleEndian.PutUint32(b[12:], fi.SessionID)
binary.LittleEndian.PutUint32(b[16:], fi.PayloadSize)
binary.LittleEndian.PutUint32(b[20:], fi.FrameNo)
// Bytes 24-39 are DeviceID and Padding (not stored in struct)
hexStr := hex.EncodeToString(b)
formatted := ""
for i := 0; i < len(hexStr); i += 2 {
if i > 0 {
formatted += " "
}
formatted += hexStr[i : i+2]
}
return formatted
}
+52 -9
View File
@@ -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,46 @@ 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
}
func FindHL(data []byte, offset int) []byte {
for i := offset; i+16 <= len(data); i++ {
if data[i] == 'H' && data[i+1] == 'L' {
return data[i:]
}
}
return nil
}
-6
View File
@@ -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
}
+55
View File
@@ -0,0 +1,55 @@
package wyze
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/tutk"
"github.com/pion/rtp"
)
func (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
if err := p.client.StartIntercom(); err != nil {
return fmt.Errorf("wyze: failed to enable intercom: %w", err)
}
// Get the camera's audio codec info (what it sent us = what it accepts)
tutkCodec, sampleRate, channels := p.client.GetBackchannelCodec()
if tutkCodec == 0 {
return fmt.Errorf("wyze: no audio codec detected from camera")
}
if p.client.verbose {
fmt.Printf("[Wyze] Intercom enabled, using codec=0x%04x rate=%d ch=%d\n", tutkCodec, sampleRate, channels)
}
sender := core.NewSender(media, track.Codec)
// Track our own timestamp - camera expects timestamps starting from 0
// and incrementing by frame duration in microseconds
var timestamp uint32 = 0
samplesPerFrame := tutk.GetSamplesPerFrame(tutkCodec)
frameDurationUS := samplesPerFrame * 1000000 / sampleRate
sender.Handler = func(pkt *rtp.Packet) {
if err := p.client.WriteAudio(tutkCodec, pkt.Payload, timestamp, sampleRate, channels); err == nil {
p.Send += len(pkt.Payload)
}
timestamp += frameDurationUS
}
switch track.Codec.Name {
case core.CodecAAC:
if track.Codec.IsRTP() {
sender.Handler = aac.RTPToADTS(codec, sender.Handler)
} else {
sender.Handler = aac.EncodeToADTS(codec, sender.Handler)
}
}
sender.HandleRTP(track)
p.Senders = append(p.Senders, sender)
return nil
}
+618
View File
@@ -0,0 +1,618 @@
package wyze
import (
"crypto/rand"
"encoding/binary"
"encoding/json"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/AlexxIT/go2rtc/pkg/tutk"
"github.com/AlexxIT/go2rtc/pkg/tutk/dtls"
)
const (
FrameSize1080P = 0
FrameSize360P = 1
FrameSize720P = 2
FrameSize2K = 3
FrameSizeFloodlight = 4
)
const (
BitrateMax uint16 = 0xF0
BitrateSD uint16 = 0x3C
)
const (
MediaTypeVideo = 1
MediaTypeAudio = 2
MediaTypeReturnAudio = 3
MediaTypeRDT = 4
)
const (
KCmdAuth = 10000
KCmdChallenge = 10001
KCmdChallengeResp = 10002
KCmdAuthResult = 10003
KCmdControlChannel = 10010
KCmdControlChannelResp = 10011
KCmdSetResolutionDB = 10052
KCmdSetResolutionDBRes = 10053
KCmdSetResolution = 10056
KCmdSetResolutionResp = 10057
)
type Client struct {
conn *dtls.DTLSConn
host string
uid string
enr string
mac string
model string
authKey string
verbose bool
closed bool
closeMu sync.Mutex
hasAudio bool
hasIntercom bool
audioCodecID byte
audioSampleRate uint32
audioChannels uint8
}
type AuthResponse struct {
ConnectionRes string `json:"connectionRes"`
CameraInfo map[string]any `json:"cameraInfo"`
}
func Dial(rawURL string) (*Client, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("wyze: invalid URL: %w", err)
}
query := u.Query()
if query.Get("dtls") != "true" {
return nil, fmt.Errorf("wyze: only DTLS cameras are supported")
}
c := &Client{
host: u.Host,
uid: query.Get("uid"),
enr: query.Get("enr"),
mac: query.Get("mac"),
model: query.Get("model"),
verbose: query.Get("verbose") == "true",
}
c.authKey = string(dtls.CalculateAuthKey(c.enr, c.mac))
if c.verbose {
fmt.Printf("[Wyze] Connecting to %s (UID: %s)\n", c.host, c.uid)
}
if err := c.connect(); err != nil {
c.Close()
return nil, err
}
if err := c.doAVLogin(); err != nil {
c.Close()
return nil, err
}
if err := c.doKAuth(); err != nil {
c.Close()
return nil, err
}
if c.verbose {
fmt.Printf("[Wyze] Connection established\n")
}
return c, nil
}
func (c *Client) SupportsAudio() bool {
return c.hasAudio
}
func (c *Client) SupportsIntercom() bool {
return c.hasIntercom
}
func (c *Client) SetBackchannelCodec(codecID byte, sampleRate uint32, channels uint8) {
c.audioCodecID = codecID
c.audioSampleRate = sampleRate
c.audioChannels = channels
}
func (c *Client) GetBackchannelCodec() (codecID byte, sampleRate uint32, channels uint8) {
return c.audioCodecID, c.audioSampleRate, c.audioChannels
}
func (c *Client) SetResolution(quality byte) error {
var frameSize uint8
var bitrate uint16
switch quality {
case 0: // Auto/HD - use model's best
frameSize = c.hdFrameSize()
bitrate = BitrateMax
case FrameSize360P: // 1 = SD/360P
frameSize = FrameSize360P
bitrate = BitrateSD
case FrameSize720P: // 2 = 720P
frameSize = FrameSize720P
bitrate = BitrateMax
case FrameSize2K: // 3 = 2K
if c.is2K() {
frameSize = FrameSize2K
} else {
frameSize = c.hdFrameSize()
}
bitrate = BitrateMax
case FrameSizeFloodlight: // 4 = Floodlight
frameSize = c.hdFrameSize()
bitrate = BitrateMax
default:
frameSize = quality
bitrate = BitrateMax
}
if c.verbose {
fmt.Printf("[Wyze] SetResolution: quality=%d frameSize=%d bitrate=%d model=%s\n", quality, frameSize, bitrate, c.model)
}
// Use K10052 (doorbell format) for certain models
if c.useDoorbellResolution() {
k10052 := c.buildK10052(frameSize, bitrate)
_, err := c.conn.WriteAndWaitIOCtrl(k10052, c.matchHL(KCmdSetResolutionDBRes), 5*time.Second)
return err
}
k10056 := c.buildK10056(frameSize, bitrate)
_, err := c.conn.WriteAndWaitIOCtrl(k10056, c.matchHL(KCmdSetResolutionResp), 5*time.Second)
return err
}
func (c *Client) StartVideo() error {
k10010 := c.buildK10010(MediaTypeVideo, true)
_, err := c.conn.WriteAndWaitIOCtrl(k10010, c.matchHL(KCmdControlChannelResp), 5*time.Second)
return err
}
func (c *Client) StartAudio() error {
k10010 := c.buildK10010(MediaTypeAudio, true)
_, err := c.conn.WriteAndWaitIOCtrl(k10010, c.matchHL(KCmdControlChannelResp), 5*time.Second)
return err
}
func (c *Client) StartIntercom() error {
if c.conn == nil {
return fmt.Errorf("connection is nil")
}
if c.conn.IsBackchannelReady() {
return nil
}
k10010 := c.buildK10010(MediaTypeReturnAudio, true)
if _, err := c.conn.WriteAndWaitIOCtrl(k10010, c.matchHL(KCmdControlChannelResp), 5*time.Second); err != nil {
return fmt.Errorf("enable return audio: %w", err)
}
if c.verbose {
fmt.Printf("[Wyze] Speaker channel enabled, waiting for readiness...\n")
}
return c.conn.AVServStart()
}
func (c *Client) StopIntercom() error {
if c.conn == nil || !c.conn.IsBackchannelReady() {
return nil
}
k10010 := c.buildK10010(MediaTypeReturnAudio, false)
c.conn.WriteIOCtrl(k10010)
return c.conn.AVServStop()
}
func (c *Client) ReadPacket() (*tutk.Packet, error) {
return c.conn.AVRecvFrameData()
}
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%02x, payload=%d bytes, rate=%d, ch=%d\n", codec, len(payload), sampleRate, channels)
}
return c.conn.AVSendAudioData(codec, payload, timestamp, sampleRate, channels)
}
func (c *Client) SetDeadline(t time.Time) error {
if c.conn != nil {
return c.conn.SetDeadline(t)
}
return nil
}
func (c *Client) Protocol() string {
return "wyze/dtls"
}
func (c *Client) RemoteAddr() net.Addr {
if c.conn != nil {
return c.conn.RemoteAddr()
}
return nil
}
func (c *Client) Close() error {
c.closeMu.Lock()
if c.closed {
c.closeMu.Unlock()
return nil
}
c.closed = true
c.closeMu.Unlock()
if c.verbose {
fmt.Printf("[Wyze] Closing connection\n")
}
c.StopIntercom()
if c.conn != nil {
c.conn.Close()
}
if c.verbose {
fmt.Printf("[Wyze] Connection closed\n")
}
return nil
}
func (c *Client) connect() error {
host := c.host
port := 0
if idx := strings.Index(host, ":"); idx > 0 {
if p, err := strconv.Atoi(host[idx+1:]); err == nil {
port = p
}
host = host[:idx]
}
conn, err := dtls.DialDTLS(host, port, c.uid, c.authKey, c.enr, c.verbose)
if err != nil {
return fmt.Errorf("wyze: connect failed: %w", err)
}
c.conn = conn
if c.verbose {
fmt.Printf("[Wyze] Connected to %s (IOTC + DTLS)\n", conn.RemoteAddr())
}
return nil
}
func (c *Client) doAVLogin() error {
if c.verbose {
fmt.Printf("[Wyze] Sending AV Login\n")
}
if err := c.conn.AVClientStart(5 * time.Second); err != nil {
return fmt.Errorf("wyze: av login failed: %w", err)
}
if c.verbose {
fmt.Printf("[Wyze] AV Login response received\n")
}
return nil
}
func (c *Client) doKAuth() error {
// Step 1: K10000 -> K10001 (Challenge)
data, err := c.conn.WriteAndWaitIOCtrl(c.buildK10000(), c.matchHL(KCmdChallenge), 5*time.Second)
if err != nil {
return fmt.Errorf("wyze: K10001 failed: %w", err)
}
hlData := c.extractHL(data)
challenge, status, err := c.parseK10001(hlData)
if err != nil {
return fmt.Errorf("wyze: K10001 parse failed: %w", err)
}
if c.verbose {
fmt.Printf("[Wyze] K10001 challenge received, status=%d\n", status)
}
// Step 2: K10002 -> K10003 (Auth)
data, err = c.conn.WriteAndWaitIOCtrl(c.buildK10002(challenge, status), c.matchHL(KCmdAuthResult), 5*time.Second)
if err != nil {
return fmt.Errorf("wyze: K10002 failed: %w", err)
}
hlData = c.extractHL(data)
// Parse K10003 response
authResp, err := c.parseK10003(hlData)
if err != nil {
return fmt.Errorf("wyze: K10003 parse failed: %w", err)
}
if c.verbose && authResp != nil {
if jsonBytes, err := json.MarshalIndent(authResp, "", " "); err == nil {
fmt.Printf("[Wyze] K10003 response:\n%s\n", jsonBytes)
}
}
// Extract audio capability from cameraInfo
if authResp != nil && authResp.CameraInfo != nil {
if channelResult, ok := authResp.CameraInfo["channelRequestResult"].(map[string]any); ok {
if audio, ok := channelResult["audio"].(string); ok {
c.hasAudio = audio == "1"
} else {
c.hasAudio = true
}
} else {
c.hasAudio = true
}
} else {
c.hasAudio = true
}
if c.verbose {
fmt.Printf("[Wyze] K10003 auth success\n")
}
c.hasIntercom = c.conn.HasTwoWayStreaming()
if c.verbose {
fmt.Printf("[Wyze] K-auth complete\n")
}
return nil
}
func (c *Client) buildK10000() []byte {
json := []byte(`{"cameraInfo":{"audioEncoderList":[137,138,140]}}`) // 137=PCMU, 138=PCMA, 140=PCM
b := make([]byte, 16+len(json))
copy(b, "HL") // magic
b[2] = 5 // version
binary.LittleEndian.PutUint16(b[4:], KCmdAuth) // 10000
binary.LittleEndian.PutUint16(b[6:], uint16(len(json))) // payload len
copy(b[16:], json)
return b
}
func (c *Client) buildK10002(challenge []byte, status byte) []byte {
resp := generateChallengeResponse(challenge, c.enr, status)
sessionID := make([]byte, 4)
rand.Read(sessionID)
b := make([]byte, 38)
copy(b, "HL") // magic
b[2] = 5 // version
binary.LittleEndian.PutUint16(b[4:], KCmdChallengeResp) // 10002
b[6] = 22 // payload len
copy(b[16:], resp[:16]) // challenge response
copy(b[32:], sessionID) // random session ID
b[36] = 1 // video enabled/disabled
b[37] = 1 // audio enabled/disabled
return b
}
func (c *Client) buildK10010(mediaType byte, enabled bool) []byte {
b := make([]byte, 18)
copy(b, "HL") // magic
b[2] = 5 // version
binary.LittleEndian.PutUint16(b[4:], KCmdControlChannel) // 10010
binary.LittleEndian.PutUint16(b[6:], 2) // payload len
b[16] = mediaType // 1=video, 2=audio, 3=return audio
b[17] = 1 // 1=enable, 2=disable
if !enabled {
b[17] = 2
}
return b
}
func (c *Client) buildK10052(frameSize uint8, bitrate uint16) []byte {
b := make([]byte, 22)
copy(b, "HL") // magic
b[2] = 5 // version
binary.LittleEndian.PutUint16(b[4:], KCmdSetResolutionDB) // 10052
binary.LittleEndian.PutUint16(b[6:], 6) // payload len
binary.LittleEndian.PutUint16(b[16:], bitrate) // bitrate (2 bytes)
b[18] = frameSize + 1 // frame size (1 byte)
// b[19] = fps, b[20:22] = zeros
return b
}
func (c *Client) buildK10056(frameSize uint8, bitrate uint16) []byte {
b := make([]byte, 21)
copy(b, "HL") // magic
b[2] = 5 // version
binary.LittleEndian.PutUint16(b[4:], KCmdSetResolution) // 10056
binary.LittleEndian.PutUint16(b[6:], 5) // payload len
b[16] = frameSize + 1 // frame size
binary.LittleEndian.PutUint16(b[17:], bitrate) // bitrate
// b[19:21] = FPS (0 = auto)
return b
}
func (c *Client) parseK10001(data []byte) (challenge []byte, status byte, err error) {
if c.verbose {
fmt.Printf("[Wyze] parseK10001: received %d bytes\n", len(data))
}
if len(data) < 33 {
return nil, 0, fmt.Errorf("data too short: %d bytes", len(data))
}
if data[0] != 'H' || data[1] != 'L' {
return nil, 0, fmt.Errorf("invalid HL magic: %x %x", data[0], data[1])
}
cmdID := binary.LittleEndian.Uint16(data[4:])
if cmdID != KCmdChallenge {
return nil, 0, fmt.Errorf("expected cmdID 10001, got %d", cmdID)
}
status = data[16]
challenge = make([]byte, 16)
copy(challenge, data[17:33])
return challenge, status, nil
}
func (c *Client) parseK10003(data []byte) (*AuthResponse, error) {
if c.verbose {
fmt.Printf("[Wyze] parseK10003: received %d bytes\n", len(data))
}
if len(data) < 16 {
return &AuthResponse{}, nil
}
if data[0] != 'H' || data[1] != 'L' {
return &AuthResponse{}, nil
}
cmdID := binary.LittleEndian.Uint16(data[4:])
textLen := binary.LittleEndian.Uint16(data[6:])
if cmdID != KCmdAuthResult {
return &AuthResponse{}, nil
}
if len(data) > 16 && textLen > 0 {
jsonData := data[16:]
for i := range jsonData {
if jsonData[i] == '{' {
var resp AuthResponse
if err := json.Unmarshal(jsonData[i:], &resp); err == nil {
if c.verbose {
fmt.Printf("[Wyze] parseK10003: parsed JSON\n")
}
return &resp, nil
}
break
}
}
}
return &AuthResponse{}, nil
}
func (c *Client) useDoorbellResolution() bool {
switch c.model {
case "WYZEDB3", "WVOD1", "HL_WCO2", "WYZEC1":
return true
}
return false
}
func (c *Client) hdFrameSize() uint8 {
if c.isFloodlight() {
return FrameSizeFloodlight
}
if c.is2K() {
return FrameSize2K
}
return FrameSize1080P
}
func (c *Client) is2K() bool {
switch c.model {
case "HL_CAM3P", "HL_PANP", "HL_CAM4", "HL_DB2", "HL_CFL2":
return true
}
return false
}
func (c *Client) isFloodlight() bool {
return c.model == "HL_CFL2"
}
func (c *Client) matchHL(expectCmd uint16) func([]byte) bool {
return func(data []byte) bool {
hlData := c.extractHL(data)
if hlData == nil {
return false
}
cmd, _, ok := tutk.ParseHL(hlData)
return ok && cmd == expectCmd
}
}
func (c *Client) extractHL(data []byte) []byte {
// Try offset 32 (magicIOCtrl, protoVersion)
if hlData := tutk.FindHL(data, 32); hlData != nil {
return hlData
}
// Try offset 36 (magicChannelMsg)
if len(data) >= 36 && data[16] == 0x00 {
return tutk.FindHL(data, 36)
}
return nil
}
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)
}
+337
View File
@@ -0,0 +1,337 @@
package wyze
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
)
const (
baseURLAuth = "https://auth-prod.api.wyze.com"
baseURLAPI = "https://api.wyzecam.com"
appName = "com.hualai.WyzeCam"
appVersion = "2.50.0"
)
type Cloud struct {
client *http.Client
apiKey string
keyID string
accessToken string
phoneID string
cameras []*Camera
}
type Camera struct {
MAC string `json:"mac"`
P2PID string `json:"p2p_id"`
ENR string `json:"enr"`
IP string `json:"ip"`
Nickname string `json:"nickname"`
ProductModel string `json:"product_model"`
ProductType string `json:"product_type"`
DTLS int `json:"dtls"`
FirmwareVer string `json:"firmware_ver"`
IsOnline bool `json:"is_online"`
}
type deviceListResponse struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data struct {
DeviceList []deviceInfo `json:"device_list"`
} `json:"data"`
}
type deviceInfo struct {
MAC string `json:"mac"`
ENR string `json:"enr"`
Nickname string `json:"nickname"`
ProductModel string `json:"product_model"`
ProductType string `json:"product_type"`
FirmwareVer string `json:"firmware_ver"`
ConnState int `json:"conn_state"`
DeviceParams deviceParams `json:"device_params"`
}
type deviceParams struct {
P2PID string `json:"p2p_id"`
P2PType int `json:"p2p_type"`
IP string `json:"ip"`
DTLS int `json:"dtls"`
}
type p2pInfoResponse struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data map[string]any `json:"data"`
}
type loginResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
UserID string `json:"user_id"`
MFAOptions []string `json:"mfa_options"`
SMSSessionID string `json:"sms_session_id"`
EmailSessionID string `json:"email_session_id"`
}
func NewCloud(apiKey, keyID string) *Cloud {
return &Cloud{
client: &http.Client{Timeout: 30 * time.Second},
phoneID: generatePhoneID(),
apiKey: apiKey,
keyID: keyID,
}
}
func (c *Cloud) Login(email, password string) error {
payload := map[string]string{
"email": strings.TrimSpace(email),
"password": hashPassword(password),
}
jsonData, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", baseURLAuth+"/api/user/login", strings.NewReader(string(jsonData)))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Apikey", c.apiKey)
req.Header.Set("Keyid", c.keyID)
req.Header.Set("User-Agent", "go2rtc")
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var errResp apiError
_ = json.Unmarshal(body, &errResp)
if errResp.hasError() {
return fmt.Errorf("wyze: login failed (code %s): %s", errResp.code(), errResp.message())
}
var result loginResponse
if err := json.Unmarshal(body, &result); err != nil {
return fmt.Errorf("wyze: failed to parse login response: %w", err)
}
if len(result.MFAOptions) > 0 {
return &AuthError{
Message: "MFA required",
NeedsMFA: true,
MFAType: strings.Join(result.MFAOptions, ","),
}
}
if result.AccessToken == "" {
return errors.New("wyze: no access token in response")
}
c.accessToken = result.AccessToken
return nil
}
func (c *Cloud) GetCameraList() ([]*Camera, error) {
payload := map[string]any{
"access_token": c.accessToken,
"phone_id": c.phoneID,
"app_name": appName,
"app_ver": appName + "___" + appVersion,
"app_version": appVersion,
"phone_system_type": 1,
"sc": "9f275790cab94a72bd206c8876429f3c",
"sv": "9d74946e652647e9b6c9d59326aef104",
"ts": time.Now().UnixMilli(),
}
jsonData, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", baseURLAPI+"/app/v2/home_page/get_object_list", strings.NewReader(string(jsonData)))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result deviceListResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("wyze: failed to parse device list: %w", err)
}
if result.Code != "1" {
return nil, fmt.Errorf("wyze: API error: %s - %s", result.Code, result.Msg)
}
c.cameras = nil
for _, dev := range result.Data.DeviceList {
if dev.ProductType != "Camera" {
continue
}
if dev.DeviceParams.IP == "" {
continue // skip cameras without IP (gwell protocol)
}
c.cameras = append(c.cameras, &Camera{
MAC: dev.MAC,
P2PID: dev.DeviceParams.P2PID,
ENR: dev.ENR,
IP: dev.DeviceParams.IP,
Nickname: dev.Nickname,
ProductModel: dev.ProductModel,
ProductType: dev.ProductType,
DTLS: dev.DeviceParams.DTLS,
FirmwareVer: dev.FirmwareVer,
IsOnline: dev.ConnState == 1,
})
}
return c.cameras, nil
}
func (c *Cloud) GetCamera(id string) (*Camera, error) {
if c.cameras == nil {
if _, err := c.GetCameraList(); err != nil {
return nil, err
}
}
id = strings.ToUpper(id)
for _, cam := range c.cameras {
if strings.ToUpper(cam.MAC) == id || strings.EqualFold(cam.Nickname, id) {
return cam, nil
}
}
return nil, fmt.Errorf("wyze: camera not found: %s", id)
}
func (c *Cloud) GetP2PInfo(mac string) (map[string]any, error) {
payload := map[string]any{
"access_token": c.accessToken,
"phone_id": c.phoneID,
"device_mac": mac,
"app_name": appName,
"app_ver": appName + "___" + appVersion,
"app_version": appVersion,
"phone_system_type": 1,
"sc": "9f275790cab94a72bd206c8876429f3c",
"sv": "9d74946e652647e9b6c9d59326aef104",
"ts": time.Now().UnixMilli(),
}
jsonData, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", baseURLAPI+"/app/v2/device/get_iotc_info", strings.NewReader(string(jsonData)))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result p2pInfoResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
if result.Code != "1" {
return nil, fmt.Errorf("wyze: API error: %s - %s", result.Code, result.Msg)
}
return result.Data, nil
}
type apiError struct {
Code string `json:"code"`
ErrorCode int `json:"errorCode"`
Msg string `json:"msg"`
Description string `json:"description"`
}
func (e *apiError) hasError() bool {
if e.Code == "1" || e.Code == "0" {
return false
}
if e.Code == "" && e.ErrorCode == 0 {
return false
}
return e.Code != "" || e.ErrorCode != 0
}
func (e *apiError) message() string {
if e.Msg != "" {
return e.Msg
}
return e.Description
}
func (e *apiError) code() string {
if e.Code != "" {
return e.Code
}
return fmt.Sprintf("%d", e.ErrorCode)
}
type AuthError struct {
Message string `json:"message"`
NeedsMFA bool `json:"needs_mfa,omitempty"`
MFAType string `json:"mfa_type,omitempty"`
}
func (e *AuthError) Error() string {
return e.Message
}
func generatePhoneID() string {
return core.RandString(16, 16) // 16 hex chars
}
func hashPassword(password string) string {
encoded := strings.TrimSpace(password)
if strings.HasPrefix(strings.ToLower(encoded), "md5:") {
return encoded[4:]
}
for range 3 {
hash := md5.Sum([]byte(encoded))
encoded = hex.EncodeToString(hash[:])
}
return encoded
}
+277
View File
@@ -0,0 +1,277 @@
package wyze
import (
"fmt"
"net/url"
"time"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/tutk"
"github.com/pion/rtp"
)
type Producer struct {
core.Connection
client *Client
model string
}
func NewProducer(rawURL string) (*Producer, error) {
client, err := Dial(rawURL)
if err != nil {
return nil, err
}
u, _ := url.Parse(rawURL)
query := u.Query()
// 0 = HD (default), 1 = SD/360P, 2 = 720P, 3 = 2K, 4 = Floodlight
var quality byte
switch s := query.Get("subtype"); s {
case "", "hd":
quality = 0
case "sd":
quality = FrameSize360P
default:
quality = core.ParseByte(s)
}
medias, err := probe(client, quality)
if err != nil {
_ = client.Close()
return nil, err
}
prod := &Producer{
Connection: core.Connection{
ID: core.NewID(),
FormatName: "wyze",
Protocol: client.Protocol(),
RemoteAddr: client.RemoteAddr().String(),
Source: rawURL,
Medias: medias,
Transport: client,
},
client: client,
model: query.Get("model"),
}
return prod, nil
}
func (p *Producer) Start() error {
for {
if p.client.verbose {
fmt.Println("[Wyze] Reading packet...")
}
_ = p.client.SetDeadline(time.Now().Add(core.ConnDeadline))
pkt, err := p.client.ReadPacket()
if err != nil {
return err
}
if pkt == nil {
continue
}
var name string
var pkt2 *core.Packet
switch codecID := pkt.Codec; codecID {
case tutk.CodecH264:
name = core.CodecH264
pkt2 = &core.Packet{
Header: rtp.Header{SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: annexb.EncodeToAVCC(pkt.Payload),
}
case tutk.CodecH265:
name = core.CodecH265
pkt2 = &core.Packet{
Header: rtp.Header{SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: annexb.EncodeToAVCC(pkt.Payload),
}
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.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.CodecAACADTS, tutk.CodecAACAlt, tutk.CodecAACRaw, tutk.CodecAACLATM:
name = core.CodecAAC
payload := pkt.Payload
if aac.IsADTS(payload) {
payload = payload[aac.ADTSHeaderLen(payload):]
}
pkt2 = &core.Packet{
Header: rtp.Header{Version: aac.RTPPacketVersionAAC, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: payload,
}
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.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.CodecMP3:
name = core.CodecMP3
pkt2 = &core.Packet{
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: pkt.Payload,
}
case tutk.CodecMJPEG:
name = core.CodecJPEG
pkt2 = &core.Packet{
Header: rtp.Header{SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: pkt.Payload,
}
default:
continue
}
for _, recv := range p.Receivers {
if recv.Codec.Name == name {
recv.WriteRTP(pkt2)
break
}
}
}
}
func probe(client *Client, quality byte) ([]*core.Media, error) {
client.SetResolution(quality)
client.SetDeadline(time.Now().Add(core.ProbeTimeout))
var vcodec, acodec *core.Codec
var tutkAudioCodec byte
for {
if client.verbose {
fmt.Println("[Wyze] Probing for codecs...")
}
pkt, err := client.ReadPacket()
if err != nil {
return nil, fmt.Errorf("wyze: probe: %w", err)
}
if pkt == nil || len(pkt.Payload) < 5 {
continue
}
switch pkt.Codec {
case tutk.CodecH264:
if vcodec == nil {
buf := annexb.EncodeToAVCC(pkt.Payload)
if len(buf) >= 5 && h264.NALUType(buf) == h264.NALUTypeSPS {
vcodec = h264.AVCCToCodec(buf)
}
}
case tutk.CodecH265:
if vcodec == nil {
buf := annexb.EncodeToAVCC(pkt.Payload)
if len(buf) >= 5 && h265.NALUType(buf) == h265.NALUTypeVPS {
vcodec = h265.AVCCToCodec(buf)
}
}
case tutk.CodecPCMU:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecPCMU, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
tutkAudioCodec = pkt.Codec
}
case tutk.CodecPCMA:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecPCMA, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
tutkAudioCodec = pkt.Codec
}
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.CodecOpus:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecOpus, ClockRate: 48000, Channels: 2}
tutkAudioCodec = pkt.Codec
}
case tutk.CodecPCML:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecPCML, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
tutkAudioCodec = pkt.Codec
}
case tutk.CodecMP3:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecMP3, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
tutkAudioCodec = pkt.Codec
}
case tutk.CodecMJPEG:
if vcodec == nil {
vcodec = &core.Codec{Name: core.CodecJPEG, ClockRate: 90000, PayloadType: core.PayloadTypeRAW}
}
}
if vcodec != nil && (acodec != nil || !client.SupportsAudio()) {
break
}
}
_ = client.SetDeadline(time.Time{})
medias := []*core.Media{
{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{vcodec},
},
}
if acodec != nil {
medias = append(medias, &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{acodec},
})
if client.SupportsIntercom() {
client.SetBackchannelCodec(tutkAudioCodec, acodec.ClockRate, uint8(acodec.Channels))
medias = append(medias, &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{acodec.Clone()},
})
}
}
if client.verbose {
fmt.Printf("[Wyze] Probed codecs: video=%s audio=%s\n", vcodec.Name, acodec.Name)
if client.SupportsIntercom() {
fmt.Printf("[Wyze] Intercom supported, audio send codec=%s\n", acodec.Name)
}
}
return medias, nil
}
+1 -1
View File
@@ -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)
}
}
+2 -2
View File
@@ -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,