refactor
This commit is contained in:
+141
-247
@@ -14,6 +14,47 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/wyze/tutk"
|
"github.com/AlexxIT/go2rtc/pkg/wyze/tutk"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FrameSize1080P = 0
|
||||||
|
FrameSize360P = 1
|
||||||
|
FrameSize720P = 2
|
||||||
|
FrameSize2K = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
BitrateMax uint16 = 0xF0
|
||||||
|
BitrateSD uint16 = 0x3C
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
QualityUnknown = 0
|
||||||
|
QualityMax = 1
|
||||||
|
QualityHigh = 2
|
||||||
|
QualityMiddle = 3
|
||||||
|
QualityLow = 4
|
||||||
|
QualityMin = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MediaTypeVideo = 1
|
||||||
|
MediaTypeAudio = 2
|
||||||
|
MediaTypeReturnAudio = 3
|
||||||
|
MediaTypeRDT = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
KCmdAuth = 10000
|
||||||
|
KCmdChallenge = 10001
|
||||||
|
KCmdChallengeResp = 10002
|
||||||
|
KCmdAuthResult = 10003
|
||||||
|
KCmdAuthWithPayload = 10008
|
||||||
|
KCmdAuthSuccess = 10009
|
||||||
|
KCmdControlChannel = 10010
|
||||||
|
KCmdControlChannelResp = 10011
|
||||||
|
KCmdSetResolution = 10056
|
||||||
|
KCmdSetResolutionResp = 10057
|
||||||
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
conn *tutk.Conn
|
conn *tutk.Conn
|
||||||
|
|
||||||
@@ -36,6 +77,11 @@ type Client struct {
|
|||||||
audioChannels uint8
|
audioChannels uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthResponse struct {
|
||||||
|
ConnectionRes string `json:"connectionRes"`
|
||||||
|
CameraInfo map[string]any `json:"cameraInfo"`
|
||||||
|
}
|
||||||
|
|
||||||
func Dial(rawURL string) (*Client, error) {
|
func Dial(rawURL string) (*Client, error) {
|
||||||
u, err := url.Parse(rawURL)
|
u, err := url.Parse(rawURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -107,11 +153,11 @@ func (c *Client) SetResolution(sd bool) error {
|
|||||||
var bitrate uint16
|
var bitrate uint16
|
||||||
|
|
||||||
if sd {
|
if sd {
|
||||||
frameSize = tutk.FrameSize360P
|
frameSize = FrameSize360P
|
||||||
bitrate = tutk.BitrateSD
|
bitrate = BitrateSD
|
||||||
} else {
|
} else {
|
||||||
frameSize = tutk.FrameSize2K
|
frameSize = FrameSize2K
|
||||||
bitrate = tutk.BitrateMax
|
bitrate = BitrateMax
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.verbose {
|
if c.verbose {
|
||||||
@@ -119,120 +165,33 @@ func (c *Client) SetResolution(sd bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
k10056 := c.buildK10056(frameSize, bitrate)
|
k10056 := c.buildK10056(frameSize, bitrate)
|
||||||
if err := c.conn.SendIOCtrl(tutk.KCmdSetResolution, k10056); err != nil {
|
_, err := c.conn.WriteAndWaitIOCtrl(KCmdSetResolution, k10056, KCmdSetResolutionResp, 5*time.Second)
|
||||||
return fmt.Errorf("wyze: K10056 send failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for K10057 response
|
|
||||||
cmdID, data, err := c.conn.RecvIOCtrl(1 * time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
if c.verbose {
|
|
||||||
fmt.Printf("[Wyze] SetResolution response: K%d (%d bytes)\n", cmdID, len(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmdID == tutk.KCmdSetResolutionResp && len(data) >= 17 {
|
|
||||||
result := data[16]
|
|
||||||
if c.verbose {
|
|
||||||
fmt.Printf("[Wyze] K10057 result: %d\n", result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) StartVideo() error {
|
func (c *Client) StartVideo() error {
|
||||||
k10010 := c.buildK10010(tutk.MediaTypeVideo, true)
|
k10010 := c.buildK10010(MediaTypeVideo, true)
|
||||||
if c.verbose {
|
_, err := c.conn.WriteAndWaitIOCtrl(KCmdControlChannel, k10010, KCmdControlChannelResp, 5*time.Second)
|
||||||
fmt.Printf("[Wyze] TX K10010 video (%d bytes): % x\n", len(k10010), k10010)
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.conn.SendIOCtrl(tutk.KCmdControlChannel, k10010); err != nil {
|
|
||||||
return fmt.Errorf("K10010 video send failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for K10011 response
|
|
||||||
cmdID, data, err := c.conn.RecvIOCtrl(5 * time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("K10011 video recv failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.verbose {
|
|
||||||
fmt.Printf("[Wyze] K10011 video response: cmdID=%d, len=%d\n", cmdID, len(data))
|
|
||||||
if len(data) >= 18 {
|
|
||||||
fmt.Printf("[Wyze] K10011 video: media=%d status=%d\n", data[16], data[17])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) StartAudio() error {
|
func (c *Client) StartAudio() error {
|
||||||
k10010 := c.buildK10010(tutk.MediaTypeAudio, true)
|
k10010 := c.buildK10010(MediaTypeAudio, true)
|
||||||
if c.verbose {
|
_, err := c.conn.WriteAndWaitIOCtrl(KCmdControlChannel, k10010, KCmdControlChannelResp, 5*time.Second)
|
||||||
fmt.Printf("[Wyze] TX K10010 audio (%d bytes): % x\n", len(k10010), k10010)
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.conn.SendIOCtrl(tutk.KCmdControlChannel, k10010); err != nil {
|
|
||||||
return fmt.Errorf("K10010 audio send failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for K10011 response
|
|
||||||
cmdID, data, err := c.conn.RecvIOCtrl(5 * time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("K10011 audio recv failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.verbose {
|
|
||||||
fmt.Printf("[Wyze] K10011 audio response: cmdID=%d, len=%d\n", cmdID, len(data))
|
|
||||||
if len(data) >= 18 {
|
|
||||||
fmt.Printf("[Wyze] K10011 audio: media=%d status=%d\n", data[16], data[17])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) StartIntercom() error {
|
func (c *Client) StartIntercom() error {
|
||||||
if c.conn.IsBackchannelReady() {
|
if c.conn.IsBackchannelReady() {
|
||||||
return nil // Already enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.verbose {
|
|
||||||
fmt.Printf("[Wyze] Sending K10010 (enable return audio)\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
k10010 := c.buildK10010(tutk.MediaTypeReturnAudio, true)
|
|
||||||
if err := c.conn.SendIOCtrl(tutk.KCmdControlChannel, k10010); err != nil {
|
|
||||||
return fmt.Errorf("K10010 send failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for K10011 response
|
|
||||||
cmdID, data, err := c.conn.RecvIOCtrl(5 * time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("K10011 recv failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.verbose {
|
|
||||||
fmt.Printf("[Wyze] K10011 response: cmdID=%d, len=%d\n", cmdID, len(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform DTLS server handshake on backchannel (camera connects to us)
|
|
||||||
if c.verbose {
|
|
||||||
fmt.Printf("[Wyze] Starting speaker channel DTLS handshake\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.conn.AVServStart(); err != nil {
|
|
||||||
return fmt.Errorf("speaker channel handshake failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.verbose {
|
|
||||||
fmt.Printf("[Wyze] Backchannel ready\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
k10010 := c.buildK10010(MediaTypeReturnAudio, true)
|
||||||
|
if _, err := c.conn.WriteAndWaitIOCtrl(KCmdControlChannel, k10010, KCmdControlChannelResp, 5*time.Second); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.conn.AVServStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ReadPacket() (*tutk.Packet, error) {
|
func (c *Client) ReadPacket() (*tutk.Packet, error) {
|
||||||
@@ -324,23 +283,10 @@ func (c *Client) doAVLogin() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) doKAuth() error {
|
func (c *Client) doKAuth() error {
|
||||||
if c.verbose {
|
// Step 1: K10000 -> K10001
|
||||||
fmt.Printf("[Wyze] Starting K-command authentication\n")
|
data, err := c.conn.WriteAndWaitIOCtrl(KCmdAuth, c.buildK10000(), KCmdChallenge, 10*time.Second)
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1: Send K10000
|
|
||||||
k10000 := c.buildK10000()
|
|
||||||
if err := c.conn.SendIOCtrl(tutk.KCmdAuth, k10000); err != nil {
|
|
||||||
return fmt.Errorf("wyze: K10000 send failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Wait for K10001
|
|
||||||
cmdID, data, err := c.conn.RecvIOCtrl(10 * time.Second)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wyze: K10001 recv failed: %w", err)
|
return fmt.Errorf("wyze: K10001 failed: %w", err)
|
||||||
}
|
|
||||||
if cmdID != tutk.KCmdChallenge {
|
|
||||||
return fmt.Errorf("wyze: expected K10001, got K%d", cmdID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
challenge, status, err := c.parseK10001(data)
|
challenge, status, err := c.parseK10001(data)
|
||||||
@@ -348,45 +294,18 @@ func (c *Client) doKAuth() error {
|
|||||||
return fmt.Errorf("wyze: K10001 parse failed: %w", err)
|
return fmt.Errorf("wyze: K10001 parse failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.verbose {
|
// Step 2: K10002 -> K10009
|
||||||
fmt.Printf("[Wyze] K10001 received, status=%d\n", status)
|
data, err = c.conn.WriteAndWaitIOCtrl(KCmdChallengeResp, c.buildK10002(challenge, status), KCmdAuthResult, 10*time.Second)
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Send K10008
|
|
||||||
k10008 := c.buildK10008(challenge, status)
|
|
||||||
|
|
||||||
if err := c.conn.SendIOCtrl(tutk.KCmdChallengeResp, k10008); err != nil {
|
|
||||||
return fmt.Errorf("wyze: K10008 send failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4: Wait for K10009
|
|
||||||
cmdID, data, err = c.conn.RecvIOCtrl(10 * time.Second)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wyze: K10009 recv failed: %w", err)
|
return fmt.Errorf("wyze: K10009 failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmdID != tutk.KCmdAuthSuccess {
|
authResp, _ := c.parseK10003(data)
|
||||||
return fmt.Errorf("wyze: expected K10009, got K%d", cmdID)
|
|
||||||
}
|
|
||||||
|
|
||||||
authResp, err := c.parseK10009(data)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("wyze: K10009 parse failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse capabilities
|
|
||||||
if authResp != nil && authResp.CameraInfo != nil {
|
if authResp != nil && authResp.CameraInfo != nil {
|
||||||
if c.verbose {
|
|
||||||
fmt.Printf("[Wyze] CameraInfo authResp: ")
|
|
||||||
b, _ := json.Marshal(authResp)
|
|
||||||
fmt.Printf("%s\n", b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audio receiving support
|
|
||||||
if audio, ok := authResp.CameraInfo["audio"].(bool); ok {
|
if audio, ok := authResp.CameraInfo["audio"].(bool); ok {
|
||||||
c.hasAudio = audio
|
c.hasAudio = audio
|
||||||
} else {
|
} else {
|
||||||
c.hasAudio = true // Default to true
|
c.hasAudio = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.hasAudio = true
|
c.hasAudio = true
|
||||||
@@ -394,9 +313,6 @@ func (c *Client) doKAuth() error {
|
|||||||
|
|
||||||
if avResp := c.conn.GetAVLoginResponse(); avResp != nil {
|
if avResp := c.conn.GetAVLoginResponse(); avResp != nil {
|
||||||
c.hasIntercom = avResp.TwoWayStreaming == 1
|
c.hasIntercom = avResp.TwoWayStreaming == 1
|
||||||
if c.verbose {
|
|
||||||
fmt.Printf("[Wyze] two_way_streaming=%d (from AV Login Response)\n", avResp.TwoWayStreaming)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.verbose {
|
if c.verbose {
|
||||||
@@ -407,94 +323,72 @@ func (c *Client) doKAuth() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) buildK10000() []byte {
|
func (c *Client) buildK10000() []byte {
|
||||||
// 137 = G.711 μ-law (PCMU)
|
json := []byte(`{"cameraInfo":{"audioEncoderList":[137,138,140]}}`) // 137=PCMU, 138=PCMA, 140=PCM
|
||||||
// 138 = G.711 A-law (PCMA)
|
b := make([]byte, 16+len(json))
|
||||||
// 140 = PCM 16-bit
|
copy(b, "HL") // magic
|
||||||
jsonPayload := []byte(`{"cameraInfo":{"audioEncoderList":[137,138,140]}}`)
|
b[2] = 5 // version
|
||||||
|
binary.LittleEndian.PutUint16(b[4:], KCmdAuth) // 10000
|
||||||
buf := make([]byte, 16+len(jsonPayload))
|
binary.LittleEndian.PutUint16(b[6:], uint16(len(json))) // payload len
|
||||||
buf[0] = 'H'
|
copy(b[16:], json)
|
||||||
buf[1] = 'L'
|
return b
|
||||||
buf[2] = 5
|
|
||||||
binary.LittleEndian.PutUint16(buf[4:], tutk.KCmdAuth)
|
|
||||||
binary.LittleEndian.PutUint16(buf[6:], uint16(len(jsonPayload)))
|
|
||||||
copy(buf[16:], jsonPayload)
|
|
||||||
return buf
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) buildK10002(challenge []byte, status byte) []byte {
|
func (c *Client) buildK10002(challenge []byte, status byte) []byte {
|
||||||
response := crypto.GenerateChallengeResponse(challenge, c.enr, status)
|
resp := crypto.GenerateChallengeResponse(challenge, c.enr, status)
|
||||||
|
b := make([]byte, 38)
|
||||||
buf := make([]byte, 38)
|
copy(b, "HL") // magic
|
||||||
buf[0] = 'H'
|
b[2] = 5 // version
|
||||||
buf[1] = 'L'
|
binary.LittleEndian.PutUint16(b[4:], KCmdChallengeResp) // 10002
|
||||||
buf[2] = 5
|
b[6] = 22 // payload len
|
||||||
binary.LittleEndian.PutUint16(buf[4:], tutk.KCmdChallengeResp)
|
copy(b[16:], resp[:16]) // challenge response
|
||||||
buf[6] = 22 // Payload length
|
copy(b[32:], c.uid[:4]) // UID prefix
|
||||||
|
b[36] = 1 // video enabled
|
||||||
if len(response) >= 16 {
|
b[37] = 1 // audio enabled
|
||||||
copy(buf[16:], response[:16])
|
return b
|
||||||
}
|
|
||||||
|
|
||||||
if len(c.uid) >= 4 {
|
|
||||||
copy(buf[32:], c.uid[:4])
|
|
||||||
}
|
|
||||||
|
|
||||||
buf[36] = 1 // Video flag (0 = disabled, 1 = enabled > will start video stream immediately)
|
|
||||||
buf[37] = 1 // Audio flag (0 = disabled, 1 = enabled > will start audio stream immediately)
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) buildK10008(challenge []byte, status byte) []byte {
|
func (c *Client) buildK10008(challenge []byte, status byte) []byte {
|
||||||
response := crypto.GenerateChallengeResponse(challenge, c.enr, status)
|
resp := crypto.GenerateChallengeResponse(challenge, c.enr, status)
|
||||||
openUserID := []byte(c.enr)
|
userID := []byte(c.enr)
|
||||||
payloadLen := 16 + 4 + 1 + 1 + 1 + len(openUserID)
|
payloadLen := 16 + 4 + 1 + 1 + 1 + len(userID)
|
||||||
|
b := make([]byte, 16+payloadLen)
|
||||||
buf := make([]byte, 16+payloadLen)
|
copy(b, "HL") // magic
|
||||||
buf[0] = 'H'
|
b[2] = 5 // version
|
||||||
buf[1] = 'L'
|
binary.LittleEndian.PutUint16(b[4:], KCmdAuthWithPayload) // 10008
|
||||||
buf[2] = 5 // Protocol version
|
binary.LittleEndian.PutUint16(b[6:], uint16(payloadLen)) // payload len
|
||||||
binary.LittleEndian.PutUint16(buf[4:], tutk.KCmdAuthWithPayload) // 10008
|
copy(b[16:], resp[:16]) // challenge response
|
||||||
binary.LittleEndian.PutUint16(buf[6:], uint16(payloadLen))
|
copy(b[32:], c.uid[:4]) // UID prefix
|
||||||
|
b[36] = 1 // video enabled
|
||||||
copy(buf[16:], response[:16]) // Challenge response
|
b[37] = 1 // audio enabled
|
||||||
copy(buf[32:], c.uid[:4]) // UID prefix
|
b[38] = byte(len(userID)) // userID len
|
||||||
buf[36] = 1 // Video enabled
|
copy(b[39:], userID) // userID
|
||||||
buf[37] = 1 // Audio enabled
|
return b
|
||||||
buf[38] = byte(len(openUserID))
|
|
||||||
copy(buf[39:], openUserID)
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) buildK10010(mediaType byte, enabled bool) []byte {
|
func (c *Client) buildK10010(mediaType byte, enabled bool) []byte {
|
||||||
buf := make([]byte, 18)
|
b := make([]byte, 18)
|
||||||
buf[0] = 'H'
|
copy(b, "HL") // magic
|
||||||
buf[1] = 'L'
|
b[2] = 5 // version
|
||||||
buf[2] = 5 // Version
|
binary.LittleEndian.PutUint16(b[4:], KCmdControlChannel) // 10010
|
||||||
binary.LittleEndian.PutUint16(buf[4:], tutk.KCmdControlChannel) // 0x271a = 10010
|
binary.LittleEndian.PutUint16(b[6:], 2) // payload len
|
||||||
binary.LittleEndian.PutUint16(buf[6:], 2) // Payload length = 2
|
b[16] = mediaType // 1=video, 2=audio, 3=return audio
|
||||||
buf[16] = mediaType // 1=Video, 2=Audio, 3=ReturnAudio
|
b[17] = 1 // 1=enable, 2=disable
|
||||||
if enabled {
|
if !enabled {
|
||||||
buf[17] = 1
|
b[17] = 2
|
||||||
} else {
|
|
||||||
buf[17] = 2
|
|
||||||
}
|
}
|
||||||
return buf
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) buildK10056(frameSize uint8, bitrate uint16) []byte {
|
func (c *Client) buildK10056(frameSize uint8, bitrate uint16) []byte {
|
||||||
buf := make([]byte, 21)
|
b := make([]byte, 21)
|
||||||
buf[0] = 'H'
|
copy(b, "HL") // magic
|
||||||
buf[1] = 'L'
|
b[2] = 5 // version
|
||||||
buf[2] = 5 // Version
|
binary.LittleEndian.PutUint16(b[4:], KCmdSetResolution) // 10056
|
||||||
binary.LittleEndian.PutUint16(buf[4:], tutk.KCmdSetResolution) // 0x2748 = 10056
|
binary.LittleEndian.PutUint16(b[6:], 5) // payload len
|
||||||
binary.LittleEndian.PutUint16(buf[6:], 5) // Payload length = 5
|
b[16] = frameSize + 1 // frame size
|
||||||
buf[16] = frameSize + 1 // 4 = HD
|
binary.LittleEndian.PutUint16(b[17:], bitrate) // bitrate
|
||||||
binary.LittleEndian.PutUint16(buf[17:], bitrate) // 0x00f0 = 240
|
// b[19:21] = FPS (0 = auto)
|
||||||
// buf[19], buf[20] = FPS (0 = auto)
|
return b
|
||||||
return buf
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) parseK10001(data []byte) (challenge []byte, status byte, err error) {
|
func (c *Client) parseK10001(data []byte) (challenge []byte, status byte, err error) {
|
||||||
@@ -511,7 +405,7 @@ func (c *Client) parseK10001(data []byte) (challenge []byte, status byte, err er
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmdID := binary.LittleEndian.Uint16(data[4:])
|
cmdID := binary.LittleEndian.Uint16(data[4:])
|
||||||
if cmdID != tutk.KCmdChallenge {
|
if cmdID != KCmdChallenge {
|
||||||
return nil, 0, fmt.Errorf("expected cmdID 10001, got %d", cmdID)
|
return nil, 0, fmt.Errorf("expected cmdID 10001, got %d", cmdID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,31 +416,31 @@ func (c *Client) parseK10001(data []byte) (challenge []byte, status byte, err er
|
|||||||
return challenge, status, nil
|
return challenge, status, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) parseK10003(data []byte) (*tutk.AuthResponse, error) {
|
func (c *Client) parseK10003(data []byte) (*AuthResponse, error) {
|
||||||
if c.verbose {
|
if c.verbose {
|
||||||
fmt.Printf("[Wyze] parseK10003: received %d bytes\n", len(data))
|
fmt.Printf("[Wyze] parseK10003: received %d bytes\n", len(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(data) < 16 {
|
if len(data) < 16 {
|
||||||
return &tutk.AuthResponse{}, nil
|
return &AuthResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if data[0] != 'H' || data[1] != 'L' {
|
if data[0] != 'H' || data[1] != 'L' {
|
||||||
return &tutk.AuthResponse{}, nil
|
return &AuthResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdID := binary.LittleEndian.Uint16(data[4:])
|
cmdID := binary.LittleEndian.Uint16(data[4:])
|
||||||
textLen := binary.LittleEndian.Uint16(data[6:])
|
textLen := binary.LittleEndian.Uint16(data[6:])
|
||||||
|
|
||||||
if cmdID != tutk.KCmdAuthResult {
|
if cmdID != KCmdAuthResult {
|
||||||
return &tutk.AuthResponse{}, nil
|
return &AuthResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(data) > 16 && textLen > 0 {
|
if len(data) > 16 && textLen > 0 {
|
||||||
jsonData := data[16:]
|
jsonData := data[16:]
|
||||||
for i := range jsonData {
|
for i := range jsonData {
|
||||||
if jsonData[i] == '{' {
|
if jsonData[i] == '{' {
|
||||||
var resp tutk.AuthResponse
|
var resp AuthResponse
|
||||||
if err := json.Unmarshal(jsonData[i:], &resp); err == nil {
|
if err := json.Unmarshal(jsonData[i:], &resp); err == nil {
|
||||||
if c.verbose {
|
if c.verbose {
|
||||||
fmt.Printf("[Wyze] parseK10003: parsed JSON\n")
|
fmt.Printf("[Wyze] parseK10003: parsed JSON\n")
|
||||||
@@ -558,34 +452,34 @@ func (c *Client) parseK10003(data []byte) (*tutk.AuthResponse, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &tutk.AuthResponse{}, nil
|
return &AuthResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) parseK10009(data []byte) (*tutk.AuthResponse, error) {
|
func (c *Client) parseK10009(data []byte) (*AuthResponse, error) {
|
||||||
if c.verbose {
|
if c.verbose {
|
||||||
fmt.Printf("[Wyze] parseK10009: received %d bytes\n", len(data))
|
fmt.Printf("[Wyze] parseK10009: received %d bytes\n", len(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(data) < 16 {
|
if len(data) < 16 {
|
||||||
return &tutk.AuthResponse{}, nil
|
return &AuthResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if data[0] != 'H' || data[1] != 'L' {
|
if data[0] != 'H' || data[1] != 'L' {
|
||||||
return &tutk.AuthResponse{}, nil
|
return &AuthResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdID := binary.LittleEndian.Uint16(data[4:])
|
cmdID := binary.LittleEndian.Uint16(data[4:])
|
||||||
textLen := binary.LittleEndian.Uint16(data[6:])
|
textLen := binary.LittleEndian.Uint16(data[6:])
|
||||||
|
|
||||||
if cmdID != tutk.KCmdAuthSuccess {
|
if cmdID != KCmdAuthSuccess {
|
||||||
return &tutk.AuthResponse{}, nil
|
return &AuthResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(data) > 16 && textLen > 0 {
|
if len(data) > 16 && textLen > 0 {
|
||||||
jsonData := data[16:]
|
jsonData := data[16:]
|
||||||
for i := range jsonData {
|
for i := range jsonData {
|
||||||
if jsonData[i] == '{' {
|
if jsonData[i] == '{' {
|
||||||
var resp tutk.AuthResponse
|
var resp AuthResponse
|
||||||
if err := json.Unmarshal(jsonData[i:], &resp); err == nil {
|
if err := json.Unmarshal(jsonData[i:], &resp); err == nil {
|
||||||
if c.verbose {
|
if c.verbose {
|
||||||
fmt.Printf("[Wyze] parseK10009: parsed JSON\n")
|
fmt.Printf("[Wyze] parseK10009: parsed JSON\n")
|
||||||
@@ -597,5 +491,5 @@ func (c *Client) parseK10009(data []byte) (*tutk.AuthResponse, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &tutk.AuthResponse{}, nil
|
return &AuthResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
package tutk
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
|
||||||
)
|
|
||||||
|
|
||||||
const FrameInfoSize = 40
|
|
||||||
|
|
||||||
// Wire format (little-endian) - Wyze extended FRAMEINFO:
|
|
||||||
//
|
|
||||||
// [0-1] codec_id uint16 (0x004e=H.264, 0x0050=H.265, 0x0088=AAC)
|
|
||||||
// [2] flags uint8 (Video: 0=P/1=I, Audio: sr_idx<<2|bits16<<1|ch)
|
|
||||||
// [3] cam_index uint8
|
|
||||||
// [4] online_num uint8
|
|
||||||
// [5] framerate uint8 (FPS, e.g. 20)
|
|
||||||
// [6] frame_size uint8 (Resolution: 1=1080P, 2=360P, 4=2K)
|
|
||||||
// [7] bitrate uint8 (e.g. 0xF0=240)
|
|
||||||
// [8-11] timestamp_us uint32 (microseconds component)
|
|
||||||
// [12-15] timestamp uint32 (Unix timestamp in seconds)
|
|
||||||
// [16-19] payload_sz uint32 (frame payload size)
|
|
||||||
// [20-23] frame_no uint32 (frame number)
|
|
||||||
// [24-39] device_id 16 bytes (MAC address + padding)
|
|
||||||
type FrameInfo struct {
|
|
||||||
CodecID uint16
|
|
||||||
Flags uint8
|
|
||||||
CamIndex uint8
|
|
||||||
OnlineNum uint8
|
|
||||||
Framerate uint8 // FPS (e.g. 20)
|
|
||||||
FrameSize uint8 // Resolution: 1=1080P, 2=360P, 4=2K
|
|
||||||
Bitrate uint8 // Bitrate value (e.g. 240)
|
|
||||||
TimestampUS uint32
|
|
||||||
Timestamp uint32
|
|
||||||
PayloadSize uint32
|
|
||||||
FrameNo uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolution constants (as received in FrameSize field)
|
|
||||||
// Note: Some cameras only support 2K + 360P, others support 1080P + 360P
|
|
||||||
// The actual resolution depends on camera model!
|
|
||||||
const (
|
|
||||||
ResolutionUnknown = 0
|
|
||||||
ResolutionSD = 1 // 360P (640x360) on 2K cameras, or 1080P on older cams
|
|
||||||
Resolution360P = 2 // 360P (640x360)
|
|
||||||
Resolution2K = 4 // 2K (2560x1440)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (fi *FrameInfo) IsKeyframe() bool {
|
|
||||||
return fi.Flags == 0x01
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolution returns a human-readable resolution string
|
|
||||||
func (fi *FrameInfo) Resolution() string {
|
|
||||||
switch fi.FrameSize {
|
|
||||||
case ResolutionSD:
|
|
||||||
return "SD" // Could be 360P or 1080P depending on camera
|
|
||||||
case Resolution360P:
|
|
||||||
return "360P"
|
|
||||||
case Resolution2K:
|
|
||||||
return "2K"
|
|
||||||
default:
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fi *FrameInfo) SampleRate() uint32 {
|
|
||||||
srIdx := (fi.Flags >> 2) & 0x0F
|
|
||||||
return uint32(SampleRateValue(srIdx))
|
|
||||||
}
|
|
||||||
|
|
||||||
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],
|
|
||||||
Framerate: fi[5],
|
|
||||||
FrameSize: fi[6],
|
|
||||||
Bitrate: fi[7],
|
|
||||||
TimestampUS: binary.LittleEndian.Uint32(fi[8:]),
|
|
||||||
Timestamp: binary.LittleEndian.Uint32(fi[12:]),
|
|
||||||
PayloadSize: binary.LittleEndian.Uint32(fi[16:]),
|
|
||||||
FrameNo: binary.LittleEndian.Uint32(fi[20:]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseAudioParams(payload []byte, fi *FrameInfo) (sampleRate uint32, channels uint8) {
|
|
||||||
// Try ADTS header first (more reliable than FRAMEINFO flags)
|
|
||||||
if aac.IsADTS(payload) {
|
|
||||||
codec := aac.ADTSToCodec(payload)
|
|
||||||
if codec != nil {
|
|
||||||
return codec.ClockRate, codec.Channels
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to FRAMEINFO flags
|
|
||||||
if fi != nil {
|
|
||||||
return fi.SampleRate(), fi.Channels()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default values
|
|
||||||
return 16000, 1
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package tutk
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ChannelAdapter struct {
|
|
||||||
conn *Conn
|
|
||||||
channel uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ChannelAdapter) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
|
||||||
var buf chan []byte
|
|
||||||
if a.channel == IOTCChannelMain {
|
|
||||||
buf = a.conn.mainBuf
|
|
||||||
} else {
|
|
||||||
buf = a.conn.speakerBuf
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case data := <-buf:
|
|
||||||
n = copy(p, data)
|
|
||||||
if a.conn.verbose && len(data) >= 1 {
|
|
||||||
fmt.Printf("[ChannelAdapter] ch=%d ReadFrom: len=%d contentType=%d\n",
|
|
||||||
a.channel, len(data), data[0])
|
|
||||||
}
|
|
||||||
return n, a.conn.addr, nil
|
|
||||||
case <-a.conn.done:
|
|
||||||
return 0, nil, net.ErrClosed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ChannelAdapter) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
|
||||||
if a.conn.verbose {
|
|
||||||
fmt.Printf("[IOTC TX] channel=%d size=%d\n", a.channel, len(p))
|
|
||||||
}
|
|
||||||
_, err = a.conn.sendIOTC(p, a.channel)
|
|
||||||
if 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{IP: net.IPv4(0, 0, 0, 0), Port: 0}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ChannelAdapter) SetDeadline(time.Time) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ChannelAdapter) SetReadDeadline(time.Time) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ChannelAdapter) SetWriteDeadline(time.Time) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
+482
-1368
File diff suppressed because it is too large
Load Diff
@@ -1,306 +0,0 @@
|
|||||||
package tutk
|
|
||||||
|
|
||||||
const (
|
|
||||||
CodecUnknown uint16 = 0x00 // Unknown codec
|
|
||||||
CodecMPEG4 uint16 = 0x4C // 76 - MPEG4
|
|
||||||
CodecH263 uint16 = 0x4D // 77 - H.263
|
|
||||||
CodecH264 uint16 = 0x4E // 78 - H.264/AVC
|
|
||||||
CodecMJPEG uint16 = 0x4F // 79 - MJPEG
|
|
||||||
CodecH265 uint16 = 0x50 // 80 - H.265/HEVC
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
AudioCodecAACRaw uint16 = 0x86 // 134 - AAC raw format
|
|
||||||
AudioCodecAACADTS uint16 = 0x87 // 135 - AAC with ADTS header
|
|
||||||
AudioCodecAACLATM uint16 = 0x88 // 136 - AAC with LATM format
|
|
||||||
AudioCodecG711U uint16 = 0x89 // 137 - G.711 μ-law (PCMU)
|
|
||||||
AudioCodecG711A uint16 = 0x8A // 138 - G.711 A-law (PCMA)
|
|
||||||
AudioCodecADPCM uint16 = 0x8B // 139 - ADPCM
|
|
||||||
AudioCodecPCM uint16 = 0x8C // 140 - PCM 16-bit signed LE
|
|
||||||
AudioCodecSPEEX uint16 = 0x8D // 141 - Speex
|
|
||||||
AudioCodecMP3 uint16 = 0x8E // 142 - MP3
|
|
||||||
AudioCodecG726 uint16 = 0x8F // 143 - G.726
|
|
||||||
AudioCodecAACWyze uint16 = 0x90 // 144 - Wyze AAC
|
|
||||||
AudioCodecOpus uint16 = 0x92 // 146 - Opus codec
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
SampleRate8K uint8 = 0x00 // 8000 Hz
|
|
||||||
SampleRate11K uint8 = 0x01 // 11025 Hz
|
|
||||||
SampleRate12K uint8 = 0x02 // 12000 Hz
|
|
||||||
SampleRate16K uint8 = 0x03 // 16000 Hz
|
|
||||||
SampleRate22K uint8 = 0x04 // 22050 Hz
|
|
||||||
SampleRate24K uint8 = 0x05 // 24000 Hz
|
|
||||||
SampleRate32K uint8 = 0x06 // 32000 Hz
|
|
||||||
SampleRate44K uint8 = 0x07 // 44100 Hz
|
|
||||||
SampleRate48K uint8 = 0x08 // 48000 Hz
|
|
||||||
)
|
|
||||||
|
|
||||||
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, // AAC frame = 1024 samples
|
|
||||||
AudioCodecAACADTS: 1024,
|
|
||||||
AudioCodecAACLATM: 1024,
|
|
||||||
AudioCodecAACWyze: 1024,
|
|
||||||
AudioCodecG711U: 160, // G.711 typically 20ms = 160 samples at 8kHz
|
|
||||||
AudioCodecG711A: 160,
|
|
||||||
AudioCodecPCM: 160,
|
|
||||||
AudioCodecADPCM: 160,
|
|
||||||
AudioCodecSPEEX: 160,
|
|
||||||
AudioCodecMP3: 1152, // MP3 frame = 1152 samples
|
|
||||||
AudioCodecG726: 160,
|
|
||||||
AudioCodecOpus: 960, // Opus typically 20ms = 960 samples at 48kHz
|
|
||||||
}
|
|
||||||
|
|
||||||
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 DTLS Protocol (IOTC/TransCode) commands and sizes
|
|
||||||
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
|
|
||||||
OldProtoHeaderSize = 16
|
|
||||||
OldProtoMinPacketSize = 16
|
|
||||||
OldProtoDiscoBodySize = 72
|
|
||||||
OldProtoDiscoPacketSize = OldProtoHeaderSize + OldProtoDiscoBodySize
|
|
||||||
OldProtoSessionBodySize = 36
|
|
||||||
OldProtoSessionPacketSize = OldProtoHeaderSize + OldProtoSessionBodySize
|
|
||||||
)
|
|
||||||
|
|
||||||
// NEW DTLS Protocol (0xCC51) commands and sizes
|
|
||||||
const (
|
|
||||||
MagicNewProto uint16 = 0xCC51
|
|
||||||
CmdNewProtoDiscovery uint16 = 0x1002
|
|
||||||
CmdNewProtoDTLS uint16 = 0x1502
|
|
||||||
NewProtoPayloadSize uint16 = 0x0028
|
|
||||||
NewProtoPacketSize = 52
|
|
||||||
NewProtoHeaderSize = 28
|
|
||||||
NewProtoAuthSize = 20
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
UIDSize = 20
|
|
||||||
RandomIDSize = 8
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
MagicAVLoginResp uint16 = 0x2100
|
|
||||||
MagicIOCtrl uint16 = 0x7000
|
|
||||||
MagicChannelMsg uint16 = 0x1000
|
|
||||||
MagicACK uint16 = 0x0009
|
|
||||||
MagicAVLogin1 uint16 = 0x0000
|
|
||||||
MagicAVLogin2 uint16 = 0x2000
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ProtocolVersion uint16 = 0x000c // Version 12
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultCapabilities uint32 = 0x001f07fb
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
KCmdAuth = 10000
|
|
||||||
KCmdChallenge = 10001
|
|
||||||
KCmdChallengeResp = 10002
|
|
||||||
KCmdAuthResult = 10003
|
|
||||||
KCmdAuthWithPayload = 10008
|
|
||||||
KCmdAuthSuccess = 10009
|
|
||||||
KCmdControlChannel = 10010
|
|
||||||
KCmdControlChannelResp = 10011
|
|
||||||
KCmdSetResolution = 10056
|
|
||||||
KCmdSetResolutionResp = 10057
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
MediaTypeVideo = 1
|
|
||||||
MediaTypeAudio = 2
|
|
||||||
MediaTypeReturnAudio = 3
|
|
||||||
MediaTypeRDT = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
IOTCChannelMain = 0 // Main AV channel (we = DTLS Client, camera = Server)
|
|
||||||
IOTCChannelBack = 1 // Backchannel for Return Audio (we = DTLS Server, camera = Client)
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
BitrateMax uint16 = 0xF0 // 240 KB/s
|
|
||||||
BitrateSD uint16 = 0x3C // 60 KB/s
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
FrameSize1080P = 0
|
|
||||||
FrameSize360P = 1
|
|
||||||
FrameSize720P = 2
|
|
||||||
FrameSize2K = 3
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
QualityUnknown = 0
|
|
||||||
QualityMax = 1
|
|
||||||
QualityHigh = 2
|
|
||||||
QualityMiddle = 3
|
|
||||||
QualityLow = 4
|
|
||||||
QualityMin = 5
|
|
||||||
)
|
|
||||||
|
|
||||||
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(enum uint8) int {
|
|
||||||
if rate, ok := SampleRates[enum]; ok {
|
|
||||||
return rate
|
|
||||||
}
|
|
||||||
return 16000 // Default
|
|
||||||
}
|
|
||||||
|
|
||||||
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 // Default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BuildAudioFlags(sampleRate uint32, bits16 bool, 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 // Default to AAC
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package tutk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ChannelAdapter) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||||
|
var buf chan []byte
|
||||||
|
if a.channel == IOTCChannelMain {
|
||||||
|
buf = a.conn.mainBuf
|
||||||
|
} else {
|
||||||
|
buf = a.conn.speakBuf
|
||||||
|
}
|
||||||
|
|
||||||
|
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(time.Time) error { return nil }
|
||||||
|
func (a *ChannelAdapter) SetReadDeadline(time.Time) error { return nil }
|
||||||
|
func (a *ChannelAdapter) SetWriteDeadline(time.Time) error { return nil }
|
||||||
@@ -0,0 +1,505 @@
|
|||||||
|
package tutk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resolution constants
|
||||||
|
const (
|
||||||
|
ResolutionUnknown = 0
|
||||||
|
ResolutionSD = 1
|
||||||
|
Resolution360P = 2
|
||||||
|
Resolution2K = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
const FrameInfoSize = 40
|
||||||
|
|
||||||
|
// FrameInfo - Wyze extended FRAMEINFO (40 bytes at end of packet)
|
||||||
|
type FrameInfo struct {
|
||||||
|
CodecID uint16
|
||||||
|
Flags uint8
|
||||||
|
CamIndex uint8
|
||||||
|
OnlineNum uint8
|
||||||
|
Framerate uint8
|
||||||
|
FrameSize uint8
|
||||||
|
Bitrate uint8
|
||||||
|
TimestampUS uint32
|
||||||
|
Timestamp uint32
|
||||||
|
PayloadSize uint32
|
||||||
|
FrameNo uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fi *FrameInfo) IsKeyframe() bool {
|
||||||
|
return fi.Flags == 0x01
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fi *FrameInfo) Resolution() string {
|
||||||
|
switch fi.FrameSize {
|
||||||
|
case ResolutionSD:
|
||||||
|
return "SD"
|
||||||
|
case Resolution360P:
|
||||||
|
return "360P"
|
||||||
|
case Resolution2K:
|
||||||
|
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],
|
||||||
|
Framerate: fi[5],
|
||||||
|
FrameSize: fi[6],
|
||||||
|
Bitrate: fi[7],
|
||||||
|
TimestampUS: binary.LittleEndian.Uint32(fi[8:]),
|
||||||
|
Timestamp: 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 IsEndFrame(frameType) && pktIdxOrMarker == 0x0028 {
|
||||||
|
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 IsEndFrame(frameType) && pktIdxOrMarker == 0x0028 {
|
||||||
|
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 FrameAssembler struct {
|
||||||
|
FrameNo uint32
|
||||||
|
PktTotal uint16
|
||||||
|
Packets map[uint16][]byte
|
||||||
|
FrameInfo *FrameInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
type FrameHandler struct {
|
||||||
|
assemblers map[byte]*FrameAssembler
|
||||||
|
baseTS uint64
|
||||||
|
output chan *Packet
|
||||||
|
verbose bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFrameHandler(verbose bool) *FrameHandler {
|
||||||
|
return &FrameHandler{
|
||||||
|
assemblers: make(map[byte]*FrameAssembler),
|
||||||
|
output: make(chan *Packet, 128),
|
||||||
|
verbose: verbose,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FrameHandler) Recv() <-chan *Packet {
|
||||||
|
return h.output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FrameHandler) Close() {
|
||||||
|
close(h.output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FrameHandler) Handle(data []byte) {
|
||||||
|
hdr := ParsePacketHeader(data)
|
||||||
|
if hdr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.verbose {
|
||||||
|
h.logWireHeader(data, hdr)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, fi := h.extractPayload(data, hdr.Channel)
|
||||||
|
if payload == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.verbose {
|
||||||
|
h.logAVPacket(hdr.Channel, hdr.FrameType, payload, fi)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
asm := h.assemblers[channel]
|
||||||
|
|
||||||
|
// Frame transition: new frame number = previous frame complete
|
||||||
|
if asm != nil && hdr.FrameNo != asm.FrameNo {
|
||||||
|
gotAll := uint16(len(asm.Packets)) == asm.PktTotal
|
||||||
|
if gotAll && asm.FrameInfo != nil {
|
||||||
|
h.assembleAndQueue(channel, asm)
|
||||||
|
}
|
||||||
|
asm = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new assembler if needed
|
||||||
|
if asm == nil {
|
||||||
|
asm = &FrameAssembler{
|
||||||
|
FrameNo: hdr.FrameNo,
|
||||||
|
PktTotal: hdr.PktTotal,
|
||||||
|
Packets: make(map[uint16][]byte, hdr.PktTotal),
|
||||||
|
}
|
||||||
|
h.assemblers[channel] = asm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store packet (copy payload - buffer is reused by worker)
|
||||||
|
payloadCopy := make([]byte, len(payload))
|
||||||
|
copy(payloadCopy, payload)
|
||||||
|
asm.Packets[hdr.PktIdx] = payloadCopy
|
||||||
|
|
||||||
|
if fi != nil {
|
||||||
|
asm.FrameInfo = fi
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if frame is complete
|
||||||
|
if uint16(len(asm.Packets)) == asm.PktTotal && asm.FrameInfo != nil {
|
||||||
|
h.assembleAndQueue(channel, asm)
|
||||||
|
delete(h.assemblers, channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FrameHandler) assembleAndQueue(channel byte, asm *FrameAssembler) {
|
||||||
|
fi := asm.FrameInfo
|
||||||
|
|
||||||
|
// Assemble packets in correct order
|
||||||
|
var payload []byte
|
||||||
|
for i := uint16(0); i < asm.PktTotal; i++ {
|
||||||
|
if pkt, ok := asm.Packets[i]; ok {
|
||||||
|
payload = append(payload, pkt...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size validation
|
||||||
|
if fi.PayloadSize > 0 && len(payload) != int(fi.PayloadSize) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate RTP timestamp (90kHz for video) using relative timestamps
|
||||||
|
absoluteTS := uint64(fi.Timestamp)*1000000 + uint64(fi.TimestampUS)
|
||||||
|
if h.baseTS == 0 {
|
||||||
|
h.baseTS = absoluteTS
|
||||||
|
}
|
||||||
|
relativeUS := absoluteTS - h.baseTS
|
||||||
|
const clockRate uint64 = 90000
|
||||||
|
rtpTS := uint32(relativeUS * clockRate / 1000000)
|
||||||
|
|
||||||
|
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 = "I"
|
||||||
|
}
|
||||||
|
fmt.Printf("[VIDEO] #%d %s %s size=%d rtp=%d\n",
|
||||||
|
fi.FrameNo, CodecName(fi.CodecID), frameType, len(payload), rtpTS)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate RTP timestamp using relative timestamps (shared baseTS for A/V sync)
|
||||||
|
absoluteTS := uint64(fi.Timestamp)*1000000 + uint64(fi.TimestampUS)
|
||||||
|
if h.baseTS == 0 {
|
||||||
|
h.baseTS = absoluteTS
|
||||||
|
}
|
||||||
|
relativeUS := absoluteTS - h.baseTS
|
||||||
|
clockRate := uint64(sampleRate)
|
||||||
|
rtpTS := uint32(relativeUS * clockRate / 1000000)
|
||||||
|
|
||||||
|
pkt := &Packet{
|
||||||
|
Channel: ChannelAudio,
|
||||||
|
Payload: payload,
|
||||||
|
Codec: fi.CodecID,
|
||||||
|
Timestamp: rtpTS,
|
||||||
|
SampleRate: sampleRate,
|
||||||
|
Channels: channels,
|
||||||
|
FrameNo: fi.FrameNo,
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.verbose {
|
||||||
|
fmt.Printf("[AUDIO] #%d %s size=%d rate=%d ch=%d rtp=%d\n",
|
||||||
|
fi.FrameNo, AudioCodecName(fi.CodecID), len(payload), sampleRate, channels, rtpTS)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.queue(pkt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FrameHandler) queue(pkt *Packet) {
|
||||||
|
select {
|
||||||
|
case h.output <- pkt:
|
||||||
|
default:
|
||||||
|
// Queue full - drop oldest
|
||||||
|
select {
|
||||||
|
case <-h.output:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
h.output <- pkt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FrameHandler) logWireHeader(data []byte, hdr *PacketHeader) {
|
||||||
|
fmt.Printf("[WIRE] ch=0x%02x type=0x%02x len=%d pkt=%d/%d frame=%d\n",
|
||||||
|
hdr.Channel, hdr.FrameType, len(data), hdr.PktIdx, hdr.PktTotal, hdr.FrameNo)
|
||||||
|
fmt.Printf(" RAW[0..35]: ")
|
||||||
|
for i := 0; i < 36 && i < len(data); i++ {
|
||||||
|
fmt.Printf("%02x ", data[i])
|
||||||
|
}
|
||||||
|
fmt.Printf("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FrameHandler) logAVPacket(channel, frameType byte, payload []byte, fi *FrameInfo) {
|
||||||
|
fmt.Printf("[AV] ch=0x%02x type=0x%02x len=%d", channel, frameType, len(payload))
|
||||||
|
if fi != nil {
|
||||||
|
fmt.Printf(" fi={codec=0x%04x flags=0x%02x ts=%d}", fi.CodecID, fi.Flags, fi.Timestamp)
|
||||||
|
}
|
||||||
|
fmt.Printf("\n")
|
||||||
|
}
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
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
|
||||||
|
CmdNewDTLS uint16 = 0x1502
|
||||||
|
NewPayloadSize uint16 = 0x0028
|
||||||
|
NewPacketSize = 52
|
||||||
|
NewHeaderSize = 28
|
||||||
|
NewAuthSize = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
package tutk
|
|
||||||
|
|
||||||
import "encoding/binary"
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Start packets - first fragment of a frame
|
|
||||||
// 0x08: Extended start (36-byte header, no FrameInfo)
|
|
||||||
// 0x09: StartAlt (36-byte header, FrameInfo only if pkt_total==1)
|
|
||||||
FrameTypeStart uint8 = 0x08
|
|
||||||
FrameTypeStartAlt uint8 = 0x09
|
|
||||||
|
|
||||||
// Continuation packets - middle fragment (28-byte header, no FrameInfo)
|
|
||||||
FrameTypeCont uint8 = 0x00
|
|
||||||
FrameTypeContAlt uint8 = 0x04
|
|
||||||
|
|
||||||
// End packets - last fragment (with 40-byte FrameInfo)
|
|
||||||
// 0x01: Single-packet frame (28-byte header)
|
|
||||||
// 0x05: Multi-packet end (28-byte header)
|
|
||||||
// 0x0d: Extended end (36-byte header)
|
|
||||||
FrameTypeEndSingle uint8 = 0x01
|
|
||||||
FrameTypeEndMulti uint8 = 0x05
|
|
||||||
FrameTypeEndExt uint8 = 0x0d
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ChannelIVideo uint8 = 0x05
|
|
||||||
ChannelAudio uint8 = 0x03
|
|
||||||
ChannelPVideo uint8 = 0x07
|
|
||||||
)
|
|
||||||
|
|
||||||
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 AuthResponse struct {
|
|
||||||
ConnectionRes string `json:"connectionRes"`
|
|
||||||
CameraInfo map[string]any `json:"cameraInfo"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AVLoginResponse struct {
|
|
||||||
ServerType uint32
|
|
||||||
Resend int32
|
|
||||||
TwoWayStreaming int32
|
|
||||||
SyncRecvData int32
|
|
||||||
SecurityMode uint32
|
|
||||||
VideoOnConnect int32
|
|
||||||
AudioOnConnect int32
|
|
||||||
}
|
|
||||||
|
|
||||||
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 PacketHeader struct {
|
|
||||||
Channel byte
|
|
||||||
FrameType byte
|
|
||||||
HeaderSize int // 28 or 36
|
|
||||||
FrameNo uint32 // Frame number (from [24-27] for 28-byte, [32-35] for 36-byte)
|
|
||||||
PktIdx uint16 // Packet index within frame (0-based)
|
|
||||||
PktTotal uint16 // Total packets in this frame
|
|
||||||
PayloadSize uint16
|
|
||||||
HasFrameInfo bool // true if [14-15] or [22-23] == 0x0028
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParsePacketHeader(data []byte) *PacketHeader {
|
|
||||||
if len(data) < 28 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
frameType := data[1]
|
|
||||||
hdr := &PacketHeader{
|
|
||||||
Channel: data[0],
|
|
||||||
FrameType: frameType,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header size based on FrameType (NOT magic bytes!)
|
|
||||||
switch frameType {
|
|
||||||
case FrameTypeStart, FrameTypeStartAlt, FrameTypeEndExt: // 0x08, 0x09, 0x0d
|
|
||||||
hdr.HeaderSize = 36
|
|
||||||
default: // 0x00, 0x01, 0x04, 0x05
|
|
||||||
hdr.HeaderSize = 28
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data) < hdr.HeaderSize {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if hdr.HeaderSize == 28 {
|
|
||||||
// 28-Byte Header Layout:
|
|
||||||
// [12-13] pkt_total
|
|
||||||
// [14-15] pkt_idx OR 0x0028 (FrameInfo marker) - ONLY 0x0028 in End packets!
|
|
||||||
// [16-17] payload_size
|
|
||||||
// [24-27] frame_no (uint32)
|
|
||||||
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:])
|
|
||||||
|
|
||||||
// 0x0028 is FrameInfo marker ONLY in End packets, otherwise it's pkt_idx=40
|
|
||||||
if IsEndFrame(hdr.FrameType) && pktIdxOrMarker == 0x0028 {
|
|
||||||
hdr.HasFrameInfo = true
|
|
||||||
if hdr.PktTotal > 0 {
|
|
||||||
hdr.PktIdx = hdr.PktTotal - 1 // Last packet
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hdr.PktIdx = pktIdxOrMarker
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 36-Byte Header Layout:
|
|
||||||
// [20-21] pkt_total
|
|
||||||
// [22-23] pkt_idx OR 0x0028 (FrameInfo marker) - ONLY 0x0028 in End packets!
|
|
||||||
// [24-25] payload_size
|
|
||||||
// [32-35] frame_no (uint32) - GLOBAL frame counter, matches 28-byte [24-27]
|
|
||||||
// NOTE: [18-19] is channel-specific frame index, NOT used for reassembly!
|
|
||||||
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:])
|
|
||||||
|
|
||||||
// 0x0028 is FrameInfo marker ONLY in End packets, otherwise it's pkt_idx=40
|
|
||||||
if IsEndFrame(hdr.FrameType) && pktIdxOrMarker == 0x0028 {
|
|
||||||
hdr.HasFrameInfo = true
|
|
||||||
if hdr.PktTotal > 0 {
|
|
||||||
hdr.PktIdx = hdr.PktTotal - 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hdr.PktIdx = pktIdxOrMarker
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hdr
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user