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