This commit is contained in:
Liam 2024-06-14 21:26:12 -04:00
parent 19eb5be999
commit 6151138525
12 changed files with 744 additions and 0 deletions

View file

@ -0,0 +1,36 @@
defmodule PhilomenaMedia.Libavcodec.Aac do
@moduledoc """
Represents the `aac` encoder, which takes an audio input and generates encoded output.
## Example
Aac.new()
No options are exposed. However, see https://ffmpeg.org/ffmpeg-codecs.html#aac for
additional information.
"""
@type opts :: []
@type t :: %__MODULE__{
name: String.t(),
opts: opts(),
force_format: nil,
type: :audio
}
defstruct name: "aac",
opts: [],
force_format: nil,
type: :audio
@doc """
Construct a new AAC encoder.
See module documentation for usage.
"""
@spec new() :: t()
def new do
%__MODULE__{}
end
end

View file

@ -0,0 +1,36 @@
defmodule PhilomenaMedia.Libavcodec.EncodeStream do
@moduledoc """
Represents a stream which encodes data.
"""
@type threads :: non_neg_integer()
@type slices :: non_neg_integer()
@type max_muxing_queue_size :: non_neg_integer()
@type opts :: [
threads: threads(),
slices: slices(),
max_muxing_queue_size: max_muxing_queue_size()
]
@type t :: %__MODULE__{
encoder: nil | struct(),
opts: opts()
}
defstruct encoder: nil,
opts: []
@doc """
Constructs a new encode stream.
See the individual encoders for additional options.
"""
@spec new(opts(), nil | struct()) :: t()
def new(opts, encoder \\ nil) do
%__MODULE__{
encoder: encoder,
opts: Keyword.take(opts, [:threads, :slices, :max_muxing_queue_size])
}
end
end

View file

@ -0,0 +1,36 @@
defmodule PhilomenaMedia.Libavcodec.Gif do
@moduledoc """
Represents the `gif` encoder, which takes a video input and generates encoded output.
## Example
Gif.new()
No options are exposed. However, see https://ffmpeg.org/ffmpeg-codecs.html#GIF for
additional information.
"""
@type opts :: []
@type t :: %__MODULE__{
name: String.t(),
opts: opts(),
force_format: nil,
type: :video
}
defstruct name: "gif",
opts: [],
force_format: nil,
type: :video
@doc """
Construct a new GIF encoder.
See module documentation for usage.
"""
@spec new() :: t()
def new do
%__MODULE__{}
end
end

View file

@ -0,0 +1,36 @@
defmodule PhilomenaMedia.Libavcodec.Libopus do
@moduledoc """
Represents the `libopus` encoder, which takes an audio input and generates encoded output.
## Example
Libopus.new()
No options are exposed. However, see https://ffmpeg.org/ffmpeg-codecs.html#libopus-1 for
additional information.
"""
@type opts :: []
@type t :: %__MODULE__{
name: String.t(),
opts: opts(),
force_format: nil,
type: :audio
}
defstruct name: "libopus",
opts: [],
force_format: nil,
type: :audio
@doc """
Construct a new libopus encoder.
See module documentation for usage.
"""
@spec new() :: t()
def new do
%__MODULE__{}
end
end

View file

@ -0,0 +1,50 @@
defmodule PhilomenaMedia.Libavcodec.Libvpx do
@moduledoc """
Represents the `libvpx` (VP8) encoder, which takes a video input and generates encoded output.
## Example with all options
Libvpx.new(
deadline: :good,
"cpu-used": 5,
crf: 31
)
See https://ffmpeg.org/ffmpeg-codecs.html#libvpx for more information about the options.
"""
@type deadline :: :best | :good | :realtime
@type cpu_used :: -16..16
@type qrange :: 0..63
@type crf :: qrange()
@type opts :: [
deadline: deadline(),
"cpu-used": cpu_used(),
crf: qrange()
]
@type t :: %__MODULE__{
name: String.t(),
opts: opts(),
force_format: :yuv420p,
type: :video
}
defstruct name: "libvpx",
opts: [],
force_format: :yuv420p,
type: :video
@doc """
Construct a new libvpx (VP8) encoder.
See module documentation for usage.
"""
@spec new(opts()) :: t()
def new(opts) do
%__MODULE__{
opts: Keyword.take(opts, [:deadline, :"cpu-used", :crf])
}
end
end

