From 648f782927c280d2006eb87ae914b6c860e95a8e Mon Sep 17 00:00:00 2001 From: Jakub Pisarek <99591440+sgfn@users.noreply.github.com> Date: Thu, 9 Feb 2023 14:24:34 +0100 Subject: [PATCH 1/3] Add timestamp generation for profiles baseline, constrained baseline --- lib/membrane_h264_plugin/parser.ex | 141 ++++++++++++---- mix.exs | 2 +- test/fixtures/input-10-720p-baseline.h264 | Bin 0 -> 25230 bytes test/integration/modes_test.exs | 44 +---- test/integration/test_source.ex | 38 +++++ .../integration/timestamp_generation_test.exs | 156 ++++++++++++++++++ .../{caps_test.exs => stream_format_test.exs} | 0 test/test_helper.exs | 1 + 8 files changed, 305 insertions(+), 77 deletions(-) create mode 100644 test/fixtures/input-10-720p-baseline.h264 create mode 100644 test/integration/test_source.ex create mode 100644 test/integration/timestamp_generation_test.exs rename test/parser/{caps_test.exs => stream_format_test.exs} (100%) diff --git a/lib/membrane_h264_plugin/parser.ex b/lib/membrane_h264_plugin/parser.ex index 977bd29..79d66d7 100644 --- a/lib/membrane_h264_plugin/parser.ex +++ b/lib/membrane_h264_plugin/parser.ex @@ -21,8 +21,11 @@ defmodule Membrane.H264.Parser do * Receiving `%Membrane.H264.RemoteStream{alignment: :au}` results in the parser mode being set to `:au_aligned` The distinguishment between parser modes was introduced to eliminate the redundant operations and to provide a reliable way - for timestamps rewritting: - * in the `:bytestream` mode, the output buffers have their `:pts` and `:dts` set to nil + for rewriting of timestamps: + * in the `:bytestream` mode: + * if option `:framerate` is set to nil, the output buffers have their `:pts` and `:dts` set to nil + * if framerate is specified, `:pts` and `:dts` will be generated automatically + This may only be used with h264 profiles `:baseline` and `:constrained_baseline`. * in the `:nalu_aligned` mode, the output buffers have their `:pts` and `:dts` set to `:pts` and `:dts` of the input buffer that was holding the first NAL unit making up given access unit (that is being sent inside that output buffer). * in the `:au_aligned` mode, the output buffers have their `:pts` and `:dts` set to `:pts` and `:dts` of the input buffer @@ -72,7 +75,7 @@ defmodule Membrane.H264.Parser do description: """ Framerate of the video, represented as a tuple consisting of a numerator and the denominator. - It's value will be sent inside the output Membrane.H264 caps. + Its value will be sent inside the output Membrane.H264 stream format. """ ] @@ -83,6 +86,7 @@ defmodule Membrane.H264.Parser do nalu_parser: NALuParser.new(), au_splitter: AUSplitter.new(), mode: nil, + profile: nil, previous_timestamps: {nil, nil}, framerate: opts.framerate } @@ -138,21 +142,7 @@ defmodule Membrane.H264.Parser do {access_units, au_splitter} end - {pts, dts} = - case state.mode do - :bytestream -> {nil, nil} - :nalu_aligned -> state.previous_timestamps - :au_aligned -> {buffer.pts, buffer.dts} - end - - state = - if state.mode == :nalu_aligned and state.previous_timestamps != {buffer.pts, buffer.dts} do - %{state | previous_timestamps: {buffer.pts, buffer.dts}} - else - state - end - - actions = prepare_actions_for_aus(access_units, pts, dts, state) + {actions, state} = prepare_actions_for_aus(access_units, buffer.pts, buffer.dts, state) state = %{ state @@ -184,13 +174,7 @@ defmodule Membrane.H264.Parser do {remaining_nalus, au_splitter} = AUSplitter.flush(au_splitter) maybe_improper_aus = access_units ++ [remaining_nalus] - {pts, dts} = - case state.mode do - :bytestream -> {nil, nil} - :nalu_aligned -> state.previous_timestamps - end - - actions = prepare_actions_for_aus(maybe_improper_aus, pts, dts, state) + {actions, state} = prepare_actions_for_aus(maybe_improper_aus, state) actions = if stream_format_sent?(actions, ctx), do: actions, else: [] state = %{ @@ -208,19 +192,91 @@ defmodule Membrane.H264.Parser do {[end_of_stream: :output], state} end - defp prepare_actions_for_aus(aus, pts, dts, state) do - Enum.reduce(aus, [], fn au, acc -> - sps_actions = - case Enum.find(au, &(&1.type == :sps)) do - nil -> - [] + defp prepare_actions_for_aus(aus, state) do + prepare_actions_for_aus(aus, nil, nil, state) + end - sps_nalu -> - [stream_format: {:output, Format.from_sps(sps_nalu, framerate: state.framerate)}] - end + defp prepare_actions_for_aus(aus, buffer_pts, buffer_dts, state) do + {pts, dts} = + case state.mode do + :bytestream -> + if state.previous_timestamps == {nil, nil}, + do: {-1, -1}, + else: state.previous_timestamps + + :nalu_aligned -> + state.previous_timestamps + + :au_aligned -> + {buffer_pts, buffer_dts} + end - acc ++ sps_actions ++ [{:buffer, {:output, wrap_into_buffer(au, pts, dts)}}] - end) + {actions, au_count, profile} = + Enum.reduce(aus, {[], 0, state.profile}, fn au, {acc, cnt, profile} -> + {sps_actions, profile} = maybe_parse_sps(au, state, profile) + {pts, dts} = maybe_generate_timestamps(pts, dts, state, profile, cnt) + + {acc ++ sps_actions ++ [{:buffer, {:output, wrap_into_buffer(au, pts, dts)}}], cnt + 1, + profile} + end) + + state = + maybe_update_state(buffer_pts, buffer_dts, pts, dts, au_count, %{state | profile: profile}) + + {actions, state} + end + + defp maybe_parse_sps(au, state, profile) do + case Enum.find(au, &(&1.type == :sps)) do + nil -> + {[], profile} + + sps_nalu -> + fmt = Format.from_sps(sps_nalu, framerate: state.framerate) + {[stream_format: {:output, fmt}], fmt.profile} + end + end + + defp maybe_generate_timestamps(pts, dts, state, _profile, _order_number) + when state.mode != :bytestream do + {pts, dts} + end + + defp maybe_generate_timestamps( + presentation_order_number, + decoding_order_number, + state, + profile, + offset + ) do + cond do + state.framerate == nil or profile == nil -> + {nil, nil} + + h264_profile_tsgen_supported?(profile) -> + generate_timestamps( + state.framerate, + presentation_order_number + offset + 1, + decoding_order_number + offset + 1 + ) + + true -> + raise("Timestamp generation for H264 profile `#{inspect(profile)}` is unsupported") + end + end + + defp maybe_update_state(buffer_pts, buffer_dts, pts, dts, au_count, state) do + cond do + state.mode == :nalu_aligned and state.previous_timestamps != {buffer_pts, buffer_dts} -> + %{state | previous_timestamps: {buffer_pts, buffer_dts}} + + state.mode == :bytestream and state.framerate != nil and state.profile != nil and + au_count > 0 -> + %{state | previous_timestamps: {pts + au_count, dts + au_count}} + + true -> + state + end end defp wrap_into_buffer(access_unit, pts, dts) do @@ -281,4 +337,17 @@ defmodule Membrane.H264.Parser do do: Enum.any?(actions, &match?({:stream_format, _stream_format}, &1)) defp stream_format_sent?(_actions, _ctx), do: true + + defp h264_profile_tsgen_supported?(profile), + do: profile in [:baseline, :constrained_baseline] + + defp generate_timestamps( + {frames, seconds} = _framerate, + presentation_order_number, + decoding_order_number + ) do + pts = div(presentation_order_number * seconds * Membrane.Time.second(), frames) + dts = div(decoding_order_number * seconds * Membrane.Time.second(), frames) + {pts, dts} + end end diff --git a/mix.exs b/mix.exs index d469a0a..ef2006d 100644 --- a/mix.exs +++ b/mix.exs @@ -66,7 +66,7 @@ defmodule Membrane.H264.TODO.Mixfile do licenses: ["Apache-2.0"], links: %{ "GitHub" => @github_url, - "Membrane Framework Homepage" => "https://membraneframework.org" + "Membrane Framework Homepage" => "https://membrane.stream" } ] end diff --git a/test/fixtures/input-10-720p-baseline.h264 b/test/fixtures/input-10-720p-baseline.h264 new file mode 100644 index 0000000000000000000000000000000000000000..311d19cff59c573bf9765d052210a5c0815ead92 GIT binary patch literal 25230 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 literal 0 HcmV?d00001 diff --git a/test/integration/modes_test.exs b/test/integration/modes_test.exs index 334aed8..7e2ae84 100644 --- a/test/integration/modes_test.exs +++ b/test/integration/modes_test.exs @@ -7,6 +7,7 @@ defmodule Membrane.H264.ModesTest do alias Membrane.Buffer alias Membrane.H264.Parser alias Membrane.H264.Parser.{AUSplitter, NALuParser, NALuSplitter} + alias Membrane.H264.TestSource alias Membrane.Testing.{Pipeline, Sink} @h264_input_file "test/fixtures/input-10-720p.h264" @@ -54,43 +55,6 @@ defmodule Membrane.H264.ModesTest do |> elem(0) end - defmodule ModeTestSource do - use Membrane.Source - - def_options mode: [] - - def_output_pad :output, - demand_mode: :auto, - mode: :push, - accepted_format: - any_of( - %Membrane.RemoteStream{type: :bytestream}, - %Membrane.H264.RemoteStream{alignment: alignment} when alignment in [:au, :nalu] - ) - - @impl true - def handle_init(_ctx, opts) do - {[], %{mode: opts.mode}} - end - - @impl true - def handle_parent_notification(actions, _ctx, state) do - {actions, state} - end - - @impl true - def handle_playing(_ctx, state) do - stream_format = - case state.mode do - :bytestream -> %Membrane.RemoteStream{type: :bytestream} - :nalu_aligned -> %Membrane.H264.RemoteStream{alignment: :nalu} - :au_aligned -> %Membrane.H264.RemoteStream{alignment: :au} - end - - {[stream_format: {:output, stream_format}], state} - end - end - test "if the pts and dts are set to nil in :bytestream mode" do binary = File.read!(@h264_input_file) mode = :bytestream @@ -99,7 +63,7 @@ defmodule Membrane.H264.ModesTest do {:ok, _supervisor_pid, pid} = Pipeline.start_supervised( structure: [ - child(:source, %ModeTestSource{mode: mode}) + child(:source, %TestSource{mode: mode}) |> child(:parser, Parser) |> child(:sink, Sink) ] @@ -127,7 +91,7 @@ defmodule Membrane.H264.ModesTest do {:ok, _supervisor_pid, pid} = Pipeline.start_supervised( structure: [ - child(:source, %ModeTestSource{mode: mode}) + child(:source, %TestSource{mode: mode}) |> child(:parser, Parser) |> child(:sink, Sink) ] @@ -157,7 +121,7 @@ defmodule Membrane.H264.ModesTest do {:ok, _supervisor_pid, pid} = Pipeline.start_supervised( structure: [ - child(:source, %ModeTestSource{mode: mode}) + child(:source, %TestSource{mode: mode}) |> child(:parser, Parser) |> child(:sink, Sink) ] diff --git a/test/integration/test_source.ex b/test/integration/test_source.ex new file mode 100644 index 0000000..4c5506b --- /dev/null +++ b/test/integration/test_source.ex @@ -0,0 +1,38 @@ +defmodule Membrane.H264.TestSource do + @moduledoc false + + use Membrane.Source + + def_options mode: [] + + def_output_pad :output, + demand_mode: :auto, + mode: :push, + accepted_format: + any_of( + %Membrane.RemoteStream{type: :bytestream}, + %Membrane.H264.RemoteStream{alignment: alignment} when alignment in [:au, :nalu] + ) + + @impl true + def handle_init(_ctx, opts) do + {[], %{mode: opts.mode}} + end + + @impl true + def handle_parent_notification(actions, _ctx, state) do + {actions, state} + end + + @impl true + def handle_playing(_ctx, state) do + stream_format = + case state.mode do + :bytestream -> %Membrane.RemoteStream{type: :bytestream} + :nalu_aligned -> %Membrane.H264.RemoteStream{alignment: :nalu} + :au_aligned -> %Membrane.H264.RemoteStream{alignment: :au} + end + + {[stream_format: {:output, stream_format}], state} + end +end diff --git a/test/integration/timestamp_generation_test.exs b/test/integration/timestamp_generation_test.exs new file mode 100644 index 0000000..a2a4fc1 --- /dev/null +++ b/test/integration/timestamp_generation_test.exs @@ -0,0 +1,156 @@ +defmodule Membrane.H264.TimestampGenerationTest do + @moduledoc false + + use ExUnit.Case + + import Membrane.ChildrenSpec + import Membrane.Testing.Assertions + + alias Membrane.Buffer + alias Membrane.H264.Parser + alias Membrane.H264.Parser.{AUSplitter, NALuParser, NALuSplitter} + alias Membrane.H264.TestSource + alias Membrane.Testing.{Pipeline, Sink} + + defmodule EnhancedPipeline do + use Membrane.Pipeline + + @impl true + def handle_init(_ctx, args) do + {[spec: args], %{}} + end + + @impl true + def handle_call({:get_child_pid, child}, ctx, state) do + child_pid = Map.get(ctx.children, child).pid + {[reply: child_pid], state} + end + end + + @h264_input_file "test/fixtures/input-10-720p.h264" + @h264_input_file_baseline "test/fixtures/input-10-720p-baseline.h264" + defp prepare_buffers(binary, :bytestream) do + buffers = + :binary.bin_to_list(binary) |> Enum.chunk_every(400) |> Enum.map(&:binary.list_to_bin(&1)) + + Enum.map(buffers, &%Membrane.Buffer{payload: &1}) + end + + defp prepare_buffers(binary, :au_aligned) do + {nalus_payloads, nalu_splitter} = NALuSplitter.split(binary, NALuSplitter.new()) + {last_nalu_payload, _nalu_splitter} = NALuSplitter.flush(nalu_splitter) + nalus_payloads = nalus_payloads ++ [last_nalu_payload] + + {nalus, _nalu_parser} = + Enum.map_reduce(nalus_payloads, NALuParser.new(), &NALuParser.parse(&1, &2)) + + {aus, au_splitter} = nalus |> AUSplitter.split(AUSplitter.new()) + {last_au, _au_splitter} = AUSplitter.flush(au_splitter) + aus = aus ++ [last_au] + + Enum.map_reduce(aus, 0, fn au, ts -> + {%Membrane.Buffer{payload: Enum.map_join(au, & &1.payload), pts: ts, dts: ts}, ts + 1} + end) + |> elem(0) + end + + test "if the pts and dts are set to nil in :bytestream mode when framerate isn't given" do + binary = File.read!(@h264_input_file_baseline) + mode = :bytestream + input_buffers = prepare_buffers(binary, mode) + + {:ok, _supervisor_pid, pid} = + Pipeline.start_supervised( + structure: [ + child(:source, %TestSource{mode: mode}) + |> child(:parser, Parser) + |> child(:sink, Sink) + ] + ) + + assert_pipeline_play(pid) + send_buffers_actions = for buffer <- input_buffers, do: {:buffer, {:output, buffer}} + Pipeline.message_child(pid, :source, send_buffers_actions ++ [end_of_stream: :output]) + + output_buffers = prepare_buffers(binary, :au_aligned) + + Enum.each(output_buffers, fn buf -> + payload = buf.payload + assert_sink_buffer(pid, :sink, %Buffer{payload: ^payload, pts: nil, dts: nil}) + end) + + Pipeline.terminate(pid, blocking?: true) + end + + test """ + if the pts and dts are generated correctly for profiles :baseline and :constrained_baseline + in :bytestream mode when framerate is given + """ do + binary = File.read!(@h264_input_file_baseline) + mode = :bytestream + input_buffers = prepare_buffers(binary, mode) + + framerate = {30, 1} + + {:ok, _supervisor_pid, pid} = + Pipeline.start_supervised( + structure: [ + child(:source, %TestSource{mode: mode}) + |> child(:parser, %Parser{framerate: framerate}) + |> child(:sink, Sink) + ] + ) + + assert_pipeline_play(pid) + send_buffers_actions = for buffer <- input_buffers, do: {:buffer, {:output, buffer}} + Pipeline.message_child(pid, :source, send_buffers_actions ++ [end_of_stream: :output]) + + output_buffers = prepare_buffers(binary, :au_aligned) + + {frames, seconds} = framerate + + Enum.reduce(output_buffers, 0, fn buf, order_number -> + payload = buf.payload + timestamp = div(seconds * Membrane.Time.second() * order_number, frames) + assert_sink_buffer(pid, :sink, %Buffer{payload: ^payload, pts: ^timestamp, dts: ^timestamp}) + order_number + 1 + end) + + Pipeline.terminate(pid, blocking?: true) + end + + test "if an error is raised when framerate is given for profiles other than :baseline and :constrained_baseline" do + binary = File.read!(@h264_input_file) + mode = :bytestream + input_buffers = prepare_buffers(binary, mode) + + {:ok, _supervisor_pid, pid} = + Pipeline.start_supervised( + custom_args: [ + child(:source, %TestSource{mode: mode}) + |> child(:parser, %Parser{framerate: {30, 1}}) + |> child(:sink, Sink) + ], + module: EnhancedPipeline + ) + + Pipeline.execute_actions(pid, playback: :playing) + assert_pipeline_play(pid) + parser_pid = Membrane.Pipeline.call(pid, {:get_child_pid, :parser}) + send_buffers_actions = for buffer <- input_buffers, do: {:buffer, {:output, buffer}} + + Process.monitor(parser_pid) + Pipeline.message_child(pid, :source, send_buffers_actions ++ [end_of_stream: :output]) + + error = + receive do + {:DOWN, _ref, :process, ^parser_pid, {%RuntimeError{message: msg}, _stacktrace}} -> msg + after + 2000 -> nil + end + + assert error =~ ~r/timestamp.*generation.*unsupported/i + + Pipeline.terminate(pid, blocking?: true) + end +end diff --git a/test/parser/caps_test.exs b/test/parser/stream_format_test.exs similarity index 100% rename from test/parser/caps_test.exs rename to test/parser/stream_format_test.exs diff --git a/test/test_helper.exs b/test/test_helper.exs index 6a0af57..b3af8e6 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1,2 @@ ExUnit.start(capture_log: true) +Code.require_file("test/integration/test_source.ex") From 7cbe09d5654f4eaa9d1f3d90c51bfa16256a3319 Mon Sep 17 00:00:00 2001 From: Jakub Pisarek <99591440+sgfn@users.noreply.github.com> Date: Fri, 10 Feb 2023 14:11:21 +0100 Subject: [PATCH 2/3] Fixes after review --- lib/membrane_h264_plugin/parser.ex | 89 +++++++------------ test/integration/modes_test.exs | 2 +- .../integration/timestamp_generation_test.exs | 2 +- test/{integration => support}/test_source.ex | 2 +- test/test_helper.exs | 1 - 5 files changed, 35 insertions(+), 61 deletions(-) rename test/{integration => support}/test_source.ex (94%) diff --git a/lib/membrane_h264_plugin/parser.ex b/lib/membrane_h264_plugin/parser.ex index 79d66d7..495c281 100644 --- a/lib/membrane_h264_plugin/parser.ex +++ b/lib/membrane_h264_plugin/parser.ex @@ -24,8 +24,8 @@ defmodule Membrane.H264.Parser do for rewriting of timestamps: * in the `:bytestream` mode: * if option `:framerate` is set to nil, the output buffers have their `:pts` and `:dts` set to nil - * if framerate is specified, `:pts` and `:dts` will be generated automatically - This may only be used with h264 profiles `:baseline` and `:constrained_baseline`. + * if framerate is specified, `:pts` and `:dts` will be generated automatically, based on that framerate, starting from 0 + This may only be used with h264 profiles `:baseline` and `:constrained_baseline`, where `PTS==DTS`. * in the `:nalu_aligned` mode, the output buffers have their `:pts` and `:dts` set to `:pts` and `:dts` of the input buffer that was holding the first NAL unit making up given access unit (that is being sent inside that output buffer). * in the `:au_aligned` mode, the output buffers have their `:pts` and `:dts` set to `:pts` and `:dts` of the input buffer @@ -88,7 +88,8 @@ defmodule Membrane.H264.Parser do mode: nil, profile: nil, previous_timestamps: {nil, nil}, - framerate: opts.framerate + framerate: opts.framerate, + au_counter: 0 } {[], state} @@ -142,7 +143,7 @@ defmodule Membrane.H264.Parser do {access_units, au_splitter} end - {actions, state} = prepare_actions_for_aus(access_units, buffer.pts, buffer.dts, state) + {actions, state} = prepare_actions_for_aus(access_units, state, buffer.pts, buffer.dts) state = %{ state @@ -192,36 +193,18 @@ defmodule Membrane.H264.Parser do {[end_of_stream: :output], state} end - defp prepare_actions_for_aus(aus, state) do - prepare_actions_for_aus(aus, nil, nil, state) - end - - defp prepare_actions_for_aus(aus, buffer_pts, buffer_dts, state) do - {pts, dts} = - case state.mode do - :bytestream -> - if state.previous_timestamps == {nil, nil}, - do: {-1, -1}, - else: state.previous_timestamps - - :nalu_aligned -> - state.previous_timestamps - - :au_aligned -> - {buffer_pts, buffer_dts} - end - - {actions, au_count, profile} = - Enum.reduce(aus, {[], 0, state.profile}, fn au, {acc, cnt, profile} -> + defp prepare_actions_for_aus(aus, state, buffer_pts \\ nil, buffer_dts \\ nil) do + {actions, au_counter, profile} = + Enum.reduce(aus, {[], state.au_counter, state.profile}, fn au, + {actions_acc, cnt, profile} -> {sps_actions, profile} = maybe_parse_sps(au, state, profile) - {pts, dts} = maybe_generate_timestamps(pts, dts, state, profile, cnt) + {pts, dts} = prepare_timestamps(buffer_pts, buffer_dts, state, profile, cnt) - {acc ++ sps_actions ++ [{:buffer, {:output, wrap_into_buffer(au, pts, dts)}}], cnt + 1, - profile} + {actions_acc ++ sps_actions ++ [{:buffer, {:output, wrap_into_buffer(au, pts, dts)}}], + cnt + 1, profile} end) - state = - maybe_update_state(buffer_pts, buffer_dts, pts, dts, au_count, %{state | profile: profile}) + state = maybe_update_state(buffer_pts, buffer_dts, au_counter, profile, state) {actions, state} end @@ -237,45 +220,37 @@ defmodule Membrane.H264.Parser do end end - defp maybe_generate_timestamps(pts, dts, state, _profile, _order_number) - when state.mode != :bytestream do - {pts, dts} - end - - defp maybe_generate_timestamps( - presentation_order_number, - decoding_order_number, - state, - profile, - offset - ) do + defp prepare_timestamps(_buffer_pts, _buffer_dts, state, profile, order_number) + when state.mode == :bytestream do cond do state.framerate == nil or profile == nil -> {nil, nil} h264_profile_tsgen_supported?(profile) -> - generate_timestamps( - state.framerate, - presentation_order_number + offset + 1, - decoding_order_number + offset + 1 - ) + calculate_timestamps(state.framerate, order_number, order_number) true -> raise("Timestamp generation for H264 profile `#{inspect(profile)}` is unsupported") end end - defp maybe_update_state(buffer_pts, buffer_dts, pts, dts, au_count, state) do - cond do - state.mode == :nalu_aligned and state.previous_timestamps != {buffer_pts, buffer_dts} -> - %{state | previous_timestamps: {buffer_pts, buffer_dts}} + defp prepare_timestamps(_buffer_pts, _buffer_dts, state, _profile, _order_number) + when state.mode == :nalu_aligned do + state.previous_timestamps + end - state.mode == :bytestream and state.framerate != nil and state.profile != nil and - au_count > 0 -> - %{state | previous_timestamps: {pts + au_count, dts + au_count}} + defp prepare_timestamps(buffer_pts, buffer_dts, state, _profile, _order_number) + when state.mode == :au_aligned do + {buffer_pts, buffer_dts} + end - true -> - state + defp maybe_update_state(buffer_pts, buffer_dts, au_counter, profile, state) do + state = %{state | profile: profile, au_counter: au_counter} + + if state.mode == :nalu_aligned and state.previous_timestamps != {buffer_pts, buffer_dts} do + %{state | previous_timestamps: {buffer_pts, buffer_dts}} + else + state end end @@ -341,7 +316,7 @@ defmodule Membrane.H264.Parser do defp h264_profile_tsgen_supported?(profile), do: profile in [:baseline, :constrained_baseline] - defp generate_timestamps( + defp calculate_timestamps( {frames, seconds} = _framerate, presentation_order_number, decoding_order_number diff --git a/test/integration/modes_test.exs b/test/integration/modes_test.exs index 7e2ae84..a31c2d6 100644 --- a/test/integration/modes_test.exs +++ b/test/integration/modes_test.exs @@ -7,7 +7,7 @@ defmodule Membrane.H264.ModesTest do alias Membrane.Buffer alias Membrane.H264.Parser alias Membrane.H264.Parser.{AUSplitter, NALuParser, NALuSplitter} - alias Membrane.H264.TestSource + alias Membrane.H264.Support.TestSource alias Membrane.Testing.{Pipeline, Sink} @h264_input_file "test/fixtures/input-10-720p.h264" diff --git a/test/integration/timestamp_generation_test.exs b/test/integration/timestamp_generation_test.exs index a2a4fc1..44d4f90 100644 --- a/test/integration/timestamp_generation_test.exs +++ b/test/integration/timestamp_generation_test.exs @@ -9,7 +9,7 @@ defmodule Membrane.H264.TimestampGenerationTest do alias Membrane.Buffer alias Membrane.H264.Parser alias Membrane.H264.Parser.{AUSplitter, NALuParser, NALuSplitter} - alias Membrane.H264.TestSource + alias Membrane.H264.Support.TestSource alias Membrane.Testing.{Pipeline, Sink} defmodule EnhancedPipeline do diff --git a/test/integration/test_source.ex b/test/support/test_source.ex similarity index 94% rename from test/integration/test_source.ex rename to test/support/test_source.ex index 4c5506b..9eace62 100644 --- a/test/integration/test_source.ex +++ b/test/support/test_source.ex @@ -1,4 +1,4 @@ -defmodule Membrane.H264.TestSource do +defmodule Membrane.H264.Support.TestSource do @moduledoc false use Membrane.Source diff --git a/test/test_helper.exs b/test/test_helper.exs index b3af8e6..6a0af57 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,2 +1 @@ ExUnit.start(capture_log: true) -Code.require_file("test/integration/test_source.ex") From 1525e9b7ebda9c5056c84477e4f6a20584520290 Mon Sep 17 00:00:00 2001 From: Jakub Pisarek <99591440+sgfn@users.noreply.github.com> Date: Fri, 10 Feb 2023 15:32:36 +0100 Subject: [PATCH 3/3] Bugfix regarding timestamps in nalu_aligned mode --- lib/membrane_h264_plugin/parser.ex | 39 +++++++++++++++++------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/lib/membrane_h264_plugin/parser.ex b/lib/membrane_h264_plugin/parser.ex index 495c281..df8c360 100644 --- a/lib/membrane_h264_plugin/parser.ex +++ b/lib/membrane_h264_plugin/parser.ex @@ -204,7 +204,14 @@ defmodule Membrane.H264.Parser do cnt + 1, profile} end) - state = maybe_update_state(buffer_pts, buffer_dts, au_counter, profile, state) + state = %{state | profile: profile, au_counter: au_counter} + + state = + if state.mode == :nalu_aligned and state.previous_timestamps != {buffer_pts, buffer_dts} do + %{state | previous_timestamps: {buffer_pts, buffer_dts}} + else + state + end {actions, state} end @@ -220,40 +227,38 @@ defmodule Membrane.H264.Parser do end end - defp prepare_timestamps(_buffer_pts, _buffer_dts, state, profile, order_number) + defp prepare_timestamps(_buffer_pts, _buffer_dts, state, profile, frame_order_number) when state.mode == :bytestream do cond do state.framerate == nil or profile == nil -> {nil, nil} h264_profile_tsgen_supported?(profile) -> - calculate_timestamps(state.framerate, order_number, order_number) + generate_ts_with_constant_framerate( + state.framerate, + frame_order_number, + frame_order_number + ) true -> raise("Timestamp generation for H264 profile `#{inspect(profile)}` is unsupported") end end - defp prepare_timestamps(_buffer_pts, _buffer_dts, state, _profile, _order_number) + defp prepare_timestamps(buffer_pts, buffer_dts, state, _profile, _frame_order_number) when state.mode == :nalu_aligned do - state.previous_timestamps + if state.previous_timestamps == {nil, nil} do + {buffer_pts, buffer_dts} + else + state.previous_timestamps + end end - defp prepare_timestamps(buffer_pts, buffer_dts, state, _profile, _order_number) + defp prepare_timestamps(buffer_pts, buffer_dts, state, _profile, _frame_order_number) when state.mode == :au_aligned do {buffer_pts, buffer_dts} end - defp maybe_update_state(buffer_pts, buffer_dts, au_counter, profile, state) do - state = %{state | profile: profile, au_counter: au_counter} - - if state.mode == :nalu_aligned and state.previous_timestamps != {buffer_pts, buffer_dts} do - %{state | previous_timestamps: {buffer_pts, buffer_dts}} - else - state - end - end - defp wrap_into_buffer(access_unit, pts, dts) do metadata = prepare_metadata(access_unit) @@ -316,7 +321,7 @@ defmodule Membrane.H264.Parser do defp h264_profile_tsgen_supported?(profile), do: profile in [:baseline, :constrained_baseline] - defp calculate_timestamps( + defp generate_ts_with_constant_framerate( {frames, seconds} = _framerate, presentation_order_number, decoding_order_number