Reply with NetStream.Play.Failed or NetStream.Publish.Unauthorized when a client is not authorized to play or publish. This makes clients like OBS to stop recreating the connection in case of authentication failures.
417 lines
8.6 KiB
Go
417 lines
8.6 KiB
Go
// Package rtmp contains a RTMP server.
|
|
package rtmp
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"reflect"
|
|
"sort"
|
|
"sync"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/bluenviron/mediamtx/internal/certloader"
|
|
"github.com/bluenviron/mediamtx/internal/conf"
|
|
"github.com/bluenviron/mediamtx/internal/defs"
|
|
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
|
"github.com/bluenviron/mediamtx/internal/logger"
|
|
"github.com/bluenviron/mediamtx/internal/packetdumper"
|
|
"github.com/bluenviron/mediamtx/internal/protocols/proxy"
|
|
"github.com/bluenviron/mediamtx/internal/restrictnetwork"
|
|
)
|
|
|
|
// ErrConnNotFound is returned when a connection is not found.
|
|
var ErrConnNotFound = errors.New("connection not found")
|
|
|
|
func interfaceIsEmpty(i any) bool {
|
|
return reflect.ValueOf(i).Kind() != reflect.Pointer || reflect.ValueOf(i).IsNil()
|
|
}
|
|
|
|
type serverAPIConnsListRes struct {
|
|
data *defs.APIRTMPConnList
|
|
err error
|
|
}
|
|
|
|
type serverAPIConnsListReq struct {
|
|
res chan serverAPIConnsListRes
|
|
}
|
|
|
|
type serverAPIConnsGetRes struct {
|
|
data *defs.APIRTMPConn
|
|
err error
|
|
}
|
|
|
|
type serverAPIConnsGetReq struct {
|
|
uuid uuid.UUID
|
|
res chan serverAPIConnsGetRes
|
|
}
|
|
|
|
type serverAPIConnsKickRes struct {
|
|
err error
|
|
}
|
|
|
|
type serverAPIConnsKickReq struct {
|
|
uuid uuid.UUID
|
|
res chan serverAPIConnsKickRes
|
|
}
|
|
|
|
type serverMetrics interface {
|
|
SetRTMPSServer(defs.APIRTMPServer)
|
|
SetRTMPServer(defs.APIRTMPServer)
|
|
}
|
|
|
|
type serverPathManager interface {
|
|
FindPathConf(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error)
|
|
AddPublisher(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error)
|
|
AddReader(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error)
|
|
}
|
|
|
|
type serverParent interface {
|
|
logger.Writer
|
|
}
|
|
|
|
// Server is a RTMP server.
|
|
type Server struct {
|
|
Address string
|
|
DumpPackets bool
|
|
ReadTimeout conf.Duration
|
|
WriteTimeout conf.Duration
|
|
Encryption bool
|
|
ServerCert string
|
|
ServerKey string
|
|
RTSPAddress string
|
|
TrustedProxies conf.IPNetworks
|
|
RunOnConnect string
|
|
RunOnConnectRestart bool
|
|
RunOnDisconnect string
|
|
ExternalCmdPool *externalcmd.Pool
|
|
Metrics serverMetrics
|
|
PathManager serverPathManager
|
|
Parent serverParent
|
|
|
|
ctx context.Context
|
|
ctxCancel func()
|
|
wg sync.WaitGroup
|
|
ln net.Listener
|
|
conns map[*conn]struct{}
|
|
loader *certloader.CertLoader
|
|
|
|
// in
|
|
chNewConn chan net.Conn
|
|
chAcceptErr chan error
|
|
chCloseConn chan *conn
|
|
chAPIConnsList chan serverAPIConnsListReq
|
|
chAPIConnsGet chan serverAPIConnsGetReq
|
|
chAPIConnsKick chan serverAPIConnsKickReq
|
|
}
|
|
|
|
// Initialize initializes the server.
|
|
func (s *Server) Initialize() error {
|
|
listen := func(network, address string) (net.Listener, error) {
|
|
ln, err := net.Listen(network, address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if s.DumpPackets {
|
|
var proto string
|
|
if s.Encryption {
|
|
proto = "rtmps"
|
|
} else {
|
|
proto = "rtmp"
|
|
}
|
|
|
|
ln = &packetdumper.Listener{
|
|
Wrapped: ln,
|
|
Prefix: proto + "_server_conn",
|
|
}
|
|
}
|
|
|
|
if len(s.TrustedProxies) > 0 {
|
|
pl := &proxy.Listener{
|
|
Wrapped: ln,
|
|
TrustedProxies: s.TrustedProxies,
|
|
}
|
|
pl.Initialize()
|
|
ln = pl
|
|
}
|
|
|
|
return ln, nil
|
|
}
|
|
|
|
tlsListen := func(network string, laddr string, config *tls.Config) (net.Listener, error) {
|
|
ln, err := listen(network, laddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if s.DumpPackets {
|
|
ln = &packetdumper.TLSListener{
|
|
Wrapped: ln,
|
|
TLSConfig: config,
|
|
}
|
|
} else {
|
|
ln = tls.NewListener(ln, config)
|
|
}
|
|
|
|
return ln, nil
|
|
}
|
|
|
|
if s.Encryption {
|
|
s.loader = &certloader.CertLoader{
|
|
CertPath: s.ServerCert,
|
|
KeyPath: s.ServerKey,
|
|
Parent: s.Parent,
|
|
}
|
|
err := s.loader.Initialize()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
net, addr := restrictnetwork.Restrict("tcp", s.Address)
|
|
s.ln, err = tlsListen(net, addr, &tls.Config{GetCertificate: s.loader.GetCertificate()})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
var err error
|
|
s.ln, err = listen(restrictnetwork.Restrict("tcp", s.Address))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
s.ctx, s.ctxCancel = context.WithCancel(context.Background())
|
|
|
|
s.conns = make(map[*conn]struct{})
|
|
s.chNewConn = make(chan net.Conn)
|
|
s.chAcceptErr = make(chan error)
|
|
s.chCloseConn = make(chan *conn)
|
|
s.chAPIConnsList = make(chan serverAPIConnsListReq)
|
|
s.chAPIConnsGet = make(chan serverAPIConnsGetReq)
|
|
s.chAPIConnsKick = make(chan serverAPIConnsKickReq)
|
|
|
|
str := "started with listener on " + s.Address
|
|
if s.Encryption {
|
|
str += " (TCP/RTMPS)"
|
|
} else {
|
|
str += " (TCP/RTMP)"
|
|
}
|
|
s.Log(logger.Info, str)
|
|
|
|
l := &listener{
|
|
ln: s.ln,
|
|
wg: &s.wg,
|
|
parent: s,
|
|
}
|
|
l.initialize()
|
|
|
|
s.wg.Add(1)
|
|
go s.run()
|
|
|
|
if !interfaceIsEmpty(s.Metrics) {
|
|
if s.Encryption {
|
|
s.Metrics.SetRTMPSServer(s)
|
|
} else {
|
|
s.Metrics.SetRTMPServer(s)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Log implements logger.Writer.
|
|
func (s *Server) Log(level logger.Level, format string, args ...any) {
|
|
label := func() string {
|
|
if s.Encryption {
|
|
return "RTMPS"
|
|
}
|
|
return "RTMP"
|
|
}()
|
|
s.Parent.Log(level, "[%s] "+format, append([]any{label}, args...)...)
|
|
}
|
|
|
|
// Close closes the server.
|
|
func (s *Server) Close() {
|
|
s.Log(logger.Info, "closing")
|
|
|
|
if !interfaceIsEmpty((s.Metrics)) {
|
|
if s.Encryption {
|
|
s.Metrics.SetRTMPSServer(nil)
|
|
} else {
|
|
s.Metrics.SetRTMPServer(nil)
|
|
}
|
|
}
|
|
|
|
s.ctxCancel()
|
|
s.wg.Wait()
|
|
|
|
if s.loader != nil {
|
|
s.loader.Close()
|
|
}
|
|
}
|
|
|
|
func (s *Server) run() {
|
|
defer s.wg.Done()
|
|
|
|
outer:
|
|
for {
|
|
select {
|
|
case err := <-s.chAcceptErr:
|
|
s.Log(logger.Error, "%s", err)
|
|
break outer
|
|
|
|
case nconn := <-s.chNewConn:
|
|
c := &conn{
|
|
parentCtx: s.ctx,
|
|
encryption: s.Encryption,
|
|
rtspAddress: s.RTSPAddress,
|
|
readTimeout: s.ReadTimeout,
|
|
writeTimeout: s.WriteTimeout,
|
|
runOnConnect: s.RunOnConnect,
|
|
runOnConnectRestart: s.RunOnConnectRestart,
|
|
runOnDisconnect: s.RunOnDisconnect,
|
|
wg: &s.wg,
|
|
nconn: nconn,
|
|
externalCmdPool: s.ExternalCmdPool,
|
|
pathManager: s.PathManager,
|
|
parent: s,
|
|
}
|
|
c.initialize()
|
|
s.conns[c] = struct{}{}
|
|
|
|
case c := <-s.chCloseConn:
|
|
delete(s.conns, c)
|
|
|
|
case req := <-s.chAPIConnsList:
|
|
data := &defs.APIRTMPConnList{
|
|
Items: []defs.APIRTMPConn{},
|
|
}
|
|
|
|
for c := range s.conns {
|
|
data.Items = append(data.Items, *c.apiItem())
|
|
}
|
|
|
|
sort.Slice(data.Items, func(i, j int) bool {
|
|
return data.Items[i].Created.Before(data.Items[j].Created)
|
|
})
|
|
|
|
req.res <- serverAPIConnsListRes{data: data}
|
|
|
|
case req := <-s.chAPIConnsGet:
|
|
c := s.findConnByUUID(req.uuid)
|
|
if c == nil {
|
|
req.res <- serverAPIConnsGetRes{err: ErrConnNotFound}
|
|
continue
|
|
}
|
|
|
|
req.res <- serverAPIConnsGetRes{data: c.apiItem()}
|
|
|
|
case req := <-s.chAPIConnsKick:
|
|
c := s.findConnByUUID(req.uuid)
|
|
if c == nil {
|
|
req.res <- serverAPIConnsKickRes{err: ErrConnNotFound}
|
|
continue
|
|
}
|
|
|
|
delete(s.conns, c)
|
|
c.Close()
|
|
req.res <- serverAPIConnsKickRes{}
|
|
|
|
case <-s.ctx.Done():
|
|
break outer
|
|
}
|
|
}
|
|
|
|
s.ctxCancel()
|
|
|
|
s.ln.Close()
|
|
}
|
|
|
|
func (s *Server) findConnByUUID(uuid uuid.UUID) *conn {
|
|
for c := range s.conns {
|
|
if c.uuid == uuid {
|
|
return c
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// newConn is called by rtmpListener.
|
|
func (s *Server) newConn(conn net.Conn) {
|
|
select {
|
|
case s.chNewConn <- conn:
|
|
case <-s.ctx.Done():
|
|
conn.Close()
|
|
}
|
|
}
|
|
|
|
// acceptError is called by rtmpListener.
|
|
func (s *Server) acceptError(err error) {
|
|
select {
|
|
case s.chAcceptErr <- err:
|
|
case <-s.ctx.Done():
|
|
}
|
|
}
|
|
|
|
// closeConn is called by conn.
|
|
func (s *Server) closeConn(c *conn) {
|
|
select {
|
|
case s.chCloseConn <- c:
|
|
case <-s.ctx.Done():
|
|
}
|
|
}
|
|
|
|
// APIConnsList implements defs.APIRTMPServer.
|
|
func (s *Server) APIConnsList() (*defs.APIRTMPConnList, error) {
|
|
req := serverAPIConnsListReq{
|
|
res: make(chan serverAPIConnsListRes),
|
|
}
|
|
|
|
select {
|
|
case s.chAPIConnsList <- req:
|
|
res := <-req.res
|
|
return res.data, res.err
|
|
|
|
case <-s.ctx.Done():
|
|
return nil, fmt.Errorf("terminated")
|
|
}
|
|
}
|
|
|
|
// APIConnsGet implements defs.APIRTMPServer.
|
|
func (s *Server) APIConnsGet(uuid uuid.UUID) (*defs.APIRTMPConn, error) {
|
|
req := serverAPIConnsGetReq{
|
|
uuid: uuid,
|
|
res: make(chan serverAPIConnsGetRes),
|
|
}
|
|
|
|
select {
|
|
case s.chAPIConnsGet <- req:
|
|
res := <-req.res
|
|
return res.data, res.err
|
|
|
|
case <-s.ctx.Done():
|
|
return nil, fmt.Errorf("terminated")
|
|
}
|
|
}
|
|
|
|
// APIConnsKick implements defs.APIRTMPServer.
|
|
func (s *Server) APIConnsKick(uuid uuid.UUID) error {
|
|
req := serverAPIConnsKickReq{
|
|
uuid: uuid,
|
|
res: make(chan serverAPIConnsKickRes),
|
|
}
|
|
|
|
select {
|
|
case s.chAPIConnsKick <- req:
|
|
res := <-req.res
|
|
return res.err
|
|
|
|
case <-s.ctx.Done():
|
|
return fmt.Errorf("terminated")
|
|
}
|
|
}
|