This commit is contained in:
seydx
2026-01-17 20:22:08 +01:00
parent c03cd9f156
commit 160695857e
18 changed files with 613 additions and 2314 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/wyze/tutk"
"github.com/AlexxIT/go2rtc/pkg/tutk"
"github.com/pion/rtp"
)
+50 -23
View File
@@ -12,8 +12,7 @@ import (
"sync"
"time"
"github.com/AlexxIT/go2rtc/pkg/wyze/crypto"
"github.com/AlexxIT/go2rtc/pkg/wyze/tutk"
"github.com/AlexxIT/go2rtc/pkg/tutk"
)
const (
@@ -29,15 +28,6 @@ const (
BitrateSD uint16 = 0x3C
)
const (
QualityUnknown = 0
QualityMax = 1
QualityHigh = 2
QualityMiddle = 3
QualityLow = 4
QualityMin = 5
)
const (
MediaTypeVideo = 1
MediaTypeAudio = 2
@@ -59,7 +49,7 @@ const (
)
type Client struct {
conn *tutk.Conn
conn *tutk.DTLSConn
host string
uid string
@@ -76,7 +66,7 @@ type Client struct {
hasAudio bool
hasIntercom bool
audioCodecID uint16
audioCodecID byte
audioSampleRate uint32
audioChannels uint8
}
@@ -107,7 +97,7 @@ func Dial(rawURL string) (*Client, error) {
verbose: query.Get("verbose") == "true",
}
c.authKey = string(crypto.CalculateAuthKey(c.enr, c.mac))
c.authKey = string(tutk.CalculateAuthKey(c.enr, c.mac))
if c.verbose {
fmt.Printf("[Wyze] Connecting to %s (UID: %s)\n", c.host, c.uid)
@@ -143,13 +133,13 @@ func (c *Client) SupportsIntercom() bool {
return c.hasIntercom
}
func (c *Client) SetBackchannelCodec(codecID uint16, sampleRate uint32, channels uint8) {
func (c *Client) SetBackchannelCodec(codecID byte, sampleRate uint32, channels uint8) {
c.audioCodecID = codecID
c.audioSampleRate = sampleRate
c.audioChannels = channels
}
func (c *Client) GetBackchannelCodec() (codecID uint16, sampleRate uint32, channels uint8) {
func (c *Client) GetBackchannelCodec() (codecID byte, sampleRate uint32, channels uint8) {
return c.audioCodecID, c.audioSampleRate, c.audioChannels
}
@@ -238,13 +228,13 @@ func (c *Client) ReadPacket() (*tutk.Packet, error) {
return c.conn.AVRecvFrameData()
}
func (c *Client) WriteAudio(codec uint16, payload []byte, timestamp uint32, sampleRate uint32, channels uint8) error {
func (c *Client) WriteAudio(codec byte, payload []byte, timestamp uint32, sampleRate uint32, channels uint8) error {
if !c.conn.IsBackchannelReady() {
return fmt.Errorf("speaker channel not connected")
}
if c.verbose {
fmt.Printf("[Wyze] WriteAudio: codec=0x%04x, payload=%d bytes, rate=%d, ch=%d\n", codec, len(payload), sampleRate, channels)
fmt.Printf("[Wyze] WriteAudio: codec=0x%02x, payload=%d bytes, rate=%d, ch=%d\n", codec, len(payload), sampleRate, channels)
}
return c.conn.AVSendAudioData(codec, payload, timestamp, sampleRate, channels)
@@ -305,7 +295,7 @@ func (c *Client) connect() error {
host = host[:idx]
}
conn, err := tutk.Dial(host, port, c.uid, c.authKey, c.enr, c.mac, c.verbose)
conn, err := tutk.DialDTLS(host, port, c.uid, c.authKey, c.enr, c.verbose)
if err != nil {
return fmt.Errorf("wyze: connect failed: %w", err)
}
@@ -386,9 +376,7 @@ func (c *Client) doKAuth() error {
fmt.Printf("[Wyze] K10003 auth success\n")
}
if avResp := c.conn.GetAVLoginResponse(); avResp != nil {
c.hasIntercom = avResp.TwoWayStreaming == 1
}
c.hasIntercom = c.conn.HasTwoWayStreaming()
if c.verbose {
fmt.Printf("[Wyze] K-auth complete\n")
@@ -409,7 +397,7 @@ func (c *Client) buildK10000() []byte {
}
func (c *Client) buildK10002(challenge []byte, status byte) []byte {
resp := crypto.GenerateChallengeResponse(challenge, c.enr, status)
resp := generateChallengeResponse(challenge, c.enr, status)
sessionID := make([]byte, 4)
rand.Read(sessionID)
b := make([]byte, 38)
@@ -555,3 +543,42 @@ func (c *Client) is2K() bool {
func (c *Client) isFloodlight() bool {
return c.model == "HL_CFL2"
}
const (
statusDefault byte = 1
statusENR16 byte = 3
statusENR32 byte = 6
)
func generateChallengeResponse(challengeBytes []byte, enr string, status byte) []byte {
var secretKey []byte
switch status {
case statusDefault:
secretKey = []byte("FFFFFFFFFFFFFFFF")
case statusENR16:
if len(enr) >= 16 {
secretKey = []byte(enr[:16])
} else {
secretKey = make([]byte, 16)
copy(secretKey, enr)
}
case statusENR32:
if len(enr) >= 16 {
firstKey := []byte(enr[:16])
challengeBytes = tutk.XXTEADecryptVar(challengeBytes, firstKey)
}
if len(enr) >= 32 {
secretKey = []byte(enr[16:32])
} else if len(enr) > 16 {
secretKey = make([]byte, 16)
copy(secretKey, []byte(enr[16:]))
} else {
secretKey = []byte("FFFFFFFFFFFFFFFF")
}
default:
secretKey = []byte("FFFFFFFFFFFFFFFF")
}
return tutk.XXTEADecryptVar(challengeBytes, secretKey)
}
-143
View File
@@ -1,143 +0,0 @@
package crypto
import (
"bytes"
"crypto/rand"
"encoding/binary"
"math/bits"
)
const charlie = "Charlie is the designer of P2P!!"
func TransCodePartial(src []byte) []byte {
n := len(src)
tmp := make([]byte, n)
dst := bytes.Clone(src)
src16, tmp16, dst16 := src, tmp, dst
for ; n >= 16; n -= 16 {
for i := 0; i < 16; i += 4 {
x := binary.LittleEndian.Uint32(src16[i:])
binary.LittleEndian.PutUint32(tmp16[i:], bits.RotateLeft32(x, -i-1))
}
for i := range 16 {
dst16[i] = tmp16[i] ^ charlie[i]
}
swap(dst16, tmp16, 16)
for i := 0; i < 16; i += 4 {
x := binary.LittleEndian.Uint32(tmp16[i:])
binary.LittleEndian.PutUint32(dst16[i:], bits.RotateLeft32(x, -i-3))
}
tmp16, dst16, src16 = tmp16[16:], dst16[16:], src16[16:]
}
for i := 0; i < n; i++ {
tmp16[i] = src16[i] ^ charlie[i]
}
swap(tmp16, dst16, n)
return dst
}
func ReverseTransCodePartial(src []byte) []byte {
n := len(src)
tmp := make([]byte, n)
dst := bytes.Clone(src)
src16, tmp16, dst16 := src, tmp, dst
for ; n >= 16; n -= 16 {
for i := 0; i < 16; i += 4 {
x := binary.LittleEndian.Uint32(src16[i:])
binary.LittleEndian.PutUint32(tmp16[i:], bits.RotateLeft32(x, i+3))
}
swap(tmp16, dst16, 16)
for i := range 16 {
tmp16[i] = dst16[i] ^ charlie[i]
}
for i := 0; i < 16; i += 4 {
x := binary.LittleEndian.Uint32(tmp16[i:])
binary.LittleEndian.PutUint32(dst16[i:], bits.RotateLeft32(x, i+1))
}
tmp16, dst16, src16 = tmp16[16:], dst16[16:], src16[16:]
}
swap(src16, tmp16, n)
for i := 0; i < n; i++ {
dst16[i] = tmp16[i] ^ charlie[i]
}
return dst
}
func TransCodeBlob(src []byte) []byte {
if len(src) < 16 {
return TransCodePartial(src)
}
dst := make([]byte, len(src))
header := TransCodePartial(src[:16])
copy(dst, header)
if len(src) > 16 {
if src[3]&1 != 0 { // Partial encryption
remaining := len(src) - 16
encryptLen := min(remaining, 48)
if encryptLen > 0 {
encrypted := TransCodePartial(src[16 : 16+encryptLen])
copy(dst[16:], encrypted)
}
if remaining > 48 {
copy(dst[64:], src[64:])
}
} else { // Full encryption
encrypted := TransCodePartial(src[16:])
copy(dst[16:], encrypted)
}
}
return dst
}
func ReverseTransCodeBlob(src []byte) []byte {
if len(src) < 16 {
return ReverseTransCodePartial(src)
}
dst := make([]byte, len(src))
header := ReverseTransCodePartial(src[:16])
copy(dst, header)
if len(src) > 16 {
if dst[3]&1 != 0 { // Partial encryption (check decrypted header)
remaining := len(src) - 16
decryptLen := min(remaining, 48)
if decryptLen > 0 {
decrypted := ReverseTransCodePartial(src[16 : 16+decryptLen])
copy(dst[16:], decrypted)
}
if remaining > 48 {
copy(dst[64:], src[64:])
}
} else { // Full decryption
decrypted := ReverseTransCodePartial(src[16:])
copy(dst[16:], decrypted)
}
}
return dst
}
func RandRead(b []byte) {
_, _ = rand.Read(b)
}
func swap(src, dst []byte, n int) {
switch n {
case 8:
dst[0], dst[1], dst[2], dst[3] = src[7], src[4], src[3], src[2]
dst[4], dst[5], dst[6], dst[7] = src[1], src[6], src[5], src[0]
case 16:
dst[0], dst[1], dst[2], dst[3] = src[11], src[9], src[8], src[15]
dst[4], dst[5], dst[6], dst[7] = src[13], src[10], src[12], src[14]
dst[8], dst[9], dst[10], dst[11] = src[2], src[1], src[5], src[0]
dst[12], dst[13], dst[14], dst[15] = src[6], src[4], src[7], src[3]
default:
copy(dst, src[:n])
}
}
-147
View File
@@ -1,147 +0,0 @@
package crypto
import (
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"strings"
)
const delta = 0x9e3779b9
const (
StatusDefault byte = 1
StatusENR16 byte = 3
StatusENR32 byte = 6
)
func XXTEADecrypt(data, key []byte) []byte {
if len(data) < 8 || len(key) < 16 {
return nil
}
k := make([]uint32, 4)
for i := range 4 {
k[i] = binary.LittleEndian.Uint32(key[i*4:])
}
n := max(len(data)/4, 2)
v := make([]uint32, n)
for i := 0; i < len(data)/4; i++ {
v[i] = binary.LittleEndian.Uint32(data[i*4:])
}
rounds := 6 + 52/n
sum := uint32(rounds) * delta
y := v[0]
for rounds > 0 {
e := (sum >> 2) & 3
for p := n - 1; p > 0; p-- {
z := v[p-1]
v[p] -= mx(sum, y, z, p, e, k)
y = v[p]
}
z := v[n-1]
v[0] -= mx(sum, y, z, 0, e, k)
y = v[0]
sum -= delta
rounds--
}
result := make([]byte, n*4)
for i := range n {
binary.LittleEndian.PutUint32(result[i*4:], v[i])
}
return result[:len(data)]
}
func XXTEAEncrypt(data, key []byte) []byte {
if len(data) < 8 || len(key) < 16 {
return nil
}
k := make([]uint32, 4)
for i := range 4 {
k[i] = binary.LittleEndian.Uint32(key[i*4:])
}
n := max(len(data)/4, 2)
v := make([]uint32, n)
for i := 0; i < len(data)/4; i++ {
v[i] = binary.LittleEndian.Uint32(data[i*4:])
}
rounds := 6 + 52/n
var sum uint32
z := v[n-1]
for rounds > 0 {
sum += delta
e := (sum >> 2) & 3
for p := 0; p < n-1; p++ {
y := v[p+1]
v[p] += mx(sum, y, z, p, e, k)
z = v[p]
}
y := v[0]
v[n-1] += mx(sum, y, z, n-1, e, k)
z = v[n-1]
rounds--
}
result := make([]byte, n*4)
for i := range n {
binary.LittleEndian.PutUint32(result[i*4:], v[i])
}
return result[:len(data)]
}
func mx(sum, y, z uint32, p int, e uint32, k []uint32) uint32 {
return ((z>>5 ^ y<<2) + (y>>3 ^ z<<4)) ^ ((sum ^ y) + (k[(p&3)^int(e)] ^ z))
}
func GenerateChallengeResponse(challengeBytes []byte, enr string, status byte) []byte {
var secretKey []byte
switch status {
case StatusDefault:
secretKey = []byte("FFFFFFFFFFFFFFFF")
case StatusENR16:
if len(enr) >= 16 {
secretKey = []byte(enr[:16])
} else {
secretKey = make([]byte, 16)
copy(secretKey, enr)
}
case StatusENR32:
if len(enr) >= 16 {
firstKey := []byte(enr[:16])
challengeBytes = XXTEADecrypt(challengeBytes, firstKey)
}
if len(enr) >= 32 {
secretKey = []byte(enr[16:32])
} else if len(enr) > 16 {
secretKey = make([]byte, 16)
copy(secretKey, []byte(enr[16:]))
} else {
secretKey = []byte("FFFFFFFFFFFFFFFF")
}
default:
secretKey = []byte("FFFFFFFFFFFFFFFF")
}
return XXTEADecrypt(challengeBytes, secretKey)
}
func CalculateAuthKey(enr, mac string) []byte {
data := enr + strings.ToUpper(mac)
hash := sha256.Sum256([]byte(data))
b64 := base64.StdEncoding.EncodeToString(hash[:6])
b64 = strings.ReplaceAll(b64, "+", "Z")
b64 = strings.ReplaceAll(b64, "/", "9")
b64 = strings.ReplaceAll(b64, "=", "A")
return []byte(b64)
}
+14 -14
View File
@@ -10,7 +10,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/wyze/tutk"
"github.com/AlexxIT/go2rtc/pkg/tutk"
"github.com/pion/rtp"
)
@@ -96,21 +96,21 @@ func (p *Producer) Start() error {
Payload: annexb.EncodeToAVCC(pkt.Payload),
}
case tutk.AudioCodecG711U:
case tutk.CodecPCMU:
name = core.CodecPCMU
pkt2 = &core.Packet{
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: pkt.Payload,
}
case tutk.AudioCodecG711A:
case tutk.CodecPCMA:
name = core.CodecPCMA
pkt2 = &core.Packet{
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: pkt.Payload,
}
case tutk.AudioCodecAACADTS, tutk.AudioCodecAACWyze, tutk.AudioCodecAACRaw, tutk.AudioCodecAACLATM:
case tutk.CodecAACADTS, tutk.CodecAACAlt, tutk.CodecAACRaw, tutk.CodecAACLATM:
name = core.CodecAAC
payload := pkt.Payload
if aac.IsADTS(payload) {
@@ -121,21 +121,21 @@ func (p *Producer) Start() error {
Payload: payload,
}
case tutk.AudioCodecOpus:
case tutk.CodecOpus:
name = core.CodecOpus
pkt2 = &core.Packet{
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: pkt.Payload,
}
case tutk.AudioCodecPCM:
case tutk.CodecPCML:
name = core.CodecPCML
pkt2 = &core.Packet{
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: pkt.Payload,
}
case tutk.AudioCodecMP3:
case tutk.CodecMP3:
name = core.CodecMP3
pkt2 = &core.Packet{
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
@@ -167,7 +167,7 @@ func probe(client *Client, quality byte) ([]*core.Media, error) {
client.SetDeadline(time.Now().Add(core.ProbeTimeout))
var vcodec, acodec *core.Codec
var tutkAudioCodec uint16
var tutkAudioCodec byte
for {
if client.verbose {
@@ -197,33 +197,33 @@ func probe(client *Client, quality byte) ([]*core.Media, error) {
vcodec = h265.AVCCToCodec(buf)
}
}
case tutk.AudioCodecG711U:
case tutk.CodecPCMU:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecPCMU, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
tutkAudioCodec = pkt.Codec
}
case tutk.AudioCodecG711A:
case tutk.CodecPCMA:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecPCMA, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
tutkAudioCodec = pkt.Codec
}
case tutk.AudioCodecAACWyze, tutk.AudioCodecAACADTS, tutk.AudioCodecAACRaw, tutk.AudioCodecAACLATM:
case tutk.CodecAACAlt, tutk.CodecAACADTS, tutk.CodecAACRaw, tutk.CodecAACLATM:
if acodec == nil {
config := aac.EncodeConfig(aac.TypeAACLC, pkt.SampleRate, pkt.Channels, false)
acodec = aac.ConfigToCodec(config)
tutkAudioCodec = pkt.Codec
}
case tutk.AudioCodecOpus:
case tutk.CodecOpus:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecOpus, ClockRate: 48000, Channels: 2}
tutkAudioCodec = pkt.Codec
}
case tutk.AudioCodecPCM:
case tutk.CodecPCML:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecPCML, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
tutkAudioCodec = pkt.Codec
}
case tutk.AudioCodecMP3:
case tutk.CodecMP3:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecMP3, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
tutkAudioCodec = pkt.Codec
File diff suppressed because it is too large Load Diff
-218
View File
@@ -1,218 +0,0 @@
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 := 0; i < chachaNonceLength; i++ {
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(),
}
}
File diff suppressed because it is too large Load Diff
-120
View File
@@ -1,120 +0,0 @@
package tutk
import (
"net"
"sync"
"time"
"github.com/pion/dtls/v3"
)
func NewDtlsClient(c *Conn, channel uint8, psk []byte) (*dtls.Conn, error) {
adapter := &ChannelAdapter{conn: c, channel: channel}
return dtls.Client(adapter, c.addr, buildDtlsConfig(psk, false))
}
func NewDtlsServer(c *Conn, channel uint8, psk []byte) (*dtls.Conn, error) {
adapter := &ChannelAdapter{conn: c, channel: channel}
return dtls.Server(adapter, c.addr, buildDtlsConfig(psk, true))
}
func buildDtlsConfig(psk []byte, isServer bool) *dtls.Config {
config := &dtls.Config{
PSK: func(hint []byte) ([]byte, error) {
return psk, nil
},
PSKIdentityHint: []byte(PSKIdentity),
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 {
conn *Conn
channel uint8
mu sync.Mutex
readDeadline time.Time
}
func (a *ChannelAdapter) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
var buf chan []byte
if a.channel == IOTCChannelMain {
buf = a.conn.clientBuf
} else {
buf = a.conn.serverBuf
}
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 := <-buf:
return copy(p, data), a.conn.addr, nil
case <-timer.C:
return 0, nil, &timeoutError{}
case <-a.conn.ctx.Done():
return 0, nil, net.ErrClosed
}
}
select {
case data := <-buf:
return copy(p, data), a.conn.addr, nil
case <-a.conn.ctx.Done():
return 0, nil, net.ErrClosed
}
}
func (a *ChannelAdapter) WriteTo(p []byte, _ net.Addr) (int, error) {
if err := a.conn.WriteDTLS(p, a.channel); err != nil {
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 }
-615
View File
@@ -1,615 +0,0 @@
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 (
ResTierLow uint8 = 1 // 360P/SD
ResTierHigh uint8 = 4 // HD/2K
)
const (
Bitrate360P uint8 = 30
BitrateHD uint8 = 100
Bitrate2K uint8 = 200
)
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 uint16 // 0-1
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) Resolution() string {
switch fi.Bitrate {
case Bitrate360P:
return "360P"
case BitrateHD:
return "HD"
case Bitrate2K:
return "2K"
default:
return "unknown"
}
}
func (fi *FrameInfo) SampleRate() uint32 {
idx := (fi.Flags >> 2) & 0x0F
return uint32(SampleRateValue(idx))
}
func (fi *FrameInfo) Channels() uint8 {
if fi.Flags&0x01 == 1 {
return 2
}
return 1
}
func (fi *FrameInfo) IsVideo() bool {
return IsVideoCodec(fi.CodecID)
}
func (fi *FrameInfo) IsAudio() bool {
return IsAudioCodec(fi.CodecID)
}
func ParseFrameInfo(data []byte) *FrameInfo {
if len(data) < FrameInfoSize {
return nil
}
offset := len(data) - FrameInfoSize
fi := data[offset:]
return &FrameInfo{
CodecID: binary.LittleEndian.Uint16(fi),
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 uint16
Timestamp uint32
Payload []byte
IsKeyframe bool
FrameNo uint32
SampleRate uint32
Channels uint8
}
func (p *Packet) IsVideo() bool {
return p.Channel == ChannelIVideo || p.Channel == ChannelPVideo
}
func (p *Packet) IsAudio() bool {
return p.Channel == ChannelAudio
}
type PacketHeader struct {
Channel byte
FrameType byte
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
frameInfoSize := 0
switch frameType {
case FrameTypeStart:
headerSize = 36
case FrameTypeStartAlt:
headerSize = 36
if len(data) >= 22 {
pktTotal := binary.LittleEndian.Uint16(data[20:])
if pktTotal == 1 {
frameInfoSize = FrameInfoSize
}
}
case FrameTypeCont, FrameTypeContAlt:
headerSize = 28
case FrameTypeEndSingle, FrameTypeEndMulti:
headerSize = 28
frameInfoSize = FrameInfoSize
case FrameTypeEndExt:
headerSize = 36
frameInfoSize = FrameInfoSize
default:
headerSize = 28
}
if len(data) < headerSize {
return nil, nil
}
if frameInfoSize == 0 {
return data[headerSize:], nil
}
if len(data) < headerSize+frameInfoSize {
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)-frameInfoSize]
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
}
// Sequential check: 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
}
// Append payload (simple sequential accumulation)
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 {
h.emitVideo(channel, cs)
cs.reset()
}
}
func (h *FrameHandler) emitVideo(channel byte, cs *channelState) {
fi := cs.frameInfo
// Size validation
if fi.PayloadSize > 0 && uint32(len(cs.waitData)) != fi.PayloadSize {
fmt.Printf("[SIZE] ch=0x%02x #%d mismatch: expected %d, got %d\n",
channel, cs.frameNo, fi.PayloadSize, len(cs.waitData))
return
}
if len(cs.waitData) == 0 {
return
}
accumUS := h.videoTS.update(fi.Timestamp)
rtpTS := uint32(accumUS * 90000 / 1000000)
// Copy payload (buffer will be reused)
payload := make([]byte, len(cs.waitData))
copy(payload, cs.waitData)
pkt := &Packet{
Channel: channel,
Payload: payload,
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 %s %s size=%d\n",
channel, fi.FrameNo, CodecName(fi.CodecID), frameType, len(payload))
fmt.Printf(" [0-1]codec=0x%x(%s) [2]flags=0x%x [3]=%d [4]=%d\n",
fi.CodecID, CodecName(fi.CodecID), fi.Flags, fi.CamIndex, fi.OnlineNum)
fmt.Printf(" [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 AudioCodecAACRaw, AudioCodecAACADTS, AudioCodecAACLATM, AudioCodecAACWyze:
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 %s size=%d\n",
fi.FrameNo, AudioCodecName(fi.CodecID), len(payload))
fmt.Printf(" [0-1]codec=0x%x(%s) [2]flags=0x%x(%dHz/%dbit/%dch)\n",
fi.CodecID, AudioCodecName(fi.CodecID), fi.Flags, sampleRate, bits, channels)
fmt.Printf(" [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)
binary.LittleEndian.PutUint16(b[0:], fi.CodecID)
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
}
-281
View File
@@ -1,281 +0,0 @@
package tutk
type AVLoginResponse struct {
ServerType uint32
Resend int32
TwoWayStreaming int32
SyncRecvData int32
SecurityMode uint32
VideoOnConnect int32
AudioOnConnect int32
}
const (
CodecUnknown uint16 = 0x00
CodecMPEG4 uint16 = 0x4C // 76
CodecH263 uint16 = 0x4D // 77
CodecH264 uint16 = 0x4E // 78
CodecMJPEG uint16 = 0x4F // 79
CodecH265 uint16 = 0x50 // 80
)
const (
AudioCodecAACRaw uint16 = 0x86 // 134
AudioCodecAACADTS uint16 = 0x87 // 135
AudioCodecAACLATM uint16 = 0x88 // 136
AudioCodecG711U uint16 = 0x89 // 137
AudioCodecG711A uint16 = 0x8A // 138
AudioCodecADPCM uint16 = 0x8B // 139
AudioCodecPCM uint16 = 0x8C // 140
AudioCodecSPEEX uint16 = 0x8D // 141
AudioCodecMP3 uint16 = 0x8E // 142
AudioCodecG726 uint16 = 0x8F // 143
AudioCodecAACWyze uint16 = 0x90 // 144
AudioCodecOpus uint16 = 0x92 // 146
)
const (
SampleRate8K uint8 = 0x00
SampleRate11K uint8 = 0x01
SampleRate12K uint8 = 0x02
SampleRate16K uint8 = 0x03
SampleRate22K uint8 = 0x04
SampleRate24K uint8 = 0x05
SampleRate32K uint8 = 0x06
SampleRate44K uint8 = 0x07
SampleRate48K uint8 = 0x08
)
var sampleRates = map[uint8]int{
SampleRate8K: 8000,
SampleRate11K: 11025,
SampleRate12K: 12000,
SampleRate16K: 16000,
SampleRate22K: 22050,
SampleRate24K: 24000,
SampleRate32K: 32000,
SampleRate44K: 44100,
SampleRate48K: 48000,
}
var samplesPerFrame = map[uint16]uint32{
AudioCodecAACRaw: 1024,
AudioCodecAACADTS: 1024,
AudioCodecAACLATM: 1024,
AudioCodecAACWyze: 1024,
AudioCodecG711U: 160,
AudioCodecG711A: 160,
AudioCodecPCM: 160,
AudioCodecADPCM: 160,
AudioCodecSPEEX: 160,
AudioCodecMP3: 1152,
AudioCodecG726: 160,
AudioCodecOpus: 960,
}
const (
IOTypeVideoStart = 0x01FF
IOTypeVideoStop = 0x02FF
IOTypeAudioStart = 0x0300
IOTypeAudioStop = 0x0301
IOTypeSpeakerStart = 0x0350
IOTypeSpeakerStop = 0x0351
IOTypeGetAudioOutFormatReq = 0x032A
IOTypeGetAudioOutFormatRes = 0x032B
IOTypeSetStreamCtrlReq = 0x0320
IOTypeSetStreamCtrlRes = 0x0321
IOTypeGetStreamCtrlReq = 0x0322
IOTypeGetStreamCtrlRes = 0x0323
IOTypeDevInfoReq = 0x0340
IOTypeDevInfoRes = 0x0341
IOTypeGetSupportStreamReq = 0x0344
IOTypeGetSupportStreamRes = 0x0345
IOTypeSetRecordReq = 0x0310
IOTypeSetRecordRes = 0x0311
IOTypeGetRecordReq = 0x0312
IOTypeGetRecordRes = 0x0313
IOTypePTZCommand = 0x1001
IOTypeReceiveFirstFrame = 0x1002
IOTypeGetEnvironmentReq = 0x030A
IOTypeGetEnvironmentRes = 0x030B
IOTypeSetVideoModeReq = 0x030C
IOTypeSetVideoModeRes = 0x030D
IOTypeGetVideoModeReq = 0x030E
IOTypeGetVideoModeRes = 0x030F
IOTypeSetTimeReq = 0x0316
IOTypeSetTimeRes = 0x0317
IOTypeGetTimeReq = 0x0318
IOTypeGetTimeRes = 0x0319
IOTypeSetWifiReq = 0x0102
IOTypeSetWifiRes = 0x0103
IOTypeGetWifiReq = 0x0104
IOTypeGetWifiRes = 0x0105
IOTypeListWifiAPReq = 0x0106
IOTypeListWifiAPRes = 0x0107
IOTypeSetMotionDetectReq = 0x0306
IOTypeSetMotionDetectRes = 0x0307
IOTypeGetMotionDetectReq = 0x0308
IOTypeGetMotionDetectRes = 0x0309
)
// OLD Protocol (IOTC/TransCode)
const (
CmdDiscoReq uint16 = 0x0601
CmdDiscoRes uint16 = 0x0602
CmdSessionReq uint16 = 0x0402
CmdSessionRes uint16 = 0x0404
CmdDataTX uint16 = 0x0407
CmdDataRX uint16 = 0x0408
CmdKeepaliveReq uint16 = 0x0427
CmdKeepaliveRes uint16 = 0x0428
OldHeaderSize = 16
OldDiscoBodySize = 72
OldDiscoSize = OldHeaderSize + OldDiscoBodySize
OldSessionBody = 36
OldSessionSize = OldHeaderSize + OldSessionBody
)
// NEW Protocol (0xCC51)
const (
MagicNewProto uint16 = 0xCC51
CmdNewDisco uint16 = 0x1002
CmdNewKeepalive uint16 = 0x1202
CmdNewClose uint16 = 0x1302
CmdNewDTLS uint16 = 0x1502
NewPayloadSize uint16 = 0x0028
NewPacketSize = 52
NewHeaderSize = 28
NewAuthSize = 20
NewKeepaliveSize = 48
)
const (
UIDSize = 20
RandIDSize = 8
)
const (
MagicAVLoginResp uint16 = 0x2100
MagicIOCtrl uint16 = 0x7000
MagicChannelMsg uint16 = 0x1000
MagicACK uint16 = 0x0009
MagicAVLogin1 uint16 = 0x0000
MagicAVLogin2 uint16 = 0x2000
)
const (
ProtoVersion uint16 = 0x000c
DefaultCaps uint32 = 0x001f07fb
)
const (
IOTCChannelMain = 0 // Main AV (we = DTLS Client)
IOTCChannelBack = 1 // Backchannel (we = DTLS Server)
)
const (
PSKIdentity = "AUTHPWD_admin"
DefaultUser = "admin"
DefaultPort = 32761
)
func CodecName(id uint16) string {
switch id {
case CodecH264:
return "H264"
case CodecH265:
return "H265"
case CodecMPEG4:
return "MPEG4"
case CodecH263:
return "H263"
case CodecMJPEG:
return "MJPEG"
default:
return "Unknown"
}
}
func AudioCodecName(id uint16) string {
switch id {
case AudioCodecG711U:
return "PCMU"
case AudioCodecG711A:
return "PCMA"
case AudioCodecPCM:
return "PCM"
case AudioCodecAACLATM, AudioCodecAACRaw, AudioCodecAACADTS, AudioCodecAACWyze:
return "AAC"
case AudioCodecOpus:
return "Opus"
case AudioCodecSPEEX:
return "Speex"
case AudioCodecMP3:
return "MP3"
case AudioCodecG726:
return "G726"
case AudioCodecADPCM:
return "ADPCM"
default:
return "Unknown"
}
}
func SampleRateValue(idx uint8) int {
if rate, ok := sampleRates[idx]; ok {
return rate
}
return 16000
}
func SampleRateIndex(hz uint32) uint8 {
switch hz {
case 8000:
return SampleRate8K
case 11025:
return SampleRate11K
case 12000:
return SampleRate12K
case 16000:
return SampleRate16K
case 22050:
return SampleRate22K
case 24000:
return SampleRate24K
case 32000:
return SampleRate32K
case 44100:
return SampleRate44K
case 48000:
return SampleRate48K
default:
return SampleRate16K
}
}
func BuildAudioFlags(sampleRate uint32, bits16, stereo bool) uint8 {
flags := SampleRateIndex(sampleRate) << 2
if bits16 {
flags |= 0x02
}
if stereo {
flags |= 0x01
}
return flags
}
func IsVideoCodec(id uint16) bool {
return id >= CodecMPEG4 && id <= CodecH265
}
func IsAudioCodec(id uint16) bool {
return id >= AudioCodecAACRaw && id <= AudioCodecOpus
}
func GetSamplesPerFrame(codecID uint16) uint32 {
if samples, ok := samplesPerFrame[codecID]; ok {
return samples
}
return 1024
}