Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/dtmf/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
23 changes: 23 additions & 0 deletions examples/dtmf/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
dtmf-*.tar

# Temporary files, for example, from tests.
/tmp/
11 changes: 11 additions & 0 deletions examples/dtmf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# DTMF

Receive DTMF tones from a browser and log them in the console.

While in `examples/dtmf` directory

1. Run `mix deps.get`
2. Run `mix run --no-halt`
3. Visit `http://127.0.0.1:8829/index.html` in your browser and start sending events via dial pad.

The IP and port of the app can be configured in `config/config.exs`.
8 changes: 8 additions & 0 deletions examples/dtmf/config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Config

config :logger, level: :info

# normally you take these from env variables in `config/runtime.exs`
config :dtmf,
ip: {127, 0, 0, 1},
port: 8829
15 changes: 15 additions & 0 deletions examples/dtmf/lib/dtmf.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Dtmf do
use Application

@ip Application.compile_env!(:dtmf, :ip)
@port Application.compile_env!(:dtmf, :port)

@impl true
def start(_type, _args) do
children = [
{Bandit, plug: __MODULE__.Router, ip: @ip, port: @port}
]

Supervisor.start_link(children, strategy: :one_for_one)
end
end
207 changes: 207 additions & 0 deletions examples/dtmf/lib/dtmf/peer_handler.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
defmodule Dtmf.PeerHandler do
require Logger

alias ExWebRTC.{
ICECandidate,
MediaStreamTrack,
PeerConnection,
RTPCodecParameters,
RTP.Depayloader,
RTP.JitterBuffer,
SessionDescription
}

@behaviour WebSock

@ice_servers [
%{urls: "stun:stun.l.google.com:19302"}
]

@audio_codecs [
%RTPCodecParameters{
payload_type: 111,
mime_type: "audio/opus",
clock_rate: 48_000,
channels: 2
},
%RTPCodecParameters{
payload_type: 126,
mime_type: "audio/telephone-event",
clock_rate: 8000,
channels: 1
}
]

@impl true
def init(_) do
{:ok, pc} =
PeerConnection.start_link(
ice_servers: @ice_servers,
video_codecs: [],
audio_codecs: @audio_codecs
)

state = %{
peer_connection: pc,
in_audio_track_id: nil,
# The flow of this example is as follows:
# we first feed rtp packets into jitter buffer to
# wait for retransmissions and fix ordering.
# Once ordering and gaps are fixed, we feed packets
# to the depayloader, which detects DTMF events.
# Note that depayloader takes all RTP packets (both Opus and DTMF),
# but ignores those that are not DTMF ones.
# This is to avoid demuxing packets by the user.
jitter_buffer: nil,
jitter_timer: nil,
depayloader: nil
}

{:ok, state}
end

@impl true
def handle_in({msg, [opcode: :text]}, state) do
msg
|> Jason.decode!()
|> handle_ws_msg(state)
end

@impl true
def handle_info({:ex_webrtc, _from, msg}, state) do
handle_webrtc_msg(msg, state)
end

@impl true
def handle_info(:jitter_buffer_timeout, state) do
state = %{state | jitter_timer: nil}

state.jitter_buffer
|> JitterBuffer.handle_timeout()
|> handle_jitter_buffer_result(state)
end

@impl true
def handle_info({:EXIT, pc, reason}, %{peer_connection: pc} = state) do
# Bandit traps exits under the hood so our PeerConnection.start_link
# won't automatically bring this process down.
Logger.info("Peer connection process exited, reason: #{inspect(reason)}")
{:stop, {:shutdown, :pc_closed}, state}
end

@impl true
def terminate(reason, _state) do
Logger.info("WebSocket connection was terminated, reason: #{inspect(reason)}")
end

defp handle_ws_msg(%{"type" => "offer", "data" => data}, state) do
Logger.info("Received SDP offer:\n#{data["sdp"]}")

offer = SessionDescription.from_json(data)
:ok = PeerConnection.set_remote_description(state.peer_connection, offer)

{:ok, answer} = PeerConnection.create_answer(state.peer_connection)
:ok = PeerConnection.set_local_description(state.peer_connection, answer)

answer_json = SessionDescription.to_json(answer)

msg =
%{"type" => "answer", "data" => answer_json}
|> Jason.encode!()

Logger.info("Sent SDP answer:\n#{answer_json["sdp"]}")

{:push, {:text, msg}, state}
end

