Support reading with RTMP (#218)
This commit is contained in:
@@ -9,14 +9,14 @@
|
||||
[](https://github.com/aler9/rtsp-simple-server/releases)
|
||||
[](https://hub.docker.com/r/aler9/rtsp-simple-server)
|
||||
|
||||
_rtsp-simple-server_ is a simple, ready-to-use and zero-dependency RTSP/RTMP server and proxy, a software that allows users to publish, read and proxy live video and audio streams. RTSP is a specification that describes how to perform these operations with the help of a server, that is contacted by both publishers and readers and relays the publisher's streams to the readers.
|
||||
_rtsp-simple-server_ is a simple, ready-to-use and zero-dependency RTSP / RTMP server and proxy, a software that allows users to publish, read and proxy live video and audio streams. RTSP is a specification that describes how to perform these operations with the help of a server, that is contacted by both publishers and readers and relays the publisher's streams to the readers.
|
||||
|
||||
Features:
|
||||
|
||||
* Publish live streams with RTSP (UDP or TCP mode) or RTMP
|
||||
* Read live streams with RTSP
|
||||
* Read live streams with RTSP or RTMP
|
||||
* Pull and serve streams from other RTSP / RTMP servers or cameras, always or on-demand (RTSP proxy)
|
||||
* Each stream can have multiple video and audio tracks, encoded with any codec (including H264, H265, VP8, VP9, MPEG2, MP3, AAC, Opus, PCM)
|
||||
* Each stream can have multiple video and audio tracks, encoded with any codec (including H264, H265, VP8, VP9, MPEG2, MP3, AAC, Opus, PCM, JPEG)
|
||||
* Serve multiple streams at once in separate paths
|
||||
* Encrypt streams with TLS (RTSPS)
|
||||
* Authenticate readers and publishers
|
||||
@@ -37,7 +37,7 @@ Features:
|
||||
* [Authentication](#authentication)
|
||||
* [Encrypt the configuration](#encrypt-the-configuration)
|
||||
* [RTSP proxy mode](#rtsp-proxy-mode)
|
||||
* [RTMP server](#rtmp-server)
|
||||
* [RTMP protocol](#rtmp-protocol)
|
||||
* [Publish a webcam](#publish-a-webcam)
|
||||
* [Publish a Raspberry Pi Camera](#publish-a-raspberry-pi-camera)
|
||||
* [Convert streams to HLS](#convert-streams-to-hls)
|
||||
@@ -280,15 +280,15 @@ paths:
|
||||
sourceOnDemand: yes
|
||||
```
|
||||
|
||||
### RTMP server
|
||||
### RTMP protocol
|
||||
|
||||
RTMP is a protocol that is used to read and publish streams, but is less versatile and less efficient than RTSP (doesn't support UDP, encryption, most RTSP codecs, feedback mechanism). If there is need of receiving streams from a software that supports only RTMP (for instance, OBS Studio and DJI drones), it's possible to turn on a RTMP listener:
|
||||
RTMP is a protocol that is used to read and publish streams, but is less versatile and less efficient than RTSP (doesn't support UDP, encryption, most RTSP codecs, feedback mechanism). If there is need of publishing or reading streams from a software that supports only RTMP (for instance, OBS Studio and DJI drones), it's possible to turn on a RTMP listener:
|
||||
|
||||
```yml
|
||||
rtmpEnable: yes
|
||||
```
|
||||
|
||||
Streams can then be published with the RTMP protocol, for instance with _FFmpeg_:
|
||||
Streams can then be published or read with the RTMP protocol, for instance with _FFmpeg_:
|
||||
|
||||
```
|
||||
ffmpeg -re -stream_loop -1 -i file.ts -c copy -f flv rtmp://localhost/mystream
|
||||
|
||||
@@ -5,14 +5,17 @@ go 1.15
|
||||
require (
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
|
||||
github.com/aler9/gortsplib v0.0.0-20210306084624-260af6e04194
|
||||
github.com/aler9/gortsplib v0.0.0-20210310150132-830e3079e366
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/notedit/rtmp v0.0.2
|
||||
github.com/notedit/rtmp v0.0.0
|
||||
github.com/pion/rtp v1.6.2 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.2
|
||||
github.com/stretchr/testify v1.6.1
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
)
|
||||
|
||||
replace github.com/notedit/rtmp => github.com/aler9/rtmp v0.0.0-20210309202041-2d7177b7300d
|
||||
|
||||
@@ -2,8 +2,10 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/aler9/gortsplib v0.0.0-20210306084624-260af6e04194 h1:LMhKYN1HfEvVPV5YL6ZLR6eQgXbz2H4bdb1eQK7lrQA=
|
||||
github.com/aler9/gortsplib v0.0.0-20210306084624-260af6e04194/go.mod h1:8P09VjpiPJFyfkVosyF5/TY82jNwkMN165NS/7sc32I=
|
||||
github.com/aler9/gortsplib v0.0.0-20210310150132-830e3079e366 h1:68edOFG2H1ntH5FhGZQJ72aq61wjiC9xRuvNI8/6haU=
|
||||
github.com/aler9/gortsplib v0.0.0-20210310150132-830e3079e366/go.mod h1:aj4kDzanb3JZ46sFywWShcsnqqXTLE/3PNjwDhQZGM0=
|
||||
github.com/aler9/rtmp v0.0.0-20210309202041-2d7177b7300d h1:LAX8pNvYpGgFpKdbPpEZWjNkHbmyvjMrT3vO7s7aaKU=
|
||||
github.com/aler9/rtmp v0.0.0-20210309202041-2d7177b7300d/go.mod h1:vzuE21rowz+lT1NGsWbreIvYulgBpCGnQyeTyFblUHc=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -12,14 +14,14 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/notedit/rtmp v0.0.2 h1:5+to4yezKATiJgnrcETu9LbV5G/QsWkOV9Ts2M/p33w=
|
||||
github.com/notedit/rtmp v0.0.2/go.mod h1:vzuE21rowz+lT1NGsWbreIvYulgBpCGnQyeTyFblUHc=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.4 h1:NT3H5LkUGgaEapvp0HGik+a+CpflRF7KTD7H+o7OWIM=
|
||||
github.com/pion/rtcp v1.2.4/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
|
||||
github.com/pion/rtp v1.6.1 h1:2Y2elcVBrahYnHKN2X7rMHX/r1R4TEBMP1LaVu/wNhk=
|
||||
github.com/pion/rtp v1.6.1/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/rtp v1.6.2 h1:iGBerLX6JiDjB9NXuaPzHyxHFG9JsIEdgwTC0lp5n/U=
|
||||
github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/sdp/v3 v3.0.2 h1:UNnSPVaMM+Pdu/mR9UvAyyo6zkdYbKeuOooCwZvTl/g=
|
||||
github.com/pion/sdp/v3 v3.0.2/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
||||
+27
-18
@@ -1,6 +1,8 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aler9/gortsplib"
|
||||
"github.com/aler9/gortsplib/pkg/base"
|
||||
"github.com/aler9/gortsplib/pkg/headers"
|
||||
@@ -8,17 +10,14 @@ import (
|
||||
"github.com/aler9/rtsp-simple-server/internal/conf"
|
||||
)
|
||||
|
||||
// Client can be
|
||||
// *clientrtsp.Client
|
||||
// *clientrtmp.Client
|
||||
type Client interface {
|
||||
IsClient()
|
||||
IsSource()
|
||||
Close()
|
||||
Authenticate([]headers.AuthMethod,
|
||||
string, []interface{},
|
||||
string, string, interface{}) error
|
||||
OnReaderFrame(int, gortsplib.StreamType, []byte)
|
||||
// ErrNoOnePublishing is a "no one is publishing" error.
|
||||
type ErrNoOnePublishing struct {
|
||||
PathName string
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e ErrNoOnePublishing) Error() string {
|
||||
return fmt.Sprintf("no one is publishing to path '%s'", e.PathName)
|
||||
}
|
||||
|
||||
// ErrAuthNotCritical is a non-critical authentication error.
|
||||
@@ -52,22 +51,22 @@ type DescribeRes struct {
|
||||
type DescribeReq struct {
|
||||
Client Client
|
||||
PathName string
|
||||
Req *base.Request
|
||||
Data *base.Request
|
||||
Res chan DescribeRes
|
||||
}
|
||||
|
||||
// SetupPlayRes is a setup/play response.
|
||||
type SetupPlayRes struct {
|
||||
Path Path
|
||||
Err error
|
||||
Path Path
|
||||
Tracks gortsplib.Tracks
|
||||
Err error
|
||||
}
|
||||
|
||||
// SetupPlayReq is a setup/play request.
|
||||
type SetupPlayReq struct {
|
||||
Client Client
|
||||
PathName string
|
||||
TrackID int
|
||||
Req *base.Request
|
||||
Data interface{}
|
||||
Res chan SetupPlayRes
|
||||
}
|
||||
|
||||
@@ -82,7 +81,7 @@ type AnnounceReq struct {
|
||||
Client Client
|
||||
PathName string
|
||||
Tracks gortsplib.Tracks
|
||||
Req interface{}
|
||||
Data interface{}
|
||||
Res chan AnnounceRes
|
||||
}
|
||||
|
||||
@@ -113,7 +112,6 @@ type PauseReq struct {
|
||||
// Path is implemented by path.Path.
|
||||
type Path interface {
|
||||
Name() string
|
||||
SourceTrackCount() int
|
||||
Conf() *conf.PathConf
|
||||
OnClientRemove(RemoveReq)
|
||||
OnClientPlay(PlayReq)
|
||||
@@ -121,3 +119,14 @@ type Path interface {
|
||||
OnClientPause(PauseReq)
|
||||
OnFrame(int, gortsplib.StreamType, []byte)
|
||||
}
|
||||
|
||||
// Client is implemented by all client*.
|
||||
type Client interface {
|
||||
IsClient()
|
||||
IsSource()
|
||||
Close()
|
||||
Authenticate([]headers.AuthMethod,
|
||||
string, []interface{},
|
||||
string, string, interface{}) error
|
||||
OnIncomingFrame(int, gortsplib.StreamType, []byte)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ type Parent interface {
|
||||
type ClientManager struct {
|
||||
rtspPort int
|
||||
readTimeout time.Duration
|
||||
writeTimeout time.Duration
|
||||
readBufferCount int
|
||||
runOnConnect string
|
||||
runOnConnectRestart bool
|
||||
protocols map[base.StreamProtocol]struct{}
|
||||
@@ -59,6 +61,8 @@ type ClientManager struct {
|
||||
func New(
|
||||
rtspPort int,
|
||||
readTimeout time.Duration,
|
||||
writeTimeout time.Duration,
|
||||
readBufferCount int,
|
||||
runOnConnect string,
|
||||
runOnConnectRestart bool,
|
||||
protocols map[base.StreamProtocol]struct{},
|
||||
@@ -72,6 +76,8 @@ func New(
|
||||
cm := &ClientManager{
|
||||
rtspPort: rtspPort,
|
||||
readTimeout: readTimeout,
|
||||
writeTimeout: writeTimeout,
|
||||
readBufferCount: readBufferCount,
|
||||
runOnConnect: runOnConnect,
|
||||
runOnConnectRestart: runOnConnectRestart,
|
||||
protocols: protocols,
|
||||
@@ -120,11 +126,11 @@ func (cm *ClientManager) run() {
|
||||
return make(chan *gortsplib.ServerConn)
|
||||
}()
|
||||
|
||||
rtmpAccept := func() chan rtmputils.ConnPair {
|
||||
rtmpAccept := func() chan *rtmputils.Conn {
|
||||
if cm.serverRTMP != nil {
|
||||
return cm.serverRTMP.Accept()
|
||||
}
|
||||
return make(chan rtmputils.ConnPair)
|
||||
return make(chan *rtmputils.Conn)
|
||||
}()
|
||||
|
||||
outer:
|
||||
@@ -162,6 +168,8 @@ outer:
|
||||
c := clientrtmp.New(
|
||||
cm.rtspPort,
|
||||
cm.readTimeout,
|
||||
cm.writeTimeout,
|
||||
cm.readBufferCount,
|
||||
cm.runOnConnect,
|
||||
cm.runOnConnectRestart,
|
||||
&cm.wg,
|
||||
|
||||
+371
-116
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/aler9/gortsplib"
|
||||
"github.com/aler9/gortsplib/pkg/base"
|
||||
"github.com/aler9/gortsplib/pkg/headers"
|
||||
"github.com/aler9/gortsplib/pkg/ringbuffer"
|
||||
"github.com/aler9/gortsplib/pkg/rtpaac"
|
||||
"github.com/aler9/gortsplib/pkg/rtph264"
|
||||
"github.com/notedit/rtmp/av"
|
||||
@@ -47,10 +48,19 @@ func ipEqualOrInRange(ip net.IP, ips []interface{}) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func pathNameAndQuery(inURL *url.URL) (string, url.Values) {
|
||||
// remove trailing slashes inserted by OBS and some other clients
|
||||
tmp := strings.TrimSuffix(inURL.String(), "/")
|
||||
ur, _ := url.Parse(tmp)
|
||||
pathName := strings.TrimPrefix(ur.Path, "/")
|
||||
return pathName, ur.Query()
|
||||
}
|
||||
|
||||
// Parent is implemented by clientman.ClientMan.
|
||||
type Parent interface {
|
||||
Log(logger.Level, string, ...interface{})
|
||||
OnClientClose(client.Client)
|
||||
OnClientSetupPlay(client.SetupPlayReq)
|
||||
OnClientAnnounce(client.AnnounceReq)
|
||||
}
|
||||
|
||||
@@ -58,14 +68,21 @@ type Parent interface {
|
||||
type Client struct {
|
||||
rtspPort int
|
||||
readTimeout time.Duration
|
||||
writeTimeout time.Duration
|
||||
readBufferCount int
|
||||
runOnConnect string
|
||||
runOnConnectRestart bool
|
||||
stats *stats.Stats
|
||||
wg *sync.WaitGroup
|
||||
conn rtmputils.ConnPair
|
||||
conn *rtmputils.Conn
|
||||
parent Parent
|
||||
|
||||
path client.Path
|
||||
// read mode only
|
||||
h264Decoder *rtph264.Decoder
|
||||
videoTrack *gortsplib.Track
|
||||
aacDecoder *rtpaac.Decoder
|
||||
audioTrack *gortsplib.Track
|
||||
ringBuffer *ringbuffer.RingBuffer
|
||||
|
||||
// in
|
||||
terminate chan struct{}
|
||||
@@ -75,16 +92,20 @@ type Client struct {
|
||||
func New(
|
||||
rtspPort int,
|
||||
readTimeout time.Duration,
|
||||
writeTimeout time.Duration,
|
||||
readBufferCount int,
|
||||
runOnConnect string,
|
||||
runOnConnectRestart bool,
|
||||
wg *sync.WaitGroup,
|
||||
stats *stats.Stats,
|
||||
conn rtmputils.ConnPair,
|
||||
conn *rtmputils.Conn,
|
||||
parent Parent) *Client {
|
||||
|
||||
c := &Client{
|
||||
rtspPort: rtspPort,
|
||||
readTimeout: readTimeout,
|
||||
writeTimeout: writeTimeout,
|
||||
readBufferCount: readBufferCount,
|
||||
runOnConnect: runOnConnect,
|
||||
runOnConnectRestart: runOnConnectRestart,
|
||||
wg: wg,
|
||||
@@ -116,11 +137,11 @@ func (c *Client) IsClient() {}
|
||||
func (c *Client) IsSource() {}
|
||||
|
||||
func (c *Client) log(level logger.Level, format string, args ...interface{}) {
|
||||
c.parent.Log(level, "[client %s] "+format, append([]interface{}{c.conn.NConn.RemoteAddr().String()}, args...)...)
|
||||
c.parent.Log(level, "[client %s] "+format, append([]interface{}{c.conn.NetConn().RemoteAddr().String()}, args...)...)
|
||||
}
|
||||
|
||||
func (c *Client) ip() net.IP {
|
||||
return c.conn.NConn.RemoteAddr().(*net.TCPAddr).IP
|
||||
return c.conn.NetConn().RemoteAddr().(*net.TCPAddr).IP
|
||||
}
|
||||
|
||||
func (c *Client) run() {
|
||||
@@ -135,66 +156,318 @@ func (c *Client) run() {
|
||||
defer onConnectCmd.Close()
|
||||
}
|
||||
|
||||
if !c.conn.RConn.Publishing {
|
||||
c.conn.NConn.Close()
|
||||
c.log(logger.Info, "ERR: client is not publishing")
|
||||
if c.conn.IsPublishing() {
|
||||
c.runPublish()
|
||||
} else {
|
||||
c.runRead()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) runRead() {
|
||||
var path client.Path
|
||||
var tracks gortsplib.Tracks
|
||||
|
||||
err := func() error {
|
||||
pathName, query := pathNameAndQuery(c.conn.URL())
|
||||
|
||||
resc := make(chan client.SetupPlayRes)
|
||||
c.parent.OnClientSetupPlay(client.SetupPlayReq{c, pathName, query, resc}) //nolint:govet
|
||||
res := <-resc
|
||||
|
||||
if res.Err != nil {
|
||||
switch res.Err.(type) {
|
||||
case client.ErrAuthCritical:
|
||||
// wait some seconds to stop brute force attacks
|
||||
select {
|
||||
case <-time.After(pauseAfterAuthError):
|
||||
case <-c.terminate:
|
||||
}
|
||||
}
|
||||
return res.Err
|
||||
}
|
||||
|
||||
path = res.Path
|
||||
tracks = res.Tracks
|
||||
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
c.log(logger.Info, "ERR: %s", err)
|
||||
c.conn.NetConn().Close()
|
||||
|
||||
c.parent.OnClientClose(c)
|
||||
<-c.terminate
|
||||
return
|
||||
}
|
||||
|
||||
var videoTrack *gortsplib.Track
|
||||
var h264SPS []byte
|
||||
var h264PPS []byte
|
||||
var audioTrack *gortsplib.Track
|
||||
var aacConfig []byte
|
||||
|
||||
err = func() error {
|
||||
for i, t := range tracks {
|
||||
if t.IsH264() {
|
||||
if videoTrack != nil {
|
||||
return fmt.Errorf("can't read track %d with RTMP: too many tracks", i+1)
|
||||
}
|
||||
videoTrack = t
|
||||
|
||||
var err error
|
||||
h264SPS, h264PPS, err = t.ExtractDataH264()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
} else if t.IsAAC() {
|
||||
if audioTrack != nil {
|
||||
return fmt.Errorf("can't read track %d with RTMP: too many tracks", i+1)
|
||||
}
|
||||
audioTrack = t
|
||||
|
||||
var err error
|
||||
aacConfig, err = t.ExtractDataAAC()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if videoTrack == nil && audioTrack == nil {
|
||||
return fmt.Errorf("unable to find a video or audio track")
|
||||
}
|
||||
|
||||
c.conn.NetConn().SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||
rtmputils.WriteMetadata(c.conn, videoTrack, audioTrack)
|
||||
|
||||
if videoTrack != nil {
|
||||
codec := h264.Codec{
|
||||
SPS: map[int][]byte{
|
||||
0: h264SPS,
|
||||
},
|
||||
PPS: map[int][]byte{
|
||||
0: h264PPS,
|
||||
},
|
||||
}
|
||||
b := make([]byte, 128)
|
||||
var n int
|
||||
codec.ToConfig(b, &n)
|
||||
b = b[:n]
|
||||
|
||||
c.conn.NetConn().SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||
c.conn.WritePacket(av.Packet{
|
||||
Type: av.H264DecoderConfig,
|
||||
Data: b,
|
||||
})
|
||||
|
||||
c.h264Decoder = rtph264.NewDecoder()
|
||||
c.videoTrack = videoTrack
|
||||
}
|
||||
|
||||
if audioTrack != nil {
|
||||
c.conn.NetConn().SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||
c.conn.WritePacket(av.Packet{
|
||||
Type: av.AACDecoderConfig,
|
||||
Data: aacConfig,
|
||||
})
|
||||
|
||||
c.aacDecoder = rtpaac.NewDecoder(48000)
|
||||
c.audioTrack = audioTrack
|
||||
}
|
||||
|
||||
c.ringBuffer = ringbuffer.New(uint64(c.readBufferCount))
|
||||
|
||||
resc := make(chan struct{})
|
||||
path.OnClientPlay(client.PlayReq{c, resc}) //nolint:govet
|
||||
<-resc
|
||||
|
||||
c.log(logger.Info, "is reading from path '%s'", path.Name())
|
||||
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
c.conn.NetConn().Close()
|
||||
c.log(logger.Info, "ERR: %v", err)
|
||||
|
||||
res := make(chan struct{})
|
||||
path.OnClientRemove(client.RemoveReq{c, res}) //nolint:govet
|
||||
<-res
|
||||
path = nil
|
||||
|
||||
c.parent.OnClientClose(c)
|
||||
<-c.terminate
|
||||
}
|
||||
|
||||
writerDone := make(chan error)
|
||||
go func() {
|
||||
writerDone <- func() error {
|
||||
videoInitialized := false
|
||||
var videoStartDTS time.Time
|
||||
var videoBuf [][]byte
|
||||
var videoPTS time.Duration
|
||||
|
||||
for {
|
||||
data, ok := c.ringBuffer.Pull()
|
||||
if !ok {
|
||||
return fmt.Errorf("terminated")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
switch tdata := data.(type) {
|
||||
case *rtph264.NALUAndTimestamp:
|
||||
if !videoInitialized {
|
||||
videoInitialized = true
|
||||
videoStartDTS = now
|
||||
videoPTS = tdata.Timestamp
|
||||
}
|
||||
|
||||
// aggregate NALUs by PTS
|
||||
if tdata.Timestamp != videoPTS {
|
||||
pkt := av.Packet{
|
||||
Type: av.H264,
|
||||
Data: h264.FillNALUsAVCC(videoBuf),
|
||||
Time: now.Sub(videoStartDTS),
|
||||
}
|
||||
|
||||
c.conn.NetConn().SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||
err := c.conn.WritePacket(pkt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
videoBuf = nil
|
||||
}
|
||||
|
||||
videoPTS = tdata.Timestamp
|
||||
videoBuf = append(videoBuf, tdata.NALU)
|
||||
|
||||
case *rtpaac.AUAndTimestamp:
|
||||
pkt := av.Packet{
|
||||
Type: av.AAC,
|
||||
Data: tdata.AU,
|
||||
Time: tdata.Timestamp,
|
||||
}
|
||||
|
||||
c.conn.NetConn().SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||
err := c.conn.WritePacket(pkt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-writerDone:
|
||||
c.conn.NetConn().Close()
|
||||
|
||||
if err != io.EOF {
|
||||
c.log(logger.Info, "ERR: %s", err)
|
||||
}
|
||||
|
||||
res := make(chan struct{})
|
||||
path.OnClientRemove(client.RemoveReq{c, res}) //nolint:govet
|
||||
<-res
|
||||
path = nil
|
||||
|
||||
c.parent.OnClientClose(c)
|
||||
<-c.terminate
|
||||
|
||||
case <-c.terminate:
|
||||
res := make(chan struct{})
|
||||
path.OnClientRemove(client.RemoveReq{c, res}) //nolint:govet
|
||||
<-res
|
||||
|
||||
c.ringBuffer.Close()
|
||||
c.conn.NetConn().Close()
|
||||
<-writerDone
|
||||
path = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) runPublish() {
|
||||
var videoTrack *gortsplib.Track
|
||||
var audioTrack *gortsplib.Track
|
||||
var err error
|
||||
var tracks gortsplib.Tracks
|
||||
var h264Encoder *rtph264.Encoder
|
||||
var aacEncoder *rtpaac.Encoder
|
||||
var path client.Path
|
||||
|
||||
metadataDone := make(chan struct{})
|
||||
setupDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(metadataDone)
|
||||
defer close(setupDone)
|
||||
err = func() error {
|
||||
videoTrack, audioTrack, err = rtmputils.Metadata(c.conn, c.readTimeout)
|
||||
c.conn.NetConn().SetReadDeadline(time.Now().Add(c.readTimeout))
|
||||
videoTrack, audioTrack, err = rtmputils.ReadMetadata(c.conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if videoTrack != nil {
|
||||
var err error
|
||||
h264Encoder, err = rtph264.NewEncoder(96)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h264Encoder = rtph264.NewEncoder(96, nil, nil, nil)
|
||||
tracks = append(tracks, videoTrack)
|
||||
}
|
||||
|
||||
if audioTrack != nil {
|
||||
clockRate, _ := audioTrack.ClockRate()
|
||||
var err error
|
||||
aacEncoder, err = rtpaac.NewEncoder(96, clockRate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
aacEncoder = rtpaac.NewEncoder(96, clockRate, nil, nil, nil)
|
||||
tracks = append(tracks, audioTrack)
|
||||
}
|
||||
|
||||
for i, t := range tracks {
|
||||
t.ID = i
|
||||
}
|
||||
|
||||
pathName, query := pathNameAndQuery(c.conn.URL())
|
||||
|
||||
resc := make(chan client.AnnounceRes)
|
||||
c.parent.OnClientAnnounce(client.AnnounceReq{c, pathName, tracks, query, resc}) //nolint:govet
|
||||
res := <-resc
|
||||
|
||||
if res.Err != nil {
|
||||
switch res.Err.(type) {
|
||||
case client.ErrAuthCritical:
|
||||
// wait some seconds to stop brute force attacks
|
||||
select {
|
||||
case <-time.After(pauseAfterAuthError):
|
||||
case <-c.terminate:
|
||||
}
|
||||
}
|
||||
return res.Err
|
||||
}
|
||||
|
||||
resc2 := make(chan struct{})
|
||||
res.Path.OnClientRecord(client.RecordReq{c, resc2}) //nolint:govet
|
||||
<-resc2
|
||||
path = res.Path
|
||||
|
||||
c.log(logger.Info, "is publishing to path '%s', %d %s",
|
||||
path.Name(),
|
||||
len(tracks),
|
||||
func() string {
|
||||
if len(tracks) == 1 {
|
||||
return "track"
|
||||
}
|
||||
return "tracks"
|
||||
}())
|
||||
|
||||
return nil
|
||||
}()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-metadataDone:
|
||||
case <-setupDone:
|
||||
case <-c.terminate:
|
||||
c.conn.NConn.Close()
|
||||
<-metadataDone
|
||||
c.conn.NetConn().Close()
|
||||
<-setupDone
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.conn.NConn.Close()
|
||||
c.conn.NetConn().Close()
|
||||
c.log(logger.Info, "ERR: %s", err)
|
||||
|
||||
c.parent.OnClientClose(c)
|
||||
@@ -202,65 +475,11 @@ func (c *Client) run() {
|
||||
return
|
||||
}
|
||||
|
||||
err = func() error {
|
||||
// remove trailing slash, that is inserted by OBS
|
||||
tmp := strings.TrimSuffix(c.conn.RConn.URL.String(), "/")
|
||||
ur, _ := url.Parse(tmp)
|
||||
pathName := strings.TrimPrefix(ur.Path, "/")
|
||||
|
||||
resc := make(chan client.AnnounceRes)
|
||||
c.parent.OnClientAnnounce(client.AnnounceReq{c, pathName, tracks, ur.Query(), resc}) //nolint:govet
|
||||
res := <-resc
|
||||
|
||||
if res.Err != nil {
|
||||
switch res.Err.(type) {
|
||||
case client.ErrAuthNotCritical:
|
||||
return res.Err
|
||||
|
||||
case client.ErrAuthCritical:
|
||||
// wait some seconds to stop brute force attacks
|
||||
select {
|
||||
case <-time.After(pauseAfterAuthError):
|
||||
case <-c.terminate:
|
||||
}
|
||||
return res.Err
|
||||
|
||||
default:
|
||||
return res.Err
|
||||
}
|
||||
}
|
||||
|
||||
c.path = res.Path
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
c.log(logger.Info, "ERR: %s", err)
|
||||
c.conn.NConn.Close()
|
||||
|
||||
c.parent.OnClientClose(c)
|
||||
<-c.terminate
|
||||
return
|
||||
}
|
||||
|
||||
resc := make(chan struct{})
|
||||
c.path.OnClientRecord(client.RecordReq{c, resc}) //nolint:govet
|
||||
<-resc
|
||||
|
||||
c.log(logger.Info, "is publishing to path '%s', %d %s",
|
||||
c.path.Name(),
|
||||
len(tracks),
|
||||
func() string {
|
||||
if len(tracks) == 1 {
|
||||
return "track"
|
||||
}
|
||||
return "tracks"
|
||||
}())
|
||||
|
||||
var onPublishCmd *externalcmd.Cmd
|
||||
if c.path.Conf().RunOnPublish != "" {
|
||||
onPublishCmd = externalcmd.New(c.path.Conf().RunOnPublish,
|
||||
c.path.Conf().RunOnPublishRestart, externalcmd.Environment{
|
||||
Path: c.path.Name(),
|
||||
if path.Conf().RunOnPublish != "" {
|
||||
onPublishCmd = externalcmd.New(path.Conf().RunOnPublish,
|
||||
path.Conf().RunOnPublishRestart, externalcmd.Environment{
|
||||
Path: path.Name(),
|
||||
Port: strconv.FormatInt(int64(c.rtspPort), 10),
|
||||
})
|
||||
}
|
||||
@@ -269,17 +488,17 @@ func (c *Client) run() {
|
||||
if path.Conf().RunOnPublish != "" {
|
||||
onPublishCmd.Close()
|
||||
}
|
||||
}(c.path)
|
||||
}(path)
|
||||
|
||||
readerDone := make(chan error)
|
||||
go func() {
|
||||
readerDone <- func() error {
|
||||
rtcpSenders := rtmputils.NewRTCPSenderSet(tracks, c.path.OnFrame)
|
||||
rtcpSenders := rtmputils.NewRTCPSenderSet(tracks, path.OnFrame)
|
||||
defer rtcpSenders.Close()
|
||||
|
||||
for {
|
||||
c.conn.NConn.SetReadDeadline(time.Now().Add(c.readTimeout))
|
||||
pkt, err := c.conn.RConn.ReadPacket()
|
||||
c.conn.NetConn().SetReadDeadline(time.Now().Add(c.readTimeout))
|
||||
pkt, err := c.conn.ReadPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -296,15 +515,20 @@ func (c *Client) run() {
|
||||
return fmt.Errorf("invalid NALU format (%d)", typ)
|
||||
}
|
||||
|
||||
// encode into RTP/H264 format
|
||||
frames, err := h264Encoder.Write(pkt.Time+pkt.CTime, nalus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, nalu := range nalus {
|
||||
frames, err := h264Encoder.Encode(&rtph264.NALUAndTimestamp{
|
||||
Timestamp: pkt.Time + pkt.CTime,
|
||||
NALU: nalu,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, f := range frames {
|
||||
rtcpSenders.ProcessFrame(videoTrack.ID, time.Now(), gortsplib.StreamTypeRTP, f)
|
||||
c.path.OnFrame(videoTrack.ID, gortsplib.StreamTypeRTP, f)
|
||||
for _, frame := range frames {
|
||||
rtcpSenders.ProcessFrame(videoTrack.ID, time.Now(),
|
||||
gortsplib.StreamTypeRTP, frame)
|
||||
path.OnFrame(videoTrack.ID, gortsplib.StreamTypeRTP, frame)
|
||||
}
|
||||
}
|
||||
|
||||
case av.AAC:
|
||||
@@ -312,15 +536,17 @@ func (c *Client) run() {
|
||||
return fmt.Errorf("ERR: received an AAC frame, but track is not set up")
|
||||
}
|
||||
|
||||
frames, err := aacEncoder.Write(pkt.Time+pkt.CTime, pkt.Data)
|
||||
frame, err := aacEncoder.Encode(&rtpaac.AUAndTimestamp{
|
||||
Timestamp: pkt.Time,
|
||||
AU: pkt.Data,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, f := range frames {
|
||||
rtcpSenders.ProcessFrame(audioTrack.ID, time.Now(), gortsplib.StreamTypeRTP, f)
|
||||
c.path.OnFrame(audioTrack.ID, gortsplib.StreamTypeRTP, f)
|
||||
}
|
||||
rtcpSenders.ProcessFrame(audioTrack.ID, time.Now(),
|
||||
gortsplib.StreamTypeRTP, frame)
|
||||
path.OnFrame(audioTrack.ID, gortsplib.StreamTypeRTP, frame)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("ERR: unexpected packet: %v", pkt.Type)
|
||||
@@ -331,32 +557,28 @@ func (c *Client) run() {
|
||||
|
||||
select {
|
||||
case err := <-readerDone:
|
||||
c.conn.NConn.Close()
|
||||
c.conn.NetConn().Close()
|
||||
|
||||
if err != io.EOF {
|
||||
c.log(logger.Info, "ERR: %s", err)
|
||||
}
|
||||
|
||||
if c.path != nil {
|
||||
res := make(chan struct{})
|
||||
c.path.OnClientRemove(client.RemoveReq{c, res}) //nolint:govet
|
||||
<-res
|
||||
c.path = nil
|
||||
}
|
||||
res := make(chan struct{})
|
||||
path.OnClientRemove(client.RemoveReq{c, res}) //nolint:govet
|
||||
<-res
|
||||
path = nil
|
||||
|
||||
c.parent.OnClientClose(c)
|
||||
<-c.terminate
|
||||
|
||||
case <-c.terminate:
|
||||
c.conn.NConn.Close()
|
||||
c.conn.NetConn().Close()
|
||||
<-readerDone
|
||||
|
||||
if c.path != nil {
|
||||
res := make(chan struct{})
|
||||
c.path.OnClientRemove(client.RemoveReq{c, res}) //nolint:govet
|
||||
<-res
|
||||
c.path = nil
|
||||
}
|
||||
res := make(chan struct{})
|
||||
path.OnClientRemove(client.RemoveReq{c, res}) //nolint:govet
|
||||
<-res
|
||||
path = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,6 +613,39 @@ func (c *Client) Authenticate(authMethods []headers.AuthMethod,
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnReaderFrame implements path.Reader.
|
||||
func (c *Client) OnReaderFrame(trackID int, streamType gortsplib.StreamType, buf []byte) {
|
||||
// OnIncomingFrame implements path.Reader.
|
||||
func (c *Client) OnIncomingFrame(trackID int, streamType gortsplib.StreamType, buf []byte) {
|
||||
if streamType == gortsplib.StreamTypeRTP {
|
||||
if c.videoTrack != nil {
|
||||
if trackID == c.videoTrack.ID {
|
||||
nts, err := c.h264Decoder.Decode(buf)
|
||||
if err != nil {
|
||||
if err != rtph264.ErrMorePacketsNeeded {
|
||||
c.log(logger.Debug, "ERR while decoding video track: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, nt := range nts {
|
||||
c.ringBuffer.Push(nt)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if c.audioTrack != nil {
|
||||
if trackID == c.audioTrack.ID {
|
||||
ats, err := c.aacDecoder.Decode(buf)
|
||||
if err != nil {
|
||||
c.log(logger.Debug, "ERR while decoding audio track: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, at := range ats {
|
||||
c.ringBuffer.Push(at)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,16 +27,6 @@ const (
|
||||
pauseAfterAuthError = 2 * time.Second
|
||||
)
|
||||
|
||||
// ErrNoOnePublishing is a "no one is publishing" error.
|
||||
type ErrNoOnePublishing struct {
|
||||
PathName string
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e ErrNoOnePublishing) Error() string {
|
||||
return fmt.Sprintf("no one is publishing to path '%s'", e.PathName)
|
||||
}
|
||||
|
||||
func ipEqualOrInRange(ip net.IP, ips []interface{}) bool {
|
||||
for _, item := range ips {
|
||||
switch titem := item.(type) {
|
||||
@@ -116,9 +106,9 @@ func New(
|
||||
atomic.AddInt64(c.stats.CountClients, 1)
|
||||
c.log(logger.Info, "connected (%s)", func() string {
|
||||
if isTLS {
|
||||
return "encrypted"
|
||||
return "RTSP/TLS"
|
||||
}
|
||||
return "plain"
|
||||
return "RTSP/TCP"
|
||||
}())
|
||||
|
||||
c.wg.Add(1)
|
||||
@@ -194,7 +184,7 @@ func (c *Client) run() {
|
||||
}
|
||||
return terr.Response, errTerminated
|
||||
|
||||
case ErrNoOnePublishing:
|
||||
case client.ErrNoOnePublishing:
|
||||
return &base.Response{
|
||||
StatusCode: base.StatusNotFound,
|
||||
}, res.Err
|
||||
@@ -308,7 +298,7 @@ func (c *Client) run() {
|
||||
}
|
||||
|
||||
resc := make(chan client.SetupPlayRes)
|
||||
c.parent.OnClientSetupPlay(client.SetupPlayReq{c, reqPath, trackID, req, resc}) //nolint:govet
|
||||
c.parent.OnClientSetupPlay(client.SetupPlayReq{c, reqPath, req, resc}) //nolint:govet
|
||||
res := <-resc
|
||||
|
||||
if res.Err != nil {
|
||||
@@ -324,7 +314,7 @@ func (c *Client) run() {
|
||||
}
|
||||
return terr.Response, errTerminated
|
||||
|
||||
case ErrNoOnePublishing:
|
||||
case client.ErrNoOnePublishing:
|
||||
return &base.Response{
|
||||
StatusCode: base.StatusNotFound,
|
||||
}, res.Err
|
||||
@@ -338,6 +328,12 @@ func (c *Client) run() {
|
||||
|
||||
c.path = res.Path
|
||||
|
||||
if trackID >= len(res.Tracks) {
|
||||
return &base.Response{
|
||||
StatusCode: base.StatusBadRequest,
|
||||
}, fmt.Errorf("track %d does not exist", trackID)
|
||||
}
|
||||
|
||||
default: // record
|
||||
reqPathAndQuery, ok := req.URL.RTSPPathAndQuery()
|
||||
if !ok {
|
||||
@@ -651,8 +647,8 @@ func (c *Client) recordStop() {
|
||||
}
|
||||
}
|
||||
|
||||
// OnReaderFrame implements path.Reader.
|
||||
func (c *Client) OnReaderFrame(trackID int, streamType gortsplib.StreamType, buf []byte) {
|
||||
// OnIncomingFrame implements path.Reader.
|
||||
func (c *Client) OnIncomingFrame(trackID int, streamType gortsplib.StreamType, buf []byte) {
|
||||
if !c.conn.HasSetuppedTrack(trackID) {
|
||||
return
|
||||
}
|
||||
|
||||
+63
-90
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/aler9/gortsplib/pkg/base"
|
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/client"
|
||||
"github.com/aler9/rtsp-simple-server/internal/clientrtsp"
|
||||
"github.com/aler9/rtsp-simple-server/internal/conf"
|
||||
"github.com/aler9/rtsp-simple-server/internal/externalcmd"
|
||||
"github.com/aler9/rtsp-simple-server/internal/logger"
|
||||
@@ -34,18 +33,12 @@ type Parent interface {
|
||||
OnPathClientClose(client.Client)
|
||||
}
|
||||
|
||||
// source can be
|
||||
// * client.Client
|
||||
// * sourcertsp.Source
|
||||
// * sourcertmp.Source
|
||||
// * sourceRedirect
|
||||
// source is implemented by all sources (client* and source*).
|
||||
type source interface {
|
||||
IsSource()
|
||||
}
|
||||
|
||||
// a sourceExternal can be
|
||||
// * sourcertsp.Source
|
||||
// * sourcertmp.Source
|
||||
// sourceExternal is implemented by all source*.
|
||||
type sourceExternal interface {
|
||||
IsSource()
|
||||
IsSourceExternal()
|
||||
@@ -93,8 +86,7 @@ type Path struct {
|
||||
describeRequests []client.DescribeReq
|
||||
setupPlayRequests []client.SetupPlayReq
|
||||
source source
|
||||
sourceTrackCount int
|
||||
sourceSdp []byte
|
||||
sourceTracks gortsplib.Tracks
|
||||
readers *readersMap
|
||||
onDemandCmd *externalcmd.Cmd
|
||||
describeTimer *time.Timer
|
||||
@@ -108,15 +100,15 @@ type Path struct {
|
||||
closeTimerStarted bool
|
||||
|
||||
// in
|
||||
sourceSetReady chan struct{} // from source
|
||||
sourceSetNotReady chan struct{} // from source
|
||||
clientDescribe chan client.DescribeReq // from program
|
||||
clientAnnounce chan client.AnnounceReq // from program
|
||||
clientSetupPlay chan client.SetupPlayReq // from program
|
||||
clientPlay chan client.PlayReq // from client
|
||||
clientRecord chan client.RecordReq // from client
|
||||
clientPause chan client.PauseReq // from client
|
||||
clientRemove chan client.RemoveReq // from client
|
||||
sourceSetReady chan struct{} // from source
|
||||
sourceSetNotReady chan struct{} // from source
|
||||
clientDescribe chan client.DescribeReq
|
||||
clientSetupPlay chan client.SetupPlayReq
|
||||
clientAnnounce chan client.AnnounceReq
|
||||
clientPlay chan client.PlayReq
|
||||
clientRecord chan client.RecordReq
|
||||
clientPause chan client.PauseReq
|
||||
clientRemove chan client.RemoveReq
|
||||
terminate chan struct{}
|
||||
}
|
||||
|
||||
@@ -155,8 +147,8 @@ func New(
|
||||
sourceSetReady: make(chan struct{}),
|
||||
sourceSetNotReady: make(chan struct{}),
|
||||
clientDescribe: make(chan client.DescribeReq),
|
||||
clientAnnounce: make(chan client.AnnounceReq),
|
||||
clientSetupPlay: make(chan client.SetupPlayReq),
|
||||
clientAnnounce: make(chan client.AnnounceReq),
|
||||
clientPlay: make(chan client.PlayReq),
|
||||
clientRecord: make(chan client.RecordReq),
|
||||
clientPause: make(chan client.PauseReq),
|
||||
@@ -208,7 +200,7 @@ outer:
|
||||
pa.describeRequests = nil
|
||||
|
||||
for _, req := range pa.setupPlayRequests {
|
||||
req.Res <- client.SetupPlayRes{nil, fmt.Errorf("publisher of path '%s' has timed out", pa.name)} //nolint:govet
|
||||
req.Res <- client.SetupPlayRes{nil, nil, fmt.Errorf("publisher of path '%s' has timed out", pa.name)} //nolint:govet
|
||||
}
|
||||
pa.setupPlayRequests = nil
|
||||
|
||||
@@ -252,18 +244,13 @@ outer:
|
||||
case req := <-pa.clientSetupPlay:
|
||||
pa.onClientSetupPlay(req)
|
||||
|
||||
case req := <-pa.clientAnnounce:
|
||||
pa.onClientAnnounce(req)
|
||||
|
||||
case req := <-pa.clientPlay:
|
||||
pa.onClientPlay(req.Client)
|
||||
close(req.Res)
|
||||
|
||||
case req := <-pa.clientAnnounce:
|
||||
err := pa.onClientAnnounce(req.Client, req.Tracks)
|
||||
if err != nil {
|
||||
req.Res <- client.AnnounceRes{nil, err} //nolint:govet
|
||||
continue
|
||||
}
|
||||
req.Res <- client.AnnounceRes{pa, nil} //nolint:govet
|
||||
|
||||
case req := <-pa.clientRecord:
|
||||
pa.onClientRecord(req.Client)
|
||||
close(req.Res)
|
||||
@@ -318,7 +305,7 @@ outer:
|
||||
}
|
||||
|
||||
for _, req := range pa.setupPlayRequests {
|
||||
req.Res <- client.SetupPlayRes{nil, fmt.Errorf("terminated")} //nolint:govet
|
||||
req.Res <- client.SetupPlayRes{nil, nil, fmt.Errorf("terminated")} //nolint:govet
|
||||
}
|
||||
|
||||
for c, state := range pa.clients {
|
||||
@@ -339,8 +326,8 @@ outer:
|
||||
close(pa.sourceSetReady)
|
||||
close(pa.sourceSetNotReady)
|
||||
close(pa.clientDescribe)
|
||||
close(pa.clientAnnounce)
|
||||
close(pa.clientSetupPlay)
|
||||
close(pa.clientAnnounce)
|
||||
close(pa.clientPlay)
|
||||
close(pa.clientRecord)
|
||||
close(pa.clientPause)
|
||||
@@ -367,18 +354,18 @@ func (pa *Path) exhaustChannels() {
|
||||
}
|
||||
req.Res <- client.DescribeRes{nil, "", fmt.Errorf("terminated")} //nolint:govet
|
||||
|
||||
case req, ok := <-pa.clientSetupPlay:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
req.Res <- client.SetupPlayRes{nil, nil, fmt.Errorf("terminated")} //nolint:govet
|
||||
|
||||
case req, ok := <-pa.clientAnnounce:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
req.Res <- client.AnnounceRes{nil, fmt.Errorf("terminated")} //nolint:govet
|
||||
|
||||
case req, ok := <-pa.clientSetupPlay:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
req.Res <- client.SetupPlayRes{nil, fmt.Errorf("terminated")} //nolint:govet
|
||||
|
||||
case req, ok := <-pa.clientPlay:
|
||||
if !ok {
|
||||
return
|
||||
@@ -508,7 +495,7 @@ func (pa *Path) onSourceSetReady() {
|
||||
pa.sourceState = sourceStateReady
|
||||
|
||||
for _, req := range pa.describeRequests {
|
||||
req.Res <- client.DescribeRes{pa.sourceSdp, "", nil} //nolint:govet
|
||||
req.Res <- client.DescribeRes{pa.sourceTracks.Write(), "", nil} //nolint:govet
|
||||
}
|
||||
pa.describeRequests = nil
|
||||
|
||||
@@ -594,7 +581,7 @@ func (pa *Path) onClientDescribe(req client.DescribeReq) {
|
||||
|
||||
switch pa.sourceState {
|
||||
case sourceStateReady:
|
||||
req.Res <- client.DescribeRes{pa.sourceSdp, "", nil} //nolint:govet
|
||||
req.Res <- client.DescribeRes{pa.sourceTracks.Write(), "", nil} //nolint:govet
|
||||
return
|
||||
|
||||
case sourceStateWaitingDescribe:
|
||||
@@ -606,9 +593,9 @@ func (pa *Path) onClientDescribe(req client.DescribeReq) {
|
||||
fallbackURL := func() string {
|
||||
if strings.HasPrefix(pa.conf.Fallback, "/") {
|
||||
ur := base.URL{
|
||||
Scheme: req.Req.URL.Scheme,
|
||||
User: req.Req.URL.User,
|
||||
Host: req.Req.URL.Host,
|
||||
Scheme: req.Data.URL.Scheme,
|
||||
User: req.Data.URL.User,
|
||||
Host: req.Data.URL.Host,
|
||||
Path: pa.conf.Fallback,
|
||||
}
|
||||
return ur.String()
|
||||
@@ -619,36 +606,11 @@ func (pa *Path) onClientDescribe(req client.DescribeReq) {
|
||||
return
|
||||
}
|
||||
|
||||
req.Res <- client.DescribeRes{nil, "", clientrtsp.ErrNoOnePublishing{pa.name}} //nolint:govet
|
||||
req.Res <- client.DescribeRes{nil, "", client.ErrNoOnePublishing{pa.name}} //nolint:govet
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (pa *Path) onClientSetupPlayPost(req client.SetupPlayReq) {
|
||||
if req.TrackID >= pa.sourceTrackCount {
|
||||
req.Res <- client.SetupPlayRes{nil, fmt.Errorf("track %d does not exist", req.TrackID)} //nolint:govet
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := pa.clients[req.Client]; !ok {
|
||||
// prevent on-demand source from closing
|
||||
if pa.sourceCloseTimerStarted {
|
||||
pa.sourceCloseTimer = newEmptyTimer()
|
||||
pa.sourceCloseTimerStarted = false
|
||||
}
|
||||
|
||||
// prevent on-demand command from closing
|
||||
if pa.runOnDemandCloseTimerStarted {
|
||||
pa.runOnDemandCloseTimer = newEmptyTimer()
|
||||
pa.runOnDemandCloseTimerStarted = false
|
||||
}
|
||||
|
||||
pa.addClient(req.Client, clientStatePrePlay)
|
||||
}
|
||||
|
||||
req.Res <- client.SetupPlayRes{pa, nil} //nolint:govet
|
||||
}
|
||||
|
||||
func (pa *Path) onClientSetupPlay(req client.SetupPlayReq) {
|
||||
pa.fixedPublisherStart()
|
||||
pa.scheduleClose()
|
||||
@@ -663,11 +625,31 @@ func (pa *Path) onClientSetupPlay(req client.SetupPlayReq) {
|
||||
return
|
||||
|
||||
case sourceStateNotReady:
|
||||
req.Res <- client.SetupPlayRes{nil, clientrtsp.ErrNoOnePublishing{pa.name}} //nolint:govet
|
||||
req.Res <- client.SetupPlayRes{nil, nil, client.ErrNoOnePublishing{pa.name}} //nolint:govet
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (pa *Path) onClientSetupPlayPost(req client.SetupPlayReq) {
|
||||
if _, ok := pa.clients[req.Client]; !ok {
|
||||
// prevent on-demand source from closing
|
||||
if pa.sourceCloseTimerStarted {
|
||||
pa.sourceCloseTimer = newEmptyTimer()
|
||||
pa.sourceCloseTimerStarted = false
|
||||
}
|
||||
|
||||
// prevent on-demand command from closing
|
||||
if pa.runOnDemandCloseTimerStarted {
|
||||
pa.runOnDemandCloseTimer = newEmptyTimer()
|
||||
pa.runOnDemandCloseTimerStarted = false
|
||||
}
|
||||
|
||||
pa.addClient(req.Client, clientStatePrePlay)
|
||||
}
|
||||
|
||||
req.Res <- client.SetupPlayRes{pa, pa.sourceTracks, nil} //nolint:govet
|
||||
}
|
||||
|
||||
func (pa *Path) onClientPlay(c client.Client) {
|
||||
state, ok := pa.clients[c]
|
||||
if !ok {
|
||||
@@ -680,17 +662,18 @@ func (pa *Path) onClientPlay(c client.Client) {
|
||||
|
||||
atomic.AddInt64(pa.stats.CountReaders, 1)
|
||||
pa.clients[c] = clientStatePlay
|
||||
|
||||
pa.readers.add(c)
|
||||
}
|
||||
|
||||
func (pa *Path) onClientAnnounce(c client.Client, tracks gortsplib.Tracks) error {
|
||||
if _, ok := pa.clients[c]; ok {
|
||||
return fmt.Errorf("already subscribed")
|
||||
func (pa *Path) onClientAnnounce(req client.AnnounceReq) {
|
||||
if _, ok := pa.clients[req.Client]; ok {
|
||||
req.Res <- client.AnnounceRes{nil, fmt.Errorf("already publishing or reading")} //nolint:govet
|
||||
return
|
||||
}
|
||||
|
||||
if pa.hasExternalSource() {
|
||||
return fmt.Errorf("path '%s' is assigned to an external source", pa.name)
|
||||
req.Res <- client.AnnounceRes{nil, fmt.Errorf("path '%s' is assigned to an external source", pa.name)} //nolint:govet
|
||||
return
|
||||
}
|
||||
|
||||
if pa.source != nil {
|
||||
@@ -707,12 +690,11 @@ func (pa *Path) onClientAnnounce(c client.Client, tracks gortsplib.Tracks) error
|
||||
}
|
||||
}
|
||||
|
||||
pa.addClient(c, clientStatePreRecord)
|
||||
pa.addClient(req.Client, clientStatePreRecord)
|
||||
|
||||
pa.source = c
|
||||
pa.sourceTrackCount = len(tracks)
|
||||
pa.sourceSdp = tracks.Write()
|
||||
return nil
|
||||
pa.source = req.Client
|
||||
pa.sourceTracks = req.Tracks
|
||||
req.Res <- client.AnnounceRes{pa, nil} //nolint:govet
|
||||
}
|
||||
|
||||
func (pa *Path) onClientRecord(c client.Client) {
|
||||
@@ -727,7 +709,6 @@ func (pa *Path) onClientRecord(c client.Client) {
|
||||
|
||||
atomic.AddInt64(pa.stats.CountPublishers, 1)
|
||||
pa.clients[c] = clientStateRecord
|
||||
|
||||
pa.onSourceSetReady()
|
||||
}
|
||||
|
||||
@@ -740,13 +721,11 @@ func (pa *Path) onClientPause(c client.Client) {
|
||||
if state == clientStatePlay {
|
||||
atomic.AddInt64(pa.stats.CountReaders, -1)
|
||||
pa.clients[c] = clientStatePrePlay
|
||||
|
||||
pa.readers.remove(c)
|
||||
|
||||
} else if state == clientStateRecord {
|
||||
atomic.AddInt64(pa.stats.CountPublishers, -1)
|
||||
pa.clients[c] = clientStatePreRecord
|
||||
|
||||
pa.onSourceSetNotReady()
|
||||
}
|
||||
}
|
||||
@@ -813,15 +792,9 @@ func (pa *Path) Name() string {
|
||||
return pa.name
|
||||
}
|
||||
|
||||
// SourceTrackCount returns the number of tracks of the source this path.
|
||||
func (pa *Path) SourceTrackCount() int {
|
||||
return pa.sourceTrackCount
|
||||
}
|
||||
|
||||
// OnSourceSetReady is called by a source.
|
||||
func (pa *Path) OnSourceSetReady(tracks gortsplib.Tracks) {
|
||||
pa.sourceSdp = tracks.Write()
|
||||
pa.sourceTrackCount = len(tracks)
|
||||
pa.sourceTracks = tracks
|
||||
pa.sourceSetReady <- struct{}{}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
type reader interface {
|
||||
OnReaderFrame(int, gortsplib.StreamType, []byte)
|
||||
OnIncomingFrame(int, gortsplib.StreamType, []byte)
|
||||
}
|
||||
|
||||
type readersMap struct {
|
||||
@@ -40,6 +40,6 @@ func (m *readersMap) forwardFrame(trackID int, streamType gortsplib.StreamType,
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
for c := range m.ma {
|
||||
c.OnReaderFrame(trackID, streamType, buf)
|
||||
c.OnIncomingFrame(trackID, streamType, buf)
|
||||
}
|
||||
}
|
||||
|
||||
+50
-54
@@ -38,8 +38,8 @@ type PathManager struct {
|
||||
confReload chan map[string]*conf.PathConf
|
||||
pathClose chan *path.Path
|
||||
clientDescribe chan client.DescribeReq
|
||||
clientAnnounce chan client.AnnounceReq
|
||||
clientSetupPlay chan client.SetupPlayReq
|
||||
clientAnnounce chan client.AnnounceReq
|
||||
terminate chan struct{}
|
||||
|
||||
// out
|
||||
@@ -73,8 +73,8 @@ func New(
|
||||
confReload: make(chan map[string]*conf.PathConf),
|
||||
pathClose: make(chan *path.Path),
|
||||
clientDescribe: make(chan client.DescribeReq),
|
||||
clientAnnounce: make(chan client.AnnounceReq),
|
||||
clientSetupPlay: make(chan client.SetupPlayReq),
|
||||
clientAnnounce: make(chan client.AnnounceReq),
|
||||
terminate: make(chan struct{}),
|
||||
clientClose: make(chan client.Client),
|
||||
done: make(chan struct{}),
|
||||
@@ -155,9 +155,13 @@ outer:
|
||||
continue
|
||||
}
|
||||
|
||||
err = req.Client.Authenticate(pm.authMethods, req.PathName,
|
||||
pathConf.ReadIpsParsed, pathConf.ReadUser, pathConf.ReadPass,
|
||||
req.Req)
|
||||
err = req.Client.Authenticate(
|
||||
pm.authMethods,
|
||||
req.PathName,
|
||||
pathConf.ReadIpsParsed,
|
||||
pathConf.ReadUser,
|
||||
pathConf.ReadPass,
|
||||
req.Data)
|
||||
if err != nil {
|
||||
req.Res <- client.DescribeRes{nil, "", err} //nolint:govet
|
||||
continue
|
||||
@@ -165,37 +169,15 @@ outer:
|
||||
|
||||
// create path if it doesn't exist
|
||||
if _, ok := pm.paths[req.PathName]; !ok {
|
||||
pm.paths[req.PathName] = pm.createPath(pathName, pathConf, req.PathName)
|
||||
pm.createPath(pathName, pathConf, req.PathName)
|
||||
}
|
||||
|
||||
pm.paths[req.PathName].OnPathManDescribe(req)
|
||||
|
||||
case req := <-pm.clientAnnounce:
|
||||
pathName, pathConf, err := pm.findPathConf(req.PathName)
|
||||
if err != nil {
|
||||
req.Res <- client.AnnounceRes{nil, err} //nolint:govet
|
||||
continue
|
||||
}
|
||||
|
||||
err = req.Client.Authenticate(pm.authMethods, req.PathName,
|
||||
pathConf.PublishIpsParsed, pathConf.PublishUser,
|
||||
pathConf.PublishPass, req.Req)
|
||||
if err != nil {
|
||||
req.Res <- client.AnnounceRes{nil, err} //nolint:govet
|
||||
continue
|
||||
}
|
||||
|
||||
// create path if it doesn't exist
|
||||
if _, ok := pm.paths[req.PathName]; !ok {
|
||||
pm.paths[req.PathName] = pm.createPath(pathName, pathConf, req.PathName)
|
||||
}
|
||||
|
||||
pm.paths[req.PathName].OnPathManAnnounce(req)
|
||||
|
||||
case req := <-pm.clientSetupPlay:
|
||||
pathName, pathConf, err := pm.findPathConf(req.PathName)
|
||||
if err != nil {
|
||||
req.Res <- client.SetupPlayRes{nil, err} //nolint:govet
|
||||
req.Res <- client.SetupPlayRes{nil, nil, err} //nolint:govet
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -205,31 +187,45 @@ outer:
|
||||
pathConf.ReadIpsParsed,
|
||||
pathConf.ReadUser,
|
||||
pathConf.ReadPass,
|
||||
req.Req)
|
||||
req.Data)
|
||||
if err != nil {
|
||||
req.Res <- client.SetupPlayRes{nil, err} //nolint:govet
|
||||
req.Res <- client.SetupPlayRes{nil, nil, err} //nolint:govet
|
||||
continue
|
||||
}
|
||||
|
||||
// create path if it doesn't exist
|
||||
if _, ok := pm.paths[req.PathName]; !ok {
|
||||
pa := path.New(
|
||||
pm.rtspPort,
|
||||
pm.readTimeout,
|
||||
pm.writeTimeout,
|
||||
pm.readBufferCount,
|
||||
pm.readBufferSize,
|
||||
pathName,
|
||||
pathConf,
|
||||
req.PathName,
|
||||
&pm.wg,
|
||||
pm.stats,
|
||||
pm)
|
||||
pm.paths[req.PathName] = pa
|
||||
pm.createPath(pathName, pathConf, req.PathName)
|
||||
}
|
||||
|
||||
pm.paths[req.PathName].OnPathManSetupPlay(req)
|
||||
|
||||
case req := <-pm.clientAnnounce:
|
||||
pathName, pathConf, err := pm.findPathConf(req.PathName)
|
||||
if err != nil {
|
||||
req.Res <- client.AnnounceRes{nil, err} //nolint:govet
|
||||
continue
|
||||
}
|
||||
|
||||
err = req.Client.Authenticate(
|
||||
pm.authMethods,
|
||||
req.PathName,
|
||||
pathConf.PublishIpsParsed,
|
||||
pathConf.PublishUser,
|
||||
pathConf.PublishPass,
|
||||
req.Data)
|
||||
if err != nil {
|
||||
req.Res <- client.AnnounceRes{nil, err} //nolint:govet
|
||||
continue
|
||||
}
|
||||
|
||||
// create path if it doesn't exist
|
||||
if _, ok := pm.paths[req.PathName]; !ok {
|
||||
pm.createPath(pathName, pathConf, req.PathName)
|
||||
}
|
||||
|
||||
pm.paths[req.PathName].OnPathManAnnounce(req)
|
||||
|
||||
case <-pm.terminate:
|
||||
break outer
|
||||
}
|
||||
@@ -254,17 +250,17 @@ outer:
|
||||
}
|
||||
req.Res <- client.DescribeRes{nil, "", fmt.Errorf("terminated")} //nolint:govet
|
||||
|
||||
case req, ok := <-pm.clientSetupPlay:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
req.Res <- client.SetupPlayRes{nil, nil, fmt.Errorf("terminated")} //nolint:govet
|
||||
|
||||
case req, ok := <-pm.clientAnnounce:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
req.Res <- client.AnnounceRes{nil, fmt.Errorf("terminated")} //nolint:govet
|
||||
|
||||
case req, ok := <-pm.clientSetupPlay:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
req.Res <- client.SetupPlayRes{nil, fmt.Errorf("terminated")} //nolint:govet
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -278,12 +274,12 @@ outer:
|
||||
close(pm.clientClose)
|
||||
close(pm.pathClose)
|
||||
close(pm.clientDescribe)
|
||||
close(pm.clientAnnounce)
|
||||
close(pm.clientSetupPlay)
|
||||
close(pm.clientAnnounce)
|
||||
}
|
||||
|
||||
func (pm *PathManager) createPath(confName string, conf *conf.PathConf, name string) *path.Path {
|
||||
return path.New(
|
||||
func (pm *PathManager) createPath(confName string, conf *conf.PathConf, name string) {
|
||||
pm.paths[name] = path.New(
|
||||
pm.rtspPort,
|
||||
pm.readTimeout,
|
||||
pm.writeTimeout,
|
||||
@@ -300,7 +296,7 @@ func (pm *PathManager) createPath(confName string, conf *conf.PathConf, name str
|
||||
func (pm *PathManager) createPaths() {
|
||||
for pathName, pathConf := range pm.pathConfs {
|
||||
if _, ok := pm.paths[pathName]; !ok && pathConf.Regexp == nil {
|
||||
pm.paths[pathName] = pm.createPath(pathName, pathConf, pathName)
|
||||
pm.createPath(pathName, pathConf, pathName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package rtmputils
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
"github.com/notedit/rtmp/av"
|
||||
"github.com/notedit/rtmp/format/rtmp"
|
||||
)
|
||||
|
||||
// Conn contains a RTMP connection and a net connection.
|
||||
type Conn struct {
|
||||
rconn *rtmp.Conn
|
||||
nconn net.Conn
|
||||
}
|
||||
|
||||
// NewConn allocates a Conn.
|
||||
func NewConn(rconn *rtmp.Conn, nconn net.Conn) *Conn {
|
||||
return &Conn{
|
||||
rconn: rconn,
|
||||
nconn: nconn,
|
||||
}
|
||||
}
|
||||
|
||||
// NetConn returns the underlying net.Conn.
|
||||
func (c *Conn) NetConn() net.Conn {
|
||||
return c.nconn
|
||||
}
|
||||
|
||||
// IsPublishing returns whether the connection is publishing.
|
||||
func (c *Conn) IsPublishing() bool {
|
||||
return c.rconn.Publishing
|
||||
}
|
||||
|
||||
// URL returns the URL requested by the connection.
|
||||
func (c *Conn) URL() *url.URL {
|
||||
return c.rconn.URL
|
||||
}
|
||||
|
||||
// ReadPacket reads a packet.
|
||||
func (c *Conn) ReadPacket() (av.Packet, error) {
|
||||
return c.rconn.ReadPacket()
|
||||
}
|
||||
|
||||
// WritePacket writes a packet.
|
||||
func (c *Conn) WritePacket(pkt av.Packet) error {
|
||||
err := c.rconn.WritePacket(pkt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.rconn.FlushWrite()
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package rtmputils
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/notedit/rtmp/format/rtmp"
|
||||
)
|
||||
|
||||
// ConnPair contains a RTMP connection and a net connection.
|
||||
type ConnPair struct {
|
||||
RConn *rtmp.Conn
|
||||
NConn net.Conn
|
||||
}
|
||||
@@ -2,13 +2,11 @@ package rtmputils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/aler9/gortsplib"
|
||||
"github.com/notedit/rtmp/av"
|
||||
"github.com/notedit/rtmp/codec/h264"
|
||||
"github.com/notedit/rtmp/format/flv/flvio"
|
||||
"github.com/notedit/rtmp/format/rtmp"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -16,8 +14,8 @@ const (
|
||||
codecAAC = 10
|
||||
)
|
||||
|
||||
func readMetadata(rconn *rtmp.Conn) (flvio.AMFMap, error) {
|
||||
pkt, err := rconn.ReadPacket()
|
||||
func readMetadata(conn *Conn) (flvio.AMFMap, error) {
|
||||
pkt, err := conn.ReadPacket()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -43,16 +41,12 @@ func readMetadata(rconn *rtmp.Conn) (flvio.AMFMap, error) {
|
||||
return ma, nil
|
||||
}
|
||||
|
||||
// Metadata extracts track informations from a RTMP connection that is publishing.
|
||||
func Metadata(conn ConnPair, readTimeout time.Duration) (
|
||||
*gortsplib.Track, *gortsplib.Track, error) {
|
||||
// ReadMetadata extracts track informations from a RTMP connection that is publishing.
|
||||
func ReadMetadata(conn *Conn) (*gortsplib.Track, *gortsplib.Track, error) {
|
||||
var videoTrack *gortsplib.Track
|
||||
var audioTrack *gortsplib.Track
|
||||
|
||||
// configuration must be completed within readTimeout
|
||||
conn.NConn.SetReadDeadline(time.Now().Add(readTimeout))
|
||||
|
||||
md, err := readMetadata(conn.RConn)
|
||||
md, err := readMetadata(conn)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -121,7 +115,7 @@ func Metadata(conn ConnPair, readTimeout time.Duration) (
|
||||
|
||||
for {
|
||||
var pkt av.Packet
|
||||
pkt, err = conn.RConn.ReadPacket()
|
||||
pkt, err = conn.ReadPacket()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -165,3 +159,38 @@ func Metadata(conn ConnPair, readTimeout time.Duration) (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteMetadata writes track informations to a RTMP connection that is reading.
|
||||
func WriteMetadata(conn *Conn, videoTrack *gortsplib.Track, audioTrack *gortsplib.Track) error {
|
||||
return conn.WritePacket(av.Packet{
|
||||
Type: av.Metadata,
|
||||
Data: flvio.FillAMF0ValMalloc(flvio.AMFMap{
|
||||
{
|
||||
K: "videodatarate",
|
||||
V: float64(0),
|
||||
},
|
||||
{
|
||||
K: "videocodecid",
|
||||
V: func() float64 {
|
||||
if videoTrack != nil {
|
||||
return codecH264
|
||||
}
|
||||
return 0
|
||||
}(),
|
||||
},
|
||||
{
|
||||
K: "audiodatarate",
|
||||
V: float64(0),
|
||||
},
|
||||
{
|
||||
K: "audiocodecid",
|
||||
V: func() float64 {
|
||||
if audioTrack != nil {
|
||||
return codecAAC
|
||||
}
|
||||
return 0
|
||||
}(),
|
||||
},
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ type Server struct {
|
||||
srv *rtmp.Server
|
||||
wg sync.WaitGroup
|
||||
|
||||
accept chan rtmputils.ConnPair
|
||||
accept chan *rtmputils.Conn
|
||||
}
|
||||
|
||||
// New allocates a Server.
|
||||
@@ -39,7 +39,7 @@ func New(
|
||||
|
||||
s := &Server{
|
||||
l: l,
|
||||
accept: make(chan rtmputils.ConnPair),
|
||||
accept: make(chan *rtmputils.Conn),
|
||||
}
|
||||
|
||||
s.srv = rtmp.NewServer()
|
||||
@@ -57,7 +57,7 @@ func New(
|
||||
func (s *Server) Close() {
|
||||
go func() {
|
||||
for co := range s.accept {
|
||||
co.NConn.Close()
|
||||
co.NetConn().Close()
|
||||
}
|
||||
}()
|
||||
s.l.Close()
|
||||
@@ -83,10 +83,10 @@ func (s *Server) run() {
|
||||
}
|
||||
|
||||
func (s *Server) innerHandleConn(rconn *rtmp.Conn, nconn net.Conn) {
|
||||
s.accept <- rtmputils.ConnPair{rconn, nconn} //nolint:govet
|
||||
s.accept <- rtmputils.NewConn(rconn, nconn)
|
||||
}
|
||||
|
||||
// Accept returns a channel to accept incoming connections.
|
||||
func (s *Server) Accept() chan rtmputils.ConnPair {
|
||||
func (s *Server) Accept() chan *rtmputils.Conn {
|
||||
return s.accept
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ func (s *Source) run() {
|
||||
func (s *Source) runInner() bool {
|
||||
s.log(logger.Info, "connecting")
|
||||
|
||||
var conn rtmputils.ConnPair
|
||||
var conn *rtmputils.Conn
|
||||
var err error
|
||||
dialDone := make(chan struct{}, 1)
|
||||
go func() {
|
||||
@@ -120,7 +120,7 @@ func (s *Source) runInner() bool {
|
||||
var rconn *rtmp.Conn
|
||||
var nconn net.Conn
|
||||
rconn, nconn, err = rtmp.NewClient().Dial(s.ur, rtmp.PrepareReading)
|
||||
conn = rtmputils.ConnPair{rconn, nconn} //nolint:govet
|
||||
conn = rtmputils.NewConn(rconn, nconn)
|
||||
}()
|
||||
|
||||
select {
|
||||
@@ -139,14 +139,14 @@ func (s *Source) runInner() bool {
|
||||
metadataDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(metadataDone)
|
||||
videoTrack, audioTrack, err = rtmputils.Metadata(
|
||||
conn, s.readTimeout) //nolint:govet
|
||||
conn.NetConn().SetReadDeadline(time.Now().Add(s.readTimeout))
|
||||
videoTrack, audioTrack, err = rtmputils.ReadMetadata(conn)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-metadataDone:
|
||||
case <-s.terminate:
|
||||
conn.NConn.Close()
|
||||
conn.NetConn().Close()
|
||||
<-metadataDone
|
||||
return false
|
||||
}
|
||||
@@ -160,26 +160,14 @@ func (s *Source) runInner() bool {
|
||||
|
||||
var h264Encoder *rtph264.Encoder
|
||||
if videoTrack != nil {
|
||||
var err error
|
||||
h264Encoder, err = rtph264.NewEncoder(96)
|
||||
if err != nil {
|
||||
conn.NConn.Close()
|
||||
s.log(logger.Info, "ERR: %s", err)
|
||||
return true
|
||||
}
|
||||
h264Encoder = rtph264.NewEncoder(96, nil, nil, nil)
|
||||
tracks = append(tracks, videoTrack)
|
||||
}
|
||||
|
||||
var aacEncoder *rtpaac.Encoder
|
||||
if audioTrack != nil {
|
||||
clockRate, _ := audioTrack.ClockRate()
|
||||
var err error
|
||||
aacEncoder, err = rtpaac.NewEncoder(96, clockRate)
|
||||
if err != nil {
|
||||
conn.NConn.Close()
|
||||
s.log(logger.Info, "ERR: %s", err)
|
||||
return true
|
||||
}
|
||||
aacEncoder = rtpaac.NewEncoder(96, clockRate, nil, nil, nil)
|
||||
tracks = append(tracks, audioTrack)
|
||||
}
|
||||
|
||||
@@ -198,8 +186,8 @@ func (s *Source) runInner() bool {
|
||||
defer rtcpSenders.Close()
|
||||
|
||||
for {
|
||||
conn.NConn.SetReadDeadline(time.Now().Add(s.readTimeout))
|
||||
pkt, err := conn.RConn.ReadPacket()
|
||||
conn.NetConn().SetReadDeadline(time.Now().Add(s.readTimeout))
|
||||
pkt, err := conn.ReadPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -216,15 +204,21 @@ func (s *Source) runInner() bool {
|
||||
return fmt.Errorf("invalid NALU format (%d)", typ)
|
||||
}
|
||||
|
||||
// encode into RTP/H264 format
|
||||
frames, err := h264Encoder.Write(pkt.Time+pkt.CTime, nalus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, nalu := range nalus {
|
||||
// encode into RTP/H264 format
|
||||
frames, err := h264Encoder.Encode(&rtph264.NALUAndTimestamp{
|
||||
Timestamp: pkt.Time + pkt.CTime,
|
||||
NALU: nalu,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, f := range frames {
|
||||
rtcpSenders.ProcessFrame(videoTrack.ID, time.Now(), gortsplib.StreamTypeRTP, f)
|
||||
s.parent.OnFrame(videoTrack.ID, gortsplib.StreamTypeRTP, f)
|
||||
for _, frame := range frames {
|
||||
rtcpSenders.ProcessFrame(videoTrack.ID, time.Now(),
|
||||
gortsplib.StreamTypeRTP, frame)
|
||||
s.parent.OnFrame(videoTrack.ID, gortsplib.StreamTypeRTP, frame)
|
||||
}
|
||||
}
|
||||
|
||||
case av.AAC:
|
||||
@@ -232,15 +226,17 @@ func (s *Source) runInner() bool {
|
||||
return fmt.Errorf("ERR: received an AAC frame, but track is not set up")
|
||||
}
|
||||
|
||||
frames, err := aacEncoder.Write(pkt.Time+pkt.CTime, pkt.Data)
|
||||
frame, err := aacEncoder.Encode(&rtpaac.AUAndTimestamp{
|
||||
Timestamp: pkt.Time + pkt.CTime,
|
||||
AU: pkt.Data,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, f := range frames {
|
||||
rtcpSenders.ProcessFrame(audioTrack.ID, time.Now(), gortsplib.StreamTypeRTP, f)
|
||||
s.parent.OnFrame(audioTrack.ID, gortsplib.StreamTypeRTP, f)
|
||||
}
|
||||
rtcpSenders.ProcessFrame(audioTrack.ID, time.Now(),
|
||||
gortsplib.StreamTypeRTP, frame)
|
||||
s.parent.OnFrame(audioTrack.ID, gortsplib.StreamTypeRTP, frame)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("ERR: unexpected packet: %v", pkt.Type)
|
||||
@@ -252,12 +248,12 @@ func (s *Source) runInner() bool {
|
||||
for {
|
||||
select {
|
||||
case err := <-readerDone:
|
||||
conn.NConn.Close()
|
||||
conn.NetConn().Close()
|
||||
s.log(logger.Info, "ERR: %s", err)
|
||||
return true
|
||||
|
||||
case <-s.terminate:
|
||||
conn.NConn.Close()
|
||||
conn.NetConn().Close()
|
||||
<-readerDone
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -257,6 +257,8 @@ func (p *program) createResources(initial bool) error {
|
||||
p.clientMan = clientman.New(
|
||||
p.conf.RTSPPort,
|
||||
p.conf.ReadTimeout,
|
||||
p.conf.WriteTimeout,
|
||||
p.conf.ReadBufferCount,
|
||||
p.conf.RunOnConnect,
|
||||
p.conf.RunOnConnectRestart,
|
||||
p.conf.ProtocolsParsed,
|
||||
@@ -350,6 +352,8 @@ func (p *program) closeResources(newConf *conf.Conf) {
|
||||
closePathMan ||
|
||||
newConf.RTSPPort != p.conf.RTSPPort ||
|
||||
newConf.ReadTimeout != p.conf.ReadTimeout ||
|
||||
newConf.WriteTimeout != p.conf.WriteTimeout ||
|
||||
newConf.ReadBufferCount != p.conf.ReadBufferCount ||
|
||||
newConf.RunOnConnect != p.conf.RunOnConnect ||
|
||||
newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||
|
||||
!reflect.DeepEqual(newConf.ProtocolsParsed, p.conf.ProtocolsParsed) {
|
||||
|
||||
+209
-78
@@ -185,7 +185,7 @@ y++U32uuSFiXDcSLarfIsE992MEJLSAynbF1Rsgsr3gXbGiuToJRyxbIeVy7gwzD
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`)
|
||||
|
||||
func TestPublishRead(t *testing.T) {
|
||||
func TestRTSPPublishRead(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
encrypted bool
|
||||
publisherSoft string
|
||||
@@ -315,7 +315,7 @@ func TestPublishRead(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutomaticProtocol(t *testing.T) {
|
||||
func TestRTSPAutomaticProtocol(t *testing.T) {
|
||||
for _, source := range []string{
|
||||
"ffmpeg",
|
||||
} {
|
||||
@@ -351,7 +351,7 @@ func TestAutomaticProtocol(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublisherOverride(t *testing.T) {
|
||||
func TestRTSPPublisherOverride(t *testing.T) {
|
||||
p, ok := testProgram("")
|
||||
require.Equal(t, true, ok)
|
||||
defer p.close()
|
||||
@@ -445,7 +445,7 @@ func TestPublisherOverride(t *testing.T) {
|
||||
defer conn2.Close()
|
||||
}
|
||||
|
||||
func TestPath(t *testing.T) {
|
||||
func TestRTSPPath(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
path string
|
||||
@@ -490,7 +490,7 @@ func TestPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuth(t *testing.T) {
|
||||
func TestRTSPAuth(t *testing.T) {
|
||||
t.Run("publish", func(t *testing.T) {
|
||||
p, ok := testProgram("paths:\n" +
|
||||
" all:\n" +
|
||||
@@ -607,43 +607,9 @@ func TestAuth(t *testing.T) {
|
||||
defer cnt2.close()
|
||||
require.Equal(t, 0, cnt2.wait())
|
||||
})
|
||||
|
||||
t.Run("rtmp", func(t *testing.T) {
|
||||
p, ok := testProgram("rtmpEnable: yes\n" +
|
||||
"paths:\n" +
|
||||
" all:\n" +
|
||||
" publishUser: testuser\n" +
|
||||
" publishPass: testpass\n")
|
||||
require.Equal(t, true, ok)
|
||||
defer p.close()
|
||||
|
||||
cnt1, err := newContainer("ffmpeg", "source", []string{
|
||||
"-re",
|
||||
"-stream_loop", "-1",
|
||||
"-i", "emptyvideo.ts",
|
||||
"-c", "copy",
|
||||
"-f", "flv",
|
||||
"rtmp://" + ownDockerIP + "/test1/test2?user=testuser&pass=testpass",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer cnt1.close()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
cnt2, err := newContainer("ffmpeg", "dest", []string{
|
||||
"-rtsp_transport", "udp",
|
||||
"-i", "rtsp://" + ownDockerIP + ":8554/test1/test2",
|
||||
"-vframes", "1",
|
||||
"-f", "image2",
|
||||
"-y", "/dev/null",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer cnt2.close()
|
||||
require.Equal(t, 0, cnt2.wait())
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthFail(t *testing.T) {
|
||||
func TestRTSPAuthFail(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
user string
|
||||
@@ -757,7 +723,7 @@ func TestAuthFail(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthIpFail(t *testing.T) {
|
||||
func TestRTSPAuthIpFail(t *testing.T) {
|
||||
p, ok := testProgram("paths:\n" +
|
||||
" all:\n" +
|
||||
" publishIps: [127.0.0.1/32]\n")
|
||||
@@ -778,12 +744,207 @@ func TestAuthIpFail(t *testing.T) {
|
||||
require.NotEqual(t, 0, cnt1.wait())
|
||||
}
|
||||
|
||||
func TestRTMPPublish(t *testing.T) {
|
||||
p, ok := testProgram("rtmpEnable: yes\n")
|
||||
require.Equal(t, true, ok)
|
||||
defer p.close()
|
||||
|
||||
cnt1, err := newContainer("ffmpeg", "source", []string{
|
||||
"-re",
|
||||
"-stream_loop", "-1",
|
||||
"-i", "emptyvideo.ts",
|
||||
"-c", "copy",
|
||||
"-f", "flv",
|
||||
"rtmp://" + ownDockerIP + ":1935/test1/test2",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer cnt1.close()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
cnt2, err := newContainer("ffmpeg", "dest", []string{
|
||||
"-rtsp_transport", "udp",
|
||||
"-i", "rtsp://" + ownDockerIP + ":8554/test1/test2",
|
||||
"-vframes", "1",
|
||||
"-f", "image2",
|
||||
"-y", "/dev/null",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer cnt2.close()
|
||||
require.Equal(t, 0, cnt2.wait())
|
||||
}
|
||||
|
||||
func TestRTMPRead(t *testing.T) {
|
||||
p, ok := testProgram("rtmpEnable: yes\n")
|
||||
require.Equal(t, true, ok)
|
||||
defer p.close()
|
||||
|
||||
cnt1, err := newContainer("ffmpeg", "source", []string{
|
||||
"-re",
|
||||
"-stream_loop", "-1",
|
||||
"-i", "emptyvideo.ts",
|
||||
"-c", "copy",
|
||||
"-f", "rtsp",
|
||||
"rtsp://" + ownDockerIP + ":8554/teststream",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer cnt1.close()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
cnt2, err := newContainer("ffmpeg", "dest", []string{
|
||||
"-i", "rtmp://" + ownDockerIP + ":1935/teststream",
|
||||
"-vframes", "1",
|
||||
"-f", "image2",
|
||||
"-y", "/dev/null",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer cnt2.close()
|
||||
require.Equal(t, 0, cnt2.wait())
|
||||
}
|
||||
|
||||
func TestRTMPAuth(t *testing.T) {
|
||||
t.Run("publish", func(t *testing.T) {
|
||||
p, ok := testProgram("rtmpEnable: yes\n" +
|
||||
"paths:\n" +
|
||||
" all:\n" +
|
||||
" publishUser: testuser\n" +
|
||||
" publishPass: testpass\n")
|
||||
require.Equal(t, true, ok)
|
||||
defer p.close()
|
||||
|
||||
cnt1, err := newContainer("ffmpeg", "source", []string{
|
||||
"-re",
|
||||
"-stream_loop", "-1",
|
||||
"-i", "emptyvideo.ts",
|
||||
"-c", "copy",
|
||||
"-f", "flv",
|
||||
"rtmp://" + ownDockerIP + "/teststream?user=testuser&pass=testpass",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer cnt1.close()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
cnt2, err := newContainer("ffmpeg", "dest", []string{
|
||||
"-i", "rtmp://" + ownDockerIP + "/teststream",
|
||||
"-vframes", "1",
|
||||
"-f", "image2",
|
||||
"-y", "/dev/null",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer cnt2.close()
|
||||
require.Equal(t, 0, cnt2.wait())
|
||||
})
|
||||
|
||||
t.Run("read", func(t *testing.T) {
|
||||
p, ok := testProgram("rtmpEnable: yes\n" +
|
||||
"paths:\n" +
|
||||
" all:\n" +
|
||||
" readUser: testuser\n" +
|
||||
" readPass: testpass\n")
|
||||
require.Equal(t, true, ok)
|
||||
defer p.close()
|
||||
|
||||
cnt1, err := newContainer("ffmpeg", "source", []string{
|
||||
"-re",
|
||||
"-stream_loop", "-1",
|
||||
"-i", "emptyvideo.ts",
|
||||
"-c", "copy",
|
||||
"-f", "flv",
|
||||
"rtmp://" + ownDockerIP + "/teststream",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer cnt1.close()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
cnt2, err := newContainer("ffmpeg", "dest", []string{
|
||||
"-i", "rtmp://" + ownDockerIP + "/teststream?user=testuser&pass=testpass",
|
||||
"-vframes", "1",
|
||||
"-f", "image2",
|
||||
"-y", "/dev/null",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer cnt2.close()
|
||||
require.Equal(t, 0, cnt2.wait())
|
||||
})
|
||||
}
|
||||
|
||||
func TestRTMPAuthFail(t *testing.T) {
|
||||
t.Run("publish", func(t *testing.T) {
|
||||
p, ok := testProgram("rtmpEnable: yes\n" +
|
||||
"paths:\n" +
|
||||
" all:\n" +
|
||||
" publishUser: testuser2\n" +
|
||||
" publishPass: testpass\n")
|
||||
require.Equal(t, true, ok)
|
||||
defer p.close()
|
||||
|
||||
cnt1, err := newContainer("ffmpeg", "source", []string{
|
||||
"-re",
|
||||
"-stream_loop", "-1",
|
||||
"-i", "emptyvideo.ts",
|
||||
"-c", "copy",
|
||||
"-f", "flv",
|
||||
"rtmp://" + ownDockerIP + "/teststream?user=testuser&pass=testpass",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer cnt1.close()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
cnt2, err := newContainer("ffmpeg", "dest", []string{
|
||||
"-i", "rtmp://" + ownDockerIP + "/teststream",
|
||||
"-vframes", "1",
|
||||
"-f", "image2",
|
||||
"-y", "/dev/null",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer cnt2.close()
|
||||
require.NotEqual(t, 0, cnt2.wait())
|
||||
})
|
||||
|
||||
t.Run("read", func(t *testing.T) {
|
||||
p, ok := testProgram("rtmpEnable: yes\n" +
|
||||
"paths:\n" +
|
||||
" all:\n" +
|
||||
" readUser: testuser2\n" +
|
||||
" readPass: testpass\n")
|
||||
require.Equal(t, true, ok)
|
||||
defer p.close()
|
||||
|
||||
cnt1, err := newContainer("ffmpeg", "source", []string{
|
||||
"-re",
|
||||
"-stream_loop", "-1",
|
||||
"-i", "emptyvideo.ts",
|
||||
"-c", "copy",
|
||||
"-f", "flv",
|
||||
"rtmp://" + ownDockerIP + "/teststream",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer cnt1.close()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
cnt2, err := newContainer("ffmpeg", "dest", []string{
|
||||
"-i", "rtmp://" + ownDockerIP + "/teststream?user=testuser&pass=testpass",
|
||||
"-vframes", "1",
|
||||
"-f", "image2",
|
||||
"-y", "/dev/null",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer cnt2.close()
|
||||
require.NotEqual(t, 0, cnt2.wait())
|
||||
})
|
||||
}
|
||||
|
||||
func TestSource(t *testing.T) {
|
||||
for _, source := range []string{
|
||||
"rtsp_udp",
|
||||
"rtsp_tcp",
|
||||
"rtsps",
|
||||
"rtmp_videoaudio",
|
||||
//"rtsp_udp",
|
||||
//"rtsp_tcp",
|
||||
//"rtsps",
|
||||
//"rtmp_videoaudio",
|
||||
"rtmp_video",
|
||||
} {
|
||||
t.Run(source, func(t *testing.T) {
|
||||
@@ -910,7 +1071,7 @@ func TestSource(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirect(t *testing.T) {
|
||||
func TestRTSPRedirect(t *testing.T) {
|
||||
p1, ok := testProgram("paths:\n" +
|
||||
" path1:\n" +
|
||||
" source: redirect\n" +
|
||||
@@ -945,7 +1106,7 @@ func TestRedirect(t *testing.T) {
|
||||
require.Equal(t, 0, cnt2.wait())
|
||||
}
|
||||
|
||||
func TestFallback(t *testing.T) {
|
||||
func TestRTSPFallback(t *testing.T) {
|
||||
for _, ca := range []string{
|
||||
"absolute",
|
||||
"relative",
|
||||
@@ -993,37 +1154,7 @@ func TestFallback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRTMP(t *testing.T) {
|
||||
p, ok := testProgram("rtmpEnable: yes\n")
|
||||
require.Equal(t, true, ok)
|
||||
defer p.close()
|
||||
|
||||
cnt1, err := newContainer("ffmpeg", "source", []string{
|
||||
"-re",
|
||||
"-stream_loop", "-1",
|
||||
"-i", "emptyvideo.ts",
|
||||
"-c", "copy",
|
||||
"-f", "flv",
|
||||
"rtmp://" + ownDockerIP + "/test1/test2",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer cnt1.close()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
cnt2, err := newContainer("ffmpeg", "dest", []string{
|
||||
"-rtsp_transport", "udp",
|
||||
"-i", "rtsp://" + ownDockerIP + ":8554/test1/test2",
|
||||
"-vframes", "1",
|
||||
"-f", "image2",
|
||||
"-y", "/dev/null",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer cnt2.close()
|
||||
require.Equal(t, 0, cnt2.wait())
|
||||
}
|
||||
|
||||
func TestRunOnDemand(t *testing.T) {
|
||||
func TestRTSPRunOnDemand(t *testing.T) {
|
||||
doneFile := filepath.Join(os.TempDir(), "ondemand_done")
|
||||
onDemandFile, err := writeTempFile([]byte(fmt.Sprintf(`#!/bin/sh
|
||||
trap 'touch %s; [ -z "$(jobs -p)" ] || kill $(jobs -p)' INT
|
||||
|
||||
Reference in New Issue
Block a user