diff --git a/assets/drawio_schemes/spec_with_audio.drawio b/assets/drawio_schemes/spec_with_audio.drawio new file mode 100644 index 000000000..831ab7674 --- /dev/null +++ b/assets/drawio_schemes/spec_with_audio.drawio @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/drawio_schemes/spec_without_audio.drawio b/assets/drawio_schemes/spec_without_audio.drawio new file mode 100644 index 000000000..a4c64f4b3 --- /dev/null +++ b/assets/drawio_schemes/spec_without_audio.drawio @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/spec_with_audio.svg b/assets/images/spec_with_audio.svg new file mode 100644 index 000000000..9d13f6a25 --- /dev/null +++ b/assets/images/spec_with_audio.svg @@ -0,0 +1,4 @@ + + + +
:file_source
:file_s...
:demuxer
:demuxer
:decoder
:decoder
:ai_filter
:ai_fil...
:encoder
:encoder
:webrtc_sink
:webrtc...
:output
:output
:input
:input
:video
:video
:input
:input
:output
:output
:output
:output
:output
:output
:video
:video
:input
:input
:input
:input
:scratch_remover
:scratch_remover
:audio
:audio
:audio
:audio
:output
:output
:input
:input
\ No newline at end of file diff --git a/assets/images/spec_without_audio.svg b/assets/images/spec_without_audio.svg new file mode 100644 index 000000000..82184489b --- /dev/null +++ b/assets/images/spec_without_audio.svg @@ -0,0 +1,4 @@ + + + +
:file_source
:file_s...
:demuxer
:demuxer
:decoder
:decoder
:ai_filter
:ai_fil...
:encoder
:encoder
:webrtc_sink
:webrtc...
:output
:output
:input
:input
:video
:video
:input
:input
:output
:output
:output
:output
:output
:output
:video
:video
:input
:input
:input
:input
\ No newline at end of file diff --git a/guides/components_lifecycle.md b/guides/components_lifecycle.md new file mode 100644 index 000000000..29c82c08c --- /dev/null +++ b/guides/components_lifecycle.md @@ -0,0 +1,55 @@ +# Lifecycle of Membrane Components + +The lifecycle of Membrane Components is closely related to the execution of Membrane callbacks within these components. While some differences exist among the lifecycles of Membrane Pipelines, Bins, and Elements, they share many similarities. Let's explore the component lifecycle and identify differences depending on the type of component being addressed. + +## Initialization +The first callback executed in every Membrane Component is `handle_init/2`. This function is executed synchronously and blocks the parent process (the process that spawns a pipeline or the parent component) until it is done and actions returned from it are handled. It is advisable to avoid heavy computations within this function. `handle_init/2` is ideally used for spawning children in a Pipeline or Bin through the `:spec` action or parsing some options. + +## Setup +Following `handle_init/2` is `handle_setup/2`, which is executed asynchronously (the parent process does not wait for its completion). This is an optimal time to set up resources or perform other intensive operations required for the component to function properly. + +Note: By default, after `handle_setup/2`, a component's setup is considered complete. This behavior can be modified by returning `setup: :incomplete` from `handle_setup/2`. The component must then mark its setup as completed by returning `setup: :complete` from another callback, like `handle_info/3`, to enter `:playing` playback and go further in lifecycle. + +## Linking pads +For components with pads having `availability: :on_request`, the corresponding `handle_pad_added/3` callbacks are called just after setup is completed, but only if they are linked in the same spec where the component was spawned. Linking a pad in a different spec from the one spawning the component may lead to `handle_pad_added/3` being invoked after `handle_playing/2`. + +## Playing +Once the setup is completed and appropriate `handle_pad_added/3` are invoked, a component can enter the `:playing` state by invoking the `handle_playing/2` callback. Note that: +- Components spawned within the same `:spec` always enter the `:playing` state simultaneously. If the setup of the one component lasts longer, the others will wait. +- Elements and Bins wait for their parent to enter the `playing` state before executing `handle_playing/2`. + +## Processing the data (applies only to Elements) +After `handle_playing/2`, Elements are prepared to process the data flowing through their pads. + +### Events +Events are one type of data that can be sent via an Element's pads and are managed in `handle_event/4`. Events are the only items that can travel both upstream and downstream - all other types of data sent through pads have to follow the direction of the link. + +### Stream Formats +The stream format, which defines the type of data carried by `Membrane.Buffer`s, must be declared before the first data buffer and is managed in `handle_stream_format/4`. + +### Start of Stream +Callback `handle_start_of_stream/3` is invoked just before processing the first `Membrane.Buffer` from a specific input pad. + +### Buffers +The core of multimedia processing is handling `Membrane.Buffer`s, which contain multimedia payload and may also include metadata or timestamps. Buffers are managed within the `handle_buffer/4` callback. + +### Demands +If the Element has pads with `flow_control: :manual`, entering `:playing` playback allows it to send demand on input pads using `:demand` action or to receive the demand via output pads in `handle_demand/5` callback. + +## After processing the data +When an Element determines that it will no longer send buffers from a specific pad, it can return `:end_of_stream` action to that pad. The linked element receives it in `handle_end_of_stream/3`. The parent component (either a Bin or Pipeline) is notified via `handle_element_end_of_stream/4`. + +## Terminating +Typically, the last callback executed within a Membrane Component is `handle_terminate_request`. By default, it returns a `terminate: :normal` action, terminating the component process with the reason `:normal`. This behavior can be modified by overriding the default implementation, but ensure to return a `terminate: reason` elsewhere to avoid termination issues in your Pipeline. + +## Callbacks not strictly related to the lifecycle +Some callbacks are not confined to specific stages of the Membrane Component lifecycle. + +### Handling parent or child notification +`handle_parent_notification/3` and `handle_child_notification/4` can occur at any point during the component's lifecycle and are tasked with managing notifications from a parent or child component, respectively. + +### Handling messages from non-Membrane Erlang Processes +The `handle_info/3` callback is present in all Membrane Components and `handle_call/3` in Membrane Pipelines. These can be triggered at any time while the component is alive, functioning similarly to their counterparts in `GenServer`. + +## Default implementations +If you are new to Membrane and you just read about some new callbacks that are not implemented in your Element or Pipeline, that was supposed to work fine - don't worry! We decided to provide default implementations for the majority of the callbacks (some of the exceptions are `handle_buffer/4` or `handle_demand/5`), which in general is to do nothing or to do the most expected operation. For more details, visit the documentation of `Membrane.Pipeline`, `Membrane.Bin` and `Membrane.Element`. diff --git a/guides/timer.md b/guides/timer.md new file mode 100644 index 000000000..a8b6e2596 --- /dev/null +++ b/guides/timer.md @@ -0,0 +1,108 @@ +# Timer usage examples +Examples below illustrate how to use `:start_timer`, `:timer_interval` and `:stop_timer` actions on the example of `Membrane.Source`, but the API looks the same for all kinds of the Membrane Components + +### Simple example +The example below emits an empty buffer every 100 milliseconds. + +```elixir +defmodule MySource do + use Membrane.Source + + def_output_pad :output, accepted_format: SomeFormat + + @impl true + def handle_init(_ctx, _opts), do: {[], %{}} + + @impl true + def handle_playing(_ctx, state) do + # let's start a timer named :my_timer that will tick every 100 milliseconds ... + + start_timer_action = [ + start_timer: {:my_timer, Membrane.Time.milliseconds(100)} + ] + + # ... and send a stream format + actions = start_timer_action ++ [stream_format: {:output, %SomeFormat{}}] + {actions, state} + end + + # this callback is executed every 100 milliseconds + @impl true + def handle_tick(:my_timer, ctx, state) do + actions = [buffer: {:output, %Membrane.Buffer{payload: ""}}] + {actions, state} + end +end +``` + +### Advanced example +The example below emits an empty buffer every 100 milliseconds if it hasn't been stopped or paused by the parent. + +The source accepts the following notifications from the parent: + - `:pause` - after receiving it the source will pause sending buffers. The paused source can be resumed again. + - `:resume` - resumes sending buffers from the paused source. + - `:stop` - the stopped source won't send any buffer again. + +```elixir +defmodule MyComplexSource + use Membrane.Source + + def_output_pad :output, accepted_format: SomeFormat + + @impl true + def handle_init(_ctx, _opts) do + # after starting a timer, the status will always be either :resumed, :paused + # or :pause_on_next_handle_tick + {[], %{status: nil}} + end + + @impl true + def handle_playing(_ctx, state) do + # let's start a timer named :my_timer ... + start_timer_action = [ + start_timer: {:my_timer, Membrane.Time.milliseconds(100)} + ] + + # ... and send a stream format + actions = start_timer_action ++ [stream_format: {:output, %SomeFormat{}}] + {actions, %{state | status: :resumed}} + end + + @impl true + def handle_parent_notification(notification, _ctx, state) when notification in [:pause, :resume, :stop] do + case notification do + :pause when state.status == :resumed -> + # let's postpone pausing :my_timer to the upcoming handle_tick + {[], %{state | status: :pause_on_next_handle_tick}} + + :resume when state.status == :paused -> + # resume :my_timer by returning :timer_interval action + actions = [timer_interval: {:my_timer, Membrane.Time.milliseconds(100)}] + {actions, %{state | status: :resumed}} + + :resume when state.status == :pause_on_next_handle_tick -> + # case when we receive :pause and :resume notifications without a tick + # between them + {[], %{state | status: :resumed}} + + :stop -> + # stop :my_timer using :stop_timer action + {[stop_timer: :my_timer], %{state | status: :stopped}} + end + end + + @impl true + def handle_tick(:my_timer, _ctx, state) do + case state.status do + :resumed -> + buffer = %Membrane.Buffer{payload: ""} + {[buffer: {:output, buffer}], state} + + :pause_on_next_handle_tick -> + # pause :my_timer using :timer_interval action with interval set to :no_interval + actions = [timer_interval: {:my_timer, :no_interval}] + {actions, %{state | status: :paused}} + end + end +end +``` diff --git a/lib/membrane/bin/action.ex b/lib/membrane/bin/action.ex index 1e259c64f..d8612c59b 100644 --- a/lib/membrane/bin/action.ex +++ b/lib/membrane/bin/action.ex @@ -37,7 +37,59 @@ defmodule Membrane.Bin.Action do Action that instantiates children and links them according to `Membrane.ChildrenSpec`. Children's playback is changed to the current bin playback. - `c:Membrane.Parent.handle_spec_started/3` callback is executed once the children are spawned. + `c:Membrane.Bin.handle_spec_started/3` callback is executed once the children are spawned. + + This is an example of a value that could be passed within `spec` action + ```elixir + child(:file_source, %My.File.Source{path: path}) + |> child(:demuxer, My.Demuxer) + |> via_out(:video) + |> child(:decoder, My.Decoder) + |> child(:ai_filter, %My.AI.Filter{mode: :picasso_effect}) + |> child(:encoder, My.Encoder) + |> via_in(:video) + |> child(:webrtc_sink, My.WebRTC.Sink) + ``` + along with it's visualisation + + [comment]: <> (in case of need to edit the diagram below, open assets/drawio_schemes/spec_without_audio.drawio using draw.io) + ![](assets/images/spec_without_audio.svg) + + Returning another spec (on top of the previous one) + ```elixir + get_child(:demuxer) + |> via_out(:audio) + |> child(:scratch_remover, My.Scratch.Remover) + |> via_in(:audio) + |> get_child(:webrtc_sink) + ``` + + will result in the following children's topology: + + [comment]: <> (in case of need to edit the diagram below, open assets/drawio_schemes/spec_with_audio.drawio using draw.io) + ![](assets/images/spec_with_audio.svg) + + Both specs could also be returned together, in a single `spec` action. + This could done by wrapping them into a two-element list. + ```elixir + [ + # first part + child(:file_source, %My.File.Source{path: path}) + |> child(:demuxer, My.Demuxer) + |> via_out(:video) + |> child(:decoder, My.Decoder) + |> child(:ai_filter, %My.AI.Filter{mode: :picasso_effect}) + |> child(:encoder, My.Encoder) + |> via_in(:video) + |> child(:webrtc_sink, My.WebRTC.Sink), + # second part + get_child(:demuxer) + |> via_out(:audio) + |> child(:scratch_remover, My.Scratch.Remover) + |> via_in(:audio) + |> get_child(:webrtc_sink) + ] + ``` """ @type spec :: {:spec, ChildrenSpec.t()} diff --git a/lib/membrane/element/action.ex b/lib/membrane/element/action.ex index 0d5db32a9..4758e9f3f 100644 --- a/lib/membrane/element/action.ex +++ b/lib/membrane/element/action.ex @@ -223,6 +223,8 @@ defmodule Membrane.Element.Action do This action sets the latency for the element. This action is permitted only in callback `c:Membrane.Element.Base.handle_init/2`. + + The example of usage of these actions is [there](../../../guides/timer.md) """ @type latency :: {:latency, latency :: Membrane.Time.non_neg()} diff --git a/lib/membrane/pipeline/action.ex b/lib/membrane/pipeline/action.ex index 9663fdcd7..14d4ed637 100644 --- a/lib/membrane/pipeline/action.ex +++ b/lib/membrane/pipeline/action.ex @@ -31,6 +31,58 @@ defmodule Membrane.Pipeline.Action do Children's playback is changed to the current pipeline state. `c:Membrane.Pipeline.handle_spec_started/3` callback is executed once it happens. + + This is an example of a value that could be passed within `spec` action + ```elixir + child(:file_source, %My.File.Source{path: path}) + |> child(:demuxer, My.Demuxer) + |> via_out(:video) + |> child(:decoder, My.Decoder) + |> child(:ai_filter, %My.AI.Filter{mode: :picasso_effect}) + |> child(:encoder, My.Encoder) + |> via_in(:video) + |> child(:webrtc_sink, My.WebRTC.Sink) + ``` + along with it's visualisation + + [comment]: <> (in case of need to edit the diagram below, open assets/drawio_schemes/spec_without_audio.drawio using draw.io) + ![](assets/images/spec_without_audio.svg) + + Returning another spec (on top of the previous one) + ```elixir + get_child(:demuxer) + |> via_out(:audio) + |> child(:scratch_remover, My.Scratch.Remover) + |> via_in(:audio) + |> get_child(:webrtc_sink) + ``` + + will result in the following children's topology: + + [comment]: <> (in case of need to edit the diagram below, open assets/drawio_schemes/spec_with_audio.drawio using draw.io) + ![](assets/images/spec_with_audio.svg) + + Both specs could also be returned together, in a single `spec` action. + This could done by wrapping them into a two-element list. + ```elixir + [ + # first part + child(:file_source, %My.File.Source{path: path}) + |> child(:demuxer, My.Demuxer) + |> via_out(:video) + |> child(:decoder, My.Decoder) + |> child(:ai_filter, %My.AI.Filter{mode: :picasso_effect}) + |> child(:encoder, My.Encoder) + |> via_in(:video) + |> child(:webrtc_sink, My.WebRTC.Sink), + # second part + get_child(:demuxer) + |> via_out(:audio) + |> child(:scratch_remover, My.Scratch.Remover) + |> via_in(:audio) + |> get_child(:webrtc_sink) + ] + ``` """ @type spec :: {:spec, ChildrenSpec.t()} diff --git a/mix.exs b/mix.exs index 77b38ce84..43f63b19a 100644 --- a/mix.exs +++ b/mix.exs @@ -68,6 +68,8 @@ defmodule Membrane.Mixfile do "guides/upgrading/v1.0.0-rc0.md", "guides/upgrading/v1.0.0-rc1.md", "guides/upgrading/v1.0.0.md", + "guides/components_lifecycle.md", + "guides/timer.md", LICENSE: [title: "License"] ], formatters: ["html"], diff --git a/mix.lock b/mix.lock index c59ec0d56..299b5d623 100644 --- a/mix.lock +++ b/mix.lock @@ -2,23 +2,23 @@ "bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, - "credo": {:hex, :credo, "1.7.8", "9722ba1681e973025908d542ec3d95db5f9c549251ba5b028e251ad8c24ab8c5", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cb9e87cc64f152f3ed1c6e325e7b894dea8f5ef2e41123bd864e3cd5ceb44968"}, - "dialyxir": {:hex, :dialyxir, "1.4.4", "fb3ce8741edeaea59c9ae84d5cec75da00fa89fe401c72d6e047d11a61f65f70", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cd6111e8017ccd563e65621a4d9a4a1c5cd333df30cebc7face8029cacb4eff6"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, + "credo": {:hex, :credo, "1.7.10", "6e64fe59be8da5e30a1b96273b247b5cf1cc9e336b5fd66302a64b25749ad44d", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "71fbc9a6b8be21d993deca85bf151df023a3097b01e09a2809d460348561d8cd"}, + "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.42", "f23d856f41919f17cd06a493923a722d87a2d684f143a1e663c04a2b93100682", [:mix], [], "hexpm", "6915b6ca369b5f7346636a2f41c6a6d78b5af419d61a611079189233358b8b8b"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, - "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, + "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"}, "excoveralls": {:hex, :excoveralls, "0.18.3", "bca47a24d69a3179951f51f1db6d3ed63bca9017f476fe520eb78602d45f7756", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "746f404fcd09d5029f1b211739afb8fb8575d775b21f6a3908e7ce3e640724c6"}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "junit_formatter": {:hex, :junit_formatter, "3.4.0", "d0e8db6c34dab6d3c4154c3b46b21540db1109ae709d6cf99ba7e7a2ce4b1ac2", [:mix], [], "hexpm", "bb36e2ae83f1ced6ab931c4ce51dd3dbef1ef61bb4932412e173b0cfa259dacd"}, - "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_diff": {:hex, :makeup_diff, "0.1.1", "01498f8c95970081297837eaf4686b6f3813e535795b8421f15ace17a59aea37", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "fadb0bf014bd328badb7be986eadbce1a29955dd51c27a9e401c3045cf24184e"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"}, "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, - "nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"}, + "nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, "qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"},