defp handle_ws_msg(%{"type" => "ice", "data" => data}, state) do
Logger.info("Received ICE candidate: #{data["candidate"]}")

candidate = ICECandidate.from_json(data)
:ok = PeerConnection.add_ice_candidate(state.peer_connection, candidate)
{:ok, state}
end

defp handle_webrtc_msg({:connection_state_change, conn_state}, state) do
Logger.info("Connection state changed: #{conn_state}")

if conn_state == :failed do
{:stop, {:shutdown, :pc_failed}, state}
else
{:ok, state}
end
end

defp handle_webrtc_msg({:ice_candidate, candidate}, state) do
candidate_json = ICECandidate.to_json(candidate)

msg =
%{"type" => "ice", "data" => candidate_json}
|> Jason.encode!()

Logger.info("Sent ICE candidate: #{candidate_json["candidate"]}")

{:push, {:text, msg}, state}
end

defp handle_webrtc_msg({:track, %MediaStreamTrack{kind: :audio, id: id}}, state) do
# Find dtmf codec. Its config (payload type) might have changed during negotiation.
tr =
state.peer_connection
|> PeerConnection.get_transceivers()
|> Enum.find(fn tr -> tr.receiver.track.id == id end)

codec = Enum.find(tr.codecs, fn codec -> codec.mime_type == "audio/telephone-event" end)

if codec == nil do
raise "DTMF for the track has not been negotiated."
end

jitter_buffer = JitterBuffer.new()
{:ok, depayloader} = Depayloader.new(codec)

state = %{
state
| in_audio_track_id: id,
jitter_buffer: jitter_buffer,
depayloader: depayloader
}

{:ok, state}
end

defp handle_webrtc_msg({:rtp, id, nil, packet}, %{in_audio_track_id: id} = state) do
state.jitter_buffer
|> JitterBuffer.insert(packet)
|> handle_jitter_buffer_result(state)
end

defp handle_webrtc_msg(_msg, state), do: {:ok, state}

defp handle_jitter_buffer_result({packets, timeout, jitter_buffer}, state) do
state = %{state | jitter_buffer: jitter_buffer}

# set a new timer only if the previous one has expired
state =
if timeout != nil and state.jitter_timer == nil do
timer = Process.send_after(self(), :jitter_buffer_timeout, timeout)
%{state | jitter_timer: timer}
else
state
end

state =
Enum.reduce(packets, state, fn packet, state ->
case Depayloader.depayload(state.depayloader, packet) do
{nil, depayloader} ->
%{state | depayloader: depayloader}

{event, depayloader} ->
Logger.info("Received DTMF event: #{event.event}")
%{state | depayloader: depayloader}
end
end)

{:ok, state}
end
end
15 changes: 15 additions & 0 deletions examples/dtmf/lib/dtmf/router.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Dtmf.Router do
use Plug.Router

plug(Plug.Static, at: "/", from: :dtmf)
plug(:match)
plug(:dispatch)

get "/ws" do
WebSockAdapter.upgrade(conn, Dtmf.PeerHandler, %{}, [])
end

match _ do
send_resp(conn, 404, "not found")
end
end
32 changes: 32 additions & 0 deletions examples/dtmf/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule Dtmf.MixProject do
use Mix.Project

