install go2rtc on bob
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
## Useful links
|
||||
|
||||
- https://www.kurento.org/blog/rtp-i-intro-rtp-and-sdp
|
||||
@@ -0,0 +1,456 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp/websocket"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
var Timeout = time.Second * 5
|
||||
|
||||
func NewClient(uri string) *Conn {
|
||||
return &Conn{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "rtsp",
|
||||
},
|
||||
uri: uri,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) Dial() (err error) {
|
||||
if c.URL, err = url.Parse(c.uri); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var conn net.Conn
|
||||
|
||||
switch c.Transport {
|
||||
case "", "tcp", "udp":
|
||||
var timeout time.Duration
|
||||
if c.Timeout != 0 {
|
||||
timeout = time.Second * time.Duration(c.Timeout)
|
||||
} else {
|
||||
timeout = core.ConnDialTimeout
|
||||
}
|
||||
conn, err = tcp.Dial(c.URL, timeout)
|
||||
|
||||
if c.Transport != "udp" {
|
||||
c.Protocol = "rtsp+tcp"
|
||||
} else {
|
||||
c.Protocol = "rtsp+udp"
|
||||
}
|
||||
default:
|
||||
conn, err = websocket.Dial(c.Transport)
|
||||
c.Protocol = "ws"
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// remove UserInfo from URL
|
||||
c.auth = tcp.NewAuth(c.URL.User)
|
||||
c.URL.User = nil
|
||||
|
||||
c.conn = conn
|
||||
c.reader = bufio.NewReaderSize(conn, core.BufferSize)
|
||||
c.session = ""
|
||||
c.sequence = 0
|
||||
c.state = StateConn
|
||||
|
||||
c.udpConn = nil
|
||||
c.udpAddr = nil
|
||||
|
||||
c.Connection.RemoteAddr = conn.RemoteAddr().String()
|
||||
c.Connection.Transport = conn
|
||||
c.Connection.URL = c.uri
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do send WriteRequest and receive and process WriteResponse
|
||||
func (c *Conn) Do(req *tcp.Request) (*tcp.Response, error) {
|
||||
if err := c.WriteRequest(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := c.ReadResponse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.Fire(res)
|
||||
|
||||
switch res.StatusCode {
|
||||
case http.StatusOK:
|
||||
return res, nil
|
||||
|
||||
case http.StatusMovedPermanently, http.StatusFound:
|
||||
rawURL := res.Header.Get("Location")
|
||||
|
||||
var u *url.URL
|
||||
if u, err = url.Parse(rawURL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.User == nil {
|
||||
u.User = c.auth.UserInfo() // restore auth if we don't have it in the new URL
|
||||
}
|
||||
|
||||
c.uri = u.String() // so auth will be saved on reconnect
|
||||
|
||||
_ = c.conn.Close()
|
||||
|
||||
if err = c.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.URL = c.URL // because path was changed
|
||||
|
||||
return c.Do(req)
|
||||
|
||||
case http.StatusUnauthorized:
|
||||
switch c.auth.Method {
|
||||
case tcp.AuthNone:
|
||||
if c.auth.ReadNone(res) {
|
||||
return c.Do(req)
|
||||
}
|
||||
return nil, errors.New("user/pass not provided")
|
||||
case tcp.AuthUnknown:
|
||||
if c.auth.Read(res) {
|
||||
return c.Do(req)
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("wrong user/pass")
|
||||
}
|
||||
}
|
||||
|
||||
return res, fmt.Errorf("wrong response on %s", req.Method)
|
||||
}
|
||||
|
||||
func (c *Conn) Options() error {
|
||||
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
|
||||
|
||||
res, err := c.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if val := res.Header.Get("Content-Base"); val != "" {
|
||||
c.URL, err = urlParse(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) Describe() error {
|
||||
// 5.3 Back channel connection
|
||||
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf
|
||||
req := &tcp.Request{
|
||||
Method: MethodDescribe,
|
||||
URL: c.URL,
|
||||
Header: map[string][]string{
|
||||
"Accept": {"application/sdp"},
|
||||
},
|
||||
}
|
||||
|
||||
if c.Backchannel {
|
||||
req.Header.Set("Require", "www.onvif.org/ver20/backchannel")
|
||||
}
|
||||
|
||||
if c.UserAgent != "" {
|
||||
// this camera will answer with 401 on DESCRIBE without User-Agent
|
||||
// https://github.com/AlexxIT/go2rtc/issues/235
|
||||
req.Header.Set("User-Agent", c.UserAgent)
|
||||
}
|
||||
|
||||
res, err := c.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if val := res.Header.Get("Content-Base"); val != "" {
|
||||
c.URL, err = urlParse(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.SDP = string(res.Body) // for info
|
||||
|
||||
medias, err := UnmarshalSDP(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Media != "" {
|
||||
clone := make([]*core.Media, 0, len(medias))
|
||||
for _, media := range medias {
|
||||
if strings.Contains(c.Media, media.Kind) {
|
||||
clone = append(clone, media)
|
||||
}
|
||||
}
|
||||
medias = clone
|
||||
}
|
||||
|
||||
// TODO: rewrite more smart
|
||||
if c.Medias == nil {
|
||||
c.Medias = medias
|
||||
} else if len(c.Medias) > len(medias) {
|
||||
c.Medias = c.Medias[:len(medias)]
|
||||
}
|
||||
|
||||
c.mode = core.ModeActiveProducer
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) Announce() (err error) {
|
||||
req := &tcp.Request{
|
||||
Method: MethodAnnounce,
|
||||
URL: c.URL,
|
||||
Header: map[string][]string{
|
||||
"Content-Type": {"application/sdp"},
|
||||
},
|
||||
}
|
||||
|
||||
req.Body, err = core.MarshalSDP(c.SessionName, c.Medias)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.Do(req)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) Record() (err error) {
|
||||
req := &tcp.Request{
|
||||
Method: MethodRecord,
|
||||
URL: c.URL,
|
||||
Header: map[string][]string{
|
||||
"Range": {"npt=0.000-"},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = c.Do(req)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) SetupMedia(media *core.Media) (byte, error) {
|
||||
var transport string
|
||||
|
||||
if c.Transport == "udp" {
|
||||
conn1, conn2, err := ListenUDPPair()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
c.udpConn = append(c.udpConn, conn1, conn2)
|
||||
|
||||
port := conn1.LocalAddr().(*net.UDPAddr).Port
|
||||
transport = fmt.Sprintf("RTP/AVP;unicast;client_port=%d-%d", port, port+1)
|
||||
} else {
|
||||
// try to use media position as channel number
|
||||
for i, m := range c.Medias {
|
||||
if m.Equal(media) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if transport == "" {
|
||||
return 0, fmt.Errorf("wrong media: %v", media)
|
||||
}
|
||||
|
||||
rawURL := media.ID // control
|
||||
if !strings.Contains(rawURL, "://") {
|
||||
rawURL = c.URL.String()
|
||||
// prefix check for https://github.com/AlexxIT/go2rtc/issues/1236
|
||||
if !strings.HasSuffix(rawURL, "/") && !strings.HasPrefix(media.ID, "/") {
|
||||
rawURL += "/"
|
||||
}
|
||||
rawURL += media.ID
|
||||
}
|
||||
trackURL, err := urlParse(rawURL)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
req := &tcp.Request{
|
||||
Method: MethodSetup,
|
||||
URL: trackURL,
|
||||
Header: map[string][]string{
|
||||
"Transport": {transport},
|
||||
},
|
||||
}
|
||||
|
||||
res, err := c.Do(req)
|
||||
if err != nil {
|
||||
// some Dahua/Amcrest cameras fail here because two simultaneous
|
||||
// backchannel connections
|
||||
if c.Backchannel {
|
||||
c.Backchannel = false
|
||||
if err = c.Reconnect(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return c.SetupMedia(media)
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if c.session == "" {
|
||||
// Session: 7116520596809429228
|
||||
// Session: 216525287999;timeout=60
|
||||
if s := res.Header.Get("Session"); s != "" {
|
||||
if i := strings.IndexByte(s, ';'); i > 0 {
|
||||
c.session = s[:i]
|
||||
if i = strings.Index(s, "timeout="); i > 0 {
|
||||
c.keepalive, _ = strconv.Atoi(s[i+8:])
|
||||
}
|
||||
} else {
|
||||
c.session = s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse server response
|
||||
transport = res.Header.Get("Transport")
|
||||
|
||||
if c.Transport == "udp" {
|
||||
channel := byte(len(c.udpConn) - 2)
|
||||
|
||||
// Dahua: RTP/AVP/UDP;unicast;client_port=49292-49293;server_port=43670-43671;ssrc=7CB694B4
|
||||
// OpenIPC: RTP/AVP/UDP;unicast;client_port=59612-59613
|
||||
if s := core.Between(transport, "server_port=", ";"); s != "" {
|
||||
s1, s2, _ := strings.Cut(s, "-")
|
||||
port1 := core.Atoi(s1)
|
||||
port2 := core.Atoi(s2)
|
||||
// TODO: more smart handling empty server ports
|
||||
if port1 > 0 && port2 > 0 {
|
||||
remoteIP := c.conn.RemoteAddr().(*net.TCPAddr).IP
|
||||
c.udpAddr = append(c.udpAddr,
|
||||
&net.UDPAddr{IP: remoteIP, Port: port1},
|
||||
&net.UDPAddr{IP: remoteIP, Port: port2},
|
||||
)
|
||||
|
||||
go func() {
|
||||
// Try to open a hole in the NAT router (to allow incoming UDP packets)
|
||||
// by send a UDP packet for RTP and RTCP to the remote RTSP server.
|
||||
// https://github.com/FFmpeg/FFmpeg/blob/aa91ae25b88e195e6af4248e0ab30605735ca1cd/libavformat/rtpdec.c#L416-L438
|
||||
_, _ = c.WriteToUDP([]byte{0x80, 0x00, 0x00, 0x00}, channel)
|
||||
_, _ = c.WriteToUDP([]byte{0x80, 0xC8, 0x00, 0x01}, channel+1)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
} else {
|
||||
// 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
|
||||
// Escam Q6 has a bug:
|
||||
// Transport: RTP/AVP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0-1
|
||||
s := core.Between(transport, "interleaved=", "-")
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("wrong transport: %s", transport)
|
||||
}
|
||||
|
||||
return byte(i), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) Play() (err error) {
|
||||
req := &tcp.Request{Method: MethodPlay, URL: c.URL}
|
||||
return c.WriteRequest(req)
|
||||
}
|
||||
|
||||
func (c *Conn) Teardown() (err error) {
|
||||
// allow TEARDOWN from any state (ex. ANNOUNCE > SETUP)
|
||||
req := &tcp.Request{Method: MethodTeardown, URL: c.URL}
|
||||
return c.WriteRequest(req)
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
if c.mode == core.ModeActiveProducer {
|
||||
_ = c.Teardown()
|
||||
}
|
||||
if c.OnClose != nil {
|
||||
_ = c.OnClose()
|
||||
}
|
||||
for _, conn := range c.udpConn {
|
||||
_ = conn.Close()
|
||||
}
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Conn) WriteToUDP(b []byte, channel byte) (int, error) {
|
||||
return c.udpConn[channel].WriteToUDP(b, c.udpAddr[channel])
|
||||
}
|
||||
|
||||
const listenUDPAttemps = 10
|
||||
|
||||
var listenUDPMu sync.Mutex
|
||||
|
||||
func ListenUDPPair() (*net.UDPConn, *net.UDPConn, error) {
|
||||
listenUDPMu.Lock()
|
||||
defer listenUDPMu.Unlock()
|
||||
|
||||
for i := 0; i < listenUDPAttemps; i++ {
|
||||
// Get a random even port from the OS
|
||||
ln1, err := net.ListenUDP("udp", &net.UDPAddr{IP: nil, Port: 0})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var port1 = ln1.LocalAddr().(*net.UDPAddr).Port
|
||||
var port2 int
|
||||
|
||||
// 11. RTP over Network and Transport Protocols (https://www.ietf.org/rfc/rfc3550.txt)
|
||||
// For UDP and similar protocols,
|
||||
// RTP SHOULD use an even destination port number and the corresponding
|
||||
// RTCP stream SHOULD use the next higher (odd) destination port number
|
||||
if port1&1 > 0 {
|
||||
port2 = port1 - 1
|
||||
} else {
|
||||
port2 = port1 + 1
|
||||
}
|
||||
|
||||
ln2, err := net.ListenUDP("udp", &net.UDPAddr{IP: nil, Port: port2})
|
||||
if err != nil {
|
||||
_ = ln1.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
if port1 < port2 {
|
||||
return ln1, ln2, nil
|
||||
} else {
|
||||
return ln2, ln1, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, fmt.Errorf("can't open two UDP ports")
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTimeout(t *testing.T) {
|
||||
Timeout = time.Millisecond
|
||||
|
||||
ln, err := net.Listen("tcp", "localhost:0")
|
||||
require.Nil(t, err)
|
||||
|
||||
client := NewClient("rtsp://" + ln.Addr().String() + "/stream")
|
||||
client.Backchannel = true
|
||||
|
||||
err = client.Dial()
|
||||
require.Nil(t, err)
|
||||
|
||||
err = client.Describe()
|
||||
require.ErrorIs(t, err, os.ErrDeadlineExceeded)
|
||||
}
|
||||
|
||||
func TestMissedControl(t *testing.T) {
|
||||
Timeout = time.Millisecond
|
||||
|
||||
ln, err := net.Listen("tcp", "localhost:0")
|
||||
require.Nil(t, err)
|
||||
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
require.Nil(t, err)
|
||||
|
||||
b := make([]byte, 8192)
|
||||
for {
|
||||
n, err := conn.Read(b)
|
||||
require.Nil(t, err)
|
||||
|
||||
req := string(b[:n])
|
||||
|
||||
switch req[:4] {
|
||||
case "DESC":
|
||||
_, _ = conn.Write([]byte(`RTSP/1.0 200 OK
|
||||
Cseq: 1
|
||||
Content-Length: 495
|
||||
Content-Type: application/sdp
|
||||
|
||||
v=0
|
||||
o=- 1 1 IN IP4 0.0.0.0
|
||||
s=go2rtc/1.2.0
|
||||
c=IN IP4 0.0.0.0
|
||||
t=0 0
|
||||
m=audio 0 RTP/AVP 96
|
||||
a=rtpmap:96 MPEG4-GENERIC/48000/2
|
||||
a=fmtp:96 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=119056E500
|
||||
m=audio 0 RTP/AVP 97
|
||||
a=rtpmap:97 OPUS/48000/2
|
||||
a=fmtp:97 sprop-stereo=1
|
||||
m=video 0 RTP/AVP 98
|
||||
a=rtpmap:98 H264/90000
|
||||
a=fmtp:98 packetization-mode=1; sprop-parameter-sets=Z2QAKaw0yAeAIn5cBagICAoAAAfQAAE4gdDAAjhAACOEF3lxoYAEcIAARwgu8uFA,aO48MAA=; profile-level-id=640029
|
||||
`))
|
||||
|
||||
case "SETU":
|
||||
_, _ = conn.Write([]byte(`RTSP/1.0 200 OK
|
||||
Transport: RTP/AVP/TCP;unicast;interleaved=4-5
|
||||
Cseq: 3
|
||||
Session: 1
|
||||
|
||||
`))
|
||||
|
||||
default:
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
client := NewClient("rtsp://" + ln.Addr().String() + "/stream")
|
||||
client.Backchannel = true
|
||||
|
||||
err = client.Dial()
|
||||
require.Nil(t, err)
|
||||
|
||||
err = client.Describe()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, client.Medias, 3)
|
||||
|
||||
ch, err := client.SetupMedia(client.Medias[2])
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, ch, byte(4))
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
core.Connection
|
||||
core.Listener
|
||||
|
||||
// public
|
||||
|
||||
Backchannel bool
|
||||
Media string
|
||||
OnClose func() error
|
||||
PacketSize uint16
|
||||
SessionName string
|
||||
Timeout int
|
||||
Transport string // custom transport support, ex. RTSP over WebSocket
|
||||
|
||||
URL *url.URL
|
||||
|
||||
// internal
|
||||
|
||||
auth *tcp.Auth
|
||||
conn net.Conn
|
||||
keepalive int
|
||||
mode core.Mode
|
||||
playOK bool
|
||||
playErr error
|
||||
reader *bufio.Reader
|
||||
sequence int
|
||||
session string
|
||||
uri string
|
||||
|
||||
state State
|
||||
stateMu sync.Mutex
|
||||
|
||||
udpConn []*net.UDPConn
|
||||
udpAddr []*net.UDPAddr
|
||||
}
|
||||
|
||||
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 MethodSetup
|
||||
case StatePlay:
|
||||
return MethodPlay
|
||||
}
|
||||
return strconv.Itoa(int(s))
|
||||
}
|
||||
|
||||
const (
|
||||
StateNone State = iota
|
||||
StateConn
|
||||
StateSetup
|
||||
StatePlay
|
||||
)
|
||||
|
||||
func (c *Conn) Handle() (err error) {
|
||||
var timeout time.Duration
|
||||
|
||||
switch c.mode {
|
||||
case core.ModeActiveProducer:
|
||||
var keepaliveDT time.Duration
|
||||
|
||||
if c.keepalive > 5 {
|
||||
keepaliveDT = time.Duration(c.keepalive-5) * time.Second
|
||||
} else {
|
||||
keepaliveDT = 25 * time.Second
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go c.handleKeepalive(ctx, keepaliveDT)
|
||||
defer cancel()
|
||||
|
||||
if c.Timeout == 0 {
|
||||
// polling frames from remote RTSP Server (ex Camera)
|
||||
timeout = time.Second * 5
|
||||
|
||||
if len(c.Receivers) == 0 || c.Transport == "udp" {
|
||||
// if we only send audio to camera
|
||||
// https://github.com/AlexxIT/go2rtc/issues/659
|
||||
timeout += keepaliveDT
|
||||
}
|
||||
} else {
|
||||
timeout = time.Second * time.Duration(c.Timeout)
|
||||
}
|
||||
|
||||
case core.ModePassiveProducer:
|
||||
// polling frames from remote RTSP Client (ex FFmpeg)
|
||||
if c.Timeout == 0 {
|
||||
timeout = time.Second * 15
|
||||
} else {
|
||||
timeout = time.Second * time.Duration(c.Timeout)
|
||||
}
|
||||
|
||||
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 i := 0; i < len(c.udpConn); i++ {
|
||||
go c.handleUDPData(byte(i))
|
||||
}
|
||||
|
||||
for c.state != StateNone {
|
||||
ts := time.Now()
|
||||
|
||||
_ = c.conn.SetReadDeadline(ts.Add(timeout))
|
||||
|
||||
if err = c.handleTCPData(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) handleKeepalive(ctx context.Context, d time.Duration) {
|
||||
ticker := time.NewTicker(d)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
|
||||
if err := c.WriteRequest(req); err != nil {
|
||||
return
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) handleUDPData(channel byte) {
|
||||
// TODO: handle timeouts and drop TCP connection after any error
|
||||
conn := c.udpConn[channel]
|
||||
|
||||
for {
|
||||
// TP-Link Tapo camera has crazy 10000 bytes packet size
|
||||
buf := make([]byte, 10240)
|
||||
|
||||
n, _, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = c.handleRawPacket(channel, buf[:n]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) handleTCPData() error {
|
||||
// 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
|
||||
var err error
|
||||
|
||||
buf4, err = c.reader.Peek(4)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var channel byte
|
||||
var size uint16
|
||||
|
||||
if buf4[0] != '$' {
|
||||
switch string(buf4) {
|
||||
case "RTSP":
|
||||
var res *tcp.Response
|
||||
if res, err = c.ReadResponse(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Fire(res)
|
||||
// for playing backchannel only after OK response on play
|
||||
c.playOK = true
|
||||
return nil
|
||||
|
||||
case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_":
|
||||
var req *tcp.Request
|
||||
if req, err = c.ReadRequest(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Fire(req)
|
||||
if req.Method == MethodOptions {
|
||||
res := &tcp.Response{Request: req}
|
||||
if err = c.WriteResponse(res); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
default:
|
||||
c.Fire("RTSP wrong input")
|
||||
|
||||
for i := 0; ; i++ {
|
||||
// search next start symbol
|
||||
if _, err = c.reader.ReadBytes('$'); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if channel, err = c.reader.ReadByte(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: better check maximum good channel ID
|
||||
if channel >= 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// hope that the odd channels are always RTCP
|
||||
channel = 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 err
|
||||
}
|
||||
}
|
||||
|
||||
// init memory for data
|
||||
buf := make([]byte, size)
|
||||
if _, err = io.ReadFull(c.reader, buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Recv += int(size)
|
||||
|
||||
return c.handleRawPacket(channel, buf)
|
||||
}
|
||||
|
||||
func (c *Conn) handleRawPacket(channel byte, buf []byte) error {
|
||||
if channel&1 == 0 {
|
||||
packet := &rtp.Packet{}
|
||||
if err := packet.Unmarshal(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, receiver := range c.Receivers {
|
||||
if receiver.ID == channel {
|
||||
receiver.WriteRTP(packet)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
msg := &RTCP{Channel: channel}
|
||||
|
||||
if err := msg.Header.Unmarshal(buf); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
//var err error
|
||||
//msg.Packets, err = rtcp.Unmarshal(buf)
|
||||
//if err != nil {
|
||||
// return nil
|
||||
//}
|
||||
|
||||
c.Fire(msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) WriteRequest(req *tcp.Request) error {
|
||||
if req.Proto == "" {
|
||||
req.Proto = ProtoRTSP
|
||||
}
|
||||
|
||||
if req.Header == nil {
|
||||
req.Header = make(map[string][]string)
|
||||
}
|
||||
|
||||
c.sequence++
|
||||
// important to send case sensitive CSeq
|
||||
// https://github.com/AlexxIT/go2rtc/issues/7
|
||||
req.Header["CSeq"] = []string{strconv.Itoa(c.sequence)}
|
||||
|
||||
c.auth.Write(req)
|
||||
|
||||
if c.session != "" {
|
||||
req.Header.Set("Session", c.session)
|
||||
}
|
||||
|
||||
if req.Body != nil {
|
||||
val := strconv.Itoa(len(req.Body))
|
||||
req.Header.Set("Content-Length", val)
|
||||
}
|
||||
|
||||
c.Fire(req)
|
||||
|
||||
if err := c.conn.SetWriteDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return req.Write(c.conn)
|
||||
}
|
||||
|
||||
func (c *Conn) ReadRequest() (*tcp.Request, error) {
|
||||
if err := c.conn.SetReadDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tcp.ReadRequest(c.reader)
|
||||
}
|
||||
|
||||
func (c *Conn) WriteResponse(res *tcp.Response) error {
|
||||
if res.Proto == "" {
|
||||
res.Proto = ProtoRTSP
|
||||
}
|
||||
|
||||
if res.Status == "" {
|
||||
res.Status = "200 OK"
|
||||
}
|
||||
|
||||
if res.Header == nil {
|
||||
res.Header = make(map[string][]string)
|
||||
}
|
||||
|
||||
if res.Request != nil && res.Request.Header != nil {
|
||||
seq := res.Request.Header.Get("CSeq")
|
||||
if seq != "" {
|
||||
res.Header.Set("CSeq", seq)
|
||||
}
|
||||
}
|
||||
|
||||
if c.session != "" {
|
||||
if res.Request != nil && res.Request.Method == MethodSetup {
|
||||
res.Header.Set("Session", c.session+";timeout=60")
|
||||
} else {
|
||||
res.Header.Set("Session", c.session)
|
||||
}
|
||||
}
|
||||
|
||||
if res.Body != nil {
|
||||
val := strconv.Itoa(len(res.Body))
|
||||
res.Header.Set("Content-Length", val)
|
||||
}
|
||||
|
||||
c.Fire(res)
|
||||
|
||||
if err := c.conn.SetWriteDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return res.Write(c.conn)
|
||||
}
|
||||
|
||||
func (c *Conn) ReadResponse() (*tcp.Response, error) {
|
||||
if err := c.conn.SetReadDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tcp.ReadResponse(c.reader)
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"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/pcm"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func (c *Conn) GetMedias() []*core.Media {
|
||||
//core.Assert(c.Medias != nil)
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) (err error) {
|
||||
var channel byte
|
||||
|
||||
switch c.mode {
|
||||
case core.ModeActiveProducer: // backchannel
|
||||
c.stateMu.Lock()
|
||||
defer c.stateMu.Unlock()
|
||||
|
||||
if c.state == StatePlay {
|
||||
if err = c.Reconnect(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if channel, err = c.SetupMedia(media); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.state = StateSetup
|
||||
|
||||
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)
|
||||
// important to send original codec for valid IsRTP check
|
||||
sender.Handler = c.packetWriter(track.Codec, channel, codec.PayloadType)
|
||||
|
||||
if c.mode == core.ModeActiveProducer && track.Codec.Name == core.CodecPCMA {
|
||||
// Fix Reolink Doorbell https://github.com/AlexxIT/go2rtc/issues/331
|
||||
sender.Handler = pcm.RepackG711(true, sender.Handler)
|
||||
}
|
||||
|
||||
sender.HandleRTP(track)
|
||||
|
||||
c.Senders = append(c.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
startVideoBuf = 32 * 1024 // 32KB
|
||||
startAudioBuf = 2 * 1024 // 2KB
|
||||
maxBuf = 1024 * 1024 // 1MB
|
||||
rtpHdr = 12 // basic RTP header size
|
||||
intHdr = 4 // interleaved header size
|
||||
)
|
||||
|
||||
func (c *Conn) packetWriter(codec *core.Codec, channel, payloadType uint8) core.HandlerFunc {
|
||||
var buf []byte
|
||||
var n int
|
||||
|
||||
video := codec.IsVideo()
|
||||
if video {
|
||||
buf = make([]byte, startVideoBuf)
|
||||
} else {
|
||||
buf = make([]byte, startAudioBuf)
|
||||
}
|
||||
|
||||
flushBuf := func() {
|
||||
//log.Printf("[rtsp] channel:%2d write_size:%6d buffer_size:%6d", channel, n, len(buf))
|
||||
if err := c.writeInterleavedData(buf[:n]); err != nil {
|
||||
c.Send += n
|
||||
}
|
||||
n = 0
|
||||
}
|
||||
|
||||
handlerFunc := func(packet *rtp.Packet) {
|
||||
if c.state == StateNone {
|
||||
return
|
||||
}
|
||||
|
||||
clone := rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: packet.Marker,
|
||||
PayloadType: payloadType,
|
||||
SequenceNumber: packet.SequenceNumber,
|
||||
Timestamp: packet.Timestamp,
|
||||
SSRC: packet.SSRC,
|
||||
},
|
||||
Payload: packet.Payload,
|
||||
}
|
||||
|
||||
if !video {
|
||||
packet.Marker = true // better to have marker on all audio packets
|
||||
}
|
||||
|
||||
size := rtpHdr + len(packet.Payload)
|
||||
|
||||
if l := len(buf); n+intHdr+size > l {
|
||||
if l < maxBuf {
|
||||
buf = append(buf, make([]byte, l)...) // double buffer size
|
||||
} else {
|
||||
flushBuf()
|
||||
}
|
||||
}
|
||||
|
||||
//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)
|
||||
|
||||
chunk := buf[n:]
|
||||
_ = chunk[4] // bounds
|
||||
chunk[0] = '$'
|
||||
chunk[1] = channel
|
||||
chunk[2] = byte(size >> 8)
|
||||
chunk[3] = byte(size)
|
||||
|
||||
if _, err := clone.MarshalTo(chunk[4:]); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
n += 4 + size
|
||||
|
||||
if !packet.Marker || !c.playOK {
|
||||
// collect continious video packets to buffer
|
||||
// or wait OK for PLAY command for backchannel
|
||||
//log.Printf("[rtsp] collecting buffer ok=%t", c.playOK)
|
||||
return
|
||||
}
|
||||
|
||||
flushBuf()
|
||||
}
|
||||
|
||||
if !codec.IsRTP() {
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
handlerFunc = h264.RTPPay(c.PacketSize, handlerFunc)
|
||||
case core.CodecH265:
|
||||
handlerFunc = h265.RTPPay(c.PacketSize, handlerFunc)
|
||||
case core.CodecAAC:
|
||||
handlerFunc = aac.RTPPay(handlerFunc)
|
||||
case core.CodecJPEG:
|
||||
handlerFunc = mjpeg.RTPPay(handlerFunc)
|
||||
}
|
||||
} else if codec.Name == core.CodecPCML {
|
||||
handlerFunc = pcm.LittleToBig(handlerFunc)
|
||||
} else if c.PacketSize != 0 {
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
handlerFunc = h264.RTPPay(c.PacketSize, handlerFunc)
|
||||
handlerFunc = h264.RTPDepay(codec, handlerFunc)
|
||||
case core.CodecH265:
|
||||
handlerFunc = h265.RTPPay(c.PacketSize, handlerFunc)
|
||||
handlerFunc = h265.RTPDepay(codec, handlerFunc)
|
||||
}
|
||||
}
|
||||
|
||||
return handlerFunc
|
||||
}
|
||||
|
||||
func (c *Conn) writeInterleavedData(data []byte) error {
|
||||
if c.Transport != "udp" {
|
||||
_ = c.conn.SetWriteDeadline(time.Now().Add(Timeout))
|
||||
_, err := c.conn.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
for len(data) >= 4 && data[0] == '$' {
|
||||
channel := data[1]
|
||||
size := uint16(data[2])<<8 | uint16(data[3])
|
||||
rtpData := data[4 : 4+size]
|
||||
|
||||
if _, err := c.WriteToUDP(rtpData, channel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data = data[4+size:]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtcp"
|
||||
"github.com/pion/sdp/v3"
|
||||
)
|
||||
|
||||
type RTCP struct {
|
||||
Channel byte
|
||||
Header rtcp.Header
|
||||
Packets []rtcp.Packet
|
||||
}
|
||||
|
||||
const sdpHeader = `v=0
|
||||
o=- 0 0 IN IP4 0.0.0.0
|
||||
s=-
|
||||
t=0 0`
|
||||
|
||||
func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {
|
||||
sd := &sdp.SessionDescription{}
|
||||
if err := sd.Unmarshal(rawSDP); err != nil {
|
||||
// fix multiple `s=` https://github.com/AlexxIT/WebRTC/issues/417
|
||||
rawSDP = regexp.MustCompile("\ns=[^\n]+").ReplaceAll(rawSDP, nil)
|
||||
|
||||
// fix broken `c=` https://github.com/AlexxIT/go2rtc/issues/1426
|
||||
rawSDP = regexp.MustCompile("\nc=[^\n]+").ReplaceAll(rawSDP, nil)
|
||||
|
||||
// fix SDP header for some cameras
|
||||
if i := bytes.Index(rawSDP, []byte("\nm=")); i > 0 {
|
||||
rawSDP = append([]byte(sdpHeader), rawSDP[i:]...)
|
||||
}
|
||||
|
||||
// Fix invalid media type (errSDPInvalidValue) caused by
|
||||
// some TP-LINK IP camera, e.g. TL-IPC44GW
|
||||
for _, b := range regexp.MustCompile("m=[^ ]+ ").FindAll(rawSDP, -1) {
|
||||
switch string(b[2 : len(b)-1]) {
|
||||
case "audio", "video", "application":
|
||||
default:
|
||||
rawSDP = bytes.Replace(rawSDP, b, []byte("m=application "), 1)
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
rawSDP = append(rawSDP, '\n')
|
||||
}
|
||||
|
||||
sd = &sdp.SessionDescription{}
|
||||
err = sd.Unmarshal(rawSDP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// fix buggy camera https://github.com/AlexxIT/go2rtc/issues/771
|
||||
forceDirection := sd.Origin.Username == "CV-RTSPHandler"
|
||||
|
||||
var medias []*core.Media
|
||||
|
||||
for _, md := range sd.MediaDescriptions {
|
||||
media := core.UnmarshalMedia(md)
|
||||
|
||||
// Check buggy SDP with fmtp for H264 on another track
|
||||
// https://github.com/AlexxIT/WebRTC/issues/419
|
||||
for _, codec := range media.Codecs {
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
if codec.FmtpLine == "" {
|
||||
codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions)
|
||||
}
|
||||
case core.CodecH265:
|
||||
if codec.FmtpLine != "" {
|
||||
// all three parameters are needed for a valid fmtp line
|
||||
// https://github.com/AlexxIT/go2rtc/pull/1588
|
||||
if !strings.Contains(codec.FmtpLine, "sprop-vps=") ||
|
||||
!strings.Contains(codec.FmtpLine, "sprop-sps=") ||
|
||||
!strings.Contains(codec.FmtpLine, "sprop-pps=") {
|
||||
codec.FmtpLine = ""
|
||||
}
|
||||
}
|
||||
case core.CodecOpus:
|
||||
// fix OPUS for some cameras https://datatracker.ietf.org/doc/html/rfc7587
|
||||
codec.ClockRate = 48000
|
||||
codec.Channels = 2
|
||||
}
|
||||
}
|
||||
|
||||
if media.Direction == "" || forceDirection {
|
||||
media.Direction = core.DirectionRecvonly
|
||||
}
|
||||
|
||||
medias = append(medias, media)
|
||||
}
|
||||
|
||||
return medias, nil
|
||||
}
|
||||
|
||||
func findFmtpLine(payloadType uint8, descriptions []*sdp.MediaDescription) string {
|
||||
s := strconv.Itoa(int(payloadType))
|
||||
for _, md := range descriptions {
|
||||
codec := core.UnmarshalCodec(md, s)
|
||||
if codec.FmtpLine != "" {
|
||||
return codec.FmtpLine
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// urlParse fix bugs:
|
||||
// 1. Content-Base: rtsp://::ffff:192.168.1.123/onvif/profile.1/
|
||||
// 2. Content-Base: rtsp://rtsp://turret2-cam.lan:554/stream1/
|
||||
// 3. Content-Base: 192.168.253.220:1935/
|
||||
func urlParse(rawURL string) (*url.URL, error) {
|
||||
// fix https://github.com/AlexxIT/go2rtc/issues/830
|
||||
if strings.HasPrefix(rawURL, "rtsp://rtsp://") {
|
||||
rawURL = rawURL[7:]
|
||||
}
|
||||
|
||||
// fix https://github.com/AlexxIT/go2rtc/issues/1852
|
||||
if !strings.Contains(rawURL, "://") {
|
||||
rawURL = "rtsp://" + rawURL
|
||||
}
|
||||
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil && strings.HasSuffix(err.Error(), "after host") {
|
||||
if i := indexN(rawURL, '/', 3); i > 0 {
|
||||
return urlParse(rawURL[:i] + ":" + rawURL[i:])
|
||||
}
|
||||
}
|
||||
|
||||
return u, err
|
||||
}
|
||||
|
||||
func indexN(s string, c byte, n int) int {
|
||||
var offset int
|
||||
for {
|
||||
i := strings.IndexByte(s[offset:], c)
|
||||
if i < 0 {
|
||||
break
|
||||
}
|
||||
if n--; n == 0 {
|
||||
return offset + i
|
||||
}
|
||||
offset += i + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
core.Assert(media.Direction == core.DirectionRecvonly)
|
||||
|
||||
for _, track := range c.Receivers {
|
||||
if track.Codec == codec {
|
||||
return track, nil
|
||||
}
|
||||
}
|
||||
|
||||
c.stateMu.Lock()
|
||||
defer c.stateMu.Unlock()
|
||||
|
||||
var channel byte
|
||||
|
||||
switch c.mode {
|
||||
case core.ModeActiveProducer:
|
||||
if c.state == StatePlay {
|
||||
if err := c.Reconnect(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
channel, err = c.SetupMedia(media)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.state = StateSetup
|
||||
case core.ModePassiveConsumer:
|
||||
// Backchannel
|
||||
channel = byte(len(c.Senders)) * 2
|
||||
default:
|
||||
return nil, errors.New("rtsp: wrong mode for GetTrack")
|
||||
}
|
||||
|
||||
track := core.NewReceiver(media, codec)
|
||||
track.ID = channel
|
||||
c.Receivers = append(c.Receivers, track)
|
||||
|
||||
return track, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Start() (err error) {
|
||||
core.Assert(c.mode == core.ModeActiveProducer || c.mode == core.ModePassiveProducer)
|
||||
|
||||
for {
|
||||
ok := false
|
||||
|
||||
c.stateMu.Lock()
|
||||
switch c.state {
|
||||
case StateNone:
|
||||
err = nil
|
||||
case StateConn:
|
||||
err = errors.New("start from CONN state")
|
||||
case StateSetup:
|
||||
switch c.mode {
|
||||
case core.ModeActiveProducer:
|
||||
err = c.Play()
|
||||
case core.ModePassiveProducer:
|
||||
err = nil
|
||||
default:
|
||||
err = errors.New("start from wrong mode: " + c.mode.String())
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
c.state = StatePlay
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
c.stateMu.Unlock()
|
||||
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Handler can return different states:
|
||||
// 1. None after PLAY should exit without error
|
||||
// 2. Play after PLAY should exit from Start with error
|
||||
// 3. Setup after PLAY should Play once again
|
||||
err = c.Handle()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) Stop() (err error) {
|
||||
for _, receiver := range c.Receivers {
|
||||
receiver.Close()
|
||||
}
|
||||
for _, sender := range c.Senders {
|
||||
sender.Close()
|
||||
}
|
||||
|
||||
c.stateMu.Lock()
|
||||
if c.state != StateNone {
|
||||
c.state = StateNone
|
||||
err = c.Close()
|
||||
}
|
||||
c.stateMu.Unlock()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(c.Connection)
|
||||
}
|
||||
|
||||
func (c *Conn) Reconnect() error {
|
||||
c.Fire("RTSP reconnect")
|
||||
|
||||
// close current session
|
||||
_ = c.Close()
|
||||
|
||||
// start new session
|
||||
if err := c.Dial(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.Describe(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// restore previous medias
|
||||
for _, receiver := range c.Receivers {
|
||||
if _, err := c.SetupMedia(receiver.Media); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, sender := range c.Senders {
|
||||
if _, err := c.SetupMedia(sender.Media); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestURLParse(t *testing.T) {
|
||||
// https://github.com/AlexxIT/WebRTC/issues/395
|
||||
base := "rtsp://::ffff:192.168.1.123/onvif/profile.1/"
|
||||
u, err := urlParse(base)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "::ffff:192.168.1.123:", u.Host)
|
||||
|
||||
// https://github.com/AlexxIT/go2rtc/issues/208
|
||||
base = "rtsp://rtsp://turret2-cam.lan:554/stream1/"
|
||||
u, err = urlParse(base)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "turret2-cam.lan:554", u.Host)
|
||||
|
||||
// https://github.com/AlexxIT/go2rtc/issues/1852
|
||||
base = "192.168.253.220:1935/"
|
||||
u, err = urlParse(base)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "192.168.253.220:1935", u.Host)
|
||||
}
|
||||
|
||||
func TestBugSDP1(t *testing.T) {
|
||||
// https://github.com/AlexxIT/WebRTC/issues/417
|
||||
s := `v=0
|
||||
o=- 91674849066 1 IN IP4 192.168.1.123
|
||||
s=RtspServer
|
||||
i=live
|
||||
t=0 0
|
||||
a=control:*
|
||||
a=range:npt=0-
|
||||
m=video 0 RTP/AVP 96
|
||||
c=IN IP4 0.0.0.0
|
||||
s=RtspServer
|
||||
i=live
|
||||
a=control:track0
|
||||
a=range:npt=0-
|
||||
a=rtpmap:96 H264/90000
|
||||
a=fmtp:96 packetization-mode=1;profile-level-id=42001E;sprop-parameter-sets=Z0IAHvQCgC3I,aM48gA==
|
||||
a=control:track0
|
||||
m=audio 0 RTP/AVP 97
|
||||
c=IN IP4 0.0.0.0
|
||||
s=RtspServer
|
||||
i=live
|
||||
a=control:track1
|
||||
a=range:npt=0-
|
||||
a=rtpmap:97 MPEG4-GENERIC/8000/1
|
||||
a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
|
||||
a=control:track1
|
||||
`
|
||||
medias, err := UnmarshalSDP([]byte(s))
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, medias)
|
||||
}
|
||||
|
||||
func TestBugSDP2(t *testing.T) {
|
||||
// https://github.com/AlexxIT/WebRTC/issues/419
|
||||
s := `v=0
|
||||
o=- 1675628282 1675628283 IN IP4 192.168.1.123
|
||||
s=streamed by the RTSP server
|
||||
t=0 0
|
||||
m=video 0 RTP/AVP 96
|
||||
a=rtpmap:96 H264/90000
|
||||
a=control:track0
|
||||
m=audio 0 RTP/AVP 8
|
||||
a=rtpmap:0 pcma/8000/1
|
||||
a=control:track1
|
||||
a=framerate:25
|
||||
a=range:npt=now-
|
||||
a=fmtp:96 packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z0IAH5WoFAFuQA==,aM48gA==
|
||||
`
|
||||
medias, err := UnmarshalSDP([]byte(s))
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, medias)
|
||||
assert.NotEqual(t, "", medias[0].Codecs[0].FmtpLine)
|
||||
}
|
||||
|
||||
func TestBugSDP3(t *testing.T) {
|
||||
s := `v=0
|
||||
o=- 1680614126554766 1 IN IP4 192.168.0.3
|
||||
s=Session streamed by "preview"
|
||||
t=0 0
|
||||
a=tool:BC Streaming Media v202210012022.10.01
|
||||
a=type:broadcast
|
||||
a=control:*
|
||||
a=range:npt=now-
|
||||
a=x-qt-text-nam:Session streamed by "preview"
|
||||
m=video 0 RTP/AVP 96
|
||||
c=IN IP4 0.0.0.0
|
||||
b=AS:8192
|
||||
a=rtpmap:96 H264/90000
|
||||
a=range:npt=now-
|
||||
a=fmtp:96 packetization-mode=1;profile-level-id=640033;sprop-parameter-sets=Z2QAM6wVFKAoAPGQ,aO48sA==
|
||||
a=recvonly
|
||||
a=control:track1
|
||||
m=audio 0 RTP/AVP 97
|
||||
c=IN IP4 0.0.0.0
|
||||
b=AS:8192
|
||||
a=rtpmap:97 MPEG4-GENERIC/16000
|
||||
a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408;
|
||||
a=recvonly
|
||||
a=control:track2
|
||||
m=audio 0 RTP/AVP 8
|
||||
a=control:track3
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=sendonly`
|
||||
medias, err := UnmarshalSDP([]byte(s))
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, medias, 3)
|
||||
}
|
||||
|
||||
func TestBugSDP4(t *testing.T) {
|
||||
s := `v=0
|
||||
o=- 14665860 31787219 1 IN IP4 10.0.0.94
|
||||
s=Session streamed by "MERCURY RTSP Server"
|
||||
t=0 0
|
||||
m=video 0 RTP/AVP 96
|
||||
c=IN IP4 0.0.0.0
|
||||
b=AS:4096
|
||||
a=range:npt=0-
|
||||
a=control:track1
|
||||
a=rtpmap:96 H264/90000
|
||||
a=fmtp:96 packetization-mode=1; profile-level-id=640016; sprop-parameter-sets=Z2QAFqzGoCgPaEAAAAMAQAAAB6E=,aOqPLA==
|
||||
m=audio 0 RTP/AVP 8
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=control:track2
|
||||
m=application/MERCURY 0 RTP/AVP smart/1/90000
|
||||
a=rtpmap:95 MERCURY/90000
|
||||
a=control:track3
|
||||
`
|
||||
medias, err := UnmarshalSDP([]byte(s))
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, medias, 3)
|
||||
}
|
||||
|
||||
func TestBugSDP5(t *testing.T) {
|
||||
s := `v=0
|
||||
o=CV-RTSPHandler 1123412 0 IN IP4 192.168.1.22
|
||||
s=Camera
|
||||
c=IN IP4 192.168.1.22
|
||||
t=0 0
|
||||
a=charset:Shift_JIS
|
||||
a=range:npt=0-
|
||||
a=control:*
|
||||
a=etag:1234567890
|
||||
m=video 0 RTP/AVP 99
|
||||
a=rtpmap:99 H264/90000
|
||||
a=fmtp:99 profile-level-id=42A01E;packetization-mode=1;sprop-parameter-sets=Z0KgKedAPAET8uAIEAABd2AAK/IGAAADAC+vCAAAHc1lP//jAAADABfXhAAADuayn//wIA==,aN48gA==
|
||||
a=control:trackID=1
|
||||
a=sendonly
|
||||
m=audio 0 RTP/AVP 127
|
||||
a=rtpmap:127 mpeg4-generic/8000/1
|
||||
a=fmtp:127 streamtype=5; profile-level-id=15; mode=AAC-hbr; sizeLength=13; indexLength=3; indexDeltalength=3; config=1588; CTSDeltaLength=0; DTSDeltaLength=0;
|
||||
a=control:trackID=2
|
||||
`
|
||||
medias, err := UnmarshalSDP([]byte(s))
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, medias, 2)
|
||||
assert.Equal(t, "recvonly", medias[0].Direction)
|
||||
assert.Equal(t, "recvonly", medias[1].Direction)
|
||||
}
|
||||
|
||||
func TestBugSDP6(t *testing.T) {
|
||||
// https://github.com/AlexxIT/go2rtc/issues/1278
|
||||
s := `v=0
|
||||
o=- 3730506281693 1 IN IP4 172.20.0.215
|
||||
s=IP camera Live streaming
|
||||
i=stream1
|
||||
t=0 0
|
||||
a=tool:LIVE555 Streaming Media v2014.02.04
|
||||
a=type:broadcast
|
||||
a=control:*
|
||||
a=range:npt=0-
|
||||
a=x-qt-text-nam:IP camera Live streaming
|
||||
a=x-qt-text-inf:stream1
|
||||
m=video 0 RTP/AVP 26
|
||||
c=IN IP4 172.20.0.215
|
||||
b=AS:1500
|
||||
a=x-bufferdelay:0.55000
|
||||
a=x-dimensions:1280,960
|
||||
a=control:track1
|
||||
m=audio 0 RTP/AVP 0
|
||||
c=IN IP4 172.20.0.215
|
||||
b=AS:64
|
||||
a=x-bufferdelay:0.55000
|
||||
a=control:track2
|
||||
m=application 0 RTP/AVP 107
|
||||
c=IN IP4 172.20.0.215
|
||||
b=AS:1
|
||||
a=x-bufferdelay:0.55000
|
||||
a=rtpmap:107 vnd.onvif.metadata/90000/500
|
||||
a=control:track4
|
||||
m=vana 0 RTP/AVP 108
|
||||
c=IN IP4 172.20.0.215
|
||||
b=AS:1
|
||||
a=x-bufferdelay:0.55000
|
||||
a=rtpmap:108 video.analysis/90000/500
|
||||
a=control:track5
|
||||
`
|
||||
medias, err := UnmarshalSDP([]byte(s))
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, medias, 4)
|
||||
}
|
||||
|
||||
func TestBugSDP7(t *testing.T) {
|
||||
// https://github.com/AlexxIT/go2rtc/issues/1426
|
||||
s := `v=0
|
||||
o=- 1001 1 IN
|
||||
s=VCP IPC Realtime stream
|
||||
m=video 0 RTP/AVP 105
|
||||
c=IN
|
||||
a=control:rtsp://1.0.1.113/media/video2/video
|
||||
a=rtpmap:105 H264/90000
|
||||
a=fmtp:105 profile-level-id=640016; packetization-mode=1; sprop-parameter-sets=Z2QAFqw7UFAX/LCAAAH0AABOIEI=,aOqPLA==
|
||||
a=recvonly
|
||||
m=audio 0 RTP/AVP 0
|
||||
c=IN
|
||||
a=fmtp:0 RTCP=0
|
||||
a=control:rtsp://1.0.1.113/media/video2/audio1
|
||||
a=recvonly
|
||||
m=audio 0 RTP/AVP 0
|
||||
c=IN
|
||||
a=control:rtsp://1.0.1.113/media/video2/backchannel
|
||||
a=rtpmap:0 PCMA/8000
|
||||
a=rtpmap:0 PCMU/8000
|
||||
a=sendonly
|
||||
m=application 0 RTP/AVP 107
|
||||
c=IN
|
||||
a=control:rtsp://1.0.1.113/media/video2/metadata
|
||||
a=rtpmap:107 vnd.onvif.metadata/90000
|
||||
a=fmtp:107 DecoderTag=h3c-v3 RTCP=0
|
||||
a=recvonly
|
||||
`
|
||||
medias, err := UnmarshalSDP([]byte(s))
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, medias, 4)
|
||||
}
|
||||
|
||||
func TestHikvisionPCM(t *testing.T) {
|
||||
s := `v=0
|
||||
o=- 1721969533379665 1721969533379665 IN IP4 192.168.1.12
|
||||
s=Media Presentation
|
||||
e=NONE
|
||||
b=AS:5100
|
||||
t=0 0
|
||||
a=control:rtsp://192.168.1.12:554/Streaming/channels/101/
|
||||
m=video 0 RTP/AVP 96
|
||||
c=IN IP4 0.0.0.0
|
||||
b=AS:5000
|
||||
a=recvonly
|
||||
a=x-dimensions:3200,1800
|
||||
a=control:rtsp://192.168.1.12:554/Streaming/channels/101/trackID=1
|
||||
a=rtpmap:96 H264/90000
|
||||
a=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=Z2QAM6wVFKAyAOP5f/AAEAAWyAAAH0AAB1MAIA==,aO48sA==
|
||||
m=audio 0 RTP/AVP 11
|
||||
c=IN IP4 0.0.0.0
|
||||
b=AS:50
|
||||
a=recvonly
|
||||
a=control:rtsp://192.168.1.12:554/Streaming/channels/101/trackID=2
|
||||
a=rtpmap:11 PCM/48000
|
||||
a=Media_header:MEDIAINFO=494D4B4801030000040000010170011080BB0000007D000000000000000000000000000000000000;
|
||||
a=appversion:1.0
|
||||
`
|
||||
medias, err := UnmarshalSDP([]byte(s))
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, medias, 2)
|
||||
assert.Equal(t, core.CodecPCML, medias[1].Codecs[0].Name)
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
var FailedAuth = errors.New("failed authentication")
|
||||
|
||||
func NewServer(conn net.Conn) *Conn {
|
||||
return &Conn{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "rtsp",
|
||||
Protocol: "rtsp+tcp",
|
||||
RemoteAddr: conn.RemoteAddr().String(),
|
||||
},
|
||||
conn: conn,
|
||||
reader: bufio.NewReader(conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) Auth(username, password string) {
|
||||
info := url.UserPassword(username, password)
|
||||
c.auth = tcp.NewAuth(info)
|
||||
}
|
||||
|
||||
func (c *Conn) Accept() error {
|
||||
for {
|
||||
req, err := c.ReadRequest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.URL == nil {
|
||||
c.URL = req.URL
|
||||
c.UserAgent = req.Header.Get("User-Agent")
|
||||
}
|
||||
|
||||
c.Fire(req)
|
||||
|
||||
if valid, empty := c.auth.Validate(req); !valid {
|
||||
res := &tcp.Response{
|
||||
Status: "401 Unauthorized",
|
||||
Header: map[string][]string{"Www-Authenticate": {`Basic realm="go2rtc"`}},
|
||||
Request: req,
|
||||
}
|
||||
if err = c.WriteResponse(res); err != nil {
|
||||
return err
|
||||
}
|
||||
if empty {
|
||||
// eliminate false positive: ffmpeg sends first request without
|
||||
// authorization header even if the user provides credentials
|
||||
continue
|
||||
}
|
||||
return FailedAuth
|
||||
}
|
||||
|
||||
// Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN
|
||||
// Sender: OPTIONS > ANNOUNCE > SETUP... > RECORD > TEARDOWN
|
||||
switch req.Method {
|
||||
case MethodOptions:
|
||||
res := &tcp.Response{
|
||||
Header: map[string][]string{
|
||||
"Public": {"OPTIONS, SETUP, TEARDOWN, DESCRIBE, PLAY, PAUSE, ANNOUNCE, RECORD"},
|
||||
},
|
||||
Request: req,
|
||||
}
|
||||
if err = c.WriteResponse(res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case MethodAnnounce:
|
||||
if req.Header.Get("Content-Type") != "application/sdp" {
|
||||
return errors.New("wrong content type")
|
||||
}
|
||||
|
||||
c.SDP = string(req.Body) // for info
|
||||
|
||||
c.Medias, err = UnmarshalSDP(req.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: fix someday...
|
||||
for i, media := range c.Medias {
|
||||
track := core.NewReceiver(media, media.Codecs[0])
|
||||
track.ID = byte(i * 2)
|
||||
c.Receivers = append(c.Receivers, track)
|
||||
}
|
||||
|
||||
c.mode = core.ModePassiveProducer
|
||||
c.Fire(MethodAnnounce)
|
||||
|
||||
res := &tcp.Response{Request: req}
|
||||
if err = c.WriteResponse(res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case MethodDescribe:
|
||||
c.mode = core.ModePassiveConsumer
|
||||
c.Fire(MethodDescribe)
|
||||
|
||||
if c.Senders == nil {
|
||||
res := &tcp.Response{
|
||||
Status: "404 Not Found",
|
||||
Request: req,
|
||||
}
|
||||
return c.WriteResponse(res)
|
||||
}
|
||||
|
||||
res := &tcp.Response{
|
||||
Header: map[string][]string{
|
||||
"Content-Type": {"application/sdp"},
|
||||
},
|
||||
Request: req,
|
||||
}
|
||||
|
||||
// convert tracks to real output medias medias
|
||||
var medias []*core.Media
|
||||
for i, track := range c.Senders {
|
||||
media := &core.Media{
|
||||
Kind: core.GetKind(track.Codec.Name),
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{track.Codec},
|
||||
ID: "trackID=" + strconv.Itoa(i),
|
||||
}
|
||||
medias = append(medias, media)
|
||||
}
|
||||
|
||||
for i, track := range c.Receivers {
|
||||
media := &core.Media{
|
||||
Kind: core.GetKind(track.Codec.Name),
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{track.Codec},
|
||||
ID: "trackID=" + strconv.Itoa(i+len(c.Senders)),
|
||||
}
|
||||
medias = append(medias, media)
|
||||
}
|
||||
|
||||
res.Body, err = core.MarshalSDP(c.SessionName, medias)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.SDP = string(res.Body) // for info
|
||||
|
||||
if err = c.WriteResponse(res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case MethodSetup:
|
||||
res := &tcp.Response{
|
||||
Header: map[string][]string{},
|
||||
Request: req,
|
||||
}
|
||||
|
||||
// Test if client requests TCP transport, otherwise return 461 Transport not supported
|
||||
// This allows smart clients who initially requested UDP to fall back on TCP transport
|
||||
if tr := req.Header.Get("Transport"); strings.HasPrefix(tr, "RTP/AVP/TCP") {
|
||||
c.session = core.RandString(8, 10)
|
||||
c.state = StateSetup
|
||||
|
||||
if c.mode == core.ModePassiveConsumer {
|
||||
if i := reqTrackID(req); i >= 0 && i < len(c.Senders)+len(c.Receivers) {
|
||||
if i < len(c.Senders) {
|
||||
c.Senders[i].Media.ID = MethodSetup
|
||||
} else {
|
||||
c.Receivers[i-len(c.Senders)].Media.ID = MethodSetup
|
||||
}
|
||||
tr = fmt.Sprintf("RTP/AVP/TCP;unicast;interleaved=%d-%d", i*2, i*2+1)
|
||||
res.Header.Set("Transport", tr)
|
||||
} else {
|
||||
res.Status = "400 Bad Request"
|
||||
}
|
||||
} else {
|
||||
res.Header.Set("Transport", tr)
|
||||
}
|
||||
} else {
|
||||
res.Status = "461 Unsupported transport"
|
||||
}
|
||||
|
||||
if err = c.WriteResponse(res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case MethodRecord, MethodPlay:
|
||||
if c.mode == core.ModePassiveConsumer {
|
||||
// stop unconfigured senders
|
||||
for _, track := range c.Senders {
|
||||
if track.Media.ID != MethodSetup {
|
||||
track.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res := &tcp.Response{Request: req}
|
||||
err = c.WriteResponse(res)
|
||||
c.playOK = true
|
||||
return err
|
||||
|
||||
case MethodTeardown:
|
||||
res := &tcp.Response{Request: req}
|
||||
_ = c.WriteResponse(res)
|
||||
c.state = StateNone
|
||||
return c.conn.Close()
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported method: %s", req.Method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reqTrackID(req *tcp.Request) int {
|
||||
var s string
|
||||
if req.URL.RawQuery != "" {
|
||||
s = req.URL.RawQuery
|
||||
} else {
|
||||
s = req.URL.Path
|
||||
}
|
||||
if i := strings.LastIndexByte(s, '='); i > 0 {
|
||||
if i, err := strconv.Atoi(s[i+1:]); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
Reference in New Issue
Block a user