install go2rtc on bob

This commit is contained in:
2026-04-04 19:36:14 +02:00
parent f0b56e63d1
commit ccf88187b8
537 changed files with 69213 additions and 0 deletions
@@ -0,0 +1,79 @@
package dvrip
import (
"encoding/binary"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
type Backchannel struct {
core.Connection
client *Client
}
func (c *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
return nil, core.ErrCantGetTrack
}
func (c *Backchannel) Start() error {
if err := c.client.conn.SetReadDeadline(time.Time{}); err != nil {
return err
}
b := make([]byte, 4096)
for {
if _, err := c.client.rd.Read(b); err != nil {
return err
}
}
}
func (c *Backchannel) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
if err := c.client.Talk(); err != nil {
return err
}
const PacketSize = 320
buf := make([]byte, 8+PacketSize)
binary.BigEndian.PutUint32(buf, 0x1FA)
switch track.Codec.Name {
case core.CodecPCMU:
buf[4] = 10
case core.CodecPCMA:
buf[4] = 14
}
//for i, rate := range sampleRates {
// if rate == track.Codec.ClockRate {
// buf[5] = byte(i) + 1
// break
// }
//}
buf[5] = 2 // ClockRate=8000
binary.LittleEndian.PutUint16(buf[6:], PacketSize)
var payload []byte
sender := core.NewSender(media, track.Codec)
sender.Handler = func(packet *rtp.Packet) {
payload = append(payload, packet.Payload...)
for len(payload) >= PacketSize {
buf = append(buf[:8], payload[:PacketSize]...)
if n, err := c.client.WriteCmd(OPTalkData, buf); err != nil {
c.Send += n
}
payload = payload[PacketSize:]
}
}
sender.HandleRTP(track)
c.Senders = append(c.Senders, sender)
return nil
}
+247
View File
@@ -0,0 +1,247 @@
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","AudioFormat":{"EncodeType":"G711_ALAW"}}}` + "\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)
}
@@ -0,0 +1,39 @@
package dvrip
import "github.com/AlexxIT/go2rtc/pkg/core"
func Dial(url string) (core.Producer, error) {
client := &Client{}
if err := client.Dial(url); err != nil {
return nil, err
}
conn := core.Connection{
ID: core.NewID(),
FormatName: "dvrip",
Protocol: "tcp",
RemoteAddr: client.conn.RemoteAddr().String(),
Transport: client.conn,
}
if client.stream != "" {
prod := &Producer{Connection: conn, client: client}
if err := prod.probe(); err != nil {
return nil, err
}
return prod, nil
} else {
conn.Medias = []*core.Media{
{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
// leave only one codec here for better compatibility with cameras
// https://github.com/AlexxIT/go2rtc/issues/1111
{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},
},
},
}
return &Backchannel{Connection: conn, client: client}, nil
}
}
@@ -0,0 +1,262 @@
package dvrip
import (
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/pion/rtp"
)
type Producer struct {
core.Connection
client *Client
video, audio *core.Receiver
videoTS uint32
videoDT uint32
audioTS uint32
audioSeq uint16
}
func (c *Producer) Start() error {
for {
pType, b, err := c.client.ReadPacket()
if err != nil {
return err
}
//log.Printf("[DVR] type: %d, len: %d", dataType, len(b))
switch pType {
case 0xFC, 0xFE, 0xFD:
if c.video == nil {
continue
}
var payload []byte
if pType != 0xFD {
payload = b[16:] // iframe
} else {
payload = b[8:] // pframe
}
c.videoTS += c.videoDT
packet := &rtp.Packet{
Header: rtp.Header{Timestamp: c.videoTS},
Payload: annexb.EncodeToAVCC(payload),
}
//log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp)
c.video.WriteRTP(packet)
case 0xFA: // audio
if c.audio == nil {
continue
}
payload := b[8:]
c.audioTS += uint32(len(payload))
c.audioSeq++
packet := &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
SequenceNumber: c.audioSeq,
Timestamp: c.audioTS,
},
Payload: payload,
}
//log.Printf("[DVR] len: %d, ts: %10d", len(packet.Payload), packet.Timestamp)
c.audio.WriteRTP(packet)
case 0xF9: // unknown
default:
println(fmt.Sprintf("dvrip: unknown packet type: %d", pType))
}
}
}
func (c *Producer) probe() error {
if err := c.client.Play(); err != nil {
return err
}
rd := core.NewReadBuffer(c.client.rd)
rd.BufferSize = core.ProbeSize
defer func() {
c.client.buf = nil
rd.Reset()
}()
c.client.rd = rd
// some awful cameras has VERY rare keyframes
// so we wait video+audio for default probe time
// and wait anything for 15 seconds
timeoutBoth := time.Now().Add(core.ProbeTimeout)
timeoutAny := time.Now().Add(time.Second * 15)
for {
if now := time.Now(); now.Before(timeoutBoth) {
if c.video != nil && c.audio != nil {
return nil
}
} else if now.Before(timeoutAny) {
if c.video != nil || c.audio != nil {
return nil
}
} else {
return errors.New("dvrip: can't probe medias")
}
tag, b, err := c.client.ReadPacket()
if err != nil {
return err
}
switch tag {
case 0xFC, 0xFE: // video
if c.video != nil {
continue
}
fps := b[5]
//width := uint16(b[6]) * 8
//height := uint16(b[7]) * 8
//println(width, height)
ts := b[8:]
// the exact value of the start TS does not matter
c.videoTS = binary.LittleEndian.Uint32(ts)
c.videoDT = 90000 / uint32(fps)
payload := annexb.EncodeToAVCC(b[16:])
c.addVideoTrack(b[4], payload)
case 0xFA: // audio
if c.audio != nil {
continue
}
// the exact value of the start TS does not matter
c.audioTS = c.videoTS
c.addAudioTrack(b[4], b[5])
}
}
}
func (c *Producer) addVideoTrack(mediaCode byte, payload []byte) {
var codec *core.Codec
switch mediaCode {
case 0x02, 0x12:
codec = &core.Codec{
Name: core.CodecH264,
ClockRate: 90000,
PayloadType: core.PayloadTypeRAW,
FmtpLine: h264.GetFmtpLine(payload),
}
case 0x03, 0x13, 0x43, 0x53:
codec = &core.Codec{
Name: core.CodecH265,
ClockRate: 90000,
PayloadType: core.PayloadTypeRAW,
FmtpLine: "profile-id=1",
}
for {
size := 4 + int(binary.BigEndian.Uint32(payload))
switch h265.NALUType(payload) {
case h265.NALUTypeVPS:
codec.FmtpLine += ";sprop-vps=" + base64.StdEncoding.EncodeToString(payload[4:size])
case h265.NALUTypeSPS:
codec.FmtpLine += ";sprop-sps=" + base64.StdEncoding.EncodeToString(payload[4:size])
case h265.NALUTypePPS:
codec.FmtpLine += ";sprop-pps=" + base64.StdEncoding.EncodeToString(payload[4:size])
}
if size < len(payload) {
payload = payload[size:]
} else {
break
}
}
default:
println("[DVRIP] unsupported video codec:", mediaCode)
return
}
media := &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
c.Medias = append(c.Medias, media)
c.video = core.NewReceiver(media, codec)
c.Receivers = append(c.Receivers, c.video)
}
var sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100, 48000}
func (c *Producer) addAudioTrack(mediaCode byte, sampleRate byte) {
// https://github.com/vigoss30611/buildroot-ltc/blob/master/system/qm/ipc/ProtocolService/src/ZhiNuo/inc/zn_dh_base_type.h
// PCM8 = 7, G729, IMA_ADPCM, G711U, G721, PCM8_VWIS, MS_ADPCM, G711A, PCM16
var codec *core.Codec
switch mediaCode {
case 10: // G711U
codec = &core.Codec{
Name: core.CodecPCMU,
}
case 14: // G711A
codec = &core.Codec{
Name: core.CodecPCMA,
}
default:
println("[DVRIP] unsupported audio codec:", mediaCode)
return
}
if sampleRate <= byte(len(sampleRates)) {
codec.ClockRate = sampleRates[sampleRate-1]
}
media := &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
c.Medias = append(c.Medias, media)
c.audio = core.NewReceiver(media, codec)
c.Receivers = append(c.Receivers, c.audio)
}
//func (c *Client) MarshalJSON() ([]byte, error) {
// info := &core.Info{
// Type: "DVRIP active producer",
// RemoteAddr: c.conn.RemoteAddr().String(),
// Medias: c.Medias,
// Receivers: c.Receivers,
// Recv: c.Recv,
// }
// return json.Marshal(info)
//}