Skip to content

Add option to enforce transcoding #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The package can be installed by adding `membrane_transcoder_plugin` to your list
```elixir
def deps do
[
{:membrane_transcoder_plugin, "~> 0.1.2"}
{:membrane_transcoder_plugin, "~> 0.1.3"}
]
end
```
Expand Down
43 changes: 36 additions & 7 deletions lib/transcoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,31 @@ defmodule Membrane.Transcoder do
accepted_format: format when Audio.is_audio_format(format) or Video.is_video_format(format)

def_options output_stream_format: [
spec: stream_format() | stream_format_module() | stream_format_resolver(),
spec:
stream_format()
| stream_format_module()
| stream_format_resolver(),
description: """
An option specifying desired output format.

Can be either:
* a struct being a Membrane stream format,
* a module in which Membrane stream format struct is defined,
* a function which receives input stream format as an input argument
and is supposed to return the desired output stream format or its module.
"""
],
enforce_transcoding?: [
spec: boolean() | (stream_format() -> boolean()),
default: false,
description: """
If set to `true`, the input media stream will be decoded and encoded, even
if the input stream format and the output stream format are the same type.

Cen be either:
* a boolean,
* a function that receives the input stream format and returns a boolean.
"""
]

@impl true
Expand All @@ -78,7 +94,9 @@ defmodule Membrane.Transcoder do
state =
opts
|> Map.from_struct()
|> Map.put(:input_stream_format, nil)
|> Map.merge(%{
input_stream_format: nil
})

{[spec: spec], state}
end
Expand All @@ -94,9 +112,18 @@ defmodule Membrane.Transcoder do
%{state | input_stream_format: format}
|> resolve_output_stream_format()

state =
with %{enforce_transcoding?: f} when is_function(f) <- state do
%{state | enforce_transcoding?: f.(format)}
end

spec =
get_child(:forwarding_filter)
|> plug_transcoding(format, state.output_stream_format)
|> plug_transcoding(
format,
state.output_stream_format,
state.enforce_transcoding?
)
|> get_child(:output_funnel)

{[spec: spec], state}
Expand Down Expand Up @@ -139,13 +166,15 @@ defmodule Membrane.Transcoder do
end
end

defp plug_transcoding(builder, input_format, output_format)
defp plug_transcoding(builder, input_format, output_format, enforce_transcoding?)
when Audio.is_audio_format(input_format) do
builder |> Audio.plug_audio_transcoding(input_format, output_format)
builder
|> Audio.plug_audio_transcoding(input_format, output_format, enforce_transcoding?)
end

defp plug_transcoding(builder, input_format, output_format)
defp plug_transcoding(builder, input_format, output_format, enforce_transcoding?)
when Video.is_video_format(input_format) do
builder |> Video.plug_video_transcoding(input_format, output_format)
builder
|> Video.plug_video_transcoding(input_format, output_format, enforce_transcoding?)
end
end
23 changes: 17 additions & 6 deletions lib/transcoder/audio.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,37 @@ defmodule Membrane.Transcoder.Audio do
@spec plug_audio_transcoding(
ChildrenSpec.builder(),
audio_stream_format() | RemoteStream.t(),
audio_stream_format()
audio_stream_format(),
boolean()
) :: ChildrenSpec.builder()
def plug_audio_transcoding(builder, input_format, output_format)
def plug_audio_transcoding(builder, input_format, output_format, enforce_transcoding?)
when is_audio_format(input_format) and is_audio_format(output_format) do
do_plug_audio_transcoding(builder, input_format, output_format)
do_plug_audio_transcoding(builder, input_format, output_format, enforce_transcoding?)
end

defp do_plug_audio_transcoding(builder, %format_module{}, %format_module{}) do
defp do_plug_audio_transcoding(
builder,
%format_module{},
%format_module{},
false = _enforce_transcoding?
) do
Membrane.Logger.debug("""
This bin will only forward buffers, as the input stream format is the same as the output stream format.
""")

builder
end

defp do_plug_audio_transcoding(builder, %RemoteStream{content_format: Opus}, %Opus{}) do
defp do_plug_audio_transcoding(
builder,
%RemoteStream{content_format: Opus},
%Opus{},
false = _enforce_transcoding?
) do
builder |> child(:opus_parser, Opus.Parser)
end

