diff --git a/lib/ex_webrtc/app.ex b/lib/ex_webrtc/app.ex new file mode 100644 index 00000000..4b6cb8ca --- /dev/null +++ b/lib/ex_webrtc/app.ex @@ -0,0 +1,9 @@ +defmodule ExWebRTC.App do + use Application + + def start(_type, _args) do + IO.inspect(:loading_ex_webrtc_app) + children = [{Registry, keys: :unique, name: ExWebRTC.Registry}] + Supervisor.start_link(children, strategy: :one_for_one) + end +end diff --git a/lib/ex_webrtc/dtls_transport.ex b/lib/ex_webrtc/dtls_transport.ex index 2a9909de..2b7fdda0 100644 --- a/lib/ex_webrtc/dtls_transport.ex +++ b/lib/ex_webrtc/dtls_transport.ex @@ -49,6 +49,16 @@ defmodule ExWebRTC.DTLSTransport do GenServer.call(dtls_transport, :set_ice_connected) end + @spec get_local_cert_info(dtls_transport()) :: map() + def get_local_cert_info(dtls_transport) do + GenServer.call(dtls_transport, :get_local_cert_info) + end + + @spec get_remote_cert_info(dtls_transport()) :: map() + def get_remote_cert_info(dtls_transport) do + GenServer.call(dtls_transport, :get_remote_cert_info) + end + @spec get_fingerprint(dtls_transport()) :: binary() def get_fingerprint(dtls_transport) do GenServer.call(dtls_transport, :get_fingerprint) @@ -87,8 +97,12 @@ defmodule ExWebRTC.DTLSTransport do ice_connected: false, buffered_packets: nil, cert: cert, + base64_cert: Base.encode64(cert), pkey: pkey, fingerprint: fingerprint, + remote_cert: nil, + remote_base64_cert: nil, + remote_fingerprint: nil, in_srtp: ExLibSRTP.new(), out_srtp: ExLibSRTP.new(), # sha256 hex dump @@ -133,6 +147,28 @@ defmodule ExWebRTC.DTLSTransport do end end + @impl true + def handle_call(:get_local_cert_info, _from, state) do + cert_info = %{ + fingerprint: state.fingerprint, + fingerprint_algorithm: :sha_256, + base64_certificate: state.base64_cert + } + + {:reply, cert_info, state} + end + + @impl true + def handle_call(:get_remote_cert_info, _from, state) do + cert_info = %{ + fingerprint: state.remote_fingerprint, + fingerprint_algorithm: :sha_256, + base64_certificate: state.remote_base64_cert + } + + {:reply, cert_info, state} + end + @impl true def handle_call(:get_fingerprint, _from, state) do {:reply, state.fingerprint, state} @@ -247,6 +283,7 @@ defmodule ExWebRTC.DTLSTransport do {:handshake_finished, lkm, rkm, profile, packets} -> Logger.debug("DTLS handshake finished") + state = update_remote_cert_info(state) state.ice_transport.send_data(state.ice_pid, packets) peer_fingerprint = @@ -269,6 +306,7 @@ defmodule ExWebRTC.DTLSTransport do Logger.debug("DTLS handshake finished") :ok = setup_srtp(state, lkm, rkm, profile) state = update_dtls_state(state, :connected) + state = update_remote_cert_info(state) {:ok, state} :handshake_want_read -> @@ -340,5 +378,13 @@ defmodule ExWebRTC.DTLSTransport do %{state | dtls_state: new_dtls_state} end + defp update_remote_cert_info(state) do + cert = ExDTLS.get_cert(state.dtls) + fingerprint = ExDTLS.get_cert_fingerprint(cert) + base64_cert = Base.encode64(cert) + + %{state | remote_cert: cert, remote_base64_cert: base64_cert, remote_fingerprint: fingerprint} + end + defp notify(dst, msg), do: send(dst, {:dtls_transport, self(), msg}) end diff --git a/lib/ex_webrtc/ice_transport.ex b/lib/ex_webrtc/ice_transport.ex index 32efebb0..49110093 100644 --- a/lib/ex_webrtc/ice_transport.ex +++ b/lib/ex_webrtc/ice_transport.ex @@ -14,6 +14,7 @@ defmodule ExWebRTC.ICETransport do @callback restart(pid()) :: :ok @callback send_data(pid(), binary()) :: :ok @callback set_remote_credentials(pid(), ufrag :: binary(), pwd :: binary()) :: :ok + @callback get_stats(pid()) :: map() @callback stop(pid()) :: :ok end @@ -43,5 +44,7 @@ defmodule ExWebRTC.DefaultICETransport do @impl true defdelegate set_remote_credentials(pid, ufrag, pwd), to: ICEAgent @impl true + defdelegate get_stats(pid), to: ICEAgent + @impl true defdelegate stop(pid), to: ICEAgent end diff --git a/lib/ex_webrtc/peer_connection.ex b/lib/ex_webrtc/peer_connection.ex index c381fea6..d38f89b0 100644 --- a/lib/ex_webrtc/peer_connection.ex +++ b/lib/ex_webrtc/peer_connection.ex @@ -18,6 +18,7 @@ defmodule ExWebRTC.PeerConnection do MediaStreamTrack, RTPTransceiver, RTPSender, + RTPReceiver, SDPUtils, SessionDescription, Utils @@ -59,6 +60,11 @@ defmodule ExWebRTC.PeerConnection do @type connection_state() :: :closed | :failed | :disconnected | :new | :connecting | :connected #### API #### + @spec get_all_peer_connections() :: [pid()] + def get_all_peer_connections() do + Registry.select(ExWebRTC.Registry, [{{:_, :"$1", :_}, [], [:"$1"]}]) + end + @spec start_link(Configuration.options()) :: GenServer.on_start() def start_link(options \\ []) do configuration = Configuration.from_options!(options) @@ -157,6 +163,11 @@ defmodule ExWebRTC.PeerConnection do GenServer.call(peer_connection, {:remove_track, sender_id}) end + @spec get_stats(peer_connection()) :: %{String.t() => term()} + def get_stats(peer_connection) do + GenServer.call(peer_connection, :get_stats) + end + @spec send_rtp(peer_connection(), String.t(), ExRTP.Packet.t()) :: :ok def send_rtp(peer_connection, track_id, packet) do GenServer.cast(peer_connection, {:send_rtp, track_id, packet}) @@ -171,6 +182,7 @@ defmodule ExWebRTC.PeerConnection do @impl true def init({owner, config}) do + {:ok, _} = Registry.register(ExWebRTC.Registry, self(), self()) ice_config = [stun_servers: config.ice_servers, ip_filter: config.ice_ip_filter, on_data: nil] {:ok, ice_pid} = DefaultICETransport.start_link(:controlled, ice_config) {:ok, dtls_transport} = DTLSTransport.start_link(DefaultICETransport, ice_pid) @@ -182,6 +194,7 @@ defmodule ExWebRTC.PeerConnection do state = %{ owner: owner, + stats_id: Utils.generate_id(), config: config, current_local_desc: nil, pending_local_desc: nil, @@ -519,7 +532,7 @@ defmodule ExWebRTC.PeerConnection do {:reply, :ok, state} true -> - # that's not compliant with the W3C but it's safer not + # that's not compliant with the W3C but it's safer not # to allow for this until we have clear use case {:reply, {:error, :invalid_transceiver_direction}, state} end @@ -558,6 +571,72 @@ defmodule ExWebRTC.PeerConnection do end end + @impl true + def handle_call(:get_stats, _from, state) do + timestamp = System.os_time(:millisecond) + + ice_stats = state.ice_transport.get_stats(state.ice_pid) + local_cert_info = DTLSTransport.get_local_cert_info(state.dtls_transport) + remote_cert_info = DTLSTransport.get_remote_cert_info(state.dtls_transport) + + rtp_stats = + Enum.flat_map(state.transceivers, fn tr -> + case tr.current_direction do + :sendonly -> + [RTPSender.get_stats(tr.sender, timestamp)] + + :recvonly -> + [RTPReceiver.get_stats(tr.receiver, timestamp)] + + :sendrecv -> + [ + RTPSender.get_stats(tr.sender, timestamp), + RTPReceiver.get_stats(tr.receiver, timestamp) + ] + + _other -> + [] + end + end) + |> Map.new(fn stats -> {stats.id, stats} end) + + stats = %{ + state.stats_id => %{ + id: state.stats_id, + type: :peer_connection, + timestamp: timestamp, + datachannels_opened: 0, + datachannels_closed: 0 + }, + :transport => %{ + id: :transport, + type: :transport, + timestamp: timestamp, + bytes_sent: ice_stats.bytes_sent, + bytes_received: ice_stats.bytes_received + }, + :local_certificate => %{ + id: :local_certificate, + type: :certificate, + timestamp: timestamp, + fingerprint: Utils.hex_dump(local_cert_info.fingerprint), + fingerprint_algorithm: local_cert_info.fingerprint_algorithm, + base64_certificate: local_cert_info.base64_certificate + }, + :remote_certificate => %{ + id: :remote_certificate, + type: :certificate, + timestamp: timestamp, + fingerprint: Utils.hex_dump(remote_cert_info.fingerprint), + fingerprint_algorithm: remote_cert_info.fingerprint_algorithm, + base64_certificate: remote_cert_info.base64_certificate + } + } + + stats = Map.merge(stats, rtp_stats) + {:reply, stats, state} + end + @impl true def handle_cast({:send_rtp, track_id, packet}, state) do # TODO: iterating over transceivers is not optimal @@ -638,10 +717,12 @@ defmodule ExWebRTC.PeerConnection do @impl true def handle_info({:dtls_transport, _pid, {:rtp, data}}, state) do with {:ok, demuxer, mid, packet} <- Demuxer.demux(state.demuxer, data), - %RTPTransceiver{} = t <- Enum.find(state.transceivers, &(&1.mid == mid)) do - track_id = t.receiver.track.id - notify(state.owner, {:rtp, track_id, packet}) - {:noreply, %{state | demuxer: demuxer}} + {idx, %RTPTransceiver{} = t} <- find_transceiver(state.transceivers, mid) do + receiver = RTPReceiver.receive(t.receiver, packet, data) + transceivers = List.update_at(state.transceivers, idx, &%{&1 | receiver: receiver}) + state = %{state | demuxer: demuxer, transceivers: transceivers} + notify(state.owner, {:rtp, t.receiver.track.id, packet}) + {:noreply, state} else nil -> Logger.warning("Received RTP with unrecognized MID: #{inspect(data)}") @@ -717,7 +798,7 @@ defmodule ExWebRTC.PeerConnection do # mline from the last offer/answer, do it (i.e. recycle free mline) # * If there is no transceiver's mline, just rewrite # mline from the offer/answer respecting its port number i.e. whether - # it is rejected or not. + # it is rejected or not. # This is to preserve the same number of mlines # between subsequent offer/anser exchanges. # * At the end, add remaining transceiver mlines @@ -751,7 +832,7 @@ defmodule ExWebRTC.PeerConnection do end # next_mline_idx is future mline idx to use if there are no mlines to recycle - # next_mid is the next free mid + # next_mid is the next free mid defp assign_mlines( transceivers, last_answer, @@ -1154,7 +1235,7 @@ defmodule ExWebRTC.PeerConnection do # If signaling state is not stable i.e. we are during negotiation, # don't fire negotiation needed notification. - # We will do this when moving to the stable state as part of the + # We will do this when moving to the stable state as part of the # steps for setting remote description. defp update_negotiation_needed(%{signaling_state: sig_state} = state) when sig_state != :stable, do: state @@ -1172,14 +1253,14 @@ defmodule ExWebRTC.PeerConnection do negotiation_needed == false -> # We need to clear the flag. - # Consider scenario where we add a transceiver and then - # remove it without performing negotiation. + # Consider scenario where we add a transceiver and then + # remove it without performing negotiation. # At the end of the day, negotiation_needed flag has to be cleared. %{state | negotiation_needed: false} end end - # We don't support MSIDs and stopping transceivers so + # We don't support MSIDs and stopping transceivers so # we only check 5.2 and 5.3 from 4.7.3#check-if-negotiation-is-needed # https://www.w3.org/TR/webrtc/#dfn-check-if-negotiation-is-needed defp negotiation_needed?([], _), do: false @@ -1199,7 +1280,7 @@ defmodule ExWebRTC.PeerConnection do cond do # Consider the following scenario: # 1. offerer offers sendrecv - # 2. answerer answers with recvonly + # 2. answerer answers with recvonly # 3. offerer changes from sendrecv to sendonly # We don't need to renegotiate in such a case. local_desc_type == :offer and diff --git a/lib/ex_webrtc/rtp_receiver.ex b/lib/ex_webrtc/rtp_receiver.ex index 82304531..e3b6924c 100644 --- a/lib/ex_webrtc/rtp_receiver.ex +++ b/lib/ex_webrtc/rtp_receiver.ex @@ -6,8 +6,34 @@ defmodule ExWebRTC.RTPReceiver do alias ExWebRTC.MediaStreamTrack @type t() :: %__MODULE__{ - track: MediaStreamTrack.t() | nil + track: MediaStreamTrack.t(), + ssrc: non_neg_integer() | nil, + bytes_received: non_neg_integer(), + packets_received: non_neg_integer() } - defstruct [:track] + defstruct [:track, :ssrc, bytes_received: 0, packets_received: 0] + + @doc false + def receive(receiver, packet, raw_packet) do + %__MODULE__{ + receiver + | ssrc: packet.ssrc, + bytes_received: receiver.bytes_received + byte_size(raw_packet), + packets_received: receiver.packets_received + 1 + } + end + + @doc false + @spec get_stats(t(), non_neg_integer()) :: map() + def get_stats(receiver, timestamp) do + %{ + id: receiver.track.id, + type: :inbound_rtp, + timestamp: timestamp, + ssrc: receiver.ssrc, + bytes_received: receiver.bytes_received, + packets_received: receiver.packets_received + } + end end diff --git a/lib/ex_webrtc/rtp_sender.ex b/lib/ex_webrtc/rtp_sender.ex index e0a03728..4b13487e 100644 --- a/lib/ex_webrtc/rtp_sender.ex +++ b/lib/ex_webrtc/rtp_sender.ex @@ -19,11 +19,23 @@ defmodule ExWebRTC.RTPSender do mid: String.t() | nil, pt: non_neg_integer() | nil, ssrc: non_neg_integer() | nil, - last_seq_num: non_neg_integer() + last_seq_num: non_neg_integer(), + packets_sent: non_neg_integer(), + bytes_sent: non_neg_integer() } @enforce_keys [:id, :last_seq_num] - defstruct @enforce_keys ++ [:track, :codec, :mid, :pt, :ssrc, rtp_hdr_exts: %{}] + defstruct @enforce_keys ++ + [ + :track, + :codec, + :mid, + :pt, + :ssrc, + rtp_hdr_exts: %{}, + packets_sent: 0, + bytes_sent: 0 + ] @doc false @spec new( @@ -47,7 +59,9 @@ defmodule ExWebRTC.RTPSender do pt: pt, ssrc: ssrc, last_seq_num: random_seq_num(), - mid: mid + mid: mid, + packets_sent: 0, + bytes_sent: 0 } end @@ -82,9 +96,31 @@ defmodule ExWebRTC.RTPSender do |> ExRTP.Packet.add_extension(mid_ext) |> ExRTP.Packet.encode() - sender = %{sender | last_seq_num: next_seq_num} + sender = %{ + sender + | last_seq_num: next_seq_num, + packets_sent: sender.packets_sent + 1, + bytes_sent: sender.bytes_sent + byte_size(packet) + } + {packet, sender} end + @doc false + @spec get_stats(t(), non_neg_integer()) :: map() | nil + def get_stats(%{track: nil}, _timestamp), do: nil + + def get_stats(sender, timestamp) do + %{ + timestamp: timestamp, + type: :outbound_rtp, + id: sender.id, + ssrc: sender.ssrc, + kind: sender.track.kind, + packets_sent: sender.packets_sent, + bytes_sent: sender.bytes_sent + } + end + defp random_seq_num(), do: Enum.random(0..65_535) end diff --git a/mix.exs b/mix.exs index e7360c81..0108ff7e 100644 --- a/mix.exs +++ b/mix.exs @@ -33,6 +33,7 @@ defmodule ExWebRTC.MixProject do def application do [ + mod: {ExWebRTC.App, []}, extra_applications: [:logger] ] end @@ -50,7 +51,8 @@ defmodule ExWebRTC.MixProject do defp deps do [ {:ex_sdp, "~> 0.14.0"}, - {:ex_ice, "~> 0.5.0"}, + # {:ex_ice, "~> 0.5.0"}, + {:ex_ice, github: "elixir-webrtc/ex_ice"}, {:ex_dtls, "~> 0.15.0"}, {:ex_libsrtp, "~> 0.7.1"}, {:ex_rtp, "~> 0.3.0"}, diff --git a/mix.lock b/mix.lock index d435b814..96b8a4c4 100644 --- a/mix.lock +++ b/mix.lock @@ -13,7 +13,7 @@ "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"}, "ex_dtls": {:hex, :ex_dtls, "0.15.0", "71a11b0379f4ebeb781548ead0e72194e650cfb6d6c435015c8b36e9e5574f62", [:mix], [{:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "3ff1d9ef6c4b0cc800105979c1c58dc67cada91c593beeae56e05094fdbce865"}, - "ex_ice": {:hex, :ex_ice, "0.5.0", "8e380afb611b4d48a74d4a8016a0284ed11c77400230e2840e2476856cf6d572", [:mix], [{:ex_stun, "~> 0.1.0", [hex: :ex_stun, repo: "hexpm", optional: false]}], "hexpm", "d5a9d5d4b54650385a9b5365b38d919d5158ab2fec5c13669ae45db494516bb6"}, + "ex_ice": {:git, "https://github.com/elixir-webrtc/ex_ice.git", "4c1917bcf1a80b7de12fa3e8c918f7f71abbb8f7", []}, "ex_libsrtp": {:hex, :ex_libsrtp, "0.7.2", "211bd89c08026943ce71f3e2c0231795b99cee748808ed3ae7b97cd8d2450b6b", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:unifex, "~> 1.1", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "2e20645d0d739a4ecdcf8d4810a0c198120c8a2f617f2b75b2e2e704d59f492a"}, "ex_rtcp": {:hex, :ex_rtcp, "0.1.0", "2e02d23fc6ccc7e00aed13358ffdbcb23e34d3a7f35c66cadaa54447383ecae4", [:mix], [], "hexpm", "1c9a7e636f3950fbcefedce31f3e4ca60b84ea80ad519789f9d215167c60cb2b"}, "ex_rtp": {:hex, :ex_rtp, "0.3.0", "d18d0de39875958902816284f79c8cd51ec83d39bdd14edf6a5b069926268a43", [:mix], [], "hexpm", "cec8398237095b02438842cfc74487c3cbaeb7fe29e4c62ae11457a0ddd99754"}, diff --git a/test/ex_webrtc/dtls_transport_test.exs b/test/ex_webrtc/dtls_transport_test.exs index 0db16187..0fe6408d 100644 --- a/test/ex_webrtc/dtls_transport_test.exs +++ b/test/ex_webrtc/dtls_transport_test.exs @@ -41,6 +41,9 @@ defmodule ExWebRTC.DTLSTransportTest do @impl true def set_remote_credentials(ice_pid, _ufrag, _pwd), do: ice_pid + @impl true + def get_stats(_ice_pid), do: %{} + @impl true def stop(ice_pid), do: GenServer.stop(ice_pid)