lib/membrane/rtcp/transport_feedback_packet/nack.ex

defmodule Membrane.RTCP.TransportFeedbackPacket.NACK do
  @moduledoc """
  Generic Negative Acknowledgment packet that informs about lost RTP packet(s)

  Quoting [RFC4585](https://datatracker.ietf.org/doc/html/rfc4585#section-6.2.1):
  The Generic NACK is used to indicate the loss of one or more RTP packets.
  The lost packet(s) are identified by the means of a packet identifier and a bit mask.

  The Feedback Control Information (FCI) field has the following Syntax (Figure 4):

  ```txt
    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |            PID                |             BLP               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

               Figure 4: Syntax for the Generic NACK message

  Packet ID (PID): 16 bits
     The PID field is used to specify a lost packet.  The PID field
     refers to the RTP sequence number of the lost packet.


  bitmask of following lost packets (BLP): 16 bits
     The BLP allows for reporting losses of any of the 16 RTP packets
     immediately following the RTP packet indicated by the PID.  The
     BLP's definition is identical to that given in [6].  Denoting the
     BLP's least significant bit as bit 1, and its most significant bit
     as bit 16, then bit i of the bit mask is set to 1 if the receiver
     has not received RTP packet number (PID+i) (modulo 2^16) and
     indicates this packet is lost; bit i is set to 0 otherwise.  Note
     that the sender MUST NOT assume that a receiver has received a
     packet because its bit mask was set to 0.  For example, the least
     significant bit of the BLP would be set to 1 if the packet
     corresponding to the PID and the following packet have been lost.
     However, the sender cannot infer that packets PID+2 through PID+16
     have been received simply because bits 2 through 15 of the BLP are
     0; all the sender knows is that the receiver has not reported them
     as lost at this time.

  ```
  Implementation based on https://datatracker.ietf.org/doc/html/rfc4585#section-6.2.1
  and https://datatracker.ietf.org/doc/html/rfc2032#section-5.2.2
  """

  @behaviour Membrane.RTCP.TransportFeedbackPacket

  import Bitwise

  defstruct lost_packet_ids: []

  @impl true
  def decode(nack_fci) do
    for <<packet_id::unsigned-size(16), bit_mask::unsigned-size(16) <- nack_fci>> do
      next_lost_packets =
        0..15
        |> Enum.map(fn i ->
          if (bit_mask >>> i &&& 1) == 1 do
            mod_16bit(packet_id + i + 1)
          else
            nil
          end
        end)
        |> Enum.reject(&is_nil/1)

      [packet_id | next_lost_packets]
    end
    |> then(&{:ok, %__MODULE__{lost_packet_ids: List.flatten(&1)}})
  end

  @impl true
  def encode(%__MODULE__{lost_packet_ids: lost_packet_ids}) do
    # TODO: This code does handle rollover, so 65_535 and 0 will be put in separate FCIs
    #       That's not optimal, but rare and still correctly reports the lost packets
    ids_to_encode = Enum.sort(lost_packet_ids)

    # code splitting ids into tuples with reference_id and a list of following ids
    # greater by at most 16. They will fit into one FCI.
    chunk_fun = fn
      # Initial step - initilaize reference_id
      id, nil ->
        {:cont, {id, []}}

      # Id to group in the same FCI - no chunk emitted
      id, {reference_id, rest} when id > reference_id and id - reference_id <= 16 ->
        {:cont, {reference_id, [id | rest]}}

      # Id that should start a next FCI - emit a chunk and set a new reference_id
      id, {reference_id, rest} when id - reference_id > 16 ->
        {:cont, {reference_id, rest}, {id, []}}
    end

    # Emit a chunk with what has been gathered
    after_fun = fn acc -> {:cont, acc, nil} end

    ids_to_encode
    |> Enum.chunk_while(nil, chunk_fun, after_fun)
    |> Enum.map_join(&encode_fci/1)
  end

  defp encode_fci({reference_id, ids}) when is_integer(reference_id) do
    bit_mask =
      ids
      |> Enum.reduce(0, fn id, acc ->
        # ID must be between reference_id + 1 and reference_id + 16
        # we set bit 0 for reference_id + 1 and 15 for reference_id + 16
        bit_to_set = id - reference_id - 1
        bor(acc, 1 <<< bit_to_set)
      end)

    <<reference_id::unsigned-size(16), bit_mask::unsigned-size(16)>>
  end

  defp mod_16bit(number), do: number &&& 0xFFFF
end