defp do_plug_audio_transcoding(builder, input_format, output_format) do
defp do_plug_audio_transcoding(builder, input_format, output_format, _enforce_transcoding?) do
builder
|> maybe_plug_parser(input_format)
|> maybe_plug_decoder(input_format)
Expand Down
23 changes: 17 additions & 6 deletions lib/transcoder/video.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,20 @@ defmodule Membrane.Transcoder.Video do
@spec plug_video_transcoding(
ChildrenSpec.builder(),
video_stream_format(),
video_stream_format()
video_stream_format(),
boolean()
) :: ChildrenSpec.builder()
def plug_video_transcoding(builder, input_format, output_format)
def plug_video_transcoding(builder, input_format, output_format, enforce_transcoding?)
when is_video_format(input_format) and is_video_format(output_format) do
do_plug_video_transcoding(builder, input_format, output_format)
do_plug_video_transcoding(builder, input_format, output_format, enforce_transcoding?)
end

defp do_plug_video_transcoding(builder, %h26x{}, %h26x{} = output_format)
defp do_plug_video_transcoding(
builder,
%h26x{},
%h26x{} = output_format,
false = _enforce_transcoding?
)
when h26x in [H264, H265] do
parser =
h26x
Expand All @@ -36,15 +42,20 @@ defmodule Membrane.Transcoder.Video do
builder |> child(:h264_parser, parser)
end

defp do_plug_video_transcoding(builder, %format_module{}, %format_module{}) do
defp do_plug_video_transcoding(
builder,
%format_module{},
%format_module{},
false = _enforce_transcoding?
) do
Membrane.Logger.debug("""
This bin will only forward buffers, as the input stream format is the same type as the output stream format.
""")

builder
end

defp do_plug_video_transcoding(builder, input_format, output_format) do
defp do_plug_video_transcoding(builder, input_format, output_format, _enforce_transcoding?) do
builder
|> maybe_plug_parser_and_decoder(input_format)
|> maybe_plug_encoder_and_parser(output_format)
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Membrane.Transcoder.Plugin.Mixfile do
use Mix.Project

@version "0.1.2"
@version "0.1.3"
@github_url "https://github.com/membraneframework/membrane_transcoder_plugin"

def project do
Expand Down
58 changes: 53 additions & 5 deletions test/integration_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Membrane.Transcoder.IntegrationTest do
import Membrane.ChildrenSpec

alias Membrane.{AAC, H264, H265, Opus, RawAudio, RawVideo, VP8}
alias Membrane.Testing.Pipeline
alias Membrane.Testing
alias Membrane.Transcoder.Support.Preprocessors

@video_inputs [
Expand Down Expand Up @@ -36,20 +36,68 @@ defmodule Membrane.Transcoder.IntegrationTest do

Enum.map(@test_cases, fn test_case ->
test "if transcoder support #{inspect(test_case.input_format)} input and #{inspect(test_case.output_format)} output" do
pid = Pipeline.start_link_supervised!()
pid = Testing.Pipeline.start_link_supervised!()

spec =
child(%Membrane.File.Source{
location: Path.join("./test/fixtures", unquote(test_case.input_file))
})
|> then(unquote(test_case.preprocess))
|> child(%Membrane.Transcoder{output_stream_format: unquote(test_case.output_format)})
|> child(:sink, Membrane.Testing.Sink)
|> child(:sink, Testing.Sink)

Pipeline.execute_actions(pid, spec: spec)
Testing.Pipeline.execute_actions(pid, spec: spec)

assert_sink_stream_format(pid, :sink, %unquote(test_case.output_format){})
Pipeline.terminate(pid)
Testing.Pipeline.terminate(pid)
end
end)

defmodule FormatSource do
use Membrane.Source

def_output_pad :output, accepted_format: _any, flow_control: :push
def_options format: []

@impl true
def handle_init(_ctx, opts), do: {[], opts |> Map.from_struct()}

@impl true
def handle_playing(_ctx, state),
do: {[stream_format: {:output, state.format}], state}
end

test "if encoder and decoder are spawned or not, depending on the value of `enforce_transcoding?` option" do
for format <- [%AAC{channels: 1}, %H264{alignment: :au, stream_structure: :annexb}],
enforce_transcoding? <- [true, false] do
spec =
child(:source, %FormatSource{format: format})
|> child(:transcoder, %Membrane.Transcoder{
output_stream_format: format,
enforce_transcoding?: enforce_transcoding?
})
|> child(:sink, Testing.Sink)

pipeline = Testing.Pipeline.start_link_supervised!(spec: spec)

Process.sleep(500)

case format do
%H264{} -> [:h264_encoder, :h264_decoder]
%AAC{} -> [:aac_encoder, :aac_decoder]
end
|> Enum.each(fn child_name ->
get_child_result = Testing.Pipeline.get_child_pid(pipeline, [:transcoder, child_name])

if enforce_transcoding? do
assert {:ok, child_pid} = get_child_result
assert is_pid(child_pid)
else
assert {:error, :child_not_found} = get_child_result
end
end)

Testing.Pipeline.terminate(pipeline)
end
end
end