install go2rtc on bob

This commit is contained in:
2026-04-04 19:36:14 +02:00
parent f0b56e63d1
commit ccf88187b8
537 changed files with 69213 additions and 0 deletions
+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
}
+264
View File
@@ -0,0 +1,264 @@
package tutk
import (
"fmt"
"io"
"net"
"sync"
"sync/atomic"
"time"
)
func Dial(host, uid, username, password string) (*Conn, error) {
addr, err := net.ResolveUDPAddr("udp", host)
if err != nil {
// Default port for listening incoming LAN connections.
// Important. It's not using for real connection.
addr = &net.UDPAddr{IP: net.ParseIP(host), Port: 32761}
}
udpConn, err := net.ListenUDP("udp", nil)
if err != nil {
return nil, err
}
c := &Conn{UDPConn: udpConn, addr: addr}
sid := GenSessionID()
_ = c.SetDeadline(time.Now().Add(5 * time.Second))
if addr.Port != 10001 {
err = c.connectDirect(uid, sid)
} else {
err = c.connectRemote(uid, sid)
}
if err != nil {
_ = c.Close()
return nil, err
}
if c.ver[0] >= 25 {
c.session = NewSession25(c, sid)
} else {
c.session = NewSession16(c, sid)
}
if err = c.clientStart(username, password); err != nil {
_ = c.Close()
return nil, err
}
go c.worker()
return c, nil
}
type Conn struct {
*net.UDPConn
addr *net.UDPAddr
session Session
ver []byte
err error
cmdMu sync.Mutex
cmdAck func()
}
// Read overwrite net.Conn
func (c *Conn) Read(buf []byte) (n int, err error) {
for {
var addr *net.UDPAddr
if n, addr, err = c.UDPConn.ReadFromUDP(buf); err != nil {
return 0, err
}
if string(c.addr.IP) != string(addr.IP) || n < 16 {
continue // skip messages from another IP
}
if c.addr.Port != addr.Port {
c.addr.Port = addr.Port
}
ReverseTransCodePartial(buf, buf[:n])
//log.Printf("<- %x", buf[:n])
return n, nil
}
}
// Write overwrite net.Conn
func (c *Conn) Write(b []byte) (n int, err error) {
//log.Printf("-> %x", b)
return c.UDPConn.WriteToUDP(TransCodePartial(nil, b), c.addr)
}
// RemoteAddr overwrite net.Conn
func (c *Conn) RemoteAddr() net.Addr {
return c.addr
}
func (c *Conn) Protocol() string {
return "tutk+udp"
}
func (c *Conn) Version() string {
if len(c.ver) == 1 {
return fmt.Sprintf("TUTK/%d", c.ver[0])
}
return fmt.Sprintf("TUTK/%d SDK %d.%d.%d.%d", c.ver[0], c.ver[1], c.ver[2], c.ver[3], c.ver[4])
}
func (c *Conn) ReadCommand() (ctrlType uint32, ctrlData []byte, err error) {
return c.session.RecvIOCtrl()
}
func (c *Conn) WriteCommand(ctrlType uint32, ctrlData []byte) error {
c.cmdMu.Lock()
defer c.cmdMu.Unlock()
var repeat atomic.Int32
repeat.Store(5)
timeout := time.NewTicker(time.Second)
defer timeout.Stop()
c.cmdAck = func() {
repeat.Store(0)
timeout.Reset(1)
}
buf := c.session.SendIOCtrl(ctrlType, ctrlData)
for {
if err := c.session.SessionWrite(0, buf); err != nil {
return err
}
<-timeout.C
r := repeat.Add(-1)
if r < 0 {
return nil
}
if r == 0 {
return fmt.Errorf("%s: can't send command %d", "tutk", ctrlType)
}
}
}
func (c *Conn) ReadPacket() (hdr, payload []byte, err error) {
return c.session.RecvFrameData()
}
func (c *Conn) WritePacket(hdr, payload []byte) error {
buf := c.session.SendFrameData(hdr, payload)
return c.session.SessionWrite(1, buf)
}
func (c *Conn) Error() error {
if c.err != nil {
return c.err
}
return io.EOF
}
func (c *Conn) worker() {
defer c.session.Close()
buf := make([]byte, 1200)
for {
n, err := c.Read(buf)
if err != nil {
c.err = fmt.Errorf("%s: %w", "tutk", err)
return
}
switch c.handleMsg(buf[:n]) {
case msgUnknown:
fmt.Printf("tutk: unknown msg: %x\n", buf[:n])
case msgError:
return
case msgCommandAck:
if c.cmdAck != nil {
c.cmdAck()
}
}
}
}
const (
msgUnknown = iota
msgError
msgPing
msgUnknownPing
msgClientStart
msgClientStart2
msgClientStartAck2
msgCommand
msgCommandAck
msgCounters
msgMediaChunk
msgMediaFrame
msgMediaReorder
msgMediaLost
msgCh5
msgUnknown0007 // time sync without data?
msgUnknown0008 // time sync with data?
msgUnknown0010
msgUnknown0013
msgUnknown0900
msgUnknown0a08
msgUnknownCh1c
msgDafang0012
)
func (c *Conn) handleMsg(msg []byte) int {
// off sample
// 0 0402 tutk magic
// 2 120a tutk version (120a, 190a...)
// 4 0800 msg size = len(b)-16
// 6 0000 channel seq
// 8 28041200 msg type
// 14 0100 channel (not all msg)
// 28 0700 msg data (not all msg)
switch msg[8] {
case 0x08:
switch ch := msg[14]; ch {
case 0, 1:
return c.session.SessionRead(ch, msg[28:])
case 5:
if len(msg) == 48 {
_, _ = c.Write(msgAckCh5(msg))
return msgCh5
}
case 0x1c:
return msgUnknownCh1c
}
case 0x18:
return msgUnknownPing
case 0x28:
if len(msg) == 24 {
_, _ = c.Write(msgAckPing(msg))
return msgPing
}
}
return msgUnknown
}
func msgAckPing(msg []byte) []byte {
// <- [24] 0402120a 08000000 28041200 000000005b0d4202070aa8c0
// -> [24] 04021a0a 08000000 27042100 000000005b0d4202070aa8c0
msg[8] = 0x27
msg[10] = 0x21
return msg
}
func msgAckCh5(msg []byte) []byte {
// <- [48] 0402190a 20000400 07042100 7ecc05000c0000007ecc93c456c2561f 5a97c2f101050000000000000000000000010000
// -> [48] 0402190a 20000400 08041200 7ecc05000c0000007ecc93c456c2561f 5a97c2f141050000000000000000000000010000
msg[8] = 0x07
msg[10] = 0x21
msg[32] = 0x41
return msg
}
+279
View File
@@ -0,0 +1,279 @@
package tutk
import (
"encoding/binary"
"math/bits"
)
// I'd like to say hello to Charlie. Your name is forever etched into the history of streaming software.
const charlie = "Charlie is the designer of P2P!!"
func ReverseTransCodePartial(dst, src []byte) []byte {
n := len(src)
tmp := make([]byte, n)
if len(dst) < n {
dst = make([]byte, n)
}
src16 := src
tmp16 := tmp
dst16 := 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(dst16, tmp16, 16)
for i := 0; i != 16; i++ {
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 = tmp16[16:]
dst16 = dst16[16:]
src16 = src16[16:]
}
swap(tmp16, src16, n)
for i := 0; i < n; i++ {
dst16[i] = tmp16[i] ^ charlie[i]
}
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)
if len(dst) < n {
dst = make([]byte, n)
}
src16 := src
tmp16 := tmp
dst16 := 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 := 0; i != 16; i++ {
dst16[i] = tmp16[i] ^ charlie[i]
}
swap(tmp16, dst16, 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 = tmp16[16:]
dst16 = dst16[16:]
src16 = src16[16:]
}
for i := 0; i < n; i++ {
tmp16[i] = src16[i] ^ charlie[i]
}
swap(dst16, tmp16, n)
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:
_, _ = src[1], dst[1]
dst[0] = src[1]
dst[1] = src[0]
return
case 4:
_, _ = src[3], dst[3]
dst[0] = src[2]
dst[1] = src[3]
dst[2] = src[0]
dst[3] = src[1]
return
case 8:
_, _ = src[7], dst[7]
dst[0] = src[7]
dst[1] = src[4]
dst[2] = src[3]
dst[3] = src[2]
dst[4] = src[1]
dst[5] = src[6]
dst[6] = src[5]
dst[7] = src[0]
return
case 16:
_, _ = src[15], dst[15]
dst[0] = src[11]
dst[1] = src[9]
dst[2] = src[8]
dst[3] = src[15]
dst[4] = src[13]
dst[5] = src[10]
dst[6] = src[12]
dst[7] = src[14]
dst[8] = src[2]
dst[9] = src[1]
dst[10] = src[5]
dst[11] = src[0]
dst[12] = src[6]
dst[13] = src[4]
dst[14] = src[7]
dst[15] = src[3]
return
}
copy(dst, src[:n])
}
const delta = 0x9e3779b9
func XXTEADecrypt(dst, src, key []byte) {
const n = int8(4) // support only 16 bytes src
var w, k [n]uint32
for i := int8(0); i < n; i++ {
w[i] = binary.LittleEndian.Uint32(src)
k[i] = binary.LittleEndian.Uint32(key)
src = src[4:]
key = key[4:]
}
rounds := 52/n + 6
sum := uint32(rounds) * delta
for ; rounds > 0; rounds-- {
w0 := w[0]
i2 := int8((sum >> 2) & 3)
for i := n - 1; i >= 0; i-- {
wi := w[(i-1)&3]
ki := k[i^i2]
t1 := (w0 ^ sum) + (wi ^ ki)
t2 := (wi >> 5) ^ (w0 << 2)
t3 := (w0 >> 3) ^ (wi << 4)
w[i] -= t1 ^ (t2 + t3)
w0 = w[i]
}
sum -= delta
}
for _, i := range w {
binary.LittleEndian.PutUint32(dst, i)
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))
}
@@ -0,0 +1,14 @@
package tutk
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestXXTEADecrypt(t *testing.T) {
buf := []byte("WERhJxb87WF3zgPa")
key := []byte("GAgDiwVPg2E4GMke")
XXTEADecrypt(buf, buf, key)
require.Equal(t, "\xc4\xa6\x2c\xa1\x10\x64\x17\xa5\xda\x02\xe1\x62\xa5\xf0\x62\x71", string(buf))
}
@@ -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
}
@@ -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(),
}
}
@@ -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
}
@@ -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
}
@@ -0,0 +1,71 @@
package tutk
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
// 8 00000000000000
// 15 02 args count
// 16 00000000000000
// 23 0101 args
n := byte(len(args))
b := make([]byte, 23+n)
copy(b, "ICAM")
binary.LittleEndian.PutUint32(b[4:], cmd)
b[15] = n
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
}
@@ -0,0 +1,157 @@
package tutk
import (
"bytes"
"encoding/binary"
"net"
"time"
)
func (c *Conn) connectDirect(uid string, sid []byte) error {
res, err := writeAndWait(
c, func(res []byte) bool { return bytes.Index(res, []byte("\x02\x06\x12\x00")) == 8 },
ConnectByUID(stageBroadcast, uid, sid),
)
if err != nil {
return err
}
n := len(res) // should be 200
c.ver = []byte{res[2], res[n-13], res[n-14], res[n-15], res[n-16]}
_, err = c.Write(ConnectByUID(stageDirect, uid, sid))
return err
}
func (c *Conn) connectRemote(uid string, sid []byte) error {
res, err := writeAndWait(
c, func(res []byte) bool { return bytes.Index(res, []byte("\x01\x03\x43")) == 8 },
ConnectByUID(stageGetRemoteIP, uid, sid),
)
if err != nil {
return err
}
// Read real IP from cloud server response.
// Important ot use net.IPv4 because slice will be 16 bytes.
c.addr.IP = net.IPv4(res[40], res[41], res[42], res[43])
c.addr.Port = int(binary.BigEndian.Uint16(res[38:]))
res, err = writeAndWait(
c, func(res []byte) bool { return bytes.Index(res, []byte("\x04\x04\x33")) == 8 },
ConnectByUID(stageRemoteAck, uid, sid),
)
if err != nil {
return err
}
if len(res) == 52 {
c.ver = []byte{res[2], res[51], res[50], res[49], res[48]}
} else {
c.ver = []byte{res[2]}
}
_, err = c.Write(ConnectByUID(stageRemoteOK, uid, sid))
return err
}
func (c *Conn) clientStart(username, password string) error {
_, err := writeAndWait(
c, func(res []byte) bool {
return len(res) >= 84 && res[28] == 0 && (res[29] == 0x14 || res[29] == 0x21)
},
c.session.ClientStart(0, username, password),
c.session.ClientStart(1, username, password),
)
return err
}
func writeAndWait(conn net.Conn, ok func(res []byte) bool, req ...[]byte) ([]byte, error) {
var t *time.Timer
t = time.AfterFunc(1, func() {
for _, b := range req {
if _, err := conn.Write(b); err != nil {
return
}
}
if t != nil {
t.Reset(time.Second)
}
})
defer t.Stop()
buf := make([]byte, 1200)
for {
n, err := conn.Read(buf)
if err != nil {
return nil, err
}
if ok(buf[:n]) {
return buf[:n], nil
}
}
}
const (
magic = "\x04\x02\x19" // include version 0x19
sdkVersion = "\x06\x00\x03\x03" // 3.3.0.6
)
const (
stageBroadcast = iota + 1
stageDirect
stageGetPublicIP
stageGetRemoteIP
stageRemoteReq
stageRemoteAck
stageRemoteOK
)
func ConnectByUID(stage byte, uid string, sid8 []byte) []byte {
var b []byte
switch stage {
case stageBroadcast, stageDirect:
b = make([]byte, 68)
copy(b[8:], "\x01\x06\x21")
copy(b[52:], sdkVersion)
copy(b[56:], sid8)
b[64] = stage // 1 or 2
case stageGetPublicIP:
b = make([]byte, 54)
copy(b[8:], "\x07\x10\x18")
case stageGetRemoteIP:
b = make([]byte, 112)
copy(b[8:], "\x03\x02\x34")
copy(b[100:], sid8)
b[108] = stageDirect
case stageRemoteReq:
b = make([]byte, 52)
copy(b[8:], "\x01\x04\x33")
copy(b[36:], sid8)
copy(b[48:], sdkVersion)
case stageRemoteAck:
b = make([]byte, 44)
copy(b[8:], "\x02\x04\x33")
copy(b[36:], sid8)
case stageRemoteOK:
b = make([]byte, 52)
copy(b[8:], "\x04\x04\x33")
copy(b[36:], sid8)
copy(b[48:], sdkVersion)
}
copy(b, magic)
b[3] = 0x02 // connection stage
binary.LittleEndian.PutUint16(b[4:], uint16(len(b))-16)
copy(b[16:], uid)
return b
}
@@ -0,0 +1,381 @@
package tutk
import (
"bytes"
"encoding/binary"
"io"
"net"
"time"
)
type Session interface {
Close() error
ClientStart(i byte, username, password string) []byte
SendIOCtrl(ctrlType uint32, ctrlData []byte) []byte
SendFrameData(frameInfo, frameData []byte) []byte
RecvIOCtrl() (ctrlType uint32, ctrlData []byte, err error)
RecvFrameData() (frameInfo, frameData []byte, err error)
SessionRead(chID byte, buf []byte) int
SessionWrite(chID byte, buf []byte) error
}
func NewSession16(conn net.Conn, sid8 []byte) *Session16 {
sid16 := make([]byte, 16)
copy(sid16[8:], sid8)
copy(sid16, sid8[:2])
sid16[4] = 0x0c
return &Session16{
conn: conn,
sid16: sid16,
rawCmd: make(chan []byte, 10),
rawPkt: make(chan [2][]byte, 100),
}
}
type Session16 struct {
conn net.Conn
sid16 []byte
rawCmd chan []byte
rawPkt chan [2][]byte
seqSendCh0 uint16
seqSendCh1 uint16
seqSendCmd1 uint16
seqSendAud uint16
waitFSeq uint16
waitCSeq uint16
waitSize int
waitData []byte
}
func (s *Session16) Close() error {
close(s.rawCmd)
close(s.rawPkt)
return nil
}
func (s *Session16) Msg(size uint16) []byte {
b := make([]byte, size)
copy(b, magic)
b[3] = 0x0a // connected stage
binary.LittleEndian.PutUint16(b[4:], size-16)
copy(b[8:], "\x07\x04\x21") // client request
copy(b[12:], s.sid16)
return b
}
const (
msgHhrSize = 28
cmdHdrSize = 24
)
func (s *Session16) ClientStart(i byte, username, password string) []byte {
const size = 566 + 32
msg := s.Msg(size)
// 0 00000b0000000000000000000000000022020000fcfc7284
// 24 4d69737300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
// 281 636c69656e740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
// 538 0100000004000000fb071f000000000000000000000003000000000001000000
cmd := msg[msgHhrSize:]
copy(cmd, "\x00\x00\x0b\x00")
binary.LittleEndian.PutUint16(cmd[16:], size-52)
if i == 0 {
cmd[18] = 1
} else {
cmd[1] = 0x20
}
binary.LittleEndian.PutUint32(cmd[20:], uint32(time.Now().UnixMilli()))
// important values for some cameras (not for df3)
data := cmd[cmdHdrSize:]
copy(data, username)
copy(data[257:], password)
// 0100000004000000fb071f000000000000000000000003000000000001000000
cfg := data[257+257:]
//cfg[0] = 1 // 0 - simple proto, 1 - complex proto with "0Cxx" commands
cfg[4] = 4
copy(cfg[8:], "\xfb\x07\x1f\x00")
cfg[22] = 3
//cfg[28] = 1 // unknown
return msg
}
func (s *Session16) SendIOCtrl(ctrlType uint32, ctrlData []byte) []byte {
dataSize := 4 + uint16(len(ctrlData))
msg := s.Msg(msgHhrSize + cmdHdrSize + dataSize)
cmd := msg[msgHhrSize:]
copy(cmd, "\x00\x70\x0b\x00")
s.seqSendCmd1++ // start from 1, important!
binary.LittleEndian.PutUint16(cmd[4:], s.seqSendCmd1)
binary.LittleEndian.PutUint16(cmd[16:], dataSize)
binary.LittleEndian.PutUint32(cmd[20:], uint32(time.Now().UnixMilli()))
data := cmd[cmdHdrSize:]
binary.LittleEndian.PutUint32(data, ctrlType)
copy(data[4:], ctrlData)
return msg
}
func (s *Session16) SendFrameData(frameInfo, frameData []byte) []byte {
// -> 01030b001d0000008802000000002800b0020bf501000000 ... 4f4455412000000088020000030400001d000000000000000bf51f7a9b0100000000000000000000
n := uint16(len(frameData))
dataSize := n + 8 + 32
msg := s.Msg(msgHhrSize + cmdHdrSize + dataSize)
// 0 01030b00 command + version
// 4 1d000000 seq
// 8 8802 media size (648)
// 10 00000000
// 14 2800 tail (pkt header) size?
// 16 b002 size (648 + 8 + 32)
// 18 0bf5 random msg id (unixms)
// 20 01000000 fixed
cmd := msg[msgHhrSize:]
copy(cmd, "\x01\x03\x0b\x00")
binary.LittleEndian.PutUint16(cmd[4:], s.seqSendAud)
s.seqSendAud++
binary.LittleEndian.PutUint16(cmd[8:], n)
cmd[14] = 0x28 // important!
binary.LittleEndian.PutUint16(cmd[16:], dataSize)
binary.LittleEndian.PutUint16(cmd[18:], uint16(time.Now().UnixMilli()))
cmd[20] = 1
data := cmd[cmdHdrSize:]
copy(data, frameData)
copy(data[n:], "ODUA\x20\x00\x00\x00")
copy(data[n+8:], frameInfo)
return msg
}
func (s *Session16) RecvIOCtrl() (ctrlType uint32, ctrlData []byte, err error) {
buf, ok := <-s.rawCmd
if !ok {
return 0, nil, io.EOF
}
return binary.LittleEndian.Uint32(buf), buf[4:], nil
}
func (s *Session16) RecvFrameData() (frameInfo, frameData []byte, err error) {
buf, ok := <-s.rawPkt
if !ok {
return nil, nil, io.EOF
}
return buf[0], buf[1], nil
}
func (s *Session16) SessionRead(chID byte, cmd []byte) int {
if chID != 0 {
return s.handleCh1(cmd)
}
// 0 01030800 command + version
// 4 00000000 frame seq
// 8 ac880100 total size
// 12 6200 chunk seq
// 14 2000 tail (pkt header) size
// 16 cc00 size
// 18 0000
// 20 01000000 fixed
switch cmd[0] {
case 0x01:
var packetData [2][]byte
switch cmd[1] {
case 0x03:
frameSeq := binary.LittleEndian.Uint16(cmd[4:])
chunkSeq := binary.LittleEndian.Uint16(cmd[12:])
if chunkSeq == 0 {
s.waitFSeq = frameSeq
s.waitCSeq = 0
s.waitData = s.waitData[:0]
payloadSize := binary.LittleEndian.Uint32(cmd[8:])
hdrSize := binary.LittleEndian.Uint16(cmd[14:])
s.waitSize = int(hdrSize) + int(payloadSize)
} else if frameSeq != s.waitFSeq || chunkSeq != s.waitCSeq {
s.waitCSeq = 0
return msgMediaLost
}
s.waitData = append(s.waitData, cmd[24:]...)
if n := len(s.waitData); n < s.waitSize {
s.waitCSeq++
return msgMediaChunk
}
s.waitCSeq = 0
payloadSize := binary.LittleEndian.Uint32(cmd[8:])
packetData[0] = bytes.Clone(s.waitData[payloadSize:])
packetData[1] = bytes.Clone(s.waitData[:payloadSize])
case 0x04:
data := cmd[24:]
hdrSize := binary.LittleEndian.Uint16(cmd[14:])
packetData[0] = bytes.Clone(data[:hdrSize])
packetData[1] = bytes.Clone(data[hdrSize:])
default:
return msgUnknown
}
select {
case s.rawPkt <- packetData:
default:
return msgError
}
return msgMediaFrame
case 0x00:
switch cmd[1] {
case 0x70:
_ = s.SessionWrite(0, s.msgAck0070(cmd))
select {
case s.rawCmd <- append([]byte{}, cmd[24:]...):
default:
}
return msgCommand
case 0x12:
_ = s.SessionWrite(0, s.msgAck0012(cmd))
return msgDafang0012
case 0x71:
return msgCommandAck
}
}
return msgUnknown
}
func (s *Session16) msgAck0070(msg28 []byte) []byte {
// <- 00700800010000000000000000000000340000007625a02f ...
// -> 00710800010000000000000000000000000000007625a02f
msg := s.Msg(msgHhrSize + cmdHdrSize)
cmd := msg[msgHhrSize:]
copy(cmd, "\x00\x71")
copy(cmd[2:], msg28[2:6]) // same version and seq
copy(cmd[20:], msg28[20:24]) // same msg random
return msg
}
func (s *Session16) msgAck0012(msg28 []byte) []byte {
// <- 001208000000000000000000000000000c00000000000000 020000000100000001000000
// -> 00130b000000000000000000000000001400000000000000 0200000001000000010000000000000000000000
const dataSize = 20
msg := s.Msg(msgHhrSize + cmdHdrSize + dataSize)
cmd := msg[msgHhrSize:]
copy(cmd, "\x00\x13\x0b\x00")
cmd[16] = dataSize
data := cmd[cmdHdrSize:]
copy(data, msg28[cmdHdrSize:])
return msg
}
func (s *Session16) handleCh1(cmd []byte) int {
// Channel 1 used for two-way audio. It's important:
// - answer on 0000 command with exact config response (can't set simple proto)
// - send 0012 command at start
// - respond on every 0008 command for smooth playback
switch cid := string(cmd[:2]); cid {
case "\x00\x00": // client start
_ = s.SessionWrite(1, s.msgAck0000(cmd))
_ = s.SessionWrite(1, s.msg0012())
return msgClientStart
case "\x00\x07": // time sync without data
_ = s.SessionWrite(1, s.msgAck0007(cmd))
return msgUnknown0007
case "\x00\x08": // time sync with data
_ = s.SessionWrite(1, s.msgAck0008(cmd))
return msgUnknown0008
case "\x00\x13": // ack for 0012
return msgUnknown0013
}
return msgUnknown
}
func (s *Session16) msgAck0000(msg28 []byte) []byte {
// <- 000008000000000000000000000000001a0200004f47c714 ... 00000000000000000100000004000000fb071f00000000000000000000000300
// -> 00140b00000000000000000000000000200000004f47c714 00000000000000000100000004000000fb071f00000000000000000000000300
const cmdDataSize = 32
msg := s.Msg(msgHhrSize + cmdHdrSize + cmdDataSize)
cmd := msg[msgHhrSize:]
copy(cmd, "\x00\x14\x0b\x00")
cmd[16] = cmdDataSize
copy(cmd[20:], msg28[20:24]) // request id (random)
// Important to answer with same data.
data := cmd[cmdHdrSize:]
copy(data, msg28[len(msg28)-32:])
return msg
}
func (s *Session16) msg0012() []byte {
// -> 00120b000000000000000000000000000c00000000000000020000000100000001000000
const dataSize = 12
msg := s.Msg(msgHhrSize + cmdHdrSize + dataSize)
cmd := msg[msgHhrSize:]
copy(cmd, "\x00\x12\x0b\x00")
cmd[16] = dataSize
data := cmd[cmdHdrSize:]
data[0] = 2
data[4] = 1
data[9] = 1
return msg
}
func (s *Session16) msgAck0007(msg28 []byte) []byte {
// <- 000708000000000000000000000000000c00000001000000000000001c551f7a00000000
// -> 010a0b00000000000000000000000000000000000100000000000000
msg := s.Msg(msgHhrSize + 28)
cmd := msg[msgHhrSize:]
copy(cmd, "\x01\x0a\x0b\x00")
cmd[20] = 1
return msg
}
func (s *Session16) msgAck0008(msg28 []byte) []byte {
// <- 000808000000000000000000000000000000f9f0010000000200000050f31f7a
// -> 01090b0000000000000000000000000000000000010000000200000050f31f7a
msg := s.Msg(msgHhrSize + 28)
cmd := msg[msgHhrSize:]
copy(cmd, "\x01\x09\x0b\x00")
copy(cmd[20:], msg28[20:])
return msg
}
func (s *Session16) SessionWrite(chID byte, buf []byte) error {
switch chID {
case 0:
binary.LittleEndian.PutUint16(buf[6:], s.seqSendCh0)
s.seqSendCh0++
case 1:
binary.LittleEndian.PutUint16(buf[6:], s.seqSendCh1)
s.seqSendCh1++
buf[14] = 1 // channel
}
_, err := s.conn.Write(buf)
return err
}
@@ -0,0 +1,337 @@
package tutk
import (
"bytes"
"encoding/binary"
"net"
"time"
)
func NewSession25(conn net.Conn, sid []byte) *Session25 {
return &Session25{
Session16: NewSession16(conn, sid),
rb: NewReorderBuffer(5),
}
}
type Session25 struct {
*Session16
rb *ReorderBuffer
seqSendCmd2 uint16
seqSendCnt uint16
seqRecvPkt0 uint16
seqRecvPkt1 uint16
seqRecvCmd2 uint16
}
const cmdHdrSize25 = 28
func (s *Session25) SendIOCtrl(ctrlType uint32, ctrlData []byte) []byte {
size := msgHhrSize + cmdHdrSize25 + 4 + uint16(len(ctrlData))
msg := s.Msg(size)
// 0 0070 command
// 2 0b00 version
// 4 1000 seq
// 6 0076 ???
cmd := msg[msgHhrSize:]
copy(cmd, "\x00\x70\x0b\x00")
binary.LittleEndian.PutUint16(cmd[4:], s.seqSendCmd1)
s.seqSendCmd1++
// 8 0070 command (second time)
// 10 0300 seq
// 12 0100 chunks count
// 14 0000 chunk seq (starts from 0)
// 16 5500 size
// 18 0000 random msg id (always 0)
// 20 03000000 seq (second time)
// 24 00000000
// 28 01010000 ctrlType
cmd[9] = 0x70
cmd[12] = 1
binary.LittleEndian.PutUint16(cmd[16:], size-52)
binary.LittleEndian.PutUint16(cmd[10:], s.seqSendCmd2)
binary.LittleEndian.PutUint16(cmd[20:], s.seqSendCmd2)
s.seqSendCmd2++
data := cmd[28:]
binary.LittleEndian.PutUint32(data, ctrlType)
copy(data[4:], ctrlData)
return msg
}
func (s *Session25) SendFrameData(frameInfo, frameData []byte) []byte {
return nil
}
func (s *Session25) SessionRead(chID byte, cmd []byte) (res int) {
if chID != 0 {
return s.handleCh1(cmd)
}
switch cmd[0] {
case 0x03, 0x05, 0x07:
for i := 0; cmd != nil; i++ {
res = s.handleChunk(cmd, i == 0)
cmd = s.rb.Pop()
}
return
case 0x00:
_ = s.SessionWrite(0, s.msgAckCounters())
s.seqRecvCmd2 = binary.LittleEndian.Uint16(cmd[2:])
switch cmd[1] {
case 0x10:
return msgUnknown0010 // unknown
case 0x21:
return msgClientStartAck2
case 0x70:
select {
case s.rawCmd <- cmd[28:]:
default:
}
return msgCommand // cmd from camera
case 0x71:
return msgCommandAck
}
case 0x09:
// off sample
// 0 09000b00 cmd1
// 4 0d000000 seqCmd1
// 12 0000 seqRecvCmd2
seq := binary.LittleEndian.Uint16(cmd[12:])
if s.seqSendCmd1 > seq {
return msgCommandAck
}
return msgCounters
case 0x0a:
// seq sample
// 0 0a080b00
// 4 03000000
// 8 e2043200
// 12 01000000
_ = s.SessionWrite(0, s.msgAck0A08(cmd))
return msgUnknown0a08
}
return msgUnknown
}
func (s *Session25) handleChunk(cmd []byte, checkSeq bool) int {
var cmd2 []byte
flags := cmd[1]
if flags&0b1000 == 0 {
// off sample
// 0 0700 command
// 2 0b00 version
// 4 2700 seq
// 6 0000 ???
// 8 0700 command (second time)
// 10 1400 seq
// 12 1300 chunks count per this frame
// 14 1100 chunk seq, starts from 0 (0x20 for last chunk)
// 16 0004 frame data size
// 18 0000 random msg id (always 0)
// 20 02000000 previous frame seq, starts from 0
// 24 03000000 current frame seq, starts from 1
cmd2 = cmd[8:]
} else {
// off sample
// 0 070d0b00
// 4 30000000
// 8 5c965500 ???
// 12 ffff0000 ???
// 16 0701 fixed command
// 18 190001002000a802000006000000070000000
cmd2 = cmd[16:]
}
seq := binary.LittleEndian.Uint16(cmd2[2:])
if checkSeq {
if s.rb.Check(seq) {
s.rb.Next()
} else {
s.rb.Push(seq, cmd)
return msgMediaReorder
}
}
// Check if this is first chunk for frame.
// Handle protocol bug "0x20 chunk seq for last chunk" and sometimes
// "0x20 chunk seq for first chunk if only one chunk".
if binary.LittleEndian.Uint16(cmd2[6:]) == 0 || binary.LittleEndian.Uint16(cmd2[4:]) == 1 {
s.waitData = s.waitData[:0]
s.waitCSeq = seq
} else if seq != s.waitCSeq {
return msgMediaLost
}
s.waitData = append(s.waitData, cmd2[20:]...)
if flags&0b0001 == 0 {
s.waitCSeq++
return msgMediaChunk
}
s.seqRecvPkt1 = seq
_ = s.SessionWrite(0, s.msgAckCounters())
n := len(s.waitData) - 32
packetData := [2][]byte{bytes.Clone(s.waitData[n:]), bytes.Clone(s.waitData[:n])}
select {
case s.rawPkt <- packetData:
default:
return msgError
}
return msgMediaFrame
}
func (s *Session25) msgAckCounters() []byte {
msg := s.Msg(msgHhrSize + cmdHdrSize)
// off sample
// 0 09000b00 cmd1
// 4 2700 seqCmd1
// 6 0000
// 8 1300 seqRecvPkt0
// 10 2600 seqRecvPkt1
// 12 0400 seqRecvCmd2
// 14 00000000
// 18 1400 seqSendCnt
// 20 d91a random
// 22 0000
cmd := msg[msgHhrSize:]
copy(cmd, "\x09\x00\x0b\x00")
binary.LittleEndian.PutUint16(cmd[4:], s.seqSendCmd1)
s.seqSendCmd1++
// seqRecvPkt0 stores previous value of seqRecvPkt1
// don't understand why this needs
binary.LittleEndian.PutUint16(cmd[8:], s.seqRecvPkt0)
s.seqRecvPkt0 = s.seqRecvPkt1
binary.LittleEndian.PutUint16(cmd[10:], s.seqRecvPkt1)
binary.LittleEndian.PutUint16(cmd[12:], s.seqRecvCmd2)
binary.LittleEndian.PutUint16(cmd[18:], s.seqSendCnt)
s.seqSendCnt++
binary.LittleEndian.PutUint16(cmd[20:], uint16(time.Now().UnixMilli()))
return msg
}
func (s *Session25) handleCh1(cmd []byte) int {
switch cid := string(cmd[:2]); cid {
case "\x00\x00": // client start
return msgClientStart
case "\x00\x07": // time sync without data
_ = s.SessionWrite(1, s.msgAck0007(cmd))
return msgUnknown0007
case "\x00\x20": // client start2
_ = s.SessionWrite(1, s.msgAck0020(cmd))
return msgClientStart2
case "\x09\x00":
return msgUnknown0900
case "\x0a\x08":
return msgUnknown0a08
}
return msgUnknown
}
func (s *Session25) msgAck0020(msg28 []byte) []byte {
const cmdDataSize = 36
msg := s.Msg(msgHhrSize + cmdHdrSize25 + cmdDataSize)
cmd := msg[msgHhrSize:]
copy(cmd, "\x00\x21\x0b\x00")
cmd[16] = cmdDataSize
copy(cmd[20:], msg28[20:24]) // request id (random)
// 0 00000000
// 4 00010001
// 8 01000000
// 12 04000000
// 16 fb071f00
// 20 00000000
// 24 00000000
// 28 00000300
// 32 01000000
data := cmd[cmdHdrSize25:]
data[5] = 1
data[7] = 1
data[8] = 1
data[12] = 4
copy(data[16:], "\xfb\x07\x1f\x00")
data[30] = 3
data[32] = 1
return msg
}
func (s *Session25) msgAck0A08(msg28 []byte) []byte {
// <- 0a080b005b0000000b51590002000000
// -> 0b000b00000001000b5103000300000000000000
msg := s.Msg(msgHhrSize + 20)
cmd := msg[msgHhrSize:]
copy(cmd, "\x0b\x00\x0b\x00")
copy(cmd[8:], msg28[8:10])
return msg
}
// ReorderBuffer used for UDP incoming data. Because the order of the packets may be mixed up.
type ReorderBuffer struct {
buf map[uint16][]byte
seq uint16
size int
}
func NewReorderBuffer(size int) *ReorderBuffer {
return &ReorderBuffer{buf: make(map[uint16][]byte), size: size}
}
// Check return OK if this is the seq we are waiting for.
func (r *ReorderBuffer) Check(seq uint16) (ok bool) {
return seq == r.seq
}
func (r *ReorderBuffer) Next() {
r.seq++
}
// Available return how much free slots is in the buffer.
func (r *ReorderBuffer) Available() int {
return r.size - len(r.buf)
}
// Push new item to buffer. Important! There is no buffer full check here.
func (r *ReorderBuffer) Push(seq uint16, data []byte) {
//log.Printf("push seq=%d wait=%d", seq, r.seq)
r.buf[seq] = bytes.Clone(data)
}
// Pop latest item from buffer. OK - if items wasn't dropped.
func (r *ReorderBuffer) Pop() []byte {
for {
if data := r.buf[r.seq]; data != nil {
delete(r.buf, r.seq)
r.Next()
//log.Printf("pop seq=%d", r.seq)
return data
}
if r.Available() > 0 {
return nil
}
//log.Printf("drop seq=%d", r.seq)
r.Next() // drop item
}
}