diff --git a/internal/dvrip/dvrip.go b/internal/dvrip/dvrip.go index 0db0a80b..470e8afd 100644 --- a/internal/dvrip/dvrip.go +++ b/internal/dvrip/dvrip.go @@ -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 diff --git a/pkg/dvrip/client.go b/pkg/dvrip/client.go index 6df6ea68..eefca775 100644 --- a/pkg/dvrip/client.go +++ b/pkg/dvrip/client.go @@ -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" diff --git a/pkg/dvrip/consumer.go b/pkg/dvrip/consumer.go new file mode 100644 index 00000000..3cd61204 --- /dev/null +++ b/pkg/dvrip/consumer.go @@ -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 +} diff --git a/pkg/dvrip/dvrip.go b/pkg/dvrip/dvrip.go new file mode 100644 index 00000000..35823ccf --- /dev/null +++ b/pkg/dvrip/dvrip.go @@ -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 + } +} diff --git a/pkg/dvrip/producer.go b/pkg/dvrip/producer.go index 6c1ffe4d..4367a684 100644 --- a/pkg/dvrip/producer.go +++ b/pkg/dvrip/producer.go @@ -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) +//}