2023-05-16 13:59:37 +00:00
|
|
|
package core
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2023-08-03 21:12:05 +00:00
|
|
|
"context"
|
2023-05-16 13:59:37 +00:00
|
|
|
"net/http"
|
2023-10-24 14:30:44 +00:00
|
|
|
"net/url"
|
2023-05-16 13:59:37 +00:00
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
2023-08-26 16:54:28 +00:00
|
|
|
"github.com/bluenviron/gortsplib/v4"
|
2023-11-12 23:09:42 +00:00
|
|
|
"github.com/bluenviron/gortsplib/v4/pkg/base"
|
2023-08-26 16:54:28 +00:00
|
|
|
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
|
|
|
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
2023-05-16 13:59:37 +00:00
|
|
|
"github.com/pion/rtp"
|
2023-10-25 09:48:57 +00:00
|
|
|
pwebrtc "github.com/pion/webrtc/v3"
|
2023-05-16 13:59:37 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
2023-10-26 19:46:18 +00:00
|
|
|
"github.com/bluenviron/mediamtx/internal/protocols/webrtc"
|
2023-08-03 21:12:05 +00:00
|
|
|
)
|
2023-05-16 13:59:37 +00:00
|
|
|
|
2023-10-28 12:08:34 +00:00
|
|
|
func TestWebRTCPages(t *testing.T) {
|
|
|
|
p, ok := newInstance("paths:\n" +
|
|
|
|
" stream:\n")
|
|
|
|
require.Equal(t, true, ok)
|
|
|
|
defer p.Close()
|
|
|
|
|
|
|
|
hc := &http.Client{Transport: &http.Transport{}}
|
|
|
|
|
|
|
|
for _, path := range []string{"/stream", "/stream/publish"} {
|
|
|
|
func() {
|
|
|
|
req, err := http.NewRequest(http.MethodGet, "http://localhost:8889"+path, nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
res, err := hc.Do(req)
|
|
|
|
require.NoError(t, err)
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
require.Equal(t, http.StatusOK, res.StatusCode)
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-16 13:59:37 +00:00
|
|
|
func TestWebRTCRead(t *testing.T) {
|
2023-06-21 11:53:58 +00:00
|
|
|
for _, auth := range []string{
|
|
|
|
"none",
|
|
|
|
"internal",
|
|
|
|
"external",
|
|
|
|
} {
|
|
|
|
t.Run("auth_"+auth, func(t *testing.T) {
|
|
|
|
var conf string
|
|
|
|
|
|
|
|
switch auth {
|
|
|
|
case "none":
|
|
|
|
conf = "paths:\n" +
|
2023-10-09 16:13:44 +00:00
|
|
|
" all_others:\n"
|
2023-06-21 11:53:58 +00:00
|
|
|
|
|
|
|
case "internal":
|
|
|
|
conf = "paths:\n" +
|
2023-10-09 16:13:44 +00:00
|
|
|
" all_others:\n" +
|
2023-06-21 11:53:58 +00:00
|
|
|
" readUser: myuser\n" +
|
|
|
|
" readPass: mypass\n"
|
|
|
|
|
|
|
|
case "external":
|
|
|
|
conf = "externalAuthenticationURL: http://localhost:9120/auth\n" +
|
|
|
|
"paths:\n" +
|
2023-10-09 16:13:44 +00:00
|
|
|
" all_others:\n"
|
2023-06-21 11:53:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
p, ok := newInstance(conf)
|
|
|
|
require.Equal(t, true, ok)
|
|
|
|
defer p.Close()
|
|
|
|
|
|
|
|
var a *testHTTPAuthenticator
|
|
|
|
if auth == "external" {
|
|
|
|
a = newTestHTTPAuthenticator(t, "rtsp", "publish")
|
|
|
|
}
|
|
|
|
|
2023-08-26 16:54:28 +00:00
|
|
|
medi := &description.Media{
|
|
|
|
Type: description.MediaTypeVideo,
|
|
|
|
Formats: []format.Format{&format.H264{
|
2023-06-21 11:53:58 +00:00
|
|
|
PayloadTyp: 96,
|
|
|
|
PacketizationMode: 1,
|
|
|
|
}},
|
|
|
|
}
|
|
|
|
|
|
|
|
v := gortsplib.TransportTCP
|
|
|
|
source := gortsplib.Client{
|
|
|
|
Transport: &v,
|
|
|
|
}
|
|
|
|
err := source.StartRecording(
|
2023-08-26 16:54:28 +00:00
|
|
|
"rtsp://testpublisher:testpass@localhost:8554/teststream?param=value",
|
|
|
|
&description.Session{Medias: []*description.Media{medi}})
|
2023-06-21 11:53:58 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
defer source.Close()
|
|
|
|
|
|
|
|
if auth == "external" {
|
|
|
|
a.close()
|
|
|
|
a = newTestHTTPAuthenticator(t, "webrtc", "read")
|
|
|
|
defer a.close()
|
|
|
|
}
|
|
|
|
|
|
|
|
hc := &http.Client{Transport: &http.Transport{}}
|
|
|
|
|
|
|
|
user := ""
|
|
|
|
pass := ""
|
|
|
|
|
|
|
|
switch auth {
|
|
|
|
case "internal":
|
|
|
|
user = "myuser"
|
|
|
|
pass = "mypass"
|
|
|
|
|
|
|
|
case "external":
|
|
|
|
user = "testreader"
|
|
|
|
pass = "testpass"
|
|
|
|
}
|
|
|
|
|
|
|
|
ur := "http://"
|
|
|
|
if user != "" {
|
|
|
|
ur += user + ":" + pass + "@"
|
|
|
|
}
|
|
|
|
ur += "localhost:8889/teststream/whep?param=value"
|
|
|
|
|
2023-10-25 09:48:57 +00:00
|
|
|
go func() {
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
|
|
|
|
err := source.WritePacketRTP(medi, &rtp.Packet{
|
|
|
|
Header: rtp.Header{
|
|
|
|
Version: 2,
|
|
|
|
Marker: true,
|
|
|
|
PayloadType: 96,
|
|
|
|
SequenceNumber: 123,
|
|
|
|
Timestamp: 45343,
|
|
|
|
SSRC: 563423,
|
|
|
|
},
|
|
|
|
Payload: []byte{5, 3},
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
}()
|
2023-06-21 11:53:58 +00:00
|
|
|
|
2023-10-25 09:48:57 +00:00
|
|
|
u, err := url.Parse(ur)
|
2023-08-13 14:38:23 +00:00
|
|
|
require.NoError(t, err)
|
2023-06-21 11:53:58 +00:00
|
|
|
|
2023-10-25 09:48:57 +00:00
|
|
|
c := &webrtc.WHIPClient{
|
|
|
|
HTTPClient: hc,
|
|
|
|
URL: u,
|
|
|
|
}
|
|
|
|
|
|
|
|
tracks, err := c.Read(context.Background())
|
|
|
|
require.NoError(t, err)
|
|
|
|
defer checkClose(t, c.Close)
|
2023-06-21 11:53:58 +00:00
|
|
|
|
2023-10-25 09:48:57 +00:00
|
|
|
pkt, err := tracks[0].ReadRTP()
|
2023-06-21 11:53:58 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, &rtp.Packet{
|
|
|
|
Header: rtp.Header{
|
|
|
|
Version: 2,
|
|
|
|
Marker: true,
|
2023-08-03 21:12:05 +00:00
|
|
|
PayloadType: 100,
|
2023-06-21 11:53:58 +00:00
|
|
|
SequenceNumber: pkt.SequenceNumber,
|
|
|
|
Timestamp: pkt.Timestamp,
|
|
|
|
SSRC: pkt.SSRC,
|
|
|
|
CSRC: []uint32{},
|
|
|
|
},
|
2023-08-26 16:54:28 +00:00
|
|
|
Payload: []byte{5, 3},
|
2023-06-21 11:53:58 +00:00
|
|
|
}, pkt)
|
|
|
|
})
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-16 16:01:05 +00:00
|
|
|
func TestWebRTCReadNotFound(t *testing.T) {
|
|
|
|
p, ok := newInstance("paths:\n" +
|
2023-10-09 16:13:44 +00:00
|
|
|
" all_others:\n")
|
2023-05-16 16:01:05 +00:00
|
|
|
require.Equal(t, true, ok)
|
|
|
|
defer p.Close()
|
|
|
|
|
2023-05-18 17:16:07 +00:00
|
|
|
hc := &http.Client{Transport: &http.Transport{}}
|
|
|
|
|
2023-10-25 09:48:57 +00:00
|
|
|
iceServers, err := webrtc.WHIPOptionsICEServers(context.Background(), hc, "http://localhost:8889/stream/whep")
|
2023-08-03 21:12:05 +00:00
|
|
|
require.NoError(t, err)
|
2023-05-16 16:01:05 +00:00
|
|
|
|
2023-10-25 09:48:57 +00:00
|
|
|
pc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{
|
2023-05-16 16:01:05 +00:00
|
|
|
ICEServers: iceServers,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
2023-08-13 14:38:23 +00:00
|
|
|
defer pc.Close() //nolint:errcheck
|
2023-05-16 16:01:05 +00:00
|
|
|
|
2023-10-25 09:48:57 +00:00
|
|
|
_, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo)
|
2023-05-16 16:01:05 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
offer, err := pc.CreateOffer(nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2023-10-28 12:08:34 +00:00
|
|
|
req, err := http.NewRequest(http.MethodPost, "http://localhost:8889/stream/whep", bytes.NewReader([]byte(offer.SDP)))
|
2023-05-16 16:01:05 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/sdp")
|
|
|
|
|
2023-05-18 17:16:07 +00:00
|
|
|
res, err := hc.Do(req)
|
2023-05-16 16:01:05 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
|
|
|
}
|
|
|
|
|
2023-05-16 13:59:37 +00:00
|
|
|
func TestWebRTCPublish(t *testing.T) {
|
2023-06-21 11:53:58 +00:00
|
|
|
for _, auth := range []string{
|
|
|
|
"none",
|
|
|
|
"internal",
|
|
|
|
"external",
|
|
|
|
} {
|
|
|
|
t.Run("auth_"+auth, func(t *testing.T) {
|
|
|
|
var conf string
|
|
|
|
|
|
|
|
switch auth {
|
|
|
|
case "none":
|
|
|
|
conf = "paths:\n" +
|
2023-10-09 16:13:44 +00:00
|
|
|
" all_others:\n"
|
2023-06-21 11:53:58 +00:00
|
|
|
|
|
|
|
case "internal":
|
|
|
|
conf = "paths:\n" +
|
2023-10-09 16:13:44 +00:00
|
|
|
" all_others:\n" +
|
2023-06-21 11:53:58 +00:00
|
|
|
" publishUser: myuser\n" +
|
|
|
|
" publishPass: mypass\n"
|
|
|
|
|
|
|
|
case "external":
|
|
|
|
conf = "externalAuthenticationURL: http://localhost:9120/auth\n" +
|
|
|
|
"paths:\n" +
|
2023-10-09 16:13:44 +00:00
|
|
|
" all_others:\n"
|
2023-06-21 11:53:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
p, ok := newInstance(conf)
|
|
|
|
require.Equal(t, true, ok)
|
|
|
|
defer p.Close()
|
|
|
|
|
|
|
|
var a *testHTTPAuthenticator
|
|
|
|
if auth == "external" {
|
|
|
|
a = newTestHTTPAuthenticator(t, "webrtc", "publish")
|
|
|
|
}
|
|
|
|
|
|
|
|
hc := &http.Client{Transport: &http.Transport{}}
|
|
|
|
|
2023-06-22 10:49:59 +00:00
|
|
|
// preflight requests must always work, without authentication
|
2023-06-21 11:53:58 +00:00
|
|
|
func() {
|
2023-10-28 12:08:34 +00:00
|
|
|
req, err := http.NewRequest(http.MethodOptions, "http://localhost:8889/teststream/whip", nil)
|
2023-06-21 11:53:58 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2023-06-22 10:49:59 +00:00
|
|
|
req.Header.Set("Access-Control-Request-Method", "OPTIONS")
|
|
|
|
|
2023-06-21 11:53:58 +00:00
|
|
|
res, err := hc.Do(req)
|
|
|
|
require.NoError(t, err)
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
2023-08-02 18:52:26 +00:00
|
|
|
require.Equal(t, http.StatusNoContent, res.StatusCode)
|
2023-06-21 11:53:58 +00:00
|
|
|
|
|
|
|
if auth != "none" {
|
|
|
|
_, ok := res.Header["Link"]
|
|
|
|
require.Equal(t, false, ok)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
user := ""
|
|
|
|
pass := ""
|
|
|
|
|
|
|
|
switch auth {
|
|
|
|
case "internal":
|
|
|
|
user = "myuser"
|
|
|
|
pass = "mypass"
|
|
|
|
|
|
|
|
case "external":
|
|
|
|
user = "testpublisher"
|
|
|
|
pass = "testpass"
|
|
|
|
}
|
|
|
|
|
|
|
|
ur := "http://"
|
|
|
|
if user != "" {
|
|
|
|
ur += user + ":" + pass + "@"
|
|
|
|
}
|
|
|
|
ur += "localhost:8889/teststream/whip?param=value"
|
|
|
|
|
2023-10-25 09:48:57 +00:00
|
|
|
su, err := url.Parse(ur)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
s := &webrtc.WHIPClient{
|
|
|
|
HTTPClient: hc,
|
|
|
|
URL: su,
|
|
|
|
}
|
|
|
|
|
|
|
|
tracks, err := s.Publish(context.Background(), testMediaH264.Formats[0], nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
defer checkClose(t, s.Close)
|
|
|
|
|
|
|
|
err = tracks[0].WriteRTP(&rtp.Packet{
|
|
|
|
Header: rtp.Header{
|
|
|
|
Version: 2,
|
|
|
|
Marker: true,
|
|
|
|
PayloadType: 96,
|
|
|
|
SequenceNumber: 123,
|
|
|
|
Timestamp: 45343,
|
|
|
|
SSRC: 563423,
|
|
|
|
},
|
|
|
|
Payload: []byte{1},
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
2023-06-21 11:53:58 +00:00
|
|
|
|
|
|
|
if auth == "external" {
|
|
|
|
a.close()
|
|
|
|
a = newTestHTTPAuthenticator(t, "rtsp", "read")
|
|
|
|
defer a.close()
|
|
|
|
}
|
|
|
|
|
|
|
|
c := gortsplib.Client{
|
|
|
|
OnDecodeError: func(err error) {
|
|
|
|
panic(err)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2023-11-12 23:09:42 +00:00
|
|
|
u, err := base.ParseURL("rtsp://testreader:testpass@127.0.0.1:8554/teststream?param=value")
|
2023-06-21 11:53:58 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
err = c.Start(u.Scheme, u.Host)
|
|
|
|
require.NoError(t, err)
|
|
|
|
defer c.Close()
|
|
|
|
|
2023-08-26 16:54:28 +00:00
|
|
|
desc, _, err := c.Describe(u)
|
2023-06-21 11:53:58 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2023-10-25 09:48:57 +00:00
|
|
|
var forma *format.H264
|
2023-08-26 16:54:28 +00:00
|
|
|
medi := desc.FindFormat(&forma)
|
2023-06-21 11:53:58 +00:00
|
|
|
|
2023-08-26 16:54:28 +00:00
|
|
|
_, err = c.Setup(desc.BaseURL, medi, 0, 0)
|
2023-06-21 11:53:58 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
received := make(chan struct{})
|
|
|
|
|
|
|
|
c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
|
2023-08-03 21:12:05 +00:00
|
|
|
require.Equal(t, []byte{3}, pkt.Payload)
|
2023-06-21 11:53:58 +00:00
|
|
|
close(received)
|
|
|
|
})
|
|
|
|
|
|
|
|
_, err = c.Play(nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2023-10-25 09:48:57 +00:00
|
|
|
err = tracks[0].WriteRTP(&rtp.Packet{
|
2023-06-21 11:53:58 +00:00
|
|
|
Header: rtp.Header{
|
|
|
|
Version: 2,
|
|
|
|
Marker: true,
|
|
|
|
PayloadType: 96,
|
|
|
|
SequenceNumber: 124,
|
|
|
|
Timestamp: 45343,
|
|
|
|
SSRC: 563423,
|
|
|
|
},
|
2023-08-03 21:12:05 +00:00
|
|
|
Payload: []byte{3},
|
2023-06-21 11:53:58 +00:00
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
<-received
|
|
|
|
})
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
}
|