diff --git a/.github/workflows/enforce-changelog-update.yml b/.github/workflows/enforce-changelog-update.yml deleted file mode 100644 index 86150df..0000000 --- a/.github/workflows/enforce-changelog-update.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: Enforce CHANGELOG Update -on: - pull_request: - branches: - - master -jobs: - check-changelog-update: - uses: membraneframework/membrane_core/.github/workflows/enforce-changelog-update.yml@master - secrets: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN}} diff --git a/README.md b/README.md index 1734afa..50e9195 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The package can be installed by adding `membrane_ivf_plugin` to your list of dep ```elixir def deps do [ - {:membrane_ivf_plugin, "~> 0.7.0"} + {:membrane_ivf_plugin, "~> 0.8.0"} ] end ``` diff --git a/lib/deserializer.ex b/lib/membrane_ivf/deserializer.ex similarity index 61% rename from lib/deserializer.ex rename to lib/membrane_ivf/deserializer.ex index 0f21209..4f5c8b9 100644 --- a/lib/deserializer.ex +++ b/lib/membrane_ivf/deserializer.ex @@ -1,4 +1,4 @@ -defmodule Membrane.Element.IVF.Deserializer do +defmodule Membrane.IVF.Deserializer do @moduledoc """ Deserializer is capable of converting stream representing video in IVF format into stream of Membrane.Buffer's with video frames with correct timestamps in @@ -7,25 +7,28 @@ defmodule Membrane.Element.IVF.Deserializer do use Membrane.Filter use Numbers, overload_operators: true - alias Membrane.Element.IVF.Headers - alias Membrane.Element.IVF.Headers.FrameHeader - alias Membrane.{Buffer, RemoteStream, Time} + alias Membrane.IVF.Headers + alias Membrane.IVF.Headers.FrameHeader + alias Membrane.{Buffer, IVF, Time} alias Membrane.{VP8, VP9} - def_input_pad :input, accepted_format: _any + def_input_pad :input, + accepted_format: %Membrane.RemoteStream{content_format: fmt} when fmt in [IVF, nil] def_output_pad :output, - accepted_format: - %RemoteStream{content_format: format, type: :packetized} when format in [VP9, VP8] + accepted_format: any_of(VP8, VP9) defmodule State do @moduledoc false - - @doc """ - frame_acc is tuple of {bytes_left_to_accumulate, accumulated_binary} - When bytes_left_to_accumulate is equal to 0 it means that whole frame has been accumulated - """ - defstruct [:timebase, frame_acc: <<>>, start_of_stream?: true] + @type t :: %__MODULE__{ + timebase: Ratio.t(), + frame_acc: binary(), + beginning_of_stream: boolean() + } + + defstruct timebase: nil, + frame_acc: <<>>, + beginning_of_stream: true end @impl true @@ -35,29 +38,29 @@ defmodule Membrane.Element.IVF.Deserializer do @impl true def handle_stream_format(_pad, _stream_format, _ctx, state) do - # ignore incoming stream_format, we will send our own - # in handle_buffer {[], state} end @impl true - def handle_buffer(:input, buffer, _ctx, %State{start_of_stream?: true} = state) do + def handle_buffer(:input, buffer, _ctx, %State{beginning_of_stream: true} = state) do state = %State{state | frame_acc: state.frame_acc <> buffer.payload} with {:ok, file_header, rest} <- Headers.parse_ivf_header(state.frame_acc), - {:ok, buffer, rest} <- get_buffer(rest, Ratio.new(file_header.scale, file_header.rate)) do + {:ok, buffer, rest} <- get_buffer(rest, file_header.timebase) do stream_format = case file_header.four_cc do - "VP90" -> %Membrane.RemoteStream{content_format: VP9, type: :packetized} - "VP80" -> %Membrane.RemoteStream{content_format: VP8, type: :packetized} + "VP90" -> %VP9{width: file_header.width, height: file_header.height} + "VP80" -> %VP8{width: file_header.width, height: file_header.height} end - {[stream_format: {:output, stream_format}, buffer: {:output, buffer}], - %State{ - frame_acc: rest, - start_of_stream?: false, - timebase: Ratio.new(file_header.scale, file_header.rate) - }} + { + [stream_format: {:output, stream_format}, buffer: {:output, buffer}], + %State{ + frame_acc: rest, + beginning_of_stream: false, + timebase: file_header.timebase + } + } else {:error, :too_short} -> {[], state} diff --git a/lib/membrane_element_ivf/headers.ex b/lib/membrane_ivf/headers.ex similarity index 61% rename from lib/membrane_element_ivf/headers.ex rename to lib/membrane_ivf/headers.ex index 50b6c9b..3d51698 100644 --- a/lib/membrane_element_ivf/headers.ex +++ b/lib/membrane_ivf/headers.ex @@ -1,35 +1,32 @@ -defmodule Membrane.Element.IVF.Headers do +defmodule Membrane.IVF.Headers do @moduledoc false alias Membrane.{VP8, VP9} + @signature "DKIF" + @version 0 + @header_length 32 + defmodule FileHeader do @moduledoc """ A struct representing IVF file header """ @type t :: %__MODULE__{ - signature: String.t(), - version: non_neg_integer(), - length_of_header: non_neg_integer(), four_cc: String.t(), - width: non_neg_integer(), - height: non_neg_integer(), - rate: non_neg_integer(), - scale: non_neg_integer(), + width: pos_integer(), + height: pos_integer(), + timebase: Ratio.t(), frame_count: non_neg_integer() } - defstruct [ - :signature, - :version, - :length_of_header, + @enforce_keys [ :four_cc, :width, :height, - :rate, - :scale, + :timebase, :frame_count ] + defstruct @enforce_keys end defmodule FrameHeader do @@ -49,17 +46,12 @@ defmodule Membrane.Element.IVF.Headers do # bytes 4-11 64-bit presentation timestamp # bytes 12.. frame data - # Function firstly calculate # calculating ivf timestamp from membrane timestamp(timebase for membrane timestamp is nanosecond, and timebase for ivf is passed in options) @spec create_ivf_frame_header(integer, number | Ratio.t(), number | Ratio.t()) :: binary def create_ivf_frame_header(size, timestamp, timebase) do ivf_timestamp = Membrane.Time.divide_by_timebase(timestamp, Membrane.Time.seconds(timebase)) - # conversion to little-endian binary strings - size_le = <> - timestamp_le = <> - - size_le <> timestamp_le + <> end # IVF Header: @@ -73,39 +65,23 @@ defmodule Membrane.Element.IVF.Headers do # bytes 20-23 time base numerator (scale) # bytes 24-27 number of frames in file # bytes 28-31 unused - @spec create_ivf_header(integer, integer, Ratio.t(), integer, any) :: binary + @spec create_ivf_header(pos_integer(), pos_integer(), Ratio.t(), non_neg_integer(), any()) :: + binary() def create_ivf_header(width, height, timebase, frame_count, stream_format) do codec_four_cc = case stream_format do %Membrane.RemoteStream{content_format: VP9} -> "VP90" + %VP9{} -> "VP90" %Membrane.RemoteStream{content_format: VP8} -> "VP80" + %VP8{} -> "VP80" _unknown -> "\0\0\0\0" end %Ratio{denominator: rate, numerator: scale} = timebase - signature = "DKIF" - version = <<0, 0>> - length_of_header = <<32, 0>> - # conversion to little-endian binary strings - width_le = <> - height_le = <> - rate_le = <> - scale_le = <> - frame_count = <> - # field is not used so it is set to 0 - unused = <<0::32>> - - signature <> - version <> - length_of_header <> - codec_four_cc <> - width_le <> - height_le <> - rate_le <> - scale_le <> - frame_count <> - unused + <<@signature, @version::16-little, @header_length::16-little, codec_four_cc::binary-4, + width::16-little, height::16-little, rate::32-little, scale::32-little, + frame_count::32-little, 0::32>> end @spec parse_ivf_frame_header(binary()) :: @@ -122,21 +98,17 @@ defmodule Membrane.Element.IVF.Headers do def parse_ivf_header(payload) when byte_size(payload) < 32, do: {:error, :too_short} def parse_ivf_header( - <> + <<@signature, @version::16-little, @header_length::16-little, four_cc::binary-size(4), + width::16-little, height::16-little, rate::32-little, scale::32-little, + frame_count::32-little, _unused::32, rest::binary>> ) do - if String.valid?(signature) and String.valid?(four_cc) do + if String.valid?(four_cc) do {:ok, %FileHeader{ - signature: signature, - version: version, - length_of_header: length_of_header, four_cc: four_cc, width: width, height: height, - rate: rate, - scale: scale, + timebase: Ratio.new(scale, rate), frame_count: frame_count }, rest} else diff --git a/lib/membrane_ivf/serializer.ex b/lib/membrane_ivf/serializer.ex new file mode 100644 index 0000000..ae098b8 --- /dev/null +++ b/lib/membrane_ivf/serializer.ex @@ -0,0 +1,147 @@ +defmodule Membrane.IVF.Serializer do + @moduledoc """ + Serializes video stream into IVF format. + """ + + use Membrane.Filter + use Numbers, overload_operators: true + + alias Membrane.IVF.Headers + alias Membrane.{Buffer, File, IVF, RemoteStream} + alias Membrane.{VP8, VP9} + + @frame_count_file_position 24 + + def_options width: [ + spec: non_neg_integer() | nil, + default: nil, + description: """ + Width of a frame, needed if not provided with stream format. If it's not specified either in this option or the stream format, the element will crash. + """ + ], + height: [ + spec: non_neg_integer() | nil, + default: nil, + description: """ + Height of a frame, needed if not provided with stream format. If it's not specified either in this option or the stream format, the element will crash. + """ + ], + timebase: [ + spec: {pos_integer(), pos_integer()}, + default: {1, 1_000_000_000}, + description: """ + Timebase for the timestamps added to the frames + """ + ], + frame_count: [ + spec: non_neg_integer() | :dynamic, + default: :dynamic, + description: """ + Number of frames in the stream. If set to `:dynamic` the frames will be counted and + a `Membrane.File.SeekSinkEvent` will be sent at the end of the stream to insert this value in + the file header. In that case the element MUST be used along with `Membrane.File.Sink` + or any other sink that can handle `Membrane.File.SeekSinkEvent`. + """ + ] + + def_input_pad :input, + accepted_format: + any_of( + %RemoteStream{content_format: format, type: :packetized} when format in [VP9, VP8], + VP8, + VP9 + ) + + def_output_pad :output, accepted_format: _any + + defmodule State do + @moduledoc false + + @type t :: %__MODULE__{ + width: non_neg_integer() | nil, + height: non_neg_integer() | nil, + timebase: Ratio.t(), + frame_count: non_neg_integer() | :dynamic + } + + @enforce_keys [:width, :height, :timebase, :frame_count] + defstruct @enforce_keys ++ + [ + frames_processed: 0 + ] + end + + @impl true + def handle_init(_ctx, options) do + {timebase_num, timebase_den} = options.timebase + + {[], + %State{ + width: options.width, + height: options.height, + timebase: Ratio.new(timebase_num, timebase_den), + frame_count: options.frame_count + }} + end + + @impl true + def handle_playing(_ctx, state) do + {[stream_format: {:output, %Membrane.RemoteStream{type: :packetized, content_format: IVF}}], + state} + end + + @impl true + def handle_stream_format(:input, stream_format, _ctx, state) do + {width, height} = get_frame_dimensions(stream_format, state) + + frame_count = if state.frame_count == :dynamic, do: 0, else: state.frame_count + + ivf_header = + Headers.create_ivf_header(width, height, state.timebase, frame_count, stream_format) + + {[buffer: {:output, %Buffer{payload: ivf_header}}], state} + end + + @impl true + def handle_buffer(:input, buffer, _ctx, state) do + %Buffer{payload: frame, pts: timestamp} = buffer + + ivf_frame = + Headers.create_ivf_frame_header(byte_size(frame), timestamp, state.timebase) <> + frame + + {[buffer: {:output, %Buffer{buffer | payload: ivf_frame}}], + %{state | frames_processed: state.frames_processed + 1}} + end + + @impl true + def handle_end_of_stream(:input, _ctx, state) do + actions = + if state.frame_count == :dynamic do + [ + event: {:output, %File.SeekSinkEvent{position: @frame_count_file_position}}, + buffer: {:output, %Buffer{payload: <>}}, + end_of_stream: :output + ] + else + [end_of_stream: :output] + end + + {actions, state} + end + + @spec get_frame_dimensions(RemoteStream.t() | VP8.t() | VP9.t(), State.t()) :: + {width :: non_neg_integer(), height :: non_neg_integer()} + defp get_frame_dimensions(input_stream_format, state) do + case input_stream_format do + %RemoteStream{} -> + { + state.width || raise("Width not provided"), + state.height || raise("Height not provided") + } + + %{width: width, height: height} -> + {width, height} + end + end +end diff --git a/lib/serializer.ex b/lib/serializer.ex deleted file mode 100644 index 98c4cdf..0000000 --- a/lib/serializer.ex +++ /dev/null @@ -1,74 +0,0 @@ -defmodule Membrane.Element.IVF.Serializer do - @moduledoc """ - Serializes video stream into IVF format. - """ - - use Membrane.Filter - - alias Membrane.Element.IVF - alias Membrane.{Buffer, RemoteStream} - alias Membrane.{VP8, VP9} - - def_options width: [spec: [integer], description: "width of frame"], - height: [spec: [integer], description: "height of frame"], - scale: [spec: [integer], default: 1, description: "scale"], - rate: [spec: [integer], default: 1_000_000, description: "rate"], - frame_count: [spec: [integer], default: 0, description: "number of frames"] - - def_input_pad :input, - accepted_format: - %RemoteStream{content_format: format, type: :packetized} when format in [VP9, VP8], - flow_control: :manual, - demand_unit: :buffers - - def_output_pad :output, flow_control: :manual, accepted_format: _any - - defmodule State do - @moduledoc false - defstruct [:width, :height, :timebase, :first_frame, :frame_count] - end - - @impl true - def handle_init(_ctx, options) do - use Numbers, overload_operators: true - - {[], - %State{ - width: options.width, - height: options.height, - timebase: Ratio.new(options.scale, options.rate), - frame_count: options.frame_count, - first_frame: true - }} - end - - @impl true - def handle_demand(:output, size, :buffers, _ctx, state) do - {[demand: {:input, size}], state} - end - - @impl true - def handle_buffer(:input, buffer, ctx, state) do - %Buffer{payload: frame, pts: timestamp} = buffer - - ivf_frame = - IVF.Headers.create_ivf_frame_header(byte_size(frame), timestamp, state.timebase) <> - frame - - ivf_file_header = - if state.first_frame, - do: - IVF.Headers.create_ivf_header( - state.width, - state.height, - state.timebase, - state.frame_count, - ctx.pads.input.stream_format - ) - - ivf_buffer = (ivf_file_header || "") <> ivf_frame - - {[buffer: {:output, %Buffer{buffer | payload: ivf_buffer}}, redemand: :output], - %State{state | first_frame: false}} - end -end diff --git a/mix.exs b/mix.exs index 8c619af..70836e6 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Membrane.IVF.Plugin.MixProject do use Mix.Project - @version "0.7.0" + @version "0.8.0" @github_url "https://github.com/membraneframework/membrane_ivf_plugin" def project do @@ -38,10 +38,12 @@ defmodule Membrane.IVF.Plugin.MixProject do defp deps do [ {:membrane_core, "~> 1.0"}, + {:membrane_vp8_format, "~> 0.5.0"}, + {:membrane_vp9_format, "~> 0.5.0"}, + {:membrane_file_plugin, "~> 0.17.0"}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, {:dialyxir, ">= 0.0.0", only: :dev, runtime: false}, - {:credo, ">= 0.0.0", only: :dev, runtime: false}, - {:membrane_file_plugin, "~> 0.16.0", only: :test} + {:credo, ">= 0.0.0", only: :dev, runtime: false} ] end diff --git a/mix.lock b/mix.lock index 1b3b222..0856372 100644 --- a/mix.lock +++ b/mix.lock @@ -1,22 +1,25 @@ %{ - "bunch": {:hex, :bunch, "1.6.0", "4775f8cdf5e801c06beed3913b0bd53fceec9d63380cdcccbda6be125a6cfd54", [:mix], [], "hexpm", "ef4e9abf83f0299d599daed3764d19e8eac5d27a5237e5e4d5e2c129cfeb9a22"}, - "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, - "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, - "dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, - "membrane_core": {:hex, :membrane_core, "1.0.0", "1b543aefd952283be1f2a215a1db213aa4d91222722ba03cd35280622f1905ee", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:qex, "~> 0.3", [hex: :qex, repo: "hexpm", optional: false]}, {:ratio, "~> 3.0", [hex: :ratio, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "352c90fd0a29942143c4bf7a727cc05c632e323f50a1a4e99321b1e8982f1533"}, - "membrane_file_plugin": {:hex, :membrane_file_plugin, "0.16.0", "7917f6682c22b9bcfc2ca20ed960eee0f7d03ad31fd5f59ed850f1fe3ddd545a", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "b0727998f75a9b4dab8a2baefdfc13c3eac00a04e061ab1b0e61dc5566927acc"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, + "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, + "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, + "ex_doc": {:hex, :ex_doc, "0.34.1", "9751a0419bc15bc7580c73fde506b17b07f6402a1e5243be9e0f05a68c723368", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d441f1a86a235f59088978eff870de2e815e290e44a8bd976fe5d64470a4c9d2"}, + "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, + "membrane_core": {:hex, :membrane_core, "1.1.0", "c3bbaa5af7c26a7c3748e573efe343c2104801e3463b9e491a607e82860334a4", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:qex, "~> 0.3", [hex: :qex, repo: "hexpm", optional: false]}, {:ratio, "~> 3.0 or ~> 4.0", [hex: :ratio, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3209d7f7e86d736cb7caffbba16b075c571cebb9439ab939ed6119c50fb59a5"}, + "membrane_file_plugin": {:hex, :membrane_file_plugin, "0.17.0", "e855a848e84eaed537b41fd4436712038fc5518059eadc8609c83cd2d819653a", [:mix], [{:logger_backends, "~> 1.0", [hex: :logger_backends, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "9c3653ca9f13bb409b36257d6094798d4625c739ab7a4035c12308622eb16e0b"}, + "membrane_vp8_format": {:hex, :membrane_vp8_format, "0.5.0", "a589c20bb9d97ddc9b717684d00cefc84e2500ce63a0c33c4b9618d9b2f9b2ea", [:mix], [], "hexpm", "d29e0dae4bebc6838e82e031c181fe626d168c687e4bc617c1d0772bdeed19d5"}, + "membrane_vp9_format": {:hex, :membrane_vp9_format, "0.5.0", "c6a4f2cbfc39dba5d80ad8287162c52b5cf6488676bd64435c1ac957bd16e66f", [:mix], [], "hexpm", "68752d8cbe7270ec222fc84a7d1553499f0d8ff86ef9d9e89f8955d49e20278e"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, "qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"}, - "ratio": {:hex, :ratio, "3.0.2", "60a5976872a4dc3d873ecc57eed1738589e99d1094834b9c935b118231297cfb", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "3a13ed5a30ad0bfd7e4a86bf86d93d2b5a06f5904417d38d3f3ea6406cdfc7bb"}, + "ratio": {:hex, :ratio, "4.0.1", "3044166f2fc6890aa53d3aef0c336f84b2bebb889dc57d5f95cc540daa1912f8", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "c60cbb3ccdff9ffa56e7d6d1654b5c70d9f90f4d753ab3a43a6bf40855b881ce"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, } diff --git a/test/fixtures/input_vp8.ivf b/test/fixtures/input_vp8.ivf index 1c21cf2..a82c2c3 100644 Binary files a/test/fixtures/input_vp8.ivf and b/test/fixtures/input_vp8.ivf differ diff --git a/test/fixtures/input_vp9.ivf b/test/fixtures/input_vp9.ivf index 0efb89f..9d3aaa7 100644 Binary files a/test/fixtures/input_vp9.ivf and b/test/fixtures/input_vp9.ivf differ diff --git a/test/integration_test.exs b/test/integration_test.exs index 91977af..022ddf4 100644 --- a/test/integration_test.exs +++ b/test/integration_test.exs @@ -1,73 +1,45 @@ -defmodule Membrane.Element.IVF.IntegrationTest do +defmodule Membrane.IVF.IntegrationTest do use ExUnit.Case import Membrane.Testing.Assertions + import Membrane.ChildrenSpec - alias Membrane.Element.IVF - alias Membrane.{Testing} + alias Membrane.{IVF, Testing} - @input_video_vp8 %{path: "./test/fixtures/input_vp8.ivf", width: 1080, height: 720} - @input_video_vp9 %{path: "./test/fixtures/input_vp9.ivf", width: 1080, height: 720} - @results_dir "./test/results/" - @result_file_vp8 "result_vp8.ivf" - @result_file_vp9 "result_vp9.ivf" + @input_path_vp8 "./test/fixtures/input_vp8.ivf" + @input_path_vp9 "./test/fixtures/input_vp9.ivf" + @output_file_vp8 "output_vp8.ivf" + @output_file_vp9 "output_vp9.ivf" - defmodule TestPipeline do - use Membrane.Pipeline - - @impl true - def handle_init(_ctx, options) do - spec = [ - child(:serializer, %IVF.Serializer{ - width: options.input.width, - height: options.input.height, - rate: 30 - }), - child(:file_source, %Membrane.File.Source{location: options.input.path}) - |> child(:deserializer, IVF.Deserializer) - |> get_child(:serializer) - |> child(:file_sink, %Membrane.File.Sink{location: options.result_file}) - ] - - {[spec: spec], %{}} + describe "deserializing ivf and serializing back for" do + @describetag :tmp_dir + test "VP8", %{tmp_dir: tmp_dir} do + test_stream(@input_path_vp8, @output_file_vp8, tmp_dir) end - @impl true - def handle_child_notification(_notification, _child, _ctx, state) do - {[], state} + test "VP9", %{tmp_dir: tmp_dir} do + test_stream(@input_path_vp9, @output_file_vp9, tmp_dir) end end - test "deserializing vp8 ivf and serializing back" do - test_stream(@input_video_vp8, @result_file_vp8) - end - - test "deserializing vp9 ivf and serializing back" do - test_stream(@input_video_vp9, @result_file_vp9) - end - - defp test_stream(input, result) do - if !File.exists?(@results_dir) do - File.mkdir!(@results_dir) - end - - result_file = Path.join(@results_dir, result) + defp test_stream(input_path, output_file, tmp_dir) do + output_path = Path.join(tmp_dir, output_file) pipeline = - [ - module: TestPipeline, - custom_args: %{ - input: input, - result_file: result_file - } - ] - |> Testing.Pipeline.start_link_supervised!() + Testing.Pipeline.start_link_supervised!( + spec: + child(:file_source, %Membrane.File.Source{location: input_path}) + |> child(:deserializer, IVF.Deserializer) + |> child(:serializer, %IVF.Serializer{ + timebase: {1, 30} + }) + |> child(:file_sink, %Membrane.File.Sink{location: output_path}) + ) assert_end_of_stream(pipeline, :file_sink) Testing.Pipeline.terminate(pipeline) - assert File.read!(input.path) == - File.read!(result_file) + assert File.read!(input_path) == File.read!(output_path) end end diff --git a/test/results/result_vp8.ivf b/test/results/result_vp8.ivf deleted file mode 100644 index 1c21cf2..0000000 Binary files a/test/results/result_vp8.ivf and /dev/null differ diff --git a/test/results/result_vp9.ivf b/test/results/result_vp9.ivf deleted file mode 100644 index 0efb89f..0000000 Binary files a/test/results/result_vp9.ivf and /dev/null differ diff --git a/test/serializer_test.exs b/test/serializer_test.exs index e92876a..8c9bbee 100644 --- a/test/serializer_test.exs +++ b/test/serializer_test.exs @@ -1,17 +1,16 @@ -defmodule Membrane.Element.IVF.SerializerTest do +defmodule Membrane.IVF.SerializerTest do use ExUnit.Case use Numbers, overload_operators: true import Membrane.Testing.Assertions alias Membrane.{Buffer, RemoteStream, Testing} - alias Membrane.Element.IVF + alias Membrane.IVF alias Membrane.VP9 @fixtures_dir "./test/fixtures/" - @results_dir "./test/results/" @input_file "input_vp9.ivf" - @result_file "result_vp9.ivf" + @result_file "output_vp9.ivf" defmodule TestPipeline do use Membrane.Pipeline @@ -23,15 +22,13 @@ defmodule Membrane.Element.IVF.SerializerTest do do: %Membrane.File.Sink{location: options.result_file}, else: Testing.Sink - spec = [ + spec = child(:source, %Testing.Source{ output: Testing.Source.output_from_buffers(options.buffers), stream_format: %RemoteStream{content_format: VP9, type: :packetized} - }), - get_child(:source) - |> child(:ivf_serializer, %IVF.Serializer{width: 1080, height: 720, rate: 30}) + }) + |> child(:ivf_serializer, %IVF.Serializer{width: 1080, height: 720, timebase: {1, 30}}) |> child(:sink, sink) - ] {[spec: spec], %{}} end @@ -66,15 +63,17 @@ defmodule Membrane.Element.IVF.SerializerTest do assert_start_of_stream(pipeline, :sink) + assert_sink_buffer(pipeline, :sink, ivf_file_header_buffer) assert_sink_buffer(pipeline, :sink, ivf_buffer) - <> = - ivf_buffer.payload + <> = ivf_file_header_buffer.payload + + <> = ivf_buffer.payload <> = file_header + _number_of_frames::binary-size(4), _unused::binary-size(4)>> = file_header assert signature == "DKIF" assert version == <<0::16>> @@ -84,7 +83,6 @@ defmodule Membrane.Element.IVF.SerializerTest do assert height == <<720::16-little>> assert time_base_denominator == <<30::32-little>> assert time_base_numerator == <<1::32-little>> - assert number_of_frames == <<0::32-little>> <> = frame_header assert size_of_frame == <> @@ -103,20 +101,25 @@ defmodule Membrane.Element.IVF.SerializerTest do # x = 30 * 1/30 * [s/s] = 1 assert timestamp == <<1::64-little>> + assert_sink_event(pipeline, :sink, %Membrane.File.SeekSinkEvent{position: 24}) + assert_sink_buffer(pipeline, :sink, %Buffer{payload: number_of_frames}) + assert number_of_frames == <<2::32-little>> + assert_end_of_stream(pipeline, :sink) Testing.Pipeline.terminate(pipeline) end - test "serialize real vp9 buffers" do - buffers = File.read!(@fixtures_dir <> "capture.dump") |> :erlang.binary_to_term() + @tag :tmp_dir + test "serialize real vp9 buffers", %{tmp_dir: tmp_dir} do + buffers = Path.join(@fixtures_dir, "capture.dump") |> File.read!() |> :erlang.binary_to_term() pipeline = [ module: TestPipeline, custom_args: %{ to_file?: true, - result_file: @results_dir <> @result_file, + result_file: Path.join(tmp_dir, @result_file), buffers: buffers } ] @@ -126,8 +129,8 @@ defmodule Membrane.Element.IVF.SerializerTest do assert_end_of_stream(pipeline, :sink) - assert File.read!(@results_dir <> @result_file) == - File.read!(@fixtures_dir <> @input_file) + assert Path.join(tmp_dir, @result_file) |> File.read!() == + Path.join(@fixtures_dir, @input_file) |> File.read!() Testing.Pipeline.terminate(pipeline) end