View file

@ -0,0 +1,50 @@
defmodule PhilomenaMedia.Libavcodec.Libx264 do
@moduledoc """
Represents the `libx264` (H.264) encoder, which takes a video input and generates encoded output.
## Example with all options
Libx264.new(
profile: :main,
preset: :medium,
crf: 18,
)
See https://ffmpeg.org/ffmpeg-codecs.html#libx264_002c-libx264rgb for more information about the options.
"""
@type profile :: :baseline | :main | :high
@type preset :: :slow | :medium
@type qrange :: 0..51
@type crf :: qrange()
@type opts :: [
profile: profile(),
preset: preset(),
crf: qrange()
]
@type t :: %__MODULE__{
name: String.t(),
opts: opts(),
force_format: :yuv420p,
type: :video
}
defstruct name: "libx264",
opts: [],
force_format: :yuv420p,
type: :video
@doc """
Construct a new libx264 (H.264) encoder.
See module documentation for usage.
"""
@spec new(opts()) :: t()
def new(opts) do
%__MODULE__{
opts: Keyword.take(opts, [:profile, :preset, :crf])
}
end
end

View file

@ -0,0 +1,58 @@
defmodule PhilomenaMedia.Libavfilter.Endpoint do
@moduledoc """
Represents an endpoint vertex of the filter graph. Processing starts or stops here.
An endpoint which has no input but produces one output is a source. An endpoint which has one
input but produces no output is a sink.
See https://ffmpeg.org/ffmpeg-filters.html#Filtergraph-description for more information.
"""
@type index :: non_neg_integer()
@type pad_type :: PhilomenaMedia.Libavfilter.FilterNode.pad_type()
@type t :: %__MODULE__{
name: nil,
opts: [],
inputs: PhilomenaMedia.Libavfilter.FilterNode.pad_list(),
outputs: PhilomenaMedia.Libavfilter.FilterNode.pad_list(),
index: index()
}
@derive [PhilomenaMedia.Libavfilter.FilterNode]
defstruct name: nil,
opts: [],
inputs: [],
outputs: [],
index: 0
@doc """
Create a new source endpoint with the given pad type.
By default, this corresponds to stream index 0. Has one output pad with name `source`.
See the moduledoc for `m:PhilomenaMedia.Libavfilter.FilterGraph` for a usage example.
"""
@spec new_source(index(), pad_type()) :: t()
def new_source(index \\ 0, pad_type) do
%__MODULE__{
outputs: [source: pad_type],
index: index
}
end
@doc """
Create a new sink endpoint with the given pad type.
By default, this corresponds to stream index 0. Has one input pad with name `sink`.
See the moduledoc for `m:PhilomenaMedia.Libavfilter.FilterGraph` for a usage example.
"""
@spec new_sink(index(), pad_type()) :: t()
def new_sink(index \\ 0, pad_type) do
%__MODULE__{
inputs: [sink: pad_type],
index: index
}
end
end

View file

