Skip to content

Commit 4d873a4

Browse files
committed
Add get_stats
1 parent 7f85ac4 commit 4d873a4

9 files changed

+308
-23
lines changed

lib/ex_webrtc/app.ex

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
defmodule ExWebRTC.App do
2+
@moduledoc false
3+
use Application
4+
5+
@impl true
6+
def start(_type, _args) do
7+
children = [{Registry, keys: :unique, name: ExWebRTC.Registry}]
8+
Supervisor.start_link(children, strategy: :one_for_one)
9+
end
10+
end

lib/ex_webrtc/dtls_transport.ex

+78
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@ defmodule ExWebRTC.DTLSTransport do
3333
"""
3434
@type dtls_state() :: :new | :connecting | :connected | :closed | :failed
3535

36+
@typedoc """
37+
Information about DTLS certificate.
38+
39+
* `fingerprint` - hex dump of the cert fingerprint
40+
* `fingerprint_algorithm` - always `:sha_256`
41+
* `base64_certificate` - base 64 encoded certificate
42+
"""
43+
@type cert_info :: %{
44+
fingerprint: binary(),
45+
fingerprint_algorithm: :sha_256,
46+
base64_certificate: binary()
47+
}
48+
3649
@spec start_link(ICETransport.t(), pid()) :: GenServer.on_start()
3750
def start_link(ice_transport \\ DefaultICETransport, ice_pid) do
3851
behaviour = ice_transport.__info__(:attributes)[:behaviour] || []
@@ -49,6 +62,14 @@ defmodule ExWebRTC.DTLSTransport do
4962
GenServer.call(dtls_transport, :set_ice_connected)
5063
end
5164

65+
@spec get_certs_info(dtls_transport()) :: %{
66+
local_cert_info: cert_info(),
67+
remote_cert_info: cert_info() | nil
68+
}
69+
def get_certs_info(dtls_transport) do
70+
GenServer.call(dtls_transport, :get_certs_info)
71+
end
72+
5273
@spec get_fingerprint(dtls_transport()) :: binary()
5374
def get_fingerprint(dtls_transport) do
5475
GenServer.call(dtls_transport, :get_fingerprint)
@@ -87,8 +108,12 @@ defmodule ExWebRTC.DTLSTransport do
87108
ice_connected: false,
88109
buffered_packets: nil,
89110
cert: cert,
111+
base64_cert: Base.encode64(cert),
90112
pkey: pkey,
91113
fingerprint: fingerprint,
114+
remote_cert: nil,
115+
remote_base64_cert: nil,
116+
remote_fingerprint: nil,
92117
in_srtp: ExLibSRTP.new(),
93118
out_srtp: ExLibSRTP.new(),
94119
# sha256 hex dump
@@ -133,6 +158,49 @@ defmodule ExWebRTC.DTLSTransport do
133158
end
134159
end
135160

161+
@impl true
162+
def handle_call(:get_certs_info, _from, state) do
163+
local_cert_info = %{
164+
fingerprint: Utils.hex_dump(state.fingerprint),
165+
fingerprint_algorithm: :sha_256,
166+
base64_certificate: state.base64_cert
167+
}
168+
169+
remote_cert_info =
170+
if state.dtls_state == :connected do
171+
%{
172+
fingerprint: Utils.hex_dump(state.remote_fingerprint),
173+
fingerprint_algorithm: :sha_256,
174+
base64_certificate: state.remote_base64_cert
175+
}
176+
else
177+
nil
178+
end
179+
180+
certs_info = %{
181+
local_cert_info: local_cert_info,
182+
remote_cert_info: remote_cert_info
183+
}
184+
185+
{:reply, certs_info, state}
186+
end
187+
188+
@impl true
189+
def handle_call(:get_remote_cert_info, _from, %{dtls_state: :connected} = state) do
190+
cert_info = %{
191+
fingerprint: state.remote_fingerprint,
192+
fingerprint_algorithm: :sha_256,
193+
base64_certificate: state.remote_base64_cert
194+
}
195+
196+
{:reply, cert_info, state}
197+
end
198+
199+
@impl true
200+
def handle_call(:get_remote_cert_info, _from, state) do
201+
{:reply, nil, state}
202+
end
203+
136204
@impl true
137205
def handle_call(:get_fingerprint, _from, state) do
138206
{:reply, state.fingerprint, state}
@@ -247,6 +315,7 @@ defmodule ExWebRTC.DTLSTransport do
247315

248316
{:handshake_finished, lkm, rkm, profile, packets} ->
249317
Logger.debug("DTLS handshake finished")
318+
state = update_remote_cert_info(state)
250319
state.ice_transport.send_data(state.ice_pid, packets)
251320

252321
peer_fingerprint =
@@ -269,6 +338,7 @@ defmodule ExWebRTC.DTLSTransport do
269338
Logger.debug("DTLS handshake finished")
270339
:ok = setup_srtp(state, lkm, rkm, profile)
271340
state = update_dtls_state(state, :connected)
341+
state = update_remote_cert_info(state)
272342
{:ok, state}
273343

274344
:handshake_want_read ->
@@ -340,5 +410,13 @@ defmodule ExWebRTC.DTLSTransport do
340410
%{state | dtls_state: new_dtls_state}
341411
end
342412

413+
defp update_remote_cert_info(state) do
414+
cert = ExDTLS.get_cert(state.dtls)
415+
fingerprint = ExDTLS.get_cert_fingerprint(cert)
416+
base64_cert = Base.encode64(cert)
417+
418+
%{state | remote_cert: cert, remote_base64_cert: base64_cert, remote_fingerprint: fingerprint}
419+
end
420+
343421
defp notify(dst, msg), do: send(dst, {:dtls_transport, self(), msg})
344422
end

lib/ex_webrtc/ice_transport.ex

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ defmodule ExWebRTC.ICETransport do
1414
@callback restart(pid()) :: :ok
1515
@callback send_data(pid(), binary()) :: :ok
1616
@callback set_remote_credentials(pid(), ufrag :: binary(), pwd :: binary()) :: :ok
17+
@callback get_stats(pid()) :: map()
1718
@callback stop(pid()) :: :ok
1819
end
1920

@@ -43,5 +44,7 @@ defmodule ExWebRTC.DefaultICETransport do
4344
@impl true
4445
defdelegate set_remote_credentials(pid, ufrag, pwd), to: ICEAgent
4546
@impl true
47+
defdelegate get_stats(pid), to: ICEAgent
48+
@impl true
4649
defdelegate stop(pid), to: ICEAgent
4750
end

lib/ex_webrtc/peer_connection.ex

+139-12
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ defmodule ExWebRTC.PeerConnection do
1818
MediaStreamTrack,
1919
RTPTransceiver,
2020
RTPSender,
21+
RTPReceiver,
2122
SDPUtils,
2223
SessionDescription,
2324
Utils
@@ -59,6 +60,11 @@ defmodule ExWebRTC.PeerConnection do
5960
@type connection_state() :: :closed | :failed | :disconnected | :new | :connecting | :connected
6061

6162
#### API ####
63+
@spec get_all_peer_connections() :: [pid()]
64+
def get_all_peer_connections() do
65+
Registry.select(ExWebRTC.Registry, [{{:_, :"$1", :_}, [], [:"$1"]}])
66+
end
67+
6268
@spec start_link(Configuration.options()) :: GenServer.on_start()
6369
def start_link(options \\ []) do
6470
configuration = Configuration.from_options!(options)
@@ -157,6 +163,11 @@ defmodule ExWebRTC.PeerConnection do
157163
GenServer.call(peer_connection, {:remove_track, sender_id})
158164
end
159165

166+
@spec get_stats(peer_connection()) :: %{String.t() => term()}
167+
def get_stats(peer_connection) do
168+
GenServer.call(peer_connection, :get_stats)
169+
end
170+
160171
@spec send_rtp(peer_connection(), String.t(), ExRTP.Packet.t()) :: :ok
161172
def send_rtp(peer_connection, track_id, packet) do
162173
GenServer.cast(peer_connection, {:send_rtp, track_id, packet})
@@ -171,6 +182,7 @@ defmodule ExWebRTC.PeerConnection do
171182

172183
@impl true
173184
def init({owner, config}) do
185+
{:ok, _} = Registry.register(ExWebRTC.Registry, self(), self())
174186
ice_config = [stun_servers: config.ice_servers, ip_filter: config.ice_ip_filter, on_data: nil]
175187
{:ok, ice_pid} = DefaultICETransport.start_link(:controlled, ice_config)
176188
{:ok, dtls_transport} = DTLSTransport.start_link(DefaultICETransport, ice_pid)
@@ -519,7 +531,7 @@ defmodule ExWebRTC.PeerConnection do
519531
{:reply, :ok, state}
520532

521533
true ->
522-
# that's not compliant with the W3C but it's safer not
534+
# that's not compliant with the W3C but it's safer not
523535
# to allow for this until we have clear use case
524536
{:reply, {:error, :invalid_transceiver_direction}, state}
525537
end
@@ -558,6 +570,119 @@ defmodule ExWebRTC.PeerConnection do
558570
end
559571
end
560572

573+
@impl true
574+
def handle_call(:get_stats, _from, state) do
575+
timestamp = System.os_time(:millisecond)
576+
577+
ice_stats = state.ice_transport.get_stats(state.ice_pid)
578+
579+
%{local_cert_info: local_cert_info, remote_cert_info: remote_cert_info} =
580+
DTLSTransport.get_certs_info(state.dtls_transport)
581+
582+
remote_certificate =
583+
if remote_cert_info != nil do
584+
%{
585+
id: :remote_certificate,
586+
type: :certificate,
587+
timestamp: timestamp,
588+
fingerprint: remote_cert_info.fingerprint,
589+
fingerprint_algorithm: remote_cert_info.fingerprint_algorithm,
590+
base64_certificate: remote_cert_info.base64_certificate
591+
}
592+
else
593+
%{
594+
id: :remote_certificate,
595+
type: :certificate,
596+
timestamp: timestamp,
597+
fingerprint: nil,
598+
fingerprint_algorithm: nil,
599+
base64_certificate: nil
600+
}
601+
end
602+
603+
local_cands =
604+
Map.new(ice_stats.local_candidates, fn local_cand ->
605+
cand = %{
606+
id: local_cand.id,
607+
timestamp: timestamp,
608+
type: :local_candidate,
609+
address: local_cand.address,
610+
port: local_cand.port,
611+
protocol: local_cand.transport,
612+
candidate_type: local_cand.type,
613+
priority: local_cand.priority,
614+
foundation: local_cand.foundation,
615+
related_address: local_cand.base_address,
616+
related_port: local_cand.base_port
617+
}
618+
619+
{cand.id, cand}
620+
end)
621+
622+
rtp_stats =
623+
Enum.flat_map(state.transceivers, fn tr ->
624+
case tr.current_direction do
625+
:sendonly ->
626+
[RTPSender.get_stats(tr.sender, timestamp)]
627+
628+
:recvonly ->
629+
[RTPReceiver.get_stats(tr.receiver, timestamp)]
630+
631+
:sendrecv ->
632+
[
633+
RTPSender.get_stats(tr.sender, timestamp),
634+
RTPReceiver.get_stats(tr.receiver, timestamp)
635+
]
636+
637+
_other ->
638+
[]
639+
end
640+
end)
641+
|> Map.new(fn stats -> {stats.id, stats} end)
642+
643+
stats = %{
644+
peer_connection: %{
645+
id: :peer_connection,
646+
type: :peer_connection,
647+
timestamp: timestamp,
648+
signaling_state: state.signaling_state,
649+
ice_state: state.ice_state,
650+
ice_gathering_state: state.ice_gathering_state,
651+
dtls_state: state.dtls_state,
652+
negotiation_needed: state.negotiation_needed,
653+
connection_state: state.conn_state
654+
},
655+
transport: %{
656+
id: :transport,
657+
type: :transport,
658+
timestamp: timestamp,
659+
bytes_sent: ice_stats.bytes_sent,
660+
bytes_received: ice_stats.bytes_received,
661+
packets_sent: ice_stats.packets_sent,
662+
packets_received: ice_stats.packets_received,
663+
ice_role: ice_stats.role,
664+
ice_local_ufrag: ice_stats.local_ufrag,
665+
ice_state: ice_stats.state
666+
},
667+
local_certificate: %{
668+
id: :local_certificate,
669+
type: :certificate,
670+
timestamp: timestamp,
671+
fingerprint: local_cert_info.fingerprint,
672+
fingerprint_algorithm: local_cert_info.fingerprint_algorithm,
673+
base64_certificate: local_cert_info.base64_certificate
674+
},
675+
remote_certificate: remote_certificate
676+
}
677+
678+
stats =
679+
stats
680+
|> Map.merge(local_cands)
681+
|> Map.merge(rtp_stats)
682+
683+
{:reply, stats, state}
684+
end
685+
561686
@impl true
562687
def handle_cast({:send_rtp, track_id, packet}, state) do
563688
# TODO: iterating over transceivers is not optimal
@@ -638,10 +763,12 @@ defmodule ExWebRTC.PeerConnection do
638763
@impl true
639764
def handle_info({:dtls_transport, _pid, {:rtp, data}}, state) do
640765
with {:ok, demuxer, mid, packet} <- Demuxer.demux(state.demuxer, data),
641-
%RTPTransceiver{} = t <- Enum.find(state.transceivers, &(&1.mid == mid)) do
642-
track_id = t.receiver.track.id
643-
notify(state.owner, {:rtp, track_id, packet})
644-
{:noreply, %{state | demuxer: demuxer}}
766+
{idx, %RTPTransceiver{} = t} <- find_transceiver(state.transceivers, mid) do
767+
receiver = RTPReceiver.receive(t.receiver, packet, data)
768+
transceivers = List.update_at(state.transceivers, idx, &%{&1 | receiver: receiver})
769+
state = %{state | demuxer: demuxer, transceivers: transceivers}
770+
notify(state.owner, {:rtp, t.receiver.track.id, packet})
771+
{:noreply, state}
645772
else
646773
nil ->
647774
Logger.warning("Received RTP with unrecognized MID: #{inspect(data)}")
@@ -717,7 +844,7 @@ defmodule ExWebRTC.PeerConnection do
717844
# mline from the last offer/answer, do it (i.e. recycle free mline)
718845
# * If there is no transceiver's mline, just rewrite
719846
# mline from the offer/answer respecting its port number i.e. whether
720-
# it is rejected or not.
847+
# it is rejected or not.
721848
# This is to preserve the same number of mlines
722849
# between subsequent offer/anser exchanges.
723850
# * At the end, add remaining transceiver mlines
@@ -751,7 +878,7 @@ defmodule ExWebRTC.PeerConnection do
751878
end
752879

753880
# next_mline_idx is future mline idx to use if there are no mlines to recycle
754-
# next_mid is the next free mid
881+
# next_mid is the next free mid
755882
defp assign_mlines(
756883
transceivers,
757884
last_answer,
@@ -1154,7 +1281,7 @@ defmodule ExWebRTC.PeerConnection do
11541281

11551282
# If signaling state is not stable i.e. we are during negotiation,
11561283
# don't fire negotiation needed notification.
1157-
# We will do this when moving to the stable state as part of the
1284+
# We will do this when moving to the stable state as part of the
11581285
# steps for setting remote description.
11591286
defp update_negotiation_needed(%{signaling_state: sig_state} = state) when sig_state != :stable,
11601287
do: state
@@ -1172,14 +1299,14 @@ defmodule ExWebRTC.PeerConnection do
11721299

11731300
negotiation_needed == false ->
11741301
# We need to clear the flag.
1175-
# Consider scenario where we add a transceiver and then
1176-
# remove it without performing negotiation.
1302+
# Consider scenario where we add a transceiver and then
1303+
# remove it without performing negotiation.
11771304
# At the end of the day, negotiation_needed flag has to be cleared.
11781305
%{state | negotiation_needed: false}
11791306
end
11801307
end
11811308

1182-
# We don't support MSIDs and stopping transceivers so
1309+
# We don't support MSIDs and stopping transceivers so
11831310
# we only check 5.2 and 5.3 from 4.7.3#check-if-negotiation-is-needed
11841311
# https://www.w3.org/TR/webrtc/#dfn-check-if-negotiation-is-needed
11851312
defp negotiation_needed?([], _), do: false
@@ -1199,7 +1326,7 @@ defmodule ExWebRTC.PeerConnection do
11991326
cond do
12001327
# Consider the following scenario:
12011328
# 1. offerer offers sendrecv
1202-
# 2. answerer answers with recvonly
1329+
# 2. answerer answers with recvonly
12031330
# 3. offerer changes from sendrecv to sendonly
12041331
# We don't need to renegotiate in such a case.
12051332
local_desc_type == :offer and

0 commit comments

Comments
 (0)