From bad621c7b698ae98ce01d334bb6db7900372e851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wala?= Date: Mon, 8 Jul 2024 17:04:39 +0200 Subject: [PATCH 1/2] Improve inbound/outbound RTP stats --- lib/ex_webrtc/peer_connection.ex | 68 ++++++++++++++-------------- lib/ex_webrtc/rtp_receiver.ex | 36 +++++++++++++-- lib/ex_webrtc/rtp_sender.ex | 37 +++++++++++++-- lib/ex_webrtc/rtp_transceiver.ex | 49 +++++++++++++++++++- mix.exs | 1 + mix.lock | 1 + test/ex_webrtc/rtp_receiver_test.exs | 10 +++- test/ex_webrtc/rtp_sender_test.exs | 18 ++++++-- 8 files changed, 171 insertions(+), 49 deletions(-) diff --git a/lib/ex_webrtc/peer_connection.ex b/lib/ex_webrtc/peer_connection.ex index 2c988239..078de2c5 100644 --- a/lib/ex_webrtc/peer_connection.ex +++ b/lib/ex_webrtc/peer_connection.ex @@ -18,7 +18,6 @@ defmodule ExWebRTC.PeerConnection do MediaStreamTrack, RTPTransceiver, RTPSender, - RTPReceiver, SDPUtils, SessionDescription, Utils @@ -907,29 +906,8 @@ defmodule ExWebRTC.PeerConnection do end) rtp_stats = - Enum.flat_map(state.transceivers, fn tr -> - tr_stats = %{kind: tr.kind, mid: tr.mid} - - case tr.current_direction do - :sendonly -> - stats = RTPSender.get_stats(tr.sender, timestamp) - [Map.merge(stats, tr_stats)] - - :recvonly -> - stats = RTPReceiver.get_stats(tr.receiver, timestamp) - Enum.map(stats, &Map.merge(&1, tr_stats)) - - :sendrecv -> - sender_stats = RTPSender.get_stats(tr.sender, timestamp) - receiver_stats = RTPReceiver.get_stats(tr.receiver, timestamp) - - [Map.merge(sender_stats, tr_stats)] ++ - Enum.map(receiver_stats, &Map.merge(&1, tr_stats)) - - _other -> - [] - end - end) + state.transceivers + |> Enum.flat_map(&RTPTransceiver.get_stats(&1, timestamp)) |> Map.new(fn stats -> {stats.id, stats} end) stats = %{ @@ -1026,23 +1004,28 @@ defmodule ExWebRTC.PeerConnection do @impl true def handle_cast({:send_pli, track_id, rid}, state) do state.transceivers - |> Enum.find(fn tr -> tr.receiver.track.id == track_id end) + |> Enum.with_index() + |> Enum.find(fn {tr, _idx} -> tr.receiver.track.id == track_id end) |> case do - %{receiver: %{layers: %{^rid => %{ssrc: ssrc}}}} when ssrc != nil -> - encoded = - %ExRTCP.Packet.PayloadFeedback.PLI{sender_ssrc: 1, media_ssrc: ssrc} - |> ExRTCP.Packet.encode() + {tr, idx} -> + case RTPTransceiver.get_pli(tr, rid) do + {pli, tr} -> + encoded = ExRTCP.Packet.encode(pli) + :ok = DTLSTransport.send_rtcp(state.dtls_transport, encoded) + {:noreply, %{state | transceivers: List.replace_at(state.transceivers, idx, tr)}} - :ok = DTLSTransport.send_rtcp(state.dtls_transport, encoded) + :error -> + Logger.warning( + "Unable to send PLI for track #{inspect(track_id)}, rid #{inspect(rid)}" + ) + + {:noreply, state} + end nil -> Logger.warning("Attempted to send PLI for non existent track #{inspect(track_id)}") - - _other -> - Logger.warning("Unable to send PLI for track #{inspect(track_id)}, rid #{inspect(rid)}") + {:noreply, state} end - - {:noreply, state} end @impl true @@ -1852,6 +1835,21 @@ defmodule ExWebRTC.PeerConnection do end end + defp handle_rtcp_packet(state, %ExRTCP.Packet.PayloadFeedback.PLI{} = pli) do + state.transceivers + |> Enum.with_index() + |> Enum.find(fn {tr, _idx} -> tr.sender.ssrc == pli.media_ssrc end) + |> case do + nil -> + state + + {tr, idx} -> + tr = RTPTransceiver.receive_pli(tr, pli) + transceivers = List.replace_at(state.transceivers, idx, tr) + %{state | transceivers: transceivers} + end + end + defp handle_rtcp_packet(state, _packet), do: state defp do_get_description(nil, _candidates), do: nil diff --git a/lib/ex_webrtc/rtp_receiver.ex b/lib/ex_webrtc/rtp_receiver.ex index 5dfbf7e5..c82e4538 100644 --- a/lib/ex_webrtc/rtp_receiver.ex +++ b/lib/ex_webrtc/rtp_receiver.ex @@ -5,7 +5,7 @@ defmodule ExWebRTC.RTPReceiver do require Logger - alias ExRTCP.Packet.TransportFeedback.NACK + alias ExRTCP.Packet.{TransportFeedback.NACK, PayloadFeedback.PLI} alias ExSDP.Attribute.Extmap alias ExWebRTC.{MediaStreamTrack, Utils, RTPCodecParameters} alias __MODULE__.{NACKGenerator, ReportRecorder, SimulcastDemuxer} @@ -29,6 +29,8 @@ defmodule ExWebRTC.RTPReceiver do bytes_received: non_neg_integer(), packets_received: non_neg_integer(), markers_received: non_neg_integer(), + nack_count: non_neg_integer(), + pli_count: non_neg_integer(), report_recorder: ReportRecorder.t(), nack_generator: NACKGenerator.t() } @@ -221,7 +223,13 @@ defmodule ExWebRTC.RTPReceiver do Enum.map_reduce(receiver.layers, [], fn {rid, layer}, nacks -> {nack, nack_generator} = NACKGenerator.get_feedback(layer.nack_generator) nacks = if(nack != nil, do: [nack | nacks], else: nacks) - layer = %{layer | nack_generator: nack_generator} + + layer = %{ + layer + | nack_generator: nack_generator, + nack_count: layer.nack_count + length(nacks) + } + {{rid, layer}, nacks} end) @@ -229,21 +237,39 @@ defmodule ExWebRTC.RTPReceiver do {nacks, receiver} end + @doc false + @spec get_pli(receiver(), String.t() | nil) :: {PLI.t(), receiver()} | :error + def get_pli(receiver, rid) do + case receiver do + %{layers: %{^rid => %{ssrc: ssrc} = layer}} when ssrc != nil -> + layer = %{layer | pli_count: layer.pli_count + 1} + pli = %PLI{sender_ssrc: 1, media_ssrc: ssrc} + {pli, %{receiver | layers: Map.put(receiver.layers, rid, layer)}} + + _other -> + :error + end + end + @doc false @spec get_stats(receiver(), non_neg_integer()) :: [map()] def get_stats(receiver, timestamp) do Enum.map(receiver.layers, fn {rid, layer} -> id = if(rid == nil, do: receiver.track.id, else: "#{receiver.track.id}:#{rid}") + codec = receiver.codec && String.split(receiver.codec.mime_type, "/") |> List.last() %{ id: id, rid: rid, + codec: codec, type: :inbound_rtp, timestamp: timestamp, ssrc: layer.ssrc, bytes_received: layer.bytes_received, packets_received: layer.packets_received, - markers_received: layer.markers_received + markers_received: layer.markers_received, + nack_count: layer.nack_count, + pli_count: layer.pli_count } end) end @@ -259,7 +285,9 @@ defmodule ExWebRTC.RTPReceiver do packets_received: 0, markers_received: 0, report_recorder: report_recorder, - nack_generator: %NACKGenerator{} + nack_generator: %NACKGenerator{}, + nack_count: 0, + pli_count: 0 } end end diff --git a/lib/ex_webrtc/rtp_sender.ex b/lib/ex_webrtc/rtp_sender.ex index e1c3e073..04779df5 100644 --- a/lib/ex_webrtc/rtp_sender.ex +++ b/lib/ex_webrtc/rtp_sender.ex @@ -3,6 +3,7 @@ defmodule ExWebRTC.RTPSender do Implementation of the [RTCRtpSender](https://www.w3.org/TR/webrtc/#rtcrtpsender-interface). """ + alias ExRTCP.Packet.{TransportFeedback.NACK, PayloadFeedback.PLI} alias ExWebRTC.{MediaStreamTrack, RTPCodecParameters, Utils} alias ExSDP.Attribute.Extmap alias __MODULE__.{NACKResponder, ReportRecorder} @@ -24,7 +25,11 @@ defmodule ExWebRTC.RTPSender do rtx_ssrc: non_neg_integer() | nil, packets_sent: non_neg_integer(), bytes_sent: non_neg_integer(), + retransmitted_packets_sent: non_neg_integer(), + retransmitted_bytes_sent: non_neg_integer(), markers_sent: non_neg_integer(), + nack_count: non_neg_integer(), + pli_count: non_neg_integer(), reports?: boolean(), outbound_rtx?: boolean(), report_recorder: ReportRecorder.t(), @@ -86,7 +91,11 @@ defmodule ExWebRTC.RTPSender do mid: mid, packets_sent: 0, bytes_sent: 0, + retransmitted_packets_sent: 0, + retransmitted_bytes_sent: 0, markers_sent: 0, + nack_count: 0, + pli_count: 0, reports?: :rtcp_reports in features, outbound_rtx?: :outbound_rtx in features, report_recorder: %ReportRecorder{clock_rate: codec && codec.clock_rate}, @@ -159,6 +168,17 @@ defmodule ExWebRTC.RTPSender do data = ExRTP.Packet.encode(packet) + sender = + if rtx? do + %{ + sender + | retransmitted_packets_sent: sender.retransmitted_packets_sent + 1, + retransmitted_bytes_sent: sender.retransmitted_bytes_sent + byte_size(data) + } + else + sender + end + sender = %{ sender | packets_sent: sender.packets_sent + 1, @@ -172,15 +192,20 @@ defmodule ExWebRTC.RTPSender do end @doc false - @spec receive_nack(sender(), ExRTCP.Packet.TransportFeedback.NACK.t()) :: - {[ExRTP.Packet.t()], sender()} + @spec receive_nack(sender(), NACK.t()) :: {[ExRTP.Packet.t()], sender()} def receive_nack(sender, nack) do {packets, nack_responder} = NACKResponder.get_rtx(sender.nack_responder, nack) - sender = %{sender | nack_responder: nack_responder} + sender = %{sender | nack_responder: nack_responder, nack_count: sender.nack_count + 1} {packets, sender} end + @doc false + @spec receive_pli(sender(), PLI.t()) :: sender() + def receive_pli(sender, _pli) do + %{sender | pli_count: sender.pli_count + 1} + end + @doc false @spec get_reports(sender()) :: {[ExRTCP.Packet.SenderReport.t()], sender()} def get_reports(sender) do @@ -204,7 +229,11 @@ defmodule ExWebRTC.RTPSender do ssrc: sender.ssrc, packets_sent: sender.packets_sent, bytes_sent: sender.bytes_sent, - markers_sent: sender.markers_sent + markers_sent: sender.markers_sent, + retransmitted_packets_sent: sender.retransmitted_packets_sent, + retransmitted_bytes_sent: sender.retransmitted_bytes_sent, + nack_count: sender.nack_count, + pli_count: sender.pli_count } end end diff --git a/lib/ex_webrtc/rtp_transceiver.ex b/lib/ex_webrtc/rtp_transceiver.ex index 21052570..5c3b156b 100644 --- a/lib/ex_webrtc/rtp_transceiver.ex +++ b/lib/ex_webrtc/rtp_transceiver.ex @@ -15,7 +15,7 @@ defmodule ExWebRTC.RTPTransceiver do Utils } - alias ExRTCP.Packet.{ReceiverReport, SenderReport, TransportFeedback.NACK} + alias ExRTCP.Packet.{ReceiverReport, SenderReport, TransportFeedback.NACK, PayloadFeedback.PLI} @report_interval 1000 @nack_interval 100 @@ -324,6 +324,11 @@ defmodule ExWebRTC.RTPTransceiver do @spec receive_packet(transceiver(), ExRTP.Packet.t(), non_neg_integer()) :: {:ok, {String.t() | nil, ExRTP.Packet.t()}, transceiver()} | :error def receive_packet(transceiver, packet, size) do + :telemetry.execute([:ex_webrtc, :inbound, :rtp], %{size: size}, %{ + peer_connection: self(), + ssrc: packet.ssrc + }) + case check_if_rtx(transceiver.codecs, packet) do {:ok, apt} -> RTPReceiver.receive_rtx(transceiver.receiver, packet, apt) :error -> {:ok, packet, transceiver.receiver} @@ -355,6 +360,13 @@ defmodule ExWebRTC.RTPTransceiver do {packets, transceiver} end + @doc false + @spec receive_pli(transceiver(), PLI.t()) :: transceiver() + def receive_pli(transceiver, pli) do + sender = RTPSender.receive_pli(transceiver.sender, pli) + %{transceiver | sender: sender} + end + @doc false @spec send_packet(transceiver(), ExRTP.Packet.t(), boolean()) :: {binary(), transceiver()} def send_packet(transceiver, packet, rtx?) do @@ -395,6 +407,15 @@ defmodule ExWebRTC.RTPTransceiver do {nacks, transceiver} end + @doc false + @spec get_pli(transceiver(), String.t() | nil) :: {PLI.t(), transceiver()} | :error + def get_pli(transceiver, rid) do + case RTPReceiver.get_pli(transceiver.receiver, rid) do + :error -> :error + {pli, receiver} -> {pli, %{transceiver | receiver: receiver}} + end + end + @doc false @spec to_answer_mline(transceiver(), ExSDP.Media.t(), Keyword.t()) :: ExSDP.Media.t() def to_answer_mline(transceiver, mline, opts) do @@ -467,6 +488,32 @@ defmodule ExWebRTC.RTPTransceiver do %{transceiver | direction: :inactive, stopping: true} end + @doc false + @spec get_stats(transceiver(), non_neg_integer()) :: [map()] + def get_stats(transceiver, timestamp) do + tr_stats = %{kind: transceiver.kind, mid: transceiver.mid} + + case transceiver.current_direction do + :sendonly -> + stats = RTPSender.get_stats(transceiver.sender, timestamp) + [Map.merge(stats, tr_stats)] + + :recvonly -> + stats = RTPReceiver.get_stats(transceiver.receiver, timestamp) + Enum.map(stats, &Map.merge(&1, tr_stats)) + + :sendrecv -> + sender_stats = RTPSender.get_stats(transceiver.sender, timestamp) + receiver_stats = RTPReceiver.get_stats(transceiver.receiver, timestamp) + + [Map.merge(sender_stats, tr_stats)] ++ + Enum.map(receiver_stats, &Map.merge(&1, tr_stats)) + + _other -> + [] + end + end + defp to_mline(transceiver, opts) do pt = Enum.map(transceiver.codecs, fn codec -> codec.payload_type end) diff --git a/mix.exs b/mix.exs index df296b40..0b6106d6 100644 --- a/mix.exs +++ b/mix.exs @@ -63,6 +63,7 @@ defmodule ExWebRTC.MixProject do {:ex_rtp, "~> 0.4.0"}, {:ex_rtcp, "~> 0.4.0"}, {:crc, "~> 0.10"}, + {:telemetry, "~> 1.0"}, # dev/test {:excoveralls, "~> 0.18.0", only: [:dev, :test], runtime: false}, diff --git a/mix.lock b/mix.lock index 0bdf3cab..e290c481 100644 --- a/mix.lock +++ b/mix.lock @@ -39,6 +39,7 @@ "req": {:hex, :req, "0.4.14", "103de133a076a31044e5458e0f850d5681eef23dfabf3ea34af63212e3b902e2", [:mix], [{:aws_signature, "~> 0.3.2", [hex: :aws_signature, repo: "hexpm", optional: true]}, {:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:nimble_ownership, "~> 0.2.0 or ~> 0.3.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "2ddd3d33f9ab714ced8d3c15fd03db40c14dbf129003c4a3eb80fac2cc0b1b08"}, "shmex": {:hex, :shmex, "0.5.1", "81dd209093416bf6608e66882cb7e676089307448a1afd4fc906c1f7e5b94cf4", [:mix], [{:bunch_native, "~> 0.5.0", [hex: :bunch_native, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "c29f8286891252f64c4e1dac40b217d960f7d58def597c4e606ff8fbe71ceb80"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, "unifex": {:hex, :unifex, "1.2.0", "90d1ec5e6d788350e07e474f7bd8b0ee866d6606beb9ca4e20dbb26328712a84", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.4", [hex: :bundlex, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}], "hexpm", "7a8395aabc3ba6cff04bbe5b995de7f899a38eb57f189e49927d6b8b6ccb6883"}, "zarex": {:hex, :zarex, "1.0.5", "58239e3ee5d75f343262bb4df5cf466555a1c689f920e5d3651a9333972f7c7e", [:mix], [], "hexpm", "9fb72ef0567c2b2742f5119a1ba8a24a2fabb21b8d09820aefbf3e592fa9a46a"}, } diff --git a/test/ex_webrtc/rtp_receiver_test.exs b/test/ex_webrtc/rtp_receiver_test.exs index d1652f1c..d1ac2822 100644 --- a/test/ex_webrtc/rtp_receiver_test.exs +++ b/test/ex_webrtc/rtp_receiver_test.exs @@ -28,7 +28,10 @@ defmodule ExWebRTC.RTPReceiverTest do ssrc: 1234, bytes_received: byte_size(raw_packet1), packets_received: 1, - markers_received: 0 + markers_received: 0, + codec: "opus", + nack_count: 0, + pli_count: 0 } ] == RTPReceiver.get_stats(receiver, timestamp) @@ -45,7 +48,10 @@ defmodule ExWebRTC.RTPReceiverTest do ssrc: 1234, bytes_received: byte_size(raw_packet1) + byte_size(raw_packet2), packets_received: 2, - markers_received: 1 + markers_received: 1, + codec: "opus", + nack_count: 0, + pli_count: 0 } ] == RTPReceiver.get_stats(receiver, timestamp) end diff --git a/test/ex_webrtc/rtp_sender_test.exs b/test/ex_webrtc/rtp_sender_test.exs index 6793ada1..c56dcd6a 100644 --- a/test/ex_webrtc/rtp_sender_test.exs +++ b/test/ex_webrtc/rtp_sender_test.exs @@ -64,7 +64,11 @@ defmodule ExWebRTC.RTPSenderTest do ssrc: sender.ssrc, packets_sent: 0, bytes_sent: 0, - markers_sent: 0 + markers_sent: 0, + nack_count: 0, + pli_count: 0, + retransmitted_packets_sent: 0, + retransmitted_bytes_sent: 0 } == RTPSender.get_stats(sender, timestamp) packet = ExRTP.Packet.new(payload) @@ -77,7 +81,11 @@ defmodule ExWebRTC.RTPSenderTest do ssrc: sender.ssrc, packets_sent: 1, bytes_sent: byte_size(data1), - markers_sent: 0 + markers_sent: 0, + nack_count: 0, + pli_count: 0, + retransmitted_packets_sent: 0, + retransmitted_bytes_sent: 0 } == RTPSender.get_stats(sender, timestamp) packet = ExRTP.Packet.new(payload, marker: true) @@ -90,7 +98,11 @@ defmodule ExWebRTC.RTPSenderTest do ssrc: sender.ssrc, packets_sent: 2, bytes_sent: byte_size(data1) + byte_size(data2), - markers_sent: 1 + markers_sent: 1, + nack_count: 0, + pli_count: 0, + retransmitted_packets_sent: 0, + retransmitted_bytes_sent: 0 } == RTPSender.get_stats(sender, timestamp) end end From cfc7392172d2b72ca77a24c606a67bcffed425d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wala?= Date: Tue, 9 Jul 2024 15:38:58 +0200 Subject: [PATCH 2/2] Minor fixes --- lib/ex_webrtc/rtp_receiver.ex | 2 +- lib/ex_webrtc/rtp_transceiver.ex | 5 ----- mix.exs | 1 - mix.lock | 1 - 4 files changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/ex_webrtc/rtp_receiver.ex b/lib/ex_webrtc/rtp_receiver.ex index c82e4538..63552c6d 100644 --- a/lib/ex_webrtc/rtp_receiver.ex +++ b/lib/ex_webrtc/rtp_receiver.ex @@ -227,7 +227,7 @@ defmodule ExWebRTC.RTPReceiver do layer = %{ layer | nack_generator: nack_generator, - nack_count: layer.nack_count + length(nacks) + nack_count: layer.nack_count + if(nack != nil, do: 1, else: 0) } {{rid, layer}, nacks} diff --git a/lib/ex_webrtc/rtp_transceiver.ex b/lib/ex_webrtc/rtp_transceiver.ex index 5c3b156b..56b99cbc 100644 --- a/lib/ex_webrtc/rtp_transceiver.ex +++ b/lib/ex_webrtc/rtp_transceiver.ex @@ -324,11 +324,6 @@ defmodule ExWebRTC.RTPTransceiver do @spec receive_packet(transceiver(), ExRTP.Packet.t(), non_neg_integer()) :: {:ok, {String.t() | nil, ExRTP.Packet.t()}, transceiver()} | :error def receive_packet(transceiver, packet, size) do - :telemetry.execute([:ex_webrtc, :inbound, :rtp], %{size: size}, %{ - peer_connection: self(), - ssrc: packet.ssrc - }) - case check_if_rtx(transceiver.codecs, packet) do {:ok, apt} -> RTPReceiver.receive_rtx(transceiver.receiver, packet, apt) :error -> {:ok, packet, transceiver.receiver} diff --git a/mix.exs b/mix.exs index 0b6106d6..df296b40 100644 --- a/mix.exs +++ b/mix.exs @@ -63,7 +63,6 @@ defmodule ExWebRTC.MixProject do {:ex_rtp, "~> 0.4.0"}, {:ex_rtcp, "~> 0.4.0"}, {:crc, "~> 0.10"}, - {:telemetry, "~> 1.0"}, # dev/test {:excoveralls, "~> 0.18.0", only: [:dev, :test], runtime: false}, diff --git a/mix.lock b/mix.lock index e290c481..0bdf3cab 100644 --- a/mix.lock +++ b/mix.lock @@ -39,7 +39,6 @@ "req": {:hex, :req, "0.4.14", "103de133a076a31044e5458e0f850d5681eef23dfabf3ea34af63212e3b902e2", [:mix], [{:aws_signature, "~> 0.3.2", [hex: :aws_signature, repo: "hexpm", optional: true]}, {:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:nimble_ownership, "~> 0.2.0 or ~> 0.3.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "2ddd3d33f9ab714ced8d3c15fd03db40c14dbf129003c4a3eb80fac2cc0b1b08"}, "shmex": {:hex, :shmex, "0.5.1", "81dd209093416bf6608e66882cb7e676089307448a1afd4fc906c1f7e5b94cf4", [:mix], [{:bunch_native, "~> 0.5.0", [hex: :bunch_native, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "c29f8286891252f64c4e1dac40b217d960f7d58def597c4e606ff8fbe71ceb80"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, - "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, "unifex": {:hex, :unifex, "1.2.0", "90d1ec5e6d788350e07e474f7bd8b0ee866d6606beb9ca4e20dbb26328712a84", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.4", [hex: :bundlex, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}], "hexpm", "7a8395aabc3ba6cff04bbe5b995de7f899a38eb57f189e49927d6b8b6ccb6883"}, "zarex": {:hex, :zarex, "1.0.5", "58239e3ee5d75f343262bb4df5cf466555a1c689f920e5d3651a9333972f7c7e", [:mix], [], "hexpm", "9fb72ef0567c2b2742f5119a1ba8a24a2fabb21b8d09820aefbf3e592fa9a46a"}, }