Packages
membrane_core
0.8.2
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.7
1.2.6
1.2.5
retired
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.2.0-rc1
1.1.2
1.1.1
1.1.0
1.1.0-rc1
1.1.0-rc0
1.0.1
1.0.0
1.0.0-rc1
1.0.0-rc0
0.12.9
0.12.8
0.12.7
0.12.6
0.12.5
0.12.4
0.12.3
0.12.2
0.12.1
0.12.0
retired
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.2
0.10.1
0.10.0
0.9.0
0.8.2
0.8.1
0.8.0
0.7.0
0.6.1
0.6.0
0.5.3
0.5.2
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.2
0.2.1
0.2.0
0.1.1
0.1.0
Membrane Multimedia Framework (Core)
Current section
Files
Jump to
Current section
Files
lib/membrane/caps/matcher.ex
defmodule Membrane.Caps.Matcher do
@moduledoc """
Module that allows to specify valid caps and verify that they match specification.
Caps specifications (specs) should be in one of the formats:
* simply module name of the desired caps (e.g. `Membrane.Caps.Audio.Raw` or `Raw` with proper alias)
* tuple with module name and keyword list of specs for specific caps fields (e.g. `{Raw, format: :s24le}`)
* list of the formats described above
Field values can be specified in following ways:
* By a raw value for the field (e.g. `:s24le`)
* Using `range/2` for values comparable with `Kernel.<=/2` and `Kernel.>=/2` (e.g. `Matcher.range(0, 255)`)
* With `one_of/1` and a list of valid values (e.g `Matcher.one_of([:u8, :s16le, :s32le])`)
Checks on the values from list are performed recursively i.e. it can contain another `range/2`,
for example `Matcher.one_of([0, Matcher.range(2, 4), Matcher.range(10, 20)])`
If the specs are defined inside of `Membrane.Element.WithInputPads.def_input_pad/2` and
`Membrane.Element.WithOutputPads.def_output_pad/2` module name can be omitted from
`range/2` and `one_of/1` calls.
## Example
Below is a pad definition with an example of specs for caps matcher:
alias Membrane.Caps.Video.Raw
def_input_pad :input,
demand_unit: :buffers,
caps: {Raw, format: one_of([:I420, :I422]), aligned: true}
"""
import Kernel, except: [match?: 2]
alias Bunch
require Record
@type caps_spec_t :: module() | {module(), keyword()}
@type caps_specs_t :: :any | caps_spec_t() | [caps_spec_t()]
defmodule Range do
@moduledoc false
@enforce_keys [:min, :max]
defstruct @enforce_keys
end
@opaque range_t :: %Range{min: any, max: any}
defimpl Inspect, for: Range do
import Inspect.Algebra
@impl true
def inspect(%Range{min: min, max: max}, opts) do
concat(["range(", to_doc(min, opts), ", ", to_doc(max, opts), ")"])
end
end
defmodule OneOf do
@moduledoc false
@enforce_keys [:list]
defstruct @enforce_keys
end
@opaque one_of_t :: %OneOf{list: list()}
defimpl Inspect, for: OneOf do
import Inspect.Algebra
@impl true
def inspect(%OneOf{list: list}, opts) do
concat(["one_of(", to_doc(list, opts), ")"])
end
end
@doc """
Returns opaque specification of range of valid values for caps field.
"""
@spec range(any, any) :: range_t()
def range(min, max) do
%Range{min: min, max: max}
end
@doc """
Returns opaque specification of list of valid values for caps field.
"""
@spec one_of(list()) :: one_of_t()
def one_of(values) when is_list(values) do
%OneOf{list: values}
end
@doc """
Function used to make sure caps specs are valid.
In particular, valid caps:
* Have shape described by `t:caps_specs_t/0` type
* If they contain keyword list, the keys are present in requested caps type
It returns `:ok` when caps are valid and `{:error, reason}` otherwise
"""
@spec validate_specs(caps_specs_t() | any()) :: :ok | {:error, reason :: tuple()}
def validate_specs(specs_list) when is_list(specs_list) do
specs_list |> Bunch.Enum.try_each(&validate_specs/1)
end
def validate_specs({type, keyword_specs}) do
caps = type.__struct__
caps_keys = caps |> Map.from_struct() |> Map.keys() |> MapSet.new()
spec_keys = keyword_specs |> Keyword.keys() |> MapSet.new()
if MapSet.subset?(spec_keys, caps_keys) do
:ok
else
invalid_keys = spec_keys |> MapSet.difference(caps_keys) |> MapSet.to_list()
{:error, {:invalid_keys, type, invalid_keys}}
end
end
def validate_specs(specs) when is_atom(specs), do: :ok
def validate_specs(specs), do: {:error, {:invalid_specs, specs}}
@doc """
Function determining whether the caps match provided specs.
When `:any` is used as specs, caps can by anything (i.e. they can be invalid)
"""
@spec match?(caps_specs_t(), struct() | any()) :: boolean()
def match?(:any, _caps), do: true
def match?(specs, %_{} = caps) when is_list(specs) do
specs |> Enum.any?(fn spec -> match?(spec, caps) end)
end
def match?({type, keyword_specs}, %caps_type{} = caps) do
type == caps_type && keyword_specs |> Enum.all?(fn kv -> kv |> match_caps_entry(caps) end)
end
def match?(type, %caps_type{}) when is_atom(type) do
type == caps_type
end
defp match_caps_entry({spec_key, spec_value}, %{} = caps) do
{:ok, caps_value} = caps |> Map.fetch(spec_key)
match_value(spec_value, caps_value)
end
defp match_value(%OneOf{list: specs}, value) when is_list(specs) do
specs |> Enum.any?(fn spec -> match_value(spec, value) end)
end
defp match_value(%Range{min: min, max: max}, value) do
min <= value && value <= max
end
defp match_value(spec, value) do
spec == value
end
end