Current section

Files

Jump to
kim_hls lib hls playlist media.ex
Raw

lib/hls/playlist/media.ex

defmodule HLS.Playlist.Media do
alias HLS.Playlist.Tag
alias HLS.Segment
@type t :: %__MODULE__{
version: pos_integer(),
# specifies the maximum length of segments as a decimal-integer.
# https://www.rfc-editor.org/rfc/rfc8216#section-4.3.3.1
target_segment_duration: pos_integer() | float(),
# the relative sequence number this playlist starts from. Used when
# not all segments are listed in the playlist.
# https://www.rfc-editor.org/rfc/# rfc8216#section-4.3.3.2
media_sequence_number: pos_integer(),
# allows synchronization of Renditions with discontinuity tags
# https://www.rfc-editor.org/rfc/rfc8216#section-4.3.3.7
discontinuity_sequence: non_neg_integer(),
uri: URI.t() | nil,
# Indicates that the playlist has become VOD.
finished: boolean(),
type: Tag.PlaylistType.type() | nil,
segments: [Segment.t()]
}
defstruct [
:target_segment_duration,
:uri,
finished: false,
type: nil,
media_sequence_number: 0,
discontinuity_sequence: 0,
version: 7,
segments: []
]
@spec new(URI.t(), number()) :: t()
def new(uri, target_segment_duration) do
%__MODULE__{uri: uri, target_segment_duration: ceil_target_duration(target_segment_duration)}
end
@doc false
# RFC 8216 §4.3.3.1: EXT-X-TARGETDURATION is a decimal-integer.
# Coerce floats to the smallest integer >= the value so the constraint
# "rounded EXTINF <= target" is never violated by truncation.
def ceil_target_duration(d) when is_integer(d) and d > 0, do: d
def ceil_target_duration(d) when is_float(d) and d > 0, do: ceil(d)
@doc """
Builds segment absolute uri.
"""
@spec build_segment_uri(media :: URI.t(), segment :: URI.t()) :: URI.t()
def build_segment_uri(media_uri, segment_uri), do: HLS.Helper.merge_uri(media_uri, segment_uri)
@doc """
Returns the segments for this playlist.
"""
@spec segments(t) :: [Segment.t()]
def segments(%__MODULE__{segments: segs}), do: segs
@spec compute_playlist_duration(t()) :: float()
def compute_playlist_duration(%__MODULE__{segments: segments}) do
Enum.reduce(segments, 0.0, fn %Segment{duration: d}, acc -> acc + d end)
end
end
defimpl HLS.Playlist.Marshaler, for: HLS.Playlist.Media do
alias HLS.Playlist.Tag
alias HLS.Segment
@impl true
def marshal(
playlist = %HLS.Playlist.Media{
version: 7,
segments: segments,
finished: finished
}
) do
header_tags =
playlist
|> header_tags_with_values()
|> Enum.filter(fn {_, value} -> value != nil end)
|> Enum.map(fn {tag, value} -> Tag.marshal(tag, value) end)
|> Enum.join("\n")
segments =
segments
# Deduplicate init sections: only emit when changed
|> Stream.transform(nil, fn segment, last ->
if segment.init_section == last do
{[%{segment | init_section: nil}], last}
else
{[segment], segment.init_section}
end
end)
# Deduplicate keys: only emit when changed
|> Stream.transform(nil, fn segment, last_key ->
if segment.key == last_key do
{[%{segment | key: nil}], last_key}
else
{[segment], segment.key}
end
end)
|> Stream.map(fn %Segment{duration: duration, uri: uri} = segment ->
[
segment.discontinuity && Tag.marshal(Tag.Discontinuity),
segment.key &&
Tag.marshal(Tag.Key, Tag.Key.marshal_attributes(segment.key)),
segment.init_section &&
Tag.marshal(Tag.Map, Tag.Map.marshal_uri_and_byterange(segment.init_section)),
segment.program_date_time &&
Tag.marshal(
Tag.ProgramDateTime,
Tag.ProgramDateTime.marshal_datetime(segment.program_date_time)
),
Tag.marshal(Tag.Inf, :erlang.float_to_binary(duration * 1.0, decimals: 5)) <> ",",
segment.byterange &&
Tag.marshal(Tag.Byterange, Tag.Byterange.marshal(segment.byterange)),
to_string(uri)
]
|> Enum.filter(& &1)
|> Enum.join("\n")
end)
|> Enum.join("\n")
rows = ["#EXTM3U", header_tags, segments]
rows =
if finished do
rows ++ [Tag.marshal_id(Tag.EndList.id())]
else
rows
end
payload = rows |> Enum.join("\n")
payload <> "\n"
end
def header_tags_with_values(%HLS.Playlist.Media{
version: version,
target_segment_duration: target_segment_duration,
media_sequence_number: media_sequence_number,
discontinuity_sequence: discontinuity_sequence,
type: type,
segments: segments
}) do
has_discontinuities = Enum.any?(segments, fn segment -> segment.discontinuity end)
[
{Tag.Version, version},
if(type == nil,
do: nil,
else: {Tag.PlaylistType, Tag.PlaylistType.marshal_playlist_type(type)}
),
{Tag.TargetSegmentDuration, trunc(target_segment_duration)},
{Tag.MediaSequenceNumber, media_sequence_number},
if(discontinuity_sequence > 0 or has_discontinuities,
do: {Tag.DiscontinuitySequence, discontinuity_sequence},
else: nil
)
]
|> Enum.reject(fn x -> x == nil end)
end
end
defimpl HLS.Playlist.Unmarshaler, for: HLS.Playlist.Media do
alias HLS.Playlist.Tag
alias HLS.Segment
@impl true
def supported_tags(_) do
[
Tag.Version,
Tag.PlaylistType,
Tag.TargetSegmentDuration,
Tag.MediaSequenceNumber,
Tag.DiscontinuitySequence,
Tag.EndList,
Tag.Inf,
Tag.SegmentURI,
Tag.Discontinuity,
Tag.Byterange,
Tag.Map,
Tag.Key,
Tag.ProgramDateTime
]
end
@impl true
def load_tags(playlist, tags) do
[version] = Map.get(tags, Tag.Version.id(), [%{value: 1}])
[segment_duration] = Map.fetch!(tags, Tag.TargetSegmentDuration.id())
# RFC 8216 §4.3.3.2: "If the Media Playlist file does not contain an
# EXT-X-MEDIA-SEQUENCE tag, the Media Sequence Number of the first
# Media Segment … SHALL be considered to be 0."
[sequence_number] = Map.get(tags, Tag.MediaSequenceNumber.id(), [%{value: 0}])
discontinuity_sequence =
case Map.get(tags, Tag.DiscontinuitySequence.id(), nil) do
nil -> 0
[discontinuity_seq] -> discontinuity_seq.value
end
finished? = Map.has_key?(tags, Tag.EndList.id())
type =
case Map.get(tags, Tag.PlaylistType.id(), nil) do
# defaults to PlaylisType EVENT if the tag is not present
nil -> nil
[playlist_type] -> playlist_type.value
end
# Optimized: combine filter and mapping in a single pass, then process segments
segment_list =
tags
|> Enum.reduce([], fn
{{seq, :segment}, val}, acc -> [{seq, val} | acc]
_other, acc -> acc
end)
|> Enum.sort()
# Process segments with init section / key tracking and from time calculation
segments =
segment_list
|> Enum.reduce({[], nil, nil, 0}, fn {_, val}, {acc, last_init, last_key, from_acc} ->
segment =
val
|> Segment.from_tags()
|> Segment.update_absolute_sequence(sequence_number.value)
# Handle init section tracking
{init_section, new_last_init} =
if segment.init_section != nil do
{segment.init_section, segment.init_section}
else
{last_init, last_init}
end
# Handle key tracking — EXT-X-KEY applies to all subsequent
# segments until the next EXT-X-KEY.
{key, new_last_key} =
if segment.key != nil do
{segment.key, segment.key}
else
{last_key, last_key}
end
# Set from time, init section and key
segment = %Segment{segment | init_section: init_section, key: key, from: from_acc}
{[segment | acc], new_last_init, new_last_key, from_acc + segment.duration}
end)
|> elem(0)
|> Enum.reverse()
%HLS.Playlist.Media{
playlist
| version: version.value,
target_segment_duration: segment_duration.value,
media_sequence_number: sequence_number.value,
discontinuity_sequence: discontinuity_sequence,
finished: finished?,
type: type,
segments: segments
}
end
end