Browse Source

move servers into internal/servers (#2792)

pull/2794/head
Alessandro Ros 2 years ago committed by GitHub
parent
commit
11988249df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .github/workflows/bump_hls_js.yml
  2. 4
      README.md
  3. 133
      internal/core/api.go
  4. 72
      internal/core/auth.go
  5. 315
      internal/core/core.go
  6. 309
      internal/core/hls_manager.go
  7. 2
      internal/core/hls_server_test.go
  8. 51
      internal/core/metrics.go
  9. 16
      internal/core/metrics_test.go
  10. 363
      internal/core/path.go
  11. 166
      internal/core/path_manager.go
  12. 3
      internal/core/path_manager_test.go
  13. 5
      internal/core/path_test.go
  14. 1
      internal/core/pprof.go
  15. 7
      internal/core/publisher.go
  16. 17
      internal/core/reader.go
  17. 48
      internal/core/rtmp_listener.go
  18. 327
      internal/core/rtmp_server.go
  19. 454
      internal/core/rtsp_server.go
  20. 330
      internal/core/srt_server.go
  21. 3
      internal/core/static_source_handler.go
  22. 0
      internal/core/webrtc_server_test.go
  23. 23
      internal/defs/auth.go
  24. 137
      internal/defs/path.go
  25. 9
      internal/defs/path_manager.go
  26. 7
      internal/defs/publisher.go
  27. 7
      internal/defs/reader.go
  28. 17
      internal/defs/source.go
  29. 10
      internal/protocols/mpegts/from_stream.go
  30. 2
      internal/record/agent.go
  31. 2
      internal/record/cleaner.go
  32. 0
      internal/servers/hls/hls.min.js
  33. 99
      internal/servers/hls/http_server.go
  34. 0
      internal/servers/hls/index.html
  35. 145
      internal/servers/hls/muxer.go
  36. 290
      internal/servers/hls/server.go
  37. 198
      internal/servers/rtmp/conn.go
  38. 36
      internal/servers/rtmp/listener.go
  39. 308
      internal/servers/rtmp/server.go
  40. 133
      internal/servers/rtsp/conn.go
  41. 430
      internal/servers/rtsp/server.go
  42. 230
      internal/servers/rtsp/session.go
  43. 212
      internal/servers/srt/conn.go
  44. 28
      internal/servers/srt/listener.go
  45. 310
      internal/servers/srt/server.go
  46. 150
      internal/servers/webrtc/http_server.go
  47. 0
      internal/servers/webrtc/publish_index.html
  48. 0
      internal/servers/webrtc/read_index.html
  49. 327
      internal/servers/webrtc/server.go
  50. 199
      internal/servers/webrtc/session.go
  51. 2
      internal/staticsources/hls/source.go
  52. 16
      internal/staticsources/hls/source_test.go
  53. 2
      internal/staticsources/rpicamera/source.go
  54. 2
      internal/staticsources/rtmp/source.go
  55. 2
      internal/staticsources/rtsp/source.go
  56. 4
      internal/staticsources/srt/source.go
  57. 2
      internal/staticsources/udp/source.go
  58. 2
      internal/staticsources/webrtc/source.go

4
.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) && ((git checkout deps/hlsjs && git rebase ${GITHUB_REF_NAME}) || git checkout -b deps/hlsjs)
- run: > - run: >
curl -o internal/core/hls.min.js https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js curl -o internal/servers/hls/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 && 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 - id: check_repo
run: > run: >

4
README.md

@ -435,7 +435,7 @@ This web page can be embedded into another web page by using an iframe:
<iframe src="http://mediamtx-ip:8889/mystream/publish" scrolling="no"></iframe> <iframe src="http://mediamtx-ip:8889/mystream/publish" scrolling="no"></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 ### By device
@ -823,7 +823,7 @@ This web page can be embedded into another web page by using an iframe:
<iframe src="http://mediamtx-ip:8889/mystream" scrolling="no"></iframe> <iframe src="http://mediamtx-ip:8889/mystream" scrolling="no"></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: 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:

133
internal/core/api.go

@ -102,35 +102,35 @@ type apiPathManager interface {
apiPathsGet(string) (*defs.APIPath, error) apiPathsGet(string) (*defs.APIPath, error)
} }
type apiHLSManager interface { type apiHLSServer interface {
apiMuxersList() (*defs.APIHLSMuxerList, error) APIMuxersList() (*defs.APIHLSMuxerList, error)
apiMuxersGet(string) (*defs.APIHLSMuxer, error) APIMuxersGet(string) (*defs.APIHLSMuxer, error)
} }
type apiRTSPServer interface { type apiRTSPServer interface {
apiConnsList() (*defs.APIRTSPConnsList, error) APIConnsList() (*defs.APIRTSPConnsList, error)
apiConnsGet(uuid.UUID) (*defs.APIRTSPConn, error) APIConnsGet(uuid.UUID) (*defs.APIRTSPConn, error)
apiSessionsList() (*defs.APIRTSPSessionList, error) APISessionsList() (*defs.APIRTSPSessionList, error)
apiSessionsGet(uuid.UUID) (*defs.APIRTSPSession, error) APISessionsGet(uuid.UUID) (*defs.APIRTSPSession, error)
apiSessionsKick(uuid.UUID) error APISessionsKick(uuid.UUID) error
} }
type apiRTMPServer interface { type apiRTMPServer interface {
apiConnsList() (*defs.APIRTMPConnList, error) APIConnsList() (*defs.APIRTMPConnList, error)
apiConnsGet(uuid.UUID) (*defs.APIRTMPConn, error) APIConnsGet(uuid.UUID) (*defs.APIRTMPConn, error)
apiConnsKick(uuid.UUID) error APIConnsKick(uuid.UUID) error
} }
type apiWebRTCManager interface { type apiSRTServer interface {
apiSessionsList() (*defs.APIWebRTCSessionList, error) APIConnsList() (*defs.APISRTConnList, error)
apiSessionsGet(uuid.UUID) (*defs.APIWebRTCSession, error) APIConnsGet(uuid.UUID) (*defs.APISRTConn, error)
apiSessionsKick(uuid.UUID) error APIConnsKick(uuid.UUID) error
} }
type apiSRTServer interface { type apiWebRTCServer interface {
apiConnsList() (*defs.APISRTConnList, error) APISessionsList() (*defs.APIWebRTCSessionList, error)
apiConnsGet(uuid.UUID) (*defs.APISRTConn, error) APISessionsGet(uuid.UUID) (*defs.APIWebRTCSession, error)
apiConnsKick(uuid.UUID) error APISessionsKick(uuid.UUID) error
} }
type apiParent interface { type apiParent interface {
@ -139,16 +139,16 @@ type apiParent interface {
} }
type api struct { type api struct {
conf *conf.Conf conf *conf.Conf
pathManager apiPathManager pathManager apiPathManager
rtspServer apiRTSPServer rtspServer apiRTSPServer
rtspsServer apiRTSPServer rtspsServer apiRTSPServer
rtmpServer apiRTMPServer rtmpServer apiRTMPServer
rtmpsServer apiRTMPServer rtmpsServer apiRTMPServer
hlsManager apiHLSManager hlsManager apiHLSServer
webRTCManager apiWebRTCManager webRTCServer apiWebRTCServer
srtServer apiSRTServer srtServer apiSRTServer
parent apiParent parent apiParent
httpServer *httpserv.WrappedServer httpServer *httpserv.WrappedServer
mutex sync.Mutex mutex sync.Mutex
@ -163,22 +163,22 @@ func newAPI(
rtspsServer apiRTSPServer, rtspsServer apiRTSPServer,
rtmpServer apiRTMPServer, rtmpServer apiRTMPServer,
rtmpsServer apiRTMPServer, rtmpsServer apiRTMPServer,
hlsManager apiHLSManager, hlsManager apiHLSServer,
webRTCManager apiWebRTCManager, webRTCServer apiWebRTCServer,
srtServer apiSRTServer, srtServer apiSRTServer,
parent apiParent, parent apiParent,
) (*api, error) { ) (*api, error) {
a := &api{ a := &api{
conf: conf, conf: conf,
pathManager: pathManager, pathManager: pathManager,
rtspServer: rtspServer, rtspServer: rtspServer,
rtspsServer: rtspsServer, rtspsServer: rtspsServer,
rtmpServer: rtmpServer, rtmpServer: rtmpServer,
rtmpsServer: rtmpsServer, rtmpsServer: rtmpsServer,
hlsManager: hlsManager, hlsManager: hlsManager,
webRTCManager: webRTCManager, webRTCServer: webRTCServer,
srtServer: srtServer, srtServer: srtServer,
parent: parent, parent: parent,
} }
router := gin.New() router := gin.New()
@ -235,7 +235,7 @@ func newAPI(
group.POST("/v3/rtmpsconns/kick/:id", a.onRTMPSConnsKick) 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/list", a.onWebRTCSessionsList)
group.GET("/v3/webrtcsessions/get/:id", a.onWebRTCSessionsGet) group.GET("/v3/webrtcsessions/get/:id", a.onWebRTCSessionsGet)
group.POST("/v3/webrtcsessions/kick/:id", a.onWebRTCSessionsKick) group.POST("/v3/webrtcsessions/kick/:id", a.onWebRTCSessionsKick)
@ -273,6 +273,7 @@ func (a *api) close() {
a.httpServer.Close() a.httpServer.Close()
} }
// Log implements logger.Writer.
func (a *api) Log(level logger.Level, format string, args ...interface{}) { func (a *api) Log(level logger.Level, format string, args ...interface{}) {
a.parent.Log(level, "[API] "+format, args...) 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) { func (a *api) onRTSPConnsList(ctx *gin.Context) {
data, err := a.rtspServer.apiConnsList() data, err := a.rtspServer.APIConnsList()
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -605,7 +606,7 @@ func (a *api) onRTSPConnsGet(ctx *gin.Context) {
return return
} }
data, err := a.rtspServer.apiConnsGet(uuid) data, err := a.rtspServer.APIConnsGet(uuid)
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -615,7 +616,7 @@ func (a *api) onRTSPConnsGet(ctx *gin.Context) {
} }
func (a *api) onRTSPSessionsList(ctx *gin.Context) { func (a *api) onRTSPSessionsList(ctx *gin.Context) {
data, err := a.rtspServer.apiSessionsList() data, err := a.rtspServer.APISessionsList()
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -639,7 +640,7 @@ func (a *api) onRTSPSessionsGet(ctx *gin.Context) {
return return
} }
data, err := a.rtspServer.apiSessionsGet(uuid) data, err := a.rtspServer.APISessionsGet(uuid)
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -655,7 +656,7 @@ func (a *api) onRTSPSessionsKick(ctx *gin.Context) {
return return
} }
err = a.rtspServer.apiSessionsKick(uuid) err = a.rtspServer.APISessionsKick(uuid)
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -665,7 +666,7 @@ func (a *api) onRTSPSessionsKick(ctx *gin.Context) {
} }
func (a *api) onRTSPSConnsList(ctx *gin.Context) { func (a *api) onRTSPSConnsList(ctx *gin.Context) {
data, err := a.rtspsServer.apiConnsList() data, err := a.rtspsServer.APIConnsList()
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -689,7 +690,7 @@ func (a *api) onRTSPSConnsGet(ctx *gin.Context) {
return return
} }
data, err := a.rtspsServer.apiConnsGet(uuid) data, err := a.rtspsServer.APIConnsGet(uuid)
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -699,7 +700,7 @@ func (a *api) onRTSPSConnsGet(ctx *gin.Context) {
} }
func (a *api) onRTSPSSessionsList(ctx *gin.Context) { func (a *api) onRTSPSSessionsList(ctx *gin.Context) {
data, err := a.rtspsServer.apiSessionsList() data, err := a.rtspsServer.APISessionsList()
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -723,7 +724,7 @@ func (a *api) onRTSPSSessionsGet(ctx *gin.Context) {
return return
} }
data, err := a.rtspsServer.apiSessionsGet(uuid) data, err := a.rtspsServer.APISessionsGet(uuid)
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -739,7 +740,7 @@ func (a *api) onRTSPSSessionsKick(ctx *gin.Context) {
return return
} }
err = a.rtspsServer.apiSessionsKick(uuid) err = a.rtspsServer.APISessionsKick(uuid)
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -749,7 +750,7 @@ func (a *api) onRTSPSSessionsKick(ctx *gin.Context) {
} }
func (a *api) onRTMPConnsList(ctx *gin.Context) { func (a *api) onRTMPConnsList(ctx *gin.Context) {
data, err := a.rtmpServer.apiConnsList() data, err := a.rtmpServer.APIConnsList()
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -773,7 +774,7 @@ func (a *api) onRTMPConnsGet(ctx *gin.Context) {
return return
} }
data, err := a.rtmpServer.apiConnsGet(uuid) data, err := a.rtmpServer.APIConnsGet(uuid)
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -789,7 +790,7 @@ func (a *api) onRTMPConnsKick(ctx *gin.Context) {
return return
} }
err = a.rtmpServer.apiConnsKick(uuid) err = a.rtmpServer.APIConnsKick(uuid)
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -799,7 +800,7 @@ func (a *api) onRTMPConnsKick(ctx *gin.Context) {
} }
func (a *api) onRTMPSConnsList(ctx *gin.Context) { func (a *api) onRTMPSConnsList(ctx *gin.Context) {
data, err := a.rtmpsServer.apiConnsList() data, err := a.rtmpsServer.APIConnsList()
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -823,7 +824,7 @@ func (a *api) onRTMPSConnsGet(ctx *gin.Context) {
return return
} }
data, err := a.rtmpsServer.apiConnsGet(uuid) data, err := a.rtmpsServer.APIConnsGet(uuid)
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -839,7 +840,7 @@ func (a *api) onRTMPSConnsKick(ctx *gin.Context) {
return return
} }
err = a.rtmpsServer.apiConnsKick(uuid) err = a.rtmpsServer.APIConnsKick(uuid)
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -849,7 +850,7 @@ func (a *api) onRTMPSConnsKick(ctx *gin.Context) {
} }
func (a *api) onHLSMuxersList(ctx *gin.Context) { func (a *api) onHLSMuxersList(ctx *gin.Context) {
data, err := a.hlsManager.apiMuxersList() data, err := a.hlsManager.APIMuxersList()
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -873,7 +874,7 @@ func (a *api) onHLSMuxersGet(ctx *gin.Context) {
return return
} }
data, err := a.hlsManager.apiMuxersGet(name) data, err := a.hlsManager.APIMuxersGet(name)
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -883,7 +884,7 @@ func (a *api) onHLSMuxersGet(ctx *gin.Context) {
} }
func (a *api) onWebRTCSessionsList(ctx *gin.Context) { func (a *api) onWebRTCSessionsList(ctx *gin.Context) {
data, err := a.webRTCManager.apiSessionsList() data, err := a.webRTCServer.APISessionsList()
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -907,7 +908,7 @@ func (a *api) onWebRTCSessionsGet(ctx *gin.Context) {
return return
} }
data, err := a.webRTCManager.apiSessionsGet(uuid) data, err := a.webRTCServer.APISessionsGet(uuid)
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -923,7 +924,7 @@ func (a *api) onWebRTCSessionsKick(ctx *gin.Context) {
return return
} }
err = a.webRTCManager.apiSessionsKick(uuid) err = a.webRTCServer.APISessionsKick(uuid)
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -933,7 +934,7 @@ func (a *api) onWebRTCSessionsKick(ctx *gin.Context) {
} }
func (a *api) onSRTConnsList(ctx *gin.Context) { func (a *api) onSRTConnsList(ctx *gin.Context) {
data, err := a.srtServer.apiConnsList() data, err := a.srtServer.APIConnsList()
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -957,7 +958,7 @@ func (a *api) onSRTConnsGet(ctx *gin.Context) {
return return
} }
data, err := a.srtServer.apiConnsGet(uuid) data, err := a.srtServer.APIConnsGet(uuid)
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return
@ -973,7 +974,7 @@ func (a *api) onSRTConnsKick(ctx *gin.Context) {
return return
} }
err = a.srtServer.apiConnsKick(uuid) err = a.srtServer.APIConnsKick(uuid)
if err != nil { if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err) a.writeError(ctx, http.StatusInternalServerError, err)
return return

72
internal/core/authentication.go → internal/core/auth.go

