mirror of
https://github.com/bluenviron/mediamtx
synced 2025-01-20 22:21:01 +00:00
move static sources into dedicated package (#2616)
This commit is contained in:
parent
e9528c0917
commit
43d41c070b
@ -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)),
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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),
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
|
@ -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)),
|
||||
|
@ -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
|
||||
}
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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),
|
||||
|
@ -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(
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
}
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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(),
|
||||
|
@ -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")
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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: "",
|
||||
}
|
||||
|
@ -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():
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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: "",
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
262
internal/core/static_source_handler.go
Normal file
262
internal/core/static_source_handler.go
Normal 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():
|
||||
}
|
||||
}
|
@ -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),
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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(
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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: "",
|
||||
}
|
||||
}
|
@ -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
2
internal/defs/defs.go
Normal file
@ -0,0 +1,2 @@
|
||||
// Package defs contains shared definitions.
|
||||
package defs
|
25
internal/defs/path.go
Normal file
25
internal/defs/path.go
Normal 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{}
|
||||
}
|
29
internal/defs/static_source.go
Normal file
29
internal/defs/static_source.go
Normal 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
|
||||
}
|
204
internal/protocols/mpegts/to_stream.go
Normal file
204
internal/protocols/mpegts/to_stream.go
Normal 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
|
||||
}
|
35
internal/protocols/tls/tls_config.go
Normal file
35
internal/protocols/tls/tls_config.go
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
20
internal/protocols/webrtc/track_wrapper.go
Normal file
20
internal/protocols/webrtc/track_wrapper.go
Normal 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
|
||||
}
|
32
internal/protocols/webrtc/tracks_to_medias.go
Normal file
32
internal/protocols/webrtc/tracks_to_medias.go
Normal 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
|
||||
}
|
18
internal/restrictnetwork/restrict_network.go
Normal file
18
internal/restrictnetwork/restrict_network.go
Normal 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
|
||||
}
|
@ -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: "",
|
||||
}
|
117
internal/staticsources/hls/source_test.go
Normal file
117
internal/staticsources/hls/source_test.go
Normal 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
|
||||
}
|
@ -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: "",
|
||||
}
|
@ -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: "",
|
||||
}
|
189
internal/staticsources/rtmp/source_test.go
Normal file
189
internal/staticsources/rtmp/source_test.go
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
@ -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: "",
|
||||
}
|
432
internal/staticsources/rtsp/source_test.go
Normal file
432
internal/staticsources/rtsp/source_test.go
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
115
internal/staticsources/srt/source.go
Normal file
115
internal/staticsources/srt/source.go
Normal 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: "",
|
||||
}
|
||||
}
|
73
internal/staticsources/srt/source_test.go
Normal file
73
internal/staticsources/srt/source_test.go
Normal 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
|
||||
}
|
86
internal/staticsources/tester/tester.go
Normal file
86
internal/staticsources/tester/tester.go
Normal 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) {
|
||||
}
|
@ -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: "",
|
||||
}
|
59
internal/staticsources/udp/source_test.go
Normal file
59
internal/staticsources/udp/source_test.go
Normal 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
|
||||
}
|
103
internal/staticsources/webrtc/source.go
Normal file
103
internal/staticsources/webrtc/source.go
Normal 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: "",
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user