Skip to content

Commit a3cd06c

Browse files
committed
Merge remote-tracking branch 'origin/master' into use-membrane_transcoder_plugin
2 parents 1095be1 + 5859bd1 commit a3cd06c

15 files changed

+226
-307
lines changed

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ For more examples, see [examples.livemd](examples.livemd).
7070
|---|---|---|
7171
| MP4 | `"*.mp4"` | `"*.mp4"` |
7272
| WebRTC | `{:webrtc, signaling}` | `{:webrtc, signaling}` |
73+
| WHIP | `{:whip, "http://*", token: "token"}` | `{:whip, "http://*", token: "token"}` |
7374
| RTMP | `"rtmp://*"` | _not supported_ |
7475
| RTSP | `"rtsp://*"` | _not supported_ |
7576
| RTP | `{:rtp, opts}` | _not yet supported_ |
@@ -112,7 +113,7 @@ The CLI API is a direct mapping of the Elixir API:
112113
For example:
113114

114115
```elixir
115-
Boombox.run(input: "file.mp4", output: {:webrtc, "ws://localhost:8830"})
116+
Boombox.run(input: "file.mp4", output: {:whip, "http://localhost:3721", token: "token"})
116117
Boombox.run(
117118
input:
118119
{:rtp,
@@ -127,7 +128,7 @@ Boombox.run(
127128
are equivalent to:
128129

129130
```sh
130-
./boombox -i file.mp4 -o --webrtc ws://localhost:8830
131+
./boombox -i file.mp4 -o --whip http://localhost:3721 --token token
131132
./boombox -i --rtp --port 50001 --audio-encoding AAC --audio-specific-config a13f --aac-bitrate-mode hbr -o index.m3u8
132133
```
133134

bin/boombox_local

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#/bin/sh
2+
mix run -e 'Logger.configure(level: :info);Boombox.run_cli()' -- $@

boombox_examples_data/webrtc_from_browser.html

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ <h1>Boombox stream WebRTC from browser example</h1>
3232
const localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
3333
preview.srcObject = localStream;
3434
const pc = new RTCPeerConnection(pcConfig);
35+
window.pc = pc; // for debugging purposes
3536

3637
pc.onicecandidate = event => {
3738
if (event.candidate === null) return;

boombox_examples_data/webrtc_to_browser.html

+8-2
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
style="background-color: black; color: white; font-family: Arial, Helvetica, sans-serif; min-height: 100vh; margin: 0px; padding: 5px 0px 5px 0px">
1313
<main>
1414
<h1>Boombox stream WebRTC to browser example</h1>
15-
<div id="status"></div>
1615
<div>
1716
Boombox URL: <input type="text" value="ws://localhost:8830" id="url" /> <button id="button">Connect</button>
1817
</div>
18+
<div id="status"></div>
1919
<br>
2020
<video id="videoPlayer" controls muted autoplay></video>
2121
</main>
@@ -30,6 +30,7 @@ <h1>Boombox stream WebRTC to browser example</h1>
3030
videoPlayer.srcObject = new MediaStream();
3131

3232
const pc = new RTCPeerConnection(pcConfig);
33+
window.pc = pc; // for debugging purposes
3334
pc.ontrack = event => videoPlayer.srcObject.addTrack(event.track);
3435
videoPlayer.play();
3536
pc.onicecandidate = event => {
@@ -42,6 +43,7 @@ <h1>Boombox stream WebRTC to browser example</h1>
4243
pc.onconnectionstatechange = () => {
4344
if (pc.connectionState == "connected") {
4445
connStatus.innerHTML = "Connected";
46+
button.innerHTML = "Disconnect";
4547
}
4648
}
4749

@@ -67,7 +69,11 @@ <h1>Boombox stream WebRTC to browser example</h1>
6769
const connect = () => {
6870
const ws = new WebSocket(url.value);
6971
ws.onopen = () => connectRTC(ws);
70-
ws.onclose = event => console.log("WebSocket connection was terminated:", event);
72+
ws.onclose = event => {
73+
console.log("WebSocket connection was terminated:", event);
74+
connStatus.innerHTML = "Disconnected";
75+
button.innerHTML = "Connect";
76+
}
7177
}
7278

7379
button.onclick = connect;

boombox_examples_data/whip.html

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
8+
<title>Membrane WebRTC WHIP/WHEP Example</title>
9+
</head>
10+
11+
<body
12+
style="background-color: black; color: white; font-family: Arial, Helvetica, sans-serif; min-height: 100vh; margin: 0px; padding: 5px 0px 5px 0px">
13+
<h1>Boombox WHIP Example</h1>
14+
<div>
15+
Boombox URL: <input type="text" value="http://localhost:8829" id="url" />
16+
Token: <input type="text" value="whip_it!" id="token" />
17+
<button id="button">Connect</button>
18+
</div>
19+
<div id="status"></div>
20+
<br>
21+
<video id="preview" autoplay muted></video>
22+
<script type="module">
23+
import { WHIPClient } from 'https://cdn.jsdelivr.net/npm/[email protected]/whip.js'
24+
25+
const button = document.getElementById("button");
26+
const connStatus = document.getElementById("status");
27+
const preview = document.getElementById("preview");
28+
const url = document.getElementById("url");
29+
const status = document.getElementById("status");
30+
const token = document.getElementById("token");
31+
const pcConfig = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] };
32+
const mediaConstraints = { video: true, audio: true };
33+
34+
const connect = async () => {
35+
connStatus.innerHTML = "Connecting..."
36+
const localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
37+
preview.srcObject = localStream;
38+
const pc = new RTCPeerConnection(pcConfig);
39+
window.pc = pc; // for debugging purposes
40+
for (const track of localStream.getTracks()) { pc.addTransceiver(track, { 'direction': 'sendonly' }) }
41+
const whip = new WHIPClient();
42+
await whip.publish(pc, url.value, token.value);
43+
connStatus.innerHTML = "Connected";
44+
button.innerHTML = "Disconnect";
45+
button.onclick = async () => {
46+
await whip.stop();
47+
status.innerHTML = "Disconnected";
48+
button.onclick = connect;
49+
}
50+
}
51+
52+
button.onclick = connect;
53+
</script>
54+
</body>
55+
56+
</html>

examples.livemd

+3-3
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,14 @@ Boombox.run(input: {:webrtc, "ws://localhost:8829"}, output: {:webrtc, "ws://loc
8484

8585
<!-- livebook:{"branch_parent_index":0} -->
8686

87-
## Record WebRTC to MP4
87+
## Record WebRTC via WHIP to MP4
8888

89-
To send the stream, visit http://localhost:1234/webrtc_from_browser.html.
89+
To send the stream, visit http://localhost:1234/whip.html.
9090

9191
Note: don't stop this cell to finish recording - click 'disconnect' or close the browser tab instead, so the recording is finalized properly.
9292

9393
```elixir
94-
Boombox.run(input: {:webrtc, "ws://localhost:8829"}, output: "#{out_dir}/webrtc_to_mp4.mp4")
94+
Boombox.run(input: {:whip, "http://localhost:8829", token: "whip_it!"}, output: "#{out_dir}/webrtc_to_mp4.mp4")
9595
```
9696

9797
```elixir

