refactor
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
package tutk
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CalculateAuthKey(enr, mac string) []byte {
|
||||
data := enr + strings.ToUpper(mac)
|
||||
hash := sha256.Sum256([]byte(data))
|
||||
b64 := base64.StdEncoding.EncodeToString(hash[:6])
|
||||
b64 = strings.ReplaceAll(b64, "+", "Z")
|
||||
b64 = strings.ReplaceAll(b64, "/", "9")
|
||||
b64 = strings.ReplaceAll(b64, "=", "A")
|
||||
return []byte(b64)
|
||||
}
|
||||
|
||||
func DerivePSK(enr string) []byte {
|
||||
// DerivePSK derives the DTLS PSK from ENR
|
||||
// TUTK SDK treats the PSK as a NULL-terminated C string, so if SHA256(ENR)
|
||||
// contains a 0x00 byte, the PSK is truncated at that position.
|
||||
hash := sha256.Sum256([]byte(enr))
|
||||
pskLen := 32
|
||||
for i := range 32 {
|
||||
if hash[i] == 0x00 {
|
||||
pskLen = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
psk := make([]byte, 32)
|
||||
copy(psk[:pskLen], hash[:pskLen])
|
||||
return psk
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package tutk
|
||||
|
||||
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,50 @@
|
||||
package tutk
|
||||
|
||||
// https://github.com/seydx/tutk_wyze#11-codec-reference
|
||||
const (
|
||||
CodecMPEG4 byte = 0x4C
|
||||
CodecH263 byte = 0x4D
|
||||
CodecH264 byte = 0x4E
|
||||
CodecMJPEG byte = 0x4F
|
||||
CodecH265 byte = 0x50
|
||||
)
|
||||
|
||||
const (
|
||||
CodecAACRaw byte = 0x86
|
||||
CodecAACADTS byte = 0x87
|
||||
CodecAACLATM byte = 0x88
|
||||
CodecPCMU byte = 0x89
|
||||
CodecPCMA byte = 0x8A
|
||||
CodecADPCM byte = 0x8B
|
||||
CodecPCML byte = 0x8C
|
||||
CodecSPEEX byte = 0x8D
|
||||
CodecMP3 byte = 0x8E
|
||||
CodecG726 byte = 0x8F
|
||||
CodecAACAlt byte = 0x90
|
||||
CodecOpus byte = 0x92
|
||||
)
|
||||
|
||||
var sampleRates = [9]uint32{8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000}
|
||||
|
||||
func GetSamplesPerFrame(codecID byte) uint32 {
|
||||
switch codecID {
|
||||
case CodecAACRaw, CodecAACADTS, CodecAACLATM, CodecAACAlt:
|
||||
return 1024
|
||||
case CodecPCMU, CodecPCMA, CodecPCML, CodecADPCM, CodecSPEEX, CodecG726:
|
||||
return 160
|
||||
case CodecMP3:
|
||||
return 1152
|
||||
case CodecOpus:
|
||||
return 960
|
||||
default:
|
||||
return 1024
|
||||
}
|
||||
}
|
||||
|
||||
func IsVideoCodec(id byte) bool {
|
||||
return id >= CodecMPEG4 && id <= CodecH265
|
||||
}
|
||||
|
||||
func IsAudioCodec(id byte) bool {
|
||||
return id >= CodecAACRaw && id <= CodecOpus
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,34 @@ func ReverseTransCodePartial(dst, src []byte) []byte {
|
||||
return dst
|
||||
}
|
||||
|
||||
func ReverseTransCodeBlob(src []byte) []byte {
|
||||
if len(src) < 16 {
|
||||
return ReverseTransCodePartial(nil, src)
|
||||
}
|
||||
|
||||
dst := make([]byte, len(src))
|
||||
header := ReverseTransCodePartial(nil, src[:16])
|
||||
copy(dst, header)
|
||||
|
||||
if len(src) > 16 {
|
||||
if dst[3]&1 != 0 { // Partial encryption (check decrypted header)
|
||||
remaining := len(src) - 16
|
||||
decryptLen := min(remaining, 48)
|
||||
if decryptLen > 0 {
|
||||
decrypted := ReverseTransCodePartial(nil, src[16:16+decryptLen])
|
||||
copy(dst[16:], decrypted)
|
||||
}
|
||||
if remaining > 48 {
|
||||
copy(dst[64:], src[64:])
|
||||
}
|
||||
} else { // Full decryption
|
||||
decrypted := ReverseTransCodePartial(nil, src[16:])
|
||||
copy(dst[16:], decrypted)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func TransCodePartial(dst, src []byte) []byte {
|
||||
n := len(src)
|
||||
tmp := make([]byte, n)
|
||||
@@ -92,6 +120,34 @@ func TransCodePartial(dst, src []byte) []byte {
|
||||
return dst
|
||||
}
|
||||
|
||||
func TransCodeBlob(src []byte) []byte {
|
||||
if len(src) < 16 {
|
||||
return TransCodePartial(nil, src)
|
||||
}
|
||||
|
||||
dst := make([]byte, len(src))
|
||||
header := TransCodePartial(nil, src[:16])
|
||||
copy(dst, header)
|
||||
|
||||
if len(src) > 16 {
|
||||
if src[3]&1 != 0 { // Partial encryption
|
||||
remaining := len(src) - 16
|
||||
encryptLen := min(remaining, 48)
|
||||
if encryptLen > 0 {
|
||||
encrypted := TransCodePartial(nil, src[16:16+encryptLen])
|
||||
copy(dst[16:], encrypted)
|
||||
}
|
||||
if remaining > 48 {
|
||||
copy(dst[64:], src[64:])
|
||||
}
|
||||
} else { // Full encryption
|
||||
encrypted := TransCodePartial(nil, src[16:])
|
||||
copy(dst[16:], encrypted)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func swap(dst, src []byte, n int) {
|
||||
switch n {
|
||||
case 2:
|
||||
@@ -175,3 +231,49 @@ func XXTEADecrypt(dst, src, key []byte) {
|
||||
dst = dst[4:]
|
||||
}
|
||||
}
|
||||
|
||||
func XXTEADecryptVar(data, key []byte) []byte {
|
||||
if len(data) < 8 || len(key) < 16 {
|
||||
return nil
|
||||
}
|
||||
|
||||
k := make([]uint32, 4)
|
||||
for i := range 4 {
|
||||
k[i] = binary.LittleEndian.Uint32(key[i*4:])
|
||||
}
|
||||
|
||||
n := max(len(data)/4, 2)
|
||||
v := make([]uint32, n)
|
||||
for i := 0; i < len(data)/4; i++ {
|
||||
v[i] = binary.LittleEndian.Uint32(data[i*4:])
|
||||
}
|
||||
|
||||
rounds := 6 + 52/n
|
||||
sum := uint32(rounds) * delta
|
||||
y := v[0]
|
||||
|
||||
for rounds > 0 {
|
||||
e := (sum >> 2) & 3
|
||||
for p := n - 1; p > 0; p-- {
|
||||
z := v[p-1]
|
||||
v[p] -= xxteaMX(sum, y, z, p, e, k)
|
||||
y = v[p]
|
||||
}
|
||||
z := v[n-1]
|
||||
v[0] -= xxteaMX(sum, y, z, 0, e, k)
|
||||
y = v[0]
|
||||
sum -= delta
|
||||
rounds--
|
||||
}
|
||||
|
||||
result := make([]byte, n*4)
|
||||
for i := range n {
|
||||
binary.LittleEndian.PutUint32(result[i*4:], v[i])
|
||||
}
|
||||
|
||||
return result[:len(data)]
|
||||
}
|
||||
|
||||
func xxteaMX(sum, y, z uint32, p int, e uint32, k []uint32) uint32 {
|
||||
return ((z>>5 ^ y<<2) + (y>>3 ^ z<<4)) ^ ((sum ^ y) + (k[(p&3)^int(e)] ^ z))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package tutk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pion/dtls/v3"
|
||||
)
|
||||
|
||||
type DTLSConfig struct {
|
||||
PSK []byte
|
||||
Identity string
|
||||
IsServer bool
|
||||
}
|
||||
|
||||
func NewDTLSClient(adapter net.PacketConn, addr net.Addr, psk []byte) (*dtls.Conn, error) {
|
||||
return dtls.Client(adapter, addr, buildDTLSConfig(psk, false))
|
||||
}
|
||||
|
||||
func NewDTLSServer(adapter net.PacketConn, addr net.Addr, psk []byte) (*dtls.Conn, error) {
|
||||
return dtls.Server(adapter, addr, buildDTLSConfig(psk, true))
|
||||
}
|
||||
|
||||
func buildDTLSConfig(psk []byte, isServer bool) *dtls.Config {
|
||||
config := &dtls.Config{
|
||||
PSK: func(hint []byte) ([]byte, error) {
|
||||
return psk, nil
|
||||
},
|
||||
PSKIdentityHint: []byte("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 NewChannelAdapter(ctx context.Context, channel uint8, addr net.Addr, writeFn func([]byte, uint8) error, readChan chan []byte) *ChannelAdapter {
|
||||
return &ChannelAdapter{
|
||||
ctx: ctx,
|
||||
channel: channel,
|
||||
addr: addr,
|
||||
writeFn: writeFn,
|
||||
readChan: readChan,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ChannelAdapter) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
a.mu.Lock()
|
||||
deadline := a.readDeadline
|
||||
a.mu.Unlock()
|
||||
|
||||
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
|
||||
}
|
||||
+43
-9
@@ -1,16 +1,16 @@
|
||||
package tutk
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// https://github.com/seydx/tutk_wyze#11-codec-reference
|
||||
const (
|
||||
CodecH264 = 0x4e
|
||||
CodecH265 = 0x50
|
||||
CodecPCMA = 0x8a
|
||||
CodecPCML = 0x8c
|
||||
CodecAAC = 0x88
|
||||
import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GenSessionID() []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(b, uint64(time.Now().UnixNano()))
|
||||
return b
|
||||
}
|
||||
|
||||
func ICAM(cmd uint32, args ...byte) []byte {
|
||||
// 0 4943414d ICAM
|
||||
// 4 d807ff00 command
|
||||
@@ -26,3 +26,37 @@ func ICAM(cmd uint32, args ...byte) []byte {
|
||||
copy(b[23:], args)
|
||||
return b
|
||||
}
|
||||
|
||||
func HL(cmdID uint16, payload []byte) []byte {
|
||||
// 0-1 "HL" magic
|
||||
// 2 version (typically 5)
|
||||
// 3 reserved
|
||||
// 4-5 cmdID command ID (uint16 LE)
|
||||
// 6-7 payloadLen payload length (uint16 LE)
|
||||
// 8-15 reserved
|
||||
// 16+ payload
|
||||
const headerSize = 16
|
||||
const version = 5
|
||||
|
||||
b := make([]byte, headerSize+len(payload))
|
||||
copy(b, "HL")
|
||||
b[2] = version
|
||||
binary.LittleEndian.PutUint16(b[4:], cmdID)
|
||||
binary.LittleEndian.PutUint16(b[6:], uint16(len(payload)))
|
||||
copy(b[headerSize:], payload)
|
||||
return b
|
||||
}
|
||||
|
||||
func ParseHL(data []byte) (cmdID uint16, payload []byte, ok bool) {
|
||||
if len(data) < 16 || data[0] != 'H' || data[1] != 'L' {
|
||||
return 0, nil, false
|
||||
}
|
||||
cmdID = binary.LittleEndian.Uint16(data[4:])
|
||||
payloadLen := binary.LittleEndian.Uint16(data[6:])
|
||||
if len(data) >= 16+int(payloadLen) {
|
||||
payload = data[16 : 16+payloadLen]
|
||||
} else if len(data) > 16 {
|
||||
payload = data[16:]
|
||||
}
|
||||
return cmdID, payload, true
|
||||
}
|
||||
|
||||
@@ -155,9 +155,3 @@ func ConnectByUID(stage byte, uid string, sid8 []byte) []byte {
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func GenSessionID() []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(b, uint64(time.Now().UnixNano()))
|
||||
return b
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user