golanggohlsrtmpwebrtcmedia-serverobs-studiortcprtmp-proxyrtmp-serverrtprtsprtsp-proxyrtsp-relayrtsp-serversrtstreamingwebrtc-proxy
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
572 lines
14 KiB
572 lines
14 KiB
package core |
|
|
|
import ( |
|
"bufio" |
|
"context" |
|
"fmt" |
|
"net" |
|
"net/http" |
|
"net/url" |
|
"os" |
|
"os/exec" |
|
"path/filepath" |
|
"strings" |
|
"testing" |
|
"time" |
|
|
|
"github.com/bluenviron/gortsplib/v4" |
|
"github.com/bluenviron/gortsplib/v4/pkg/base" |
|
"github.com/bluenviron/gortsplib/v4/pkg/description" |
|
"github.com/bluenviron/gortsplib/v4/pkg/headers" |
|
"github.com/bluenviron/gortsplib/v4/pkg/sdp" |
|
"github.com/datarhei/gosrt" |
|
"github.com/pion/rtp" |
|
"github.com/stretchr/testify/require" |
|
|
|
"github.com/bluenviron/mediamtx/internal/protocols/rtmp" |
|
"github.com/bluenviron/mediamtx/internal/protocols/webrtc" |
|
) |
|
|
|
var runOnDemandSampleScript = ` |
|
package main |
|
|
|
import ( |
|
"os" |
|
"os/signal" |
|
"syscall" |
|
"github.com/bluenviron/gortsplib/v4" |
|
"github.com/bluenviron/gortsplib/v4/pkg/description" |
|
"github.com/bluenviron/gortsplib/v4/pkg/format" |
|
) |
|
|
|
func main() { |
|
if os.Getenv("MTX_PATH") != "ondemand" || |
|
os.Getenv("MTX_QUERY") != "param=value" || |
|
os.Getenv("G1") != "on" { |
|
panic("environment not set") |
|
} |
|
|
|
medi := &description.Media{ |
|
Type: description.MediaTypeVideo, |
|
Formats: []format.Format{&format.H264{ |
|
PayloadTyp: 96, |
|
SPS: []byte{ |
|
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{0x01, 0x02, 0x03, 0x04}, |
|
PacketizationMode: 1, |
|
}}, |
|
} |
|
|
|
source := gortsplib.Client{} |
|
|
|
err := source.StartRecording( |
|
"rtsp://localhost:" + os.Getenv("RTSP_PORT") + "/" + os.Getenv("MTX_PATH"), |
|
&description.Session{Medias: []*description.Media{medi}}) |
|
if err != nil { |
|
panic(err) |
|
} |
|
defer source.Close() |
|
|
|
c := make(chan os.Signal, 1) |
|
signal.Notify(c, syscall.SIGINT) |
|
<-c |
|
|
|
err = os.WriteFile("ON_DEMAND_FILE", []byte(""), 0644) |
|
if err != nil { |
|
panic(err) |
|
} |
|
} |
|
` |
|
|
|
func TestPathRunOnDemand(t *testing.T) { |
|
onDemandFile := filepath.Join(os.TempDir(), "ondemand") |
|
onUnDemandFile := filepath.Join(os.TempDir(), "onundemand") |
|
|
|
srcFile := filepath.Join(os.TempDir(), "ondemand.go") |
|
err := os.WriteFile(srcFile, |
|
[]byte(strings.ReplaceAll(runOnDemandSampleScript, "ON_DEMAND_FILE", onDemandFile)), 0o644) |
|
require.NoError(t, err) |
|
|
|
execFile := filepath.Join(os.TempDir(), "ondemand_cmd") |
|
cmd := exec.Command("go", "build", "-o", execFile, srcFile) |
|
cmd.Stdout = os.Stdout |
|
cmd.Stderr = os.Stderr |
|
err = cmd.Run() |
|
require.NoError(t, err) |
|
defer os.Remove(execFile) |
|
|
|
os.Remove(srcFile) |
|
|
|
for _, ca := range []string{"describe", "setup", "describe and setup"} { |
|
t.Run(ca, func(t *testing.T) { |
|
defer os.Remove(onDemandFile) |
|
defer os.Remove(onUnDemandFile) |
|
|
|
p1, ok := newInstance(fmt.Sprintf("rtmp: no\n"+ |
|
"hls: no\n"+ |
|
"webrtc: no\n"+ |
|
"paths:\n"+ |
|
" '~^(on)demand$':\n"+ |
|
" runOnDemand: %s\n"+ |
|
" runOnDemandCloseAfter: 1s\n"+ |
|
" runOnUnDemand: touch %s\n", execFile, onUnDemandFile)) |
|
require.Equal(t, true, ok) |
|
defer p1.Close() |
|
|
|
var control string |
|
|
|
func() { |
|
conn, err := net.Dial("tcp", "localhost:8554") |
|
require.NoError(t, err) |
|
defer conn.Close() |
|
br := bufio.NewReader(conn) |
|
|
|
if ca == "describe" || ca == "describe and setup" { |
|
u, err := base.ParseURL("rtsp://localhost:8554/ondemand?param=value") |
|
require.NoError(t, err) |
|
|
|
byts, _ := base.Request{ |
|
Method: base.Describe, |
|
URL: u, |
|
Header: base.Header{ |
|
"CSeq": base.HeaderValue{"1"}, |
|
}, |
|
}.Marshal() |
|
_, err = conn.Write(byts) |
|
require.NoError(t, err) |
|
|
|
var res base.Response |
|
err = res.Unmarshal(br) |
|
require.NoError(t, err) |
|
require.Equal(t, base.StatusOK, res.StatusCode) |
|
|
|
var desc sdp.SessionDescription |
|
err = desc.Unmarshal(res.Body) |
|
require.NoError(t, err) |
|
control, _ = desc.MediaDescriptions[0].Attribute("control") |
|
} else { |
|
control = "rtsp://localhost:8554/ondemand?param=value/" |
|
} |
|
|
|
if ca == "setup" || ca == "describe and setup" { |
|
u, err := base.ParseURL(control) |
|
require.NoError(t, err) |
|
|
|
byts, _ := base.Request{ |
|
Method: base.Setup, |
|
URL: u, |
|
Header: base.Header{ |
|
"CSeq": base.HeaderValue{"2"}, |
|
"Transport": headers.Transport{ |
|
Mode: func() *headers.TransportMode { |
|
v := headers.TransportModePlay |
|
return &v |
|
}(), |
|
Protocol: headers.TransportProtocolTCP, |
|
InterleavedIDs: &[2]int{0, 1}, |
|
}.Marshal(), |
|
}, |
|
}.Marshal() |
|
_, err = conn.Write(byts) |
|
require.NoError(t, err) |
|
|
|
var res base.Response |
|
err = res.Unmarshal(br) |
|
require.NoError(t, err) |
|
require.Equal(t, base.StatusOK, res.StatusCode) |
|
} |
|
}() |
|
|
|
for { |
|
_, err := os.Stat(onUnDemandFile) |
|
if err == nil { |
|
break |
|
} |
|
time.Sleep(100 * time.Millisecond) |
|
} |
|
|
|
_, err := os.Stat(onDemandFile) |
|
require.NoError(t, err) |
|
}) |
|
} |
|
} |
|
|
|
func TestPathRunOnConnect(t *testing.T) { |
|
for _, ca := range []string{"rtsp", "rtmp", "srt"} { |
|
t.Run(ca, func(t *testing.T) { |
|
onConnectFile := filepath.Join(os.TempDir(), "onconnect") |
|
defer os.Remove(onConnectFile) |
|
|
|
onDisconnectFile := filepath.Join(os.TempDir(), "ondisconnect") |
|
defer os.Remove(onDisconnectFile) |
|
|
|
func() { |
|
p, ok := newInstance(fmt.Sprintf( |
|
"paths:\n"+ |
|
" test:\n"+ |
|
"runOnConnect: touch %s\n"+ |
|
"runOnDisconnect: touch %s\n", |
|
onConnectFile, onDisconnectFile)) |
|
require.Equal(t, true, ok) |
|
defer p.Close() |
|
|
|
switch ca { |
|
case "rtsp": |
|
c := gortsplib.Client{} |
|
|
|
err := c.StartRecording( |
|
"rtsp://localhost:8554/test", |
|
&description.Session{Medias: []*description.Media{testMediaH264}}) |
|
require.NoError(t, err) |
|
defer c.Close() |
|
|
|
case "rtmp": |
|
u, err := url.Parse("rtmp://127.0.0.1:1935/test") |
|
require.NoError(t, err) |
|
|
|
nconn, err := net.Dial("tcp", u.Host) |
|
require.NoError(t, err) |
|
defer nconn.Close() |
|
|
|
_, err = rtmp.NewClientConn(nconn, u, true) |
|
require.NoError(t, err) |
|
|
|
case "srt": |
|
conf := srt.DefaultConfig() |
|
address, err := conf.UnmarshalURL("srt://localhost:8890?streamid=publish:test") |
|
require.NoError(t, err) |
|
|
|
err = conf.Validate() |
|
require.NoError(t, err) |
|
|
|
c, err := srt.Dial("srt", address, conf) |
|
require.NoError(t, err) |
|
defer c.Close() |
|
} |
|
|
|
time.Sleep(500 * time.Millisecond) |
|
}() |
|
|
|
_, err := os.Stat(onConnectFile) |
|
require.NoError(t, err) |
|
|
|
_, err = os.Stat(onDisconnectFile) |
|
require.NoError(t, err) |
|
}) |
|
} |
|
} |
|
|
|
func TestPathRunOnReady(t *testing.T) { |
|
onReadyFile := filepath.Join(os.TempDir(), "onready") |
|
defer os.Remove(onReadyFile) |
|
|
|
onNotReadyFile := filepath.Join(os.TempDir(), "onunready") |
|
defer os.Remove(onNotReadyFile) |
|
|
|
func() { |
|
p, ok := newInstance(fmt.Sprintf("rtmp: no\n"+ |
|
"hls: no\n"+ |
|
"webrtc: no\n"+ |
|
"paths:\n"+ |
|
" test:\n"+ |
|
" runOnReady: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n"+ |
|
" runOnNotReady: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n", |
|
onReadyFile, onNotReadyFile)) |
|
require.Equal(t, true, ok) |
|
defer p.Close() |
|
|
|
c := gortsplib.Client{} |
|
err := c.StartRecording( |
|
"rtsp://localhost:8554/test?query=value", |
|
&description.Session{Medias: []*description.Media{testMediaH264}}) |
|
require.NoError(t, err) |
|
defer c.Close() |
|
|
|
time.Sleep(500 * time.Millisecond) |
|
}() |
|
|
|
byts, err := os.ReadFile(onReadyFile) |
|
require.NoError(t, err) |
|
require.Equal(t, "test query=value\n", string(byts)) |
|
|
|
byts, err = os.ReadFile(onNotReadyFile) |
|
require.NoError(t, err) |
|
require.Equal(t, "test query=value\n", string(byts)) |
|
} |
|
|
|
func TestPathRunOnRead(t *testing.T) { |
|
for _, ca := range []string{"rtsp", "rtmp", "srt", "webrtc"} { |
|
t.Run(ca, func(t *testing.T) { |
|
onReadFile := filepath.Join(os.TempDir(), "onread") |
|
defer os.Remove(onReadFile) |
|
|
|
onUnreadFile := filepath.Join(os.TempDir(), "onunread") |
|
defer os.Remove(onUnreadFile) |
|
|
|
func() { |
|
p, ok := newInstance(fmt.Sprintf( |
|
"paths:\n"+ |
|
" test:\n"+ |
|
" runOnRead: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n"+ |
|
" runOnUnread: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n", |
|
onReadFile, onUnreadFile)) |
|
require.Equal(t, true, ok) |
|
defer p.Close() |
|
|
|
source := gortsplib.Client{} |
|
err := source.StartRecording( |
|
"rtsp://localhost:8554/test", |
|
&description.Session{Medias: []*description.Media{testMediaH264}}) |
|
require.NoError(t, err) |
|
defer source.Close() |
|
|
|
switch ca { |
|
case "rtsp": |
|
reader := gortsplib.Client{} |
|
|
|
u, err := base.ParseURL("rtsp://127.0.0.1:8554/test?query=value") |
|
require.NoError(t, err) |
|
|
|
err = reader.Start(u.Scheme, u.Host) |
|
require.NoError(t, err) |
|
defer reader.Close() |
|
|
|
desc, _, err := reader.Describe(u) |
|
require.NoError(t, err) |
|
|
|
err = reader.SetupAll(desc.BaseURL, desc.Medias) |
|
require.NoError(t, err) |
|
|
|
_, err = reader.Play(nil) |
|
require.NoError(t, err) |
|
|
|
case "rtmp": |
|
u, err := url.Parse("rtmp://127.0.0.1:1935/test?query=value") |
|
require.NoError(t, err) |
|
|
|
nconn, err := net.Dial("tcp", u.Host) |
|
require.NoError(t, err) |
|
defer nconn.Close() |
|
|
|
conn, err := rtmp.NewClientConn(nconn, u, false) |
|
require.NoError(t, err) |
|
|
|
_, err = rtmp.NewReader(conn) |
|
require.NoError(t, err) |
|
|
|
case "srt": |
|
conf := srt.DefaultConfig() |
|
address, err := conf.UnmarshalURL("srt://localhost:8890?streamid=read:test:query=value") |
|
require.NoError(t, err) |
|
|
|
err = conf.Validate() |
|
require.NoError(t, err) |
|
|
|
reader, err := srt.Dial("srt", address, conf) |
|
require.NoError(t, err) |
|
defer reader.Close() |
|
|
|
case "webrtc": |
|
hc := &http.Client{Transport: &http.Transport{}} |
|
|
|
u, err := url.Parse("http://localhost:8889/test/whep?query=value") |
|
require.NoError(t, err) |
|
|
|
c := &webrtc.WHIPClient{ |
|
HTTPClient: hc, |
|
URL: u, |
|
} |
|
|
|
_, err = c.Read(context.Background()) |
|
require.NoError(t, err) |
|
defer checkClose(t, c.Close) |
|
} |
|
|
|
time.Sleep(500 * time.Millisecond) |
|
}() |
|
|
|
byts, err := os.ReadFile(onReadFile) |
|
require.NoError(t, err) |
|
require.Equal(t, "test query=value\n", string(byts)) |
|
|
|
byts, err = os.ReadFile(onUnreadFile) |
|
require.NoError(t, err) |
|
require.Equal(t, "test query=value\n", string(byts)) |
|
}) |
|
} |
|
} |
|
|
|
func TestPathMaxReaders(t *testing.T) { |
|
p, ok := newInstance("paths:\n" + |
|
" all_others:\n" + |
|
" maxReaders: 1\n") |
|
require.Equal(t, true, ok) |
|
defer p.Close() |
|
|
|
source := gortsplib.Client{} |
|
err := source.StartRecording( |
|
"rtsp://localhost:8554/mystream", |
|
&description.Session{Medias: []*description.Media{ |
|
testMediaH264, |
|
testMediaAAC, |
|
}}) |
|
require.NoError(t, err) |
|
defer source.Close() |
|
|
|
for i := 0; i < 2; i++ { |
|
reader := gortsplib.Client{} |
|
|
|
u, err := base.ParseURL("rtsp://127.0.0.1:8554/mystream") |
|
require.NoError(t, err) |
|
|
|
err = reader.Start(u.Scheme, u.Host) |
|
require.NoError(t, err) |
|
defer reader.Close() |
|
|
|
desc, _, err := reader.Describe(u) |
|
require.NoError(t, err) |
|
|
|
err = reader.SetupAll(desc.BaseURL, desc.Medias) |
|
if i != 1 { |
|
require.NoError(t, err) |
|
} else { |
|
require.Error(t, err) |
|
} |
|
} |
|
} |
|
|
|
func TestPathRecord(t *testing.T) { |
|
dir, err := os.MkdirTemp("", "rtsp-path-record") |
|
require.NoError(t, err) |
|
defer os.RemoveAll(dir) |
|
|
|
p, ok := newInstance("api: yes\n" + |
|
"record: yes\n" + |
|
"recordPath: " + filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f") + "\n" + |
|
"paths:\n" + |
|
" all_others:\n" + |
|
" record: yes\n") |
|
require.Equal(t, true, ok) |
|
defer p.Close() |
|
|
|
source := gortsplib.Client{} |
|
err = source.StartRecording( |
|
"rtsp://localhost:8554/mystream", |
|
&description.Session{Medias: []*description.Media{testMediaH264}}) |
|
require.NoError(t, err) |
|
defer source.Close() |
|
|
|
for i := 0; i < 4; i++ { |
|
err := source.WritePacketRTP(testMediaH264, &rtp.Packet{ |
|
Header: rtp.Header{ |
|
Version: 2, |
|
Marker: true, |
|
PayloadType: 96, |
|
SequenceNumber: 1123 + uint16(i), |
|
Timestamp: 45343 + 90000*uint32(i), |
|
SSRC: 563423, |
|
}, |
|
Payload: []byte{5}, |
|
}) |
|
require.NoError(t, err) |
|
} |
|
|
|
time.Sleep(500 * time.Millisecond) |
|
|
|
files, err := os.ReadDir(filepath.Join(dir, "mystream")) |
|
require.NoError(t, err) |
|
require.Equal(t, 1, len(files)) |
|
|
|
hc := &http.Client{Transport: &http.Transport{}} |
|
|
|
httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/paths/patch/all_others", map[string]interface{}{ |
|
"record": false, |
|
}, nil) |
|
|
|
time.Sleep(500 * time.Millisecond) |
|
|
|
httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/paths/patch/all_others", map[string]interface{}{ |
|
"record": true, |
|
}, nil) |
|
|
|
time.Sleep(500 * time.Millisecond) |
|
|
|
for i := 4; i < 8; i++ { |
|
err := source.WritePacketRTP(testMediaH264, &rtp.Packet{ |
|
Header: rtp.Header{ |
|
Version: 2, |
|
Marker: true, |
|
PayloadType: 96, |
|
SequenceNumber: 1123 + uint16(i), |
|
Timestamp: 45343 + 90000*uint32(i), |
|
SSRC: 563423, |
|
}, |
|
Payload: []byte{5}, |
|
}) |
|
require.NoError(t, err) |
|
} |
|
|
|
time.Sleep(500 * time.Millisecond) |
|
|
|
files, err = os.ReadDir(filepath.Join(dir, "mystream")) |
|
require.NoError(t, err) |
|
require.Equal(t, 2, len(files)) |
|
} |
|
|
|
func TestPathFallback(t *testing.T) { |
|
for _, ca := range []string{ |
|
"absolute", |
|
"relative", |
|
"source", |
|
} { |
|
t.Run(ca, func(t *testing.T) { |
|
var conf string |
|
|
|
switch ca { |
|
case "absolute": |
|
conf = "paths:\n" + |
|
" path1:\n" + |
|
" fallback: rtsp://localhost:8554/path2\n" + |
|
" path2:\n" |
|
|
|
case "relative": |
|
conf = "paths:\n" + |
|
" path1:\n" + |
|
" fallback: /path2\n" + |
|
" path2:\n" |
|
|
|
case "source": |
|
conf = "paths:\n" + |
|
" path1:\n" + |
|
" fallback: /path2\n" + |
|
" source: rtsp://localhost:3333/nonexistent\n" + |
|
" path2:\n" |
|
} |
|
|
|
p1, ok := newInstance(conf) |
|
require.Equal(t, true, ok) |
|
defer p1.Close() |
|
|
|
source := gortsplib.Client{} |
|
err := source.StartRecording("rtsp://localhost:8554/path2", |
|
&description.Session{Medias: []*description.Media{testMediaH264}}) |
|
require.NoError(t, err) |
|
defer source.Close() |
|
|
|
u, err := base.ParseURL("rtsp://localhost:8554/path1") |
|
require.NoError(t, err) |
|
|
|
dest := gortsplib.Client{} |
|
err = dest.Start(u.Scheme, u.Host) |
|
require.NoError(t, err) |
|
defer dest.Close() |
|
|
|
desc, _, err := dest.Describe(u) |
|
require.NoError(t, err) |
|
require.Equal(t, 1, len(desc.Medias)) |
|
}) |
|
} |
|
}
|
|
|