lib/boombox.ex

+10
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ defmodule Boombox do
6969
(path_or_uri :: String.t())
7070
| {:mp4, location :: String.t(), transport: :file | :http}
7171
| {:webrtc, webrtc_signaling()}
72+
| {:whip, uri :: String.t(), token: String.t()}
7273
| {:rtmp, (uri :: String.t()) | (client_handler :: pid)}
7374
| {:rtsp, url :: String.t()}
7475
| {:rtp, in_rtp_opts()}
@@ -78,6 +79,7 @@ defmodule Boombox do
7879
(path_or_uri :: String.t())
7980
| {:mp4, location :: String.t()}
8081
| {:webrtc, webrtc_signaling()}
82+
| {:whip, uri :: String.t(), [{:token, String.t()} | {bandit_option :: atom(), term()}]}
8183
| {:hls, location :: String.t()}
8284
| {:rtp, out_rtp_opts()}
8385
| {:stream, out_stream_opts()}
@@ -190,6 +192,14 @@ defmodule Boombox do
190192
{:webrtc, uri} when is_binary(uri) ->
191193
value
192194

195+
{:whip, uri} when is_binary(uri) ->
196+
parse_opt!(direction, {:whip, uri, []})
197+
198+
{:whip, uri, opts} when is_binary(uri) and is_list(opts) ->
199+
if Keyword.keyword?(opts) do
200+
{:webrtc, {:whip, uri, opts}}
201+
end
202+
193203
{:rtmp, arg} when direction == :input and (is_binary(arg) or is_pid(arg)) ->
194204
value
195205

