diff --git a/README.md b/README.md index 12a4036..a43054c 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/lib/transcoder.ex b/lib/transcoder.ex index 4c25934..b889133 100644 --- a/lib/transcoder.ex +++ b/lib/transcoder.ex @@ -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. + + Can be either: + * a boolean, + * a function that receives the input stream format and returns a boolean. + """ ] @impl true @@ -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 @@ -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} @@ -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 diff --git a/lib/transcoder/audio.ex b/lib/transcoder/audio.ex index 2c71c5e..5f43402 100644 --- a/lib/transcoder/audio.ex +++ b/lib/transcoder/audio.ex @@ -32,14 +32,20 @@ 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. """) @@ -47,11 +53,16 @@ defmodule Membrane.Transcoder.Audio do 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) diff --git a/lib/transcoder/video.ex b/lib/transcoder/video.ex index b025ebd..3aed1b3 100644 --- a/lib/transcoder/video.ex +++ b/lib/transcoder/video.ex @@ -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 @@ -36,7 +42,12 @@ 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. """) @@ -44,7 +55,7 @@ defmodule Membrane.Transcoder.Video do 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) diff --git a/mix.exs b/mix.exs index 4583f5e..da2cb83 100644 --- a/mix.exs +++ b/mix.exs @@ -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 diff --git a/test/integration_test.exs b/test/integration_test.exs index 6f0a3bf..9e4c896 100644 --- a/test/integration_test.exs +++ b/test/integration_test.exs @@ -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 [ @@ -36,7 +36,7 @@ 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{ @@ -44,12 +44,60 @@ defmodule Membrane.Transcoder.IntegrationTest do }) |> 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