mediamtx/internal/servers/rtsp/session.go
Alessandro Ros 0cdae40fe3
estimate absolute timestamp more precisely (#5078)
When the absolute timestamp of incoming frames was not available, it
was filled with the current timestamp, which is influenced by latency
over time.

This mechanism is replaced by an algorithm that detects when latency is
the lowest, stores the current timestamp and uses it as reference
throughout the rest of the stream.
2025-10-12 11:02:14 +02:00

448 lines
12 KiB
Go

package rtsp
import (
"encoding/hex"
"errors"
"fmt"
"net"
"slices"
"time"
"github.com/bluenviron/gortsplib/v5"
rtspauth "github.com/bluenviron/gortsplib/v5/pkg/auth"
"github.com/bluenviron/gortsplib/v5/pkg/base"
"github.com/bluenviron/gortsplib/v5/pkg/headers"
"github.com/google/uuid"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/counterdumper"
"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/rtsp"
"github.com/bluenviron/mediamtx/internal/stream"
)
func profileLabel(p headers.TransportProfile) string {
switch p {
case headers.TransportProfileSAVP:
return "SAVP"
case headers.TransportProfileAVP:
return "AVP"
}
return "unknown"
}
type session struct {
isTLS bool
transports conf.RTSPTransports
rsession *gortsplib.ServerSession
rconn *gortsplib.ServerConn
rserver *gortsplib.Server
externalCmdPool *externalcmd.Pool
pathManager serverPathManager
parent logger.Writer
uuid uuid.UUID
created time.Time
pathConf *conf.Path // record only
path defs.Path
stream *stream.Stream
onUnreadHook func()
packetsLost *counterdumper.CounterDumper
decodeErrors *counterdumper.CounterDumper
discardedFrames *counterdumper.CounterDumper
}
func (s *session) initialize() {
s.uuid = uuid.New()
s.created = time.Now()
s.packetsLost = &counterdumper.CounterDumper{
OnReport: func(val uint64) {
s.Log(logger.Warn, "%d RTP %s lost",
val,
func() string {
if val == 1 {
return "packet"
}
return "packets"
}())
},
}
s.packetsLost.Start()
s.decodeErrors = &counterdumper.CounterDumper{
OnReport: func(val uint64) {
s.Log(logger.Warn, "%d decode %s",
val,
func() string {
if val == 1 {
return "error"
}
return "errors"
}())
},
}
s.decodeErrors.Start()
s.discardedFrames = &counterdumper.CounterDumper{
OnReport: func(val uint64) {
s.Log(logger.Warn, "reader is too slow, discarding %d %s",
val,
func() string {
if val == 1 {
return "frame"
}
return "frames"
}())
},
}
s.discardedFrames.Start()
s.Log(logger.Info, "created by %v", s.rconn.NetConn().RemoteAddr())
}
// Close closes a Session.
// this is not always called, so things that need to be released
// must go in onClose().
func (s *session) Close() {
s.rsession.Close()
}
func (s *session) remoteAddr() net.Addr {
return s.rconn.NetConn().RemoteAddr()
}
// Log implements logger.Writer.
func (s *session) Log(level logger.Level, format string, args ...interface{}) {
id := hex.EncodeToString(s.uuid[:4])
s.parent.Log(level, "[session %s] "+format, append([]interface{}{id}, args...)...)
}
// onClose is called by rtspServer.
func (s *session) onClose(err error) {
if s.rsession.State() == gortsplib.ServerSessionStatePlay {
s.onUnreadHook()
}
switch s.rsession.State() {
case gortsplib.ServerSessionStatePrePlay, gortsplib.ServerSessionStatePlay:
s.path.RemoveReader(defs.PathRemoveReaderReq{Author: s})
case gortsplib.ServerSessionStateRecord:
s.path.RemovePublisher(defs.PathRemovePublisherReq{Author: s})
}
s.path = nil
s.stream = nil
s.discardedFrames.Stop()
s.decodeErrors.Stop()
s.packetsLost.Stop()
s.Log(logger.Info, "destroyed: %v", err)
}
// onAnnounce is called by rtspServer.
func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) {
if len(ctx.Path) == 0 || ctx.Path[0] != '/' {
return &base.Response{
StatusCode: base.StatusBadRequest,
}, fmt.Errorf("invalid path")
}
ctx.Path = ctx.Path[1:]
// CustomVerifyFunc prevents hashed credentials from working.
// Use it only when strictly needed.
var customVerifyFunc func(expectedUser, expectedPass string) bool
if slices.Contains(c.authMethods, rtspauth.VerifyMethodDigestMD5) {
customVerifyFunc = func(expectedUser, expectedPass string) bool {
return c.rconn.VerifyCredentials(ctx.Request, expectedUser, expectedPass)
}
}
pathConf, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
AccessRequest: defs.PathAccessRequest{
Name: ctx.Path,
Query: ctx.Query,
Publish: true,
Proto: auth.ProtocolRTSP,
ID: &c.uuid,
Credentials: rtsp.Credentials(ctx.Request),
IP: c.ip(),
CustomVerifyFunc: customVerifyFunc,
},
})
if err != nil {
var terr *auth.Error
if errors.As(err, &terr) {
return c.handleAuthError(terr)
}
return &base.Response{
StatusCode: base.StatusBadRequest,
}, err
}
s.pathConf = pathConf
return &base.Response{
StatusCode: base.StatusOK,
}, nil
}
// onSetup is called by rtspServer.
func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx,
) (*base.Response, *gortsplib.ServerStream, error) {
if len(ctx.Path) == 0 || ctx.Path[0] != '/' {
return &base.Response{
StatusCode: base.StatusBadRequest,
}, nil, fmt.Errorf("invalid path")
}
ctx.Path = ctx.Path[1:]
// in case the client is setupping a stream with UDP or UDP-multicast, and these
// transport protocols are disabled, gortsplib already blocks the request.
// we only have to handle the case in which the transport protocol is TCP
// and it is disabled.
if ctx.Transport.Protocol == gortsplib.ProtocolTCP {
if _, ok := s.transports[gortsplib.ProtocolTCP]; !ok {
return &base.Response{
StatusCode: base.StatusUnsupportedTransport,
}, nil, nil
}
}
// CustomVerifyFunc prevents hashed credentials from working.
// Use it only when strictly needed.
var customVerifyFunc func(expectedUser, expectedPass string) bool
if slices.Contains(c.authMethods, rtspauth.VerifyMethodDigestMD5) {
customVerifyFunc = func(expectedUser, expectedPass string) bool {
return c.rconn.VerifyCredentials(ctx.Request, expectedUser, expectedPass)
}
}
switch s.rsession.State() {
case gortsplib.ServerSessionStateInitial, gortsplib.ServerSessionStatePrePlay: // play
path, stream, err := s.pathManager.AddReader(defs.PathAddReaderReq{
Author: s,
AccessRequest: defs.PathAccessRequest{
Name: ctx.Path,
Query: ctx.Query,
Proto: auth.ProtocolRTSP,
ID: &c.uuid,
Credentials: rtsp.Credentials(ctx.Request),
IP: c.ip(),
CustomVerifyFunc: customVerifyFunc,
},
})
if err != nil {
var terr *auth.Error
if errors.As(err, &terr) {
res, err2 := c.handleAuthError(terr)
return res, nil, err2
}
var terr2 defs.PathNoStreamAvailableError
if errors.As(err, &terr2) {
return &base.Response{
StatusCode: base.StatusNotFound,
}, nil, err
}
return &base.Response{
StatusCode: base.StatusBadRequest,
}, nil, err
}
s.path = path
s.stream = stream
var rstream *gortsplib.ServerStream
if !s.isTLS {
rstream = stream.RTSPStream(s.rserver)
} else {
rstream = stream.RTSPSStream(s.rserver)
}
return &base.Response{
StatusCode: base.StatusOK,
}, rstream, nil
default: // record
return &base.Response{
StatusCode: base.StatusOK,
}, nil, nil
}
}
// onPlay is called by rtspServer.
func (s *session) onPlay(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
h := make(base.Header)
if s.rsession.State() == gortsplib.ServerSessionStatePrePlay {
s.Log(logger.Info, "is reading from path '%s', with %s, %s",
s.path.Name(),
s.rsession.Transport().Protocol,
defs.MediasInfo(s.rsession.Medias()))
s.onUnreadHook = hooks.OnRead(hooks.OnReadParams{
Logger: s,
ExternalCmdPool: s.externalCmdPool,
Conf: s.path.SafeConf(),
ExternalCmdEnv: s.path.ExternalCmdEnv(),
Reader: s.APIReaderDescribe(),
Query: s.rsession.Query(),
})
}
return &base.Response{
StatusCode: base.StatusOK,
Header: h,
}, nil
}
// onRecord is called by rtspServer.
func (s *session) onRecord(_ *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) {
path, stream, err := s.pathManager.AddPublisher(defs.PathAddPublisherReq{
Author: s,
Desc: s.rsession.AnnouncedDescription(),
GenerateRTPPackets: false,
FillNTP: !s.pathConf.UseAbsoluteTimestamp,
ConfToCompare: s.pathConf,
AccessRequest: defs.PathAccessRequest{
Name: s.rsession.Path()[1:],
Query: s.rsession.Query(),
Publish: true,
SkipAuth: true,
},
})
if err != nil {
return &base.Response{
StatusCode: base.StatusBadRequest,
}, err
}
s.path = path
s.stream = stream
rtsp.ToStream(
s.rsession,
s.rsession.AnnouncedDescription().Medias,
s.path.SafeConf(),
stream,
s)
return &base.Response{
StatusCode: base.StatusOK,
}, nil
}
// onPause is called by rtspServer.
func (s *session) onPause(_ *gortsplib.ServerHandlerOnPauseCtx) (*base.Response, error) {
switch s.rsession.State() {
case gortsplib.ServerSessionStatePlay:
s.onUnreadHook()
case gortsplib.ServerSessionStateRecord:
s.path.RemovePublisher(defs.PathRemovePublisherReq{Author: s})
}
return &base.Response{
StatusCode: base.StatusOK,
}, nil
}
// APIReaderDescribe implements reader.
func (s *session) APIReaderDescribe() defs.APIPathSourceOrReader {
return defs.APIPathSourceOrReader{
Type: func() string {
if s.isTLS {
return "rtspsSession"
}
return "rtspSession"
}(),
ID: s.uuid.String(),
}
}
// APISourceDescribe implements source.
func (s *session) APISourceDescribe() defs.APIPathSourceOrReader {
return s.APIReaderDescribe()
}
// onPacketLost is called by rtspServer.
func (s *session) onPacketsLost(ctx *gortsplib.ServerHandlerOnPacketsLostCtx) {
s.packetsLost.Add(ctx.Lost)
}
// onDecodeError is called by rtspServer.
func (s *session) onDecodeError(_ *gortsplib.ServerHandlerOnDecodeErrorCtx) {
s.decodeErrors.Increase()
}
// onStreamWriteError is called by rtspServer.
func (s *session) onStreamWriteError(_ *gortsplib.ServerHandlerOnStreamWriteErrorCtx) {
// currently the only error returned by OnStreamWriteError is ErrServerWriteQueueFull
s.discardedFrames.Increase()
}
func (s *session) apiItem() *defs.APIRTSPSession {
stats := s.rsession.Stats()
return &defs.APIRTSPSession{
ID: s.uuid,
Created: s.created,
RemoteAddr: s.remoteAddr().String(),
State: func() defs.APIRTSPSessionState {
state := s.rsession.State()
switch state {
case gortsplib.ServerSessionStatePlay:
return defs.APIRTSPSessionStateRead
case gortsplib.ServerSessionStateRecord:
return defs.APIRTSPSessionStatePublish
default:
return defs.APIRTSPSessionStateIdle
}
}(),
Path: func() string {
pa := s.rsession.Path()
if len(pa) >= 1 {
return pa[1:]
}
return ""
}(),
Query: s.rsession.Query(),
Transport: func() *string {
transport := s.rsession.Transport()
if transport == nil {
return nil
}
v := transport.Protocol.String()
return &v
}(),
Profile: func() *string {
transport := s.rsession.Transport()
if transport == nil {
return nil
}
v := profileLabel(transport.Profile)
return &v
}(),
BytesReceived: stats.BytesReceived,
BytesSent: stats.BytesSent,
RTPPacketsReceived: stats.RTPPacketsReceived,
RTPPacketsSent: stats.RTPPacketsSent,
RTPPacketsLost: stats.RTPPacketsLost,
RTPPacketsInError: stats.RTPPacketsInError,
RTPPacketsJitter: stats.RTPPacketsJitter,
RTCPPacketsReceived: stats.RTCPPacketsReceived,
RTCPPacketsSent: stats.RTCPPacketsSent,
RTCPPacketsInError: stats.RTCPPacketsInError,
}
}