Browse Source

Support reading with RTMP (#218)

pull/235/head
aler9 4 years ago
parent
commit
186a91800a
  1. 14
      README.md
  2. 7
      go.mod
  3. 10
      go.sum
  4. 45
      internal/client/client.go
  5. 12
      internal/clientman/clientman.go
  6. 487
      internal/clientrtmp/client.go
  7. 30
      internal/clientrtsp/client.go
  8. 143
      internal/path/path.go
  9. 4
      internal/path/readersmap.go
  10. 82
      internal/pathman/pathman.go
  11. 52
      internal/rtmputils/conn.go
  12. 13
      internal/rtmputils/connpair.go
  13. 53
      internal/rtmputils/metadata.go
  14. 10
      internal/serverrtmp/server.go
  15. 70
      internal/sourcertmp/source.go
  16. 4
      main.go
  17. 287
      main_test.go

14
README.md

@ -9,14 +9,14 @@ @@ -9,14 +9,14 @@
[![Release](https://img.shields.io/github/v/release/aler9/rtsp-simple-server)](https://github.com/aler9/rtsp-simple-server/releases)
[![Docker Hub](https://img.shields.io/badge/docker-aler9/rtsp--simple--server-blue)](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: @@ -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: @@ -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

7
go.mod

@ -5,14 +5,17 @@ go 1.15 @@ -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

10
go.sum

@ -2,8 +2,10 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo @@ -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 @@ -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=

45
internal/client/client.go

@ -1,6 +1,8 @@ @@ -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 ( @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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)
}

12
internal/clientman/clientman.go

@ -34,6 +34,8 @@ type Parent interface { @@ -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 { @@ -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( @@ -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() { @@ -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: @@ -162,6 +168,8 @@ outer:
c := clientrtmp.New(
cm.rtspPort,
cm.readTimeout,
cm.writeTimeout,
cm.readBufferCount,
cm.runOnConnect,
cm.runOnConnectRestart,
&cm.wg,

487
internal/clientrtmp/client.go

@ -14,6 +14,7 @@ import ( @@ -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 { @@ -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 { @@ -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 { @@ -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() {} @@ -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,9 +156,44 @@ func (c *Client) run() { @@ -135,9 +156,44 @@ 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
@ -145,122 +201,285 @@ func (c *Client) run() { @@ -145,122 +201,285 @@ func (c *Client) run() {
}
var videoTrack *gortsplib.Track
var h264SPS []byte
var h264PPS []byte
var audioTrack *gortsplib.Track
var err error
var tracks gortsplib.Tracks
var h264Encoder *rtph264.Encoder
var aacEncoder *rtpaac.Encoder
var aacConfig []byte
metadataDone := make(chan struct{})
go func() {
defer close(metadataDone)
err = func() error {
videoTrack, audioTrack, err = rtmputils.Metadata(c.conn, c.readTimeout)
if err != nil {
return err
}
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
if videoTrack != nil {
var err error
h264Encoder, err = rtph264.NewEncoder(96)
h264SPS, h264PPS, err = t.ExtractDataH264()
if err != nil {
return err
}
tracks = append(tracks, videoTrack)
}
if audioTrack != nil {
clockRate, _ := audioTrack.ClockRate()
} 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
aacEncoder, err = rtpaac.NewEncoder(96, clockRate)
aacConfig, err = t.ExtractDataAAC()
if err != nil {
return err
}
tracks = append(tracks, audioTrack)
}
}
for i, t := range tracks {
t.ID = i
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
}
}
}
return nil
}()
}()
select {
case <-metadataDone:
case <-c.terminate:
c.conn.NConn.Close()
<-metadataDone
}
case err := <-writerDone:
c.conn.NetConn().Close()
if err != nil {
c.conn.NConn.Close()
c.log(logger.Info, "ERR: %s", err)
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
return
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
}
}
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, "/")
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
resc := make(chan client.AnnounceRes)
c.parent.OnClientAnnounce(client.AnnounceReq{c, pathName, tracks, ur.Query(), resc}) //nolint:govet
res := <-resc
setupDone := make(chan struct{})
go func() {
defer close(setupDone)
err = func() error {
c.conn.NetConn().SetReadDeadline(time.Now().Add(c.readTimeout))
videoTrack, audioTrack, err = rtmputils.ReadMetadata(c.conn)
if err != nil {
return err
}
if res.Err != nil {
switch res.Err.(type) {
case client.ErrAuthNotCritical:
return res.Err
if videoTrack != nil {
h264Encoder = rtph264.NewEncoder(96, nil, nil, nil)
tracks = append(tracks, videoTrack)
}
case client.ErrAuthCritical:
// wait some seconds to stop brute force attacks
select {
case <-time.After(pauseAfterAuthError):
case <-c.terminate:
}
return res.Err
if audioTrack != nil {
clockRate, _ := audioTrack.ClockRate()
aacEncoder = rtpaac.NewEncoder(96, clockRate, nil, nil, nil)
tracks = append(tracks, audioTrack)
}
default:
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
}
}
c.path = res.Path
return nil
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 <-setupDone:
case <-c.terminate:
c.conn.NetConn().Close()
<-setupDone
}
if err != nil {
c.conn.NetConn().Close()
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() { @@ -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() { @@ -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 _, f := range frames {
rtcpSenders.ProcessFrame(videoTrack.ID, time.Now(), gortsplib.StreamTypeRTP, f)
c.path.OnFrame(videoTrack.ID, gortsplib.StreamTypeRTP, f)
for _, nalu := range nalus {
frames, err := h264Encoder.Encode(&rtph264.NALUAndTimestamp{
Timestamp: pkt.Time + pkt.CTime,
NALU: nalu,
})
if err != nil {
return err
}
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() { @@ -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() { @@ -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, @@ -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
}
}
}
}

30
internal/clientrtsp/client.go

@ -27,16 +27,6 @@ const ( @@ -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( @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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
}

143
internal/path/path.go

@ -12,7 +12,6 @@ import ( @@ -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 { @@ -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 { @@ -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 { @@ -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( @@ -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: @@ -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: @@ -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: @@ -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: @@ -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,17 +354,17 @@ func (pa *Path) exhaustChannels() { @@ -367,17 +354,17 @@ func (pa *Path) exhaustChannels() {
}
req.Res <- client.DescribeRes{nil, "", fmt.Errorf("terminated")} //nolint:govet
case req, ok := <-pa.clientAnnounce:
case req, ok := <-pa.clientSetupPlay:
if !ok {
return
}
req.Res <- client.AnnounceRes{nil, fmt.Errorf("terminated")} //nolint:govet
req.Res <- client.SetupPlayRes{nil, nil, fmt.Errorf("terminated")} //nolint:govet
case req, ok := <-pa.clientSetupPlay:
case req, ok := <-pa.clientAnnounce:
if !ok {
return
}
req.Res <- client.SetupPlayRes{nil, fmt.Errorf("terminated")} //nolint:govet
req.Res <- client.AnnounceRes{nil, fmt.Errorf("terminated")} //nolint:govet
case req, ok := <-pa.clientPlay:
if !ok {
@ -508,7 +495,7 @@ func (pa *Path) onSourceSetReady() { @@ -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) { @@ -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) { @@ -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,17 +606,31 @@ func (pa *Path) onClientDescribe(req client.DescribeReq) { @@ -619,17 +606,31 @@ 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
func (pa *Path) onClientSetupPlay(req client.SetupPlayReq) {
pa.fixedPublisherStart()
pa.scheduleClose()
switch pa.sourceState {
case sourceStateReady:
pa.onClientSetupPlayPost(req)
return
case sourceStateWaitingDescribe:
pa.setupPlayRequests = append(pa.setupPlayRequests, req)
return
case sourceStateNotReady:
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 {
@ -646,26 +647,7 @@ func (pa *Path) onClientSetupPlayPost(req client.SetupPlayReq) { @@ -646,26 +647,7 @@ func (pa *Path) onClientSetupPlayPost(req client.SetupPlayReq) {
pa.addClient(req.Client, clientStatePrePlay)
}
req.Res <- client.SetupPlayRes{pa, nil} //nolint:govet
}
func (pa *Path) onClientSetupPlay(req client.SetupPlayReq) {
pa.fixedPublisherStart()
pa.scheduleClose()
switch pa.sourceState {
case sourceStateReady:
pa.onClientSetupPlayPost(req)
return
case sourceStateWaitingDescribe:
pa.setupPlayRequests = append(pa.setupPlayRequests, req)
return
case sourceStateNotReady:
req.Res <- client.SetupPlayRes{nil, clientrtsp.ErrNoOnePublishing{pa.name}} //nolint:govet
return
}
req.Res <- client.SetupPlayRes{pa, pa.sourceTracks, nil} //nolint:govet
}
func (pa *Path) onClientPlay(c client.Client) {
@ -680,17 +662,18 @@ func (pa *Path) onClientPlay(c client.Client) { @@ -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 @@ -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) { @@ -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) { @@ -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 { @@ -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{}{}
}

4
internal/path/readersmap.go

@ -7,7 +7,7 @@ import ( @@ -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, @@ -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)
}
}

82
internal/pathman/pathman.go

@ -38,8 +38,8 @@ type PathManager struct { @@ -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( @@ -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: @@ -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,70 +169,62 @@ outer: @@ -165,70 +169,62 @@ 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:
case req := <-pm.clientSetupPlay:
pathName, pathConf, err := pm.findPathConf(req.PathName)
if err != nil {
req.Res <- client.AnnounceRes{nil, err} //nolint:govet
req.Res <- client.SetupPlayRes{nil, nil, err} //nolint:govet
continue
}
err = req.Client.Authenticate(pm.authMethods, req.PathName,
pathConf.PublishIpsParsed, pathConf.PublishUser,
pathConf.PublishPass, req.Req)
err = req.Client.Authenticate(
pm.authMethods,
req.PathName,
pathConf.ReadIpsParsed,
pathConf.ReadUser,
pathConf.ReadPass,
req.Data)
if err != nil {
req.Res <- client.AnnounceRes{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 {
pm.paths[req.PathName] = pm.createPath(pathName, pathConf, req.PathName)
pm.createPath(pathName, pathConf, req.PathName)
}
pm.paths[req.PathName].OnPathManAnnounce(req)
pm.paths[req.PathName].OnPathManSetupPlay(req)
case req := <-pm.clientSetupPlay:
case req := <-pm.clientAnnounce:
pathName, pathConf, err := pm.findPathConf(req.PathName)
if err != nil {
req.Res <- client.SetupPlayRes{nil, err} //nolint:govet
req.Res <- client.AnnounceRes{nil, err} //nolint:govet
continue
}
err = req.Client.Authenticate(
pm.authMethods,
req.PathName,
pathConf.ReadIpsParsed,
pathConf.ReadUser,
pathConf.ReadPass,
req.Req)
pathConf.PublishIpsParsed,
pathConf.PublishUser,
pathConf.PublishPass,
req.Data)
if err != nil {
req.Res <- client.SetupPlayRes{nil, err} //nolint:govet
req.Res <- client.AnnounceRes{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)
pm.paths[req.PathName].OnPathManAnnounce(req)
case <-pm.terminate:
break outer
@ -254,17 +250,17 @@ outer: @@ -254,17 +250,17 @@ outer:
}
req.Res <- client.DescribeRes{nil, "", fmt.Errorf("terminated")} //nolint:govet
case req, ok := <-pm.clientAnnounce:
case req, ok := <-pm.clientSetupPlay:
if !ok {
return
}
req.Res <- client.AnnounceRes{nil, fmt.Errorf("terminated")} //nolint:govet
req.Res <- client.SetupPlayRes{nil, nil, fmt.Errorf("terminated")} //nolint:govet
case req, ok := <-pm.clientSetupPlay:
case req, ok := <-pm.clientAnnounce:
if !ok {
return
}
req.Res <- client.SetupPlayRes{nil, fmt.Errorf("terminated")} //nolint:govet
req.Res <- client.AnnounceRes{nil, fmt.Errorf("terminated")} //nolint:govet
}
}
}()
@ -278,12 +274,12 @@ outer: @@ -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 @@ -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)
}
}
}

52
internal/rtmputils/conn.go

@ -0,0 +1,52 @@ @@ -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()
}

13
internal/rtmputils/connpair.go

@ -1,13 +0,0 @@ @@ -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
}

53
internal/rtmputils/metadata.go

@ -2,13 +2,11 @@ package rtmputils @@ -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 ( @@ -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) { @@ -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) ( @@ -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) ( @@ -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
}(),
},
}),
})
}

10
internal/serverrtmp/server.go

@ -22,7 +22,7 @@ type Server struct { @@ -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( @@ -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( @@ -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() { @@ -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
}

70
internal/sourcertmp/source.go

@ -112,7 +112,7 @@ func (s *Source) run() { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 _, f := range frames {
rtcpSenders.ProcessFrame(videoTrack.ID, time.Now(), gortsplib.StreamTypeRTP, f)
s.parent.OnFrame(videoTrack.ID, gortsplib.StreamTypeRTP, f)
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 _, 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 { @@ -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 { @@ -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
}

4
main.go

@ -257,6 +257,8 @@ func (p *program) createResources(initial bool) error { @@ -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) { @@ -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) {

287
main_test.go

@ -185,7 +185,7 @@ y++U32uuSFiXDcSLarfIsE992MEJLSAynbF1Rsgsr3gXbGiuToJRyxbIeVy7gwzD @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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

Loading…
Cancel
Save