@ -0,0 +1,194 @@
defmodule PhilomenaMedia.Libavfilter.FilterGraph do
@moduledoc """
Represents a complex filter graph.
## Example
{graph, [source, palettegen, paletteuse, sink]} =
FilterGraph.new([
Endpoint.new_source(:video),
Palettegen.new(stats_mode: :single),
Paletteuse.new(new: true),
Endpoint.new_sink(:video)
])
graph
|> FilterGraph.connect({source, :source}, {palettegen, :source})
|> FilterGraph.connect({source, :source}, {paletteuse, :source})
|> FilterGraph.connect({palettegen, :result}, {paletteuse, :palette})
|> FilterGraph.connect({paletteuse, :result}, {sink, :sink})
This creates the following conceptual graph:
source --> palettegen
| |
| |
| v
--------paletteuse --> sink
See https://ffmpeg.org/ffmpeg-filters.html#Filtergraph-description for more information.
"""
alias PhilomenaMedia.Libavfilter.FilterNode
alias PhilomenaMedia.Libavfilter.Endpoint
@type t :: %__MODULE__{
forward_adjacency: %{tagged_vertex() => MapSet.t()},
reverse_adjacency: %{tagged_vertex() => MapSet.t()},
vertices: %{integer() => struct()},
index: integer()
}
@type vertex :: integer()
@type pad_name :: FilterNode.pad_name()
@type pad :: {vertex(), pad_name()}
@type pad_index :: integer()
@type tagged_vertex :: {vertex(), pad_index()}
defstruct forward_adjacency: %{},
reverse_adjacency: %{},
vertices: %{},
index: 0
@doc """
Creates a new filtergraph instance with an optional list of vertices to add to the graph.
See the moduledoc for a full example.
"""
@spec new([struct()]) :: {t(), [vertex()]}
def new(nodes \\ []) do
add(%__MODULE__{}, nodes)
end
@doc """
Adds the specified list of vertices to the graph.
Returns the updated filtergraph structure and a list of vertices.
See the moduledoc for a full example.
"""
@spec add(t(), [struct()]) :: {t(), [vertex()]}
def add(g, nodes) do
{vertices, g} =
Enum.map_reduce(nodes, g, fn node, acc ->
{
acc.index,
%{acc | vertices: Map.put(acc.vertices, acc.index, node), index: acc.index + 1}
}
end)
{g, vertices}
end
@doc """
Connects two vertex references together with the given pad name.
Returns the updated filtergraph structure.
See the moduledoc for a full example.
"""
@spec connect(t(), pad(), pad()) :: t()
def connect(g, {output_vert, output_name}, {input_vert, input_name}) do
output_node = Map.fetch!(g.vertices, output_vert)
output_pad = {output_vert, pad_name_to_index!(FilterNode.outputs(output_node), output_name)}
input_node = Map.fetch!(g.vertices, input_vert)
input_pad = {input_vert, pad_name_to_index!(FilterNode.inputs(input_node), input_name)}
forward_adjacency =
Map.update(g.forward_adjacency, output_pad, MapSet.new([input_pad]), fn v ->
MapSet.put(v, input_pad)
end)
reverse_adjacency =
Map.update(g.reverse_adjacency, input_pad, MapSet.new([output_pad]), fn v ->
MapSet.put(v, output_pad)
end)
%{g | forward_adjacency: forward_adjacency, reverse_adjacency: reverse_adjacency}
end
@spec pad_name_to_index!(FilterNode.pad_list(), FilterNode.pad_name()) :: pad_index()
defp pad_name_to_index!(pads, pad_name) do
pads
|> Enum.find_index(fn {name, _type} -> name == pad_name end)
|> case do
nil ->
raise "Pad #{inspect(pad_name)} not found in list #{inspect(pads)}"
value ->
value
end
end
@doc """
Convert the filtergraph to a textual representation.
"""
@spec to_graph(t()) :: String.t()
def to_graph(g) do
g.vertices
|> Enum.map(fn
{_vert, %Endpoint{}} ->
[]
{vert, node} ->
[
incoming_pads!(g, vert),
FilterNode.name(node),
encode_opts!(node),
outgoing_pads!(g, vert),
";"
]
end)
|> IO.iodata_to_binary()
end
@spec incoming_pad!(t(), tagged_vertex()) :: iodata()
defp incoming_pad!(g, {target_vert, target_index}) do
[{source_vert, source_index}] =
g.reverse_adjacency
|> Map.fetch!({target_vert, target_index})
|> MapSet.to_list()
case g.vertices[source_vert] do
%_{index: index} ->
"[#{index}:v]"
_ ->
"[p#{source_vert}_#{source_index}:v]"
end
end
@spec incoming_pads!(t(), vertex()) :: iodata()
defp incoming_pads!(g, target_vert) do
g.vertices
|> Map.fetch!(target_vert)
|> FilterNode.inputs()
|> Enum.with_index()
|> Enum.map(fn {_name, index} -> incoming_pad!(g, {target_vert, index}) end)
end
@spec outgoing_pad!(t(), tagged_vertex()) :: iodata()
defp outgoing_pad!(_g, {source_vert, source_index}) do
"[p#{source_vert}_#{source_index}:v]"
end
@spec outgoing_pads!(t(), vertex()) :: iodata()
defp outgoing_pads!(g, source_vert) do
g.vertices
|> Map.fetch!(source_vert)
|> FilterNode.outputs()
|> Enum.with_index()
|> Enum.map(fn {_name, index} -> outgoing_pad!(g, {source_vert, index}) end)
end
@spec encode_opts!(struct()) :: iodata()
defp encode_opts!(filter_node)
defp encode_opts!(%_{opts: opts}) when opts == %{} do
""
end
defp encode_opts!(%_{opts: opts}) do
["=", Enum.map_join(opts, ":", fn {k, v} -> "#{k}=#{v}" end)]
end
end

