lib/membrane_h264_plugin/parser/format.ex

defmodule Membrane.H264.Parser.Format do
  @moduledoc """
  Module providing functionalities for preparing H264
  format based on the parsed SPS NAL units.
  """

  alias Membrane.H264

  @profiles_description [
    high_cavlc_4_4_4_intra: [profile_idc: 44],
    constrained_baseline: [profile_idc: 66, constraint_set1: 1],
    baseline: [profile_idc: 66],
    main: [profile_idc: 77],
    extended: [profile_idc: 88],
    constrained_high: [profile_idc: 100, constraint_set4: 1, constraint_set5: 1],
    progressive_high: [profile_idc: 100, constraint_set4: 1],
    high: [profile_idc: 100],
    high_10_intra: [profile_idc: 110, constraint_set3: 1],
    high_10: [profile_idc: 110],
    high_4_2_2_intra: [profile_idc: 122, constraint_set3: 1],
    high_4_2_2: [profile_idc: 122],
    high_4_4_4_intra: [profile_idc: 244, constraint_set3: 1],
    high_4_4_4_predictive: [profile_idc: 244]
  ]

  @doc """
  Prepares the `Membrane.H264.t()` format based on the parsed SPS NALu.
  During the process, the function determines the profile of
  the h264 stream and the picture resolution.
  """
  @spec from_sps(
          sps_nalu :: H264.Parser.NALu.t(),
          output_raw_stream_structure :: H264.stream_structure(),
          options_fields :: [
            framerate: {pos_integer(), pos_integer()},
            output_alignment: :au | :nalu
          ]
        ) :: H264.t()
  def from_sps(sps_nalu, output_raw_stream_structure, options_fields) do
    sps = sps_nalu.parsed_fields

    chroma_array_type = if sps.separate_colour_plane_flag == 0, do: sps.chroma_format_idc, else: 0

    {sub_width_c, sub_height_c} =
      case sps.chroma_format_idc do
        1 -> {2, 2}
        2 -> {2, 1}
        3 -> {1, 1}
        _other -> {nil, nil}
      end

    {crop_unit_x, crop_unit_y} =
      if chroma_array_type == 0,
        do: {1, 2 - sps.frame_mbs_only_flag},
        else: {sub_width_c, sub_height_c * (2 - sps.frame_mbs_only_flag)}

    {width_offset, height_offset} =
      if sps.frame_cropping_flag == 1,
        do:
          {(sps.frame_crop_right_offset + sps.frame_crop_left_offset) * crop_unit_x,
           (sps.frame_crop_top_offset +
              sps.frame_crop_bottom_offset) * crop_unit_y},
        else: {0, 0}

    width_in_mbs = sps.pic_width_in_mbs_minus1 + 1
    width = width_in_mbs * 16 - width_offset

    height_in_map_units = sps.pic_height_in_map_units_minus1 + 1
    height_in_mbs = (2 - sps.frame_mbs_only_flag) * height_in_map_units
    height = height_in_mbs * 16 - height_offset

    profile = parse_profile(sps_nalu)

    %H264{
      width: width,
      height: height,
      profile: profile,
      framerate: Keyword.get(options_fields, :framerate),
      alignment: Keyword.get(options_fields, :output_alignment),
      nalu_in_metadata?: true,
      stream_structure: output_raw_stream_structure
    }
  end

  defp parse_profile(sps_nalu) do
    fields = sps_nalu.parsed_fields

    {profile_name, _constraints_list} =
      Enum.find(@profiles_description, {nil, nil}, fn {_profile_name, constraints_list} ->
        Enum.all?(constraints_list, fn {key, value} ->
          Map.has_key?(fields, key) and fields[key] == value
        end)
      end)

    if profile_name == nil, do: raise("Cannot read the profile name based on SPS's fields.")
    profile_name
  end
end