From ea7e4e82cad6e53214c8f40489ab58865a534332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kita?= Date: Wed, 24 Jul 2024 13:28:47 +0200 Subject: [PATCH 1/6] Ignore unknown sample types --- lib/membrane_mp4/demuxer/isom.ex | 7 +++++++ lib/membrane_mp4/movie_box/sample_table_box.ex | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/lib/membrane_mp4/demuxer/isom.ex b/lib/membrane_mp4/demuxer/isom.ex index 26ea986..cc42bb9 100644 --- a/lib/membrane_mp4/demuxer/isom.ex +++ b/lib/membrane_mp4/demuxer/isom.ex @@ -464,6 +464,9 @@ defmodule Membrane.MP4.Demuxer.ISOM do kind_to_tracks = sample_tables + |> Enum.reject(fn {_track_id, table} -> + table.sample_description == nil + end) |> Enum.group_by( fn {_track_id, table} -> sample_description_to_kind(table.sample_description) end, fn {track_id, _table} -> track_id end @@ -500,6 +503,7 @@ defmodule Membrane.MP4.Demuxer.ISOM do tracks_codecs = state.samples_info.sample_tables + |> Enum.reject(fn {_track, table} -> table.sample_description == nil end) |> Enum.map(fn {_track, table} -> table.sample_description.__struct__ end) raise """ @@ -512,12 +516,14 @@ defmodule Membrane.MP4.Demuxer.ISOM do defp sample_description_to_kind(%Membrane.H265{}), do: :video defp sample_description_to_kind(%Membrane.AAC{}), do: :audio defp sample_description_to_kind(%Membrane.Opus{}), do: :audio + defp sample_description_to_kind(_other), do: :unknown defp maybe_get_track_notifications(%{pads_linked_before_notification?: true}), do: [] defp maybe_get_track_notifications(%{pads_linked_before_notification?: false} = state) do new_tracks = state.samples_info.sample_tables + |> Enum.reject(fn {_track_id, table} -> table.sample_description == nil end) |> Enum.map(fn {track_id, table} -> pad_id = state.track_to_pad_id[track_id] {pad_id, table.sample_description} @@ -528,6 +534,7 @@ defmodule Membrane.MP4.Demuxer.ISOM do defp get_stream_format(state) do state.samples_info.sample_tables + |> Enum.reject(fn {_track_id, table} -> table.sample_description == nil end) |> Enum.map(fn {track_id, table} -> pad_id = state.track_to_pad_id[track_id] {:stream_format, {Pad.ref(:output, pad_id), table.sample_description}} diff --git a/lib/membrane_mp4/movie_box/sample_table_box.ex b/lib/membrane_mp4/movie_box/sample_table_box.ex index 66bf91e..4e1b8e3 100644 --- a/lib/membrane_mp4/movie_box/sample_table_box.ex +++ b/lib/membrane_mp4/movie_box/sample_table_box.ex @@ -2,6 +2,7 @@ defmodule Membrane.MP4.MovieBox.SampleTableBox do @moduledoc false require Membrane.H264 + require Membrane.Logger alias Membrane.MP4.{Container, Helper, Track.SampleTable} alias Membrane.{AAC, H264, H265, Opus} @@ -299,4 +300,9 @@ defmodule Membrane.MP4.MovieBox.SampleTableBox do defp unpack_sample_description(%{children: [{:Opus, %{children: boxes}}]}) do %Opus{channels: boxes[:dOps].fields.output_channel_count, self_delimiting?: false} end + + defp unpack_sample_description(%{children: [{sample_type, _sample_metadata}]}) do + Membrane.Logger.warning("Unknown sample type: #{inspect(sample_type)}") + nil + end end From 995f56ab7967bf8a177af401361c489e2be2d338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kita?= Date: Wed, 24 Jul 2024 13:32:05 +0200 Subject: [PATCH 2/6] Filter out nil sample descriptions --- lib/membrane_mp4/demuxer/isom.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/membrane_mp4/demuxer/isom.ex b/lib/membrane_mp4/demuxer/isom.ex index cc42bb9..8258866 100644 --- a/lib/membrane_mp4/demuxer/isom.ex +++ b/lib/membrane_mp4/demuxer/isom.ex @@ -430,7 +430,9 @@ defmodule Membrane.MP4.Demuxer.ISOM do end defp match_tracks_with_pads(ctx, state) do - sample_tables = state.samples_info.sample_tables + sample_tables = + state.samples_info.sample_tables + |> Enum.reject(fn {_track_id, table} -> table.sample_description == nil end) output_pads_data = ctx.pads From 680cd863473146bd31aa3a4a89bfaac459ec6d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kita?= Date: Wed, 24 Jul 2024 13:33:14 +0200 Subject: [PATCH 3/6] Put the filtered out sample tables into a map --- lib/membrane_mp4/demuxer/isom.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/membrane_mp4/demuxer/isom.ex b/lib/membrane_mp4/demuxer/isom.ex index 8258866..b47c307 100644 --- a/lib/membrane_mp4/demuxer/isom.ex +++ b/lib/membrane_mp4/demuxer/isom.ex @@ -433,6 +433,7 @@ defmodule Membrane.MP4.Demuxer.ISOM do sample_tables = state.samples_info.sample_tables |> Enum.reject(fn {_track_id, table} -> table.sample_description == nil end) + |> Enum.into(%{}) output_pads_data = ctx.pads From d9adf342162e56ff4fc592de9d52d886691ba499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kita?= Date: Wed, 24 Jul 2024 16:46:49 +0200 Subject: [PATCH 4/6] Make sure the stsz entries list is only parsed when sample size equals 0 (as the specification defines. Ignore unsupported sample types. Add tests checking if unsupported sample types are ignored --- lib/membrane_mp4/container/schema.ex | 6 ++- lib/membrane_mp4/demuxer/isom.ex | 18 +++++++-- .../movie_box/sample_table_box.ex | 4 ++ test/fixtures/isom/ref_video_with_tmcd.mp4 | Bin 0 -> 35312 bytes .../demuxer/isom/demuxer_test.exs | 35 +++++++++++++++++- 5 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/isom/ref_video_with_tmcd.mp4 diff --git a/lib/membrane_mp4/container/schema.ex b/lib/membrane_mp4/container/schema.ex index 54724c3..5cd20cd 100644 --- a/lib/membrane_mp4/container/schema.ex +++ b/lib/membrane_mp4/container/schema.ex @@ -315,11 +315,13 @@ defmodule Membrane.MP4.Container.Schema do [ sample_size: :uint32, sample_count: :uint32, - entry_list: + entry_list: { {:list, [ entry_size: :uint32 - ]} + ]}, + when: {:sample_size, value: 0} + } ] ], stco: [ diff --git a/lib/membrane_mp4/demuxer/isom.ex b/lib/membrane_mp4/demuxer/isom.ex index b47c307..96b897c 100644 --- a/lib/membrane_mp4/demuxer/isom.ex +++ b/lib/membrane_mp4/demuxer/isom.ex @@ -649,7 +649,14 @@ defmodule Membrane.MP4.Demuxer.ISOM do defp all_pads_connected?(_ctx, %{samples_info: nil}), do: false defp all_pads_connected?(ctx, state) do - tracks = 1..state.samples_info.tracks_number + how_many_unsupported_tracks = + state.samples_info.sample_tables + |> Enum.filter(fn {_track_id, table} -> + table.sample_description == nil + end) + |> length() + + tracks = 1..(state.samples_info.tracks_number - how_many_unsupported_tracks) pads = ctx.pads @@ -663,14 +670,19 @@ defmodule Membrane.MP4.Demuxer.ISOM do defp flush_samples(state) do actions = - Enum.map(state.buffered_samples, fn {track_id, track_samples} -> + Enum.flat_map(state.buffered_samples, fn {track_id, track_samples} -> buffers = track_samples |> Enum.reverse() |> Enum.map(fn {buffer, ^track_id} -> buffer end) pad_id = state.track_to_pad_id[track_id] - {:buffer, {Pad.ref(:output, pad_id), buffers}} + + if pad_id != nil do + [buffer: {Pad.ref(:output, pad_id), buffers}] + else + [] + end end) state = %{state | buffered_samples: %{}} diff --git a/lib/membrane_mp4/movie_box/sample_table_box.ex b/lib/membrane_mp4/movie_box/sample_table_box.ex index 4e1b8e3..bb86698 100644 --- a/lib/membrane_mp4/movie_box/sample_table_box.ex +++ b/lib/membrane_mp4/movie_box/sample_table_box.ex @@ -265,6 +265,10 @@ defmodule Membrane.MP4.MovieBox.SampleTableBox do offsets |> Enum.map(fn %{chunk_offset: offset} -> offset end) end + defp unpack_sample_sizes(%{fields: %{entry_list: [], sample_count: 1, sample_size: sample_size}}) do + [sample_size] + end + defp unpack_sample_sizes(%{fields: %{entry_list: sizes}}) do sizes |> Enum.map(fn %{entry_size: size} -> size end) end diff --git a/test/fixtures/isom/ref_video_with_tmcd.mp4 b/test/fixtures/isom/ref_video_with_tmcd.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..cd0449a248b03fd24547bd97529a9ef3346c8622 GIT binary patch literal 35312 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 literal 0 HcmV?d00001 diff --git a/test/membrane_mp4/demuxer/isom/demuxer_test.exs b/test/membrane_mp4/demuxer/isom/demuxer_test.exs index 2a6fbb8..65e8e27 100644 --- a/test/membrane_mp4/demuxer/isom/demuxer_test.exs +++ b/test/membrane_mp4/demuxer/isom/demuxer_test.exs @@ -242,7 +242,40 @@ defmodule Membrane.MP4.Demuxer.ISOM.DemuxerTest do @tag :tmp_dir test "output pad connected after moov box has been read", %{tmp_dir: dir} do out_path = Path.join(dir, "out") - filename = "test/fixtures/isom/ref_video_fast_start.mp4" + filename = "test/fixtures/isom/ref_video.mp4" + + pipeline = + start_remote_pipeline!( + filename: filename, + file_source_chunk_size: File.stat!(filename).size - 1 + ) + + assert_receive %RCMessage.Notification{ + element: :demuxer, + data: {:new_tracks, [{1, _payload}]}, + from: _ + }, + 2000 + + structure = [ + get_child(:demuxer) + |> via_out(Pad.ref(:output, 1)) + |> child(:sink, %Membrane.File.Sink{location: out_path}) + ] + + RCPipeline.exec_actions(pipeline, spec: {structure, []}) + assert_receive %RCMessage.EndOfStream{element: :demuxer, pad: :input}, 2000 + assert_receive %RCMessage.EndOfStream{element: :sink, pad: :input}, 2000 + + RCPipeline.terminate(pipeline) + + assert_files_equal(out_path, ref_path_for("video")) + end + + @tag :tmp_dir + test "file is properly demuxed when unsupported sample type is present", %{tmp_dir: dir} do + out_path = Path.join(dir, "out") + filename = "test/fixtures/isom/ref_video_with_tmcd.mp4" pipeline = start_remote_pipeline!( From efe5e7a7d671c7cdc1964278f2909d18d183bb87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kita?= Date: Wed, 31 Jul 2024 14:34:12 +0200 Subject: [PATCH 5/6] Implement reviewers suggestions --- lib/membrane_mp4/demuxer/isom.ex | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/membrane_mp4/demuxer/isom.ex b/lib/membrane_mp4/demuxer/isom.ex index 96b897c..258d4b9 100644 --- a/lib/membrane_mp4/demuxer/isom.ex +++ b/lib/membrane_mp4/demuxer/isom.ex @@ -432,8 +432,7 @@ defmodule Membrane.MP4.Demuxer.ISOM do defp match_tracks_with_pads(ctx, state) do sample_tables = state.samples_info.sample_tables - |> Enum.reject(fn {_track_id, table} -> table.sample_description == nil end) - |> Enum.into(%{}) + |> reject_unsupported_sample_types() output_pads_data = ctx.pads @@ -467,9 +466,7 @@ defmodule Membrane.MP4.Demuxer.ISOM do kind_to_tracks = sample_tables - |> Enum.reject(fn {_track_id, table} -> - table.sample_description == nil - end) + |> reject_unsupported_sample_types() |> Enum.group_by( fn {_track_id, table} -> sample_description_to_kind(table.sample_description) end, fn {track_id, _table} -> track_id end @@ -506,7 +503,7 @@ defmodule Membrane.MP4.Demuxer.ISOM do tracks_codecs = state.samples_info.sample_tables - |> Enum.reject(fn {_track, table} -> table.sample_description == nil end) + |> reject_unsupported_sample_types() |> Enum.map(fn {_track, table} -> table.sample_description.__struct__ end) raise """ @@ -519,14 +516,13 @@ defmodule Membrane.MP4.Demuxer.ISOM do defp sample_description_to_kind(%Membrane.H265{}), do: :video defp sample_description_to_kind(%Membrane.AAC{}), do: :audio defp sample_description_to_kind(%Membrane.Opus{}), do: :audio - defp sample_description_to_kind(_other), do: :unknown defp maybe_get_track_notifications(%{pads_linked_before_notification?: true}), do: [] defp maybe_get_track_notifications(%{pads_linked_before_notification?: false} = state) do new_tracks = state.samples_info.sample_tables - |> Enum.reject(fn {_track_id, table} -> table.sample_description == nil end) + |> reject_unsupported_sample_types() |> Enum.map(fn {track_id, table} -> pad_id = state.track_to_pad_id[track_id] {pad_id, table.sample_description} @@ -537,7 +533,7 @@ defmodule Membrane.MP4.Demuxer.ISOM do defp get_stream_format(state) do state.samples_info.sample_tables - |> Enum.reject(fn {_track_id, table} -> table.sample_description == nil end) + |> reject_unsupported_sample_types() |> Enum.map(fn {track_id, table} -> pad_id = state.track_to_pad_id[track_id] {:stream_format, {Pad.ref(:output, pad_id), table.sample_description}} @@ -651,10 +647,9 @@ defmodule Membrane.MP4.Demuxer.ISOM do defp all_pads_connected?(ctx, state) do how_many_unsupported_tracks = state.samples_info.sample_tables - |> Enum.filter(fn {_track_id, table} -> + |> Enum.count(fn {_track_id, table} -> table.sample_description == nil end) - |> length() tracks = 1..(state.samples_info.tracks_number - how_many_unsupported_tracks) @@ -707,4 +702,8 @@ defmodule Membrane.MP4.Demuxer.ISOM do defp get_mdat_header_beginning([{_other_name, box} | rest]) do box.header_size + box.size + get_mdat_header_beginning(rest) end + + defp reject_unsupported_sample_types(sample_tables) do + Map.reject(sample_tables, fn {_track_id, table} -> table.sample_description == nil end) + end end From 9f8fbbbfec84a2cab3765c0b7b027c5299579694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kita?= Date: Thu, 22 Aug 2024 16:35:27 +0200 Subject: [PATCH 6/6] Update lib/membrane_mp4/demuxer/isom.ex Co-authored-by: Mateusz Front --- lib/membrane_mp4/demuxer/isom.ex | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/membrane_mp4/demuxer/isom.ex b/lib/membrane_mp4/demuxer/isom.ex index 258d4b9..74e9546 100644 --- a/lib/membrane_mp4/demuxer/isom.ex +++ b/lib/membrane_mp4/demuxer/isom.ex @@ -645,13 +645,12 @@ defmodule Membrane.MP4.Demuxer.ISOM do defp all_pads_connected?(_ctx, %{samples_info: nil}), do: false defp all_pads_connected?(ctx, state) do - how_many_unsupported_tracks = + count_of_supported_tracks = state.samples_info.sample_tables - |> Enum.count(fn {_track_id, table} -> - table.sample_description == nil - end) + |> reject_unsupported_sample_types() + |> Enum.count() - tracks = 1..(state.samples_info.tracks_number - how_many_unsupported_tracks) + tracks = 1..count_of_supported_tracks pads = ctx.pads