1
0
Fork 0
forked from External/mediamtx

move static sources into dedicated package (#2616)

This commit is contained in:
Alessandro Ros 2023-10-31 14:19:04 +01:00 committed by GitHub
parent e9528c0917
commit 43d41c070b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 2271 additions and 2172 deletions

View file

@ -14,8 +14,10 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/httpserv" "github.com/bluenviron/mediamtx/internal/protocols/httpserv"
"github.com/bluenviron/mediamtx/internal/restrictnetwork"
) )
func interfaceIsEmpty(i interface{}) bool { func interfaceIsEmpty(i interface{}) bool {
@ -96,38 +98,38 @@ func paramName(ctx *gin.Context) (string, bool) {
} }
type apiPathManager interface { type apiPathManager interface {
apiPathsList() (*apiPathList, error) apiPathsList() (*defs.APIPathList, error)
apiPathsGet(string) (*apiPath, error) apiPathsGet(string) (*defs.APIPath, error)
} }
type apiHLSManager interface { type apiHLSManager interface {
apiMuxersList() (*apiHLSMuxerList, error) apiMuxersList() (*defs.APIHLSMuxerList, error)
apiMuxersGet(string) (*apiHLSMuxer, error) apiMuxersGet(string) (*defs.APIHLSMuxer, error)
} }
type apiRTSPServer interface { type apiRTSPServer interface {
apiConnsList() (*apiRTSPConnsList, error) apiConnsList() (*defs.APIRTSPConnsList, error)
apiConnsGet(uuid.UUID) (*apiRTSPConn, error) apiConnsGet(uuid.UUID) (*defs.APIRTSPConn, error)
apiSessionsList() (*apiRTSPSessionList, error) apiSessionsList() (*defs.APIRTSPSessionList, error)
apiSessionsGet(uuid.UUID) (*apiRTSPSession, error) apiSessionsGet(uuid.UUID) (*defs.APIRTSPSession, error)
apiSessionsKick(uuid.UUID) error apiSessionsKick(uuid.UUID) error
} }
type apiRTMPServer interface { type apiRTMPServer interface {
apiConnsList() (*apiRTMPConnList, error) apiConnsList() (*defs.APIRTMPConnList, error)
apiConnsGet(uuid.UUID) (*apiRTMPConn, error) apiConnsGet(uuid.UUID) (*defs.APIRTMPConn, error)
apiConnsKick(uuid.UUID) error apiConnsKick(uuid.UUID) error
} }
type apiWebRTCManager interface { type apiWebRTCManager interface {
apiSessionsList() (*apiWebRTCSessionList, error) apiSessionsList() (*defs.APIWebRTCSessionList, error)
apiSessionsGet(uuid.UUID) (*apiWebRTCSession, error) apiSessionsGet(uuid.UUID) (*defs.APIWebRTCSession, error)
apiSessionsKick(uuid.UUID) error apiSessionsKick(uuid.UUID) error
} }
type apiSRTServer interface { type apiSRTServer interface {
apiConnsList() (*apiSRTConnList, error) apiConnsList() (*defs.APISRTConnList, error)
apiConnsGet(uuid.UUID) (*apiSRTConn, error) apiConnsGet(uuid.UUID) (*defs.APISRTConn, error)
apiConnsKick(uuid.UUID) error apiConnsKick(uuid.UUID) error
} }
@ -245,7 +247,7 @@ func newAPI(
group.POST("/v3/srtconns/kick/:id", a.onSRTConnsKick) group.POST("/v3/srtconns/kick/:id", a.onSRTConnsKick)
} }
network, address := restrictNetwork("tcp", address) network, address := restrictnetwork.Restrict("tcp", address)
var err error var err error
a.httpServer, err = httpserv.NewWrappedServer( 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()) a.Log(logger.Error, err.Error())
// send error in response // send error in response
ctx.JSON(status, &apiError{ ctx.JSON(status, &defs.APIError{
Error: err.Error(), Error: err.Error(),
}) })
} }
@ -364,7 +366,7 @@ func (a *api) onConfigPathsList(ctx *gin.Context) {
c := a.conf c := a.conf
a.mutex.Unlock() a.mutex.Unlock()
data := &apiPathConfList{ data := &defs.APIPathConfList{
Items: make([]*conf.Path, len(c.Paths)), Items: make([]*conf.Path, len(c.Paths)),
} }

View file

@ -3,6 +3,7 @@ package core
import ( import (
"net" "net"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger" "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 != "" { if c.runOnConnect != "" {
c.logger.Log(logger.Info, "runOnConnect command started") 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 { if c.onConnectCmd != nil {
c.onConnectCmd.Close() c.onConnectCmd.Close()
c.logger.Log(logger.Info, "runOnConnect command stopped") c.logger.Log(logger.Info, "runOnConnect command stopped")

View file

@ -71,7 +71,7 @@ var cli struct {
Confpath string `arg:"" default:""` Confpath string `arg:"" default:""`
} }
// Core is an instance of mediamtx. // Core is an instance of MediaMTX.
type Core struct { type Core struct {
ctx context.Context ctx context.Context
ctxCancel func() ctxCancel func()
@ -100,7 +100,7 @@ type Core struct {
done chan struct{} done chan struct{}
} }
// New allocates a core. // New allocates a Core.
func New(args []string) (*Core, bool) { func New(args []string) (*Core, bool) {
parser, err := kong.New(&cli, parser, err := kong.New(&cli,
kong.Description("MediaMTX "+version), kong.Description("MediaMTX "+version),

View file

@ -14,6 +14,7 @@ import (
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/httpserv" "github.com/bluenviron/mediamtx/internal/protocols/httpserv"
"github.com/bluenviron/mediamtx/internal/restrictnetwork"
) )
const ( const (
@ -70,7 +71,7 @@ func newHLSHTTPServer( //nolint:dupl
router.NoRoute(s.onRequest) router.NoRoute(s.onRequest)
network, address := restrictNetwork("tcp", address) network, address := restrictnetwork.Restrict("tcp", address)
var err error var err error
s.inner, err = httpserv.NewWrappedServer( s.inner, err = httpserv.NewWrappedServer(

View file

@ -7,11 +7,12 @@ import (
"sync" "sync"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
) )
type hlsManagerAPIMuxersListRes struct { type hlsManagerAPIMuxersListRes struct {
data *apiHLSMuxerList data *defs.APIHLSMuxerList
err error err error
} }
@ -20,7 +21,7 @@ type hlsManagerAPIMuxersListReq struct {
} }
type hlsManagerAPIMuxersGetRes struct { type hlsManagerAPIMuxersGetRes struct {
data *apiHLSMuxer data *defs.APIHLSMuxer
err error err error
} }
@ -189,8 +190,8 @@ outer:
delete(m.muxers, c.PathName()) delete(m.muxers, c.PathName())
case req := <-m.chAPIMuxerList: case req := <-m.chAPIMuxerList:
data := &apiHLSMuxerList{ data := &defs.APIHLSMuxerList{
Items: []*apiHLSMuxer{}, Items: []*defs.APIHLSMuxer{},
} }
for _, muxer := range m.muxers { for _, muxer := range m.muxers {
@ -275,7 +276,7 @@ func (m *hlsManager) pathNotReady(pa *path) {
} }
// apiMuxersList is called by api. // apiMuxersList is called by api.
func (m *hlsManager) apiMuxersList() (*apiHLSMuxerList, error) { func (m *hlsManager) apiMuxersList() (*defs.APIHLSMuxerList, error) {
req := hlsManagerAPIMuxersListReq{ req := hlsManagerAPIMuxersListReq{
res: make(chan hlsManagerAPIMuxersListRes), res: make(chan hlsManagerAPIMuxersListRes),
} }
@ -291,7 +292,7 @@ func (m *hlsManager) apiMuxersList() (*apiHLSMuxerList, error) {
} }
// apiMuxersGet is called by api. // apiMuxersGet is called by api.
func (m *hlsManager) apiMuxersGet(name string) (*apiHLSMuxer, error) { func (m *hlsManager) apiMuxersGet(name string) (*defs.APIHLSMuxer, error) {
req := hlsManagerAPIMuxersGetReq{ req := hlsManagerAPIMuxersGetReq{
name: name, name: name,
res: make(chan hlsManagerAPIMuxersGetRes), res: make(chan hlsManagerAPIMuxersGetRes),

View file

@ -19,6 +19,7 @@ import (
"github.com/bluenviron/mediamtx/internal/asyncwriter" "github.com/bluenviron/mediamtx/internal/asyncwriter"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/unit" "github.com/bluenviron/mediamtx/internal/unit"
@ -527,15 +528,15 @@ func (m *hlsMuxer) processRequest(req *hlsMuxerHandleRequestReq) {
} }
// apiReaderDescribe implements reader. // apiReaderDescribe implements reader.
func (m *hlsMuxer) apiReaderDescribe() apiPathSourceOrReader { func (m *hlsMuxer) apiReaderDescribe() defs.APIPathSourceOrReader {
return apiPathSourceOrReader{ return defs.APIPathSourceOrReader{
Type: "hlsMuxer", Type: "hlsMuxer",
ID: "", ID: "",
} }
} }
func (m *hlsMuxer) apiItem() *apiHLSMuxer { func (m *hlsMuxer) apiItem() *defs.APIHLSMuxer {
return &apiHLSMuxer{ return &defs.APIHLSMuxer{
Path: m.pathName, Path: m.pathName,
Created: m.created, Created: m.created,
LastRequest: time.Unix(0, atomic.LoadInt64(m.lastRequestTime)), LastRequest: time.Unix(0, atomic.LoadInt64(m.lastRequestTime)),

View file

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

View file

@ -12,6 +12,7 @@ import (
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/httpserv" "github.com/bluenviron/mediamtx/internal/protocols/httpserv"
"github.com/bluenviron/mediamtx/internal/restrictnetwork"
) )
func metric(key string, tags string, value int64) string { func metric(key string, tags string, value int64) string {
@ -49,7 +50,7 @@ func newMetrics(
router.GET("/metrics", m.onMetrics) router.GET("/metrics", m.onMetrics)
network, address := restrictNetwork("tcp", address) network, address := restrictnetwork.Restrict("tcp", address)
var err error var err error
m.httpServer, err = httpserv.NewWrappedServer( m.httpServer, err = httpserv.NewWrappedServer(

View file

@ -2,215 +2,26 @@ package core
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"time" "time"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediacommon/pkg/codecs/ac3" "github.com/bluenviron/mediacommon/pkg/codecs/ac3"
"github.com/bluenviron/mediacommon/pkg/codecs/h264" "github.com/bluenviron/mediacommon/pkg/codecs/h264"
"github.com/bluenviron/mediacommon/pkg/codecs/h265" "github.com/bluenviron/mediacommon/pkg/codecs/h265"
"github.com/bluenviron/mediacommon/pkg/formats/mpegts" mcmpegts "github.com/bluenviron/mediacommon/pkg/formats/mpegts"
"github.com/datarhei/gosrt" "github.com/datarhei/gosrt"
"github.com/bluenviron/mediamtx/internal/asyncwriter" "github.com/bluenviron/mediamtx/internal/asyncwriter"
"github.com/bluenviron/mediamtx/internal/protocols/mpegts"
"github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/unit" "github.com/bluenviron/mediamtx/internal/unit"
) )
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 { func durationGoToMPEGTS(v time.Duration) int64 {
return int64(v.Seconds() * 90000) 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( func mpegtsSetupWrite(
stream *stream.Stream, stream *stream.Stream,
writer *asyncwriter.Writer, writer *asyncwriter.Writer,
@ -218,11 +29,11 @@ func mpegtsSetupWrite(
sconn srt.Conn, sconn srt.Conn,
writeTimeout time.Duration, writeTimeout time.Duration,
) error { ) error {
var w *mpegts.Writer var w *mcmpegts.Writer
var tracks []*mpegts.Track var tracks []*mcmpegts.Track
addTrack := func(codec mpegts.Codec) *mpegts.Track { addTrack := func(codec mcmpegts.Codec) *mcmpegts.Track {
track := &mpegts.Track{ track := &mcmpegts.Track{
Codec: codec, Codec: codec,
} }
tracks = append(tracks, track) tracks = append(tracks, track)
@ -233,7 +44,7 @@ func mpegtsSetupWrite(
for _, forma := range medi.Formats { for _, forma := range medi.Formats {
switch forma := forma.(type) { switch forma := forma.(type) {
case *format.H265: //nolint:dupl case *format.H265: //nolint:dupl
track := addTrack(&mpegts.CodecH265{}) track := addTrack(&mcmpegts.CodecH265{})
var dtsExtractor *h265.DTSExtractor var dtsExtractor *h265.DTSExtractor
@ -266,7 +77,7 @@ func mpegtsSetupWrite(
}) })
case *format.H264: //nolint:dupl case *format.H264: //nolint:dupl
track := addTrack(&mpegts.CodecH264{}) track := addTrack(&mcmpegts.CodecH264{})
var dtsExtractor *h264.DTSExtractor var dtsExtractor *h264.DTSExtractor
@ -299,7 +110,7 @@ func mpegtsSetupWrite(
}) })
case *format.MPEG4Video: case *format.MPEG4Video:
track := addTrack(&mpegts.CodecMPEG4Video{}) track := addTrack(&mcmpegts.CodecMPEG4Video{})
firstReceived := false firstReceived := false
var lastPTS time.Duration var lastPTS time.Duration
@ -326,7 +137,7 @@ func mpegtsSetupWrite(
}) })
case *format.MPEG1Video: case *format.MPEG1Video:
track := addTrack(&mpegts.CodecMPEG1Video{}) track := addTrack(&mcmpegts.CodecMPEG1Video{})
firstReceived := false firstReceived := false
var lastPTS time.Duration var lastPTS time.Duration
@ -353,7 +164,7 @@ func mpegtsSetupWrite(
}) })
case *format.Opus: case *format.Opus:
track := addTrack(&mpegts.CodecOpus{ track := addTrack(&mcmpegts.CodecOpus{
ChannelCount: func() int { ChannelCount: func() int {
if forma.IsStereo { if forma.IsStereo {
return 2 return 2
@ -377,7 +188,7 @@ func mpegtsSetupWrite(
}) })
case *format.MPEG4Audio: case *format.MPEG4Audio:
track := addTrack(&mpegts.CodecMPEG4Audio{ track := addTrack(&mcmpegts.CodecMPEG4Audio{
Config: *forma.GetConfig(), Config: *forma.GetConfig(),
}) })
@ -396,7 +207,7 @@ func mpegtsSetupWrite(
}) })
case *format.MPEG1Audio: case *format.MPEG1Audio:
track := addTrack(&mpegts.CodecMPEG1Audio{}) track := addTrack(&mcmpegts.CodecMPEG1Audio{})
stream.AddReader(writer, medi, forma, func(u unit.Unit) error { stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
tunit := u.(*unit.MPEG1Audio) tunit := u.(*unit.MPEG1Audio)
@ -413,7 +224,7 @@ func mpegtsSetupWrite(
}) })
case *format.AC3: case *format.AC3:
track := addTrack(&mpegts.CodecAC3{}) track := addTrack(&mcmpegts.CodecAC3{})
sampleRate := time.Duration(forma.SampleRate) sampleRate := time.Duration(forma.SampleRate)
@ -440,10 +251,10 @@ func mpegtsSetupWrite(
} }
if len(tracks) == 0 { if len(tracks) == 0 {
return errMPEGTSNoTracks return mpegts.ErrNoTracks
} }
w = mpegts.NewWriter(bw, tracks) w = mcmpegts.NewWriter(bw, tracks)
return nil return nil
} }

View file

@ -15,6 +15,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/record" "github.com/bluenviron/mediamtx/internal/record"
@ -69,21 +70,6 @@ type pathAccessRequest struct {
rtspNonce string 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 { type pathRemoveReaderReq struct {
author reader author reader
res chan struct{} res chan struct{}
@ -157,7 +143,7 @@ type pathStopPublisherReq struct {
} }
type pathAPIPathsListRes struct { type pathAPIPathsListRes struct {
data *apiPathList data *defs.APIPathList
paths map[string]*path paths map[string]*path
} }
@ -167,7 +153,7 @@ type pathAPIPathsListReq struct {
type pathAPIPathsGetRes struct { type pathAPIPathsGetRes struct {
path *path path *path
data *apiPath data *defs.APIPath
err error err error
} }
@ -212,8 +198,8 @@ type path struct {
// in // in
chReloadConf chan *conf.Path chReloadConf chan *conf.Path
chSourceStaticSetReady chan pathSourceStaticSetReadyReq chStaticSourceSetReady chan defs.PathSourceStaticSetReadyReq
chSourceStaticSetNotReady chan pathSourceStaticSetNotReadyReq chStaticSourceSetNotReady chan defs.PathSourceStaticSetNotReadyReq
chDescribe chan pathDescribeReq chDescribe chan pathDescribeReq
chRemovePublisher chan pathRemovePublisherReq chRemovePublisher chan pathRemovePublisherReq
chAddPublisher chan pathAddPublisherReq chAddPublisher chan pathAddPublisherReq
@ -265,8 +251,8 @@ func newPath(
onDemandPublisherReadyTimer: newEmptyTimer(), onDemandPublisherReadyTimer: newEmptyTimer(),
onDemandPublisherCloseTimer: newEmptyTimer(), onDemandPublisherCloseTimer: newEmptyTimer(),
chReloadConf: make(chan *conf.Path), chReloadConf: make(chan *conf.Path),
chSourceStaticSetReady: make(chan pathSourceStaticSetReadyReq), chStaticSourceSetReady: make(chan defs.PathSourceStaticSetReadyReq),
chSourceStaticSetNotReady: make(chan pathSourceStaticSetNotReadyReq), chStaticSourceSetNotReady: make(chan defs.PathSourceStaticSetNotReadyReq),
chDescribe: make(chan pathDescribeReq), chDescribe: make(chan pathDescribeReq),
chRemovePublisher: make(chan pathRemovePublisherReq), chRemovePublisher: make(chan pathRemovePublisherReq),
chAddPublisher: make(chan pathAddPublisherReq), chAddPublisher: make(chan pathAddPublisherReq),
@ -306,7 +292,7 @@ func (pa *path) run() {
if pa.conf.Source == "redirect" { if pa.conf.Source == "redirect" {
pa.source = &sourceRedirect{} pa.source = &sourceRedirect{}
} else if pa.conf.HasStaticSource() { } else if pa.conf.HasStaticSource() {
pa.source = newSourceStatic( pa.source = newStaticSourceHandler(
pa.conf, pa.conf,
pa.readTimeout, pa.readTimeout,
pa.writeTimeout, pa.writeTimeout,
@ -314,7 +300,7 @@ func (pa *path) run() {
pa) pa)
if !pa.conf.SourceOnDemand { 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 pa.source != nil {
if source, ok := pa.source.(*sourceStatic); ok { if source, ok := pa.source.(*staticSourceHandler); ok {
if !pa.conf.SourceOnDemand || pa.onDemandStaticSourceState != pathOnDemandStateInitial { if !pa.conf.SourceOnDemand || pa.onDemandStaticSourceState != pathOnDemandStateInitial {
source.close("path is closing") source.close("path is closing")
} }
@ -411,10 +397,10 @@ func (pa *path) runInner() error {
case newConf := <-pa.chReloadConf: case newConf := <-pa.chReloadConf:
pa.doReloadConf(newConf) pa.doReloadConf(newConf)
case req := <-pa.chSourceStaticSetReady: case req := <-pa.chStaticSourceSetReady:
pa.doSourceStaticSetReady(req) pa.doSourceStaticSetReady(req)
case req := <-pa.chSourceStaticSetNotReady: case req := <-pa.chStaticSourceSetNotReady:
pa.doSourceStaticSetNotReady(req) pa.doSourceStaticSetNotReady(req)
if pa.shouldClose() { if pa.shouldClose() {
@ -510,7 +496,7 @@ func (pa *path) doReloadConf(newConf *conf.Path) {
pa.confMutex.Unlock() pa.confMutex.Unlock()
if pa.conf.HasStaticSource() { if pa.conf.HasStaticSource() {
go pa.source.(*sourceStatic).reloadConf(newConf) go pa.source.(*staticSourceHandler).reloadConf(newConf)
} }
if pa.conf.Record { if pa.conf.Record {
@ -523,10 +509,10 @@ func (pa *path) doReloadConf(newConf *conf.Path) {
} }
} }
func (pa *path) doSourceStaticSetReady(req pathSourceStaticSetReadyReq) { func (pa *path) doSourceStaticSetReady(req defs.PathSourceStaticSetReadyReq) {
err := pa.setReady(req.desc, req.generateRTPPackets) err := pa.setReady(req.Desc, req.GenerateRTPPackets)
if err != nil { if err != nil {
req.res <- pathSourceStaticSetReadyRes{err: err} req.Res <- defs.PathSourceStaticSetReadyRes{Err: err}
return return
} }
@ -549,15 +535,15 @@ func (pa *path) doSourceStaticSetReady(req pathSourceStaticSetReadyReq) {
pa.readerAddRequestsOnHold = nil 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() pa.setNotReady()
// send response before calling onDemandStaticSourceStop() // send response before calling onDemandStaticSourceStop()
// in order to avoid a deadlock due to sourceStatic.stop() // in order to avoid a deadlock due to staticSourceHandler.stop()
close(req.res) close(req.Res)
if pa.conf.HasOnDemandStaticSource() && pa.onDemandStaticSourceState != pathOnDemandStateInitial { if pa.conf.HasOnDemandStaticSource() && pa.onDemandStaticSourceState != pathOnDemandStateInitial {
pa.onDemandStaticSourceStop("an error occurred") pa.onDemandStaticSourceStop("an error occurred")
@ -738,14 +724,14 @@ func (pa *path) doRemoveReader(req pathRemoveReaderReq) {
func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) { func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) {
req.res <- pathAPIPathsGetRes{ req.res <- pathAPIPathsGetRes{
data: &apiPath{ data: &defs.APIPath{
Name: pa.name, Name: pa.name,
ConfName: pa.confName, ConfName: pa.confName,
Source: func() *apiPathSourceOrReader { Source: func() *defs.APIPathSourceOrReader {
if pa.source == nil { if pa.source == nil {
return nil return nil
} }
v := pa.source.apiSourceDescribe() v := pa.source.APISourceDescribe()
return &v return &v
}(), }(),
Ready: pa.stream != nil, Ready: pa.stream != nil,
@ -768,8 +754,8 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) {
} }
return pa.stream.BytesReceived() return pa.stream.BytesReceived()
}(), }(),
Readers: func() []apiPathSourceOrReader { Readers: func() []defs.APIPathSourceOrReader {
ret := []apiPathSourceOrReader{} ret := []defs.APIPathSourceOrReader{}
for r := range pa.readers { for r := range pa.readers {
ret = append(ret, r.apiReaderDescribe()) ret = append(ret, r.apiReaderDescribe())
} }
@ -811,7 +797,7 @@ func (pa *path) externalCmdEnv() externalcmd.Environment {
} }
func (pa *path) onDemandStaticSourceStart() { func (pa *path) onDemandStaticSourceStart() {
pa.source.(*sourceStatic).start(true) pa.source.(*staticSourceHandler).start(true)
pa.onDemandStaticSourceReadyTimer.Stop() pa.onDemandStaticSourceReadyTimer.Stop()
pa.onDemandStaticSourceReadyTimer = time.NewTimer(time.Duration(pa.conf.SourceOnDemandStartTimeout)) pa.onDemandStaticSourceReadyTimer = time.NewTimer(time.Duration(pa.conf.SourceOnDemandStartTimeout))
@ -834,7 +820,7 @@ func (pa *path) onDemandStaticSourceStop(reason string) {
pa.onDemandStaticSourceState = pathOnDemandStateInitial pa.onDemandStaticSourceState = pathOnDemandStateInitial
pa.source.(*sourceStatic).stop(reason) pa.source.(*staticSourceHandler).stop(reason)
} }
func (pa *path) onDemandPublisherStart(query string) { func (pa *path) onDemandPublisherStart(query string) {
@ -1016,35 +1002,39 @@ func (pa *path) reloadConf(newConf *conf.Path) {
} }
} }
// sourceStaticSetReady is called by sourceStatic. // staticSourceHandlerSetReady is called by staticSourceHandler.
func (pa *path) sourceStaticSetReady(sourceStaticCtx context.Context, req pathSourceStaticSetReadyReq) { func (pa *path) staticSourceHandlerSetReady(
staticSourceHandlerCtx context.Context, req defs.PathSourceStaticSetReadyReq,
) {
select { select {
case pa.chSourceStaticSetReady <- req: case pa.chStaticSourceSetReady <- req:
case <-pa.ctx.Done(): case <-pa.ctx.Done():
req.res <- pathSourceStaticSetReadyRes{err: fmt.Errorf("terminated")} req.Res <- defs.PathSourceStaticSetReadyRes{Err: fmt.Errorf("terminated")}
// this avoids: // this avoids:
// - invalid requests sent after the source has been terminated // - invalid requests sent after the source has been terminated
// - deadlocks caused by <-done inside stop() // - deadlocks caused by <-done inside stop()
case <-sourceStaticCtx.Done(): case <-staticSourceHandlerCtx.Done():
req.res <- pathSourceStaticSetReadyRes{err: fmt.Errorf("terminated")} req.Res <- defs.PathSourceStaticSetReadyRes{Err: fmt.Errorf("terminated")}
} }
} }
// sourceStaticSetNotReady is called by sourceStatic. // staticSourceHandlerSetNotReady is called by staticSourceHandler.
func (pa *path) sourceStaticSetNotReady(sourceStaticCtx context.Context, req pathSourceStaticSetNotReadyReq) { func (pa *path) staticSourceHandlerSetNotReady(
staticSourceHandlerCtx context.Context, req defs.PathSourceStaticSetNotReadyReq,
) {
select { select {
case pa.chSourceStaticSetNotReady <- req: case pa.chStaticSourceSetNotReady <- req:
case <-pa.ctx.Done(): case <-pa.ctx.Done():
close(req.res) close(req.Res)
// this avoids: // this avoids:
// - invalid requests sent after the source has been terminated // - invalid requests sent after the source has been terminated
// - deadlocks caused by <-done inside stop() // - deadlocks caused by <-done inside stop()
case <-sourceStaticCtx.Done(): case <-staticSourceHandlerCtx.Done():
close(req.res) close(req.Res)
} }
} }
@ -1120,7 +1110,7 @@ func (pa *path) removeReader(req pathRemoveReaderReq) {
} }
// apiPathsGet is called by api. // 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) req.res = make(chan pathAPIPathsGetRes)
select { select {
case pa.chAPIPathsGet <- req: case pa.chAPIPathsGet <- req:

View file

@ -7,6 +7,7 @@ import (
"sync" "sync"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
) )
@ -540,7 +541,7 @@ func (pm *pathManager) setHLSManager(s pathManagerHLSManager) {
} }
// apiPathsList is called by api. // apiPathsList is called by api.
func (pm *pathManager) apiPathsList() (*apiPathList, error) { func (pm *pathManager) apiPathsList() (*defs.APIPathList, error) {
req := pathAPIPathsListReq{ req := pathAPIPathsListReq{
res: make(chan pathAPIPathsListRes), res: make(chan pathAPIPathsListRes),
} }
@ -549,8 +550,8 @@ func (pm *pathManager) apiPathsList() (*apiPathList, error) {
case pm.chAPIPathsList <- req: case pm.chAPIPathsList <- req:
res := <-req.res res := <-req.res
res.data = &apiPathList{ res.data = &defs.APIPathList{
Items: []*apiPath{}, Items: []*defs.APIPath{},
} }
for _, pa := range res.paths { for _, pa := range res.paths {
@ -572,7 +573,7 @@ func (pm *pathManager) apiPathsList() (*apiPathList, error) {
} }
// apiPathsGet is called by api. // apiPathsGet is called by api.
func (pm *pathManager) apiPathsGet(name string) (*apiPath, error) { func (pm *pathManager) apiPathsGet(name string) (*defs.APIPath, error) {
req := pathAPIPathsGetReq{ req := pathAPIPathsGetReq{
name: name, name: name,
res: make(chan pathAPIPathsGetRes), res: make(chan pathAPIPathsGetRes),

View file

@ -10,6 +10,7 @@ import (
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/httpserv" "github.com/bluenviron/mediamtx/internal/protocols/httpserv"
"github.com/bluenviron/mediamtx/internal/restrictnetwork"
) )
type pprofParent interface { type pprofParent interface {
@ -31,7 +32,7 @@ func newPPROF(
parent: parent, parent: parent,
} }
network, address := restrictNetwork("tcp", address) network, address := restrictnetwork.Restrict("tcp", address)
var err error var err error
pp.httpServer, err = httpserv.NewWrappedServer( pp.httpServer, err = httpserv.NewWrappedServer(

View file

@ -3,6 +3,7 @@ package core
import ( import (
"github.com/bluenviron/mediamtx/internal/asyncwriter" "github.com/bluenviron/mediamtx/internal/asyncwriter"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/stream"
@ -11,7 +12,7 @@ import (
// reader is an entity that can read a stream. // reader is an entity that can read a stream.
type reader interface { type reader interface {
close() close()
apiReaderDescribe() apiPathSourceOrReader apiReaderDescribe() defs.APIPathSourceOrReader
} }
func readerMediaInfo(r *asyncwriter.Writer, stream *stream.Stream) string { func readerMediaInfo(r *asyncwriter.Writer, stream *stream.Stream) string {
@ -22,7 +23,7 @@ func readerOnReadHook(
externalCmdPool *externalcmd.Pool, externalCmdPool *externalcmd.Pool,
pathConf *conf.Path, pathConf *conf.Path,
path *path, path *path,
reader apiPathSourceOrReader, reader defs.APIPathSourceOrReader,
query string, query string,
l logger.Writer, l logger.Writer,
) func() { ) func() {

View file

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

View file

@ -19,6 +19,7 @@ import (
"github.com/bluenviron/mediamtx/internal/asyncwriter" "github.com/bluenviron/mediamtx/internal/asyncwriter"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp" "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. // apiReaderDescribe implements reader.
func (c *rtmpConn) apiReaderDescribe() apiPathSourceOrReader { func (c *rtmpConn) apiReaderDescribe() defs.APIPathSourceOrReader {
return apiPathSourceOrReader{ return defs.APIPathSourceOrReader{
Type: func() string { Type: func() string {
if c.isTLS { if c.isTLS {
return "rtmpsConn" return "rtmpsConn"
@ -597,12 +598,12 @@ func (c *rtmpConn) apiReaderDescribe() apiPathSourceOrReader {
} }
} }
// apiSourceDescribe implements source. // APISourceDescribe implements source.
func (c *rtmpConn) apiSourceDescribe() apiPathSourceOrReader { func (c *rtmpConn) APISourceDescribe() defs.APIPathSourceOrReader {
return c.apiReaderDescribe() return c.apiReaderDescribe()
} }
func (c *rtmpConn) apiItem() *apiRTMPConn { func (c *rtmpConn) apiItem() *defs.APIRTMPConn {
c.mutex.RLock() c.mutex.RLock()
defer c.mutex.RUnlock() defer c.mutex.RUnlock()
@ -614,20 +615,20 @@ func (c *rtmpConn) apiItem() *apiRTMPConn {
bytesSent = c.rconn.BytesSent() bytesSent = c.rconn.BytesSent()
} }
return &apiRTMPConn{ return &defs.APIRTMPConn{
ID: c.uuid, ID: c.uuid,
Created: c.created, Created: c.created,
RemoteAddr: c.remoteAddr().String(), RemoteAddr: c.remoteAddr().String(),
State: func() apiRTMPConnState { State: func() defs.APIRTMPConnState {
switch c.state { switch c.state {
case rtmpConnStateRead: case rtmpConnStateRead:
return apiRTMPConnStateRead return defs.APIRTMPConnStateRead
case rtmpConnStatePublish: case rtmpConnStatePublish:
return apiRTMPConnStatePublish return defs.APIRTMPConnStatePublish
default: default:
return apiRTMPConnStateIdle return defs.APIRTMPConnStateIdle
} }
}(), }(),
Path: c.pathName, Path: c.pathName,

View file

@ -11,12 +11,14 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/restrictnetwork"
) )
type rtmpServerAPIConnsListRes struct { type rtmpServerAPIConnsListRes struct {
data *apiRTMPConnList data *defs.APIRTMPConnList
err error err error
} }
@ -25,7 +27,7 @@ type rtmpServerAPIConnsListReq struct {
} }
type rtmpServerAPIConnsGetRes struct { type rtmpServerAPIConnsGetRes struct {
data *apiRTMPConn data *defs.APIRTMPConn
err error err error
} }
@ -95,7 +97,7 @@ func newRTMPServer(
) (*rtmpServer, error) { ) (*rtmpServer, error) {
ln, err := func() (net.Listener, error) { ln, err := func() (net.Listener, error) {
if !isTLS { if !isTLS {
return net.Listen(restrictNetwork("tcp", address)) return net.Listen(restrictnetwork.Restrict("tcp", address))
} }
cert, err := tls.LoadX509KeyPair(serverCert, serverKey) cert, err := tls.LoadX509KeyPair(serverCert, serverKey)
@ -103,7 +105,7 @@ func newRTMPServer(
return nil, err 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}}) return tls.Listen(network, address, &tls.Config{Certificates: []tls.Certificate{cert}})
}() }()
if err != nil { if err != nil {
@ -203,8 +205,8 @@ outer:
delete(s.conns, c) delete(s.conns, c)
case req := <-s.chAPIConnsList: case req := <-s.chAPIConnsList:
data := &apiRTMPConnList{ data := &defs.APIRTMPConnList{
Items: []*apiRTMPConn{}, Items: []*defs.APIRTMPConn{},
} }
for c := range s.conns { for c := range s.conns {
@ -286,7 +288,7 @@ func (s *rtmpServer) closeConn(c *rtmpConn) {
} }
// apiConnsList is called by api. // apiConnsList is called by api.
func (s *rtmpServer) apiConnsList() (*apiRTMPConnList, error) { func (s *rtmpServer) apiConnsList() (*defs.APIRTMPConnList, error) {
req := rtmpServerAPIConnsListReq{ req := rtmpServerAPIConnsListReq{
res: make(chan rtmpServerAPIConnsListRes), res: make(chan rtmpServerAPIConnsListRes),
} }
@ -302,7 +304,7 @@ func (s *rtmpServer) apiConnsList() (*apiRTMPConnList, error) {
} }
// apiConnsGet is called by api. // 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{ req := rtmpServerAPIConnsGetReq{
uuid: uuid, uuid: uuid,
res: make(chan rtmpServerAPIConnsGetRes), res: make(chan rtmpServerAPIConnsGetRes),

View file

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

View file

@ -12,6 +12,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
) )
@ -79,7 +80,7 @@ func newRTSPConn(
c.Log(logger.Info, "opened") c.Log(logger.Info, "opened")
c.conn.open(apiPathSourceOrReader{ c.conn.open(defs.APIPathSourceOrReader{
Type: func() string { Type: func() string {
if isTLS { if isTLS {
return "rtspsConn" return "rtspsConn"
@ -113,7 +114,7 @@ func (c *rtspConn) ip() net.IP {
func (c *rtspConn) onClose(err error) { func (c *rtspConn) onClose(err error) {
c.Log(logger.Info, "closed: %v", err) c.Log(logger.Info, "closed: %v", err)
c.conn.close(apiPathSourceOrReader{ c.conn.close(defs.APIPathSourceOrReader{
Type: func() string { Type: func() string {
if c.isTLS { if c.isTLS {
return "rtspsConn" return "rtspsConn"
@ -231,8 +232,8 @@ func (c *rtspConn) handleAuthError(authErr error) (*base.Response, error) {
}, authErr }, authErr
} }
func (c *rtspConn) apiItem() *apiRTSPConn { func (c *rtspConn) apiItem() *defs.APIRTSPConn {
return &apiRTSPConn{ return &defs.APIRTSPConn{
ID: c.uuid, ID: c.uuid,
Created: c.created, Created: c.created,
RemoteAddr: c.remoteAddr().String(), RemoteAddr: c.remoteAddr().String(),

View file

@ -16,6 +16,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger" "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. // apiConnsList is called by api and metrics.
func (s *rtspServer) apiConnsList() (*apiRTSPConnsList, error) { func (s *rtspServer) apiConnsList() (*defs.APIRTSPConnsList, error) {
select { select {
case <-s.ctx.Done(): case <-s.ctx.Done():
return nil, fmt.Errorf("terminated") return nil, fmt.Errorf("terminated")
@ -370,8 +371,8 @@ func (s *rtspServer) apiConnsList() (*apiRTSPConnsList, error) {
s.mutex.RLock() s.mutex.RLock()
defer s.mutex.RUnlock() defer s.mutex.RUnlock()
data := &apiRTSPConnsList{ data := &defs.APIRTSPConnsList{
Items: []*apiRTSPConn{}, Items: []*defs.APIRTSPConn{},
} }
for _, c := range s.conns { for _, c := range s.conns {
@ -386,7 +387,7 @@ func (s *rtspServer) apiConnsList() (*apiRTSPConnsList, error) {
} }
// apiConnsGet is called by api. // 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 { select {
case <-s.ctx.Done(): case <-s.ctx.Done():
return nil, fmt.Errorf("terminated") 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. // apiSessionsList is called by api and metrics.
func (s *rtspServer) apiSessionsList() (*apiRTSPSessionList, error) { func (s *rtspServer) apiSessionsList() (*defs.APIRTSPSessionList, error) {
select { select {
case <-s.ctx.Done(): case <-s.ctx.Done():
return nil, fmt.Errorf("terminated") return nil, fmt.Errorf("terminated")
@ -415,8 +416,8 @@ func (s *rtspServer) apiSessionsList() (*apiRTSPSessionList, error) {
s.mutex.RLock() s.mutex.RLock()
defer s.mutex.RUnlock() defer s.mutex.RUnlock()
data := &apiRTSPSessionList{ data := &defs.APIRTSPSessionList{
Items: []*apiRTSPSession{}, Items: []*defs.APIRTSPSession{},
} }
for _, s := range s.sessions { for _, s := range s.sessions {
@ -431,7 +432,7 @@ func (s *rtspServer) apiSessionsList() (*apiRTSPSessionList, error) {
} }
// apiSessionsGet is called by api. // 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 { select {
case <-s.ctx.Done(): case <-s.ctx.Done():
return nil, fmt.Errorf("terminated") return nil, fmt.Errorf("terminated")

View file

@ -15,6 +15,7 @@ import (
"github.com/pion/rtp" "github.com/pion/rtp"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/stream"
@ -377,8 +378,8 @@ func (s *rtspSession) onPause(_ *gortsplib.ServerHandlerOnPauseCtx) (*base.Respo
} }
// apiReaderDescribe implements reader. // apiReaderDescribe implements reader.
func (s *rtspSession) apiReaderDescribe() apiPathSourceOrReader { func (s *rtspSession) apiReaderDescribe() defs.APIPathSourceOrReader {
return apiPathSourceOrReader{ return defs.APIPathSourceOrReader{
Type: func() string { Type: func() string {
if s.isTLS { if s.isTLS {
return "rtspsSession" return "rtspsSession"
@ -389,8 +390,8 @@ func (s *rtspSession) apiReaderDescribe() apiPathSourceOrReader {
} }
} }
// apiSourceDescribe implements source. // APISourceDescribe implements source.
func (s *rtspSession) apiSourceDescribe() apiPathSourceOrReader { func (s *rtspSession) APISourceDescribe() defs.APIPathSourceOrReader {
return s.apiReaderDescribe() return s.apiReaderDescribe()
} }
@ -409,25 +410,25 @@ func (s *rtspSession) onStreamWriteError(ctx *gortsplib.ServerHandlerOnStreamWri
s.writeErrLogger.Log(logger.Warn, ctx.Error.Error()) s.writeErrLogger.Log(logger.Warn, ctx.Error.Error())
} }
func (s *rtspSession) apiItem() *apiRTSPSession { func (s *rtspSession) apiItem() *defs.APIRTSPSession {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
return &apiRTSPSession{ return &defs.APIRTSPSession{
ID: s.uuid, ID: s.uuid,
Created: s.created, Created: s.created,
RemoteAddr: s.remoteAddr().String(), RemoteAddr: s.remoteAddr().String(),
State: func() apiRTSPSessionState { State: func() defs.APIRTSPSessionState {
switch s.state { switch s.state {
case gortsplib.ServerSessionStatePrePlay, case gortsplib.ServerSessionStatePrePlay,
gortsplib.ServerSessionStatePlay: gortsplib.ServerSessionStatePlay:
return apiRTSPSessionStateRead return defs.APIRTSPSessionStateRead
case gortsplib.ServerSessionStatePreRecord, case gortsplib.ServerSessionStatePreRecord,
gortsplib.ServerSessionStateRecord: gortsplib.ServerSessionStateRecord:
return apiRTSPSessionStatePublish return defs.APIRTSPSessionStatePublish
} }
return apiRTSPSessionStateIdle return defs.APIRTSPSessionStateIdle
}(), }(),
Path: s.pathName, Path: s.pathName,
Transport: func() *string { Transport: func() *string {

View file

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

View file

@ -6,18 +6,19 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
) )
// source is an entity that can provide a stream. // source is an entity that can provide a stream.
// it can be: // it can be:
// - a publisher // - publisher
// - sourceStatic // - staticSourceHandler
// - sourceRedirect // - redirectSource
type source interface { type source interface {
logger.Writer logger.Writer
apiSourceDescribe() apiPathSourceOrReader APISourceDescribe() defs.APIPathSourceOrReader
} }
func mediaDescription(media *description.Media) string { func mediaDescription(media *description.Media) string {
@ -54,7 +55,7 @@ func sourceOnReadyHook(path *path) func() {
if path.conf.RunOnReady != "" { if path.conf.RunOnReady != "" {
env = path.externalCmdEnv() env = path.externalCmdEnv()
desc := path.source.apiSourceDescribe() desc := path.source.APISourceDescribe()
env["MTX_QUERY"] = path.publisherQuery env["MTX_QUERY"] = path.publisherQuery
env["MTX_SOURCE_TYPE"] = desc.Type env["MTX_SOURCE_TYPE"] = desc.Type
env["MTX_SOURCE_ID"] = desc.ID env["MTX_SOURCE_ID"] = desc.ID

View file

@ -1,6 +1,7 @@
package core package core
import ( import (
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
) )
@ -10,9 +11,9 @@ type sourceRedirect struct{}
func (*sourceRedirect) Log(logger.Level, string, ...interface{}) { func (*sourceRedirect) Log(logger.Level, string, ...interface{}) {
} }
// apiSourceDescribe implements source. // APISourceDescribe implements source.
func (*sourceRedirect) apiSourceDescribe() apiPathSourceOrReader { func (*sourceRedirect) APISourceDescribe() defs.APIPathSourceOrReader {
return apiPathSourceOrReader{ return defs.APIPathSourceOrReader{
Type: "redirect", Type: "redirect",
ID: "", ID: "",
} }

View file

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

View file

@ -11,14 +11,16 @@ import (
"time" "time"
"github.com/bluenviron/gortsplib/v4/pkg/description" "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/datarhei/gosrt"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/bluenviron/mediamtx/internal/asyncwriter" "github.com/bluenviron/mediamtx/internal/asyncwriter"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/mpegts"
"github.com/bluenviron/mediamtx/internal/stream" "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 { func (c *srtConn) runPublishReader(sconn srt.Conn, path *path) error {
sconn.SetReadDeadline(time.Now().Add(time.Duration(c.readTimeout))) 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 { if err != nil {
return err return err
} }
@ -279,7 +281,7 @@ func (c *srtConn) runPublishReader(sconn srt.Conn, path *path) error {
var stream *stream.Stream var stream *stream.Stream
medias, err := mpegtsSetupRead(r, &stream) medias, err := mpegts.ToStream(r, &stream)
if err != nil { if err != nil {
return err return err
} }
@ -418,19 +420,19 @@ func (c *srtConn) setConn(sconn srt.Conn) {
} }
// apiReaderDescribe implements reader. // apiReaderDescribe implements reader.
func (c *srtConn) apiReaderDescribe() apiPathSourceOrReader { func (c *srtConn) apiReaderDescribe() defs.APIPathSourceOrReader {
return apiPathSourceOrReader{ return defs.APIPathSourceOrReader{
Type: "srtConn", Type: "srtConn",
ID: c.uuid.String(), ID: c.uuid.String(),
} }
} }
// apiSourceDescribe implements source. // APISourceDescribe implements source.
func (c *srtConn) apiSourceDescribe() apiPathSourceOrReader { func (c *srtConn) APISourceDescribe() defs.APIPathSourceOrReader {
return c.apiReaderDescribe() return c.apiReaderDescribe()
} }
func (c *srtConn) apiItem() *apiSRTConn { func (c *srtConn) apiItem() *defs.APISRTConn {
c.mutex.RLock() c.mutex.RLock()
defer c.mutex.RUnlock() defer c.mutex.RUnlock()
@ -444,20 +446,20 @@ func (c *srtConn) apiItem() *apiSRTConn {
bytesSent = s.Accumulated.ByteSent bytesSent = s.Accumulated.ByteSent
} }
return &apiSRTConn{ return &defs.APISRTConn{
ID: c.uuid, ID: c.uuid,
Created: c.created, Created: c.created,
RemoteAddr: c.connReq.RemoteAddr().String(), RemoteAddr: c.connReq.RemoteAddr().String(),
State: func() apiSRTConnState { State: func() defs.APISRTConnState {
switch c.state { switch c.state {
case srtConnStateRead: case srtConnStateRead:
return apiSRTConnStateRead return defs.APISRTConnStateRead
case srtConnStatePublish: case srtConnStatePublish:
return apiSRTConnStatePublish return defs.APISRTConnStatePublish
default: default:
return apiSRTConnStateIdle return defs.APISRTConnStateIdle
} }
}(), }(),
Path: c.pathName, Path: c.pathName,

View file

@ -11,6 +11,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
) )
@ -25,7 +26,7 @@ type srtNewConnReq struct {
} }
type srtServerAPIConnsListRes struct { type srtServerAPIConnsListRes struct {
data *apiSRTConnList data *defs.APISRTConnList
err error err error
} }
@ -34,7 +35,7 @@ type srtServerAPIConnsListReq struct {
} }
type srtServerAPIConnsGetRes struct { type srtServerAPIConnsGetRes struct {
data *apiSRTConn data *defs.APISRTConn
err error err error
} }
@ -191,8 +192,8 @@ outer:
delete(s.conns, c) delete(s.conns, c)
case req := <-s.chAPIConnsList: case req := <-s.chAPIConnsList:
data := &apiSRTConnList{ data := &defs.APISRTConnList{
Items: []*apiSRTConn{}, Items: []*defs.APISRTConn{},
} }
for c := range s.conns { for c := range s.conns {
@ -279,7 +280,7 @@ func (s *srtServer) closeConn(c *srtConn) {
} }
// apiConnsList is called by api. // apiConnsList is called by api.
func (s *srtServer) apiConnsList() (*apiSRTConnList, error) { func (s *srtServer) apiConnsList() (*defs.APISRTConnList, error) {
req := srtServerAPIConnsListReq{ req := srtServerAPIConnsListReq{
res: make(chan srtServerAPIConnsListRes), res: make(chan srtServerAPIConnsListRes),
} }
@ -295,7 +296,7 @@ func (s *srtServer) apiConnsList() (*apiSRTConnList, error) {
} }
// apiConnsGet is called by api. // 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{ req := srtServerAPIConnsGetReq{
uuid: uuid, uuid: uuid,
res: make(chan srtServerAPIConnsGetRes), res: make(chan srtServerAPIConnsGetRes),

View file

@ -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: "",
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -16,9 +16,11 @@ import (
pwebrtc "github.com/pion/webrtc/v3" pwebrtc "github.com/pion/webrtc/v3"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/httpserv" "github.com/bluenviron/mediamtx/internal/protocols/httpserv"
"github.com/bluenviron/mediamtx/internal/protocols/webrtc" "github.com/bluenviron/mediamtx/internal/protocols/webrtc"
"github.com/bluenviron/mediamtx/internal/restrictnetwork"
) )
//go:embed webrtc_publish_index.html //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) { func webrtcWriteError(ctx *gin.Context, statusCode int, err error) {
ctx.JSON(statusCode, &apiError{ ctx.JSON(statusCode, &defs.APIError{
Error: err.Error(), Error: err.Error(),
}) })
} }
@ -92,7 +94,7 @@ func newWebRTCHTTPServer( //nolint:dupl
router.SetTrustedProxies(trustedProxies.ToTrustedProxies()) //nolint:errcheck router.SetTrustedProxies(trustedProxies.ToTrustedProxies()) //nolint:errcheck
router.NoRoute(s.onRequest) router.NoRoute(s.onRequest)
network, address := restrictNetwork("tcp", address) network, address := restrictnetwork.Restrict("tcp", address)
var err error var err error
s.inner, err = httpserv.NewWrappedServer( s.inner, err = httpserv.NewWrappedServer(

View file

@ -19,9 +19,11 @@ import (
pwebrtc "github.com/pion/webrtc/v3" pwebrtc "github.com/pion/webrtc/v3"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/webrtc" "github.com/bluenviron/mediamtx/internal/protocols/webrtc"
"github.com/bluenviron/mediamtx/internal/restrictnetwork"
) )
const ( const (
@ -84,7 +86,7 @@ func randomTurnUser() (string, error) {
} }
type webRTCManagerAPISessionsListRes struct { type webRTCManagerAPISessionsListRes struct {
data *apiWebRTCSessionList data *defs.APIWebRTCSessionList
err error err error
} }
@ -93,7 +95,7 @@ type webRTCManagerAPISessionsListReq struct {
} }
type webRTCManagerAPISessionsGetRes struct { type webRTCManagerAPISessionsGetRes struct {
data *apiWebRTCSession data *defs.APIWebRTCSession
err error err error
} }
@ -249,7 +251,7 @@ func newWebRTCManager(
var iceUDPMux ice.UDPMux var iceUDPMux ice.UDPMux
if iceUDPMuxAddress != "" { if iceUDPMuxAddress != "" {
m.udpMuxLn, err = net.ListenPacket(restrictNetwork("udp", iceUDPMuxAddress)) m.udpMuxLn, err = net.ListenPacket(restrictnetwork.Restrict("udp", iceUDPMuxAddress))
if err != nil { if err != nil {
m.httpServer.close() m.httpServer.close()
ctxCancel() ctxCancel()
@ -261,7 +263,7 @@ func newWebRTCManager(
var iceTCPMux ice.TCPMux var iceTCPMux ice.TCPMux
if iceTCPMuxAddress != "" { if iceTCPMuxAddress != "" {
m.tcpMuxLn, err = net.Listen(restrictNetwork("tcp", iceTCPMuxAddress)) m.tcpMuxLn, err = net.Listen(restrictnetwork.Restrict("tcp", iceTCPMuxAddress))
if err != nil { if err != nil {
m.udpMuxLn.Close() m.udpMuxLn.Close()
m.httpServer.close() m.httpServer.close()
@ -364,8 +366,8 @@ outer:
req.res <- webRTCDeleteSessionRes{} req.res <- webRTCDeleteSessionRes{}
case req := <-m.chAPISessionsList: case req := <-m.chAPISessionsList:
data := &apiWebRTCSessionList{ data := &defs.APIWebRTCSessionList{
Items: []*apiWebRTCSession{}, Items: []*defs.APIWebRTCSession{},
} }
for sx := range m.sessions { for sx := range m.sessions {
@ -518,7 +520,7 @@ func (m *webRTCManager) deleteSession(req webRTCDeleteSessionReq) error {
} }
// apiSessionsList is called by api. // apiSessionsList is called by api.
func (m *webRTCManager) apiSessionsList() (*apiWebRTCSessionList, error) { func (m *webRTCManager) apiSessionsList() (*defs.APIWebRTCSessionList, error) {
req := webRTCManagerAPISessionsListReq{ req := webRTCManagerAPISessionsListReq{
res: make(chan webRTCManagerAPISessionsListRes), res: make(chan webRTCManagerAPISessionsListRes),
} }
@ -534,7 +536,7 @@ func (m *webRTCManager) apiSessionsList() (*apiWebRTCSessionList, error) {
} }
// apiSessionsGet is called by api. // 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{ req := webRTCManagerAPISessionsGetReq{
uuid: uuid, uuid: uuid,
res: make(chan webRTCManagerAPISessionsGetRes), res: make(chan webRTCManagerAPISessionsGetRes),

View file

@ -18,11 +18,11 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9" "github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9"
"github.com/bluenviron/gortsplib/v4/pkg/rtptime" "github.com/bluenviron/gortsplib/v4/pkg/rtptime"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pion/rtp"
"github.com/pion/sdp/v3" "github.com/pion/sdp/v3"
pwebrtc "github.com/pion/webrtc/v3" pwebrtc "github.com/pion/webrtc/v3"
"github.com/bluenviron/mediamtx/internal/asyncwriter" "github.com/bluenviron/mediamtx/internal/asyncwriter"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/webrtc" "github.com/bluenviron/mediamtx/internal/protocols/webrtc"
@ -30,18 +30,6 @@ import (
"github.com/bluenviron/mediamtx/internal/unit" "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 type setupStreamFunc func(*webrtc.OutgoingTrack) error
func webrtcFindVideoTrack( func webrtcFindVideoTrack(
@ -268,31 +256,6 @@ func webrtcFindAudioTrack(
return nil, nil 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 { func whipOffer(body []byte) *pwebrtc.SessionDescription {
return &pwebrtc.SessionDescription{ return &pwebrtc.SessionDescription{
Type: pwebrtc.SDPTypeOffer, Type: pwebrtc.SDPTypeOffer,
@ -497,7 +460,7 @@ func (s *webRTCSession) runPublish() (int, error) {
return 0, err return 0, err
} }
medias := webrtcMediasOfIncomingTracks(tracks) medias := webrtc.TracksToMedias(tracks)
rres := res.path.startPublisher(pathStartPublisherReq{ rres := res.path.startPublisher(pathStartPublisherReq{
author: s, author: s,
@ -513,7 +476,7 @@ func (s *webRTCSession) runPublish() (int, error) {
for i, media := range medias { for i, media := range medias {
ci := i ci := i
cmedia := media cmedia := media
trackWrapper := &webrtcTrackWrapper{clockRate: cmedia.Formats[0].ClockRate()} trackWrapper := &webrtc.TrackWrapper{ClockRat: cmedia.Formats[0].ClockRate()}
go func() { go func() {
for { for {
@ -724,20 +687,20 @@ func (s *webRTCSession) addCandidates(
} }
} }
// apiSourceDescribe implements sourceStaticImpl. // apiReaderDescribe implements reader.
func (s *webRTCSession) apiSourceDescribe() apiPathSourceOrReader { func (s *webRTCSession) apiReaderDescribe() defs.APIPathSourceOrReader {
return apiPathSourceOrReader{ return defs.APIPathSourceOrReader{
Type: "webRTCSession", Type: "webRTCSession",
ID: s.uuid.String(), ID: s.uuid.String(),
} }
} }
// apiReaderDescribe implements reader. // APISourceDescribe implements source.
func (s *webRTCSession) apiReaderDescribe() apiPathSourceOrReader { func (s *webRTCSession) APISourceDescribe() defs.APIPathSourceOrReader {
return s.apiSourceDescribe() return s.apiReaderDescribe()
} }
func (s *webRTCSession) apiItem() *apiWebRTCSession { func (s *webRTCSession) apiItem() *defs.APIWebRTCSession {
s.mutex.RLock() s.mutex.RLock()
defer s.mutex.RUnlock() defer s.mutex.RUnlock()
@ -755,18 +718,18 @@ func (s *webRTCSession) apiItem() *apiWebRTCSession {
bytesSent = s.pc.BytesSent() bytesSent = s.pc.BytesSent()
} }
return &apiWebRTCSession{ return &defs.APIWebRTCSession{
ID: s.uuid, ID: s.uuid,
Created: s.created, Created: s.created,
RemoteAddr: s.req.remoteAddr, RemoteAddr: s.req.remoteAddr,
PeerConnectionEstablished: peerConnectionEstablished, PeerConnectionEstablished: peerConnectionEstablished,
LocalCandidate: localCandidate, LocalCandidate: localCandidate,
RemoteCandidate: remoteCandidate, RemoteCandidate: remoteCandidate,
State: func() apiWebRTCSessionState { State: func() defs.APIWebRTCSessionState {
if s.req.publish { if s.req.publish {
return apiWebRTCSessionStatePublish return defs.APIWebRTCSessionStatePublish
} }
return apiWebRTCSessionStateRead return defs.APIWebRTCSessionStateRead
}(), }(),
Path: s.req.pathName, Path: s.req.pathName,
BytesReceived: bytesReceived, BytesReceived: bytesReceived,

View file

@ -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: "",
}
}

View file

@ -1,4 +1,4 @@
package core package defs
import ( import (
"time" "time"
@ -8,52 +8,60 @@ import (
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
) )
type apiError struct { // APIError is a generic error.
type APIError struct {
Error string `json:"error"` Error string `json:"error"`
} }
type apiPathConfList struct { // APIPathConfList is a list of path configurations.
type APIPathConfList struct {
ItemCount int `json:"itemCount"` ItemCount int `json:"itemCount"`
PageCount int `json:"pageCount"` PageCount int `json:"pageCount"`
Items []*conf.Path `json:"items"` Items []*conf.Path `json:"items"`
} }
type apiPathSourceOrReader struct { // APIPathSourceOrReader is a source or a reader.
type APIPathSourceOrReader struct {
Type string `json:"type"` Type string `json:"type"`
ID string `json:"id"` ID string `json:"id"`
} }
type apiPath struct { // APIPath is a path.
type APIPath struct {
Name string `json:"name"` Name string `json:"name"`
ConfName string `json:"confName"` ConfName string `json:"confName"`
Source *apiPathSourceOrReader `json:"source"` Source *APIPathSourceOrReader `json:"source"`
Ready bool `json:"ready"` Ready bool `json:"ready"`
ReadyTime *time.Time `json:"readyTime"` ReadyTime *time.Time `json:"readyTime"`
Tracks []string `json:"tracks"` Tracks []string `json:"tracks"`
BytesReceived uint64 `json:"bytesReceived"` 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"` ItemCount int `json:"itemCount"`
PageCount int `json:"pageCount"` 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"` Path string `json:"path"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
LastRequest time.Time `json:"lastRequest"` LastRequest time.Time `json:"lastRequest"`
BytesSent uint64 `json:"bytesSent"` BytesSent uint64 `json:"bytesSent"`
} }
type apiHLSMuxerList struct { // APIHLSMuxerList is a list of HLS muxers.
type APIHLSMuxerList struct {
ItemCount int `json:"itemCount"` ItemCount int `json:"itemCount"`
PageCount int `json:"pageCount"` 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"` ID uuid.UUID `json:"id"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
RemoteAddr string `json:"remoteAddr"` RemoteAddr string `json:"remoteAddr"`
@ -61,107 +69,124 @@ type apiRTSPConn struct {
BytesSent uint64 `json:"bytesSent"` BytesSent uint64 `json:"bytesSent"`
} }
type apiRTSPConnsList struct { // APIRTSPConnsList is a list of RTSP connections.
type APIRTSPConnsList struct {
ItemCount int `json:"itemCount"` ItemCount int `json:"itemCount"`
PageCount int `json:"pageCount"` 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 ( const (
apiRTMPConnStateIdle apiRTMPConnState = "idle" APIRTMPConnStateIdle APIRTMPConnState = "idle"
apiRTMPConnStateRead apiRTMPConnState = "read" APIRTMPConnStateRead APIRTMPConnState = "read"
apiRTMPConnStatePublish apiRTMPConnState = "publish" APIRTMPConnStatePublish APIRTMPConnState = "publish"
) )
type apiRTMPConn struct { // APIRTMPConn is a RTMP connection.
type APIRTMPConn struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
RemoteAddr string `json:"remoteAddr"` RemoteAddr string `json:"remoteAddr"`
State apiRTMPConnState `json:"state"` State APIRTMPConnState `json:"state"`
Path string `json:"path"` Path string `json:"path"`
BytesReceived uint64 `json:"bytesReceived"` BytesReceived uint64 `json:"bytesReceived"`
BytesSent uint64 `json:"bytesSent"` BytesSent uint64 `json:"bytesSent"`
} }
type apiRTMPConnList struct { // APIRTMPConnList is a list of RTMP connections.
type APIRTMPConnList struct {
ItemCount int `json:"itemCount"` ItemCount int `json:"itemCount"`
PageCount int `json:"pageCount"` 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 ( const (
apiRTSPSessionStateIdle apiRTSPSessionState = "idle" APIRTSPSessionStateIdle APIRTSPSessionState = "idle"
apiRTSPSessionStateRead apiRTSPSessionState = "read" APIRTSPSessionStateRead APIRTSPSessionState = "read"
apiRTSPSessionStatePublish apiRTSPSessionState = "publish" APIRTSPSessionStatePublish APIRTSPSessionState = "publish"
) )
type apiRTSPSession struct { // APIRTSPSession is a RTSP session.
type APIRTSPSession struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
RemoteAddr string `json:"remoteAddr"` RemoteAddr string `json:"remoteAddr"`
State apiRTSPSessionState `json:"state"` State APIRTSPSessionState `json:"state"`
Path string `json:"path"` Path string `json:"path"`
Transport *string `json:"transport"` Transport *string `json:"transport"`
BytesReceived uint64 `json:"bytesReceived"` BytesReceived uint64 `json:"bytesReceived"`
BytesSent uint64 `json:"bytesSent"` BytesSent uint64 `json:"bytesSent"`
} }
type apiRTSPSessionList struct { // APIRTSPSessionList is a list of RTSP sessions.
type APIRTSPSessionList struct {
ItemCount int `json:"itemCount"` ItemCount int `json:"itemCount"`
PageCount int `json:"pageCount"` 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 ( const (
apiSRTConnStateIdle apiSRTConnState = "idle" APISRTConnStateIdle APISRTConnState = "idle"
apiSRTConnStateRead apiSRTConnState = "read" APISRTConnStateRead APISRTConnState = "read"
apiSRTConnStatePublish apiSRTConnState = "publish" APISRTConnStatePublish APISRTConnState = "publish"
) )
type apiSRTConn struct { // APISRTConn is a SRT connection.
type APISRTConn struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
RemoteAddr string `json:"remoteAddr"` RemoteAddr string `json:"remoteAddr"`
State apiSRTConnState `json:"state"` State APISRTConnState `json:"state"`
Path string `json:"path"` Path string `json:"path"`
BytesReceived uint64 `json:"bytesReceived"` BytesReceived uint64 `json:"bytesReceived"`
BytesSent uint64 `json:"bytesSent"` BytesSent uint64 `json:"bytesSent"`
} }
type apiSRTConnList struct { // APISRTConnList is a list of SRT connections.
type APISRTConnList struct {
ItemCount int `json:"itemCount"` ItemCount int `json:"itemCount"`
PageCount int `json:"pageCount"` 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 ( const (
apiWebRTCSessionStateRead apiWebRTCSessionState = "read" APIWebRTCSessionStateRead APIWebRTCSessionState = "read"
apiWebRTCSessionStatePublish apiWebRTCSessionState = "publish" APIWebRTCSessionStatePublish APIWebRTCSessionState = "publish"
) )
type apiWebRTCSession struct { // APIWebRTCSession is a WebRTC session.
type APIWebRTCSession struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
RemoteAddr string `json:"remoteAddr"` RemoteAddr string `json:"remoteAddr"`
PeerConnectionEstablished bool `json:"peerConnectionEstablished"` PeerConnectionEstablished bool `json:"peerConnectionEstablished"`
LocalCandidate string `json:"localCandidate"` LocalCandidate string `json:"localCandidate"`
RemoteCandidate string `json:"remoteCandidate"` RemoteCandidate string `json:"remoteCandidate"`
State apiWebRTCSessionState `json:"state"` State APIWebRTCSessionState `json:"state"`
Path string `json:"path"` Path string `json:"path"`
BytesReceived uint64 `json:"bytesReceived"` BytesReceived uint64 `json:"bytesReceived"`
BytesSent uint64 `json:"bytesSent"` BytesSent uint64 `json:"bytesSent"`
} }
type apiWebRTCSessionList struct { // APIWebRTCSessionList is a list of WebRTC sessions.
type APIWebRTCSessionList struct {
ItemCount int `json:"itemCount"` ItemCount int `json:"itemCount"`
PageCount int `json:"pageCount"` PageCount int `json:"pageCount"`
Items []*apiWebRTCSession `json:"items"` Items []*APIWebRTCSession `json:"items"`
} }

2
internal/defs/defs.go Normal file
View file

@ -0,0 +1,2 @@
// Package defs contains shared definitions.
package defs

25
internal/defs/path.go Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
package core // Package hls contains the HLS static source.
package hls
import ( import (
"context"
"net/http" "net/http"
"time" "time"
@ -10,41 +10,30 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format" "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/logger"
"github.com/bluenviron/mediamtx/internal/protocols/tls"
"github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/unit" "github.com/bluenviron/mediamtx/internal/unit"
) )
type hlsSourceParent interface { // Source is a HLS static source.
logger.Writer type Source struct {
setReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes Parent defs.StaticSourceParent
setNotReady(req pathSourceStaticSetNotReadyReq)
} }
type hlsSource struct { // Log implements StaticSource.
parent hlsSourceParent func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
s.Parent.Log(level, "[HLS source] "+format, args...)
} }
func newHLSSource( // Run implements StaticSource.
parent hlsSourceParent, func (s *Source) Run(params defs.StaticSourceRunParams) error {
) *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 {
var stream *stream.Stream var stream *stream.Stream
defer func() { defer func() {
if stream != nil { 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 var c *gohlslib.Client
c = &gohlslib.Client{ c = &gohlslib.Client{
URI: cnf.Source, URI: params.Conf.Source,
HTTPClient: &http.Client{ HTTPClient: &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
TLSClientConfig: tlsConfigForFingerprint(cnf.SourceFingerprint), TLSClientConfig: tls.ConfigForFingerprint(params.Conf.SourceFingerprint),
}, },
}, },
OnDownloadPrimaryPlaylist: func(u string) { 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) medias = append(medias, medi)
} }
res := s.parent.setReady(pathSourceStaticSetReadyReq{ res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
desc: &description.Session{Medias: medias}, Desc: &description.Session{Medias: medias},
generateRTPPackets: true, GenerateRTPPackets: true,
}) })
if res.err != nil { if res.Err != nil {
return res.err return res.Err
} }
stream = res.stream stream = res.Stream
return nil return nil
}, },
@ -225,9 +214,9 @@ func (s *hlsSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *co
c.Close() c.Close()
return err return err
case <-reloadConf: case <-params.ReloadConf:
case <-ctx.Done(): case <-params.Context.Done():
c.Close() c.Close()
<-c.Wait() <-c.Wait()
return nil return nil
@ -235,9 +224,9 @@ func (s *hlsSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *co
} }
} }
// apiSourceDescribe implements sourceStaticImpl. // APISourceDescribe implements StaticSource.
func (*hlsSource) apiSourceDescribe() apiPathSourceOrReader { func (*Source) APISourceDescribe() defs.APIPathSourceOrReader {
return apiPathSourceOrReader{ return defs.APIPathSourceOrReader{
Type: "hlsSource", Type: "hlsSource",
ID: "", ID: "",
} }

View file

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

View file

@ -1,13 +1,14 @@
package core // Package rpicamera contains the Raspberry Pi Camera static source.
package rpicamera
import ( import (
"context"
"time" "time"
"github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/rpicamera" "github.com/bluenviron/mediamtx/internal/protocols/rpicamera"
"github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/stream"
@ -51,30 +52,18 @@ func paramsFromConf(cnf *conf.Path) rpicamera.Params {
} }
} }
type rpiCameraSourceParent interface { // Source is a Raspberry Pi Camera static source.
logger.Writer type Source struct {
setReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes Parent defs.StaticSourceParent
setNotReady(req pathSourceStaticSetNotReadyReq)
} }
type rpiCameraSource struct { // Log implements StaticSource.
parent rpiCameraSourceParent func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
s.Parent.Log(level, "[RPI Camera source] "+format, args...)
} }
func newRPICameraSource( // Run implements StaticSource.
parent rpiCameraSourceParent, func (s *Source) Run(params defs.StaticSourceRunParams) error {
) *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 {
medi := &description.Media{ medi := &description.Media{
Type: description.MediaTypeVideo, Type: description.MediaTypeVideo,
Formats: []format.Format{&format.H264{ 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) { onData := func(dts time.Duration, au [][]byte) {
if stream == nil { if stream == nil {
res := s.parent.setReady(pathSourceStaticSetReadyReq{ res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
desc: &description.Session{Medias: medias}, Desc: &description.Session{Medias: medias},
generateRTPPackets: true, GenerateRTPPackets: true,
}) })
if res.err != nil { if res.Err != nil {
return return
} }
stream = res.stream stream = res.Stream
} }
stream.WriteUnit(medi, medi.Formats[0], &unit.H264{ 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 { if err != nil {
return err return err
} }
@ -115,24 +104,24 @@ func (s *rpiCameraSource) run(ctx context.Context, cnf *conf.Path, reloadConf ch
defer func() { defer func() {
if stream != nil { if stream != nil {
s.parent.setNotReady(pathSourceStaticSetNotReadyReq{}) s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})
} }
}() }()
for { for {
select { select {
case cnf := <-reloadConf: case cnf := <-params.ReloadConf:
cam.ReloadParams(paramsFromConf(cnf)) cam.ReloadParams(paramsFromConf(cnf))
case <-ctx.Done(): case <-params.Context.Done():
return nil return nil
} }
} }
} }
// apiSourceDescribe implements sourceStaticImpl. // APISourceDescribe implements StaticSource.
func (*rpiCameraSource) apiSourceDescribe() apiPathSourceOrReader { func (*Source) APISourceDescribe() defs.APIPathSourceOrReader {
return apiPathSourceOrReader{ return defs.APIPathSourceOrReader{
Type: "rpiCameraSource", Type: "rpiCameraSource",
ID: "", ID: "",
} }

View file

@ -1,8 +1,9 @@
package core // Package rtmp contains the RTMP static source.
package rtmp
import ( import (
"context" "context"
"crypto/tls" ctls "crypto/tls"
"fmt" "fmt"
"net" "net"
"net/url" "net/url"
@ -12,45 +13,31 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp" "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/stream"
"github.com/bluenviron/mediamtx/internal/unit" "github.com/bluenviron/mediamtx/internal/unit"
) )
type rtmpSourceParent interface { // Source is a RTMP static source.
logger.Writer type Source struct {
setReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes ReadTimeout conf.StringDuration
setNotReady(req pathSourceStaticSetNotReadyReq) WriteTimeout conf.StringDuration
Parent defs.StaticSourceParent
} }
type rtmpSource struct { // Log implements StaticSource.
readTimeout conf.StringDuration func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
writeTimeout conf.StringDuration s.Parent.Log(level, "[RTMP source] "+format, args...)
parent rtmpSourceParent
} }
func newRTMPSource( // Run implements StaticSource.
readTimeout conf.StringDuration, func (s *Source) Run(params defs.StaticSourceRunParams) error {
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 {
s.Log(logger.Debug, "connecting") s.Log(logger.Debug, "connecting")
u, err := url.Parse(cnf.Source) u, err := url.Parse(params.Conf.Source)
if err != nil { if err != nil {
return err 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) { 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() defer cancel2()
if u.Scheme == "rtmp" { if u.Scheme == "rtmp" {
return (&net.Dialer{}).DialContext(ctx2, "tcp", u.Host) return (&net.Dialer{}).DialContext(ctx2, "tcp", u.Host)
} }
return (&tls.Dialer{ return (&ctls.Dialer{
Config: tlsConfigForFingerprint(cnf.SourceFingerprint), Config: tls.ConfigForFingerprint(params.Conf.SourceFingerprint),
}).DialContext(ctx2, "tcp", u.Host) }).DialContext(ctx2, "tcp", u.Host)
}() }()
if err != nil { if err != nil {
@ -88,9 +75,9 @@ func (s *rtmpSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *c
nconn.Close() nconn.Close()
return err return err
case <-reloadConf: case <-params.ReloadConf:
case <-ctx.Done(): case <-params.Context.Done():
nconn.Close() nconn.Close()
<-readDone <-readDone
return nil 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 { func (s *Source) runReader(u *url.URL, nconn net.Conn) error {
nconn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout))) nconn.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))
nconn.SetWriteDeadline(time.Now().Add(time.Duration(s.writeTimeout))) nconn.SetWriteDeadline(time.Now().Add(time.Duration(s.WriteTimeout)))
conn, err := rtmp.NewClientConn(nconn, u, false) conn, err := rtmp.NewClientConn(nconn, u, false)
if err != nil { if err != nil {
return err return err
@ -175,23 +162,23 @@ func (s *rtmpSource) runReader(u *url.URL, nconn net.Conn) error {
} }
} }
res := s.parent.setReady(pathSourceStaticSetReadyReq{ res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
desc: &description.Session{Medias: medias}, Desc: &description.Session{Medias: medias},
generateRTPPackets: true, GenerateRTPPackets: true,
}) })
if res.err != nil { if res.Err != nil {
return res.err 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 // disable write deadline to allow outgoing acknowledges
nconn.SetWriteDeadline(time.Time{}) nconn.SetWriteDeadline(time.Time{})
for { for {
nconn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout))) nconn.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))
err := mc.Read() err := mc.Read()
if err != nil { if err != nil {
return err return err
@ -199,9 +186,9 @@ func (s *rtmpSource) runReader(u *url.URL, nconn net.Conn) error {
} }
} }
// apiSourceDescribe implements sourceStaticImpl. // APISourceDescribe implements StaticSource.
func (*rtmpSource) apiSourceDescribe() apiPathSourceOrReader { func (*Source) APISourceDescribe() defs.APIPathSourceOrReader {
return apiPathSourceOrReader{ return defs.APIPathSourceOrReader{
Type: "rtmpSource", Type: "rtmpSource",
ID: "", ID: "",
} }

View file

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

View file

@ -1,7 +1,7 @@
package core // Package rtsp contains the RTSP static source.
package rtsp
import ( import (
"context"
"time" "time"
"github.com/bluenviron/gortsplib/v4" "github.com/bluenviron/gortsplib/v4"
@ -11,7 +11,9 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/url" "github.com/bluenviron/gortsplib/v4/pkg/url"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/tls"
) )
func createRangeHeader(cnf *conf.Path) (*headers.Range, error) { func createRangeHeader(cnf *conf.Path) (*headers.Range, error) {
@ -59,50 +61,32 @@ func createRangeHeader(cnf *conf.Path) (*headers.Range, error) {
} }
} }
type rtspSourceParent interface { // Source is a RTSP static source.
logger.Writer type Source struct {
setReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes ReadTimeout conf.StringDuration
setNotReady(req pathSourceStaticSetNotReadyReq) WriteTimeout conf.StringDuration
WriteQueueSize int
Parent defs.StaticSourceParent
} }
type rtspSource struct { // Log implements StaticSource.
readTimeout conf.StringDuration func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
writeTimeout conf.StringDuration s.Parent.Log(level, "[RTSP source] "+format, args...)
writeQueueSize int
parent rtspSourceParent
} }
func newRTSPSource( // Run implements StaticSource.
readTimeout conf.StringDuration, func (s *Source) Run(params defs.StaticSourceRunParams) error {
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 {
s.Log(logger.Debug, "connecting") s.Log(logger.Debug, "connecting")
decodeErrLogger := logger.NewLimitedLogger(s) decodeErrLogger := logger.NewLimitedLogger(s)
c := &gortsplib.Client{ c := &gortsplib.Client{
Transport: cnf.SourceProtocol.Transport, Transport: params.Conf.SourceProtocol.Transport,
TLSConfig: tlsConfigForFingerprint(cnf.SourceFingerprint), TLSConfig: tls.ConfigForFingerprint(params.Conf.SourceFingerprint),
ReadTimeout: time.Duration(s.readTimeout), ReadTimeout: time.Duration(s.ReadTimeout),
WriteTimeout: time.Duration(s.writeTimeout), WriteTimeout: time.Duration(s.WriteTimeout),
WriteQueueSize: s.writeQueueSize, WriteQueueSize: s.WriteQueueSize,
AnyPortEnable: cnf.SourceAnyPortEnable, AnyPortEnable: params.Conf.SourceAnyPortEnable,
OnRequest: func(req *base.Request) { OnRequest: func(req *base.Request) {
s.Log(logger.Debug, "[c->s] %v", req) 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 { if err != nil {
return err return err
} }
@ -144,15 +128,15 @@ func (s *rtspSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *c
return err return err
} }
res := s.parent.setReady(pathSourceStaticSetReadyReq{ res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
desc: desc, Desc: desc,
generateRTPPackets: false, GenerateRTPPackets: false,
}) })
if res.err != nil { if res.Err != nil {
return res.err return res.Err
} }
defer s.parent.setNotReady(pathSourceStaticSetNotReadyReq{}) defer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})
for _, medi := range desc.Medias { for _, medi := range desc.Medias {
for _, forma := range medi.Formats { for _, forma := range medi.Formats {
@ -165,12 +149,12 @@ func (s *rtspSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *c
return 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 { if err != nil {
return err return err
} }
@ -189,9 +173,9 @@ func (s *rtspSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *c
case err := <-readErr: case err := <-readErr:
return err return err
case <-reloadConf: case <-params.ReloadConf:
case <-ctx.Done(): case <-params.Context.Done():
c.Close() c.Close()
<-readErr <-readErr
return nil return nil
@ -199,9 +183,9 @@ func (s *rtspSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *c
} }
} }
// apiSourceDescribe implements sourceStaticImpl. // APISourceDescribe implements StaticSource.
func (*rtspSource) apiSourceDescribe() apiPathSourceOrReader { func (*Source) APISourceDescribe() defs.APIPathSourceOrReader {
return apiPathSourceOrReader{ return defs.APIPathSourceOrReader{
Type: "rtspSource", Type: "rtspSource",
ID: "", ID: "",
} }

View file

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

View file

@ -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: "",
}
}

View file

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

View file

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

View file

@ -1,17 +1,20 @@
package core // Package udp contains the UDP static source.
package udp
import ( import (
"context"
"fmt" "fmt"
"net" "net"
"time" "time"
"github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/multicast" "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/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/mpegts"
"github.com/bluenviron/mediamtx/internal/restrictnetwork"
"github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/stream"
) )
@ -40,36 +43,22 @@ type packetConn interface {
SetReadBuffer(int) error SetReadBuffer(int) error
} }
type udpSourceParent interface { // Source is a UDP static source.
logger.Writer type Source struct {
setReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes ReadTimeout conf.StringDuration
setNotReady(req pathSourceStaticSetNotReadyReq) Parent defs.StaticSourceParent
} }
type udpSource struct { // Log implements StaticSource.
readTimeout conf.StringDuration func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
parent udpSourceParent s.Parent.Log(level, "[UDP source] "+format, args...)
} }
func newUDPSource( // Run implements StaticSource.
readTimeout conf.StringDuration, func (s *Source) Run(params defs.StaticSourceRunParams) error {
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 {
s.Log(logger.Debug, "connecting") s.Log(logger.Debug, "connecting")
hostPort := cnf.Source[len("udp://"):] hostPort := params.Conf.Source[len("udp://"):]
addr, err := net.ResolveUDPAddr("udp", hostPort) addr, err := net.ResolveUDPAddr("udp", hostPort)
if err != nil { if err != nil {
@ -84,7 +73,7 @@ func (s *udpSource) run(ctx context.Context, cnf *conf.Path, _ chan *conf.Path)
return err return err
} }
} else { } else {
tmp, err := net.ListenPacket(restrictNetwork("udp", addr.String())) tmp, err := net.ListenPacket(restrictnetwork.Restrict("udp", addr.String()))
if err != nil { if err != nil {
return err return err
} }
@ -107,16 +96,16 @@ func (s *udpSource) run(ctx context.Context, cnf *conf.Path, _ chan *conf.Path)
case err := <-readerErr: case err := <-readerErr:
return err return err
case <-ctx.Done(): case <-params.Context.Done():
pc.Close() pc.Close()
<-readerErr <-readerErr
return fmt.Errorf("terminated") return fmt.Errorf("terminated")
} }
} }
func (s *udpSource) runReader(pc net.PacketConn) error { func (s *Source) runReader(pc net.PacketConn) error {
pc.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout))) pc.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))
r, err := mpegts.NewReader(mpegts.NewBufferedReader(newPacketConnReader(pc))) r, err := mcmpegts.NewReader(mcmpegts.NewBufferedReader(newPacketConnReader(pc)))
if err != nil { if err != nil {
return err return err
} }
@ -129,25 +118,25 @@ func (s *udpSource) runReader(pc net.PacketConn) error {
var stream *stream.Stream var stream *stream.Stream
medias, err := mpegtsSetupRead(r, &stream) medias, err := mpegts.ToStream(r, &stream)
if err != nil { if err != nil {
return err return err
} }
res := s.parent.setReady(pathSourceStaticSetReadyReq{ res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
desc: &description.Session{Medias: medias}, Desc: &description.Session{Medias: medias},
generateRTPPackets: true, GenerateRTPPackets: true,
}) })
if res.err != nil { if res.Err != nil {
return res.err return res.Err
} }
defer s.parent.setNotReady(pathSourceStaticSetNotReadyReq{}) defer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})
stream = res.stream stream = res.Stream
for { for {
pc.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout))) pc.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))
err := r.Read() err := r.Read()
if err != nil { if err != nil {
return err return err
@ -155,9 +144,9 @@ func (s *udpSource) runReader(pc net.PacketConn) error {
} }
} }
// apiSourceDescribe implements sourceStaticImpl. // APISourceDescribe implements StaticSource.
func (*udpSource) apiSourceDescribe() apiPathSourceOrReader { func (*Source) APISourceDescribe() defs.APIPathSourceOrReader {
return apiPathSourceOrReader{ return defs.APIPathSourceOrReader{
Type: "udpSource", Type: "udpSource",
ID: "", ID: "",
} }

View file

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

View file

@ -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: "",
}
}

View file

@ -1,4 +1,4 @@
package core package webrtc
import ( import (
"context" "context"
@ -6,19 +6,27 @@ import (
"net" "net"
"net/http" "net/http"
"testing" "testing"
"time"
"github.com/bluenviron/gortsplib/v4"
"github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/gortsplib/v4/pkg/url"
"github.com/pion/rtp" "github.com/pion/rtp"
pwebrtc "github.com/pion/webrtc/v3"
"github.com/stretchr/testify/require" "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/protocols/webrtc"
"github.com/bluenviron/mediamtx/internal/staticsources/tester"
) )
func TestWebRTCSource(t *testing.T) { func whipOffer(body []byte) *pwebrtc.SessionDescription {
state := 0 return &pwebrtc.SessionDescription{
Type: pwebrtc.SDPTypeOffer,
SDP: string(body),
}
}
func TestSource(t *testing.T) {
api, err := webrtc.NewAPI(webrtc.APIConf{}) api, err := webrtc.NewAPI(webrtc.APIConf{})
require.NoError(t, err) require.NoError(t, err)
@ -31,9 +39,7 @@ func TestWebRTCSource(t *testing.T) {
defer pc.Close() defer pc.Close()
tracks, err := pc.SetupOutgoingTracks( tracks, err := pc.SetupOutgoingTracks(
&format.VP8{ nil,
PayloadTyp: 96,
},
&format.Opus{ &format.Opus{
PayloadTyp: 111, PayloadTyp: 111,
IsStereo: true, IsStereo: true,
@ -41,6 +47,8 @@ func TestWebRTCSource(t *testing.T) {
) )
require.NoError(t, err) require.NoError(t, err)
state := 0
httpServ := &http.Server{ httpServ := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch state { switch state {
@ -79,23 +87,10 @@ func TestWebRTCSource(t *testing.T) {
Header: rtp.Header{ Header: rtp.Header{
Version: 2, Version: 2,
Marker: true, Marker: true,
PayloadType: 96, PayloadType: 111,
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,
SequenceNumber: 1123, SequenceNumber: 1123,
Timestamp: 45343, Timestamp: 45343,
SSRC: 563423, SSRC: 563424,
}, },
Payload: []byte{5, 2}, 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) require.NoError(t, err)
go httpServ.Serve(ln) go httpServ.Serve(ln)
defer httpServ.Shutdown(context.Background()) defer httpServ.Shutdown(context.Background())
p, ok := newInstance("paths:\n" + te := tester.New(
" proxied:\n" + func(p defs.StaticSourceParent) defs.StaticSource {
" source: whep://localhost:5555/my/resource\n" + return &Source{
" sourceOnDemand: yes\n") ReadTimeout: conf.StringDuration(10 * time.Second),
require.Equal(t, true, ok) Parent: p,
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,
}, },
Payload: []byte{5, 3}, &conf.Path{
}) Source: "whep://localhost:9003/my/resource",
require.NoError(t, err) },
)
defer te.Close()
<-received <-te.Unit
} }