diff --git a/internal/teste2e/client_vs_server_test.go b/internal/teste2e/client_vs_server_test.go new file mode 100644 index 00000000..a212b019 --- /dev/null +++ b/internal/teste2e/client_vs_server_test.go @@ -0,0 +1,182 @@ +//go:build enable_e2e_tests + +package teste2e + +import ( + "crypto/tls" + "net" + "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/format" + "github.com/pion/rtp" + "github.com/stretchr/testify/require" +) + +func multicastCapableIP(t *testing.T) string { + intfs, err := net.Interfaces() + require.NoError(t, err) + + for _, intf := range intfs { + if (intf.Flags & net.FlagMulticast) != 0 { + addrs, err := intf.Addrs() + if err != nil { + continue + } + + for _, addr := range addrs { + switch v := addr.(type) { + case *net.IPNet: + return v.IP.String() + case *net.IPAddr: + return v.IP.String() + } + } + } + } + + t.Errorf("unable to find a multicast IP") + return "" +} + +func TestClientVsServer(t *testing.T) { + for _, ca := range []struct { + publisherScheme string + publisherProto string + readerScheme string + readerProto string + }{ + { + publisherScheme: "rtsp", + publisherProto: "udp", + readerScheme: "rtsp", + readerProto: "udp", + }, + { + publisherScheme: "rtsp", + publisherProto: "tcp", + readerScheme: "rtsp", + readerProto: "udp", + }, + { + publisherScheme: "rtsp", + publisherProto: "tcp", + readerScheme: "rtsp", + readerProto: "tcp", + }, + { + publisherScheme: "rtsp", + publisherProto: "udp", + readerScheme: "rtsp", + readerProto: "tcp", + }, + { + publisherScheme: "rtsp", + publisherProto: "udp", + readerScheme: "rtsp", + readerProto: "multicast", + }, + { + publisherScheme: "rtsps", + publisherProto: "tcp", + readerScheme: "rtsps", + readerProto: "tcp", + }, + } { + t.Run(ca.publisherScheme+"_"+ca.publisherProto+"_"+ + ca.readerScheme+"_"+ca.readerProto, func(t *testing.T) { + ss := &sampleServer{} + + if ca.publisherScheme == "rtsps" { + cert, err := tls.X509KeyPair(serverCert, serverKey) + require.NoError(t, err) + ss.tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}} + } + + err := ss.initialize() + require.NoError(t, err) + defer ss.close() + + desc := &description.Session{ + Medias: []*description.Media{ + { + Type: description.MediaTypeVideo, + Formats: []format.Format{&format.H264{ + PayloadTyp: 96, + PacketizationMode: 1, + }}, + }, + }, + } + + var publisherProto gortsplib.Transport + switch ca.publisherProto { + case "udp": + publisherProto = gortsplib.TransportUDP + case "tcp": + publisherProto = gortsplib.TransportTCP + } + + publisher := &gortsplib.Client{ + TLSConfig: &tls.Config{InsecureSkipVerify: true}, + Transport: &publisherProto, + } + err = publisher.StartRecording(ca.publisherScheme+"://127.0.0.1:8554/test/stream?key=val", desc) + require.NoError(t, err) + defer publisher.Close() + + time.Sleep(1 * time.Second) + + var readerProto gortsplib.Transport + switch ca.readerProto { + case "udp": + readerProto = gortsplib.TransportUDP + case "tcp": + readerProto = gortsplib.TransportTCP + case "multicast": + readerProto = gortsplib.TransportUDPMulticast + } + + u, err := base.ParseURL(ca.readerScheme + "://" + multicastCapableIP(t) + ":8554/test/stream?key=val") + require.NoError(t, err) + + reader := &gortsplib.Client{ + TLSConfig: &tls.Config{InsecureSkipVerify: true}, + Transport: &readerProto, + } + err = reader.Start(u.Scheme, u.Host) + require.NoError(t, err) + defer reader.Close() + + desc2, _, err := reader.Describe(u) + require.NoError(t, err) + + err = reader.SetupAll(desc2.BaseURL, desc2.Medias) + require.NoError(t, err) + + packetRecv := make(chan struct{}) + + reader.OnPacketRTPAny(func(medi *description.Media, _ format.Format, _ *rtp.Packet) { + close(packetRecv) + }) + + _, err = reader.Play(nil) + require.NoError(t, err) + + err = publisher.WritePacketRTP(desc.Medias[0], &rtp.Packet{ + Header: rtp.Header{ + PayloadType: 96, + SequenceNumber: 1, + SSRC: 123, + }, + Payload: []byte{5}, + }) + require.NoError(t, err) + + <-packetRecv + }) + } +} diff --git a/internal/teste2e/images/ffmpeg/Dockerfile b/internal/teste2e/images/ffmpeg/Dockerfile index 2af66cf4..5c696330 100644 --- a/internal/teste2e/images/ffmpeg/Dockerfile +++ b/internal/teste2e/images/ffmpeg/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.14 +FROM alpine:3.22 RUN apk add --no-cache \ ffmpeg diff --git a/internal/teste2e/images/gstreamer/Dockerfile b/internal/teste2e/images/gstreamer/Dockerfile index c5f496b2..15bc973a 100644 --- a/internal/teste2e/images/gstreamer/Dockerfile +++ b/internal/teste2e/images/gstreamer/Dockerfile @@ -1,42 +1,11 @@ -###################################### -FROM ubuntu:20.04 AS build +FROM alpine:3.22 -RUN apt update && apt install -y --no-install-recommends \ - pkg-config \ - gcc \ - libgstreamer-plugins-base1.0-dev \ - && rm -rf /var/lib/apt/lists/* - -COPY exitafterframe.c /s/ -RUN cd /s \ - && gcc \ - exitafterframe.c \ - -o libexitafterframe.so \ - -Ofast \ - -s \ - -Werror \ - -Wall \ - -Wextra \ - -Wno-unused-parameter \ - -fPIC \ - -shared \ - -Wl,--no-undefined \ - $(pkg-config --cflags --libs gstreamer-1.0) \ - && mv libexitafterframe.so /usr/lib/x86_64-linux-gnu/gstreamer-1.0/ \ - && rm -rf /s - -###################################### -FROM ubuntu:20.04 - -RUN apt update && apt install -y --no-install-recommends \ - gstreamer1.0-tools \ - gstreamer1.0-plugins-good \ - gstreamer1.0-plugins-bad \ - gstreamer1.0-rtsp \ - gstreamer1.0-libav \ - && rm -rf /var/lib/apt/lists/* - -COPY --from=build /usr/lib/x86_64-linux-gnu/gstreamer-1.0/libexitafterframe.so /usr/lib/x86_64-linux-gnu/gstreamer-1.0/ +RUN apk add --no-cache \ + gstreamer-tools \ + gst-plugins-bad \ + gst-plugins-good \ + gst-rtsp-server \ + gst-libav COPY emptyvideo.mkv / diff --git a/internal/teste2e/images/gstreamer/exitafterframe.c b/internal/teste2e/images/gstreamer/exitafterframe.c deleted file mode 100644 index 2f627eda..00000000 --- a/internal/teste2e/images/gstreamer/exitafterframe.c +++ /dev/null @@ -1,92 +0,0 @@ - -#include - -GType gst_exitafterframe_get_type (); - -#define GST_TYPE_EXITAFTERFRAME (gst_exitafterframe_get_type()) -#define GST_EXITAFTERFRAME(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_EXITAFTERFRAME,GstExitAfterFrame)) - -typedef struct -{ - GstElement element; - GstPad *srcpad; - GstPad *sinkpad; - -} GstExitAfterFrame; - -typedef struct -{ - GstElementClass parent_class; - -} GstExitAfterFrameClass; - -#define gst_exitafterframe_parent_class parent_class -G_DEFINE_TYPE (GstExitAfterFrame, gst_exitafterframe, GST_TYPE_ELEMENT); - -static GstStaticPadTemplate sink_factory = GST_STATIC_PAD_TEMPLATE( - "sink", - GST_PAD_SINK, - GST_PAD_ALWAYS, - GST_STATIC_CAPS("video/x-raw") -); - -static GstStaticPadTemplate src_factory = GST_STATIC_PAD_TEMPLATE( - "src", - GST_PAD_SRC, - GST_PAD_ALWAYS, - GST_STATIC_CAPS("video/x-raw") -); - -static GstFlowReturn -gst_exitafterframe_chain (GstPad * pad, GstObject * parent, GstBuffer * buf) -{ - GstExitAfterFrame *filter = GST_EXITAFTERFRAME (parent); - exit(0); - return gst_pad_push (filter->srcpad, buf); -} - -static void -gst_exitafterframe_class_init(GstExitAfterFrameClass* klass) { - GstElementClass* element_class = (GstElementClass*)klass; - - gst_element_class_set_details_simple( - element_class, - "Plugin", - "FIXME:Generic", - "FIXME:Generic Template Element", - "AUTHOR_NAME AUTHOR_EMAIL" - ); - - gst_element_class_add_pad_template(element_class, - gst_static_pad_template_get(&src_factory)); - gst_element_class_add_pad_template(element_class, - gst_static_pad_template_get(&sink_factory)); -} - -static void -gst_exitafterframe_init (GstExitAfterFrame* filter) -{ - GstElement* element = GST_ELEMENT(filter); - - filter->sinkpad = gst_pad_new_from_static_template(&sink_factory, "sink"); - gst_pad_set_chain_function(filter->sinkpad, gst_exitafterframe_chain); - GST_PAD_SET_PROXY_CAPS(filter->sinkpad); - gst_element_add_pad(element, filter->sinkpad); - - filter->srcpad = gst_pad_new_from_static_template(&src_factory, "src"); - GST_PAD_SET_PROXY_CAPS(filter->srcpad); - gst_element_add_pad(element, filter->srcpad); -} - -static gboolean -plugin_init (GstPlugin * plugin) -{ - return gst_element_register (plugin, "exitafterframe", GST_RANK_NONE, - GST_TYPE_EXITAFTERFRAME); -} - -#define PACKAGE "exitafterframe" - -GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR, exitafterframe, - "exitafterframe", plugin_init, "1.0", "LGPL", "exitafterframe", - "http://example.com") diff --git a/internal/teste2e/sample_server_test.go b/internal/teste2e/sample_server_test.go new file mode 100644 index 00000000..3b16d9ae --- /dev/null +++ b/internal/teste2e/sample_server_test.go @@ -0,0 +1,249 @@ +//go:build enable_e2e_tests + +package teste2e + +import ( + "crypto/tls" + "fmt" + "sync" + + "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/format" + "github.com/pion/rtp" +) + +var serverCert = []byte(`-----BEGIN CERTIFICATE----- +MIIDkzCCAnugAwIBAgIUHFnymlrkEnz3ThpFvSrqybBepn4wDQYJKoZIhvcNAQEL +BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X +DTIxMTIwMzIxNDg0MFoXDTMxMTIwMTIxNDg0MFowWTELMAkGA1UEBhMCQVUxEzAR +BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5 +IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAv8h21YDIAYNzewrfQqQTlODJjuUZKxMCO7z1wIapem5I+1I8n+vD +v8qvuyZk1m9CKQPfXxhJz0TT5kECoUY0KaDtykSzfaUK34F9J1d5snDkaOtN48W+ +8l39Wtcvc5JW17jNwabppAkHHYAMQryO8urKLWKbZmLhYCJdYgNqb8ciWPsnYNA0 +zcnKML9zQphh7dxPq1wCsy/c/XZUzxTLAe8hsCKuqpESEX3MMJA9gOLmiOF0JgpT +9h6eqvJU8IK0QMIv3tekJWSBvTLyz4ghENs10sMKKNqR6NWt2SsOloeBkOhIDLOk +byLaPEvugrQsga99uhANRpXp+CHnVeAH8QIDAQABo1MwUTAdBgNVHQ4EFgQUwyEH +cMynEoy1/TnbIhgpEAs038gwHwYDVR0jBBgwFoAUwyEHcMynEoy1/TnbIhgpEAs0 +38gwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAiV56KhDoUVzW +qV1X0QbfLaifimsN3Na3lUgmjcgyUe8rHj09pXuAD/AcQw/zwKzZ6dPtizBeNLN8 +jV1dbJmR7DE3MDlndgMKTOKFsqzHjG9UTXkBGFUEM1shn2GE8XcvDF0AzKU82YjP +B0KswA1NoYTNP2PW4IhZRzv2M+fnmkvc8DSEZ+dxEMg3aJfe/WLPvYjDpFXLvuxl +YnerRQ04hFysh5eogPFpB4KyyPs6jGnQFmZCbFyk9pjKRbDPJc6FkDglkzTB6j3Q +TSfgNJswOiap13vQQKf5Vu7LTuyO4Wjfjr74QNqMLLNIgcC7n2jfQj1g5Xa0bnF5 +G4tLrCLUUw== +-----END CERTIFICATE----- +`) + +var serverKey = []byte(`-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC/yHbVgMgBg3N7 +Ct9CpBOU4MmO5RkrEwI7vPXAhql6bkj7Ujyf68O/yq+7JmTWb0IpA99fGEnPRNPm +QQKhRjQpoO3KRLN9pQrfgX0nV3mycORo603jxb7yXf1a1y9zklbXuM3BpumkCQcd +gAxCvI7y6sotYptmYuFgIl1iA2pvxyJY+ydg0DTNycowv3NCmGHt3E+rXAKzL9z9 +dlTPFMsB7yGwIq6qkRIRfcwwkD2A4uaI4XQmClP2Hp6q8lTwgrRAwi/e16QlZIG9 +MvLPiCEQ2zXSwwoo2pHo1a3ZKw6Wh4GQ6EgMs6RvIto8S+6CtCyBr326EA1Glen4 +IedV4AfxAgMBAAECggEAOqcJSNSA1o2oJKo3i374iiCRJAWGw/ilRzXMBtxoOow9 +/7av2czV6fMH+XmNf1M5bafEiaW49Q28rH+XWVFKJK0V7DVEm5l9EMveRcjn7B3A +jSHhiVZxxlfeYwjKd1L7AjB/pMjyTXuBVJFTrplSMpKB0I2GrzJwcOExpAcdZx98 +K0s5pauJH9bE0kI3p585SGQaIjrz0LvAmf6cQ5HhKfahJdWNnKZ/S4Kdqe+JCgyd +NawREHhf3tU01Cd3DOgXn4+5V/Ts6XtqY1RuSvonNv3nyeiOpX8C4cHKD5u2sNOC +3J4xWrrs0W3e8IATgAys56teKbEufHTUx52wNhAbzQKBgQD56W0tPCuaKrsjxsvE +dNHdm/9aQrN1jCJxUcGaxCIioXSyDvpSKcgxQbEqHXRTtJt5/Kadz9omq4vFTVtl +5Gf+3Lrf3ZT82SvYHtlIMdBZLlKwk6MolEa0KGAuJBNJVRIOkm5YjV/3bJebeTIb +WrLEyNCOXFAh3KVzBPU8nJ1aTwKBgQDEdISg3UsSOLBa0BfoJ5FlqGepZSufYgqh +xAJn8EbopnlzfmHBZAhE2+Igh0xcHhQqHThc3OuLtAkWu6fUSLiSA+XjU9TWPpA1 +C/325rhT23fxzYIlYFegR9BToxYhv14ufkcTXRfHRAhffk7K5A2nlJfldDZRmUh2 +5KIjXQ0pvwKBgQCa7S6VgFu3cw4Ym8DuxUzlCTRADGGcWYdwoLJY84YF2fmx+L8N ++ID2qDbgWOooiipocUwJQTWIC4jWg6JJhFNEGCpxZbhbF3aqwFULAHadEq6IcL4R +Bfre7LjTYeHi8C4FgpmNo/b+N/+0jmmVs6BnheZkmq3CkDqxFz3AmYai2QKBgQC1 +kzAmcoJ5U/YD6YO/Khsjx3QQSBb6mCZVf5HtuVIApCVqzuvRUACojEbDY+n61j4y +8pDum64FkKA557Xl6lTVeE7ZPtlgL7EfpnbT5kmGEDobPqPEofg7h0SQmRLSnEqT +VFmjFw7sOQA4Ksjuk7vfIOMHy9KMts0YPpdxcgbBhwKBgQCP8MeRPuhZ26/oIESr +I8ArLEaPebYmLXCT2ZTudGztoyYFxinRGHA4PdamSOKfB1li52wAaqgRA3cSqkUi +kabimVOvrOAWlnvznqXEHPNx6mbbKs08jh+uRRmrOmMrxAobpTqarL2Sdxb6afID +NkxNic7oHgsZpIkZ8HK+QjAAWA== +-----END PRIVATE KEY----- +`) + +type sampleServer struct { + tlsConfig *tls.Config + + s *gortsplib.Server + mutex sync.Mutex + stream *gortsplib.ServerStream + publisher *gortsplib.ServerSession +} + +func (sh *sampleServer) OnSessionClose(ctx *gortsplib.ServerHandlerOnSessionCloseCtx) { + sh.mutex.Lock() + defer sh.mutex.Unlock() + + if sh.stream != nil { + if ctx.Session == sh.publisher { + sh.stream.Close() + sh.stream = nil + } + } +} + +func (sh *sampleServer) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) { + if ctx.Path != "/test/stream" { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, nil, fmt.Errorf("invalid path (%s)", ctx.Request.URL) + } + if ctx.Query != "key=val" { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, nil, fmt.Errorf("invalid query (%s)", ctx.Query) + } + + sh.mutex.Lock() + defer sh.mutex.Unlock() + + if sh.stream == nil { + return &base.Response{ + StatusCode: base.StatusNotFound, + }, nil, nil + } + + return &base.Response{ + StatusCode: base.StatusOK, + }, sh.stream, nil +} + +func (sh *sampleServer) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) { + if ctx.Path != "/test/stream" { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, fmt.Errorf("invalid path (%s)", ctx.Request.URL) + } + if ctx.Query != "key=val" { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, fmt.Errorf("invalid query (%s)", ctx.Query) + } + + sh.mutex.Lock() + defer sh.mutex.Unlock() + + if sh.stream != nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, fmt.Errorf("someone is already publishing") + } + + sh.stream = &gortsplib.ServerStream{ + Server: sh.s, + Desc: ctx.Description, + } + err := sh.stream.Initialize() + if err != nil { + return &base.Response{ + StatusCode: base.StatusInternalServerError, + }, err + } + + sh.publisher = ctx.Session + + return &base.Response{ + StatusCode: base.StatusOK, + }, nil +} + +func (sh *sampleServer) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) { + if ctx.Path != "/test/stream" { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, nil, fmt.Errorf("invalid path (%s)", ctx.Request.URL) + } + if ctx.Query != "key=val" { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, nil, fmt.Errorf("invalid query (%s)", ctx.Query) + } + + if ctx.Session.State() == gortsplib.ServerSessionStatePreRecord { + return &base.Response{ + StatusCode: base.StatusOK, + }, nil, nil + } + + if sh.stream == nil { + return &base.Response{ + StatusCode: base.StatusNotFound, + }, nil, nil + } + + return &base.Response{ + StatusCode: base.StatusOK, + }, sh.stream, nil +} + +func (sh *sampleServer) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { + if ctx.Path != "/test/stream" { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, fmt.Errorf("invalid path (%s)", ctx.Request.URL) + } + if ctx.Query != "key=val" { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, fmt.Errorf("invalid query (%s)", ctx.Query) + } + + return &base.Response{ + StatusCode: base.StatusOK, + }, nil +} + +func (sh *sampleServer) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) { + if ctx.Path != "/test/stream" { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, fmt.Errorf("invalid path (%s)", ctx.Request.URL) + } + if ctx.Query != "key=val" { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, fmt.Errorf("invalid query (%s)", ctx.Query) + } + + ctx.Session.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) { + sh.stream.WritePacketRTP(medi, pkt) + }) + + return &base.Response{ + StatusCode: base.StatusOK, + }, nil +} + +func (sh *sampleServer) initialize() error { + sh.s = &gortsplib.Server{ + Handler: sh, + TLSConfig: sh.tlsConfig, + RTSPAddress: "0.0.0.0:8554", + } + + if sh.tlsConfig == nil { + sh.s.UDPRTPAddress = "0.0.0.0:8000" + sh.s.UDPRTCPAddress = "0.0.0.0:8001" + sh.s.MulticastIPRange = "224.1.0.0/16" + sh.s.MulticastRTPPort = 8002 + sh.s.MulticastRTCPPort = 8003 + } + + err := sh.s.Start() + if err != nil { + return err + } + + return nil +} + +func (sh *sampleServer) close() { + defer sh.s.Close() +} diff --git a/internal/teste2e/server_test.go b/internal/teste2e/server_test.go deleted file mode 100644 index 69571ae7..00000000 --- a/internal/teste2e/server_test.go +++ /dev/null @@ -1,519 +0,0 @@ -//go:build enable_e2e_tests - -package e2etests - -import ( - "crypto/tls" - "fmt" - "os" - "os/exec" - "path/filepath" - "strconv" - "sync" - "testing" - "time" - - "github.com/pion/rtp" - "github.com/stretchr/testify/require" - - "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/format" -) - -var serverCert = []byte(`-----BEGIN CERTIFICATE----- -MIIDkzCCAnugAwIBAgIUHFnymlrkEnz3ThpFvSrqybBepn4wDQYJKoZIhvcNAQEL -BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM -GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X -DTIxMTIwMzIxNDg0MFoXDTMxMTIwMTIxNDg0MFowWTELMAkGA1UEBhMCQVUxEzAR -BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5 -IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAv8h21YDIAYNzewrfQqQTlODJjuUZKxMCO7z1wIapem5I+1I8n+vD -v8qvuyZk1m9CKQPfXxhJz0TT5kECoUY0KaDtykSzfaUK34F9J1d5snDkaOtN48W+ -8l39Wtcvc5JW17jNwabppAkHHYAMQryO8urKLWKbZmLhYCJdYgNqb8ciWPsnYNA0 -zcnKML9zQphh7dxPq1wCsy/c/XZUzxTLAe8hsCKuqpESEX3MMJA9gOLmiOF0JgpT -9h6eqvJU8IK0QMIv3tekJWSBvTLyz4ghENs10sMKKNqR6NWt2SsOloeBkOhIDLOk -byLaPEvugrQsga99uhANRpXp+CHnVeAH8QIDAQABo1MwUTAdBgNVHQ4EFgQUwyEH -cMynEoy1/TnbIhgpEAs038gwHwYDVR0jBBgwFoAUwyEHcMynEoy1/TnbIhgpEAs0 -38gwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAiV56KhDoUVzW -qV1X0QbfLaifimsN3Na3lUgmjcgyUe8rHj09pXuAD/AcQw/zwKzZ6dPtizBeNLN8 -jV1dbJmR7DE3MDlndgMKTOKFsqzHjG9UTXkBGFUEM1shn2GE8XcvDF0AzKU82YjP -B0KswA1NoYTNP2PW4IhZRzv2M+fnmkvc8DSEZ+dxEMg3aJfe/WLPvYjDpFXLvuxl -YnerRQ04hFysh5eogPFpB4KyyPs6jGnQFmZCbFyk9pjKRbDPJc6FkDglkzTB6j3Q -TSfgNJswOiap13vQQKf5Vu7LTuyO4Wjfjr74QNqMLLNIgcC7n2jfQj1g5Xa0bnF5 -G4tLrCLUUw== ------END CERTIFICATE----- -`) - -var serverKey = []byte(`-----BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC/yHbVgMgBg3N7 -Ct9CpBOU4MmO5RkrEwI7vPXAhql6bkj7Ujyf68O/yq+7JmTWb0IpA99fGEnPRNPm -QQKhRjQpoO3KRLN9pQrfgX0nV3mycORo603jxb7yXf1a1y9zklbXuM3BpumkCQcd -gAxCvI7y6sotYptmYuFgIl1iA2pvxyJY+ydg0DTNycowv3NCmGHt3E+rXAKzL9z9 -dlTPFMsB7yGwIq6qkRIRfcwwkD2A4uaI4XQmClP2Hp6q8lTwgrRAwi/e16QlZIG9 -MvLPiCEQ2zXSwwoo2pHo1a3ZKw6Wh4GQ6EgMs6RvIto8S+6CtCyBr326EA1Glen4 -IedV4AfxAgMBAAECggEAOqcJSNSA1o2oJKo3i374iiCRJAWGw/ilRzXMBtxoOow9 -/7av2czV6fMH+XmNf1M5bafEiaW49Q28rH+XWVFKJK0V7DVEm5l9EMveRcjn7B3A -jSHhiVZxxlfeYwjKd1L7AjB/pMjyTXuBVJFTrplSMpKB0I2GrzJwcOExpAcdZx98 -K0s5pauJH9bE0kI3p585SGQaIjrz0LvAmf6cQ5HhKfahJdWNnKZ/S4Kdqe+JCgyd -NawREHhf3tU01Cd3DOgXn4+5V/Ts6XtqY1RuSvonNv3nyeiOpX8C4cHKD5u2sNOC -3J4xWrrs0W3e8IATgAys56teKbEufHTUx52wNhAbzQKBgQD56W0tPCuaKrsjxsvE -dNHdm/9aQrN1jCJxUcGaxCIioXSyDvpSKcgxQbEqHXRTtJt5/Kadz9omq4vFTVtl -5Gf+3Lrf3ZT82SvYHtlIMdBZLlKwk6MolEa0KGAuJBNJVRIOkm5YjV/3bJebeTIb -WrLEyNCOXFAh3KVzBPU8nJ1aTwKBgQDEdISg3UsSOLBa0BfoJ5FlqGepZSufYgqh -xAJn8EbopnlzfmHBZAhE2+Igh0xcHhQqHThc3OuLtAkWu6fUSLiSA+XjU9TWPpA1 -C/325rhT23fxzYIlYFegR9BToxYhv14ufkcTXRfHRAhffk7K5A2nlJfldDZRmUh2 -5KIjXQ0pvwKBgQCa7S6VgFu3cw4Ym8DuxUzlCTRADGGcWYdwoLJY84YF2fmx+L8N -+ID2qDbgWOooiipocUwJQTWIC4jWg6JJhFNEGCpxZbhbF3aqwFULAHadEq6IcL4R -Bfre7LjTYeHi8C4FgpmNo/b+N/+0jmmVs6BnheZkmq3CkDqxFz3AmYai2QKBgQC1 -kzAmcoJ5U/YD6YO/Khsjx3QQSBb6mCZVf5HtuVIApCVqzuvRUACojEbDY+n61j4y -8pDum64FkKA557Xl6lTVeE7ZPtlgL7EfpnbT5kmGEDobPqPEofg7h0SQmRLSnEqT -VFmjFw7sOQA4Ksjuk7vfIOMHy9KMts0YPpdxcgbBhwKBgQCP8MeRPuhZ26/oIESr -I8ArLEaPebYmLXCT2ZTudGztoyYFxinRGHA4PdamSOKfB1li52wAaqgRA3cSqkUi -kabimVOvrOAWlnvznqXEHPNx6mbbKs08jh+uRRmrOmMrxAobpTqarL2Sdxb6afID -NkxNic7oHgsZpIkZ8HK+QjAAWA== ------END PRIVATE KEY----- -`) - -type testServerHandler struct { - onConnOpen func(*gortsplib.ServerHandlerOnConnOpenCtx) - onConnClose func(*gortsplib.ServerHandlerOnConnCloseCtx) - onSessionOpen func(*gortsplib.ServerHandlerOnSessionOpenCtx) - onSessionClose func(*gortsplib.ServerHandlerOnSessionCloseCtx) - onDescribe func(*gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) - onAnnounce func(*gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) - onSetup func(*gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) - onPlay func(*gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) - onRecord func(*gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) - onPause func(*gortsplib.ServerHandlerOnPauseCtx) (*base.Response, error) - onSetParameter func(*gortsplib.ServerHandlerOnSetParameterCtx) (*base.Response, error) - onGetParameter func(*gortsplib.ServerHandlerOnGetParameterCtx) (*base.Response, error) -} - -func (sh *testServerHandler) OnConnOpen(ctx *gortsplib.ServerHandlerOnConnOpenCtx) { - if sh.onConnOpen != nil { - sh.onConnOpen(ctx) - } -} - -func (sh *testServerHandler) OnConnClose(ctx *gortsplib.ServerHandlerOnConnCloseCtx) { - if sh.onConnClose != nil { - sh.onConnClose(ctx) - } -} - -func (sh *testServerHandler) OnSessionOpen(ctx *gortsplib.ServerHandlerOnSessionOpenCtx) { - if sh.onSessionOpen != nil { - sh.onSessionOpen(ctx) - } -} - -func (sh *testServerHandler) OnSessionClose(ctx *gortsplib.ServerHandlerOnSessionCloseCtx) { - if sh.onSessionClose != nil { - sh.onSessionClose(ctx) - } -} - -func (sh *testServerHandler) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) { - if sh.onDescribe != nil { - return sh.onDescribe(ctx) - } - return nil, nil, fmt.Errorf("unimplemented") -} - -func (sh *testServerHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) { - if sh.onAnnounce != nil { - return sh.onAnnounce(ctx) - } - return nil, fmt.Errorf("unimplemented") -} - -func (sh *testServerHandler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) { - if sh.onSetup != nil { - return sh.onSetup(ctx) - } - return nil, nil, fmt.Errorf("unimplemented") -} - -func (sh *testServerHandler) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { - if sh.onPlay != nil { - return sh.onPlay(ctx) - } - return nil, fmt.Errorf("unimplemented") -} - -func (sh *testServerHandler) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) { - if sh.onRecord != nil { - return sh.onRecord(ctx) - } - return nil, fmt.Errorf("unimplemented") -} - -func (sh *testServerHandler) OnPause(ctx *gortsplib.ServerHandlerOnPauseCtx) (*base.Response, error) { - if sh.onPause != nil { - return sh.onPause(ctx) - } - return nil, fmt.Errorf("unimplemented") -} - -func (sh *testServerHandler) OnSetParameter(ctx *gortsplib.ServerHandlerOnSetParameterCtx) (*base.Response, error) { - if sh.onSetParameter != nil { - return sh.onSetParameter(ctx) - } - return nil, fmt.Errorf("unimplemented") -} - -func (sh *testServerHandler) OnGetParameter(ctx *gortsplib.ServerHandlerOnGetParameterCtx) (*base.Response, error) { - if sh.onGetParameter != nil { - return sh.onGetParameter(ctx) - } - return nil, fmt.Errorf("unimplemented") -} - -type container struct { - name string -} - -func newContainer(image string, name string, args []string) (*container, error) { - c := &container{ - name: name, - } - - exec.Command("docker", "kill", "gortsplib-test-"+name).Run() - exec.Command("docker", "wait", "gortsplib-test-"+name).Run() - - cmd := []string{ - "docker", "run", - "--network=host", - "--name=gortsplib-test-" + name, - "gortsplib-test-" + image, - } - cmd = append(cmd, args...) - ecmd := exec.Command(cmd[0], cmd[1:]...) - ecmd.Stdout = nil - ecmd.Stderr = os.Stderr - - err := ecmd.Start() - if err != nil { - return nil, err - } - - time.Sleep(1 * time.Second) - - return c, nil -} - -func (c *container) close() { - exec.Command("docker", "kill", "gortsplib-test-"+c.name).Run() - exec.Command("docker", "wait", "gortsplib-test-"+c.name).Run() - exec.Command("docker", "rm", "gortsplib-test-"+c.name).Run() -} - -func (c *container) wait() int { - exec.Command("docker", "wait", "gortsplib-test-"+c.name).Run() - out, _ := exec.Command("docker", "inspect", "gortsplib-test-"+c.name, - "--format={{.State.ExitCode}}").Output() - code, _ := strconv.ParseInt(string(out[:len(out)-1]), 10, 32) - return int(code) -} - -func buildImage(image string) error { - ecmd := exec.Command("docker", "build", filepath.Join("images", image), - "-t", "gortsplib-test-"+image) - ecmd.Stdout = nil - ecmd.Stderr = os.Stderr - return ecmd.Run() -} - -func TestServerRecordRead(t *testing.T) { - files, err := os.ReadDir("images") - require.NoError(t, err) - - for _, file := range files { - err := buildImage(file.Name()) - require.NoError(t, err) - } - - for _, ca := range []struct { - publisherSoft string - publisherProto string - readerSoft string - readerProto string - }{ - {"ffmpeg", "udp", "ffmpeg", "udp"}, - {"ffmpeg", "udp", "gstreamer", "udp"}, - {"gstreamer", "udp", "ffmpeg", "udp"}, - {"gstreamer", "udp", "gstreamer", "udp"}, - - {"ffmpeg", "tcp", "ffmpeg", "tcp"}, - {"ffmpeg", "tcp", "gstreamer", "tcp"}, - {"gstreamer", "tcp", "ffmpeg", "tcp"}, - {"gstreamer", "tcp", "gstreamer", "tcp"}, - - {"ffmpeg", "tcp", "ffmpeg", "udp"}, - {"ffmpeg", "udp", "ffmpeg", "tcp"}, - - {"ffmpeg", "tls", "ffmpeg", "tls"}, - {"ffmpeg", "tls", "gstreamer", "tls"}, - {"gstreamer", "tls", "ffmpeg", "tls"}, - {"gstreamer", "tls", "gstreamer", "tls"}, - - {"ffmpeg", "udp", "ffmpeg", "multicast"}, - {"ffmpeg", "udp", "gstreamer", "multicast"}, - } { - t.Run(ca.publisherSoft+"_"+ca.publisherProto+"_"+ - ca.readerSoft+"_"+ca.readerProto, func(t *testing.T) { - var mutex sync.Mutex - var stream *gortsplib.ServerStream - var publisher *gortsplib.ServerSession - - var s *gortsplib.Server - s = &gortsplib.Server{ - Handler: &testServerHandler{ - onSessionClose: func(ctx *gortsplib.ServerHandlerOnSessionCloseCtx) { - mutex.Lock() - defer mutex.Unlock() - - if stream != nil { - if ctx.Session == publisher { - stream.Close() - stream = nil - } - } - }, - onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) { - if ctx.Path != "/test/stream" { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, nil, fmt.Errorf("invalid path (%s)", ctx.Request.URL) - } - if ctx.Query != "key=val" { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, nil, fmt.Errorf("invalid query (%s)", ctx.Query) - } - - mutex.Lock() - defer mutex.Unlock() - - if stream == nil { - return &base.Response{ - StatusCode: base.StatusNotFound, - }, nil, nil - } - - return &base.Response{ - StatusCode: base.StatusOK, - }, stream, nil - }, - onAnnounce: func(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) { - if ctx.Path != "/test/stream" { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, fmt.Errorf("invalid path (%s)", ctx.Request.URL) - } - if ctx.Query != "key=val" { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, fmt.Errorf("invalid query (%s)", ctx.Query) - } - - mutex.Lock() - defer mutex.Unlock() - - if stream != nil { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, fmt.Errorf("someone is already publishing") - } - - stream = &gortsplib.ServerStream{ - Server: s, - Desc: ctx.Description, - } - err := stream.Initialize() - require.NoError(t, err) - publisher = ctx.Session - - return &base.Response{ - StatusCode: base.StatusOK, - }, nil - }, - onSetup: func(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) { - if ctx.Path != "/test/stream" { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, nil, fmt.Errorf("invalid path (%s)", ctx.Request.URL) - } - if ctx.Query != "key=val" { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, nil, fmt.Errorf("invalid query (%s)", ctx.Query) - } - - if ctx.Session.State() == gortsplib.ServerSessionStatePreRecord { - return &base.Response{ - StatusCode: base.StatusOK, - }, nil, nil - } - - if stream == nil { - return &base.Response{ - StatusCode: base.StatusNotFound, - }, nil, nil - } - - return &base.Response{ - StatusCode: base.StatusOK, - }, stream, nil - }, - onPlay: func(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { - if ctx.Path != "/test/stream" { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, fmt.Errorf("invalid path (%s)", ctx.Request.URL) - } - if ctx.Query != "key=val" { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, fmt.Errorf("invalid query (%s)", ctx.Query) - } - - return &base.Response{ - StatusCode: base.StatusOK, - }, nil - }, - onRecord: func(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) { - if ctx.Path != "/test/stream" { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, fmt.Errorf("invalid path (%s)", ctx.Request.URL) - } - if ctx.Query != "key=val" { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, fmt.Errorf("invalid query (%s)", ctx.Query) - } - - ctx.Session.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) { - stream.WritePacketRTP(medi, pkt) - }) - - return &base.Response{ - StatusCode: base.StatusOK, - }, nil - }, - }, - RTSPAddress: "localhost:8554", - } - - var proto string - if ca.publisherProto == "tls" { - proto = "rtsps" - cert, err := tls.X509KeyPair(serverCert, serverKey) - require.NoError(t, err) - s.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}} - } else { - proto = "rtsp" - s.UDPRTPAddress = "127.0.0.1:8000" - s.UDPRTCPAddress = "127.0.0.1:8001" - s.MulticastIPRange = "224.1.0.0/16" - s.MulticastRTPPort = 8002 - s.MulticastRTCPPort = 8003 - } - - err := s.Start() - require.NoError(t, err) - defer s.Close() - - switch ca.publisherSoft { - case "ffmpeg": - ts := func() string { - switch ca.publisherProto { - case "udp", "tcp": - return ca.publisherProto - } - return "tcp" - }() - - cnt1, err := newContainer("ffmpeg", "publish", []string{ - "-re", - "-stream_loop", "-1", - "-i", "emptyvideo.mkv", - "-c", "copy", - "-f", "rtsp", - "-rtsp_transport", ts, - proto + "://localhost:8554/test/stream?key=val", - }) - require.NoError(t, err) - defer cnt1.close() - - case "gstreamer": - ts := func() string { - switch ca.publisherProto { - case "udp", "tcp": - return ca.publisherProto - } - return "tcp" - }() - - cnt1, err := newContainer("gstreamer", "publish", []string{ - "filesrc location=emptyvideo.mkv ! matroskademux ! video/x-h264 ! rtspclientsink " + - "location=" + proto + "://127.0.0.1:8554/test/stream?key=val protocols=" + ts + - " tls-validation-flags=0 latency=0 timeout=0 rtx-time=0", - }) - require.NoError(t, err) - defer cnt1.close() - - time.Sleep(1 * time.Second) - } - - time.Sleep(1 * time.Second) - - switch ca.readerSoft { - case "ffmpeg": - ts := func() string { - switch ca.readerProto { - case "udp", "tcp": - return ca.readerProto - case "multicast": - return "udp_multicast" - } - return "tcp" - }() - - cnt2, err := newContainer("ffmpeg", "read", []string{ - "-rtsp_transport", ts, - "-i", proto + "://localhost:8554/test/stream?key=val", - "-vframes", "1", - "-f", "image2", - "-y", "/dev/null", - }) - require.NoError(t, err) - defer cnt2.close() - require.Equal(t, 0, cnt2.wait()) - - case "gstreamer": - ts := func() string { - switch ca.readerProto { - case "udp", "tcp": - return ca.readerProto - case "multicast": - return "udp-mcast" - } - return "tcp" - }() - - cnt2, err := newContainer("gstreamer", "read", []string{ - "rtspsrc location=" + proto + "://127.0.0.1:8554/test/stream?key=val protocols=" + ts + - " tls-validation-flags=0 latency=0 " + - "! application/x-rtp,media=video ! decodebin ! exitafterframe ! fakesink", - }) - require.NoError(t, err) - defer cnt2.close() - require.Equal(t, 0, cnt2.wait()) - } - }) - } -} diff --git a/internal/teste2e/server_vs_external_test.go b/internal/teste2e/server_vs_external_test.go new file mode 100644 index 00000000..3ccb9cad --- /dev/null +++ b/internal/teste2e/server_vs_external_test.go @@ -0,0 +1,343 @@ +//go:build enable_e2e_tests + +package teste2e + +import ( + "crypto/tls" + "os" + "os/exec" + "path/filepath" + "strconv" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +type container struct { + name string +} + +func newContainer(image string, name string, args []string) (*container, error) { + c := &container{ + name: name, + } + + exec.Command("docker", "kill", "gortsplib-test-"+name).Run() + exec.Command("docker", "wait", "gortsplib-test-"+name).Run() + + cmd := []string{ + "docker", "run", + "--network=host", + "--name=gortsplib-test-" + name, + "gortsplib-test-" + image, + } + cmd = append(cmd, args...) + ecmd := exec.Command(cmd[0], cmd[1:]...) + ecmd.Stdout = nil + ecmd.Stderr = os.Stderr + + err := ecmd.Start() + if err != nil { + return nil, err + } + + time.Sleep(1 * time.Second) + + return c, nil +} + +func (c *container) close() { + exec.Command("docker", "kill", "gortsplib-test-"+c.name).Run() + exec.Command("docker", "wait", "gortsplib-test-"+c.name).Run() + exec.Command("docker", "rm", "gortsplib-test-"+c.name).Run() +} + +func (c *container) wait() int { + exec.Command("docker", "wait", "gortsplib-test-"+c.name).Run() + out, _ := exec.Command("docker", "inspect", "gortsplib-test-"+c.name, + "--format={{.State.ExitCode}}").Output() + code, _ := strconv.ParseInt(string(out[:len(out)-1]), 10, 32) + return int(code) +} + +func buildImage(image string) error { + ecmd := exec.Command("docker", "build", filepath.Join("images", image), + "-t", "gortsplib-test-"+image) + ecmd.Stdout = nil + ecmd.Stderr = os.Stderr + return ecmd.Run() +} + +func TestServerVsExternal(t *testing.T) { + files, err := os.ReadDir("images") + require.NoError(t, err) + + var wg sync.WaitGroup + + for _, file := range files { + cfile := file + wg.Add(1) + go func() { + err := buildImage(cfile.Name()) + require.NoError(t, err) + wg.Done() + }() + } + + wg.Wait() + + for _, ca := range []struct { + publisherSoft string + publisherScheme string + publisherProto string + publisherSecure string + readerSoft string + readerScheme string + readerProto string + readerSecure string + }{ + { + publisherSoft: "ffmpeg", + publisherScheme: "rtsp", + publisherProto: "udp", + publisherSecure: "unsecure", + readerSoft: "ffmpeg", + readerScheme: "rtsp", + readerProto: "udp", + readerSecure: "unsecure", + }, + { + publisherSoft: "ffmpeg", + publisherScheme: "rtsp", + publisherProto: "udp", + publisherSecure: "unsecure", + readerSoft: "gstreamer", + readerScheme: "rtsp", + readerProto: "udp", + readerSecure: "unsecure", + }, + { + publisherSoft: "gstreamer", + publisherScheme: "rtsp", + publisherProto: "udp", + publisherSecure: "unsecure", + readerSoft: "ffmpeg", + readerScheme: "rtsp", + readerProto: "udp", + readerSecure: "unsecure", + }, + { + publisherSoft: "gstreamer", + publisherScheme: "rtsp", + publisherProto: "udp", + publisherSecure: "unsecure", + readerSoft: "gstreamer", + readerScheme: "rtsp", + readerProto: "udp", + readerSecure: "unsecure", + }, + { + publisherSoft: "ffmpeg", + publisherScheme: "rtsp", + publisherProto: "udp", + publisherSecure: "unsecure", + readerSoft: "ffmpeg", + readerScheme: "rtsp", + readerProto: "multicast", + readerSecure: "unsecure", + }, + { + publisherSoft: "ffmpeg", + publisherScheme: "rtsp", + publisherProto: "udp", + publisherSecure: "unsecure", + readerSoft: "gstreamer", + readerScheme: "rtsp", + readerProto: "multicast", + readerSecure: "unsecure", + }, + { + publisherSoft: "ffmpeg", + publisherScheme: "rtsp", + publisherProto: "tcp", + publisherSecure: "unsecure", + readerSoft: "ffmpeg", + readerScheme: "rtsp", + readerProto: "tcp", + readerSecure: "unsecure", + }, + { + publisherSoft: "ffmpeg", + publisherScheme: "rtsp", + publisherProto: "tcp", + publisherSecure: "unsecure", + readerSoft: "gstreamer", + readerScheme: "rtsp", + readerProto: "tcp", + readerSecure: "unsecure", + }, + { + publisherSoft: "gstreamer", + publisherScheme: "rtsp", + publisherProto: "tcp", + publisherSecure: "unsecure", + readerSoft: "ffmpeg", + readerScheme: "rtsp", + readerProto: "tcp", + readerSecure: "unsecure", + }, + { + publisherSoft: "gstreamer", + publisherScheme: "rtsp", + publisherProto: "tcp", + publisherSecure: "unsecure", + readerSoft: "gstreamer", + readerScheme: "rtsp", + readerProto: "tcp", + readerSecure: "unsecure", + }, + { + publisherSoft: "ffmpeg", + publisherScheme: "rtsp", + publisherProto: "tcp", + publisherSecure: "unsecure", + readerSoft: "ffmpeg", + readerScheme: "rtsp", + readerProto: "udp", + readerSecure: "unsecure", + }, + { + publisherSoft: "ffmpeg", + publisherScheme: "rtsp", + publisherProto: "udp", + publisherSecure: "unsecure", + readerSoft: "ffmpeg", + readerScheme: "rtsp", + readerProto: "tcp", + readerSecure: "unsecure", + }, + { + publisherSoft: "ffmpeg", + publisherScheme: "rtsps", + publisherProto: "tcp", + publisherSecure: "unsecure", + readerSoft: "ffmpeg", + readerScheme: "rtsps", + readerProto: "tcp", + readerSecure: "unsecure", + }, + { + publisherSoft: "ffmpeg", + publisherScheme: "rtsps", + publisherProto: "tcp", + publisherSecure: "unsecure", + readerSoft: "gstreamer", + readerScheme: "rtsps", + readerProto: "tcp", + readerSecure: "unsecure", + }, + { + publisherSoft: "gstreamer", + publisherScheme: "rtsps", + publisherProto: "tcp", + publisherSecure: "unsecure", + readerSoft: "ffmpeg", + readerScheme: "rtsps", + readerProto: "tcp", + readerSecure: "unsecure", + }, + } { + t.Run(ca.publisherSoft+"_"+ca.publisherScheme+"_"+ca.publisherProto+"_"+ca.publisherSecure+"_"+ + ca.readerSoft+"_"+ca.readerScheme+"_"+ca.readerProto+"_"+ca.readerSecure, func(t *testing.T) { + ss := &sampleServer{} + + if ca.publisherScheme == "rtsps" { + cert, err := tls.X509KeyPair(serverCert, serverKey) + require.NoError(t, err) + ss.tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}} + } + + err := ss.initialize() + require.NoError(t, err) + defer ss.close() + + switch ca.publisherSoft { + case "ffmpeg": + cnt1, err := newContainer("ffmpeg", "publish", []string{ + "-re", + "-stream_loop", "-1", + "-i", "emptyvideo.mkv", + "-c", "copy", + "-f", "rtsp", + "-rtsp_transport", ca.publisherProto, + ca.publisherScheme + "://localhost:8554/test/stream?key=val", + }) + require.NoError(t, err) + defer cnt1.close() + + case "gstreamer": + var profile string + if ca.publisherSecure == "secure" { + profile = "GST_RTSP_PROFILE_SAVP" + } else { + profile = "GST_RTSP_PROFILE_AVP" + } + + cnt1, err := newContainer("gstreamer", "publish", []string{ + "filesrc location=emptyvideo.mkv ! matroskademux ! video/x-h264 ! rtspclientsink " + + "location=" + ca.publisherScheme + "://127.0.0.1:8554/test/stream?key=val" + + " protocols=" + ca.publisherProto + + " profiles=" + profile + + " tls-validation-flags=0 latency=0 timeout=0 rtx-time=0", + }) + require.NoError(t, err) + defer cnt1.close() + + time.Sleep(1 * time.Second) + } + + time.Sleep(1 * time.Second) + + switch ca.readerSoft { + case "ffmpeg": + var proto string + if ca.readerProto == "multicast" { + proto = "udp_multicast" + } else { + proto = ca.readerProto + } + + cnt2, err := newContainer("ffmpeg", "read", []string{ + "-rtsp_transport", proto, + "-i", ca.readerScheme + "://localhost:8554/test/stream?key=val", + "-vframes", "1", + "-f", "image2", + "-y", "/dev/null", + }) + require.NoError(t, err) + defer cnt2.close() + require.Equal(t, 0, cnt2.wait()) + + case "gstreamer": + var proto string + if ca.readerProto == "multicast" { + proto = "udp-mcast" + } else { + proto = ca.readerProto + } + + cnt2, err := newContainer("gstreamer", "read", []string{ + "rtspsrc location=" + ca.readerScheme + "://127.0.0.1:8554/test/stream?key=val" + + " protocols=" + proto + + " tls-validation-flags=0 latency=0 " + + "! application/x-rtp,media=video ! decodebin ! video/x-raw ! fakesink num-buffers=1", + }) + require.NoError(t, err) + defer cnt2.close() + require.Equal(t, 0, cnt2.wait()) + } + }) + } +}