mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-10-05 16:26:50 +08:00
248 lines
5.0 KiB
Go
248 lines
5.0 KiB
Go
package dvrip
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/md5"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/url"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
Login = 1000
|
|
OPMonitorClaim = 1413
|
|
OPMonitorStart = 1410
|
|
OPTalkClaim = 1434
|
|
OPTalkStart = 1430
|
|
OPTalkData = 1432
|
|
)
|
|
|
|
type Client struct {
|
|
conn net.Conn
|
|
session uint32
|
|
seq uint32
|
|
stream string
|
|
|
|
rd io.Reader
|
|
buf []byte
|
|
}
|
|
|
|
func (c *Client) Dial(rawURL string) (err error) {
|
|
u, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if u.Port() == "" {
|
|
// add default TCP port
|
|
u.Host += ":34567"
|
|
}
|
|
|
|
c.conn, err = net.DialTimeout("tcp", u.Host, time.Second*3)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if query := u.Query(); query.Get("backchannel") != "1" {
|
|
channel := query.Get("channel")
|
|
if channel == "" {
|
|
channel = "0"
|
|
}
|
|
|
|
subtype := query.Get("subtype")
|
|
switch subtype {
|
|
case "", "0":
|
|
subtype = "Main"
|
|
case "1":
|
|
subtype = "Extra1"
|
|
}
|
|
|
|
c.stream = fmt.Sprintf(
|
|
`{"Channel":%s,"CombinMode":"NONE","StreamType":"%s","TransMode":"TCP"}`,
|
|
channel, subtype,
|
|
)
|
|
}
|
|
|
|
c.rd = bufio.NewReader(c.conn)
|
|
|
|
if u.User != nil {
|
|
pass, _ := u.User.Password()
|
|
return c.Login(u.User.Username(), pass)
|
|
} else {
|
|
return c.Login("admin", "admin")
|
|
}
|
|
}
|
|
|
|
func (c *Client) Close() error {
|
|
return c.conn.Close()
|
|
}
|
|
|
|
func (c *Client) Login(user, pass string) (err error) {
|
|
data := fmt.Sprintf(
|
|
`{"EncryptType":"MD5","LoginType":"DVRIP-Web","PassWord":"%s","UserName":"%s"}`+"\x0A\x00",
|
|
SofiaHash(pass), user,
|
|
)
|
|
|
|
if _, err = c.WriteCmd(Login, []byte(data)); err != nil {
|
|
return
|
|
}
|
|
|
|
_, err = c.ReadJSON()
|
|
return
|
|
}
|
|
|
|
func (c *Client) Play() error {
|
|
format := `{"Name":"OPMonitor","SessionID":"0x%08X","OPMonitor":{"Action":"%s","Parameter":%s}}` + "\x0A\x00"
|
|
|
|
data := fmt.Sprintf(format, c.session, "Claim", c.stream)
|
|
if _, err := c.WriteCmd(OPMonitorClaim, []byte(data)); err != nil {
|
|
return err
|
|
}
|
|
if _, err := c.ReadJSON(); err != nil {
|
|
return err
|
|
}
|
|
|
|
data = fmt.Sprintf(format, c.session, "Start", c.stream)
|
|
_, err := c.WriteCmd(OPMonitorStart, []byte(data))
|
|
return err
|
|
}
|
|
|
|
func (c *Client) Talk() error {
|
|
format := `{"Name":"OPTalk","SessionID":"0x%08X","OPTalk":{"Action":"%s"}}` + "\x0A\x00"
|
|
|
|
data := fmt.Sprintf(format, c.session, "Claim")
|
|
if _, err := c.WriteCmd(OPTalkClaim, []byte(data)); err != nil {
|
|
return err
|
|
}
|
|
if _, err := c.ReadJSON(); err != nil {
|
|
return err
|
|
}
|
|
|
|
data = fmt.Sprintf(format, c.session, "Start")
|
|
_, err := c.WriteCmd(OPTalkStart, []byte(data))
|
|
return err
|
|
}
|
|
|
|
func (c *Client) WriteCmd(cmd uint16, payload []byte) (n int, err error) {
|
|
b := make([]byte, 20, 128)
|
|
b[0] = 255
|
|
binary.LittleEndian.PutUint32(b[4:], c.session)
|
|
binary.LittleEndian.PutUint32(b[8:], c.seq)
|
|
binary.LittleEndian.PutUint16(b[14:], cmd)
|
|
binary.LittleEndian.PutUint32(b[16:], uint32(len(payload)))
|
|
b = append(b, payload...)
|
|
|
|
c.seq++
|
|
|
|
if err = c.conn.SetWriteDeadline(time.Now().Add(time.Second * 5)); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return c.conn.Write(b)
|
|
}
|
|
|
|
func (c *Client) ReadChunk() (b []byte, err error) {
|
|
if err = c.conn.SetReadDeadline(time.Now().Add(time.Second * 5)); err != nil {
|
|
return
|
|
}
|
|
|
|
b = make([]byte, 20)
|
|
if _, err = io.ReadFull(c.rd, b); err != nil {
|
|
return
|
|
}
|
|
|
|
if b[0] != 255 {
|
|
return nil, errors.New("read error")
|
|
}
|
|
|
|
c.session = binary.LittleEndian.Uint32(b[4:])
|
|
size := binary.LittleEndian.Uint32(b[16:])
|
|
|
|
b = make([]byte, size)
|
|
if _, err = io.ReadFull(c.rd, b); err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (c *Client) ReadPacket() (pType byte, payload []byte, err error) {
|
|
var b []byte
|
|
|
|
// many cameras may split packet to multiple chunks
|
|
// some rare cameras may put multiple packets to single chunk
|
|
for len(c.buf) < 16 {
|
|
if b, err = c.ReadChunk(); err != nil {
|
|
return 0, nil, err
|
|
}
|
|
c.buf = append(c.buf, b...)
|
|
}
|
|
|
|
if !bytes.HasPrefix(c.buf, []byte{0, 0, 1}) {
|
|
return 0, nil, fmt.Errorf("dvrip: wrong packet: %0.16x", c.buf)
|
|
}
|
|
|
|
var size int
|
|
|
|
switch pType = c.buf[3]; pType {
|
|
case 0xFC, 0xFE:
|
|
size = int(binary.LittleEndian.Uint32(c.buf[12:])) + 16
|
|
case 0xFD: // PFrame
|
|
size = int(binary.LittleEndian.Uint32(c.buf[4:])) + 8
|
|
case 0xFA, 0xF9:
|
|
size = int(binary.LittleEndian.Uint16(c.buf[6:])) + 8
|
|
default:
|
|
return 0, nil, fmt.Errorf("dvrip: unknown packet type: %X", pType)
|
|
}
|
|
|
|
for len(c.buf) < size {
|
|
if b, err = c.ReadChunk(); err != nil {
|
|
return 0, nil, err
|
|
}
|
|
c.buf = append(c.buf, b...)
|
|
}
|
|
|
|
payload = c.buf[:size]
|
|
c.buf = c.buf[size:]
|
|
|
|
return
|
|
}
|
|
|
|
type Response map[string]any
|
|
|
|
func (c *Client) ReadJSON() (res Response, err error) {
|
|
b, err := c.ReadChunk()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
res = Response{}
|
|
if err = json.Unmarshal(b[:len(b)-2], &res); err != nil {
|
|
return
|
|
}
|
|
|
|
if v, ok := res["Ret"].(float64); !ok || (v != 100 && v != 515) {
|
|
err = fmt.Errorf("wrong response: %s", b)
|
|
}
|
|
return
|
|
}
|
|
|
|
func SofiaHash(password string) string {
|
|
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
|
|
sofia := make([]byte, 0, 8)
|
|
hash := md5.Sum([]byte(password))
|
|
for i := 0; i < md5.Size; i += 2 {
|
|
j := uint16(hash[i]) + uint16(hash[i+1])
|
|
sofia = append(sofia, chars[j%62])
|
|
}
|
|
|
|
return string(sofia)
|
|
}
|