Total rework DVRIP source + add two way audio #633
This commit is contained in:
@@ -23,17 +23,11 @@ func Init() {
|
||||
}
|
||||
|
||||
func handle(url string) (core.Producer, error) {
|
||||
conn := dvrip.NewClient(url)
|
||||
if err := conn.Dial(); err != nil {
|
||||
client, err := dvrip.Dial(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := conn.Play(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := conn.Handle(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
return client, nil
|
||||
}
|
||||
|
||||
const Port = 34569 // UDP port number for dvrip discovery
|
||||
|
||||
+63
-298
@@ -3,7 +3,6 @@ package dvrip
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -12,49 +11,28 @@ import (
|
||||
"net"
|
||||
"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"
|
||||
const (
|
||||
Login = 1000
|
||||
OPMonitorClaim = 1413
|
||||
OPMonitorStart = 1410
|
||||
OPTalkClaim = 1434
|
||||
OPTalkStart = 1430
|
||||
OPTalkData = 1432
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
core.Listener
|
||||
|
||||
uri string
|
||||
conn net.Conn
|
||||
reader *bufio.Reader
|
||||
session uint32
|
||||
seq uint32
|
||||
stream string
|
||||
|
||||
medias []*core.Media
|
||||
receivers []*core.Receiver
|
||||
videoTrack *core.Receiver
|
||||
audioTrack *core.Receiver
|
||||
|
||||
videoTS uint32
|
||||
videoDT uint32
|
||||
audioTS uint32
|
||||
audioSeq uint16
|
||||
|
||||
recv uint32
|
||||
rd io.Reader
|
||||
}
|
||||
|
||||
type Response map[string]any
|
||||
|
||||
const Login = uint16(1000)
|
||||
const OPMonitorClaim = uint16(1413)
|
||||
const OPMonitorStart = uint16(1410)
|
||||
|
||||
func NewClient(url string) *Client {
|
||||
return &Client{uri: url}
|
||||
}
|
||||
|
||||
func (c *Client) Dial() (err error) {
|
||||
u, err := url.Parse(c.uri)
|
||||
func (c *Client) Dial(rawURL string) (err error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -69,26 +47,27 @@ func (c *Client) Dial() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
c.reader = bufio.NewReader(c.conn)
|
||||
if query := u.Query(); query.Get("backchannel") != "1" {
|
||||
channel := query.Get("channel")
|
||||
if channel == "" {
|
||||
channel = "0"
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -98,13 +77,17 @@ func (c *Client) Dial() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
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"}`,
|
||||
`{"EncryptType":"MD5","LoginType":"DVRIP-Web","PassWord":"%s","UserName":"%s"}`+"\x0A\x00",
|
||||
SofiaHash(pass), user,
|
||||
)
|
||||
|
||||
if err = c.Request(Login, data); err != nil {
|
||||
if _, err = c.Request(Login, []byte(data)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -112,182 +95,54 @@ func (c *Client) Login(user, pass string) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Play() (err error) {
|
||||
format := `{"Name":"OPMonitor","SessionID":"0x%08X","OPMonitor":{"Action":"%s","Parameter":%s}}`
|
||||
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.Request(OPMonitorClaim, data); err != nil {
|
||||
return
|
||||
if _, err := c.Request(OPMonitorClaim, []byte(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = c.ResponseJSON(); err != nil {
|
||||
return
|
||||
if _, err := c.ResponseJSON(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data = fmt.Sprintf(format, c.session, "Start", c.stream)
|
||||
return c.Request(OPMonitorStart, data)
|
||||
_, err := c.Request(OPMonitorStart, []byte(data))
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) Handle() error {
|
||||
var buf []byte
|
||||
var size int
|
||||
func (c *Client) Talk() error {
|
||||
format := `{"Name":"OPTalk","SessionID":"0x%08X","OPTalk":{"Action":"%s"}}` + "\x0A\x00"
|
||||
|
||||
var probe byte
|
||||
if c.medias == nil {
|
||||
probe = 1
|
||||
data := fmt.Sprintf(format, c.session, "Claim")
|
||||
if _, err := c.Request(OPTalkClaim, []byte(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.ResponseJSON(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
b, err := c.Response()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// collect data from multiple packets
|
||||
if size > 0 {
|
||||
buf = append(buf, b...)
|
||||
if len(buf) < size {
|
||||
continue
|
||||
}
|
||||
if len(buf) > size {
|
||||
return errors.New("wrong size")
|
||||
}
|
||||
b = buf
|
||||
}
|
||||
|
||||
dataType := binary.BigEndian.Uint32(b)
|
||||
switch dataType {
|
||||
case 0x1FC, 0x1FE:
|
||||
size = int(binary.LittleEndian.Uint32(b[12:])) + 16
|
||||
case 0x1FD: // PFrame
|
||||
size = int(binary.LittleEndian.Uint32(b[4:])) + 8
|
||||
case 0x1FA, 0x1F9:
|
||||
size = int(binary.LittleEndian.Uint16(b[6:])) + 8
|
||||
default:
|
||||
return fmt.Errorf("unknown type: %X", dataType)
|
||||
}
|
||||
|
||||
if len(b) < size {
|
||||
buf = b
|
||||
continue // need to collect data from next packets
|
||||
}
|
||||
|
||||
//log.Printf("[DVR] type: %d, len: %d", dataType, len(b))
|
||||
|
||||
switch dataType {
|
||||
case 0x1FC, 0x1FE: // video IFrame
|
||||
payload := annexb.EncodeToAVCC(b[16:], false)
|
||||
|
||||
if c.videoTrack == nil {
|
||||
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)
|
||||
|
||||
c.AddVideoTrack(b[4], payload)
|
||||
}
|
||||
|
||||
if c.videoTrack != nil {
|
||||
c.videoTS += c.videoDT
|
||||
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: c.videoTS},
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp)
|
||||
|
||||
c.videoTrack.WriteRTP(packet)
|
||||
}
|
||||
|
||||
case 0x1FD: // PFrame
|
||||
if c.videoTrack != nil {
|
||||
c.videoTS += c.videoDT
|
||||
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: c.videoTS},
|
||||
Payload: annexb.EncodeToAVCC(b[8:], false),
|
||||
}
|
||||
|
||||
//log.Printf("[DVR] %v, len: %d, ts: %10d", h265.Types(packet.Payload), len(packet.Payload), packet.Timestamp)
|
||||
|
||||
c.videoTrack.WriteRTP(packet)
|
||||
}
|
||||
|
||||
case 0x1FA, 0x1F9: // audio
|
||||
if c.audioTrack == nil {
|
||||
// the exact value of the start TS does not matter
|
||||
c.audioTS = c.videoTS
|
||||
|
||||
c.AddAudioTrack(b[4], b[5])
|
||||
}
|
||||
|
||||
if c.audioTrack != nil {
|
||||
for b != nil {
|
||||
payload := b[8:size]
|
||||
if len(b) > size {
|
||||
b = b[size:]
|
||||
} else {
|
||||
b = nil
|
||||
}
|
||||
|
||||
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.audioTrack.WriteRTP(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if probe != 0 {
|
||||
probe++
|
||||
if (c.videoTS > 0 && c.audioTS > 0) || probe == 20 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
size = 0
|
||||
}
|
||||
data = fmt.Sprintf(format, c.session, "Start")
|
||||
_, err := c.Request(OPTalkStart, []byte(data))
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Client) Request(cmd uint16, data string) (err error) {
|
||||
func (c *Client) Request(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(data))+2)
|
||||
b = append(b, data...)
|
||||
b = append(b, 0x0A, 0x00)
|
||||
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
|
||||
return 0, err
|
||||
}
|
||||
|
||||
_, err = c.conn.Write(b)
|
||||
return
|
||||
return c.conn.Write(b)
|
||||
}
|
||||
|
||||
func (c *Client) Response() (b []byte, err error) {
|
||||
@@ -296,12 +151,10 @@ func (c *Client) Response() (b []byte, err error) {
|
||||
}
|
||||
|
||||
b = make([]byte, 20)
|
||||
if _, err = io.ReadFull(c.reader, b); err != nil {
|
||||
if _, err = io.ReadFull(c.rd, b); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.recv += 20
|
||||
|
||||
if b[0] != 255 {
|
||||
return nil, errors.New("read error")
|
||||
}
|
||||
@@ -310,15 +163,15 @@ func (c *Client) Response() (b []byte, err error) {
|
||||
size := binary.LittleEndian.Uint32(b[16:])
|
||||
|
||||
b = make([]byte, size)
|
||||
if _, err = io.ReadFull(c.reader, b); err != nil {
|
||||
if _, err = io.ReadFull(c.rd, b); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.recv += size
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type Response map[string]any
|
||||
|
||||
func (c *Client) ResponseJSON() (res Response, err error) {
|
||||
b, err := c.Response()
|
||||
if err != nil {
|
||||
@@ -336,94 +189,6 @@ func (c *Client) ResponseJSON() (res Response, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) 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.videoTrack = core.NewReceiver(media, codec)
|
||||
c.receivers = append(c.receivers, c.videoTrack)
|
||||
}
|
||||
|
||||
var sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100, 48000}
|
||||
|
||||
func (c *Client) 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.audioTrack = core.NewReceiver(media, codec)
|
||||
c.receivers = append(c.receivers, c.audioTrack)
|
||||
}
|
||||
|
||||
func SofiaHash(password string) string {
|
||||
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package dvrip
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
core.SuperConsumer
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (c *Consumer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Consumer) 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 *Consumer) Stop() error {
|
||||
_ = c.SuperConsumer.Close()
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
func (c *Consumer) 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.Request(OPTalkData, buf); err != nil {
|
||||
c.Send += n
|
||||
}
|
||||
|
||||
payload = payload[PacketSize:]
|
||||
}
|
||||
}
|
||||
|
||||
sender.HandleRTP(track)
|
||||
c.Senders = append(c.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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
|
||||
}
|
||||
|
||||
if client.stream != "" {
|
||||
prod := &Producer{client: client}
|
||||
prod.Type = "DVRIP active producer"
|
||||
if err := prod.probe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return prod, nil
|
||||
} else {
|
||||
cons := &Consumer{client: client}
|
||||
cons.Type = "DVRIP active consumer"
|
||||
cons.Medias = []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},
|
||||
{Name: core.CodecPCMU, ClockRate: 8000, PayloadType: 0},
|
||||
},
|
||||
},
|
||||
}
|
||||
return cons, nil
|
||||
}
|
||||
}
|
||||
+264
-22
@@ -1,41 +1,283 @@
|
||||
package dvrip
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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"
|
||||
)
|
||||
|
||||
func (c *Client) GetMedias() []*core.Media {
|
||||
return c.medias
|
||||
type Producer struct {
|
||||
core.SuperProducer
|
||||
|
||||
client *Client
|
||||
|
||||
video, audio *core.Receiver
|
||||
|
||||
videoTS uint32
|
||||
videoDT uint32
|
||||
audioTS uint32
|
||||
audioSeq uint16
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
for _, track := range c.receivers {
|
||||
if track.Codec == codec {
|
||||
return track, nil
|
||||
func (c *Producer) Start() error {
|
||||
for {
|
||||
tag, size, b, err := c.readPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//log.Printf("[DVR] type: %d, len: %d", dataType, len(b))
|
||||
|
||||
switch tag {
|
||||
case 0xFC, 0xFE, 0xFD:
|
||||
if c.video == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var payload []byte
|
||||
if tag != 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, false),
|
||||
}
|
||||
|
||||
//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
|
||||
}
|
||||
|
||||
for b != nil {
|
||||
payload := b[8:size]
|
||||
if len(b) > size {
|
||||
b = b[size:]
|
||||
} else {
|
||||
b = nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
return c.Handle()
|
||||
func (c *Producer) Stop() error {
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
for _, receiver := range c.receivers {
|
||||
receiver.Close()
|
||||
func (c *Producer) probe() error {
|
||||
if err := c.client.Play(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Close()
|
||||
|
||||
rd := core.NewReadBuffer(c.client.rd)
|
||||
rd.BufferSize = core.ProbeSize
|
||||
defer rd.Reset()
|
||||
|
||||
c.client.rd = rd
|
||||
|
||||
timeout := time.Now().Add(core.ProbeTimeout)
|
||||
|
||||
for (c.video == nil || c.audio == nil) && time.Now().Before(timeout) {
|
||||
tag, _, b, err := c.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:], false)
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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: int(c.recv),
|
||||
func (c *Producer) readPacket() (tag byte, size int, data []byte, err error) {
|
||||
if data, err = c.client.Response(); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
return json.Marshal(info)
|
||||
|
||||
switch tag = data[3]; tag {
|
||||
case 0xFC, 0xFE:
|
||||
size = int(binary.LittleEndian.Uint32(data[12:])) + 16
|
||||
case 0xFD: // PFrame
|
||||
size = int(binary.LittleEndian.Uint32(data[4:])) + 8
|
||||
case 0xFA, 0xF9:
|
||||
size = int(binary.LittleEndian.Uint16(data[6:])) + 8
|
||||
default:
|
||||
return 0, 0, nil, fmt.Errorf("unknown type: %X", tag)
|
||||
}
|
||||
|
||||
// collect data from multiple packets
|
||||
for len(data) < size {
|
||||
b, err := c.client.Response()
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
data = append(data, b...)
|
||||
}
|
||||
|
||||
if len(data) > size {
|
||||
return 0, 0, nil, errors.New("wrong size")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
//}
|
||||
|
||||
Reference in New Issue
Block a user