Files
mediamtx/internal/servers/rtmp/conn.go
T
Alessandro RosandGitHub a56408db19 add always available streams (#5335)
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.
2026-01-31 14:44:58 +01:00

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,
}
}