View file

@ -0,0 +1,62 @@
defprotocol PhilomenaMedia.Libavfilter.FilterNode do
@doc """
The name of this filter, as a string, or nil if the filter is an endpoint.
"""
@spec name(t) :: String.t() | nil
def name(t)
@doc """
A list of options passed to this filter, if any.
## Example
iex> FilterNode.opts(%Paletteuse{...})
[dither: :bayer, bayer_scale: 5]
"""
@spec opts(t) :: keyword()
def opts(t)
@type pad_name :: atom()
@type pad_type :: :video
@type pad_list :: [{pad_name(), pad_type()}]
@doc """
A list of inputs passed to this filter, if any.
> #### Info {: .info}
>
> Inputs are given names for clarity, but libavfilter addresses inputs
> by *numeric index*, not by name, so this must not be placed into a map.
## Example
iex> FilterNode.inputs(%Paletteuse{...})
[source: :video, palette: :video]
"""
@spec inputs(t) :: pad_list()
def inputs(t)
@doc """
A list of outputs generated by this filter, if any.
> #### Info {: .info}
>
> Outputs are given names for clarity, but libavfilter addresses outputs
> by *numeric index*, not by name, so this must not be placed into a map.
## Example
iex> FilterNode.outputs(%Scale{...})
[result: :video]
"""
@spec outputs(t) :: pad_list()
def outputs(t)
end
defimpl PhilomenaMedia.Libavfilter.FilterNode, for: Any do
def name(t), do: t.name
def opts(t), do: t.opts
def inputs(t), do: t.inputs
def outputs(t), do: t.outputs
end

View file

@ -0,0 +1,58 @@
defmodule PhilomenaMedia.Libavfilter.Palettegen do
@moduledoc """
Represents the `palettegen` filter, which takes a video input and generates a video output.
The palette for each frame processed is generated using the median cut algorithm.
Has one input pad with name `source`. Has one output pad with name `result`.
## Example with all options
Palettegen.new(
max_colors: 255,
reserve_transparent: true,
stats_mode: :diff
)
See https://ffmpeg.org/ffmpeg-filters.html#palettegen for more information about the options.
"""
@type max_colors :: 0..256
@type reserve_transparent :: boolean()
@type stats_mode :: :full | :diff | :single
@type opts :: [
max_colors: max_colors(),
reserve_transparent: reserve_transparent(),
stats_mode: stats_mode()
]
@type t :: %__MODULE__{
name: String.t(),
opts: opts(),
inputs: PhilomenaMedia.Libavfilter.FilterNode.pad_list(),
outputs: PhilomenaMedia.Libavfilter.FilterNode.pad_list()
}
@derive [PhilomenaMedia.Libavfilter.FilterNode]
defstruct name: "palettegen",
opts: [],
inputs: [source: :video],
outputs: [result: :video]
@doc """
Construct a new palettegen filter.
See module documentation for usage.
"""
@spec new(opts()) :: t()
def new(opts) do
%__MODULE__{
opts: [
max_colors: Keyword.get(opts, :max_colors, 255),
reserve_transparent: Keyword.get(opts, :reserve_transparent, true),
stats_mode: Keyword.get(opts, :stats_mode, :diff)
]
}
end
end

