lib/membrane_element_ivf/headers.ex

defmodule Membrane.Element.IVF.Headers do
  @moduledoc false

  use Ratio

  alias Membrane.Time
  alias Membrane.{VP9, VP8}

  defmodule FileHeader do
    @moduledoc """
    A struct representing IVF file header
    """
    @type t :: %__MODULE__{
            signature: String.t(),
            version: non_neg_integer(),
            length_of_header: non_neg_integer(),
            four_cc: String.t(),
            width: non_neg_integer(),
            height: non_neg_integer(),
            rate: non_neg_integer(),
            scale: non_neg_integer(),
            frame_count: non_neg_integer()
          }

    defstruct [
      :signature,
      :version,
      :length_of_header,
      :four_cc,
      :width,
      :height,
      :rate,
      :scale,
      :frame_count
    ]
  end

  defmodule FrameHeader do
    @moduledoc """
    A struct representing IVF frame header
    """

    @type t :: %__MODULE__{
            size_of_frame: non_neg_integer(),
            timestamp: non_neg_integer()
          }
    defstruct [:size_of_frame, :timestamp]
  end

  # IVF Frame Header:
  # bytes 0-3    size of frame in bytes (not including the 12-byte header)
  # bytes 4-11   64-bit presentation timestamp
  # bytes 12..   frame data

  # Function firstly calculate
  # calculating ivf timestamp from membrane timestamp(timebase for membrane timestamp is nanosecond, and timebase for ivf is passed in options)

  @spec create_ivf_frame_header(integer, number | Ratio.t(), number | Ratio.t()) :: binary
  def create_ivf_frame_header(size, timestamp, timebase) do
    ivf_timestamp = timestamp / (timebase * Time.second())
    # conversion to little-endian binary strings
    size_le = <<size::32-little>>
    timestamp_le = <<Ratio.floor(ivf_timestamp)::64-little>>

    size_le <> timestamp_le
  end

  # IVF Header:
  # bytes 0-3    signature: 'DKIF'
  # bytes 4-5    version (should be 0)
  # bytes 6-7    length of header in bytes
  # bytes 8-11   codec FourCC (e.g., 'VP80')
  # bytes 12-13  width in pixels
  # bytes 14-15  height in pixels
  # bytes 16-19  time base denominator (rate)
  # bytes 20-23  time base numerator (scale)
  # bytes 24-27  number of frames in file
  # bytes 28-31  unused
  @spec create_ivf_header(integer, integer, Ratio.t(), integer, any) :: binary
  def create_ivf_header(width, height, timebase, frame_count, caps) do
    codec_four_cc =
      case caps do
        %Membrane.RemoteStream{content_format: VP9} -> "VP90"
        %Membrane.RemoteStream{content_format: VP8} -> "VP80"
        _unknown -> "\0\0\0\0"
      end

    %Ratio{denominator: rate, numerator: scale} = timebase

    signature = "DKIF"
    version = <<0, 0>>
    length_of_header = <<32, 0>>
    # conversion to little-endian binary stirngs
    width_le = <<width::16-little>>
    height_le = <<height::16-little>>
    rate_le = <<rate::32-little>>
    scale_le = <<scale::32-little>>
    frame_count = <<frame_count::32>>
    # field is not used so it is set to 0
    unused = <<0::32>>

    signature <>
      version <>
      length_of_header <>
      codec_four_cc <>
      width_le <>
      height_le <>
      rate_le <>
      scale_le <>
      frame_count <>
      unused
  end

  @spec parse_ivf_frame_header(binary()) ::
          {:ok, FrameHeader.t(), binary()} | {:error_too_short, binary()}
  def parse_ivf_frame_header(payload) when byte_size(payload) < 12,
    do: {:error_too_short, payload}

  def parse_ivf_frame_header(<<size_of_frame::32-little, timestamp::64-little, rest::binary()>>) do
    {:ok, %FrameHeader{size_of_frame: size_of_frame, timestamp: timestamp}, rest}
  end

  @spec parse_ivf_header(binary()) ::
          {:ok, FileHeader.t(), binary()} | {:error_too_short | :error_invalid_data, binary()}
  def parse_ivf_header(payload) when byte_size(payload) < 32, do: {:error_too_short, payload}

  def parse_ivf_header(
        <<signature::binary-size(4), version::16-little, length_of_header::16-little,
          four_cc::binary-size(4), width::16-little, height::16-little, rate::32-little,
          scale::32-little, frame_count::32-little, _unused::32, rest::binary()>> = payload
      ) do
    if String.valid?(signature) and String.valid?(four_cc) do
      {:ok,
       %FileHeader{
         signature: signature,
         version: version,
         length_of_header: length_of_header,
         four_cc: four_cc,
         width: width,
         height: height,
         rate: rate,
         scale: scale,
         frame_count: frame_count
       }, rest}
    else
      {:error_invalid_data, payload}
    end
  end
end