lib/membrane_http_adaptive_stream/bandwidth_calculator.ex

defmodule Membrane.HTTPAdaptiveStream.BandwidthCalculator do
  @moduledoc """
  Functions to calculate multimedia track bandwidth according to: https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.4.2
  """

  use Ratio, comparison: true

  alias Membrane.HTTPAdaptiveStream.Manifest.Track
  alias Membrane.Time

  # Default value that is returned when track bandwidth calculation is impossible. High value is used since it is less
  # harmful to overestimate bandwidth and force HLS client to switch to track with lower bandwidth than to underestimate
  # and risk that client will use track with actual bandwidth that is beyond its capabilities.
  @default_bandwidth 2_560_000

  @spec calculate_bandwidth(Track.t()) :: integer()
  def calculate_bandwidth(track) do
    target_duration = track.target_segment_duration / Time.second()
    segments_sequences = generate_subsequences(track.segments |> Enum.to_list())

    segments_meta =
      segments_sequences
      |> Enum.map(
        &{
          Enum.map(&1, fn sg -> 8 * sg.bytes_size end) |> Enum.sum(),
          Enum.map(&1, fn sg -> sg.duration / Time.second() end)
          |> Enum.reduce(fn acc, x -> acc + x end)
        }
      )
      |> Enum.filter(fn {_bits_size, duration} -> duration != 0.0 end)

    segments_bitrates =
      segments_meta |> Enum.map(fn {bits_size, duration} -> bits_size / duration end)

    # According to HLS rfc only segment subsequences with duration between 0.5 and 1.5 of target duration should be
    # considered
    validated_segments_meta =
      segments_meta
      |> Enum.filter(fn {_bits_size, duration} ->
        duration >= 0.5 * target_duration and duration <= 1.5 * target_duration
      end)

    validated_segments_bitrates =
      validated_segments_meta |> Enum.map(fn {bits_size, duration} -> bits_size / duration end)

    cond do
      Enum.empty?(segments_meta) ->
        @default_bandwidth

      Enum.empty?(validated_segments_meta) ->
        segments_bitrates |> Enum.max(&Ratio.>=/2) |> Ratio.floor()

      true ->
        validated_segments_bitrates |> Enum.max(&Ratio.>=/2) |> Ratio.floor()
    end
  end

  # Generates all contiguous subsequences of a list. This is needed because rfc defines bandwidth
  # in HLS as the highest size to duration ratio of all contiguous subsequences of media track segments.
  defp generate_subsequences(sequence) do
    n = length(sequence)
    subsequence_ranges = for(i <- 0..(n - 1), do: for(j <- 0..i, do: {j, i})) |> List.flatten()
    Enum.map(subsequence_ranges, fn {i, j} -> Enum.slice(sequence, i..j) end)
  end
end