api: add more attributes to WebRTC connections

new attributes: peerConnectionEstablished, localCandidate, remoteCandidate
This commit is contained in:
aler9 2023-01-07 13:48:00 +01:00
parent cca4702357
commit e7e8d5ce20
6 changed files with 280 additions and 136 deletions

View File

@ -534,6 +534,12 @@ components:
type: string
remoteAddr:
type: string
peerConnectionEstablished:
type: bool
localCandidate:
type: string
remoteCandidate:
type: string
bytesReceived:
type: number
bytesSent:

View File

@ -405,6 +405,7 @@ func TestAPIProtocolSpecificList(t *testing.T) {
"rtmp",
"rtmps",
"hls",
"webrtc",
} {
t.Run(ca, func(t *testing.T) {
conf := "api: yes\n"
@ -490,7 +491,6 @@ func TestAPIProtocolSpecificList(t *testing.T) {
case "hls":
source := gortsplib.Client{}
err := source.StartRecording("rtsp://localhost:8554/mypath",
media.Medias{medi})
require.NoError(t, err)
@ -502,6 +502,17 @@ func TestAPIProtocolSpecificList(t *testing.T) {
defer res.Body.Close()
require.Equal(t, 200, res.StatusCode)
}()
case "webrtc":
source := gortsplib.Client{}
err := source.StartRecording("rtsp://localhost:8554/mypath",
media.Medias{medi})
require.NoError(t, err)
defer source.Close()
c, err := newWebRTCTestClient("ws://localhost:8889/mypath/ws")
require.NoError(t, err)
defer c.close()
}
switch ca {
@ -562,6 +573,31 @@ func TestAPIProtocolSpecificList(t *testing.T) {
s := fmt.Sprintf("^%d-", time.Now().Year())
require.Regexp(t, s, out.Items[firstID].Created)
require.Regexp(t, s, out.Items[firstID].LastRequest)
case "webrtc":
type item struct {
Created time.Time `json:"created"`
RemoteAddr string `json:"remoteAddr"`
PeerConnectionEstablished bool `json:"peerConnectionEstablished"`
LocalCandidate string `json:"localCandidate"`
RemoteCandidate string `json:"remoteCandidate"`
BytesReceived uint64 `json:"bytesReceived"`
BytesSent uint64 `json:"bytesSent"`
}
var out struct {
Items map[string]item `json:"items"`
}
err = httpRequest(http.MethodGet, "http://localhost:9997/v1/webrtcconns/list", nil, &out)
require.NoError(t, err)
var firstID string
for k := range out.Items {
firstID = k
}
itm := out.Items[firstID]
require.Equal(t, true, itm.PeerConnectionEstablished)
}
})
}

View File

@ -565,7 +565,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closePathManager ||
closeMetrics
closeWebrtcServer := newConf == nil ||
closeWebRTCServer := newConf == nil ||
newConf.WebRTCDisable != p.conf.WebRTCDisable ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
newConf.WebRTCAddress != p.conf.WebRTCAddress ||
@ -590,7 +590,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closeRTSPSServer ||
closeRTMPServer ||
closeHLSServer ||
closeWebrtcServer
closeWebRTCServer
if newConf == nil && p.confWatcher != nil {
p.confWatcher.Close()
@ -621,7 +621,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
p.pathManager = nil
}
if closeWebrtcServer && p.webRTCServer != nil {
if closeWebRTCServer && p.webRTCServer != nil {
p.webRTCServer.close()
p.webRTCServer = nil
}

View File

@ -62,37 +62,6 @@ func newPeerConnection(configuration webrtc.Configuration,
return api.NewPeerConnection(configuration)
}
func describeActiveCandidates(pc *webrtc.PeerConnection) (string, string) {
var lcid string
var rcid string
for _, stats := range pc.GetStats() {
if tstats, ok := stats.(webrtc.ICECandidatePairStats); ok && tstats.Nominated {
lcid = tstats.LocalCandidateID
rcid = tstats.RemoteCandidateID
break
}
}
var ldesc string
var rdesc string
for _, stats := range pc.GetStats() {
if tstats, ok := stats.(webrtc.ICECandidateStats); ok {
str := tstats.CandidateType.String() + "/" + tstats.Protocol + "/" +
tstats.IP + "/" + strconv.FormatInt(int64(tstats.Port), 10)
if tstats.ID == lcid {
ldesc = str
} else if tstats.ID == rcid {
rdesc = str
}
}
}
return ldesc, rdesc
}
type webRTCTrack struct {
media *media.Media
format format.Format
@ -187,13 +156,73 @@ func (c *webRTCConn) remoteAddr() net.Addr {
return c.wsconn.RemoteAddr()
}
func (c *webRTCConn) peerConnectionEstablished() bool {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.curPC != nil
}
func (c *webRTCConn) localCandidate() string {
c.mutex.RLock()
defer c.mutex.RUnlock()
if c.curPC != nil {
var cid string
for _, stats := range c.curPC.GetStats() {
if tstats, ok := stats.(webrtc.ICECandidatePairStats); ok && tstats.Nominated {
cid = tstats.LocalCandidateID
break
}
}
if cid != "" {
for _, stats := range c.curPC.GetStats() {
if tstats, ok := stats.(webrtc.ICECandidateStats); ok && tstats.ID == cid {
return tstats.CandidateType.String() + "/" + tstats.Protocol + "/" +
tstats.IP + "/" + strconv.FormatInt(int64(tstats.Port), 10)
}
}
}
}
return ""
}
func (c *webRTCConn) remoteCandidate() string {
c.mutex.RLock()
defer c.mutex.RUnlock()
if c.curPC != nil {
var cid string
for _, stats := range c.curPC.GetStats() {
if tstats, ok := stats.(webrtc.ICECandidatePairStats); ok && tstats.Nominated {
cid = tstats.RemoteCandidateID
break
}
}
if cid != "" {
for _, stats := range c.curPC.GetStats() {
if tstats, ok := stats.(webrtc.ICECandidateStats); ok && tstats.ID == cid {
return tstats.CandidateType.String() + "/" + tstats.Protocol + "/" +
tstats.IP + "/" + strconv.FormatInt(int64(tstats.Port), 10)
}
}
}
}
return ""
}
func (c *webRTCConn) bytesReceived() uint64 {
c.mutex.RLock()
defer c.mutex.RUnlock()
for _, stats := range c.curPC.GetStats() {
if tstats, ok := stats.(webrtc.TransportStats); ok {
if tstats.ID == "iceTransport" {
return tstats.BytesReceived
if c.curPC != nil {
for _, stats := range c.curPC.GetStats() {
if tstats, ok := stats.(webrtc.TransportStats); ok {
if tstats.ID == "iceTransport" {
return tstats.BytesReceived
}
}
}
}
@ -203,10 +232,13 @@ func (c *webRTCConn) bytesReceived() uint64 {
func (c *webRTCConn) bytesSent() uint64 {
c.mutex.RLock()
defer c.mutex.RUnlock()
for _, stats := range c.curPC.GetStats() {
if tstats, ok := stats.(webrtc.TransportStats); ok {
if tstats.ID == "iceTransport" {
return tstats.BytesSent
if c.curPC != nil {
for _, stats := range c.curPC.GetStats() {
if tstats, ok := stats.(webrtc.TransportStats); ok {
if tstats.ID == "iceTransport" {
return tstats.BytesSent
}
}
}
}
@ -335,10 +367,6 @@ func (c *webRTCConn) runInner(ctx context.Context) error {
<-pcClosed
}()
c.mutex.Lock()
c.curPC = pc
c.mutex.Unlock()
for _, track := range tracks {
rtpSender, err := pc.AddTrack(track.webRTCTrack)
if err != nil {
@ -440,8 +468,12 @@ outer:
// in order to allow the other side of the connection
// to switch to the "connected" state before WebSocket is closed.
ldesc, rdesc := describeActiveCandidates(pc)
c.log(logger.Info, "peer connection established, local candidate: %v, remote candidate: %v", ldesc, rdesc)
c.mutex.Lock()
c.curPC = pc
c.mutex.Unlock()
c.log(logger.Info, "peer connection established, local candidate: %v, remote candidate: %v",
c.localCandidate(), c.remoteCandidate())
ringBuffer, _ := ringbuffer.New(uint64(c.readBufferCount))
defer ringBuffer.Close()

View File

@ -32,10 +32,13 @@ var upgrader = websocket.Upgrader{
}
type webRTCServerAPIConnsListItem struct {
Created time.Time `json:"created"`
RemoteAddr string `json:"remoteAddr"`
BytesReceived uint64 `json:"bytesReceived"`
BytesSent uint64 `json:"bytesSent"`
Created time.Time `json:"created"`
RemoteAddr string `json:"remoteAddr"`
PeerConnectionEstablished bool `json:"peerConnectionEstablished"`
LocalCandidate string `json:"localCandidate"`
RemoteCandidate string `json:"remoteCandidate"`
BytesReceived uint64 `json:"bytesReceived"`
BytesSent uint64 `json:"bytesSent"`
}
type webRTCServerAPIConnsListData struct {
@ -264,10 +267,13 @@ outer:
for c := range s.conns {
data.Items[c.uuid.String()] = webRTCServerAPIConnsListItem{
Created: c.created,
RemoteAddr: c.remoteAddr().String(),
BytesReceived: c.bytesReceived(),
BytesSent: c.bytesSent(),
Created: c.created,
RemoteAddr: c.remoteAddr().String(),
PeerConnectionEstablished: c.peerConnectionEstablished(),
LocalCandidate: c.localCandidate(),
RemoteCandidate: c.remoteCandidate(),
BytesReceived: c.bytesReceived(),
BytesSent: c.bytesSent(),
}
}

View File

@ -14,6 +14,146 @@ import (
"github.com/stretchr/testify/require"
)
type webRTCTestClient struct {
wc *websocket.Conn
pc *webrtc.PeerConnection
track chan *webrtc.TrackRemote
}
func newWebRTCTestClient(addr string) (*webRTCTestClient, error) {
wc, _, err := websocket.DefaultDialer.Dial(addr, nil) //nolint:bodyclose
if err != nil {
return nil, err
}
_, msg, err := wc.ReadMessage()
if err != nil {
wc.Close()
return nil, err
}
var iceServers []webrtc.ICEServer
err = json.Unmarshal(msg, &iceServers)
if err != nil {
wc.Close()
return nil, err
}
pc, err := newPeerConnection(webrtc.Configuration{
ICEServers: iceServers,
})
if err != nil {
wc.Close()
return nil, err
}
pc.OnICECandidate(func(i *webrtc.ICECandidate) {
if i != nil {
enc, _ := json.Marshal(i.ToJSON())
wc.WriteMessage(websocket.TextMessage, enc)
}
})
connected := make(chan struct{})
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
if state == webrtc.PeerConnectionStateConnected {
close(connected)
}
})
track := make(chan *webrtc.TrackRemote, 1)
pc.OnTrack(func(trak *webrtc.TrackRemote, recv *webrtc.RTPReceiver) {
track <- trak
})
_, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo)
if err != nil {
wc.Close()
pc.Close()
return nil, err
}
localOffer, err := pc.CreateOffer(nil)
if err != nil {
wc.Close()
pc.Close()
return nil, err
}
enc, err := json.Marshal(localOffer)
if err != nil {
wc.Close()
pc.Close()
return nil, err
}
err = wc.WriteMessage(websocket.TextMessage, enc)
if err != nil {
wc.Close()
pc.Close()
return nil, err
}
err = pc.SetLocalDescription(localOffer)
if err != nil {
wc.Close()
pc.Close()
return nil, err
}
_, msg, err = wc.ReadMessage()
if err != nil {
wc.Close()
pc.Close()
return nil, err
}
var remoteOffer webrtc.SessionDescription
err = json.Unmarshal(msg, &remoteOffer)
if err != nil {
wc.Close()
pc.Close()
return nil, err
}
err = pc.SetRemoteDescription(remoteOffer)
if err != nil {
wc.Close()
pc.Close()
return nil, err
}
go func() {
for {
_, msg, err := wc.ReadMessage()
if err != nil {
return
}
var candidate webrtc.ICECandidateInit
err = json.Unmarshal(msg, &candidate)
if err != nil {
return
}
pc.AddICECandidate(candidate)
}
}()
<-connected
return &webRTCTestClient{
wc: wc,
pc: pc,
track: track,
}, nil
}
func (c *webRTCTestClient) close() {
c.pc.Close()
c.wc.Close()
}
func TestWebRTCServer(t *testing.T) {
p, ok := newInstance("paths:\n" +
" all:\n")
@ -36,85 +176,9 @@ func TestWebRTCServer(t *testing.T) {
require.NoError(t, err)
defer source.Close()
c, _, err := websocket.DefaultDialer.Dial("ws://localhost:8889/stream/ws", nil) //nolint:bodyclose
c, err := newWebRTCTestClient("ws://localhost:8889/stream/ws")
require.NoError(t, err)
defer c.Close()
_, msg, err := c.ReadMessage()
require.NoError(t, err)
var iceServers []webrtc.ICEServer
err = json.Unmarshal(msg, &iceServers)
require.NoError(t, err)
pc, err := newPeerConnection(webrtc.Configuration{
ICEServers: iceServers,
})
require.NoError(t, err)
defer pc.Close()
pc.OnICECandidate(func(i *webrtc.ICECandidate) {
if i != nil {
enc, _ := json.Marshal(i.ToJSON())
c.WriteMessage(websocket.TextMessage, enc)
}
})
connected := make(chan struct{})
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
if state == webrtc.PeerConnectionStateConnected {
close(connected)
}
})
track := make(chan *webrtc.TrackRemote)
pc.OnTrack(func(trak *webrtc.TrackRemote, recv *webrtc.RTPReceiver) {
track <- trak
})
_, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo)
require.NoError(t, err)
localOffer, err := pc.CreateOffer(nil)
require.NoError(t, err)
enc, err := json.Marshal(localOffer)
require.NoError(t, err)
err = c.WriteMessage(websocket.TextMessage, enc)
require.NoError(t, err)
err = pc.SetLocalDescription(localOffer)
require.NoError(t, err)
_, msg, err = c.ReadMessage()
require.NoError(t, err)
var remoteOffer webrtc.SessionDescription
err = json.Unmarshal(msg, &remoteOffer)
require.NoError(t, err)
err = pc.SetRemoteDescription(remoteOffer)
require.NoError(t, err)
go func() {
for {
_, msg, err := c.ReadMessage()
if err != nil {
return
}
var candidate webrtc.ICECandidateInit
err = json.Unmarshal(msg, &candidate)
if err != nil {
return
}
pc.AddICECandidate(candidate)
}
}()
<-connected
defer c.close()
time.Sleep(500 * time.Millisecond)
@ -130,7 +194,7 @@ func TestWebRTCServer(t *testing.T) {
Payload: []byte{0x01, 0x02, 0x03, 0x04},
})
trak := <-track
trak := <-c.track
pkt, _, err := trak.ReadRTP()
require.NoError(t, err)