@ -15,6 +15,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
) )
func sha256Base64(in string) string { func sha256Base64(in string) string {
@ -31,28 +32,9 @@ func checkCredential(right string, guess string) bool {
return right == guess 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( func doExternalAuthentication(
ur string, ur string,
accessRequest pathAccessRequest, accessRequest defs.PathAccessRequest,
) error { ) error {
enc, _ := json.Marshal(struct { enc, _ := json.Marshal(struct {
IP string `json:"ip"` IP string `json:"ip"`
@ -64,19 +46,19 @@ func doExternalAuthentication(
Action string `json:"action"` Action string `json:"action"`
Query string `json:"query"` Query string `json:"query"`
}{ }{
IP: accessRequest.ip.String(), IP: accessRequest.IP.String(),
User: accessRequest.user, User: accessRequest.User,
Password: accessRequest.pass, Password: accessRequest.Pass,
Path: accessRequest.name, Path: accessRequest.Name,
Protocol: string(accessRequest.proto), Protocol: string(accessRequest.Proto),
ID: accessRequest.id, ID: accessRequest.ID,
Action: func() string { Action: func() string {
if accessRequest.publish { if accessRequest.Publish {
return "publish" return "publish"
} }
return "read" return "read"
}(), }(),
Query: accessRequest.query, Query: accessRequest.Query,
}) })
res, err := http.Post(ur, "application/json", bytes.NewReader(enc)) res, err := http.Post(ur, "application/json", bytes.NewReader(enc))
if err != nil { if err != nil {
@ -98,14 +80,14 @@ func doAuthentication(
externalAuthenticationURL string, externalAuthenticationURL string,
rtspAuthMethods conf.AuthMethods, rtspAuthMethods conf.AuthMethods,
pathConf *conf.Path, pathConf *conf.Path,
accessRequest pathAccessRequest, accessRequest defs.PathAccessRequest,
) error { ) error {
var rtspAuth headers.Authorization var rtspAuth headers.Authorization
if accessRequest.rtspRequest != nil { if accessRequest.RTSPRequest != nil {
err := rtspAuth.Unmarshal(accessRequest.rtspRequest.Header["Authorization"]) err := rtspAuth.Unmarshal(accessRequest.RTSPRequest.Header["Authorization"])
if err == nil && rtspAuth.Method == headers.AuthBasic { if err == nil && rtspAuth.Method == headers.AuthBasic {
accessRequest.user = rtspAuth.BasicUser accessRequest.User = rtspAuth.BasicUser
accessRequest.pass = rtspAuth.BasicPass accessRequest.Pass = rtspAuth.BasicPass
} }
} }
@ -115,7 +97,7 @@ func doAuthentication(
accessRequest, accessRequest,
) )
if err != nil { 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 pathUser string
var pathPass string var pathPass string
if accessRequest.publish { if accessRequest.Publish {
pathIPs = pathConf.PublishIPs pathIPs = pathConf.PublishIPs
pathUser = string(pathConf.PublishUser) pathUser = string(pathConf.PublishUser)
pathPass = string(pathConf.PublishPass) pathPass = string(pathConf.PublishPass)
@ -134,27 +116,27 @@ func doAuthentication(
} }
if pathIPs != nil { if pathIPs != nil {
if !ipEqualOrInRange(accessRequest.ip, pathIPs) { if !ipEqualOrInRange(accessRequest.IP, pathIPs) {
return &errAuthentication{message: fmt.Sprintf("IP %s not allowed", accessRequest.ip)} return &defs.ErrAuthentication{Message: fmt.Sprintf("IP %s not allowed", accessRequest.IP)}
} }
} }
if pathUser != "" { if pathUser != "" {
if accessRequest.rtspRequest != nil && rtspAuth.Method == headers.AuthDigest { if accessRequest.RTSPRequest != nil && rtspAuth.Method == headers.AuthDigest {
err := auth.Validate( err := auth.Validate(
accessRequest.rtspRequest, accessRequest.RTSPRequest,
pathUser, pathUser,
pathPass, pathPass,
accessRequest.rtspBaseURL, accessRequest.RTSPBaseURL,
rtspAuthMethods, rtspAuthMethods,
"IPCAM", "IPCAM",
accessRequest.rtspNonce) accessRequest.RTSPNonce)
if err != nil { if err != nil {
return &errAuthentication{message: err.Error()} return &defs.ErrAuthentication{Message: err.Error()}
} }
} else if !checkCredential(pathUser, accessRequest.user) || } else if !checkCredential(pathUser, accessRequest.User) ||
!checkCredential(pathPass, accessRequest.pass) { !checkCredential(pathPass, accessRequest.Pass) {
return &errAuthentication{message: "invalid credentials"} return &defs.ErrAuthentication{Message: "invalid credentials"}
} }
} }

315
internal/core/core.go

@ -22,6 +22,11 @@ import (
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/record" "github.com/bluenviron/mediamtx/internal/record"
"github.com/bluenviron/mediamtx/internal/rlimit" "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" var version = "v0.0.0"
@ -83,13 +88,13 @@ type Core struct {
pprof *pprof pprof *pprof
recordCleaner *record.Cleaner recordCleaner *record.Cleaner
pathManager *pathManager pathManager *pathManager
rtspServer *rtspServer rtspServer *rtsp.Server
rtspsServer *rtspServer rtspsServer *rtsp.Server
rtmpServer *rtmpServer rtmpServer *rtmp.Server
rtmpsServer *rtmpServer rtmpsServer *rtmp.Server
hlsManager *hlsManager hlsServer *hls.Server
webRTCManager *webRTCManager webRTCServer *webrtc.Server
srtServer *srtServer srtServer *srt.Server
api *api api *api
confWatcher *confwatcher.ConfWatcher confWatcher *confwatcher.ConfWatcher
@ -168,7 +173,7 @@ func (p *Core) Wait() {
<-p.done <-p.done
} }
// Log is the main logging function. // Log implements logger.Writer.
func (p *Core) Log(level logger.Level, format string, args ...interface{}) { func (p *Core) Log(level logger.Level, format string, args ...interface{}) {
p.logger.Log(level, format, args...) 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)] _, useUDP := p.conf.Protocols[conf.Protocol(gortsplib.TransportUDP)]
_, useMulticast := p.conf.Protocols[conf.Protocol(gortsplib.TransportUDPMulticast)] _, useMulticast := p.conf.Protocols[conf.Protocol(gortsplib.TransportUDPMulticast)]
p.rtspServer, err = newRTSPServer( p.rtspServer = &rtsp.Server{
p.conf.RTSPAddress, Address: p.conf.RTSPAddress,
p.conf.AuthMethods, AuthMethods: p.conf.AuthMethods,
p.conf.ReadTimeout, ReadTimeout: p.conf.ReadTimeout,
p.conf.WriteTimeout, WriteTimeout: p.conf.WriteTimeout,
p.conf.WriteQueueSize, WriteQueueSize: p.conf.WriteQueueSize,
useUDP, UseUDP: useUDP,
useMulticast, UseMulticast: useMulticast,
p.conf.RTPAddress, RTPAddress: p.conf.RTPAddress,
p.conf.RTCPAddress, RTCPAddress: p.conf.RTCPAddress,
p.conf.MulticastIPRange, MulticastIPRange: p.conf.MulticastIPRange,
p.conf.MulticastRTPPort, MulticastRTPPort: p.conf.MulticastRTPPort,
p.conf.MulticastRTCPPort, MulticastRTCPPort: p.conf.MulticastRTCPPort,
false, IsTLS: false,
"", ServerCert: "",
"", ServerKey: "",
p.conf.RTSPAddress, RTSPAddress: p.conf.RTSPAddress,
p.conf.Protocols, Protocols: p.conf.Protocols,
p.conf.RunOnConnect, RunOnConnect: p.conf.RunOnConnect,
p.conf.RunOnConnectRestart, RunOnConnectRestart: p.conf.RunOnConnectRestart,
p.conf.RunOnDisconnect, RunOnDisconnect: p.conf.RunOnDisconnect,
p.externalCmdPool, ExternalCmdPool: p.externalCmdPool,
p.pathManager, PathManager: p.pathManager,
p, Parent: p,
) }
err := p.rtspServer.Initialize()
if err != nil { if err != nil {
return err return err
} }
@ -367,31 +373,32 @@ func (p *Core) createResources(initial bool) error {
(p.conf.Encryption == conf.EncryptionStrict || (p.conf.Encryption == conf.EncryptionStrict ||
p.conf.Encryption == conf.EncryptionOptional) && p.conf.Encryption == conf.EncryptionOptional) &&
p.rtspsServer == nil { p.rtspsServer == nil {
p.rtspsServer, err = newRTSPServer( p.rtspsServer = &rtsp.Server{
p.conf.RTSPSAddress, Address: p.conf.RTSPSAddress,
p.conf.AuthMethods, AuthMethods: p.conf.AuthMethods,
p.conf.ReadTimeout, ReadTimeout: p.conf.ReadTimeout,
p.conf.WriteTimeout, WriteTimeout: p.conf.WriteTimeout,
p.conf.WriteQueueSize, WriteQueueSize: p.conf.WriteQueueSize,
false, UseUDP: false,
false, UseMulticast: false,
"", RTPAddress: "",
"", RTCPAddress: "",
"", MulticastIPRange: "",
0, MulticastRTPPort: 0,
0, MulticastRTCPPort: 0,
true, IsTLS: true,
p.conf.ServerCert, ServerCert: p.conf.ServerCert,
p.conf.ServerKey, ServerKey: p.conf.ServerKey,
p.conf.RTSPAddress, RTSPAddress: p.conf.RTSPAddress,
p.conf.Protocols, Protocols: p.conf.Protocols,
p.conf.RunOnConnect, RunOnConnect: p.conf.RunOnConnect,
p.conf.RunOnConnectRestart, RunOnConnectRestart: p.conf.RunOnConnectRestart,
p.conf.RunOnDisconnect, RunOnDisconnect: p.conf.RunOnDisconnect,
p.externalCmdPool, ExternalCmdPool: p.externalCmdPool,
p.pathManager, PathManager: p.pathManager,
p, Parent: p,
) }
err := p.rtspsServer.Initialize()
if err != nil { if err != nil {
return err return err
} }
@ -405,22 +412,23 @@ func (p *Core) createResources(initial bool) error {
(p.conf.RTMPEncryption == conf.EncryptionNo || (p.conf.RTMPEncryption == conf.EncryptionNo ||
p.conf.RTMPEncryption == conf.EncryptionOptional) && p.conf.RTMPEncryption == conf.EncryptionOptional) &&
p.rtmpServer == nil { p.rtmpServer == nil {
p.rtmpServer, err = newRTMPServer( p.rtmpServer = &rtmp.Server{
p.conf.RTMPAddress, Address: p.conf.RTMPAddress,
p.conf.ReadTimeout, ReadTimeout: p.conf.ReadTimeout,
p.conf.WriteTimeout, WriteTimeout: p.conf.WriteTimeout,
p.conf.WriteQueueSize, WriteQueueSize: p.conf.WriteQueueSize,
false, IsTLS: false,
"", ServerCert: "",
"", ServerKey: "",
p.conf.RTSPAddress, RTSPAddress: p.conf.RTSPAddress,
p.conf.RunOnConnect, RunOnConnect: p.conf.RunOnConnect,
p.conf.RunOnConnectRestart, RunOnConnectRestart: p.conf.RunOnConnectRestart,
p.conf.RunOnDisconnect, RunOnDisconnect: p.conf.RunOnDisconnect,
p.externalCmdPool, ExternalCmdPool: p.externalCmdPool,
p.pathManager, PathManager: p.pathManager,
p, Parent: p,
) }
err := p.rtmpServer.Initialize()
if err != nil { if err != nil {
return err return err
} }
@ -434,22 +442,23 @@ func (p *Core) createResources(initial bool) error {
(p.conf.RTMPEncryption == conf.EncryptionStrict || (p.conf.RTMPEncryption == conf.EncryptionStrict ||
p.conf.RTMPEncryption == conf.EncryptionOptional) && p.conf.RTMPEncryption == conf.EncryptionOptional) &&
p.rtmpsServer == nil { p.rtmpsServer == nil {
p.rtmpsServer, err = newRTMPServer( p.rtmpsServer = &rtmp.Server{
p.conf.RTMPSAddress, Address: p.conf.RTMPSAddress,
p.conf.ReadTimeout, ReadTimeout: p.conf.ReadTimeout,
p.conf.WriteTimeout, WriteTimeout: p.conf.WriteTimeout,
p.conf.WriteQueueSize, WriteQueueSize: p.conf.WriteQueueSize,
true, IsTLS: true,
p.conf.RTMPServerCert, ServerCert: p.conf.RTMPServerCert,
p.conf.RTMPServerKey, ServerKey: p.conf.RTMPServerKey,
p.conf.RTSPAddress, RTSPAddress: p.conf.RTSPAddress,
p.conf.RunOnConnect, RunOnConnect: p.conf.RunOnConnect,
p.conf.RunOnConnectRestart, RunOnConnectRestart: p.conf.RunOnConnectRestart,
p.conf.RunOnDisconnect, RunOnDisconnect: p.conf.RunOnDisconnect,
p.externalCmdPool, ExternalCmdPool: p.externalCmdPool,
p.pathManager, PathManager: p.pathManager,
p, Parent: p,
) }
err := p.rtmpsServer.Initialize()
if err != nil { if err != nil {
return err return err
} }
@ -460,41 +469,42 @@ func (p *Core) createResources(initial bool) error {
} }
if p.conf.HLS && if p.conf.HLS &&
p.hlsManager == nil { p.hlsServer == nil {
p.hlsManager, err = newHLSManager( p.hlsServer = &hls.Server{
p.conf.HLSAddress, Address: p.conf.HLSAddress,
p.conf.HLSEncryption, Encryption: p.conf.HLSEncryption,
p.conf.HLSServerKey, ServerKey: p.conf.HLSServerKey,
p.conf.HLSServerCert, ServerCert: p.conf.HLSServerCert,
p.conf.ExternalAuthenticationURL, ExternalAuthenticationURL: p.conf.ExternalAuthenticationURL,
p.conf.HLSAlwaysRemux, AlwaysRemux: p.conf.HLSAlwaysRemux,
p.conf.HLSVariant, Variant: p.conf.HLSVariant,
p.conf.HLSSegmentCount, SegmentCount: p.conf.HLSSegmentCount,
p.conf.HLSSegmentDuration, SegmentDuration: p.conf.HLSSegmentDuration,
p.conf.HLSPartDuration, PartDuration: p.conf.HLSPartDuration,
p.conf.HLSSegmentMaxSize, SegmentMaxSize: p.conf.HLSSegmentMaxSize,
p.conf.HLSAllowOrigin, AllowOrigin: p.conf.HLSAllowOrigin,
p.conf.HLSTrustedProxies, TrustedProxies: p.conf.HLSTrustedProxies,
p.conf.HLSDirectory, Directory: p.conf.HLSDirectory,
p.conf.ReadTimeout, ReadTimeout: p.conf.ReadTimeout,
p.conf.WriteQueueSize, WriteQueueSize: p.conf.WriteQueueSize,
p.pathManager, PathManager: p.pathManager,
p, Parent: p,
) }
err := p.hlsServer.Initialize()
if err != nil { if err != nil {
return err return err
} }
p.pathManager.setHLSManager(p.hlsManager) p.pathManager.setHLSServer(p.hlsServer)
if p.metrics != nil { if p.metrics != nil {
p.metrics.setHLSManager(p.hlsManager) p.metrics.setHLSServer(p.hlsServer)
} }
} }
if p.conf.WebRTC && if p.conf.WebRTC &&
p.webRTCManager == nil { p.webRTCServer == nil {
p.webRTCManager = &webRTCManager{ p.webRTCServer = &webrtc.Server{
Address: p.conf.WebRTCAddress, Address: p.conf.WebRTCAddress,
Encryption: p.conf.WebRTCEncryption, Encryption: p.conf.WebRTCEncryption,
ServerKey: p.conf.WebRTCServerKey, ServerKey: p.conf.WebRTCServerKey,
@ -513,33 +523,34 @@ func (p *Core) createResources(initial bool) error {
PathManager: p.pathManager, PathManager: p.pathManager,
Parent: p, Parent: p,
} }
err = p.webRTCManager.initialize() err = p.webRTCServer.Initialize()
if err != nil { if err != nil {
p.webRTCManager = nil p.webRTCServer = nil
return err return err
} }
if p.metrics != nil { if p.metrics != nil {
p.metrics.setWebRTCManager(p.webRTCManager) p.metrics.setWebRTCServer(p.webRTCServer)
} }
} }
if p.conf.SRT && if p.conf.SRT &&
p.srtServer == nil { p.srtServer == nil {
p.srtServer, err = newSRTServer( p.srtServer = &srt.Server{
p.conf.SRTAddress, Address: p.conf.SRTAddress,
p.conf.RTSPAddress, RTSPAddress: p.conf.RTSPAddress,
p.conf.ReadTimeout, ReadTimeout: p.conf.ReadTimeout,
p.conf.WriteTimeout, WriteTimeout: p.conf.WriteTimeout,
p.conf.WriteQueueSize, WriteQueueSize: p.conf.WriteQueueSize,
p.conf.UDPMaxPayloadSize, UDPMaxPayloadSize: p.conf.UDPMaxPayloadSize,
p.conf.RunOnConnect, RunOnConnect: p.conf.RunOnConnect,
p.conf.RunOnConnectRestart, RunOnConnectRestart: p.conf.RunOnConnectRestart,
p.conf.RunOnDisconnect, RunOnDisconnect: p.conf.RunOnDisconnect,
p.externalCmdPool, ExternalCmdPool: p.externalCmdPool,
p.pathManager, PathManager: p.pathManager,
p, Parent: p,
) }
err = p.srtServer.Initialize()
if err != nil { if err != nil {
return err return err
} }
@ -560,8 +571,8 @@ func (p *Core) createResources(initial bool) error {
p.rtspsServer, p.rtspsServer,
p.rtmpServer, p.rtmpServer,
p.rtmpsServer, p.rtmpsServer,
p.hlsManager, p.hlsServer,
p.webRTCManager, p.webRTCServer,
p.srtServer, p.srtServer,
p, p,
) )
@ -690,7 +701,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closePathManager || closePathManager ||
closeLogger closeLogger
closeHLSManager := newConf == nil || closeHLSServer := newConf == nil ||
newConf.HLS != p.conf.HLS || newConf.HLS != p.conf.HLS ||
newConf.HLSAddress != p.conf.HLSAddress || newConf.HLSAddress != p.conf.HLSAddress ||
newConf.HLSEncryption != p.conf.HLSEncryption || newConf.HLSEncryption != p.conf.HLSEncryption ||
@ -712,7 +723,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closeMetrics || closeMetrics ||
closeLogger closeLogger
closeWebRTCManager := newConf == nil || closeWebRTCServer := newConf == nil ||
newConf.WebRTC != p.conf.WebRTC || newConf.WebRTC != p.conf.WebRTC ||
newConf.WebRTCAddress != p.conf.WebRTCAddress || newConf.WebRTCAddress != p.conf.WebRTCAddress ||
newConf.WebRTCEncryption != p.conf.WebRTCEncryption || newConf.WebRTCEncryption != p.conf.WebRTCEncryption ||
@ -754,8 +765,8 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closeRTSPServer || closeRTSPServer ||
closeRTSPSServer || closeRTSPSServer ||
closeRTMPServer || closeRTMPServer ||
closeHLSManager || closeHLSServer ||
closeWebRTCManager || closeWebRTCServer ||
closeSRTServer || closeSRTServer ||
closeLogger closeLogger
@ -778,28 +789,28 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
p.metrics.setSRTServer(nil) p.metrics.setSRTServer(nil)
} }
p.srtServer.close() p.srtServer.Close()
p.srtServer = nil p.srtServer = nil
} }
if closeWebRTCManager && p.webRTCManager != nil { if closeWebRTCServer && p.webRTCServer != nil {
if p.metrics != nil { if p.metrics != nil {
p.metrics.setWebRTCManager(nil) p.metrics.setWebRTCServer(nil)
} }
p.webRTCManager.close() p.webRTCServer.Close()
p.webRTCManager = nil p.webRTCServer = nil
} }
if closeHLSManager && p.hlsManager != nil { if closeHLSServer && p.hlsServer != nil {
if p.metrics != 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.hlsServer.Close()
p.hlsManager = nil p.hlsServer = nil
} }
if closeRTMPSServer && p.rtmpsServer != nil { if closeRTMPSServer && p.rtmpsServer != nil {
@ -807,7 +818,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
p.metrics.setRTMPSServer(nil) p.metrics.setRTMPSServer(nil)
} }
p.rtmpsServer.close() p.rtmpsServer.Close()
p.rtmpsServer = nil p.rtmpsServer = nil
} }
@ -816,7 +827,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
p.metrics.setRTMPServer(nil) p.metrics.setRTMPServer(nil)
} }
p.rtmpServer.close() p.rtmpServer.Close()
p.rtmpServer = nil p.rtmpServer = nil
} }
@ -825,7 +836,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
p.metrics.setRTSPSServer(nil) p.metrics.setRTSPSServer(nil)
} }
p.rtspsServer.close() p.rtspsServer.Close()
p.rtspsServer = nil p.rtspsServer = nil
} }
@ -834,7 +845,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
p.metrics.setRTSPServer(nil) p.metrics.setRTSPServer(nil)
} }
p.rtspServer.close() p.rtspServer.Close()
p.rtspServer = nil p.rtspServer = nil
} }

309
internal/core/hls_manager.go

@ -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():
}
}

2
internal/core/hls_manager_test.go → internal/core/hls_server_test.go

@ -197,7 +197,7 @@ func TestHLSRead(t *testing.T) {
"#EXT-X-GAP\n"+ "#EXT-X-GAP\n"+
"#EXTINF:1\\.00000,\n"+ "#EXTINF:1\\.00000,\n"+
"gap.mp4\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"+ "#EXT-X-PART:DURATION=1\\.00000,URI=\".*?_part0.mp4\",INDEPENDENT=YES\n"+
"#EXTINF:1\\.00000,\n"+ "#EXTINF:1\\.00000,\n"+
".*?_seg7.mp4\n"+ ".*?_seg7.mp4\n"+

51
internal/core/metrics.go

@ -28,16 +28,16 @@ type metrics struct {
ReadTimeout conf.StringDuration ReadTimeout conf.StringDuration
Parent metricsParent Parent metricsParent
httpServer *httpserv.WrappedServer httpServer *httpserv.WrappedServer
mutex sync.Mutex mutex sync.Mutex
pathManager apiPathManager pathManager apiPathManager
rtspServer apiRTSPServer rtspServer apiRTSPServer
rtspsServer apiRTSPServer rtspsServer apiRTSPServer
rtmpServer apiRTMPServer rtmpServer apiRTMPServer
rtmpsServer apiRTMPServer rtmpsServer apiRTMPServer
srtServer apiSRTServer srtServer apiSRTServer
hlsManager apiHLSManager hlsManager apiHLSServer
webRTCManager apiWebRTCManager webRTCServer apiWebRTCServer
} }
func (m *metrics) initialize() error { func (m *metrics) initialize() error {
@ -72,6 +72,7 @@ func (m *metrics) close() {
m.httpServer.Close() m.httpServer.Close()
} }
// Log implements logger.Writer.
func (m *metrics) Log(level logger.Level, format string, args ...interface{}) { func (m *metrics) Log(level logger.Level, format string, args ...interface{}) {
m.Parent.Log(level, "[metrics] "+format, args...) m.Parent.Log(level, "[metrics] "+format, args...)
} }
@ -99,7 +100,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
} }
if !interfaceIsEmpty(m.hlsManager) { if !interfaceIsEmpty(m.hlsManager) {
data, err := m.hlsManager.apiMuxersList() data, err := m.hlsManager.APIMuxersList()
if err == nil && len(data.Items) != 0 { if err == nil && len(data.Items) != 0 {
for _, i := range data.Items { for _, i := range data.Items {
tags := "{name=\"" + i.Path + "\"}" tags := "{name=\"" + i.Path + "\"}"
@ -114,7 +115,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
if !interfaceIsEmpty(m.rtspServer) { //nolint:dupl if !interfaceIsEmpty(m.rtspServer) { //nolint:dupl
func() { func() {
data, err := m.rtspServer.apiConnsList() data, err := m.rtspServer.APIConnsList()
if err == nil && len(data.Items) != 0 { if err == nil && len(data.Items) != 0 {
for _, i := range data.Items { for _, i := range data.Items {
tags := "{id=\"" + i.ID.String() + "\"}" tags := "{id=\"" + i.ID.String() + "\"}"
@ -130,7 +131,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
}() }()
func() { func() {
data, err := m.rtspServer.apiSessionsList() data, err := m.rtspServer.APISessionsList()
if err == nil && len(data.Items) != 0 { if err == nil && len(data.Items) != 0 {
for _, i := range data.Items { for _, i := range data.Items {
tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}" 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 if !interfaceIsEmpty(m.rtspsServer) { //nolint:dupl
func() { func() {
data, err := m.rtspsServer.apiConnsList() data, err := m.rtspsServer.APIConnsList()
if err == nil && len(data.Items) != 0 { if err == nil && len(data.Items) != 0 {
for _, i := range data.Items { for _, i := range data.Items {
tags := "{id=\"" + i.ID.String() + "\"}" tags := "{id=\"" + i.ID.String() + "\"}"
@ -164,7 +165,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
}() }()
func() { func() {
data, err := m.rtspsServer.apiSessionsList() data, err := m.rtspsServer.APISessionsList()
if err == nil && len(data.Items) != 0 { if err == nil && len(data.Items) != 0 {
for _, i := range data.Items { for _, i := range data.Items {
tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}" tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}"
@ -181,7 +182,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
} }
if !interfaceIsEmpty(m.rtmpServer) { if !interfaceIsEmpty(m.rtmpServer) {
data, err := m.rtmpServer.apiConnsList() data, err := m.rtmpServer.APIConnsList()
if err == nil && len(data.Items) != 0 { if err == nil && len(data.Items) != 0 {
for _, i := range data.Items { for _, i := range data.Items {
tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}" tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}"
@ -197,7 +198,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
} }
if !interfaceIsEmpty(m.rtmpsServer) { if !interfaceIsEmpty(m.rtmpsServer) {
data, err := m.rtmpsServer.apiConnsList() data, err := m.rtmpsServer.APIConnsList()
if err == nil && len(data.Items) != 0 { if err == nil && len(data.Items) != 0 {
for _, i := range data.Items { for _, i := range data.Items {
tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}" tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}"
@ -213,7 +214,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
} }
if !interfaceIsEmpty(m.srtServer) { if !interfaceIsEmpty(m.srtServer) {
data, err := m.srtServer.apiConnsList() data, err := m.srtServer.APIConnsList()
if err == nil && len(data.Items) != 0 { if err == nil && len(data.Items) != 0 {
for _, i := range data.Items { for _, i := range data.Items {
tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}" tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}"
@ -228,8 +229,8 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
} }
} }
if !interfaceIsEmpty(m.webRTCManager) { if !interfaceIsEmpty(m.webRTCServer) {
data, err := m.webRTCManager.apiSessionsList() data, err := m.webRTCServer.APISessionsList()
if err == nil && len(data.Items) != 0 { if err == nil && len(data.Items) != 0 {
for _, i := range data.Items { for _, i := range data.Items {
tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}" tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}"
@ -255,8 +256,8 @@ func (m *metrics) setPathManager(s apiPathManager) {
m.pathManager = s m.pathManager = s
} }
// setHLSManager is called by core. // setHLSServer is called by core.
func (m *metrics) setHLSManager(s apiHLSManager) { func (m *metrics) setHLSServer(s apiHLSServer) {
m.mutex.Lock() m.mutex.Lock()
defer m.mutex.Unlock() defer m.mutex.Unlock()
m.hlsManager = s m.hlsManager = s
@ -297,9 +298,9 @@ func (m *metrics) setSRTServer(s apiSRTServer) {
m.srtServer = s m.srtServer = s
} }
// setWebRTCManager is called by core. // setWebRTCServer is called by core.
func (m *metrics) setWebRTCManager(s apiWebRTCManager) { func (m *metrics) setWebRTCServer(s apiWebRTCServer) {
m.mutex.Lock() m.mutex.Lock()
defer m.mutex.Unlock() defer m.mutex.Unlock()
m.webRTCManager = s m.webRTCServer = s
} }

16
internal/core/metrics_test.go

@ -229,26 +229,26 @@ webrtc_sessions_bytes_sent 0
require.Regexp(t, require.Regexp(t,
`^paths\{name=".*?",state="ready"\} 1`+"\n"+ `^paths\{name=".*?",state="ready"\} 1`+"\n"+
`paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\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\{name=".*?",state="ready"\} 1`+"\n"+
`paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\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\{name=".*?",state="ready"\} 1`+"\n"+
`paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\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\{name=".*?",state="ready"\} 1`+"\n"+
`paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\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\{name=".*?",state="ready"\} 1`+"\n"+
`paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\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\{name=".*?",state="ready"\} 1`+"\n"+
`paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\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\{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\{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\{name=".*?"\} 1`+"\n"+
`hls_muxers_bytes_sent\{name=".*?"\} 0`+"\n"+ `hls_muxers_bytes_sent\{name=".*?"\} 0`+"\n"+
`hls_muxers\{name=".*?"\} 1`+"\n"+ `hls_muxers\{name=".*?"\} 1`+"\n"+

363
internal/core/path.go

@ -11,7 +11,6 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/base" "github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/google/uuid"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/defs"
@ -28,15 +27,6 @@ func newEmptyTimer() *time.Timer {
return t 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 { type pathParent interface {
logger.Writer logger.Writer
pathReady(*path) pathReady(*path)
@ -53,95 +43,6 @@ const (
pathOnDemandStateClosing 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 { type pathAPIPathsListRes struct {
data *defs.APIPathList data *defs.APIPathList
paths map[string]*path paths map[string]*path
@ -179,16 +80,16 @@ type path struct {
ctx context.Context ctx context.Context
ctxCancel func() ctxCancel func()
confMutex sync.RWMutex confMutex sync.RWMutex
source source source defs.Source
publisherQuery string publisherQuery string
stream *stream.Stream stream *stream.Stream
recordAgent *record.Agent recordAgent *record.Agent
readyTime time.Time readyTime time.Time
onUnDemandHook func(string) onUnDemandHook func(string)
onNotReadyHook func() onNotReadyHook func()
readers map[reader]struct{} readers map[defs.Reader]struct{}
describeRequestsOnHold []pathDescribeReq describeRequestsOnHold []defs.PathDescribeReq
readerAddRequestsOnHold []pathAddReaderReq readerAddRequestsOnHold []defs.PathAddReaderReq
onDemandStaticSourceState pathOnDemandState onDemandStaticSourceState pathOnDemandState
onDemandStaticSourceReadyTimer *time.Timer onDemandStaticSourceReadyTimer *time.Timer
onDemandStaticSourceCloseTimer *time.Timer onDemandStaticSourceCloseTimer *time.Timer
@ -200,13 +101,13 @@ type path struct {
chReloadConf chan *conf.Path chReloadConf chan *conf.Path
chStaticSourceSetReady chan defs.PathSourceStaticSetReadyReq chStaticSourceSetReady chan defs.PathSourceStaticSetReadyReq
chStaticSourceSetNotReady chan defs.PathSourceStaticSetNotReadyReq chStaticSourceSetNotReady chan defs.PathSourceStaticSetNotReadyReq
chDescribe chan pathDescribeReq chDescribe chan defs.PathDescribeReq
chAddPublisher chan pathAddPublisherReq chAddPublisher chan defs.PathAddPublisherReq
chRemovePublisher chan pathRemovePublisherReq chRemovePublisher chan defs.PathRemovePublisherReq
chStartPublisher chan pathStartPublisherReq chStartPublisher chan defs.PathStartPublisherReq
chStopPublisher chan pathStopPublisherReq chStopPublisher chan defs.PathStopPublisherReq
chAddReader chan pathAddReaderReq chAddReader chan defs.PathAddReaderReq
chRemoveReader chan pathRemoveReaderReq chRemoveReader chan defs.PathRemoveReaderReq
chAPIPathsGet chan pathAPIPathsGetReq chAPIPathsGet chan pathAPIPathsGetReq
// out // out
@ -245,7 +146,7 @@ func newPath(
parent: parent, parent: parent,
ctx: ctx, ctx: ctx,
ctxCancel: ctxCancel, ctxCancel: ctxCancel,
readers: make(map[reader]struct{}), readers: make(map[defs.Reader]struct{}),
onDemandStaticSourceReadyTimer: newEmptyTimer(), onDemandStaticSourceReadyTimer: newEmptyTimer(),
onDemandStaticSourceCloseTimer: newEmptyTimer(), onDemandStaticSourceCloseTimer: newEmptyTimer(),
onDemandPublisherReadyTimer: newEmptyTimer(), onDemandPublisherReadyTimer: newEmptyTimer(),
@ -253,13 +154,13 @@ func newPath(
chReloadConf: make(chan *conf.Path), chReloadConf: make(chan *conf.Path),
chStaticSourceSetReady: make(chan defs.PathSourceStaticSetReadyReq), chStaticSourceSetReady: make(chan defs.PathSourceStaticSetReadyReq),
chStaticSourceSetNotReady: make(chan defs.PathSourceStaticSetNotReadyReq), chStaticSourceSetNotReady: make(chan defs.PathSourceStaticSetNotReadyReq),
chDescribe: make(chan pathDescribeReq), chDescribe: make(chan defs.PathDescribeReq),
chAddPublisher: make(chan pathAddPublisherReq), chAddPublisher: make(chan defs.PathAddPublisherReq),
chRemovePublisher: make(chan pathRemovePublisherReq), chRemovePublisher: make(chan defs.PathRemovePublisherReq),
chStartPublisher: make(chan pathStartPublisherReq), chStartPublisher: make(chan defs.PathStartPublisherReq),
chStopPublisher: make(chan pathStopPublisherReq), chStopPublisher: make(chan defs.PathStopPublisherReq),
chAddReader: make(chan pathAddReaderReq), chAddReader: make(chan defs.PathAddReaderReq),
chRemoveReader: make(chan pathRemoveReaderReq), chRemoveReader: make(chan defs.PathRemoveReaderReq),
chAPIPathsGet: make(chan pathAPIPathsGetReq), chAPIPathsGet: make(chan pathAPIPathsGetReq),
done: make(chan struct{}), done: make(chan struct{}),
} }
@ -280,11 +181,15 @@ func (pa *path) wait() {
<-pa.done <-pa.done
} }
// Log is the main logging function. // Log implements logger.Writer.
func (pa *path) Log(level logger.Level, format string, args ...interface{}) { func (pa *path) Log(level logger.Level, format string, args ...interface{}) {
pa.parent.Log(level, "[path "+pa.name+"] "+format, args...) pa.parent.Log(level, "[path "+pa.name+"] "+format, args...)
} }
func (pa *path) Name() string {
return pa.name
}
func (pa *path) run() { func (pa *path) run() {
defer close(pa.done) defer close(pa.done)
defer pa.wg.Done() defer pa.wg.Done()
@ -308,7 +213,7 @@ func (pa *path) run() {
Logger: pa, Logger: pa,
ExternalCmdPool: pa.externalCmdPool, ExternalCmdPool: pa.externalCmdPool,
Conf: pa.conf, Conf: pa.conf,
ExternalCmdEnv: pa.externalCmdEnv(), ExternalCmdEnv: pa.ExternalCmdEnv(),
}) })
err := pa.runInner() err := pa.runInner()
@ -326,11 +231,11 @@ func (pa *path) run() {
onUnInitHook() onUnInitHook()
for _, req := range pa.describeRequestsOnHold { 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 { for _, req := range pa.readerAddRequestsOnHold {
req.res <- pathAddReaderRes{err: fmt.Errorf("terminated")} req.Res <- defs.PathAddReaderRes{Err: fmt.Errorf("terminated")}
} }
if pa.stream != nil { if pa.stream != nil {
@ -342,8 +247,8 @@ func (pa *path) run() {
if !pa.conf.SourceOnDemand || pa.onDemandStaticSourceState != pathOnDemandStateInitial { if !pa.conf.SourceOnDemand || pa.onDemandStaticSourceState != pathOnDemandStateInitial {
source.close("path is closing") source.close("path is closing")
} }
} else if source, ok := pa.source.(publisher); ok { } else if source, ok := pa.source.(defs.Publisher); ok {
source.close() source.Close()
} }
} }
@ -442,12 +347,12 @@ func (pa *path) runInner() error {
func (pa *path) doOnDemandStaticSourceReadyTimer() { func (pa *path) doOnDemandStaticSourceReadyTimer() {
for _, req := range pa.describeRequestsOnHold { 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 pa.describeRequestsOnHold = nil
for _, req := range pa.readerAddRequestsOnHold { 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 pa.readerAddRequestsOnHold = nil
@ -461,12 +366,12 @@ func (pa *path) doOnDemandStaticSourceCloseTimer() {
func (pa *path) doOnDemandPublisherReadyTimer() { func (pa *path) doOnDemandPublisherReadyTimer() {
for _, req := range pa.describeRequestsOnHold { 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 pa.describeRequestsOnHold = nil
for _, req := range pa.readerAddRequestsOnHold { 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 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 { if _, ok := pa.source.(*sourceRedirect); ok {
req.res <- pathDescribeRes{ req.Res <- defs.PathDescribeRes{
redirect: pa.conf.SourceRedirect, Redirect: pa.conf.SourceRedirect,
} }
return return
} }
if pa.stream != nil { if pa.stream != nil {
req.res <- pathDescribeRes{ req.Res <- defs.PathDescribeRes{
stream: pa.stream, Stream: pa.stream,
} }
return return
} }
@ -551,7 +456,7 @@ func (pa *path) doDescribe(req pathDescribeReq) {
if pa.conf.HasOnDemandPublisher() { if pa.conf.HasOnDemandPublisher() {
if pa.onDemandPublisherState == pathOnDemandStateInitial { if pa.onDemandPublisherState == pathOnDemandStateInitial {
pa.onDemandPublisherStart(req.accessRequest.query) pa.onDemandPublisherStart(req.AccessRequest.Query)
} }
pa.describeRequestsOnHold = append(pa.describeRequestsOnHold, req) pa.describeRequestsOnHold = append(pa.describeRequestsOnHold, req)
return return
@ -561,69 +466,69 @@ func (pa *path) doDescribe(req pathDescribeReq) {
fallbackURL := func() string { fallbackURL := func() string {
if strings.HasPrefix(pa.conf.Fallback, "/") { if strings.HasPrefix(pa.conf.Fallback, "/") {
ur := base.URL{ ur := base.URL{
Scheme: req.accessRequest.rtspRequest.URL.Scheme, Scheme: req.AccessRequest.RTSPRequest.URL.Scheme,
User: req.accessRequest.rtspRequest.URL.User, User: req.AccessRequest.RTSPRequest.URL.User,
Host: req.accessRequest.rtspRequest.URL.Host, Host: req.AccessRequest.RTSPRequest.URL.Host,
Path: pa.conf.Fallback, Path: pa.conf.Fallback,
} }
return ur.String() return ur.String()
} }
return pa.conf.Fallback return pa.conf.Fallback
}() }()
req.res <- pathDescribeRes{redirect: fallbackURL} req.Res <- defs.PathDescribeRes{Redirect: fallbackURL}
return 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) { func (pa *path) doRemovePublisher(req defs.PathRemovePublisherReq) {
if pa.source == req.author { if pa.source == req.Author {
pa.executeRemovePublisher() 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" { if pa.conf.Source != "publisher" {
req.res <- pathAddPublisherRes{ req.Res <- defs.PathAddPublisherRes{
err: fmt.Errorf("can't publish to path '%s' since 'source' is not 'publisher'", pa.name), Err: fmt.Errorf("can't publish to path '%s' since 'source' is not 'publisher'", pa.name),
} }
return return
} }
if pa.source != nil { if pa.source != nil {
if !pa.conf.OverridePublisher { 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 return
} }
pa.Log(logger.Info, "closing existing publisher") pa.Log(logger.Info, "closing existing publisher")
pa.source.(publisher).close() pa.source.(defs.Publisher).Close()
pa.executeRemovePublisher() pa.executeRemovePublisher()
} }
pa.source = req.author pa.source = req.Author
pa.publisherQuery = req.accessRequest.query pa.publisherQuery = req.AccessRequest.Query
req.res <- pathAddPublisherRes{path: pa} req.Res <- defs.PathAddPublisherRes{Path: pa}
} }
func (pa *path) doStartPublisher(req pathStartPublisherReq) { func (pa *path) doStartPublisher(req defs.PathStartPublisherReq) {
if pa.source != req.author { if pa.source != req.Author {
req.res <- pathStartPublisherRes{err: fmt.Errorf("publisher is not assigned to this path anymore")} req.Res <- defs.PathStartPublisherRes{Err: fmt.Errorf("publisher is not assigned to this path anymore")}
return return
} }
err := pa.setReady(req.desc, req.generateRTPPackets) err := pa.setReady(req.Desc, req.GenerateRTPPackets)
if err != nil { if err != nil {
req.res <- pathStartPublisherRes{err: err} req.Res <- defs.PathStartPublisherRes{Err: err}
return 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, pa.name,
mediaInfo(req.desc.Medias)) defs.MediasInfo(req.Desc.Medias))
if pa.conf.HasOnDemandPublisher() && pa.onDemandPublisherState != pathOnDemandStateInitial { if pa.conf.HasOnDemandPublisher() && pa.onDemandPublisherState != pathOnDemandStateInitial {
pa.onDemandPublisherReadyTimer.Stop() pa.onDemandPublisherReadyTimer.Stop()
@ -633,17 +538,17 @@ func (pa *path) doStartPublisher(req pathStartPublisherReq) {
pa.consumeOnHoldRequests() pa.consumeOnHoldRequests()
req.res <- pathStartPublisherRes{stream: pa.stream} req.Res <- defs.PathStartPublisherRes{Stream: pa.stream}
} }
func (pa *path) doStopPublisher(req pathStopPublisherReq) { func (pa *path) doStopPublisher(req defs.PathStopPublisherReq) {
if req.author == pa.source && pa.stream != nil { if req.Author == pa.source && pa.stream != nil {
pa.setNotReady() 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 { if pa.stream != nil {
pa.addReaderPost(req) pa.addReaderPost(req)
return return
@ -659,20 +564,20 @@ func (pa *path) doAddReader(req pathAddReaderReq) {
if pa.conf.HasOnDemandPublisher() { if pa.conf.HasOnDemandPublisher() {
if pa.onDemandPublisherState == pathOnDemandStateInitial { if pa.onDemandPublisherState == pathOnDemandStateInitial {
pa.onDemandPublisherStart(req.accessRequest.query) pa.onDemandPublisherStart(req.AccessRequest.Query)
} }
pa.readerAddRequestsOnHold = append(pa.readerAddRequestsOnHold, req) pa.readerAddRequestsOnHold = append(pa.readerAddRequestsOnHold, req)
return 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) { func (pa *path) doRemoveReader(req defs.PathRemoveReaderReq) {
if _, ok := pa.readers[req.author]; ok { if _, ok := pa.readers[req.Author]; ok {
pa.executeRemoveReader(req.author) pa.executeRemoveReader(req.Author)
} }
close(req.res) close(req.Res)
if len(pa.readers) == 0 { if len(pa.readers) == 0 {
if pa.conf.HasOnDemandStaticSource() { if pa.conf.HasOnDemandStaticSource() {
@ -711,7 +616,7 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) {
if pa.stream == nil { if pa.stream == nil {
return []string{} return []string{}
} }
return mediasDescription(pa.stream.Desc().Medias) return defs.MediasDescription(pa.stream.Desc().Medias)
}(), }(),
BytesReceived: func() uint64 { BytesReceived: func() uint64 {
if pa.stream == nil { if pa.stream == nil {
@ -728,7 +633,7 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) {
Readers: func() []defs.APIPathSourceOrReader { Readers: func() []defs.APIPathSourceOrReader {
ret := []defs.APIPathSourceOrReader{} ret := []defs.APIPathSourceOrReader{}
for r := range pa.readers { for r := range pa.readers {
ret = append(ret, r.apiReaderDescribe()) ret = append(ret, r.APIReaderDescribe())
} }
return ret 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() pa.confMutex.RLock()
defer pa.confMutex.RUnlock() defer pa.confMutex.RUnlock()
return pa.conf return pa.conf
} }
func (pa *path) shouldClose() bool { func (pa *path) ExternalCmdEnv() externalcmd.Environment {
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 {
_, port, _ := net.SplitHostPort(pa.rtspAddress) _, port, _ := net.SplitHostPort(pa.rtspAddress)
env := externalcmd.Environment{ env := externalcmd.Environment{
"MTX_PATH": pa.name, "MTX_PATH": pa.name,
@ -767,6 +664,14 @@ func (pa *path) externalCmdEnv() externalcmd.Environment {
return env 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() { func (pa *path) onDemandStaticSourceStart() {
pa.source.(*staticSourceHandler).start(true) pa.source.(*staticSourceHandler).start(true)
@ -799,7 +704,7 @@ func (pa *path) onDemandPublisherStart(query string) {
Logger: pa, Logger: pa,
ExternalCmdPool: pa.externalCmdPool, ExternalCmdPool: pa.externalCmdPool,
Conf: pa.conf, Conf: pa.conf,
ExternalCmdEnv: pa.externalCmdEnv(), ExternalCmdEnv: pa.ExternalCmdEnv(),
Query: query, Query: query,
}) })
@ -850,7 +755,7 @@ func (pa *path) setReady(desc *description.Session, allocateEncoder bool) error
Logger: pa, Logger: pa,
ExternalCmdPool: pa.externalCmdPool, ExternalCmdPool: pa.externalCmdPool,
Conf: pa.conf, Conf: pa.conf,
ExternalCmdEnv: pa.externalCmdEnv(), ExternalCmdEnv: pa.ExternalCmdEnv(),
Desc: pa.source.APISourceDescribe(), Desc: pa.source.APISourceDescribe(),
Query: pa.publisherQuery, Query: pa.publisherQuery,
}) })
@ -862,8 +767,8 @@ func (pa *path) setReady(desc *description.Session, allocateEncoder bool) error
func (pa *path) consumeOnHoldRequests() { func (pa *path) consumeOnHoldRequests() {
for _, req := range pa.describeRequestsOnHold { for _, req := range pa.describeRequestsOnHold {
req.res <- pathDescribeRes{ req.Res <- defs.PathDescribeRes{
stream: pa.stream, Stream: pa.stream,
} }
} }
pa.describeRequestsOnHold = nil pa.describeRequestsOnHold = nil
@ -879,7 +784,7 @@ func (pa *path) setNotReady() {
for r := range pa.readers { for r := range pa.readers {
pa.executeRemoveReader(r) pa.executeRemoveReader(r)
r.close() r.Close()
} }
pa.onNotReadyHook() pa.onNotReadyHook()
@ -904,10 +809,10 @@ func (pa *path) startRecording() {
SegmentDuration: time.Duration(pa.conf.RecordSegmentDuration), SegmentDuration: time.Duration(pa.conf.RecordSegmentDuration),
PathName: pa.name, PathName: pa.name,
Stream: pa.stream, Stream: pa.stream,
OnSegmentCreate: func(path string) { OnSegmentCreate: func(segmentPath string) {
if pa.conf.RunOnRecordSegmentCreate != "" { if pa.conf.RunOnRecordSegmentCreate != "" {
env := pa.externalCmdEnv() env := pa.ExternalCmdEnv()
env["MTX_SEGMENT_PATH"] = path env["MTX_SEGMENT_PATH"] = segmentPath
pa.Log(logger.Info, "runOnRecordSegmentCreate command launched") pa.Log(logger.Info, "runOnRecordSegmentCreate command launched")
externalcmd.NewCmd( externalcmd.NewCmd(
@ -918,10 +823,10 @@ func (pa *path) startRecording() {
nil) nil)
} }
}, },
OnSegmentComplete: func(path string) { OnSegmentComplete: func(segmentPath string) {
if pa.conf.RunOnRecordSegmentComplete != "" { if pa.conf.RunOnRecordSegmentComplete != "" {
env := pa.externalCmdEnv() env := pa.ExternalCmdEnv()
env["MTX_SEGMENT_PATH"] = path env["MTX_SEGMENT_PATH"] = segmentPath
pa.Log(logger.Info, "runOnRecordSegmentComplete command launched") pa.Log(logger.Info, "runOnRecordSegmentComplete command launched")
externalcmd.NewCmd( externalcmd.NewCmd(
@ -937,7 +842,7 @@ func (pa *path) startRecording() {
pa.recordAgent.Initialize() pa.recordAgent.Initialize()
} }
func (pa *path) executeRemoveReader(r reader) { func (pa *path) executeRemoveReader(r defs.Reader) {
delete(pa.readers, r) delete(pa.readers, r)
} }
@ -949,23 +854,21 @@ func (pa *path) executeRemovePublisher() {
pa.source = nil pa.source = nil
} }
func (pa *path) addReaderPost(req pathAddReaderReq) { func (pa *path) addReaderPost(req defs.PathAddReaderReq) {
if _, ok := pa.readers[req.author]; ok { if _, ok := pa.readers[req.Author]; ok {
req.res <- pathAddReaderRes{ req.Res <- defs.PathAddReaderRes{
path: pa, Path: pa,
stream: pa.stream, Stream: pa.stream,
} }
return return
} }
if pa.conf.MaxReaders != 0 && len(pa.readers) >= pa.conf.MaxReaders { if pa.conf.MaxReaders != 0 && len(pa.readers) >= pa.conf.MaxReaders {
req.res <- pathAddReaderRes{ req.Res <- defs.PathAddReaderRes{Err: fmt.Errorf("maximum reader count reached")}
err: fmt.Errorf("maximum reader count reached"),
}
return return
} }
pa.readers[req.author] = struct{}{} pa.readers[req.Author] = struct{}{}
if pa.conf.HasOnDemandStaticSource() { if pa.conf.HasOnDemandStaticSource() {
if pa.onDemandStaticSourceState == pathOnDemandStateClosing { if pa.onDemandStaticSourceState == pathOnDemandStateClosing {
@ -981,9 +884,9 @@ func (pa *path) addReaderPost(req pathAddReaderReq) {
} }
} }
req.res <- pathAddReaderRes{ req.Res <- defs.PathAddReaderRes{
path: pa, Path: pa,
stream: pa.stream, Stream: pa.stream,
} }
} }
@ -1032,72 +935,72 @@ func (pa *path) staticSourceHandlerSetNotReady(
} }
// describe is called by a reader or publisher through pathManager. // 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 { select {
case pa.chDescribe <- req: case pa.chDescribe <- req:
return <-req.res return <-req.Res
case <-pa.ctx.Done(): 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. // 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 { select {
case pa.chAddPublisher <- req: case pa.chAddPublisher <- req:
return <-req.res return <-req.Res
case <-pa.ctx.Done(): case <-pa.ctx.Done():
return pathAddPublisherRes{err: fmt.Errorf("terminated")} return defs.PathAddPublisherRes{Err: fmt.Errorf("terminated")}
} }
} }
// removePublisher is called by a publisher. // RemovePublisher is called by a publisher.
func (pa *path) removePublisher(req pathRemovePublisherReq) { func (pa *path) RemovePublisher(req defs.PathRemovePublisherReq) {
req.res = make(chan struct{}) req.Res = make(chan struct{})
select { select {
case pa.chRemovePublisher <- req: case pa.chRemovePublisher <- req:
<-req.res <-req.Res
case <-pa.ctx.Done(): case <-pa.ctx.Done():
} }
} }
// startPublisher is called by a publisher. // StartPublisher is called by a publisher.
func (pa *path) startPublisher(req pathStartPublisherReq) pathStartPublisherRes { func (pa *path) StartPublisher(req defs.PathStartPublisherReq) defs.PathStartPublisherRes {
req.res = make(chan pathStartPublisherRes) req.Res = make(chan defs.PathStartPublisherRes)
select { select {
case pa.chStartPublisher <- req: case pa.chStartPublisher <- req:
return <-req.res return <-req.Res
case <-pa.ctx.Done(): case <-pa.ctx.Done():
return pathStartPublisherRes{err: fmt.Errorf("terminated")} return defs.PathStartPublisherRes{Err: fmt.Errorf("terminated")}
} }
} }
// stopPublisher is called by a publisher. // StopPublisher is called by a publisher.
func (pa *path) stopPublisher(req pathStopPublisherReq) { func (pa *path) StopPublisher(req defs.PathStopPublisherReq) {
req.res = make(chan struct{}) req.Res = make(chan struct{})
select { select {
case pa.chStopPublisher <- req: case pa.chStopPublisher <- req:
<-req.res <-req.Res
case <-pa.ctx.Done(): case <-pa.ctx.Done():
} }
} }
// addReader is called by a reader through pathManager. // 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 { select {
case pa.chAddReader <- req: case pa.chAddReader <- req:
return <-req.res return <-req.Res
case <-pa.ctx.Done(): case <-pa.ctx.Done():
return pathAddReaderRes{err: fmt.Errorf("terminated")} return defs.PathAddReaderRes{Err: fmt.Errorf("terminated")}
} }
} }
// removeReader is called by a reader. // RemoveReader is called by a reader.
func (pa *path) removeReader(req pathRemoveReaderReq) { func (pa *path) RemoveReader(req defs.PathRemoveReaderReq) {
req.res = make(chan struct{}) req.Res = make(chan struct{})
select { select {
case pa.chRemoveReader <- req: case pa.chRemoveReader <- req:
<-req.res <-req.Res
case <-pa.ctx.Done(): case <-pa.ctx.Done():
} }
} }

166
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) return "", nil, nil, fmt.Errorf("path '%s' is not configured", name)
} }
type pathManagerHLSManager interface { type pathManagerHLSServer interface {
pathReady(*path) PathReady(defs.Path)
pathNotReady(*path) PathNotReady(defs.Path)
} }
type pathManagerParent interface { type pathManagerParent interface {
@ -83,20 +83,20 @@ type pathManager struct {
ctx context.Context ctx context.Context
ctxCancel func() ctxCancel func()
wg sync.WaitGroup wg sync.WaitGroup
hlsManager pathManagerHLSManager hlsManager pathManagerHLSServer
paths map[string]*path paths map[string]*path
pathsByConf map[string]map[*path]struct{} pathsByConf map[string]map[*path]struct{}
// in // in
chReloadConf chan map[string]*conf.Path chReloadConf chan map[string]*conf.Path
chSetHLSManager chan pathManagerHLSManager chSetHLSServer chan pathManagerHLSServer
chClosePath chan *path chClosePath chan *path
chPathReady chan *path chPathReady chan *path
chPathNotReady chan *path chPathNotReady chan *path
chGetConfForPath chan pathGetConfForPathReq chGetConfForPath chan defs.PathGetConfForPathReq
chDescribe chan pathDescribeReq chDescribe chan defs.PathDescribeReq
chAddReader chan pathAddReaderReq chAddReader chan defs.PathAddReaderReq
chAddPublisher chan pathAddPublisherReq chAddPublisher chan defs.PathAddPublisherReq
chAPIPathsList chan pathAPIPathsListReq chAPIPathsList chan pathAPIPathsListReq
chAPIPathsGet chan pathAPIPathsGetReq chAPIPathsGet chan pathAPIPathsGetReq
} }
@ -131,14 +131,14 @@ func newPathManager(
paths: make(map[string]*path), paths: make(map[string]*path),
pathsByConf: make(map[string]map[*path]struct{}), pathsByConf: make(map[string]map[*path]struct{}),
chReloadConf: make(chan map[string]*conf.Path), chReloadConf: make(chan map[string]*conf.Path),
chSetHLSManager: make(chan pathManagerHLSManager), chSetHLSServer: make(chan pathManagerHLSServer),
chClosePath: make(chan *path), chClosePath: make(chan *path),
chPathReady: make(chan *path), chPathReady: make(chan *path),
chPathNotReady: make(chan *path), chPathNotReady: make(chan *path),
chGetConfForPath: make(chan pathGetConfForPathReq), chGetConfForPath: make(chan defs.PathGetConfForPathReq),
chDescribe: make(chan pathDescribeReq), chDescribe: make(chan defs.PathDescribeReq),
chAddReader: make(chan pathAddReaderReq), chAddReader: make(chan defs.PathAddReaderReq),
chAddPublisher: make(chan pathAddPublisherReq), chAddPublisher: make(chan defs.PathAddPublisherReq),
chAPIPathsList: make(chan pathAPIPathsListReq), chAPIPathsList: make(chan pathAPIPathsListReq),
chAPIPathsGet: make(chan pathAPIPathsGetReq), chAPIPathsGet: make(chan pathAPIPathsGetReq),
} }
@ -163,7 +163,7 @@ func (pm *pathManager) close() {
pm.wg.Wait() pm.wg.Wait()
} }
// Log is the main logging function. // Log implements logger.Writer.
func (pm *pathManager) Log(level logger.Level, format string, args ...interface{}) { func (pm *pathManager) Log(level logger.Level, format string, args ...interface{}) {
pm.parent.Log(level, format, args...) pm.parent.Log(level, format, args...)
} }
@ -177,8 +177,8 @@ outer:
case newPaths := <-pm.chReloadConf: case newPaths := <-pm.chReloadConf:
pm.doReloadConf(newPaths) pm.doReloadConf(newPaths)
case m := <-pm.chSetHLSManager: case m := <-pm.chSetHLSServer:
pm.doSetHLSManager(m) pm.doSetHLSServer(m)
case pa := <-pm.chClosePath: case pa := <-pm.chClosePath:
pm.doClosePath(pa) 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 pm.hlsManager = m
} }
@ -265,101 +265,101 @@ func (pm *pathManager) doClosePath(pa *path) {
func (pm *pathManager) doPathReady(pa *path) { func (pm *pathManager) doPathReady(pa *path) {
if pm.hlsManager != nil { if pm.hlsManager != nil {
pm.hlsManager.pathReady(pa) pm.hlsManager.PathReady(pa)
} }
} }
func (pm *pathManager) doPathNotReady(pa *path) { func (pm *pathManager) doPathNotReady(pa *path) {
if pm.hlsManager != nil { if pm.hlsManager != nil {
pm.hlsManager.pathNotReady(pa) pm.hlsManager.PathNotReady(pa)
} }
} }
func (pm *pathManager) doGetConfForPath(req pathGetConfForPathReq) { func (pm *pathManager) doGetConfForPath(req defs.PathGetConfForPathReq) {
_, pathConf, _, err := getConfForPath(pm.pathConfs, req.accessRequest.name) _, pathConf, _, err := getConfForPath(pm.pathConfs, req.AccessRequest.Name)
if err != nil { if err != nil {
req.res <- pathGetConfForPathRes{err: err} req.Res <- defs.PathGetConfForPathRes{Err: err}
return return
} }
err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods,
pathConf, req.accessRequest) pathConf, req.AccessRequest)
if err != nil { if err != nil {
req.res <- pathGetConfForPathRes{err: err} req.Res <- defs.PathGetConfForPathRes{Err: err}
return return
} }
req.res <- pathGetConfForPathRes{conf: pathConf} req.Res <- defs.PathGetConfForPathRes{Conf: pathConf}
} }
func (pm *pathManager) doDescribe(req pathDescribeReq) { func (pm *pathManager) doDescribe(req defs.PathDescribeReq) {
pathConfName, pathConf, pathMatches, err := getConfForPath(pm.pathConfs, req.accessRequest.name) pathConfName, pathConf, pathMatches, err := getConfForPath(pm.pathConfs, req.AccessRequest.Name)
if err != nil { if err != nil {
req.res <- pathDescribeRes{err: err} req.Res <- defs.PathDescribeRes{Err: err}
return return
} }
err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods,
pathConf, req.accessRequest) pathConf, req.AccessRequest)
if err != nil { if err != nil {
req.res <- pathDescribeRes{err: err} req.Res <- defs.PathDescribeRes{Err: err}
return return
} }
// create path if it doesn't exist // create path if it doesn't exist
if _, ok := pm.paths[req.accessRequest.name]; !ok { if _, ok := pm.paths[req.AccessRequest.Name]; !ok {
pm.createPath(pathConfName, pathConf, req.accessRequest.name, pathMatches) 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) { func (pm *pathManager) doAddReader(req defs.PathAddReaderReq) {
pathConfName, pathConf, pathMatches, err := getConfForPath(pm.pathConfs, req.accessRequest.name) pathConfName, pathConf, pathMatches, err := getConfForPath(pm.pathConfs, req.AccessRequest.Name)
if err != nil { if err != nil {
req.res <- pathAddReaderRes{err: err} req.Res <- defs.PathAddReaderRes{Err: err}
return return
} }
if !req.accessRequest.skipAuth { if !req.AccessRequest.SkipAuth {
err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods,
pathConf, req.accessRequest) pathConf, req.AccessRequest)
if err != nil { if err != nil {
req.res <- pathAddReaderRes{err: err} req.Res <- defs.PathAddReaderRes{Err: err}
return return
} }
} }
// create path if it doesn't exist // create path if it doesn't exist
if _, ok := pm.paths[req.accessRequest.name]; !ok { if _, ok := pm.paths[req.AccessRequest.Name]; !ok {
pm.createPath(pathConfName, pathConf, req.accessRequest.name, pathMatches) 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) { func (pm *pathManager) doAddPublisher(req defs.PathAddPublisherReq) {
pathConfName, pathConf, pathMatches, err := getConfForPath(pm.pathConfs, req.accessRequest.name) pathConfName, pathConf, pathMatches, err := getConfForPath(pm.pathConfs, req.AccessRequest.Name)
if err != nil { if err != nil {
req.res <- pathAddPublisherRes{err: err} req.Res <- defs.PathAddPublisherRes{Err: err}
return return
} }
if !req.accessRequest.skipAuth { if !req.AccessRequest.SkipAuth {
err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods,
pathConf, req.accessRequest) pathConf, req.AccessRequest)
if err != nil { if err != nil {
req.res <- pathAddPublisherRes{err: err} req.Res <- defs.PathAddPublisherRes{Err: err}
return return
} }
} }
// create path if it doesn't exist // create path if it doesn't exist
if _, ok := pm.paths[req.accessRequest.name]; !ok { if _, ok := pm.paths[req.AccessRequest.Name]; !ok {
pm.createPath(pathConfName, pathConf, req.accessRequest.name, pathMatches) 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) { func (pm *pathManager) doAPIPathsList(req pathAPIPathsListReq) {
@ -454,79 +454,79 @@ func (pm *pathManager) closePath(pa *path) {
} }
} }
// getConfForPath is called by a reader or publisher. // GetConfForPath is called by a reader or publisher.
func (pm *pathManager) getConfForPath(req pathGetConfForPathReq) pathGetConfForPathRes { func (pm *pathManager) GetConfForPath(req defs.PathGetConfForPathReq) defs.PathGetConfForPathRes {
req.res = make(chan pathGetConfForPathRes) req.Res = make(chan defs.PathGetConfForPathRes)
select { select {
case pm.chGetConfForPath <- req: case pm.chGetConfForPath <- req:
return <-req.res return <-req.Res
case <-pm.ctx.Done(): 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. // Describe is called by a reader or publisher.
func (pm *pathManager) describe(req pathDescribeReq) pathDescribeRes { func (pm *pathManager) Describe(req defs.PathDescribeReq) defs.PathDescribeRes {
req.res = make(chan pathDescribeRes) req.Res = make(chan defs.PathDescribeRes)
select { select {
case pm.chDescribe <- req: case pm.chDescribe <- req:
res1 := <-req.res res1 := <-req.Res
if res1.err != nil { if res1.Err != nil {
return res1 return res1
} }
res2 := res1.path.describe(req) res2 := res1.Path.(*path).describe(req)
if res2.err != nil { if res2.Err != nil {
return res2 return res2
} }
res2.path = res1.path res2.Path = res1.Path
return res2 return res2
case <-pm.ctx.Done(): case <-pm.ctx.Done():
return pathDescribeRes{err: fmt.Errorf("terminated")} return defs.PathDescribeRes{Err: fmt.Errorf("terminated")}
} }
} }
// addPublisher is called by a publisher. // AddPublisher is called by a publisher.
func (pm *pathManager) addPublisher(req pathAddPublisherReq) pathAddPublisherRes { func (pm *pathManager) AddPublisher(req defs.PathAddPublisherReq) defs.PathAddPublisherRes {
req.res = make(chan pathAddPublisherRes) req.Res = make(chan defs.PathAddPublisherRes)
select { select {
case pm.chAddPublisher <- req: case pm.chAddPublisher <- req:
res := <-req.res res := <-req.Res
if res.err != nil { if res.Err != nil {
return res return res
} }
return res.path.addPublisher(req) return res.Path.(*path).addPublisher(req)
case <-pm.ctx.Done(): case <-pm.ctx.Done():
return pathAddPublisherRes{err: fmt.Errorf("terminated")} return defs.PathAddPublisherRes{Err: fmt.Errorf("terminated")}
} }
} }
// addReader is called by a reader. // AddReader is called by a reader.
func (pm *pathManager) addReader(req pathAddReaderReq) pathAddReaderRes { func (pm *pathManager) AddReader(req defs.PathAddReaderReq) defs.PathAddReaderRes {
req.res = make(chan pathAddReaderRes) req.Res = make(chan defs.PathAddReaderRes)
select { select {
case pm.chAddReader <- req: case pm.chAddReader <- req:
res := <-req.res res := <-req.Res
if res.err != nil { if res.Err != nil {
return res return res
} }
return res.path.addReader(req) return res.Path.(*path).addReader(req)
case <-pm.ctx.Done(): case <-pm.ctx.Done():
return pathAddReaderRes{err: fmt.Errorf("terminated")} return defs.PathAddReaderRes{Err: fmt.Errorf("terminated")}
} }
} }
// setHLSManager is called by hlsManager. // setHLSServer is called by hlsManager.
func (pm *pathManager) setHLSManager(s pathManagerHLSManager) { func (pm *pathManager) setHLSServer(s pathManagerHLSServer) {
select { select {
case pm.chSetHLSManager <- s: case pm.chSetHLSServer <- s:
case <-pm.ctx.Done(): case <-pm.ctx.Done():
} }
} }

3
internal/core/path_manager_test.go

@ -7,9 +7,12 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/base" "github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/headers" "github.com/bluenviron/gortsplib/v4/pkg/headers"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var _ defs.PathManager = &pathManager{}
func TestPathAutoDeletion(t *testing.T) { func TestPathAutoDeletion(t *testing.T) {
for _, ca := range []string{"describe", "setup"} { for _, ca := range []string{"describe", "setup"} {
t.Run(ca, func(t *testing.T) { t.Run(ca, func(t *testing.T) {

5
internal/core/path_test.go

@ -19,10 +19,11 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/headers" "github.com/bluenviron/gortsplib/v4/pkg/headers"
"github.com/bluenviron/gortsplib/v4/pkg/sdp" "github.com/bluenviron/gortsplib/v4/pkg/sdp"
"github.com/datarhei/gosrt" srt "github.com/datarhei/gosrt"
"github.com/pion/rtp" "github.com/pion/rtp"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp" "github.com/bluenviron/mediamtx/internal/protocols/rtmp"
"github.com/bluenviron/mediamtx/internal/protocols/webrtc" "github.com/bluenviron/mediamtx/internal/protocols/webrtc"
) )
@ -81,6 +82,8 @@ func main() {
} }
` `
var _ defs.Path = &path{}
func TestPathRunOnDemand(t *testing.T) { func TestPathRunOnDemand(t *testing.T) {
onDemandFile := filepath.Join(os.TempDir(), "ondemand") onDemandFile := filepath.Join(os.TempDir(), "ondemand")
onUnDemandFile := filepath.Join(os.TempDir(), "onundemand") onUnDemandFile := filepath.Join(os.TempDir(), "onundemand")

1
internal/core/pprof.go

@ -58,6 +58,7 @@ func (pp *pprof) close() {
pp.httpServer.Close() pp.httpServer.Close()
} }
// Log implements logger.Writer.
func (pp *pprof) Log(level logger.Level, format string, args ...interface{}) { func (pp *pprof) Log(level logger.Level, format string, args ...interface{}) {
pp.parent.Log(level, "[pprof] "+format, args...) pp.parent.Log(level, "[pprof] "+format, args...)
} }

7
internal/core/publisher.go

@ -1,7 +0,0 @@
package core
// publisher is an entity that can publish a stream.
type publisher interface {
source
close()
}

17
internal/core/reader.go

@ -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))
}

48
internal/core/rtmp_listener.go

@ -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)
}
}

327
internal/core/rtmp_server.go

@ -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")
}
}

454
internal/core/rtsp_server.go

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

330
internal/core/srt_server.go

@ -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")
}
}

3
internal/core/static_source_handler.go

@ -153,6 +153,7 @@ func (s *staticSourceHandler) stop(reason string) {
<-s.done <-s.done
} }
// Log implements logger.Writer.
func (s *staticSourceHandler) Log(level logger.Level, format string, args ...interface{}) { func (s *staticSourceHandler) Log(level logger.Level, format string, args ...interface{}) {
s.parent.Log(level, format, args...) s.parent.Log(level, format, args...)
} }
@ -242,7 +243,7 @@ func (s *staticSourceHandler) SetReady(req defs.PathSourceStaticSetReadyReq) def
res := <-req.Res res := <-req.Res
if res.Err == nil { 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 return res

0
internal/core/webrtc_manager_test.go → internal/core/webrtc_server_test.go

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

137
internal/defs/path.go

@ -1,25 +1,156 @@
package defs package defs
import ( import (
"fmt"
"net"
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/description" "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" "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 { type PathSourceStaticSetReadyRes struct {
Stream *stream.Stream Stream *stream.Stream
Err error Err error
} }
// PathSourceStaticSetReadyReq is a set ready request from a static source. // PathSourceStaticSetReadyReq contains arguments of SetReady().
type PathSourceStaticSetReadyReq struct { type PathSourceStaticSetReadyReq struct {
Desc *description.Session Desc *description.Session
GenerateRTPPackets bool GenerateRTPPackets bool
Res chan PathSourceStaticSetReadyRes Res chan PathSourceStaticSetReadyRes
} }
// PathSourceStaticSetNotReadyReq is a set not ready request from a static source. // PathSourceStaticSetNotReadyReq contains arguments of SetNotReady().
type PathSourceStaticSetNotReadyReq struct { type PathSourceStaticSetNotReadyReq struct {
Res chan struct{} Res chan struct{}
} }

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

7
internal/defs/publisher.go

@ -0,0 +1,7 @@
package defs
// Publisher is an entity that can publish a stream.
type Publisher interface {
Source
Close()
}

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

17
internal/core/source.go → internal/defs/source.go

@ -1,4 +1,4 @@
package core package defs
import ( import (
"fmt" "fmt"
@ -6,18 +6,17 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/logger" "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: // it can be:
// - publisher // - publisher
// - staticSourceHandler // - staticSourceHandler
// - redirectSource // - redirectSource
type source interface { type Source interface {
logger.Writer logger.Writer
APISourceDescribe() defs.APIPathSourceOrReader APISourceDescribe() APIPathSourceOrReader
} }
func mediaDescription(media *description.Media) string { func mediaDescription(media *description.Media) string {
@ -28,7 +27,8 @@ func mediaDescription(media *description.Media) string {
return strings.Join(ret, "/") 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)) ret := make([]string, len(medias))
for i, media := range medias { for i, media := range medias {
ret[i] = mediaDescription(media) ret[i] = mediaDescription(media)
@ -36,7 +36,8 @@ func mediasDescription(medias []*description.Media) []string {
return ret 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)", return fmt.Sprintf("%d %s (%s)",
len(medias), len(medias),
func() string { func() string {
@ -45,5 +46,5 @@ func mediaInfo(medias []*description.Media) string {
} }
return "tracks" return "tracks"
}(), }(),
strings.Join(mediasDescription(medias), ", ")) strings.Join(MediasDescription(medias), ", "))
} }

10
internal/core/mpegts.go → internal/protocols/mpegts/from_stream.go

@ -1,4 +1,4 @@
package core package mpegts
import ( import (
"bufio" "bufio"
@ -10,10 +10,9 @@ import (
"github.com/bluenviron/mediacommon/pkg/codecs/h264" "github.com/bluenviron/mediacommon/pkg/codecs/h264"
"github.com/bluenviron/mediacommon/pkg/codecs/h265" "github.com/bluenviron/mediacommon/pkg/codecs/h265"
mcmpegts "github.com/bluenviron/mediacommon/pkg/formats/mpegts" 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/asyncwriter"
"github.com/bluenviron/mediamtx/internal/protocols/mpegts"
"github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/unit" "github.com/bluenviron/mediamtx/internal/unit"
) )
@ -22,7 +21,8 @@ func durationGoToMPEGTS(v time.Duration) int64 {
return int64(v.Seconds() * 90000) return int64(v.Seconds() * 90000)
} }
func mpegtsSetupWrite( // FromStream links a server stream to a MPEG-TS writer.
func FromStream(
stream *stream.Stream, stream *stream.Stream,
writer *asyncwriter.Writer, writer *asyncwriter.Writer,
bw *bufio.Writer, bw *bufio.Writer,
@ -251,7 +251,7 @@ func mpegtsSetupWrite(
} }
if len(tracks) == 0 { if len(tracks) == 0 {
return mpegts.ErrNoTracks return ErrNoTracks
} }
w = mcmpegts.NewWriter(bw, tracks) w = mcmpegts.NewWriter(bw, tracks)

2
internal/record/agent.go

@ -54,7 +54,7 @@ func (w *Agent) Initialize() {
go w.run() go w.run()
} }
// Log is the main logging function. // Log implements logger.Writer.
func (w *Agent) Log(level logger.Level, format string, args ...interface{}) { func (w *Agent) Log(level logger.Level, format string, args ...interface{}) {
w.Parent.Log(level, "[record] "+format, args...) w.Parent.Log(level, "[record] "+format, args...)
} }

2
internal/record/cleaner.go

@ -71,7 +71,7 @@ func (c *Cleaner) Close() {
<-c.done <-c.done
} }
// Log is the main logging function. // Log implements logger.Writer.
func (c *Cleaner) Log(level logger.Level, format string, args ...interface{}) { func (c *Cleaner) Log(level logger.Level, format string, args ...interface{}) {
c.Parent.Log(level, "[record cleaner]"+format, args...) c.Parent.Log(level, "[record cleaner]"+format, args...)
} }

0
internal/core/hls.min.js → internal/servers/hls/hls.min.js vendored

99
internal/core/hls_http_server.go → internal/servers/hls/http_server.go

@ -1,4 +1,4 @@
package core package hls
import ( import (
_ "embed" _ "embed"
@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/httpserv" "github.com/bluenviron/mediamtx/internal/protocols/httpserv"
"github.com/bluenviron/mediamtx/internal/restrictnetwork" "github.com/bluenviron/mediamtx/internal/restrictnetwork"
@ -21,84 +22,70 @@ const (
hlsPauseAfterAuthError = 2 * time.Second hlsPauseAfterAuthError = 2 * time.Second
) )
//go:embed hls_index.html //go:embed index.html
var hlsIndex []byte var hlsIndex []byte
//go:embed hls.min.js //go:embed hls.min.js
var hlsMinJS []byte var hlsMinJS []byte
type hlsHTTPServerParent interface { type httpServer struct {
logger.Writer address string
handleRequest(req hlsMuxerHandleRequestReq) encryption bool
} serverKey string
serverCert string
type hlsHTTPServer struct { allowOrigin string
allowOrigin string trustedProxies conf.IPsOrCIDRs
pathManager *pathManager readTimeout conf.StringDuration
parent hlsHTTPServerParent pathManager defs.PathManager
parent *Server
inner *httpserv.WrappedServer inner *httpserv.WrappedServer
} }
func newHLSHTTPServer( //nolint:dupl func (s *httpServer) initialize() error {
address string, if s.encryption {
encryption bool, if s.serverCert == "" {
serverKey string, return fmt.Errorf("server cert is missing")
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")
} }
} else { } else {
serverKey = "" s.serverKey = ""
serverCert = "" s.serverCert = ""
}
s := &hlsHTTPServer{
allowOrigin: allowOrigin,
pathManager: pathManager,
parent: parent,
} }
router := gin.New() router := gin.New()
router.SetTrustedProxies(trustedProxies.ToTrustedProxies()) //nolint:errcheck router.SetTrustedProxies(s.trustedProxies.ToTrustedProxies()) //nolint:errcheck
router.NoRoute(s.onRequest) router.NoRoute(s.onRequest)
network, address := restrictnetwork.Restrict("tcp", address) network, address := restrictnetwork.Restrict("tcp", s.address)
var err error var err error
s.inner, err = httpserv.NewWrappedServer( s.inner, err = httpserv.NewWrappedServer(
network, network,
address, address,
time.Duration(readTimeout), time.Duration(s.readTimeout),
serverCert, s.serverCert,
serverKey, s.serverKey,
router, router,
s, s,
) )
if err != nil { 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...) s.parent.Log(level, format, args...)
} }
func (s *hlsHTTPServer) close() { func (s *httpServer) close() {
s.inner.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-Origin", s.allowOrigin)
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 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() user, pass, hasCredentials := ctx.Request.BasicAuth()
res := s.pathManager.getConfForPath(pathGetConfForPathReq{ res := s.pathManager.GetConfForPath(defs.PathGetConfForPathReq{
accessRequest: pathAccessRequest{ AccessRequest: defs.PathAccessRequest{
name: dir, Name: dir,
query: ctx.Request.URL.RawQuery, Query: ctx.Request.URL.RawQuery,
publish: false, Publish: false,
ip: net.ParseIP(ctx.ClientIP()), IP: net.ParseIP(ctx.ClientIP()),
user: user, User: user,
pass: pass, Pass: pass,
proto: authProtocolHLS, Proto: defs.AuthProtocolHLS,
}, },
}) })
if res.err != nil { if res.Err != nil {
if terr, ok := res.err.(*errAuthentication); ok { if terr, ok := res.Err.(*defs.ErrAuthentication); ok {
if !hasCredentials { if !hasCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.Writer.WriteHeader(http.StatusUnauthorized) ctx.Writer.WriteHeader(http.StatusUnauthorized)
@ -182,7 +169,7 @@ func (s *hlsHTTPServer) onRequest(ctx *gin.Context) {
_, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr) _, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr)
remoteAddr := net.JoinHostPort(ip, port) 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 // wait some seconds to stop brute force attacks
<-time.After(hlsPauseAfterAuthError) <-time.After(hlsPauseAfterAuthError)
@ -203,7 +190,7 @@ func (s *hlsHTTPServer) onRequest(ctx *gin.Context) {
ctx.Writer.Write(hlsIndex) ctx.Writer.Write(hlsIndex)
default: default:
s.parent.handleRequest(hlsMuxerHandleRequestReq{ s.parent.handleRequest(muxerHandleRequestReq{
path: dir, path: dir,
file: fname, file: fname,
ctx: ctx, ctx: ctx,

0
internal/core/hls_index.html → internal/servers/hls/index.html

145
internal/core/hls_muxer.go → internal/servers/hls/muxer.go

@ -1,4 +1,4 @@
package core package hls
import ( import (
"context" "context"
@ -26,15 +26,21 @@ import (
) )
const ( const (
closeCheckPeriod = 1 * time.Second closeCheckPeriod = 1 * time.Second
closeAfterInactivity = 60 * time.Second closeAfterInactivity = 60 * time.Second
hlsMuxerRecreatePause = 10 * time.Second muxerRecreatePause = 10 * time.Second
) )
func int64Ptr(v int64) *int64 { func int64Ptr(v int64) *int64 {
return &v return &v
} }
func newEmptyTimer() *time.Timer {
t := time.NewTimer(0)
<-t.C
return t
}
type responseWriterWithCounter struct { type responseWriterWithCounter struct {
http.ResponseWriter http.ResponseWriter
bytesSent *uint64 bytesSent *uint64
@ -46,19 +52,15 @@ func (w *responseWriterWithCounter) Write(p []byte) (int, error) {
return n, err return n, err
} }
type hlsMuxerHandleRequestReq struct { type muxerHandleRequestReq struct {
path string path string
file string file string
ctx *gin.Context ctx *gin.Context
res chan *hlsMuxer res chan *muxer
}
type hlsMuxerParent interface {
logger.Writer
closeMuxer(*hlsMuxer)
} }
type hlsMuxer struct { type muxer struct {
parentCtx context.Context
remoteAddr string remoteAddr string
externalAuthenticationURL string externalAuthenticationURL string
variant conf.HLSVariant variant conf.HLSVariant
@ -70,90 +72,59 @@ type hlsMuxer struct {
writeQueueSize int writeQueueSize int
wg *sync.WaitGroup wg *sync.WaitGroup
pathName string pathName string
pathManager *pathManager pathManager defs.PathManager
parent hlsMuxerParent parent *Server
ctx context.Context ctx context.Context
ctxCancel func() ctxCancel func()
created time.Time created time.Time
path *path path defs.Path
writer *asyncwriter.Writer writer *asyncwriter.Writer
lastRequestTime *int64 lastRequestTime *int64
muxer *gohlslib.Muxer muxer *gohlslib.Muxer
requests []*hlsMuxerHandleRequestReq requests []*muxerHandleRequestReq
bytesSent *uint64 bytesSent *uint64
// in // in
chRequest chan *hlsMuxerHandleRequestReq chRequest chan *muxerHandleRequestReq
} }
func newHLSMuxer( func (m *muxer) initialize() {
parentCtx context.Context, ctx, ctxCancel := context.WithCancel(m.parentCtx)
remoteAddr string,
externalAuthenticationURL string, m.ctx = ctx
variant conf.HLSVariant, m.ctxCancel = ctxCancel
segmentCount int, m.created = time.Now()
segmentDuration conf.StringDuration, m.lastRequestTime = int64Ptr(time.Now().UnixNano())
partDuration conf.StringDuration, m.bytesSent = new(uint64)
segmentMaxSize conf.StringSize, m.chRequest = make(chan *muxerHandleRequestReq)
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),
}
m.Log(logger.Info, "created %s", func() string { m.Log(logger.Info, "created %s", func() string {
if remoteAddr == "" { if m.remoteAddr == "" {
return "automatically" return "automatically"
} }
return "(requested by " + remoteAddr + ")" return "(requested by " + m.remoteAddr + ")"
}()) }())
m.wg.Add(1) m.wg.Add(1)
go m.run() go m.run()
return m
} }
func (m *hlsMuxer) close() { func (m *muxer) Close() {
m.ctxCancel() 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...)...) m.parent.Log(level, "[muxer %s] "+format, append([]interface{}{m.pathName}, args...)...)
} }
// PathName returns the path name. // PathName returns the path name.
func (m *hlsMuxer) PathName() string { func (m *muxer) PathName() string {
return m.pathName return m.pathName
} }
func (m *hlsMuxer) run() { func (m *muxer) run() {
defer m.wg.Done() defer m.wg.Done()
err := func() error { err := func() error {
@ -213,7 +184,7 @@ func (m *hlsMuxer) run() {
m.clearQueuedRequests() m.clearQueuedRequests()
isReady = false isReady = false
isRecreating = true isRecreating = true
recreateTimer = time.NewTimer(hlsMuxerRecreatePause) recreateTimer = time.NewTimer(muxerRecreatePause)
} else { } else {
return err return err
} }
@ -234,41 +205,41 @@ func (m *hlsMuxer) run() {
m.Log(logger.Info, "destroyed: %v", err) m.Log(logger.Info, "destroyed: %v", err)
} }
func (m *hlsMuxer) clearQueuedRequests() { func (m *muxer) clearQueuedRequests() {
for _, req := range m.requests { for _, req := range m.requests {
req.res <- nil req.res <- nil
} }
m.requests = nil m.requests = nil
} }
func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{}) error { func (m *muxer) runInner(innerCtx context.Context, innerReady chan struct{}) error {
res := m.pathManager.addReader(pathAddReaderReq{ res := m.pathManager.AddReader(defs.PathAddReaderReq{
author: m, Author: m,
accessRequest: pathAccessRequest{ AccessRequest: defs.PathAccessRequest{
name: m.pathName, Name: m.pathName,
skipAuth: true, SkipAuth: true,
}, },
}) })
if res.err != nil { if res.Err != nil {
return res.err 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) m.writer = asyncwriter.New(m.writeQueueSize, m)
defer res.stream.RemoveReader(m.writer) defer res.Stream.RemoveReader(m.writer)
var medias []*description.Media var medias []*description.Media
videoMedia, videoTrack := m.createVideoTrack(res.stream) videoMedia, videoTrack := m.createVideoTrack(res.Stream)
if videoMedia != nil { if videoMedia != nil {
medias = append(medias, videoMedia) medias = append(medias, videoMedia)
} }
audioMedia, audioTrack := m.createAudioTrack(res.stream) audioMedia, audioTrack := m.createAudioTrack(res.Stream)
if audioMedia != nil { if audioMedia != nil {
medias = append(medias, audioMedia) medias = append(medias, audioMedia)
} }
@ -305,7 +276,7 @@ func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{})
innerReady <- struct{}{} innerReady <- struct{}{}
m.Log(logger.Info, "is converting into HLS, %s", m.Log(logger.Info, "is converting into HLS, %s",
mediaInfo(medias)) defs.MediasInfo(medias))
m.writer.Start() 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 var videoFormatAV1 *format.AV1
videoMedia := stream.Desc().FindFormat(&videoFormatAV1) videoMedia := stream.Desc().FindFormat(&videoFormatAV1)
@ -444,7 +415,7 @@ func (m *hlsMuxer) createVideoTrack(stream *stream.Stream) (*description.Media,
return nil, nil 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 var audioFormatOpus *format.Opus
audioMedia := stream.Desc().FindFormat(&audioFormatOpus) audioMedia := stream.Desc().FindFormat(&audioFormatOpus)
@ -507,7 +478,7 @@ func (m *hlsMuxer) createAudioTrack(stream *stream.Stream) (*description.Media,
return nil, nil return nil, nil
} }
func (m *hlsMuxer) handleRequest(ctx *gin.Context) { func (m *muxer) handleRequest(ctx *gin.Context) {
atomic.StoreInt64(m.lastRequestTime, time.Now().UnixNano()) atomic.StoreInt64(m.lastRequestTime, time.Now().UnixNano())
w := &responseWriterWithCounter{ w := &responseWriterWithCounter{
@ -519,7 +490,7 @@ func (m *hlsMuxer) handleRequest(ctx *gin.Context) {
} }
// processRequest is called by hlsserver.Server (forwarded from ServeHTTP). // processRequest is called by hlsserver.Server (forwarded from ServeHTTP).
func (m *hlsMuxer) processRequest(req *hlsMuxerHandleRequestReq) { func (m *muxer) processRequest(req *muxerHandleRequestReq) {
select { select {
case m.chRequest <- req: case m.chRequest <- req:
case <-m.ctx.Done(): case <-m.ctx.Done():
@ -527,15 +498,15 @@ func (m *hlsMuxer) processRequest(req *hlsMuxerHandleRequestReq) {
} }
} }
// apiReaderDescribe implements reader. // APIReaderDescribe implements reader.
func (m *hlsMuxer) apiReaderDescribe() defs.APIPathSourceOrReader { func (m *muxer) APIReaderDescribe() defs.APIPathSourceOrReader {
return defs.APIPathSourceOrReader{ return defs.APIPathSourceOrReader{
Type: "hlsMuxer", Type: "hlsMuxer",
ID: "", ID: "",
} }
} }
func (m *hlsMuxer) apiItem() *defs.APIHLSMuxer { func (m *muxer) apiItem() *defs.APIHLSMuxer {
return &defs.APIHLSMuxer{ return &defs.APIHLSMuxer{
Path: m.pathName, Path: m.pathName,
Created: m.created, Created: m.created,

290
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")
}
}

198
internal/core/rtmp_conn.go → internal/servers/rtmp/conn.go

@ -1,4 +1,4 @@
package core package rtmp
import ( import (
"context" "context"
@ -40,24 +40,15 @@ func pathNameAndQuery(inURL *url.URL) (string, url.Values, string) {
return pathName, ur.Query(), ur.RawQuery return pathName, ur.Query(), ur.RawQuery
} }
type rtmpConnState int type connState int
const ( const (
rtmpConnStateRead rtmpConnState = iota + 1 connStateRead connState = iota + 1
rtmpConnStatePublish connStatePublish
) )
type rtmpConnPathManager interface { type conn struct {
addReader(req pathAddReaderReq) pathAddReaderRes parentCtx context.Context
addPublisher(req pathAddPublisherReq) pathAddPublisherRes
}
type rtmpConnParent interface {
logger.Writer
closeConn(*rtmpConn)
}
type rtmpConn struct {
isTLS bool isTLS bool
rtspAddress string rtspAddress string
readTimeout conf.StringDuration readTimeout conf.StringDuration
@ -69,8 +60,8 @@ type rtmpConn struct {
wg *sync.WaitGroup wg *sync.WaitGroup
nconn net.Conn nconn net.Conn
externalCmdPool *externalcmd.Pool externalCmdPool *externalcmd.Pool
pathManager rtmpConnPathManager pathManager defs.PathManager
parent rtmpConnParent parent *Server
ctx context.Context ctx context.Context
ctxCancel func() ctxCancel func()
@ -78,73 +69,40 @@ type rtmpConn struct {
created time.Time created time.Time
mutex sync.RWMutex mutex sync.RWMutex
rconn *rtmp.Conn rconn *rtmp.Conn
state rtmpConnState state connState
pathName string pathName string
} }
func newRTMPConn( func (c *conn) initialize() {
parentCtx context.Context, c.ctx, c.ctxCancel = context.WithCancel(c.parentCtx)
isTLS bool,
rtspAddress string, c.uuid = uuid.New()
readTimeout conf.StringDuration, c.created = time.Now()
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(),
}
c.Log(logger.Info, "opened") c.Log(logger.Info, "opened")
c.wg.Add(1) c.wg.Add(1)
go c.run() go c.run()
return c
} }
func (c *rtmpConn) close() { func (c *conn) Close() {
c.ctxCancel() c.ctxCancel()
} }
func (c *rtmpConn) remoteAddr() net.Addr { func (c *conn) remoteAddr() net.Addr {
return c.nconn.RemoteAddr() 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...)...) 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 return c.nconn.RemoteAddr().(*net.TCPAddr).IP
} }
func (c *rtmpConn) run() { //nolint:dupl func (c *conn) run() { //nolint:dupl
defer c.wg.Done() defer c.wg.Done()
onDisconnectHook := hooks.OnConnect(hooks.OnConnectParams{ onDisconnectHook := hooks.OnConnect(hooks.OnConnectParams{
@ -154,7 +112,7 @@ func (c *rtmpConn) run() { //nolint:dupl
RunOnConnectRestart: c.runOnConnectRestart, RunOnConnectRestart: c.runOnConnectRestart,
RunOnDisconnect: c.runOnDisconnect, RunOnDisconnect: c.runOnDisconnect,
RTSPAddress: c.rtspAddress, RTSPAddress: c.rtspAddress,
Desc: c.apiReaderDescribe(), Desc: c.APIReaderDescribe(),
}) })
defer onDisconnectHook() defer onDisconnectHook()
@ -167,7 +125,7 @@ func (c *rtmpConn) run() { //nolint:dupl
c.Log(logger.Info, "closed: %v", err) c.Log(logger.Info, "closed: %v", err)
} }
func (c *rtmpConn) runInner() error { func (c *conn) runInner() error {
readerErr := make(chan error) readerErr := make(chan error)
go func() { go func() {
readerErr <- c.runReader() 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.SetReadDeadline(time.Now().Add(time.Duration(c.readTimeout)))
c.nconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout))) c.nconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout)))
conn, u, publish, err := rtmp.NewServerConn(c.nconn) conn, u, publish, err := rtmp.NewServerConn(c.nconn)
@ -203,52 +161,52 @@ func (c *rtmpConn) runReader() error {
return c.runPublish(conn, u) 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) pathName, query, rawQuery := pathNameAndQuery(u)
res := c.pathManager.addReader(pathAddReaderReq{ res := c.pathManager.AddReader(defs.PathAddReaderReq{
author: c, Author: c,
accessRequest: pathAccessRequest{ AccessRequest: defs.PathAccessRequest{
name: pathName, Name: pathName,
query: rawQuery, Query: rawQuery,
ip: c.ip(), IP: c.ip(),
user: query.Get("user"), User: query.Get("user"),
pass: query.Get("pass"), Pass: query.Get("pass"),
proto: authProtocolRTMP, Proto: defs.AuthProtocolRTMP,
id: &c.uuid, ID: &c.uuid,
}, },
}) })
if res.err != nil { if res.Err != nil {
if terr, ok := res.err.(*errAuthentication); ok { if terr, ok := res.Err.(*defs.ErrAuthentication); ok {
// wait some seconds to stop brute force attacks // wait some seconds to stop brute force attacks
<-time.After(rtmpPauseAfterAuthError) <-time.After(rtmpPauseAfterAuthError)
return terr 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.mutex.Lock()
c.state = rtmpConnStateRead c.state = connStateRead
c.pathName = pathName c.pathName = pathName
c.mutex.Unlock() c.mutex.Unlock()
writer := asyncwriter.New(c.writeQueueSize, c) writer := asyncwriter.New(c.writeQueueSize, c)
defer res.stream.RemoveReader(writer) defer res.Stream.RemoveReader(writer)
var w *rtmp.Writer var w *rtmp.Writer
videoFormat := c.setupVideo( videoFormat := c.setupVideo(
&w, &w,
res.stream, res.Stream,
writer) writer)
audioFormat := c.setupAudio( audioFormat := c.setupAudio(
&w, &w,
res.stream, res.Stream,
writer) writer)
if videoFormat == nil && audioFormat == nil { 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", 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{ onUnreadHook := hooks.OnRead(hooks.OnReadParams{
Logger: c, Logger: c,
ExternalCmdPool: c.externalCmdPool, ExternalCmdPool: c.externalCmdPool,
Conf: res.path.safeConf(), Conf: res.Path.SafeConf(),
ExternalCmdEnv: res.path.externalCmdEnv(), ExternalCmdEnv: res.Path.ExternalCmdEnv(),
Reader: c.APISourceDescribe(), Reader: c.APISourceDescribe(),
Query: rawQuery, 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, w **rtmp.Writer,
stream *stream.Stream, stream *stream.Stream,
writer *asyncwriter.Writer, writer *asyncwriter.Writer,
@ -359,7 +317,7 @@ func (c *rtmpConn) setupVideo(
return nil return nil
} }
func (c *rtmpConn) setupAudio( func (c *conn) setupAudio(
w **rtmp.Writer, w **rtmp.Writer,
stream *stream.Stream, stream *stream.Stream,
writer *asyncwriter.Writer, writer *asyncwriter.Writer,
@ -432,36 +390,36 @@ func (c *rtmpConn) setupAudio(
return nil 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) pathName, query, rawQuery := pathNameAndQuery(u)
res := c.pathManager.addPublisher(pathAddPublisherReq{ res := c.pathManager.AddPublisher(defs.PathAddPublisherReq{
author: c, Author: c,
accessRequest: pathAccessRequest{ AccessRequest: defs.PathAccessRequest{
name: pathName, Name: pathName,
query: rawQuery, Query: rawQuery,
publish: true, Publish: true,
ip: c.ip(), IP: c.ip(),
user: query.Get("user"), User: query.Get("user"),
pass: query.Get("pass"), Pass: query.Get("pass"),
proto: authProtocolRTMP, Proto: defs.AuthProtocolRTMP,
id: &c.uuid, ID: &c.uuid,
}, },
}) })
if res.err != nil { if res.Err != nil {
if terr, ok := res.err.(*errAuthentication); ok { if terr, ok := res.Err.(*defs.ErrAuthentication); ok {
// wait some seconds to stop brute force attacks // wait some seconds to stop brute force attacks
<-time.After(rtmpPauseAfterAuthError) <-time.After(rtmpPauseAfterAuthError)
return terr 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.mutex.Lock()
c.state = rtmpConnStatePublish c.state = connStatePublish
c.pathName = pathName c.pathName = pathName
c.mutex.Unlock() c.mutex.Unlock()
@ -566,16 +524,16 @@ func (c *rtmpConn) runPublish(conn *rtmp.Conn, u *url.URL) error {
} }
} }
rres := res.path.startPublisher(pathStartPublisherReq{ rres := res.Path.StartPublisher(defs.PathStartPublisherReq{
author: c, Author: c,
desc: &description.Session{Medias: medias}, Desc: &description.Session{Medias: medias},
generateRTPPackets: true, GenerateRTPPackets: true,
}) })
if rres.err != nil { if rres.Err != nil {
return rres.err return rres.Err
} }
stream = rres.stream stream = rres.Stream
// disable write deadline to allow outgoing acknowledges // disable write deadline to allow outgoing acknowledges
c.nconn.SetWriteDeadline(time.Time{}) c.nconn.SetWriteDeadline(time.Time{})
@ -589,8 +547,8 @@ func (c *rtmpConn) runPublish(conn *rtmp.Conn, u *url.URL) error {
} }
} }
// apiReaderDescribe implements reader. // APIReaderDescribe implements reader.
func (c *rtmpConn) apiReaderDescribe() defs.APIPathSourceOrReader { func (c *conn) APIReaderDescribe() defs.APIPathSourceOrReader {
return defs.APIPathSourceOrReader{ return defs.APIPathSourceOrReader{
Type: func() string { Type: func() string {
if c.isTLS { if c.isTLS {
@ -603,11 +561,11 @@ func (c *rtmpConn) apiReaderDescribe() defs.APIPathSourceOrReader {
} }
// APISourceDescribe implements source. // APISourceDescribe implements source.
func (c *rtmpConn) APISourceDescribe() defs.APIPathSourceOrReader { func (c *conn) APISourceDescribe() defs.APIPathSourceOrReader {
return c.apiReaderDescribe() return c.APIReaderDescribe()
} }
func (c *rtmpConn) apiItem() *defs.APIRTMPConn { func (c *conn) apiItem() *defs.APIRTMPConn {
c.mutex.RLock() c.mutex.RLock()
defer c.mutex.RUnlock() defer c.mutex.RUnlock()
@ -625,10 +583,10 @@ func (c *rtmpConn) apiItem() *defs.APIRTMPConn {
RemoteAddr: c.remoteAddr().String(), RemoteAddr: c.remoteAddr().String(),
State: func() defs.APIRTMPConnState { State: func() defs.APIRTMPConnState {
switch c.state { switch c.state {
case rtmpConnStateRead: case connStateRead:
return defs.APIRTMPConnStateRead return defs.APIRTMPConnStateRead
case rtmpConnStatePublish: case connStatePublish:
return defs.APIRTMPConnStatePublish return defs.APIRTMPConnStatePublish
default: default:

36
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)
}
}

308
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")
}
}

133
internal/core/rtsp_conn.go → internal/servers/rtsp/conn.go

@ -1,4 +1,4 @@
package core package rtsp
import ( import (
"fmt" "fmt"
@ -22,20 +22,19 @@ const (
rtspPauseAfterAuthError = 2 * time.Second rtspPauseAfterAuthError = 2 * time.Second
) )
type rtspConnParent interface { type conn struct {
logger.Writer isTLS bool
getISTLS() bool rtspAddress string
getServer() *gortsplib.Server authMethods []headers.AuthMethod
} readTimeout conf.StringDuration
runOnConnect string
type rtspConn struct { runOnConnectRestart bool
isTLS bool runOnDisconnect string
rtspAddress string externalCmdPool *externalcmd.Pool
authMethods []headers.AuthMethod pathManager defs.PathManager
readTimeout conf.StringDuration rconn *gortsplib.ServerConn
pathManager *pathManager rserver *gortsplib.Server
rconn *gortsplib.ServerConn parent *Server
parent rtspConnParent
uuid uuid.UUID uuid uuid.UUID
created time.Time created time.Time
@ -44,92 +43,70 @@ type rtspConn struct {
authFailures int authFailures int
} }
func newRTSPConn( func (c *conn) initialize() {
isTLS bool, c.uuid = uuid.New()
rtspAddress string, c.created = time.Now()
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(),
}
c.Log(logger.Info, "opened") c.Log(logger.Info, "opened")
desc := defs.APIPathSourceOrReader{ desc := defs.APIPathSourceOrReader{
Type: func() string { Type: func() string {
if isTLS { if c.isTLS {
return "rtspsConn" return "rtspsConn"
} }
return "rtspConn" return "conn"
}(), }(),
ID: c.uuid.String(), ID: c.uuid.String(),
} }
c.onDisconnectHook = hooks.OnConnect(hooks.OnConnectParams{ c.onDisconnectHook = hooks.OnConnect(hooks.OnConnectParams{
Logger: c, Logger: c,
ExternalCmdPool: externalCmdPool, ExternalCmdPool: c.externalCmdPool,
RunOnConnect: runOnConnect, RunOnConnect: c.runOnConnect,
RunOnConnectRestart: runOnConnectRestart, RunOnConnectRestart: c.runOnConnectRestart,
RunOnDisconnect: runOnDisconnect, RunOnDisconnect: c.runOnDisconnect,
RTSPAddress: rtspAddress, RTSPAddress: c.rtspAddress,
Desc: desc, 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...)...) c.parent.Log(level, "[conn %v] "+format, append([]interface{}{c.rconn.NetConn().RemoteAddr()}, args...)...)
} }
// Conn returns the RTSP connection. // Conn returns the RTSP connection.
func (c *rtspConn) Conn() *gortsplib.ServerConn { func (c *conn) Conn() *gortsplib.ServerConn {
return c.rconn return c.rconn
} }
func (c *rtspConn) remoteAddr() net.Addr { func (c *conn) remoteAddr() net.Addr {
return c.rconn.NetConn().RemoteAddr() 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 return c.rconn.NetConn().RemoteAddr().(*net.TCPAddr).IP
} }
// onClose is called by rtspServer. // 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.Log(logger.Info, "closed: %v", err)
c.onDisconnectHook() c.onDisconnectHook()
} }
// onRequest is called by rtspServer. // 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) c.Log(logger.Debug, "[c->s] %v", req)
} }
// OnResponse is called by rtspServer. // 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) c.Log(logger.Debug, "[s->c] %v", res)
} }
// onDescribe is called by rtspServer. // onDescribe is called by rtspServer.
func (c *rtspConn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
) (*base.Response, *gortsplib.ServerStream, error) { ) (*base.Response, *gortsplib.ServerStream, error) {
if len(ctx.Path) == 0 || ctx.Path[0] != '/' { if len(ctx.Path) == 0 || ctx.Path[0] != '/' {
return &base.Response{ return &base.Response{
@ -148,50 +125,50 @@ func (c *rtspConn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
} }
} }
res := c.pathManager.describe(pathDescribeReq{ res := c.pathManager.Describe(defs.PathDescribeReq{
accessRequest: pathAccessRequest{ AccessRequest: defs.PathAccessRequest{
name: ctx.Path, Name: ctx.Path,
query: ctx.Query, Query: ctx.Query,
ip: c.ip(), IP: c.ip(),
proto: authProtocolRTSP, Proto: defs.AuthProtocolRTSP,
id: &c.uuid, ID: &c.uuid,
rtspRequest: ctx.Request, RTSPRequest: ctx.Request,
rtspNonce: c.authNonce, RTSPNonce: c.authNonce,
}, },
}) })
if res.err != nil { if res.Err != nil {
switch terr := res.err.(type) { switch terr := res.Err.(type) {
case *errAuthentication: case *defs.ErrAuthentication:
res, err := c.handleAuthError(terr) res, err := c.handleAuthError(terr)
return res, nil, err return res, nil, err
case errPathNoOnePublishing: case defs.ErrPathNoOnePublishing:
return &base.Response{ return &base.Response{
StatusCode: base.StatusNotFound, StatusCode: base.StatusNotFound,
}, nil, res.err }, nil, res.Err
default: default:
return &base.Response{ return &base.Response{
StatusCode: base.StatusBadRequest, StatusCode: base.StatusBadRequest,
}, nil, res.err }, nil, res.Err
} }
} }
if res.redirect != "" { if res.Redirect != "" {
return &base.Response{ return &base.Response{
StatusCode: base.StatusMovedPermanently, StatusCode: base.StatusMovedPermanently,
Header: base.Header{ Header: base.Header{
"Location": base.HeaderValue{res.redirect}, "Location": base.HeaderValue{res.Redirect},
}, },
}, nil, nil }, nil, nil
} }
var stream *gortsplib.ServerStream var stream *gortsplib.ServerStream
if !c.parent.getISTLS() { if !c.isTLS {
stream = res.stream.RTSPStream(c.parent.getServer()) stream = res.Stream.RTSPStream(c.rserver)
} else { } else {
stream = res.stream.RTSPSStream(c.parent.getServer()) stream = res.Stream.RTSPSStream(c.rserver)
} }
return &base.Response{ return &base.Response{
@ -199,7 +176,7 @@ func (c *rtspConn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
}, stream, nil }, stream, nil
} }
func (c *rtspConn) handleAuthError(authErr error) (*base.Response, error) { func (c *conn) handleAuthError(authErr error) (*base.Response, error) {
c.authFailures++ c.authFailures++
// VLC with login prompt sends 4 requests: // VLC with login prompt sends 4 requests:
@ -225,7 +202,7 @@ func (c *rtspConn) handleAuthError(authErr error) (*base.Response, error) {
}, authErr }, authErr
} }
func (c *rtspConn) apiItem() *defs.APIRTSPConn { func (c *conn) apiItem() *defs.APIRTSPConn {
return &defs.APIRTSPConn{ return &defs.APIRTSPConn{
ID: c.uuid, ID: c.uuid,
Created: c.created, Created: c.created,

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

230
internal/core/rtsp_session.go → internal/servers/rtsp/session.go

@ -1,4 +1,4 @@
package core package rtsp
import ( import (
"encoding/hex" "encoding/hex"
@ -21,29 +21,19 @@ import (
"github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/stream"
) )
type rtspSessionPathManager interface { type session struct {
addPublisher(req pathAddPublisherReq) pathAddPublisherRes
addReader(req pathAddReaderReq) pathAddReaderRes
}
type rtspSessionParent interface {
logger.Writer
getISTLS() bool
getServer() *gortsplib.Server
}
type rtspSession struct {
isTLS bool isTLS bool
protocols map[conf.Protocol]struct{} protocols map[conf.Protocol]struct{}
session *gortsplib.ServerSession rsession *gortsplib.ServerSession
author *gortsplib.ServerConn rconn *gortsplib.ServerConn
rserver *gortsplib.Server
externalCmdPool *externalcmd.Pool externalCmdPool *externalcmd.Pool
pathManager rtspSessionPathManager pathManager defs.PathManager
parent rtspSessionParent parent *Server
uuid uuid.UUID uuid uuid.UUID
created time.Time created time.Time
path *path path defs.Path
stream *stream.Stream stream *stream.Stream
onUnreadHook func() onUnreadHook func()
mutex sync.Mutex mutex sync.Mutex
@ -54,61 +44,43 @@ type rtspSession struct {
writeErrLogger logger.Writer writeErrLogger logger.Writer
} }
func newRTSPSession( func (s *session) initialize() {
isTLS bool, s.uuid = uuid.New()
protocols map[conf.Protocol]struct{}, s.created = time.Now()
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(),
}
s.decodeErrLogger = logger.NewLimitedLogger(s) s.decodeErrLogger = logger.NewLimitedLogger(s)
s.writeErrLogger = logger.NewLimitedLogger(s) s.writeErrLogger = logger.NewLimitedLogger(s)
s.Log(logger.Info, "created by %v", s.author.NetConn().RemoteAddr()) s.Log(logger.Info, "created by %v", s.rconn.NetConn().RemoteAddr())
return s
} }
// Close closes a Session. // Close closes a Session.
func (s *rtspSession) close() { func (s *session) Close() {
s.session.Close() s.rsession.Close()
} }
func (s *rtspSession) remoteAddr() net.Addr { func (s *session) remoteAddr() net.Addr {
return s.author.NetConn().RemoteAddr() 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]) id := hex.EncodeToString(s.uuid[:4])
s.parent.Log(level, "[session %s] "+format, append([]interface{}{id}, args...)...) s.parent.Log(level, "[session %s] "+format, append([]interface{}{id}, args...)...)
} }
// onClose is called by rtspServer. // onClose is called by rtspServer.
func (s *rtspSession) onClose(err error) { func (s *session) onClose(err error) {
if s.session.State() == gortsplib.ServerSessionStatePlay { if s.rsession.State() == gortsplib.ServerSessionStatePlay {
s.onUnreadHook() s.onUnreadHook()
} }
switch s.session.State() { switch s.rsession.State() {
case gortsplib.ServerSessionStatePrePlay, gortsplib.ServerSessionStatePlay: case gortsplib.ServerSessionStatePrePlay, gortsplib.ServerSessionStatePlay:
s.path.removeReader(pathRemoveReaderReq{author: s}) s.path.RemoveReader(defs.PathRemoveReaderReq{Author: s})
case gortsplib.ServerSessionStatePreRecord, gortsplib.ServerSessionStateRecord: case gortsplib.ServerSessionStatePreRecord, gortsplib.ServerSessionStateRecord:
s.path.removePublisher(pathRemovePublisherReq{author: s}) s.path.RemovePublisher(defs.PathRemovePublisherReq{Author: s})
} }
s.path = nil s.path = nil
@ -118,7 +90,7 @@ func (s *rtspSession) onClose(err error) {
} }
// onAnnounce is called by rtspServer. // 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] != '/' { if len(ctx.Path) == 0 || ctx.Path[0] != '/' {
return &base.Response{ return &base.Response{
StatusCode: base.StatusBadRequest, StatusCode: base.StatusBadRequest,
@ -136,34 +108,34 @@ func (s *rtspSession) onAnnounce(c *rtspConn, ctx *gortsplib.ServerHandlerOnAnno
} }
} }
res := s.pathManager.addPublisher(pathAddPublisherReq{ res := s.pathManager.AddPublisher(defs.PathAddPublisherReq{
author: s, Author: s,
accessRequest: pathAccessRequest{ AccessRequest: defs.PathAccessRequest{
name: ctx.Path, Name: ctx.Path,
query: ctx.Query, Query: ctx.Query,
publish: true, Publish: true,
ip: c.ip(), IP: c.ip(),
proto: authProtocolRTSP, Proto: defs.AuthProtocolRTSP,
id: &c.uuid, ID: &c.uuid,
rtspRequest: ctx.Request, RTSPRequest: ctx.Request,
rtspBaseURL: nil, RTSPBaseURL: nil,
rtspNonce: c.authNonce, RTSPNonce: c.authNonce,
}, },
}) })
if res.err != nil { if res.Err != nil {
switch terr := res.err.(type) { switch terr := res.Err.(type) {
case *errAuthentication: case *defs.ErrAuthentication:
return c.handleAuthError(terr) return c.handleAuthError(terr)
default: default:
return &base.Response{ return &base.Response{
StatusCode: base.StatusBadRequest, StatusCode: base.StatusBadRequest,
}, res.err }, res.Err
} }
} }
s.path = res.path s.path = res.Path
s.mutex.Lock() s.mutex.Lock()
s.state = gortsplib.ServerSessionStatePreRecord s.state = gortsplib.ServerSessionStatePreRecord
@ -176,7 +148,7 @@ func (s *rtspSession) onAnnounce(c *rtspConn, ctx *gortsplib.ServerHandlerOnAnno
} }
// onSetup is called by rtspServer. // 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) { ) (*base.Response, *gortsplib.ServerStream, error) {
if len(ctx.Path) == 0 || ctx.Path[0] != '/' { if len(ctx.Path) == 0 || ctx.Path[0] != '/' {
return &base.Response{ 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 case gortsplib.ServerSessionStateInitial, gortsplib.ServerSessionStatePrePlay: // play
baseURL := &base.URL{ baseURL := &base.URL{
Scheme: ctx.Request.URL.Scheme, Scheme: ctx.Request.URL.Scheme,
@ -222,40 +194,40 @@ func (s *rtspSession) onSetup(c *rtspConn, ctx *gortsplib.ServerHandlerOnSetupCt
} }
} }
res := s.pathManager.addReader(pathAddReaderReq{ res := s.pathManager.AddReader(defs.PathAddReaderReq{
author: s, Author: s,
accessRequest: pathAccessRequest{ AccessRequest: defs.PathAccessRequest{
name: ctx.Path, Name: ctx.Path,
query: ctx.Query, Query: ctx.Query,
ip: c.ip(), IP: c.ip(),
proto: authProtocolRTSP, Proto: defs.AuthProtocolRTSP,
id: &c.uuid, ID: &c.uuid,
rtspRequest: ctx.Request, RTSPRequest: ctx.Request,
rtspBaseURL: baseURL, RTSPBaseURL: baseURL,
rtspNonce: c.authNonce, RTSPNonce: c.authNonce,
}, },
}) })
if res.err != nil { if res.Err != nil {
switch terr := res.err.(type) { switch terr := res.Err.(type) {
case *errAuthentication: case *defs.ErrAuthentication:
res, err := c.handleAuthError(terr) res, err := c.handleAuthError(terr)
return res, nil, err return res, nil, err
case errPathNoOnePublishing: case defs.ErrPathNoOnePublishing:
return &base.Response{ return &base.Response{
StatusCode: base.StatusNotFound, StatusCode: base.StatusNotFound,
}, nil, res.err }, nil, res.Err
default: default:
return &base.Response{ return &base.Response{
StatusCode: base.StatusBadRequest, StatusCode: base.StatusBadRequest,
}, nil, res.err }, nil, res.Err
} }
} }
s.path = res.path s.path = res.Path
s.stream = res.stream s.stream = res.Stream
s.mutex.Lock() s.mutex.Lock()
s.state = gortsplib.ServerSessionStatePrePlay s.state = gortsplib.ServerSessionStatePrePlay
@ -263,10 +235,10 @@ func (s *rtspSession) onSetup(c *rtspConn, ctx *gortsplib.ServerHandlerOnSetupCt
s.mutex.Unlock() s.mutex.Unlock()
var stream *gortsplib.ServerStream var stream *gortsplib.ServerStream
if !s.parent.getISTLS() { if !s.isTLS {
stream = res.stream.RTSPStream(s.parent.getServer()) stream = res.Stream.RTSPStream(s.rserver)
} else { } else {
stream = res.stream.RTSPSStream(s.parent.getServer()) stream = res.Stream.RTSPSStream(s.rserver)
} }
return &base.Response{ return &base.Response{
@ -281,27 +253,27 @@ func (s *rtspSession) onSetup(c *rtspConn, ctx *gortsplib.ServerHandlerOnSetupCt
} }
// onPlay is called by rtspServer. // 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) 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.Log(logger.Info, "is reading from path '%s', with %s, %s",
s.path.name, s.path.Name(),
s.session.SetuppedTransport(), s.rsession.SetuppedTransport(),
mediaInfo(s.session.SetuppedMedias())) defs.MediasInfo(s.rsession.SetuppedMedias()))
s.onUnreadHook = hooks.OnRead(hooks.OnReadParams{ s.onUnreadHook = hooks.OnRead(hooks.OnReadParams{
Logger: s, Logger: s,
ExternalCmdPool: s.externalCmdPool, ExternalCmdPool: s.externalCmdPool,
Conf: s.path.safeConf(), Conf: s.path.SafeConf(),
ExternalCmdEnv: s.path.externalCmdEnv(), ExternalCmdEnv: s.path.ExternalCmdEnv(),
Reader: s.apiReaderDescribe(), Reader: s.APIReaderDescribe(),
Query: s.session.SetuppedQuery(), Query: s.rsession.SetuppedQuery(),
}) })
s.mutex.Lock() s.mutex.Lock()
s.state = gortsplib.ServerSessionStatePlay s.state = gortsplib.ServerSessionStatePlay
s.transport = s.session.SetuppedTransport() s.transport = s.rsession.SetuppedTransport()
s.mutex.Unlock() s.mutex.Unlock()
} }
@ -312,39 +284,39 @@ func (s *rtspSession) onPlay(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Respons
} }
// onRecord is called by rtspServer. // onRecord is called by rtspServer.
func (s *rtspSession) onRecord(_ *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) { func (s *session) onRecord(_ *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) {
res := s.path.startPublisher(pathStartPublisherReq{ res := s.path.StartPublisher(defs.PathStartPublisherReq{
author: s, Author: s,
desc: s.session.AnnouncedDescription(), Desc: s.rsession.AnnouncedDescription(),
generateRTPPackets: false, GenerateRTPPackets: false,
}) })
if res.err != nil { if res.Err != nil {
return &base.Response{ return &base.Response{
StatusCode: base.StatusBadRequest, 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 { for _, forma := range medi.Formats {
cmedi := medi cmedi := medi
cforma := forma cforma := forma
s.session.OnPacketRTP(cmedi, cforma, func(pkt *rtp.Packet) { s.rsession.OnPacketRTP(cmedi, cforma, func(pkt *rtp.Packet) {
pts, ok := s.session.PacketPTS(cmedi, pkt) pts, ok := s.rsession.PacketPTS(cmedi, pkt)
if !ok { if !ok {
return return
} }
res.stream.WriteRTPPacket(cmedi, cforma, pkt, time.Now(), pts) res.Stream.WriteRTPPacket(cmedi, cforma, pkt, time.Now(), pts)
}) })
} }
} }
s.mutex.Lock() s.mutex.Lock()
s.state = gortsplib.ServerSessionStateRecord s.state = gortsplib.ServerSessionStateRecord
s.transport = s.session.SetuppedTransport() s.transport = s.rsession.SetuppedTransport()
s.mutex.Unlock() s.mutex.Unlock()
return &base.Response{ return &base.Response{
@ -353,8 +325,8 @@ func (s *rtspSession) onRecord(_ *gortsplib.ServerHandlerOnRecordCtx) (*base.Res
} }
// onPause is called by rtspServer. // onPause is called by rtspServer.
func (s *rtspSession) onPause(_ *gortsplib.ServerHandlerOnPauseCtx) (*base.Response, error) { func (s *session) onPause(_ *gortsplib.ServerHandlerOnPauseCtx) (*base.Response, error) {
switch s.session.State() { switch s.rsession.State() {
case gortsplib.ServerSessionStatePlay: case gortsplib.ServerSessionStatePlay:
s.onUnreadHook() s.onUnreadHook()
@ -363,7 +335,7 @@ func (s *rtspSession) onPause(_ *gortsplib.ServerHandlerOnPauseCtx) (*base.Respo
s.mutex.Unlock() s.mutex.Unlock()
case gortsplib.ServerSessionStateRecord: case gortsplib.ServerSessionStateRecord:
s.path.stopPublisher(pathStopPublisherReq{author: s}) s.path.StopPublisher(defs.PathStopPublisherReq{Author: s})
s.mutex.Lock() s.mutex.Lock()
s.state = gortsplib.ServerSessionStatePreRecord s.state = gortsplib.ServerSessionStatePreRecord
@ -375,8 +347,8 @@ func (s *rtspSession) onPause(_ *gortsplib.ServerHandlerOnPauseCtx) (*base.Respo
}, nil }, nil
} }
// apiReaderDescribe implements reader. // APIReaderDescribe implements reader.
func (s *rtspSession) apiReaderDescribe() defs.APIPathSourceOrReader { func (s *session) APIReaderDescribe() defs.APIPathSourceOrReader {
return defs.APIPathSourceOrReader{ return defs.APIPathSourceOrReader{
Type: func() string { Type: func() string {
if s.isTLS { if s.isTLS {
@ -389,26 +361,26 @@ func (s *rtspSession) apiReaderDescribe() defs.APIPathSourceOrReader {
} }
// APISourceDescribe implements source. // APISourceDescribe implements source.
func (s *rtspSession) APISourceDescribe() defs.APIPathSourceOrReader { func (s *session) APISourceDescribe() defs.APIPathSourceOrReader {
return s.apiReaderDescribe() return s.APIReaderDescribe()
} }
// onPacketLost is called by rtspServer. // 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()) s.decodeErrLogger.Log(logger.Warn, ctx.Error.Error())
} }
// onDecodeError is called by rtspServer. // 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()) s.decodeErrLogger.Log(logger.Warn, ctx.Error.Error())
} }
// onStreamWriteError is called by rtspServer. // 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()) s.writeErrLogger.Log(logger.Warn, ctx.Error.Error())
} }
func (s *rtspSession) apiItem() *defs.APIRTSPSession { func (s *session) apiItem() *defs.APIRTSPSession {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
@ -436,7 +408,7 @@ func (s *rtspSession) apiItem() *defs.APIRTSPSession {
v := s.transport.String() v := s.transport.String()
return &v return &v
}(), }(),
BytesReceived: s.session.BytesReceived(), BytesReceived: s.rsession.BytesReceived(),
BytesSent: s.session.BytesSent(), BytesSent: s.rsession.BytesSent(),
} }
} }

212
internal/core/srt_conn.go → internal/servers/srt/conn.go

@ -1,4 +1,4 @@
package core package srt
import ( import (
"bufio" "bufio"
@ -12,7 +12,7 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/description"
mcmpegts "github.com/bluenviron/mediacommon/pkg/formats/mpegts" mcmpegts "github.com/bluenviron/mediacommon/pkg/formats/mpegts"
"github.com/datarhei/gosrt" srt "github.com/datarhei/gosrt"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/bluenviron/mediamtx/internal/asyncwriter" "github.com/bluenviron/mediamtx/internal/asyncwriter"
@ -42,24 +42,15 @@ func srtCheckPassphrase(connReq srt.ConnRequest, passphrase string) error {
return nil return nil
} }
type srtConnState int type connState int
const ( const (
srtConnStateRead srtConnState = iota + 1 connStateRead connState = iota + 1
srtConnStatePublish connStatePublish
) )
type srtConnPathManager interface { type conn struct {
addReader(req pathAddReaderReq) pathAddReaderRes parentCtx context.Context
addPublisher(req pathAddPublisherReq) pathAddPublisherRes
}
type srtConnParent interface {
logger.Writer
closeConn(*srtConn)
}
type srtConn struct {
rtspAddress string rtspAddress string
readTimeout conf.StringDuration readTimeout conf.StringDuration
writeTimeout conf.StringDuration writeTimeout conf.StringDuration
@ -71,15 +62,15 @@ type srtConn struct {
runOnDisconnect string runOnDisconnect string
wg *sync.WaitGroup wg *sync.WaitGroup
externalCmdPool *externalcmd.Pool externalCmdPool *externalcmd.Pool
pathManager srtConnPathManager pathManager defs.PathManager
parent srtConnParent parent *Server
ctx context.Context ctx context.Context
ctxCancel func() ctxCancel func()
created time.Time created time.Time
uuid uuid.UUID uuid uuid.UUID
mutex sync.RWMutex mutex sync.RWMutex
state srtConnState state connState
pathName string pathName string
sconn srt.Conn sconn srt.Conn
@ -87,67 +78,34 @@ type srtConn struct {
chSetConn chan srt.Conn chSetConn chan srt.Conn
} }
func newSRTConn( func (c *conn) initialize() {
parentCtx context.Context, c.ctx, c.ctxCancel = context.WithCancel(c.parentCtx)
rtspAddress string,
readTimeout conf.StringDuration, c.created = time.Now()
writeTimeout conf.StringDuration, c.uuid = uuid.New()
writeQueueSize int, c.chNew = make(chan srtNewConnReq)
udpMaxPayloadSize int, c.chSetConn = make(chan srt.Conn)
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),
}
c.Log(logger.Info, "opened") c.Log(logger.Info, "opened")
c.wg.Add(1) c.wg.Add(1)
go c.run() go c.run()
return c
} }
func (c *srtConn) close() { func (c *conn) Close() {
c.ctxCancel() 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...)...) 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 return c.connReq.RemoteAddr().(*net.UDPAddr).IP
} }
func (c *srtConn) run() { //nolint:dupl func (c *conn) run() { //nolint:dupl
defer c.wg.Done() defer c.wg.Done()
onDisconnectHook := hooks.OnConnect(hooks.OnConnectParams{ onDisconnectHook := hooks.OnConnect(hooks.OnConnectParams{
@ -157,7 +115,7 @@ func (c *srtConn) run() { //nolint:dupl
RunOnConnectRestart: c.runOnConnectRestart, RunOnConnectRestart: c.runOnConnectRestart,
RunOnDisconnect: c.runOnDisconnect, RunOnDisconnect: c.runOnDisconnect,
RTSPAddress: c.rtspAddress, RTSPAddress: c.rtspAddress,
Desc: c.apiReaderDescribe(), Desc: c.APIReaderDescribe(),
}) })
defer onDisconnectHook() defer onDisconnectHook()
@ -170,7 +128,7 @@ func (c *srtConn) run() { //nolint:dupl
c.Log(logger.Info, "closed: %v", err) c.Log(logger.Info, "closed: %v", err)
} }
func (c *srtConn) runInner() error { func (c *conn) runInner() error {
var req srtNewConnReq var req srtNewConnReq
select { select {
case req = <-c.chNew: case req = <-c.chNew:
@ -187,7 +145,7 @@ func (c *srtConn) runInner() error {
return err return err
} }
func (c *srtConn) runInner2(req srtNewConnReq) (bool, error) { func (c *conn) runInner2(req srtNewConnReq) (bool, error) {
parts := strings.Split(req.connReq.StreamId(), ":") parts := strings.Split(req.connReq.StreamId(), ":")
if (len(parts) < 2 || len(parts) > 5) || (parts[0] != "read" && parts[0] != "publish") { if (len(parts) < 2 || len(parts) > 5) || (parts[0] != "read" && parts[0] != "publish") {
return false, fmt.Errorf("invalid streamid '%s':"+ 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) return c.runRead(req, pathName, user, pass, query)
} }
func (c *srtConn) runPublish(req srtNewConnReq, pathName string, user string, pass string, query string) (bool, error) { func (c *conn) runPublish(req srtNewConnReq, pathName string, user string, pass string, query string) (bool, error) {
res := c.pathManager.addPublisher(pathAddPublisherReq{ res := c.pathManager.AddPublisher(defs.PathAddPublisherReq{
author: c, Author: c,
accessRequest: pathAccessRequest{ AccessRequest: defs.PathAccessRequest{
name: pathName, Name: pathName,
ip: c.ip(), IP: c.ip(),
publish: true, Publish: true,
user: user, User: user,
pass: pass, Pass: pass,
proto: authProtocolSRT, Proto: defs.AuthProtocolSRT,
id: &c.uuid, ID: &c.uuid,
query: query, Query: query,
}, },
}) })
if res.err != nil { if res.Err != nil {
if terr, ok := res.err.(*errAuthentication); ok { if terr, ok := res.Err.(*defs.ErrAuthentication); ok {
// TODO: re-enable. Currently this freezes the listener. // TODO: re-enable. Currently this freezes the listener.
// wait some seconds to stop brute force attacks // wait some seconds to stop brute force attacks
// <-time.After(srtPauseAfterAuthError) // <-time.After(srtPauseAfterAuthError)
return false, terr 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 { if err != nil {
return false, err return false, err
} }
@ -258,14 +216,14 @@ func (c *srtConn) runPublish(req srtNewConnReq, pathName string, user string, pa
} }
c.mutex.Lock() c.mutex.Lock()
c.state = srtConnStatePublish c.state = connStatePublish
c.pathName = pathName c.pathName = pathName
c.sconn = sconn c.sconn = sconn
c.mutex.Unlock() c.mutex.Unlock()
readerErr := make(chan error) readerErr := make(chan error)
go func() { go func() {
readerErr <- c.runPublishReader(sconn, res.path) readerErr <- c.runPublishReader(sconn, res.Path)
}() }()
select { 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))) sconn.SetReadDeadline(time.Now().Add(time.Duration(c.readTimeout)))
r, err := mcmpegts.NewReader(mcmpegts.NewBufferedReader(sconn)) r, err := mcmpegts.NewReader(mcmpegts.NewBufferedReader(sconn))
if err != nil { if err != nil {
@ -300,16 +258,16 @@ func (c *srtConn) runPublishReader(sconn srt.Conn, path *path) error {
return err return err
} }
rres := path.startPublisher(pathStartPublisherReq{ rres := path.StartPublisher(defs.PathStartPublisherReq{
author: c, Author: c,
desc: &description.Session{Medias: medias}, Desc: &description.Session{Medias: medias},
generateRTPPackets: true, GenerateRTPPackets: true,
}) })
if rres.err != nil { if rres.Err != nil {
return rres.err return rres.Err
} }
stream = rres.stream stream = rres.Stream
for { for {
err := r.Read() 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) { func (c *conn) runRead(req srtNewConnReq, pathName string, user string, pass string, query string) (bool, error) {
res := c.pathManager.addReader(pathAddReaderReq{ res := c.pathManager.AddReader(defs.PathAddReaderReq{
author: c, Author: c,
accessRequest: pathAccessRequest{ AccessRequest: defs.PathAccessRequest{
name: pathName, Name: pathName,
ip: c.ip(), IP: c.ip(),
user: user, User: user,
pass: pass, Pass: pass,
proto: authProtocolSRT, Proto: defs.AuthProtocolSRT,
id: &c.uuid, ID: &c.uuid,
query: query, Query: query,
}, },
}) })
if res.err != nil { if res.Err != nil {
if terr, ok := res.err.(*errAuthentication); ok { if terr, ok := res.Err.(*defs.ErrAuthentication); ok {
// TODO: re-enable. Currently this freezes the listener. // TODO: re-enable. Currently this freezes the listener.
// wait some seconds to stop brute force attacks // wait some seconds to stop brute force attacks
// <-time.After(srtPauseAfterAuthError) // <-time.After(srtPauseAfterAuthError)
return false, terr 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 { if err != nil {
return false, err return false, err
} }
@ -357,31 +315,31 @@ func (c *srtConn) runRead(req srtNewConnReq, pathName string, user string, pass
defer sconn.Close() defer sconn.Close()
c.mutex.Lock() c.mutex.Lock()
c.state = srtConnStateRead c.state = connStateRead
c.pathName = pathName c.pathName = pathName
c.sconn = sconn c.sconn = sconn
c.mutex.Unlock() c.mutex.Unlock()
writer := asyncwriter.New(c.writeQueueSize, c) writer := asyncwriter.New(c.writeQueueSize, c)
defer res.stream.RemoveReader(writer) defer res.Stream.RemoveReader(writer)
bw := bufio.NewWriterSize(sconn, srtMaxPayloadSize(c.udpMaxPayloadSize)) 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 { if err != nil {
return true, err return true, err
} }
c.Log(logger.Info, "is reading from path '%s', %s", 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{ onUnreadHook := hooks.OnRead(hooks.OnReadParams{
Logger: c, Logger: c,
ExternalCmdPool: c.externalCmdPool, ExternalCmdPool: c.externalCmdPool,
Conf: res.path.safeConf(), Conf: res.Path.SafeConf(),
ExternalCmdEnv: res.path.externalCmdEnv(), ExternalCmdEnv: res.Path.ExternalCmdEnv(),
Reader: c.apiReaderDescribe(), Reader: c.APIReaderDescribe(),
Query: query, Query: query,
}) })
defer onUnreadHook() 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 req.res <- c
select { select {
@ -414,7 +372,7 @@ func (c *srtConn) exchangeRequestWithConn(req srtNewConnReq) (srt.Conn, error) {
} }
// new is called by srtListener through srtServer. // new is called by srtListener through srtServer.
func (c *srtConn) new(req srtNewConnReq) *srtConn { func (c *conn) new(req srtNewConnReq) *conn {
select { select {
case c.chNew <- req: case c.chNew <- req:
return <-req.res return <-req.res
@ -425,15 +383,15 @@ func (c *srtConn) new(req srtNewConnReq) *srtConn {
} }
// setConn is called by srtListener . // setConn is called by srtListener .
func (c *srtConn) setConn(sconn srt.Conn) { func (c *conn) setConn(sconn srt.Conn) {
select { select {
case c.chSetConn <- sconn: case c.chSetConn <- sconn:
case <-c.ctx.Done(): case <-c.ctx.Done():
} }
} }
// apiReaderDescribe implements reader. // APIReaderDescribe implements reader.
func (c *srtConn) apiReaderDescribe() defs.APIPathSourceOrReader { func (c *conn) APIReaderDescribe() defs.APIPathSourceOrReader {
return defs.APIPathSourceOrReader{ return defs.APIPathSourceOrReader{
Type: "srtConn", Type: "srtConn",
ID: c.uuid.String(), ID: c.uuid.String(),
@ -441,11 +399,11 @@ func (c *srtConn) apiReaderDescribe() defs.APIPathSourceOrReader {
} }
// APISourceDescribe implements source. // APISourceDescribe implements source.
func (c *srtConn) APISourceDescribe() defs.APIPathSourceOrReader { func (c *conn) APISourceDescribe() defs.APIPathSourceOrReader {
return c.apiReaderDescribe() return c.APIReaderDescribe()
} }
func (c *srtConn) apiItem() *defs.APISRTConn { func (c *conn) apiItem() *defs.APISRTConn {
c.mutex.RLock() c.mutex.RLock()
defer c.mutex.RUnlock() defer c.mutex.RUnlock()
@ -465,10 +423,10 @@ func (c *srtConn) apiItem() *defs.APISRTConn {
RemoteAddr: c.connReq.RemoteAddr().String(), RemoteAddr: c.connReq.RemoteAddr().String(),
State: func() defs.APISRTConnState { State: func() defs.APISRTConnState {
switch c.state { switch c.state {
case srtConnStateRead: case connStateRead:
return defs.APISRTConnStateRead return defs.APISRTConnStateRead
case srtConnStatePublish: case connStatePublish:
return defs.APISRTConnStatePublish return defs.APISRTConnStatePublish
default: default:

28
internal/core/srt_listener.go → internal/servers/srt/listener.go

@ -1,35 +1,23 @@
package core package srt
import ( import (
"sync" "sync"
"github.com/datarhei/gosrt" srt "github.com/datarhei/gosrt"
) )
type srtListener struct { type listener struct {
ln srt.Listener ln srt.Listener
wg *sync.WaitGroup wg *sync.WaitGroup
parent *srtServer parent *Server
} }
func newSRTListener( func (l *listener) initialize() {
ln srt.Listener,
wg *sync.WaitGroup,
parent *srtServer,
) *srtListener {
l := &srtListener{
ln: ln,
wg: wg,
parent: parent,
}
l.wg.Add(1) l.wg.Add(1)
go l.run() go l.run()
return l
} }
func (l *srtListener) run() { func (l *listener) run() {
defer l.wg.Done() defer l.wg.Done()
err := l.runInner() err := l.runInner()
@ -37,9 +25,9 @@ func (l *srtListener) run() {
l.parent.acceptError(err) l.parent.acceptError(err)
} }
func (l *srtListener) runInner() error { func (l *listener) runInner() error {
for { for {
var sconn *srtConn var sconn *conn
conn, _, err := l.ln.Accept(func(req srt.ConnRequest) srt.ConnType { conn, _, err := l.ln.Accept(func(req srt.ConnRequest) srt.ConnType {
sconn = l.parent.newConnRequest(req) sconn = l.parent.newConnRequest(req)
if sconn == nil { if sconn == nil {

310
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")
}
}

150
internal/core/webrtc_http_server.go → internal/servers/webrtc/http_server.go

@ -1,4 +1,4 @@
package core package webrtc
import ( import (
_ "embed" _ "embed"
@ -12,7 +12,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
pwebrtc "github.com/pion/webrtc/v3"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/defs"
@ -22,18 +21,18 @@ import (
"github.com/bluenviron/mediamtx/internal/restrictnetwork" "github.com/bluenviron/mediamtx/internal/restrictnetwork"
) )
//go:embed webrtc_publish_index.html //go:embed publish_index.html
var webrtcPublishIndex []byte var publishIndex []byte
//go:embed webrtc_read_index.html //go:embed read_index.html
var webrtcReadIndex []byte var readIndex []byte
var ( var (
reWHIPWHEPNoID = regexp.MustCompile("^/(.+?)/(whip|whep)$") reWHIPWHEPNoID = regexp.MustCompile("^/(.+?)/(whip|whep)$")
reWHIPWHEPWithID = 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{ ctx.JSON(statusCode, &defs.APIError{
Error: err.Error(), Error: err.Error(),
}) })
@ -50,128 +49,111 @@ func sessionLocation(publish bool, secret uuid.UUID) string {
return ret return ret
} }
type webRTCHTTPServerParent interface { type httpServer struct {
logger.Writer address string
generateICEServers() ([]pwebrtc.ICEServer, error) encryption bool
newSession(req webRTCNewSessionReq) webRTCNewSessionRes serverKey string
addSessionCandidates(req webRTCAddSessionCandidatesReq) webRTCAddSessionCandidatesRes serverCert string
deleteSession(req webRTCDeleteSessionReq) error allowOrigin string
} trustedProxies conf.IPsOrCIDRs
readTimeout conf.StringDuration
type webRTCHTTPServer struct { pathManager defs.PathManager
allowOrigin string parent *Server
pathManager *pathManager
parent webRTCHTTPServerParent
inner *httpserv.WrappedServer inner *httpserv.WrappedServer
} }
func newWebRTCHTTPServer( //nolint:dupl func (s *httpServer) initialize() error {
address string, if s.encryption {
encryption bool, if s.serverCert == "" {
serverKey string, return fmt.Errorf("server cert is missing")
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")
} }
} else { } else {
serverKey = "" s.serverKey = ""
serverCert = "" s.serverCert = ""
}
s := &webRTCHTTPServer{
allowOrigin: allowOrigin,
pathManager: pathManager,
parent: parent,
} }
router := gin.New() router := gin.New()
router.SetTrustedProxies(trustedProxies.ToTrustedProxies()) //nolint:errcheck router.SetTrustedProxies(s.trustedProxies.ToTrustedProxies()) //nolint:errcheck
router.NoRoute(s.onRequest) router.NoRoute(s.onRequest)
network, address := restrictnetwork.Restrict("tcp", address) network, address := restrictnetwork.Restrict("tcp", s.address)
var err error var err error
s.inner, err = httpserv.NewWrappedServer( s.inner, err = httpserv.NewWrappedServer(
network, network,
address, address,
time.Duration(readTimeout), time.Duration(s.readTimeout),
serverCert, s.serverCert,
serverKey, s.serverKey,
router, router,
s, s,
) )
if err != nil { 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...) s.parent.Log(level, format, args...)
} }
func (s *webRTCHTTPServer) close() { func (s *httpServer) close() {
s.inner.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() ip := ctx.ClientIP()
_, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr) _, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr)
remoteAddr := net.JoinHostPort(ip, port) remoteAddr := net.JoinHostPort(ip, port)
user, pass, hasCredentials := ctx.Request.BasicAuth() user, pass, hasCredentials := ctx.Request.BasicAuth()
res := s.pathManager.getConfForPath(pathGetConfForPathReq{ res := s.pathManager.GetConfForPath(defs.PathGetConfForPathReq{
accessRequest: pathAccessRequest{ AccessRequest: defs.PathAccessRequest{
name: path, Name: path,
query: ctx.Request.URL.RawQuery, Query: ctx.Request.URL.RawQuery,
publish: publish, Publish: publish,
ip: net.ParseIP(ip), IP: net.ParseIP(ip),
user: user, User: user,
pass: pass, Pass: pass,
proto: authProtocolWebRTC, Proto: defs.AuthProtocolWebRTC,
}, },
}) })
if res.err != nil { if res.Err != nil {
if terr, ok := res.err.(*errAuthentication); ok { if terr, ok := res.Err.(*defs.ErrAuthentication); ok {
if !hasCredentials { if !hasCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.Writer.WriteHeader(http.StatusUnauthorized) ctx.Writer.WriteHeader(http.StatusUnauthorized)
return false 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 // wait some seconds to stop brute force attacks
<-time.After(webrtcPauseAfterAuthError) <-time.After(webrtcPauseAfterAuthError)
webrtcWriteError(ctx, http.StatusUnauthorized, terr) writeError(ctx, http.StatusUnauthorized, terr)
return false return false
} }
webrtcWriteError(ctx, http.StatusInternalServerError, res.err) writeError(ctx, http.StatusInternalServerError, res.Err)
return false return false
} }
return true 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) { if !s.checkAuthOutsideSession(ctx, path, publish) {
return return
} }
servers, err := s.parent.generateICEServers() servers, err := s.parent.generateICEServers()
if err != nil { if err != nil {
webrtcWriteError(ctx, http.StatusInternalServerError, err) writeError(ctx, http.StatusInternalServerError, err)
return return
} }
@ -182,9 +164,9 @@ func (s *webRTCHTTPServer) onWHIPOptions(ctx *gin.Context, path string, publish
ctx.Writer.WriteHeader(http.StatusNoContent) 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" { 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 return
} }
@ -208,13 +190,13 @@ func (s *webRTCHTTPServer) onWHIPPost(ctx *gin.Context, path string, publish boo
publish: publish, publish: publish,
}) })
if res.err != nil { if res.err != nil {
webrtcWriteError(ctx, res.errStatusCode, res.err) writeError(ctx, res.errStatusCode, res.err)
return return
} }
servers, err := s.parent.generateICEServers() servers, err := s.parent.generateICEServers()
if err != nil { if err != nil {
webrtcWriteError(ctx, http.StatusInternalServerError, err) writeError(ctx, http.StatusInternalServerError, err)
return return
} }
@ -229,15 +211,15 @@ func (s *webRTCHTTPServer) onWHIPPost(ctx *gin.Context, path string, publish boo
ctx.Writer.Write(res.answer) 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) secret, err := uuid.Parse(rawSecret)
if err != nil { if err != nil {
webrtcWriteError(ctx, http.StatusBadRequest, fmt.Errorf("invalid secret")) writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid secret"))
return return
} }
if ctx.Request.Header.Get("Content-Type") != "application/trickle-ice-sdpfrag" { 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 return
} }
@ -248,7 +230,7 @@ func (s *webRTCHTTPServer) onWHIPPatch(ctx *gin.Context, rawSecret string) {
candidates, err := webrtc.ICEFragmentUnmarshal(byts) candidates, err := webrtc.ICEFragmentUnmarshal(byts)
if err != nil { if err != nil {
webrtcWriteError(ctx, http.StatusBadRequest, err) writeError(ctx, http.StatusBadRequest, err)
return return
} }
@ -257,17 +239,17 @@ func (s *webRTCHTTPServer) onWHIPPatch(ctx *gin.Context, rawSecret string) {
candidates: candidates, candidates: candidates,
}) })
if res.err != nil { if res.err != nil {
webrtcWriteError(ctx, http.StatusInternalServerError, res.err) writeError(ctx, http.StatusInternalServerError, res.err)
return return
} }
ctx.Writer.WriteHeader(http.StatusNoContent) 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) secret, err := uuid.Parse(rawSecret)
if err != nil { if err != nil {
webrtcWriteError(ctx, http.StatusBadRequest, fmt.Errorf("invalid secret")) writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid secret"))
return return
} }
@ -275,14 +257,14 @@ func (s *webRTCHTTPServer) onWHIPDelete(ctx *gin.Context, rawSecret string) {
secret: secret, secret: secret,
}) })
if err != nil { if err != nil {
webrtcWriteError(ctx, http.StatusInternalServerError, err) writeError(ctx, http.StatusInternalServerError, err)
return return
} }
ctx.Writer.WriteHeader(http.StatusOK) 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) { if !s.checkAuthOutsideSession(ctx, path, publish) {
return return
} }
@ -292,13 +274,13 @@ func (s *webRTCHTTPServer) onPage(ctx *gin.Context, path string, publish bool) {
ctx.Writer.WriteHeader(http.StatusOK) ctx.Writer.WriteHeader(http.StatusOK)
if publish { if publish {
ctx.Writer.Write(webrtcPublishIndex) ctx.Writer.Write(publishIndex)
} else { } 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-Origin", s.allowOrigin)
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 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 // RFC draft-ietf-whip-09
// The WHIP endpoints MUST return an "405 Method Not Allowed" response // The WHIP endpoints MUST return an "405 Method Not Allowed" response
// for any HTTP GET, HEAD or PUT requests // 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 return
} }

0
internal/core/webrtc_publish_index.html → internal/servers/webrtc/publish_index.html

0
internal/core/webrtc_read_index.html → internal/servers/webrtc/read_index.html

327
internal/core/webrtc_manager.go → internal/servers/webrtc/server.go

@ -1,4 +1,5 @@
package core // Package webrtc contains a WebRTC server.
package webrtc
import ( import (
"context" "context"
@ -93,36 +94,36 @@ func randomTurnUser() (string, error) {
return string(b), nil return string(b), nil
} }
type webRTCManagerAPISessionsListRes struct { type serverAPISessionsListRes struct {
data *defs.APIWebRTCSessionList data *defs.APIWebRTCSessionList
err error err error
} }
type webRTCManagerAPISessionsListReq struct { type serverAPISessionsListReq struct {
res chan webRTCManagerAPISessionsListRes res chan serverAPISessionsListRes
} }
type webRTCManagerAPISessionsGetRes struct { type serverAPISessionsGetRes struct {
data *defs.APIWebRTCSession data *defs.APIWebRTCSession
err error err error
} }
type webRTCManagerAPISessionsGetReq struct { type serverAPISessionsGetReq struct {
uuid uuid.UUID uuid uuid.UUID
res chan webRTCManagerAPISessionsGetRes res chan serverAPISessionsGetRes
} }
type webRTCManagerAPISessionsKickRes struct { type serverAPISessionsKickRes struct {
err error err error
} }
type webRTCManagerAPISessionsKickReq struct { type serverAPISessionsKickReq struct {
uuid uuid.UUID uuid uuid.UUID
res chan webRTCManagerAPISessionsKickRes res chan serverAPISessionsKickRes
} }
type webRTCNewSessionRes struct { type webRTCNewSessionRes struct {
sx *webRTCSession sx *session
answer []byte answer []byte
errStatusCode int errStatusCode int
err error err error
@ -140,7 +141,7 @@ type webRTCNewSessionReq struct {
} }
type webRTCAddSessionCandidatesRes struct { type webRTCAddSessionCandidatesRes struct {
sx *webRTCSession sx *session
err error err error
} }
@ -159,11 +160,12 @@ type webRTCDeleteSessionReq struct {
res chan webRTCDeleteSessionRes res chan webRTCDeleteSessionRes
} }
type webRTCManagerParent interface { type serverParent interface {
logger.Writer logger.Writer
} }
type webRTCManager struct { // Server is a WebRTC server.
type Server struct {
Address string Address string
Encryption bool Encryption bool
ServerKey string ServerKey string
@ -179,59 +181,60 @@ type webRTCManager struct {
AdditionalHosts []string AdditionalHosts []string
ICEServers []conf.WebRTCICEServer ICEServers []conf.WebRTCICEServer
ExternalCmdPool *externalcmd.Pool ExternalCmdPool *externalcmd.Pool
PathManager *pathManager PathManager defs.PathManager
Parent webRTCManagerParent Parent serverParent
ctx context.Context ctx context.Context
ctxCancel func() ctxCancel func()
httpServer *webRTCHTTPServer httpServer *httpServer
udpMuxLn net.PacketConn udpMuxLn net.PacketConn
tcpMuxLn net.Listener tcpMuxLn net.Listener
api *pwebrtc.API api *pwebrtc.API
sessions map[*webRTCSession]struct{} sessions map[*session]struct{}
sessionsBySecret map[uuid.UUID]*webRTCSession sessionsBySecret map[uuid.UUID]*session
// in // in
chNewSession chan webRTCNewSessionReq chNewSession chan webRTCNewSessionReq
chCloseSession chan *webRTCSession chCloseSession chan *session
chAddSessionCandidates chan webRTCAddSessionCandidatesReq chAddSessionCandidates chan webRTCAddSessionCandidatesReq
chDeleteSession chan webRTCDeleteSessionReq chDeleteSession chan webRTCDeleteSessionReq
chAPISessionsList chan webRTCManagerAPISessionsListReq chAPISessionsList chan serverAPISessionsListReq
chAPISessionsGet chan webRTCManagerAPISessionsGetReq chAPISessionsGet chan serverAPISessionsGetReq
chAPIConnsKick chan webRTCManagerAPISessionsKickReq chAPIConnsKick chan serverAPISessionsKickReq
// out // out
done chan struct{} done chan struct{}
} }
func (m *webRTCManager) initialize() error { // Initialize initializes the server.
func (s *Server) Initialize() error {
ctx, ctxCancel := context.WithCancel(context.Background()) ctx, ctxCancel := context.WithCancel(context.Background())
m.ctx = ctx s.ctx = ctx
m.ctxCancel = ctxCancel s.ctxCancel = ctxCancel
m.sessions = make(map[*webRTCSession]struct{}) s.sessions = make(map[*session]struct{})
m.sessionsBySecret = make(map[uuid.UUID]*webRTCSession) s.sessionsBySecret = make(map[uuid.UUID]*session)
m.chNewSession = make(chan webRTCNewSessionReq) s.chNewSession = make(chan webRTCNewSessionReq)
m.chCloseSession = make(chan *webRTCSession) s.chCloseSession = make(chan *session)
m.chAddSessionCandidates = make(chan webRTCAddSessionCandidatesReq) s.chAddSessionCandidates = make(chan webRTCAddSessionCandidatesReq)
m.chDeleteSession = make(chan webRTCDeleteSessionReq) s.chDeleteSession = make(chan webRTCDeleteSessionReq)
m.chAPISessionsList = make(chan webRTCManagerAPISessionsListReq) s.chAPISessionsList = make(chan serverAPISessionsListReq)
m.chAPISessionsGet = make(chan webRTCManagerAPISessionsGetReq) s.chAPISessionsGet = make(chan serverAPISessionsGetReq)
m.chAPIConnsKick = make(chan webRTCManagerAPISessionsKickReq) s.chAPIConnsKick = make(chan serverAPISessionsKickReq)
m.done = make(chan struct{}) s.done = make(chan struct{})
var err error s.httpServer = &httpServer{
m.httpServer, err = newWebRTCHTTPServer( address: s.Address,
m.Address, encryption: s.Encryption,
m.Encryption, serverKey: s.ServerKey,
m.ServerKey, serverCert: s.ServerCert,
m.ServerCert, allowOrigin: s.AllowOrigin,
m.AllowOrigin, trustedProxies: s.TrustedProxies,
m.TrustedProxies, readTimeout: s.ReadTimeout,
m.ReadTimeout, pathManager: s.PathManager,
m.PathManager, parent: s,
m, }
) err := s.httpServer.initialize()
if err != nil { if err != nil {
ctxCancel() ctxCancel()
return err return err
@ -239,95 +242,97 @@ func (m *webRTCManager) initialize() error {
apiConf := webrtc.APIConf{ apiConf := webrtc.APIConf{
LocalRandomUDP: false, LocalRandomUDP: false,
IPsFromInterfaces: m.IPsFromInterfaces, IPsFromInterfaces: s.IPsFromInterfaces,
IPsFromInterfacesList: m.IPsFromInterfacesList, IPsFromInterfacesList: s.IPsFromInterfacesList,
AdditionalHosts: m.AdditionalHosts, AdditionalHosts: s.AdditionalHosts,
} }
if m.LocalUDPAddress != "" { if s.LocalUDPAddress != "" {
m.udpMuxLn, err = net.ListenPacket(restrictnetwork.Restrict("udp", m.LocalUDPAddress)) s.udpMuxLn, err = net.ListenPacket(restrictnetwork.Restrict("udp", s.LocalUDPAddress))
if err != nil { if err != nil {
m.httpServer.close() s.httpServer.close()
ctxCancel() ctxCancel()
return err return err
} }
apiConf.ICEUDPMux = pwebrtc.NewICEUDPMux(webrtcNilLogger, m.udpMuxLn) apiConf.ICEUDPMux = pwebrtc.NewICEUDPMux(webrtcNilLogger, s.udpMuxLn)
} }
if m.LocalTCPAddress != "" { if s.LocalTCPAddress != "" {
m.tcpMuxLn, err = net.Listen(restrictnetwork.Restrict("tcp", m.LocalTCPAddress)) s.tcpMuxLn, err = net.Listen(restrictnetwork.Restrict("tcp", s.LocalTCPAddress))
if err != nil { if err != nil {
m.udpMuxLn.Close() s.udpMuxLn.Close()
m.httpServer.close() s.httpServer.close()
ctxCancel() ctxCancel()
return err 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 { if err != nil {
m.udpMuxLn.Close() s.udpMuxLn.Close()
m.tcpMuxLn.Close() s.tcpMuxLn.Close()
m.httpServer.close() s.httpServer.close()
ctxCancel() ctxCancel()
return err return err
} }
str := "listener opened on " + m.Address + " (HTTP)" str := "listener opened on " + s.Address + " (HTTP)"
if m.udpMuxLn != nil { if s.udpMuxLn != nil {
str += ", " + m.LocalUDPAddress + " (ICE/UDP)" str += ", " + s.LocalUDPAddress + " (ICE/UDP)"
} }
if m.tcpMuxLn != nil { if s.tcpMuxLn != nil {
str += ", " + m.LocalTCPAddress + " (ICE/TCP)" str += ", " + s.LocalTCPAddress + " (ICE/TCP)"
} }
m.Log(logger.Info, str) s.Log(logger.Info, str)
go m.run() go s.run()
return nil return nil
} }
// Log is the main logging function. // Log implements logger.Writer.
func (m *webRTCManager) Log(level logger.Level, format string, args ...interface{}) { func (s *Server) Log(level logger.Level, format string, args ...interface{}) {
m.Parent.Log(level, "[WebRTC] "+format, args...) s.Parent.Log(level, "[WebRTC] "+format, args...)
} }
func (m *webRTCManager) close() { // Close closes the server.
m.Log(logger.Info, "listener is closing") func (s *Server) Close() {
m.ctxCancel() s.Log(logger.Info, "listener is closing")
<-m.done s.ctxCancel()
<-s.done
} }
func (m *webRTCManager) run() { func (s *Server) run() {
defer close(m.done) defer close(s.done)
var wg sync.WaitGroup var wg sync.WaitGroup
outer: outer:
for { for {
select { select {
case req := <-m.chNewSession: case req := <-s.chNewSession:
sx := newWebRTCSession( sx := &session{
m.ctx, parentCtx: s.ctx,
m.WriteQueueSize, writeQueueSize: s.WriteQueueSize,
m.api, api: s.api,
req, req: req,
&wg, wg: &wg,
m.ExternalCmdPool, externalCmdPool: s.ExternalCmdPool,
m.PathManager, pathManager: s.PathManager,
m, parent: s,
) }
m.sessions[sx] = struct{}{} sx.initialize()
m.sessionsBySecret[sx.secret] = sx s.sessions[sx] = struct{}{}
s.sessionsBySecret[sx.secret] = sx
req.res <- webRTCNewSessionRes{sx: sx} req.res <- webRTCNewSessionRes{sx: sx}
case sx := <-m.chCloseSession: case sx := <-s.chCloseSession:
delete(m.sessions, sx) delete(s.sessions, sx)
delete(m.sessionsBySecret, sx.secret) delete(s.sessionsBySecret, sx.secret)
case req := <-m.chAddSessionCandidates: case req := <-s.chAddSessionCandidates:
sx, ok := m.sessionsBySecret[req.secret] sx, ok := s.sessionsBySecret[req.secret]
if !ok { if !ok {
req.res <- webRTCAddSessionCandidatesRes{err: fmt.Errorf("session not found")} req.res <- webRTCAddSessionCandidatesRes{err: fmt.Errorf("session not found")}
continue continue
@ -335,25 +340,25 @@ outer:
req.res <- webRTCAddSessionCandidatesRes{sx: sx} req.res <- webRTCAddSessionCandidatesRes{sx: sx}
case req := <-m.chDeleteSession: case req := <-s.chDeleteSession:
sx, ok := m.sessionsBySecret[req.secret] sx, ok := s.sessionsBySecret[req.secret]
if !ok { if !ok {
req.res <- webRTCDeleteSessionRes{err: fmt.Errorf("session not found")} req.res <- webRTCDeleteSessionRes{err: fmt.Errorf("session not found")}
continue continue
} }
delete(m.sessions, sx) delete(s.sessions, sx)
delete(m.sessionsBySecret, sx.secret) delete(s.sessionsBySecret, sx.secret)
sx.close() sx.Close()
req.res <- webRTCDeleteSessionRes{} req.res <- webRTCDeleteSessionRes{}
case req := <-m.chAPISessionsList: case req := <-s.chAPISessionsList:
data := &defs.APIWebRTCSessionList{ data := &defs.APIWebRTCSessionList{
Items: []*defs.APIWebRTCSession{}, Items: []*defs.APIWebRTCSession{},
} }
for sx := range m.sessions { for sx := range s.sessions {
data.Items = append(data.Items, sx.apiItem()) data.Items = append(data.Items, sx.apiItem())
} }
@ -361,52 +366,52 @@ outer:
return data.Items[i].Created.Before(data.Items[j].Created) return data.Items[i].Created.Before(data.Items[j].Created)
}) })
req.res <- webRTCManagerAPISessionsListRes{data: data} req.res <- serverAPISessionsListRes{data: data}
case req := <-m.chAPISessionsGet: case req := <-s.chAPISessionsGet:
sx := m.findSessionByUUID(req.uuid) sx := s.findSessionByUUID(req.uuid)
if sx == nil { if sx == nil {
req.res <- webRTCManagerAPISessionsGetRes{err: fmt.Errorf("session not found")} req.res <- serverAPISessionsGetRes{err: fmt.Errorf("session not found")}
continue continue
} }
req.res <- webRTCManagerAPISessionsGetRes{data: sx.apiItem()} req.res <- serverAPISessionsGetRes{data: sx.apiItem()}
case req := <-m.chAPIConnsKick: case req := <-s.chAPIConnsKick:
sx := m.findSessionByUUID(req.uuid) sx := s.findSessionByUUID(req.uuid)
if sx == nil { if sx == nil {
req.res <- webRTCManagerAPISessionsKickRes{err: fmt.Errorf("session not found")} req.res <- serverAPISessionsKickRes{err: fmt.Errorf("session not found")}
continue continue
} }
delete(m.sessions, sx) delete(s.sessions, sx)
delete(m.sessionsBySecret, sx.secret) delete(s.sessionsBySecret, sx.secret)
sx.close() sx.Close()
req.res <- webRTCManagerAPISessionsKickRes{} req.res <- serverAPISessionsKickRes{}
case <-m.ctx.Done(): case <-s.ctx.Done():
break outer break outer
} }
} }
m.ctxCancel() s.ctxCancel()
wg.Wait() wg.Wait()
m.httpServer.close() s.httpServer.close()
if m.udpMuxLn != nil { if s.udpMuxLn != nil {
m.udpMuxLn.Close() s.udpMuxLn.Close()
} }
if m.tcpMuxLn != nil { if s.tcpMuxLn != nil {
m.tcpMuxLn.Close() s.tcpMuxLn.Close()
} }
} }
func (m *webRTCManager) findSessionByUUID(uuid uuid.UUID) *webRTCSession { func (s *Server) findSessionByUUID(uuid uuid.UUID) *session {
for sx := range m.sessions { for sx := range s.sessions {
if sx.uuid == uuid { if sx.uuid == uuid {
return sx return sx
} }
@ -414,10 +419,10 @@ func (m *webRTCManager) findSessionByUUID(uuid uuid.UUID) *webRTCSession {
return nil return nil
} }
func (m *webRTCManager) generateICEServers() ([]pwebrtc.ICEServer, error) { func (s *Server) generateICEServers() ([]pwebrtc.ICEServer, error) {
ret := make([]pwebrtc.ICEServer, len(m.ICEServers)) ret := make([]pwebrtc.ICEServer, len(s.ICEServers))
for i, server := range m.ICEServers { for i, server := range s.ICEServers {
if server.Username == "AUTH_SECRET" { if server.Username == "AUTH_SECRET" {
expireDate := time.Now().Add(webrtcTurnSecretExpiration).Unix() expireDate := time.Now().Add(webrtcTurnSecretExpiration).Unix()
@ -445,16 +450,16 @@ func (m *webRTCManager) generateICEServers() ([]pwebrtc.ICEServer, error) {
} }
// newSession is called by webRTCHTTPServer. // newSession is called by webRTCHTTPServer.
func (m *webRTCManager) newSession(req webRTCNewSessionReq) webRTCNewSessionRes { func (s *Server) newSession(req webRTCNewSessionReq) webRTCNewSessionRes {
req.res = make(chan webRTCNewSessionRes) req.res = make(chan webRTCNewSessionRes)
select { select {
case m.chNewSession <- req: case s.chNewSession <- req:
res := <-req.res res := <-req.res
return res.sx.new(req) return res.sx.new(req)
case <-m.ctx.Done(): case <-s.ctx.Done():
return webRTCNewSessionRes{ return webRTCNewSessionRes{
errStatusCode: http.StatusInternalServerError, errStatusCode: http.StatusInternalServerError,
err: fmt.Errorf("terminated"), err: fmt.Errorf("terminated"),
@ -462,21 +467,21 @@ func (m *webRTCManager) newSession(req webRTCNewSessionReq) webRTCNewSessionRes
} }
} }
// closeSession is called by webRTCSession. // closeSession is called by session.
func (m *webRTCManager) closeSession(sx *webRTCSession) { func (s *Server) closeSession(sx *session) {
select { select {
case m.chCloseSession <- sx: case s.chCloseSession <- sx:
case <-m.ctx.Done(): case <-s.ctx.Done():
} }
} }
// addSessionCandidates is called by webRTCHTTPServer. // addSessionCandidates is called by webRTCHTTPServer.
func (m *webRTCManager) addSessionCandidates( func (s *Server) addSessionCandidates(
req webRTCAddSessionCandidatesReq, req webRTCAddSessionCandidatesReq,
) webRTCAddSessionCandidatesRes { ) webRTCAddSessionCandidatesRes {
req.res = make(chan webRTCAddSessionCandidatesRes) req.res = make(chan webRTCAddSessionCandidatesRes)
select { select {
case m.chAddSessionCandidates <- req: case s.chAddSessionCandidates <- req:
res1 := <-req.res res1 := <-req.res
if res1.err != nil { if res1.err != nil {
return res1 return res1
@ -484,70 +489,70 @@ func (m *webRTCManager) addSessionCandidates(
return res1.sx.addCandidates(req) return res1.sx.addCandidates(req)
case <-m.ctx.Done(): case <-s.ctx.Done():
return webRTCAddSessionCandidatesRes{err: fmt.Errorf("terminated")} return webRTCAddSessionCandidatesRes{err: fmt.Errorf("terminated")}
} }
} }
// deleteSession is called by webRTCHTTPServer. // deleteSession is called by webRTCHTTPServer.
func (m *webRTCManager) deleteSession(req webRTCDeleteSessionReq) error { func (s *Server) deleteSession(req webRTCDeleteSessionReq) error {
req.res = make(chan webRTCDeleteSessionRes) req.res = make(chan webRTCDeleteSessionRes)
select { select {
case m.chDeleteSession <- req: case s.chDeleteSession <- req:
res := <-req.res res := <-req.res
return res.err return res.err
case <-m.ctx.Done(): case <-s.ctx.Done():
return fmt.Errorf("terminated") return fmt.Errorf("terminated")
} }
} }
// apiSessionsList is called by api. // APISessionsList is called by api.
func (m *webRTCManager) apiSessionsList() (*defs.APIWebRTCSessionList, error) { func (s *Server) APISessionsList() (*defs.APIWebRTCSessionList, error) {
req := webRTCManagerAPISessionsListReq{ req := serverAPISessionsListReq{
res: make(chan webRTCManagerAPISessionsListRes), res: make(chan serverAPISessionsListRes),
} }
select { select {
case m.chAPISessionsList <- req: case s.chAPISessionsList <- req:
res := <-req.res res := <-req.res
return res.data, res.err return res.data, res.err
case <-m.ctx.Done(): case <-s.ctx.Done():
return nil, fmt.Errorf("terminated") return nil, fmt.Errorf("terminated")
} }
} }
// apiSessionsGet is called by api. // APISessionsGet is called by api.
func (m *webRTCManager) apiSessionsGet(uuid uuid.UUID) (*defs.APIWebRTCSession, error) { func (s *Server) APISessionsGet(uuid uuid.UUID) (*defs.APIWebRTCSession, error) {
req := webRTCManagerAPISessionsGetReq{ req := serverAPISessionsGetReq{
uuid: uuid, uuid: uuid,
res: make(chan webRTCManagerAPISessionsGetRes), res: make(chan serverAPISessionsGetRes),
} }
select { select {
case m.chAPISessionsGet <- req: case s.chAPISessionsGet <- req:
res := <-req.res res := <-req.res
return res.data, res.err return res.data, res.err
case <-m.ctx.Done(): case <-s.ctx.Done():
return nil, fmt.Errorf("terminated") return nil, fmt.Errorf("terminated")
} }
} }
// apiSessionsKick is called by api. // APISessionsKick is called by api.
func (m *webRTCManager) apiSessionsKick(uuid uuid.UUID) error { func (s *Server) APISessionsKick(uuid uuid.UUID) error {
req := webRTCManagerAPISessionsKickReq{ req := serverAPISessionsKickReq{
uuid: uuid, uuid: uuid,
res: make(chan webRTCManagerAPISessionsKickRes), res: make(chan serverAPISessionsKickRes),
} }
select { select {
case m.chAPIConnsKick <- req: case s.chAPIConnsKick <- req:
res := <-req.res res := <-req.res
return res.err return res.err
case <-m.ctx.Done(): case <-s.ctx.Done():
return fmt.Errorf("terminated") return fmt.Errorf("terminated")
} }
} }

199
internal/core/webrtc_session.go → internal/servers/webrtc/session.go

@ -1,4 +1,4 @@
package core package webrtc
import ( import (
"context" "context"
@ -33,7 +33,7 @@ import (
type setupStreamFunc func(*webrtc.OutgoingTrack) error type setupStreamFunc func(*webrtc.OutgoingTrack) error
func webrtcFindVideoTrack( func findVideoTrack(
stream *stream.Stream, stream *stream.Stream,
writer *asyncwriter.Writer, writer *asyncwriter.Writer,
) (format.Format, setupStreamFunc) { ) (format.Format, setupStreamFunc) {
@ -202,7 +202,7 @@ func webrtcFindVideoTrack(
return nil, nil return nil, nil
} }
func webrtcFindAudioTrack( func findAudioTrack(
stream *stream.Stream, stream *stream.Stream,
writer *asyncwriter.Writer, writer *asyncwriter.Writer,
) (format.Format, setupStreamFunc) { ) (format.Format, setupStreamFunc) {
@ -264,19 +264,15 @@ func whipOffer(body []byte) *pwebrtc.SessionDescription {
} }
} }
type webRTCSessionPathManager interface { type session struct {
addPublisher(req pathAddPublisherReq) pathAddPublisherRes parentCtx context.Context
addReader(req pathAddReaderReq) pathAddReaderRes
}
type webRTCSession struct {
writeQueueSize int writeQueueSize int
api *pwebrtc.API api *pwebrtc.API
req webRTCNewSessionReq req webRTCNewSessionReq
wg *sync.WaitGroup wg *sync.WaitGroup
externalCmdPool *externalcmd.Pool externalCmdPool *externalcmd.Pool
pathManager webRTCSessionPathManager pathManager defs.PathManager
parent *webRTCManager parent *Server
ctx context.Context ctx context.Context
ctxCancel func() ctxCancel func()
@ -290,53 +286,34 @@ type webRTCSession struct {
chAddCandidates chan webRTCAddSessionCandidatesReq chAddCandidates chan webRTCAddSessionCandidatesReq
} }
func newWebRTCSession( func (s *session) initialize() {
parentCtx context.Context, ctx, ctxCancel := context.WithCancel(s.parentCtx)
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()
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]) id := hex.EncodeToString(s.uuid[:4])
s.parent.Log(level, "[session %v] "+format, append([]interface{}{id}, args...)...) s.parent.Log(level, "[session %v] "+format, append([]interface{}{id}, args...)...)
} }
func (s *webRTCSession) close() { func (s *session) Close() {
s.ctxCancel() s.ctxCancel()
} }
func (s *webRTCSession) run() { func (s *session) run() {
defer s.wg.Done() defer s.wg.Done()
err := s.runInner() err := s.runInner()
@ -348,7 +325,7 @@ func (s *webRTCSession) run() {
s.Log(logger.Info, "closed: %v", err) s.Log(logger.Info, "closed: %v", err)
} }
func (s *webRTCSession) runInner() error { func (s *session) runInner() error {
select { select {
case <-s.chNew: case <-s.chNew:
case <-s.ctx.Done(): case <-s.ctx.Done():
@ -367,41 +344,41 @@ func (s *webRTCSession) runInner() error {
return err return err
} }
func (s *webRTCSession) runInner2() (int, error) { func (s *session) runInner2() (int, error) {
if s.req.publish { if s.req.publish {
return s.runPublish() return s.runPublish()
} }
return s.runRead() return s.runRead()
} }
func (s *webRTCSession) runPublish() (int, error) { func (s *session) runPublish() (int, error) {
ip, _, _ := net.SplitHostPort(s.req.remoteAddr) ip, _, _ := net.SplitHostPort(s.req.remoteAddr)
res := s.pathManager.addPublisher(pathAddPublisherReq{ res := s.pathManager.AddPublisher(defs.PathAddPublisherReq{
author: s, Author: s,
accessRequest: pathAccessRequest{ AccessRequest: defs.PathAccessRequest{
name: s.req.pathName, Name: s.req.pathName,
query: s.req.query, Query: s.req.query,
publish: true, Publish: true,
ip: net.ParseIP(ip), IP: net.ParseIP(ip),
user: s.req.user, User: s.req.user,
pass: s.req.pass, Pass: s.req.pass,
proto: authProtocolWebRTC, Proto: defs.AuthProtocolWebRTC,
id: &s.uuid, ID: &s.uuid,
}, },
}) })
if res.err != nil { if res.Err != nil {
if _, ok := res.err.(*errAuthentication); ok { if _, ok := res.Err.(*defs.ErrAuthentication); ok {
// wait some seconds to stop brute force attacks // wait some seconds to stop brute force attacks
<-time.After(webrtcPauseAfterAuthError) <-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() iceServers, err := s.parent.generateICEServers()
if err != nil { if err != nil {
@ -463,13 +440,13 @@ func (s *webRTCSession) runPublish() (int, error) {
medias := webrtc.TracksToMedias(tracks) medias := webrtc.TracksToMedias(tracks)
rres := res.path.startPublisher(pathStartPublisherReq{ rres := res.Path.StartPublisher(defs.PathStartPublisherReq{
author: s, Author: s,
desc: &description.Session{Medias: medias}, Desc: &description.Session{Medias: medias},
generateRTPPackets: false, GenerateRTPPackets: false,
}) })
if rres.err != nil { if rres.Err != nil {
return 0, rres.err return 0, rres.Err
} }
timeDecoder := rtptime.NewGlobalDecoder() timeDecoder := rtptime.NewGlobalDecoder()
@ -491,7 +468,7 @@ func (s *webRTCSession) runPublish() (int, error) {
continue 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) ip, _, _ := net.SplitHostPort(s.req.remoteAddr)
res := s.pathManager.addReader(pathAddReaderReq{ res := s.pathManager.AddReader(defs.PathAddReaderReq{
author: s, Author: s,
accessRequest: pathAccessRequest{ AccessRequest: defs.PathAccessRequest{
name: s.req.pathName, Name: s.req.pathName,
query: s.req.query, Query: s.req.query,
ip: net.ParseIP(ip), IP: net.ParseIP(ip),
user: s.req.user, User: s.req.user,
pass: s.req.pass, Pass: s.req.pass,
proto: authProtocolWebRTC, Proto: defs.AuthProtocolWebRTC,
id: &s.uuid, ID: &s.uuid,
}, },
}) })
if res.err != nil { if res.Err != nil {
if _, ok := res.err.(*errAuthentication); ok { if _, ok := res.Err.(*defs.ErrAuthentication); ok {
// wait some seconds to stop brute force attacks // wait some seconds to stop brute force attacks
<-time.After(webrtcPauseAfterAuthError) <-time.After(webrtcPauseAfterAuthError)
return http.StatusUnauthorized, res.err return http.StatusUnauthorized, res.Err
} }
if strings.HasPrefix(res.err.Error(), "no one is publishing") { if strings.HasPrefix(res.Err.Error(), "no one is publishing") {
return http.StatusNotFound, res.err 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() iceServers, err := s.parent.generateICEServers()
if err != nil { if err != nil {
@ -556,8 +533,8 @@ func (s *webRTCSession) runRead() (int, error) {
writer := asyncwriter.New(s.writeQueueSize, s) writer := asyncwriter.New(s.writeQueueSize, s)
videoTrack, videoSetup := webrtcFindVideoTrack(res.stream, writer) videoTrack, videoSetup := findVideoTrack(res.Stream, writer)
audioTrack, audioSetup := webrtcFindAudioTrack(res.stream, writer) audioTrack, audioSetup := findAudioTrack(res.Stream, writer)
if videoTrack == nil && audioTrack == nil { if videoTrack == nil && audioTrack == nil {
return http.StatusBadRequest, fmt.Errorf( return http.StatusBadRequest, fmt.Errorf(
@ -589,7 +566,7 @@ func (s *webRTCSession) runRead() (int, error) {
s.pc = pc s.pc = pc
s.mutex.Unlock() s.mutex.Unlock()
defer res.stream.RemoveReader(writer) defer res.Stream.RemoveReader(writer)
n := 0 n := 0
@ -609,14 +586,14 @@ func (s *webRTCSession) runRead() (int, error) {
} }
s.Log(logger.Info, "is reading from path '%s', %s", 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{ onUnreadHook := hooks.OnRead(hooks.OnReadParams{
Logger: s, Logger: s,
ExternalCmdPool: s.externalCmdPool, ExternalCmdPool: s.externalCmdPool,
Conf: res.path.safeConf(), Conf: res.Path.SafeConf(),
ExternalCmdEnv: res.path.externalCmdEnv(), ExternalCmdEnv: res.Path.ExternalCmdEnv(),
Reader: s.apiReaderDescribe(), Reader: s.APIReaderDescribe(),
Query: s.req.query, Query: s.req.query,
}) })
defer onUnreadHook() 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{ s.req.res <- webRTCNewSessionRes{
sx: s, sx: s,
answer: []byte(answer.SDP), answer: []byte(answer.SDP),
} }
} }
func (s *webRTCSession) readRemoteCandidates(pc *webrtc.PeerConnection) { func (s *session) readRemoteCandidates(pc *webrtc.PeerConnection) {
for { for {
select { select {
case req := <-s.chAddCandidates: case req := <-s.chAddCandidates:
@ -662,8 +639,8 @@ func (s *webRTCSession) readRemoteCandidates(pc *webrtc.PeerConnection) {
} }
} }
// new is called by webRTCHTTPServer through webRTCManager. // new is called by webRTCHTTPServer through Server.
func (s *webRTCSession) new(req webRTCNewSessionReq) webRTCNewSessionRes { func (s *session) new(req webRTCNewSessionReq) webRTCNewSessionRes {
select { select {
case s.chNew <- req: case s.chNew <- req:
return <-req.res return <-req.res
@ -673,8 +650,8 @@ func (s *webRTCSession) new(req webRTCNewSessionReq) webRTCNewSessionRes {
} }
} }
// addCandidates is called by webRTCHTTPServer through webRTCManager. // addCandidates is called by webRTCHTTPServer through Server.
func (s *webRTCSession) addCandidates( func (s *session) addCandidates(
req webRTCAddSessionCandidatesReq, req webRTCAddSessionCandidatesReq,
) webRTCAddSessionCandidatesRes { ) webRTCAddSessionCandidatesRes {
select { select {
@ -686,20 +663,20 @@ func (s *webRTCSession) addCandidates(
} }
} }
// apiReaderDescribe implements reader. // APIReaderDescribe implements reader.
func (s *webRTCSession) apiReaderDescribe() defs.APIPathSourceOrReader { func (s *session) APIReaderDescribe() defs.APIPathSourceOrReader {
return defs.APIPathSourceOrReader{ return defs.APIPathSourceOrReader{
Type: "webRTCSession", Type: "webrtcSession",
ID: s.uuid.String(), ID: s.uuid.String(),
} }
} }
// APISourceDescribe implements source. // APISourceDescribe implements source.
func (s *webRTCSession) APISourceDescribe() defs.APIPathSourceOrReader { func (s *session) APISourceDescribe() defs.APIPathSourceOrReader {
return s.apiReaderDescribe() return s.APIReaderDescribe()
} }
func (s *webRTCSession) apiItem() *defs.APIWebRTCSession { func (s *session) apiItem() *defs.APIWebRTCSession {
s.mutex.RLock() s.mutex.RLock()
defer s.mutex.RUnlock() defer s.mutex.RUnlock()

2
internal/staticsources/hls/source.go

@ -24,7 +24,7 @@ type Source struct {
Parent defs.StaticSourceParent Parent defs.StaticSourceParent
} }
// Log implements StaticSource. // Log implements logger.Writer.
func (s *Source) Log(level logger.Level, format string, args ...interface{}) { func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
s.Parent.Log(level, "[HLS source] "+format, args...) s.Parent.Log(level, "[HLS source] "+format, args...)
} }

16
internal/staticsources/hls/source_test.go

@ -32,17 +32,17 @@ var track2 = &mpegts.Track{
}, },
} }
type testHLSManager struct { type testHLSServer struct {
s *http.Server s *http.Server
} }
func newTestHLSManager() (*testHLSManager, error) { func newTestHLSServer() (*testHLSServer, error) {
ln, err := net.Listen("tcp", "localhost:5780") ln, err := net.Listen("tcp", "localhost:5780")
if err != nil { if err != nil {
return nil, err return nil, err
} }
ts := &testHLSManager{} ts := &testHLSServer{}
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
router := gin.New() router := gin.New()
@ -56,11 +56,11 @@ func newTestHLSManager() (*testHLSManager, error) {
return ts, nil return ts, nil
} }
func (ts *testHLSManager) close() { func (ts *testHLSServer) close() {
ts.s.Shutdown(context.Background()) ts.s.Shutdown(context.Background())
} }
func (ts *testHLSManager) onPlaylist(ctx *gin.Context) { func (ts *testHLSServer) onPlaylist(ctx *gin.Context) {
cnt := `#EXTM3U cnt := `#EXTM3U
#EXT-X-VERSION:3 #EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:NO #EXT-X-ALLOW-CACHE:NO
@ -77,7 +77,7 @@ segment2.ts
io.Copy(ctx.Writer, bytes.NewReader([]byte(cnt))) 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`) ctx.Writer.Header().Set("Content-Type", `video/MP2T`)
w := mpegts.NewWriter(ctx.Writer, []*mpegts.Track{track1, track2}) 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 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`) ctx.Writer.Header().Set("Content-Type", `video/MP2T`)
w := mpegts.NewWriter(ctx.Writer, []*mpegts.Track{track1, track2}) 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) { func TestSource(t *testing.T) {
ts, err := newTestHLSManager() ts, err := newTestHLSServer()
require.NoError(t, err) require.NoError(t, err)
defer ts.close() defer ts.close()

2
internal/staticsources/rpicamera/source.go

@ -57,7 +57,7 @@ type Source struct {
Parent defs.StaticSourceParent Parent defs.StaticSourceParent
} }
// Log implements StaticSource. // Log implements logger.Writer.
func (s *Source) Log(level logger.Level, format string, args ...interface{}) { func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
s.Parent.Log(level, "[RPI Camera source] "+format, args...) s.Parent.Log(level, "[RPI Camera source] "+format, args...)
} }

2
internal/staticsources/rtmp/source.go

@ -28,7 +28,7 @@ type Source struct {
Parent defs.StaticSourceParent Parent defs.StaticSourceParent
} }
// Log implements StaticSource. // Log implements logger.Writer.
func (s *Source) Log(level logger.Level, format string, args ...interface{}) { func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
s.Parent.Log(level, "[RTMP source] "+format, args...) s.Parent.Log(level, "[RTMP source] "+format, args...)
} }

2
internal/staticsources/rtsp/source.go

@ -68,7 +68,7 @@ type Source struct {
Parent defs.StaticSourceParent Parent defs.StaticSourceParent
} }
// Log implements StaticSource. // Log implements logger.Writer.
func (s *Source) Log(level logger.Level, format string, args ...interface{}) { func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
s.Parent.Log(level, "[RTSP source] "+format, args...) s.Parent.Log(level, "[RTSP source] "+format, args...)
} }

4
internal/staticsources/srt/source.go

@ -6,7 +6,7 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/description"
mcmpegts "github.com/bluenviron/mediacommon/pkg/formats/mpegts" 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/conf"
"github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/defs"
@ -21,7 +21,7 @@ type Source struct {
Parent defs.StaticSourceParent Parent defs.StaticSourceParent
} }
// Log implements StaticSource. // Log implements logger.Writer.
func (s *Source) Log(level logger.Level, format string, args ...interface{}) { func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
s.Parent.Log(level, "[SRT source] "+format, args...) s.Parent.Log(level, "[SRT source] "+format, args...)
} }

2
internal/staticsources/udp/source.go

@ -49,7 +49,7 @@ type Source struct {
Parent defs.StaticSourceParent Parent defs.StaticSourceParent
} }
// Log implements StaticSource. // Log implements logger.Writer.
func (s *Source) Log(level logger.Level, format string, args ...interface{}) { func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
s.Parent.Log(level, "[UDP source] "+format, args...) s.Parent.Log(level, "[UDP source] "+format, args...)
} }

2
internal/staticsources/webrtc/source.go

@ -23,7 +23,7 @@ type Source struct {
Parent defs.StaticSourceParent Parent defs.StaticSourceParent
} }
// Log implements StaticSource. // Log implements logger.Writer.
func (s *Source) Log(level logger.Level, format string, args ...interface{}) { func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
s.Parent.Log(level, "[WebRTC source] "+format, args...) s.Parent.Log(level, "[WebRTC source] "+format, args...)
} }

Loading…
Cancel
Save