diff --git a/.gitignore b/.gitignore index ba41f7177..ca2756db4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +tmp/ + .elixir_ls /priv/plts/* !/priv/plts/.gitkeep diff --git a/CHANGELOG.md b/CHANGELOG.md index aa38b3e5c..cce026f89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Deprecate `:links` option for `:unsafely_name_processes_for_observer` in favour of `:report_links_to_observer` configuration entry * [Set label](https://hexdocs.pm/elixir/Process.html#set_label/1) for all the Membrane processes (i.e. for both Membrane components like pipelines, elements and bins and for utility components like subprocess supervisors) * Update required Elixir version to `~> 1.17` +* Add Mix tasks for easier initialization of components [#1059](https://github.com/membraneframework/membrane_core/pull/1059) ## 1.2.6 * Add tutorials [#1007](https://github.com/membraneframework/membrane_core/pull/1007), plugins [#1012](https://github.com/membraneframework/membrane_core/pull/1012) and demos [#1013](https://github.com/membraneframework/membrane_core/pull/1013) to the docs. diff --git a/lib/membrane/bin.ex b/lib/membrane/bin.ex index e57cca83f..08ac17ae6 100644 --- a/lib/membrane/bin.ex +++ b/lib/membrane/bin.ex @@ -99,8 +99,9 @@ defmodule Membrane.Bin do The callback won't be invoked, when you have initiated the pad removal, eg. when you have returned `t:Membrane.Bin.Action.remove_link()` action - which made one of your children's pads be removed. - By default, it does nothing. + which made one of your children's pads be removed. Not having the + callback implemented when a child removes its pad will result in the + bin crashing. """ @callback handle_child_pad_removed( child :: Child.name(), diff --git a/lib/membrane/core/options_specs.ex b/lib/membrane/core/options_specs.ex index ac8fbab19..4d4e1fcee 100644 --- a/lib/membrane/core/options_specs.ex +++ b/lib/membrane/core/options_specs.ex @@ -7,17 +7,17 @@ defmodule Membrane.Core.OptionsSpecs do alias Membrane.Pad @spec options_doc() :: String.t() - def options_doc do + def options_doc() do """ Options are defined by a keyword list, where each key is an option name and is described by another keyword list with following fields: - * `spec:` typespec for value in struct - * `default:` default value for option. If not present, value for this option - will have to be provided each time options struct is created - * `inspector:` function converting fields' value to a string. Used when - creating documentation instead of `inspect/1`, eg. `inspector: &Membrane.Time.inspect/1` - * `description:` string describing an option. It will be used for generating the docs + * `spec:` Typespec of the values this option can assume. Will be used in t() type. + * `default:` Default value for option. If not present, value for this option + will have to be provided each time options struct is created. + * `inspector:` Function converting fields' value to a string. Used when + creating documentation instead of `inspect/1`, eg. `inspector: &Membrane.Time.inspect/1`. + * `description:` String describing the option. It will be used for generating the docs. """ end @@ -55,6 +55,21 @@ defmodule Membrane.Core.OptionsSpecs do end end + @spec pad_options_doc() :: String.t() + def pad_options_doc() do + """ + Pad options are defined by a keyword list, where each key is an option name and + is described by another keyword list with following fields: + + * `spec:` Typespec of the values this option can assume. Will be used in the type definition of pad options for a given pad. + * `default:` Default value for option. If not present, value for this option + will have to be provided each time options the pad is created. + * `inspector:` Function converting fields' value to a string. Used when + creating documentation instead of `inspect/1`, eg. `inspector: &Membrane.Time.inspect/1`. + * `description:` String describing the option. It will be used for generating the docs. + """ + end + @spec def_pad_options(Pad.name(), nil | Keyword.t()) :: {Macro.t(), Macro.t()} def def_pad_options(_pad_name, []) do no_code = diff --git a/lib/membrane/pad.ex b/lib/membrane/pad.ex index 97570282b..16d0f3ef0 100644 --- a/lib/membrane/pad.ex +++ b/lib/membrane/pad.ex @@ -13,6 +13,7 @@ defmodule Membrane.Pad do use Bunch alias Membrane.Buffer + alias Membrane.Core.OptionsSpecs @availability_values [:always, :on_request] @@ -120,6 +121,8 @@ defmodule Membrane.Pad do Demand unit is derived from the first element inside the bin linked to the given input. + + #{OptionsSpecs.pad_options_doc()} """ @type bin_spec :: {name(), @@ -130,6 +133,8 @@ defmodule Membrane.Pad do @typedoc """ Describes how a pad should be declared inside an element. + + #{OptionsSpecs.pad_options_doc()} """ @type element_spec :: {name(), diff --git a/lib/membrane/pipeline.ex b/lib/membrane/pipeline.ex index c424ae666..44214a46f 100644 --- a/lib/membrane/pipeline.ex +++ b/lib/membrane/pipeline.ex @@ -143,8 +143,9 @@ defmodule Membrane.Pipeline do The callback won't be invoked, when you have initiated the pad removal, eg. when you have returned `t:Membrane.Pipeline.Action.remove_link()` - action which made one of your children's pads be removed. - By default, it does nothing. + action which made one of your children's pads be removed. Not having the + callback implemented when a child removes its pad will result in the + pipeline crashing. """ @callback handle_child_pad_removed( child :: Child.name(), diff --git a/lib/mix/tasks/membrane.gen.component.ex b/lib/mix/tasks/membrane.gen.component.ex new file mode 100644 index 000000000..00bbda077 --- /dev/null +++ b/lib/mix/tasks/membrane.gen.component.ex @@ -0,0 +1,88 @@ +[Filter, Endpoint, Sink, Source, Bin, Pipeline] +|> Enum.map(fn component_type -> + component_name = inspect(component_type) + + defmodule Module.concat(Mix.Tasks.Membrane.Gen, component_type) do + @shortdoc "Generates a template for a Membrane #{component_name}" + @moduledoc """ + Generates a template for a Membrane #{component_name} with the provided module name. + + $ mix membrane.gen.#{String.downcase(component_name)} module_name [-l target_location] + + ## Options + * `-l, --location` - If a target location is provided, the #{component_name} will be created there, relative to the `lib` directory. + The filename must also be present and have an `.ex` extension. If location is not provided, then it will be + inferred from the provided module name - it will be converted to lowercase and `.` separators will be interpreted + as directory separators. For example, a #{component_name} with module name `Foo.Bar` will be created at `lib/foo/bar.ex`. + """ + use Mix.Task + + @switches [location: :string] + @aliases [l: :location] + + @impl true + def run(argv) do + do_run("lib", argv) + end + + @spec do_run(binary(), [binary()]) :: any() + def do_run(base_dir, argv) do + {option, argv} = OptionParser.parse!(argv, aliases: @aliases, strict: @switches) + + module_name = + case argv do + [] -> + Mix.raise(""" + Module name not provided. + + This task expects a module name for the newly created #{unquote(component_type)}: + + $ mix membrane.gen.#{String.downcase(unquote(component_name))} My#{unquote(component_name)} + + """) + + [module_name | _rest] -> + module_name + end + + if not valid_module_name?(module_name) do + Mix.raise(""" + Invalid module name, please provide a valid one. + (no other special characters than dots are allowed and the module name as well as dot-separated segments in it must start with uppercase letters). + """) + end + + component_path = + case option do + [] -> Macro.underscore(module_name) <> ".ex" + [{:location, path} | _rest] -> path + end + |> then(&Path.join(base_dir, &1)) + + component_path |> Path.dirname() |> File.mkdir_p!() + + File.write!(component_path, get_component(module_name)) + end + + defp get_component(module_name) do + template = + "../../../templates" + |> Path.expand(__DIR__) + |> Path.join(Macro.underscore(unquote(component_type)) <> ".ex") + |> File.read!() + + template + |> String.split("\n") + |> List.replace_at(0, "defmodule #{module_name} do") + |> Enum.join("\n") + end + + @spec valid_module_name?(String.t()) :: boolean() + defp valid_module_name?(name) do + case Code.string_to_quoted(name) do + {:ok, {:__aliases__, _metadata, _name}} -> true + _other -> false + end + end + end +end) diff --git a/lib/mix/tasks/membrane.gen.ex b/lib/mix/tasks/membrane.gen.ex new file mode 100644 index 000000000..767ebba90 --- /dev/null +++ b/lib/mix/tasks/membrane.gen.ex @@ -0,0 +1,10 @@ +defmodule Mix.Tasks.Membrane.Gen do + @shortdoc "Lists all available Membrane component generators" + @moduledoc @shortdoc + use Mix.Task + + @impl true + def run(_argv) do + Mix.Tasks.Help.run(["--search", "membrane.gen."]) + end +end diff --git a/mix.exs b/mix.exs index 8017b394c..9fc94d352 100644 --- a/mix.exs +++ b/mix.exs @@ -45,7 +45,8 @@ defmodule Membrane.Mixfile do defp dialyzer() do opts = [ plt_local_path: "priv/plts", - flags: [:error_handling, :unmatched_returns] + flags: [:error_handling, :unmatched_returns], + plt_add_apps: [:mix] ] if System.get_env("CI") == "true" do @@ -238,7 +239,8 @@ defmodule Membrane.Mixfile do "GitHub" => link(), "Membrane Framework Homepage" => "https://membrane.stream" }, - files: ~w"lib .formatter.exs mix.exs README* LICENSE* CHANGELOG* #{@hex_packages_path}" + files: + ~w"lib .formatter.exs mix.exs README* LICENSE* CHANGELOG* #{@hex_packages_path} templates" ] end diff --git a/templates/bin.ex b/templates/bin.ex new file mode 100644 index 000000000..2ce3f2741 --- /dev/null +++ b/templates/bin.ex @@ -0,0 +1,154 @@ +defmodule Membrane.TemplateBin do + @moduledoc """ + This is a generated template for a Membrane.Bin. Uncomment the snippets as necessary. + """ + use Membrane.Bin + + require Membrane.Logger + + # def_input_pad :input, + # accepted_format: _any + # availability: :on_request | :always, # default - :always + # max_instances: pos_integer() | :infinity, # relevant only for pads with `availability: :on_request`, default - :infinity + # options: [ + # Same structure as in def_options/1 + # ] + + # def_output_pad :output, + # accepted_format: _any + # availability: :on_request | :always, # default - :always + # max_instances: pos_integer() | :infinity, # relevant only for pads with `availability: :on_request`, default - :infinity + # options: [ + # Same structure as in def_options/1 + # ] + + # This macro also defines a struct of this module, which is then used for providing options + # when instantiating this component. + # def_options some_option: [ + # spec: typespec of the option. + # default: default value - if not set, providing this option will be mandatory. + # inspector: function converting fields' value to a string for documentation purposes. If not set, &inspect/1 will be used. + # description: """ + # Desription of the option. + # """ + # ] + + defmodule State do + # Using this struct is not strictly necessary, but it's considered a good practice + # and is strongly encouraged. Having a state with static fields with defined + # typespecs adds robustness to the codebase and can prevent many bugs. + @moduledoc false + + # When you add new fields to the struct remember to add them to this spec too. + @type t :: %__MODULE__{} + + defstruct [] + end + + # ----------------- + # --- CALLBACKS --- + # ----------------- + # These callbacks have been ordered as they are usually being executed in the lifecycle + # of a typical Membrane component - see https://hexdocs.pm/membrane_core/components_lifecycle.html. + # Most of them are optional and have default implementations, which are present here as commented out code. + # Any exceptions to this rule are mentioned above the relevant callbacks. + + # By default this callback will return with state set to an empty map %{}, + # however we recommend using a dedicated State struct. + @impl true + def handle_init(_ctx, _opts) do + {[], %State{}} + end + + # This callback will be executed only for pads with `availability: :on_request`. + # @impl true + # def handle_pad_added(_pad, _context, state) do + # {[], state} + # end + + # This callback will be executed only for pads with `availability: :on_request`. + # @impl true + # def handle_pad_removed(_pad, _context, state) do + # {[], state} + # end + + # @impl true + # def handle_setup(_context, state) do + # {[], state} + # end + + # @impl true + # def handle_playing(_context, state) do + # {[], state} + # end + + # @impl true + # def handle_info(message, _context, state) do + # Membrane.Logger.warning(""" + # Received message but no handle_info callback has been specified. Ignoring. + # Message: #{inspect(message)}\ + # """) + # + # {[], state} + # end + + # @impl true + # def handle_child_setup_completed(_child, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_child_playing(_child, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_element_start_of_stream(_element, _pad, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_element_end_of_stream(_element, _pad, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_child_notification(_notification, _element, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_parent_notification(_notification, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_crash_group_down(_group_name, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_terminate_request(_ctx, state) do + # {[terminate: :normal], state} + # end + + # @impl true + # def handle_child_terminated(_child, _ctx, state) do + # {[], state} + # end + + # This callback doesn't have a default implementation, but will be called only + # if a `:start_timer` action has been executed. For more information and examples + # of timer usage see https://hexdocs.pm/membrane_core/timer.html. + # @impl true + # def handle_tick(timer_id, context, state) do + # ... + # end + + # This callback doesn't have a default implementation and will be called only if + # a child removes it's own dynamic pad. If not implemented, the bin will crash. + # @impl true + # handle_child_pad_removed(element, pad, ctx, state) do + # ... + # end +end diff --git a/templates/endpoint.ex b/templates/endpoint.ex new file mode 100644 index 000000000..3561a6374 --- /dev/null +++ b/templates/endpoint.ex @@ -0,0 +1,150 @@ +defmodule Membrane.TemplateEndpoint do + @moduledoc """ + This is a generated template for a Membrane.Endpoint. Uncomment the snippets as necessary. + """ + use Membrane.Endpoint + + require Membrane.Logger + + def_input_pad :input, + accepted_format: _any, + flow_control: :auto # | :manual | :push, + # availability: :on_request | :always, # default - :always + # max_instances: pos_integer() | :infinity, # relevant only for pads with `availability: :on_request`, default - :infinity + # demand_unit: :buffers | :bytes, # When using :manual flow control, this needs to be provided + # options: [ + # Same structure as in def_options/1 + # ] + + def_output_pad :output, + accepted_format: _any, + flow_control: :push # | :manual | :auto, + # availability: :on_request | :always, # default - :always + # max_instances: pos_integer() | :infinity, # relevant only for pads with `availability: :on_request`, default - :infinity + # demand_unit: :buffers | :bytes, + # options: [ + # Same structure as in def_options/1 + # ] + + # This macro also defines a struct of this module, which is then used for providing options + # when instantiating this component. + # def_options some_option: [ + # spec: typespec of the option. + # default: default value - if not set, providing this option will be mandatory. + # inspector: function converting fields' value to a string for documentation purposes. If not set, &inspect/1 will be used. + # description: """ + # Desription of the option. + # """ + # ] + + defmodule State do + # Using this struct is not strictly necessary, but it's considered a good practice + # and is strongly encouraged. Having a state with static fields with defined + # typespecs adds robustness to the codebase and can prevent many bugs. + @moduledoc false + + # When you add new fields to the struct remember to add them to this spec too. + @type t :: %__MODULE__{} + + defstruct [] + end + + # ----------------- + # --- CALLBACKS --- + # ----------------- + # These callbacks have been ordered as they are usually being executed in the lifecycle + # of a typical Membrane component - see https://hexdocs.pm/membrane_core/components_lifecycle.html. + # Most of them are optional and have default implementations, which are present here as commented out code. + # Any exceptions to this rule are mentioned above the relevant callbacks. + + # By default this callback will return with state set to an empty map %{}, + # however we recommend using a dedicated State struct. + @impl true + def handle_init(_ctx, _opts) do + {[], %State{}} + end + + # @impl true + # def handle_setup(_context, state) do + # {[], state} + # end + + # @impl true + # def handle_playing(_context, state) do + # {[], state} + # end + + # @impl true + # def handle_info(message, _context, state) do + # Membrane.Logger.warning(""" + # Received message but no handle_info callback has been specified. Ignoring. + # Message: #{inspect(message)}\ + # """) + # + # {[], state} + # end + + # This callback will be executed only for pads with `availability: :on_request`. + # @impl true + # def handle_pad_added(_pad, _context, state) do + # {[], state} + # end + + # This callback will be executed only for pads with `availability: :on_request`. + # @impl true + # def handle_pad_removed(_pad, _context, state) do + # {[], state} + # end + + # @impl true + # def handle_stream_format(_pad, _stream_format, _context, state) do + # {[], state} + # end + + # @impl true + # def handle_start_of_stream(_pad, _context, state) do + # {[], state} + # end + + # This callback doesn't have a default implementation. The one present + # here forwards all buffers received without doing anything else. + @impl true + def handle_buffer(_pad, buffer, _ctx, state) do + {[forward: buffer], state} + end + + # Important: this callback needs to be implemented when any output pads operate + # in `:manual` flow control. + # @impl true + # def handle_demand(pad, size, unit, context, state) do + # ... + # end + + # @impl true + # def handle_event(_pad, _event, _context, state) do + # {[], state} + # end + + # @impl true + # def handle_parent_notification(_notification, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_end_of_stream(_pad, _context, state) do + # {[], state} + # end + + # @impl true + # def handle_terminate_request(_ctx, state) do + # {[terminate: :normal], state} + # end + + # This callback doesn't have a default implementation, but will be called only + # if a `:start_timer` action has been executed. For more information and examples + # of timer usage see https://hexdocs.pm/membrane_core/timer.html. + # @impl true + # def handle_tick(timer_id, context, state) do + # ... + # end +end diff --git a/templates/filter.ex b/templates/filter.ex new file mode 100644 index 000000000..ab4dccbce --- /dev/null +++ b/templates/filter.ex @@ -0,0 +1,150 @@ +defmodule Membrane.TemplateFilter do + @moduledoc """ + This is a generated template for a Membrane.Filter. Uncomment the snippets as necessary. + """ + use Membrane.Filter + + require Membrane.Logger + + def_input_pad :input, + accepted_format: _any, + flow_control: :auto # | :manual | :push, + # availability: :on_request | :always, # default - :always + # max_instances: pos_integer() | :infinity, # relevant only for pads with `availability: :on_request`, default - :infinity + # demand_unit: :buffers | :bytes, # When using :manual flow control, this needs to be provided + # options: [ + # Same structure as in def_options/1 + # ] + + def_output_pad :output, + accepted_format: _any, + flow_control: :auto # | :manual | :push, + # availability: :on_request | :always, # default - :always + # max_instances: pos_integer() | :infinity, # relevant only for pads with `availability: :on_request`, default - :infinity + # demand_unit: :buffers | :bytes, + # options: [ + # Same structure as in def_options/1 + # ] + + # This macro also defines a struct of this module, which is then used for providing options + # when instantiating this component. + # def_options some_option: [ + # spec: typespec of the option. + # default: default value - if not set, providing this option will be mandatory. + # inspector: function converting fields' value to a string for documentation purposes. If not set, &inspect/1 will be used. + # description: """ + # Desription of the option. + # """ + # ] + + defmodule State do + # Using this struct is not strictly necessary, but it's considered a good practice + # and is strongly encouraged. Having a state with static fields with defined + # typespecs adds robustness to the codebase and can prevent many bugs. + @moduledoc false + + # When you add new fields to the struct remember to add them to this spec too. + @type t :: %__MODULE__{} + + defstruct [] + end + + # ----------------- + # --- CALLBACKS --- + # ----------------- + # These callbacks have been ordered as they are usually being executed in the lifecycle + # of a typical Membrane component - see https://hexdocs.pm/membrane_core/components_lifecycle.html. + # Most of them are optional and have default implementations, which are present here as commented out code. + # Any exceptions to this rule are mentioned above the relevant callbacks. + + # By default this callback will return with state set to an empty map %{}, + # however we recommend using a dedicated State struct. + @impl true + def handle_init(_ctx, _opts) do + {[], %State{}} + end + + # @impl true + # def handle_setup(_context, state) do + # {[], state} + # end + + # @impl true + # def handle_playing(_context, state) do + # {[], state} + # end + + # @impl true + # def handle_info(message, _context, state) do + # Membrane.Logger.warning(""" + # Received message but no handle_info callback has been specified. Ignoring. + # Message: #{inspect(message)}\ + # """) + # + # {[], state} + # end + + # This callback will be executed only for pads with `availability: :on_request`. + # @impl true + # def handle_pad_added(_pad, _context, state) do + # {[], state} + # end + + # This callback will be executed only for pads with `availability: :on_request`. + # @impl true + # def handle_pad_removed(_pad, _context, state) do + # {[], state} + # end + + # @impl true + # def handle_stream_format(_pad, _stream_format, _context, state) do + # {[], state} + # end + + # @impl true + # def handle_start_of_stream(_pad, _context, state) do + # {[], state} + # end + + # This callback doesn't have a default implementation. The one present + # here forwards all buffers received without doing anything else. + @impl true + def handle_buffer(_pad, buffer, _ctx, state) do + {[forward: buffer], state} + end + + # Important: this callback needs to be implemented when any output pads operate + # in `:manual` flow control. + # @impl true + # def handle_demand(pad, size, unit, context, state) do + # ... + # end + + # @impl true + # def handle_event(_pad, _event, _context, state) do + # {[], state} + # end + + # @impl true + # def handle_parent_notification(_notification, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_end_of_stream(_pad, _context, state) do + # {[], state} + # end + + # @impl true + # def handle_terminate_request(_ctx, state) do + # {[terminate: :normal], state} + # end + + # This callback doesn't have a default implementation, but will be called only + # if a `:start_timer` action has been executed. For more information and examples + # of timer usage see https://hexdocs.pm/membrane_core/timer.html. + # @impl true + # def handle_tick(timer_id, context, state) do + # ... + # end +end diff --git a/templates/pipeline.ex b/templates/pipeline.ex new file mode 100644 index 000000000..0f0120180 --- /dev/null +++ b/templates/pipeline.ex @@ -0,0 +1,115 @@ +defmodule Membrane.TemplatePipeline do + @moduledoc """ + This is a generated template for a Membrane.Pipeline. Uncomment the snippets as necessary. + """ + use Membrane.Pipeline + + require Membrane.Logger + + defmodule State do + # Using this struct is not strictly necessary, but it's considered a good practice + # and is strongly encouraged. Having a state with static fields with defined + # typespecs adds robustness to the codebase and can prevent many bugs. + @moduledoc false + + # When you add new fields to the struct remember to add them to this spec too. + @type t :: %__MODULE__{} + + defstruct [] + end + + # ----------------- + # --- CALLBACKS --- + # ----------------- + # These callbacks have been ordered as they are usually being executed in the lifecycle + # of a typical Membrane component - see https://hexdocs.pm/membrane_core/components_lifecycle.html. + # Most of them are optional and have default implementations, which are present here as commented out code. + # Any exceptions to this rule are mentioned above the relevant callbacks. + + # By default this callback will return with state set to an empty map %{}, + # however we recommend using a dedicated State struct. + @impl true + def handle_init(_ctx, _opts) do + {[], %State{}} + end + + # @impl true + # def handle_setup(_ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_playing(_ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_info(message, _ctx, state) do + # Membrane.Logger.warning(""" + # Received message but no handle_info callback has been specified. Ignoring. + # Message: #{inspect(message)}\ + # """) + # + # {[], state} + # end + + # @impl true + # def handle_child_setup_completed(_child, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_child_terminated(_child, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_child_playing(_child, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_element_start_of_stream(_element, _pad, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_element_end_of_stream(_element, _pad, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_child_notification(notification, element, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_crash_group_down(_group_name, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_call(message, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_terminate_request(_ctx, state) do + # {[terminate: :normal], state} + # end + + # This callback doesn't have a default implementation, but will be called only + # if a `:start_timer` action has been executed. For more information and examples + # of timer usage see https://hexdocs.pm/membrane_core/timer.html. + # @impl true + # def handle_tick(timer_id, context, state) do + # ... + # end + + # This callback doesn't have a default implementation and will be called only if + # a child removes it's own dynamic pad. If not implemented, the bin will crash. + # @impl true + # handle_child_pad_removed(element, pad, ctx, state) do + # ... + # end +end diff --git a/templates/sink.ex b/templates/sink.ex new file mode 100644 index 000000000..b4e2e1639 --- /dev/null +++ b/templates/sink.ex @@ -0,0 +1,133 @@ +defmodule Membrane.TemplateSink do + @moduledoc """ + This is a generated template for a Membrane.Sink. Uncomment the snippets as necessary. + """ + use Membrane.Sink + + require Membrane.Logger + + def_input_pad :input, + accepted_format: _any, + flow_control: :auto # | :manual | :push, + # availability: :on_request | :always, # default - :always + # max_instances: pos_integer() | :infinity, # relevant only for pads with `availability: :on_request`, default - :infinity + # demand_unit: :buffers | :bytes, # When using :manual flow control, this needs to be provided + # options: [ + # Same structure as in def_options/1 + # ] + + # This macro also defines a struct of this module, which is then used for providing options + # when instantiating this component. + # def_options some_option: [ + # spec: typespec of the option. + # default: default value - if not set, providing this option will be mandatory. + # inspector: function converting fields' value to a string for documentation purposes. If not set, &inspect/1 will be used. + # description: """ + # Desription of the option. + # """ + # ] + + defmodule State do + # Using this struct is not strictly necessary, but it's considered a good practice + # and is strongly encouraged. Having a state with static fields with defined + # typespecs adds robustness to the codebase and can prevent many bugs. + @moduledoc false + + # When you add new fields to the struct remember to add them to this spec too. + @type t :: %__MODULE__{} + + defstruct [] + end + + # ----------------- + # --- CALLBACKS --- + # ----------------- + # These callbacks have been ordered as they are usually being executed in the lifecycle + # of a typical Membrane component - see https://hexdocs.pm/membrane_core/components_lifecycle.html. + # Most of them are optional and have default implementations, which are present here as commented out code. + # Any exceptions to this rule are mentioned above the relevant callbacks. + + # By default this callback will return with state set to an empty map %{}, + # however we recommend using a dedicated State struct. + @impl true + def handle_init(_ctx, _opts) do + {[], %State{}} + end + + # @impl true + # def handle_setup(_context, state) do + # {[], state} + # end + + # @impl true + # def handle_playing(_context, state) do + # {[], state} + # end + + # @impl true + # def handle_info(message, _context, state) do + # Membrane.Logger.warning(""" + # Received message but no handle_info callback has been specified. Ignoring. + # Message: #{inspect(message)}\ + # """) + # + # {[], state} + # end + + # This callback will be executed only for pads with `availability: :on_request`. + # @impl true + # def handle_pad_added(_pad, _context, state) do + # {[], state} + # end + + # This callback will be executed only for pads with `availability: :on_request`. + # @impl true + # def handle_pad_removed(_pad, _context, state) do + # {[], state} + # end + + # @impl true + # def handle_stream_format(_pad, _stream_format, _context, state) do + # {[], state} + # end + + # @impl true + # def handle_start_of_stream(_pad, _context, state) do + # {[], state} + # end + + # This callback doesn't have a default implementation. The one present + # here forwards all buffers received without doing anything else. + @impl true + def handle_buffer(_pad, buffer, _ctx, state) do + {[forward: buffer], state} + end + + # @impl true + # def handle_event(_pad, _event, _context, state) do + # {[], state} + # end + + # @impl true + # def handle_parent_notification(_notification, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_end_of_stream(_pad, _context, state) do + # {[], state} + # end + + # @impl true + # def handle_terminate_request(_ctx, state) do + # {[terminate: :normal], state} + # end + + # This callback doesn't have a default implementation, but will be called only + # if a `:start_timer` action has been executed. For more information and examples + # of timer usage see https://hexdocs.pm/membrane_core/timer.html. + # @impl true + # def handle_tick(timer_id, context, state) do + # ... + # end +end diff --git a/templates/source.ex b/templates/source.ex new file mode 100644 index 000000000..5c9fd2778 --- /dev/null +++ b/templates/source.ex @@ -0,0 +1,118 @@ +defmodule Membrane.TemplateSource do + @moduledoc """ + This is a generated template for a Membrane.Source. Uncomment the snippets as necessary. + """ + use Membrane.Source + + require Membrane.Logger + + def_output_pad :output, + accepted_format: _any, + flow_control: :push # | :manual | :auto, + # availability: :on_request | :always, # default - :always + # max_instances: pos_integer() | :infinity, # relevant only for pads with `availability: :on_request`, default - :infinity + # demand_unit: :buffers | :bytes, + # options: [ + # Same structure as in def_options/1 + # ] + + # This macro also defines a struct of this module, which is then used for providing options + # when instantiating this component. + # def_options some_option: [ + # spec: typespec of the option. + # default: default value - if not set, providing this option will be mandatory. + # inspector: function converting fields' value to a string for documentation purposes. If not set, &inspect/1 will be used. + # description: """ + # Desription of the option. + # """ + # ] + + defmodule State do + # Using this struct is not strictly necessary, but it's considered a good practice + # and is strongly encouraged. Having a state with static fields with defined + # typespecs adds robustness to the codebase and can prevent many bugs. + @moduledoc false + + # When you add new fields to the struct remember to add them to this spec too. + @type t :: %__MODULE__{} + + defstruct [] + end + + # ----------------- + # --- CALLBACKS --- + # ----------------- + # These callbacks have been ordered as they are usually being executed in the lifecycle + # of a typical Membrane component - see https://hexdocs.pm/membrane_core/components_lifecycle.html. + # Most of them are optional and have default implementations, which are present here as commented out code. + # Any exceptions to this rule are mentioned above the relevant callbacks. + + # By default this callback will return with state set to an empty map %{}, + # however we recommend using a dedicated State struct. + @impl true + def handle_init(_ctx, _opts) do + {[], %State{}} + end + + # @impl true + # def handle_setup(_context, state) do + # {[], state} + # end + + # @impl true + # def handle_playing(_context, state) do + # {[], state} + # end + + # @impl true + # def handle_info(message, _context, state) do + # Membrane.Logger.warning(""" + # Received message but no handle_info callback has been specified. Ignoring. + # Message: #{inspect(message)}\ + # """) + # + # {[], state} + # end + + # This callback will be executed only for pads with `availability: :on_request`. + # @impl true + # def handle_pad_added(_pad, _context, state) do + # {[], state} + # end + + # This callback will be executed only for pads with `availability: :on_request`. + # @impl true + # def handle_pad_removed(_pad, _context, state) do + # {[], state} + # end + + # Important: this callback needs to be implemented when any output pads operate + # in `:manual` flow control. + # @impl true + # def handle_demand(pad, size, unit, context, state) do + # ... + # end + + # @impl true + # def handle_event(_pad, _event, _context, state) do + # {[], state} + # end + + # @impl true + # def handle_parent_notification(_notification, _ctx, state) do + # {[], state} + # end + + # @impl true + # def handle_terminate_request(_ctx, state) do + # {[terminate: :normal], state} + # end + + # This callback doesn't have a default implementation, but will be called only + # if a `:start_timer` action has been executed. For more information and examples + # of timer usage see https://hexdocs.pm/membrane_core/timer.html. + # @impl true + # def handle_tick(timer_id, context, state) do + # ... + # end +end diff --git a/test/mix/tasks/membrane.gen_test.exs b/test/mix/tasks/membrane.gen_test.exs new file mode 100644 index 000000000..693838406 --- /dev/null +++ b/test/mix/tasks/membrane.gen_test.exs @@ -0,0 +1,40 @@ +defmodule Mix.Tasks.Membrane.GenTest do + use ExUnit.Case + alias Mix.Tasks.Membrane.Gen + + Enum.each([Filter, Endpoint, Sink, Source, Bin, Pipeline], fn component_type -> + @tag :tmp_dir + test "#{inspect(component_type)} template generates correctly", %{tmp_dir: tmp} do + Module.concat(Gen, unquote(component_type)).do_run(tmp, [inspect(unquote(component_type))]) + + {{:module, module, _contents, __result}, []} = + Path.join(tmp, Macro.underscore(unquote(component_type)) <> ".ex") + |> Code.eval_file() + + attributes = module.__info__(:attributes) + functions = module.__info__(:functions) + + if {:behaviour, [Membrane.Element.WithInputPads]} in attributes do + assert {:handle_buffer, 4} in functions + + assert %{ + name: :input, + direction: :input, + accepted_formats_str: ["_any"], + availability: :always + } = module.membrane_pads()[:input] + end + + if {:behaviour, [Membrane.Element.WithOutputPads]} in attributes do + assert %{ + name: :output, + direction: :output, + accepted_formats_str: ["_any"], + availability: :always + } = module.membrane_pads()[:output] + end + + assert Module.concat(module, State).__info__(:struct) != nil + end + end) +end