install go2rtc on bob
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user