58 changed files with 2271 additions and 2172 deletions
@ -1,208 +0,0 @@
@@ -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 |
||||
} |
||||
@ -1,17 +0,0 @@
@@ -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 |
||||
} |
||||
@ -1,147 +0,0 @@
@@ -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) |
||||
}) |
||||
} |
||||
} |
||||
@ -1,312 +0,0 @@
@@ -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 |
||||
}) |
||||
} |
||||
} |
||||
@ -1,249 +0,0 @@
@@ -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(): |
||||
} |
||||
} |
||||
@ -1,129 +0,0 @@
@@ -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 @@
@@ -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) |
||||
} |
||||
@ -0,0 +1,262 @@
@@ -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 @@
@@ -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 @@
@@ -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 |
||||
} |
||||
@ -1,118 +0,0 @@
@@ -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: "", |
||||
} |
||||
} |
||||
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
// Package defs contains shared definitions.
|
||||
package defs |
||||
@ -0,0 +1,25 @@
@@ -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{} |
||||
} |
||||
@ -0,0 +1,29 @@
@@ -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 |
||||
} |
||||
@ -0,0 +1,204 @@
@@ -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 |
||||
} |
||||
@ -0,0 +1,35 @@
@@ -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 |
||||
}, |
||||
} |
||||
} |
||||
@ -0,0 +1,20 @@
@@ -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 |
||||
} |
||||
@ -0,0 +1,32 @@
@@ -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 |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -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 |
||||
} |
||||
@ -0,0 +1,117 @@
@@ -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 |
||||
} |
||||
@ -0,0 +1,189 @@
@@ -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 |
||||
}) |
||||
} |
||||
} |
||||
@ -0,0 +1,432 @@
@@ -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 |
||||
}) |
||||
} |
||||
} |
||||
@ -0,0 +1,115 @@
@@ -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: "", |
||||
} |
||||
} |
||||
@ -0,0 +1,73 @@
@@ -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 |
||||
} |
||||
@ -0,0 +1,86 @@
@@ -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) { |
||||
} |
||||
@ -0,0 +1,59 @@
@@ -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 |
||||
} |
||||
@ -0,0 +1,103 @@
@@ -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: "", |
||||
} |
||||
} |
||||
Loading…
Reference in new issue