Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 4 additions & 1 deletion lib/membrane_mp4/container/schema_default.ex
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,10 @@ defmodule Membrane.MP4.Container.Schema.Default do
{:list,
[
sample_count: :uint32,
sample_composition_offset: :uint32
sample_composition_offset:
{:uint32, when: {:version, value: 0}},
sample_composition_offset:
{:int32, when: {:version, value: 1}}
]}
]
],
Expand Down
32 changes: 17 additions & 15 deletions lib/membrane_mp4/movie_box/sample_table_box.ex
Original file line number Diff line number Diff line change
Expand Up @@ -201,22 +201,24 @@ defmodule Membrane.MP4.MovieBox.SampleTableBox do
timescale: timescale,
composition_offsets: composition_offsets
}) do
composition_offsets
|> Enum.map(fn %{sample_count: count, sample_composition_offset: offset} ->
%{sample_count: count, sample_composition_offset: Helper.timescalify(offset, timescale)}
end)
|> then(
&[
ctts: %{
fields: %{
version: 0,
flags: 0,
entry_count: length(&1),
entry_list: &1
}
composition_offsets =
Enum.map(composition_offsets, fn %{sample_count: count, sample_composition_offset: offset} ->
%{sample_count: count, sample_composition_offset: Helper.timescalify(offset, timescale)}
end)

version =
if Enum.any?(composition_offsets, &(&1.sample_composition_offset < 0)), do: 1, else: 0

[
ctts: %{
fields: %{
version: version,
flags: 0,
entry_count: length(composition_offsets),
entry_list: composition_offsets
}
]
)
}
]
end

defp maybe_sample_sync(%{sync_samples: []}), do: []
Expand Down
22 changes: 16 additions & 6 deletions lib/membrane_mp4/movie_fragment_box.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,17 @@ defmodule Membrane.MP4.MovieFragmentBox do
@mdat_data_offset 8

@spec assemble(%{
sequence_number: integer,
elapsed_time: integer,
timescale: integer,
duration: integer,
samples_table: [%{sample_size: integer, sample_flags: integer}]
id: pos_integer,
sequence_number: non_neg_integer,
base_timestamp: non_neg_integer,
samples_table: [
%{
sample_size: non_neg_integer,
sample_flags: non_neg_integer,
sample_duration: non_neg_integer,
sample_composition_time_offset: integer
}
]
}) :: Container.t()
def assemble(config) do
config =
Expand Down Expand Up @@ -94,7 +100,7 @@ defmodule Membrane.MP4.MovieFragmentBox do
@trun_flags.sample_composition_time_offsets_present,
sample_count: config.sample_count,
samples: config.samples_table,
version: 0
version: trun_version(config.samples_table)
}
}
],
Expand All @@ -105,4 +111,8 @@ defmodule Membrane.MP4.MovieFragmentBox do
}
]
end

defp trun_version(samples) do
if Enum.any?(samples, &(&1.sample_composition_time_offset < 0)), do: 1, else: 0
end
end
Binary file added test/fixtures/isom/ref_video_hevc_bframes.mp4
Binary file not shown.
29 changes: 29 additions & 0 deletions test/membrane_mp4/demuxer/isom/transmuxing_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ defmodule Membrane.MP4.Demuxer.ISOM.TransmuxingTest do
in_mp4[:moov].children[:trak].children[:thkd]

assert out_mp4[:mdat] == in_mp4[:mdat]

{in_mp4, out_mp4}
end

@tag :tmp_dir
Expand Down Expand Up @@ -56,6 +58,33 @@ defmodule Membrane.MP4.Demuxer.ISOM.TransmuxingTest do
perform_test(pipeline, in_path, out_path)
end

@tag :tmp_dir
test "single H265 track with B-frames", %{tmp_dir: dir} do
# Generated with:
# ffmpeg -f lavfi -i testsrc2=size=320x180:rate=30 -t 1 -an -c:v libx265
# -preset ultrafast -pix_fmt yuv420p
# -x265-params bframes=2:b-adapt=0:open-gop=0:keyint=30:min-keyint=30
# -movflags +negative_cts_offsets ref_video_hevc_bframes.mp4
in_path = "test/fixtures/isom/ref_video_hevc_bframes.mp4"
out_path = Path.join(dir, "out")

pipeline =
start_testing_pipeline!(
input_file: in_path,
output_file: out_path
)

{in_mp4, out_mp4} = perform_test(pipeline, in_path, out_path)

input_ctts = Container.get_box(in_mp4, [:moov, :trak, :mdia, :minf, :stbl, :ctts])
output_ctts = Container.get_box(out_mp4, [:moov, :trak, :mdia, :minf, :stbl, :ctts])

