lib/membrane_http_adaptive_stream/manifest.ex

defmodule Membrane.HTTPAdaptiveStream.Manifest do
  @moduledoc """
  Behaviour for manifest serialization.
  """
  use Bunch.Access

  alias __MODULE__.{Changeset, Track}

  @type serialized_manifest_t :: {manifest_name :: String.t(), manifest_content :: String.t()}

  @type serialized_manifests_t :: %{
          master_manifest: serialized_manifest_t(),
          manifest_per_track: %{
            optional(track_id :: any()) => serialized_manifest_t()
          }
        }

  @callback serialize(t) :: serialized_manifests_t()

  @type t :: %__MODULE__{
          name: String.t(),
          module: module,
          tracks: %{(id :: any) => Track.t()}
        }

  @enforce_keys [:name, :module]
  defstruct @enforce_keys ++ [tracks: %{}]

  @doc """
  Add a track to the manifest.

  Returns the name under which the header file should be stored.
  """
  @spec add_track(t, Track.Config.t()) :: {header_name :: String.t(), t}
  def add_track(manifest, %Track.Config{} = config) do
    track = Track.new(config)
    manifest = %__MODULE__{manifest | tracks: Map.put(manifest.tracks, config.id, track)}
    {track.header_name, manifest}
  end

  @doc """
  Adds segment to the manifest. In case of ll-hls it will add partial segment, and also full segment if needed.
  Returns `Membrane.HTTPAdaptiveStream.Manifest.Track.Changeset`.
  """
  @spec add_chunk(
          t,
          track_id :: Track.id_t(),
          Membrane.Buffer.t()
        ) ::
          {Changeset.t(), t}
  def add_chunk(%__MODULE__{} = manifest, track_id, buffer) do
    opts = %{
      payload: buffer.payload,
      size: byte_size(buffer.payload),
      independent?: Map.get(buffer.metadata, :independent?, true),
      last_chunk?: Map.fetch!(buffer.metadata, :last_chunk?),
      duration: buffer.metadata.duration,
      complete?: true
    }

    get_and_update_in(
      manifest,
      [:tracks, track_id],
      &Track.add_chunk(&1, opts)
    )
  end

  @spec serialize(t) :: serialized_manifests_t()
  def serialize(%__MODULE__{module: module} = manifest) do
    module.serialize(manifest)
  end

  @spec has_track?(t(), Track.id_t()) :: boolean()
  def has_track?(%__MODULE__{tracks: tracks}, track_id), do: Map.has_key?(tracks, track_id)

  @spec persisted?(t(), Track.id_t()) :: boolean()
  def persisted?(%__MODULE__{tracks: tracks}, track_id),
    do: Track.persisted?(Map.get(tracks, track_id))

  @doc """
  Append a discontinuity to the track.

  This will inform the player that eg. the parameters of the encoder changed and allow you to provide a new MP4 header.
  For details on discontinuities refer to [RFC 8216](https://datatracker.ietf.org/doc/html/rfc8216).
  """
  @spec discontinue_track(t(), Track.id_t()) :: {header_name :: String.t(), t()}
  def discontinue_track(%__MODULE__{} = manifest, track_id) do
    get_and_update_in(
      manifest,
      [:tracks, track_id],
      &Track.discontinue/1
    )
  end

  @spec finish(t, Track.id_t()) :: {Changeset.t(), t}
  def finish(%__MODULE__{} = manifest, track_id) do
    get_and_update_in(manifest, [:tracks, track_id], &Track.finish/1)
  end

  @doc """
  Filter all tracks that have option `:persisted?` set to true, then
  restores all the stale segments in those tracks.
  """
  @spec from_beginning(t()) :: t
  def from_beginning(%__MODULE__{} = manifest) do
    tracks =
      manifest.tracks
      |> Enum.filter(fn {_track_id, track} -> Track.persisted?(track) end)
      |> Map.new(fn {track_id, track} -> {track_id, Track.from_beginning(track)} end)

    %__MODULE__{manifest | tracks: tracks}
  end

  @doc """
  Returns all segments grouped by the track id.
  """
  @spec segments_per_track(t()) :: %{
          optional(track_id :: term()) => [segment_name :: String.t()]
        }
  def segments_per_track(%__MODULE__{} = manifest) do
    Map.new(manifest.tracks, fn {track_id, track} -> {track_id, Track.all_segments(track)} end)
  end

  @doc """
  Returns one header per track
  """
  @spec header_per_track(t()) :: %{optional(track_id :: term()) => String.t()}
  def header_per_track(%__MODULE__{} = manifest) do
    Map.new(manifest.tracks, fn {track_id, track} -> {track_id, Track.header(track)} end)
  end
end