When the publisher or source of a stream is offline, the server can be configured to fill gaps in the stream with a video that is played on repeat until a publisher comes back online. This allows readers to stay connected regardless of the state of the stream. The offline video and any future online stream are concatenated without decoding or re-encoding packets, using the original codec.
336 lines
7.0 KiB
Go
336 lines
7.0 KiB
Go
package rtmp
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/bluenviron/gortmplib"
|
|
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/bluenviron/mediamtx/internal/auth"
|
|
"github.com/bluenviron/mediamtx/internal/conf"
|
|
"github.com/bluenviron/mediamtx/internal/defs"
|
|
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
|
"github.com/bluenviron/mediamtx/internal/hooks"
|
|
"github.com/bluenviron/mediamtx/internal/logger"
|
|
"github.com/bluenviron/mediamtx/internal/protocols/rtmp"
|
|
"github.com/bluenviron/mediamtx/internal/stream"
|
|
)
|
|
|
|
type conn struct {
|
|
parentCtx context.Context
|
|
isTLS bool
|
|
rtspAddress string
|
|
readTimeout conf.Duration
|
|
writeTimeout conf.Duration
|
|
runOnConnect string
|
|
runOnConnectRestart bool
|
|
runOnDisconnect string
|
|
wg *sync.WaitGroup
|
|
nconn net.Conn
|
|
externalCmdPool *externalcmd.Pool
|
|
pathManager serverPathManager
|
|
parent *Server
|
|
|
|
ctx context.Context
|
|
ctxCancel func()
|
|
uuid uuid.UUID
|
|
created time.Time
|
|
mutex sync.RWMutex
|
|
rconn *gortmplib.ServerConn
|
|
state defs.APIRTMPConnState
|
|
pathName string
|
|
query string
|
|
}
|
|
|
|
func (c *conn) initialize() {
|
|
c.ctx, c.ctxCancel = context.WithCancel(c.parentCtx)
|
|
|
|
c.uuid = uuid.New()
|
|
c.created = time.Now()
|
|
c.state = defs.APIRTMPConnStateIdle
|
|
|
|
c.Log(logger.Info, "opened")
|
|
|
|
c.wg.Add(1)
|
|
go c.run()
|
|
}
|
|
|
|
func (c *conn) Close() {
|
|
c.ctxCancel()
|
|
}
|
|
|
|
func (c *conn) remoteAddr() net.Addr {
|
|
return c.nconn.RemoteAddr()
|
|
}
|
|
|
|
// Log implements logger.Writer.
|
|
func (c *conn) Log(level logger.Level, format string, args ...any) {
|
|
c.parent.Log(level, "[conn %v] "+format, append([]any{c.nconn.RemoteAddr()}, args...)...)
|
|
}
|
|
|
|
func (c *conn) ip() net.IP {
|
|
return c.nconn.RemoteAddr().(*net.TCPAddr).IP
|
|
}
|
|
|
|
func (c *conn) run() { //nolint:dupl
|
|
defer c.wg.Done()
|
|
|
|
onDisconnectHook := hooks.OnConnect(hooks.OnConnectParams{
|
|
Logger: c,
|
|
ExternalCmdPool: c.externalCmdPool,
|
|
RunOnConnect: c.runOnConnect,
|
|
RunOnConnectRestart: c.runOnConnectRestart,
|
|
RunOnDisconnect: c.runOnDisconnect,
|
|
RTSPAddress: c.rtspAddress,
|
|
Desc: *c.APIReaderDescribe(),
|
|
})
|
|
defer onDisconnectHook()
|
|
|
|
err := c.runInner()
|
|
|
|
c.ctxCancel()
|
|
|
|
c.parent.closeConn(c)
|
|
|
|
c.Log(logger.Info, "closed: %v", err)
|
|
}
|
|
|
|
func (c *conn) runInner() error {
|
|
readerErr := make(chan error)
|
|
go func() {
|
|
readerErr <- c.runReader()
|
|
}()
|
|
|
|
select {
|
|
case err := <-readerErr:
|
|
c.nconn.Close()
|
|
return err
|
|
|
|
case <-c.ctx.Done():
|
|
c.nconn.Close()
|
|
<-readerErr
|
|
return errors.New("terminated")
|
|
}
|
|
}
|
|
|
|
func (c *conn) runReader() error {
|
|
c.nconn.SetReadDeadline(time.Now().Add(time.Duration(c.readTimeout)))
|
|
c.nconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout)))
|
|
|
|
conn := &gortmplib.ServerConn{
|
|
RW: c.nconn,
|
|
}
|
|
err := conn.Initialize()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = conn.Accept()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.mutex.Lock()
|
|
c.rconn = conn
|
|
c.mutex.Unlock()
|
|
|
|
if !conn.Publish {
|
|
return c.runRead()
|
|
}
|
|
return c.runPublish()
|
|
}
|
|
|
|
func (c *conn) runRead() error {
|
|
pathName := strings.TrimLeft(c.rconn.URL.Path, "/")
|
|
query := c.rconn.URL.Query()
|
|
|
|
path, strm, err := c.pathManager.AddReader(defs.PathAddReaderReq{
|
|
Author: c,
|
|
AccessRequest: defs.PathAccessRequest{
|
|
Name: pathName,
|
|
Query: c.rconn.URL.RawQuery,
|
|
Proto: auth.ProtocolRTMP,
|
|
ID: &c.uuid,
|
|
Credentials: &auth.Credentials{
|
|
User: query.Get("user"),
|
|
Pass: query.Get("pass"),
|
|
},
|
|
IP: c.ip(),
|
|
},
|
|
})
|
|
if err != nil {
|
|
var terr *auth.Error
|
|
if errors.As(err, &terr) {
|
|
// wait some seconds to delay brute force attacks
|
|
<-time.After(auth.PauseAfterError)
|
|
return terr
|
|
}
|
|
return err
|
|
}
|
|
|
|
defer path.RemoveReader(defs.PathRemoveReaderReq{Author: c})
|
|
|
|
c.mutex.Lock()
|
|
c.state = defs.APIRTMPConnStateRead
|
|
c.pathName = pathName
|
|
c.query = c.rconn.URL.RawQuery
|
|
c.mutex.Unlock()
|
|
|
|
r := &stream.Reader{Parent: c}
|
|
|
|
err = rtmp.FromStream(strm.Desc, r, c.rconn, c.nconn, time.Duration(c.writeTimeout))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.Log(logger.Info, "is reading from path '%s', %s",
|
|
path.Name(), defs.FormatsInfo(r.Formats()))
|
|
|
|
onUnreadHook := hooks.OnRead(hooks.OnReadParams{
|
|
Logger: c,
|
|
ExternalCmdPool: c.externalCmdPool,
|
|
Conf: path.SafeConf(),
|
|
ExternalCmdEnv: path.ExternalCmdEnv(),
|
|
Reader: *c.APIReaderDescribe(),
|
|
Query: c.rconn.URL.RawQuery,
|
|
})
|
|
defer onUnreadHook()
|
|
|
|
c.nconn.SetReadDeadline(time.Time{})
|
|
|
|
strm.AddReader(r)
|
|
defer strm.RemoveReader(r)
|
|
|
|
select {
|
|
case <-c.ctx.Done():
|
|
return fmt.Errorf("terminated")
|
|
|
|
case err = <-r.Error():
|
|
return err
|
|
}
|
|
}
|
|
|
|
func (c *conn) runPublish() error {
|
|
pathName := strings.TrimLeft(c.rconn.URL.Path, "/")
|
|
query := c.rconn.URL.Query()
|
|
|
|
r := &gortmplib.Reader{
|
|
Conn: c.rconn,
|
|
}
|
|
err := r.Initialize()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var subStream *stream.SubStream
|
|
|
|
medias, err := rtmp.ToStream(r, &subStream)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var path defs.Path
|
|
path, subStream, err = c.pathManager.AddPublisher(defs.PathAddPublisherReq{
|
|
Author: c,
|
|
Desc: &description.Session{Medias: medias},
|
|
UseRTPPackets: false,
|
|
ReplaceNTP: true,
|
|
AccessRequest: defs.PathAccessRequest{
|
|
Name: pathName,
|
|
Query: c.rconn.URL.RawQuery,
|
|
Publish: true,
|
|
Proto: auth.ProtocolRTMP,
|
|
ID: &c.uuid,
|
|
Credentials: &auth.Credentials{
|
|
User: query.Get("user"),
|
|
Pass: query.Get("pass"),
|
|
},
|
|
IP: c.ip(),
|
|
},
|
|
})
|
|
if err != nil {
|
|
var terr *auth.Error
|
|
if errors.As(err, &terr) {
|
|
// wait some seconds to delay brute force attacks
|
|
<-time.After(auth.PauseAfterError)
|
|
return terr
|
|
}
|
|
return err
|
|
}
|
|
|
|
defer path.RemovePublisher(defs.PathRemovePublisherReq{Author: c})
|
|
|
|
c.mutex.Lock()
|
|
c.state = defs.APIRTMPConnStatePublish
|
|
c.pathName = pathName
|
|
c.query = c.rconn.URL.RawQuery
|
|
c.mutex.Unlock()
|
|
|
|
c.nconn.SetWriteDeadline(time.Time{})
|
|
|
|
for {
|
|
c.nconn.SetReadDeadline(time.Now().Add(time.Duration(c.readTimeout)))
|
|
err = r.Read()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// APIReaderDescribe implements reader.
|
|
func (c *conn) APIReaderDescribe() *defs.APIPathReader {
|
|
return &defs.APIPathReader{
|
|
Type: func() string {
|
|
if c.isTLS {
|
|
return "rtmpsConn"
|
|
}
|
|
return "rtmpConn"
|
|
}(),
|
|
ID: c.uuid.String(),
|
|
}
|
|
}
|
|
|
|
// APISourceDescribe implements source.
|
|
func (c *conn) APISourceDescribe() *defs.APIPathSource {
|
|
return &defs.APIPathSource{
|
|
Type: func() string {
|
|
if c.isTLS {
|
|
return "rtmpsConn"
|
|
}
|
|
return "rtmpConn"
|
|
}(),
|
|
ID: c.uuid.String(),
|
|
}
|
|
}
|
|
|
|
func (c *conn) apiItem() *defs.APIRTMPConn {
|
|
c.mutex.RLock()
|
|
defer c.mutex.RUnlock()
|
|
|
|
bytesReceived := uint64(0)
|
|
bytesSent := uint64(0)
|
|
|
|
if c.rconn != nil {
|
|
bytesReceived = c.rconn.BytesReceived()
|
|
bytesSent = c.rconn.BytesSent()
|
|
}
|
|
|
|
return &defs.APIRTMPConn{
|
|
ID: c.uuid,
|
|
Created: c.created,
|
|
RemoteAddr: c.remoteAddr().String(),
|
|
State: c.state,
|
|
Path: c.pathName,
|
|
Query: c.query,
|
|
BytesReceived: bytesReceived,
|
|
BytesSent: bytesSent,
|
|
}
|
|
}
|