Skip to content

Commit

Permalink
Use nimble options for converter and reader
Browse files Browse the repository at this point in the history
  • Loading branch information
gBillal committed Jan 27, 2025
1 parent c43f710 commit 17d45d6
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 113 deletions.
3 changes: 2 additions & 1 deletion lib/encoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ defmodule Xav.Encoder do
"""
],
profile: [
type: {:in, [:constrained_baseline, :baseline, :main, :high, :main_10, :main_still_picture]},
type:
{:in, [:constrained_baseline, :baseline, :main, :high, :main_10, :main_still_picture]},
type_doc: "`t:atom/0`",
doc: """
The encoder's profile.
Expand Down
151 changes: 85 additions & 66 deletions lib/reader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,37 @@ defmodule Xav.Reader do
Audio/video file reader.
"""

@typedoc """
Reader options.
* `read` - determines which stream to read from a file.
Defaults to `:video`.
* `device?` - determines whether path points to the camera. Defaults to `false`.
"""
@type opts :: [
read: :audio | :video,
device?: boolean,
out_format: Xav.Frame.format(),
out_sample_rate: integer(),
out_channels: integer()
]
@audio_out_formats [:u8, :s16, :s32, :s64, :f32, :f64]

@reader_options_schema [
read: [
type: {:in, [:audio, :video]},
default: :video,
doc: "The type of the stream to read from the input, either `video` or `audio`"
],
device?: [
type: :boolean,
default: false,
doc: "Whether the path points to the camera"
],
out_format: [
type: {:in, @audio_out_formats},
doc: """
The output format of the audio samples. It should be one of
the following values: `#{Enum.join(@audio_out_formats, ", ")}`.
For video samples, it is always `:rgb24`.
"""
],
out_sample_rate: [
type: :pos_integer,
doc: "The output sample rate of the audio samples"
],
out_channels: [
type: :pos_integer,
doc: "The output number of channels of the audio samples"
]
]

@type t() :: %__MODULE__{
reader: reference(),
Expand All @@ -37,9 +54,9 @@ defmodule Xav.Reader do
[:in_sample_rate, :out_sample_rate, :in_channels, :out_channels, :framerate]

@doc """
The same as new/1 but raises on error.
The same as `new/1` but raises on error.
"""
@spec new!(String.t(), opts()) :: t()
@spec new!(String.t(), Keyword.t()) :: t()
def new!(path, opts \\ []) do
case new(path, opts) do
{:ok, reader} -> reader
Expand All @@ -56,57 +73,12 @@ defmodule Xav.Reader do
Microphone input is not supported.
`opts` can be used to specify desired output parameters.
Video frames are always returned in RGB format. This setting cannot be changed.
Audio samples are always in the packed form.
See `Xav.Decoder.new/2` for more information.
The following options can be provided:\n#{NimbleOptions.docs(@reader_options_schema)}
"""
@spec new(String.t(), opts()) :: {:ok, t()} | {:error, term()}
@spec new(String.t(), Keyword.t()) :: {:ok, t()} | {:error, term()}
def new(path, opts \\ []) do
read = opts[:read] || :video
device? = opts[:device?] || false
out_format = opts[:out_format]
out_sample_rate = opts[:out_sample_rate] || 0
out_channels = opts[:out_channels] || 0

case Xav.Reader.NIF.new(
path,
to_int(device?),
to_int(read),
out_format,
out_sample_rate,
out_channels
) do
{:ok, reader, in_format, out_format, in_sample_rate, out_sample_rate, in_channels,
out_channels, bit_rate, duration, codec} ->
{:ok,
%__MODULE__{
reader: reader,
in_format: in_format,
out_format: out_format,
in_sample_rate: in_sample_rate,
out_sample_rate: out_sample_rate,
in_channels: in_channels,
out_channels: out_channels,
bit_rate: bit_rate,
duration: duration,
codec: to_human_readable(codec)
}}

{:ok, reader, in_format, out_format, bit_rate, duration, codec, framerate} ->
{:ok,
%__MODULE__{
reader: reader,
in_format: in_format,
out_format: out_format,
bit_rate: bit_rate,
duration: duration,
codec: to_human_readable(codec),
framerate: framerate
}}

{:error, _reason} = err ->
err
with {:ok, opts} <- NimbleOptions.validate(opts, @reader_options_schema) do
do_create_reader(path, opts)
end
end

Expand Down Expand Up @@ -144,8 +116,10 @@ defmodule Xav.Reader do

@doc """
Creates a new reader stream.
Check `new/1` for the available options.
"""
@spec stream!(String.t(), opts()) :: Enumerable.t()
@spec stream!(String.t(), Keyword.t()) :: Enumerable.t()
def stream!(path, opts \\ []) do
Stream.resource(
fn ->
Expand All @@ -167,6 +141,51 @@ defmodule Xav.Reader do
)
end

defp do_create_reader(path, opts) do
out_sample_rate = opts[:out_sample_rate] || 0
out_channels = opts[:out_channels] || 0