lib/boombox/utils/cli.ex

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ defmodule Boombox.Utils.CLI do
2222
audio_specific_config: {:string, :binary},
2323
pps: {:string, :binary},
2424
sps: {:string, :binary},
25-
vps: {:string, :binary}
25+
vps: {:string, :binary},
26+
whip: {:string, :string},
27+
token: {:string, :string}
2628
]
2729

2830
@spec parse_argv([String.t()]) ::

lib/boombox/webrtc.ex

+52-12
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,17 @@ defmodule Boombox.WebRTC do
33

44
import Membrane.ChildrenSpec
55
require Membrane.Pad, as: Pad
6-
alias Membrane.WebRTC.SimpleWebSocketServer
76
alias Boombox.Pipeline.{Ready, State, Wait}
8-
alias Membrane.{H264, RemoteStream, VP8}
7+
alias Membrane.{H264, RemoteStream, VP8, WebRTC}
98
alias Membrane.Pipeline.CallbackContext
10-
alias Membrane.WebRTC.SignalingChannel
119

1210
@type output_webrtc_state :: %{negotiated_video_codecs: [:vp8 | :h264] | nil}
1311
@type webrtc_sink_new_tracks :: [%{id: term, kind: :audio | :video}]
1412

1513
@spec create_input(Boombox.webrtc_signaling(), Boombox.output(), CallbackContext.t(), State.t()) ::
1614
Wait.t()
1715
def create_input(signaling, output, ctx, state) do
18-
signaling = resolve_signaling(signaling, ctx.utility_supervisor)
16+
signaling = resolve_signaling(signaling, :input, ctx.utility_supervisor)
1917

2018
keyframe_interval =
2119
case output do
@@ -40,7 +38,7 @@ defmodule Boombox.WebRTC do
4038
end
4139

