mirror of
https://github.com/bluenviron/mediamtx.git
synced 2025-12-20 10:10:03 -08:00
Some checks are pending
code_lint / golangci_lint (push) Waiting to run
code_lint / mod_tidy (push) Waiting to run
code_lint / api_docs (push) Waiting to run
code_test / test_64 (push) Waiting to run
code_test / test_32 (push) Waiting to run
code_test / test_e2e (push) Waiting to run
some clients require PayloadType to be unique among all tracks, not only among tracks of same kind.
403 lines
10 KiB
Go
403 lines
10 KiB
Go
package webrtc
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/bluenviron/mediamtx/internal/conf"
|
|
"github.com/bluenviron/mediamtx/internal/test"
|
|
"github.com/pion/ice/v4"
|
|
"github.com/pion/logging"
|
|
"github.com/pion/rtp"
|
|
"github.com/pion/sdp/v3"
|
|
"github.com/pion/webrtc/v4"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type nilWriter struct{}
|
|
|
|
func (nilWriter) Write(p []byte) (int, error) {
|
|
return len(p), nil
|
|
}
|
|
|
|
var webrtcNilLogger = logging.NewDefaultLeveledLoggerForScope("", 0, &nilWriter{})
|
|
|
|
func gatherCodecs(tracks []*IncomingTrack) []webrtc.RTPCodecParameters {
|
|
codecs := make([]webrtc.RTPCodecParameters, len(tracks))
|
|
for i, track := range tracks {
|
|
codecs[i] = track.Codec()
|
|
}
|
|
return codecs
|
|
}
|
|
|
|
func TestPeerConnectionCloseImmediately(t *testing.T) {
|
|
pc := &PeerConnection{
|
|
LocalRandomUDP: true,
|
|
IPsFromInterfaces: true,
|
|
HandshakeTimeout: conf.Duration(10 * time.Second),
|
|
TrackGatherTimeout: conf.Duration(2 * time.Second),
|
|
STUNGatherTimeout: conf.Duration(5 * time.Second),
|
|
Publish: false,
|
|
Log: test.NilLogger,
|
|
}
|
|
err := pc.Start()
|
|
require.NoError(t, err)
|
|
defer pc.Close()
|
|
|
|
_, err = pc.CreatePartialOffer()
|
|
require.NoError(t, err)
|
|
|
|
// wait for ICE candidates to be generated
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
pc.Close()
|
|
}
|
|
|
|
func TestPeerConnectionCandidates(t *testing.T) {
|
|
for _, ca := range []string{
|
|
"udp",
|
|
"stun",
|
|
"udp+stun",
|
|
} {
|
|
t.Run(ca, func(t *testing.T) {
|
|
pc := &PeerConnection{
|
|
IPsFromInterfaces: true,
|
|
IPsFromInterfacesList: []string{"lo"},
|
|
HandshakeTimeout: conf.Duration(10 * time.Second),
|
|
TrackGatherTimeout: conf.Duration(2 * time.Second),
|
|
Log: test.NilLogger,
|
|
}
|
|
|
|
if ca == "udp" || ca == "udp+stun" {
|
|
pc.LocalRandomUDP = true
|
|
}
|
|
if ca == "stun" || ca == "udp+stun" {
|
|
pc.ICEServers = []webrtc.ICEServer{{
|
|
URLs: []string{"stun:stun.l.google.com:19302"},
|
|
}}
|
|
}
|
|
|
|
err := pc.Start()
|
|
require.NoError(t, err)
|
|
defer pc.Close()
|
|
|
|
_, err = pc.CreatePartialOffer()
|
|
require.NoError(t, err)
|
|
|
|
// convert partial offer into full offer
|
|
err = pc.waitGatheringDone(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
offer := pc.wr.LocalDescription()
|
|
|
|
if ca == "udp" || ca == "udp+stun" {
|
|
require.Equal(t, 2, strings.Count(offer.SDP, "typ host"))
|
|
}
|
|
if ca == "stun" || ca == "udp+stun" {
|
|
require.Equal(t, 2, strings.Count(offer.SDP, "typ srflx"))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPeerConnectionConnectivity(t *testing.T) {
|
|
for _, mode := range []string{
|
|
"passive udp",
|
|
"passive tcp",
|
|
"active udp",
|
|
"active udp + stun",
|
|
} {
|
|
for _, ip := range []string{
|
|
"from interfaces",
|
|
"additional hosts",
|
|
} {
|
|
// LocalRandomUDP doesn't work with AdditionalHosts
|
|
// we do not care since currently we are not using them together
|
|
if mode == "active udp" && ip == "additional hosts" {
|
|
continue
|
|
}
|
|
|
|
t.Run(mode+"_"+ip, func(t *testing.T) {
|
|
var iceServers []webrtc.ICEServer
|
|
|
|
if mode == "active udp + stun" {
|
|
iceServers = []webrtc.ICEServer{{
|
|
URLs: []string{"stun:stun.l.google.com:19302"},
|
|
}}
|
|
}
|
|
|
|
clientPC := &PeerConnection{
|
|
LocalRandomUDP: (mode == "passive udp" || mode == "active udp"),
|
|
IPsFromInterfaces: true,
|
|
IPsFromInterfacesList: []string{"lo"},
|
|
ICEServers: iceServers,
|
|
HandshakeTimeout: conf.Duration(10 * time.Second),
|
|
TrackGatherTimeout: conf.Duration(2 * time.Second),
|
|
Log: test.NilLogger,
|
|
}
|
|
err := clientPC.Start()
|
|
require.NoError(t, err)
|
|
defer clientPC.Close()
|
|
|
|
var udpMux ice.UDPMux
|
|
var tcpMux ice.TCPMux
|
|
|
|
switch mode {
|
|
case "passive udp":
|
|
var ln net.PacketConn
|
|
ln, err = net.ListenPacket("udp4", ":4458")
|
|
require.NoError(t, err)
|
|
defer ln.Close()
|
|
udpMux = webrtc.NewICEUDPMux(webrtcNilLogger, ln)
|
|
|
|
case "passive tcp":
|
|
var ln net.Listener
|
|
ln, err = net.Listen("tcp4", ":4458")
|
|
require.NoError(t, err)
|
|
defer ln.Close()
|
|
tcpMux = webrtc.NewICETCPMux(webrtcNilLogger, ln, 8)
|
|
}
|
|
|
|
serverPC := &PeerConnection{
|
|
LocalRandomUDP: (mode == "active udp"),
|
|
ICEUDPMux: udpMux,
|
|
ICETCPMux: tcpMux,
|
|
ICEServers: iceServers,
|
|
HandshakeTimeout: conf.Duration(10 * time.Second),
|
|
TrackGatherTimeout: conf.Duration(2 * time.Second),
|
|
Publish: true,
|
|
OutgoingTracks: []*OutgoingTrack{{
|
|
Caps: webrtc.RTPCodecCapability{
|
|
MimeType: webrtc.MimeTypeAV1,
|
|
ClockRate: 90000,
|
|
},
|
|
}},
|
|
Log: test.NilLogger,
|
|
}
|
|
|
|
if ip == "from interfaces" {
|
|
serverPC.IPsFromInterfaces = true
|
|
serverPC.IPsFromInterfacesList = []string{"lo"}
|
|
} else {
|
|
serverPC.AdditionalHosts = []string{"127.0.0.2"}
|
|
}
|
|
|
|
err = serverPC.Start()
|
|
require.NoError(t, err)
|
|
defer serverPC.Close()
|
|
|
|
_, err = clientPC.CreatePartialOffer()
|
|
require.NoError(t, err)
|
|
|
|
// convert partial offer into full offer
|
|
err = clientPC.waitGatheringDone(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
answer, err := serverPC.CreateFullAnswer(context.Background(), clientPC.wr.LocalDescription())
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, strings.Count(answer.SDP, "a=candidate:"))
|
|
|
|
err = clientPC.SetAnswer(answer)
|
|
require.NoError(t, err)
|
|
|
|
err = serverPC.WaitUntilConnected(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
switch mode {
|
|
case "passive udp":
|
|
if ip == "from interfaces" {
|
|
require.Regexp(t, "^host/udp/127\\.0\\.0\\.1/4458$", serverPC.LocalCandidate())
|
|
} else {
|
|
require.Regexp(t, "^host/udp/127\\.0\\.0\\.2/4458$", serverPC.LocalCandidate())
|
|
}
|
|
|
|
case "passive tcp":
|
|
if ip == "from interfaces" {
|
|
require.Regexp(t, "^host/tcp/127\\.0\\.0\\.1/4458$", serverPC.LocalCandidate())
|
|
} else {
|
|
require.Regexp(t, "^host/tcp/127\\.0\\.0\\.2/4458$", serverPC.LocalCandidate())
|
|
}
|
|
|
|
case "active udp":
|
|
require.Regexp(t, "^host/udp/127\\.0\\.0\\.1", serverPC.LocalCandidate())
|
|
|
|
case "active udp + stun":
|
|
require.Regexp(t, "^srflx/udp/", serverPC.LocalCandidate())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPeerConnectionPublishRead(t *testing.T) {
|
|
pc1 := &PeerConnection{
|
|
LocalRandomUDP: true,
|
|
IPsFromInterfaces: true,
|
|
HandshakeTimeout: conf.Duration(10 * time.Second),
|
|
TrackGatherTimeout: conf.Duration(2 * time.Second),
|
|
Publish: false,
|
|
Log: test.NilLogger,
|
|
}
|
|
err := pc1.Start()
|
|
require.NoError(t, err)
|
|
defer pc1.Close()
|
|
|
|
pc2 := &PeerConnection{
|
|
LocalRandomUDP: true,
|
|
IPsFromInterfaces: true,
|
|
HandshakeTimeout: conf.Duration(10 * time.Second),
|
|
TrackGatherTimeout: conf.Duration(2 * time.Second),
|
|
Publish: true,
|
|
OutgoingTracks: []*OutgoingTrack{
|
|
{
|
|
Caps: webrtc.RTPCodecCapability{
|
|
MimeType: webrtc.MimeTypeH264,
|
|
ClockRate: 90000,
|
|
},
|
|
},
|
|
{
|
|
Caps: webrtc.RTPCodecCapability{
|
|
MimeType: webrtc.MimeTypeOpus,
|
|
ClockRate: 48000,
|
|
Channels: 2,
|
|
},
|
|
},
|
|
},
|
|
Log: test.NilLogger,
|
|
}
|
|
err = pc2.Start()
|
|
require.NoError(t, err)
|
|
defer pc2.Close()
|
|
|
|
offer, err := pc1.CreatePartialOffer()
|
|
require.NoError(t, err)
|
|
|
|
answer, err := pc2.CreateFullAnswer(context.Background(), offer)
|
|
require.NoError(t, err)
|
|
|
|
err = pc1.SetAnswer(answer)
|
|
require.NoError(t, err)
|
|
|
|
err = pc1.WaitUntilConnected(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
err = pc2.WaitUntilConnected(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
for _, track := range pc2.OutgoingTracks {
|
|
err = track.WriteRTP(&rtp.Packet{
|
|
Header: rtp.Header{
|
|
Version: 2,
|
|
Marker: true,
|
|
PayloadType: 111,
|
|
SequenceNumber: 1123,
|
|
Timestamp: 45343,
|
|
SSRC: 563424,
|
|
},
|
|
Payload: []byte{5, 2},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
err = pc1.GatherIncomingTracks(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
codecs := gatherCodecs(pc1.IncomingTracks())
|
|
|
|
sort.Slice(codecs, func(i, j int) bool {
|
|
return codecs[i].PayloadType < codecs[j].PayloadType
|
|
})
|
|
|
|
require.Equal(t, []webrtc.RTPCodecParameters{
|
|
{
|
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
MimeType: webrtc.MimeTypeH264,
|
|
ClockRate: 90000,
|
|
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
|
|
RTCPFeedback: codecs[0].RTCPFeedback,
|
|
},
|
|
PayloadType: 105,
|
|
},
|
|
{
|
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
MimeType: webrtc.MimeTypeOpus,
|
|
ClockRate: 48000,
|
|
Channels: 2,
|
|
SDPFmtpLine: "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1",
|
|
RTCPFeedback: codecs[1].RTCPFeedback,
|
|
},
|
|
PayloadType: 111,
|
|
},
|
|
}, codecs)
|
|
}
|
|
|
|
// test that an audio codec is present regardless of the fact that an audio track is.
|
|
func TestPeerConnectionFallbackCodecs(t *testing.T) {
|
|
pc1 := &PeerConnection{
|
|
LocalRandomUDP: true,
|
|
IPsFromInterfaces: true,
|
|
HandshakeTimeout: conf.Duration(10 * time.Second),
|
|
TrackGatherTimeout: conf.Duration(2 * time.Second),
|
|
Publish: false,
|
|
Log: test.NilLogger,
|
|
}
|
|
err := pc1.Start()
|
|
require.NoError(t, err)
|
|
defer pc1.Close()
|
|
|
|
pc2 := &PeerConnection{
|
|
LocalRandomUDP: true,
|
|
IPsFromInterfaces: true,
|
|
HandshakeTimeout: conf.Duration(10 * time.Second),
|
|
TrackGatherTimeout: conf.Duration(2 * time.Second),
|
|
Publish: true,
|
|
OutgoingTracks: []*OutgoingTrack{{
|
|
Caps: webrtc.RTPCodecCapability{
|
|
MimeType: webrtc.MimeTypeAV1,
|
|
ClockRate: 90000,
|
|
},
|
|
}},
|
|
Log: test.NilLogger,
|
|
}
|
|
err = pc2.Start()
|
|
require.NoError(t, err)
|
|
defer pc2.Close()
|
|
|
|
offer, err := pc1.CreatePartialOffer()
|
|
require.NoError(t, err)
|
|
|
|
answer, err := pc2.CreateFullAnswer(context.Background(), offer)
|
|
require.NoError(t, err)
|
|
|
|
var s sdp.SessionDescription
|
|
err = s.Unmarshal([]byte(answer.SDP))
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, []*sdp.MediaDescription{
|
|
{
|
|
MediaName: sdp.MediaName{
|
|
Media: "video",
|
|
Port: sdp.RangedPort{Value: 9},
|
|
Protos: []string{"UDP", "TLS", "RTP", "SAVPF"},
|
|
Formats: []string{"97"},
|
|
},
|
|
ConnectionInformation: s.MediaDescriptions[0].ConnectionInformation,
|
|
Attributes: s.MediaDescriptions[0].Attributes,
|
|
},
|
|
{
|
|
MediaName: sdp.MediaName{
|
|
Media: "audio",
|
|
Port: sdp.RangedPort{Value: 9},
|
|
Protos: []string{"UDP", "TLS", "RTP", "SAVPF"},
|
|
Formats: []string{"0"},
|
|
},
|
|
ConnectionInformation: s.MediaDescriptions[1].ConnectionInformation,
|
|
Attributes: s.MediaDescriptions[1].Attributes,
|
|
},
|
|
}, s.MediaDescriptions)
|
|
}
|