case Xav.Reader.NIF.new(
path,
to_int(opts[:device?]),
to_int(opts[:read]),
opts[:out_format],
out_sample_rate,
out_channels
) do
{:ok, reader, in_format, out_format, in_sample_rate, out_sample_rate, in_channels,
out_channels, bit_rate, duration, codec} ->
{:ok,
%__MODULE__{
reader: reader,
in_format: in_format,
out_format: out_format,
in_sample_rate: in_sample_rate,
out_sample_rate: out_sample_rate,
in_channels: in_channels,
out_channels: out_channels,
bit_rate: bit_rate,
duration: duration,
codec: to_human_readable(codec)
}}

{:ok, reader, in_format, out_format, bit_rate, duration, codec, framerate} ->
{:ok,
%__MODULE__{
reader: reader,
in_format: in_format,
out_format: out_format,
bit_rate: bit_rate,
duration: duration,
codec: to_human_readable(codec),
framerate: framerate
}}

{:error, _reason} = err ->
err
end
end

defp to_human_readable(:libdav1d), do: :av1
defp to_human_readable(:mp3float), do: :mp3
defp to_human_readable(other), do: other
Expand Down
71 changes: 27 additions & 44 deletions lib/video_converter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,45 @@ defmodule Xav.VideoConverter do
out_height: Frame.height()
}

@typedoc """
Type definition for converter options.
* `out_format` - video format to convert to (`e.g. :rgb24`).
* `out_width` - scale the video frame to this width.
* `out_height` - scale the video frame to this height.
If `out_width` and `out_height` are both not provided, scaling is not performed. If one of the
dimensions is `nil`, the other will be calculated based on the input dimensions as
to keep the aspect ratio.
"""
@type converter_opts() :: [
out_format: Frame.video_format(),
out_width: Frame.width(),
out_height: Frame.height()
]
@converter_schema [
out_width: [
type: :pos_integer,
required: false,
doc: """
scale the video frame to this width
If `out_width` and `out_height` are both not provided, scaling is not performed. If one of the
dimensions is `nil`, the other will be calculated based on the input dimensions as
to keep the aspect ratio.
"""
],
out_height: [
type: :pos_integer,
required: false,
doc: "scale the video frame to this height"
],
out_format: [
type: :atom,
required: false,
doc: "video format to convert to (e.g. `:rgb24`)"
]
]

defstruct [:converter, :out_format, :out_width, :out_height]

@doc """
Creates a new video converter.
The following options can be passed:\n#{NimbleOptions.docs(@converter_schema)}
"""
@spec new(converter_opts()) :: t()
@spec new(Keyword.t()) :: t()
def new(converter_opts) do
opts = Keyword.validate!(converter_opts, [:out_format, :out_width, :out_height])
opts = NimbleOptions.validate!(converter_opts, @converter_schema)

if is_nil(opts[:out_format]) and is_nil(opts[:out_width]) and is_nil(opts[:out_height]) do
raise "At least one of `out_format`, `out_width` or `out_height` must be provided"
end

:ok = validate_converter_options(opts)

converter = NIF.new(opts[:out_format], opts[:out_width] || -1, opts[:out_height] || -1)

%__MODULE__{
Expand Down Expand Up @@ -80,28 +87,4 @@ defmodule Xav.VideoConverter do
pts: frame.pts
}
end

defp validate_converter_options([]), do: :ok

defp validate_converter_options([{_key, nil} | opts]) do
validate_converter_options(opts)
end

defp validate_converter_options([{key, value} | _opts])
when key in [:out_width, :out_height] and not is_integer(value) do
raise %ArgumentError{
message: "Expected an integer value for #{inspect(key)}, received: #{inspect(value)}"
}
end

defp validate_converter_options([{key, value} | _opts])
when key in [:out_width, :out_height] and value < 1 do
raise %ArgumentError{
message: "Invalid value for #{inspect(key)}, expected a value to be >= 1"
}
end

defp validate_converter_options([{_key, _value} | opts]) do
validate_converter_options(opts)
end
end
6 changes: 4 additions & 2 deletions test/video_converter_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule Xav.VideoConverterTest do
use ExUnit.Case, async: true

alias NimbleOptions.ValidationError

describe "new/1" do
test "new converter" do
assert %Xav.VideoConverter{out_format: :rgb24, converter: converter} =
Expand All @@ -14,8 +16,8 @@ defmodule Xav.VideoConverterTest do
end

test "fails on invalid options" do
assert_raise ArgumentError, fn -> Xav.VideoConverter.new(out_width: 0) end
assert_raise ArgumentError, fn -> Xav.VideoConverter.new(out_height: "15") end
assert_raise ValidationError, fn -> Xav.VideoConverter.new(out_width: 0) end
assert_raise ValidationError, fn -> Xav.VideoConverter.new(out_height: "15") end
end
end

Expand Down

0 comments on commit 17d45d6

Please sign in to comment.