BIG core logic rewrite
This commit is contained in:
+47
-388
@@ -3,115 +3,25 @@ package rtsp
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/pion/rtcp"
|
||||
"github.com/pion/rtp"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ProtoRTSP = "RTSP/1.0"
|
||||
MethodOptions = "OPTIONS"
|
||||
MethodSetup = "SETUP"
|
||||
MethodTeardown = "TEARDOWN"
|
||||
MethodDescribe = "DESCRIBE"
|
||||
MethodPlay = "PLAY"
|
||||
MethodPause = "PAUSE"
|
||||
MethodAnnounce = "ANNOUNCE"
|
||||
MethodRecord = "RECORD"
|
||||
)
|
||||
|
||||
type Mode byte
|
||||
|
||||
const (
|
||||
ModeUnknown Mode = iota
|
||||
ModeClientProducer // conn act as RTSP client that receive data from RTSP server (ex. camera)
|
||||
ModeServerUnknown
|
||||
ModeServerProducer // conn act as RTSP server that reseive data from RTSP client (ex. ffmpeg output)
|
||||
ModeServerConsumer // conn act as RTSP server that send data to RTSP client (ex. ffmpeg input)
|
||||
)
|
||||
|
||||
type State byte
|
||||
|
||||
func (s State) String() string {
|
||||
switch s {
|
||||
case StateNone:
|
||||
return "NONE"
|
||||
case StateConn:
|
||||
return "CONN"
|
||||
case StateSetup:
|
||||
return "SETUP"
|
||||
case StatePlay:
|
||||
return "PLAY"
|
||||
case StateHandle:
|
||||
return "HANDLE"
|
||||
}
|
||||
return strconv.Itoa(int(s))
|
||||
func NewClient(uri string) *Conn {
|
||||
return &Conn{uri: uri}
|
||||
}
|
||||
|
||||
const (
|
||||
StateNone State = iota
|
||||
StateConn
|
||||
StateSetup
|
||||
StatePlay
|
||||
StateHandle
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
streamer.Element
|
||||
|
||||
// public
|
||||
|
||||
Backchannel bool
|
||||
SessionName string
|
||||
|
||||
Medias []*streamer.Media
|
||||
Session string
|
||||
UserAgent string
|
||||
URL *url.URL
|
||||
|
||||
// internal
|
||||
|
||||
auth *tcp.Auth
|
||||
conn net.Conn
|
||||
mode Mode
|
||||
state State
|
||||
stateMu sync.Mutex
|
||||
reader *bufio.Reader
|
||||
sequence int
|
||||
uri string
|
||||
|
||||
tracks []*streamer.Track
|
||||
channels map[byte]*streamer.Track
|
||||
|
||||
// stats
|
||||
|
||||
receive int
|
||||
send int
|
||||
}
|
||||
|
||||
func NewClient(uri string) (*Conn, error) {
|
||||
c := new(Conn)
|
||||
c.mode = ModeClientProducer
|
||||
c.uri = uri
|
||||
return c, c.parseURI()
|
||||
}
|
||||
|
||||
func (c *Conn) parseURI() (err error) {
|
||||
c.URL, err = url.Parse(c.uri)
|
||||
if err != nil {
|
||||
return err
|
||||
func (c *Conn) Dial() (err error) {
|
||||
if c.URL, err = url.Parse(c.uri); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.IndexByte(c.URL.Host, ':') < 0 {
|
||||
@@ -122,14 +32,6 @@ func (c *Conn) parseURI() (err error) {
|
||||
c.auth = tcp.NewAuth(c.URL.User)
|
||||
c.URL.User = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) Dial() (err error) {
|
||||
if c.conn != nil {
|
||||
_ = c.parseURI()
|
||||
}
|
||||
|
||||
c.conn, err = net.DialTimeout("tcp", c.URL.Host, time.Second*5)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -314,7 +216,7 @@ func (c *Conn) Describe() error {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mode = ModeClientProducer
|
||||
c.mode = core.ModeActiveProducer
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -328,7 +230,7 @@ func (c *Conn) Announce() (err error) {
|
||||
},
|
||||
}
|
||||
|
||||
req.Body, err = streamer.MarshalSDP(c.SessionName, c.Medias)
|
||||
req.Body, err = core.MarshalSDP(c.SessionName, c.Medias)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -342,7 +244,7 @@ func (c *Conn) Announce() (err error) {
|
||||
|
||||
func (c *Conn) Setup() error {
|
||||
for _, media := range c.Medias {
|
||||
_, err := c.SetupMedia(media, media.Codecs[0], true)
|
||||
_, err := c.SetupMedia(media, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -351,7 +253,7 @@ func (c *Conn) Setup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bool) (*streamer.Track, error) {
|
||||
func (c *Conn) SetupMedia(media *core.Media, first bool) (byte, error) {
|
||||
// TODO: rewrite recoonection and first flag
|
||||
if first {
|
||||
c.stateMu.Lock()
|
||||
@@ -359,36 +261,45 @@ func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bo
|
||||
}
|
||||
|
||||
if c.state != StateConn && c.state != StateSetup {
|
||||
return nil, fmt.Errorf("RTSP SETUP from wrong state: %s", c.state)
|
||||
return 0, fmt.Errorf("RTSP SETUP from wrong state: %s", c.state)
|
||||
}
|
||||
|
||||
ch := c.GetChannel(media)
|
||||
if ch < 0 {
|
||||
return nil, fmt.Errorf("wrong media: %v", media)
|
||||
var transport string
|
||||
|
||||
// try to use media position as channel number
|
||||
for i, m := range c.Medias {
|
||||
if m.ID == media.ID {
|
||||
transport = fmt.Sprintf(
|
||||
// i - RTP (data channel)
|
||||
// i+1 - RTCP (control channel)
|
||||
"RTP/AVP/TCP;unicast;interleaved=%d-%d", i*2, i*2+1,
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
rawURL := media.Control
|
||||
if transport == "" {
|
||||
return 0, fmt.Errorf("wrong media: %v", media)
|
||||
}
|
||||
|
||||
rawURL := media.ID // control
|
||||
if !strings.Contains(rawURL, "://") {
|
||||
rawURL = c.URL.String()
|
||||
if !strings.HasSuffix(rawURL, "/") {
|
||||
rawURL += "/"
|
||||
}
|
||||
rawURL += media.Control
|
||||
rawURL += media.ID
|
||||
}
|
||||
trackURL, err := urlParse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
req := &tcp.Request{
|
||||
Method: MethodSetup,
|
||||
URL: trackURL,
|
||||
Header: map[string][]string{
|
||||
"Transport": {fmt.Sprintf(
|
||||
// i - RTP (data channel)
|
||||
// i+1 - RTCP (control channel)
|
||||
"RTP/AVP/TCP;unicast;interleaved=%d-%d", ch*2, ch*2+1,
|
||||
)},
|
||||
"Transport": {transport},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -400,20 +311,20 @@ func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bo
|
||||
if c.Backchannel {
|
||||
c.Backchannel = false
|
||||
if err := c.Dial(); err != nil {
|
||||
return nil, err
|
||||
return 0, err
|
||||
}
|
||||
if err := c.Describe(); err != nil {
|
||||
return nil, err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
for _, newMedia := range c.Medias {
|
||||
if newMedia.Control == media.Control {
|
||||
return c.SetupMedia(newMedia, newMedia.Codecs[0], false)
|
||||
if newMedia.ID == media.ID {
|
||||
return c.SetupMedia(newMedia, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if c.Session == "" {
|
||||
@@ -426,60 +337,29 @@ func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bo
|
||||
}
|
||||
}
|
||||
|
||||
// in case the track has already been setup before
|
||||
if codec == nil {
|
||||
c.state = StateSetup
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// we send our `interleaved`, but camera can answer with another
|
||||
|
||||
// Transport: RTP/AVP/TCP;unicast;interleaved=10-11;ssrc=10117CB7
|
||||
// Transport: RTP/AVP/TCP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0
|
||||
// Transport: RTP/AVP/TCP;ssrc=22345682;interleaved=0-1
|
||||
s := res.Header.Get("Transport")
|
||||
// TODO: rewrite
|
||||
if !strings.HasPrefix(s, "RTP/AVP/TCP;") {
|
||||
transport = res.Header.Get("Transport")
|
||||
if !strings.HasPrefix(transport, "RTP/AVP/TCP;") {
|
||||
// Escam Q6 has a bug:
|
||||
// Transport: RTP/AVP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0-1
|
||||
if !strings.Contains(s, ";interleaved=") {
|
||||
return nil, fmt.Errorf("wrong transport: %s", s)
|
||||
if !strings.Contains(transport, ";interleaved=") {
|
||||
return 0, fmt.Errorf("wrong transport: %s", transport)
|
||||
}
|
||||
}
|
||||
|
||||
i := strings.Index(s, "interleaved=")
|
||||
if i < 0 {
|
||||
return nil, fmt.Errorf("wrong transport: %s", s)
|
||||
}
|
||||
|
||||
s = s[i+len("interleaved="):]
|
||||
i = strings.IndexAny(s, "-;")
|
||||
if i > 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
|
||||
ch, err = strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
track := streamer.NewTrack(media, codec)
|
||||
|
||||
switch track.Direction {
|
||||
case streamer.DirectionSendonly:
|
||||
if c.channels == nil {
|
||||
c.channels = make(map[byte]*streamer.Track)
|
||||
}
|
||||
c.channels[byte(ch)] = track
|
||||
|
||||
case streamer.DirectionRecvonly:
|
||||
track = c.bindTrack(track, byte(ch), codec.PayloadType)
|
||||
}
|
||||
|
||||
c.state = StateSetup
|
||||
c.tracks = append(c.tracks, track)
|
||||
|
||||
return track, nil
|
||||
channel := core.Between(transport, "interleaved=", "-")
|
||||
i, err := strconv.Atoi(channel)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return byte(i), nil
|
||||
}
|
||||
|
||||
func (c *Conn) Play() (err error) {
|
||||
@@ -516,224 +396,3 @@ func (c *Conn) Close() error {
|
||||
c.state = StateNone
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Conn) Handle() (err error) {
|
||||
c.stateMu.Lock()
|
||||
|
||||
switch c.state {
|
||||
case StateNone: // Close after PLAY and before Handle is OK (because SETUP after PLAY)
|
||||
case StatePlay:
|
||||
c.state = StateHandle
|
||||
default:
|
||||
err = fmt.Errorf("RTSP HANDLE from wrong state: %s", c.state)
|
||||
|
||||
c.state = StateNone
|
||||
_ = c.conn.Close()
|
||||
}
|
||||
|
||||
ok := c.state == StateHandle
|
||||
|
||||
c.stateMu.Unlock()
|
||||
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
c.stateMu.Lock()
|
||||
defer c.stateMu.Unlock()
|
||||
|
||||
if c.state == StateNone {
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
|
||||
// may have gotten here because of the deadline
|
||||
// so close the connection to stop keepalive
|
||||
c.state = StateNone
|
||||
_ = c.conn.Close()
|
||||
}()
|
||||
|
||||
var timeout time.Duration
|
||||
|
||||
switch c.mode {
|
||||
case ModeClientProducer:
|
||||
// polling frames from remote RTSP Server (ex Camera)
|
||||
go c.keepalive()
|
||||
|
||||
if c.HasSendTracks() {
|
||||
// if we receiving video/audio from camera
|
||||
timeout = time.Second * 5
|
||||
} else {
|
||||
// if we only send audio to camera
|
||||
timeout = time.Second * 30
|
||||
}
|
||||
|
||||
case ModeServerProducer:
|
||||
// polling frames from remote RTSP Client (ex FFmpeg)
|
||||
timeout = time.Second * 15
|
||||
|
||||
case ModeServerConsumer:
|
||||
// pushing frames to remote RTSP Client (ex VLC)
|
||||
timeout = time.Second * 60
|
||||
|
||||
default:
|
||||
return fmt.Errorf("wrong RTSP conn mode: %d", c.mode)
|
||||
}
|
||||
|
||||
for {
|
||||
if c.state == StateNone {
|
||||
return
|
||||
}
|
||||
|
||||
if err = c.conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// we can read:
|
||||
// 1. RTP interleaved: `$` + 1B channel number + 2B size
|
||||
// 2. RTSP response: RTSP/1.0 200 OK
|
||||
// 3. RTSP request: OPTIONS ...
|
||||
var buf4 []byte // `$` + 1B channel number + 2B size
|
||||
buf4, err = c.reader.Peek(4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var channelID byte
|
||||
var size uint16
|
||||
|
||||
if buf4[0] != '$' {
|
||||
switch string(buf4) {
|
||||
case "RTSP":
|
||||
var res *tcp.Response
|
||||
if res, err = tcp.ReadResponse(c.reader); err != nil {
|
||||
return
|
||||
}
|
||||
c.Fire(res)
|
||||
continue
|
||||
|
||||
case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_":
|
||||
var req *tcp.Request
|
||||
if req, err = tcp.ReadRequest(c.reader); err != nil {
|
||||
return
|
||||
}
|
||||
c.Fire(req)
|
||||
continue
|
||||
|
||||
default:
|
||||
for i := 0; ; i++ {
|
||||
// search next start symbol
|
||||
if _, err = c.reader.ReadBytes('$'); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if channelID, err = c.reader.ReadByte(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if channel ID exists
|
||||
if c.channels[channelID] == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
buf4 = make([]byte, 2)
|
||||
if _, err = io.ReadFull(c.reader, buf4); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if size good for RTP
|
||||
size = binary.BigEndian.Uint16(buf4)
|
||||
if size <= 1500 {
|
||||
break
|
||||
}
|
||||
|
||||
// 10 tries to find good packet
|
||||
if i >= 10 {
|
||||
return fmt.Errorf("RTSP wrong input")
|
||||
}
|
||||
}
|
||||
|
||||
c.Fire("RTSP wrong input")
|
||||
}
|
||||
} else {
|
||||
// hope that the odd channels are always RTCP
|
||||
channelID = buf4[1]
|
||||
|
||||
// get data size
|
||||
size = binary.BigEndian.Uint16(buf4[2:])
|
||||
|
||||
// skip 4 bytes from c.reader.Peek
|
||||
if _, err = c.reader.Discard(4); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// init memory for data
|
||||
buf := make([]byte, size)
|
||||
if _, err = io.ReadFull(c.reader, buf); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.receive += int(size)
|
||||
|
||||
if channelID&1 == 0 {
|
||||
packet := &rtp.Packet{}
|
||||
if err = packet.Unmarshal(buf); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
track := c.channels[channelID]
|
||||
if track != nil {
|
||||
_ = track.WriteRTP(packet)
|
||||
} else {
|
||||
//c.Fire("wrong channelID: " + strconv.Itoa(int(channelID)))
|
||||
}
|
||||
} else {
|
||||
msg := &RTCP{Channel: channelID}
|
||||
|
||||
if err = msg.Header.Unmarshal(buf); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
msg.Packets, err = rtcp.Unmarshal(buf)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
c.Fire(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) keepalive() {
|
||||
// TODO: rewrite to RTCP
|
||||
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
|
||||
for {
|
||||
time.Sleep(time.Second * 25)
|
||||
if c.state == StateNone {
|
||||
return
|
||||
}
|
||||
if err := c.Request(req); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) GetChannel(media *streamer.Media) int {
|
||||
for i, m := range c.Medias {
|
||||
if m == media {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (c *Conn) HasSendTracks() bool {
|
||||
for _, track := range c.tracks {
|
||||
if track.Direction == streamer.DirectionSendonly {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/pion/rtcp"
|
||||
"github.com/pion/rtp"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
core.Listener
|
||||
|
||||
// public
|
||||
|
||||
Backchannel bool
|
||||
SessionName string
|
||||
|
||||
Medias []*core.Media
|
||||
Session string
|
||||
UserAgent string
|
||||
URL *url.URL
|
||||
|
||||
// internal
|
||||
|
||||
auth *tcp.Auth
|
||||
conn net.Conn
|
||||
mode core.Mode
|
||||
state State
|
||||
stateMu sync.Mutex
|
||||
reader *bufio.Reader
|
||||
sequence int
|
||||
uri string
|
||||
|
||||
receivers []*core.Receiver
|
||||
senders []*core.Sender
|
||||
|
||||
// stats
|
||||
|
||||
recv int
|
||||
send int
|
||||
}
|
||||
|
||||
const (
|
||||
ProtoRTSP = "RTSP/1.0"
|
||||
MethodOptions = "OPTIONS"
|
||||
MethodSetup = "SETUP"
|
||||
MethodTeardown = "TEARDOWN"
|
||||
MethodDescribe = "DESCRIBE"
|
||||
MethodPlay = "PLAY"
|
||||
MethodPause = "PAUSE"
|
||||
MethodAnnounce = "ANNOUNCE"
|
||||
MethodRecord = "RECORD"
|
||||
)
|
||||
|
||||
type State byte
|
||||
|
||||
func (s State) String() string {
|
||||
switch s {
|
||||
case StateNone:
|
||||
return "NONE"
|
||||
case StateConn:
|
||||
return "CONN"
|
||||
case StateSetup:
|
||||
return "SETUP"
|
||||
case StatePlay:
|
||||
return "PLAY"
|
||||
case StateHandle:
|
||||
return "HANDLE"
|
||||
}
|
||||
return strconv.Itoa(int(s))
|
||||
}
|
||||
|
||||
const (
|
||||
StateNone State = iota
|
||||
StateConn
|
||||
StateSetup
|
||||
StatePlay
|
||||
StateHandle
|
||||
)
|
||||
|
||||
func (c *Conn) Handle() (err error) {
|
||||
c.stateMu.Lock()
|
||||
|
||||
switch c.state {
|
||||
case StateNone: // Close after PLAY and before Handle is OK (because SETUP after PLAY)
|
||||
case StatePlay:
|
||||
c.state = StateHandle
|
||||
default:
|
||||
err = fmt.Errorf("RTSP HANDLE from wrong state: %s", c.state)
|
||||
|
||||
c.state = StateNone
|
||||
_ = c.conn.Close()
|
||||
}
|
||||
|
||||
ok := c.state == StateHandle
|
||||
|
||||
c.stateMu.Unlock()
|
||||
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var timeout time.Duration
|
||||
|
||||
switch c.mode {
|
||||
case core.ModeActiveProducer:
|
||||
// polling frames from remote RTSP Server (ex Camera)
|
||||
go c.keepalive()
|
||||
|
||||
if len(c.receivers) > 0 {
|
||||
// if we receiving video/audio from camera
|
||||
timeout = time.Second * 5
|
||||
} else {
|
||||
// if we only send audio to camera
|
||||
timeout = time.Second * 30
|
||||
}
|
||||
|
||||
case core.ModePassiveProducer:
|
||||
// polling frames from remote RTSP Client (ex FFmpeg)
|
||||
timeout = time.Second * 15
|
||||
|
||||
case core.ModePassiveConsumer:
|
||||
// pushing frames to remote RTSP Client (ex VLC)
|
||||
timeout = time.Second * 60
|
||||
|
||||
default:
|
||||
return fmt.Errorf("wrong RTSP conn mode: %d", c.mode)
|
||||
}
|
||||
|
||||
for c.state != StateNone {
|
||||
if err = c.conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// we can read:
|
||||
// 1. RTP interleaved: `$` + 1B channel number + 2B size
|
||||
// 2. RTSP response: RTSP/1.0 200 OK
|
||||
// 3. RTSP request: OPTIONS ...
|
||||
var buf4 []byte // `$` + 1B channel number + 2B size
|
||||
buf4, err = c.reader.Peek(4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var channelID byte
|
||||
var size uint16
|
||||
|
||||
if buf4[0] != '$' {
|
||||
switch string(buf4) {
|
||||
case "RTSP":
|
||||
var res *tcp.Response
|
||||
if res, err = tcp.ReadResponse(c.reader); err != nil {
|
||||
return
|
||||
}
|
||||
c.Fire(res)
|
||||
continue
|
||||
|
||||
case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_":
|
||||
var req *tcp.Request
|
||||
if req, err = tcp.ReadRequest(c.reader); err != nil {
|
||||
return
|
||||
}
|
||||
c.Fire(req)
|
||||
continue
|
||||
|
||||
default:
|
||||
for i := 0; ; i++ {
|
||||
// search next start symbol
|
||||
if _, err = c.reader.ReadBytes('$'); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if channelID, err = c.reader.ReadByte(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: better check maximum good channel ID
|
||||
if channelID >= 20 {
|
||||
continue
|
||||
}
|
||||
|
||||
buf4 = make([]byte, 2)
|
||||
if _, err = io.ReadFull(c.reader, buf4); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if size good for RTP
|
||||
size = binary.BigEndian.Uint16(buf4)
|
||||
if size <= 1500 {
|
||||
break
|
||||
}
|
||||
|
||||
// 10 tries to find good packet
|
||||
if i >= 10 {
|
||||
return fmt.Errorf("RTSP wrong input")
|
||||
}
|
||||
}
|
||||
|
||||
c.Fire("RTSP wrong input")
|
||||
}
|
||||
} else {
|
||||
// hope that the odd channels are always RTCP
|
||||
channelID = buf4[1]
|
||||
|
||||
// get data size
|
||||
size = binary.BigEndian.Uint16(buf4[2:])
|
||||
|
||||
// skip 4 bytes from c.reader.Peek
|
||||
if _, err = c.reader.Discard(4); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// init memory for data
|
||||
buf := make([]byte, size)
|
||||
if _, err = io.ReadFull(c.reader, buf); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.recv += int(size)
|
||||
|
||||
if channelID&1 == 0 {
|
||||
packet := &rtp.Packet{}
|
||||
if err = packet.Unmarshal(buf); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, receiver := range c.receivers {
|
||||
if receiver.ID == channelID {
|
||||
receiver.WriteRTP(packet)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
msg := &RTCP{Channel: channelID}
|
||||
|
||||
if err = msg.Header.Unmarshal(buf); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
msg.Packets, err = rtcp.Unmarshal(buf)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
c.Fire(msg)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) keepalive() {
|
||||
// TODO: rewrite to RTCP
|
||||
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
|
||||
for {
|
||||
time.Sleep(time.Second * 25)
|
||||
if c.state == StateNone {
|
||||
return
|
||||
}
|
||||
if err := c.Request(req); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
+64
-75
@@ -1,112 +1,101 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
switch c.mode {
|
||||
// send our track to RTSP consumer (ex. FFmpeg)
|
||||
case ModeServerConsumer:
|
||||
i := len(c.tracks)
|
||||
channelID := byte(i << 1)
|
||||
func (c *Conn) GetMedias() []*core.Media {
|
||||
core.Assert(c.Medias != nil)
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
codec := track.Codec.Clone()
|
||||
codec.PayloadType = uint8(96 + i)
|
||||
func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) (err error) {
|
||||
core.Assert(media.Direction == core.DirectionSendonly)
|
||||
|
||||
if media.MatchAll() {
|
||||
// fill consumer medias list
|
||||
c.Medias = append(c.Medias, &streamer.Media{
|
||||
Kind: media.Kind, Direction: media.Direction,
|
||||
Codecs: []*streamer.Codec{codec},
|
||||
})
|
||||
} else {
|
||||
// find consumer media and replace codec with right one
|
||||
for i, m := range c.Medias {
|
||||
if m == media {
|
||||
media.Codecs = []*streamer.Codec{codec}
|
||||
c.Medias[i] = media
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, sender := range c.senders {
|
||||
if sender.Codec == codec {
|
||||
sender.HandleRTP(track)
|
||||
return
|
||||
}
|
||||
|
||||
track = c.bindTrack(track, channelID, codec.PayloadType)
|
||||
track.Codec = codec
|
||||
c.tracks = append(c.tracks, track)
|
||||
|
||||
return track
|
||||
|
||||
// camera with backchannel support
|
||||
case ModeClientProducer:
|
||||
consCodec := media.MatchCodec(track.Codec)
|
||||
consTrack := c.GetTrack(media, consCodec)
|
||||
if consTrack == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return track.Bind(func(packet *rtp.Packet) error {
|
||||
return consTrack.WriteRTP(packet)
|
||||
})
|
||||
}
|
||||
|
||||
println("WARNING: rtsp: AddTrack to wrong mode")
|
||||
var channel byte
|
||||
|
||||
switch c.mode {
|
||||
case core.ModeActiveProducer: // backchannel
|
||||
if channel, err = c.SetupMedia(media, true); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case core.ModePassiveConsumer:
|
||||
channel = byte(len(c.senders)) * 2
|
||||
|
||||
// for consumer is better to use original track codec
|
||||
codec = track.Codec.Clone()
|
||||
// generate new payload type, starting from 96
|
||||
codec.PayloadType = byte(96 + len(c.senders))
|
||||
|
||||
default:
|
||||
panic(core.Caller())
|
||||
}
|
||||
|
||||
// save original codec to sender (can have Codec.Name = ANY)
|
||||
sender := core.NewSender(media, codec)
|
||||
sender.Handler = c.packetWriter(codec, channel)
|
||||
sender.HandleRTP(track)
|
||||
|
||||
c.senders = append(c.senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) bindTrack(
|
||||
track *streamer.Track, channel uint8, payloadType uint8,
|
||||
) *streamer.Track {
|
||||
push := func(packet *rtp.Packet) error {
|
||||
func (c *Conn) packetWriter(codec *core.Codec, channel uint8) core.HandlerFunc {
|
||||
handlerFunc := func(packet *rtp.Packet) {
|
||||
if c.state == StateNone {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
packet.Header.PayloadType = payloadType
|
||||
|
||||
size := packet.MarshalSize()
|
||||
clone := *packet
|
||||
clone.Header.PayloadType = codec.PayloadType
|
||||
|
||||
//log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", track.Codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
|
||||
size := clone.MarshalSize()
|
||||
|
||||
//log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
|
||||
|
||||
data := make([]byte, 4+size)
|
||||
data[0] = '$'
|
||||
data[1] = channel
|
||||
binary.BigEndian.PutUint16(data[2:], uint16(size))
|
||||
data[2] = byte(size >> 8)
|
||||
data[3] = byte(size)
|
||||
|
||||
if _, err := packet.MarshalTo(data[4:]); err != nil {
|
||||
return nil
|
||||
if _, err := clone.MarshalTo(data[4:]); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := c.conn.Write(data); err != nil {
|
||||
return err
|
||||
n, err := c.conn.Write(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.send += size
|
||||
|
||||
return nil
|
||||
c.send += n
|
||||
}
|
||||
|
||||
if !track.Codec.IsRTP() {
|
||||
switch track.Codec.Name {
|
||||
case streamer.CodecH264:
|
||||
wrapper := h264.RTPPay(1500)
|
||||
push = wrapper(push)
|
||||
case streamer.CodecH265:
|
||||
wrapper := h265.RTPPay(1500)
|
||||
push = wrapper(push)
|
||||
case streamer.CodecAAC:
|
||||
wrapper := aac.RTPPay(1500)
|
||||
push = wrapper(push)
|
||||
case streamer.CodecJPEG:
|
||||
wrapper := mjpeg.RTPPay()
|
||||
push = wrapper(push)
|
||||
if !codec.IsRTP() {
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
handlerFunc = h264.RTPPay(1500, handlerFunc)
|
||||
case core.CodecH265:
|
||||
handlerFunc = h265.RTPPay(1500, handlerFunc)
|
||||
case core.CodecAAC:
|
||||
handlerFunc = aac.RTPPay(handlerFunc)
|
||||
case core.CodecJPEG:
|
||||
handlerFunc = mjpeg.RTPPay(handlerFunc)
|
||||
}
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
return handlerFunc
|
||||
}
|
||||
|
||||
+12
-13
@@ -2,7 +2,7 @@ package rtsp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtcp"
|
||||
"github.com/pion/sdp/v3"
|
||||
"net/url"
|
||||
@@ -22,7 +22,7 @@ o=- 0 0 IN IP4 0.0.0.0
|
||||
s=-
|
||||
t=0 0`
|
||||
|
||||
func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
|
||||
func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {
|
||||
// fix bug from Reolink Doorbell
|
||||
if i := bytes.Index(rawSDP, []byte("a=sendonlym=")); i > 0 {
|
||||
rawSDP = append(rawSDP[:i+11], rawSDP[i+10:]...)
|
||||
@@ -47,25 +47,24 @@ func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
|
||||
}
|
||||
}
|
||||
|
||||
medias := streamer.UnmarshalMedias(sd.MediaDescriptions)
|
||||
var medias []*core.Media
|
||||
|
||||
for _, md := range sd.MediaDescriptions {
|
||||
media := core.UnmarshalMedia(md)
|
||||
|
||||
for _, media := range medias {
|
||||
// Check buggy SDP with fmtp for H264 on another track
|
||||
// https://github.com/AlexxIT/WebRTC/issues/419
|
||||
for _, codec := range media.Codecs {
|
||||
if codec.Name == streamer.CodecH264 && codec.FmtpLine == "" {
|
||||
if codec.Name == core.CodecH264 && codec.FmtpLine == "" {
|
||||
codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions)
|
||||
}
|
||||
}
|
||||
|
||||
// fix bug in ONVIF spec
|
||||
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
|
||||
switch media.Direction {
|
||||
case streamer.DirectionRecvonly, "":
|
||||
media.Direction = streamer.DirectionSendonly
|
||||
case streamer.DirectionSendonly:
|
||||
media.Direction = streamer.DirectionRecvonly
|
||||
if media.Direction == "" {
|
||||
media.Direction = core.DirectionRecvonly
|
||||
}
|
||||
|
||||
medias = append(medias, media)
|
||||
}
|
||||
|
||||
return medias, nil
|
||||
@@ -74,7 +73,7 @@ func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
|
||||
func findFmtpLine(payloadType uint8, descriptions []*sdp.MediaDescription) string {
|
||||
s := strconv.Itoa(int(payloadType))
|
||||
for _, md := range descriptions {
|
||||
codec := streamer.UnmarshalCodec(md, s)
|
||||
codec := core.UnmarshalCodec(md, s)
|
||||
if codec.FmtpLine != "" {
|
||||
return codec.FmtpLine
|
||||
}
|
||||
|
||||
+36
-58
@@ -3,87 +3,74 @@ package rtsp
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func (c *Conn) GetMedias() []*streamer.Media {
|
||||
if c.Medias != nil {
|
||||
return c.Medias
|
||||
}
|
||||
func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
core.Assert(media.Direction == core.DirectionRecvonly)
|
||||
|
||||
return []*streamer.Media{
|
||||
{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecAll},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: streamer.KindAudio,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecAll},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||
for _, track := range c.tracks {
|
||||
for _, track := range c.receivers {
|
||||
if track.Codec == codec {
|
||||
return track
|
||||
return track, nil
|
||||
}
|
||||
}
|
||||
|
||||
// can't setup new tracks from play state - forcing a reconnection feature
|
||||
switch c.state {
|
||||
case StatePlay, StateHandle:
|
||||
go c.Close()
|
||||
return streamer.NewTrack(media, codec)
|
||||
case StateConn, StateSetup:
|
||||
default:
|
||||
return nil, fmt.Errorf("RTSP GetTrack from wrong state: %s", c.state)
|
||||
}
|
||||
|
||||
track, err := c.SetupMedia(media, codec, true)
|
||||
channel, err := c.SetupMedia(media, true)
|
||||
if err != nil {
|
||||
return nil
|
||||
return nil, err
|
||||
}
|
||||
return track
|
||||
|
||||
track := core.NewReceiver(media, codec)
|
||||
track.ID = byte(channel)
|
||||
c.receivers = append(c.receivers, track)
|
||||
|
||||
return track, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Start() error {
|
||||
switch c.mode {
|
||||
case ModeClientProducer:
|
||||
case core.ModeActiveProducer:
|
||||
if err := c.Play(); err != nil {
|
||||
return err
|
||||
}
|
||||
case ModeServerProducer:
|
||||
case core.ModePassiveProducer:
|
||||
default:
|
||||
return fmt.Errorf("start wrong mode: %d", c.mode)
|
||||
}
|
||||
|
||||
return c.Handle()
|
||||
if err := c.Handle(); c.state != StateNone {
|
||||
_ = c.conn.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) Stop() error {
|
||||
for _, receiver := range c.receivers {
|
||||
receiver.Close()
|
||||
}
|
||||
for _, sender := range c.senders {
|
||||
sender.Close()
|
||||
}
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
func (c *Conn) MarshalJSON() ([]byte, error) {
|
||||
info := &streamer.Info{
|
||||
info := &core.Info{
|
||||
Type: "RTSP " + c.mode.String(),
|
||||
UserAgent: c.UserAgent,
|
||||
Medias: c.Medias,
|
||||
Tracks: c.tracks,
|
||||
Recv: uint32(c.receive),
|
||||
Send: uint32(c.send),
|
||||
}
|
||||
|
||||
switch c.mode {
|
||||
case ModeUnknown:
|
||||
info.Type = "RTSP unknown"
|
||||
case ModeClientProducer, ModeServerProducer:
|
||||
info.Type = "RTSP source"
|
||||
case ModeServerConsumer:
|
||||
info.Type = "RTSP client"
|
||||
Receivers: c.receivers,
|
||||
Senders: c.senders,
|
||||
Recv: c.recv,
|
||||
Send: c.send,
|
||||
}
|
||||
|
||||
if c.URL != nil {
|
||||
@@ -93,14 +80,5 @@ func (c *Conn) MarshalJSON() ([]byte, error) {
|
||||
info.RemoteAddr = c.conn.RemoteAddr().String()
|
||||
}
|
||||
|
||||
//for i, track := range c.tracks {
|
||||
// k := "track:" + strconv.Itoa(i+1)
|
||||
// if track.MimeType() == streamer.MimeTypeH264 {
|
||||
// v[k] = h264.Describe(track.Caps())
|
||||
// } else {
|
||||
// v[k] = track.MimeType()
|
||||
// }
|
||||
//}
|
||||
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -131,7 +131,7 @@ a=appversion:1.0
|
||||
assert.Nil(t, err)
|
||||
|
||||
codec := medias[0].Codecs[0]
|
||||
assert.Equal(t, streamer.CodecH264, codec.Name)
|
||||
assert.Equal(t, core.CodecH264, codec.Name)
|
||||
|
||||
sps, _ := h264.GetParameterSet(codec.FmtpLine)
|
||||
assert.Nil(t, sps)
|
||||
|
||||
+15
-18
@@ -4,7 +4,7 @@ import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"net"
|
||||
"net/url"
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
func NewServer(conn net.Conn) *Conn {
|
||||
c := new(Conn)
|
||||
c.conn = conn
|
||||
c.mode = ModeServerUnknown
|
||||
c.reader = bufio.NewReader(conn)
|
||||
return c
|
||||
}
|
||||
@@ -24,8 +23,6 @@ func (c *Conn) Auth(username, password string) {
|
||||
c.auth = tcp.NewAuth(info)
|
||||
}
|
||||
|
||||
const transport = "RTP/AVP/TCP;unicast;interleaved="
|
||||
|
||||
func (c *Conn) Accept() error {
|
||||
for {
|
||||
req, err := tcp.ReadRequest(c.reader)
|
||||
@@ -76,14 +73,13 @@ func (c *Conn) Accept() error {
|
||||
}
|
||||
|
||||
// TODO: fix someday...
|
||||
c.channels = map[byte]*streamer.Track{}
|
||||
for i, media := range c.Medias {
|
||||
track := streamer.NewTrack(media, nil)
|
||||
c.tracks = append(c.tracks, track)
|
||||
c.channels[byte(i<<1)] = track
|
||||
track := core.NewReceiver(media, media.Codecs[0])
|
||||
track.ID = byte(i * 2)
|
||||
c.receivers = append(c.receivers, track)
|
||||
}
|
||||
|
||||
c.mode = ModeServerProducer
|
||||
c.mode = core.ModePassiveProducer
|
||||
c.Fire(MethodAnnounce)
|
||||
|
||||
res := &tcp.Response{Request: req}
|
||||
@@ -92,10 +88,10 @@ func (c *Conn) Accept() error {
|
||||
}
|
||||
|
||||
case MethodDescribe:
|
||||
c.mode = ModeServerConsumer
|
||||
c.mode = core.ModePassiveConsumer
|
||||
c.Fire(MethodDescribe)
|
||||
|
||||
if c.tracks == nil {
|
||||
if c.senders == nil {
|
||||
res := &tcp.Response{
|
||||
Status: "404 Not Found",
|
||||
Request: req,
|
||||
@@ -111,17 +107,17 @@ func (c *Conn) Accept() error {
|
||||
}
|
||||
|
||||
// convert tracks to real output medias medias
|
||||
var medias []*streamer.Media
|
||||
for _, track := range c.tracks {
|
||||
media := &streamer.Media{
|
||||
Kind: streamer.GetKind(track.Codec.Name),
|
||||
Direction: streamer.DirectionSendonly,
|
||||
Codecs: []*streamer.Codec{track.Codec},
|
||||
var medias []*core.Media
|
||||
for _, track := range c.senders {
|
||||
media := &core.Media{
|
||||
Kind: core.GetKind(track.Codec.Name),
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{track.Codec},
|
||||
}
|
||||
medias = append(medias, media)
|
||||
}
|
||||
|
||||
res.Body, err = streamer.MarshalSDP(c.SessionName, medias)
|
||||
res.Body, err = core.MarshalSDP(c.SessionName, medias)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -138,6 +134,7 @@ func (c *Conn) Accept() error {
|
||||
Request: req,
|
||||
}
|
||||
|
||||
const transport = "RTP/AVP/TCP;unicast;interleaved="
|
||||
if strings.HasPrefix(tr, transport) {
|
||||
c.Session = "1" // TODO: fixme
|
||||
c.state = StateSetup
|
||||
|
||||
Reference in New Issue
Block a user