Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
129 changes: 129 additions & 0 deletions lib/ex_webrtc/media/ivf_reader.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
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: 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()
}

@enforce_keys [
:signature,
:version,
:header_size,
:fourcc,
:width,
:height,
:timebase_denum,
:timebase_num,
:num_frames,
:unused
]
defstruct @enforce_keys
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()
}

@enforce_keys [:timestamp, :data]
defstruct @enforce_keys
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
with <<len_frame::little-integer-size(32), timestamp::little-integer-size(64)>> <-
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
64 changes: 64 additions & 0 deletions test/ex_webrtc/media/ivf_reader_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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

assert :eof == IVFReader.next_frame(reader)
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
5 changes: 5 additions & 0 deletions test/fixtures/ivf/README.md
Original file line number Diff line number Diff line change
@@ -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
Empty file added test/fixtures/ivf/empty.ivf
Empty file.
Binary file added test/fixtures/ivf/vp8_correct.ivf
Binary file not shown.
Binary file added test/fixtures/ivf/vp8_invalid_last_frame.ivf
Binary file not shown.