assert input_ctts.fields.version == 1
assert Enum.any?(input_ctts.fields.entry_list, &(&1.sample_composition_offset < 0))
assert output_ctts.fields.version == 1
assert Enum.any?(output_ctts.fields.entry_list, &(&1.sample_composition_offset < 0))
end

defp start_testing_pipeline!(opts) do
structure = [
child(:file, %Membrane.File.Source{location: opts[:input_file]})
Expand Down
41 changes: 40 additions & 1 deletion test/membrane_mp4/muxer/cmaf/integration_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,29 @@ defmodule Membrane.MP4.Muxer.CMAF.IntegrationTest do
:ok = Testing.Pipeline.terminate(pipeline)
end

test "video hevc with negative composition offsets" do
pipeline =
prepare_pipeline(:video_hevc,
add_dts_offset: false,
header_file: "ref_video_hevc_header.mp4"
)

truns =
for _segment <- 1..2 do
assert_sink_buffer(pipeline, :sink, buffer)
assert_composition_offset_version(buffer.payload)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, do I get it right that we expect first segment to use version 1 box and the second segment to use version 0?
If so, I would prefer to explicitly assert on the desired box version that have assert_composition_offset_version determine the version itself, using the same logic that is used in the lib

end

assert Enum.any?(truns, fn trun ->
Enum.any?(trun.fields.samples, &(&1.sample_composition_time_offset < 0))
end)

assert_end_of_stream(pipeline, :sink)
refute_sink_buffer(pipeline, :sink, _buffer, 0)

:ok = Testing.Pipeline.terminate(pipeline)
end

test "audio" do
pipeline = prepare_pipeline(:audio)

Expand Down Expand Up @@ -437,7 +460,10 @@ defmodule Membrane.MP4.Muxer.CMAF.IntegrationTest do

:video_hevc ->
%Membrane.H265.Parser{
generate_best_effort_timestamps: %{framerate: {30, 1}},
generate_best_effort_timestamps: %{
framerate: {30, 1},
add_dts_offset: Keyword.get(opts, :add_dts_offset, true)
},
output_stream_structure: :hvc1
}
end
Expand Down Expand Up @@ -467,6 +493,19 @@ defmodule Membrane.MP4.Muxer.CMAF.IntegrationTest do
pipeline
end

defp assert_composition_offset_version(segment) do
assert {parsed_segment, <<>>} = Container.parse!(segment)
trun = Container.get_box(parsed_segment, [:moof, :traf, :trun])

expected_version =
if Enum.any?(trun.fields.samples, &(&1.sample_composition_time_offset < 0)),
do: 1,
else: 0

assert trun.fields.version == expected_version
trun
end

@fixtures_dir "test/fixtures/cmaf"
defp assert_mp4_equal(output, ref_file) do
{parsed_out, <<>>} = Container.parse!(output)
Expand Down
39 changes: 39 additions & 0 deletions test/membrane_mp4/muxer/isom/integration_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -254,5 +254,44 @@ defmodule Membrane.MP4.Muxer.ISOM.IntegrationTest do
assert Container.get_box(parsed_out, [:moov, :trak, :mdia, :minf, :stbl, :stts])
refute Container.get_box(parsed_out, [:moov, :trak, :mdia, :minf, :stbl, :ctts])
end

test "uses version 0 for non-negative composition offsets" do
ctts = mux_h265_and_get_ctts("video_hevc_ctts_v0", true)

assert ctts.fields.version == 0
assert Enum.all?(ctts.fields.entry_list, &(&1.sample_composition_offset >= 0))
end

test "uses version 1 for negative composition offsets" do
ctts = mux_h265_and_get_ctts("video_hevc_ctts_v1", false)

assert ctts.fields.version == 1
assert Enum.any?(ctts.fields.entry_list, &(&1.sample_composition_offset < 0))
end
end

defp mux_h265_and_get_ctts(filename, add_dts_offset) do
prepare_test(filename)

structure =
child(:file, %Membrane.File.Source{location: "test/fixtures/in_video_hevc.h265"})
|> child(:parser, %Membrane.H265.Parser{
generate_best_effort_timestamps: %{
framerate: {30, 1},
add_dts_offset: add_dts_offset
},
output_stream_structure: :hvc1
})
|> child(:muxer, %Membrane.MP4.Muxer.ISOM{chunk_duration: Time.seconds(1)})
|> child(:sink, %Membrane.File.Sink{location: out_path_for(filename)})

pid = Pipeline.start_link_supervised!(spec: structure)

assert_end_of_stream(pid, :sink, :input)
refute_sink_buffer(pid, :sink, _buffer, 0)
assert :ok == Pipeline.terminate(pid)

assert {parsed_out, <<>>} = out_path_for(filename) |> File.read!() |> Container.parse!()
Container.get_box(parsed_out, [:moov, :trak, :mdia, :minf, :stbl, :ctts])
end
end
Loading