4240
spec =
43-
child(:webrtc_input, %Membrane.WebRTC.Source{
41+
child(:webrtc_input, %WebRTC.Source{
4442
signaling: signaling,
4543
preferred_video_codec: preferred_video_codec,
4644
allowed_video_codecs: allowed_video_codecs,
@@ -50,7 +48,7 @@ defmodule Boombox.WebRTC do
5048
%Wait{actions: [spec: spec]}
5149
end
5250

53-
@spec handle_input_tracks(Membrane.WebRTC.Source.new_tracks()) :: Ready.t()
51+
@spec handle_input_tracks(WebRTC.Source.new_tracks()) :: Ready.t()
5452
def handle_input_tracks(tracks) do
5553
track_builders =
5654
Map.new(tracks, fn
@@ -75,11 +73,11 @@ defmodule Boombox.WebRTC do
7573
@spec create_output(Boombox.webrtc_signaling(), CallbackContext.t(), State.t()) ::
7674
{Ready.t() | Wait.t(), State.t()}
7775
def create_output(signaling, ctx, state) do
78-
signaling = resolve_signaling(signaling, ctx.utility_supervisor)
76+
signaling = resolve_signaling(signaling, :output, ctx.utility_supervisor)
7977
startup_tracks = if webrtc_input?(state), do: [:audio, :video], else: []
8078

8179
spec =
82-
child(:webrtc_output, %Membrane.WebRTC.Sink{
80+
child(:webrtc_output, %WebRTC.Sink{
8381
signaling: signaling,
8482
tracks: startup_tracks,
8583
video_codec: [:vp8, :h264]
@@ -91,7 +89,7 @@ defmodule Boombox.WebRTC do
9189
if webrtc_input?(state) do
9290
# let's spawn websocket server for webrtc source before the source starts
9391
{:webrtc, input_signaling} = state.input
94-
signaling_channel = resolve_signaling(input_signaling, ctx.utility_supervisor)
92+
signaling_channel = resolve_signaling(input_signaling, :input, ctx.utility_supervisor)
9593
state = %{state | input: {:webrtc, signaling_channel}}
9694

9795
{%Wait{actions: [spec: spec]}, state}
@@ -189,16 +187,58 @@ defmodule Boombox.WebRTC do
189187
%Ready{actions: [spec: spec], eos_info: Map.values(tracks)}
190188
end
191189

192-
defp resolve_signaling(%SignalingChannel{} = signaling, _utility_supervisor) do
190+
defp resolve_signaling(
191+
%WebRTC.SignalingChannel{} = signaling,
192+
_direction,
193+
_utility_supervisor
194+
) do
193195
signaling
194196
end
195197

196-
defp resolve_signaling(uri, utility_supervisor) when is_binary(uri) do
198+
defp resolve_signaling({:whip, uri, opts}, :input, utility_supervisor) do
199+
uri = URI.new!(uri)
200+
{:ok, ip} = :inet.getaddr(~c"#{uri.host}", :inet)
201+
setup_whip_server([ip: ip, port: uri.port] ++ opts, utility_supervisor)
202+
end
203+
204+
defp resolve_signaling({:whip, uri, opts}, :output, utility_supervisor) do
205+
signaling = WebRTC.SignalingChannel.new()
206+
207+
Membrane.UtilitySupervisor.start_link_child(
208+
utility_supervisor,
209+
{WebRTC.WhipClient, [signaling: signaling, uri: uri] ++ opts}
210+
)
211+
212+
signaling
213+
end
214+
215+
defp resolve_signaling(uri, _direction, utility_supervisor) when is_binary(uri) do
197216
uri = URI.new!(uri)
198217
{:ok, ip} = :inet.getaddr(~c"#{uri.host}", :inet)
199218
opts = [ip: ip, port: uri.port]
200219

201-
SimpleWebSocketServer.start_link_supervised(utility_supervisor, opts)
220+
WebRTC.SimpleWebSocketServer.start_link_supervised(utility_supervisor, opts)
221+
end
222+
223+
defp setup_whip_server(opts, utility_supervisor) do
224+
signaling = WebRTC.SignalingChannel.new()
225+
clients_cnt = :atomics.new(1, [])
226+
{valid_token, opts} = Keyword.pop(opts, :token)
227+
228+
handle_new_client = fn token ->
229+
cond do
230+
valid_token not in [nil, token] -> {:error, :invalid_token}
231+
:atomics.add_get(clients_cnt, 1, 1) > 1 -> {:error, :already_connected}
232+
true -> {:ok, signaling}
233+
end
234+
end
235+
236+
Membrane.UtilitySupervisor.start_child(utility_supervisor, {
237+
WebRTC.WhipServer,
238+
[handle_new_client: handle_new_client] ++ opts
239+
})
240+
241+
signaling
202242
end
203243

204244
defp webrtc_input?(%{input: {:webrtc, _signalling}}), do: true

mix.exs

+2-1
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ defmodule Boombox.Mixfile do
4646
defp deps do
4747
[
4848
{:membrane_core, "~> 1.1"},
49-
{:membrane_webrtc_plugin, "~> 0.23.2"},
5049
{:membrane_transcoder_plugin, "~> 0.1.2"},
50+
# {:membrane_webrtc_plugin, "~> 0.23.2"},
51+
{:membrane_webrtc_plugin, github: "membraneframework/membrane_webrtc_plugin"},
5152
{:membrane_mp4_plugin, "~> 0.35.2"},
5253
{:membrane_realtimer_plugin, "~> 0.9.0"},
5354
{:membrane_http_adaptive_stream_plugin, "~> 0.18.5"},

mix.lock

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
%{
2-
"bandit": {:hex, :bandit, "1.6.5", "24096d6232e0d050096acec96a0a382c44de026f9b591b883ed45497e1ef4916", [:mix], [{:hpax, "~> 1.0", [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", "b6b91f630699c8b41f3f0184bd4f60b281e19a336ad9dc1a0da90637b6688332"},
2+
"bandit": {:hex, :bandit, "1.6.6", "f2019a95261d400579075df5bc15641ba8e446cc4777ede6b4ec19e434c3340d", [:mix], [{:hpax, "~> 1.0", [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", "ceb19bf154bc2c07ee0c9addf407d817c48107e36a66351500846fc325451bf9"},
33
"bimap": {:hex, :bimap, "1.3.0", "3ea4832e58dc83a9b5b407c6731e7bae87458aa618e6d11d8e12114a17afa4b3", [:mix], [], "hexpm", "bf5a2b078528465aa705f405a5c638becd63e41d280ada41e0f77e6d255a10b4"},
44
"bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"},
55
"bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"},
@@ -89,7 +89,7 @@
8989
"membrane_vp8_format": {:hex, :membrane_vp8_format, "0.5.0", "a589c20bb9d97ddc9b717684d00cefc84e2500ce63a0c33c4b9618d9b2f9b2ea", [:mix], [], "hexpm", "d29e0dae4bebc6838e82e031c181fe626d168c687e4bc617c1d0772bdeed19d5"},
9090
"membrane_vp9_format": {:hex, :membrane_vp9_format, "0.5.0", "c6a4f2cbfc39dba5d80ad8287162c52b5cf6488676bd64435c1ac957bd16e66f", [:mix], [], "hexpm", "68752d8cbe7270ec222fc84a7d1553499f0d8ff86ef9d9e89f8955d49e20278e"},
9191
"membrane_vpx_plugin": {:hex, :membrane_vpx_plugin, "0.3.0", "60404d1b1511b4c62ba6bbf7b6212570f1732ba477015c4072e0aa33e18a8809", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:membrane_raw_video_format, "~> 0.4.0", [hex: :membrane_raw_video_format, repo: "hexpm", optional: false]}, {:membrane_vp8_format, "~> 0.5.0", [hex: :membrane_vp8_format, repo: "hexpm", optional: false]}, {:membrane_vp9_format, "~> 0.5.0", [hex: :membrane_vp9_format, repo: "hexpm", optional: false]}, {:unifex, "~> 1.2", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "effa7762bbf73efd8d21d0978bce79538414719284194db97672afbce665b56a"},
92-
"membrane_webrtc_plugin": {:hex, :membrane_webrtc_plugin, "0.23.2", "5f3aa18d54d808fcefc89f0047300d840eccc49af87a729b76907df987dd9074", [:mix], [{:bandit, "~> 1.2", [hex: :bandit, repo: "hexpm", optional: false]}, {:corsica, "~> 2.0", [hex: :corsica, repo: "hexpm", optional: false]}, {:ex_webrtc, "~> 0.4.0", [hex: :ex_webrtc, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.1", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_rtp_h264_plugin, "~> 0.20.1", [hex: :membrane_rtp_h264_plugin, repo: "hexpm", optional: false]}, {:membrane_rtp_opus_plugin, "~> 0.10.0", [hex: :membrane_rtp_opus_plugin, repo: "hexpm", optional: false]}, {:membrane_rtp_plugin, "~> 0.30.0", [hex: :membrane_rtp_plugin, repo: "hexpm", optional: false]}, {:membrane_rtp_vp8_plugin, "~> 0.9.4", [hex: :membrane_rtp_vp8_plugin, repo: "hexpm", optional: false]}, {:membrane_timestamp_queue, "~> 0.2.0", [hex: :membrane_timestamp_queue, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.0", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "22aad6b69a94ced429091bd24887d54ff908dc61945dfd427c57a7b3b9ed1ac3"},
92+
"membrane_webrtc_plugin": {:git, "https://github.com/membraneframework/membrane_webrtc_plugin.git", "8c1567c212f6ce4dfb06b6d5ed183b37d435336f", []},
9393
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
9494
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
9595
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
@@ -112,7 +112,7 @@
112112
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
113113
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
114114
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
115-
"telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"},
115+
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
116116
"thousand_island": {:hex, :thousand_island, "1.3.9", "095db3e2650819443e33237891271943fad3b7f9ba341073947581362582ab5a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25ab4c07badadf7f87adb4ab414e0ed374e5f19e72503aa85132caa25776e54f"},
117117
"typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
118118
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},

0 commit comments

Comments
 (0)