From 11988249df97289c6358c11ff9c98fc830a90271 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Fri, 8 Dec 2023 19:17:17 +0100 Subject: [PATCH] move servers into internal/servers (#2792) --- .github/workflows/bump_hls_js.yml | 4 +- README.md | 4 +- internal/core/api.go | 133 ++--- internal/core/{authentication.go => auth.go} | 72 ++- internal/core/core.go | 315 ++++++------ internal/core/hls_manager.go | 309 ------------ ...hls_manager_test.go => hls_server_test.go} | 2 +- internal/core/metrics.go | 51 +- internal/core/metrics_test.go | 16 +- internal/core/path.go | 363 +++++--------- internal/core/path_manager.go | 166 +++---- internal/core/path_manager_test.go | 3 + internal/core/path_test.go | 5 +- internal/core/pprof.go | 1 + internal/core/publisher.go | 7 - internal/core/reader.go | 17 - internal/core/rtmp_listener.go | 48 -- internal/core/rtmp_server.go | 327 ------------- internal/core/rtsp_server.go | 454 ------------------ internal/core/srt_server.go | 330 ------------- internal/core/static_source_handler.go | 3 +- ..._manager_test.go => webrtc_server_test.go} | 0 internal/defs/auth.go | 23 + internal/defs/path.go | 137 +++++- internal/defs/path_manager.go | 9 + internal/defs/publisher.go | 7 + internal/defs/reader.go | 7 + internal/{core => defs}/source.go | 17 +- .../mpegts/from_stream.go} | 10 +- internal/record/agent.go | 2 +- internal/record/cleaner.go | 2 +- internal/{core => servers/hls}/hls.min.js | 0 .../hls/http_server.go} | 99 ++-- .../hls_index.html => servers/hls/index.html} | 0 .../hls_muxer.go => servers/hls/muxer.go} | 145 +++--- internal/servers/hls/server.go | 290 +++++++++++ .../rtmp_conn.go => servers/rtmp/conn.go} | 198 +++----- internal/servers/rtmp/listener.go | 36 ++ internal/servers/rtmp/server.go | 308 ++++++++++++ .../rtsp_conn.go => servers/rtsp/conn.go} | 133 +++-- internal/servers/rtsp/server.go | 430 +++++++++++++++++ .../rtsp/session.go} | 230 ++++----- .../{core/srt_conn.go => servers/srt/conn.go} | 212 ++++---- .../srt/listener.go} | 28 +- internal/servers/srt/server.go | 310 ++++++++++++ .../webrtc/http_server.go} | 150 +++--- .../webrtc/publish_index.html} | 0 .../webrtc/read_index.html} | 0 .../webrtc/server.go} | 327 ++++++------- .../webrtc/session.go} | 199 ++++---- internal/staticsources/hls/source.go | 2 +- internal/staticsources/hls/source_test.go | 16 +- internal/staticsources/rpicamera/source.go | 2 +- internal/staticsources/rtmp/source.go | 2 +- internal/staticsources/rtsp/source.go | 2 +- internal/staticsources/srt/source.go | 4 +- internal/staticsources/udp/source.go | 2 +- internal/staticsources/webrtc/source.go | 2 +- 58 files changed, 2856 insertions(+), 3115 deletions(-) rename internal/core/{authentication.go => auth.go} (60%) delete mode 100644 internal/core/hls_manager.go rename internal/core/{hls_manager_test.go => hls_server_test.go} (99%) delete mode 100644 internal/core/publisher.go delete mode 100644 internal/core/reader.go delete mode 100644 internal/core/rtmp_listener.go delete mode 100644 internal/core/rtmp_server.go delete mode 100644 internal/core/rtsp_server.go delete mode 100644 internal/core/srt_server.go rename internal/core/{webrtc_manager_test.go => webrtc_server_test.go} (100%) create mode 100644 internal/defs/auth.go create mode 100644 internal/defs/path_manager.go create mode 100644 internal/defs/publisher.go create mode 100644 internal/defs/reader.go rename internal/{core => defs}/source.go (63%) rename internal/{core/mpegts.go => protocols/mpegts/from_stream.go} (97%) rename internal/{core => servers/hls}/hls.min.js (100%) rename internal/{core/hls_http_server.go => servers/hls/http_server.go} (65%) rename internal/{core/hls_index.html => servers/hls/index.html} (100%) rename internal/{core/hls_muxer.go => servers/hls/muxer.go} (74%) create mode 100644 internal/servers/hls/server.go rename internal/{core/rtmp_conn.go => servers/rtmp/conn.go} (74%) create mode 100644 internal/servers/rtmp/listener.go create mode 100644 internal/servers/rtmp/server.go rename internal/{core/rtsp_conn.go => servers/rtsp/conn.go} (58%) create mode 100644 internal/servers/rtsp/server.go rename internal/{core/rtsp_session.go => servers/rtsp/session.go} (57%) rename internal/{core/srt_conn.go => servers/srt/conn.go} (60%) rename internal/{core/srt_listener.go => servers/srt/listener.go} (59%) create mode 100644 internal/servers/srt/server.go rename internal/{core/webrtc_http_server.go => servers/webrtc/http_server.go} (65%) rename internal/{core/webrtc_publish_index.html => servers/webrtc/publish_index.html} (100%) rename internal/{core/webrtc_read_index.html => servers/webrtc/read_index.html} (100%) rename internal/{core/webrtc_manager.go => servers/webrtc/server.go} (52%) rename internal/{core/webrtc_session.go => servers/webrtc/session.go} (76%) diff --git a/.github/workflows/bump_hls_js.yml b/.github/workflows/bump_hls_js.yml index aed1c5f5..cd78035b 100644 --- a/.github/workflows/bump_hls_js.yml +++ b/.github/workflows/bump_hls_js.yml @@ -20,8 +20,8 @@ jobs: && ((git checkout deps/hlsjs && git rebase ${GITHUB_REF_NAME}) || git checkout -b deps/hlsjs) - run: > - curl -o internal/core/hls.min.js https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js - && echo VERSION=$(cat internal/core/hls.min.js | grep -o '"version",get:function(){return".*"}' | sed 's/"version",get:function(){return"\(.*\)"}/\1/') >> $GITHUB_ENV + curl -o internal/servers/hls/hls.min.js https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js + && echo VERSION=$(cat internal/servers/hls/hls.min.js | grep -o '"version",get:function(){return".*"}' | sed 's/"version",get:function(){return"\(.*\)"}/\1/') >> $GITHUB_ENV - id: check_repo run: > diff --git a/README.md b/README.md index bc1f47b4..a7e9c2ba 100644 --- a/README.md +++ b/README.md @@ -435,7 +435,7 @@ This web page can be embedded into another web page by using an iframe: ``` -For more advanced setups, you can create and serve a custom web page by starting from the [source code of the publish page](internal/core/webrtc_publish_index.html). +For more advanced setups, you can create and serve a custom web page by starting from the [source code of the publish page](internal/core/servers/webrtc/publish_index.html). ### By device @@ -823,7 +823,7 @@ This web page can be embedded into another web page by using an iframe: ``` -For more advanced setups, you can create and serve a custom web page by starting from the [source code of the read page](internal/core/webrtc_read_index.html). +For more advanced setups, you can create and serve a custom web page by starting from the [source code of the read page](internal/core/servers/webrtc/read_index.html). Web browsers can also read a stream with the [HLS protocol](#hls). Latency is higher but there are less problems related to connectivity between server and clients, furthermore the server load can be balanced by using a common HTTP CDN (like CloudFront or Cloudflare), and this allows to handle readers in the order of millions. Visit the web page: diff --git a/internal/core/api.go b/internal/core/api.go index 51e7a1c7..ddbc2dac 100644 --- a/internal/core/api.go +++ b/internal/core/api.go @@ -102,35 +102,35 @@ type apiPathManager interface { apiPathsGet(string) (*defs.APIPath, error) } -type apiHLSManager interface { - apiMuxersList() (*defs.APIHLSMuxerList, error) - apiMuxersGet(string) (*defs.APIHLSMuxer, error) +type apiHLSServer interface { + APIMuxersList() (*defs.APIHLSMuxerList, error) + APIMuxersGet(string) (*defs.APIHLSMuxer, error) } type apiRTSPServer interface { - apiConnsList() (*defs.APIRTSPConnsList, error) - apiConnsGet(uuid.UUID) (*defs.APIRTSPConn, error) - apiSessionsList() (*defs.APIRTSPSessionList, error) - apiSessionsGet(uuid.UUID) (*defs.APIRTSPSession, error) - apiSessionsKick(uuid.UUID) error + APIConnsList() (*defs.APIRTSPConnsList, error) + APIConnsGet(uuid.UUID) (*defs.APIRTSPConn, error) + APISessionsList() (*defs.APIRTSPSessionList, error) + APISessionsGet(uuid.UUID) (*defs.APIRTSPSession, error) + APISessionsKick(uuid.UUID) error } type apiRTMPServer interface { - apiConnsList() (*defs.APIRTMPConnList, error) - apiConnsGet(uuid.UUID) (*defs.APIRTMPConn, error) - apiConnsKick(uuid.UUID) error + APIConnsList() (*defs.APIRTMPConnList, error) + APIConnsGet(uuid.UUID) (*defs.APIRTMPConn, error) + APIConnsKick(uuid.UUID) error } -type apiWebRTCManager interface { - apiSessionsList() (*defs.APIWebRTCSessionList, error) - apiSessionsGet(uuid.UUID) (*defs.APIWebRTCSession, error) - apiSessionsKick(uuid.UUID) error +type apiSRTServer interface { + APIConnsList() (*defs.APISRTConnList, error) + APIConnsGet(uuid.UUID) (*defs.APISRTConn, error) + APIConnsKick(uuid.UUID) error } -type apiSRTServer interface { - apiConnsList() (*defs.APISRTConnList, error) - apiConnsGet(uuid.UUID) (*defs.APISRTConn, error) - apiConnsKick(uuid.UUID) error +type apiWebRTCServer interface { + APISessionsList() (*defs.APIWebRTCSessionList, error) + APISessionsGet(uuid.UUID) (*defs.APIWebRTCSession, error) + APISessionsKick(uuid.UUID) error } type apiParent interface { @@ -139,16 +139,16 @@ type apiParent interface { } type api struct { - conf *conf.Conf - pathManager apiPathManager - rtspServer apiRTSPServer - rtspsServer apiRTSPServer - rtmpServer apiRTMPServer - rtmpsServer apiRTMPServer - hlsManager apiHLSManager - webRTCManager apiWebRTCManager - srtServer apiSRTServer - parent apiParent + conf *conf.Conf + pathManager apiPathManager + rtspServer apiRTSPServer + rtspsServer apiRTSPServer + rtmpServer apiRTMPServer + rtmpsServer apiRTMPServer + hlsManager apiHLSServer + webRTCServer apiWebRTCServer + srtServer apiSRTServer + parent apiParent httpServer *httpserv.WrappedServer mutex sync.Mutex @@ -163,22 +163,22 @@ func newAPI( rtspsServer apiRTSPServer, rtmpServer apiRTMPServer, rtmpsServer apiRTMPServer, - hlsManager apiHLSManager, - webRTCManager apiWebRTCManager, + hlsManager apiHLSServer, + webRTCServer apiWebRTCServer, srtServer apiSRTServer, parent apiParent, ) (*api, error) { a := &api{ - conf: conf, - pathManager: pathManager, - rtspServer: rtspServer, - rtspsServer: rtspsServer, - rtmpServer: rtmpServer, - rtmpsServer: rtmpsServer, - hlsManager: hlsManager, - webRTCManager: webRTCManager, - srtServer: srtServer, - parent: parent, + conf: conf, + pathManager: pathManager, + rtspServer: rtspServer, + rtspsServer: rtspsServer, + rtmpServer: rtmpServer, + rtmpsServer: rtmpsServer, + hlsManager: hlsManager, + webRTCServer: webRTCServer, + srtServer: srtServer, + parent: parent, } router := gin.New() @@ -235,7 +235,7 @@ func newAPI( group.POST("/v3/rtmpsconns/kick/:id", a.onRTMPSConnsKick) } - if !interfaceIsEmpty(a.webRTCManager) { + if !interfaceIsEmpty(a.webRTCServer) { group.GET("/v3/webrtcsessions/list", a.onWebRTCSessionsList) group.GET("/v3/webrtcsessions/get/:id", a.onWebRTCSessionsGet) group.POST("/v3/webrtcsessions/kick/:id", a.onWebRTCSessionsKick) @@ -273,6 +273,7 @@ func (a *api) close() { a.httpServer.Close() } +// Log implements logger.Writer. func (a *api) Log(level logger.Level, format string, args ...interface{}) { a.parent.Log(level, "[API] "+format, args...) } @@ -581,7 +582,7 @@ func (a *api) onPathsGet(ctx *gin.Context) { } func (a *api) onRTSPConnsList(ctx *gin.Context) { - data, err := a.rtspServer.apiConnsList() + data, err := a.rtspServer.APIConnsList() if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -605,7 +606,7 @@ func (a *api) onRTSPConnsGet(ctx *gin.Context) { return } - data, err := a.rtspServer.apiConnsGet(uuid) + data, err := a.rtspServer.APIConnsGet(uuid) if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -615,7 +616,7 @@ func (a *api) onRTSPConnsGet(ctx *gin.Context) { } func (a *api) onRTSPSessionsList(ctx *gin.Context) { - data, err := a.rtspServer.apiSessionsList() + data, err := a.rtspServer.APISessionsList() if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -639,7 +640,7 @@ func (a *api) onRTSPSessionsGet(ctx *gin.Context) { return } - data, err := a.rtspServer.apiSessionsGet(uuid) + data, err := a.rtspServer.APISessionsGet(uuid) if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -655,7 +656,7 @@ func (a *api) onRTSPSessionsKick(ctx *gin.Context) { return } - err = a.rtspServer.apiSessionsKick(uuid) + err = a.rtspServer.APISessionsKick(uuid) if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -665,7 +666,7 @@ func (a *api) onRTSPSessionsKick(ctx *gin.Context) { } func (a *api) onRTSPSConnsList(ctx *gin.Context) { - data, err := a.rtspsServer.apiConnsList() + data, err := a.rtspsServer.APIConnsList() if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -689,7 +690,7 @@ func (a *api) onRTSPSConnsGet(ctx *gin.Context) { return } - data, err := a.rtspsServer.apiConnsGet(uuid) + data, err := a.rtspsServer.APIConnsGet(uuid) if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -699,7 +700,7 @@ func (a *api) onRTSPSConnsGet(ctx *gin.Context) { } func (a *api) onRTSPSSessionsList(ctx *gin.Context) { - data, err := a.rtspsServer.apiSessionsList() + data, err := a.rtspsServer.APISessionsList() if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -723,7 +724,7 @@ func (a *api) onRTSPSSessionsGet(ctx *gin.Context) { return } - data, err := a.rtspsServer.apiSessionsGet(uuid) + data, err := a.rtspsServer.APISessionsGet(uuid) if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -739,7 +740,7 @@ func (a *api) onRTSPSSessionsKick(ctx *gin.Context) { return } - err = a.rtspsServer.apiSessionsKick(uuid) + err = a.rtspsServer.APISessionsKick(uuid) if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -749,7 +750,7 @@ func (a *api) onRTSPSSessionsKick(ctx *gin.Context) { } func (a *api) onRTMPConnsList(ctx *gin.Context) { - data, err := a.rtmpServer.apiConnsList() + data, err := a.rtmpServer.APIConnsList() if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -773,7 +774,7 @@ func (a *api) onRTMPConnsGet(ctx *gin.Context) { return } - data, err := a.rtmpServer.apiConnsGet(uuid) + data, err := a.rtmpServer.APIConnsGet(uuid) if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -789,7 +790,7 @@ func (a *api) onRTMPConnsKick(ctx *gin.Context) { return } - err = a.rtmpServer.apiConnsKick(uuid) + err = a.rtmpServer.APIConnsKick(uuid) if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -799,7 +800,7 @@ func (a *api) onRTMPConnsKick(ctx *gin.Context) { } func (a *api) onRTMPSConnsList(ctx *gin.Context) { - data, err := a.rtmpsServer.apiConnsList() + data, err := a.rtmpsServer.APIConnsList() if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -823,7 +824,7 @@ func (a *api) onRTMPSConnsGet(ctx *gin.Context) { return } - data, err := a.rtmpsServer.apiConnsGet(uuid) + data, err := a.rtmpsServer.APIConnsGet(uuid) if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -839,7 +840,7 @@ func (a *api) onRTMPSConnsKick(ctx *gin.Context) { return } - err = a.rtmpsServer.apiConnsKick(uuid) + err = a.rtmpsServer.APIConnsKick(uuid) if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -849,7 +850,7 @@ func (a *api) onRTMPSConnsKick(ctx *gin.Context) { } func (a *api) onHLSMuxersList(ctx *gin.Context) { - data, err := a.hlsManager.apiMuxersList() + data, err := a.hlsManager.APIMuxersList() if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -873,7 +874,7 @@ func (a *api) onHLSMuxersGet(ctx *gin.Context) { return } - data, err := a.hlsManager.apiMuxersGet(name) + data, err := a.hlsManager.APIMuxersGet(name) if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -883,7 +884,7 @@ func (a *api) onHLSMuxersGet(ctx *gin.Context) { } func (a *api) onWebRTCSessionsList(ctx *gin.Context) { - data, err := a.webRTCManager.apiSessionsList() + data, err := a.webRTCServer.APISessionsList() if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -907,7 +908,7 @@ func (a *api) onWebRTCSessionsGet(ctx *gin.Context) { return } - data, err := a.webRTCManager.apiSessionsGet(uuid) + data, err := a.webRTCServer.APISessionsGet(uuid) if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -923,7 +924,7 @@ func (a *api) onWebRTCSessionsKick(ctx *gin.Context) { return } - err = a.webRTCManager.apiSessionsKick(uuid) + err = a.webRTCServer.APISessionsKick(uuid) if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -933,7 +934,7 @@ func (a *api) onWebRTCSessionsKick(ctx *gin.Context) { } func (a *api) onSRTConnsList(ctx *gin.Context) { - data, err := a.srtServer.apiConnsList() + data, err := a.srtServer.APIConnsList() if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -957,7 +958,7 @@ func (a *api) onSRTConnsGet(ctx *gin.Context) { return } - data, err := a.srtServer.apiConnsGet(uuid) + data, err := a.srtServer.APIConnsGet(uuid) if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return @@ -973,7 +974,7 @@ func (a *api) onSRTConnsKick(ctx *gin.Context) { return } - err = a.srtServer.apiConnsKick(uuid) + err = a.srtServer.APIConnsKick(uuid) if err != nil { a.writeError(ctx, http.StatusInternalServerError, err) return diff --git a/internal/core/authentication.go b/internal/core/auth.go similarity index 60% rename from internal/core/authentication.go rename to internal/core/auth.go index aed391d0..b643c37b 100644 --- a/internal/core/authentication.go +++ b/internal/core/auth.go @@ -15,6 +15,7 @@ import ( "github.com/google/uuid" "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" ) func sha256Base64(in string) string { @@ -31,28 +32,9 @@ func checkCredential(right string, guess string) bool { return right == guess } -type errAuthentication struct { - message string -} - -// Error implements the error interface. -func (e *errAuthentication) Error() string { - return "authentication failed: " + e.message -} - -type authProtocol string - -const ( - authProtocolRTSP authProtocol = "rtsp" - authProtocolRTMP authProtocol = "rtmp" - authProtocolHLS authProtocol = "hls" - authProtocolWebRTC authProtocol = "webrtc" - authProtocolSRT authProtocol = "srt" -) - func doExternalAuthentication( ur string, - accessRequest pathAccessRequest, + accessRequest defs.PathAccessRequest, ) error { enc, _ := json.Marshal(struct { IP string `json:"ip"` @@ -64,19 +46,19 @@ func doExternalAuthentication( Action string `json:"action"` Query string `json:"query"` }{ - IP: accessRequest.ip.String(), - User: accessRequest.user, - Password: accessRequest.pass, - Path: accessRequest.name, - Protocol: string(accessRequest.proto), - ID: accessRequest.id, + IP: accessRequest.IP.String(), + User: accessRequest.User, + Password: accessRequest.Pass, + Path: accessRequest.Name, + Protocol: string(accessRequest.Proto), + ID: accessRequest.ID, Action: func() string { - if accessRequest.publish { + if accessRequest.Publish { return "publish" } return "read" }(), - Query: accessRequest.query, + Query: accessRequest.Query, }) res, err := http.Post(ur, "application/json", bytes.NewReader(enc)) if err != nil { @@ -98,14 +80,14 @@ func doAuthentication( externalAuthenticationURL string, rtspAuthMethods conf.AuthMethods, pathConf *conf.Path, - accessRequest pathAccessRequest, + accessRequest defs.PathAccessRequest, ) error { var rtspAuth headers.Authorization - if accessRequest.rtspRequest != nil { - err := rtspAuth.Unmarshal(accessRequest.rtspRequest.Header["Authorization"]) + if accessRequest.RTSPRequest != nil { + err := rtspAuth.Unmarshal(accessRequest.RTSPRequest.Header["Authorization"]) if err == nil && rtspAuth.Method == headers.AuthBasic { - accessRequest.user = rtspAuth.BasicUser - accessRequest.pass = rtspAuth.BasicPass + accessRequest.User = rtspAuth.BasicUser + accessRequest.Pass = rtspAuth.BasicPass } } @@ -115,7 +97,7 @@ func doAuthentication( accessRequest, ) if err != nil { - return &errAuthentication{message: fmt.Sprintf("external authentication failed: %s", err)} + return &defs.ErrAuthentication{Message: fmt.Sprintf("external authentication failed: %s", err)} } } @@ -123,7 +105,7 @@ func doAuthentication( var pathUser string var pathPass string - if accessRequest.publish { + if accessRequest.Publish { pathIPs = pathConf.PublishIPs pathUser = string(pathConf.PublishUser) pathPass = string(pathConf.PublishPass) @@ -134,27 +116,27 @@ func doAuthentication( } if pathIPs != nil { - if !ipEqualOrInRange(accessRequest.ip, pathIPs) { - return &errAuthentication{message: fmt.Sprintf("IP %s not allowed", accessRequest.ip)} + if !ipEqualOrInRange(accessRequest.IP, pathIPs) { + return &defs.ErrAuthentication{Message: fmt.Sprintf("IP %s not allowed", accessRequest.IP)} } } if pathUser != "" { - if accessRequest.rtspRequest != nil && rtspAuth.Method == headers.AuthDigest { + if accessRequest.RTSPRequest != nil && rtspAuth.Method == headers.AuthDigest { err := auth.Validate( - accessRequest.rtspRequest, + accessRequest.RTSPRequest, pathUser, pathPass, - accessRequest.rtspBaseURL, + accessRequest.RTSPBaseURL, rtspAuthMethods, "IPCAM", - accessRequest.rtspNonce) + accessRequest.RTSPNonce) if err != nil { - return &errAuthentication{message: err.Error()} + return &defs.ErrAuthentication{Message: err.Error()} } - } else if !checkCredential(pathUser, accessRequest.user) || - !checkCredential(pathPass, accessRequest.pass) { - return &errAuthentication{message: "invalid credentials"} + } else if !checkCredential(pathUser, accessRequest.User) || + !checkCredential(pathPass, accessRequest.Pass) { + return &defs.ErrAuthentication{Message: "invalid credentials"} } } diff --git a/internal/core/core.go b/internal/core/core.go index 5eb87347..69bf1d6f 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -22,6 +22,11 @@ import ( "github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/record" "github.com/bluenviron/mediamtx/internal/rlimit" + "github.com/bluenviron/mediamtx/internal/servers/hls" + "github.com/bluenviron/mediamtx/internal/servers/rtmp" + "github.com/bluenviron/mediamtx/internal/servers/rtsp" + "github.com/bluenviron/mediamtx/internal/servers/srt" + "github.com/bluenviron/mediamtx/internal/servers/webrtc" ) var version = "v0.0.0" @@ -83,13 +88,13 @@ type Core struct { pprof *pprof recordCleaner *record.Cleaner pathManager *pathManager - rtspServer *rtspServer - rtspsServer *rtspServer - rtmpServer *rtmpServer - rtmpsServer *rtmpServer - hlsManager *hlsManager - webRTCManager *webRTCManager - srtServer *srtServer + rtspServer *rtsp.Server + rtspsServer *rtsp.Server + rtmpServer *rtmp.Server + rtmpsServer *rtmp.Server + hlsServer *hls.Server + webRTCServer *webrtc.Server + srtServer *srt.Server api *api confWatcher *confwatcher.ConfWatcher @@ -168,7 +173,7 @@ func (p *Core) Wait() { <-p.done } -// Log is the main logging function. +// Log implements logger.Writer. func (p *Core) Log(level logger.Level, format string, args ...interface{}) { p.logger.Log(level, format, args...) } @@ -329,31 +334,32 @@ func (p *Core) createResources(initial bool) error { _, useUDP := p.conf.Protocols[conf.Protocol(gortsplib.TransportUDP)] _, useMulticast := p.conf.Protocols[conf.Protocol(gortsplib.TransportUDPMulticast)] - p.rtspServer, err = newRTSPServer( - p.conf.RTSPAddress, - p.conf.AuthMethods, - p.conf.ReadTimeout, - p.conf.WriteTimeout, - p.conf.WriteQueueSize, - useUDP, - useMulticast, - p.conf.RTPAddress, - p.conf.RTCPAddress, - p.conf.MulticastIPRange, - p.conf.MulticastRTPPort, - p.conf.MulticastRTCPPort, - false, - "", - "", - p.conf.RTSPAddress, - p.conf.Protocols, - p.conf.RunOnConnect, - p.conf.RunOnConnectRestart, - p.conf.RunOnDisconnect, - p.externalCmdPool, - p.pathManager, - p, - ) + p.rtspServer = &rtsp.Server{ + Address: p.conf.RTSPAddress, + AuthMethods: p.conf.AuthMethods, + ReadTimeout: p.conf.ReadTimeout, + WriteTimeout: p.conf.WriteTimeout, + WriteQueueSize: p.conf.WriteQueueSize, + UseUDP: useUDP, + UseMulticast: useMulticast, + RTPAddress: p.conf.RTPAddress, + RTCPAddress: p.conf.RTCPAddress, + MulticastIPRange: p.conf.MulticastIPRange, + MulticastRTPPort: p.conf.MulticastRTPPort, + MulticastRTCPPort: p.conf.MulticastRTCPPort, + IsTLS: false, + ServerCert: "", + ServerKey: "", + RTSPAddress: p.conf.RTSPAddress, + Protocols: p.conf.Protocols, + RunOnConnect: p.conf.RunOnConnect, + RunOnConnectRestart: p.conf.RunOnConnectRestart, + RunOnDisconnect: p.conf.RunOnDisconnect, + ExternalCmdPool: p.externalCmdPool, + PathManager: p.pathManager, + Parent: p, + } + err := p.rtspServer.Initialize() if err != nil { return err } @@ -367,31 +373,32 @@ func (p *Core) createResources(initial bool) error { (p.conf.Encryption == conf.EncryptionStrict || p.conf.Encryption == conf.EncryptionOptional) && p.rtspsServer == nil { - p.rtspsServer, err = newRTSPServer( - p.conf.RTSPSAddress, - p.conf.AuthMethods, - p.conf.ReadTimeout, - p.conf.WriteTimeout, - p.conf.WriteQueueSize, - false, - false, - "", - "", - "", - 0, - 0, - true, - p.conf.ServerCert, - p.conf.ServerKey, - p.conf.RTSPAddress, - p.conf.Protocols, - p.conf.RunOnConnect, - p.conf.RunOnConnectRestart, - p.conf.RunOnDisconnect, - p.externalCmdPool, - p.pathManager, - p, - ) + p.rtspsServer = &rtsp.Server{ + Address: p.conf.RTSPSAddress, + AuthMethods: p.conf.AuthMethods, + ReadTimeout: p.conf.ReadTimeout, + WriteTimeout: p.conf.WriteTimeout, + WriteQueueSize: p.conf.WriteQueueSize, + UseUDP: false, + UseMulticast: false, + RTPAddress: "", + RTCPAddress: "", + MulticastIPRange: "", + MulticastRTPPort: 0, + MulticastRTCPPort: 0, + IsTLS: true, + ServerCert: p.conf.ServerCert, + ServerKey: p.conf.ServerKey, + RTSPAddress: p.conf.RTSPAddress, + Protocols: p.conf.Protocols, + RunOnConnect: p.conf.RunOnConnect, + RunOnConnectRestart: p.conf.RunOnConnectRestart, + RunOnDisconnect: p.conf.RunOnDisconnect, + ExternalCmdPool: p.externalCmdPool, + PathManager: p.pathManager, + Parent: p, + } + err := p.rtspsServer.Initialize() if err != nil { return err } @@ -405,22 +412,23 @@ func (p *Core) createResources(initial bool) error { (p.conf.RTMPEncryption == conf.EncryptionNo || p.conf.RTMPEncryption == conf.EncryptionOptional) && p.rtmpServer == nil { - p.rtmpServer, err = newRTMPServer( - p.conf.RTMPAddress, - p.conf.ReadTimeout, - p.conf.WriteTimeout, - p.conf.WriteQueueSize, - false, - "", - "", - p.conf.RTSPAddress, - p.conf.RunOnConnect, - p.conf.RunOnConnectRestart, - p.conf.RunOnDisconnect, - p.externalCmdPool, - p.pathManager, - p, - ) + p.rtmpServer = &rtmp.Server{ + Address: p.conf.RTMPAddress, + ReadTimeout: p.conf.ReadTimeout, + WriteTimeout: p.conf.WriteTimeout, + WriteQueueSize: p.conf.WriteQueueSize, + IsTLS: false, + ServerCert: "", + ServerKey: "", + RTSPAddress: p.conf.RTSPAddress, + RunOnConnect: p.conf.RunOnConnect, + RunOnConnectRestart: p.conf.RunOnConnectRestart, + RunOnDisconnect: p.conf.RunOnDisconnect, + ExternalCmdPool: p.externalCmdPool, + PathManager: p.pathManager, + Parent: p, + } + err := p.rtmpServer.Initialize() if err != nil { return err } @@ -434,22 +442,23 @@ func (p *Core) createResources(initial bool) error { (p.conf.RTMPEncryption == conf.EncryptionStrict || p.conf.RTMPEncryption == conf.EncryptionOptional) && p.rtmpsServer == nil { - p.rtmpsServer, err = newRTMPServer( - p.conf.RTMPSAddress, - p.conf.ReadTimeout, - p.conf.WriteTimeout, - p.conf.WriteQueueSize, - true, - p.conf.RTMPServerCert, - p.conf.RTMPServerKey, - p.conf.RTSPAddress, - p.conf.RunOnConnect, - p.conf.RunOnConnectRestart, - p.conf.RunOnDisconnect, - p.externalCmdPool, - p.pathManager, - p, - ) + p.rtmpsServer = &rtmp.Server{ + Address: p.conf.RTMPSAddress, + ReadTimeout: p.conf.ReadTimeout, + WriteTimeout: p.conf.WriteTimeout, + WriteQueueSize: p.conf.WriteQueueSize, + IsTLS: true, + ServerCert: p.conf.RTMPServerCert, + ServerKey: p.conf.RTMPServerKey, + RTSPAddress: p.conf.RTSPAddress, + RunOnConnect: p.conf.RunOnConnect, + RunOnConnectRestart: p.conf.RunOnConnectRestart, + RunOnDisconnect: p.conf.RunOnDisconnect, + ExternalCmdPool: p.externalCmdPool, + PathManager: p.pathManager, + Parent: p, + } + err := p.rtmpsServer.Initialize() if err != nil { return err } @@ -460,41 +469,42 @@ func (p *Core) createResources(initial bool) error { } if p.conf.HLS && - p.hlsManager == nil { - p.hlsManager, err = newHLSManager( - p.conf.HLSAddress, - p.conf.HLSEncryption, - p.conf.HLSServerKey, - p.conf.HLSServerCert, - p.conf.ExternalAuthenticationURL, - p.conf.HLSAlwaysRemux, - p.conf.HLSVariant, - p.conf.HLSSegmentCount, - p.conf.HLSSegmentDuration, - p.conf.HLSPartDuration, - p.conf.HLSSegmentMaxSize, - p.conf.HLSAllowOrigin, - p.conf.HLSTrustedProxies, - p.conf.HLSDirectory, - p.conf.ReadTimeout, - p.conf.WriteQueueSize, - p.pathManager, - p, - ) + p.hlsServer == nil { + p.hlsServer = &hls.Server{ + Address: p.conf.HLSAddress, + Encryption: p.conf.HLSEncryption, + ServerKey: p.conf.HLSServerKey, + ServerCert: p.conf.HLSServerCert, + ExternalAuthenticationURL: p.conf.ExternalAuthenticationURL, + AlwaysRemux: p.conf.HLSAlwaysRemux, + Variant: p.conf.HLSVariant, + SegmentCount: p.conf.HLSSegmentCount, + SegmentDuration: p.conf.HLSSegmentDuration, + PartDuration: p.conf.HLSPartDuration, + SegmentMaxSize: p.conf.HLSSegmentMaxSize, + AllowOrigin: p.conf.HLSAllowOrigin, + TrustedProxies: p.conf.HLSTrustedProxies, + Directory: p.conf.HLSDirectory, + ReadTimeout: p.conf.ReadTimeout, + WriteQueueSize: p.conf.WriteQueueSize, + PathManager: p.pathManager, + Parent: p, + } + err := p.hlsServer.Initialize() if err != nil { return err } - p.pathManager.setHLSManager(p.hlsManager) + p.pathManager.setHLSServer(p.hlsServer) if p.metrics != nil { - p.metrics.setHLSManager(p.hlsManager) + p.metrics.setHLSServer(p.hlsServer) } } if p.conf.WebRTC && - p.webRTCManager == nil { - p.webRTCManager = &webRTCManager{ + p.webRTCServer == nil { + p.webRTCServer = &webrtc.Server{ Address: p.conf.WebRTCAddress, Encryption: p.conf.WebRTCEncryption, ServerKey: p.conf.WebRTCServerKey, @@ -513,33 +523,34 @@ func (p *Core) createResources(initial bool) error { PathManager: p.pathManager, Parent: p, } - err = p.webRTCManager.initialize() + err = p.webRTCServer.Initialize() if err != nil { - p.webRTCManager = nil + p.webRTCServer = nil return err } if p.metrics != nil { - p.metrics.setWebRTCManager(p.webRTCManager) + p.metrics.setWebRTCServer(p.webRTCServer) } } if p.conf.SRT && p.srtServer == nil { - p.srtServer, err = newSRTServer( - p.conf.SRTAddress, - p.conf.RTSPAddress, - p.conf.ReadTimeout, - p.conf.WriteTimeout, - p.conf.WriteQueueSize, - p.conf.UDPMaxPayloadSize, - p.conf.RunOnConnect, - p.conf.RunOnConnectRestart, - p.conf.RunOnDisconnect, - p.externalCmdPool, - p.pathManager, - p, - ) + p.srtServer = &srt.Server{ + Address: p.conf.SRTAddress, + RTSPAddress: p.conf.RTSPAddress, + ReadTimeout: p.conf.ReadTimeout, + WriteTimeout: p.conf.WriteTimeout, + WriteQueueSize: p.conf.WriteQueueSize, + UDPMaxPayloadSize: p.conf.UDPMaxPayloadSize, + RunOnConnect: p.conf.RunOnConnect, + RunOnConnectRestart: p.conf.RunOnConnectRestart, + RunOnDisconnect: p.conf.RunOnDisconnect, + ExternalCmdPool: p.externalCmdPool, + PathManager: p.pathManager, + Parent: p, + } + err = p.srtServer.Initialize() if err != nil { return err } @@ -560,8 +571,8 @@ func (p *Core) createResources(initial bool) error { p.rtspsServer, p.rtmpServer, p.rtmpsServer, - p.hlsManager, - p.webRTCManager, + p.hlsServer, + p.webRTCServer, p.srtServer, p, ) @@ -690,7 +701,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { closePathManager || closeLogger - closeHLSManager := newConf == nil || + closeHLSServer := newConf == nil || newConf.HLS != p.conf.HLS || newConf.HLSAddress != p.conf.HLSAddress || newConf.HLSEncryption != p.conf.HLSEncryption || @@ -712,7 +723,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { closeMetrics || closeLogger - closeWebRTCManager := newConf == nil || + closeWebRTCServer := newConf == nil || newConf.WebRTC != p.conf.WebRTC || newConf.WebRTCAddress != p.conf.WebRTCAddress || newConf.WebRTCEncryption != p.conf.WebRTCEncryption || @@ -754,8 +765,8 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { closeRTSPServer || closeRTSPSServer || closeRTMPServer || - closeHLSManager || - closeWebRTCManager || + closeHLSServer || + closeWebRTCServer || closeSRTServer || closeLogger @@ -778,28 +789,28 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { p.metrics.setSRTServer(nil) } - p.srtServer.close() + p.srtServer.Close() p.srtServer = nil } - if closeWebRTCManager && p.webRTCManager != nil { + if closeWebRTCServer && p.webRTCServer != nil { if p.metrics != nil { - p.metrics.setWebRTCManager(nil) + p.metrics.setWebRTCServer(nil) } - p.webRTCManager.close() - p.webRTCManager = nil + p.webRTCServer.Close() + p.webRTCServer = nil } - if closeHLSManager && p.hlsManager != nil { + if closeHLSServer && p.hlsServer != nil { if p.metrics != nil { - p.metrics.setHLSManager(nil) + p.metrics.setHLSServer(nil) } - p.pathManager.setHLSManager(nil) + p.pathManager.setHLSServer(nil) - p.hlsManager.close() - p.hlsManager = nil + p.hlsServer.Close() + p.hlsServer = nil } if closeRTMPSServer && p.rtmpsServer != nil { @@ -807,7 +818,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { p.metrics.setRTMPSServer(nil) } - p.rtmpsServer.close() + p.rtmpsServer.Close() p.rtmpsServer = nil } @@ -816,7 +827,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { p.metrics.setRTMPServer(nil) } - p.rtmpServer.close() + p.rtmpServer.Close() p.rtmpServer = nil } @@ -825,7 +836,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { p.metrics.setRTSPSServer(nil) } - p.rtspsServer.close() + p.rtspsServer.Close() p.rtspsServer = nil } @@ -834,7 +845,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { p.metrics.setRTSPServer(nil) } - p.rtspServer.close() + p.rtspServer.Close() p.rtspServer = nil } diff --git a/internal/core/hls_manager.go b/internal/core/hls_manager.go deleted file mode 100644 index 7e6915f9..00000000 --- a/internal/core/hls_manager.go +++ /dev/null @@ -1,309 +0,0 @@ -package core - -import ( - "context" - "fmt" - "sort" - "sync" - - "github.com/bluenviron/mediamtx/internal/conf" - "github.com/bluenviron/mediamtx/internal/defs" - "github.com/bluenviron/mediamtx/internal/logger" -) - -type hlsManagerAPIMuxersListRes struct { - data *defs.APIHLSMuxerList - err error -} - -type hlsManagerAPIMuxersListReq struct { - res chan hlsManagerAPIMuxersListRes -} - -type hlsManagerAPIMuxersGetRes struct { - data *defs.APIHLSMuxer - err error -} - -type hlsManagerAPIMuxersGetReq struct { - name string - res chan hlsManagerAPIMuxersGetRes -} - -type hlsManagerParent interface { - logger.Writer -} - -type hlsManager struct { - externalAuthenticationURL string - alwaysRemux bool - variant conf.HLSVariant - segmentCount int - segmentDuration conf.StringDuration - partDuration conf.StringDuration - segmentMaxSize conf.StringSize - directory string - writeQueueSize int - pathManager *pathManager - parent hlsManagerParent - - ctx context.Context - ctxCancel func() - wg sync.WaitGroup - httpServer *hlsHTTPServer - muxers map[string]*hlsMuxer - - // in - chPathReady chan *path - chPathNotReady chan *path - chHandleRequest chan hlsMuxerHandleRequestReq - chCloseMuxer chan *hlsMuxer - chAPIMuxerList chan hlsManagerAPIMuxersListReq - chAPIMuxerGet chan hlsManagerAPIMuxersGetReq -} - -func newHLSManager( - address string, - encryption bool, - serverKey string, - serverCert string, - externalAuthenticationURL string, - alwaysRemux bool, - variant conf.HLSVariant, - segmentCount int, - segmentDuration conf.StringDuration, - partDuration conf.StringDuration, - segmentMaxSize conf.StringSize, - allowOrigin string, - trustedProxies conf.IPsOrCIDRs, - directory string, - readTimeout conf.StringDuration, - writeQueueSize int, - pathManager *pathManager, - parent hlsManagerParent, -) (*hlsManager, error) { - ctx, ctxCancel := context.WithCancel(context.Background()) - - m := &hlsManager{ - externalAuthenticationURL: externalAuthenticationURL, - alwaysRemux: alwaysRemux, - variant: variant, - segmentCount: segmentCount, - segmentDuration: segmentDuration, - partDuration: partDuration, - segmentMaxSize: segmentMaxSize, - directory: directory, - writeQueueSize: writeQueueSize, - pathManager: pathManager, - parent: parent, - ctx: ctx, - ctxCancel: ctxCancel, - muxers: make(map[string]*hlsMuxer), - chPathReady: make(chan *path), - chPathNotReady: make(chan *path), - chHandleRequest: make(chan hlsMuxerHandleRequestReq), - chCloseMuxer: make(chan *hlsMuxer), - chAPIMuxerList: make(chan hlsManagerAPIMuxersListReq), - chAPIMuxerGet: make(chan hlsManagerAPIMuxersGetReq), - } - - var err error - m.httpServer, err = newHLSHTTPServer( - address, - encryption, - serverKey, - serverCert, - allowOrigin, - trustedProxies, - readTimeout, - m.pathManager, - m, - ) - if err != nil { - ctxCancel() - return nil, err - } - - m.Log(logger.Info, "listener opened on "+address) - - m.wg.Add(1) - go m.run() - - return m, nil -} - -// Log is the main logging function. -func (m *hlsManager) Log(level logger.Level, format string, args ...interface{}) { - m.parent.Log(level, "[HLS] "+format, args...) -} - -func (m *hlsManager) close() { - m.Log(logger.Info, "listener is closing") - m.ctxCancel() - m.wg.Wait() -} - -func (m *hlsManager) run() { - defer m.wg.Done() - -outer: - for { - select { - case pa := <-m.chPathReady: - if m.alwaysRemux && !pa.conf.SourceOnDemand { - if _, ok := m.muxers[pa.name]; !ok { - m.createMuxer(pa.name, "") - } - } - - case pa := <-m.chPathNotReady: - c, ok := m.muxers[pa.name] - if ok && c.remoteAddr == "" { // created with "always remux" - c.close() - delete(m.muxers, pa.name) - } - - case req := <-m.chHandleRequest: - r, ok := m.muxers[req.path] - switch { - case ok: - r.processRequest(&req) - - default: - r := m.createMuxer(req.path, req.ctx.ClientIP()) - r.processRequest(&req) - } - - case c := <-m.chCloseMuxer: - if c2, ok := m.muxers[c.PathName()]; !ok || c2 != c { - continue - } - delete(m.muxers, c.PathName()) - - case req := <-m.chAPIMuxerList: - data := &defs.APIHLSMuxerList{ - Items: []*defs.APIHLSMuxer{}, - } - - for _, muxer := range m.muxers { - data.Items = append(data.Items, muxer.apiItem()) - } - - sort.Slice(data.Items, func(i, j int) bool { - return data.Items[i].Created.Before(data.Items[j].Created) - }) - - req.res <- hlsManagerAPIMuxersListRes{ - data: data, - } - - case req := <-m.chAPIMuxerGet: - muxer, ok := m.muxers[req.name] - if !ok { - req.res <- hlsManagerAPIMuxersGetRes{err: fmt.Errorf("muxer not found")} - continue - } - - req.res <- hlsManagerAPIMuxersGetRes{data: muxer.apiItem()} - - case <-m.ctx.Done(): - break outer - } - } - - m.ctxCancel() - - m.httpServer.close() -} - -func (m *hlsManager) createMuxer(pathName string, remoteAddr string) *hlsMuxer { - r := newHLSMuxer( - m.ctx, - remoteAddr, - m.externalAuthenticationURL, - m.variant, - m.segmentCount, - m.segmentDuration, - m.partDuration, - m.segmentMaxSize, - m.directory, - m.writeQueueSize, - &m.wg, - pathName, - m.pathManager, - m) - m.muxers[pathName] = r - return r -} - -// closeMuxer is called by hlsMuxer. -func (m *hlsManager) closeMuxer(c *hlsMuxer) { - select { - case m.chCloseMuxer <- c: - case <-m.ctx.Done(): - } -} - -// pathReady is called by pathManager. -func (m *hlsManager) pathReady(pa *path) { - select { - case m.chPathReady <- pa: - case <-m.ctx.Done(): - } -} - -// pathNotReady is called by pathManager. -func (m *hlsManager) pathNotReady(pa *path) { - select { - case m.chPathNotReady <- pa: - case <-m.ctx.Done(): - } -} - -// apiMuxersList is called by api. -func (m *hlsManager) apiMuxersList() (*defs.APIHLSMuxerList, error) { - req := hlsManagerAPIMuxersListReq{ - res: make(chan hlsManagerAPIMuxersListRes), - } - - select { - case m.chAPIMuxerList <- req: - res := <-req.res - return res.data, res.err - - case <-m.ctx.Done(): - return nil, fmt.Errorf("terminated") - } -} - -// apiMuxersGet is called by api. -func (m *hlsManager) apiMuxersGet(name string) (*defs.APIHLSMuxer, error) { - req := hlsManagerAPIMuxersGetReq{ - name: name, - res: make(chan hlsManagerAPIMuxersGetRes), - } - - select { - case m.chAPIMuxerGet <- req: - res := <-req.res - return res.data, res.err - - case <-m.ctx.Done(): - return nil, fmt.Errorf("terminated") - } -} - -func (m *hlsManager) handleRequest(req hlsMuxerHandleRequestReq) { - req.res = make(chan *hlsMuxer) - - select { - case m.chHandleRequest <- req: - muxer := <-req.res - if muxer != nil { - req.ctx.Request.URL.Path = req.file - muxer.handleRequest(req.ctx) - } - - case <-m.ctx.Done(): - } -} diff --git a/internal/core/hls_manager_test.go b/internal/core/hls_server_test.go similarity index 99% rename from internal/core/hls_manager_test.go rename to internal/core/hls_server_test.go index 3631e5c3..e8ea9fc0 100644 --- a/internal/core/hls_manager_test.go +++ b/internal/core/hls_server_test.go @@ -197,7 +197,7 @@ func TestHLSRead(t *testing.T) { "#EXT-X-GAP\n"+ "#EXTINF:1\\.00000,\n"+ "gap.mp4\n"+ - "#EXT-X-PROGRAM-DATE-TIME:.+?Z\n"+ + "#EXT-X-PROGRAM-DATE-TIME:.+?\n"+ "#EXT-X-PART:DURATION=1\\.00000,URI=\".*?_part0.mp4\",INDEPENDENT=YES\n"+ "#EXTINF:1\\.00000,\n"+ ".*?_seg7.mp4\n"+ diff --git a/internal/core/metrics.go b/internal/core/metrics.go index 72419121..9068ecb8 100644 --- a/internal/core/metrics.go +++ b/internal/core/metrics.go @@ -28,16 +28,16 @@ type metrics struct { ReadTimeout conf.StringDuration Parent metricsParent - httpServer *httpserv.WrappedServer - mutex sync.Mutex - pathManager apiPathManager - rtspServer apiRTSPServer - rtspsServer apiRTSPServer - rtmpServer apiRTMPServer - rtmpsServer apiRTMPServer - srtServer apiSRTServer - hlsManager apiHLSManager - webRTCManager apiWebRTCManager + httpServer *httpserv.WrappedServer + mutex sync.Mutex + pathManager apiPathManager + rtspServer apiRTSPServer + rtspsServer apiRTSPServer + rtmpServer apiRTMPServer + rtmpsServer apiRTMPServer + srtServer apiSRTServer + hlsManager apiHLSServer + webRTCServer apiWebRTCServer } func (m *metrics) initialize() error { @@ -72,6 +72,7 @@ func (m *metrics) close() { m.httpServer.Close() } +// Log implements logger.Writer. func (m *metrics) Log(level logger.Level, format string, args ...interface{}) { m.Parent.Log(level, "[metrics] "+format, args...) } @@ -99,7 +100,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) { } if !interfaceIsEmpty(m.hlsManager) { - data, err := m.hlsManager.apiMuxersList() + data, err := m.hlsManager.APIMuxersList() if err == nil && len(data.Items) != 0 { for _, i := range data.Items { tags := "{name=\"" + i.Path + "\"}" @@ -114,7 +115,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) { if !interfaceIsEmpty(m.rtspServer) { //nolint:dupl func() { - data, err := m.rtspServer.apiConnsList() + data, err := m.rtspServer.APIConnsList() if err == nil && len(data.Items) != 0 { for _, i := range data.Items { tags := "{id=\"" + i.ID.String() + "\"}" @@ -130,7 +131,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) { }() func() { - data, err := m.rtspServer.apiSessionsList() + data, err := m.rtspServer.APISessionsList() if err == nil && len(data.Items) != 0 { for _, i := range data.Items { tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}" @@ -148,7 +149,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) { if !interfaceIsEmpty(m.rtspsServer) { //nolint:dupl func() { - data, err := m.rtspsServer.apiConnsList() + data, err := m.rtspsServer.APIConnsList() if err == nil && len(data.Items) != 0 { for _, i := range data.Items { tags := "{id=\"" + i.ID.String() + "\"}" @@ -164,7 +165,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) { }() func() { - data, err := m.rtspsServer.apiSessionsList() + data, err := m.rtspsServer.APISessionsList() if err == nil && len(data.Items) != 0 { for _, i := range data.Items { tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}" @@ -181,7 +182,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) { } if !interfaceIsEmpty(m.rtmpServer) { - data, err := m.rtmpServer.apiConnsList() + data, err := m.rtmpServer.APIConnsList() if err == nil && len(data.Items) != 0 { for _, i := range data.Items { tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}" @@ -197,7 +198,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) { } if !interfaceIsEmpty(m.rtmpsServer) { - data, err := m.rtmpsServer.apiConnsList() + data, err := m.rtmpsServer.APIConnsList() if err == nil && len(data.Items) != 0 { for _, i := range data.Items { tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}" @@ -213,7 +214,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) { } if !interfaceIsEmpty(m.srtServer) { - data, err := m.srtServer.apiConnsList() + data, err := m.srtServer.APIConnsList() if err == nil && len(data.Items) != 0 { for _, i := range data.Items { tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}" @@ -228,8 +229,8 @@ func (m *metrics) onMetrics(ctx *gin.Context) { } } - if !interfaceIsEmpty(m.webRTCManager) { - data, err := m.webRTCManager.apiSessionsList() + if !interfaceIsEmpty(m.webRTCServer) { + data, err := m.webRTCServer.APISessionsList() if err == nil && len(data.Items) != 0 { for _, i := range data.Items { tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}" @@ -255,8 +256,8 @@ func (m *metrics) setPathManager(s apiPathManager) { m.pathManager = s } -// setHLSManager is called by core. -func (m *metrics) setHLSManager(s apiHLSManager) { +// setHLSServer is called by core. +func (m *metrics) setHLSServer(s apiHLSServer) { m.mutex.Lock() defer m.mutex.Unlock() m.hlsManager = s @@ -297,9 +298,9 @@ func (m *metrics) setSRTServer(s apiSRTServer) { m.srtServer = s } -// setWebRTCManager is called by core. -func (m *metrics) setWebRTCManager(s apiWebRTCManager) { +// setWebRTCServer is called by core. +func (m *metrics) setWebRTCServer(s apiWebRTCServer) { m.mutex.Lock() defer m.mutex.Unlock() - m.webRTCManager = s + m.webRTCServer = s } diff --git a/internal/core/metrics_test.go b/internal/core/metrics_test.go index 65359afa..4c5bc08d 100644 --- a/internal/core/metrics_test.go +++ b/internal/core/metrics_test.go @@ -229,26 +229,26 @@ webrtc_sessions_bytes_sent 0 require.Regexp(t, `^paths\{name=".*?",state="ready"\} 1`+"\n"+ `paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\n"+ - `paths_bytes_sent\{name=".*?",state="ready"\} 0`+"\n"+ + `paths_bytes_sent\{name=".*?",state="ready"\} [0-9]+`+"\n"+ `paths\{name=".*?",state="ready"\} 1`+"\n"+ `paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\n"+ - `paths_bytes_sent\{name=".*?",state="ready"\} 0`+"\n"+ + `paths_bytes_sent\{name=".*?",state="ready"\} [0-9]+`+"\n"+ `paths\{name=".*?",state="ready"\} 1`+"\n"+ `paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\n"+ - `paths_bytes_sent\{name=".*?",state="ready"\} 0`+"\n"+ + `paths_bytes_sent\{name=".*?",state="ready"\} [0-9]+`+"\n"+ `paths\{name=".*?",state="ready"\} 1`+"\n"+ `paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\n"+ - `paths_bytes_sent\{name=".*?",state="ready"\} 0`+"\n"+ + `paths_bytes_sent\{name=".*?",state="ready"\} [0-9]+`+"\n"+ `paths\{name=".*?",state="ready"\} 1`+"\n"+ `paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\n"+ - `paths_bytes_sent\{name=".*?",state="ready"\} 0`+"\n"+ + `paths_bytes_sent\{name=".*?",state="ready"\} [0-9]+`+"\n"+ `paths\{name=".*?",state="ready"\} 1`+"\n"+ `paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\n"+ - `paths_bytes_sent\{name=".*?",state="ready"\} 0`+"\n"+ + `paths_bytes_sent\{name=".*?",state="ready"\} [0-9]+`+"\n"+ `hls_muxers\{name=".*?"\} 1`+"\n"+ - `hls_muxers_bytes_sent\{name=".*?"\} [0-9]+`+"\n"+ + `hls_muxers_bytes_sent\{name=".*?"\} 0`+"\n"+ `hls_muxers\{name=".*?"\} 1`+"\n"+ - `hls_muxers_bytes_sent\{name=".*?"\} [0-9]+`+"\n"+ + `hls_muxers_bytes_sent\{name=".*?"\} 0`+"\n"+ `hls_muxers\{name=".*?"\} 1`+"\n"+ `hls_muxers_bytes_sent\{name=".*?"\} 0`+"\n"+ `hls_muxers\{name=".*?"\} 1`+"\n"+ diff --git a/internal/core/path.go b/internal/core/path.go index 50deab66..81848343 100644 --- a/internal/core/path.go +++ b/internal/core/path.go @@ -11,7 +11,6 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/base" "github.com/bluenviron/gortsplib/v4/pkg/description" - "github.com/google/uuid" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" @@ -28,15 +27,6 @@ func newEmptyTimer() *time.Timer { return t } -type errPathNoOnePublishing struct { - pathName string -} - -// Error implements the error interface. -func (e errPathNoOnePublishing) Error() string { - return fmt.Sprintf("no one is publishing to path '%s'", e.pathName) -} - type pathParent interface { logger.Writer pathReady(*path) @@ -53,95 +43,6 @@ const ( pathOnDemandStateClosing ) -type pathAccessRequest struct { - name string - query string - publish bool - skipAuth bool - - // only if skipAuth = false - ip net.IP - user string - pass string - proto authProtocol - id *uuid.UUID - rtspRequest *base.Request - rtspBaseURL *base.URL - rtspNonce string -} - -type pathRemoveReaderReq struct { - author reader - res chan struct{} -} - -type pathRemovePublisherReq struct { - author publisher - res chan struct{} -} - -type pathGetConfForPathRes struct { - conf *conf.Path - err error -} - -type pathGetConfForPathReq struct { - accessRequest pathAccessRequest - res chan pathGetConfForPathRes -} - -type pathDescribeRes struct { - path *path - stream *stream.Stream - redirect string - err error -} - -type pathDescribeReq struct { - accessRequest pathAccessRequest - res chan pathDescribeRes -} - -type pathAddReaderRes struct { - path *path - stream *stream.Stream - err error -} - -type pathAddReaderReq struct { - author reader - accessRequest pathAccessRequest - res chan pathAddReaderRes -} - -type pathAddPublisherRes struct { - path *path - err error -} - -type pathAddPublisherReq struct { - author publisher - accessRequest pathAccessRequest - res chan pathAddPublisherRes -} - -type pathStartPublisherRes struct { - stream *stream.Stream - err error -} - -type pathStartPublisherReq struct { - author publisher - desc *description.Session - generateRTPPackets bool - res chan pathStartPublisherRes -} - -type pathStopPublisherReq struct { - author publisher - res chan struct{} -} - type pathAPIPathsListRes struct { data *defs.APIPathList paths map[string]*path @@ -179,16 +80,16 @@ type path struct { ctx context.Context ctxCancel func() confMutex sync.RWMutex - source source + source defs.Source publisherQuery string stream *stream.Stream recordAgent *record.Agent readyTime time.Time onUnDemandHook func(string) onNotReadyHook func() - readers map[reader]struct{} - describeRequestsOnHold []pathDescribeReq - readerAddRequestsOnHold []pathAddReaderReq + readers map[defs.Reader]struct{} + describeRequestsOnHold []defs.PathDescribeReq + readerAddRequestsOnHold []defs.PathAddReaderReq onDemandStaticSourceState pathOnDemandState onDemandStaticSourceReadyTimer *time.Timer onDemandStaticSourceCloseTimer *time.Timer @@ -200,13 +101,13 @@ type path struct { chReloadConf chan *conf.Path chStaticSourceSetReady chan defs.PathSourceStaticSetReadyReq chStaticSourceSetNotReady chan defs.PathSourceStaticSetNotReadyReq - chDescribe chan pathDescribeReq - chAddPublisher chan pathAddPublisherReq - chRemovePublisher chan pathRemovePublisherReq - chStartPublisher chan pathStartPublisherReq - chStopPublisher chan pathStopPublisherReq - chAddReader chan pathAddReaderReq - chRemoveReader chan pathRemoveReaderReq + chDescribe chan defs.PathDescribeReq + chAddPublisher chan defs.PathAddPublisherReq + chRemovePublisher chan defs.PathRemovePublisherReq + chStartPublisher chan defs.PathStartPublisherReq + chStopPublisher chan defs.PathStopPublisherReq + chAddReader chan defs.PathAddReaderReq + chRemoveReader chan defs.PathRemoveReaderReq chAPIPathsGet chan pathAPIPathsGetReq // out @@ -245,7 +146,7 @@ func newPath( parent: parent, ctx: ctx, ctxCancel: ctxCancel, - readers: make(map[reader]struct{}), + readers: make(map[defs.Reader]struct{}), onDemandStaticSourceReadyTimer: newEmptyTimer(), onDemandStaticSourceCloseTimer: newEmptyTimer(), onDemandPublisherReadyTimer: newEmptyTimer(), @@ -253,13 +154,13 @@ func newPath( chReloadConf: make(chan *conf.Path), chStaticSourceSetReady: make(chan defs.PathSourceStaticSetReadyReq), chStaticSourceSetNotReady: make(chan defs.PathSourceStaticSetNotReadyReq), - chDescribe: make(chan pathDescribeReq), - chAddPublisher: make(chan pathAddPublisherReq), - chRemovePublisher: make(chan pathRemovePublisherReq), - chStartPublisher: make(chan pathStartPublisherReq), - chStopPublisher: make(chan pathStopPublisherReq), - chAddReader: make(chan pathAddReaderReq), - chRemoveReader: make(chan pathRemoveReaderReq), + chDescribe: make(chan defs.PathDescribeReq), + chAddPublisher: make(chan defs.PathAddPublisherReq), + chRemovePublisher: make(chan defs.PathRemovePublisherReq), + chStartPublisher: make(chan defs.PathStartPublisherReq), + chStopPublisher: make(chan defs.PathStopPublisherReq), + chAddReader: make(chan defs.PathAddReaderReq), + chRemoveReader: make(chan defs.PathRemoveReaderReq), chAPIPathsGet: make(chan pathAPIPathsGetReq), done: make(chan struct{}), } @@ -280,11 +181,15 @@ func (pa *path) wait() { <-pa.done } -// Log is the main logging function. +// Log implements logger.Writer. func (pa *path) Log(level logger.Level, format string, args ...interface{}) { pa.parent.Log(level, "[path "+pa.name+"] "+format, args...) } +func (pa *path) Name() string { + return pa.name +} + func (pa *path) run() { defer close(pa.done) defer pa.wg.Done() @@ -308,7 +213,7 @@ func (pa *path) run() { Logger: pa, ExternalCmdPool: pa.externalCmdPool, Conf: pa.conf, - ExternalCmdEnv: pa.externalCmdEnv(), + ExternalCmdEnv: pa.ExternalCmdEnv(), }) err := pa.runInner() @@ -326,11 +231,11 @@ func (pa *path) run() { onUnInitHook() for _, req := range pa.describeRequestsOnHold { - req.res <- pathDescribeRes{err: fmt.Errorf("terminated")} + req.Res <- defs.PathDescribeRes{Err: fmt.Errorf("terminated")} } for _, req := range pa.readerAddRequestsOnHold { - req.res <- pathAddReaderRes{err: fmt.Errorf("terminated")} + req.Res <- defs.PathAddReaderRes{Err: fmt.Errorf("terminated")} } if pa.stream != nil { @@ -342,8 +247,8 @@ func (pa *path) run() { if !pa.conf.SourceOnDemand || pa.onDemandStaticSourceState != pathOnDemandStateInitial { source.close("path is closing") } - } else if source, ok := pa.source.(publisher); ok { - source.close() + } else if source, ok := pa.source.(defs.Publisher); ok { + source.Close() } } @@ -442,12 +347,12 @@ func (pa *path) runInner() error { func (pa *path) doOnDemandStaticSourceReadyTimer() { for _, req := range pa.describeRequestsOnHold { - req.res <- pathDescribeRes{err: fmt.Errorf("source of path '%s' has timed out", pa.name)} + req.Res <- defs.PathDescribeRes{Err: fmt.Errorf("source of path '%s' has timed out", pa.name)} } pa.describeRequestsOnHold = nil for _, req := range pa.readerAddRequestsOnHold { - req.res <- pathAddReaderRes{err: fmt.Errorf("source of path '%s' has timed out", pa.name)} + req.Res <- defs.PathAddReaderRes{Err: fmt.Errorf("source of path '%s' has timed out", pa.name)} } pa.readerAddRequestsOnHold = nil @@ -461,12 +366,12 @@ func (pa *path) doOnDemandStaticSourceCloseTimer() { func (pa *path) doOnDemandPublisherReadyTimer() { for _, req := range pa.describeRequestsOnHold { - req.res <- pathDescribeRes{err: fmt.Errorf("source of path '%s' has timed out", pa.name)} + req.Res <- defs.PathDescribeRes{Err: fmt.Errorf("source of path '%s' has timed out", pa.name)} } pa.describeRequestsOnHold = nil for _, req := range pa.readerAddRequestsOnHold { - req.res <- pathAddReaderRes{err: fmt.Errorf("source of path '%s' has timed out", pa.name)} + req.Res <- defs.PathAddReaderRes{Err: fmt.Errorf("source of path '%s' has timed out", pa.name)} } pa.readerAddRequestsOnHold = nil @@ -526,17 +431,17 @@ func (pa *path) doSourceStaticSetNotReady(req defs.PathSourceStaticSetNotReadyRe } } -func (pa *path) doDescribe(req pathDescribeReq) { +func (pa *path) doDescribe(req defs.PathDescribeReq) { if _, ok := pa.source.(*sourceRedirect); ok { - req.res <- pathDescribeRes{ - redirect: pa.conf.SourceRedirect, + req.Res <- defs.PathDescribeRes{ + Redirect: pa.conf.SourceRedirect, } return } if pa.stream != nil { - req.res <- pathDescribeRes{ - stream: pa.stream, + req.Res <- defs.PathDescribeRes{ + Stream: pa.stream, } return } @@ -551,7 +456,7 @@ func (pa *path) doDescribe(req pathDescribeReq) { if pa.conf.HasOnDemandPublisher() { if pa.onDemandPublisherState == pathOnDemandStateInitial { - pa.onDemandPublisherStart(req.accessRequest.query) + pa.onDemandPublisherStart(req.AccessRequest.Query) } pa.describeRequestsOnHold = append(pa.describeRequestsOnHold, req) return @@ -561,69 +466,69 @@ func (pa *path) doDescribe(req pathDescribeReq) { fallbackURL := func() string { if strings.HasPrefix(pa.conf.Fallback, "/") { ur := base.URL{ - Scheme: req.accessRequest.rtspRequest.URL.Scheme, - User: req.accessRequest.rtspRequest.URL.User, - Host: req.accessRequest.rtspRequest.URL.Host, + Scheme: req.AccessRequest.RTSPRequest.URL.Scheme, + User: req.AccessRequest.RTSPRequest.URL.User, + Host: req.AccessRequest.RTSPRequest.URL.Host, Path: pa.conf.Fallback, } return ur.String() } return pa.conf.Fallback }() - req.res <- pathDescribeRes{redirect: fallbackURL} + req.Res <- defs.PathDescribeRes{Redirect: fallbackURL} return } - req.res <- pathDescribeRes{err: errPathNoOnePublishing{pathName: pa.name}} + req.Res <- defs.PathDescribeRes{Err: defs.ErrPathNoOnePublishing{PathName: pa.name}} } -func (pa *path) doRemovePublisher(req pathRemovePublisherReq) { - if pa.source == req.author { +func (pa *path) doRemovePublisher(req defs.PathRemovePublisherReq) { + if pa.source == req.Author { pa.executeRemovePublisher() } - close(req.res) + close(req.Res) } -func (pa *path) doAddPublisher(req pathAddPublisherReq) { +func (pa *path) doAddPublisher(req defs.PathAddPublisherReq) { if pa.conf.Source != "publisher" { - req.res <- pathAddPublisherRes{ - err: fmt.Errorf("can't publish to path '%s' since 'source' is not 'publisher'", pa.name), + req.Res <- defs.PathAddPublisherRes{ + Err: fmt.Errorf("can't publish to path '%s' since 'source' is not 'publisher'", pa.name), } return } if pa.source != nil { if !pa.conf.OverridePublisher { - req.res <- pathAddPublisherRes{err: fmt.Errorf("someone is already publishing to path '%s'", pa.name)} + req.Res <- defs.PathAddPublisherRes{Err: fmt.Errorf("someone is already publishing to path '%s'", pa.name)} return } pa.Log(logger.Info, "closing existing publisher") - pa.source.(publisher).close() + pa.source.(defs.Publisher).Close() pa.executeRemovePublisher() } - pa.source = req.author - pa.publisherQuery = req.accessRequest.query + pa.source = req.Author + pa.publisherQuery = req.AccessRequest.Query - req.res <- pathAddPublisherRes{path: pa} + req.Res <- defs.PathAddPublisherRes{Path: pa} } -func (pa *path) doStartPublisher(req pathStartPublisherReq) { - if pa.source != req.author { - req.res <- pathStartPublisherRes{err: fmt.Errorf("publisher is not assigned to this path anymore")} +func (pa *path) doStartPublisher(req defs.PathStartPublisherReq) { + if pa.source != req.Author { + req.Res <- defs.PathStartPublisherRes{Err: fmt.Errorf("publisher is not assigned to this path anymore")} return } - err := pa.setReady(req.desc, req.generateRTPPackets) + err := pa.setReady(req.Desc, req.GenerateRTPPackets) if err != nil { - req.res <- pathStartPublisherRes{err: err} + req.Res <- defs.PathStartPublisherRes{Err: err} return } - req.author.Log(logger.Info, "is publishing to path '%s', %s", + req.Author.Log(logger.Info, "is publishing to path '%s', %s", pa.name, - mediaInfo(req.desc.Medias)) + defs.MediasInfo(req.Desc.Medias)) if pa.conf.HasOnDemandPublisher() && pa.onDemandPublisherState != pathOnDemandStateInitial { pa.onDemandPublisherReadyTimer.Stop() @@ -633,17 +538,17 @@ func (pa *path) doStartPublisher(req pathStartPublisherReq) { pa.consumeOnHoldRequests() - req.res <- pathStartPublisherRes{stream: pa.stream} + req.Res <- defs.PathStartPublisherRes{Stream: pa.stream} } -func (pa *path) doStopPublisher(req pathStopPublisherReq) { - if req.author == pa.source && pa.stream != nil { +func (pa *path) doStopPublisher(req defs.PathStopPublisherReq) { + if req.Author == pa.source && pa.stream != nil { pa.setNotReady() } - close(req.res) + close(req.Res) } -func (pa *path) doAddReader(req pathAddReaderReq) { +func (pa *path) doAddReader(req defs.PathAddReaderReq) { if pa.stream != nil { pa.addReaderPost(req) return @@ -659,20 +564,20 @@ func (pa *path) doAddReader(req pathAddReaderReq) { if pa.conf.HasOnDemandPublisher() { if pa.onDemandPublisherState == pathOnDemandStateInitial { - pa.onDemandPublisherStart(req.accessRequest.query) + pa.onDemandPublisherStart(req.AccessRequest.Query) } pa.readerAddRequestsOnHold = append(pa.readerAddRequestsOnHold, req) return } - req.res <- pathAddReaderRes{err: errPathNoOnePublishing{pathName: pa.name}} + req.Res <- defs.PathAddReaderRes{Err: defs.ErrPathNoOnePublishing{PathName: pa.name}} } -func (pa *path) doRemoveReader(req pathRemoveReaderReq) { - if _, ok := pa.readers[req.author]; ok { - pa.executeRemoveReader(req.author) +func (pa *path) doRemoveReader(req defs.PathRemoveReaderReq) { + if _, ok := pa.readers[req.Author]; ok { + pa.executeRemoveReader(req.Author) } - close(req.res) + close(req.Res) if len(pa.readers) == 0 { if pa.conf.HasOnDemandStaticSource() { @@ -711,7 +616,7 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) { if pa.stream == nil { return []string{} } - return mediasDescription(pa.stream.Desc().Medias) + return defs.MediasDescription(pa.stream.Desc().Medias) }(), BytesReceived: func() uint64 { if pa.stream == nil { @@ -728,7 +633,7 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) { Readers: func() []defs.APIPathSourceOrReader { ret := []defs.APIPathSourceOrReader{} for r := range pa.readers { - ret = append(ret, r.apiReaderDescribe()) + ret = append(ret, r.APIReaderDescribe()) } return ret }(), @@ -736,21 +641,13 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) { } } -func (pa *path) safeConf() *conf.Path { +func (pa *path) SafeConf() *conf.Path { pa.confMutex.RLock() defer pa.confMutex.RUnlock() return pa.conf } -func (pa *path) shouldClose() bool { - return pa.conf.Regexp != nil && - pa.source == nil && - len(pa.readers) == 0 && - len(pa.describeRequestsOnHold) == 0 && - len(pa.readerAddRequestsOnHold) == 0 -} - -func (pa *path) externalCmdEnv() externalcmd.Environment { +func (pa *path) ExternalCmdEnv() externalcmd.Environment { _, port, _ := net.SplitHostPort(pa.rtspAddress) env := externalcmd.Environment{ "MTX_PATH": pa.name, @@ -767,6 +664,14 @@ func (pa *path) externalCmdEnv() externalcmd.Environment { return env } +func (pa *path) shouldClose() bool { + return pa.conf.Regexp != nil && + pa.source == nil && + len(pa.readers) == 0 && + len(pa.describeRequestsOnHold) == 0 && + len(pa.readerAddRequestsOnHold) == 0 +} + func (pa *path) onDemandStaticSourceStart() { pa.source.(*staticSourceHandler).start(true) @@ -799,7 +704,7 @@ func (pa *path) onDemandPublisherStart(query string) { Logger: pa, ExternalCmdPool: pa.externalCmdPool, Conf: pa.conf, - ExternalCmdEnv: pa.externalCmdEnv(), + ExternalCmdEnv: pa.ExternalCmdEnv(), Query: query, }) @@ -850,7 +755,7 @@ func (pa *path) setReady(desc *description.Session, allocateEncoder bool) error Logger: pa, ExternalCmdPool: pa.externalCmdPool, Conf: pa.conf, - ExternalCmdEnv: pa.externalCmdEnv(), + ExternalCmdEnv: pa.ExternalCmdEnv(), Desc: pa.source.APISourceDescribe(), Query: pa.publisherQuery, }) @@ -862,8 +767,8 @@ func (pa *path) setReady(desc *description.Session, allocateEncoder bool) error func (pa *path) consumeOnHoldRequests() { for _, req := range pa.describeRequestsOnHold { - req.res <- pathDescribeRes{ - stream: pa.stream, + req.Res <- defs.PathDescribeRes{ + Stream: pa.stream, } } pa.describeRequestsOnHold = nil @@ -879,7 +784,7 @@ func (pa *path) setNotReady() { for r := range pa.readers { pa.executeRemoveReader(r) - r.close() + r.Close() } pa.onNotReadyHook() @@ -904,10 +809,10 @@ func (pa *path) startRecording() { SegmentDuration: time.Duration(pa.conf.RecordSegmentDuration), PathName: pa.name, Stream: pa.stream, - OnSegmentCreate: func(path string) { + OnSegmentCreate: func(segmentPath string) { if pa.conf.RunOnRecordSegmentCreate != "" { - env := pa.externalCmdEnv() - env["MTX_SEGMENT_PATH"] = path + env := pa.ExternalCmdEnv() + env["MTX_SEGMENT_PATH"] = segmentPath pa.Log(logger.Info, "runOnRecordSegmentCreate command launched") externalcmd.NewCmd( @@ -918,10 +823,10 @@ func (pa *path) startRecording() { nil) } }, - OnSegmentComplete: func(path string) { + OnSegmentComplete: func(segmentPath string) { if pa.conf.RunOnRecordSegmentComplete != "" { - env := pa.externalCmdEnv() - env["MTX_SEGMENT_PATH"] = path + env := pa.ExternalCmdEnv() + env["MTX_SEGMENT_PATH"] = segmentPath pa.Log(logger.Info, "runOnRecordSegmentComplete command launched") externalcmd.NewCmd( @@ -937,7 +842,7 @@ func (pa *path) startRecording() { pa.recordAgent.Initialize() } -func (pa *path) executeRemoveReader(r reader) { +func (pa *path) executeRemoveReader(r defs.Reader) { delete(pa.readers, r) } @@ -949,23 +854,21 @@ func (pa *path) executeRemovePublisher() { pa.source = nil } -func (pa *path) addReaderPost(req pathAddReaderReq) { - if _, ok := pa.readers[req.author]; ok { - req.res <- pathAddReaderRes{ - path: pa, - stream: pa.stream, +func (pa *path) addReaderPost(req defs.PathAddReaderReq) { + if _, ok := pa.readers[req.Author]; ok { + req.Res <- defs.PathAddReaderRes{ + Path: pa, + Stream: pa.stream, } return } if pa.conf.MaxReaders != 0 && len(pa.readers) >= pa.conf.MaxReaders { - req.res <- pathAddReaderRes{ - err: fmt.Errorf("maximum reader count reached"), - } + req.Res <- defs.PathAddReaderRes{Err: fmt.Errorf("maximum reader count reached")} return } - pa.readers[req.author] = struct{}{} + pa.readers[req.Author] = struct{}{} if pa.conf.HasOnDemandStaticSource() { if pa.onDemandStaticSourceState == pathOnDemandStateClosing { @@ -981,9 +884,9 @@ func (pa *path) addReaderPost(req pathAddReaderReq) { } } - req.res <- pathAddReaderRes{ - path: pa, - stream: pa.stream, + req.Res <- defs.PathAddReaderRes{ + Path: pa, + Stream: pa.stream, } } @@ -1032,72 +935,72 @@ func (pa *path) staticSourceHandlerSetNotReady( } // describe is called by a reader or publisher through pathManager. -func (pa *path) describe(req pathDescribeReq) pathDescribeRes { +func (pa *path) describe(req defs.PathDescribeReq) defs.PathDescribeRes { select { case pa.chDescribe <- req: - return <-req.res + return <-req.Res case <-pa.ctx.Done(): - return pathDescribeRes{err: fmt.Errorf("terminated")} + return defs.PathDescribeRes{Err: fmt.Errorf("terminated")} } } // addPublisher is called by a publisher through pathManager. -func (pa *path) addPublisher(req pathAddPublisherReq) pathAddPublisherRes { +func (pa *path) addPublisher(req defs.PathAddPublisherReq) defs.PathAddPublisherRes { select { case pa.chAddPublisher <- req: - return <-req.res + return <-req.Res case <-pa.ctx.Done(): - return pathAddPublisherRes{err: fmt.Errorf("terminated")} + return defs.PathAddPublisherRes{Err: fmt.Errorf("terminated")} } } -// removePublisher is called by a publisher. -func (pa *path) removePublisher(req pathRemovePublisherReq) { - req.res = make(chan struct{}) +// RemovePublisher is called by a publisher. +func (pa *path) RemovePublisher(req defs.PathRemovePublisherReq) { + req.Res = make(chan struct{}) select { case pa.chRemovePublisher <- req: - <-req.res + <-req.Res case <-pa.ctx.Done(): } } -// startPublisher is called by a publisher. -func (pa *path) startPublisher(req pathStartPublisherReq) pathStartPublisherRes { - req.res = make(chan pathStartPublisherRes) +// StartPublisher is called by a publisher. +func (pa *path) StartPublisher(req defs.PathStartPublisherReq) defs.PathStartPublisherRes { + req.Res = make(chan defs.PathStartPublisherRes) select { case pa.chStartPublisher <- req: - return <-req.res + return <-req.Res case <-pa.ctx.Done(): - return pathStartPublisherRes{err: fmt.Errorf("terminated")} + return defs.PathStartPublisherRes{Err: fmt.Errorf("terminated")} } } -// stopPublisher is called by a publisher. -func (pa *path) stopPublisher(req pathStopPublisherReq) { - req.res = make(chan struct{}) +// StopPublisher is called by a publisher. +func (pa *path) StopPublisher(req defs.PathStopPublisherReq) { + req.Res = make(chan struct{}) select { case pa.chStopPublisher <- req: - <-req.res + <-req.Res case <-pa.ctx.Done(): } } // addReader is called by a reader through pathManager. -func (pa *path) addReader(req pathAddReaderReq) pathAddReaderRes { +func (pa *path) addReader(req defs.PathAddReaderReq) defs.PathAddReaderRes { select { case pa.chAddReader <- req: - return <-req.res + return <-req.Res case <-pa.ctx.Done(): - return pathAddReaderRes{err: fmt.Errorf("terminated")} + return defs.PathAddReaderRes{Err: fmt.Errorf("terminated")} } } -// removeReader is called by a reader. -func (pa *path) removeReader(req pathRemoveReaderReq) { - req.res = make(chan struct{}) +// RemoveReader is called by a reader. +func (pa *path) RemoveReader(req defs.PathRemoveReaderReq) { + req.Res = make(chan struct{}) select { case pa.chRemoveReader <- req: - <-req.res + <-req.Res case <-pa.ctx.Done(): } } diff --git a/internal/core/path_manager.go b/internal/core/path_manager.go index f63d4e50..3f43aea3 100644 --- a/internal/core/path_manager.go +++ b/internal/core/path_manager.go @@ -59,9 +59,9 @@ func getConfForPath(pathConfs map[string]*conf.Path, name string) (string, *conf return "", nil, nil, fmt.Errorf("path '%s' is not configured", name) } -type pathManagerHLSManager interface { - pathReady(*path) - pathNotReady(*path) +type pathManagerHLSServer interface { + PathReady(defs.Path) + PathNotReady(defs.Path) } type pathManagerParent interface { @@ -83,20 +83,20 @@ type pathManager struct { ctx context.Context ctxCancel func() wg sync.WaitGroup - hlsManager pathManagerHLSManager + hlsManager pathManagerHLSServer paths map[string]*path pathsByConf map[string]map[*path]struct{} // in chReloadConf chan map[string]*conf.Path - chSetHLSManager chan pathManagerHLSManager + chSetHLSServer chan pathManagerHLSServer chClosePath chan *path chPathReady chan *path chPathNotReady chan *path - chGetConfForPath chan pathGetConfForPathReq - chDescribe chan pathDescribeReq - chAddReader chan pathAddReaderReq - chAddPublisher chan pathAddPublisherReq + chGetConfForPath chan defs.PathGetConfForPathReq + chDescribe chan defs.PathDescribeReq + chAddReader chan defs.PathAddReaderReq + chAddPublisher chan defs.PathAddPublisherReq chAPIPathsList chan pathAPIPathsListReq chAPIPathsGet chan pathAPIPathsGetReq } @@ -131,14 +131,14 @@ func newPathManager( paths: make(map[string]*path), pathsByConf: make(map[string]map[*path]struct{}), chReloadConf: make(chan map[string]*conf.Path), - chSetHLSManager: make(chan pathManagerHLSManager), + chSetHLSServer: make(chan pathManagerHLSServer), chClosePath: make(chan *path), chPathReady: make(chan *path), chPathNotReady: make(chan *path), - chGetConfForPath: make(chan pathGetConfForPathReq), - chDescribe: make(chan pathDescribeReq), - chAddReader: make(chan pathAddReaderReq), - chAddPublisher: make(chan pathAddPublisherReq), + chGetConfForPath: make(chan defs.PathGetConfForPathReq), + chDescribe: make(chan defs.PathDescribeReq), + chAddReader: make(chan defs.PathAddReaderReq), + chAddPublisher: make(chan defs.PathAddPublisherReq), chAPIPathsList: make(chan pathAPIPathsListReq), chAPIPathsGet: make(chan pathAPIPathsGetReq), } @@ -163,7 +163,7 @@ func (pm *pathManager) close() { pm.wg.Wait() } -// Log is the main logging function. +// Log implements logger.Writer. func (pm *pathManager) Log(level logger.Level, format string, args ...interface{}) { pm.parent.Log(level, format, args...) } @@ -177,8 +177,8 @@ outer: case newPaths := <-pm.chReloadConf: pm.doReloadConf(newPaths) - case m := <-pm.chSetHLSManager: - pm.doSetHLSManager(m) + case m := <-pm.chSetHLSServer: + pm.doSetHLSServer(m) case pa := <-pm.chClosePath: pm.doClosePath(pa) @@ -252,7 +252,7 @@ func (pm *pathManager) doReloadConf(newPaths map[string]*conf.Path) { } } -func (pm *pathManager) doSetHLSManager(m pathManagerHLSManager) { +func (pm *pathManager) doSetHLSServer(m pathManagerHLSServer) { pm.hlsManager = m } @@ -265,101 +265,101 @@ func (pm *pathManager) doClosePath(pa *path) { func (pm *pathManager) doPathReady(pa *path) { if pm.hlsManager != nil { - pm.hlsManager.pathReady(pa) + pm.hlsManager.PathReady(pa) } } func (pm *pathManager) doPathNotReady(pa *path) { if pm.hlsManager != nil { - pm.hlsManager.pathNotReady(pa) + pm.hlsManager.PathNotReady(pa) } } -func (pm *pathManager) doGetConfForPath(req pathGetConfForPathReq) { - _, pathConf, _, err := getConfForPath(pm.pathConfs, req.accessRequest.name) +func (pm *pathManager) doGetConfForPath(req defs.PathGetConfForPathReq) { + _, pathConf, _, err := getConfForPath(pm.pathConfs, req.AccessRequest.Name) if err != nil { - req.res <- pathGetConfForPathRes{err: err} + req.Res <- defs.PathGetConfForPathRes{Err: err} return } err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, - pathConf, req.accessRequest) + pathConf, req.AccessRequest) if err != nil { - req.res <- pathGetConfForPathRes{err: err} + req.Res <- defs.PathGetConfForPathRes{Err: err} return } - req.res <- pathGetConfForPathRes{conf: pathConf} + req.Res <- defs.PathGetConfForPathRes{Conf: pathConf} } -func (pm *pathManager) doDescribe(req pathDescribeReq) { - pathConfName, pathConf, pathMatches, err := getConfForPath(pm.pathConfs, req.accessRequest.name) +func (pm *pathManager) doDescribe(req defs.PathDescribeReq) { + pathConfName, pathConf, pathMatches, err := getConfForPath(pm.pathConfs, req.AccessRequest.Name) if err != nil { - req.res <- pathDescribeRes{err: err} + req.Res <- defs.PathDescribeRes{Err: err} return } err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, - pathConf, req.accessRequest) + pathConf, req.AccessRequest) if err != nil { - req.res <- pathDescribeRes{err: err} + req.Res <- defs.PathDescribeRes{Err: err} return } // create path if it doesn't exist - if _, ok := pm.paths[req.accessRequest.name]; !ok { - pm.createPath(pathConfName, pathConf, req.accessRequest.name, pathMatches) + if _, ok := pm.paths[req.AccessRequest.Name]; !ok { + pm.createPath(pathConfName, pathConf, req.AccessRequest.Name, pathMatches) } - req.res <- pathDescribeRes{path: pm.paths[req.accessRequest.name]} + req.Res <- defs.PathDescribeRes{Path: pm.paths[req.AccessRequest.Name]} } -func (pm *pathManager) doAddReader(req pathAddReaderReq) { - pathConfName, pathConf, pathMatches, err := getConfForPath(pm.pathConfs, req.accessRequest.name) +func (pm *pathManager) doAddReader(req defs.PathAddReaderReq) { + pathConfName, pathConf, pathMatches, err := getConfForPath(pm.pathConfs, req.AccessRequest.Name) if err != nil { - req.res <- pathAddReaderRes{err: err} + req.Res <- defs.PathAddReaderRes{Err: err} return } - if !req.accessRequest.skipAuth { + if !req.AccessRequest.SkipAuth { err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, - pathConf, req.accessRequest) + pathConf, req.AccessRequest) if err != nil { - req.res <- pathAddReaderRes{err: err} + req.Res <- defs.PathAddReaderRes{Err: err} return } } // create path if it doesn't exist - if _, ok := pm.paths[req.accessRequest.name]; !ok { - pm.createPath(pathConfName, pathConf, req.accessRequest.name, pathMatches) + if _, ok := pm.paths[req.AccessRequest.Name]; !ok { + pm.createPath(pathConfName, pathConf, req.AccessRequest.Name, pathMatches) } - req.res <- pathAddReaderRes{path: pm.paths[req.accessRequest.name]} + req.Res <- defs.PathAddReaderRes{Path: pm.paths[req.AccessRequest.Name]} } -func (pm *pathManager) doAddPublisher(req pathAddPublisherReq) { - pathConfName, pathConf, pathMatches, err := getConfForPath(pm.pathConfs, req.accessRequest.name) +func (pm *pathManager) doAddPublisher(req defs.PathAddPublisherReq) { + pathConfName, pathConf, pathMatches, err := getConfForPath(pm.pathConfs, req.AccessRequest.Name) if err != nil { - req.res <- pathAddPublisherRes{err: err} + req.Res <- defs.PathAddPublisherRes{Err: err} return } - if !req.accessRequest.skipAuth { + if !req.AccessRequest.SkipAuth { err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, - pathConf, req.accessRequest) + pathConf, req.AccessRequest) if err != nil { - req.res <- pathAddPublisherRes{err: err} + req.Res <- defs.PathAddPublisherRes{Err: err} return } } // create path if it doesn't exist - if _, ok := pm.paths[req.accessRequest.name]; !ok { - pm.createPath(pathConfName, pathConf, req.accessRequest.name, pathMatches) + if _, ok := pm.paths[req.AccessRequest.Name]; !ok { + pm.createPath(pathConfName, pathConf, req.AccessRequest.Name, pathMatches) } - req.res <- pathAddPublisherRes{path: pm.paths[req.accessRequest.name]} + req.Res <- defs.PathAddPublisherRes{Path: pm.paths[req.AccessRequest.Name]} } func (pm *pathManager) doAPIPathsList(req pathAPIPathsListReq) { @@ -454,79 +454,79 @@ func (pm *pathManager) closePath(pa *path) { } } -// getConfForPath is called by a reader or publisher. -func (pm *pathManager) getConfForPath(req pathGetConfForPathReq) pathGetConfForPathRes { - req.res = make(chan pathGetConfForPathRes) +// GetConfForPath is called by a reader or publisher. +func (pm *pathManager) GetConfForPath(req defs.PathGetConfForPathReq) defs.PathGetConfForPathRes { + req.Res = make(chan defs.PathGetConfForPathRes) select { case pm.chGetConfForPath <- req: - return <-req.res + return <-req.Res case <-pm.ctx.Done(): - return pathGetConfForPathRes{err: fmt.Errorf("terminated")} + return defs.PathGetConfForPathRes{Err: fmt.Errorf("terminated")} } } -// describe is called by a reader or publisher. -func (pm *pathManager) describe(req pathDescribeReq) pathDescribeRes { - req.res = make(chan pathDescribeRes) +// Describe is called by a reader or publisher. +func (pm *pathManager) Describe(req defs.PathDescribeReq) defs.PathDescribeRes { + req.Res = make(chan defs.PathDescribeRes) select { case pm.chDescribe <- req: - res1 := <-req.res - if res1.err != nil { + res1 := <-req.Res + if res1.Err != nil { return res1 } - res2 := res1.path.describe(req) - if res2.err != nil { + res2 := res1.Path.(*path).describe(req) + if res2.Err != nil { return res2 } - res2.path = res1.path + res2.Path = res1.Path return res2 case <-pm.ctx.Done(): - return pathDescribeRes{err: fmt.Errorf("terminated")} + return defs.PathDescribeRes{Err: fmt.Errorf("terminated")} } } -// addPublisher is called by a publisher. -func (pm *pathManager) addPublisher(req pathAddPublisherReq) pathAddPublisherRes { - req.res = make(chan pathAddPublisherRes) +// AddPublisher is called by a publisher. +func (pm *pathManager) AddPublisher(req defs.PathAddPublisherReq) defs.PathAddPublisherRes { + req.Res = make(chan defs.PathAddPublisherRes) select { case pm.chAddPublisher <- req: - res := <-req.res - if res.err != nil { + res := <-req.Res + if res.Err != nil { return res } - return res.path.addPublisher(req) + return res.Path.(*path).addPublisher(req) case <-pm.ctx.Done(): - return pathAddPublisherRes{err: fmt.Errorf("terminated")} + return defs.PathAddPublisherRes{Err: fmt.Errorf("terminated")} } } -// addReader is called by a reader. -func (pm *pathManager) addReader(req pathAddReaderReq) pathAddReaderRes { - req.res = make(chan pathAddReaderRes) +// AddReader is called by a reader. +func (pm *pathManager) AddReader(req defs.PathAddReaderReq) defs.PathAddReaderRes { + req.Res = make(chan defs.PathAddReaderRes) select { case pm.chAddReader <- req: - res := <-req.res - if res.err != nil { + res := <-req.Res + if res.Err != nil { return res } - return res.path.addReader(req) + return res.Path.(*path).addReader(req) case <-pm.ctx.Done(): - return pathAddReaderRes{err: fmt.Errorf("terminated")} + return defs.PathAddReaderRes{Err: fmt.Errorf("terminated")} } } -// setHLSManager is called by hlsManager. -func (pm *pathManager) setHLSManager(s pathManagerHLSManager) { +// setHLSServer is called by hlsManager. +func (pm *pathManager) setHLSServer(s pathManagerHLSServer) { select { - case pm.chSetHLSManager <- s: + case pm.chSetHLSServer <- s: case <-pm.ctx.Done(): } } diff --git a/internal/core/path_manager_test.go b/internal/core/path_manager_test.go index dcb062f3..79c33380 100644 --- a/internal/core/path_manager_test.go +++ b/internal/core/path_manager_test.go @@ -7,9 +7,12 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/base" "github.com/bluenviron/gortsplib/v4/pkg/headers" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/stretchr/testify/require" ) +var _ defs.PathManager = &pathManager{} + func TestPathAutoDeletion(t *testing.T) { for _, ca := range []string{"describe", "setup"} { t.Run(ca, func(t *testing.T) { diff --git a/internal/core/path_test.go b/internal/core/path_test.go index f19ae397..25850d50 100644 --- a/internal/core/path_test.go +++ b/internal/core/path_test.go @@ -19,10 +19,11 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/headers" "github.com/bluenviron/gortsplib/v4/pkg/sdp" - "github.com/datarhei/gosrt" + srt "github.com/datarhei/gosrt" "github.com/pion/rtp" "github.com/stretchr/testify/require" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/protocols/rtmp" "github.com/bluenviron/mediamtx/internal/protocols/webrtc" ) @@ -81,6 +82,8 @@ func main() { } ` +var _ defs.Path = &path{} + func TestPathRunOnDemand(t *testing.T) { onDemandFile := filepath.Join(os.TempDir(), "ondemand") onUnDemandFile := filepath.Join(os.TempDir(), "onundemand") diff --git a/internal/core/pprof.go b/internal/core/pprof.go index e71d9397..b92396c0 100644 --- a/internal/core/pprof.go +++ b/internal/core/pprof.go @@ -58,6 +58,7 @@ func (pp *pprof) close() { pp.httpServer.Close() } +// Log implements logger.Writer. func (pp *pprof) Log(level logger.Level, format string, args ...interface{}) { pp.parent.Log(level, "[pprof] "+format, args...) } diff --git a/internal/core/publisher.go b/internal/core/publisher.go deleted file mode 100644 index c76f8d0f..00000000 --- a/internal/core/publisher.go +++ /dev/null @@ -1,7 +0,0 @@ -package core - -// publisher is an entity that can publish a stream. -type publisher interface { - source - close() -} diff --git a/internal/core/reader.go b/internal/core/reader.go deleted file mode 100644 index dcac29eb..00000000 --- a/internal/core/reader.go +++ /dev/null @@ -1,17 +0,0 @@ -package core - -import ( - "github.com/bluenviron/mediamtx/internal/asyncwriter" - "github.com/bluenviron/mediamtx/internal/defs" - "github.com/bluenviron/mediamtx/internal/stream" -) - -// reader is an entity that can read a stream. -type reader interface { - close() - apiReaderDescribe() defs.APIPathSourceOrReader -} - -func readerMediaInfo(r *asyncwriter.Writer, stream *stream.Stream) string { - return mediaInfo(stream.MediasForReader(r)) -} diff --git a/internal/core/rtmp_listener.go b/internal/core/rtmp_listener.go deleted file mode 100644 index 7c058a4b..00000000 --- a/internal/core/rtmp_listener.go +++ /dev/null @@ -1,48 +0,0 @@ -package core - -import ( - "net" - "sync" -) - -type rtmpListener struct { - ln net.Listener - wg *sync.WaitGroup - parent *rtmpServer -} - -func newRTMPListener( - ln net.Listener, - wg *sync.WaitGroup, - parent *rtmpServer, -) *rtmpListener { - l := &rtmpListener{ - ln: ln, - wg: wg, - parent: parent, - } - - l.wg.Add(1) - go l.run() - - return l -} - -func (l *rtmpListener) run() { - defer l.wg.Done() - - err := l.runInner() - - l.parent.acceptError(err) -} - -func (l *rtmpListener) runInner() error { - for { - conn, err := l.ln.Accept() - if err != nil { - return err - } - - l.parent.newConn(conn) - } -} diff --git a/internal/core/rtmp_server.go b/internal/core/rtmp_server.go deleted file mode 100644 index 5733d2c4..00000000 --- a/internal/core/rtmp_server.go +++ /dev/null @@ -1,327 +0,0 @@ -package core - -import ( - "context" - "crypto/tls" - "fmt" - "net" - "sort" - "sync" - - "github.com/google/uuid" - - "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/restrictnetwork" -) - -type rtmpServerAPIConnsListRes struct { - data *defs.APIRTMPConnList - err error -} - -type rtmpServerAPIConnsListReq struct { - res chan rtmpServerAPIConnsListRes -} - -type rtmpServerAPIConnsGetRes struct { - data *defs.APIRTMPConn - err error -} - -type rtmpServerAPIConnsGetReq struct { - uuid uuid.UUID - res chan rtmpServerAPIConnsGetRes -} - -type rtmpServerAPIConnsKickRes struct { - err error -} - -type rtmpServerAPIConnsKickReq struct { - uuid uuid.UUID - res chan rtmpServerAPIConnsKickRes -} - -type rtmpServerParent interface { - logger.Writer -} - -type rtmpServer struct { - readTimeout conf.StringDuration - writeTimeout conf.StringDuration - writeQueueSize int - isTLS bool - rtspAddress string - runOnConnect string - runOnConnectRestart bool - runOnDisconnect string - externalCmdPool *externalcmd.Pool - pathManager *pathManager - parent rtmpServerParent - - ctx context.Context - ctxCancel func() - wg sync.WaitGroup - ln net.Listener - conns map[*rtmpConn]struct{} - - // in - chNewConn chan net.Conn - chAcceptErr chan error - chCloseConn chan *rtmpConn - chAPIConnsList chan rtmpServerAPIConnsListReq - chAPIConnsGet chan rtmpServerAPIConnsGetReq - chAPIConnsKick chan rtmpServerAPIConnsKickReq -} - -func newRTMPServer( - address string, - readTimeout conf.StringDuration, - writeTimeout conf.StringDuration, - writeQueueSize int, - isTLS bool, - serverCert string, - serverKey string, - rtspAddress string, - runOnConnect string, - runOnConnectRestart bool, - runOnDisconnect string, - externalCmdPool *externalcmd.Pool, - pathManager *pathManager, - parent rtmpServerParent, -) (*rtmpServer, error) { - ln, err := func() (net.Listener, error) { - if !isTLS { - return net.Listen(restrictnetwork.Restrict("tcp", address)) - } - - cert, err := tls.LoadX509KeyPair(serverCert, serverKey) - if err != nil { - return nil, err - } - - network, address := restrictnetwork.Restrict("tcp", address) - return tls.Listen(network, address, &tls.Config{Certificates: []tls.Certificate{cert}}) - }() - if err != nil { - return nil, err - } - - ctx, ctxCancel := context.WithCancel(context.Background()) - - s := &rtmpServer{ - readTimeout: readTimeout, - writeTimeout: writeTimeout, - writeQueueSize: writeQueueSize, - rtspAddress: rtspAddress, - runOnConnect: runOnConnect, - runOnConnectRestart: runOnConnectRestart, - runOnDisconnect: runOnDisconnect, - isTLS: isTLS, - externalCmdPool: externalCmdPool, - pathManager: pathManager, - parent: parent, - ctx: ctx, - ctxCancel: ctxCancel, - ln: ln, - conns: make(map[*rtmpConn]struct{}), - chNewConn: make(chan net.Conn), - chAcceptErr: make(chan error), - chCloseConn: make(chan *rtmpConn), - chAPIConnsList: make(chan rtmpServerAPIConnsListReq), - chAPIConnsGet: make(chan rtmpServerAPIConnsGetReq), - chAPIConnsKick: make(chan rtmpServerAPIConnsKickReq), - } - - s.Log(logger.Info, "listener opened on %s", address) - - newRTMPListener( - s.ln, - &s.wg, - s, - ) - - s.wg.Add(1) - go s.run() - - return s, nil -} - -func (s *rtmpServer) Log(level logger.Level, format string, args ...interface{}) { - label := func() string { - if s.isTLS { - return "RTMPS" - } - return "RTMP" - }() - s.parent.Log(level, "[%s] "+format, append([]interface{}{label}, args...)...) -} - -func (s *rtmpServer) close() { - s.Log(logger.Info, "listener is closing") - s.ctxCancel() - s.wg.Wait() -} - -func (s *rtmpServer) 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 := newRTMPConn( - s.ctx, - s.isTLS, - s.rtspAddress, - s.readTimeout, - s.writeTimeout, - s.writeQueueSize, - s.runOnConnect, - s.runOnConnectRestart, - s.runOnDisconnect, - &s.wg, - nconn, - s.externalCmdPool, - s.pathManager, - s) - 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 <- rtmpServerAPIConnsListRes{data: data} - - case req := <-s.chAPIConnsGet: - c := s.findConnByUUID(req.uuid) - if c == nil { - req.res <- rtmpServerAPIConnsGetRes{err: fmt.Errorf("connection not found")} - continue - } - - req.res <- rtmpServerAPIConnsGetRes{data: c.apiItem()} - - case req := <-s.chAPIConnsKick: - c := s.findConnByUUID(req.uuid) - if c == nil { - req.res <- rtmpServerAPIConnsKickRes{err: fmt.Errorf("connection not found")} - continue - } - - delete(s.conns, c) - c.close() - req.res <- rtmpServerAPIConnsKickRes{} - - case <-s.ctx.Done(): - break outer - } - } - - s.ctxCancel() - - s.ln.Close() -} - -func (s *rtmpServer) findConnByUUID(uuid uuid.UUID) *rtmpConn { - for c := range s.conns { - if c.uuid == uuid { - return c - } - } - return nil -} - -// newConn is called by rtmpListener. -func (s *rtmpServer) newConn(conn net.Conn) { - select { - case s.chNewConn <- conn: - case <-s.ctx.Done(): - conn.Close() - } -} - -// acceptError is called by rtmpListener. -func (s *rtmpServer) acceptError(err error) { - select { - case s.chAcceptErr <- err: - case <-s.ctx.Done(): - } -} - -// closeConn is called by rtmpConn. -func (s *rtmpServer) closeConn(c *rtmpConn) { - select { - case s.chCloseConn <- c: - case <-s.ctx.Done(): - } -} - -// apiConnsList is called by api. -func (s *rtmpServer) apiConnsList() (*defs.APIRTMPConnList, error) { - req := rtmpServerAPIConnsListReq{ - res: make(chan rtmpServerAPIConnsListRes), - } - - select { - case s.chAPIConnsList <- req: - res := <-req.res - return res.data, res.err - - case <-s.ctx.Done(): - return nil, fmt.Errorf("terminated") - } -} - -// apiConnsGet is called by api. -func (s *rtmpServer) apiConnsGet(uuid uuid.UUID) (*defs.APIRTMPConn, error) { - req := rtmpServerAPIConnsGetReq{ - uuid: uuid, - res: make(chan rtmpServerAPIConnsGetRes), - } - - select { - case s.chAPIConnsGet <- req: - res := <-req.res - return res.data, res.err - - case <-s.ctx.Done(): - return nil, fmt.Errorf("terminated") - } -} - -// apiConnsKick is called by api. -func (s *rtmpServer) apiConnsKick(uuid uuid.UUID) error { - req := rtmpServerAPIConnsKickReq{ - uuid: uuid, - res: make(chan rtmpServerAPIConnsKickRes), - } - - select { - case s.chAPIConnsKick <- req: - res := <-req.res - return res.err - - case <-s.ctx.Done(): - return fmt.Errorf("terminated") - } -} diff --git a/internal/core/rtsp_server.go b/internal/core/rtsp_server.go deleted file mode 100644 index 96ba9444..00000000 --- a/internal/core/rtsp_server.go +++ /dev/null @@ -1,454 +0,0 @@ -package core - -import ( - "context" - "crypto/tls" - "fmt" - "sort" - "strings" - "sync" - "time" - - "github.com/bluenviron/gortsplib/v4" - "github.com/bluenviron/gortsplib/v4/pkg/base" - "github.com/bluenviron/gortsplib/v4/pkg/headers" - "github.com/bluenviron/gortsplib/v4/pkg/liberrors" - "github.com/google/uuid" - - "github.com/bluenviron/mediamtx/internal/conf" - "github.com/bluenviron/mediamtx/internal/defs" - "github.com/bluenviron/mediamtx/internal/externalcmd" - "github.com/bluenviron/mediamtx/internal/logger" -) - -type rtspServerParent interface { - logger.Writer -} - -func printAddresses(srv *gortsplib.Server) string { - var ret []string - - ret = append(ret, fmt.Sprintf("%s (TCP)", srv.RTSPAddress)) - - if srv.UDPRTPAddress != "" { - ret = append(ret, fmt.Sprintf("%s (UDP/RTP)", srv.UDPRTPAddress)) - } - - if srv.UDPRTCPAddress != "" { - ret = append(ret, fmt.Sprintf("%s (UDP/RTCP)", srv.UDPRTCPAddress)) - } - - return strings.Join(ret, ", ") -} - -type rtspServer struct { - authMethods []headers.AuthMethod - readTimeout conf.StringDuration - isTLS bool - rtspAddress string - protocols map[conf.Protocol]struct{} - runOnConnect string - runOnConnectRestart bool - runOnDisconnect string - externalCmdPool *externalcmd.Pool - pathManager *pathManager - parent rtspServerParent - - ctx context.Context - ctxCancel func() - wg sync.WaitGroup - srv *gortsplib.Server - mutex sync.RWMutex - conns map[*gortsplib.ServerConn]*rtspConn - sessions map[*gortsplib.ServerSession]*rtspSession -} - -func newRTSPServer( - address string, - authMethods []headers.AuthMethod, - readTimeout conf.StringDuration, - writeTimeout conf.StringDuration, - writeQueueSize int, - useUDP bool, - useMulticast bool, - rtpAddress string, - rtcpAddress string, - multicastIPRange string, - multicastRTPPort int, - multicastRTCPPort int, - isTLS bool, - serverCert string, - serverKey string, - rtspAddress string, - protocols map[conf.Protocol]struct{}, - runOnConnect string, - runOnConnectRestart bool, - runOnDisconnect string, - externalCmdPool *externalcmd.Pool, - pathManager *pathManager, - parent rtspServerParent, -) (*rtspServer, error) { - ctx, ctxCancel := context.WithCancel(context.Background()) - - s := &rtspServer{ - authMethods: authMethods, - readTimeout: readTimeout, - isTLS: isTLS, - rtspAddress: rtspAddress, - protocols: protocols, - runOnConnect: runOnConnect, - runOnConnectRestart: runOnConnectRestart, - runOnDisconnect: runOnDisconnect, - externalCmdPool: externalCmdPool, - pathManager: pathManager, - parent: parent, - ctx: ctx, - ctxCancel: ctxCancel, - conns: make(map[*gortsplib.ServerConn]*rtspConn), - sessions: make(map[*gortsplib.ServerSession]*rtspSession), - } - - s.srv = &gortsplib.Server{ - Handler: s, - ReadTimeout: time.Duration(readTimeout), - WriteTimeout: time.Duration(writeTimeout), - WriteQueueSize: writeQueueSize, - RTSPAddress: address, - } - - if useUDP { - s.srv.UDPRTPAddress = rtpAddress - s.srv.UDPRTCPAddress = rtcpAddress - } - - if useMulticast { - s.srv.MulticastIPRange = multicastIPRange - s.srv.MulticastRTPPort = multicastRTPPort - s.srv.MulticastRTCPPort = multicastRTCPPort - } - - if isTLS { - cert, err := tls.LoadX509KeyPair(serverCert, serverKey) - if err != nil { - return nil, err - } - - s.srv.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}} - } - - err := s.srv.Start() - if err != nil { - return nil, err - } - - s.Log(logger.Info, "listener opened on %s", printAddresses(s.srv)) - - s.wg.Add(1) - go s.run() - - return s, nil -} - -func (s *rtspServer) Log(level logger.Level, format string, args ...interface{}) { - label := func() string { - if s.isTLS { - return "RTSPS" - } - return "RTSP" - }() - s.parent.Log(level, "[%s] "+format, append([]interface{}{label}, args...)...) -} - -func (s *rtspServer) getISTLS() bool { - return s.isTLS -} - -func (s *rtspServer) getServer() *gortsplib.Server { - return s.srv -} - -func (s *rtspServer) close() { - s.Log(logger.Info, "listener is closing") - s.ctxCancel() - s.wg.Wait() -} - -func (s *rtspServer) run() { - defer s.wg.Done() - - serverErr := make(chan error) - go func() { - serverErr <- s.srv.Wait() - }() - -outer: - select { - case err := <-serverErr: - s.Log(logger.Error, "%s", err) - break outer - - case <-s.ctx.Done(): - s.srv.Close() - <-serverErr - break outer - } - - s.ctxCancel() -} - -// OnConnOpen implements gortsplib.ServerHandlerOnConnOpen. -func (s *rtspServer) OnConnOpen(ctx *gortsplib.ServerHandlerOnConnOpenCtx) { - c := newRTSPConn( - s.isTLS, - s.rtspAddress, - s.authMethods, - s.readTimeout, - s.runOnConnect, - s.runOnConnectRestart, - s.runOnDisconnect, - s.externalCmdPool, - s.pathManager, - ctx.Conn, - s) - s.mutex.Lock() - s.conns[ctx.Conn] = c - s.mutex.Unlock() - - ctx.Conn.SetUserData(c) -} - -// OnConnClose implements gortsplib.ServerHandlerOnConnClose. -func (s *rtspServer) OnConnClose(ctx *gortsplib.ServerHandlerOnConnCloseCtx) { - s.mutex.Lock() - c := s.conns[ctx.Conn] - delete(s.conns, ctx.Conn) - s.mutex.Unlock() - c.onClose(ctx.Error) -} - -// OnRequest implements gortsplib.ServerHandlerOnRequest. -func (s *rtspServer) OnRequest(sc *gortsplib.ServerConn, req *base.Request) { - c := sc.UserData().(*rtspConn) - c.onRequest(req) -} - -// OnResponse implements gortsplib.ServerHandlerOnResponse. -func (s *rtspServer) OnResponse(sc *gortsplib.ServerConn, res *base.Response) { - c := sc.UserData().(*rtspConn) - c.OnResponse(res) -} - -// OnSessionOpen implements gortsplib.ServerHandlerOnSessionOpen. -func (s *rtspServer) OnSessionOpen(ctx *gortsplib.ServerHandlerOnSessionOpenCtx) { - se := newRTSPSession( - s.isTLS, - s.protocols, - ctx.Session, - ctx.Conn, - s.externalCmdPool, - s.pathManager, - s) - s.mutex.Lock() - s.sessions[ctx.Session] = se - s.mutex.Unlock() - ctx.Session.SetUserData(se) -} - -// OnSessionClose implements gortsplib.ServerHandlerOnSessionClose. -func (s *rtspServer) OnSessionClose(ctx *gortsplib.ServerHandlerOnSessionCloseCtx) { - s.mutex.Lock() - se := s.sessions[ctx.Session] - delete(s.sessions, ctx.Session) - s.mutex.Unlock() - - if se != nil { - se.onClose(ctx.Error) - } -} - -// OnDescribe implements gortsplib.ServerHandlerOnDescribe. -func (s *rtspServer) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, -) (*base.Response, *gortsplib.ServerStream, error) { - c := ctx.Conn.UserData().(*rtspConn) - return c.onDescribe(ctx) -} - -// OnAnnounce implements gortsplib.ServerHandlerOnAnnounce. -func (s *rtspServer) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) { - c := ctx.Conn.UserData().(*rtspConn) - se := ctx.Session.UserData().(*rtspSession) - return se.onAnnounce(c, ctx) -} - -// OnSetup implements gortsplib.ServerHandlerOnSetup. -func (s *rtspServer) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) { - c := ctx.Conn.UserData().(*rtspConn) - se := ctx.Session.UserData().(*rtspSession) - return se.onSetup(c, ctx) -} - -// OnPlay implements gortsplib.ServerHandlerOnPlay. -func (s *rtspServer) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { - se := ctx.Session.UserData().(*rtspSession) - return se.onPlay(ctx) -} - -// OnRecord implements gortsplib.ServerHandlerOnRecord. -func (s *rtspServer) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) { - se := ctx.Session.UserData().(*rtspSession) - return se.onRecord(ctx) -} - -// OnPause implements gortsplib.ServerHandlerOnPause. -func (s *rtspServer) OnPause(ctx *gortsplib.ServerHandlerOnPauseCtx) (*base.Response, error) { - se := ctx.Session.UserData().(*rtspSession) - return se.onPause(ctx) -} - -// OnPacketLost implements gortsplib.ServerHandlerOnDecodeError. -func (s *rtspServer) OnPacketLost(ctx *gortsplib.ServerHandlerOnPacketLostCtx) { - se := ctx.Session.UserData().(*rtspSession) - se.onPacketLost(ctx) -} - -// OnDecodeError implements gortsplib.ServerHandlerOnDecodeError. -func (s *rtspServer) OnDecodeError(ctx *gortsplib.ServerHandlerOnDecodeErrorCtx) { - se := ctx.Session.UserData().(*rtspSession) - se.onDecodeError(ctx) -} - -// OnDecodeError implements gortsplib.ServerHandlerOnStreamWriteError. -func (s *rtspServer) OnStreamWriteError(ctx *gortsplib.ServerHandlerOnStreamWriteErrorCtx) { - se := ctx.Session.UserData().(*rtspSession) - se.onStreamWriteError(ctx) -} - -func (s *rtspServer) findConnByUUID(uuid uuid.UUID) *rtspConn { - for _, c := range s.conns { - if c.uuid == uuid { - return c - } - } - return nil -} - -func (s *rtspServer) findSessionByUUID(uuid uuid.UUID) (*gortsplib.ServerSession, *rtspSession) { - for key, sx := range s.sessions { - if sx.uuid == uuid { - return key, sx - } - } - return nil, nil -} - -// apiConnsList is called by api and metrics. -func (s *rtspServer) apiConnsList() (*defs.APIRTSPConnsList, error) { - select { - case <-s.ctx.Done(): - return nil, fmt.Errorf("terminated") - default: - } - - s.mutex.RLock() - defer s.mutex.RUnlock() - - data := &defs.APIRTSPConnsList{ - Items: []*defs.APIRTSPConn{}, - } - - 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) - }) - - return data, nil -} - -// apiConnsGet is called by api. -func (s *rtspServer) apiConnsGet(uuid uuid.UUID) (*defs.APIRTSPConn, error) { - select { - case <-s.ctx.Done(): - return nil, fmt.Errorf("terminated") - default: - } - - s.mutex.RLock() - defer s.mutex.RUnlock() - - conn := s.findConnByUUID(uuid) - if conn == nil { - return nil, fmt.Errorf("connection not found") - } - - return conn.apiItem(), nil -} - -// apiSessionsList is called by api and metrics. -func (s *rtspServer) apiSessionsList() (*defs.APIRTSPSessionList, error) { - select { - case <-s.ctx.Done(): - return nil, fmt.Errorf("terminated") - default: - } - - s.mutex.RLock() - defer s.mutex.RUnlock() - - data := &defs.APIRTSPSessionList{ - Items: []*defs.APIRTSPSession{}, - } - - for _, s := range s.sessions { - data.Items = append(data.Items, s.apiItem()) - } - - sort.Slice(data.Items, func(i, j int) bool { - return data.Items[i].Created.Before(data.Items[j].Created) - }) - - return data, nil -} - -// apiSessionsGet is called by api. -func (s *rtspServer) apiSessionsGet(uuid uuid.UUID) (*defs.APIRTSPSession, error) { - select { - case <-s.ctx.Done(): - return nil, fmt.Errorf("terminated") - default: - } - - s.mutex.RLock() - defer s.mutex.RUnlock() - - _, sx := s.findSessionByUUID(uuid) - if sx == nil { - return nil, fmt.Errorf("session not found") - } - - return sx.apiItem(), nil -} - -// apiSessionsKick is called by api. -func (s *rtspServer) apiSessionsKick(uuid uuid.UUID) error { - select { - case <-s.ctx.Done(): - return fmt.Errorf("terminated") - default: - } - - s.mutex.RLock() - defer s.mutex.RUnlock() - - key, sx := s.findSessionByUUID(uuid) - if sx == nil { - return fmt.Errorf("session not found") - } - - sx.close() - delete(s.sessions, key) - sx.onClose(liberrors.ErrServerTerminated{}) - return nil -} diff --git a/internal/core/srt_server.go b/internal/core/srt_server.go deleted file mode 100644 index 96fa0721..00000000 --- a/internal/core/srt_server.go +++ /dev/null @@ -1,330 +0,0 @@ -package core - -import ( - "context" - "fmt" - "sort" - "sync" - "time" - - srt "github.com/datarhei/gosrt" - "github.com/google/uuid" - - "github.com/bluenviron/mediamtx/internal/conf" - "github.com/bluenviron/mediamtx/internal/defs" - "github.com/bluenviron/mediamtx/internal/externalcmd" - "github.com/bluenviron/mediamtx/internal/logger" -) - -func srtMaxPayloadSize(u int) int { - return ((u - 16) / 188) * 188 // 16 = SRT header, 188 = MPEG-TS packet -} - -type srtNewConnReq struct { - connReq srt.ConnRequest - res chan *srtConn -} - -type srtServerAPIConnsListRes struct { - data *defs.APISRTConnList - err error -} - -type srtServerAPIConnsListReq struct { - res chan srtServerAPIConnsListRes -} - -type srtServerAPIConnsGetRes struct { - data *defs.APISRTConn - err error -} - -type srtServerAPIConnsGetReq struct { - uuid uuid.UUID - res chan srtServerAPIConnsGetRes -} - -type srtServerAPIConnsKickRes struct { - err error -} - -type srtServerAPIConnsKickReq struct { - uuid uuid.UUID - res chan srtServerAPIConnsKickRes -} - -type srtServerParent interface { - logger.Writer -} - -type srtServer struct { - rtspAddress string - readTimeout conf.StringDuration - writeTimeout conf.StringDuration - writeQueueSize int - udpMaxPayloadSize int - runOnConnect string - runOnConnectRestart bool - runOnDisconnect string - externalCmdPool *externalcmd.Pool - pathManager *pathManager - parent srtServerParent - - ctx context.Context - ctxCancel func() - wg sync.WaitGroup - ln srt.Listener - conns map[*srtConn]struct{} - - // in - chNewConnRequest chan srtNewConnReq - chAcceptErr chan error - chCloseConn chan *srtConn - chAPIConnsList chan srtServerAPIConnsListReq - chAPIConnsGet chan srtServerAPIConnsGetReq - chAPIConnsKick chan srtServerAPIConnsKickReq -} - -func newSRTServer( - address string, - rtspAddress string, - readTimeout conf.StringDuration, - writeTimeout conf.StringDuration, - writeQueueSize int, - udpMaxPayloadSize int, - runOnConnect string, - runOnConnectRestart bool, - runOnDisconnect string, - externalCmdPool *externalcmd.Pool, - pathManager *pathManager, - parent srtServerParent, -) (*srtServer, error) { - conf := srt.DefaultConfig() - conf.ConnectionTimeout = time.Duration(readTimeout) - conf.PayloadSize = uint32(srtMaxPayloadSize(udpMaxPayloadSize)) - - ln, err := srt.Listen("srt", address, conf) - if err != nil { - return nil, err - } - - ctx, ctxCancel := context.WithCancel(context.Background()) - - s := &srtServer{ - rtspAddress: rtspAddress, - readTimeout: readTimeout, - writeTimeout: writeTimeout, - writeQueueSize: writeQueueSize, - udpMaxPayloadSize: udpMaxPayloadSize, - runOnConnect: runOnConnect, - runOnConnectRestart: runOnConnectRestart, - runOnDisconnect: runOnDisconnect, - externalCmdPool: externalCmdPool, - pathManager: pathManager, - parent: parent, - ctx: ctx, - ctxCancel: ctxCancel, - ln: ln, - conns: make(map[*srtConn]struct{}), - chNewConnRequest: make(chan srtNewConnReq), - chAcceptErr: make(chan error), - chCloseConn: make(chan *srtConn), - chAPIConnsList: make(chan srtServerAPIConnsListReq), - chAPIConnsGet: make(chan srtServerAPIConnsGetReq), - chAPIConnsKick: make(chan srtServerAPIConnsKickReq), - } - - s.Log(logger.Info, "listener opened on "+address+" (UDP)") - - newSRTListener( - s.ln, - &s.wg, - s, - ) - - s.wg.Add(1) - go s.run() - - return s, nil -} - -// Log is the main logging function. -func (s *srtServer) Log(level logger.Level, format string, args ...interface{}) { - s.parent.Log(level, "[SRT] "+format, args...) -} - -func (s *srtServer) close() { - s.Log(logger.Info, "listener is closing") - s.ctxCancel() - s.wg.Wait() -} - -func (s *srtServer) run() { - defer s.wg.Done() - -outer: - for { - select { - case err := <-s.chAcceptErr: - s.Log(logger.Error, "%s", err) - break outer - - case req := <-s.chNewConnRequest: - c := newSRTConn( - s.ctx, - s.rtspAddress, - s.readTimeout, - s.writeTimeout, - s.writeQueueSize, - s.udpMaxPayloadSize, - req.connReq, - s.runOnConnect, - s.runOnConnectRestart, - s.runOnDisconnect, - &s.wg, - s.externalCmdPool, - s.pathManager, - s) - s.conns[c] = struct{}{} - req.res <- c - - case c := <-s.chCloseConn: - delete(s.conns, c) - - case req := <-s.chAPIConnsList: - data := &defs.APISRTConnList{ - Items: []*defs.APISRTConn{}, - } - - 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 <- srtServerAPIConnsListRes{data: data} - - case req := <-s.chAPIConnsGet: - c := s.findConnByUUID(req.uuid) - if c == nil { - req.res <- srtServerAPIConnsGetRes{err: fmt.Errorf("connection not found")} - continue - } - - req.res <- srtServerAPIConnsGetRes{data: c.apiItem()} - - case req := <-s.chAPIConnsKick: - c := s.findConnByUUID(req.uuid) - if c == nil { - req.res <- srtServerAPIConnsKickRes{err: fmt.Errorf("connection not found")} - continue - } - - delete(s.conns, c) - c.close() - req.res <- srtServerAPIConnsKickRes{} - - case <-s.ctx.Done(): - break outer - } - } - - s.ctxCancel() - - s.ln.Close() -} - -func (s *srtServer) findConnByUUID(uuid uuid.UUID) *srtConn { - for sx := range s.conns { - if sx.uuid == uuid { - return sx - } - } - return nil -} - -// newConnRequest is called by srtListener. -func (s *srtServer) newConnRequest(connReq srt.ConnRequest) *srtConn { - req := srtNewConnReq{ - connReq: connReq, - res: make(chan *srtConn), - } - - select { - case s.chNewConnRequest <- req: - c := <-req.res - - return c.new(req) - - case <-s.ctx.Done(): - return nil - } -} - -// acceptError is called by srtListener. -func (s *srtServer) acceptError(err error) { - select { - case s.chAcceptErr <- err: - case <-s.ctx.Done(): - } -} - -// closeConn is called by srtConn. -func (s *srtServer) closeConn(c *srtConn) { - select { - case s.chCloseConn <- c: - case <-s.ctx.Done(): - } -} - -// apiConnsList is called by api. -func (s *srtServer) apiConnsList() (*defs.APISRTConnList, error) { - req := srtServerAPIConnsListReq{ - res: make(chan srtServerAPIConnsListRes), - } - - select { - case s.chAPIConnsList <- req: - res := <-req.res - return res.data, res.err - - case <-s.ctx.Done(): - return nil, fmt.Errorf("terminated") - } -} - -// apiConnsGet is called by api. -func (s *srtServer) apiConnsGet(uuid uuid.UUID) (*defs.APISRTConn, error) { - req := srtServerAPIConnsGetReq{ - uuid: uuid, - res: make(chan srtServerAPIConnsGetRes), - } - - select { - case s.chAPIConnsGet <- req: - res := <-req.res - return res.data, res.err - - case <-s.ctx.Done(): - return nil, fmt.Errorf("terminated") - } -} - -// apiConnsKick is called by api. -func (s *srtServer) apiConnsKick(uuid uuid.UUID) error { - req := srtServerAPIConnsKickReq{ - uuid: uuid, - res: make(chan srtServerAPIConnsKickRes), - } - - select { - case s.chAPIConnsKick <- req: - res := <-req.res - return res.err - - case <-s.ctx.Done(): - return fmt.Errorf("terminated") - } -} diff --git a/internal/core/static_source_handler.go b/internal/core/static_source_handler.go index 34184cd8..1a329419 100644 --- a/internal/core/static_source_handler.go +++ b/internal/core/static_source_handler.go @@ -153,6 +153,7 @@ func (s *staticSourceHandler) stop(reason string) { <-s.done } +// Log implements logger.Writer. func (s *staticSourceHandler) Log(level logger.Level, format string, args ...interface{}) { s.parent.Log(level, format, args...) } @@ -242,7 +243,7 @@ func (s *staticSourceHandler) SetReady(req defs.PathSourceStaticSetReadyReq) def res := <-req.Res if res.Err == nil { - s.instance.Log(logger.Info, "ready: %s", mediaInfo(req.Desc.Medias)) + s.instance.Log(logger.Info, "ready: %s", defs.MediasInfo(req.Desc.Medias)) } return res diff --git a/internal/core/webrtc_manager_test.go b/internal/core/webrtc_server_test.go similarity index 100% rename from internal/core/webrtc_manager_test.go rename to internal/core/webrtc_server_test.go diff --git a/internal/defs/auth.go b/internal/defs/auth.go new file mode 100644 index 00000000..98266f1c --- /dev/null +++ b/internal/defs/auth.go @@ -0,0 +1,23 @@ +package defs + +// AuthProtocol is a authentication protocol. +type AuthProtocol string + +// authentication protocols. +const ( + AuthProtocolRTSP AuthProtocol = "rtsp" + AuthProtocolRTMP AuthProtocol = "rtmp" + AuthProtocolHLS AuthProtocol = "hls" + AuthProtocolWebRTC AuthProtocol = "webrtc" + AuthProtocolSRT AuthProtocol = "srt" +) + +// ErrAuthentication is a authentication error. +type ErrAuthentication struct { + Message string +} + +// Error implements the error interface. +func (e *ErrAuthentication) Error() string { + return "authentication failed: " + e.Message +} diff --git a/internal/defs/path.go b/internal/defs/path.go index 8877ea4c..fe061005 100644 --- a/internal/defs/path.go +++ b/internal/defs/path.go @@ -1,25 +1,156 @@ package defs import ( + "fmt" + "net" + + "github.com/bluenviron/gortsplib/v4/pkg/base" "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/google/uuid" + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/stream" ) -// PathSourceStaticSetReadyRes is a set ready response to a static source. +// ErrPathNoOnePublishing is returned when no one is publishing. +type ErrPathNoOnePublishing struct { + PathName string +} + +// Error implements the error interface. +func (e ErrPathNoOnePublishing) Error() string { + return fmt.Sprintf("no one is publishing to path '%s'", e.PathName) +} + +// Path is a path. +type Path interface { + Name() string + SafeConf() *conf.Path + ExternalCmdEnv() externalcmd.Environment + StartPublisher(req PathStartPublisherReq) PathStartPublisherRes + StopPublisher(req PathStopPublisherReq) + RemovePublisher(req PathRemovePublisherReq) + RemoveReader(req PathRemoveReaderReq) +} + +// PathAccessRequest is an access request. +type PathAccessRequest struct { + Name string + Query string + Publish bool + SkipAuth bool + + // only if skipAuth = false + IP net.IP + User string + Pass string + Proto AuthProtocol + ID *uuid.UUID + RTSPRequest *base.Request + RTSPBaseURL *base.URL + RTSPNonce string +} + +// PathGetConfForPathRes contains the response of GetConfForPath(). +type PathGetConfForPathRes struct { + Conf *conf.Path + Err error +} + +// PathGetConfForPathReq contains arguments of GetConfForPath(). +type PathGetConfForPathReq struct { + AccessRequest PathAccessRequest + Res chan PathGetConfForPathRes +} + +// PathDescribeRes contains the response of Describe(). +type PathDescribeRes struct { + Path Path + Stream *stream.Stream + Redirect string + Err error +} + +// PathDescribeReq contains arguments of Describe(). +type PathDescribeReq struct { + AccessRequest PathAccessRequest + Res chan PathDescribeRes +} + +// PathAddPublisherRes contains the response of AddPublisher(). +type PathAddPublisherRes struct { + Path Path + Err error +} + +// PathAddPublisherReq contains arguments of AddPublisher(). +type PathAddPublisherReq struct { + Author Publisher + AccessRequest PathAccessRequest + Res chan PathAddPublisherRes +} + +// PathRemovePublisherReq contains arguments of RemovePublisher(). +type PathRemovePublisherReq struct { + Author Publisher + Res chan struct{} +} + +// PathStartPublisherRes contains the response of StartPublisher(). +type PathStartPublisherRes struct { + Stream *stream.Stream + Err error +} + +// PathStartPublisherReq contains arguments of StartPublisher(). +type PathStartPublisherReq struct { + Author Publisher + Desc *description.Session + GenerateRTPPackets bool + Res chan PathStartPublisherRes +} + +// PathStopPublisherReq contains arguments of StopPublisher(). +type PathStopPublisherReq struct { + Author Publisher + Res chan struct{} +} + +// PathAddReaderRes contains the response of AddReader(). +type PathAddReaderRes struct { + Path Path + Stream *stream.Stream + Err error +} + +// PathAddReaderReq contains arguments of AddReader(). +type PathAddReaderReq struct { + Author Reader + AccessRequest PathAccessRequest + Res chan PathAddReaderRes +} + +// PathRemoveReaderReq contains arguments of RemoveReader(). +type PathRemoveReaderReq struct { + Author Reader + Res chan struct{} +} + +// PathSourceStaticSetReadyRes contains the response of SetReadu(). type PathSourceStaticSetReadyRes struct { Stream *stream.Stream Err error } -// PathSourceStaticSetReadyReq is a set ready request from a static source. +// PathSourceStaticSetReadyReq contains arguments of SetReady(). type PathSourceStaticSetReadyReq struct { Desc *description.Session GenerateRTPPackets bool Res chan PathSourceStaticSetReadyRes } -// PathSourceStaticSetNotReadyReq is a set not ready request from a static source. +// PathSourceStaticSetNotReadyReq contains arguments of SetNotReady(). type PathSourceStaticSetNotReadyReq struct { Res chan struct{} } diff --git a/internal/defs/path_manager.go b/internal/defs/path_manager.go new file mode 100644 index 00000000..b6e5c715 --- /dev/null +++ b/internal/defs/path_manager.go @@ -0,0 +1,9 @@ +package defs + +// PathManager is a path manager. +type PathManager interface { + GetConfForPath(req PathGetConfForPathReq) PathGetConfForPathRes + Describe(req PathDescribeReq) PathDescribeRes + AddPublisher(req PathAddPublisherReq) PathAddPublisherRes + AddReader(req PathAddReaderReq) PathAddReaderRes +} diff --git a/internal/defs/publisher.go b/internal/defs/publisher.go new file mode 100644 index 00000000..72ffc74d --- /dev/null +++ b/internal/defs/publisher.go @@ -0,0 +1,7 @@ +package defs + +// Publisher is an entity that can publish a stream. +type Publisher interface { + Source + Close() +} diff --git a/internal/defs/reader.go b/internal/defs/reader.go new file mode 100644 index 00000000..c6f9f640 --- /dev/null +++ b/internal/defs/reader.go @@ -0,0 +1,7 @@ +package defs + +// Reader is an entity that can read a stream. +type Reader interface { + Close() + APIReaderDescribe() APIPathSourceOrReader +} diff --git a/internal/core/source.go b/internal/defs/source.go similarity index 63% rename from internal/core/source.go rename to internal/defs/source.go index ca0a58f2..0a0bcab0 100644 --- a/internal/core/source.go +++ b/internal/defs/source.go @@ -1,4 +1,4 @@ -package core +package defs import ( "fmt" @@ -6,18 +6,17 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/description" - "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/logger" ) -// source is an entity that can provide a stream. +// Source is an entity that can provide a stream. // it can be: // - publisher // - staticSourceHandler // - redirectSource -type source interface { +type Source interface { logger.Writer - APISourceDescribe() defs.APIPathSourceOrReader + APISourceDescribe() APIPathSourceOrReader } func mediaDescription(media *description.Media) string { @@ -28,7 +27,8 @@ func mediaDescription(media *description.Media) string { return strings.Join(ret, "/") } -func mediasDescription(medias []*description.Media) []string { +// MediasDescription returns the description of medias. +func MediasDescription(medias []*description.Media) []string { ret := make([]string, len(medias)) for i, media := range medias { ret[i] = mediaDescription(media) @@ -36,7 +36,8 @@ func mediasDescription(medias []*description.Media) []string { return ret } -func mediaInfo(medias []*description.Media) string { +// MediasInfo returns the description of medias. +func MediasInfo(medias []*description.Media) string { return fmt.Sprintf("%d %s (%s)", len(medias), func() string { @@ -45,5 +46,5 @@ func mediaInfo(medias []*description.Media) string { } return "tracks" }(), - strings.Join(mediasDescription(medias), ", ")) + strings.Join(MediasDescription(medias), ", ")) } diff --git a/internal/core/mpegts.go b/internal/protocols/mpegts/from_stream.go similarity index 97% rename from internal/core/mpegts.go rename to internal/protocols/mpegts/from_stream.go index ec533245..b1a91344 100644 --- a/internal/core/mpegts.go +++ b/internal/protocols/mpegts/from_stream.go @@ -1,4 +1,4 @@ -package core +package mpegts import ( "bufio" @@ -10,10 +10,9 @@ import ( "github.com/bluenviron/mediacommon/pkg/codecs/h264" "github.com/bluenviron/mediacommon/pkg/codecs/h265" mcmpegts "github.com/bluenviron/mediacommon/pkg/formats/mpegts" - "github.com/datarhei/gosrt" + srt "github.com/datarhei/gosrt" "github.com/bluenviron/mediamtx/internal/asyncwriter" - "github.com/bluenviron/mediamtx/internal/protocols/mpegts" "github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/unit" ) @@ -22,7 +21,8 @@ func durationGoToMPEGTS(v time.Duration) int64 { return int64(v.Seconds() * 90000) } -func mpegtsSetupWrite( +// FromStream links a server stream to a MPEG-TS writer. +func FromStream( stream *stream.Stream, writer *asyncwriter.Writer, bw *bufio.Writer, @@ -251,7 +251,7 @@ func mpegtsSetupWrite( } if len(tracks) == 0 { - return mpegts.ErrNoTracks + return ErrNoTracks } w = mcmpegts.NewWriter(bw, tracks) diff --git a/internal/record/agent.go b/internal/record/agent.go index e4503a37..7ecde7db 100644 --- a/internal/record/agent.go +++ b/internal/record/agent.go @@ -54,7 +54,7 @@ func (w *Agent) Initialize() { go w.run() } -// Log is the main logging function. +// Log implements logger.Writer. func (w *Agent) Log(level logger.Level, format string, args ...interface{}) { w.Parent.Log(level, "[record] "+format, args...) } diff --git a/internal/record/cleaner.go b/internal/record/cleaner.go index 2d606459..afcc18b4 100644 --- a/internal/record/cleaner.go +++ b/internal/record/cleaner.go @@ -71,7 +71,7 @@ func (c *Cleaner) Close() { <-c.done } -// Log is the main logging function. +// Log implements logger.Writer. func (c *Cleaner) Log(level logger.Level, format string, args ...interface{}) { c.Parent.Log(level, "[record cleaner]"+format, args...) } diff --git a/internal/core/hls.min.js b/internal/servers/hls/hls.min.js similarity index 100% rename from internal/core/hls.min.js rename to internal/servers/hls/hls.min.js diff --git a/internal/core/hls_http_server.go b/internal/servers/hls/http_server.go similarity index 65% rename from internal/core/hls_http_server.go rename to internal/servers/hls/http_server.go index cd8a3b8a..bfba4c45 100644 --- a/internal/core/hls_http_server.go +++ b/internal/servers/hls/http_server.go @@ -1,4 +1,4 @@ -package core +package hls import ( _ "embed" @@ -12,6 +12,7 @@ import ( "github.com/gin-gonic/gin" "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/protocols/httpserv" "github.com/bluenviron/mediamtx/internal/restrictnetwork" @@ -21,84 +22,70 @@ const ( hlsPauseAfterAuthError = 2 * time.Second ) -//go:embed hls_index.html +//go:embed index.html var hlsIndex []byte //go:embed hls.min.js var hlsMinJS []byte -type hlsHTTPServerParent interface { - logger.Writer - handleRequest(req hlsMuxerHandleRequestReq) -} - -type hlsHTTPServer struct { - allowOrigin string - pathManager *pathManager - parent hlsHTTPServerParent +type httpServer struct { + address string + encryption bool + serverKey string + serverCert string + allowOrigin string + trustedProxies conf.IPsOrCIDRs + readTimeout conf.StringDuration + pathManager defs.PathManager + parent *Server inner *httpserv.WrappedServer } -func newHLSHTTPServer( //nolint:dupl - address string, - encryption bool, - serverKey string, - serverCert string, - allowOrigin string, - trustedProxies conf.IPsOrCIDRs, - readTimeout conf.StringDuration, - pathManager *pathManager, - parent hlsHTTPServerParent, -) (*hlsHTTPServer, error) { - if encryption { - if serverCert == "" { - return nil, fmt.Errorf("server cert is missing") +func (s *httpServer) initialize() error { + if s.encryption { + if s.serverCert == "" { + return fmt.Errorf("server cert is missing") } } else { - serverKey = "" - serverCert = "" - } - - s := &hlsHTTPServer{ - allowOrigin: allowOrigin, - pathManager: pathManager, - parent: parent, + s.serverKey = "" + s.serverCert = "" } router := gin.New() - router.SetTrustedProxies(trustedProxies.ToTrustedProxies()) //nolint:errcheck + router.SetTrustedProxies(s.trustedProxies.ToTrustedProxies()) //nolint:errcheck router.NoRoute(s.onRequest) - network, address := restrictnetwork.Restrict("tcp", address) + network, address := restrictnetwork.Restrict("tcp", s.address) var err error s.inner, err = httpserv.NewWrappedServer( network, address, - time.Duration(readTimeout), - serverCert, - serverKey, + time.Duration(s.readTimeout), + s.serverCert, + s.serverKey, router, s, ) if err != nil { - return nil, err + return err } - return s, nil + return nil } -func (s *hlsHTTPServer) Log(level logger.Level, format string, args ...interface{}) { +// Log implements logger.Writer. +func (s *httpServer) Log(level logger.Level, format string, args ...interface{}) { s.parent.Log(level, format, args...) } -func (s *hlsHTTPServer) close() { +func (s *httpServer) close() { s.inner.Close() } -func (s *hlsHTTPServer) onRequest(ctx *gin.Context) { +func (s *httpServer) onRequest(ctx *gin.Context) { ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.allowOrigin) ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") @@ -159,19 +146,19 @@ func (s *hlsHTTPServer) onRequest(ctx *gin.Context) { user, pass, hasCredentials := ctx.Request.BasicAuth() - res := s.pathManager.getConfForPath(pathGetConfForPathReq{ - accessRequest: pathAccessRequest{ - name: dir, - query: ctx.Request.URL.RawQuery, - publish: false, - ip: net.ParseIP(ctx.ClientIP()), - user: user, - pass: pass, - proto: authProtocolHLS, + res := s.pathManager.GetConfForPath(defs.PathGetConfForPathReq{ + AccessRequest: defs.PathAccessRequest{ + Name: dir, + Query: ctx.Request.URL.RawQuery, + Publish: false, + IP: net.ParseIP(ctx.ClientIP()), + User: user, + Pass: pass, + Proto: defs.AuthProtocolHLS, }, }) - if res.err != nil { - if terr, ok := res.err.(*errAuthentication); ok { + if res.Err != nil { + if terr, ok := res.Err.(*defs.ErrAuthentication); ok { if !hasCredentials { ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.Writer.WriteHeader(http.StatusUnauthorized) @@ -182,7 +169,7 @@ func (s *hlsHTTPServer) onRequest(ctx *gin.Context) { _, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr) remoteAddr := net.JoinHostPort(ip, port) - s.Log(logger.Info, "connection %v failed to authenticate: %v", remoteAddr, terr.message) + s.Log(logger.Info, "connection %v failed to authenticate: %v", remoteAddr, terr.Message) // wait some seconds to stop brute force attacks <-time.After(hlsPauseAfterAuthError) @@ -203,7 +190,7 @@ func (s *hlsHTTPServer) onRequest(ctx *gin.Context) { ctx.Writer.Write(hlsIndex) default: - s.parent.handleRequest(hlsMuxerHandleRequestReq{ + s.parent.handleRequest(muxerHandleRequestReq{ path: dir, file: fname, ctx: ctx, diff --git a/internal/core/hls_index.html b/internal/servers/hls/index.html similarity index 100% rename from internal/core/hls_index.html rename to internal/servers/hls/index.html diff --git a/internal/core/hls_muxer.go b/internal/servers/hls/muxer.go similarity index 74% rename from internal/core/hls_muxer.go rename to internal/servers/hls/muxer.go index f3a854ed..75f2620e 100644 --- a/internal/core/hls_muxer.go +++ b/internal/servers/hls/muxer.go @@ -1,4 +1,4 @@ -package core +package hls import ( "context" @@ -26,15 +26,21 @@ import ( ) const ( - closeCheckPeriod = 1 * time.Second - closeAfterInactivity = 60 * time.Second - hlsMuxerRecreatePause = 10 * time.Second + closeCheckPeriod = 1 * time.Second + closeAfterInactivity = 60 * time.Second + muxerRecreatePause = 10 * time.Second ) func int64Ptr(v int64) *int64 { return &v } +func newEmptyTimer() *time.Timer { + t := time.NewTimer(0) + <-t.C + return t +} + type responseWriterWithCounter struct { http.ResponseWriter bytesSent *uint64 @@ -46,19 +52,15 @@ func (w *responseWriterWithCounter) Write(p []byte) (int, error) { return n, err } -type hlsMuxerHandleRequestReq struct { +type muxerHandleRequestReq struct { path string file string ctx *gin.Context - res chan *hlsMuxer -} - -type hlsMuxerParent interface { - logger.Writer - closeMuxer(*hlsMuxer) + res chan *muxer } -type hlsMuxer struct { +type muxer struct { + parentCtx context.Context remoteAddr string externalAuthenticationURL string variant conf.HLSVariant @@ -70,90 +72,59 @@ type hlsMuxer struct { writeQueueSize int wg *sync.WaitGroup pathName string - pathManager *pathManager - parent hlsMuxerParent + pathManager defs.PathManager + parent *Server ctx context.Context ctxCancel func() created time.Time - path *path + path defs.Path writer *asyncwriter.Writer lastRequestTime *int64 muxer *gohlslib.Muxer - requests []*hlsMuxerHandleRequestReq + requests []*muxerHandleRequestReq bytesSent *uint64 // in - chRequest chan *hlsMuxerHandleRequestReq + chRequest chan *muxerHandleRequestReq } -func newHLSMuxer( - parentCtx context.Context, - remoteAddr string, - externalAuthenticationURL string, - variant conf.HLSVariant, - segmentCount int, - segmentDuration conf.StringDuration, - partDuration conf.StringDuration, - segmentMaxSize conf.StringSize, - directory string, - writeQueueSize int, - wg *sync.WaitGroup, - pathName string, - pathManager *pathManager, - parent hlsMuxerParent, -) *hlsMuxer { - ctx, ctxCancel := context.WithCancel(parentCtx) - - m := &hlsMuxer{ - remoteAddr: remoteAddr, - externalAuthenticationURL: externalAuthenticationURL, - variant: variant, - segmentCount: segmentCount, - segmentDuration: segmentDuration, - partDuration: partDuration, - segmentMaxSize: segmentMaxSize, - directory: directory, - writeQueueSize: writeQueueSize, - wg: wg, - pathName: pathName, - pathManager: pathManager, - parent: parent, - ctx: ctx, - ctxCancel: ctxCancel, - created: time.Now(), - lastRequestTime: int64Ptr(time.Now().UnixNano()), - bytesSent: new(uint64), - chRequest: make(chan *hlsMuxerHandleRequestReq), - } +func (m *muxer) initialize() { + ctx, ctxCancel := context.WithCancel(m.parentCtx) + + m.ctx = ctx + m.ctxCancel = ctxCancel + m.created = time.Now() + m.lastRequestTime = int64Ptr(time.Now().UnixNano()) + m.bytesSent = new(uint64) + m.chRequest = make(chan *muxerHandleRequestReq) m.Log(logger.Info, "created %s", func() string { - if remoteAddr == "" { + if m.remoteAddr == "" { return "automatically" } - return "(requested by " + remoteAddr + ")" + return "(requested by " + m.remoteAddr + ")" }()) m.wg.Add(1) go m.run() - - return m } -func (m *hlsMuxer) close() { +func (m *muxer) Close() { m.ctxCancel() } -func (m *hlsMuxer) Log(level logger.Level, format string, args ...interface{}) { +// Log implements logger.Writer. +func (m *muxer) Log(level logger.Level, format string, args ...interface{}) { m.parent.Log(level, "[muxer %s] "+format, append([]interface{}{m.pathName}, args...)...) } // PathName returns the path name. -func (m *hlsMuxer) PathName() string { +func (m *muxer) PathName() string { return m.pathName } -func (m *hlsMuxer) run() { +func (m *muxer) run() { defer m.wg.Done() err := func() error { @@ -213,7 +184,7 @@ func (m *hlsMuxer) run() { m.clearQueuedRequests() isReady = false isRecreating = true - recreateTimer = time.NewTimer(hlsMuxerRecreatePause) + recreateTimer = time.NewTimer(muxerRecreatePause) } else { return err } @@ -234,41 +205,41 @@ func (m *hlsMuxer) run() { m.Log(logger.Info, "destroyed: %v", err) } -func (m *hlsMuxer) clearQueuedRequests() { +func (m *muxer) clearQueuedRequests() { for _, req := range m.requests { req.res <- nil } m.requests = nil } -func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{}) error { - res := m.pathManager.addReader(pathAddReaderReq{ - author: m, - accessRequest: pathAccessRequest{ - name: m.pathName, - skipAuth: true, +func (m *muxer) runInner(innerCtx context.Context, innerReady chan struct{}) error { + res := m.pathManager.AddReader(defs.PathAddReaderReq{ + Author: m, + AccessRequest: defs.PathAccessRequest{ + Name: m.pathName, + SkipAuth: true, }, }) - if res.err != nil { - return res.err + if res.Err != nil { + return res.Err } - m.path = res.path + m.path = res.Path - defer m.path.removeReader(pathRemoveReaderReq{author: m}) + defer m.path.RemoveReader(defs.PathRemoveReaderReq{Author: m}) m.writer = asyncwriter.New(m.writeQueueSize, m) - defer res.stream.RemoveReader(m.writer) + defer res.Stream.RemoveReader(m.writer) var medias []*description.Media - videoMedia, videoTrack := m.createVideoTrack(res.stream) + videoMedia, videoTrack := m.createVideoTrack(res.Stream) if videoMedia != nil { medias = append(medias, videoMedia) } - audioMedia, audioTrack := m.createAudioTrack(res.stream) + audioMedia, audioTrack := m.createAudioTrack(res.Stream) if audioMedia != nil { medias = append(medias, audioMedia) } @@ -305,7 +276,7 @@ func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{}) innerReady <- struct{}{} m.Log(logger.Info, "is converting into HLS, %s", - mediaInfo(medias)) + defs.MediasInfo(medias)) m.writer.Start() @@ -333,7 +304,7 @@ func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{}) } } -func (m *hlsMuxer) createVideoTrack(stream *stream.Stream) (*description.Media, *gohlslib.Track) { +func (m *muxer) createVideoTrack(stream *stream.Stream) (*description.Media, *gohlslib.Track) { var videoFormatAV1 *format.AV1 videoMedia := stream.Desc().FindFormat(&videoFormatAV1) @@ -444,7 +415,7 @@ func (m *hlsMuxer) createVideoTrack(stream *stream.Stream) (*description.Media, return nil, nil } -func (m *hlsMuxer) createAudioTrack(stream *stream.Stream) (*description.Media, *gohlslib.Track) { +func (m *muxer) createAudioTrack(stream *stream.Stream) (*description.Media, *gohlslib.Track) { var audioFormatOpus *format.Opus audioMedia := stream.Desc().FindFormat(&audioFormatOpus) @@ -507,7 +478,7 @@ func (m *hlsMuxer) createAudioTrack(stream *stream.Stream) (*description.Media, return nil, nil } -func (m *hlsMuxer) handleRequest(ctx *gin.Context) { +func (m *muxer) handleRequest(ctx *gin.Context) { atomic.StoreInt64(m.lastRequestTime, time.Now().UnixNano()) w := &responseWriterWithCounter{ @@ -519,7 +490,7 @@ func (m *hlsMuxer) handleRequest(ctx *gin.Context) { } // processRequest is called by hlsserver.Server (forwarded from ServeHTTP). -func (m *hlsMuxer) processRequest(req *hlsMuxerHandleRequestReq) { +func (m *muxer) processRequest(req *muxerHandleRequestReq) { select { case m.chRequest <- req: case <-m.ctx.Done(): @@ -527,15 +498,15 @@ func (m *hlsMuxer) processRequest(req *hlsMuxerHandleRequestReq) { } } -// apiReaderDescribe implements reader. -func (m *hlsMuxer) apiReaderDescribe() defs.APIPathSourceOrReader { +// APIReaderDescribe implements reader. +func (m *muxer) APIReaderDescribe() defs.APIPathSourceOrReader { return defs.APIPathSourceOrReader{ Type: "hlsMuxer", ID: "", } } -func (m *hlsMuxer) apiItem() *defs.APIHLSMuxer { +func (m *muxer) apiItem() *defs.APIHLSMuxer { return &defs.APIHLSMuxer{ Path: m.pathName, Created: m.created, diff --git a/internal/servers/hls/server.go b/internal/servers/hls/server.go new file mode 100644 index 00000000..d76e309e --- /dev/null +++ b/internal/servers/hls/server.go @@ -0,0 +1,290 @@ +// Package hls contains a HLS server. +package hls + +import ( + "context" + "fmt" + "sort" + "sync" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" + "github.com/bluenviron/mediamtx/internal/logger" +) + +type serverAPIMuxersListRes struct { + data *defs.APIHLSMuxerList + err error +} + +type serverAPIMuxersListReq struct { + res chan serverAPIMuxersListRes +} + +type serverAPIMuxersGetRes struct { + data *defs.APIHLSMuxer + err error +} + +type serverAPIMuxersGetReq struct { + name string + res chan serverAPIMuxersGetRes +} + +type serverParent interface { + logger.Writer +} + +// Server is a HLS server. +type Server struct { + Address string + Encryption bool + ServerKey string + ServerCert string + ExternalAuthenticationURL string + AlwaysRemux bool + Variant conf.HLSVariant + SegmentCount int + SegmentDuration conf.StringDuration + PartDuration conf.StringDuration + SegmentMaxSize conf.StringSize + AllowOrigin string + TrustedProxies conf.IPsOrCIDRs + Directory string + ReadTimeout conf.StringDuration + WriteQueueSize int + PathManager defs.PathManager + Parent serverParent + + ctx context.Context + ctxCancel func() + wg sync.WaitGroup + httpServer *httpServer + muxers map[string]*muxer + + // in + chPathReady chan defs.Path + chPathNotReady chan defs.Path + chHandleRequest chan muxerHandleRequestReq + chCloseMuxer chan *muxer + chAPIMuxerList chan serverAPIMuxersListReq + chAPIMuxerGet chan serverAPIMuxersGetReq +} + +// Initialize initializes the server. +func (s *Server) Initialize() error { + ctx, ctxCancel := context.WithCancel(context.Background()) + + s.ctx = ctx + s.ctxCancel = ctxCancel + s.muxers = make(map[string]*muxer) + s.chPathReady = make(chan defs.Path) + s.chPathNotReady = make(chan defs.Path) + s.chHandleRequest = make(chan muxerHandleRequestReq) + s.chCloseMuxer = make(chan *muxer) + s.chAPIMuxerList = make(chan serverAPIMuxersListReq) + s.chAPIMuxerGet = make(chan serverAPIMuxersGetReq) + + s.httpServer = &httpServer{ + address: s.Address, + encryption: s.Encryption, + serverKey: s.ServerKey, + serverCert: s.ServerCert, + allowOrigin: s.AllowOrigin, + trustedProxies: s.TrustedProxies, + readTimeout: s.ReadTimeout, + pathManager: s.PathManager, + parent: s, + } + err := s.httpServer.initialize() + if err != nil { + ctxCancel() + return err + } + + s.Log(logger.Info, "listener opened on "+s.Address) + + s.wg.Add(1) + go s.run() + + return nil +} + +// Log implements logger.Writer. +func (s *Server) Log(level logger.Level, format string, args ...interface{}) { + s.Parent.Log(level, "[HLS] "+format, args...) +} + +// Close closes the server. +func (s *Server) Close() { + s.Log(logger.Info, "listener is closing") + s.ctxCancel() + s.wg.Wait() +} + +func (s *Server) run() { + defer s.wg.Done() + +outer: + for { + select { + case pa := <-s.chPathReady: + if s.AlwaysRemux && !pa.SafeConf().SourceOnDemand { + if _, ok := s.muxers[pa.Name()]; !ok { + s.createMuxer(pa.Name(), "") + } + } + + case pa := <-s.chPathNotReady: + c, ok := s.muxers[pa.Name()] + if ok && c.remoteAddr == "" { // created with "always remux" + c.Close() + delete(s.muxers, pa.Name()) + } + + case req := <-s.chHandleRequest: + r, ok := s.muxers[req.path] + switch { + case ok: + r.processRequest(&req) + + default: + r := s.createMuxer(req.path, req.ctx.ClientIP()) + r.processRequest(&req) + } + + case c := <-s.chCloseMuxer: + if c2, ok := s.muxers[c.PathName()]; !ok || c2 != c { + continue + } + delete(s.muxers, c.PathName()) + + case req := <-s.chAPIMuxerList: + data := &defs.APIHLSMuxerList{ + Items: []*defs.APIHLSMuxer{}, + } + + for _, muxer := range s.muxers { + data.Items = append(data.Items, muxer.apiItem()) + } + + sort.Slice(data.Items, func(i, j int) bool { + return data.Items[i].Created.Before(data.Items[j].Created) + }) + + req.res <- serverAPIMuxersListRes{ + data: data, + } + + case req := <-s.chAPIMuxerGet: + muxer, ok := s.muxers[req.name] + if !ok { + req.res <- serverAPIMuxersGetRes{err: fmt.Errorf("muxer not found")} + continue + } + + req.res <- serverAPIMuxersGetRes{data: muxer.apiItem()} + + case <-s.ctx.Done(): + break outer + } + } + + s.ctxCancel() + + s.httpServer.close() +} + +func (s *Server) createMuxer(pathName string, remoteAddr string) *muxer { + r := &muxer{ + parentCtx: s.ctx, + remoteAddr: remoteAddr, + externalAuthenticationURL: s.ExternalAuthenticationURL, + variant: s.Variant, + segmentCount: s.SegmentCount, + segmentDuration: s.SegmentDuration, + partDuration: s.PartDuration, + segmentMaxSize: s.SegmentMaxSize, + directory: s.Directory, + writeQueueSize: s.WriteQueueSize, + wg: &s.wg, + pathName: pathName, + pathManager: s.PathManager, + parent: s, + } + r.initialize() + s.muxers[pathName] = r + return r +} + +// closeMuxer is called by muxer. +func (s *Server) closeMuxer(c *muxer) { + select { + case s.chCloseMuxer <- c: + case <-s.ctx.Done(): + } +} + +func (s *Server) handleRequest(req muxerHandleRequestReq) { + req.res = make(chan *muxer) + + select { + case s.chHandleRequest <- req: + muxer := <-req.res + if muxer != nil { + req.ctx.Request.URL.Path = req.file + muxer.handleRequest(req.ctx) + } + + case <-s.ctx.Done(): + } +} + +// PathReady is called by pathManager. +func (s *Server) PathReady(pa defs.Path) { + select { + case s.chPathReady <- pa: + case <-s.ctx.Done(): + } +} + +// PathNotReady is called by pathManager. +func (s *Server) PathNotReady(pa defs.Path) { + select { + case s.chPathNotReady <- pa: + case <-s.ctx.Done(): + } +} + +// APIMuxersList is called by api. +func (s *Server) APIMuxersList() (*defs.APIHLSMuxerList, error) { + req := serverAPIMuxersListReq{ + res: make(chan serverAPIMuxersListRes), + } + + select { + case s.chAPIMuxerList <- req: + res := <-req.res + return res.data, res.err + + case <-s.ctx.Done(): + return nil, fmt.Errorf("terminated") + } +} + +// APIMuxersGet is called by api. +func (s *Server) APIMuxersGet(name string) (*defs.APIHLSMuxer, error) { + req := serverAPIMuxersGetReq{ + name: name, + res: make(chan serverAPIMuxersGetRes), + } + + select { + case s.chAPIMuxerGet <- req: + res := <-req.res + return res.data, res.err + + case <-s.ctx.Done(): + return nil, fmt.Errorf("terminated") + } +} diff --git a/internal/core/rtmp_conn.go b/internal/servers/rtmp/conn.go similarity index 74% rename from internal/core/rtmp_conn.go rename to internal/servers/rtmp/conn.go index 60e6f347..a5eedfb4 100644 --- a/internal/core/rtmp_conn.go +++ b/internal/servers/rtmp/conn.go @@ -1,4 +1,4 @@ -package core +package rtmp import ( "context" @@ -40,24 +40,15 @@ func pathNameAndQuery(inURL *url.URL) (string, url.Values, string) { return pathName, ur.Query(), ur.RawQuery } -type rtmpConnState int +type connState int const ( - rtmpConnStateRead rtmpConnState = iota + 1 - rtmpConnStatePublish + connStateRead connState = iota + 1 + connStatePublish ) -type rtmpConnPathManager interface { - addReader(req pathAddReaderReq) pathAddReaderRes - addPublisher(req pathAddPublisherReq) pathAddPublisherRes -} - -type rtmpConnParent interface { - logger.Writer - closeConn(*rtmpConn) -} - -type rtmpConn struct { +type conn struct { + parentCtx context.Context isTLS bool rtspAddress string readTimeout conf.StringDuration @@ -69,8 +60,8 @@ type rtmpConn struct { wg *sync.WaitGroup nconn net.Conn externalCmdPool *externalcmd.Pool - pathManager rtmpConnPathManager - parent rtmpConnParent + pathManager defs.PathManager + parent *Server ctx context.Context ctxCancel func() @@ -78,73 +69,40 @@ type rtmpConn struct { created time.Time mutex sync.RWMutex rconn *rtmp.Conn - state rtmpConnState + state connState pathName string } -func newRTMPConn( - parentCtx context.Context, - isTLS bool, - rtspAddress string, - readTimeout conf.StringDuration, - writeTimeout conf.StringDuration, - writeQueueSize int, - runOnConnect string, - runOnConnectRestart bool, - runOnDisconnect string, - wg *sync.WaitGroup, - nconn net.Conn, - externalCmdPool *externalcmd.Pool, - pathManager rtmpConnPathManager, - parent rtmpConnParent, -) *rtmpConn { - ctx, ctxCancel := context.WithCancel(parentCtx) - - c := &rtmpConn{ - isTLS: isTLS, - rtspAddress: rtspAddress, - readTimeout: readTimeout, - writeTimeout: writeTimeout, - writeQueueSize: writeQueueSize, - runOnConnect: runOnConnect, - runOnConnectRestart: runOnConnectRestart, - runOnDisconnect: runOnDisconnect, - wg: wg, - nconn: nconn, - externalCmdPool: externalCmdPool, - pathManager: pathManager, - parent: parent, - ctx: ctx, - ctxCancel: ctxCancel, - uuid: uuid.New(), - created: time.Now(), - } +func (c *conn) initialize() { + c.ctx, c.ctxCancel = context.WithCancel(c.parentCtx) + + c.uuid = uuid.New() + c.created = time.Now() c.Log(logger.Info, "opened") c.wg.Add(1) go c.run() - - return c } -func (c *rtmpConn) close() { +func (c *conn) Close() { c.ctxCancel() } -func (c *rtmpConn) remoteAddr() net.Addr { +func (c *conn) remoteAddr() net.Addr { return c.nconn.RemoteAddr() } -func (c *rtmpConn) Log(level logger.Level, format string, args ...interface{}) { +// Log implements logger.Writer. +func (c *conn) Log(level logger.Level, format string, args ...interface{}) { c.parent.Log(level, "[conn %v] "+format, append([]interface{}{c.nconn.RemoteAddr()}, args...)...) } -func (c *rtmpConn) ip() net.IP { +func (c *conn) ip() net.IP { return c.nconn.RemoteAddr().(*net.TCPAddr).IP } -func (c *rtmpConn) run() { //nolint:dupl +func (c *conn) run() { //nolint:dupl defer c.wg.Done() onDisconnectHook := hooks.OnConnect(hooks.OnConnectParams{ @@ -154,7 +112,7 @@ func (c *rtmpConn) run() { //nolint:dupl RunOnConnectRestart: c.runOnConnectRestart, RunOnDisconnect: c.runOnDisconnect, RTSPAddress: c.rtspAddress, - Desc: c.apiReaderDescribe(), + Desc: c.APIReaderDescribe(), }) defer onDisconnectHook() @@ -167,7 +125,7 @@ func (c *rtmpConn) run() { //nolint:dupl c.Log(logger.Info, "closed: %v", err) } -func (c *rtmpConn) runInner() error { +func (c *conn) runInner() error { readerErr := make(chan error) go func() { readerErr <- c.runReader() @@ -185,7 +143,7 @@ func (c *rtmpConn) runInner() error { } } -func (c *rtmpConn) runReader() error { +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, u, publish, err := rtmp.NewServerConn(c.nconn) @@ -203,52 +161,52 @@ func (c *rtmpConn) runReader() error { return c.runPublish(conn, u) } -func (c *rtmpConn) runRead(conn *rtmp.Conn, u *url.URL) error { +func (c *conn) runRead(conn *rtmp.Conn, u *url.URL) error { pathName, query, rawQuery := pathNameAndQuery(u) - res := c.pathManager.addReader(pathAddReaderReq{ - author: c, - accessRequest: pathAccessRequest{ - name: pathName, - query: rawQuery, - ip: c.ip(), - user: query.Get("user"), - pass: query.Get("pass"), - proto: authProtocolRTMP, - id: &c.uuid, + res := c.pathManager.AddReader(defs.PathAddReaderReq{ + Author: c, + AccessRequest: defs.PathAccessRequest{ + Name: pathName, + Query: rawQuery, + IP: c.ip(), + User: query.Get("user"), + Pass: query.Get("pass"), + Proto: defs.AuthProtocolRTMP, + ID: &c.uuid, }, }) - if res.err != nil { - if terr, ok := res.err.(*errAuthentication); ok { + if res.Err != nil { + if terr, ok := res.Err.(*defs.ErrAuthentication); ok { // wait some seconds to stop brute force attacks <-time.After(rtmpPauseAfterAuthError) return terr } - return res.err + return res.Err } - defer res.path.removeReader(pathRemoveReaderReq{author: c}) + defer res.Path.RemoveReader(defs.PathRemoveReaderReq{Author: c}) c.mutex.Lock() - c.state = rtmpConnStateRead + c.state = connStateRead c.pathName = pathName c.mutex.Unlock() writer := asyncwriter.New(c.writeQueueSize, c) - defer res.stream.RemoveReader(writer) + defer res.Stream.RemoveReader(writer) var w *rtmp.Writer videoFormat := c.setupVideo( &w, - res.stream, + res.Stream, writer) audioFormat := c.setupAudio( &w, - res.stream, + res.Stream, writer) if videoFormat == nil && audioFormat == nil { @@ -257,13 +215,13 @@ func (c *rtmpConn) runRead(conn *rtmp.Conn, u *url.URL) error { } c.Log(logger.Info, "is reading from path '%s', %s", - res.path.name, readerMediaInfo(writer, res.stream)) + res.Path.Name, defs.MediasInfo(res.Stream.MediasForReader(writer))) onUnreadHook := hooks.OnRead(hooks.OnReadParams{ Logger: c, ExternalCmdPool: c.externalCmdPool, - Conf: res.path.safeConf(), - ExternalCmdEnv: res.path.externalCmdEnv(), + Conf: res.Path.SafeConf(), + ExternalCmdEnv: res.Path.ExternalCmdEnv(), Reader: c.APISourceDescribe(), Query: rawQuery, }) @@ -290,7 +248,7 @@ func (c *rtmpConn) runRead(conn *rtmp.Conn, u *url.URL) error { } } -func (c *rtmpConn) setupVideo( +func (c *conn) setupVideo( w **rtmp.Writer, stream *stream.Stream, writer *asyncwriter.Writer, @@ -359,7 +317,7 @@ func (c *rtmpConn) setupVideo( return nil } -func (c *rtmpConn) setupAudio( +func (c *conn) setupAudio( w **rtmp.Writer, stream *stream.Stream, writer *asyncwriter.Writer, @@ -432,36 +390,36 @@ func (c *rtmpConn) setupAudio( return nil } -func (c *rtmpConn) runPublish(conn *rtmp.Conn, u *url.URL) error { +func (c *conn) runPublish(conn *rtmp.Conn, u *url.URL) error { pathName, query, rawQuery := pathNameAndQuery(u) - res := c.pathManager.addPublisher(pathAddPublisherReq{ - author: c, - accessRequest: pathAccessRequest{ - name: pathName, - query: rawQuery, - publish: true, - ip: c.ip(), - user: query.Get("user"), - pass: query.Get("pass"), - proto: authProtocolRTMP, - id: &c.uuid, + res := c.pathManager.AddPublisher(defs.PathAddPublisherReq{ + Author: c, + AccessRequest: defs.PathAccessRequest{ + Name: pathName, + Query: rawQuery, + Publish: true, + IP: c.ip(), + User: query.Get("user"), + Pass: query.Get("pass"), + Proto: defs.AuthProtocolRTMP, + ID: &c.uuid, }, }) - if res.err != nil { - if terr, ok := res.err.(*errAuthentication); ok { + if res.Err != nil { + if terr, ok := res.Err.(*defs.ErrAuthentication); ok { // wait some seconds to stop brute force attacks <-time.After(rtmpPauseAfterAuthError) return terr } - return res.err + return res.Err } - defer res.path.removePublisher(pathRemovePublisherReq{author: c}) + defer res.Path.RemovePublisher(defs.PathRemovePublisherReq{Author: c}) c.mutex.Lock() - c.state = rtmpConnStatePublish + c.state = connStatePublish c.pathName = pathName c.mutex.Unlock() @@ -566,16 +524,16 @@ func (c *rtmpConn) runPublish(conn *rtmp.Conn, u *url.URL) error { } } - rres := res.path.startPublisher(pathStartPublisherReq{ - author: c, - desc: &description.Session{Medias: medias}, - generateRTPPackets: true, + rres := res.Path.StartPublisher(defs.PathStartPublisherReq{ + Author: c, + Desc: &description.Session{Medias: medias}, + GenerateRTPPackets: true, }) - if rres.err != nil { - return rres.err + if rres.Err != nil { + return rres.Err } - stream = rres.stream + stream = rres.Stream // disable write deadline to allow outgoing acknowledges c.nconn.SetWriteDeadline(time.Time{}) @@ -589,8 +547,8 @@ func (c *rtmpConn) runPublish(conn *rtmp.Conn, u *url.URL) error { } } -// apiReaderDescribe implements reader. -func (c *rtmpConn) apiReaderDescribe() defs.APIPathSourceOrReader { +// APIReaderDescribe implements reader. +func (c *conn) APIReaderDescribe() defs.APIPathSourceOrReader { return defs.APIPathSourceOrReader{ Type: func() string { if c.isTLS { @@ -603,11 +561,11 @@ func (c *rtmpConn) apiReaderDescribe() defs.APIPathSourceOrReader { } // APISourceDescribe implements source. -func (c *rtmpConn) APISourceDescribe() defs.APIPathSourceOrReader { - return c.apiReaderDescribe() +func (c *conn) APISourceDescribe() defs.APIPathSourceOrReader { + return c.APIReaderDescribe() } -func (c *rtmpConn) apiItem() *defs.APIRTMPConn { +func (c *conn) apiItem() *defs.APIRTMPConn { c.mutex.RLock() defer c.mutex.RUnlock() @@ -625,10 +583,10 @@ func (c *rtmpConn) apiItem() *defs.APIRTMPConn { RemoteAddr: c.remoteAddr().String(), State: func() defs.APIRTMPConnState { switch c.state { - case rtmpConnStateRead: + case connStateRead: return defs.APIRTMPConnStateRead - case rtmpConnStatePublish: + case connStatePublish: return defs.APIRTMPConnStatePublish default: diff --git a/internal/servers/rtmp/listener.go b/internal/servers/rtmp/listener.go new file mode 100644 index 00000000..5bc49988 --- /dev/null +++ b/internal/servers/rtmp/listener.go @@ -0,0 +1,36 @@ +package rtmp + +import ( + "net" + "sync" +) + +type listener struct { + ln net.Listener + wg *sync.WaitGroup + parent *Server +} + +func (l *listener) initialize() { + l.wg.Add(1) + go l.run() +} + +func (l *listener) run() { + defer l.wg.Done() + + err := l.runInner() + + l.parent.acceptError(err) +} + +func (l *listener) runInner() error { + for { + conn, err := l.ln.Accept() + if err != nil { + return err + } + + l.parent.newConn(conn) + } +} diff --git a/internal/servers/rtmp/server.go b/internal/servers/rtmp/server.go new file mode 100644 index 00000000..dce8f3cc --- /dev/null +++ b/internal/servers/rtmp/server.go @@ -0,0 +1,308 @@ +// Package rtmp contains a RTMP server. +package rtmp + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "sort" + "sync" + + "github.com/google/uuid" + + "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/restrictnetwork" +) + +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 serverParent interface { + logger.Writer +} + +// Server is a RTMP server. +type Server struct { + Address string + ReadTimeout conf.StringDuration + WriteTimeout conf.StringDuration + WriteQueueSize int + IsTLS bool + ServerCert string + ServerKey string + RTSPAddress string + RunOnConnect string + RunOnConnectRestart bool + RunOnDisconnect string + ExternalCmdPool *externalcmd.Pool + PathManager defs.PathManager + Parent serverParent + + ctx context.Context + ctxCancel func() + wg sync.WaitGroup + ln net.Listener + conns map[*conn]struct{} + + // 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 { + ln, err := func() (net.Listener, error) { + if !s.IsTLS { + return net.Listen(restrictnetwork.Restrict("tcp", s.Address)) + } + + cert, err := tls.LoadX509KeyPair(s.ServerCert, s.ServerKey) + if err != nil { + return nil, err + } + + network, address := restrictnetwork.Restrict("tcp", s.Address) + return tls.Listen(network, address, &tls.Config{Certificates: []tls.Certificate{cert}}) + }() + if err != nil { + return err + } + + s.ctx, s.ctxCancel = context.WithCancel(context.Background()) + + s.ln = ln + 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) + + s.Log(logger.Info, "listener opened on %s", s.Address) + + l := &listener{ + ln: s.ln, + wg: &s.wg, + parent: s, + } + l.initialize() + + s.wg.Add(1) + go s.run() + + return nil +} + +// Log implements logger.Writer. +func (s *Server) Log(level logger.Level, format string, args ...interface{}) { + label := func() string { + if s.IsTLS { + return "RTMPS" + } + return "RTMP" + }() + s.Parent.Log(level, "[%s] "+format, append([]interface{}{label}, args...)...) +} + +// Close closes the server. +func (s *Server) Close() { + s.Log(logger.Info, "listener is closing") + s.ctxCancel() + s.wg.Wait() +} + +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, + isTLS: s.IsTLS, + rtspAddress: s.RTSPAddress, + readTimeout: s.ReadTimeout, + writeTimeout: s.WriteTimeout, + writeQueueSize: s.WriteQueueSize, + 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: fmt.Errorf("connection not found")} + continue + } + + req.res <- serverAPIConnsGetRes{data: c.apiItem()} + + case req := <-s.chAPIConnsKick: + c := s.findConnByUUID(req.uuid) + if c == nil { + req.res <- serverAPIConnsKickRes{err: fmt.Errorf("connection not found")} + 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 is called by api. +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 is called by api. +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 is called by api. +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") + } +} diff --git a/internal/core/rtsp_conn.go b/internal/servers/rtsp/conn.go similarity index 58% rename from internal/core/rtsp_conn.go rename to internal/servers/rtsp/conn.go index 5ace139f..db28fdb4 100644 --- a/internal/core/rtsp_conn.go +++ b/internal/servers/rtsp/conn.go @@ -1,4 +1,4 @@ -package core +package rtsp import ( "fmt" @@ -22,20 +22,19 @@ const ( rtspPauseAfterAuthError = 2 * time.Second ) -type rtspConnParent interface { - logger.Writer - getISTLS() bool - getServer() *gortsplib.Server -} - -type rtspConn struct { - isTLS bool - rtspAddress string - authMethods []headers.AuthMethod - readTimeout conf.StringDuration - pathManager *pathManager - rconn *gortsplib.ServerConn - parent rtspConnParent +type conn struct { + isTLS bool + rtspAddress string + authMethods []headers.AuthMethod + readTimeout conf.StringDuration + runOnConnect string + runOnConnectRestart bool + runOnDisconnect string + externalCmdPool *externalcmd.Pool + pathManager defs.PathManager + rconn *gortsplib.ServerConn + rserver *gortsplib.Server + parent *Server uuid uuid.UUID created time.Time @@ -44,92 +43,70 @@ type rtspConn struct { authFailures int } -func newRTSPConn( - isTLS bool, - rtspAddress string, - authMethods []headers.AuthMethod, - readTimeout conf.StringDuration, - runOnConnect string, - runOnConnectRestart bool, - runOnDisconnect string, - externalCmdPool *externalcmd.Pool, - pathManager *pathManager, - conn *gortsplib.ServerConn, - parent rtspConnParent, -) *rtspConn { - c := &rtspConn{ - isTLS: isTLS, - rtspAddress: rtspAddress, - authMethods: authMethods, - readTimeout: readTimeout, - pathManager: pathManager, - rconn: conn, - parent: parent, - uuid: uuid.New(), - created: time.Now(), - } +func (c *conn) initialize() { + c.uuid = uuid.New() + c.created = time.Now() c.Log(logger.Info, "opened") desc := defs.APIPathSourceOrReader{ Type: func() string { - if isTLS { + if c.isTLS { return "rtspsConn" } - return "rtspConn" + return "conn" }(), ID: c.uuid.String(), } c.onDisconnectHook = hooks.OnConnect(hooks.OnConnectParams{ Logger: c, - ExternalCmdPool: externalCmdPool, - RunOnConnect: runOnConnect, - RunOnConnectRestart: runOnConnectRestart, - RunOnDisconnect: runOnDisconnect, - RTSPAddress: rtspAddress, + ExternalCmdPool: c.externalCmdPool, + RunOnConnect: c.runOnConnect, + RunOnConnectRestart: c.runOnConnectRestart, + RunOnDisconnect: c.runOnDisconnect, + RTSPAddress: c.rtspAddress, Desc: desc, }) - - return c } -func (c *rtspConn) Log(level logger.Level, format string, args ...interface{}) { +// Log implements logger.Writer. +func (c *conn) Log(level logger.Level, format string, args ...interface{}) { c.parent.Log(level, "[conn %v] "+format, append([]interface{}{c.rconn.NetConn().RemoteAddr()}, args...)...) } // Conn returns the RTSP connection. -func (c *rtspConn) Conn() *gortsplib.ServerConn { +func (c *conn) Conn() *gortsplib.ServerConn { return c.rconn } -func (c *rtspConn) remoteAddr() net.Addr { +func (c *conn) remoteAddr() net.Addr { return c.rconn.NetConn().RemoteAddr() } -func (c *rtspConn) ip() net.IP { +func (c *conn) ip() net.IP { return c.rconn.NetConn().RemoteAddr().(*net.TCPAddr).IP } // onClose is called by rtspServer. -func (c *rtspConn) onClose(err error) { +func (c *conn) onClose(err error) { c.Log(logger.Info, "closed: %v", err) c.onDisconnectHook() } // onRequest is called by rtspServer. -func (c *rtspConn) onRequest(req *base.Request) { +func (c *conn) onRequest(req *base.Request) { c.Log(logger.Debug, "[c->s] %v", req) } // OnResponse is called by rtspServer. -func (c *rtspConn) OnResponse(res *base.Response) { +func (c *conn) OnResponse(res *base.Response) { c.Log(logger.Debug, "[s->c] %v", res) } // onDescribe is called by rtspServer. -func (c *rtspConn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, +func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, ) (*base.Response, *gortsplib.ServerStream, error) { if len(ctx.Path) == 0 || ctx.Path[0] != '/' { return &base.Response{ @@ -148,50 +125,50 @@ func (c *rtspConn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, } } - res := c.pathManager.describe(pathDescribeReq{ - accessRequest: pathAccessRequest{ - name: ctx.Path, - query: ctx.Query, - ip: c.ip(), - proto: authProtocolRTSP, - id: &c.uuid, - rtspRequest: ctx.Request, - rtspNonce: c.authNonce, + res := c.pathManager.Describe(defs.PathDescribeReq{ + AccessRequest: defs.PathAccessRequest{ + Name: ctx.Path, + Query: ctx.Query, + IP: c.ip(), + Proto: defs.AuthProtocolRTSP, + ID: &c.uuid, + RTSPRequest: ctx.Request, + RTSPNonce: c.authNonce, }, }) - if res.err != nil { - switch terr := res.err.(type) { - case *errAuthentication: + if res.Err != nil { + switch terr := res.Err.(type) { + case *defs.ErrAuthentication: res, err := c.handleAuthError(terr) return res, nil, err - case errPathNoOnePublishing: + case defs.ErrPathNoOnePublishing: return &base.Response{ StatusCode: base.StatusNotFound, - }, nil, res.err + }, nil, res.Err default: return &base.Response{ StatusCode: base.StatusBadRequest, - }, nil, res.err + }, nil, res.Err } } - if res.redirect != "" { + if res.Redirect != "" { return &base.Response{ StatusCode: base.StatusMovedPermanently, Header: base.Header{ - "Location": base.HeaderValue{res.redirect}, + "Location": base.HeaderValue{res.Redirect}, }, }, nil, nil } var stream *gortsplib.ServerStream - if !c.parent.getISTLS() { - stream = res.stream.RTSPStream(c.parent.getServer()) + if !c.isTLS { + stream = res.Stream.RTSPStream(c.rserver) } else { - stream = res.stream.RTSPSStream(c.parent.getServer()) + stream = res.Stream.RTSPSStream(c.rserver) } return &base.Response{ @@ -199,7 +176,7 @@ func (c *rtspConn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, }, stream, nil } -func (c *rtspConn) handleAuthError(authErr error) (*base.Response, error) { +func (c *conn) handleAuthError(authErr error) (*base.Response, error) { c.authFailures++ // VLC with login prompt sends 4 requests: @@ -225,7 +202,7 @@ func (c *rtspConn) handleAuthError(authErr error) (*base.Response, error) { }, authErr } -func (c *rtspConn) apiItem() *defs.APIRTSPConn { +func (c *conn) apiItem() *defs.APIRTSPConn { return &defs.APIRTSPConn{ ID: c.uuid, Created: c.created, diff --git a/internal/servers/rtsp/server.go b/internal/servers/rtsp/server.go new file mode 100644 index 00000000..d22a8526 --- /dev/null +++ b/internal/servers/rtsp/server.go @@ -0,0 +1,430 @@ +// Package rtsp contains a RTSP server. +package rtsp + +import ( + "context" + "crypto/tls" + "fmt" + "sort" + "strings" + "sync" + "time" + + "github.com/bluenviron/gortsplib/v4" + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/headers" + "github.com/bluenviron/gortsplib/v4/pkg/liberrors" + "github.com/google/uuid" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" + "github.com/bluenviron/mediamtx/internal/externalcmd" + "github.com/bluenviron/mediamtx/internal/logger" +) + +func printAddresses(srv *gortsplib.Server) string { + var ret []string + + ret = append(ret, fmt.Sprintf("%s (TCP)", srv.RTSPAddress)) + + if srv.UDPRTPAddress != "" { + ret = append(ret, fmt.Sprintf("%s (UDP/RTP)", srv.UDPRTPAddress)) + } + + if srv.UDPRTCPAddress != "" { + ret = append(ret, fmt.Sprintf("%s (UDP/RTCP)", srv.UDPRTCPAddress)) + } + + return strings.Join(ret, ", ") +} + +type serverParent interface { + logger.Writer +} + +// Server is a RTSP server. +type Server struct { + Address string + AuthMethods []headers.AuthMethod + ReadTimeout conf.StringDuration + WriteTimeout conf.StringDuration + WriteQueueSize int + UseUDP bool + UseMulticast bool + RTPAddress string + RTCPAddress string + MulticastIPRange string + MulticastRTPPort int + MulticastRTCPPort int + IsTLS bool + ServerCert string + ServerKey string + RTSPAddress string + Protocols map[conf.Protocol]struct{} + RunOnConnect string + RunOnConnectRestart bool + RunOnDisconnect string + ExternalCmdPool *externalcmd.Pool + PathManager defs.PathManager + Parent serverParent + + ctx context.Context + ctxCancel func() + wg sync.WaitGroup + srv *gortsplib.Server + mutex sync.RWMutex + conns map[*gortsplib.ServerConn]*conn + sessions map[*gortsplib.ServerSession]*session +} + +// Initialize initializes the server. +func (s *Server) Initialize() error { + s.ctx, s.ctxCancel = context.WithCancel(context.Background()) + + s.conns = make(map[*gortsplib.ServerConn]*conn) + s.sessions = make(map[*gortsplib.ServerSession]*session) + + s.srv = &gortsplib.Server{ + Handler: s, + ReadTimeout: time.Duration(s.ReadTimeout), + WriteTimeout: time.Duration(s.WriteTimeout), + WriteQueueSize: s.WriteQueueSize, + RTSPAddress: s.Address, + } + + if s.UseUDP { + s.srv.UDPRTPAddress = s.RTPAddress + s.srv.UDPRTCPAddress = s.RTCPAddress + } + + if s.UseMulticast { + s.srv.MulticastIPRange = s.MulticastIPRange + s.srv.MulticastRTPPort = s.MulticastRTPPort + s.srv.MulticastRTCPPort = s.MulticastRTCPPort + } + + if s.IsTLS { + cert, err := tls.LoadX509KeyPair(s.ServerCert, s.ServerKey) + if err != nil { + return err + } + + s.srv.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}} + } + + err := s.srv.Start() + if err != nil { + return err + } + + s.Log(logger.Info, "listener opened on %s", printAddresses(s.srv)) + + s.wg.Add(1) + go s.run() + + return nil +} + +// Log implements logger.Writer. +func (s *Server) Log(level logger.Level, format string, args ...interface{}) { + label := func() string { + if s.IsTLS { + return "RTSPS" + } + return "RTSP" + }() + s.Parent.Log(level, "[%s] "+format, append([]interface{}{label}, args...)...) +} + +// Close closes the server. +func (s *Server) Close() { + s.Log(logger.Info, "listener is closing") + s.ctxCancel() + s.wg.Wait() +} + +func (s *Server) run() { + defer s.wg.Done() + + serverErr := make(chan error) + go func() { + serverErr <- s.srv.Wait() + }() + +outer: + select { + case err := <-serverErr: + s.Log(logger.Error, "%s", err) + break outer + + case <-s.ctx.Done(): + s.srv.Close() + <-serverErr + break outer + } + + s.ctxCancel() +} + +// OnConnOpen implements gortsplib.ServerHandlerOnConnOpen. +func (s *Server) OnConnOpen(ctx *gortsplib.ServerHandlerOnConnOpenCtx) { + c := &conn{ + isTLS: s.IsTLS, + rtspAddress: s.RTSPAddress, + authMethods: s.AuthMethods, + readTimeout: s.ReadTimeout, + runOnConnect: s.RunOnConnect, + runOnConnectRestart: s.RunOnConnectRestart, + runOnDisconnect: s.RunOnDisconnect, + externalCmdPool: s.ExternalCmdPool, + pathManager: s.PathManager, + rconn: ctx.Conn, + rserver: s.srv, + parent: s, + } + c.initialize() + s.mutex.Lock() + s.conns[ctx.Conn] = c + s.mutex.Unlock() + + ctx.Conn.SetUserData(c) +} + +// OnConnClose implements gortsplib.ServerHandlerOnConnClose. +func (s *Server) OnConnClose(ctx *gortsplib.ServerHandlerOnConnCloseCtx) { + s.mutex.Lock() + c := s.conns[ctx.Conn] + delete(s.conns, ctx.Conn) + s.mutex.Unlock() + c.onClose(ctx.Error) +} + +// OnRequest implements gortsplib.ServerHandlerOnRequest. +func (s *Server) OnRequest(sc *gortsplib.ServerConn, req *base.Request) { + c := sc.UserData().(*conn) + c.onRequest(req) +} + +// OnResponse implements gortsplib.ServerHandlerOnResponse. +func (s *Server) OnResponse(sc *gortsplib.ServerConn, res *base.Response) { + c := sc.UserData().(*conn) + c.OnResponse(res) +} + +// OnSessionOpen implements gortsplib.ServerHandlerOnSessionOpen. +func (s *Server) OnSessionOpen(ctx *gortsplib.ServerHandlerOnSessionOpenCtx) { + se := &session{ + isTLS: s.IsTLS, + protocols: s.Protocols, + rsession: ctx.Session, + rconn: ctx.Conn, + rserver: s.srv, + externalCmdPool: s.ExternalCmdPool, + pathManager: s.PathManager, + parent: s, + } + se.initialize() + s.mutex.Lock() + s.sessions[ctx.Session] = se + s.mutex.Unlock() + ctx.Session.SetUserData(se) +} + +// OnSessionClose implements gortsplib.ServerHandlerOnSessionClose. +func (s *Server) OnSessionClose(ctx *gortsplib.ServerHandlerOnSessionCloseCtx) { + s.mutex.Lock() + se := s.sessions[ctx.Session] + delete(s.sessions, ctx.Session) + s.mutex.Unlock() + + if se != nil { + se.onClose(ctx.Error) + } +} + +// OnDescribe implements gortsplib.ServerHandlerOnDescribe. +func (s *Server) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, +) (*base.Response, *gortsplib.ServerStream, error) { + c := ctx.Conn.UserData().(*conn) + return c.onDescribe(ctx) +} + +// OnAnnounce implements gortsplib.ServerHandlerOnAnnounce. +func (s *Server) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) { + c := ctx.Conn.UserData().(*conn) + se := ctx.Session.UserData().(*session) + return se.onAnnounce(c, ctx) +} + +// OnSetup implements gortsplib.ServerHandlerOnSetup. +func (s *Server) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) { + c := ctx.Conn.UserData().(*conn) + se := ctx.Session.UserData().(*session) + return se.onSetup(c, ctx) +} + +// OnPlay implements gortsplib.ServerHandlerOnPlay. +func (s *Server) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { + se := ctx.Session.UserData().(*session) + return se.onPlay(ctx) +} + +// OnRecord implements gortsplib.ServerHandlerOnRecord. +func (s *Server) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) { + se := ctx.Session.UserData().(*session) + return se.onRecord(ctx) +} + +// OnPause implements gortsplib.ServerHandlerOnPause. +func (s *Server) OnPause(ctx *gortsplib.ServerHandlerOnPauseCtx) (*base.Response, error) { + se := ctx.Session.UserData().(*session) + return se.onPause(ctx) +} + +// OnPacketLost implements gortsplib.ServerHandlerOnDecodeError. +func (s *Server) OnPacketLost(ctx *gortsplib.ServerHandlerOnPacketLostCtx) { + se := ctx.Session.UserData().(*session) + se.onPacketLost(ctx) +} + +// OnDecodeError implements gortsplib.ServerHandlerOnDecodeError. +func (s *Server) OnDecodeError(ctx *gortsplib.ServerHandlerOnDecodeErrorCtx) { + se := ctx.Session.UserData().(*session) + se.onDecodeError(ctx) +} + +// OnStreamWriteError implements gortsplib.ServerHandlerOnStreamWriteError. +func (s *Server) OnStreamWriteError(ctx *gortsplib.ServerHandlerOnStreamWriteErrorCtx) { + se := ctx.Session.UserData().(*session) + se.onStreamWriteError(ctx) +} + +func (s *Server) findConnByUUID(uuid uuid.UUID) *conn { + for _, c := range s.conns { + if c.uuid == uuid { + return c + } + } + return nil +} + +func (s *Server) findSessionByUUID(uuid uuid.UUID) (*gortsplib.ServerSession, *session) { + for key, sx := range s.sessions { + if sx.uuid == uuid { + return key, sx + } + } + return nil, nil +} + +// APIConnsList is called by api and metrics. +func (s *Server) APIConnsList() (*defs.APIRTSPConnsList, error) { + select { + case <-s.ctx.Done(): + return nil, fmt.Errorf("terminated") + default: + } + + s.mutex.RLock() + defer s.mutex.RUnlock() + + data := &defs.APIRTSPConnsList{ + Items: []*defs.APIRTSPConn{}, + } + + 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) + }) + + return data, nil +} + +// APIConnsGet is called by api. +func (s *Server) APIConnsGet(uuid uuid.UUID) (*defs.APIRTSPConn, error) { + select { + case <-s.ctx.Done(): + return nil, fmt.Errorf("terminated") + default: + } + + s.mutex.RLock() + defer s.mutex.RUnlock() + + conn := s.findConnByUUID(uuid) + if conn == nil { + return nil, fmt.Errorf("connection not found") + } + + return conn.apiItem(), nil +} + +// APISessionsList is called by api and metrics. +func (s *Server) APISessionsList() (*defs.APIRTSPSessionList, error) { + select { + case <-s.ctx.Done(): + return nil, fmt.Errorf("terminated") + default: + } + + s.mutex.RLock() + defer s.mutex.RUnlock() + + data := &defs.APIRTSPSessionList{ + Items: []*defs.APIRTSPSession{}, + } + + for _, s := range s.sessions { + data.Items = append(data.Items, s.apiItem()) + } + + sort.Slice(data.Items, func(i, j int) bool { + return data.Items[i].Created.Before(data.Items[j].Created) + }) + + return data, nil +} + +// APISessionsGet is called by api. +func (s *Server) APISessionsGet(uuid uuid.UUID) (*defs.APIRTSPSession, error) { + select { + case <-s.ctx.Done(): + return nil, fmt.Errorf("terminated") + default: + } + + s.mutex.RLock() + defer s.mutex.RUnlock() + + _, sx := s.findSessionByUUID(uuid) + if sx == nil { + return nil, fmt.Errorf("session not found") + } + + return sx.apiItem(), nil +} + +// APISessionsKick is called by api. +func (s *Server) APISessionsKick(uuid uuid.UUID) error { + select { + case <-s.ctx.Done(): + return fmt.Errorf("terminated") + default: + } + + s.mutex.RLock() + defer s.mutex.RUnlock() + + key, sx := s.findSessionByUUID(uuid) + if sx == nil { + return fmt.Errorf("session not found") + } + + sx.Close() + delete(s.sessions, key) + sx.onClose(liberrors.ErrServerTerminated{}) + return nil +} diff --git a/internal/core/rtsp_session.go b/internal/servers/rtsp/session.go similarity index 57% rename from internal/core/rtsp_session.go rename to internal/servers/rtsp/session.go index 91c77fb7..2150ec8e 100644 --- a/internal/core/rtsp_session.go +++ b/internal/servers/rtsp/session.go @@ -1,4 +1,4 @@ -package core +package rtsp import ( "encoding/hex" @@ -21,29 +21,19 @@ import ( "github.com/bluenviron/mediamtx/internal/stream" ) -type rtspSessionPathManager interface { - addPublisher(req pathAddPublisherReq) pathAddPublisherRes - addReader(req pathAddReaderReq) pathAddReaderRes -} - -type rtspSessionParent interface { - logger.Writer - getISTLS() bool - getServer() *gortsplib.Server -} - -type rtspSession struct { +type session struct { isTLS bool protocols map[conf.Protocol]struct{} - session *gortsplib.ServerSession - author *gortsplib.ServerConn + rsession *gortsplib.ServerSession + rconn *gortsplib.ServerConn + rserver *gortsplib.Server externalCmdPool *externalcmd.Pool - pathManager rtspSessionPathManager - parent rtspSessionParent + pathManager defs.PathManager + parent *Server uuid uuid.UUID created time.Time - path *path + path defs.Path stream *stream.Stream onUnreadHook func() mutex sync.Mutex @@ -54,61 +44,43 @@ type rtspSession struct { writeErrLogger logger.Writer } -func newRTSPSession( - isTLS bool, - protocols map[conf.Protocol]struct{}, - session *gortsplib.ServerSession, - sc *gortsplib.ServerConn, - externalCmdPool *externalcmd.Pool, - pathManager rtspSessionPathManager, - parent rtspSessionParent, -) *rtspSession { - s := &rtspSession{ - isTLS: isTLS, - protocols: protocols, - session: session, - author: sc, - externalCmdPool: externalCmdPool, - pathManager: pathManager, - parent: parent, - uuid: uuid.New(), - created: time.Now(), - } +func (s *session) initialize() { + s.uuid = uuid.New() + s.created = time.Now() s.decodeErrLogger = logger.NewLimitedLogger(s) s.writeErrLogger = logger.NewLimitedLogger(s) - s.Log(logger.Info, "created by %v", s.author.NetConn().RemoteAddr()) - - return s + s.Log(logger.Info, "created by %v", s.rconn.NetConn().RemoteAddr()) } // Close closes a Session. -func (s *rtspSession) close() { - s.session.Close() +func (s *session) Close() { + s.rsession.Close() } -func (s *rtspSession) remoteAddr() net.Addr { - return s.author.NetConn().RemoteAddr() +func (s *session) remoteAddr() net.Addr { + return s.rconn.NetConn().RemoteAddr() } -func (s *rtspSession) Log(level logger.Level, format string, args ...interface{}) { +// 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 *rtspSession) onClose(err error) { - if s.session.State() == gortsplib.ServerSessionStatePlay { +func (s *session) onClose(err error) { + if s.rsession.State() == gortsplib.ServerSessionStatePlay { s.onUnreadHook() } - switch s.session.State() { + switch s.rsession.State() { case gortsplib.ServerSessionStatePrePlay, gortsplib.ServerSessionStatePlay: - s.path.removeReader(pathRemoveReaderReq{author: s}) + s.path.RemoveReader(defs.PathRemoveReaderReq{Author: s}) case gortsplib.ServerSessionStatePreRecord, gortsplib.ServerSessionStateRecord: - s.path.removePublisher(pathRemovePublisherReq{author: s}) + s.path.RemovePublisher(defs.PathRemovePublisherReq{Author: s}) } s.path = nil @@ -118,7 +90,7 @@ func (s *rtspSession) onClose(err error) { } // onAnnounce is called by rtspServer. -func (s *rtspSession) onAnnounce(c *rtspConn, ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) { +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, @@ -136,34 +108,34 @@ func (s *rtspSession) onAnnounce(c *rtspConn, ctx *gortsplib.ServerHandlerOnAnno } } - res := s.pathManager.addPublisher(pathAddPublisherReq{ - author: s, - accessRequest: pathAccessRequest{ - name: ctx.Path, - query: ctx.Query, - publish: true, - ip: c.ip(), - proto: authProtocolRTSP, - id: &c.uuid, - rtspRequest: ctx.Request, - rtspBaseURL: nil, - rtspNonce: c.authNonce, + res := s.pathManager.AddPublisher(defs.PathAddPublisherReq{ + Author: s, + AccessRequest: defs.PathAccessRequest{ + Name: ctx.Path, + Query: ctx.Query, + Publish: true, + IP: c.ip(), + Proto: defs.AuthProtocolRTSP, + ID: &c.uuid, + RTSPRequest: ctx.Request, + RTSPBaseURL: nil, + RTSPNonce: c.authNonce, }, }) - if res.err != nil { - switch terr := res.err.(type) { - case *errAuthentication: + if res.Err != nil { + switch terr := res.Err.(type) { + case *defs.ErrAuthentication: return c.handleAuthError(terr) default: return &base.Response{ StatusCode: base.StatusBadRequest, - }, res.err + }, res.Err } } - s.path = res.path + s.path = res.Path s.mutex.Lock() s.state = gortsplib.ServerSessionStatePreRecord @@ -176,7 +148,7 @@ func (s *rtspSession) onAnnounce(c *rtspConn, ctx *gortsplib.ServerHandlerOnAnno } // onSetup is called by rtspServer. -func (s *rtspSession) onSetup(c *rtspConn, ctx *gortsplib.ServerHandlerOnSetupCtx, +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{ @@ -197,7 +169,7 @@ func (s *rtspSession) onSetup(c *rtspConn, ctx *gortsplib.ServerHandlerOnSetupCt } } - switch s.session.State() { + switch s.rsession.State() { case gortsplib.ServerSessionStateInitial, gortsplib.ServerSessionStatePrePlay: // play baseURL := &base.URL{ Scheme: ctx.Request.URL.Scheme, @@ -222,40 +194,40 @@ func (s *rtspSession) onSetup(c *rtspConn, ctx *gortsplib.ServerHandlerOnSetupCt } } - res := s.pathManager.addReader(pathAddReaderReq{ - author: s, - accessRequest: pathAccessRequest{ - name: ctx.Path, - query: ctx.Query, - ip: c.ip(), - proto: authProtocolRTSP, - id: &c.uuid, - rtspRequest: ctx.Request, - rtspBaseURL: baseURL, - rtspNonce: c.authNonce, + res := s.pathManager.AddReader(defs.PathAddReaderReq{ + Author: s, + AccessRequest: defs.PathAccessRequest{ + Name: ctx.Path, + Query: ctx.Query, + IP: c.ip(), + Proto: defs.AuthProtocolRTSP, + ID: &c.uuid, + RTSPRequest: ctx.Request, + RTSPBaseURL: baseURL, + RTSPNonce: c.authNonce, }, }) - if res.err != nil { - switch terr := res.err.(type) { - case *errAuthentication: + if res.Err != nil { + switch terr := res.Err.(type) { + case *defs.ErrAuthentication: res, err := c.handleAuthError(terr) return res, nil, err - case errPathNoOnePublishing: + case defs.ErrPathNoOnePublishing: return &base.Response{ StatusCode: base.StatusNotFound, - }, nil, res.err + }, nil, res.Err default: return &base.Response{ StatusCode: base.StatusBadRequest, - }, nil, res.err + }, nil, res.Err } } - s.path = res.path - s.stream = res.stream + s.path = res.Path + s.stream = res.Stream s.mutex.Lock() s.state = gortsplib.ServerSessionStatePrePlay @@ -263,10 +235,10 @@ func (s *rtspSession) onSetup(c *rtspConn, ctx *gortsplib.ServerHandlerOnSetupCt s.mutex.Unlock() var stream *gortsplib.ServerStream - if !s.parent.getISTLS() { - stream = res.stream.RTSPStream(s.parent.getServer()) + if !s.isTLS { + stream = res.Stream.RTSPStream(s.rserver) } else { - stream = res.stream.RTSPSStream(s.parent.getServer()) + stream = res.Stream.RTSPSStream(s.rserver) } return &base.Response{ @@ -281,27 +253,27 @@ func (s *rtspSession) onSetup(c *rtspConn, ctx *gortsplib.ServerHandlerOnSetupCt } // onPlay is called by rtspServer. -func (s *rtspSession) onPlay(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { +func (s *session) onPlay(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { h := make(base.Header) - if s.session.State() == gortsplib.ServerSessionStatePrePlay { + if s.rsession.State() == gortsplib.ServerSessionStatePrePlay { s.Log(logger.Info, "is reading from path '%s', with %s, %s", - s.path.name, - s.session.SetuppedTransport(), - mediaInfo(s.session.SetuppedMedias())) + s.path.Name(), + s.rsession.SetuppedTransport(), + defs.MediasInfo(s.rsession.SetuppedMedias())) s.onUnreadHook = hooks.OnRead(hooks.OnReadParams{ Logger: s, ExternalCmdPool: s.externalCmdPool, - Conf: s.path.safeConf(), - ExternalCmdEnv: s.path.externalCmdEnv(), - Reader: s.apiReaderDescribe(), - Query: s.session.SetuppedQuery(), + Conf: s.path.SafeConf(), + ExternalCmdEnv: s.path.ExternalCmdEnv(), + Reader: s.APIReaderDescribe(), + Query: s.rsession.SetuppedQuery(), }) s.mutex.Lock() s.state = gortsplib.ServerSessionStatePlay - s.transport = s.session.SetuppedTransport() + s.transport = s.rsession.SetuppedTransport() s.mutex.Unlock() } @@ -312,39 +284,39 @@ func (s *rtspSession) onPlay(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Respons } // onRecord is called by rtspServer. -func (s *rtspSession) onRecord(_ *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) { - res := s.path.startPublisher(pathStartPublisherReq{ - author: s, - desc: s.session.AnnouncedDescription(), - generateRTPPackets: false, +func (s *session) onRecord(_ *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) { + res := s.path.StartPublisher(defs.PathStartPublisherReq{ + Author: s, + Desc: s.rsession.AnnouncedDescription(), + GenerateRTPPackets: false, }) - if res.err != nil { + if res.Err != nil { return &base.Response{ StatusCode: base.StatusBadRequest, - }, res.err + }, res.Err } - s.stream = res.stream + s.stream = res.Stream - for _, medi := range s.session.AnnouncedDescription().Medias { + for _, medi := range s.rsession.AnnouncedDescription().Medias { for _, forma := range medi.Formats { cmedi := medi cforma := forma - s.session.OnPacketRTP(cmedi, cforma, func(pkt *rtp.Packet) { - pts, ok := s.session.PacketPTS(cmedi, pkt) + s.rsession.OnPacketRTP(cmedi, cforma, func(pkt *rtp.Packet) { + pts, ok := s.rsession.PacketPTS(cmedi, pkt) if !ok { return } - res.stream.WriteRTPPacket(cmedi, cforma, pkt, time.Now(), pts) + res.Stream.WriteRTPPacket(cmedi, cforma, pkt, time.Now(), pts) }) } } s.mutex.Lock() s.state = gortsplib.ServerSessionStateRecord - s.transport = s.session.SetuppedTransport() + s.transport = s.rsession.SetuppedTransport() s.mutex.Unlock() return &base.Response{ @@ -353,8 +325,8 @@ func (s *rtspSession) onRecord(_ *gortsplib.ServerHandlerOnRecordCtx) (*base.Res } // onPause is called by rtspServer. -func (s *rtspSession) onPause(_ *gortsplib.ServerHandlerOnPauseCtx) (*base.Response, error) { - switch s.session.State() { +func (s *session) onPause(_ *gortsplib.ServerHandlerOnPauseCtx) (*base.Response, error) { + switch s.rsession.State() { case gortsplib.ServerSessionStatePlay: s.onUnreadHook() @@ -363,7 +335,7 @@ func (s *rtspSession) onPause(_ *gortsplib.ServerHandlerOnPauseCtx) (*base.Respo s.mutex.Unlock() case gortsplib.ServerSessionStateRecord: - s.path.stopPublisher(pathStopPublisherReq{author: s}) + s.path.StopPublisher(defs.PathStopPublisherReq{Author: s}) s.mutex.Lock() s.state = gortsplib.ServerSessionStatePreRecord @@ -375,8 +347,8 @@ func (s *rtspSession) onPause(_ *gortsplib.ServerHandlerOnPauseCtx) (*base.Respo }, nil } -// apiReaderDescribe implements reader. -func (s *rtspSession) apiReaderDescribe() defs.APIPathSourceOrReader { +// APIReaderDescribe implements reader. +func (s *session) APIReaderDescribe() defs.APIPathSourceOrReader { return defs.APIPathSourceOrReader{ Type: func() string { if s.isTLS { @@ -389,26 +361,26 @@ func (s *rtspSession) apiReaderDescribe() defs.APIPathSourceOrReader { } // APISourceDescribe implements source. -func (s *rtspSession) APISourceDescribe() defs.APIPathSourceOrReader { - return s.apiReaderDescribe() +func (s *session) APISourceDescribe() defs.APIPathSourceOrReader { + return s.APIReaderDescribe() } // onPacketLost is called by rtspServer. -func (s *rtspSession) onPacketLost(ctx *gortsplib.ServerHandlerOnPacketLostCtx) { +func (s *session) onPacketLost(ctx *gortsplib.ServerHandlerOnPacketLostCtx) { s.decodeErrLogger.Log(logger.Warn, ctx.Error.Error()) } // onDecodeError is called by rtspServer. -func (s *rtspSession) onDecodeError(ctx *gortsplib.ServerHandlerOnDecodeErrorCtx) { +func (s *session) onDecodeError(ctx *gortsplib.ServerHandlerOnDecodeErrorCtx) { s.decodeErrLogger.Log(logger.Warn, ctx.Error.Error()) } // onStreamWriteError is called by rtspServer. -func (s *rtspSession) onStreamWriteError(ctx *gortsplib.ServerHandlerOnStreamWriteErrorCtx) { +func (s *session) onStreamWriteError(ctx *gortsplib.ServerHandlerOnStreamWriteErrorCtx) { s.writeErrLogger.Log(logger.Warn, ctx.Error.Error()) } -func (s *rtspSession) apiItem() *defs.APIRTSPSession { +func (s *session) apiItem() *defs.APIRTSPSession { s.mutex.Lock() defer s.mutex.Unlock() @@ -436,7 +408,7 @@ func (s *rtspSession) apiItem() *defs.APIRTSPSession { v := s.transport.String() return &v }(), - BytesReceived: s.session.BytesReceived(), - BytesSent: s.session.BytesSent(), + BytesReceived: s.rsession.BytesReceived(), + BytesSent: s.rsession.BytesSent(), } } diff --git a/internal/core/srt_conn.go b/internal/servers/srt/conn.go similarity index 60% rename from internal/core/srt_conn.go rename to internal/servers/srt/conn.go index 9315ae1a..63baf09d 100644 --- a/internal/core/srt_conn.go +++ b/internal/servers/srt/conn.go @@ -1,4 +1,4 @@ -package core +package srt import ( "bufio" @@ -12,7 +12,7 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/description" mcmpegts "github.com/bluenviron/mediacommon/pkg/formats/mpegts" - "github.com/datarhei/gosrt" + srt "github.com/datarhei/gosrt" "github.com/google/uuid" "github.com/bluenviron/mediamtx/internal/asyncwriter" @@ -42,24 +42,15 @@ func srtCheckPassphrase(connReq srt.ConnRequest, passphrase string) error { return nil } -type srtConnState int +type connState int const ( - srtConnStateRead srtConnState = iota + 1 - srtConnStatePublish + connStateRead connState = iota + 1 + connStatePublish ) -type srtConnPathManager interface { - addReader(req pathAddReaderReq) pathAddReaderRes - addPublisher(req pathAddPublisherReq) pathAddPublisherRes -} - -type srtConnParent interface { - logger.Writer - closeConn(*srtConn) -} - -type srtConn struct { +type conn struct { + parentCtx context.Context rtspAddress string readTimeout conf.StringDuration writeTimeout conf.StringDuration @@ -71,15 +62,15 @@ type srtConn struct { runOnDisconnect string wg *sync.WaitGroup externalCmdPool *externalcmd.Pool - pathManager srtConnPathManager - parent srtConnParent + pathManager defs.PathManager + parent *Server ctx context.Context ctxCancel func() created time.Time uuid uuid.UUID mutex sync.RWMutex - state srtConnState + state connState pathName string sconn srt.Conn @@ -87,67 +78,34 @@ type srtConn struct { chSetConn chan srt.Conn } -func newSRTConn( - parentCtx context.Context, - rtspAddress string, - readTimeout conf.StringDuration, - writeTimeout conf.StringDuration, - writeQueueSize int, - udpMaxPayloadSize int, - connReq srt.ConnRequest, - runOnConnect string, - runOnConnectRestart bool, - runOnDisconnect string, - wg *sync.WaitGroup, - externalCmdPool *externalcmd.Pool, - pathManager srtConnPathManager, - parent srtConnParent, -) *srtConn { - ctx, ctxCancel := context.WithCancel(parentCtx) - - c := &srtConn{ - rtspAddress: rtspAddress, - readTimeout: readTimeout, - writeTimeout: writeTimeout, - writeQueueSize: writeQueueSize, - udpMaxPayloadSize: udpMaxPayloadSize, - connReq: connReq, - runOnConnect: runOnConnect, - runOnConnectRestart: runOnConnectRestart, - runOnDisconnect: runOnDisconnect, - wg: wg, - externalCmdPool: externalCmdPool, - pathManager: pathManager, - parent: parent, - ctx: ctx, - ctxCancel: ctxCancel, - created: time.Now(), - uuid: uuid.New(), - chNew: make(chan srtNewConnReq), - chSetConn: make(chan srt.Conn), - } +func (c *conn) initialize() { + c.ctx, c.ctxCancel = context.WithCancel(c.parentCtx) + + c.created = time.Now() + c.uuid = uuid.New() + c.chNew = make(chan srtNewConnReq) + c.chSetConn = make(chan srt.Conn) c.Log(logger.Info, "opened") c.wg.Add(1) go c.run() - - return c } -func (c *srtConn) close() { +func (c *conn) Close() { c.ctxCancel() } -func (c *srtConn) Log(level logger.Level, format string, args ...interface{}) { +// Log implements logger.Writer. +func (c *conn) Log(level logger.Level, format string, args ...interface{}) { c.parent.Log(level, "[conn %v] "+format, append([]interface{}{c.connReq.RemoteAddr()}, args...)...) } -func (c *srtConn) ip() net.IP { +func (c *conn) ip() net.IP { return c.connReq.RemoteAddr().(*net.UDPAddr).IP } -func (c *srtConn) run() { //nolint:dupl +func (c *conn) run() { //nolint:dupl defer c.wg.Done() onDisconnectHook := hooks.OnConnect(hooks.OnConnectParams{ @@ -157,7 +115,7 @@ func (c *srtConn) run() { //nolint:dupl RunOnConnectRestart: c.runOnConnectRestart, RunOnDisconnect: c.runOnDisconnect, RTSPAddress: c.rtspAddress, - Desc: c.apiReaderDescribe(), + Desc: c.APIReaderDescribe(), }) defer onDisconnectHook() @@ -170,7 +128,7 @@ func (c *srtConn) run() { //nolint:dupl c.Log(logger.Info, "closed: %v", err) } -func (c *srtConn) runInner() error { +func (c *conn) runInner() error { var req srtNewConnReq select { case req = <-c.chNew: @@ -187,7 +145,7 @@ func (c *srtConn) runInner() error { return err } -func (c *srtConn) runInner2(req srtNewConnReq) (bool, error) { +func (c *conn) runInner2(req srtNewConnReq) (bool, error) { parts := strings.Split(req.connReq.StreamId(), ":") if (len(parts) < 2 || len(parts) > 5) || (parts[0] != "read" && parts[0] != "publish") { return false, fmt.Errorf("invalid streamid '%s':"+ @@ -220,34 +178,34 @@ func (c *srtConn) runInner2(req srtNewConnReq) (bool, error) { return c.runRead(req, pathName, user, pass, query) } -func (c *srtConn) runPublish(req srtNewConnReq, pathName string, user string, pass string, query string) (bool, error) { - res := c.pathManager.addPublisher(pathAddPublisherReq{ - author: c, - accessRequest: pathAccessRequest{ - name: pathName, - ip: c.ip(), - publish: true, - user: user, - pass: pass, - proto: authProtocolSRT, - id: &c.uuid, - query: query, +func (c *conn) runPublish(req srtNewConnReq, pathName string, user string, pass string, query string) (bool, error) { + res := c.pathManager.AddPublisher(defs.PathAddPublisherReq{ + Author: c, + AccessRequest: defs.PathAccessRequest{ + Name: pathName, + IP: c.ip(), + Publish: true, + User: user, + Pass: pass, + Proto: defs.AuthProtocolSRT, + ID: &c.uuid, + Query: query, }, }) - if res.err != nil { - if terr, ok := res.err.(*errAuthentication); ok { + if res.Err != nil { + if terr, ok := res.Err.(*defs.ErrAuthentication); ok { // TODO: re-enable. Currently this freezes the listener. // wait some seconds to stop brute force attacks // <-time.After(srtPauseAfterAuthError) return false, terr } - return false, res.err + return false, res.Err } - defer res.path.removePublisher(pathRemovePublisherReq{author: c}) + defer res.Path.RemovePublisher(defs.PathRemovePublisherReq{Author: c}) - err := srtCheckPassphrase(req.connReq, res.path.conf.SRTPublishPassphrase) + err := srtCheckPassphrase(req.connReq, res.Path.SafeConf().SRTPublishPassphrase) if err != nil { return false, err } @@ -258,14 +216,14 @@ func (c *srtConn) runPublish(req srtNewConnReq, pathName string, user string, pa } c.mutex.Lock() - c.state = srtConnStatePublish + c.state = connStatePublish c.pathName = pathName c.sconn = sconn c.mutex.Unlock() readerErr := make(chan error) go func() { - readerErr <- c.runPublishReader(sconn, res.path) + readerErr <- c.runPublishReader(sconn, res.Path) }() select { @@ -280,7 +238,7 @@ func (c *srtConn) runPublish(req srtNewConnReq, pathName string, user string, pa } } -func (c *srtConn) runPublishReader(sconn srt.Conn, path *path) error { +func (c *conn) runPublishReader(sconn srt.Conn, path defs.Path) error { sconn.SetReadDeadline(time.Now().Add(time.Duration(c.readTimeout))) r, err := mcmpegts.NewReader(mcmpegts.NewBufferedReader(sconn)) if err != nil { @@ -300,16 +258,16 @@ func (c *srtConn) runPublishReader(sconn srt.Conn, path *path) error { return err } - rres := path.startPublisher(pathStartPublisherReq{ - author: c, - desc: &description.Session{Medias: medias}, - generateRTPPackets: true, + rres := path.StartPublisher(defs.PathStartPublisherReq{ + Author: c, + Desc: &description.Session{Medias: medias}, + GenerateRTPPackets: true, }) - if rres.err != nil { - return rres.err + if rres.Err != nil { + return rres.Err } - stream = rres.stream + stream = rres.Stream for { err := r.Read() @@ -319,33 +277,33 @@ func (c *srtConn) runPublishReader(sconn srt.Conn, path *path) error { } } -func (c *srtConn) runRead(req srtNewConnReq, pathName string, user string, pass string, query string) (bool, error) { - res := c.pathManager.addReader(pathAddReaderReq{ - author: c, - accessRequest: pathAccessRequest{ - name: pathName, - ip: c.ip(), - user: user, - pass: pass, - proto: authProtocolSRT, - id: &c.uuid, - query: query, +func (c *conn) runRead(req srtNewConnReq, pathName string, user string, pass string, query string) (bool, error) { + res := c.pathManager.AddReader(defs.PathAddReaderReq{ + Author: c, + AccessRequest: defs.PathAccessRequest{ + Name: pathName, + IP: c.ip(), + User: user, + Pass: pass, + Proto: defs.AuthProtocolSRT, + ID: &c.uuid, + Query: query, }, }) - if res.err != nil { - if terr, ok := res.err.(*errAuthentication); ok { + if res.Err != nil { + if terr, ok := res.Err.(*defs.ErrAuthentication); ok { // TODO: re-enable. Currently this freezes the listener. // wait some seconds to stop brute force attacks // <-time.After(srtPauseAfterAuthError) return false, terr } - return false, res.err + return false, res.Err } - defer res.path.removeReader(pathRemoveReaderReq{author: c}) + defer res.Path.RemoveReader(defs.PathRemoveReaderReq{Author: c}) - err := srtCheckPassphrase(req.connReq, res.path.conf.SRTReadPassphrase) + err := srtCheckPassphrase(req.connReq, res.Path.SafeConf().SRTReadPassphrase) if err != nil { return false, err } @@ -357,31 +315,31 @@ func (c *srtConn) runRead(req srtNewConnReq, pathName string, user string, pass defer sconn.Close() c.mutex.Lock() - c.state = srtConnStateRead + c.state = connStateRead c.pathName = pathName c.sconn = sconn c.mutex.Unlock() writer := asyncwriter.New(c.writeQueueSize, c) - defer res.stream.RemoveReader(writer) + defer res.Stream.RemoveReader(writer) bw := bufio.NewWriterSize(sconn, srtMaxPayloadSize(c.udpMaxPayloadSize)) - err = mpegtsSetupWrite(res.stream, writer, bw, sconn, time.Duration(c.writeTimeout)) + err = mpegts.FromStream(res.Stream, writer, bw, sconn, time.Duration(c.writeTimeout)) if err != nil { return true, err } c.Log(logger.Info, "is reading from path '%s', %s", - res.path.name, readerMediaInfo(writer, res.stream)) + res.Path.Name, defs.MediasInfo(res.Stream.MediasForReader(writer))) onUnreadHook := hooks.OnRead(hooks.OnReadParams{ Logger: c, ExternalCmdPool: c.externalCmdPool, - Conf: res.path.safeConf(), - ExternalCmdEnv: res.path.externalCmdEnv(), - Reader: c.apiReaderDescribe(), + Conf: res.Path.SafeConf(), + ExternalCmdEnv: res.Path.ExternalCmdEnv(), + Reader: c.APIReaderDescribe(), Query: query, }) defer onUnreadHook() @@ -401,7 +359,7 @@ func (c *srtConn) runRead(req srtNewConnReq, pathName string, user string, pass } } -func (c *srtConn) exchangeRequestWithConn(req srtNewConnReq) (srt.Conn, error) { +func (c *conn) exchangeRequestWithConn(req srtNewConnReq) (srt.Conn, error) { req.res <- c select { @@ -414,7 +372,7 @@ func (c *srtConn) exchangeRequestWithConn(req srtNewConnReq) (srt.Conn, error) { } // new is called by srtListener through srtServer. -func (c *srtConn) new(req srtNewConnReq) *srtConn { +func (c *conn) new(req srtNewConnReq) *conn { select { case c.chNew <- req: return <-req.res @@ -425,15 +383,15 @@ func (c *srtConn) new(req srtNewConnReq) *srtConn { } // setConn is called by srtListener . -func (c *srtConn) setConn(sconn srt.Conn) { +func (c *conn) setConn(sconn srt.Conn) { select { case c.chSetConn <- sconn: case <-c.ctx.Done(): } } -// apiReaderDescribe implements reader. -func (c *srtConn) apiReaderDescribe() defs.APIPathSourceOrReader { +// APIReaderDescribe implements reader. +func (c *conn) APIReaderDescribe() defs.APIPathSourceOrReader { return defs.APIPathSourceOrReader{ Type: "srtConn", ID: c.uuid.String(), @@ -441,11 +399,11 @@ func (c *srtConn) apiReaderDescribe() defs.APIPathSourceOrReader { } // APISourceDescribe implements source. -func (c *srtConn) APISourceDescribe() defs.APIPathSourceOrReader { - return c.apiReaderDescribe() +func (c *conn) APISourceDescribe() defs.APIPathSourceOrReader { + return c.APIReaderDescribe() } -func (c *srtConn) apiItem() *defs.APISRTConn { +func (c *conn) apiItem() *defs.APISRTConn { c.mutex.RLock() defer c.mutex.RUnlock() @@ -465,10 +423,10 @@ func (c *srtConn) apiItem() *defs.APISRTConn { RemoteAddr: c.connReq.RemoteAddr().String(), State: func() defs.APISRTConnState { switch c.state { - case srtConnStateRead: + case connStateRead: return defs.APISRTConnStateRead - case srtConnStatePublish: + case connStatePublish: return defs.APISRTConnStatePublish default: diff --git a/internal/core/srt_listener.go b/internal/servers/srt/listener.go similarity index 59% rename from internal/core/srt_listener.go rename to internal/servers/srt/listener.go index d0a11821..38b9e297 100644 --- a/internal/core/srt_listener.go +++ b/internal/servers/srt/listener.go @@ -1,35 +1,23 @@ -package core +package srt import ( "sync" - "github.com/datarhei/gosrt" + srt "github.com/datarhei/gosrt" ) -type srtListener struct { +type listener struct { ln srt.Listener wg *sync.WaitGroup - parent *srtServer + parent *Server } -func newSRTListener( - ln srt.Listener, - wg *sync.WaitGroup, - parent *srtServer, -) *srtListener { - l := &srtListener{ - ln: ln, - wg: wg, - parent: parent, - } - +func (l *listener) initialize() { l.wg.Add(1) go l.run() - - return l } -func (l *srtListener) run() { +func (l *listener) run() { defer l.wg.Done() err := l.runInner() @@ -37,9 +25,9 @@ func (l *srtListener) run() { l.parent.acceptError(err) } -func (l *srtListener) runInner() error { +func (l *listener) runInner() error { for { - var sconn *srtConn + var sconn *conn conn, _, err := l.ln.Accept(func(req srt.ConnRequest) srt.ConnType { sconn = l.parent.newConnRequest(req) if sconn == nil { diff --git a/internal/servers/srt/server.go b/internal/servers/srt/server.go new file mode 100644 index 00000000..046a26b9 --- /dev/null +++ b/internal/servers/srt/server.go @@ -0,0 +1,310 @@ +// Package srt contains a SRT server. +package srt + +import ( + "context" + "fmt" + "sort" + "sync" + "time" + + srt "github.com/datarhei/gosrt" + "github.com/google/uuid" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" + "github.com/bluenviron/mediamtx/internal/externalcmd" + "github.com/bluenviron/mediamtx/internal/logger" +) + +func srtMaxPayloadSize(u int) int { + return ((u - 16) / 188) * 188 // 16 = SRT header, 188 = MPEG-TS packet +} + +type srtNewConnReq struct { + connReq srt.ConnRequest + res chan *conn +} + +type serverAPIConnsListRes struct { + data *defs.APISRTConnList + err error +} + +type serverAPIConnsListReq struct { + res chan serverAPIConnsListRes +} + +type serverAPIConnsGetRes struct { + data *defs.APISRTConn + 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 serverParent interface { + logger.Writer +} + +// Server is a SRT server. +type Server struct { + Address string + RTSPAddress string + ReadTimeout conf.StringDuration + WriteTimeout conf.StringDuration + WriteQueueSize int + UDPMaxPayloadSize int + RunOnConnect string + RunOnConnectRestart bool + RunOnDisconnect string + ExternalCmdPool *externalcmd.Pool + PathManager defs.PathManager + Parent serverParent + + ctx context.Context + ctxCancel func() + wg sync.WaitGroup + ln srt.Listener + conns map[*conn]struct{} + + // in + chNewConnRequest chan srtNewConnReq + chAcceptErr chan error + chCloseConn chan *conn + chAPIConnsList chan serverAPIConnsListReq + chAPIConnsGet chan serverAPIConnsGetReq + chAPIConnsKick chan serverAPIConnsKickReq +} + +// Initialize initializes the server. +func (s *Server) Initialize() error { + conf := srt.DefaultConfig() + conf.ConnectionTimeout = time.Duration(s.ReadTimeout) + conf.PayloadSize = uint32(srtMaxPayloadSize(s.UDPMaxPayloadSize)) + + var err error + s.ln, err = srt.Listen("srt", s.Address, conf) + if err != nil { + return err + } + + s.ctx, s.ctxCancel = context.WithCancel(context.Background()) + + s.conns = make(map[*conn]struct{}) + s.chNewConnRequest = make(chan srtNewConnReq) + 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) + + s.Log(logger.Info, "listener opened on "+s.Address+" (UDP)") + + l := &listener{ + ln: s.ln, + wg: &s.wg, + parent: s, + } + l.initialize() + + s.wg.Add(1) + go s.run() + + return nil +} + +// Log implements logger.Writer. +func (s *Server) Log(level logger.Level, format string, args ...interface{}) { + s.Parent.Log(level, "[SRT] "+format, args...) +} + +// Close closes the server. +func (s *Server) Close() { + s.Log(logger.Info, "listener is closing") + s.ctxCancel() + s.wg.Wait() +} + +func (s *Server) run() { + defer s.wg.Done() + +outer: + for { + select { + case err := <-s.chAcceptErr: + s.Log(logger.Error, "%s", err) + break outer + + case req := <-s.chNewConnRequest: + c := &conn{ + parentCtx: s.ctx, + rtspAddress: s.RTSPAddress, + readTimeout: s.ReadTimeout, + writeTimeout: s.WriteTimeout, + writeQueueSize: s.WriteQueueSize, + udpMaxPayloadSize: s.UDPMaxPayloadSize, + connReq: req.connReq, + runOnConnect: s.RunOnConnect, + runOnConnectRestart: s.RunOnConnectRestart, + runOnDisconnect: s.RunOnDisconnect, + wg: &s.wg, + externalCmdPool: s.ExternalCmdPool, + pathManager: s.PathManager, + parent: s, + } + c.initialize() + s.conns[c] = struct{}{} + req.res <- c + + case c := <-s.chCloseConn: + delete(s.conns, c) + + case req := <-s.chAPIConnsList: + data := &defs.APISRTConnList{ + Items: []*defs.APISRTConn{}, + } + + 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: fmt.Errorf("connection not found")} + continue + } + + req.res <- serverAPIConnsGetRes{data: c.apiItem()} + + case req := <-s.chAPIConnsKick: + c := s.findConnByUUID(req.uuid) + if c == nil { + req.res <- serverAPIConnsKickRes{err: fmt.Errorf("connection not found")} + 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 sx := range s.conns { + if sx.uuid == uuid { + return sx + } + } + return nil +} + +// newConnRequest is called by srtListener. +func (s *Server) newConnRequest(connReq srt.ConnRequest) *conn { + req := srtNewConnReq{ + connReq: connReq, + res: make(chan *conn), + } + + select { + case s.chNewConnRequest <- req: + c := <-req.res + + return c.new(req) + + case <-s.ctx.Done(): + return nil + } +} + +// acceptError is called by srtListener. +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 is called by api. +func (s *Server) APIConnsList() (*defs.APISRTConnList, 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 is called by api. +func (s *Server) APIConnsGet(uuid uuid.UUID) (*defs.APISRTConn, 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 is called by api. +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") + } +} diff --git a/internal/core/webrtc_http_server.go b/internal/servers/webrtc/http_server.go similarity index 65% rename from internal/core/webrtc_http_server.go rename to internal/servers/webrtc/http_server.go index ea98e9c9..2b7d4e5c 100644 --- a/internal/core/webrtc_http_server.go +++ b/internal/servers/webrtc/http_server.go @@ -1,4 +1,4 @@ -package core +package webrtc import ( _ "embed" @@ -12,7 +12,6 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" - pwebrtc "github.com/pion/webrtc/v3" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" @@ -22,18 +21,18 @@ import ( "github.com/bluenviron/mediamtx/internal/restrictnetwork" ) -//go:embed webrtc_publish_index.html -var webrtcPublishIndex []byte +//go:embed publish_index.html +var publishIndex []byte -//go:embed webrtc_read_index.html -var webrtcReadIndex []byte +//go:embed read_index.html +var readIndex []byte var ( reWHIPWHEPNoID = regexp.MustCompile("^/(.+?)/(whip|whep)$") reWHIPWHEPWithID = regexp.MustCompile("^/(.+?)/(whip|whep)/(.+?)$") ) -func webrtcWriteError(ctx *gin.Context, statusCode int, err error) { +func writeError(ctx *gin.Context, statusCode int, err error) { ctx.JSON(statusCode, &defs.APIError{ Error: err.Error(), }) @@ -50,128 +49,111 @@ func sessionLocation(publish bool, secret uuid.UUID) string { return ret } -type webRTCHTTPServerParent interface { - logger.Writer - generateICEServers() ([]pwebrtc.ICEServer, error) - newSession(req webRTCNewSessionReq) webRTCNewSessionRes - addSessionCandidates(req webRTCAddSessionCandidatesReq) webRTCAddSessionCandidatesRes - deleteSession(req webRTCDeleteSessionReq) error -} - -type webRTCHTTPServer struct { - allowOrigin string - pathManager *pathManager - parent webRTCHTTPServerParent +type httpServer struct { + address string + encryption bool + serverKey string + serverCert string + allowOrigin string + trustedProxies conf.IPsOrCIDRs + readTimeout conf.StringDuration + pathManager defs.PathManager + parent *Server inner *httpserv.WrappedServer } -func newWebRTCHTTPServer( //nolint:dupl - address string, - encryption bool, - serverKey string, - serverCert string, - allowOrigin string, - trustedProxies conf.IPsOrCIDRs, - readTimeout conf.StringDuration, - pathManager *pathManager, - parent webRTCHTTPServerParent, -) (*webRTCHTTPServer, error) { - if encryption { - if serverCert == "" { - return nil, fmt.Errorf("server cert is missing") +func (s *httpServer) initialize() error { + if s.encryption { + if s.serverCert == "" { + return fmt.Errorf("server cert is missing") } } else { - serverKey = "" - serverCert = "" - } - - s := &webRTCHTTPServer{ - allowOrigin: allowOrigin, - pathManager: pathManager, - parent: parent, + s.serverKey = "" + s.serverCert = "" } router := gin.New() - router.SetTrustedProxies(trustedProxies.ToTrustedProxies()) //nolint:errcheck + router.SetTrustedProxies(s.trustedProxies.ToTrustedProxies()) //nolint:errcheck router.NoRoute(s.onRequest) - network, address := restrictnetwork.Restrict("tcp", address) + network, address := restrictnetwork.Restrict("tcp", s.address) var err error s.inner, err = httpserv.NewWrappedServer( network, address, - time.Duration(readTimeout), - serverCert, - serverKey, + time.Duration(s.readTimeout), + s.serverCert, + s.serverKey, router, s, ) if err != nil { - return nil, err + return err } - return s, nil + return nil } -func (s *webRTCHTTPServer) Log(level logger.Level, format string, args ...interface{}) { +// Log implements logger.Writer. +func (s *httpServer) Log(level logger.Level, format string, args ...interface{}) { s.parent.Log(level, format, args...) } -func (s *webRTCHTTPServer) close() { +func (s *httpServer) close() { s.inner.Close() } -func (s *webRTCHTTPServer) checkAuthOutsideSession(ctx *gin.Context, path string, publish bool) bool { +func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, path string, publish bool) bool { ip := ctx.ClientIP() _, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr) remoteAddr := net.JoinHostPort(ip, port) user, pass, hasCredentials := ctx.Request.BasicAuth() - res := s.pathManager.getConfForPath(pathGetConfForPathReq{ - accessRequest: pathAccessRequest{ - name: path, - query: ctx.Request.URL.RawQuery, - publish: publish, - ip: net.ParseIP(ip), - user: user, - pass: pass, - proto: authProtocolWebRTC, + res := s.pathManager.GetConfForPath(defs.PathGetConfForPathReq{ + AccessRequest: defs.PathAccessRequest{ + Name: path, + Query: ctx.Request.URL.RawQuery, + Publish: publish, + IP: net.ParseIP(ip), + User: user, + Pass: pass, + Proto: defs.AuthProtocolWebRTC, }, }) - if res.err != nil { - if terr, ok := res.err.(*errAuthentication); ok { + if res.Err != nil { + if terr, ok := res.Err.(*defs.ErrAuthentication); ok { if !hasCredentials { ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.Writer.WriteHeader(http.StatusUnauthorized) return false } - s.Log(logger.Info, "connection %v failed to authenticate: %v", remoteAddr, terr.message) + s.Log(logger.Info, "connection %v failed to authenticate: %v", remoteAddr, terr.Message) // wait some seconds to stop brute force attacks <-time.After(webrtcPauseAfterAuthError) - webrtcWriteError(ctx, http.StatusUnauthorized, terr) + writeError(ctx, http.StatusUnauthorized, terr) return false } - webrtcWriteError(ctx, http.StatusInternalServerError, res.err) + writeError(ctx, http.StatusInternalServerError, res.Err) return false } return true } -func (s *webRTCHTTPServer) onWHIPOptions(ctx *gin.Context, path string, publish bool) { +func (s *httpServer) onWHIPOptions(ctx *gin.Context, path string, publish bool) { if !s.checkAuthOutsideSession(ctx, path, publish) { return } servers, err := s.parent.generateICEServers() if err != nil { - webrtcWriteError(ctx, http.StatusInternalServerError, err) + writeError(ctx, http.StatusInternalServerError, err) return } @@ -182,9 +164,9 @@ func (s *webRTCHTTPServer) onWHIPOptions(ctx *gin.Context, path string, publish ctx.Writer.WriteHeader(http.StatusNoContent) } -func (s *webRTCHTTPServer) onWHIPPost(ctx *gin.Context, path string, publish bool) { +func (s *httpServer) onWHIPPost(ctx *gin.Context, path string, publish bool) { if ctx.Request.Header.Get("Content-Type") != "application/sdp" { - webrtcWriteError(ctx, http.StatusBadRequest, fmt.Errorf("invalid Content-Type")) + writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid Content-Type")) return } @@ -208,13 +190,13 @@ func (s *webRTCHTTPServer) onWHIPPost(ctx *gin.Context, path string, publish boo publish: publish, }) if res.err != nil { - webrtcWriteError(ctx, res.errStatusCode, res.err) + writeError(ctx, res.errStatusCode, res.err) return } servers, err := s.parent.generateICEServers() if err != nil { - webrtcWriteError(ctx, http.StatusInternalServerError, err) + writeError(ctx, http.StatusInternalServerError, err) return } @@ -229,15 +211,15 @@ func (s *webRTCHTTPServer) onWHIPPost(ctx *gin.Context, path string, publish boo ctx.Writer.Write(res.answer) } -func (s *webRTCHTTPServer) onWHIPPatch(ctx *gin.Context, rawSecret string) { +func (s *httpServer) onWHIPPatch(ctx *gin.Context, rawSecret string) { secret, err := uuid.Parse(rawSecret) if err != nil { - webrtcWriteError(ctx, http.StatusBadRequest, fmt.Errorf("invalid secret")) + writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid secret")) return } if ctx.Request.Header.Get("Content-Type") != "application/trickle-ice-sdpfrag" { - webrtcWriteError(ctx, http.StatusBadRequest, fmt.Errorf("invalid Content-Type")) + writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid Content-Type")) return } @@ -248,7 +230,7 @@ func (s *webRTCHTTPServer) onWHIPPatch(ctx *gin.Context, rawSecret string) { candidates, err := webrtc.ICEFragmentUnmarshal(byts) if err != nil { - webrtcWriteError(ctx, http.StatusBadRequest, err) + writeError(ctx, http.StatusBadRequest, err) return } @@ -257,17 +239,17 @@ func (s *webRTCHTTPServer) onWHIPPatch(ctx *gin.Context, rawSecret string) { candidates: candidates, }) if res.err != nil { - webrtcWriteError(ctx, http.StatusInternalServerError, res.err) + writeError(ctx, http.StatusInternalServerError, res.err) return } ctx.Writer.WriteHeader(http.StatusNoContent) } -func (s *webRTCHTTPServer) onWHIPDelete(ctx *gin.Context, rawSecret string) { +func (s *httpServer) onWHIPDelete(ctx *gin.Context, rawSecret string) { secret, err := uuid.Parse(rawSecret) if err != nil { - webrtcWriteError(ctx, http.StatusBadRequest, fmt.Errorf("invalid secret")) + writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid secret")) return } @@ -275,14 +257,14 @@ func (s *webRTCHTTPServer) onWHIPDelete(ctx *gin.Context, rawSecret string) { secret: secret, }) if err != nil { - webrtcWriteError(ctx, http.StatusInternalServerError, err) + writeError(ctx, http.StatusInternalServerError, err) return } ctx.Writer.WriteHeader(http.StatusOK) } -func (s *webRTCHTTPServer) onPage(ctx *gin.Context, path string, publish bool) { +func (s *httpServer) onPage(ctx *gin.Context, path string, publish bool) { if !s.checkAuthOutsideSession(ctx, path, publish) { return } @@ -292,13 +274,13 @@ func (s *webRTCHTTPServer) onPage(ctx *gin.Context, path string, publish bool) { ctx.Writer.WriteHeader(http.StatusOK) if publish { - ctx.Writer.Write(webrtcPublishIndex) + ctx.Writer.Write(publishIndex) } else { - ctx.Writer.Write(webrtcReadIndex) + ctx.Writer.Write(readIndex) } } -func (s *webRTCHTTPServer) onRequest(ctx *gin.Context) { +func (s *httpServer) onRequest(ctx *gin.Context) { ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.allowOrigin) ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") @@ -324,7 +306,7 @@ func (s *webRTCHTTPServer) onRequest(ctx *gin.Context) { // RFC draft-ietf-whip-09 // The WHIP endpoints MUST return an "405 Method Not Allowed" response // for any HTTP GET, HEAD or PUT requests - webrtcWriteError(ctx, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed")) + writeError(ctx, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed")) } return } diff --git a/internal/core/webrtc_publish_index.html b/internal/servers/webrtc/publish_index.html similarity index 100% rename from internal/core/webrtc_publish_index.html rename to internal/servers/webrtc/publish_index.html diff --git a/internal/core/webrtc_read_index.html b/internal/servers/webrtc/read_index.html similarity index 100% rename from internal/core/webrtc_read_index.html rename to internal/servers/webrtc/read_index.html diff --git a/internal/core/webrtc_manager.go b/internal/servers/webrtc/server.go similarity index 52% rename from internal/core/webrtc_manager.go rename to internal/servers/webrtc/server.go index 5a4939c6..d261d42b 100644 --- a/internal/core/webrtc_manager.go +++ b/internal/servers/webrtc/server.go @@ -1,4 +1,5 @@ -package core +// Package webrtc contains a WebRTC server. +package webrtc import ( "context" @@ -93,36 +94,36 @@ func randomTurnUser() (string, error) { return string(b), nil } -type webRTCManagerAPISessionsListRes struct { +type serverAPISessionsListRes struct { data *defs.APIWebRTCSessionList err error } -type webRTCManagerAPISessionsListReq struct { - res chan webRTCManagerAPISessionsListRes +type serverAPISessionsListReq struct { + res chan serverAPISessionsListRes } -type webRTCManagerAPISessionsGetRes struct { +type serverAPISessionsGetRes struct { data *defs.APIWebRTCSession err error } -type webRTCManagerAPISessionsGetReq struct { +type serverAPISessionsGetReq struct { uuid uuid.UUID - res chan webRTCManagerAPISessionsGetRes + res chan serverAPISessionsGetRes } -type webRTCManagerAPISessionsKickRes struct { +type serverAPISessionsKickRes struct { err error } -type webRTCManagerAPISessionsKickReq struct { +type serverAPISessionsKickReq struct { uuid uuid.UUID - res chan webRTCManagerAPISessionsKickRes + res chan serverAPISessionsKickRes } type webRTCNewSessionRes struct { - sx *webRTCSession + sx *session answer []byte errStatusCode int err error @@ -140,7 +141,7 @@ type webRTCNewSessionReq struct { } type webRTCAddSessionCandidatesRes struct { - sx *webRTCSession + sx *session err error } @@ -159,11 +160,12 @@ type webRTCDeleteSessionReq struct { res chan webRTCDeleteSessionRes } -type webRTCManagerParent interface { +type serverParent interface { logger.Writer } -type webRTCManager struct { +// Server is a WebRTC server. +type Server struct { Address string Encryption bool ServerKey string @@ -179,59 +181,60 @@ type webRTCManager struct { AdditionalHosts []string ICEServers []conf.WebRTCICEServer ExternalCmdPool *externalcmd.Pool - PathManager *pathManager - Parent webRTCManagerParent + PathManager defs.PathManager + Parent serverParent ctx context.Context ctxCancel func() - httpServer *webRTCHTTPServer + httpServer *httpServer udpMuxLn net.PacketConn tcpMuxLn net.Listener api *pwebrtc.API - sessions map[*webRTCSession]struct{} - sessionsBySecret map[uuid.UUID]*webRTCSession + sessions map[*session]struct{} + sessionsBySecret map[uuid.UUID]*session // in chNewSession chan webRTCNewSessionReq - chCloseSession chan *webRTCSession + chCloseSession chan *session chAddSessionCandidates chan webRTCAddSessionCandidatesReq chDeleteSession chan webRTCDeleteSessionReq - chAPISessionsList chan webRTCManagerAPISessionsListReq - chAPISessionsGet chan webRTCManagerAPISessionsGetReq - chAPIConnsKick chan webRTCManagerAPISessionsKickReq + chAPISessionsList chan serverAPISessionsListReq + chAPISessionsGet chan serverAPISessionsGetReq + chAPIConnsKick chan serverAPISessionsKickReq // out done chan struct{} } -func (m *webRTCManager) initialize() error { +// Initialize initializes the server. +func (s *Server) Initialize() error { ctx, ctxCancel := context.WithCancel(context.Background()) - m.ctx = ctx - m.ctxCancel = ctxCancel - m.sessions = make(map[*webRTCSession]struct{}) - m.sessionsBySecret = make(map[uuid.UUID]*webRTCSession) - m.chNewSession = make(chan webRTCNewSessionReq) - m.chCloseSession = make(chan *webRTCSession) - m.chAddSessionCandidates = make(chan webRTCAddSessionCandidatesReq) - m.chDeleteSession = make(chan webRTCDeleteSessionReq) - m.chAPISessionsList = make(chan webRTCManagerAPISessionsListReq) - m.chAPISessionsGet = make(chan webRTCManagerAPISessionsGetReq) - m.chAPIConnsKick = make(chan webRTCManagerAPISessionsKickReq) - m.done = make(chan struct{}) - - var err error - m.httpServer, err = newWebRTCHTTPServer( - m.Address, - m.Encryption, - m.ServerKey, - m.ServerCert, - m.AllowOrigin, - m.TrustedProxies, - m.ReadTimeout, - m.PathManager, - m, - ) + s.ctx = ctx + s.ctxCancel = ctxCancel + s.sessions = make(map[*session]struct{}) + s.sessionsBySecret = make(map[uuid.UUID]*session) + s.chNewSession = make(chan webRTCNewSessionReq) + s.chCloseSession = make(chan *session) + s.chAddSessionCandidates = make(chan webRTCAddSessionCandidatesReq) + s.chDeleteSession = make(chan webRTCDeleteSessionReq) + s.chAPISessionsList = make(chan serverAPISessionsListReq) + s.chAPISessionsGet = make(chan serverAPISessionsGetReq) + s.chAPIConnsKick = make(chan serverAPISessionsKickReq) + s.done = make(chan struct{}) + + s.httpServer = &httpServer{ + address: s.Address, + encryption: s.Encryption, + serverKey: s.ServerKey, + serverCert: s.ServerCert, + allowOrigin: s.AllowOrigin, + trustedProxies: s.TrustedProxies, + readTimeout: s.ReadTimeout, + pathManager: s.PathManager, + parent: s, + } + err := s.httpServer.initialize() if err != nil { ctxCancel() return err @@ -239,95 +242,97 @@ func (m *webRTCManager) initialize() error { apiConf := webrtc.APIConf{ LocalRandomUDP: false, - IPsFromInterfaces: m.IPsFromInterfaces, - IPsFromInterfacesList: m.IPsFromInterfacesList, - AdditionalHosts: m.AdditionalHosts, + IPsFromInterfaces: s.IPsFromInterfaces, + IPsFromInterfacesList: s.IPsFromInterfacesList, + AdditionalHosts: s.AdditionalHosts, } - if m.LocalUDPAddress != "" { - m.udpMuxLn, err = net.ListenPacket(restrictnetwork.Restrict("udp", m.LocalUDPAddress)) + if s.LocalUDPAddress != "" { + s.udpMuxLn, err = net.ListenPacket(restrictnetwork.Restrict("udp", s.LocalUDPAddress)) if err != nil { - m.httpServer.close() + s.httpServer.close() ctxCancel() return err } - apiConf.ICEUDPMux = pwebrtc.NewICEUDPMux(webrtcNilLogger, m.udpMuxLn) + apiConf.ICEUDPMux = pwebrtc.NewICEUDPMux(webrtcNilLogger, s.udpMuxLn) } - if m.LocalTCPAddress != "" { - m.tcpMuxLn, err = net.Listen(restrictnetwork.Restrict("tcp", m.LocalTCPAddress)) + if s.LocalTCPAddress != "" { + s.tcpMuxLn, err = net.Listen(restrictnetwork.Restrict("tcp", s.LocalTCPAddress)) if err != nil { - m.udpMuxLn.Close() - m.httpServer.close() + s.udpMuxLn.Close() + s.httpServer.close() ctxCancel() return err } - apiConf.ICETCPMux = pwebrtc.NewICETCPMux(webrtcNilLogger, m.tcpMuxLn, 8) + apiConf.ICETCPMux = pwebrtc.NewICETCPMux(webrtcNilLogger, s.tcpMuxLn, 8) } - m.api, err = webrtc.NewAPI(apiConf) + s.api, err = webrtc.NewAPI(apiConf) if err != nil { - m.udpMuxLn.Close() - m.tcpMuxLn.Close() - m.httpServer.close() + s.udpMuxLn.Close() + s.tcpMuxLn.Close() + s.httpServer.close() ctxCancel() return err } - str := "listener opened on " + m.Address + " (HTTP)" - if m.udpMuxLn != nil { - str += ", " + m.LocalUDPAddress + " (ICE/UDP)" + str := "listener opened on " + s.Address + " (HTTP)" + if s.udpMuxLn != nil { + str += ", " + s.LocalUDPAddress + " (ICE/UDP)" } - if m.tcpMuxLn != nil { - str += ", " + m.LocalTCPAddress + " (ICE/TCP)" + if s.tcpMuxLn != nil { + str += ", " + s.LocalTCPAddress + " (ICE/TCP)" } - m.Log(logger.Info, str) + s.Log(logger.Info, str) - go m.run() + go s.run() return nil } -// Log is the main logging function. -func (m *webRTCManager) Log(level logger.Level, format string, args ...interface{}) { - m.Parent.Log(level, "[WebRTC] "+format, args...) +// Log implements logger.Writer. +func (s *Server) Log(level logger.Level, format string, args ...interface{}) { + s.Parent.Log(level, "[WebRTC] "+format, args...) } -func (m *webRTCManager) close() { - m.Log(logger.Info, "listener is closing") - m.ctxCancel() - <-m.done +// Close closes the server. +func (s *Server) Close() { + s.Log(logger.Info, "listener is closing") + s.ctxCancel() + <-s.done } -func (m *webRTCManager) run() { - defer close(m.done) +func (s *Server) run() { + defer close(s.done) var wg sync.WaitGroup outer: for { select { - case req := <-m.chNewSession: - sx := newWebRTCSession( - m.ctx, - m.WriteQueueSize, - m.api, - req, - &wg, - m.ExternalCmdPool, - m.PathManager, - m, - ) - m.sessions[sx] = struct{}{} - m.sessionsBySecret[sx.secret] = sx + case req := <-s.chNewSession: + sx := &session{ + parentCtx: s.ctx, + writeQueueSize: s.WriteQueueSize, + api: s.api, + req: req, + wg: &wg, + externalCmdPool: s.ExternalCmdPool, + pathManager: s.PathManager, + parent: s, + } + sx.initialize() + s.sessions[sx] = struct{}{} + s.sessionsBySecret[sx.secret] = sx req.res <- webRTCNewSessionRes{sx: sx} - case sx := <-m.chCloseSession: - delete(m.sessions, sx) - delete(m.sessionsBySecret, sx.secret) + case sx := <-s.chCloseSession: + delete(s.sessions, sx) + delete(s.sessionsBySecret, sx.secret) - case req := <-m.chAddSessionCandidates: - sx, ok := m.sessionsBySecret[req.secret] + case req := <-s.chAddSessionCandidates: + sx, ok := s.sessionsBySecret[req.secret] if !ok { req.res <- webRTCAddSessionCandidatesRes{err: fmt.Errorf("session not found")} continue @@ -335,25 +340,25 @@ outer: req.res <- webRTCAddSessionCandidatesRes{sx: sx} - case req := <-m.chDeleteSession: - sx, ok := m.sessionsBySecret[req.secret] + case req := <-s.chDeleteSession: + sx, ok := s.sessionsBySecret[req.secret] if !ok { req.res <- webRTCDeleteSessionRes{err: fmt.Errorf("session not found")} continue } - delete(m.sessions, sx) - delete(m.sessionsBySecret, sx.secret) - sx.close() + delete(s.sessions, sx) + delete(s.sessionsBySecret, sx.secret) + sx.Close() req.res <- webRTCDeleteSessionRes{} - case req := <-m.chAPISessionsList: + case req := <-s.chAPISessionsList: data := &defs.APIWebRTCSessionList{ Items: []*defs.APIWebRTCSession{}, } - for sx := range m.sessions { + for sx := range s.sessions { data.Items = append(data.Items, sx.apiItem()) } @@ -361,52 +366,52 @@ outer: return data.Items[i].Created.Before(data.Items[j].Created) }) - req.res <- webRTCManagerAPISessionsListRes{data: data} + req.res <- serverAPISessionsListRes{data: data} - case req := <-m.chAPISessionsGet: - sx := m.findSessionByUUID(req.uuid) + case req := <-s.chAPISessionsGet: + sx := s.findSessionByUUID(req.uuid) if sx == nil { - req.res <- webRTCManagerAPISessionsGetRes{err: fmt.Errorf("session not found")} + req.res <- serverAPISessionsGetRes{err: fmt.Errorf("session not found")} continue } - req.res <- webRTCManagerAPISessionsGetRes{data: sx.apiItem()} + req.res <- serverAPISessionsGetRes{data: sx.apiItem()} - case req := <-m.chAPIConnsKick: - sx := m.findSessionByUUID(req.uuid) + case req := <-s.chAPIConnsKick: + sx := s.findSessionByUUID(req.uuid) if sx == nil { - req.res <- webRTCManagerAPISessionsKickRes{err: fmt.Errorf("session not found")} + req.res <- serverAPISessionsKickRes{err: fmt.Errorf("session not found")} continue } - delete(m.sessions, sx) - delete(m.sessionsBySecret, sx.secret) - sx.close() + delete(s.sessions, sx) + delete(s.sessionsBySecret, sx.secret) + sx.Close() - req.res <- webRTCManagerAPISessionsKickRes{} + req.res <- serverAPISessionsKickRes{} - case <-m.ctx.Done(): + case <-s.ctx.Done(): break outer } } - m.ctxCancel() + s.ctxCancel() wg.Wait() - m.httpServer.close() + s.httpServer.close() - if m.udpMuxLn != nil { - m.udpMuxLn.Close() + if s.udpMuxLn != nil { + s.udpMuxLn.Close() } - if m.tcpMuxLn != nil { - m.tcpMuxLn.Close() + if s.tcpMuxLn != nil { + s.tcpMuxLn.Close() } } -func (m *webRTCManager) findSessionByUUID(uuid uuid.UUID) *webRTCSession { - for sx := range m.sessions { +func (s *Server) findSessionByUUID(uuid uuid.UUID) *session { + for sx := range s.sessions { if sx.uuid == uuid { return sx } @@ -414,10 +419,10 @@ func (m *webRTCManager) findSessionByUUID(uuid uuid.UUID) *webRTCSession { return nil } -func (m *webRTCManager) generateICEServers() ([]pwebrtc.ICEServer, error) { - ret := make([]pwebrtc.ICEServer, len(m.ICEServers)) +func (s *Server) generateICEServers() ([]pwebrtc.ICEServer, error) { + ret := make([]pwebrtc.ICEServer, len(s.ICEServers)) - for i, server := range m.ICEServers { + for i, server := range s.ICEServers { if server.Username == "AUTH_SECRET" { expireDate := time.Now().Add(webrtcTurnSecretExpiration).Unix() @@ -445,16 +450,16 @@ func (m *webRTCManager) generateICEServers() ([]pwebrtc.ICEServer, error) { } // newSession is called by webRTCHTTPServer. -func (m *webRTCManager) newSession(req webRTCNewSessionReq) webRTCNewSessionRes { +func (s *Server) newSession(req webRTCNewSessionReq) webRTCNewSessionRes { req.res = make(chan webRTCNewSessionRes) select { - case m.chNewSession <- req: + case s.chNewSession <- req: res := <-req.res return res.sx.new(req) - case <-m.ctx.Done(): + case <-s.ctx.Done(): return webRTCNewSessionRes{ errStatusCode: http.StatusInternalServerError, err: fmt.Errorf("terminated"), @@ -462,21 +467,21 @@ func (m *webRTCManager) newSession(req webRTCNewSessionReq) webRTCNewSessionRes } } -// closeSession is called by webRTCSession. -func (m *webRTCManager) closeSession(sx *webRTCSession) { +// closeSession is called by session. +func (s *Server) closeSession(sx *session) { select { - case m.chCloseSession <- sx: - case <-m.ctx.Done(): + case s.chCloseSession <- sx: + case <-s.ctx.Done(): } } // addSessionCandidates is called by webRTCHTTPServer. -func (m *webRTCManager) addSessionCandidates( +func (s *Server) addSessionCandidates( req webRTCAddSessionCandidatesReq, ) webRTCAddSessionCandidatesRes { req.res = make(chan webRTCAddSessionCandidatesRes) select { - case m.chAddSessionCandidates <- req: + case s.chAddSessionCandidates <- req: res1 := <-req.res if res1.err != nil { return res1 @@ -484,70 +489,70 @@ func (m *webRTCManager) addSessionCandidates( return res1.sx.addCandidates(req) - case <-m.ctx.Done(): + case <-s.ctx.Done(): return webRTCAddSessionCandidatesRes{err: fmt.Errorf("terminated")} } } // deleteSession is called by webRTCHTTPServer. -func (m *webRTCManager) deleteSession(req webRTCDeleteSessionReq) error { +func (s *Server) deleteSession(req webRTCDeleteSessionReq) error { req.res = make(chan webRTCDeleteSessionRes) select { - case m.chDeleteSession <- req: + case s.chDeleteSession <- req: res := <-req.res return res.err - case <-m.ctx.Done(): + case <-s.ctx.Done(): return fmt.Errorf("terminated") } } -// apiSessionsList is called by api. -func (m *webRTCManager) apiSessionsList() (*defs.APIWebRTCSessionList, error) { - req := webRTCManagerAPISessionsListReq{ - res: make(chan webRTCManagerAPISessionsListRes), +// APISessionsList is called by api. +func (s *Server) APISessionsList() (*defs.APIWebRTCSessionList, error) { + req := serverAPISessionsListReq{ + res: make(chan serverAPISessionsListRes), } select { - case m.chAPISessionsList <- req: + case s.chAPISessionsList <- req: res := <-req.res return res.data, res.err - case <-m.ctx.Done(): + case <-s.ctx.Done(): return nil, fmt.Errorf("terminated") } } -// apiSessionsGet is called by api. -func (m *webRTCManager) apiSessionsGet(uuid uuid.UUID) (*defs.APIWebRTCSession, error) { - req := webRTCManagerAPISessionsGetReq{ +// APISessionsGet is called by api. +func (s *Server) APISessionsGet(uuid uuid.UUID) (*defs.APIWebRTCSession, error) { + req := serverAPISessionsGetReq{ uuid: uuid, - res: make(chan webRTCManagerAPISessionsGetRes), + res: make(chan serverAPISessionsGetRes), } select { - case m.chAPISessionsGet <- req: + case s.chAPISessionsGet <- req: res := <-req.res return res.data, res.err - case <-m.ctx.Done(): + case <-s.ctx.Done(): return nil, fmt.Errorf("terminated") } } -// apiSessionsKick is called by api. -func (m *webRTCManager) apiSessionsKick(uuid uuid.UUID) error { - req := webRTCManagerAPISessionsKickReq{ +// APISessionsKick is called by api. +func (s *Server) APISessionsKick(uuid uuid.UUID) error { + req := serverAPISessionsKickReq{ uuid: uuid, - res: make(chan webRTCManagerAPISessionsKickRes), + res: make(chan serverAPISessionsKickRes), } select { - case m.chAPIConnsKick <- req: + case s.chAPIConnsKick <- req: res := <-req.res return res.err - case <-m.ctx.Done(): + case <-s.ctx.Done(): return fmt.Errorf("terminated") } } diff --git a/internal/core/webrtc_session.go b/internal/servers/webrtc/session.go similarity index 76% rename from internal/core/webrtc_session.go rename to internal/servers/webrtc/session.go index 79003a4e..4499ea70 100644 --- a/internal/core/webrtc_session.go +++ b/internal/servers/webrtc/session.go @@ -1,4 +1,4 @@ -package core +package webrtc import ( "context" @@ -33,7 +33,7 @@ import ( type setupStreamFunc func(*webrtc.OutgoingTrack) error -func webrtcFindVideoTrack( +func findVideoTrack( stream *stream.Stream, writer *asyncwriter.Writer, ) (format.Format, setupStreamFunc) { @@ -202,7 +202,7 @@ func webrtcFindVideoTrack( return nil, nil } -func webrtcFindAudioTrack( +func findAudioTrack( stream *stream.Stream, writer *asyncwriter.Writer, ) (format.Format, setupStreamFunc) { @@ -264,19 +264,15 @@ func whipOffer(body []byte) *pwebrtc.SessionDescription { } } -type webRTCSessionPathManager interface { - addPublisher(req pathAddPublisherReq) pathAddPublisherRes - addReader(req pathAddReaderReq) pathAddReaderRes -} - -type webRTCSession struct { +type session struct { + parentCtx context.Context writeQueueSize int api *pwebrtc.API req webRTCNewSessionReq wg *sync.WaitGroup externalCmdPool *externalcmd.Pool - pathManager webRTCSessionPathManager - parent *webRTCManager + pathManager defs.PathManager + parent *Server ctx context.Context ctxCancel func() @@ -290,53 +286,34 @@ type webRTCSession struct { chAddCandidates chan webRTCAddSessionCandidatesReq } -func newWebRTCSession( - parentCtx context.Context, - writeQueueSize int, - api *pwebrtc.API, - req webRTCNewSessionReq, - wg *sync.WaitGroup, - externalCmdPool *externalcmd.Pool, - pathManager webRTCSessionPathManager, - parent *webRTCManager, -) *webRTCSession { - ctx, ctxCancel := context.WithCancel(parentCtx) - - s := &webRTCSession{ - writeQueueSize: writeQueueSize, - api: api, - req: req, - wg: wg, - externalCmdPool: externalCmdPool, - pathManager: pathManager, - parent: parent, - ctx: ctx, - ctxCancel: ctxCancel, - created: time.Now(), - uuid: uuid.New(), - secret: uuid.New(), - chNew: make(chan webRTCNewSessionReq), - chAddCandidates: make(chan webRTCAddSessionCandidatesReq), - } - - s.Log(logger.Info, "created by %s", req.remoteAddr) - - wg.Add(1) - go s.run() +func (s *session) initialize() { + ctx, ctxCancel := context.WithCancel(s.parentCtx) - return s + s.ctx = ctx + s.ctxCancel = ctxCancel + s.created = time.Now() + s.uuid = uuid.New() + s.secret = uuid.New() + s.chNew = make(chan webRTCNewSessionReq) + s.chAddCandidates = make(chan webRTCAddSessionCandidatesReq) + + s.Log(logger.Info, "created by %s", s.req.remoteAddr) + + s.wg.Add(1) + go s.run() } -func (s *webRTCSession) Log(level logger.Level, format string, args ...interface{}) { +// 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 %v] "+format, append([]interface{}{id}, args...)...) } -func (s *webRTCSession) close() { +func (s *session) Close() { s.ctxCancel() } -func (s *webRTCSession) run() { +func (s *session) run() { defer s.wg.Done() err := s.runInner() @@ -348,7 +325,7 @@ func (s *webRTCSession) run() { s.Log(logger.Info, "closed: %v", err) } -func (s *webRTCSession) runInner() error { +func (s *session) runInner() error { select { case <-s.chNew: case <-s.ctx.Done(): @@ -367,41 +344,41 @@ func (s *webRTCSession) runInner() error { return err } -func (s *webRTCSession) runInner2() (int, error) { +func (s *session) runInner2() (int, error) { if s.req.publish { return s.runPublish() } return s.runRead() } -func (s *webRTCSession) runPublish() (int, error) { +func (s *session) runPublish() (int, error) { ip, _, _ := net.SplitHostPort(s.req.remoteAddr) - res := s.pathManager.addPublisher(pathAddPublisherReq{ - author: s, - accessRequest: pathAccessRequest{ - name: s.req.pathName, - query: s.req.query, - publish: true, - ip: net.ParseIP(ip), - user: s.req.user, - pass: s.req.pass, - proto: authProtocolWebRTC, - id: &s.uuid, + res := s.pathManager.AddPublisher(defs.PathAddPublisherReq{ + Author: s, + AccessRequest: defs.PathAccessRequest{ + Name: s.req.pathName, + Query: s.req.query, + Publish: true, + IP: net.ParseIP(ip), + User: s.req.user, + Pass: s.req.pass, + Proto: defs.AuthProtocolWebRTC, + ID: &s.uuid, }, }) - if res.err != nil { - if _, ok := res.err.(*errAuthentication); ok { + if res.Err != nil { + if _, ok := res.Err.(*defs.ErrAuthentication); ok { // wait some seconds to stop brute force attacks <-time.After(webrtcPauseAfterAuthError) - return http.StatusUnauthorized, res.err + return http.StatusUnauthorized, res.Err } - return http.StatusBadRequest, res.err + return http.StatusBadRequest, res.Err } - defer res.path.removePublisher(pathRemovePublisherReq{author: s}) + defer res.Path.RemovePublisher(defs.PathRemovePublisherReq{Author: s}) iceServers, err := s.parent.generateICEServers() if err != nil { @@ -463,13 +440,13 @@ func (s *webRTCSession) runPublish() (int, error) { medias := webrtc.TracksToMedias(tracks) - rres := res.path.startPublisher(pathStartPublisherReq{ - author: s, - desc: &description.Session{Medias: medias}, - generateRTPPackets: false, + rres := res.Path.StartPublisher(defs.PathStartPublisherReq{ + Author: s, + Desc: &description.Session{Medias: medias}, + GenerateRTPPackets: false, }) - if rres.err != nil { - return 0, rres.err + if rres.Err != nil { + return 0, rres.Err } timeDecoder := rtptime.NewGlobalDecoder() @@ -491,7 +468,7 @@ func (s *webRTCSession) runPublish() (int, error) { continue } - rres.stream.WriteRTPPacket(cmedia, cmedia.Formats[0], pkt, time.Now(), pts) + rres.Stream.WriteRTPPacket(cmedia, cmedia.Formats[0], pkt, time.Now(), pts) } }() } @@ -505,37 +482,37 @@ func (s *webRTCSession) runPublish() (int, error) { } } -func (s *webRTCSession) runRead() (int, error) { +func (s *session) runRead() (int, error) { ip, _, _ := net.SplitHostPort(s.req.remoteAddr) - res := s.pathManager.addReader(pathAddReaderReq{ - author: s, - accessRequest: pathAccessRequest{ - name: s.req.pathName, - query: s.req.query, - ip: net.ParseIP(ip), - user: s.req.user, - pass: s.req.pass, - proto: authProtocolWebRTC, - id: &s.uuid, + res := s.pathManager.AddReader(defs.PathAddReaderReq{ + Author: s, + AccessRequest: defs.PathAccessRequest{ + Name: s.req.pathName, + Query: s.req.query, + IP: net.ParseIP(ip), + User: s.req.user, + Pass: s.req.pass, + Proto: defs.AuthProtocolWebRTC, + ID: &s.uuid, }, }) - if res.err != nil { - if _, ok := res.err.(*errAuthentication); ok { + if res.Err != nil { + if _, ok := res.Err.(*defs.ErrAuthentication); ok { // wait some seconds to stop brute force attacks <-time.After(webrtcPauseAfterAuthError) - return http.StatusUnauthorized, res.err + return http.StatusUnauthorized, res.Err } - if strings.HasPrefix(res.err.Error(), "no one is publishing") { - return http.StatusNotFound, res.err + if strings.HasPrefix(res.Err.Error(), "no one is publishing") { + return http.StatusNotFound, res.Err } - return http.StatusBadRequest, res.err + return http.StatusBadRequest, res.Err } - defer res.path.removeReader(pathRemoveReaderReq{author: s}) + defer res.Path.RemoveReader(defs.PathRemoveReaderReq{Author: s}) iceServers, err := s.parent.generateICEServers() if err != nil { @@ -556,8 +533,8 @@ func (s *webRTCSession) runRead() (int, error) { writer := asyncwriter.New(s.writeQueueSize, s) - videoTrack, videoSetup := webrtcFindVideoTrack(res.stream, writer) - audioTrack, audioSetup := webrtcFindAudioTrack(res.stream, writer) + videoTrack, videoSetup := findVideoTrack(res.Stream, writer) + audioTrack, audioSetup := findAudioTrack(res.Stream, writer) if videoTrack == nil && audioTrack == nil { return http.StatusBadRequest, fmt.Errorf( @@ -589,7 +566,7 @@ func (s *webRTCSession) runRead() (int, error) { s.pc = pc s.mutex.Unlock() - defer res.stream.RemoveReader(writer) + defer res.Stream.RemoveReader(writer) n := 0 @@ -609,14 +586,14 @@ func (s *webRTCSession) runRead() (int, error) { } s.Log(logger.Info, "is reading from path '%s', %s", - res.path.name, readerMediaInfo(writer, res.stream)) + res.Path.Name(), defs.MediasInfo(res.Stream.MediasForReader(writer))) onUnreadHook := hooks.OnRead(hooks.OnReadParams{ Logger: s, ExternalCmdPool: s.externalCmdPool, - Conf: res.path.safeConf(), - ExternalCmdEnv: res.path.externalCmdEnv(), - Reader: s.apiReaderDescribe(), + Conf: res.Path.SafeConf(), + ExternalCmdEnv: res.Path.ExternalCmdEnv(), + Reader: s.APIReaderDescribe(), Query: s.req.query, }) defer onUnreadHook() @@ -637,14 +614,14 @@ func (s *webRTCSession) runRead() (int, error) { } } -func (s *webRTCSession) writeAnswer(answer *pwebrtc.SessionDescription) { +func (s *session) writeAnswer(answer *pwebrtc.SessionDescription) { s.req.res <- webRTCNewSessionRes{ sx: s, answer: []byte(answer.SDP), } } -func (s *webRTCSession) readRemoteCandidates(pc *webrtc.PeerConnection) { +func (s *session) readRemoteCandidates(pc *webrtc.PeerConnection) { for { select { case req := <-s.chAddCandidates: @@ -662,8 +639,8 @@ func (s *webRTCSession) readRemoteCandidates(pc *webrtc.PeerConnection) { } } -// new is called by webRTCHTTPServer through webRTCManager. -func (s *webRTCSession) new(req webRTCNewSessionReq) webRTCNewSessionRes { +// new is called by webRTCHTTPServer through Server. +func (s *session) new(req webRTCNewSessionReq) webRTCNewSessionRes { select { case s.chNew <- req: return <-req.res @@ -673,8 +650,8 @@ func (s *webRTCSession) new(req webRTCNewSessionReq) webRTCNewSessionRes { } } -// addCandidates is called by webRTCHTTPServer through webRTCManager. -func (s *webRTCSession) addCandidates( +// addCandidates is called by webRTCHTTPServer through Server. +func (s *session) addCandidates( req webRTCAddSessionCandidatesReq, ) webRTCAddSessionCandidatesRes { select { @@ -686,20 +663,20 @@ func (s *webRTCSession) addCandidates( } } -// apiReaderDescribe implements reader. -func (s *webRTCSession) apiReaderDescribe() defs.APIPathSourceOrReader { +// APIReaderDescribe implements reader. +func (s *session) APIReaderDescribe() defs.APIPathSourceOrReader { return defs.APIPathSourceOrReader{ - Type: "webRTCSession", + Type: "webrtcSession", ID: s.uuid.String(), } } // APISourceDescribe implements source. -func (s *webRTCSession) APISourceDescribe() defs.APIPathSourceOrReader { - return s.apiReaderDescribe() +func (s *session) APISourceDescribe() defs.APIPathSourceOrReader { + return s.APIReaderDescribe() } -func (s *webRTCSession) apiItem() *defs.APIWebRTCSession { +func (s *session) apiItem() *defs.APIWebRTCSession { s.mutex.RLock() defer s.mutex.RUnlock() diff --git a/internal/staticsources/hls/source.go b/internal/staticsources/hls/source.go index b99a76f6..cd3d8cfe 100644 --- a/internal/staticsources/hls/source.go +++ b/internal/staticsources/hls/source.go @@ -24,7 +24,7 @@ type Source struct { Parent defs.StaticSourceParent } -// Log implements StaticSource. +// Log implements logger.Writer. func (s *Source) Log(level logger.Level, format string, args ...interface{}) { s.Parent.Log(level, "[HLS source] "+format, args...) } diff --git a/internal/staticsources/hls/source_test.go b/internal/staticsources/hls/source_test.go index c4f5f4ae..38d9bb88 100644 --- a/internal/staticsources/hls/source_test.go +++ b/internal/staticsources/hls/source_test.go @@ -32,17 +32,17 @@ var track2 = &mpegts.Track{ }, } -type testHLSManager struct { +type testHLSServer struct { s *http.Server } -func newTestHLSManager() (*testHLSManager, error) { +func newTestHLSServer() (*testHLSServer, error) { ln, err := net.Listen("tcp", "localhost:5780") if err != nil { return nil, err } - ts := &testHLSManager{} + ts := &testHLSServer{} gin.SetMode(gin.ReleaseMode) router := gin.New() @@ -56,11 +56,11 @@ func newTestHLSManager() (*testHLSManager, error) { return ts, nil } -func (ts *testHLSManager) close() { +func (ts *testHLSServer) close() { ts.s.Shutdown(context.Background()) } -func (ts *testHLSManager) onPlaylist(ctx *gin.Context) { +func (ts *testHLSServer) onPlaylist(ctx *gin.Context) { cnt := `#EXTM3U #EXT-X-VERSION:3 #EXT-X-ALLOW-CACHE:NO @@ -77,7 +77,7 @@ segment2.ts io.Copy(ctx.Writer, bytes.NewReader([]byte(cnt))) } -func (ts *testHLSManager) onSegment1(ctx *gin.Context) { +func (ts *testHLSServer) onSegment1(ctx *gin.Context) { ctx.Writer.Header().Set("Content-Type", `video/MP2T`) w := mpegts.NewWriter(ctx.Writer, []*mpegts.Track{track1, track2}) @@ -85,7 +85,7 @@ func (ts *testHLSManager) onSegment1(ctx *gin.Context) { w.WriteMPEG4Audio(track2, 1*90000, [][]byte{{1, 2, 3, 4}}) //nolint:errcheck } -func (ts *testHLSManager) onSegment2(ctx *gin.Context) { +func (ts *testHLSServer) onSegment2(ctx *gin.Context) { ctx.Writer.Header().Set("Content-Type", `video/MP2T`) w := mpegts.NewWriter(ctx.Writer, []*mpegts.Track{track1, track2}) @@ -97,7 +97,7 @@ func (ts *testHLSManager) onSegment2(ctx *gin.Context) { } func TestSource(t *testing.T) { - ts, err := newTestHLSManager() + ts, err := newTestHLSServer() require.NoError(t, err) defer ts.close() diff --git a/internal/staticsources/rpicamera/source.go b/internal/staticsources/rpicamera/source.go index 59aacf76..f44992f4 100644 --- a/internal/staticsources/rpicamera/source.go +++ b/internal/staticsources/rpicamera/source.go @@ -57,7 +57,7 @@ type Source struct { Parent defs.StaticSourceParent } -// Log implements StaticSource. +// Log implements logger.Writer. func (s *Source) Log(level logger.Level, format string, args ...interface{}) { s.Parent.Log(level, "[RPI Camera source] "+format, args...) } diff --git a/internal/staticsources/rtmp/source.go b/internal/staticsources/rtmp/source.go index 58e39d7c..70cefb52 100644 --- a/internal/staticsources/rtmp/source.go +++ b/internal/staticsources/rtmp/source.go @@ -28,7 +28,7 @@ type Source struct { Parent defs.StaticSourceParent } -// Log implements StaticSource. +// Log implements logger.Writer. func (s *Source) Log(level logger.Level, format string, args ...interface{}) { s.Parent.Log(level, "[RTMP source] "+format, args...) } diff --git a/internal/staticsources/rtsp/source.go b/internal/staticsources/rtsp/source.go index 3ad612ab..4d974755 100644 --- a/internal/staticsources/rtsp/source.go +++ b/internal/staticsources/rtsp/source.go @@ -68,7 +68,7 @@ type Source struct { Parent defs.StaticSourceParent } -// Log implements StaticSource. +// Log implements logger.Writer. func (s *Source) Log(level logger.Level, format string, args ...interface{}) { s.Parent.Log(level, "[RTSP source] "+format, args...) } diff --git a/internal/staticsources/srt/source.go b/internal/staticsources/srt/source.go index 5d6f34c0..a5adf114 100644 --- a/internal/staticsources/srt/source.go +++ b/internal/staticsources/srt/source.go @@ -6,7 +6,7 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/description" mcmpegts "github.com/bluenviron/mediacommon/pkg/formats/mpegts" - "github.com/datarhei/gosrt" + srt "github.com/datarhei/gosrt" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" @@ -21,7 +21,7 @@ type Source struct { Parent defs.StaticSourceParent } -// Log implements StaticSource. +// Log implements logger.Writer. func (s *Source) Log(level logger.Level, format string, args ...interface{}) { s.Parent.Log(level, "[SRT source] "+format, args...) } diff --git a/internal/staticsources/udp/source.go b/internal/staticsources/udp/source.go index 4c7cd8c7..08ea8b95 100644 --- a/internal/staticsources/udp/source.go +++ b/internal/staticsources/udp/source.go @@ -49,7 +49,7 @@ type Source struct { Parent defs.StaticSourceParent } -// Log implements StaticSource. +// Log implements logger.Writer. func (s *Source) Log(level logger.Level, format string, args ...interface{}) { s.Parent.Log(level, "[UDP source] "+format, args...) } diff --git a/internal/staticsources/webrtc/source.go b/internal/staticsources/webrtc/source.go index 05f8c122..f022bb84 100644 --- a/internal/staticsources/webrtc/source.go +++ b/internal/staticsources/webrtc/source.go @@ -23,7 +23,7 @@ type Source struct { Parent defs.StaticSourceParent } -// Log implements StaticSource. +// Log implements logger.Writer. func (s *Source) Log(level logger.Level, format string, args ...interface{}) { s.Parent.Log(level, "[WebRTC source] "+format, args...) }