View file

@ -0,0 +1,67 @@
defmodule PhilomenaMedia.Libavfilter.Paletteuse do
@moduledoc """
Represents the `paletteuse` filter, which takes two video inputs and generates a video output.
The first input is the video stream, and the second input is the a 256-color palette.
Video colors are mapped onto the palette using a kd-tree quantization algorithm.
Has two input pads with names `source` and `palette`. Has one output pad with name `result`.
## Example with all options
Paletteuse.new(
dither: :bayer,
bayer_scale: 5,
diff_mode: :rectangle,
new: false,
alpha_threshold: 255
)
See https://ffmpeg.org/ffmpeg-filters.html#paletteuse for more information about the options.
"""
@type dither :: :bayer | :none
@type bayer_scale :: 0..5
@type diff_mode :: :none | :rectangle
@type new :: boolean()
@type alpha_threshold :: 0..255
@type opts :: [
dither: dither(),
bayer_scale: bayer_scale(),
diff_mode: diff_mode(),
new: new(),
alpha_threshold: alpha_threshold()
]
@type t :: %__MODULE__{
name: String.t(),
opts: opts(),
inputs: PhilomenaMedia.Libavfilter.FilterNode.pad_list(),
outputs: PhilomenaMedia.Libavfilter.FilterNode.pad_list()
}
@derive [PhilomenaMedia.Libavfilter.FilterNode]
defstruct name: "paletteuse",
opts: [],
inputs: [source: :video, palette: :video],
outputs: [result: :video]
@doc """
Construct a new paletteuse filter.
See module documentation for usage.
"""
@spec new(opts()) :: t()
def new(opts) do
%__MODULE__{
opts: [
dither: Keyword.get(opts, :dither, :bayer),
bayer_scale: Keyword.get(opts, :bayer_scale, 5),
diff_mode: Keyword.get(opts, :diff_mode, :rectangle),
new: Keyword.get(opts, :new, false),
alpha_threshold: Keyword.get(opts, :alpha_threshold, 255)
]
}
end
end

View file

@ -0,0 +1,61 @@
defmodule PhilomenaMedia.Libavfilter.Scale do
@moduledoc """
Represents the `scale` filter, which takes a video input and generates a video output.
Has one input pad with name `source`. Has one output pad with name `result`.
## Example with all options
Scale.new(
width: 250,
height: 250,
force_original_aspect_ratio: :decrease,
force_divisible_by: 2
)
See https://ffmpeg.org/ffmpeg-filters.html#scale-1 for more information about the options.
"""
@type dimension :: integer()
@type width :: dimension()
@type height :: dimension()
@type force_original_aspect_ratio :: :disable | :decrease | :increase
@type force_divisible_by :: pos_integer()
@type opts :: [
width: width(),
height: height(),
force_original_aspect_ratio: force_original_aspect_ratio(),
force_divisible_by: force_divisible_by()
]
@type t :: %__MODULE__{
name: String.t(),
opts: opts(),
inputs: PhilomenaMedia.Libavfilter.FilterNode.pad_list(),
outputs: PhilomenaMedia.Libavfilter.FilterNode.pad_list()
}
@derive [PhilomenaMedia.Libavfilter.FilterNode]
defstruct name: "scale",
opts: [],
inputs: [source: :video],
outputs: [result: :video]
@doc """
Construct a new scale filter.
See module documentation for usage.
"""
@spec new(opts()) :: t()
def new(opts) do
%__MODULE__{
opts: [
width: Keyword.fetch!(opts, :width),
height: Keyword.fetch!(opts, :height),
force_original_aspect_ratio: Keyword.get(opts, :force_original_aspect_ratio, :disable),
force_divisible_by: Keyword.get(opts, :force_divisible_by, 1)
]
}
end
end