14 changed files with 955 additions and 869 deletions
@ -0,0 +1,393 @@
@@ -0,0 +1,393 @@
|
||||
package sessionrtsp |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"net" |
||||
"strconv" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/pkg/base" |
||||
"github.com/aler9/gortsplib/pkg/headers" |
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/clientrtsp" |
||||
"github.com/aler9/rtsp-simple-server/internal/externalcmd" |
||||
"github.com/aler9/rtsp-simple-server/internal/logger" |
||||
"github.com/aler9/rtsp-simple-server/internal/readpublisher" |
||||
"github.com/aler9/rtsp-simple-server/internal/streamproc" |
||||
) |
||||
|
||||
const ( |
||||
pauseAfterAuthError = 2 * time.Second |
||||
) |
||||
|
||||
var errTerminated = errors.New("terminated") |
||||
|
||||
// PathMan is implemented by pathman.PathMan.
|
||||
type PathMan interface { |
||||
OnReadPublisherSetupPlay(readpublisher.SetupPlayReq) |
||||
OnReadPublisherAnnounce(readpublisher.AnnounceReq) |
||||
} |
||||
|
||||
// Parent is implemented by serverrtsp.Server.
|
||||
type Parent interface { |
||||
Log(logger.Level, string, ...interface{}) |
||||
} |
||||
|
||||
// Session is a RTSP session.
|
||||
type Session struct { |
||||
rtspAddress string |
||||
protocols map[gortsplib.StreamProtocol]struct{} |
||||
ss *gortsplib.ServerSession |
||||
pathMan PathMan |
||||
parent Parent |
||||
|
||||
path readpublisher.Path |
||||
setuppedTracks map[int]*gortsplib.Track // read
|
||||
onReadCmd *externalcmd.Cmd // read
|
||||
sp *streamproc.StreamProc // publish
|
||||
onPublishCmd *externalcmd.Cmd // publish
|
||||
} |
||||
|
||||
// New allocates a Session.
|
||||
func New( |
||||
rtspAddress string, |
||||
protocols map[gortsplib.StreamProtocol]struct{}, |
||||
ss *gortsplib.ServerSession, |
||||
pathMan PathMan, |
||||
parent Parent) *Session { |
||||
|
||||
s := &Session{ |
||||
rtspAddress: rtspAddress, |
||||
protocols: protocols, |
||||
ss: ss, |
||||
pathMan: pathMan, |
||||
parent: parent, |
||||
} |
||||
|
||||
s.log(logger.Info, "created") |
||||
|
||||
return s |
||||
} |
||||
|
||||
// Close closes a Session.
|
||||
func (s *Session) Close() { |
||||
s.log(logger.Info, "destroyed") |
||||
|
||||
switch s.ss.State() { |
||||
case gortsplib.ServerSessionStatePlay: |
||||
if s.onReadCmd != nil { |
||||
s.onReadCmd.Close() |
||||
} |
||||
|
||||
case gortsplib.ServerSessionStateRecord: |
||||
if s.onPublishCmd != nil { |
||||
s.onPublishCmd.Close() |
||||
} |
||||
} |
||||
|
||||
if s.path != nil { |
||||
res := make(chan struct{}) |
||||
s.path.OnReadPublisherRemove(readpublisher.RemoveReq{s, res}) //nolint:govet
|
||||
<-res |
||||
s.path = nil |
||||
} |
||||
} |
||||
|
||||
// RequestClose closes a Session.
|
||||
func (s *Session) RequestClose() { |
||||
s.ss.Close() |
||||
} |
||||
|
||||
// IsReadPublisher implements readpublisher.ReadPublisher.
|
||||
func (s *Session) IsReadPublisher() {} |
||||
|
||||
// IsSource implements source.Source.
|
||||
func (s *Session) IsSource() {} |
||||
|
||||
func (s *Session) log(level logger.Level, format string, args ...interface{}) { |
||||
s.parent.Log(level, "[session %s] "+format, append([]interface{}{"TODO"}, args...)...) |
||||
} |
||||
|
||||
// OnAnnounce is called by serverrtsp.Server.
|
||||
func (s *Session) OnAnnounce(c *clientrtsp.Client, ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) { |
||||
resc := make(chan readpublisher.AnnounceRes) |
||||
s.pathMan.OnReadPublisherAnnounce(readpublisher.AnnounceReq{ |
||||
Author: s, |
||||
PathName: ctx.Path, |
||||
Tracks: ctx.Tracks, |
||||
IP: ctx.Conn.NetConn().RemoteAddr().(*net.TCPAddr).IP, |
||||
ValidateCredentials: func(authMethods []headers.AuthMethod, pathUser string, pathPass string) error { |
||||
return c.ValidateCredentials(authMethods, pathUser, pathPass, ctx.Path, ctx.Req) |
||||
}, |
||||
Res: resc, |
||||
}) |
||||
res := <-resc |
||||
|
||||
if res.Err != nil { |
||||
switch terr := res.Err.(type) { |
||||
case readpublisher.ErrAuthNotCritical: |
||||
return terr.Response, nil |
||||
|
||||
case readpublisher.ErrAuthCritical: |
||||
s.log(logger.Info, "ERR: %v", terr.Message) |
||||
|
||||
// wait some seconds to stop brute force attacks
|
||||
<-time.After(pauseAfterAuthError) |
||||
return terr.Response, errTerminated |
||||
|
||||
default: |
||||
return &base.Response{ |
||||
StatusCode: base.StatusBadRequest, |
||||
}, res.Err |
||||
} |
||||
} |
||||
|
||||
s.path = res.Path |
||||
|
||||
return &base.Response{ |
||||
StatusCode: base.StatusOK, |
||||
}, nil |
||||
} |
||||
|
||||
// OnSetup is called by serverrtsp.Server.
|
||||
func (s *Session) OnSetup(c *clientrtsp.Client, ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, error) { |
||||
if ctx.Transport.Protocol == gortsplib.StreamProtocolUDP { |
||||
if _, ok := s.protocols[gortsplib.StreamProtocolUDP]; !ok { |
||||
return &base.Response{ |
||||
StatusCode: base.StatusUnsupportedTransport, |
||||
}, nil |
||||
} |
||||
} else { |
||||
if _, ok := s.protocols[gortsplib.StreamProtocolTCP]; !ok { |
||||
return &base.Response{ |
||||
StatusCode: base.StatusUnsupportedTransport, |
||||
}, nil |
||||
} |
||||
} |
||||
|
||||
switch s.ss.State() { |
||||
case gortsplib.ServerSessionStateInitial, gortsplib.ServerSessionStatePrePlay: // play
|
||||
resc := make(chan readpublisher.SetupPlayRes) |
||||
s.pathMan.OnReadPublisherSetupPlay(readpublisher.SetupPlayReq{ |
||||
Author: s, |
||||
PathName: ctx.Path, |
||||
IP: ctx.Conn.NetConn().RemoteAddr().(*net.TCPAddr).IP, |
||||
ValidateCredentials: func(authMethods []headers.AuthMethod, pathUser string, pathPass string) error { |
||||
return c.ValidateCredentials(authMethods, pathUser, pathPass, ctx.Path, ctx.Req) |
||||
}, |
||||
Res: resc, |
||||
}) |
||||
res := <-resc |
||||
|
||||
if res.Err != nil { |
||||
switch terr := res.Err.(type) { |
||||
case readpublisher.ErrAuthNotCritical: |
||||
return terr.Response, nil |
||||
|
||||
case readpublisher.ErrAuthCritical: |
||||
s.log(logger.Info, "ERR: %v", terr.Message) |
||||
|
||||
// wait some seconds to stop brute force attacks
|
||||
<-time.After(pauseAfterAuthError) |
||||
return terr.Response, errTerminated |
||||
|
||||
case readpublisher.ErrNoOnePublishing: |
||||
return &base.Response{ |
||||
StatusCode: base.StatusNotFound, |
||||
}, res.Err |
||||
|
||||
default: |
||||
return &base.Response{ |
||||
StatusCode: base.StatusBadRequest, |
||||
}, res.Err |
||||
} |
||||
} |
||||
|
||||
s.path = res.Path |
||||
|
||||
if ctx.TrackID >= len(res.Tracks) { |
||||
return &base.Response{ |
||||
StatusCode: base.StatusBadRequest, |
||||
}, fmt.Errorf("track %d does not exist", ctx.TrackID) |
||||
} |
||||
|
||||
if s.setuppedTracks == nil { |
||||
s.setuppedTracks = make(map[int]*gortsplib.Track) |
||||
} |
||||
s.setuppedTracks[ctx.TrackID] = res.Tracks[ctx.TrackID] |
||||
} |
||||
|
||||
return &base.Response{ |
||||
StatusCode: base.StatusOK, |
||||
}, nil |
||||
} |
||||
|
||||
// OnPlay is called by serverrtsp.Server.
|
||||
func (s *Session) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { |
||||
h := make(base.Header) |
||||
|
||||
if s.ss.State() == gortsplib.ServerSessionStatePrePlay { |
||||
if ctx.Path != s.path.Name() { |
||||
return &base.Response{ |
||||
StatusCode: base.StatusBadRequest, |
||||
}, fmt.Errorf("path has changed, was '%s', now is '%s'", s.path.Name(), ctx.Path) |
||||
} |
||||
|
||||
resc := make(chan readpublisher.PlayRes) |
||||
s.path.OnReadPublisherPlay(readpublisher.PlayReq{s, resc}) //nolint:govet
|
||||
res := <-resc |
||||
|
||||
tracksLen := len(s.ss.SetuppedTracks()) |
||||
|
||||
s.log(logger.Info, "is reading from path '%s', %d %s with %s", |
||||
s.path.Name(), |
||||
tracksLen, |
||||
func() string { |
||||
if tracksLen == 1 { |
||||
return "track" |
||||
} |
||||
return "tracks" |
||||
}(), |
||||
*s.ss.StreamProtocol()) |
||||
|
||||
if s.path.Conf().RunOnRead != "" { |
||||
_, port, _ := net.SplitHostPort(s.rtspAddress) |
||||
s.onReadCmd = externalcmd.New(s.path.Conf().RunOnRead, s.path.Conf().RunOnReadRestart, externalcmd.Environment{ |
||||
Path: s.path.Name(), |
||||
Port: port, |
||||
}) |
||||
} |
||||
|
||||
// add RTP-Info
|
||||
var ri headers.RTPInfo |
||||
for trackID, ti := range res.TrackInfos { |
||||
if ti.LastTimeNTP == 0 { |
||||
continue |
||||
} |
||||
|
||||
track, ok := s.setuppedTracks[trackID] |
||||
if !ok { |
||||
continue |
||||
} |
||||
|
||||
u := &base.URL{ |
||||
Scheme: ctx.Req.URL.Scheme, |
||||
User: ctx.Req.URL.User, |
||||
Host: ctx.Req.URL.Host, |
||||
Path: "/" + s.path.Name() + "/trackID=" + strconv.FormatInt(int64(trackID), 10), |
||||
} |
||||
|
||||
clockRate, _ := track.ClockRate() |
||||
ts := uint32(uint64(ti.LastTimeRTP) + |
||||
uint64(time.Since(time.Unix(ti.LastTimeNTP, 0)).Seconds()*float64(clockRate))) |
||||
lsn := ti.LastSequenceNumber |
||||
|
||||
ri = append(ri, &headers.RTPInfoEntry{ |
||||
URL: u.String(), |
||||
SequenceNumber: &lsn, |
||||
Timestamp: &ts, |
||||
}) |
||||
} |
||||
if len(ri) > 0 { |
||||
h["RTP-Info"] = ri.Write() |
||||
} |
||||
} |
||||
|
||||
return &base.Response{ |
||||
StatusCode: base.StatusOK, |
||||
Header: h, |
||||
}, nil |
||||
} |
||||
|
||||
// OnRecord is called by serverrtsp.Server.
|
||||
func (s *Session) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) { |
||||
if ctx.Path != s.path.Name() { |
||||
return &base.Response{ |
||||
StatusCode: base.StatusBadRequest, |
||||
}, fmt.Errorf("path has changed, was '%s', now is '%s'", s.path.Name(), ctx.Path) |
||||
} |
||||
|
||||
resc := make(chan readpublisher.RecordRes) |
||||
s.path.OnReadPublisherRecord(readpublisher.RecordReq{Author: s, Res: resc}) |
||||
res := <-resc |
||||
|
||||
if res.Err != nil { |
||||
return &base.Response{ |
||||
StatusCode: base.StatusBadRequest, |
||||
}, res.Err |
||||
} |
||||
|
||||
s.sp = res.SP |
||||
|
||||
tracksLen := len(s.ss.AnnouncedTracks()) |
||||
|
||||
s.log(logger.Info, "is publishing to path '%s', %d %s with %s", |
||||
s.path.Name(), |
||||
tracksLen, |
||||
func() string { |
||||
if tracksLen == 1 { |
||||
return "track" |
||||
} |
||||
return "tracks" |
||||
}(), |
||||
*s.ss.StreamProtocol()) |
||||
|
||||
if s.path.Conf().RunOnPublish != "" { |
||||
_, port, _ := net.SplitHostPort(s.rtspAddress) |
||||
s.onPublishCmd = externalcmd.New(s.path.Conf().RunOnPublish, s.path.Conf().RunOnPublishRestart, externalcmd.Environment{ |
||||
Path: s.path.Name(), |
||||
Port: port, |
||||
}) |
||||
} |
||||
|
||||
return &base.Response{ |
||||
StatusCode: base.StatusOK, |
||||
}, nil |
||||
} |
||||
|
||||
// OnPause is called by serverrtsp.Server.
|
||||
func (s *Session) OnPause(ctx *gortsplib.ServerHandlerOnPauseCtx) (*base.Response, error) { |
||||
switch s.ss.State() { |
||||
case gortsplib.ServerSessionStatePlay: |
||||
if s.onReadCmd != nil { |
||||
s.onReadCmd.Close() |
||||
} |
||||
|
||||
res := make(chan struct{}) |
||||
s.path.OnReadPublisherPause(readpublisher.PauseReq{s, res}) //nolint:govet
|
||||
<-res |
||||
|
||||
case gortsplib.ServerSessionStateRecord: |
||||
if s.onPublishCmd != nil { |
||||
s.onPublishCmd.Close() |
||||
} |
||||
|
||||
res := make(chan struct{}) |
||||
s.path.OnReadPublisherPause(readpublisher.PauseReq{s, res}) //nolint:govet
|
||||
<-res |
||||
} |
||||
|
||||
return &base.Response{ |
||||
StatusCode: base.StatusOK, |
||||
}, nil |
||||
} |
||||
|
||||
// OnFrame implements path.Reader.
|
||||
func (s *Session) OnFrame(trackID int, streamType gortsplib.StreamType, payload []byte) { |
||||
if _, ok := s.ss.SetuppedTracks()[trackID]; !ok { |
||||
return |
||||
} |
||||
|
||||
s.ss.WriteFrame(trackID, streamType, payload) |
||||
} |
||||
|
||||
// OnIncomingFrame is called by serverrtsp.Server.
|
||||
func (s *Session) OnIncomingFrame(ctx *gortsplib.ServerHandlerOnFrameCtx) { |
||||
if s.ss.State() != gortsplib.ServerSessionStateRecord { |
||||
return |
||||
} |
||||
|
||||
s.sp.OnFrame(ctx.TrackID, ctx.StreamType, ctx.Payload) |
||||
} |
Loading…
Reference in new issue