From 43d41c070bec8d01c91d79b6d57169992b4a8ec9 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Tue, 31 Oct 2023 14:19:04 +0100 Subject: [PATCH] move static sources into dedicated package (#2616) --- internal/core/api.go | 36 +- internal/core/conn.go | 5 +- internal/core/core.go | 4 +- internal/core/hls_http_server.go | 3 +- internal/core/hls_manager.go | 13 +- internal/core/hls_muxer.go | 9 +- internal/core/hls_source_test.go | 208 --------- internal/core/metrics.go | 3 +- internal/core/mpegts.go | 221 +-------- internal/core/path.go | 98 ++-- internal/core/path_manager.go | 9 +- internal/core/pprof.go | 3 +- internal/core/reader.go | 5 +- internal/core/restrict_network.go | 17 - internal/core/rtmp_conn.go | 21 +- internal/core/rtmp_server.go | 18 +- internal/core/rtmp_source_test.go | 147 ------ internal/core/rtsp_conn.go | 9 +- internal/core/rtsp_server.go | 17 +- internal/core/rtsp_session.go | 21 +- internal/core/rtsp_source_test.go | 312 ------------- internal/core/source.go | 11 +- internal/core/source_redirect.go | 7 +- internal/core/source_static.go | 249 ---------- internal/core/srt_conn.go | 28 +- internal/core/srt_server.go | 13 +- internal/core/srt_source.go | 129 ------ internal/core/srt_source_test.go | 105 ----- internal/core/static_source_handler.go | 262 +++++++++++ internal/core/tls_fingerprint.go | 39 -- internal/core/udp_source_test.go | 90 ---- internal/core/webrtc_http_server.go | 6 +- internal/core/webrtc_manager.go | 18 +- internal/core/webrtc_session.go | 65 +-- internal/core/webrtc_source.go | 118 ----- internal/{core/api_defs.go => defs/api.go} | 117 +++-- internal/defs/defs.go | 2 + internal/defs/path.go | 25 + internal/defs/static_source.go | 29 ++ internal/protocols/mpegts/to_stream.go | 204 +++++++++ internal/protocols/tls/tls_config.go | 35 ++ internal/protocols/webrtc/track_wrapper.go | 20 + internal/protocols/webrtc/tracks_to_medias.go | 32 ++ internal/restrictnetwork/restrict_network.go | 18 + .../hls/source.go} | 63 ++- internal/staticsources/hls/source_test.go | 117 +++++ .../rpicamera/source.go} | 57 +-- .../rtmp/source.go} | 83 ++-- internal/staticsources/rtmp/source_test.go | 189 ++++++++ .../rtsp/source.go} | 86 ++-- internal/staticsources/rtsp/source_test.go | 432 ++++++++++++++++++ internal/staticsources/srt/source.go | 115 +++++ internal/staticsources/srt/source_test.go | 73 +++ internal/staticsources/tester/tester.go | 86 ++++ .../udp/source.go} | 77 ++-- internal/staticsources/udp/source_test.go | 59 +++ internal/staticsources/webrtc/source.go | 103 +++++ .../webrtc/source_test.go} | 102 ++--- 58 files changed, 2271 insertions(+), 2172 deletions(-) delete mode 100644 internal/core/hls_source_test.go delete mode 100644 internal/core/restrict_network.go delete mode 100644 internal/core/rtmp_source_test.go delete mode 100644 internal/core/rtsp_source_test.go delete mode 100644 internal/core/source_static.go delete mode 100644 internal/core/srt_source.go delete mode 100644 internal/core/srt_source_test.go create mode 100644 internal/core/static_source_handler.go delete mode 100644 internal/core/tls_fingerprint.go delete mode 100644 internal/core/udp_source_test.go delete mode 100644 internal/core/webrtc_source.go rename internal/{core/api_defs.go => defs/api.go} (54%) create mode 100644 internal/defs/defs.go create mode 100644 internal/defs/path.go create mode 100644 internal/defs/static_source.go create mode 100644 internal/protocols/mpegts/to_stream.go create mode 100644 internal/protocols/tls/tls_config.go create mode 100644 internal/protocols/webrtc/track_wrapper.go create mode 100644 internal/protocols/webrtc/tracks_to_medias.go create mode 100644 internal/restrictnetwork/restrict_network.go rename internal/{core/hls_source.go => staticsources/hls/source.go} (78%) create mode 100644 internal/staticsources/hls/source_test.go rename internal/{core/rpicamera_source.go => staticsources/rpicamera/source.go} (68%) rename internal/{core/rtmp_source.go => staticsources/rtmp/source.go} (62%) create mode 100644 internal/staticsources/rtmp/source_test.go rename internal/{core/rtsp_source.go => staticsources/rtsp/source.go} (59%) create mode 100644 internal/staticsources/rtsp/source_test.go create mode 100644 internal/staticsources/srt/source.go create mode 100644 internal/staticsources/srt/source_test.go create mode 100644 internal/staticsources/tester/tester.go rename internal/{core/udp_source.go => staticsources/udp/source.go} (52%) create mode 100644 internal/staticsources/udp/source_test.go create mode 100644 internal/staticsources/webrtc/source.go rename internal/{core/webrtc_source_test.go => staticsources/webrtc/source_test.go} (60%) diff --git a/internal/core/api.go b/internal/core/api.go index b0a3c5f7..51e7a1c7 100644 --- a/internal/core/api.go +++ b/internal/core/api.go @@ -14,8 +14,10 @@ import ( "github.com/google/uuid" "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/protocols/httpserv" + "github.com/bluenviron/mediamtx/internal/restrictnetwork" ) func interfaceIsEmpty(i interface{}) bool { @@ -96,38 +98,38 @@ func paramName(ctx *gin.Context) (string, bool) { } type apiPathManager interface { - apiPathsList() (*apiPathList, error) - apiPathsGet(string) (*apiPath, error) + apiPathsList() (*defs.APIPathList, error) + apiPathsGet(string) (*defs.APIPath, error) } type apiHLSManager interface { - apiMuxersList() (*apiHLSMuxerList, error) - apiMuxersGet(string) (*apiHLSMuxer, error) + apiMuxersList() (*defs.APIHLSMuxerList, error) + apiMuxersGet(string) (*defs.APIHLSMuxer, error) } type apiRTSPServer interface { - apiConnsList() (*apiRTSPConnsList, error) - apiConnsGet(uuid.UUID) (*apiRTSPConn, error) - apiSessionsList() (*apiRTSPSessionList, error) - apiSessionsGet(uuid.UUID) (*apiRTSPSession, error) + apiConnsList() (*defs.APIRTSPConnsList, error) + apiConnsGet(uuid.UUID) (*defs.APIRTSPConn, error) + apiSessionsList() (*defs.APIRTSPSessionList, error) + apiSessionsGet(uuid.UUID) (*defs.APIRTSPSession, error) apiSessionsKick(uuid.UUID) error } type apiRTMPServer interface { - apiConnsList() (*apiRTMPConnList, error) - apiConnsGet(uuid.UUID) (*apiRTMPConn, error) + apiConnsList() (*defs.APIRTMPConnList, error) + apiConnsGet(uuid.UUID) (*defs.APIRTMPConn, error) apiConnsKick(uuid.UUID) error } type apiWebRTCManager interface { - apiSessionsList() (*apiWebRTCSessionList, error) - apiSessionsGet(uuid.UUID) (*apiWebRTCSession, error) + apiSessionsList() (*defs.APIWebRTCSessionList, error) + apiSessionsGet(uuid.UUID) (*defs.APIWebRTCSession, error) apiSessionsKick(uuid.UUID) error } type apiSRTServer interface { - apiConnsList() (*apiSRTConnList, error) - apiConnsGet(uuid.UUID) (*apiSRTConn, error) + apiConnsList() (*defs.APISRTConnList, error) + apiConnsGet(uuid.UUID) (*defs.APISRTConn, error) apiConnsKick(uuid.UUID) error } @@ -245,7 +247,7 @@ func newAPI( group.POST("/v3/srtconns/kick/:id", a.onSRTConnsKick) } - network, address := restrictNetwork("tcp", address) + network, address := restrictnetwork.Restrict("tcp", address) var err error a.httpServer, err = httpserv.NewWrappedServer( @@ -281,7 +283,7 @@ func (a *api) writeError(ctx *gin.Context, status int, err error) { a.Log(logger.Error, err.Error()) // send error in response - ctx.JSON(status, &apiError{ + ctx.JSON(status, &defs.APIError{ Error: err.Error(), }) } @@ -364,7 +366,7 @@ func (a *api) onConfigPathsList(ctx *gin.Context) { c := a.conf a.mutex.Unlock() - data := &apiPathConfList{ + data := &defs.APIPathConfList{ Items: make([]*conf.Path, len(c.Paths)), } diff --git a/internal/core/conn.go b/internal/core/conn.go index 8f8a5c2c..5ee47b14 100644 --- a/internal/core/conn.go +++ b/internal/core/conn.go @@ -3,6 +3,7 @@ package core import ( "net" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/logger" ) @@ -36,7 +37,7 @@ func newConn( } } -func (c *conn) open(desc apiPathSourceOrReader) { +func (c *conn) open(desc defs.APIPathSourceOrReader) { if c.runOnConnect != "" { c.logger.Log(logger.Info, "runOnConnect command started") @@ -58,7 +59,7 @@ func (c *conn) open(desc apiPathSourceOrReader) { } } -func (c *conn) close(desc apiPathSourceOrReader) { +func (c *conn) close(desc defs.APIPathSourceOrReader) { if c.onConnectCmd != nil { c.onConnectCmd.Close() c.logger.Log(logger.Info, "runOnConnect command stopped") diff --git a/internal/core/core.go b/internal/core/core.go index 1f7449bc..793351ac 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -71,7 +71,7 @@ var cli struct { Confpath string `arg:"" default:""` } -// Core is an instance of mediamtx. +// Core is an instance of MediaMTX. type Core struct { ctx context.Context ctxCancel func() @@ -100,7 +100,7 @@ type Core struct { done chan struct{} } -// New allocates a core. +// New allocates a Core. func New(args []string) (*Core, bool) { parser, err := kong.New(&cli, kong.Description("MediaMTX "+version), diff --git a/internal/core/hls_http_server.go b/internal/core/hls_http_server.go index e273ab54..e65b8b0e 100644 --- a/internal/core/hls_http_server.go +++ b/internal/core/hls_http_server.go @@ -14,6 +14,7 @@ import ( "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/protocols/httpserv" + "github.com/bluenviron/mediamtx/internal/restrictnetwork" ) const ( @@ -70,7 +71,7 @@ func newHLSHTTPServer( //nolint:dupl router.NoRoute(s.onRequest) - network, address := restrictNetwork("tcp", address) + network, address := restrictnetwork.Restrict("tcp", address) var err error s.inner, err = httpserv.NewWrappedServer( diff --git a/internal/core/hls_manager.go b/internal/core/hls_manager.go index 428cc16a..b71cf81c 100644 --- a/internal/core/hls_manager.go +++ b/internal/core/hls_manager.go @@ -7,11 +7,12 @@ import ( "sync" "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/logger" ) type hlsManagerAPIMuxersListRes struct { - data *apiHLSMuxerList + data *defs.APIHLSMuxerList err error } @@ -20,7 +21,7 @@ type hlsManagerAPIMuxersListReq struct { } type hlsManagerAPIMuxersGetRes struct { - data *apiHLSMuxer + data *defs.APIHLSMuxer err error } @@ -189,8 +190,8 @@ outer: delete(m.muxers, c.PathName()) case req := <-m.chAPIMuxerList: - data := &apiHLSMuxerList{ - Items: []*apiHLSMuxer{}, + data := &defs.APIHLSMuxerList{ + Items: []*defs.APIHLSMuxer{}, } for _, muxer := range m.muxers { @@ -275,7 +276,7 @@ func (m *hlsManager) pathNotReady(pa *path) { } // apiMuxersList is called by api. -func (m *hlsManager) apiMuxersList() (*apiHLSMuxerList, error) { +func (m *hlsManager) apiMuxersList() (*defs.APIHLSMuxerList, error) { req := hlsManagerAPIMuxersListReq{ res: make(chan hlsManagerAPIMuxersListRes), } @@ -291,7 +292,7 @@ func (m *hlsManager) apiMuxersList() (*apiHLSMuxerList, error) { } // apiMuxersGet is called by api. -func (m *hlsManager) apiMuxersGet(name string) (*apiHLSMuxer, error) { +func (m *hlsManager) apiMuxersGet(name string) (*defs.APIHLSMuxer, error) { req := hlsManagerAPIMuxersGetReq{ name: name, res: make(chan hlsManagerAPIMuxersGetRes), diff --git a/internal/core/hls_muxer.go b/internal/core/hls_muxer.go index 9ce6ca1e..f3a854ed 100644 --- a/internal/core/hls_muxer.go +++ b/internal/core/hls_muxer.go @@ -19,6 +19,7 @@ import ( "github.com/bluenviron/mediamtx/internal/asyncwriter" "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/unit" @@ -527,15 +528,15 @@ func (m *hlsMuxer) processRequest(req *hlsMuxerHandleRequestReq) { } // apiReaderDescribe implements reader. -func (m *hlsMuxer) apiReaderDescribe() apiPathSourceOrReader { - return apiPathSourceOrReader{ +func (m *hlsMuxer) apiReaderDescribe() defs.APIPathSourceOrReader { + return defs.APIPathSourceOrReader{ Type: "hlsMuxer", ID: "", } } -func (m *hlsMuxer) apiItem() *apiHLSMuxer { - return &apiHLSMuxer{ +func (m *hlsMuxer) apiItem() *defs.APIHLSMuxer { + return &defs.APIHLSMuxer{ Path: m.pathName, Created: m.created, LastRequest: time.Unix(0, atomic.LoadInt64(m.lastRequestTime)), diff --git a/internal/core/hls_source_test.go b/internal/core/hls_source_test.go deleted file mode 100644 index 44e4cb62..00000000 --- a/internal/core/hls_source_test.go +++ /dev/null @@ -1,208 +0,0 @@ -package core - -import ( - "bytes" - "context" - "io" - "net" - "net/http" - "testing" - - "github.com/bluenviron/gortsplib/v4" - "github.com/bluenviron/gortsplib/v4/pkg/description" - "github.com/bluenviron/gortsplib/v4/pkg/format" - "github.com/bluenviron/gortsplib/v4/pkg/url" - "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio" - "github.com/bluenviron/mediacommon/pkg/formats/mpegts" - "github.com/gin-gonic/gin" - "github.com/pion/rtp" - "github.com/stretchr/testify/require" -) - -var track1 = &mpegts.Track{ - Codec: &mpegts.CodecH264{}, -} - -var track2 = &mpegts.Track{ - Codec: &mpegts.CodecMPEG4Audio{ - Config: mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }, - }, -} - -type testHLSManager struct { - s *http.Server - - clientConnected chan struct{} -} - -func newTestHLSManager() (*testHLSManager, error) { - ln, err := net.Listen("tcp", "localhost:5780") - if err != nil { - return nil, err - } - - ts := &testHLSManager{ - clientConnected: make(chan struct{}), - } - - gin.SetMode(gin.ReleaseMode) - router := gin.New() - router.GET("/stream.m3u8", ts.onPlaylist) - router.GET("/segment1.ts", ts.onSegment1) - router.GET("/segment2.ts", ts.onSegment2) - - ts.s = &http.Server{Handler: router} - go ts.s.Serve(ln) - - return ts, nil -} - -func (ts *testHLSManager) close() { - ts.s.Shutdown(context.Background()) -} - -func (ts *testHLSManager) onPlaylist(ctx *gin.Context) { - cnt := `#EXTM3U -#EXT-X-VERSION:3 -#EXT-X-ALLOW-CACHE:NO -#EXT-X-TARGETDURATION:2 -#EXT-X-MEDIA-SEQUENCE:0 -#EXTINF:2, -segment1.ts -#EXTINF:2, -segment2.ts -#EXT-X-ENDLIST -` - - ctx.Writer.Header().Set("Content-Type", `application/vnd.apple.mpegurl`) - io.Copy(ctx.Writer, bytes.NewReader([]byte(cnt))) -} - -func (ts *testHLSManager) onSegment1(ctx *gin.Context) { - ctx.Writer.Header().Set("Content-Type", `video/MP2T`) - - w := mpegts.NewWriter(ctx.Writer, []*mpegts.Track{track1, track2}) - - w.WriteMPEG4Audio(track2, 1*90000, [][]byte{{1, 2, 3, 4}}) //nolint:errcheck -} - -func (ts *testHLSManager) onSegment2(ctx *gin.Context) { - <-ts.clientConnected - - ctx.Writer.Header().Set("Content-Type", `video/MP2T`) - - w := mpegts.NewWriter(ctx.Writer, []*mpegts.Track{track1, track2}) - - w.WriteH26x(track1, 2*90000, 2*90000, true, [][]byte{ //nolint:errcheck - {7, 1, 2, 3}, // SPS - {8}, // PPS - }) - - w.WriteMPEG4Audio(track2, 2*90000, [][]byte{{1, 2, 3, 4}}) //nolint:errcheck - - w.WriteH26x(track1, 2*90000, 2*90000, true, [][]byte{ //nolint:errcheck - {5}, // IDR - }) -} - -func TestHLSSource(t *testing.T) { - ts, err := newTestHLSManager() - require.NoError(t, err) - defer ts.close() - - p, ok := newInstance("rtmp: no\n" + - "hls: no\n" + - "webrtc: no\n" + - "paths:\n" + - " proxied:\n" + - " source: http://localhost:5780/stream.m3u8\n" + - " sourceOnDemand: yes\n") - require.Equal(t, true, ok) - defer p.Close() - - frameRecv := make(chan struct{}) - - c := gortsplib.Client{} - - u, err := url.Parse("rtsp://localhost:8554/proxied") - require.NoError(t, err) - - err = c.Start(u.Scheme, u.Host) - require.NoError(t, err) - defer c.Close() - - desc, _, err := c.Describe(u) - require.NoError(t, err) - - require.Equal(t, []*description.Media{ - { - Type: description.MediaTypeVideo, - Control: desc.Medias[0].Control, - Formats: []format.Format{ - &format.H264{ - PayloadTyp: 96, - PacketizationMode: 1, - }, - }, - }, - { - Type: description.MediaTypeAudio, - Control: desc.Medias[1].Control, - Formats: []format.Format{ - &format.MPEG4Audio{ - PayloadTyp: 96, - ProfileLevelID: 1, - Config: &mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - }, - }, - }, - }, desc.Medias) - - var forma *format.H264 - medi := desc.FindFormat(&forma) - - _, err = c.Setup(desc.BaseURL, medi, 0, 0) - require.NoError(t, err) - - c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) { - require.Equal(t, &rtp.Packet{ - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: pkt.SequenceNumber, - Timestamp: pkt.Timestamp, - SSRC: pkt.SSRC, - CSRC: []uint32{}, - }, - Payload: []byte{ - 0x18, - 0x00, 0x04, - 0x07, 0x01, 0x02, 0x03, // SPS - 0x00, 0x01, - 0x08, // PPS - 0x00, 0x01, - 0x05, // IDR - }, - }, pkt) - close(frameRecv) - }) - - _, err = c.Play(nil) - require.NoError(t, err) - - close(ts.clientConnected) - - <-frameRecv -} diff --git a/internal/core/metrics.go b/internal/core/metrics.go index 6a3c57e3..1b3460f8 100644 --- a/internal/core/metrics.go +++ b/internal/core/metrics.go @@ -12,6 +12,7 @@ import ( "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/protocols/httpserv" + "github.com/bluenviron/mediamtx/internal/restrictnetwork" ) func metric(key string, tags string, value int64) string { @@ -49,7 +50,7 @@ func newMetrics( router.GET("/metrics", m.onMetrics) - network, address := restrictNetwork("tcp", address) + network, address := restrictnetwork.Restrict("tcp", address) var err error m.httpServer, err = httpserv.NewWrappedServer( diff --git a/internal/core/mpegts.go b/internal/core/mpegts.go index ab9f46cd..ec533245 100644 --- a/internal/core/mpegts.go +++ b/internal/core/mpegts.go @@ -2,215 +2,26 @@ package core import ( "bufio" - "errors" "fmt" "time" - "github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/mediacommon/pkg/codecs/ac3" "github.com/bluenviron/mediacommon/pkg/codecs/h264" "github.com/bluenviron/mediacommon/pkg/codecs/h265" - "github.com/bluenviron/mediacommon/pkg/formats/mpegts" + mcmpegts "github.com/bluenviron/mediacommon/pkg/formats/mpegts" "github.com/datarhei/gosrt" "github.com/bluenviron/mediamtx/internal/asyncwriter" + "github.com/bluenviron/mediamtx/internal/protocols/mpegts" "github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/unit" ) -var errMPEGTSNoTracks = errors.New("no supported tracks found (supported are H265, H264," + - " MPEG-4 Video, MPEG-1/2 Video, Opus, MPEG-4 Audio, MPEG-1 Audio, AC-3") - func durationGoToMPEGTS(v time.Duration) int64 { return int64(v.Seconds() * 90000) } -func mpegtsSetupRead(r *mpegts.Reader, stream **stream.Stream) ([]*description.Media, error) { - var medias []*description.Media //nolint:prealloc - - var td *mpegts.TimeDecoder - decodeTime := func(t int64) time.Duration { - if td == nil { - td = mpegts.NewTimeDecoder(t) - } - return td.Decode(t) - } - - for _, track := range r.Tracks() { //nolint:dupl - var medi *description.Media - - switch codec := track.Codec.(type) { - case *mpegts.CodecH265: - medi = &description.Media{ - Type: description.MediaTypeVideo, - Formats: []format.Format{&format.H265{ - PayloadTyp: 96, - }}, - } - - r.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) error { - (*stream).WriteUnit(medi, medi.Formats[0], &unit.H265{ - Base: unit.Base{ - NTP: time.Now(), - PTS: decodeTime(pts), - }, - AU: au, - }) - return nil - }) - - case *mpegts.CodecH264: - medi = &description.Media{ - Type: description.MediaTypeVideo, - Formats: []format.Format{&format.H264{ - PayloadTyp: 96, - PacketizationMode: 1, - }}, - } - - r.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) error { - (*stream).WriteUnit(medi, medi.Formats[0], &unit.H264{ - Base: unit.Base{ - NTP: time.Now(), - PTS: decodeTime(pts), - }, - AU: au, - }) - return nil - }) - - case *mpegts.CodecMPEG4Video: - medi = &description.Media{ - Type: description.MediaTypeVideo, - Formats: []format.Format{&format.MPEG4Video{ - PayloadTyp: 96, - }}, - } - - r.OnDataMPEGxVideo(track, func(pts int64, frame []byte) error { - (*stream).WriteUnit(medi, medi.Formats[0], &unit.MPEG4Video{ - Base: unit.Base{ - NTP: time.Now(), - PTS: decodeTime(pts), - }, - Frame: frame, - }) - return nil - }) - - case *mpegts.CodecMPEG1Video: - medi = &description.Media{ - Type: description.MediaTypeVideo, - Formats: []format.Format{&format.MPEG1Video{}}, - } - - r.OnDataMPEGxVideo(track, func(pts int64, frame []byte) error { - (*stream).WriteUnit(medi, medi.Formats[0], &unit.MPEG1Video{ - Base: unit.Base{ - NTP: time.Now(), - PTS: decodeTime(pts), - }, - Frame: frame, - }) - return nil - }) - - case *mpegts.CodecOpus: - medi = &description.Media{ - Type: description.MediaTypeAudio, - Formats: []format.Format{&format.Opus{ - PayloadTyp: 96, - IsStereo: (codec.ChannelCount == 2), - }}, - } - - r.OnDataOpus(track, func(pts int64, packets [][]byte) error { - (*stream).WriteUnit(medi, medi.Formats[0], &unit.Opus{ - Base: unit.Base{ - NTP: time.Now(), - PTS: decodeTime(pts), - }, - Packets: packets, - }) - return nil - }) - - case *mpegts.CodecMPEG4Audio: - medi = &description.Media{ - Type: description.MediaTypeAudio, - Formats: []format.Format{&format.MPEG4Audio{ - PayloadTyp: 96, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - Config: &codec.Config, - }}, - } - - r.OnDataMPEG4Audio(track, func(pts int64, aus [][]byte) error { - (*stream).WriteUnit(medi, medi.Formats[0], &unit.MPEG4Audio{ - Base: unit.Base{ - NTP: time.Now(), - PTS: decodeTime(pts), - }, - AUs: aus, - }) - return nil - }) - - case *mpegts.CodecMPEG1Audio: - medi = &description.Media{ - Type: description.MediaTypeAudio, - Formats: []format.Format{&format.MPEG1Audio{}}, - } - - r.OnDataMPEG1Audio(track, func(pts int64, frames [][]byte) error { - (*stream).WriteUnit(medi, medi.Formats[0], &unit.MPEG1Audio{ - Base: unit.Base{ - NTP: time.Now(), - PTS: decodeTime(pts), - }, - Frames: frames, - }) - return nil - }) - - case *mpegts.CodecAC3: - medi = &description.Media{ - Type: description.MediaTypeAudio, - Formats: []format.Format{&format.AC3{ - PayloadTyp: 96, - SampleRate: codec.SampleRate, - ChannelCount: codec.ChannelCount, - }}, - } - - r.OnDataAC3(track, func(pts int64, frame []byte) error { - (*stream).WriteUnit(medi, medi.Formats[0], &unit.AC3{ - Base: unit.Base{ - NTP: time.Now(), - PTS: decodeTime(pts), - }, - Frames: [][]byte{frame}, - }) - return nil - }) - - default: - continue - } - - medias = append(medias, medi) - } - - if len(medias) == 0 { - return nil, errMPEGTSNoTracks - } - - return medias, nil -} - func mpegtsSetupWrite( stream *stream.Stream, writer *asyncwriter.Writer, @@ -218,11 +29,11 @@ func mpegtsSetupWrite( sconn srt.Conn, writeTimeout time.Duration, ) error { - var w *mpegts.Writer - var tracks []*mpegts.Track + var w *mcmpegts.Writer + var tracks []*mcmpegts.Track - addTrack := func(codec mpegts.Codec) *mpegts.Track { - track := &mpegts.Track{ + addTrack := func(codec mcmpegts.Codec) *mcmpegts.Track { + track := &mcmpegts.Track{ Codec: codec, } tracks = append(tracks, track) @@ -233,7 +44,7 @@ func mpegtsSetupWrite( for _, forma := range medi.Formats { switch forma := forma.(type) { case *format.H265: //nolint:dupl - track := addTrack(&mpegts.CodecH265{}) + track := addTrack(&mcmpegts.CodecH265{}) var dtsExtractor *h265.DTSExtractor @@ -266,7 +77,7 @@ func mpegtsSetupWrite( }) case *format.H264: //nolint:dupl - track := addTrack(&mpegts.CodecH264{}) + track := addTrack(&mcmpegts.CodecH264{}) var dtsExtractor *h264.DTSExtractor @@ -299,7 +110,7 @@ func mpegtsSetupWrite( }) case *format.MPEG4Video: - track := addTrack(&mpegts.CodecMPEG4Video{}) + track := addTrack(&mcmpegts.CodecMPEG4Video{}) firstReceived := false var lastPTS time.Duration @@ -326,7 +137,7 @@ func mpegtsSetupWrite( }) case *format.MPEG1Video: - track := addTrack(&mpegts.CodecMPEG1Video{}) + track := addTrack(&mcmpegts.CodecMPEG1Video{}) firstReceived := false var lastPTS time.Duration @@ -353,7 +164,7 @@ func mpegtsSetupWrite( }) case *format.Opus: - track := addTrack(&mpegts.CodecOpus{ + track := addTrack(&mcmpegts.CodecOpus{ ChannelCount: func() int { if forma.IsStereo { return 2 @@ -377,7 +188,7 @@ func mpegtsSetupWrite( }) case *format.MPEG4Audio: - track := addTrack(&mpegts.CodecMPEG4Audio{ + track := addTrack(&mcmpegts.CodecMPEG4Audio{ Config: *forma.GetConfig(), }) @@ -396,7 +207,7 @@ func mpegtsSetupWrite( }) case *format.MPEG1Audio: - track := addTrack(&mpegts.CodecMPEG1Audio{}) + track := addTrack(&mcmpegts.CodecMPEG1Audio{}) stream.AddReader(writer, medi, forma, func(u unit.Unit) error { tunit := u.(*unit.MPEG1Audio) @@ -413,7 +224,7 @@ func mpegtsSetupWrite( }) case *format.AC3: - track := addTrack(&mpegts.CodecAC3{}) + track := addTrack(&mcmpegts.CodecAC3{}) sampleRate := time.Duration(forma.SampleRate) @@ -440,10 +251,10 @@ func mpegtsSetupWrite( } if len(tracks) == 0 { - return errMPEGTSNoTracks + return mpegts.ErrNoTracks } - w = mpegts.NewWriter(bw, tracks) + w = mcmpegts.NewWriter(bw, tracks) return nil } diff --git a/internal/core/path.go b/internal/core/path.go index d9ba8df9..7b57a94d 100644 --- a/internal/core/path.go +++ b/internal/core/path.go @@ -15,6 +15,7 @@ import ( "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/record" @@ -69,21 +70,6 @@ type pathAccessRequest struct { rtspNonce string } -type pathSourceStaticSetReadyRes struct { - stream *stream.Stream - err error -} - -type pathSourceStaticSetReadyReq struct { - desc *description.Session - generateRTPPackets bool - res chan pathSourceStaticSetReadyRes -} - -type pathSourceStaticSetNotReadyReq struct { - res chan struct{} -} - type pathRemoveReaderReq struct { author reader res chan struct{} @@ -157,7 +143,7 @@ type pathStopPublisherReq struct { } type pathAPIPathsListRes struct { - data *apiPathList + data *defs.APIPathList paths map[string]*path } @@ -167,7 +153,7 @@ type pathAPIPathsListReq struct { type pathAPIPathsGetRes struct { path *path - data *apiPath + data *defs.APIPath err error } @@ -212,8 +198,8 @@ type path struct { // in chReloadConf chan *conf.Path - chSourceStaticSetReady chan pathSourceStaticSetReadyReq - chSourceStaticSetNotReady chan pathSourceStaticSetNotReadyReq + chStaticSourceSetReady chan defs.PathSourceStaticSetReadyReq + chStaticSourceSetNotReady chan defs.PathSourceStaticSetNotReadyReq chDescribe chan pathDescribeReq chRemovePublisher chan pathRemovePublisherReq chAddPublisher chan pathAddPublisherReq @@ -265,8 +251,8 @@ func newPath( onDemandPublisherReadyTimer: newEmptyTimer(), onDemandPublisherCloseTimer: newEmptyTimer(), chReloadConf: make(chan *conf.Path), - chSourceStaticSetReady: make(chan pathSourceStaticSetReadyReq), - chSourceStaticSetNotReady: make(chan pathSourceStaticSetNotReadyReq), + chStaticSourceSetReady: make(chan defs.PathSourceStaticSetReadyReq), + chStaticSourceSetNotReady: make(chan defs.PathSourceStaticSetNotReadyReq), chDescribe: make(chan pathDescribeReq), chRemovePublisher: make(chan pathRemovePublisherReq), chAddPublisher: make(chan pathAddPublisherReq), @@ -306,7 +292,7 @@ func (pa *path) run() { if pa.conf.Source == "redirect" { pa.source = &sourceRedirect{} } else if pa.conf.HasStaticSource() { - pa.source = newSourceStatic( + pa.source = newStaticSourceHandler( pa.conf, pa.readTimeout, pa.writeTimeout, @@ -314,7 +300,7 @@ func (pa *path) run() { pa) if !pa.conf.SourceOnDemand { - pa.source.(*sourceStatic).start(false) + pa.source.(*staticSourceHandler).start(false) } } @@ -361,7 +347,7 @@ func (pa *path) run() { } if pa.source != nil { - if source, ok := pa.source.(*sourceStatic); ok { + if source, ok := pa.source.(*staticSourceHandler); ok { if !pa.conf.SourceOnDemand || pa.onDemandStaticSourceState != pathOnDemandStateInitial { source.close("path is closing") } @@ -411,10 +397,10 @@ func (pa *path) runInner() error { case newConf := <-pa.chReloadConf: pa.doReloadConf(newConf) - case req := <-pa.chSourceStaticSetReady: + case req := <-pa.chStaticSourceSetReady: pa.doSourceStaticSetReady(req) - case req := <-pa.chSourceStaticSetNotReady: + case req := <-pa.chStaticSourceSetNotReady: pa.doSourceStaticSetNotReady(req) if pa.shouldClose() { @@ -510,7 +496,7 @@ func (pa *path) doReloadConf(newConf *conf.Path) { pa.confMutex.Unlock() if pa.conf.HasStaticSource() { - go pa.source.(*sourceStatic).reloadConf(newConf) + go pa.source.(*staticSourceHandler).reloadConf(newConf) } if pa.conf.Record { @@ -523,10 +509,10 @@ func (pa *path) doReloadConf(newConf *conf.Path) { } } -func (pa *path) doSourceStaticSetReady(req pathSourceStaticSetReadyReq) { - err := pa.setReady(req.desc, req.generateRTPPackets) +func (pa *path) doSourceStaticSetReady(req defs.PathSourceStaticSetReadyReq) { + err := pa.setReady(req.Desc, req.GenerateRTPPackets) if err != nil { - req.res <- pathSourceStaticSetReadyRes{err: err} + req.Res <- defs.PathSourceStaticSetReadyRes{Err: err} return } @@ -549,15 +535,15 @@ func (pa *path) doSourceStaticSetReady(req pathSourceStaticSetReadyReq) { pa.readerAddRequestsOnHold = nil } - req.res <- pathSourceStaticSetReadyRes{stream: pa.stream} + req.Res <- defs.PathSourceStaticSetReadyRes{Stream: pa.stream} } -func (pa *path) doSourceStaticSetNotReady(req pathSourceStaticSetNotReadyReq) { +func (pa *path) doSourceStaticSetNotReady(req defs.PathSourceStaticSetNotReadyReq) { pa.setNotReady() // send response before calling onDemandStaticSourceStop() - // in order to avoid a deadlock due to sourceStatic.stop() - close(req.res) + // in order to avoid a deadlock due to staticSourceHandler.stop() + close(req.Res) if pa.conf.HasOnDemandStaticSource() && pa.onDemandStaticSourceState != pathOnDemandStateInitial { pa.onDemandStaticSourceStop("an error occurred") @@ -738,14 +724,14 @@ func (pa *path) doRemoveReader(req pathRemoveReaderReq) { func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) { req.res <- pathAPIPathsGetRes{ - data: &apiPath{ + data: &defs.APIPath{ Name: pa.name, ConfName: pa.confName, - Source: func() *apiPathSourceOrReader { + Source: func() *defs.APIPathSourceOrReader { if pa.source == nil { return nil } - v := pa.source.apiSourceDescribe() + v := pa.source.APISourceDescribe() return &v }(), Ready: pa.stream != nil, @@ -768,8 +754,8 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) { } return pa.stream.BytesReceived() }(), - Readers: func() []apiPathSourceOrReader { - ret := []apiPathSourceOrReader{} + Readers: func() []defs.APIPathSourceOrReader { + ret := []defs.APIPathSourceOrReader{} for r := range pa.readers { ret = append(ret, r.apiReaderDescribe()) } @@ -811,7 +797,7 @@ func (pa *path) externalCmdEnv() externalcmd.Environment { } func (pa *path) onDemandStaticSourceStart() { - pa.source.(*sourceStatic).start(true) + pa.source.(*staticSourceHandler).start(true) pa.onDemandStaticSourceReadyTimer.Stop() pa.onDemandStaticSourceReadyTimer = time.NewTimer(time.Duration(pa.conf.SourceOnDemandStartTimeout)) @@ -834,7 +820,7 @@ func (pa *path) onDemandStaticSourceStop(reason string) { pa.onDemandStaticSourceState = pathOnDemandStateInitial - pa.source.(*sourceStatic).stop(reason) + pa.source.(*staticSourceHandler).stop(reason) } func (pa *path) onDemandPublisherStart(query string) { @@ -1016,35 +1002,39 @@ func (pa *path) reloadConf(newConf *conf.Path) { } } -// sourceStaticSetReady is called by sourceStatic. -func (pa *path) sourceStaticSetReady(sourceStaticCtx context.Context, req pathSourceStaticSetReadyReq) { +// staticSourceHandlerSetReady is called by staticSourceHandler. +func (pa *path) staticSourceHandlerSetReady( + staticSourceHandlerCtx context.Context, req defs.PathSourceStaticSetReadyReq, +) { select { - case pa.chSourceStaticSetReady <- req: + case pa.chStaticSourceSetReady <- req: case <-pa.ctx.Done(): - req.res <- pathSourceStaticSetReadyRes{err: fmt.Errorf("terminated")} + req.Res <- defs.PathSourceStaticSetReadyRes{Err: fmt.Errorf("terminated")} // this avoids: // - invalid requests sent after the source has been terminated // - deadlocks caused by <-done inside stop() - case <-sourceStaticCtx.Done(): - req.res <- pathSourceStaticSetReadyRes{err: fmt.Errorf("terminated")} + case <-staticSourceHandlerCtx.Done(): + req.Res <- defs.PathSourceStaticSetReadyRes{Err: fmt.Errorf("terminated")} } } -// sourceStaticSetNotReady is called by sourceStatic. -func (pa *path) sourceStaticSetNotReady(sourceStaticCtx context.Context, req pathSourceStaticSetNotReadyReq) { +// staticSourceHandlerSetNotReady is called by staticSourceHandler. +func (pa *path) staticSourceHandlerSetNotReady( + staticSourceHandlerCtx context.Context, req defs.PathSourceStaticSetNotReadyReq, +) { select { - case pa.chSourceStaticSetNotReady <- req: + case pa.chStaticSourceSetNotReady <- req: case <-pa.ctx.Done(): - close(req.res) + close(req.Res) // this avoids: // - invalid requests sent after the source has been terminated // - deadlocks caused by <-done inside stop() - case <-sourceStaticCtx.Done(): - close(req.res) + case <-staticSourceHandlerCtx.Done(): + close(req.Res) } } @@ -1120,7 +1110,7 @@ func (pa *path) removeReader(req pathRemoveReaderReq) { } // apiPathsGet is called by api. -func (pa *path) apiPathsGet(req pathAPIPathsGetReq) (*apiPath, error) { +func (pa *path) apiPathsGet(req pathAPIPathsGetReq) (*defs.APIPath, error) { req.res = make(chan pathAPIPathsGetRes) select { case pa.chAPIPathsGet <- req: diff --git a/internal/core/path_manager.go b/internal/core/path_manager.go index 04b376d0..f5317572 100644 --- a/internal/core/path_manager.go +++ b/internal/core/path_manager.go @@ -7,6 +7,7 @@ import ( "sync" "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/logger" ) @@ -540,7 +541,7 @@ func (pm *pathManager) setHLSManager(s pathManagerHLSManager) { } // apiPathsList is called by api. -func (pm *pathManager) apiPathsList() (*apiPathList, error) { +func (pm *pathManager) apiPathsList() (*defs.APIPathList, error) { req := pathAPIPathsListReq{ res: make(chan pathAPIPathsListRes), } @@ -549,8 +550,8 @@ func (pm *pathManager) apiPathsList() (*apiPathList, error) { case pm.chAPIPathsList <- req: res := <-req.res - res.data = &apiPathList{ - Items: []*apiPath{}, + res.data = &defs.APIPathList{ + Items: []*defs.APIPath{}, } for _, pa := range res.paths { @@ -572,7 +573,7 @@ func (pm *pathManager) apiPathsList() (*apiPathList, error) { } // apiPathsGet is called by api. -func (pm *pathManager) apiPathsGet(name string) (*apiPath, error) { +func (pm *pathManager) apiPathsGet(name string) (*defs.APIPath, error) { req := pathAPIPathsGetReq{ name: name, res: make(chan pathAPIPathsGetRes), diff --git a/internal/core/pprof.go b/internal/core/pprof.go index 3ae13e74..e71d9397 100644 --- a/internal/core/pprof.go +++ b/internal/core/pprof.go @@ -10,6 +10,7 @@ import ( "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/protocols/httpserv" + "github.com/bluenviron/mediamtx/internal/restrictnetwork" ) type pprofParent interface { @@ -31,7 +32,7 @@ func newPPROF( parent: parent, } - network, address := restrictNetwork("tcp", address) + network, address := restrictnetwork.Restrict("tcp", address) var err error pp.httpServer, err = httpserv.NewWrappedServer( diff --git a/internal/core/reader.go b/internal/core/reader.go index 7c6de09b..dca897ee 100644 --- a/internal/core/reader.go +++ b/internal/core/reader.go @@ -3,6 +3,7 @@ package core import ( "github.com/bluenviron/mediamtx/internal/asyncwriter" "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/stream" @@ -11,7 +12,7 @@ import ( // reader is an entity that can read a stream. type reader interface { close() - apiReaderDescribe() apiPathSourceOrReader + apiReaderDescribe() defs.APIPathSourceOrReader } func readerMediaInfo(r *asyncwriter.Writer, stream *stream.Stream) string { @@ -22,7 +23,7 @@ func readerOnReadHook( externalCmdPool *externalcmd.Pool, pathConf *conf.Path, path *path, - reader apiPathSourceOrReader, + reader defs.APIPathSourceOrReader, query string, l logger.Writer, ) func() { diff --git a/internal/core/restrict_network.go b/internal/core/restrict_network.go deleted file mode 100644 index 0316eb4d..00000000 --- a/internal/core/restrict_network.go +++ /dev/null @@ -1,17 +0,0 @@ -package core - -import ( - "net" -) - -// do not listen on IPv6 when address is 0.0.0.0. -func restrictNetwork(network string, address string) (string, string) { - host, _, err := net.SplitHostPort(address) - if err == nil { - if host == "0.0.0.0" { - return network + "4", address - } - } - - return network, address -} diff --git a/internal/core/rtmp_conn.go b/internal/core/rtmp_conn.go index e69eb828..483da08c 100644 --- a/internal/core/rtmp_conn.go +++ b/internal/core/rtmp_conn.go @@ -19,6 +19,7 @@ import ( "github.com/bluenviron/mediamtx/internal/asyncwriter" "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/protocols/rtmp" @@ -585,8 +586,8 @@ func (c *rtmpConn) runPublish(conn *rtmp.Conn, u *url.URL) error { } // apiReaderDescribe implements reader. -func (c *rtmpConn) apiReaderDescribe() apiPathSourceOrReader { - return apiPathSourceOrReader{ +func (c *rtmpConn) apiReaderDescribe() defs.APIPathSourceOrReader { + return defs.APIPathSourceOrReader{ Type: func() string { if c.isTLS { return "rtmpsConn" @@ -597,12 +598,12 @@ func (c *rtmpConn) apiReaderDescribe() apiPathSourceOrReader { } } -// apiSourceDescribe implements source. -func (c *rtmpConn) apiSourceDescribe() apiPathSourceOrReader { +// APISourceDescribe implements source. +func (c *rtmpConn) APISourceDescribe() defs.APIPathSourceOrReader { return c.apiReaderDescribe() } -func (c *rtmpConn) apiItem() *apiRTMPConn { +func (c *rtmpConn) apiItem() *defs.APIRTMPConn { c.mutex.RLock() defer c.mutex.RUnlock() @@ -614,20 +615,20 @@ func (c *rtmpConn) apiItem() *apiRTMPConn { bytesSent = c.rconn.BytesSent() } - return &apiRTMPConn{ + return &defs.APIRTMPConn{ ID: c.uuid, Created: c.created, RemoteAddr: c.remoteAddr().String(), - State: func() apiRTMPConnState { + State: func() defs.APIRTMPConnState { switch c.state { case rtmpConnStateRead: - return apiRTMPConnStateRead + return defs.APIRTMPConnStateRead case rtmpConnStatePublish: - return apiRTMPConnStatePublish + return defs.APIRTMPConnStatePublish default: - return apiRTMPConnStateIdle + return defs.APIRTMPConnStateIdle } }(), Path: c.pathName, diff --git a/internal/core/rtmp_server.go b/internal/core/rtmp_server.go index ecfa9f9e..a1bc1a45 100644 --- a/internal/core/rtmp_server.go +++ b/internal/core/rtmp_server.go @@ -11,12 +11,14 @@ import ( "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 *apiRTMPConnList + data *defs.APIRTMPConnList err error } @@ -25,7 +27,7 @@ type rtmpServerAPIConnsListReq struct { } type rtmpServerAPIConnsGetRes struct { - data *apiRTMPConn + data *defs.APIRTMPConn err error } @@ -95,7 +97,7 @@ func newRTMPServer( ) (*rtmpServer, error) { ln, err := func() (net.Listener, error) { if !isTLS { - return net.Listen(restrictNetwork("tcp", address)) + return net.Listen(restrictnetwork.Restrict("tcp", address)) } cert, err := tls.LoadX509KeyPair(serverCert, serverKey) @@ -103,7 +105,7 @@ func newRTMPServer( return nil, err } - network, address := restrictNetwork("tcp", address) + network, address := restrictnetwork.Restrict("tcp", address) return tls.Listen(network, address, &tls.Config{Certificates: []tls.Certificate{cert}}) }() if err != nil { @@ -203,8 +205,8 @@ outer: delete(s.conns, c) case req := <-s.chAPIConnsList: - data := &apiRTMPConnList{ - Items: []*apiRTMPConn{}, + data := &defs.APIRTMPConnList{ + Items: []*defs.APIRTMPConn{}, } for c := range s.conns { @@ -286,7 +288,7 @@ func (s *rtmpServer) closeConn(c *rtmpConn) { } // apiConnsList is called by api. -func (s *rtmpServer) apiConnsList() (*apiRTMPConnList, error) { +func (s *rtmpServer) apiConnsList() (*defs.APIRTMPConnList, error) { req := rtmpServerAPIConnsListReq{ res: make(chan rtmpServerAPIConnsListRes), } @@ -302,7 +304,7 @@ func (s *rtmpServer) apiConnsList() (*apiRTMPConnList, error) { } // apiConnsGet is called by api. -func (s *rtmpServer) apiConnsGet(uuid uuid.UUID) (*apiRTMPConn, error) { +func (s *rtmpServer) apiConnsGet(uuid uuid.UUID) (*defs.APIRTMPConn, error) { req := rtmpServerAPIConnsGetReq{ uuid: uuid, res: make(chan rtmpServerAPIConnsGetRes), diff --git a/internal/core/rtmp_source_test.go b/internal/core/rtmp_source_test.go deleted file mode 100644 index a52f1408..00000000 --- a/internal/core/rtmp_source_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package core - -import ( - "crypto/tls" - "net" - "os" - "testing" - - "github.com/bluenviron/gortsplib/v4" - "github.com/bluenviron/gortsplib/v4/pkg/format" - "github.com/bluenviron/gortsplib/v4/pkg/url" - "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio" - "github.com/pion/rtp" - "github.com/stretchr/testify/require" - - "github.com/bluenviron/mediamtx/internal/protocols/rtmp" -) - -func TestRTMPSource(t *testing.T) { - for _, ca := range []string{ - "plain", - "tls", - } { - t.Run(ca, func(t *testing.T) { - ln, err := func() (net.Listener, error) { - if ca == "plain" { - return net.Listen("tcp", "127.0.0.1:1937") - } - - serverCertFpath, err := writeTempFile(serverCert) - require.NoError(t, err) - defer os.Remove(serverCertFpath) - - serverKeyFpath, err := writeTempFile(serverKey) - require.NoError(t, err) - defer os.Remove(serverKeyFpath) - - var cert tls.Certificate - cert, err = tls.LoadX509KeyPair(serverCertFpath, serverKeyFpath) - require.NoError(t, err) - - return tls.Listen("tcp", "127.0.0.1:1937", &tls.Config{Certificates: []tls.Certificate{cert}}) - }() - require.NoError(t, err) - defer ln.Close() - - connected := make(chan struct{}) - received := make(chan struct{}) - done := make(chan struct{}) - - go func() { - nconn, err := ln.Accept() - require.NoError(t, err) - defer nconn.Close() - - conn, _, _, err := rtmp.NewServerConn(nconn) - require.NoError(t, err) - - videoTrack := &format.H264{ - PayloadTyp: 96, - SPS: []byte{ // 1920x1080 baseline - 0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, - 0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, - 0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20, - }, - PPS: []byte{0x08, 0x06, 0x07, 0x08}, - PacketizationMode: 1, - } - - audioTrack := &format.MPEG4Audio{ - PayloadTyp: 96, - Config: &mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - } - - w, err := rtmp.NewWriter(conn, videoTrack, audioTrack) - require.NoError(t, err) - - <-connected - - err = w.WriteH264(0, 0, true, [][]byte{{0x05, 0x02, 0x03, 0x04}}) - require.NoError(t, err) - - <-done - }() - - if ca == "plain" { - p, ok := newInstance("paths:\n" + - " proxied:\n" + - " source: rtmp://localhost:1937/teststream\n" + - " sourceOnDemand: yes\n") - require.Equal(t, true, ok) - defer p.Close() - } else { - p, ok := newInstance("paths:\n" + - " proxied:\n" + - " source: rtmps://localhost:1937/teststream\n" + - " sourceFingerprint: 33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739\n" + - " sourceOnDemand: yes\n") - require.Equal(t, true, ok) - defer p.Close() - } - - c := gortsplib.Client{} - - u, err := url.Parse("rtsp://127.0.0.1:8554/proxied") - require.NoError(t, err) - - err = c.Start(u.Scheme, u.Host) - require.NoError(t, err) - defer c.Close() - - desc, _, err := c.Describe(u) - require.NoError(t, err) - - var forma *format.H264 - medi := desc.FindFormat(&forma) - - _, err = c.Setup(desc.BaseURL, medi, 0, 0) - require.NoError(t, err) - - c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) { - require.Equal(t, []byte{ - 0x18, 0x0, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9, - 0x0, 0x78, 0x2, 0x27, 0xe5, 0x84, 0x0, 0x0, - 0x3, 0x0, 0x4, 0x0, 0x0, 0x3, 0x0, 0xf0, - 0x3c, 0x60, 0xc9, 0x20, 0x0, 0x4, 0x8, 0x6, - 0x7, 0x8, 0x0, 0x4, 0x5, 0x2, 0x3, 0x4, - }, pkt.Payload) - close(received) - }) - - _, err = c.Play(nil) - require.NoError(t, err) - - close(connected) - <-received - close(done) - }) - } -} diff --git a/internal/core/rtsp_conn.go b/internal/core/rtsp_conn.go index 6bae8416..e27921d7 100644 --- a/internal/core/rtsp_conn.go +++ b/internal/core/rtsp_conn.go @@ -12,6 +12,7 @@ import ( "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" ) @@ -79,7 +80,7 @@ func newRTSPConn( c.Log(logger.Info, "opened") - c.conn.open(apiPathSourceOrReader{ + c.conn.open(defs.APIPathSourceOrReader{ Type: func() string { if isTLS { return "rtspsConn" @@ -113,7 +114,7 @@ func (c *rtspConn) ip() net.IP { func (c *rtspConn) onClose(err error) { c.Log(logger.Info, "closed: %v", err) - c.conn.close(apiPathSourceOrReader{ + c.conn.close(defs.APIPathSourceOrReader{ Type: func() string { if c.isTLS { return "rtspsConn" @@ -231,8 +232,8 @@ func (c *rtspConn) handleAuthError(authErr error) (*base.Response, error) { }, authErr } -func (c *rtspConn) apiItem() *apiRTSPConn { - return &apiRTSPConn{ +func (c *rtspConn) apiItem() *defs.APIRTSPConn { + return &defs.APIRTSPConn{ ID: c.uuid, Created: c.created, RemoteAddr: c.remoteAddr().String(), diff --git a/internal/core/rtsp_server.go b/internal/core/rtsp_server.go index aca7845f..da82bbe5 100644 --- a/internal/core/rtsp_server.go +++ b/internal/core/rtsp_server.go @@ -16,6 +16,7 @@ import ( "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" ) @@ -360,7 +361,7 @@ func (s *rtspServer) findSessionByUUID(uuid uuid.UUID) (*gortsplib.ServerSession } // apiConnsList is called by api and metrics. -func (s *rtspServer) apiConnsList() (*apiRTSPConnsList, error) { +func (s *rtspServer) apiConnsList() (*defs.APIRTSPConnsList, error) { select { case <-s.ctx.Done(): return nil, fmt.Errorf("terminated") @@ -370,8 +371,8 @@ func (s *rtspServer) apiConnsList() (*apiRTSPConnsList, error) { s.mutex.RLock() defer s.mutex.RUnlock() - data := &apiRTSPConnsList{ - Items: []*apiRTSPConn{}, + data := &defs.APIRTSPConnsList{ + Items: []*defs.APIRTSPConn{}, } for _, c := range s.conns { @@ -386,7 +387,7 @@ func (s *rtspServer) apiConnsList() (*apiRTSPConnsList, error) { } // apiConnsGet is called by api. -func (s *rtspServer) apiConnsGet(uuid uuid.UUID) (*apiRTSPConn, error) { +func (s *rtspServer) apiConnsGet(uuid uuid.UUID) (*defs.APIRTSPConn, error) { select { case <-s.ctx.Done(): return nil, fmt.Errorf("terminated") @@ -405,7 +406,7 @@ func (s *rtspServer) apiConnsGet(uuid uuid.UUID) (*apiRTSPConn, error) { } // apiSessionsList is called by api and metrics. -func (s *rtspServer) apiSessionsList() (*apiRTSPSessionList, error) { +func (s *rtspServer) apiSessionsList() (*defs.APIRTSPSessionList, error) { select { case <-s.ctx.Done(): return nil, fmt.Errorf("terminated") @@ -415,8 +416,8 @@ func (s *rtspServer) apiSessionsList() (*apiRTSPSessionList, error) { s.mutex.RLock() defer s.mutex.RUnlock() - data := &apiRTSPSessionList{ - Items: []*apiRTSPSession{}, + data := &defs.APIRTSPSessionList{ + Items: []*defs.APIRTSPSession{}, } for _, s := range s.sessions { @@ -431,7 +432,7 @@ func (s *rtspServer) apiSessionsList() (*apiRTSPSessionList, error) { } // apiSessionsGet is called by api. -func (s *rtspServer) apiSessionsGet(uuid uuid.UUID) (*apiRTSPSession, error) { +func (s *rtspServer) apiSessionsGet(uuid uuid.UUID) (*defs.APIRTSPSession, error) { select { case <-s.ctx.Done(): return nil, fmt.Errorf("terminated") diff --git a/internal/core/rtsp_session.go b/internal/core/rtsp_session.go index ec4221af..b9be91d2 100644 --- a/internal/core/rtsp_session.go +++ b/internal/core/rtsp_session.go @@ -15,6 +15,7 @@ import ( "github.com/pion/rtp" "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/stream" @@ -377,8 +378,8 @@ func (s *rtspSession) onPause(_ *gortsplib.ServerHandlerOnPauseCtx) (*base.Respo } // apiReaderDescribe implements reader. -func (s *rtspSession) apiReaderDescribe() apiPathSourceOrReader { - return apiPathSourceOrReader{ +func (s *rtspSession) apiReaderDescribe() defs.APIPathSourceOrReader { + return defs.APIPathSourceOrReader{ Type: func() string { if s.isTLS { return "rtspsSession" @@ -389,8 +390,8 @@ func (s *rtspSession) apiReaderDescribe() apiPathSourceOrReader { } } -// apiSourceDescribe implements source. -func (s *rtspSession) apiSourceDescribe() apiPathSourceOrReader { +// APISourceDescribe implements source. +func (s *rtspSession) APISourceDescribe() defs.APIPathSourceOrReader { return s.apiReaderDescribe() } @@ -409,25 +410,25 @@ func (s *rtspSession) onStreamWriteError(ctx *gortsplib.ServerHandlerOnStreamWri s.writeErrLogger.Log(logger.Warn, ctx.Error.Error()) } -func (s *rtspSession) apiItem() *apiRTSPSession { +func (s *rtspSession) apiItem() *defs.APIRTSPSession { s.mutex.Lock() defer s.mutex.Unlock() - return &apiRTSPSession{ + return &defs.APIRTSPSession{ ID: s.uuid, Created: s.created, RemoteAddr: s.remoteAddr().String(), - State: func() apiRTSPSessionState { + State: func() defs.APIRTSPSessionState { switch s.state { case gortsplib.ServerSessionStatePrePlay, gortsplib.ServerSessionStatePlay: - return apiRTSPSessionStateRead + return defs.APIRTSPSessionStateRead case gortsplib.ServerSessionStatePreRecord, gortsplib.ServerSessionStateRecord: - return apiRTSPSessionStatePublish + return defs.APIRTSPSessionStatePublish } - return apiRTSPSessionStateIdle + return defs.APIRTSPSessionStateIdle }(), Path: s.pathName, Transport: func() *string { diff --git a/internal/core/rtsp_source_test.go b/internal/core/rtsp_source_test.go deleted file mode 100644 index 6e203cfb..00000000 --- a/internal/core/rtsp_source_test.go +++ /dev/null @@ -1,312 +0,0 @@ -package core - -import ( - "crypto/tls" - "os" - "testing" - "time" - - "github.com/bluenviron/gortsplib/v4" - "github.com/bluenviron/gortsplib/v4/pkg/auth" - "github.com/bluenviron/gortsplib/v4/pkg/base" - "github.com/bluenviron/gortsplib/v4/pkg/description" - "github.com/bluenviron/gortsplib/v4/pkg/format" - "github.com/bluenviron/gortsplib/v4/pkg/url" - "github.com/pion/rtp" - "github.com/stretchr/testify/require" -) - -type testServer struct { - onDescribe func(*gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) - onSetup func(*gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) - onPlay func(*gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) -} - -func (sh *testServer) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, -) (*base.Response, *gortsplib.ServerStream, error) { - return sh.onDescribe(ctx) -} - -func (sh *testServer) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) { - return sh.onSetup(ctx) -} - -func (sh *testServer) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { - return sh.onPlay(ctx) -} - -func TestRTSPSource(t *testing.T) { - for _, source := range []string{ - "udp", - "tcp", - "tls", - } { - t.Run(source, func(t *testing.T) { - serverMedia := testMediaH264 - var stream *gortsplib.ServerStream - - nonce, err := auth.GenerateNonce() - require.NoError(t, err) - - s := gortsplib.Server{ - Handler: &testServer{ - onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx, - ) (*base.Response, *gortsplib.ServerStream, error) { - err := auth.Validate(ctx.Request, "testuser", "testpass", nil, nil, "IPCAM", nonce) - if err != nil { - return &base.Response{ //nolint:nilerr - StatusCode: base.StatusUnauthorized, - Header: base.Header{ - "WWW-Authenticate": auth.GenerateWWWAuthenticate(nil, "IPCAM", nonce), - }, - }, nil, nil - } - - return &base.Response{ - StatusCode: base.StatusOK, - }, stream, nil - }, - onSetup: func(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) { - return &base.Response{ - StatusCode: base.StatusOK, - }, stream, nil - }, - onPlay: func(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { - go func() { - time.Sleep(1 * time.Second) - err := stream.WritePacketRTP(serverMedia, &rtp.Packet{ - Header: rtp.Header{ - Version: 0x02, - PayloadType: 96, - SequenceNumber: 57899, - Timestamp: 345234345, - SSRC: 978651231, - Marker: true, - }, - Payload: []byte{5, 1, 2, 3, 4}, - }) - require.NoError(t, err) - }() - - return &base.Response{ - StatusCode: base.StatusOK, - }, nil - }, - }, - RTSPAddress: "127.0.0.1:8555", - } - - switch source { - case "udp": - s.UDPRTPAddress = "127.0.0.1:8002" - s.UDPRTCPAddress = "127.0.0.1:8003" - - case "tls": - serverCertFpath, err := writeTempFile(serverCert) - require.NoError(t, err) - defer os.Remove(serverCertFpath) - - serverKeyFpath, err := writeTempFile(serverKey) - require.NoError(t, err) - defer os.Remove(serverKeyFpath) - - cert, err := tls.LoadX509KeyPair(serverCertFpath, serverKeyFpath) - require.NoError(t, err) - - s.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}} - } - - err = s.Start() - require.NoError(t, err) - defer s.Wait() //nolint:errcheck - defer s.Close() - - stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{serverMedia}}) - defer stream.Close() - - if source == "udp" || source == "tcp" { - p, ok := newInstance("paths:\n" + - " proxied:\n" + - " source: rtsp://testuser:testpass@localhost:8555/teststream\n" + - " sourceProtocol: " + source + "\n" + - " sourceOnDemand: yes\n") - require.Equal(t, true, ok) - defer p.Close() - } else { - p, ok := newInstance("paths:\n" + - " proxied:\n" + - " source: rtsps://testuser:testpass@localhost:8555/teststream\n" + - " sourceFingerprint: 33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739\n" + - " sourceOnDemand: yes\n") - require.Equal(t, true, ok) - defer p.Close() - } - - received := make(chan struct{}) - - c := gortsplib.Client{} - - u, err := url.Parse("rtsp://127.0.0.1:8554/proxied") - require.NoError(t, err) - - err = c.Start(u.Scheme, u.Host) - require.NoError(t, err) - defer c.Close() - - desc, _, err := c.Describe(u) - require.NoError(t, err) - - var forma *format.H264 - medi := desc.FindFormat(&forma) - - _, err = c.Setup(desc.BaseURL, medi, 0, 0) - require.NoError(t, err) - - c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) { - require.Equal(t, []byte{5, 1, 2, 3, 4}, pkt.Payload) - close(received) - }) - - _, err = c.Play(nil) - require.NoError(t, err) - - <-received - }) - } -} - -func TestRTSPSourceNoPassword(t *testing.T) { - var stream *gortsplib.ServerStream - - nonce, err := auth.GenerateNonce() - require.NoError(t, err) - - done := make(chan struct{}) - - s := gortsplib.Server{ - Handler: &testServer{ - onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) { - err := auth.Validate(ctx.Request, "testuser", "", nil, nil, "IPCAM", nonce) - if err != nil { - return &base.Response{ //nolint:nilerr - StatusCode: base.StatusUnauthorized, - Header: base.Header{ - "WWW-Authenticate": auth.GenerateWWWAuthenticate(nil, "IPCAM", nonce), - }, - }, nil, nil - } - - return &base.Response{ - StatusCode: base.StatusOK, - }, stream, nil - }, - onSetup: func(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) { - close(done) - return &base.Response{ - StatusCode: base.StatusOK, - }, stream, nil - }, - onPlay: func(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { - return &base.Response{ - StatusCode: base.StatusOK, - }, nil - }, - }, - RTSPAddress: "127.0.0.1:8555", - } - - err = s.Start() - require.NoError(t, err) - defer s.Wait() //nolint:errcheck - defer s.Close() - - stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{testMediaH264}}) - defer stream.Close() - - p, ok := newInstance("rtmp: no\n" + - "hls: no\n" + - "webrtc: no\n" + - "paths:\n" + - " proxied:\n" + - " source: rtsp://testuser:@127.0.0.1:8555/teststream\n" + - " sourceProtocol: tcp\n") - require.Equal(t, true, ok) - defer p.Close() - - <-done -} - -func TestRTSPSourceRange(t *testing.T) { - for _, ca := range []string{"clock", "npt", "smpte"} { - t.Run(ca, func(t *testing.T) { - var stream *gortsplib.ServerStream - done := make(chan struct{}) - - s := gortsplib.Server{ - Handler: &testServer{ - onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) { - return &base.Response{ - StatusCode: base.StatusOK, - }, stream, nil - }, - onSetup: func(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) { - return &base.Response{ - StatusCode: base.StatusOK, - }, stream, nil - }, - onPlay: func(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { - switch ca { - case "clock": - require.Equal(t, base.HeaderValue{"clock=20230812T120000Z-"}, ctx.Request.Header["Range"]) - - case "npt": - require.Equal(t, base.HeaderValue{"npt=0.35-"}, ctx.Request.Header["Range"]) - - case "smpte": - require.Equal(t, base.HeaderValue{"smpte=0:02:10-"}, ctx.Request.Header["Range"]) - } - - close(done) - return &base.Response{ - StatusCode: base.StatusOK, - }, nil - }, - }, - RTSPAddress: "127.0.0.1:8555", - } - - err := s.Start() - require.NoError(t, err) - defer s.Wait() //nolint:errcheck - defer s.Close() - - stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{testMediaH264}}) - defer stream.Close() - - var addConf string - switch ca { - case "clock": - addConf += " rtspRangeType: clock\n" + - " rtspRangeStart: 20230812T120000Z\n" - - case "npt": - addConf += " rtspRangeType: npt\n" + - " rtspRangeStart: 350ms\n" - - case "smpte": - addConf += " rtspRangeType: smpte\n" + - " rtspRangeStart: 130s\n" - } - p, ok := newInstance("rtmp: no\n" + - "hls: no\n" + - "webrtc: no\n" + - "paths:\n" + - " proxied:\n" + - " source: rtsp://testuser:@127.0.0.1:8555/teststream\n" + addConf) - require.Equal(t, true, ok) - defer p.Close() - - <-done - }) - } -} diff --git a/internal/core/source.go b/internal/core/source.go index cd181fc3..fa8e6647 100644 --- a/internal/core/source.go +++ b/internal/core/source.go @@ -6,18 +6,19 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/logger" ) // source is an entity that can provide a stream. // it can be: -// - a publisher -// - sourceStatic -// - sourceRedirect +// - publisher +// - staticSourceHandler +// - redirectSource type source interface { logger.Writer - apiSourceDescribe() apiPathSourceOrReader + APISourceDescribe() defs.APIPathSourceOrReader } func mediaDescription(media *description.Media) string { @@ -54,7 +55,7 @@ func sourceOnReadyHook(path *path) func() { if path.conf.RunOnReady != "" { env = path.externalCmdEnv() - desc := path.source.apiSourceDescribe() + desc := path.source.APISourceDescribe() env["MTX_QUERY"] = path.publisherQuery env["MTX_SOURCE_TYPE"] = desc.Type env["MTX_SOURCE_ID"] = desc.ID diff --git a/internal/core/source_redirect.go b/internal/core/source_redirect.go index 4931b20d..9428430d 100644 --- a/internal/core/source_redirect.go +++ b/internal/core/source_redirect.go @@ -1,6 +1,7 @@ package core import ( + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/logger" ) @@ -10,9 +11,9 @@ type sourceRedirect struct{} func (*sourceRedirect) Log(logger.Level, string, ...interface{}) { } -// apiSourceDescribe implements source. -func (*sourceRedirect) apiSourceDescribe() apiPathSourceOrReader { - return apiPathSourceOrReader{ +// APISourceDescribe implements source. +func (*sourceRedirect) APISourceDescribe() defs.APIPathSourceOrReader { + return defs.APIPathSourceOrReader{ Type: "redirect", ID: "", } diff --git a/internal/core/source_static.go b/internal/core/source_static.go deleted file mode 100644 index 5e4446c4..00000000 --- a/internal/core/source_static.go +++ /dev/null @@ -1,249 +0,0 @@ -package core - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/bluenviron/mediamtx/internal/conf" - "github.com/bluenviron/mediamtx/internal/logger" -) - -const ( - sourceStaticRetryPause = 5 * time.Second -) - -type sourceStaticImpl interface { - logger.Writer - run(context.Context, *conf.Path, chan *conf.Path) error - apiSourceDescribe() apiPathSourceOrReader -} - -type sourceStaticParent interface { - logger.Writer - sourceStaticSetReady(context.Context, pathSourceStaticSetReadyReq) - sourceStaticSetNotReady(context.Context, pathSourceStaticSetNotReadyReq) -} - -// sourceStatic is a static source. -type sourceStatic struct { - conf *conf.Path - parent sourceStaticParent - - ctx context.Context - ctxCancel func() - impl sourceStaticImpl - running bool - - // in - chReloadConf chan *conf.Path - chSourceStaticImplSetReady chan pathSourceStaticSetReadyReq - chSourceStaticImplSetNotReady chan pathSourceStaticSetNotReadyReq - - // out - done chan struct{} -} - -func newSourceStatic( - cnf *conf.Path, - readTimeout conf.StringDuration, - writeTimeout conf.StringDuration, - writeQueueSize int, - parent sourceStaticParent, -) *sourceStatic { - s := &sourceStatic{ - conf: cnf, - parent: parent, - chReloadConf: make(chan *conf.Path), - chSourceStaticImplSetReady: make(chan pathSourceStaticSetReadyReq), - chSourceStaticImplSetNotReady: make(chan pathSourceStaticSetNotReadyReq), - } - - switch { - case strings.HasPrefix(cnf.Source, "rtsp://") || - strings.HasPrefix(cnf.Source, "rtsps://"): - s.impl = newRTSPSource( - readTimeout, - writeTimeout, - writeQueueSize, - s) - - case strings.HasPrefix(cnf.Source, "rtmp://") || - strings.HasPrefix(cnf.Source, "rtmps://"): - s.impl = newRTMPSource( - readTimeout, - writeTimeout, - s) - - case strings.HasPrefix(cnf.Source, "http://") || - strings.HasPrefix(cnf.Source, "https://"): - s.impl = newHLSSource( - s) - - case strings.HasPrefix(cnf.Source, "udp://"): - s.impl = newUDPSource( - readTimeout, - s) - - case strings.HasPrefix(cnf.Source, "srt://"): - s.impl = newSRTSource( - readTimeout, - s) - - case strings.HasPrefix(cnf.Source, "whep://") || - strings.HasPrefix(cnf.Source, "wheps://"): - s.impl = newWebRTCSource( - readTimeout, - s) - - case cnf.Source == "rpiCamera": - s.impl = newRPICameraSource( - s) - } - - return s -} - -func (s *sourceStatic) close(reason string) { - s.stop(reason) -} - -func (s *sourceStatic) start(onDemand bool) { - if s.running { - panic("should not happen") - } - - s.running = true - s.impl.Log(logger.Info, "started%s", - func() string { - if onDemand { - return " on demand" - } - return "" - }()) - - s.ctx, s.ctxCancel = context.WithCancel(context.Background()) - s.done = make(chan struct{}) - - go s.run() -} - -func (s *sourceStatic) stop(reason string) { - if !s.running { - panic("should not happen") - } - - s.running = false - s.impl.Log(logger.Info, "stopped: %s", reason) - - s.ctxCancel() - - // we must wait since s.ctx is not thread safe - <-s.done -} - -func (s *sourceStatic) Log(level logger.Level, format string, args ...interface{}) { - s.parent.Log(level, format, args...) -} - -func (s *sourceStatic) run() { - defer close(s.done) - - var innerCtx context.Context - var innerCtxCancel func() - implErr := make(chan error) - innerReloadConf := make(chan *conf.Path) - - recreate := func() { - innerCtx, innerCtxCancel = context.WithCancel(context.Background()) - go func() { - implErr <- s.impl.run(innerCtx, s.conf, innerReloadConf) - }() - } - - recreate() - - recreating := false - recreateTimer := newEmptyTimer() - - for { - select { - case err := <-implErr: - innerCtxCancel() - s.impl.Log(logger.Error, err.Error()) - recreating = true - recreateTimer = time.NewTimer(sourceStaticRetryPause) - - case newConf := <-s.chReloadConf: - s.conf = newConf - if !recreating { - cReloadConf := innerReloadConf - cInnerCtx := innerCtx - go func() { - select { - case cReloadConf <- newConf: - case <-cInnerCtx.Done(): - } - }() - } - - case req := <-s.chSourceStaticImplSetReady: - s.parent.sourceStaticSetReady(s.ctx, req) - - case req := <-s.chSourceStaticImplSetNotReady: - s.parent.sourceStaticSetNotReady(s.ctx, req) - - case <-recreateTimer.C: - recreate() - recreating = false - - case <-s.ctx.Done(): - if !recreating { - innerCtxCancel() - <-implErr - } - return - } - } -} - -func (s *sourceStatic) reloadConf(newConf *conf.Path) { - select { - case s.chReloadConf <- newConf: - case <-s.ctx.Done(): - } -} - -// apiSourceDescribe implements source. -func (s *sourceStatic) apiSourceDescribe() apiPathSourceOrReader { - return s.impl.apiSourceDescribe() -} - -// setReady is called by a sourceStaticImpl. -func (s *sourceStatic) setReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes { - req.res = make(chan pathSourceStaticSetReadyRes) - select { - case s.chSourceStaticImplSetReady <- req: - res := <-req.res - - if res.err == nil { - s.impl.Log(logger.Info, "ready: %s", mediaInfo(req.desc.Medias)) - } - - return res - - case <-s.ctx.Done(): - return pathSourceStaticSetReadyRes{err: fmt.Errorf("terminated")} - } -} - -// setNotReady is called by a sourceStaticImpl. -func (s *sourceStatic) setNotReady(req pathSourceStaticSetNotReadyReq) { - req.res = make(chan struct{}) - select { - case s.chSourceStaticImplSetNotReady <- req: - <-req.res - case <-s.ctx.Done(): - } -} diff --git a/internal/core/srt_conn.go b/internal/core/srt_conn.go index 19a92ab5..50dffa6c 100644 --- a/internal/core/srt_conn.go +++ b/internal/core/srt_conn.go @@ -11,14 +11,16 @@ import ( "time" "github.com/bluenviron/gortsplib/v4/pkg/description" - "github.com/bluenviron/mediacommon/pkg/formats/mpegts" + mcmpegts "github.com/bluenviron/mediacommon/pkg/formats/mpegts" "github.com/datarhei/gosrt" "github.com/google/uuid" "github.com/bluenviron/mediamtx/internal/asyncwriter" "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/protocols/mpegts" "github.com/bluenviron/mediamtx/internal/stream" ) @@ -266,7 +268,7 @@ func (c *srtConn) runPublish(req srtNewConnReq, pathName string, user string, pa func (c *srtConn) runPublishReader(sconn srt.Conn, path *path) error { sconn.SetReadDeadline(time.Now().Add(time.Duration(c.readTimeout))) - r, err := mpegts.NewReader(mpegts.NewBufferedReader(sconn)) + r, err := mcmpegts.NewReader(mcmpegts.NewBufferedReader(sconn)) if err != nil { return err } @@ -279,7 +281,7 @@ func (c *srtConn) runPublishReader(sconn srt.Conn, path *path) error { var stream *stream.Stream - medias, err := mpegtsSetupRead(r, &stream) + medias, err := mpegts.ToStream(r, &stream) if err != nil { return err } @@ -418,19 +420,19 @@ func (c *srtConn) setConn(sconn srt.Conn) { } // apiReaderDescribe implements reader. -func (c *srtConn) apiReaderDescribe() apiPathSourceOrReader { - return apiPathSourceOrReader{ +func (c *srtConn) apiReaderDescribe() defs.APIPathSourceOrReader { + return defs.APIPathSourceOrReader{ Type: "srtConn", ID: c.uuid.String(), } } -// apiSourceDescribe implements source. -func (c *srtConn) apiSourceDescribe() apiPathSourceOrReader { +// APISourceDescribe implements source. +func (c *srtConn) APISourceDescribe() defs.APIPathSourceOrReader { return c.apiReaderDescribe() } -func (c *srtConn) apiItem() *apiSRTConn { +func (c *srtConn) apiItem() *defs.APISRTConn { c.mutex.RLock() defer c.mutex.RUnlock() @@ -444,20 +446,20 @@ func (c *srtConn) apiItem() *apiSRTConn { bytesSent = s.Accumulated.ByteSent } - return &apiSRTConn{ + return &defs.APISRTConn{ ID: c.uuid, Created: c.created, RemoteAddr: c.connReq.RemoteAddr().String(), - State: func() apiSRTConnState { + State: func() defs.APISRTConnState { switch c.state { case srtConnStateRead: - return apiSRTConnStateRead + return defs.APISRTConnStateRead case srtConnStatePublish: - return apiSRTConnStatePublish + return defs.APISRTConnStatePublish default: - return apiSRTConnStateIdle + return defs.APISRTConnStateIdle } }(), Path: c.pathName, diff --git a/internal/core/srt_server.go b/internal/core/srt_server.go index 538c9206..2c482d13 100644 --- a/internal/core/srt_server.go +++ b/internal/core/srt_server.go @@ -11,6 +11,7 @@ import ( "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" ) @@ -25,7 +26,7 @@ type srtNewConnReq struct { } type srtServerAPIConnsListRes struct { - data *apiSRTConnList + data *defs.APISRTConnList err error } @@ -34,7 +35,7 @@ type srtServerAPIConnsListReq struct { } type srtServerAPIConnsGetRes struct { - data *apiSRTConn + data *defs.APISRTConn err error } @@ -191,8 +192,8 @@ outer: delete(s.conns, c) case req := <-s.chAPIConnsList: - data := &apiSRTConnList{ - Items: []*apiSRTConn{}, + data := &defs.APISRTConnList{ + Items: []*defs.APISRTConn{}, } for c := range s.conns { @@ -279,7 +280,7 @@ func (s *srtServer) closeConn(c *srtConn) { } // apiConnsList is called by api. -func (s *srtServer) apiConnsList() (*apiSRTConnList, error) { +func (s *srtServer) apiConnsList() (*defs.APISRTConnList, error) { req := srtServerAPIConnsListReq{ res: make(chan srtServerAPIConnsListRes), } @@ -295,7 +296,7 @@ func (s *srtServer) apiConnsList() (*apiSRTConnList, error) { } // apiConnsGet is called by api. -func (s *srtServer) apiConnsGet(uuid uuid.UUID) (*apiSRTConn, error) { +func (s *srtServer) apiConnsGet(uuid uuid.UUID) (*defs.APISRTConn, error) { req := srtServerAPIConnsGetReq{ uuid: uuid, res: make(chan srtServerAPIConnsGetRes), diff --git a/internal/core/srt_source.go b/internal/core/srt_source.go deleted file mode 100644 index be203a55..00000000 --- a/internal/core/srt_source.go +++ /dev/null @@ -1,129 +0,0 @@ -package core - -import ( - "context" - "time" - - "github.com/bluenviron/gortsplib/v4/pkg/description" - "github.com/bluenviron/mediacommon/pkg/formats/mpegts" - "github.com/datarhei/gosrt" - - "github.com/bluenviron/mediamtx/internal/conf" - "github.com/bluenviron/mediamtx/internal/logger" - "github.com/bluenviron/mediamtx/internal/stream" -) - -type srtSourceParent interface { - logger.Writer - setReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes - setNotReady(req pathSourceStaticSetNotReadyReq) -} - -type srtSource struct { - readTimeout conf.StringDuration - parent srtSourceParent -} - -func newSRTSource( - readTimeout conf.StringDuration, - parent srtSourceParent, -) *srtSource { - s := &srtSource{ - readTimeout: readTimeout, - parent: parent, - } - - return s -} - -func (s *srtSource) Log(level logger.Level, format string, args ...interface{}) { - s.parent.Log(level, "[SRT source] "+format, args...) -} - -// run implements sourceStaticImpl. -func (s *srtSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *conf.Path) error { - s.Log(logger.Debug, "connecting") - - conf := srt.DefaultConfig() - address, err := conf.UnmarshalURL(cnf.Source) - if err != nil { - return err - } - - err = conf.Validate() - if err != nil { - return err - } - - sconn, err := srt.Dial("srt", address, conf) - if err != nil { - return err - } - - readDone := make(chan error) - go func() { - readDone <- s.runReader(sconn) - }() - - for { - select { - case err := <-readDone: - sconn.Close() - return err - - case <-reloadConf: - - case <-ctx.Done(): - sconn.Close() - <-readDone - return nil - } - } -} - -func (s *srtSource) runReader(sconn srt.Conn) error { - sconn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout))) - r, err := mpegts.NewReader(mpegts.NewBufferedReader(sconn)) - if err != nil { - return err - } - - decodeErrLogger := logger.NewLimitedLogger(s) - - r.OnDecodeError(func(err error) { - decodeErrLogger.Log(logger.Warn, err.Error()) - }) - - var stream *stream.Stream - - medias, err := mpegtsSetupRead(r, &stream) - if err != nil { - return err - } - - res := s.parent.setReady(pathSourceStaticSetReadyReq{ - desc: &description.Session{Medias: medias}, - generateRTPPackets: true, - }) - if res.err != nil { - return res.err - } - - stream = res.stream - - for { - sconn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout))) - err := r.Read() - if err != nil { - return err - } - } -} - -// apiSourceDescribe implements sourceStaticImpl. -func (*srtSource) apiSourceDescribe() apiPathSourceOrReader { - return apiPathSourceOrReader{ - Type: "srtSource", - ID: "", - } -} diff --git a/internal/core/srt_source_test.go b/internal/core/srt_source_test.go deleted file mode 100644 index bb67aa25..00000000 --- a/internal/core/srt_source_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package core - -import ( - "bufio" - "testing" - - "github.com/bluenviron/gortsplib/v4" - "github.com/bluenviron/gortsplib/v4/pkg/format" - "github.com/bluenviron/gortsplib/v4/pkg/url" - "github.com/bluenviron/mediacommon/pkg/formats/mpegts" - "github.com/datarhei/gosrt" - "github.com/pion/rtp" - "github.com/stretchr/testify/require" -) - -func TestSRTSource(t *testing.T) { - ln, err := srt.Listen("srt", "localhost:9999", srt.DefaultConfig()) - require.NoError(t, err) - defer ln.Close() - - connected := make(chan struct{}) - received := make(chan struct{}) - done := make(chan struct{}) - - go func() { - conn, _, err := ln.Accept(func(req srt.ConnRequest) srt.ConnType { - require.Equal(t, "sidname", req.StreamId()) - - err := req.SetPassphrase("ttest1234567") - if err != nil { - return srt.REJECT - } - - return srt.SUBSCRIBE - }) - require.NoError(t, err) - require.NotNil(t, conn) - defer conn.Close() - - track := &mpegts.Track{ - Codec: &mpegts.CodecH264{}, - } - - bw := bufio.NewWriter(conn) - w := mpegts.NewWriter(bw, []*mpegts.Track{track}) - require.NoError(t, err) - - err = w.WriteH26x(track, 0, 0, true, [][]byte{ - { // IDR - 0x05, 1, - }, - }) - require.NoError(t, err) - - err = bw.Flush() - require.NoError(t, err) - - <-connected - - err = w.WriteH26x(track, 0, 0, true, [][]byte{{5, 2}}) - require.NoError(t, err) - - err = bw.Flush() - require.NoError(t, err) - - <-done - }() - - p, ok := newInstance("paths:\n" + - " proxied:\n" + - " source: srt://localhost:9999?streamid=sidname&passphrase=ttest1234567\n" + - " sourceOnDemand: yes\n") - require.Equal(t, true, ok) - defer p.Close() - - c := gortsplib.Client{} - - u, err := url.Parse("rtsp://127.0.0.1:8554/proxied") - require.NoError(t, err) - - err = c.Start(u.Scheme, u.Host) - require.NoError(t, err) - defer c.Close() - - desc, _, err := c.Describe(u) - require.NoError(t, err) - - var forma *format.H264 - medi := desc.FindFormat(&forma) - - _, err = c.Setup(desc.BaseURL, medi, 0, 0) - require.NoError(t, err) - - c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) { - require.Equal(t, []byte{5, 1}, pkt.Payload) - close(received) - }) - - _, err = c.Play(nil) - require.NoError(t, err) - - close(connected) - <-received - close(done) -} diff --git a/internal/core/static_source_handler.go b/internal/core/static_source_handler.go new file mode 100644 index 00000000..cec9961e --- /dev/null +++ b/internal/core/static_source_handler.go @@ -0,0 +1,262 @@ +package core + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" + "github.com/bluenviron/mediamtx/internal/logger" + hlssource "github.com/bluenviron/mediamtx/internal/staticsources/hls" + rpicamerasource "github.com/bluenviron/mediamtx/internal/staticsources/rpicamera" + rtmpsource "github.com/bluenviron/mediamtx/internal/staticsources/rtmp" + rtspsource "github.com/bluenviron/mediamtx/internal/staticsources/rtsp" + srtsource "github.com/bluenviron/mediamtx/internal/staticsources/srt" + udpsource "github.com/bluenviron/mediamtx/internal/staticsources/udp" + webrtcsource "github.com/bluenviron/mediamtx/internal/staticsources/webrtc" +) + +const ( + staticSourceHandlerRetryPause = 5 * time.Second +) + +type staticSourceHandlerParent interface { + logger.Writer + staticSourceHandlerSetReady(context.Context, defs.PathSourceStaticSetReadyReq) + staticSourceHandlerSetNotReady(context.Context, defs.PathSourceStaticSetNotReadyReq) +} + +// staticSourceHandler is a static source handler. +type staticSourceHandler struct { + conf *conf.Path + parent staticSourceHandlerParent + + ctx context.Context + ctxCancel func() + instance defs.StaticSource + running bool + + // in + chReloadConf chan *conf.Path + chInstanceSetReady chan defs.PathSourceStaticSetReadyReq + chInstanceSetNotReady chan defs.PathSourceStaticSetNotReadyReq + + // out + done chan struct{} +} + +func newStaticSourceHandler( + cnf *conf.Path, + readTimeout conf.StringDuration, + writeTimeout conf.StringDuration, + writeQueueSize int, + parent staticSourceHandlerParent, +) *staticSourceHandler { + s := &staticSourceHandler{ + conf: cnf, + parent: parent, + chReloadConf: make(chan *conf.Path), + chInstanceSetReady: make(chan defs.PathSourceStaticSetReadyReq), + chInstanceSetNotReady: make(chan defs.PathSourceStaticSetNotReadyReq), + } + + switch { + case strings.HasPrefix(cnf.Source, "rtsp://") || + strings.HasPrefix(cnf.Source, "rtsps://"): + s.instance = &rtspsource.Source{ + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + WriteQueueSize: writeQueueSize, + Parent: s, + } + + case strings.HasPrefix(cnf.Source, "rtmp://") || + strings.HasPrefix(cnf.Source, "rtmps://"): + s.instance = &rtmpsource.Source{ + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + Parent: s, + } + + case strings.HasPrefix(cnf.Source, "http://") || + strings.HasPrefix(cnf.Source, "https://"): + s.instance = &hlssource.Source{ + Parent: s, + } + + case strings.HasPrefix(cnf.Source, "udp://"): + s.instance = &udpsource.Source{ + ReadTimeout: readTimeout, + Parent: s, + } + + case strings.HasPrefix(cnf.Source, "srt://"): + s.instance = &srtsource.Source{ + ReadTimeout: readTimeout, + Parent: s, + } + + case strings.HasPrefix(cnf.Source, "whep://") || + strings.HasPrefix(cnf.Source, "wheps://"): + s.instance = &webrtcsource.Source{ + ReadTimeout: readTimeout, + Parent: s, + } + + case cnf.Source == "rpiCamera": + s.instance = &rpicamerasource.Source{ + Parent: s, + } + } + + return s +} + +func (s *staticSourceHandler) close(reason string) { + s.stop(reason) +} + +func (s *staticSourceHandler) start(onDemand bool) { + if s.running { + panic("should not happen") + } + + s.running = true + s.instance.Log(logger.Info, "started%s", + func() string { + if onDemand { + return " on demand" + } + return "" + }()) + + s.ctx, s.ctxCancel = context.WithCancel(context.Background()) + s.done = make(chan struct{}) + + go s.run() +} + +func (s *staticSourceHandler) stop(reason string) { + if !s.running { + panic("should not happen") + } + + s.running = false + s.instance.Log(logger.Info, "stopped: %s", reason) + + s.ctxCancel() + + // we must wait since s.ctx is not thread safe + <-s.done +} + +func (s *staticSourceHandler) Log(level logger.Level, format string, args ...interface{}) { + s.parent.Log(level, format, args...) +} + +func (s *staticSourceHandler) run() { + defer close(s.done) + + var runCtx context.Context + var runCtxCancel func() + runErr := make(chan error) + runReloadConf := make(chan *conf.Path) + + recreate := func() { + runCtx, runCtxCancel = context.WithCancel(context.Background()) + go func() { + runErr <- s.instance.Run(defs.StaticSourceRunParams{ + Context: runCtx, + Conf: s.conf, + ReloadConf: runReloadConf, + }) + }() + } + + recreate() + + recreating := false + recreateTimer := newEmptyTimer() + + for { + select { + case err := <-runErr: + runCtxCancel() + s.instance.Log(logger.Error, err.Error()) + recreating = true + recreateTimer = time.NewTimer(staticSourceHandlerRetryPause) + + case req := <-s.chInstanceSetReady: + s.parent.staticSourceHandlerSetReady(s.ctx, req) + + case req := <-s.chInstanceSetNotReady: + s.parent.staticSourceHandlerSetNotReady(s.ctx, req) + + case newConf := <-s.chReloadConf: + s.conf = newConf + if !recreating { + cReloadConf := runReloadConf + cInnerCtx := runCtx + go func() { + select { + case cReloadConf <- newConf: + case <-cInnerCtx.Done(): + } + }() + } + + case <-recreateTimer.C: + recreate() + recreating = false + + case <-s.ctx.Done(): + if !recreating { + runCtxCancel() + <-runErr + } + return + } + } +} + +func (s *staticSourceHandler) reloadConf(newConf *conf.Path) { + select { + case s.chReloadConf <- newConf: + case <-s.ctx.Done(): + } +} + +// APISourceDescribe instanceements source. +func (s *staticSourceHandler) APISourceDescribe() defs.APIPathSourceOrReader { + return s.instance.APISourceDescribe() +} + +// setReady is called by a staticSource. +func (s *staticSourceHandler) SetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes { + req.Res = make(chan defs.PathSourceStaticSetReadyRes) + select { + case s.chInstanceSetReady <- req: + res := <-req.Res + + if res.Err == nil { + s.instance.Log(logger.Info, "ready: %s", mediaInfo(req.Desc.Medias)) + } + + return res + + case <-s.ctx.Done(): + return defs.PathSourceStaticSetReadyRes{Err: fmt.Errorf("terminated")} + } +} + +// setNotReady is called by a staticSource. +func (s *staticSourceHandler) SetNotReady(req defs.PathSourceStaticSetNotReadyReq) { + req.Res = make(chan struct{}) + select { + case s.chInstanceSetNotReady <- req: + <-req.Res + case <-s.ctx.Done(): + } +} diff --git a/internal/core/tls_fingerprint.go b/internal/core/tls_fingerprint.go deleted file mode 100644 index ff2f70d7..00000000 --- a/internal/core/tls_fingerprint.go +++ /dev/null @@ -1,39 +0,0 @@ -package core - -import ( - "crypto/sha256" - "crypto/tls" - "encoding/hex" - "fmt" - "strings" -) - -type fingerprintValidatorFunc func(tls.ConnectionState) error - -func fingerprintValidator(fingerprint string) fingerprintValidatorFunc { - fingerprintLower := strings.ToLower(fingerprint) - - return func(cs tls.ConnectionState) error { - h := sha256.New() - h.Write(cs.PeerCertificates[0].Raw) - hstr := hex.EncodeToString(h.Sum(nil)) - - if hstr != fingerprintLower { - return fmt.Errorf("source fingerprint does not match: expected %s, got %s", - fingerprintLower, hstr) - } - - return nil - } -} - -func tlsConfigForFingerprint(fingerprint string) *tls.Config { - if fingerprint == "" { - return nil - } - - return &tls.Config{ - InsecureSkipVerify: true, - VerifyConnection: fingerprintValidator(fingerprint), - } -} diff --git a/internal/core/udp_source_test.go b/internal/core/udp_source_test.go deleted file mode 100644 index f7900c19..00000000 --- a/internal/core/udp_source_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package core - -import ( - "bufio" - "net" - "testing" - "time" - - "github.com/bluenviron/gortsplib/v4" - "github.com/bluenviron/gortsplib/v4/pkg/format" - "github.com/bluenviron/gortsplib/v4/pkg/url" - "github.com/bluenviron/mediacommon/pkg/formats/mpegts" - "github.com/pion/rtp" - "github.com/stretchr/testify/require" -) - -func TestUDPSource(t *testing.T) { - p, ok := newInstance("paths:\n" + - " proxied:\n" + - " source: udp://localhost:9999\n" + - " sourceOnDemand: yes\n") - require.Equal(t, true, ok) - defer p.Close() - - c := gortsplib.Client{} - - u, err := url.Parse("rtsp://127.0.0.1:8554/proxied") - require.NoError(t, err) - - err = c.Start(u.Scheme, u.Host) - require.NoError(t, err) - defer c.Close() - - connected := make(chan struct{}) - received := make(chan struct{}) - - go func() { - time.Sleep(200 * time.Millisecond) - - conn, err := net.Dial("udp", "localhost:9999") - require.NoError(t, err) - defer conn.Close() - - track := &mpegts.Track{ - Codec: &mpegts.CodecH264{}, - } - - bw := bufio.NewWriter(conn) - w := mpegts.NewWriter(bw, []*mpegts.Track{track}) - require.NoError(t, err) - - err = w.WriteH26x(track, 0, 0, true, [][]byte{ - { // IDR - 0x05, 1, - }, - }) - require.NoError(t, err) - - err = bw.Flush() - require.NoError(t, err) - - <-connected - - err = w.WriteH26x(track, 0, 0, true, [][]byte{{5, 2}}) - require.NoError(t, err) - - err = bw.Flush() - require.NoError(t, err) - }() - - desc, _, err := c.Describe(u) - require.NoError(t, err) - - var forma *format.H264 - medi := desc.FindFormat(&forma) - - _, err = c.Setup(desc.BaseURL, medi, 0, 0) - require.NoError(t, err) - - c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) { - require.Equal(t, []byte{5, 1}, pkt.Payload) - close(received) - }) - - _, err = c.Play(nil) - require.NoError(t, err) - - close(connected) - <-received -} diff --git a/internal/core/webrtc_http_server.go b/internal/core/webrtc_http_server.go index 30639e82..8119c77f 100644 --- a/internal/core/webrtc_http_server.go +++ b/internal/core/webrtc_http_server.go @@ -16,9 +16,11 @@ import ( pwebrtc "github.com/pion/webrtc/v3" "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/protocols/httpserv" "github.com/bluenviron/mediamtx/internal/protocols/webrtc" + "github.com/bluenviron/mediamtx/internal/restrictnetwork" ) //go:embed webrtc_publish_index.html @@ -41,7 +43,7 @@ func relativeLocation(u *url.URL) string { } func webrtcWriteError(ctx *gin.Context, statusCode int, err error) { - ctx.JSON(statusCode, &apiError{ + ctx.JSON(statusCode, &defs.APIError{ Error: err.Error(), }) } @@ -92,7 +94,7 @@ func newWebRTCHTTPServer( //nolint:dupl router.SetTrustedProxies(trustedProxies.ToTrustedProxies()) //nolint:errcheck router.NoRoute(s.onRequest) - network, address := restrictNetwork("tcp", address) + network, address := restrictnetwork.Restrict("tcp", address) var err error s.inner, err = httpserv.NewWrappedServer( diff --git a/internal/core/webrtc_manager.go b/internal/core/webrtc_manager.go index c7b16b79..a4faeb6e 100644 --- a/internal/core/webrtc_manager.go +++ b/internal/core/webrtc_manager.go @@ -19,9 +19,11 @@ import ( pwebrtc "github.com/pion/webrtc/v3" "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/protocols/webrtc" + "github.com/bluenviron/mediamtx/internal/restrictnetwork" ) const ( @@ -84,7 +86,7 @@ func randomTurnUser() (string, error) { } type webRTCManagerAPISessionsListRes struct { - data *apiWebRTCSessionList + data *defs.APIWebRTCSessionList err error } @@ -93,7 +95,7 @@ type webRTCManagerAPISessionsListReq struct { } type webRTCManagerAPISessionsGetRes struct { - data *apiWebRTCSession + data *defs.APIWebRTCSession err error } @@ -249,7 +251,7 @@ func newWebRTCManager( var iceUDPMux ice.UDPMux if iceUDPMuxAddress != "" { - m.udpMuxLn, err = net.ListenPacket(restrictNetwork("udp", iceUDPMuxAddress)) + m.udpMuxLn, err = net.ListenPacket(restrictnetwork.Restrict("udp", iceUDPMuxAddress)) if err != nil { m.httpServer.close() ctxCancel() @@ -261,7 +263,7 @@ func newWebRTCManager( var iceTCPMux ice.TCPMux if iceTCPMuxAddress != "" { - m.tcpMuxLn, err = net.Listen(restrictNetwork("tcp", iceTCPMuxAddress)) + m.tcpMuxLn, err = net.Listen(restrictnetwork.Restrict("tcp", iceTCPMuxAddress)) if err != nil { m.udpMuxLn.Close() m.httpServer.close() @@ -364,8 +366,8 @@ outer: req.res <- webRTCDeleteSessionRes{} case req := <-m.chAPISessionsList: - data := &apiWebRTCSessionList{ - Items: []*apiWebRTCSession{}, + data := &defs.APIWebRTCSessionList{ + Items: []*defs.APIWebRTCSession{}, } for sx := range m.sessions { @@ -518,7 +520,7 @@ func (m *webRTCManager) deleteSession(req webRTCDeleteSessionReq) error { } // apiSessionsList is called by api. -func (m *webRTCManager) apiSessionsList() (*apiWebRTCSessionList, error) { +func (m *webRTCManager) apiSessionsList() (*defs.APIWebRTCSessionList, error) { req := webRTCManagerAPISessionsListReq{ res: make(chan webRTCManagerAPISessionsListRes), } @@ -534,7 +536,7 @@ func (m *webRTCManager) apiSessionsList() (*apiWebRTCSessionList, error) { } // apiSessionsGet is called by api. -func (m *webRTCManager) apiSessionsGet(uuid uuid.UUID) (*apiWebRTCSession, error) { +func (m *webRTCManager) apiSessionsGet(uuid uuid.UUID) (*defs.APIWebRTCSession, error) { req := webRTCManagerAPISessionsGetReq{ uuid: uuid, res: make(chan webRTCManagerAPISessionsGetRes), diff --git a/internal/core/webrtc_session.go b/internal/core/webrtc_session.go index 3319af8e..979717e5 100644 --- a/internal/core/webrtc_session.go +++ b/internal/core/webrtc_session.go @@ -18,11 +18,11 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9" "github.com/bluenviron/gortsplib/v4/pkg/rtptime" "github.com/google/uuid" - "github.com/pion/rtp" "github.com/pion/sdp/v3" pwebrtc "github.com/pion/webrtc/v3" "github.com/bluenviron/mediamtx/internal/asyncwriter" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/protocols/webrtc" @@ -30,18 +30,6 @@ import ( "github.com/bluenviron/mediamtx/internal/unit" ) -type webrtcTrackWrapper struct { - clockRate int -} - -func (w webrtcTrackWrapper) ClockRate() int { - return w.clockRate -} - -func (webrtcTrackWrapper) PTSEqualsDTS(*rtp.Packet) bool { - return true -} - type setupStreamFunc func(*webrtc.OutgoingTrack) error func webrtcFindVideoTrack( @@ -268,31 +256,6 @@ func webrtcFindAudioTrack( return nil, nil } -func webrtcMediasOfIncomingTracks(tracks []*webrtc.IncomingTrack) []*description.Media { - ret := make([]*description.Media, len(tracks)) - - for i, track := range tracks { - forma := track.Format() - - var mediaType description.MediaType - - switch forma.(type) { - case *format.AV1, *format.VP9, *format.VP8, *format.H264: - mediaType = description.MediaTypeVideo - - default: - mediaType = description.MediaTypeAudio - } - - ret[i] = &description.Media{ - Type: mediaType, - Formats: []format.Format{forma}, - } - } - - return ret -} - func whipOffer(body []byte) *pwebrtc.SessionDescription { return &pwebrtc.SessionDescription{ Type: pwebrtc.SDPTypeOffer, @@ -497,7 +460,7 @@ func (s *webRTCSession) runPublish() (int, error) { return 0, err } - medias := webrtcMediasOfIncomingTracks(tracks) + medias := webrtc.TracksToMedias(tracks) rres := res.path.startPublisher(pathStartPublisherReq{ author: s, @@ -513,7 +476,7 @@ func (s *webRTCSession) runPublish() (int, error) { for i, media := range medias { ci := i cmedia := media - trackWrapper := &webrtcTrackWrapper{clockRate: cmedia.Formats[0].ClockRate()} + trackWrapper := &webrtc.TrackWrapper{ClockRat: cmedia.Formats[0].ClockRate()} go func() { for { @@ -724,20 +687,20 @@ func (s *webRTCSession) addCandidates( } } -// apiSourceDescribe implements sourceStaticImpl. -func (s *webRTCSession) apiSourceDescribe() apiPathSourceOrReader { - return apiPathSourceOrReader{ +// apiReaderDescribe implements reader. +func (s *webRTCSession) apiReaderDescribe() defs.APIPathSourceOrReader { + return defs.APIPathSourceOrReader{ Type: "webRTCSession", ID: s.uuid.String(), } } -// apiReaderDescribe implements reader. -func (s *webRTCSession) apiReaderDescribe() apiPathSourceOrReader { - return s.apiSourceDescribe() +// APISourceDescribe implements source. +func (s *webRTCSession) APISourceDescribe() defs.APIPathSourceOrReader { + return s.apiReaderDescribe() } -func (s *webRTCSession) apiItem() *apiWebRTCSession { +func (s *webRTCSession) apiItem() *defs.APIWebRTCSession { s.mutex.RLock() defer s.mutex.RUnlock() @@ -755,18 +718,18 @@ func (s *webRTCSession) apiItem() *apiWebRTCSession { bytesSent = s.pc.BytesSent() } - return &apiWebRTCSession{ + return &defs.APIWebRTCSession{ ID: s.uuid, Created: s.created, RemoteAddr: s.req.remoteAddr, PeerConnectionEstablished: peerConnectionEstablished, LocalCandidate: localCandidate, RemoteCandidate: remoteCandidate, - State: func() apiWebRTCSessionState { + State: func() defs.APIWebRTCSessionState { if s.req.publish { - return apiWebRTCSessionStatePublish + return defs.APIWebRTCSessionStatePublish } - return apiWebRTCSessionStateRead + return defs.APIWebRTCSessionStateRead }(), Path: s.req.pathName, BytesReceived: bytesReceived, diff --git a/internal/core/webrtc_source.go b/internal/core/webrtc_source.go deleted file mode 100644 index 3008d468..00000000 --- a/internal/core/webrtc_source.go +++ /dev/null @@ -1,118 +0,0 @@ -package core - -import ( - "context" - "net/http" - "net/url" - "strings" - "time" - - "github.com/bluenviron/gortsplib/v4/pkg/description" - "github.com/bluenviron/gortsplib/v4/pkg/rtptime" - - "github.com/bluenviron/mediamtx/internal/conf" - "github.com/bluenviron/mediamtx/internal/logger" - "github.com/bluenviron/mediamtx/internal/protocols/webrtc" -) - -type webRTCSourceParent interface { - logger.Writer - setReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes - setNotReady(req pathSourceStaticSetNotReadyReq) -} - -type webRTCSource struct { - readTimeout conf.StringDuration - - parent webRTCSourceParent -} - -func newWebRTCSource( - readTimeout conf.StringDuration, - parent webRTCSourceParent, -) *webRTCSource { - s := &webRTCSource{ - readTimeout: readTimeout, - parent: parent, - } - - return s -} - -func (s *webRTCSource) Log(level logger.Level, format string, args ...interface{}) { - s.parent.Log(level, "[WebRTC source] "+format, args...) -} - -// run implements sourceStaticImpl. -func (s *webRTCSource) run(ctx context.Context, cnf *conf.Path, _ chan *conf.Path) error { - s.Log(logger.Debug, "connecting") - - u, err := url.Parse(cnf.Source) - if err != nil { - return err - } - - u.Scheme = strings.ReplaceAll(u.Scheme, "whep", "http") - - hc := &http.Client{ - Timeout: time.Duration(s.readTimeout), - } - - client := webrtc.WHIPClient{ - HTTPClient: hc, - URL: u, - Log: s, - } - - tracks, err := client.Read(ctx) - if err != nil { - return err - } - defer client.Close() //nolint:errcheck - - medias := webrtcMediasOfIncomingTracks(tracks) - - rres := s.parent.setReady(pathSourceStaticSetReadyReq{ - desc: &description.Session{Medias: medias}, - generateRTPPackets: true, - }) - if rres.err != nil { - return rres.err - } - - defer s.parent.setNotReady(pathSourceStaticSetNotReadyReq{}) - - timeDecoder := rtptime.NewGlobalDecoder() - - for i, media := range medias { - ci := i - cmedia := media - trackWrapper := &webrtcTrackWrapper{clockRate: cmedia.Formats[0].ClockRate()} - - go func() { - for { - pkt, err := tracks[ci].ReadRTP() - if err != nil { - return - } - - pts, ok := timeDecoder.Decode(trackWrapper, pkt) - if !ok { - continue - } - - rres.stream.WriteRTPPacket(cmedia, cmedia.Formats[0], pkt, time.Now(), pts) - } - }() - } - - return client.Wait(ctx) -} - -// apiSourceDescribe implements sourceStaticImpl. -func (*webRTCSource) apiSourceDescribe() apiPathSourceOrReader { - return apiPathSourceOrReader{ - Type: "webRTCSource", - ID: "", - } -} diff --git a/internal/core/api_defs.go b/internal/defs/api.go similarity index 54% rename from internal/core/api_defs.go rename to internal/defs/api.go index fee4aea1..bee536ce 100644 --- a/internal/core/api_defs.go +++ b/internal/defs/api.go @@ -1,4 +1,4 @@ -package core +package defs import ( "time" @@ -8,52 +8,60 @@ import ( "github.com/bluenviron/mediamtx/internal/conf" ) -type apiError struct { +// APIError is a generic error. +type APIError struct { Error string `json:"error"` } -type apiPathConfList struct { +// APIPathConfList is a list of path configurations. +type APIPathConfList struct { ItemCount int `json:"itemCount"` PageCount int `json:"pageCount"` Items []*conf.Path `json:"items"` } -type apiPathSourceOrReader struct { +// APIPathSourceOrReader is a source or a reader. +type APIPathSourceOrReader struct { Type string `json:"type"` ID string `json:"id"` } -type apiPath struct { +// APIPath is a path. +type APIPath struct { Name string `json:"name"` ConfName string `json:"confName"` - Source *apiPathSourceOrReader `json:"source"` + Source *APIPathSourceOrReader `json:"source"` Ready bool `json:"ready"` ReadyTime *time.Time `json:"readyTime"` Tracks []string `json:"tracks"` BytesReceived uint64 `json:"bytesReceived"` - Readers []apiPathSourceOrReader `json:"readers"` + Readers []APIPathSourceOrReader `json:"readers"` } -type apiPathList struct { +// APIPathList is a list of paths. +type APIPathList struct { ItemCount int `json:"itemCount"` PageCount int `json:"pageCount"` - Items []*apiPath `json:"items"` + Items []*APIPath `json:"items"` } -type apiHLSMuxer struct { +// APIHLSMuxer is an HLS muxer. +type APIHLSMuxer struct { Path string `json:"path"` Created time.Time `json:"created"` LastRequest time.Time `json:"lastRequest"` BytesSent uint64 `json:"bytesSent"` } -type apiHLSMuxerList struct { +// APIHLSMuxerList is a list of HLS muxers. +type APIHLSMuxerList struct { ItemCount int `json:"itemCount"` PageCount int `json:"pageCount"` - Items []*apiHLSMuxer `json:"items"` + Items []*APIHLSMuxer `json:"items"` } -type apiRTSPConn struct { +// APIRTSPConn is a RTSP connection. +type APIRTSPConn struct { ID uuid.UUID `json:"id"` Created time.Time `json:"created"` RemoteAddr string `json:"remoteAddr"` @@ -61,107 +69,124 @@ type apiRTSPConn struct { BytesSent uint64 `json:"bytesSent"` } -type apiRTSPConnsList struct { +// APIRTSPConnsList is a list of RTSP connections. +type APIRTSPConnsList struct { ItemCount int `json:"itemCount"` PageCount int `json:"pageCount"` - Items []*apiRTSPConn `json:"items"` + Items []*APIRTSPConn `json:"items"` } -type apiRTMPConnState string +// APIRTMPConnState is the state of a RTMP connection. +type APIRTMPConnState string +// states. const ( - apiRTMPConnStateIdle apiRTMPConnState = "idle" - apiRTMPConnStateRead apiRTMPConnState = "read" - apiRTMPConnStatePublish apiRTMPConnState = "publish" + APIRTMPConnStateIdle APIRTMPConnState = "idle" + APIRTMPConnStateRead APIRTMPConnState = "read" + APIRTMPConnStatePublish APIRTMPConnState = "publish" ) -type apiRTMPConn struct { +// APIRTMPConn is a RTMP connection. +type APIRTMPConn struct { ID uuid.UUID `json:"id"` Created time.Time `json:"created"` RemoteAddr string `json:"remoteAddr"` - State apiRTMPConnState `json:"state"` + State APIRTMPConnState `json:"state"` Path string `json:"path"` BytesReceived uint64 `json:"bytesReceived"` BytesSent uint64 `json:"bytesSent"` } -type apiRTMPConnList struct { +// APIRTMPConnList is a list of RTMP connections. +type APIRTMPConnList struct { ItemCount int `json:"itemCount"` PageCount int `json:"pageCount"` - Items []*apiRTMPConn `json:"items"` + Items []*APIRTMPConn `json:"items"` } -type apiRTSPSessionState string +// APIRTSPSessionState is the state of a RTSP session. +type APIRTSPSessionState string +// states. const ( - apiRTSPSessionStateIdle apiRTSPSessionState = "idle" - apiRTSPSessionStateRead apiRTSPSessionState = "read" - apiRTSPSessionStatePublish apiRTSPSessionState = "publish" + APIRTSPSessionStateIdle APIRTSPSessionState = "idle" + APIRTSPSessionStateRead APIRTSPSessionState = "read" + APIRTSPSessionStatePublish APIRTSPSessionState = "publish" ) -type apiRTSPSession struct { +// APIRTSPSession is a RTSP session. +type APIRTSPSession struct { ID uuid.UUID `json:"id"` Created time.Time `json:"created"` RemoteAddr string `json:"remoteAddr"` - State apiRTSPSessionState `json:"state"` + State APIRTSPSessionState `json:"state"` Path string `json:"path"` Transport *string `json:"transport"` BytesReceived uint64 `json:"bytesReceived"` BytesSent uint64 `json:"bytesSent"` } -type apiRTSPSessionList struct { +// APIRTSPSessionList is a list of RTSP sessions. +type APIRTSPSessionList struct { ItemCount int `json:"itemCount"` PageCount int `json:"pageCount"` - Items []*apiRTSPSession `json:"items"` + Items []*APIRTSPSession `json:"items"` } -type apiSRTConnState string +// APISRTConnState is the state of a SRT connection. +type APISRTConnState string +// states. const ( - apiSRTConnStateIdle apiSRTConnState = "idle" - apiSRTConnStateRead apiSRTConnState = "read" - apiSRTConnStatePublish apiSRTConnState = "publish" + APISRTConnStateIdle APISRTConnState = "idle" + APISRTConnStateRead APISRTConnState = "read" + APISRTConnStatePublish APISRTConnState = "publish" ) -type apiSRTConn struct { +// APISRTConn is a SRT connection. +type APISRTConn struct { ID uuid.UUID `json:"id"` Created time.Time `json:"created"` RemoteAddr string `json:"remoteAddr"` - State apiSRTConnState `json:"state"` + State APISRTConnState `json:"state"` Path string `json:"path"` BytesReceived uint64 `json:"bytesReceived"` BytesSent uint64 `json:"bytesSent"` } -type apiSRTConnList struct { +// APISRTConnList is a list of SRT connections. +type APISRTConnList struct { ItemCount int `json:"itemCount"` PageCount int `json:"pageCount"` - Items []*apiSRTConn `json:"items"` + Items []*APISRTConn `json:"items"` } -type apiWebRTCSessionState string +// APIWebRTCSessionState is the state of a WebRTC connection. +type APIWebRTCSessionState string +// states. const ( - apiWebRTCSessionStateRead apiWebRTCSessionState = "read" - apiWebRTCSessionStatePublish apiWebRTCSessionState = "publish" + APIWebRTCSessionStateRead APIWebRTCSessionState = "read" + APIWebRTCSessionStatePublish APIWebRTCSessionState = "publish" ) -type apiWebRTCSession struct { +// APIWebRTCSession is a WebRTC session. +type APIWebRTCSession struct { ID uuid.UUID `json:"id"` Created time.Time `json:"created"` RemoteAddr string `json:"remoteAddr"` PeerConnectionEstablished bool `json:"peerConnectionEstablished"` LocalCandidate string `json:"localCandidate"` RemoteCandidate string `json:"remoteCandidate"` - State apiWebRTCSessionState `json:"state"` + State APIWebRTCSessionState `json:"state"` Path string `json:"path"` BytesReceived uint64 `json:"bytesReceived"` BytesSent uint64 `json:"bytesSent"` } -type apiWebRTCSessionList struct { +// APIWebRTCSessionList is a list of WebRTC sessions. +type APIWebRTCSessionList struct { ItemCount int `json:"itemCount"` PageCount int `json:"pageCount"` - Items []*apiWebRTCSession `json:"items"` + Items []*APIWebRTCSession `json:"items"` } diff --git a/internal/defs/defs.go b/internal/defs/defs.go new file mode 100644 index 00000000..6049823f --- /dev/null +++ b/internal/defs/defs.go @@ -0,0 +1,2 @@ +// Package defs contains shared definitions. +package defs diff --git a/internal/defs/path.go b/internal/defs/path.go new file mode 100644 index 00000000..8877ea4c --- /dev/null +++ b/internal/defs/path.go @@ -0,0 +1,25 @@ +package defs + +import ( + "github.com/bluenviron/gortsplib/v4/pkg/description" + + "github.com/bluenviron/mediamtx/internal/stream" +) + +// PathSourceStaticSetReadyRes is a set ready response to a static source. +type PathSourceStaticSetReadyRes struct { + Stream *stream.Stream + Err error +} + +// PathSourceStaticSetReadyReq is a set ready request from a static source. +type PathSourceStaticSetReadyReq struct { + Desc *description.Session + GenerateRTPPackets bool + Res chan PathSourceStaticSetReadyRes +} + +// PathSourceStaticSetNotReadyReq is a set not ready request from a static source. +type PathSourceStaticSetNotReadyReq struct { + Res chan struct{} +} diff --git a/internal/defs/static_source.go b/internal/defs/static_source.go new file mode 100644 index 00000000..eaf1cf42 --- /dev/null +++ b/internal/defs/static_source.go @@ -0,0 +1,29 @@ +package defs + +import ( + "context" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/logger" +) + +// StaticSource is a static source. +type StaticSource interface { + logger.Writer + Run(StaticSourceRunParams) error + APISourceDescribe() APIPathSourceOrReader +} + +// StaticSourceParent is the parent of a static source. +type StaticSourceParent interface { + logger.Writer + SetReady(req PathSourceStaticSetReadyReq) PathSourceStaticSetReadyRes + SetNotReady(req PathSourceStaticSetNotReadyReq) +} + +// StaticSourceRunParams is the set of params passed to Run(). +type StaticSourceRunParams struct { + Context context.Context + Conf *conf.Path + ReloadConf chan *conf.Path +} diff --git a/internal/protocols/mpegts/to_stream.go b/internal/protocols/mpegts/to_stream.go new file mode 100644 index 00000000..7496a4ad --- /dev/null +++ b/internal/protocols/mpegts/to_stream.go @@ -0,0 +1,204 @@ +// Package mpegts contains MPEG-ts utilities. +package mpegts + +import ( + "errors" + "time" + + "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/format" + "github.com/bluenviron/mediacommon/pkg/formats/mpegts" + + "github.com/bluenviron/mediamtx/internal/stream" + "github.com/bluenviron/mediamtx/internal/unit" +) + +// ErrNoTracks is returned when there are no supported tracks. +var ErrNoTracks = errors.New("no supported tracks found (supported are H265, H264," + + " MPEG-4 Video, MPEG-1/2 Video, Opus, MPEG-4 Audio, MPEG-1 Audio, AC-3") + +// ToStream converts a MPEG-TS stream to a server stream. +func ToStream(r *mpegts.Reader, stream **stream.Stream) ([]*description.Media, error) { + var medias []*description.Media //nolint:prealloc + + var td *mpegts.TimeDecoder + decodeTime := func(t int64) time.Duration { + if td == nil { + td = mpegts.NewTimeDecoder(t) + } + return td.Decode(t) + } + + for _, track := range r.Tracks() { //nolint:dupl + var medi *description.Media + + switch codec := track.Codec.(type) { + case *mpegts.CodecH265: + medi = &description.Media{ + Type: description.MediaTypeVideo, + Formats: []format.Format{&format.H265{ + PayloadTyp: 96, + }}, + } + + r.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) error { + (*stream).WriteUnit(medi, medi.Formats[0], &unit.H265{ + Base: unit.Base{ + NTP: time.Now(), + PTS: decodeTime(pts), + }, + AU: au, + }) + return nil + }) + + case *mpegts.CodecH264: + medi = &description.Media{ + Type: description.MediaTypeVideo, + Formats: []format.Format{&format.H264{ + PayloadTyp: 96, + PacketizationMode: 1, + }}, + } + + r.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) error { + (*stream).WriteUnit(medi, medi.Formats[0], &unit.H264{ + Base: unit.Base{ + NTP: time.Now(), + PTS: decodeTime(pts), + }, + AU: au, + }) + return nil + }) + + case *mpegts.CodecMPEG4Video: + medi = &description.Media{ + Type: description.MediaTypeVideo, + Formats: []format.Format{&format.MPEG4Video{ + PayloadTyp: 96, + }}, + } + + r.OnDataMPEGxVideo(track, func(pts int64, frame []byte) error { + (*stream).WriteUnit(medi, medi.Formats[0], &unit.MPEG4Video{ + Base: unit.Base{ + NTP: time.Now(), + PTS: decodeTime(pts), + }, + Frame: frame, + }) + return nil + }) + + case *mpegts.CodecMPEG1Video: + medi = &description.Media{ + Type: description.MediaTypeVideo, + Formats: []format.Format{&format.MPEG1Video{}}, + } + + r.OnDataMPEGxVideo(track, func(pts int64, frame []byte) error { + (*stream).WriteUnit(medi, medi.Formats[0], &unit.MPEG1Video{ + Base: unit.Base{ + NTP: time.Now(), + PTS: decodeTime(pts), + }, + Frame: frame, + }) + return nil + }) + + case *mpegts.CodecOpus: + medi = &description.Media{ + Type: description.MediaTypeAudio, + Formats: []format.Format{&format.Opus{ + PayloadTyp: 96, + IsStereo: (codec.ChannelCount == 2), + }}, + } + + r.OnDataOpus(track, func(pts int64, packets [][]byte) error { + (*stream).WriteUnit(medi, medi.Formats[0], &unit.Opus{ + Base: unit.Base{ + NTP: time.Now(), + PTS: decodeTime(pts), + }, + Packets: packets, + }) + return nil + }) + + case *mpegts.CodecMPEG4Audio: + medi = &description.Media{ + Type: description.MediaTypeAudio, + Formats: []format.Format{&format.MPEG4Audio{ + PayloadTyp: 96, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + Config: &codec.Config, + }}, + } + + r.OnDataMPEG4Audio(track, func(pts int64, aus [][]byte) error { + (*stream).WriteUnit(medi, medi.Formats[0], &unit.MPEG4Audio{ + Base: unit.Base{ + NTP: time.Now(), + PTS: decodeTime(pts), + }, + AUs: aus, + }) + return nil + }) + + case *mpegts.CodecMPEG1Audio: + medi = &description.Media{ + Type: description.MediaTypeAudio, + Formats: []format.Format{&format.MPEG1Audio{}}, + } + + r.OnDataMPEG1Audio(track, func(pts int64, frames [][]byte) error { + (*stream).WriteUnit(medi, medi.Formats[0], &unit.MPEG1Audio{ + Base: unit.Base{ + NTP: time.Now(), + PTS: decodeTime(pts), + }, + Frames: frames, + }) + return nil + }) + + case *mpegts.CodecAC3: + medi = &description.Media{ + Type: description.MediaTypeAudio, + Formats: []format.Format{&format.AC3{ + PayloadTyp: 96, + SampleRate: codec.SampleRate, + ChannelCount: codec.ChannelCount, + }}, + } + + r.OnDataAC3(track, func(pts int64, frame []byte) error { + (*stream).WriteUnit(medi, medi.Formats[0], &unit.AC3{ + Base: unit.Base{ + NTP: time.Now(), + PTS: decodeTime(pts), + }, + Frames: [][]byte{frame}, + }) + return nil + }) + + default: + continue + } + + medias = append(medias, medi) + } + + if len(medias) == 0 { + return nil, ErrNoTracks + } + + return medias, nil +} diff --git a/internal/protocols/tls/tls_config.go b/internal/protocols/tls/tls_config.go new file mode 100644 index 00000000..6d8870b7 --- /dev/null +++ b/internal/protocols/tls/tls_config.go @@ -0,0 +1,35 @@ +// Package tls contains TLS utilities. +package tls + +import ( + "crypto/sha256" + "crypto/tls" + "encoding/hex" + "fmt" + "strings" +) + +// ConfigForFingerprint returns a tls.Config that supports given fingerprint. +func ConfigForFingerprint(fingerprint string) *tls.Config { + if fingerprint == "" { + return nil + } + + fingerprintLower := strings.ToLower(fingerprint) + + return &tls.Config{ + InsecureSkipVerify: true, + VerifyConnection: func(cs tls.ConnectionState) error { + h := sha256.New() + h.Write(cs.PeerCertificates[0].Raw) + hstr := hex.EncodeToString(h.Sum(nil)) + + if hstr != fingerprintLower { + return fmt.Errorf("source fingerprint does not match: expected %s, got %s", + fingerprintLower, hstr) + } + + return nil + }, + } +} diff --git a/internal/protocols/webrtc/track_wrapper.go b/internal/protocols/webrtc/track_wrapper.go new file mode 100644 index 00000000..f336f30d --- /dev/null +++ b/internal/protocols/webrtc/track_wrapper.go @@ -0,0 +1,20 @@ +package webrtc + +import ( + "github.com/pion/rtp" +) + +// TrackWrapper provides ClockRate() and PTSEqualsDTS() to WebRTC tracks. +type TrackWrapper struct { + ClockRat int +} + +// ClockRate returns the clock rate. +func (w TrackWrapper) ClockRate() int { + return w.ClockRat +} + +// PTSEqualsDTS returns whether PTS equals DTS. +func (TrackWrapper) PTSEqualsDTS(*rtp.Packet) bool { + return true +} diff --git a/internal/protocols/webrtc/tracks_to_medias.go b/internal/protocols/webrtc/tracks_to_medias.go new file mode 100644 index 00000000..809eeec4 --- /dev/null +++ b/internal/protocols/webrtc/tracks_to_medias.go @@ -0,0 +1,32 @@ +package webrtc + +import ( + "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/format" +) + +// TracksToMedias converts WebRTC tracks into a media description. +func TracksToMedias(tracks []*IncomingTrack) []*description.Media { + ret := make([]*description.Media, len(tracks)) + + for i, track := range tracks { + forma := track.Format() + + var mediaType description.MediaType + + switch forma.(type) { + case *format.AV1, *format.VP9, *format.VP8, *format.H264: + mediaType = description.MediaTypeVideo + + default: + mediaType = description.MediaTypeAudio + } + + ret[i] = &description.Media{ + Type: mediaType, + Formats: []format.Format{forma}, + } + } + + return ret +} diff --git a/internal/restrictnetwork/restrict_network.go b/internal/restrictnetwork/restrict_network.go new file mode 100644 index 00000000..9c5cdef7 --- /dev/null +++ b/internal/restrictnetwork/restrict_network.go @@ -0,0 +1,18 @@ +// Package restrictnetwork contains Restrict(). +package restrictnetwork + +import ( + "net" +) + +// Restrict avoids listening on IPv6 when address is 0.0.0.0. +func Restrict(network string, address string) (string, string) { + host, _, err := net.SplitHostPort(address) + if err == nil { + if host == "0.0.0.0" { + return network + "4", address + } + } + + return network, address +} diff --git a/internal/core/hls_source.go b/internal/staticsources/hls/source.go similarity index 78% rename from internal/core/hls_source.go rename to internal/staticsources/hls/source.go index fa2d5b14..4725566e 100644 --- a/internal/core/hls_source.go +++ b/internal/staticsources/hls/source.go @@ -1,7 +1,7 @@ -package core +// Package hls contains the HLS static source. +package hls import ( - "context" "net/http" "time" @@ -10,41 +10,30 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/format" - "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/logger" + "github.com/bluenviron/mediamtx/internal/protocols/tls" "github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/unit" ) -type hlsSourceParent interface { - logger.Writer - setReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes - setNotReady(req pathSourceStaticSetNotReadyReq) +// Source is a HLS static source. +type Source struct { + Parent defs.StaticSourceParent } -type hlsSource struct { - parent hlsSourceParent +// Log implements StaticSource. +func (s *Source) Log(level logger.Level, format string, args ...interface{}) { + s.Parent.Log(level, "[HLS source] "+format, args...) } -func newHLSSource( - parent hlsSourceParent, -) *hlsSource { - return &hlsSource{ - parent: parent, - } -} - -func (s *hlsSource) Log(level logger.Level, format string, args ...interface{}) { - s.parent.Log(level, "[HLS source] "+format, args...) -} - -// run implements sourceStaticImpl. -func (s *hlsSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *conf.Path) error { +// Run implements StaticSource. +func (s *Source) Run(params defs.StaticSourceRunParams) error { var stream *stream.Stream defer func() { if stream != nil { - s.parent.setNotReady(pathSourceStaticSetNotReadyReq{}) + s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{}) } }() @@ -52,10 +41,10 @@ func (s *hlsSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *co var c *gohlslib.Client c = &gohlslib.Client{ - URI: cnf.Source, + URI: params.Conf.Source, HTTPClient: &http.Client{ Transport: &http.Transport{ - TLSClientConfig: tlsConfigForFingerprint(cnf.SourceFingerprint), + TLSClientConfig: tls.ConfigForFingerprint(params.Conf.SourceFingerprint), }, }, OnDownloadPrimaryPlaylist: func(u string) { @@ -200,15 +189,15 @@ func (s *hlsSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *co medias = append(medias, medi) } - res := s.parent.setReady(pathSourceStaticSetReadyReq{ - desc: &description.Session{Medias: medias}, - generateRTPPackets: true, + res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{ + Desc: &description.Session{Medias: medias}, + GenerateRTPPackets: true, }) - if res.err != nil { - return res.err + if res.Err != nil { + return res.Err } - stream = res.stream + stream = res.Stream return nil }, @@ -225,9 +214,9 @@ func (s *hlsSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *co c.Close() return err - case <-reloadConf: + case <-params.ReloadConf: - case <-ctx.Done(): + case <-params.Context.Done(): c.Close() <-c.Wait() return nil @@ -235,9 +224,9 @@ func (s *hlsSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *co } } -// apiSourceDescribe implements sourceStaticImpl. -func (*hlsSource) apiSourceDescribe() apiPathSourceOrReader { - return apiPathSourceOrReader{ +// APISourceDescribe implements StaticSource. +func (*Source) APISourceDescribe() defs.APIPathSourceOrReader { + return defs.APIPathSourceOrReader{ Type: "hlsSource", ID: "", } diff --git a/internal/staticsources/hls/source_test.go b/internal/staticsources/hls/source_test.go new file mode 100644 index 00000000..c4f5f4ae --- /dev/null +++ b/internal/staticsources/hls/source_test.go @@ -0,0 +1,117 @@ +package hls + +import ( + "bytes" + "context" + "io" + "net" + "net/http" + "testing" + + "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio" + "github.com/bluenviron/mediacommon/pkg/formats/mpegts" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" + "github.com/bluenviron/mediamtx/internal/staticsources/tester" +) + +var track1 = &mpegts.Track{ + Codec: &mpegts.CodecH264{}, +} + +var track2 = &mpegts.Track{ + Codec: &mpegts.CodecMPEG4Audio{ + Config: mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }, + }, +} + +type testHLSManager struct { + s *http.Server +} + +func newTestHLSManager() (*testHLSManager, error) { + ln, err := net.Listen("tcp", "localhost:5780") + if err != nil { + return nil, err + } + + ts := &testHLSManager{} + + gin.SetMode(gin.ReleaseMode) + router := gin.New() + router.GET("/stream.m3u8", ts.onPlaylist) + router.GET("/segment1.ts", ts.onSegment1) + router.GET("/segment2.ts", ts.onSegment2) + + ts.s = &http.Server{Handler: router} + go ts.s.Serve(ln) + + return ts, nil +} + +func (ts *testHLSManager) close() { + ts.s.Shutdown(context.Background()) +} + +func (ts *testHLSManager) onPlaylist(ctx *gin.Context) { + cnt := `#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-ALLOW-CACHE:NO +#EXT-X-TARGETDURATION:2 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:2, +segment1.ts +#EXTINF:2, +segment2.ts +#EXT-X-ENDLIST +` + + ctx.Writer.Header().Set("Content-Type", `application/vnd.apple.mpegurl`) + io.Copy(ctx.Writer, bytes.NewReader([]byte(cnt))) +} + +func (ts *testHLSManager) onSegment1(ctx *gin.Context) { + ctx.Writer.Header().Set("Content-Type", `video/MP2T`) + + w := mpegts.NewWriter(ctx.Writer, []*mpegts.Track{track1, track2}) + + w.WriteMPEG4Audio(track2, 1*90000, [][]byte{{1, 2, 3, 4}}) //nolint:errcheck +} + +func (ts *testHLSManager) onSegment2(ctx *gin.Context) { + ctx.Writer.Header().Set("Content-Type", `video/MP2T`) + + w := mpegts.NewWriter(ctx.Writer, []*mpegts.Track{track1, track2}) + + w.WriteH26x(track1, 2*90000, 2*90000, true, [][]byte{ //nolint:errcheck + {7, 1, 2, 3}, // SPS + {8}, // PPS + }) +} + +func TestSource(t *testing.T) { + ts, err := newTestHLSManager() + require.NoError(t, err) + defer ts.close() + + te := tester.New( + func(p defs.StaticSourceParent) defs.StaticSource { + return &Source{ + Parent: p, + } + }, + &conf.Path{ + Source: "http://localhost:5780/stream.m3u8", + }, + ) + defer te.Close() + + <-te.Unit +} diff --git a/internal/core/rpicamera_source.go b/internal/staticsources/rpicamera/source.go similarity index 68% rename from internal/core/rpicamera_source.go rename to internal/staticsources/rpicamera/source.go index 2979c0d1..59aacf76 100644 --- a/internal/core/rpicamera_source.go +++ b/internal/staticsources/rpicamera/source.go @@ -1,13 +1,14 @@ -package core +// Package rpicamera contains the Raspberry Pi Camera static source. +package rpicamera import ( - "context" "time" "github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/protocols/rpicamera" "github.com/bluenviron/mediamtx/internal/stream" @@ -51,30 +52,18 @@ func paramsFromConf(cnf *conf.Path) rpicamera.Params { } } -type rpiCameraSourceParent interface { - logger.Writer - setReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes - setNotReady(req pathSourceStaticSetNotReadyReq) +// Source is a Raspberry Pi Camera static source. +type Source struct { + Parent defs.StaticSourceParent } -type rpiCameraSource struct { - parent rpiCameraSourceParent +// Log implements StaticSource. +func (s *Source) Log(level logger.Level, format string, args ...interface{}) { + s.Parent.Log(level, "[RPI Camera source] "+format, args...) } -func newRPICameraSource( - parent rpiCameraSourceParent, -) *rpiCameraSource { - return &rpiCameraSource{ - parent: parent, - } -} - -func (s *rpiCameraSource) Log(level logger.Level, format string, args ...interface{}) { - s.parent.Log(level, "[RPI Camera source] "+format, args...) -} - -// run implements sourceStaticImpl. -func (s *rpiCameraSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *conf.Path) error { +// Run implements StaticSource. +func (s *Source) Run(params defs.StaticSourceRunParams) error { medi := &description.Media{ Type: description.MediaTypeVideo, Formats: []format.Format{&format.H264{ @@ -87,15 +76,15 @@ func (s *rpiCameraSource) run(ctx context.Context, cnf *conf.Path, reloadConf ch onData := func(dts time.Duration, au [][]byte) { if stream == nil { - res := s.parent.setReady(pathSourceStaticSetReadyReq{ - desc: &description.Session{Medias: medias}, - generateRTPPackets: true, + res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{ + Desc: &description.Session{Medias: medias}, + GenerateRTPPackets: true, }) - if res.err != nil { + if res.Err != nil { return } - stream = res.stream + stream = res.Stream } stream.WriteUnit(medi, medi.Formats[0], &unit.H264{ @@ -107,7 +96,7 @@ func (s *rpiCameraSource) run(ctx context.Context, cnf *conf.Path, reloadConf ch }) } - cam, err := rpicamera.New(paramsFromConf(cnf), onData) + cam, err := rpicamera.New(paramsFromConf(params.Conf), onData) if err != nil { return err } @@ -115,24 +104,24 @@ func (s *rpiCameraSource) run(ctx context.Context, cnf *conf.Path, reloadConf ch defer func() { if stream != nil { - s.parent.setNotReady(pathSourceStaticSetNotReadyReq{}) + s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{}) } }() for { select { - case cnf := <-reloadConf: + case cnf := <-params.ReloadConf: cam.ReloadParams(paramsFromConf(cnf)) - case <-ctx.Done(): + case <-params.Context.Done(): return nil } } } -// apiSourceDescribe implements sourceStaticImpl. -func (*rpiCameraSource) apiSourceDescribe() apiPathSourceOrReader { - return apiPathSourceOrReader{ +// APISourceDescribe implements StaticSource. +func (*Source) APISourceDescribe() defs.APIPathSourceOrReader { + return defs.APIPathSourceOrReader{ Type: "rpiCameraSource", ID: "", } diff --git a/internal/core/rtmp_source.go b/internal/staticsources/rtmp/source.go similarity index 62% rename from internal/core/rtmp_source.go rename to internal/staticsources/rtmp/source.go index 907eda49..58e39d7c 100644 --- a/internal/core/rtmp_source.go +++ b/internal/staticsources/rtmp/source.go @@ -1,8 +1,9 @@ -package core +// Package rtmp contains the RTMP static source. +package rtmp import ( "context" - "crypto/tls" + ctls "crypto/tls" "fmt" "net" "net/url" @@ -12,45 +13,31 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/protocols/rtmp" + "github.com/bluenviron/mediamtx/internal/protocols/tls" "github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/unit" ) -type rtmpSourceParent interface { - logger.Writer - setReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes - setNotReady(req pathSourceStaticSetNotReadyReq) +// Source is a RTMP static source. +type Source struct { + ReadTimeout conf.StringDuration + WriteTimeout conf.StringDuration + Parent defs.StaticSourceParent } -type rtmpSource struct { - readTimeout conf.StringDuration - writeTimeout conf.StringDuration - parent rtmpSourceParent +// Log implements StaticSource. +func (s *Source) Log(level logger.Level, format string, args ...interface{}) { + s.Parent.Log(level, "[RTMP source] "+format, args...) } -func newRTMPSource( - readTimeout conf.StringDuration, - writeTimeout conf.StringDuration, - parent rtmpSourceParent, -) *rtmpSource { - return &rtmpSource{ - readTimeout: readTimeout, - writeTimeout: writeTimeout, - parent: parent, - } -} - -func (s *rtmpSource) Log(level logger.Level, format string, args ...interface{}) { - s.parent.Log(level, "[RTMP source] "+format, args...) -} - -// run implements sourceStaticImpl. -func (s *rtmpSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *conf.Path) error { +// Run implements StaticSource. +func (s *Source) Run(params defs.StaticSourceRunParams) error { s.Log(logger.Debug, "connecting") - u, err := url.Parse(cnf.Source) + u, err := url.Parse(params.Conf.Source) if err != nil { return err } @@ -62,15 +49,15 @@ func (s *rtmpSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *c } nconn, err := func() (net.Conn, error) { - ctx2, cancel2 := context.WithTimeout(ctx, time.Duration(s.readTimeout)) + ctx2, cancel2 := context.WithTimeout(params.Context, time.Duration(s.ReadTimeout)) defer cancel2() if u.Scheme == "rtmp" { return (&net.Dialer{}).DialContext(ctx2, "tcp", u.Host) } - return (&tls.Dialer{ - Config: tlsConfigForFingerprint(cnf.SourceFingerprint), + return (&ctls.Dialer{ + Config: tls.ConfigForFingerprint(params.Conf.SourceFingerprint), }).DialContext(ctx2, "tcp", u.Host) }() if err != nil { @@ -88,9 +75,9 @@ func (s *rtmpSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *c nconn.Close() return err - case <-reloadConf: + case <-params.ReloadConf: - case <-ctx.Done(): + case <-params.Context.Done(): nconn.Close() <-readDone return nil @@ -98,9 +85,9 @@ func (s *rtmpSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *c } } -func (s *rtmpSource) runReader(u *url.URL, nconn net.Conn) error { - nconn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout))) - nconn.SetWriteDeadline(time.Now().Add(time.Duration(s.writeTimeout))) +func (s *Source) runReader(u *url.URL, nconn net.Conn) error { + nconn.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout))) + nconn.SetWriteDeadline(time.Now().Add(time.Duration(s.WriteTimeout))) conn, err := rtmp.NewClientConn(nconn, u, false) if err != nil { return err @@ -175,23 +162,23 @@ func (s *rtmpSource) runReader(u *url.URL, nconn net.Conn) error { } } - res := s.parent.setReady(pathSourceStaticSetReadyReq{ - desc: &description.Session{Medias: medias}, - generateRTPPackets: true, + res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{ + Desc: &description.Session{Medias: medias}, + GenerateRTPPackets: true, }) - if res.err != nil { - return res.err + if res.Err != nil { + return res.Err } - defer s.parent.setNotReady(pathSourceStaticSetNotReadyReq{}) + defer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{}) - stream = res.stream + stream = res.Stream // disable write deadline to allow outgoing acknowledges nconn.SetWriteDeadline(time.Time{}) for { - nconn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout))) + nconn.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout))) err := mc.Read() if err != nil { return err @@ -199,9 +186,9 @@ func (s *rtmpSource) runReader(u *url.URL, nconn net.Conn) error { } } -// apiSourceDescribe implements sourceStaticImpl. -func (*rtmpSource) apiSourceDescribe() apiPathSourceOrReader { - return apiPathSourceOrReader{ +// APISourceDescribe implements StaticSource. +func (*Source) APISourceDescribe() defs.APIPathSourceOrReader { + return defs.APIPathSourceOrReader{ Type: "rtmpSource", ID: "", } diff --git a/internal/staticsources/rtmp/source_test.go b/internal/staticsources/rtmp/source_test.go new file mode 100644 index 00000000..071f39f6 --- /dev/null +++ b/internal/staticsources/rtmp/source_test.go @@ -0,0 +1,189 @@ +package rtmp + +import ( + "crypto/tls" + "net" + "os" + "testing" + "time" + + "github.com/bluenviron/gortsplib/v4/pkg/format" + "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio" + "github.com/stretchr/testify/require" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" + "github.com/bluenviron/mediamtx/internal/protocols/rtmp" + "github.com/bluenviron/mediamtx/internal/staticsources/tester" +) + +var serverCert = []byte(`-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIUXw1hEC3LFpTsllv7D3ARJyEq7sIwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDEyMTMxNzQ0NThaFw0zMDEy +MTExNzQ0NThaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDG8DyyS51810GsGwgWr5rjJK7OE1kTTLSNEEKax8Bj +zOyiaz8rA2JGl2VUEpi2UjDr9Cm7nd+YIEVs91IIBOb7LGqObBh1kGF3u5aZxLkv +NJE+HrLVvUhaDobK2NU+Wibqc/EI3DfUkt1rSINvv9flwTFu1qHeuLWhoySzDKEp +OzYxpFhwjVSokZIjT4Red3OtFz7gl2E6OAWe2qoh5CwLYVdMWtKR0Xuw3BkDPk9I +qkQKx3fqv97LPEzhyZYjDT5WvGrgZ1WDAN3booxXF3oA1H3GHQc4m/vcLatOtb8e +nI59gMQLEbnp08cl873bAuNuM95EZieXTHNbwUnq5iybAgMBAAGjUzBRMB0GA1Ud +DgQWBBQBKhJh8eWu0a4au9X/2fKhkFX2vjAfBgNVHSMEGDAWgBQBKhJh8eWu0a4a +u9X/2fKhkFX2vjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBj +3aCW0YPKukYgVK9cwN0IbVy/D0C1UPT4nupJcy/E0iC7MXPZ9D/SZxYQoAkdptdO +xfI+RXkpQZLdODNx9uvV+cHyZHZyjtE5ENu/i5Rer2cWI/mSLZm5lUQyx+0KZ2Yu +tEI1bsebDK30msa8QSTn0WidW9XhFnl3gRi4wRdimcQapOWYVs7ih+nAlSvng7NI +XpAyRs8PIEbpDDBMWnldrX4TP6EWYUi49gCp8OUDRREKX3l6Ls1vZ02F34yHIt/7 +7IV/XSKG096bhW+icKBWV0IpcEsgTzPK1J1hMxgjhzIMxGboAeUU+kidthOob6Sd +XQxaORfgM//NzX9LhUPk +-----END CERTIFICATE----- +`) + +var serverKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAxvA8skudfNdBrBsIFq+a4ySuzhNZE0y0jRBCmsfAY8zsoms/ +KwNiRpdlVBKYtlIw6/Qpu53fmCBFbPdSCATm+yxqjmwYdZBhd7uWmcS5LzSRPh6y +1b1IWg6GytjVPlom6nPxCNw31JLda0iDb7/X5cExbtah3ri1oaMkswyhKTs2MaRY +cI1UqJGSI0+EXndzrRc+4JdhOjgFntqqIeQsC2FXTFrSkdF7sNwZAz5PSKpECsd3 +6r/eyzxM4cmWIw0+Vrxq4GdVgwDd26KMVxd6ANR9xh0HOJv73C2rTrW/HpyOfYDE +CxG56dPHJfO92wLjbjPeRGYnl0xzW8FJ6uYsmwIDAQABAoIBACi0BKcyQ3HElSJC +kaAao+Uvnzh4yvPg8Nwf5JDIp/uDdTMyIEWLtrLczRWrjGVZYbsVROinP5VfnPTT +kYwkfKINj2u+gC6lsNuPnRuvHXikF8eO/mYvCTur1zZvsQnF5kp4GGwIqr+qoPUP +bB0UMndG1PdpoMryHe+JcrvTrLHDmCeH10TqOwMsQMLHYLkowvxwJWsmTY7/Qr5S +Wm3PPpOcW2i0uyPVuyuv4yD1368fqnqJ8QFsQp1K6QtYsNnJ71Hut1/IoxK/e6hj +5Z+byKtHVtmcLnABuoOT7BhleJNFBksX9sh83jid4tMBgci+zXNeGmgqo2EmaWAb +agQslkECgYEA8B1rzjOHVQx/vwSzDa4XOrpoHQRfyElrGNz9JVBvnoC7AorezBXQ +M9WTHQIFTGMjzD8pb+YJGi3gj93VN51r0SmJRxBaBRh1ZZI9kFiFzngYev8POgD3 +ygmlS3kTHCNxCK/CJkB+/jMBgtPj5ygDpCWVcTSuWlQFphePkW7jaaECgYEA1Blz +ulqgAyJHZaqgcbcCsI2q6m527hVr9pjzNjIVmkwu38yS9RTCgdlbEVVDnS0hoifl ++jVMEGXjF3xjyMvL50BKbQUH+KAa+V4n1WGlnZOxX9TMny8MBjEuSX2+362vQ3BX +4vOlX00gvoc+sY+lrzvfx/OdPCHQGVYzoKCxhLsCgYA07HcviuIAV/HsO2/vyvhp +xF5gTu+BqNUHNOZDDDid+ge+Jre2yfQLCL8VPLXIQW3Jff53IH/PGl+NtjphuLvj +7UDJvgvpZZuymIojP6+2c3gJ3CASC9aR3JBnUzdoE1O9s2eaoMqc4scpe+SWtZYf +3vzSZ+cqF6zrD/Rf/M35IQKBgHTU4E6ShPm09CcoaeC5sp2WK8OevZw/6IyZi78a +r5Oiy18zzO97U/k6xVMy6F+38ILl/2Rn31JZDVJujniY6eSkIVsUHmPxrWoXV1HO +y++U32uuSFiXDcSLarfIsE992MEJLSAynbF1Rsgsr3gXbGiuToJRyxbIeVy7gwzD +94TpAoGAY4/PejWQj9psZfAhyk5dRGra++gYRQ/gK1IIc1g+Dd2/BxbT/RHr05GK +6vwrfjsoRyMWteC1SsNs/CurjfQ/jqCfHNP5XPvxgd5Ec8sRJIiV7V5RTuWJsPu1 ++3K6cnKEyg+0ekYmLertRFIY6SwWmY1fyKgTvxudMcsBY7dC4xs= +-----END RSA PRIVATE KEY----- +`) + +func writeTempFile(byts []byte) (string, error) { + tmpf, err := os.CreateTemp(os.TempDir(), "rtsp-") + if err != nil { + return "", err + } + defer tmpf.Close() + + _, err = tmpf.Write(byts) + if err != nil { + return "", err + } + + return tmpf.Name(), nil +} + +func TestSource(t *testing.T) { + for _, ca := range []string{ + "plain", + "tls", + } { + t.Run(ca, func(t *testing.T) { + ln, err := func() (net.Listener, error) { + if ca == "plain" { + return net.Listen("tcp", "127.0.0.1:1937") + } + + serverCertFpath, err := writeTempFile(serverCert) + require.NoError(t, err) + defer os.Remove(serverCertFpath) + + serverKeyFpath, err := writeTempFile(serverKey) + require.NoError(t, err) + defer os.Remove(serverKeyFpath) + + var cert tls.Certificate + cert, err = tls.LoadX509KeyPair(serverCertFpath, serverKeyFpath) + require.NoError(t, err) + + return tls.Listen("tcp", "127.0.0.1:1937", &tls.Config{Certificates: []tls.Certificate{cert}}) + }() + require.NoError(t, err) + defer ln.Close() + + go func() { + nconn, err := ln.Accept() + require.NoError(t, err) + defer nconn.Close() + + conn, _, _, err := rtmp.NewServerConn(nconn) + require.NoError(t, err) + + videoTrack := &format.H264{ + PayloadTyp: 96, + SPS: []byte{ // 1920x1080 baseline + 0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, + 0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, + 0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20, + }, + PPS: []byte{0x08, 0x06, 0x07, 0x08}, + PacketizationMode: 1, + } + + audioTrack := &format.MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + } + + w, err := rtmp.NewWriter(conn, videoTrack, audioTrack) + require.NoError(t, err) + + err = w.WriteH264(0, 0, true, [][]byte{{0x05, 0x02, 0x03, 0x04}}) + require.NoError(t, err) + }() + + var te *tester.Tester + + if ca == "plain" { + te = tester.New( + func(p defs.StaticSourceParent) defs.StaticSource { + return &Source{ + ReadTimeout: conf.StringDuration(10 * time.Second), + WriteTimeout: conf.StringDuration(10 * time.Second), + Parent: p, + } + }, + &conf.Path{ + Source: "rtmp://localhost:1937/teststream", + }, + ) + } else { + te = tester.New( + func(p defs.StaticSourceParent) defs.StaticSource { + return &Source{ + ReadTimeout: conf.StringDuration(10 * time.Second), + WriteTimeout: conf.StringDuration(10 * time.Second), + Parent: p, + } + }, + &conf.Path{ + Source: "rtmps://localhost:1937/teststream", + SourceFingerprint: "33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739", + }, + ) + } + + defer te.Close() + + <-te.Unit + }) + } +} diff --git a/internal/core/rtsp_source.go b/internal/staticsources/rtsp/source.go similarity index 59% rename from internal/core/rtsp_source.go rename to internal/staticsources/rtsp/source.go index c5b98c5d..dc661e65 100644 --- a/internal/core/rtsp_source.go +++ b/internal/staticsources/rtsp/source.go @@ -1,7 +1,7 @@ -package core +// Package rtsp contains the RTSP static source. +package rtsp import ( - "context" "time" "github.com/bluenviron/gortsplib/v4" @@ -11,7 +11,9 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/url" "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/logger" + "github.com/bluenviron/mediamtx/internal/protocols/tls" ) func createRangeHeader(cnf *conf.Path) (*headers.Range, error) { @@ -59,50 +61,32 @@ func createRangeHeader(cnf *conf.Path) (*headers.Range, error) { } } -type rtspSourceParent interface { - logger.Writer - setReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes - setNotReady(req pathSourceStaticSetNotReadyReq) +// Source is a RTSP static source. +type Source struct { + ReadTimeout conf.StringDuration + WriteTimeout conf.StringDuration + WriteQueueSize int + Parent defs.StaticSourceParent } -type rtspSource struct { - readTimeout conf.StringDuration - writeTimeout conf.StringDuration - writeQueueSize int - parent rtspSourceParent +// Log implements StaticSource. +func (s *Source) Log(level logger.Level, format string, args ...interface{}) { + s.Parent.Log(level, "[RTSP source] "+format, args...) } -func newRTSPSource( - readTimeout conf.StringDuration, - writeTimeout conf.StringDuration, - writeQueueSize int, - parent rtspSourceParent, -) *rtspSource { - return &rtspSource{ - readTimeout: readTimeout, - writeTimeout: writeTimeout, - writeQueueSize: writeQueueSize, - parent: parent, - } -} - -func (s *rtspSource) Log(level logger.Level, format string, args ...interface{}) { - s.parent.Log(level, "[RTSP source] "+format, args...) -} - -// run implements sourceStaticImpl. -func (s *rtspSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *conf.Path) error { +// Run implements StaticSource. +func (s *Source) Run(params defs.StaticSourceRunParams) error { s.Log(logger.Debug, "connecting") decodeErrLogger := logger.NewLimitedLogger(s) c := &gortsplib.Client{ - Transport: cnf.SourceProtocol.Transport, - TLSConfig: tlsConfigForFingerprint(cnf.SourceFingerprint), - ReadTimeout: time.Duration(s.readTimeout), - WriteTimeout: time.Duration(s.writeTimeout), - WriteQueueSize: s.writeQueueSize, - AnyPortEnable: cnf.SourceAnyPortEnable, + Transport: params.Conf.SourceProtocol.Transport, + TLSConfig: tls.ConfigForFingerprint(params.Conf.SourceFingerprint), + ReadTimeout: time.Duration(s.ReadTimeout), + WriteTimeout: time.Duration(s.WriteTimeout), + WriteQueueSize: s.WriteQueueSize, + AnyPortEnable: params.Conf.SourceAnyPortEnable, OnRequest: func(req *base.Request) { s.Log(logger.Debug, "[c->s] %v", req) }, @@ -120,7 +104,7 @@ func (s *rtspSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *c }, } - u, err := url.Parse(cnf.Source) + u, err := url.Parse(params.Conf.Source) if err != nil { return err } @@ -144,15 +128,15 @@ func (s *rtspSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *c return err } - res := s.parent.setReady(pathSourceStaticSetReadyReq{ - desc: desc, - generateRTPPackets: false, + res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{ + Desc: desc, + GenerateRTPPackets: false, }) - if res.err != nil { - return res.err + if res.Err != nil { + return res.Err } - defer s.parent.setNotReady(pathSourceStaticSetNotReadyReq{}) + defer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{}) for _, medi := range desc.Medias { for _, forma := range medi.Formats { @@ -165,12 +149,12 @@ func (s *rtspSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *c return } - res.stream.WriteRTPPacket(cmedi, cforma, pkt, time.Now(), pts) + res.Stream.WriteRTPPacket(cmedi, cforma, pkt, time.Now(), pts) }) } } - rangeHeader, err := createRangeHeader(cnf) + rangeHeader, err := createRangeHeader(params.Conf) if err != nil { return err } @@ -189,9 +173,9 @@ func (s *rtspSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *c case err := <-readErr: return err - case <-reloadConf: + case <-params.ReloadConf: - case <-ctx.Done(): + case <-params.Context.Done(): c.Close() <-readErr return nil @@ -199,9 +183,9 @@ func (s *rtspSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *c } } -// apiSourceDescribe implements sourceStaticImpl. -func (*rtspSource) apiSourceDescribe() apiPathSourceOrReader { - return apiPathSourceOrReader{ +// APISourceDescribe implements StaticSource. +func (*Source) APISourceDescribe() defs.APIPathSourceOrReader { + return defs.APIPathSourceOrReader{ Type: "rtspSource", ID: "", } diff --git a/internal/staticsources/rtsp/source_test.go b/internal/staticsources/rtsp/source_test.go new file mode 100644 index 00000000..d94f88dc --- /dev/null +++ b/internal/staticsources/rtsp/source_test.go @@ -0,0 +1,432 @@ +package rtsp + +import ( + "crypto/tls" + "os" + "testing" + "time" + + "github.com/bluenviron/gortsplib/v4" + "github.com/bluenviron/gortsplib/v4/pkg/auth" + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/format" + "github.com/pion/rtp" + "github.com/stretchr/testify/require" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" + "github.com/bluenviron/mediamtx/internal/staticsources/tester" +) + +var serverCert = []byte(`-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIUXw1hEC3LFpTsllv7D3ARJyEq7sIwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDEyMTMxNzQ0NThaFw0zMDEy +MTExNzQ0NThaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDG8DyyS51810GsGwgWr5rjJK7OE1kTTLSNEEKax8Bj +zOyiaz8rA2JGl2VUEpi2UjDr9Cm7nd+YIEVs91IIBOb7LGqObBh1kGF3u5aZxLkv +NJE+HrLVvUhaDobK2NU+Wibqc/EI3DfUkt1rSINvv9flwTFu1qHeuLWhoySzDKEp +OzYxpFhwjVSokZIjT4Red3OtFz7gl2E6OAWe2qoh5CwLYVdMWtKR0Xuw3BkDPk9I +qkQKx3fqv97LPEzhyZYjDT5WvGrgZ1WDAN3booxXF3oA1H3GHQc4m/vcLatOtb8e +nI59gMQLEbnp08cl873bAuNuM95EZieXTHNbwUnq5iybAgMBAAGjUzBRMB0GA1Ud +DgQWBBQBKhJh8eWu0a4au9X/2fKhkFX2vjAfBgNVHSMEGDAWgBQBKhJh8eWu0a4a +u9X/2fKhkFX2vjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBj +3aCW0YPKukYgVK9cwN0IbVy/D0C1UPT4nupJcy/E0iC7MXPZ9D/SZxYQoAkdptdO +xfI+RXkpQZLdODNx9uvV+cHyZHZyjtE5ENu/i5Rer2cWI/mSLZm5lUQyx+0KZ2Yu +tEI1bsebDK30msa8QSTn0WidW9XhFnl3gRi4wRdimcQapOWYVs7ih+nAlSvng7NI +XpAyRs8PIEbpDDBMWnldrX4TP6EWYUi49gCp8OUDRREKX3l6Ls1vZ02F34yHIt/7 +7IV/XSKG096bhW+icKBWV0IpcEsgTzPK1J1hMxgjhzIMxGboAeUU+kidthOob6Sd +XQxaORfgM//NzX9LhUPk +-----END CERTIFICATE----- +`) + +var serverKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAxvA8skudfNdBrBsIFq+a4ySuzhNZE0y0jRBCmsfAY8zsoms/ +KwNiRpdlVBKYtlIw6/Qpu53fmCBFbPdSCATm+yxqjmwYdZBhd7uWmcS5LzSRPh6y +1b1IWg6GytjVPlom6nPxCNw31JLda0iDb7/X5cExbtah3ri1oaMkswyhKTs2MaRY +cI1UqJGSI0+EXndzrRc+4JdhOjgFntqqIeQsC2FXTFrSkdF7sNwZAz5PSKpECsd3 +6r/eyzxM4cmWIw0+Vrxq4GdVgwDd26KMVxd6ANR9xh0HOJv73C2rTrW/HpyOfYDE +CxG56dPHJfO92wLjbjPeRGYnl0xzW8FJ6uYsmwIDAQABAoIBACi0BKcyQ3HElSJC +kaAao+Uvnzh4yvPg8Nwf5JDIp/uDdTMyIEWLtrLczRWrjGVZYbsVROinP5VfnPTT +kYwkfKINj2u+gC6lsNuPnRuvHXikF8eO/mYvCTur1zZvsQnF5kp4GGwIqr+qoPUP +bB0UMndG1PdpoMryHe+JcrvTrLHDmCeH10TqOwMsQMLHYLkowvxwJWsmTY7/Qr5S +Wm3PPpOcW2i0uyPVuyuv4yD1368fqnqJ8QFsQp1K6QtYsNnJ71Hut1/IoxK/e6hj +5Z+byKtHVtmcLnABuoOT7BhleJNFBksX9sh83jid4tMBgci+zXNeGmgqo2EmaWAb +agQslkECgYEA8B1rzjOHVQx/vwSzDa4XOrpoHQRfyElrGNz9JVBvnoC7AorezBXQ +M9WTHQIFTGMjzD8pb+YJGi3gj93VN51r0SmJRxBaBRh1ZZI9kFiFzngYev8POgD3 +ygmlS3kTHCNxCK/CJkB+/jMBgtPj5ygDpCWVcTSuWlQFphePkW7jaaECgYEA1Blz +ulqgAyJHZaqgcbcCsI2q6m527hVr9pjzNjIVmkwu38yS9RTCgdlbEVVDnS0hoifl ++jVMEGXjF3xjyMvL50BKbQUH+KAa+V4n1WGlnZOxX9TMny8MBjEuSX2+362vQ3BX +4vOlX00gvoc+sY+lrzvfx/OdPCHQGVYzoKCxhLsCgYA07HcviuIAV/HsO2/vyvhp +xF5gTu+BqNUHNOZDDDid+ge+Jre2yfQLCL8VPLXIQW3Jff53IH/PGl+NtjphuLvj +7UDJvgvpZZuymIojP6+2c3gJ3CASC9aR3JBnUzdoE1O9s2eaoMqc4scpe+SWtZYf +3vzSZ+cqF6zrD/Rf/M35IQKBgHTU4E6ShPm09CcoaeC5sp2WK8OevZw/6IyZi78a +r5Oiy18zzO97U/k6xVMy6F+38ILl/2Rn31JZDVJujniY6eSkIVsUHmPxrWoXV1HO +y++U32uuSFiXDcSLarfIsE992MEJLSAynbF1Rsgsr3gXbGiuToJRyxbIeVy7gwzD +94TpAoGAY4/PejWQj9psZfAhyk5dRGra++gYRQ/gK1IIc1g+Dd2/BxbT/RHr05GK +6vwrfjsoRyMWteC1SsNs/CurjfQ/jqCfHNP5XPvxgd5Ec8sRJIiV7V5RTuWJsPu1 ++3K6cnKEyg+0ekYmLertRFIY6SwWmY1fyKgTvxudMcsBY7dC4xs= +-----END RSA PRIVATE KEY----- +`) + +func writeTempFile(byts []byte) (string, error) { + tmpf, err := os.CreateTemp(os.TempDir(), "rtsp-") + if err != nil { + return "", err + } + defer tmpf.Close() + + _, err = tmpf.Write(byts) + if err != nil { + return "", err + } + + return tmpf.Name(), nil +} + +type testServer struct { + onDescribe func(*gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) + onSetup func(*gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) + onPlay func(*gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) +} + +func (sh *testServer) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, +) (*base.Response, *gortsplib.ServerStream, error) { + return sh.onDescribe(ctx) +} + +func (sh *testServer) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) { + return sh.onSetup(ctx) +} + +func (sh *testServer) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { + return sh.onPlay(ctx) +} + +var testMediaH264 = &description.Media{ + Type: description.MediaTypeVideo, + Formats: []format.Format{&format.H264{ + PayloadTyp: 96, + SPS: []byte{ // 1920x1080 baseline + 0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, + 0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, + 0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20, + }, + PPS: []byte{0x08, 0x06, 0x07, 0x08}, + PacketizationMode: 1, + }}, +} + +func TestRTSPSource(t *testing.T) { + for _, source := range []string{ + "udp", + "tcp", + "tls", + } { + t.Run(source, func(t *testing.T) { + var stream *gortsplib.ServerStream + + nonce, err := auth.GenerateNonce() + require.NoError(t, err) + + s := gortsplib.Server{ + Handler: &testServer{ + onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx, + ) (*base.Response, *gortsplib.ServerStream, error) { + err := auth.Validate(ctx.Request, "testuser", "testpass", nil, nil, "IPCAM", nonce) + if err != nil { + return &base.Response{ //nolint:nilerr + StatusCode: base.StatusUnauthorized, + Header: base.Header{ + "WWW-Authenticate": auth.GenerateWWWAuthenticate(nil, "IPCAM", nonce), + }, + }, nil, nil + } + + return &base.Response{ + StatusCode: base.StatusOK, + }, stream, nil + }, + onSetup: func(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) { + return &base.Response{ + StatusCode: base.StatusOK, + }, stream, nil + }, + onPlay: func(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { + go func() { + time.Sleep(100 * time.Millisecond) + err := stream.WritePacketRTP(testMediaH264, &rtp.Packet{ + Header: rtp.Header{ + Version: 0x02, + PayloadType: 96, + SequenceNumber: 57899, + Timestamp: 345234345, + SSRC: 978651231, + Marker: true, + }, + Payload: []byte{5, 1, 2, 3, 4}, + }) + require.NoError(t, err) + }() + + return &base.Response{ + StatusCode: base.StatusOK, + }, nil + }, + }, + RTSPAddress: "127.0.0.1:8555", + } + + switch source { + case "udp": + s.UDPRTPAddress = "127.0.0.1:8002" + s.UDPRTCPAddress = "127.0.0.1:8003" + + case "tls": + serverCertFpath, err := writeTempFile(serverCert) + require.NoError(t, err) + defer os.Remove(serverCertFpath) + + serverKeyFpath, err := writeTempFile(serverKey) + require.NoError(t, err) + defer os.Remove(serverKeyFpath) + + cert, err := tls.LoadX509KeyPair(serverCertFpath, serverKeyFpath) + require.NoError(t, err) + + s.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}} + } + + err = s.Start() + require.NoError(t, err) + defer s.Wait() //nolint:errcheck + defer s.Close() + + stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{testMediaH264}}) + defer stream.Close() + + var te *tester.Tester + + if source != "tls" { + var sp conf.SourceProtocol + sp.UnmarshalJSON([]byte(`"` + source + `"`)) //nolint:errcheck + + te = tester.New( + func(p defs.StaticSourceParent) defs.StaticSource { + return &Source{ + ReadTimeout: conf.StringDuration(10 * time.Second), + WriteTimeout: conf.StringDuration(10 * time.Second), + WriteQueueSize: 2048, + Parent: p, + } + }, + &conf.Path{ + Source: "rtsp://testuser:testpass@localhost:8555/teststream", + SourceProtocol: sp, + }, + ) + } else { + te = tester.New( + func(p defs.StaticSourceParent) defs.StaticSource { + return &Source{ + ReadTimeout: conf.StringDuration(10 * time.Second), + WriteTimeout: conf.StringDuration(10 * time.Second), + WriteQueueSize: 2048, + Parent: p, + } + }, + &conf.Path{ + Source: "rtsps://testuser:testpass@localhost:8555/teststream", + SourceFingerprint: "33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739", + }, + ) + } + + defer te.Close() + + <-te.Unit + }) + } +} + +func TestRTSPSourceNoPassword(t *testing.T) { + var stream *gortsplib.ServerStream + + nonce, err := auth.GenerateNonce() + require.NoError(t, err) + + s := gortsplib.Server{ + Handler: &testServer{ + onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) { + err := auth.Validate(ctx.Request, "testuser", "", nil, nil, "IPCAM", nonce) + if err != nil { + return &base.Response{ //nolint:nilerr + StatusCode: base.StatusUnauthorized, + Header: base.Header{ + "WWW-Authenticate": auth.GenerateWWWAuthenticate(nil, "IPCAM", nonce), + }, + }, nil, nil + } + + return &base.Response{ + StatusCode: base.StatusOK, + }, stream, nil + }, + onSetup: func(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) { + go func() { + time.Sleep(100 * time.Millisecond) + err := stream.WritePacketRTP(testMediaH264, &rtp.Packet{ + Header: rtp.Header{ + Version: 0x02, + PayloadType: 96, + SequenceNumber: 57899, + Timestamp: 345234345, + SSRC: 978651231, + Marker: true, + }, + Payload: []byte{5, 1, 2, 3, 4}, + }) + require.NoError(t, err) + }() + + return &base.Response{ + StatusCode: base.StatusOK, + }, stream, nil + }, + onPlay: func(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { + return &base.Response{ + StatusCode: base.StatusOK, + }, nil + }, + }, + RTSPAddress: "127.0.0.1:8555", + } + + err = s.Start() + require.NoError(t, err) + defer s.Wait() //nolint:errcheck + defer s.Close() + + stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{testMediaH264}}) + defer stream.Close() + + var sp conf.SourceProtocol + sp.UnmarshalJSON([]byte(`"tcp"`)) //nolint:errcheck + + te := tester.New( + func(p defs.StaticSourceParent) defs.StaticSource { + return &Source{ + ReadTimeout: conf.StringDuration(10 * time.Second), + WriteTimeout: conf.StringDuration(10 * time.Second), + WriteQueueSize: 2048, + Parent: p, + } + }, + &conf.Path{ + Source: "rtsp://testuser:@127.0.0.1:8555/teststream", + SourceProtocol: sp, + }, + ) + defer te.Close() + + <-te.Unit +} + +func TestRTSPSourceRange(t *testing.T) { + for _, ca := range []string{"clock", "npt", "smpte"} { + t.Run(ca, func(t *testing.T) { + var stream *gortsplib.ServerStream + + s := gortsplib.Server{ + Handler: &testServer{ + onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) { + return &base.Response{ + StatusCode: base.StatusOK, + }, stream, nil + }, + onSetup: func(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) { + return &base.Response{ + StatusCode: base.StatusOK, + }, stream, nil + }, + onPlay: func(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { + switch ca { + case "clock": + require.Equal(t, base.HeaderValue{"clock=20230812T120000Z-"}, ctx.Request.Header["Range"]) + + case "npt": + require.Equal(t, base.HeaderValue{"npt=0.35-"}, ctx.Request.Header["Range"]) + + case "smpte": + require.Equal(t, base.HeaderValue{"smpte=0:02:10-"}, ctx.Request.Header["Range"]) + } + + go func() { + time.Sleep(100 * time.Millisecond) + err := stream.WritePacketRTP(testMediaH264, &rtp.Packet{ + Header: rtp.Header{ + Version: 0x02, + PayloadType: 96, + SequenceNumber: 57899, + Timestamp: 345234345, + SSRC: 978651231, + Marker: true, + }, + Payload: []byte{5, 1, 2, 3, 4}, + }) + require.NoError(t, err) + }() + + return &base.Response{ + StatusCode: base.StatusOK, + }, nil + }, + }, + RTSPAddress: "127.0.0.1:8555", + } + + err := s.Start() + require.NoError(t, err) + defer s.Wait() //nolint:errcheck + defer s.Close() + + stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{testMediaH264}}) + defer stream.Close() + + cnf := &conf.Path{ + Source: "rtsp://127.0.0.1:8555/teststream", + } + + switch ca { + case "clock": + cnf.RTSPRangeType = conf.RTSPRangeTypeClock + cnf.RTSPRangeStart = "20230812T120000Z" + + case "npt": + cnf.RTSPRangeType = conf.RTSPRangeTypeNPT + cnf.RTSPRangeStart = "350ms" + + case "smpte": + cnf.RTSPRangeType = conf.RTSPRangeTypeSMPTE + cnf.RTSPRangeStart = "130s" + } + + te := tester.New( + func(p defs.StaticSourceParent) defs.StaticSource { + return &Source{ + ReadTimeout: conf.StringDuration(10 * time.Second), + WriteTimeout: conf.StringDuration(10 * time.Second), + WriteQueueSize: 2048, + Parent: p, + } + }, + cnf, + ) + defer te.Close() + + <-te.Unit + }) + } +} diff --git a/internal/staticsources/srt/source.go b/internal/staticsources/srt/source.go new file mode 100644 index 00000000..5d6f34c0 --- /dev/null +++ b/internal/staticsources/srt/source.go @@ -0,0 +1,115 @@ +// Package srt contains the SRT static source. +package srt + +import ( + "time" + + "github.com/bluenviron/gortsplib/v4/pkg/description" + mcmpegts "github.com/bluenviron/mediacommon/pkg/formats/mpegts" + "github.com/datarhei/gosrt" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" + "github.com/bluenviron/mediamtx/internal/logger" + "github.com/bluenviron/mediamtx/internal/protocols/mpegts" + "github.com/bluenviron/mediamtx/internal/stream" +) + +// Source is a SRT static source. +type Source struct { + ReadTimeout conf.StringDuration + Parent defs.StaticSourceParent +} + +// Log implements StaticSource. +func (s *Source) Log(level logger.Level, format string, args ...interface{}) { + s.Parent.Log(level, "[SRT source] "+format, args...) +} + +// Run implements StaticSource. +func (s *Source) Run(params defs.StaticSourceRunParams) error { + s.Log(logger.Debug, "connecting") + + conf := srt.DefaultConfig() + address, err := conf.UnmarshalURL(params.Conf.Source) + if err != nil { + return err + } + + err = conf.Validate() + if err != nil { + return err + } + + sconn, err := srt.Dial("srt", address, conf) + if err != nil { + return err + } + + readDone := make(chan error) + go func() { + readDone <- s.runReader(sconn) + }() + + for { + select { + case err := <-readDone: + sconn.Close() + return err + + case <-params.ReloadConf: + + case <-params.Context.Done(): + sconn.Close() + <-readDone + return nil + } + } +} + +func (s *Source) runReader(sconn srt.Conn) error { + sconn.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout))) + r, err := mcmpegts.NewReader(mcmpegts.NewBufferedReader(sconn)) + if err != nil { + return err + } + + decodeErrLogger := logger.NewLimitedLogger(s) + + r.OnDecodeError(func(err error) { + decodeErrLogger.Log(logger.Warn, err.Error()) + }) + + var stream *stream.Stream + + medias, err := mpegts.ToStream(r, &stream) + if err != nil { + return err + } + + res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{ + Desc: &description.Session{Medias: medias}, + GenerateRTPPackets: true, + }) + if res.Err != nil { + return res.Err + } + + stream = res.Stream + + for { + sconn.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout))) + err := r.Read() + if err != nil { + return err + } + } +} + +// APISourceDescribe implements StaticSource. +func (*Source) APISourceDescribe() defs.APIPathSourceOrReader { + return defs.APIPathSourceOrReader{ + Type: "srtSource", + ID: "", + } +} diff --git a/internal/staticsources/srt/source_test.go b/internal/staticsources/srt/source_test.go new file mode 100644 index 00000000..813a9b17 --- /dev/null +++ b/internal/staticsources/srt/source_test.go @@ -0,0 +1,73 @@ +package srt + +import ( + "bufio" + "testing" + "time" + + "github.com/bluenviron/mediacommon/pkg/formats/mpegts" + "github.com/datarhei/gosrt" + "github.com/stretchr/testify/require" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" + "github.com/bluenviron/mediamtx/internal/staticsources/tester" +) + +func TestSource(t *testing.T) { + ln, err := srt.Listen("srt", "localhost:9002", srt.DefaultConfig()) + require.NoError(t, err) + defer ln.Close() + + go func() { + conn, _, err := ln.Accept(func(req srt.ConnRequest) srt.ConnType { + require.Equal(t, "sidname", req.StreamId()) + err := req.SetPassphrase("ttest1234567") + if err != nil { + return srt.REJECT + } + return srt.SUBSCRIBE + }) + require.NoError(t, err) + require.NotNil(t, conn) + defer conn.Close() + + track := &mpegts.Track{ + Codec: &mpegts.CodecH264{}, + } + + bw := bufio.NewWriter(conn) + w := mpegts.NewWriter(bw, []*mpegts.Track{track}) + require.NoError(t, err) + + err = w.WriteH26x(track, 0, 0, true, [][]byte{{ // IDR + 5, 1, + }}) + require.NoError(t, err) + + err = w.WriteH26x(track, 0, 0, true, [][]byte{{ // non-IDR + 5, 2, + }}) + require.NoError(t, err) + + err = bw.Flush() + require.NoError(t, err) + + time.Sleep(1000 * time.Millisecond) + }() + + te := tester.New( + func(p defs.StaticSourceParent) defs.StaticSource { + return &Source{ + ReadTimeout: conf.StringDuration(10 * time.Second), + Parent: p, + } + }, + &conf.Path{ + Source: "srt://localhost:9002?streamid=sidname&passphrase=ttest1234567", + }, + ) + defer te.Close() + + <-te.Unit +} diff --git a/internal/staticsources/tester/tester.go b/internal/staticsources/tester/tester.go new file mode 100644 index 00000000..1a55e8ff --- /dev/null +++ b/internal/staticsources/tester/tester.go @@ -0,0 +1,86 @@ +// Package tester contains a static source tester. +package tester + +import ( + "context" + + "github.com/bluenviron/mediamtx/internal/asyncwriter" + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" + "github.com/bluenviron/mediamtx/internal/logger" + "github.com/bluenviron/mediamtx/internal/stream" + "github.com/bluenviron/mediamtx/internal/unit" +) + +// Tester is a static source tester. +type Tester struct { + ctx context.Context + ctxCancel func() + stream *stream.Stream + writer *asyncwriter.Writer + + Unit chan unit.Unit + done chan struct{} +} + +// New allocates a tester. +func New(createFunc func(defs.StaticSourceParent) defs.StaticSource, conf *conf.Path) *Tester { + ctx, ctxCancel := context.WithCancel(context.Background()) + + t := &Tester{ + ctx: ctx, + ctxCancel: ctxCancel, + Unit: make(chan unit.Unit), + done: make(chan struct{}), + } + + s := createFunc(t) + + go func() { + s.Run(defs.StaticSourceRunParams{ //nolint:errcheck + Context: ctx, + Conf: conf, + }) + close(t.done) + }() + + return t +} + +// Close closes the tester. +func (t *Tester) Close() { + t.ctxCancel() + t.writer.Stop() + t.stream.Close() + <-t.done +} + +// Log implements StaticSourceParent. +func (t *Tester) Log(_ logger.Level, _ string, _ ...interface{}) { +} + +// SetReady implements StaticSourceParent. +func (t *Tester) SetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes { + t.stream, _ = stream.New( + 1460, + req.Desc, + req.GenerateRTPPackets, + t, + ) + + t.writer = asyncwriter.New(2048, t) + t.stream.AddReader(t.writer, req.Desc.Medias[0], req.Desc.Medias[0].Formats[0], func(u unit.Unit) error { + t.Unit <- u + close(t.Unit) + return nil + }) + t.writer.Start() + + return defs.PathSourceStaticSetReadyRes{ + Stream: t.stream, + } +} + +// SetNotReady implements StaticSourceParent. +func (t *Tester) SetNotReady(_ defs.PathSourceStaticSetNotReadyReq) { +} diff --git a/internal/core/udp_source.go b/internal/staticsources/udp/source.go similarity index 52% rename from internal/core/udp_source.go rename to internal/staticsources/udp/source.go index 987200c6..4c7cd8c7 100644 --- a/internal/core/udp_source.go +++ b/internal/staticsources/udp/source.go @@ -1,17 +1,20 @@ -package core +// Package udp contains the UDP static source. +package udp import ( - "context" "fmt" "net" "time" "github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/multicast" - "github.com/bluenviron/mediacommon/pkg/formats/mpegts" + mcmpegts "github.com/bluenviron/mediacommon/pkg/formats/mpegts" "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/logger" + "github.com/bluenviron/mediamtx/internal/protocols/mpegts" + "github.com/bluenviron/mediamtx/internal/restrictnetwork" "github.com/bluenviron/mediamtx/internal/stream" ) @@ -40,36 +43,22 @@ type packetConn interface { SetReadBuffer(int) error } -type udpSourceParent interface { - logger.Writer - setReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes - setNotReady(req pathSourceStaticSetNotReadyReq) +// Source is a UDP static source. +type Source struct { + ReadTimeout conf.StringDuration + Parent defs.StaticSourceParent } -type udpSource struct { - readTimeout conf.StringDuration - parent udpSourceParent +// Log implements StaticSource. +func (s *Source) Log(level logger.Level, format string, args ...interface{}) { + s.Parent.Log(level, "[UDP source] "+format, args...) } -func newUDPSource( - readTimeout conf.StringDuration, - parent udpSourceParent, -) *udpSource { - return &udpSource{ - readTimeout: readTimeout, - parent: parent, - } -} - -func (s *udpSource) Log(level logger.Level, format string, args ...interface{}) { - s.parent.Log(level, "[UDP source] "+format, args...) -} - -// run implements sourceStaticImpl. -func (s *udpSource) run(ctx context.Context, cnf *conf.Path, _ chan *conf.Path) error { +// Run implements StaticSource. +func (s *Source) Run(params defs.StaticSourceRunParams) error { s.Log(logger.Debug, "connecting") - hostPort := cnf.Source[len("udp://"):] + hostPort := params.Conf.Source[len("udp://"):] addr, err := net.ResolveUDPAddr("udp", hostPort) if err != nil { @@ -84,7 +73,7 @@ func (s *udpSource) run(ctx context.Context, cnf *conf.Path, _ chan *conf.Path) return err } } else { - tmp, err := net.ListenPacket(restrictNetwork("udp", addr.String())) + tmp, err := net.ListenPacket(restrictnetwork.Restrict("udp", addr.String())) if err != nil { return err } @@ -107,16 +96,16 @@ func (s *udpSource) run(ctx context.Context, cnf *conf.Path, _ chan *conf.Path) case err := <-readerErr: return err - case <-ctx.Done(): + case <-params.Context.Done(): pc.Close() <-readerErr return fmt.Errorf("terminated") } } -func (s *udpSource) runReader(pc net.PacketConn) error { - pc.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout))) - r, err := mpegts.NewReader(mpegts.NewBufferedReader(newPacketConnReader(pc))) +func (s *Source) runReader(pc net.PacketConn) error { + pc.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout))) + r, err := mcmpegts.NewReader(mcmpegts.NewBufferedReader(newPacketConnReader(pc))) if err != nil { return err } @@ -129,25 +118,25 @@ func (s *udpSource) runReader(pc net.PacketConn) error { var stream *stream.Stream - medias, err := mpegtsSetupRead(r, &stream) + medias, err := mpegts.ToStream(r, &stream) if err != nil { return err } - res := s.parent.setReady(pathSourceStaticSetReadyReq{ - desc: &description.Session{Medias: medias}, - generateRTPPackets: true, + res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{ + Desc: &description.Session{Medias: medias}, + GenerateRTPPackets: true, }) - if res.err != nil { - return res.err + if res.Err != nil { + return res.Err } - defer s.parent.setNotReady(pathSourceStaticSetNotReadyReq{}) + defer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{}) - stream = res.stream + stream = res.Stream for { - pc.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout))) + pc.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout))) err := r.Read() if err != nil { return err @@ -155,9 +144,9 @@ func (s *udpSource) runReader(pc net.PacketConn) error { } } -// apiSourceDescribe implements sourceStaticImpl. -func (*udpSource) apiSourceDescribe() apiPathSourceOrReader { - return apiPathSourceOrReader{ +// APISourceDescribe implements StaticSource. +func (*Source) APISourceDescribe() defs.APIPathSourceOrReader { + return defs.APIPathSourceOrReader{ Type: "udpSource", ID: "", } diff --git a/internal/staticsources/udp/source_test.go b/internal/staticsources/udp/source_test.go new file mode 100644 index 00000000..dc6fc31c --- /dev/null +++ b/internal/staticsources/udp/source_test.go @@ -0,0 +1,59 @@ +package udp + +import ( + "bufio" + "net" + "testing" + "time" + + "github.com/bluenviron/mediacommon/pkg/formats/mpegts" + "github.com/stretchr/testify/require" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" + "github.com/bluenviron/mediamtx/internal/staticsources/tester" +) + +func TestSource(t *testing.T) { + te := tester.New( + func(p defs.StaticSourceParent) defs.StaticSource { + return &Source{ + ReadTimeout: conf.StringDuration(10 * time.Second), + Parent: p, + } + }, + &conf.Path{ + Source: "udp://localhost:9001", + }, + ) + defer te.Close() + + time.Sleep(50 * time.Millisecond) + + conn, err := net.Dial("udp", "localhost:9001") + require.NoError(t, err) + defer conn.Close() + + track := &mpegts.Track{ + Codec: &mpegts.CodecH264{}, + } + + bw := bufio.NewWriter(conn) + w := mpegts.NewWriter(bw, []*mpegts.Track{track}) + require.NoError(t, err) + + err = w.WriteH26x(track, 0, 0, true, [][]byte{{ // IDR + 5, 1, + }}) + require.NoError(t, err) + + err = w.WriteH26x(track, 0, 0, true, [][]byte{{ // non-IDR + 5, 2, + }}) + require.NoError(t, err) + + err = bw.Flush() + require.NoError(t, err) + + <-te.Unit +} diff --git a/internal/staticsources/webrtc/source.go b/internal/staticsources/webrtc/source.go new file mode 100644 index 00000000..d598fdd6 --- /dev/null +++ b/internal/staticsources/webrtc/source.go @@ -0,0 +1,103 @@ +// Package webrtc contains the WebRTC static source. +package webrtc + +import ( + "net/http" + "net/url" + "strings" + "time" + + "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/rtptime" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" + "github.com/bluenviron/mediamtx/internal/logger" + "github.com/bluenviron/mediamtx/internal/protocols/webrtc" +) + +// Source is a WebRTC static source. +type Source struct { + ReadTimeout conf.StringDuration + + Parent defs.StaticSourceParent +} + +// Log implements StaticSource. +func (s *Source) Log(level logger.Level, format string, args ...interface{}) { + s.Parent.Log(level, "[WebRTC source] "+format, args...) +} + +// Run implements StaticSource. +func (s *Source) Run(params defs.StaticSourceRunParams) error { + s.Log(logger.Debug, "connecting") + + u, err := url.Parse(params.Conf.Source) + if err != nil { + return err + } + + u.Scheme = strings.ReplaceAll(u.Scheme, "whep", "http") + + hc := &http.Client{ + Timeout: time.Duration(s.ReadTimeout), + } + + client := webrtc.WHIPClient{ + HTTPClient: hc, + URL: u, + Log: s, + } + + tracks, err := client.Read(params.Context) + if err != nil { + return err + } + defer client.Close() //nolint:errcheck + + medias := webrtc.TracksToMedias(tracks) + + rres := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{ + Desc: &description.Session{Medias: medias}, + GenerateRTPPackets: true, + }) + if rres.Err != nil { + return rres.Err + } + + defer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{}) + + timeDecoder := rtptime.NewGlobalDecoder() + + for i, media := range medias { + ci := i + cmedia := media + trackWrapper := &webrtc.TrackWrapper{ClockRat: cmedia.Formats[0].ClockRate()} + + go func() { + for { + pkt, err := tracks[ci].ReadRTP() + if err != nil { + return + } + + pts, ok := timeDecoder.Decode(trackWrapper, pkt) + if !ok { + continue + } + + rres.Stream.WriteRTPPacket(cmedia, cmedia.Formats[0], pkt, time.Now(), pts) + } + }() + } + + return client.Wait(params.Context) +} + +// APISourceDescribe implements StaticSource. +func (*Source) APISourceDescribe() defs.APIPathSourceOrReader { + return defs.APIPathSourceOrReader{ + Type: "webrtcSource", + ID: "", + } +} diff --git a/internal/core/webrtc_source_test.go b/internal/staticsources/webrtc/source_test.go similarity index 60% rename from internal/core/webrtc_source_test.go rename to internal/staticsources/webrtc/source_test.go index 71b697b6..0dfddfdf 100644 --- a/internal/core/webrtc_source_test.go +++ b/internal/staticsources/webrtc/source_test.go @@ -1,4 +1,4 @@ -package core +package webrtc import ( "context" @@ -6,19 +6,27 @@ import ( "net" "net/http" "testing" + "time" - "github.com/bluenviron/gortsplib/v4" "github.com/bluenviron/gortsplib/v4/pkg/format" - "github.com/bluenviron/gortsplib/v4/pkg/url" "github.com/pion/rtp" + pwebrtc "github.com/pion/webrtc/v3" "github.com/stretchr/testify/require" + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/protocols/webrtc" + "github.com/bluenviron/mediamtx/internal/staticsources/tester" ) -func TestWebRTCSource(t *testing.T) { - state := 0 +func whipOffer(body []byte) *pwebrtc.SessionDescription { + return &pwebrtc.SessionDescription{ + Type: pwebrtc.SDPTypeOffer, + SDP: string(body), + } +} +func TestSource(t *testing.T) { api, err := webrtc.NewAPI(webrtc.APIConf{}) require.NoError(t, err) @@ -31,9 +39,7 @@ func TestWebRTCSource(t *testing.T) { defer pc.Close() tracks, err := pc.SetupOutgoingTracks( - &format.VP8{ - PayloadTyp: 96, - }, + nil, &format.Opus{ PayloadTyp: 111, IsStereo: true, @@ -41,6 +47,8 @@ func TestWebRTCSource(t *testing.T) { ) require.NoError(t, err) + state := 0 + httpServ := &http.Server{ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch state { @@ -79,23 +87,10 @@ func TestWebRTCSource(t *testing.T) { Header: rtp.Header{ Version: 2, Marker: true, - PayloadType: 96, - SequenceNumber: 123, - Timestamp: 45343, - SSRC: 563423, - }, - Payload: []byte{5, 1}, - }) - require.NoError(t, err) - - err = tracks[1].WriteRTP(&rtp.Packet{ - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 97, + PayloadType: 111, SequenceNumber: 1123, Timestamp: 45343, - SSRC: 563423, + SSRC: 563424, }, Payload: []byte{5, 2}, }) @@ -120,59 +115,24 @@ func TestWebRTCSource(t *testing.T) { }), } - ln, err := net.Listen("tcp", "localhost:5555") + ln, err := net.Listen("tcp", "localhost:9003") require.NoError(t, err) go httpServ.Serve(ln) defer httpServ.Shutdown(context.Background()) - p, ok := newInstance("paths:\n" + - " proxied:\n" + - " source: whep://localhost:5555/my/resource\n" + - " sourceOnDemand: yes\n") - require.Equal(t, true, ok) - defer p.Close() - - c := gortsplib.Client{} - - u, err := url.Parse("rtsp://127.0.0.1:8554/proxied") - require.NoError(t, err) - - err = c.Start(u.Scheme, u.Host) - require.NoError(t, err) - defer c.Close() - - desc, _, err := c.Describe(u) - require.NoError(t, err) - - var forma *format.VP8 - medi := desc.FindFormat(&forma) - - _, err = c.Setup(desc.BaseURL, medi, 0, 0) - require.NoError(t, err) - - received := make(chan struct{}) - - c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) { - require.Equal(t, []byte{5, 3}, pkt.Payload) - close(received) - }) - - _, err = c.Play(nil) - require.NoError(t, err) - - err = tracks[0].WriteRTP(&rtp.Packet{ - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 124, - Timestamp: 45343, - SSRC: 563423, + te := tester.New( + func(p defs.StaticSourceParent) defs.StaticSource { + return &Source{ + ReadTimeout: conf.StringDuration(10 * time.Second), + Parent: p, + } }, - Payload: []byte{5, 3}, - }) - require.NoError(t, err) + &conf.Path{ + Source: "whep://localhost:9003/my/resource", + }, + ) + defer te.Close() - <-received + <-te.Unit }