defmodule Membrane.HTTPAdaptiveStream.HLS do @moduledoc """ `Membrane.HTTPAdaptiveStream.Manifest` implementation for HTTP Live Streaming. Currently supports up to one audio and video stream. """ @behaviour Membrane.HTTPAdaptiveStream.Manifest use Ratio alias Membrane.HTTPAdaptiveStream.{BandwidthCalculator, Manifest} alias Membrane.Time @version 7 @master_playlist_header """ #EXTM3U #EXT-X-VERSION:#{@version} #EXT-X-INDEPENDENT-SEGMENTS """ |> String.trim() @empty_segments Qex.new() @default_audio_track_id "audio_default_id" @default_audio_track_name "audio_default_name" defmodule SegmentAttribute do @moduledoc """ Implementation of `Membrane.HTTPAdaptiveStream.Manifest.SegmentAttribute` behaviour for HTTP Live Streaming """ @behaviour Membrane.HTTPAdaptiveStream.Manifest.SegmentAttribute import Membrane.HTTPAdaptiveStream.Manifest.SegmentAttribute @impl true def serialize(discontinuity(header_name, number)) do [ "#EXT-X-DISCONTINUITY-SEQUENCE:#{number}", "#EXT-X-DISCONTINUITY", "#EXT-X-MAP:URI=#{header_name}" ] end end @doc """ Generates EXTM3U playlist for the given manifest """ @impl true def serialize(%Manifest{} = manifest) do tracks_by_content = manifest.tracks |> Map.values() |> Enum.group_by(& &1.content_type) main_manifest_name = "#{manifest.name}.m3u8" if length(Map.get(tracks_by_content, :audio, [])) > 1 do raise ArgumentError, message: "Multiple audio tracks are not currently supported." end case {tracks_by_content[:audio], tracks_by_content[:video]} do {[audio], nil} -> [ {main_manifest_name, build_master_playlist({audio, nil})}, {"audio.m3u8", serialize_track(audio)} ] {nil, videos} -> List.flatten([ {main_manifest_name, build_master_playlist({nil, videos})}, videos |> Enum.filter(&(&1.segments != @empty_segments)) |> Enum.map(&{build_media_playlist_path(&1), serialize_track(&1)}) ]) {[audio], videos} -> List.flatten([ {main_manifest_name, build_master_playlist({audio, videos})}, {"audio.m3u8", serialize_track(audio)}, videos |> Enum.filter(&(&1.segments != @empty_segments)) |> Enum.map(&{build_media_playlist_path(&1), serialize_track(&1)}) ]) end end defp build_media_playlist_path(%Manifest.Track{} = track) do [track.content_type, "_", track.track_name, ".m3u8"] |> Enum.join("") end defp build_media_playlist_tag(%Manifest.Track{} = track) do case track do %Manifest.Track{content_type: :audio} -> """ #EXT-X-MEDIA:TYPE=AUDIO,NAME="#{@default_audio_track_name}",GROUP-ID="#{@default_audio_track_id}",AUTOSELECT=YES,DEFAULT=YES,URI="audio.m3u8" """ |> String.trim() %Manifest.Track{content_type: :video} -> """ #EXT-X-STREAM-INF:BANDWIDTH=#{BandwidthCalculator.calculate_bandwidth(track)},CODECS="avc1.42e00a" """ |> String.trim() end end defp build_master_playlist(tracks) do case tracks do {audio, nil} -> [@master_playlist_header, build_media_playlist_tag(audio)] |> Enum.join("") {nil, videos} -> [ @master_playlist_header | videos |> Enum.filter(&(&1.segments != @empty_segments)) |> Enum.flat_map(&[build_media_playlist_tag(&1), build_media_playlist_path(&1)]) ] |> Enum.join("\n") {audio, videos} -> video_tracks = videos |> Enum.filter(&(&1.segments != @empty_segments)) |> Enum.flat_map( &[ "#{build_media_playlist_tag(&1)},AUDIO=\"#{@default_audio_track_id}\"", build_media_playlist_path(&1) ] ) [ @master_playlist_header, build_media_playlist_tag(audio), video_tracks ] |> List.flatten() |> Enum.join("\n") end end defp serialize_track(%Manifest.Track{} = track) do target_duration = Ratio.ceil(track.target_segment_duration / Time.second()) |> trunc() media_sequence = track.current_seq_num - Enum.count(track.segments) """ #EXTM3U #EXT-X-VERSION:#{@version} #EXT-X-TARGETDURATION:#{target_duration} #EXT-X-MEDIA-SEQUENCE:#{media_sequence} #EXT-X-DISCONTINUITY-SEQUENCE:#{track.current_discontinuity_seq_num} #EXT-X-MAP:URI="#{track.header_name}" #{track.segments |> Enum.flat_map(&serialize_segment/1) |> Enum.join("\n")} #{if track.finished?, do: "#EXT-X-ENDLIST", else: ""} """ end defp serialize_segment(segment) do time = Ratio.to_float(segment.duration / Time.second()) Enum.flat_map(segment.attributes, &SegmentAttribute.serialize/1) ++ ["#EXTINF:#{time},", segment.name] end end