lib/nal_formats/fu.ex

defmodule Membrane.RTP.H265.FU do
  @moduledoc """
  Module responsible for parsing H265 Fragmentation Unit.
  """
  use Bunch
  alias __MODULE__
  alias Membrane.RTP.H265.NAL

  defstruct [:last_seq_num, data: [], type: nil, donl?: false, don: nil]

  @type don :: nil | non_neg_integer()

  @type t :: %__MODULE__{
          data: [binary()],
          last_seq_num: nil | non_neg_integer(),
          type: NAL.Header.type(),
          donl?: boolean(),
          don: don()
        }

  defguardp is_next(last_seq_num, next_seq_num) when rem(last_seq_num + 1, 65_536) == next_seq_num

  @doc """
  Parses H265 Fragmentation Unit

  If a packet that is being parsed is not considered last then a `{:incomplete, t()}`
  tuple  will be returned.
  In case of last packet `{:ok, {type, data, don}}` tuple will be returned, where data
  is `NAL Unit` created by concatenating subsequent Fragmentation Units and `don` is the
  decoding order number of the `NAL unit` in case `donl` field is present in the packet.
  """
  @spec parse(binary(), non_neg_integer(), t) ::
          {:ok, {binary(), NAL.Header.type(), don()}}
          | {:error, :packet_malformed | :invalid_first_packet}
          | {:incomplete, t()}
  def parse(data, seq_num, acc) do
    with {:ok, {header, value}} <- FU.Header.parse(data) do
      do_parse(header, value, seq_num, acc)
    end
  end

  @doc """
  Serialize H265 unit into list of FU payloads
  """
  @spec serialize(binary(), pos_integer()) :: list(binary()) | {:error, :unit_too_small}
  def serialize(data, preferred_size) do
    case data do
      <<header::2-binary, head::binary-size(preferred_size), rest::binary>> ->
        <<r::1, type::6, layer_id::6, t_id::3>> = header

        payload =
          head
          |> FU.Header.add_header(1, 0, type)
          |> NAL.Header.add_header(r, NAL.Header.encode_type(:fu), layer_id, t_id)

        [payload | do_serialize(rest, r, type, layer_id, t_id, preferred_size)]

      _data ->
        {:error, :unit_too_small}
    end
  end

  defp do_serialize(data, r, type, layer_id, t_id, preferred_size) do
    case data do
      <<head::binary-size(preferred_size), rest::binary>> ->
        payload =
          head
          |> FU.Header.add_header(0, 0, type)
          |> NAL.Header.add_header(r, NAL.Header.encode_type(:fu), layer_id, t_id)

        [payload] ++ do_serialize(rest, r, type, layer_id, t_id, preferred_size)

      <<>> ->
        []

      rest ->
        [
          rest
          |> FU.Header.add_header(0, 1, type)
          |> NAL.Header.add_header(r, NAL.Header.encode_type(:fu), layer_id, t_id)
        ]
    end
  end

  defp do_parse(header, data, seq_num, acc)

  defp do_parse(%FU.Header{start_bit: true, type: type}, data, seq_num, %{donl?: false} = acc),
    do: {:incomplete, %__MODULE__{acc | data: [data], last_seq_num: seq_num, type: type}}

  defp do_parse(%FU.Header{start_bit: true, type: type}, <<don::16, data::binary>>, seq_num, acc) do
    {:incomplete, %__MODULE__{acc | data: [data], last_seq_num: seq_num, type: type, don: don}}
  end

  defp do_parse(%FU.Header{start_bit: false}, _data, _seq_num, %__MODULE__{last_seq_num: nil}),
    do: {:error, :invalid_first_packet}

  defp do_parse(%FU.Header{end_bit: true}, data, seq_num, %__MODULE__{
         data: acc,
         last_seq_num: last,
         type: type,
         don: don
       })
       when is_next(last, seq_num) do
    result =
      [data | acc]
      |> Enum.reverse()
      |> Enum.join()

    {:ok, {result, type, don}}
  end

  defp do_parse(_header, data, seq_num, %__MODULE__{data: acc, last_seq_num: last} = fu)
       when is_next(last, seq_num),
       do: {:incomplete, %__MODULE__{fu | data: [data | acc], last_seq_num: seq_num}}

  defp do_parse(_header, _data, _seq_num, _fu), do: {:error, :missing_packet}
end