From ab4ce79b0413a3cd837ee124618dcf5221f864cb Mon Sep 17 00:00:00 2001 From: Billal GHILAS Date: Sat, 31 Jan 2026 16:55:09 +0100 Subject: [PATCH 1/2] Improve fragments (moof) generation in low latency hls We keep two buffers when using ll-hls, one for the parts and the other for the whole segment (this means that we create only one moof at the end of the segment). As an improvement, when generating a moof as a part, we store the whole moof as part of the whole segment. So at the end of the segment we'll have as much as moofs as the number of generated parts. --- CHANGELOG.md | 2 ++ lib/hlx/muxer/cmaf.ex | 84 ++++++++++--------------------------------- 2 files changed, 20 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 355653a..60624c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * Use max part target duration of all renditions to calculate part hold back. * Add ci GitHub action +* Bump ex_m3u8 dependency. +* Improve fragments (moof/mdat) generation in low latency HLS. ## v0.5.0 - 2025-12-24 diff --git a/lib/hlx/muxer/cmaf.ex b/lib/hlx/muxer/cmaf.ex index 5b4fc61..e3e1d38 100644 --- a/lib/hlx/muxer/cmaf.ex +++ b/lib/hlx/muxer/cmaf.ex @@ -13,12 +13,12 @@ defmodule HLX.Muxer.CMAF do @type t :: %__MODULE__{ tracks: %{non_neg_integer() => ExMP4.Track.t()}, header: ExMP4.Box.t(), - segments: map(), - fragments: map(), + current_fragments: map(), + fragments: [binary()], part_duration: map() } - defstruct [:tracks, :header, :segments, :fragments, :part_duration] + defstruct [:tracks, :header, :current_fragments, :fragments, :part_duration] @impl true def init(tracks) do @@ -27,8 +27,8 @@ defmodule HLX.Muxer.CMAF do %__MODULE__{ tracks: tracks, header: build_header(Map.values(tracks)), - segments: new_segments(tracks), - fragments: new_fragments(tracks), + current_fragments: new_fragments(tracks), + fragments: [], part_duration: Map.new(tracks, fn {id, _track} -> {id, 0} end) } end @@ -41,11 +41,11 @@ defmodule HLX.Muxer.CMAF do @impl true def push(sample, state) do fragments = - Map.update!(state.fragments, sample.track_id, fn {traf, data} -> + Map.update!(state.current_fragments, sample.track_id, fn {traf, data} -> {Box.Traf.store_sample(traf, sample), [sample.payload | data]} end) - %{state | fragments: fragments} + %{state | current_fragments: fragments} end @impl true @@ -86,21 +86,15 @@ defmodule HLX.Muxer.CMAF do mdat = %{mdat | content: Enum.reverse(mdat.content)} moof = Box.Moof.update_base_offsets(moof, Box.size(moof) + @mdat_header_size, true) + fragment = Box.serialize([moof, mdat]) - # push samples to main segments - state = - Enum.reduce(parts, state, fn {_track_id, samples}, state -> - Enum.reduce(samples, state, &push/2) - end) - - {Box.serialize([moof, mdat]), part_duration_s, %{state | part_duration: parts_duration}} + {fragment, part_duration_s, %{state | part_duration: parts_duration, fragments: [fragment | state.fragments]}} end @impl true - def flush_segment(state) do + def flush_segment(%{fragments: []} = state) do {moof, mdat} = build_moof_and_mdat(state) - segments = finalize_segments(state.segments, moof, mdat) - + base_data_offset = Box.size(moof) + @mdat_header_size moof = Box.Moof.update_base_offsets(moof, base_data_offset, true) @@ -114,18 +108,16 @@ defmodule HLX.Muxer.CMAF do ) end) - segment_data = Box.serialize([segments, moof, mdat]) - - state = %{ - state - | tracks: tracks, - fragments: new_fragments(tracks), - segments: new_segments(tracks) - } + segment_data = Box.serialize([moof, mdat]) + state = %{state | tracks: tracks, current_fragments: new_fragments(tracks)} {segment_data, state} end + def flush_segment(%{fragments: fragments} = state) do + {Enum.reverse(fragments), %{state | fragments: []}} + end + defp build_header(tracks) do %Box.Moov{ mvhd: %Box.Mvhd{ @@ -140,20 +132,6 @@ defmodule HLX.Muxer.CMAF do } end - defp new_segments(tracks) do - Map.new(tracks, fn {track_id, track} -> - sidx = %Box.Sidx{ - reference_id: track_id, - timescale: track.timescale, - earliest_presentation_time: track.duration, - first_offset: 0, - entries: [] - } - - {track_id, sidx} - end) - end - defp new_fragments(tracks) do Map.new(tracks, fn {id, track} -> traf = %Box.Traf{ @@ -171,7 +149,7 @@ defmodule HLX.Muxer.CMAF do mdat = %Box.Mdat{content: []} {moof, mdat} = - Enum.reduce(state.fragments, {moof, mdat}, fn {_track_id, {traf, data}}, {moof, mdat} -> + Enum.reduce(state.current_fragments, {moof, mdat}, fn {_track_id, {traf, data}}, {moof, mdat} -> traf = Box.Traf.finalize(traf, true) data = Enum.reverse(data) @@ -186,30 +164,4 @@ defmodule HLX.Muxer.CMAF do {moof, mdat} end - - defp finalize_segments(segments, moof, mdat) do - {segments, _size} = - Enum.map_reduce(moof.traf, 0, fn traf, acc -> - %Box.Sidx{} = segment = segments[traf.tfhd.track_id] - - segment = %Box.Sidx{ - segment - | first_offset: acc, - entries: [ - %{ - reference_type: 0, - referenced_size: Box.size(moof) + Box.size(mdat), - subsegment_duration: Box.Traf.duration(traf), - starts_with_sap: 1, - sap_type: 0, - sap_delta_time: 0 - } - ] - } - - {segment, acc + Box.size(segment)} - end) - - Enum.reverse(segments) - end end From 146828277e824806c42f067f976d85aaa94758b4 Mon Sep 17 00:00:00 2001 From: Billal GHILAS Date: Sat, 31 Jan 2026 16:57:54 +0100 Subject: [PATCH 2/2] mix format --- lib/hlx/muxer/cmaf.ex | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/hlx/muxer/cmaf.ex b/lib/hlx/muxer/cmaf.ex index e3e1d38..9ff78b9 100644 --- a/lib/hlx/muxer/cmaf.ex +++ b/lib/hlx/muxer/cmaf.ex @@ -88,13 +88,14 @@ defmodule HLX.Muxer.CMAF do moof = Box.Moof.update_base_offsets(moof, Box.size(moof) + @mdat_header_size, true) fragment = Box.serialize([moof, mdat]) - {fragment, part_duration_s, %{state | part_duration: parts_duration, fragments: [fragment | state.fragments]}} + {fragment, part_duration_s, + %{state | part_duration: parts_duration, fragments: [fragment | state.fragments]}} end @impl true def flush_segment(%{fragments: []} = state) do {moof, mdat} = build_moof_and_mdat(state) - + base_data_offset = Box.size(moof) + @mdat_header_size moof = Box.Moof.update_base_offsets(moof, base_data_offset, true) @@ -149,7 +150,8 @@ defmodule HLX.Muxer.CMAF do mdat = %Box.Mdat{content: []} {moof, mdat} = - Enum.reduce(state.current_fragments, {moof, mdat}, fn {_track_id, {traf, data}}, {moof, mdat} -> + Enum.reduce(state.current_fragments, {moof, mdat}, fn {_track_id, {traf, data}}, + {moof, mdat} -> traf = Box.Traf.finalize(traf, true) data = Enum.reverse(data)