def project do
[
app: :dtmf,
version: "0.1.0",
elixir: "~> 1.18",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger],
mod: {Dtmf, []}
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:plug, "~> 1.15.0"},
{:bandit, "~> 1.2.0"},
{:websock_adapter, "~> 0.5.0"},
{:jason, "~> 1.4.0"},
{:ex_webrtc, path: "../../."}
]
end
end
36 changes: 36 additions & 0 deletions examples/dtmf/mix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
%{
"bandit": {:hex, :bandit, "1.2.3", "a98d664a96fec23b68e776062296d76a94b4459795b38209f4ae89cb4225709c", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "3e29150245a9b5f56944434e5240966e75c917dad248f689ab589b32187a81af"},
"bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"},
"bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"},
"bundlex": {:hex, :bundlex, "1.5.4", "3726acd463f4d31894a59bbc177c17f3b574634a524212f13469f41c4834a1d9", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:qex, "~> 0.5", [hex: :qex, repo: "hexpm", optional: false]}, {:req, ">= 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:zarex, "~> 1.0", [hex: :zarex, repo: "hexpm", optional: false]}], "hexpm", "e745726606a560275182a8ac1c8ebd5e11a659bb7460d8abf30f397e59b4c5d2"},
"crc": {:hex, :crc, "0.10.5", "ee12a7c056ac498ef2ea985ecdc9fa53c1bfb4e53a484d9f17ff94803707dfd8", [:mix, :rebar3], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3e673b6495a9525c5c641585af1accba59a1eb33de697bedf341e247012c2c7f"},
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
"elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"},
"ex_dtls": {:hex, :ex_dtls, "0.17.0", "dbe1d494583a307c26148cb5ea5d7c14e65daa8ec96cc73002cc3313ce4b9a81", [:mix], [{:bundlex, "~> 1.5.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "3eaa7221ec08fa9e4bc9430e426cbd5eb4feb8d8f450b203cf39b2114a94d713"},
"ex_ice": {:hex, :ex_ice, "0.12.0", "b52ec3ff878d5fb632ef9facc7657dfdf59e2ff9f23e634b0918e6ce1a05af48", [:mix], [{:elixir_uuid, "~> 1.0", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:ex_stun, "~> 0.2.0", [hex: :ex_stun, repo: "hexpm", optional: false]}, {:ex_turn, "~> 0.2.0", [hex: :ex_turn, repo: "hexpm", optional: false]}], "hexpm", "a86024a5fbf9431082784be4bb3606d3cde9218fb325a9f208ccd6e0abfd0d73"},
"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.4.0", "f9e515462a9581798ff6413583a25174cfd2101c94a2ebee871cca7639886f0a", [:mix], [], "hexpm", "28956602cf210d692fcdaf3f60ca49681634e1deb28ace41246aee61ee22dc3b"},
"ex_rtp": {:hex, :ex_rtp, "0.4.0", "1f1b5c1440a904706011e3afbb41741f5da309ce251cb986690ce9fd82636658", [:mix], [], "hexpm", "0f72d80d5953a62057270040f0f1ee6f955c08eeae82ac659c038001d7d5a790"},
"ex_sdp": {:hex, :ex_sdp, "1.1.1", "1a7b049491e5ec02dad9251c53d960835dc5631321ae978ec331831f3e4f6d5f", [:mix], [{:bunch, "~> 1.3", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}], "hexpm", "1b13a72ac9c5c695b8824dbdffc671be8cbb4c0d1ccb4ff76a04a6826759f233"},
"ex_stun": {:hex, :ex_stun, "0.2.0", "feb1fc7db0356406655b2a617805e6c712b93308c8ea2bf0ba1197b1f0866deb", [:mix], [], "hexpm", "1e01ba8290082ccbf37acaa5190d1f69b51edd6de2026a8d6d51368b29d115d0"},
"ex_turn": {:hex, :ex_turn, "0.2.0", "4e1f9b089e9a5ee44928d12370cc9ea7a89b84b2f6256832de65271212eb80de", [:mix], [{:ex_stun, "~> 0.2.0", [hex: :ex_stun, repo: "hexpm", optional: false]}], "hexpm", "08e884f0af2c4a147e3f8cd4ffe33e3452a256389f0956e55a8c4d75bf0e74cd"},
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"membrane_precompiled_dependency_provider": {:hex, :membrane_precompiled_dependency_provider, "0.1.2", "8af73b7dc15ba55c9f5fbfc0453d4a8edfb007ade54b56c37d626be0d1189aba", [:mix], [{:bundlex, "~> 1.4", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "7fe3e07361510445a29bee95336adde667c4162b76b7f4c8af3aeb3415292023"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"},
"req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{: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, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"},
"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.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"thousand_island": {:hex, :thousand_island, "1.3.13", "d598c609172275f7b1648c9f6eddf900e42312b09bfc2f2020358f926ee00d39", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5a34bdf24ae2f965ddf7ba1a416f3111cfe7df50de8d66f6310e01fc2e80b02a"},
"unifex": {:hex, :unifex, "1.2.1", "6841c170a6e16509fac30b19e4e0a19937c33155a59088b50c15fc2c36251b6b", [: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", "8c9d2e3c48df031e9995dd16865bab3df402c0295ba3a31f38274bb5314c7d37"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
"zarex": {:hex, :zarex, "1.0.5", "58239e3ee5d75f343262bb4df5cf466555a1c689f920e5d3651a9333972f7c7e", [:mix], [], "hexpm", "9fb72ef0567c2b2742f5119a1ba8a24a2fabb21b8d09820aefbf3e592fa9a46a"},
}
Loading