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

View File

@ -3,6 +3,7 @@ package core
import (
"net"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger"
)
@ -36,7 +37,7 @@ func newConn(
}
}
func (c *conn) open(desc apiPathSourceOrReader) {
func (c *conn) open(desc defs.APIPathSourceOrReader) {
if c.runOnConnect != "" {
c.logger.Log(logger.Info, "runOnConnect command started")
@ -58,7 +59,7 @@ func (c *conn) open(desc apiPathSourceOrReader) {
}
}
func (c *conn) close(desc apiPathSourceOrReader) {
func (c *conn) close(desc defs.APIPathSourceOrReader) {
if c.onConnectCmd != nil {
c.onConnectCmd.Close()
c.logger.Log(logger.Info, "runOnConnect command stopped")

View File

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

View File

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

View File

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

View File

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

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

View File

@ -2,215 +2,26 @@ package core
import (
"bufio"
"errors"
"fmt"
"time"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediacommon/pkg/codecs/ac3"
"github.com/bluenviron/mediacommon/pkg/codecs/h264"
"github.com/bluenviron/mediacommon/pkg/codecs/h265"
"github.com/bluenviron/mediacommon/pkg/formats/mpegts"
mcmpegts "github.com/bluenviron/mediacommon/pkg/formats/mpegts"
"github.com/datarhei/gosrt"
"github.com/bluenviron/mediamtx/internal/asyncwriter"
"github.com/bluenviron/mediamtx/internal/protocols/mpegts"
"github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/unit"
)
var errMPEGTSNoTracks = errors.New("no supported tracks found (supported are H265, H264," +
" MPEG-4 Video, MPEG-1/2 Video, Opus, MPEG-4 Audio, MPEG-1 Audio, AC-3")
func durationGoToMPEGTS(v time.Duration) int64 {
return int64(v.Seconds() * 90000)
}
func mpegtsSetupRead(r *mpegts.Reader, stream **stream.Stream) ([]*description.Media, error) {
var medias []*description.Media //nolint:prealloc
var td *mpegts.TimeDecoder
decodeTime := func(t int64) time.Duration {
if td == nil {
td = mpegts.NewTimeDecoder(t)
}
return td.Decode(t)
}
for _, track := range r.Tracks() { //nolint:dupl
var medi *description.Media
switch codec := track.Codec.(type) {
case *mpegts.CodecH265:
medi = &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.H265{
PayloadTyp: 96,
}},
}
r.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) error {
(*stream).WriteUnit(medi, medi.Formats[0], &unit.H265{
Base: unit.Base{
NTP: time.Now(),
PTS: decodeTime(pts),
},
AU: au,
})
return nil
})
case *mpegts.CodecH264:
medi = &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.H264{
PayloadTyp: 96,
PacketizationMode: 1,
}},
}
r.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) error {
(*stream).WriteUnit(medi, medi.Formats[0], &unit.H264{
Base: unit.Base{
NTP: time.Now(),
PTS: decodeTime(pts),
},
AU: au,
})
return nil
})
case *mpegts.CodecMPEG4Video:
medi = &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.MPEG4Video{
PayloadTyp: 96,
}},
}
r.OnDataMPEGxVideo(track, func(pts int64, frame []byte) error {
(*stream).WriteUnit(medi, medi.Formats[0], &unit.MPEG4Video{
Base: unit.Base{
NTP: time.Now(),
PTS: decodeTime(pts),
},
Frame: frame,
})
return nil
})
case *mpegts.CodecMPEG1Video:
medi = &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.MPEG1Video{}},
}
r.OnDataMPEGxVideo(track, func(pts int64, frame []byte) error {
(*stream).WriteUnit(medi, medi.Formats[0], &unit.MPEG1Video{
Base: unit.Base{
NTP: time.Now(),
PTS: decodeTime(pts),
},
Frame: frame,
})
return nil
})
case *mpegts.CodecOpus:
medi = &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.Opus{
PayloadTyp: 96,
IsStereo: (codec.ChannelCount == 2),
}},
}
r.OnDataOpus(track, func(pts int64, packets [][]byte) error {
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Opus{
Base: unit.Base{
NTP: time.Now(),
PTS: decodeTime(pts),
},
Packets: packets,
})
return nil
})
case *mpegts.CodecMPEG4Audio:
medi = &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.MPEG4Audio{
PayloadTyp: 96,
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
Config: &codec.Config,
}},
}
r.OnDataMPEG4Audio(track, func(pts int64, aus [][]byte) error {
(*stream).WriteUnit(medi, medi.Formats[0], &unit.MPEG4Audio{
Base: unit.Base{
NTP: time.Now(),
PTS: decodeTime(pts),
},
AUs: aus,
})
return nil
})
case *mpegts.CodecMPEG1Audio:
medi = &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.MPEG1Audio{}},
}
r.OnDataMPEG1Audio(track, func(pts int64, frames [][]byte) error {
(*stream).WriteUnit(medi, medi.Formats[0], &unit.MPEG1Audio{
Base: unit.Base{
NTP: time.Now(),
PTS: decodeTime(pts),
},
Frames: frames,
})
return nil
})
case *mpegts.CodecAC3:
medi = &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.AC3{
PayloadTyp: 96,
SampleRate: codec.SampleRate,
ChannelCount: codec.ChannelCount,
}},
}
r.OnDataAC3(track, func(pts int64, frame []byte) error {
(*stream).WriteUnit(medi, medi.Formats[0], &unit.AC3{
Base: unit.Base{
NTP: time.Now(),
PTS: decodeTime(pts),
},
Frames: [][]byte{frame},
})
return nil
})
default:
continue
}
medias = append(medias, medi)
}
if len(medias) == 0 {
return nil, errMPEGTSNoTracks
}
return medias, nil
}
func mpegtsSetupWrite(
stream *stream.Stream,
writer *asyncwriter.Writer,
@ -218,11 +29,11 @@ func mpegtsSetupWrite(
sconn srt.Conn,
writeTimeout time.Duration,
) error {
var w *mpegts.Writer
var tracks []*mpegts.Track
var w *mcmpegts.Writer
var tracks []*mcmpegts.Track
addTrack := func(codec mpegts.Codec) *mpegts.Track {
track := &mpegts.Track{
addTrack := func(codec mcmpegts.Codec) *mcmpegts.Track {
track := &mcmpegts.Track{
Codec: codec,
}
tracks = append(tracks, track)
@ -233,7 +44,7 @@ func mpegtsSetupWrite(
for _, forma := range medi.Formats {
switch forma := forma.(type) {
case *format.H265: //nolint:dupl
track := addTrack(&mpegts.CodecH265{})
track := addTrack(&mcmpegts.CodecH265{})
var dtsExtractor *h265.DTSExtractor
@ -266,7 +77,7 @@ func mpegtsSetupWrite(
})
case *format.H264: //nolint:dupl
track := addTrack(&mpegts.CodecH264{})
track := addTrack(&mcmpegts.CodecH264{})
var dtsExtractor *h264.DTSExtractor
@ -299,7 +110,7 @@ func mpegtsSetupWrite(
})
case *format.MPEG4Video:
track := addTrack(&mpegts.CodecMPEG4Video{})
track := addTrack(&mcmpegts.CodecMPEG4Video{})
firstReceived := false
var lastPTS time.Duration
@ -326,7 +137,7 @@ func mpegtsSetupWrite(
})
case *format.MPEG1Video:
track := addTrack(&mpegts.CodecMPEG1Video{})
track := addTrack(&mcmpegts.CodecMPEG1Video{})
firstReceived := false
var lastPTS time.Duration
@ -353,7 +164,7 @@ func mpegtsSetupWrite(
})
case *format.Opus:
track := addTrack(&mpegts.CodecOpus{
track := addTrack(&mcmpegts.CodecOpus{
ChannelCount: func() int {
if forma.IsStereo {
return 2
@ -377,7 +188,7 @@ func mpegtsSetupWrite(
})
case *format.MPEG4Audio:
track := addTrack(&mpegts.CodecMPEG4Audio{
track := addTrack(&mcmpegts.CodecMPEG4Audio{
Config: *forma.GetConfig(),
})
@ -396,7 +207,7 @@ func mpegtsSetupWrite(
})
case *format.MPEG1Audio:
track := addTrack(&mpegts.CodecMPEG1Audio{})
track := addTrack(&mcmpegts.CodecMPEG1Audio{})
stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
tunit := u.(*unit.MPEG1Audio)
@ -413,7 +224,7 @@ func mpegtsSetupWrite(
})
case *format.AC3:
track := addTrack(&mpegts.CodecAC3{})
track := addTrack(&mcmpegts.CodecAC3{})
sampleRate := time.Duration(forma.SampleRate)
@ -440,10 +251,10 @@ func mpegtsSetupWrite(
}
if len(tracks) == 0 {
return errMPEGTSNoTracks
return mpegts.ErrNoTracks
}
w = mpegts.NewWriter(bw, tracks)
w = mcmpegts.NewWriter(bw, tracks)
return nil
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

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

View File

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

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

View File

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

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

View File

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

View File

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

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

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

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

View File

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

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

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

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 (
"context"
@ -6,19 +6,27 @@ import (
"net"
"net/http"
"testing"
"time"
"github.com/bluenviron/gortsplib/v4"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/gortsplib/v4/pkg/url"
"github.com/pion/rtp"
pwebrtc "github.com/pion/webrtc/v3"
"github.com/stretchr/testify/require"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/protocols/webrtc"
"github.com/bluenviron/mediamtx/internal/staticsources/tester"
)
func TestWebRTCSource(t *testing.T) {
state := 0
func whipOffer(body []byte) *pwebrtc.SessionDescription {
return &pwebrtc.SessionDescription{
Type: pwebrtc.SDPTypeOffer,
SDP: string(body),
}
}
func TestSource(t *testing.T) {
api, err := webrtc.NewAPI(webrtc.APIConf{})
require.NoError(t, err)
@ -31,9 +39,7 @@ func TestWebRTCSource(t *testing.T) {
defer pc.Close()
tracks, err := pc.SetupOutgoingTracks(
&format.VP8{
PayloadTyp: 96,
},
nil,
&format.Opus{
PayloadTyp: 111,
IsStereo: true,
@ -41,6 +47,8 @@ func TestWebRTCSource(t *testing.T) {
)
require.NoError(t, err)
state := 0
httpServ := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch state {
@ -79,23 +87,10 @@ func TestWebRTCSource(t *testing.T) {
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 96,
SequenceNumber: 123,
Timestamp: 45343,
SSRC: 563423,
},
Payload: []byte{5, 1},
})
require.NoError(t, err)
err = tracks[1].WriteRTP(&rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 97,
PayloadType: 111,
SequenceNumber: 1123,
Timestamp: 45343,
SSRC: 563423,
SSRC: 563424,
},
Payload: []byte{5, 2},
})
@ -120,59 +115,24 @@ func TestWebRTCSource(t *testing.T) {
}),
}
ln, err := net.Listen("tcp", "localhost:5555")
ln, err := net.Listen("tcp", "localhost:9003")
require.NoError(t, err)
go httpServ.Serve(ln)
defer httpServ.Shutdown(context.Background())
p, ok := newInstance("paths:\n" +
" proxied:\n" +
" source: whep://localhost:5555/my/resource\n" +
" sourceOnDemand: yes\n")
require.Equal(t, true, ok)
defer p.Close()
c := gortsplib.Client{}
u, err := url.Parse("rtsp://127.0.0.1:8554/proxied")
require.NoError(t, err)
err = c.Start(u.Scheme, u.Host)
require.NoError(t, err)
defer c.Close()
desc, _, err := c.Describe(u)
require.NoError(t, err)
var forma *format.VP8
medi := desc.FindFormat(&forma)
_, err = c.Setup(desc.BaseURL, medi, 0, 0)
require.NoError(t, err)
received := make(chan struct{})
c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
require.Equal(t, []byte{5, 3}, pkt.Payload)
close(received)
})
_, err = c.Play(nil)
require.NoError(t, err)
err = tracks[0].WriteRTP(&rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 96,
SequenceNumber: 124,
Timestamp: 45343,
SSRC: 563423,
te := tester.New(
func(p defs.StaticSourceParent) defs.StaticSource {
return &Source{
ReadTimeout: conf.StringDuration(10 * time.Second),
Parent: p,
}
},
Payload: []byte{5, 3},
})
require.NoError(t, err)
&conf.Path{
Source: "whep://localhost:9003/my/resource",
},
)
defer te.Close()
<-received
<-te.Unit
}