From 44a1795550c2ebb21ab7ba80ea34d36f112ef926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=9Aled=C5=BA?= Date: Wed, 29 Nov 2023 20:07:09 +0100 Subject: [PATCH 1/4] Add IVF reader --- lib/ex_webrtc/media/ivf_reader.ex | 139 +++++++++++++++++++ test/ex_webrtc/media/ivf_reader_test.exs | 62 +++++++++ test/fixtures/ivf/README.md | 5 + test/fixtures/ivf/empty.ivf | 0 test/fixtures/ivf/vp8_correct.ivf | Bin 0 -> 15850 bytes test/fixtures/ivf/vp8_invalid_last_frame.ivf | Bin 0 -> 15849 bytes 6 files changed, 206 insertions(+) create mode 100644 lib/ex_webrtc/media/ivf_reader.ex create mode 100644 test/ex_webrtc/media/ivf_reader_test.exs create mode 100644 test/fixtures/ivf/README.md create mode 100644 test/fixtures/ivf/empty.ivf create mode 100644 test/fixtures/ivf/vp8_correct.ivf create mode 100644 test/fixtures/ivf/vp8_invalid_last_frame.ivf diff --git a/lib/ex_webrtc/media/ivf_reader.ex b/lib/ex_webrtc/media/ivf_reader.ex new file mode 100644 index 00000000..c7579e8a --- /dev/null +++ b/lib/ex_webrtc/media/ivf_reader.ex @@ -0,0 +1,139 @@ +defmodule ExWebRTC.Media.IVFHeader do + @moduledoc """ + Defines IVF Frame Header type. + """ + + @typedoc """ + IVF Frame Header. + + Description of these fields is taken from: + https://chromium.googlesource.com/chromium/src/media/+/master/filters/ivf_parser.h + + * `signature` - always "DKIF" + * `version` - should be 0 + * `header_size` - size of header in bytes + * `fourcc` - codec FourCC (e.g, 'VP80'). + For more information, see https://fourcc.org/codecs.php + * `width` - width in pixels + * `height` - height in pixels + * `timebase_denum` - timebase denumerator + * `timebase_num` - timebase numerator. For example, if + `timebase_denum` is 30 and `timebase_num` is 2, the unit + of `ExWebRTC.Media.IVFFrame`'s timestamp is 2/30 seconds. + * `num_frames` - number of frames in a file + * `unused` - unused + """ + @type t() :: %__MODULE__{ + signature: binary(), + version: integer(), + header_size: integer(), + fourcc: integer(), + width: integer(), + height: integer(), + timebase_denum: integer(), + timebase_num: integer(), + num_frames: integer(), + unused: integer() + } + + @enforcekeyes [ + :signature, + :version, + :header_size, + :fourcc, + :width, + :height, + :timebase_denum, + :timebase_num, + :num_frames, + :unused + ] + defstruct @enforcekeyes +end + +defmodule ExWebRTC.Media.IVFFrame do + @moduledoc """ + Defines IVF Frame type. + """ + + @typedoc """ + IVF Frame. + + `timestamp` is in `timebase_num`/`timebase_denum` seconds. + For more information see `ExWebRTC.Media.IVFHeader`. + """ + @type t() :: %__MODULE__{ + timestamp: integer(), + data: binary() + } + + @enforcekeys [:timestamp, :data] + defstruct @enforcekeys +end + +defmodule ExWebRTC.Media.IVFReader do + @moduledoc """ + Defines IVF reader. + + Based on: + * https://formats.kaitai.io/vp8_ivf/ + * https://chromium.googlesource.com/chromium/src/media/+/refs/heads/main/filters/ivf_parser.cc + """ + + alias ExWebRTC.Media.{IVFHeader, IVFFrame} + + @opaque t() :: File.io_device() + + @spec open(Path.t()) :: {:ok, t()} | {:error, File.posix()} + def open(path), do: File.open(path) + + @spec read_header(t()) :: {:ok, IVFHeader.t()} | {:error, :invalid_file} | :eof + def read_header(reader) do + case IO.binread(reader, 32) do + <<"DKIF", 0::little-integer-size(16), 32::little-integer-size(16), + fourcc::little-integer-size(32), width::little-integer-size(16), + height::little-integer-size(16), timebase_denum::little-integer-size(32), + timebase_num::little-integer-size(32), num_frames::little-integer-size(32), + unused::little-integer-size(32)>> -> + {:ok, + %IVFHeader{ + signature: "DKIF", + version: 0, + header_size: 32, + fourcc: fourcc, + width: width, + height: height, + timebase_denum: timebase_denum, + timebase_num: timebase_num, + num_frames: num_frames, + unused: unused + }} + + _other -> + {:error, :invalid_file} + end + end + + @spec next_frame(t()) :: {:ok, IVFFrame.t()} | {:error, :invalid_file} | :eof + def next_frame(reader) do + case IO.binread(reader, 12) do + <> -> + case IO.binread(reader, len_frame) do + data when is_binary(data) and byte_size(data) == len_frame -> + {:ok, %IVFFrame{timestamp: timestamp, data: data}} + + :eof -> + :eof + + _other -> + {:error, :invalid_file} + end + + :eof -> + :eof + + _other -> + {:error, :invalid_file} + end + end +end diff --git a/test/ex_webrtc/media/ivf_reader_test.exs b/test/ex_webrtc/media/ivf_reader_test.exs new file mode 100644 index 00000000..5b00e6e2 --- /dev/null +++ b/test/ex_webrtc/media/ivf_reader_test.exs @@ -0,0 +1,62 @@ +defmodule ExWebRTC.Media.IVFReaderTest do + use ExUnit.Case, async: true + + alias ExWebRTC.Media.{IVFFrame, IVFHeader, IVFReader} + + test "correct file" do + assert {:ok, reader} = IVFReader.open("test/fixtures/ivf/vp8_correct.ivf") + + assert {:ok, + %IVFHeader{ + signature: "DKIF", + version: 0, + header_size: 32, + fourcc: 808_996_950, + width: 176, + height: 144, + timebase_denum: 30_000, + timebase_num: 1000, + num_frames: 29, + unused: 0 + }} == IVFReader.read_header(reader) + + for i <- 0..28 do + assert {:ok, %IVFFrame{} = frame} = IVFReader.next_frame(reader) + assert frame.timestamp == i + assert is_binary(frame.data) + assert frame.data != <<>> + end + end + + test "empty file" do + assert {:ok, reader} = IVFReader.open("test/fixtures/ivf/empty.ivf") + assert {:error, :invalid_file} == IVFReader.read_header(reader) + end + + test "invalid last frame" do + assert {:ok, reader} = IVFReader.open("test/fixtures/ivf/vp8_invalid_last_frame.ivf") + + assert {:ok, + %IVFHeader{ + signature: "DKIF", + version: 0, + header_size: 32, + fourcc: 808_996_950, + width: 176, + height: 144, + timebase_denum: 30_000, + timebase_num: 1000, + num_frames: 29, + unused: 0 + }} == IVFReader.read_header(reader) + + for i <- 0..27 do + assert {:ok, %IVFFrame{} = frame} = IVFReader.next_frame(reader) + assert frame.timestamp == i + assert is_binary(frame.data) + assert frame.data != <<>> + end + + assert {:error, :invalid_file} == IVFReader.next_frame(reader) + end +end diff --git a/test/fixtures/ivf/README.md b/test/fixtures/ivf/README.md new file mode 100644 index 00000000..50e162b3 --- /dev/null +++ b/test/fixtures/ivf/README.md @@ -0,0 +1,5 @@ +# IVF Fixtures + +* empty - just an empty file +* vp8_correct - https://chromium.googlesource.com/webm/vp8-test-vectors/+/refs/heads/main/vp80-00-comprehensive-001.ivf +* vp8_invalid_last_frame - vp8_correct without last byte diff --git a/test/fixtures/ivf/empty.ivf b/test/fixtures/ivf/empty.ivf new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/ivf/vp8_correct.ivf b/test/fixtures/ivf/vp8_correct.ivf new file mode 100644 index 0000000000000000000000000000000000000000..d7a7cd5d046e17fb50e72b743cc791092e79679c GIT binary patch literal 15850 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 literal 0 HcmV?d00001 From 5e72ee8ebdd048c044f1cfd11577fcc98d2b6b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=9Aled=C5=BA?= Date: Thu, 30 Nov 2023 18:09:20 +0100 Subject: [PATCH 2/4] Fix IVFReader typespecs --- lib/ex_webrtc/media/ivf_reader.ex | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/ex_webrtc/media/ivf_reader.ex b/lib/ex_webrtc/media/ivf_reader.ex index c7579e8a..1437aae7 100644 --- a/lib/ex_webrtc/media/ivf_reader.ex +++ b/lib/ex_webrtc/media/ivf_reader.ex @@ -25,15 +25,15 @@ defmodule ExWebRTC.Media.IVFHeader do """ @type t() :: %__MODULE__{ signature: binary(), - version: integer(), - header_size: integer(), - fourcc: integer(), - width: integer(), - height: integer(), - timebase_denum: integer(), - timebase_num: integer(), - num_frames: integer(), - unused: integer() + version: non_neg_integer(), + header_size: non_neg_integer(), + fourcc: non_neg_integer(), + width: non_neg_integer(), + height: non_neg_integer(), + timebase_denum: non_neg_integer(), + timebase_num: non_neg_integer(), + num_frames: non_neg_integer(), + unused: non_neg_integer() } @enforcekeyes [ From c35753b3e059adbdc528cdd8a77d1a7b131f6c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=9Aled=C5=BA?= Date: Thu, 30 Nov 2023 18:13:27 +0100 Subject: [PATCH 3/4] Refactor `next_frame`. Test eof. --- lib/ex_webrtc/media/ivf_reader.ex | 30 ++++++++---------------- test/ex_webrtc/media/ivf_reader_test.exs | 2 ++ 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/lib/ex_webrtc/media/ivf_reader.ex b/lib/ex_webrtc/media/ivf_reader.ex index 1437aae7..24acdc4c 100644 --- a/lib/ex_webrtc/media/ivf_reader.ex +++ b/lib/ex_webrtc/media/ivf_reader.ex @@ -36,7 +36,7 @@ defmodule ExWebRTC.Media.IVFHeader do unused: non_neg_integer() } - @enforcekeyes [ + @enforce_keys [ :signature, :version, :header_size, @@ -48,7 +48,7 @@ defmodule ExWebRTC.Media.IVFHeader do :num_frames, :unused ] - defstruct @enforcekeyes + defstruct @enforce_keys end defmodule ExWebRTC.Media.IVFFrame do @@ -116,24 +116,14 @@ defmodule ExWebRTC.Media.IVFReader do @spec next_frame(t()) :: {:ok, IVFFrame.t()} | {:error, :invalid_file} | :eof def next_frame(reader) do - case IO.binread(reader, 12) do - <> -> - case IO.binread(reader, len_frame) do - data when is_binary(data) and byte_size(data) == len_frame -> - {:ok, %IVFFrame{timestamp: timestamp, data: data}} - - :eof -> - :eof - - _other -> - {:error, :invalid_file} - end - - :eof -> - :eof - - _other -> - {:error, :invalid_file} + with <> <- + IO.binread(reader, 12), + data when is_binary(data) and byte_size(data) == len_frame <- + IO.binread(reader, len_frame) do + {:ok, %IVFFrame{timestamp: timestamp, data: data}} + else + :eof -> :eof + _other -> {:error, :invalid_file} end end end diff --git a/test/ex_webrtc/media/ivf_reader_test.exs b/test/ex_webrtc/media/ivf_reader_test.exs index 5b00e6e2..3e288d37 100644 --- a/test/ex_webrtc/media/ivf_reader_test.exs +++ b/test/ex_webrtc/media/ivf_reader_test.exs @@ -26,6 +26,8 @@ defmodule ExWebRTC.Media.IVFReaderTest do assert is_binary(frame.data) assert frame.data != <<>> end + + assert :eof == IVFReader.next_frame(reader) end test "empty file" do From ce96dd902150341caa159faa41bbb93f81d8284e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=9Aled=C5=BA?= Date: Thu, 30 Nov 2023 18:20:45 +0100 Subject: [PATCH 4/4] Fix remaining enforcekeys --- lib/ex_webrtc/media/ivf_reader.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ex_webrtc/media/ivf_reader.ex b/lib/ex_webrtc/media/ivf_reader.ex index 24acdc4c..aedcad9c 100644 --- a/lib/ex_webrtc/media/ivf_reader.ex +++ b/lib/ex_webrtc/media/ivf_reader.ex @@ -67,8 +67,8 @@ defmodule ExWebRTC.Media.IVFFrame do data: binary() } - @enforcekeys [:timestamp, :data] - defstruct @enforcekeys + @enforce_keys [:timestamp, :data] + defstruct @enforce_keys end defmodule ExWebRTC.Media.IVFReader do