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,74 @@
package miss
import (
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/opus"
"github.com/AlexxIT/go2rtc/pkg/pcm"
"github.com/pion/rtp"
)
func (p *Producer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
if err := p.client.StartSpeaker(); err != nil {
return err
}
// TODO: check this!!!
time.Sleep(time.Second)
sender := core.NewSender(media, track.Codec)
switch track.Codec.Name {
case core.CodecPCMA:
var buf []byte
if p.client.SpeakerCodec() == codecPCM {
dst := &core.Codec{Name: core.CodecPCML, ClockRate: 8000}
transcode := pcm.Transcode(dst, track.Codec)
sender.Handler = func(pkt *rtp.Packet) {
buf = append(buf, transcode(pkt.Payload)...)
const size = 2 * 8000 * 0.040 // 16bit 40ms
for len(buf) >= size {
p.Send += size
_ = p.client.WriteAudio(codecPCM, buf[:size])
buf = buf[size:]
}
}
} else {
sender.Handler = func(pkt *rtp.Packet) {
buf = append(buf, pkt.Payload...)
const size = 8000 * 0.040 // 8bit 40 ms
for len(buf) >= size {
p.Send += size
_ = p.client.WriteAudio(codecPCMA, buf[:size])
buf = buf[size:]
}
}
}
case core.CodecOpus:
if p.client.SpeakerCodec() == codecOPUS {
var buf []byte
sender.Handler = func(pkt *rtp.Packet) {
if buf == nil {
buf = pkt.Payload
} else {
// convert two 20ms to one 40ms
buf = opus.JoinFrames(buf, pkt.Payload)
p.Send += len(buf)
_ = p.client.WriteAudio(codecOPUS, buf)
buf = nil
}
}
} else {
sender.Handler = func(pkt *rtp.Packet) {
p.Send += len(pkt.Payload)
_ = p.client.WriteAudio(codecOPUS, pkt.Payload)
}
}
}
sender.HandleRTP(track)
p.Senders = append(p.Senders, sender)
return nil
}
@@ -0,0 +1,338 @@
package miss
import (
"bytes"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"net"
"net/url"
"time"
"github.com/AlexxIT/go2rtc/pkg/tutk"
"github.com/AlexxIT/go2rtc/pkg/xiaomi/crypto"
"github.com/AlexxIT/go2rtc/pkg/xiaomi/miss/cs2"
)
const (
codecH264 = 4
codecH265 = 5
codecPCM = 1024
codecPCMU = 1026
codecPCMA = 1027
codecOPUS = 1032
)
type Conn interface {
Protocol() string
Version() string
ReadCommand() (cmd uint32, data []byte, err error)
WriteCommand(cmd uint32, data []byte) error
ReadPacket() (hdr, payload []byte, err error)
WritePacket(hdr, payload []byte) error
RemoteAddr() net.Addr
SetDeadline(t time.Time) error
Close() error
}
func NewClient(rawURL string) (*Client, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
// 1. Check if we can create shared key.
query := u.Query()
key, err := crypto.CalcSharedKey(query.Get("device_public"), query.Get("client_private"))
if err != nil {
return nil, err
}
model := query.Get("model")
// 2. Check if this vendor supported.
var conn Conn
switch s := query.Get("vendor"); s {
case "cs2":
conn, err = cs2.Dial(u.Host, query.Get("transport"))
case "tutk":
conn, err = tutk.Dial(u.Host, query.Get("uid"), "Miss", "client")
default:
err = fmt.Errorf("miss: unsupported vendor %s", s)
}
if err != nil {
return nil, err
}
err = login(conn, query.Get("client_public"), query.Get("sign"))
if err != nil {
_ = conn.Close()
return nil, err
}
return &Client{Conn: conn, key: key, model: model}, nil
}
type Client struct {
Conn
key []byte
model string
}
const (
cmdAuthReq = 0x100
cmdAuthRes = 0x101
cmdVideoStart = 0x102
cmdVideoStop = 0x103
cmdAudioStart = 0x104
cmdAudioStop = 0x105
cmdSpeakerStartReq = 0x106
cmdSpeakerStartRes = 0x107
cmdSpeakerStop = 0x108
cmdStreamCtrlReq = 0x109
cmdStreamCtrlRes = 0x10A
cmdGetAudioFormatReq = 0x10B
cmdGetAudioFormatRes = 0x10C
cmdPlaybackReq = 0x10D
cmdPlaybackRes = 0x10E
cmdDevInfoReq = 0x110
cmdDevInfoRes = 0x111
cmdMotorReq = 0x112
cmdMotorRes = 0x113
cmdEncoded = 0x1001
)
func login(conn Conn, clientPublic, sign string) error {
s := fmt.Sprintf(`{"public_key":"%s","sign":"%s","uuid":"","support_encrypt":0}`, clientPublic, sign)
if err := conn.WriteCommand(cmdAuthReq, []byte(s)); err != nil {
return err
}
_, data, err := conn.ReadCommand()
if err != nil {
return err
}
if !bytes.Contains(data, []byte(`"result":"success"`)) {
return fmt.Errorf("miss: auth: %s", data)
}
return nil
}
func (c *Client) Version() string {
return fmt.Sprintf("%s (%s)", c.Conn.Version(), c.model)
}
func (c *Client) WriteCommand(data []byte) error {
data, err := crypto.Encode(data, c.key)
if err != nil {
return err
}
return c.Conn.WriteCommand(cmdEncoded, data)
}
const (
ModelDafang = "isa.camera.df3"
ModelLoockV2 = "loock.cateye.v02"
ModelC200 = "chuangmi.camera.046c04"
ModelC300 = "chuangmi.camera.72ac1"
// ModelXiaofang looks like it has the same firmware as the ModelDafang.
// There is also an older model "isa.camera.isc5" that only works with the legacy protocol.
ModelXiaofang = "isa.camera.isc5c1"
)
func (c *Client) StartMedia(channel, quality, audio string) error {
switch c.model {
case ModelDafang, ModelXiaofang:
var q, a byte
if quality == "sd" {
q = 1 // 0 - hd, 1 - sd, default - hd
}
if audio != "0" {
a = 1 // 0 - off, 1 - on, default - on
}
return errors.Join(
c.WriteCommand(dafangVideoQuality(q)),
c.WriteCommand(dafangVideoStart(1, a)),
)
}
// 0 - auto, 1 - sd, 2 - hd, default - hd
switch quality {
case "", "hd":
// Some models have broken codec settings in quality 3.
// Some models have low quality in quality 2.
// Different models require different default quality settings.
switch c.model {
case ModelC200, ModelC300:
quality = "3"
default:
quality = "2"
}
case "sd":
quality = "1"
case "auto":
quality = "0"
}
if audio == "" {
audio = "1"
}
data := binary.BigEndian.AppendUint32(nil, cmdVideoStart)
switch channel {
case "", "0":
data = fmt.Appendf(data, `{"videoquality":%s,"enableaudio":%s}`, quality, audio)
default:
data = fmt.Appendf(data, `{"videoquality":-1,"videoquality2":%s,"enableaudio":%s}`, quality, audio)
}
return c.WriteCommand(data)
}
func (c *Client) StopMedia() error {
data := binary.BigEndian.AppendUint32(nil, cmdVideoStop)
return c.WriteCommand(data)
}
func (c *Client) StartAudio() error {
data := binary.BigEndian.AppendUint32(nil, cmdAudioStart)
return c.WriteCommand(data)
}
func (c *Client) StartSpeaker() error {
data := binary.BigEndian.AppendUint32(nil, cmdSpeakerStartReq)
return c.WriteCommand(data)
}
// SpeakerCodec if the camera model has a non-standard two-way codec.
func (c *Client) SpeakerCodec() uint32 {
switch c.model {
case ModelDafang, ModelXiaofang, "isa.camera.hlc6":
return codecPCM
case "chuangmi.camera.72ac1":
return codecOPUS
}
return 0
}
const hdrSize = 32
func (c *Client) ReadPacket() (*Packet, error) {
hdr, payload, err := c.Conn.ReadPacket()
if err != nil {
return nil, fmt.Errorf("miss: read media: %w", err)
}
if len(hdr) < hdrSize {
return nil, fmt.Errorf("miss: packet header too small")
}
payload, err = crypto.Decode(payload, c.key)
if err != nil {
return nil, err
}
pkt := &Packet{
CodecID: binary.LittleEndian.Uint32(hdr[4:]),
Sequence: binary.LittleEndian.Uint32(hdr[8:]),
Flags: binary.LittleEndian.Uint32(hdr[12:]),
Payload: payload,
}
switch c.model {
case ModelDafang, ModelXiaofang, ModelLoockV2:
// Dafang has ts in sec
// LoockV2 has ts in msec for video, but zero ts for audio
pkt.Timestamp = uint64(time.Now().UnixMilli())
default:
pkt.Timestamp = binary.LittleEndian.Uint64(hdr[16:])
}
return pkt, nil
}
func (c *Client) WriteAudio(codecID uint32, payload []byte) error {
payload, err := crypto.Encode(payload, c.key) // new payload will have new size!
if err != nil {
return err
}
n := uint32(len(payload))
header := make([]byte, hdrSize)
binary.LittleEndian.PutUint32(header, n)
binary.LittleEndian.PutUint32(header[4:], codecID)
binary.LittleEndian.PutUint64(header[16:], uint64(time.Now().UnixMilli())) // not really necessary
return c.Conn.WritePacket(header, payload)
}
type Packet struct {
//Length uint32
CodecID uint32
Sequence uint32
Flags uint32
Timestamp uint64 // msec
//TimestampS uint32
//Reserved uint32
Payload []byte
}
func (p *Packet) SampleRate() uint32 {
// flag: 1 0011 000 - sample rate 16000
// flag: 100 00 01 0000 000 - sample rate 8000
v := (p.Flags >> 3) & 0b1111
if v != 0 {
return 16000
}
return 8000
}
//func (p *Packet) AudioUnknown1() byte {
// return byte((p.Flags >> 7) & 0b11)
//}
//
//func (p *Packet) AudioUnknown2() byte {
// return byte((p.Flags >> 9) & 0b11)
//}
func dafangRaw(cmd uint32, args ...byte) []byte {
payload := tutk.ICAM(cmd, args...)
data := make([]byte, 4+len(payload)*2)
copy(data, "\x7f\xff\xff\xff")
hex.Encode(data[4:], payload)
return data
}
// DafangVideoQuality 0 - hd, 1 - sd
func dafangVideoQuality(quality uint8) []byte {
return dafangRaw(0xff07d5, quality)
}
func dafangVideoStart(video, audio uint8) []byte {
return dafangRaw(0xff07d8, video, audio)
}
//func dafangLeft() []byte {
// return dafangRaw(0xff2404, 2, 0, 5)
//}
//
//func dafangRight() []byte {
// return dafangRaw(0xff2404, 1, 0, 5)
//}
//
//func dafangUp() []byte {
// return dafangRaw(0xff2404, 0, 2, 5)
//}
//
//func dafangDown() []byte {
// return dafangRaw(0xff2404, 0, 1, 5)
//}
//
//func dafangStop() []byte {
// return dafangRaw(0xff2404, 0, 0, 5)
//}
@@ -0,0 +1,506 @@
package cs2
import (
"bufio"
"bytes"
"encoding/binary"
"fmt"
"io"
"net"
"sync"
"sync/atomic"
"time"
)
func Dial(host, transport string) (*Conn, error) {
conn, err := handshake(host, transport)
if err != nil {
return nil, err
}
_, isTCP := conn.(*tcpConn)
c := &Conn{
Conn: conn,
isTCP: isTCP,
channels: [4]*dataChannel{
newDataChannel(0, 10), nil, newDataChannel(250, 100), nil,
},
}
go c.worker()
return c, nil
}
type Conn struct {
net.Conn
isTCP bool
err error
seqCh0 uint16
seqCh3 uint16
channels [4]*dataChannel
cmdMu sync.Mutex
cmdAck func()
}
const (
magic = 0xF1
magicDrw = 0xD1
magicTCP = 0x68
msgLanSearch = 0x30
msgPunchPkt = 0x41
msgP2PRdyUDP = 0x42
msgP2PRdyTCP = 0x43
msgDrw = 0xD0
msgDrwAck = 0xD1
msgPing = 0xE0
msgPong = 0xE1
msgClose = 0xF0
msgCloseAck = 0xF1
)
func handshake(host, transport string) (net.Conn, error) {
conn, err := newUDPConn(host, 32108)
if err != nil {
return nil, err
}
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
req := []byte{magic, msgLanSearch, 0, 0}
res, err := conn.(*udpConn).WriteUntil(req, func(res []byte) bool {
return res[1] == msgPunchPkt
})
if err != nil {
_ = conn.Close()
return nil, err
}
var msgUDP, msgTCP byte
if transport == "" || transport == "udp" {
msgUDP = msgP2PRdyUDP
}
if transport == "" || transport == "tcp" {
msgTCP = msgP2PRdyTCP
}
res, err = conn.(*udpConn).WriteUntil(res, func(res []byte) bool {
return res[1] == msgUDP || res[1] == msgTCP
})
if err != nil {
_ = conn.Close()
return nil, err
}
_ = conn.SetDeadline(time.Time{})
if res[1] == msgTCP {
_ = conn.Close()
//host := fmt.Sprintf("%d.%d.%d.%d:%d", b[31], b[30], b[29], b[28], uint16(b[27])<<8|uint16(b[26]))
return newTCPConn(conn.RemoteAddr().String())
}
return conn, nil
}
func (c *Conn) worker() {
defer func() {
c.channels[0].Close()
c.channels[2].Close()
}()
var keepaliveTS time.Time // only for TCP
buf := make([]byte, 1200)
for {
n, err := c.Conn.Read(buf)
if err != nil {
c.err = fmt.Errorf("%s: %w", "cs2", err)
return
}
// 0 f1d0 magic
// 2 005d size = total size + 4
// 4 d1 magic
// 5 00 channel
// 6 0000 seq
switch buf[1] {
case msgDrw:
ch := buf[5]
channel := c.channels[ch]
if c.isTCP {
// For TCP we should send ping every second to keep connection alive.
// Based on PCAP analysis: official Mi Home app sends PING every ~1s.
if now := time.Now(); now.After(keepaliveTS) {
_, _ = c.Conn.Write([]byte{magic, msgPing, 0, 0})
keepaliveTS = now.Add(time.Second)
}
err = channel.Push(buf[8:n])
} else {
var pushed int
seqHI, seqLO := buf[6], buf[7]
seq := uint16(seqHI)<<8 | uint16(seqLO)
pushed, err = channel.PushSeq(seq, buf[8:n])
if pushed >= 0 {
// For UDP we should send ACK.
ack := []byte{magic, msgDrwAck, 0, 6, magicDrw, ch, 0, 1, seqHI, seqLO}
_, _ = c.Conn.Write(ack)
}
}
if err != nil {
c.err = fmt.Errorf("%s: %w", "cs2", err)
return
}
case msgPing:
_, _ = c.Conn.Write([]byte{magic, msgPong, 0, 0})
case msgPong, msgP2PRdyUDP, msgP2PRdyTCP, msgClose, msgCloseAck: // skip it
case msgDrwAck: // only for UDP
if c.cmdAck != nil {
c.cmdAck()
}
default:
fmt.Printf("%s: unknown msg: %x\n", "cs2", buf[:n])
}
}
}
func (c *Conn) Protocol() string {
if c.isTCP {
return "cs2+tcp"
}
return "cs2+udp"
}
func (c *Conn) Version() string {
return "CS2"
}
func (c *Conn) Error() error {
if c.err != nil {
return c.err
}
return io.EOF
}
func (c *Conn) ReadCommand() (cmd uint32, data []byte, err error) {
buf, ok := c.channels[0].Pop()
if !ok {
return 0, nil, c.Error()
}
cmd = binary.LittleEndian.Uint32(buf)
data = buf[4:]
return
}
func (c *Conn) WriteCommand(cmd uint32, data []byte) error {
c.cmdMu.Lock()
defer c.cmdMu.Unlock()
req := marshalCmd(0, c.seqCh0, cmd, data)
c.seqCh0++
if c.isTCP {
_, err := c.Conn.Write(req)
return err
}
var repeat atomic.Int32
repeat.Store(5)
timeout := time.NewTicker(time.Second)
defer timeout.Stop()
c.cmdAck = func() {
repeat.Store(0)
timeout.Reset(1)
}
for {
if _, err := c.Conn.Write(req); err != nil {
return err
}
<-timeout.C
r := repeat.Add(-1)
if r < 0 {
return nil
}
if r == 0 {
return fmt.Errorf("%s: can't send command %d", "cs2", cmd)
}
}
}
const hdrSize = 32
func (c *Conn) ReadPacket() (hdr, payload []byte, err error) {
data, ok := c.channels[2].Pop()
if !ok {
return nil, nil, c.Error()
}
return data[:hdrSize], data[hdrSize:], nil
}
func (c *Conn) WritePacket(hdr, payload []byte) error {
const offset = 12
n := hdrSize + uint32(len(payload))
req := make([]byte, n+offset)
req[0] = magic
req[1] = msgDrw
binary.BigEndian.PutUint16(req[2:], uint16(n+8))
req[4] = magicDrw
req[5] = 3 // channel
binary.BigEndian.PutUint16(req[6:], c.seqCh3)
c.seqCh3++
binary.BigEndian.PutUint32(req[8:], n)
copy(req[offset:], hdr)
copy(req[offset+hdrSize:], hdr)
_, err := c.Conn.Write(req)
return err
}
func marshalCmd(channel byte, seq uint16, cmd uint32, payload []byte) []byte {
size := len(payload)
req := make([]byte, 4+4+4+4+size)
// 1. message header (4 bytes)
req[0] = magic
req[1] = msgDrw
binary.BigEndian.PutUint16(req[2:], uint16(4+4+4+size))
// 2. drw? header (4 bytes)
req[4] = magicDrw
req[5] = channel
binary.BigEndian.PutUint16(req[6:], seq)
// 3. payload size (4 bytes)
binary.BigEndian.PutUint32(req[8:], uint32(4+size))
// 4. payload command (4 bytes)
binary.BigEndian.PutUint32(req[12:], cmd)
// 5. payload
copy(req[16:], payload)
return req
}
func newUDPConn(host string, port int) (net.Conn, error) {
// We using raw net.UDPConn, because RemoteAddr should be changed during handshake.
conn, err := net.ListenUDP("udp", nil)
if err != nil {
return nil, err
}
addr, err := net.ResolveUDPAddr("udp", host)
if err != nil {
addr = &net.UDPAddr{IP: net.ParseIP(host), Port: port}
}
return &udpConn{UDPConn: conn, addr: addr}, nil
}
type udpConn struct {
*net.UDPConn
addr *net.UDPAddr
}
func (c *udpConn) Read(b []byte) (n int, err error) {
var addr *net.UDPAddr
for {
n, addr, err = c.UDPConn.ReadFromUDP(b)
if err != nil {
return 0, err
}
if string(addr.IP) == string(c.addr.IP) || n >= 8 {
//log.Printf("<- %x", b[:n])
return
}
}
}
func (c *udpConn) Write(b []byte) (n int, err error) {
//log.Printf("-> %x", b)
return c.UDPConn.WriteToUDP(b, c.addr)
}
func (c *udpConn) RemoteAddr() net.Addr {
return c.addr
}
func (c *udpConn) WriteUntil(req []byte, ok func(res []byte) bool) ([]byte, error) {
var t *time.Timer
t = time.AfterFunc(1, func() {
if _, err := c.Write(req); err == nil && t != nil {
t.Reset(time.Second)
}
})
defer t.Stop()
buf := make([]byte, 1200)
for {
n, addr, err := c.UDPConn.ReadFromUDP(buf)
if err != nil {
return nil, err
}
if string(addr.IP) != string(c.addr.IP) || n < 16 {
continue // skip messages from another IP
}
if ok(buf[:n]) {
c.addr.Port = addr.Port
return buf[:n], nil
}
}
}
func newTCPConn(addr string) (net.Conn, error) {
conn, err := net.DialTimeout("tcp", addr, 3*time.Second)
if err != nil {
return nil, err
}
return &tcpConn{conn.(*net.TCPConn), bufio.NewReader(conn)}, nil
}
type tcpConn struct {
*net.TCPConn
rd *bufio.Reader
}
func (c *tcpConn) Read(p []byte) (n int, err error) {
tmp := make([]byte, 8)
if _, err = io.ReadFull(c.rd, tmp); err != nil {
return
}
n = int(binary.BigEndian.Uint16(tmp))
if len(p) < n {
return 0, fmt.Errorf("tcp: buffer too small")
}
_, err = io.ReadFull(c.rd, p[:n])
//log.Printf("<- %x%x", tmp, p[:n])
return
}
func (c *tcpConn) Write(req []byte) (n int, err error) {
n = len(req)
buf := make([]byte, 8+n)
binary.BigEndian.PutUint16(buf, uint16(n))
buf[2] = magicTCP
copy(buf[8:], req)
//log.Printf("-> %x", buf)
_, err = c.TCPConn.Write(buf)
return
}
func newDataChannel(pushSize, popSize int) *dataChannel {
c := &dataChannel{}
if pushSize > 0 {
c.pushBuf = make(map[uint16][]byte, pushSize)
c.pushSize = pushSize
}
if popSize >= 0 {
c.popBuf = make(chan []byte, popSize)
}
return c
}
type dataChannel struct {
waitSeq uint16
pushBuf map[uint16][]byte
pushSize int
waitData []byte
waitSize int
popBuf chan []byte
}
func (c *dataChannel) Push(b []byte) error {
c.waitData = append(c.waitData, b...)
for len(c.waitData) > 4 {
// Every new data starts with size. There can be several data inside one packet.
if c.waitSize == 0 {
c.waitSize = int(binary.BigEndian.Uint32(c.waitData))
c.waitData = c.waitData[4:]
}
if c.waitSize > len(c.waitData) {
break
}
select {
case c.popBuf <- c.waitData[:c.waitSize]:
default:
return fmt.Errorf("pop buffer is full")
}
c.waitData = c.waitData[c.waitSize:]
c.waitSize = 0
}
return nil
}
func (c *dataChannel) Pop() ([]byte, bool) {
data, ok := <-c.popBuf
return data, ok
}
func (c *dataChannel) Close() {
close(c.popBuf)
}
// PushSeq returns how many seq were processed.
// Returns 0 if seq was saved or processed earlier.
// Returns -1 if seq could not be saved (buffer full or disabled).
func (c *dataChannel) PushSeq(seq uint16, data []byte) (int, error) {
diff := int16(seq - c.waitSeq)
// Check if this is seq from the future.
if diff > 0 {
// Support disabled buffer.
if c.pushSize == 0 {
return -1, nil // couldn't save seq
}
// Check if we don't have this seq in the buffer.
if c.pushBuf[seq] == nil {
// Check if there is enough space in the buffer.
if len(c.pushBuf) == c.pushSize {
return -1, nil // couldn't save seq
}
c.pushBuf[seq] = bytes.Clone(data)
//log.Printf("push buf wait=%d seq=%d len=%d", c.waitSeq, seq, len(c.pushBuf))
}
return 0, nil
}
// Check if this is seq from the past.
if diff < 0 {
return 0, nil
}
for i := 1; ; i++ {
if err := c.Push(data); err != nil {
return i, err
}
c.waitSeq++
// Check if we have next seq in the buffer.
if data = c.pushBuf[c.waitSeq]; data != nil {
delete(c.pushBuf, c.waitSeq)
} else {
return i, nil
}
}
}
@@ -0,0 +1,204 @@
package miss
import (
"fmt"
"net/url"
"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
}
func Dial(rawURL string) (core.Producer, error) {
client, err := NewClient(rawURL)
if err != nil {
return nil, err
}
u, _ := url.Parse(rawURL)
query := u.Query()
err = client.StartMedia(query.Get("channel"), query.Get("subtype"), query.Get("audio"))
if err != nil {
_ = client.Close()
return nil, err
}
medias, err := probe(client, query.Get("audio") != "0")
if err != nil {
_ = client.Close()
return nil, err
}
return &Producer{
Connection: core.Connection{
ID: core.NewID(),
FormatName: "xiaomi/miss",
Protocol: client.Protocol(),
RemoteAddr: client.RemoteAddr().String(),
UserAgent: client.Version(),
Medias: medias,
Transport: client,
},
client: client,
}, nil
}
func probe(client *Client, audio bool) ([]*core.Media, error) {
_ = client.SetDeadline(time.Now().Add(15 * time.Second))
var vcodec, acodec *core.Codec
for {
pkt, err := client.ReadPacket()
if err != nil {
if vcodec != nil {
err = fmt.Errorf("no audio")
} else if acodec != nil {
err = fmt.Errorf("no video")
}
return nil, fmt.Errorf("xiaomi: probe: %w", err)
}
switch pkt.CodecID {
case codecH264:
if vcodec == nil {
buf := annexb.EncodeToAVCC(pkt.Payload)
if h264.NALUType(buf) == h264.NALUTypeSPS {
vcodec = h264.AVCCToCodec(buf)
}
}
case codecH265:
if vcodec == nil {
buf := annexb.EncodeToAVCC(pkt.Payload)
if h265.NALUType(buf) == h265.NALUTypeVPS {
vcodec = h265.AVCCToCodec(buf)
}
}
case codecPCMA:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecPCMA, ClockRate: pkt.SampleRate()}
}
case codecOPUS:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecOpus, ClockRate: 48000, Channels: 2}
}
}
if vcodec != nil && (acodec != nil || !audio) {
break
}
}
_ = client.SetDeadline(time.Time{})
medias := []*core.Media{
{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{vcodec},
},
}
if acodec != nil {
medias = append(medias, &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{acodec},
})
medias = append(medias, &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{acodec.Clone()},
})
}
return medias, nil
}
const timestamp40ms = 48000 * 0.040
func (p *Producer) Start() error {
var audioTS uint32
for {
_ = p.client.SetDeadline(time.Now().Add(10 * time.Second))
pkt, err := p.client.ReadPacket()
if err != nil {
return err
}
p.Recv += len(pkt.Payload)
// TODO: rewrite this
var name string
var pkt2 *core.Packet
switch pkt.CodecID {
case codecH264, codecH265:
pkt2 = &core.Packet{
Header: rtp.Header{
SequenceNumber: uint16(pkt.Sequence),
Timestamp: TimeToRTP(pkt.Timestamp, 90000),
},
Payload: annexb.EncodeToAVCC(pkt.Payload),
}
if pkt.CodecID == codecH264 {
name = core.CodecH264
} else {
name = core.CodecH265
}
case codecPCMA:
name = core.CodecPCMA
pkt2 = &core.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
SequenceNumber: uint16(pkt.Sequence),
Timestamp: audioTS,
},
Payload: pkt.Payload,
}
audioTS += uint32(len(pkt.Payload))
case codecOPUS:
name = core.CodecOpus
pkt2 = &core.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
SequenceNumber: uint16(pkt.Sequence),
Timestamp: audioTS,
},
Payload: pkt.Payload,
}
// known cameras sends packets with 40ms long
audioTS += timestamp40ms
}
for _, recv := range p.Receivers {
if recv.Codec.Name == name {
recv.WriteRTP(pkt2)
break
}
}
}
}
func (p *Producer) Stop() error {
_ = p.client.StopMedia()
return p.Connection.Stop()
}
// TimeToRTP convert time in milliseconds to RTP time
func TimeToRTP(timeMS, clockRate uint64) uint32 {
return uint32(timeMS * clockRate / 1000)
}